Help us shape the future of Go! Take the 2026 Go Developer Survey

postgresadapter

package
v1.0.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Nov 2, 2026 License: Apache-2.0 Imports: 20 Imported by: 0

Documentation

Overview

Package postgresadapter is the PostgreSQL side of livewire on pgx/v5: the LISTEN change feed, the trigger and table SQL for the application's migrations, the mutation transactor, and the audit, filter, idempotency and presence stores.

A database failure is returned wrapped with %w, so errors.As finds pgx's *pgconn.PgError (its SQLSTATE in Code) and errors.Is finds pgx.ErrNoRows and context errors; the wrapping messages are not part of the API.

Example

The adapters an engine takes, built from one pool and the database URL (DES-001).

package main

import (
	"context"

	"github.com/jackc/pgx/v5/pgxpool"

	"github.com/govengers/livewire"
	"github.com/govengers/livewire/postgresadapter"
)

func main() {
	ctx := context.Background()
	databaseURL := "postgres://app@localhost/app"
	pool, err := pgxpool.New(ctx, databaseURL)
	if err != nil {
		return
	}
	defer pool.Close()
	listener, err := postgresadapter.NewListener(&postgresadapter.ListenerOptions{DSN: databaseURL})
	if err != nil {
		return
	}
	transactor, err := postgresadapter.NewTransactor(&postgresadapter.TransactorOptions{Pool: pool})
	if err != nil {
		return
	}
	filters, err := postgresadapter.NewFilterStore(&postgresadapter.FilterStoreOptions{Pool: pool})
	if err != nil {
		return
	}
	idempotency, err := postgresadapter.NewIdempotencyStore(&postgresadapter.IdempotencyStoreOptions{Pool: pool})
	if err != nil {
		return
	}
	presence, err := postgresadapter.NewPresenceStore(&postgresadapter.PresenceStoreOptions{Pool: pool})
	if err != nil {
		return
	}
	_ = livewire.EngineOptions{
		ChangeFeed: listener, Transactor: transactor, FilterStore: filters, IdempotencyStore: idempotency, PresenceStore: presence,
	}
}

Index

Examples

Constants

View Source
const AuditSQL = `` /* 2792-byte string literal not displayed */

AuditSQL creates livewire_audit, one row per mutation (D32, AuditStore), partitioned by UTC month on at. Rows of a month without its partition land in livewire_audit_default; livewire_audit_maintain(retention, as_of default now()), which AuditStore.Maintain calls, creates the partitions of the UTC month of as_of and the next one, moving their rows out of the default partition, drops the partitions whose month ended before least(as_of, now()) - retention and deletes the default partition's rows older than that. When it creates or drops a partition it holds livewire_audit in ACCESS EXCLUSIVE mode until it ends, so audit writes and reads wait meanwhile; its lock_timeout of 5 s bounds how long it waits for that lock, not how long it holds it. Call it at READ COMMITTED, as AuditStore.Maintain does: under REPEATABLE READ its snapshot predates its lock, so it misses rows committed while it waited. The session's TimeZone changes nothing.

View Source
const Channel = "livewire"

Channel is the NOTIFY channel the triggers send on and the Listener listens on.

View Source
const FiltersSQL = `` /* 856-byte string literal not displayed */

FiltersSQL creates livewire_filters, the stored filters of D13 (FilterStore). An unnamed filter is unique per owner, tenant, resource, context and document hash; a name is unique per owner, tenant and resource.

View Source
const IdempotencySQL = `` /* 401-byte string literal not displayed */

IdempotencySQL creates livewire_idempotency, the recorded answers of D16 (IdempotencyStore). Status is 0 while a call is in flight; response is the answered body as bytes (D72), so a replay answers the very bytes of the first answer. A table the earlier jsonb column created is upgraded after every replica runs this store, which reads and writes both, by an ALTER in the application's migration that takes an ACCESS EXCLUSIVE lock (D72); every connection that prepared the store's statements before fails two calls after it (SQLSTATE 0A000, then 42804), so the replicas must restart after the migration (D74).

View Source
const PresenceSQL = `` /* 818-byte string literal not displayed */

PresenceSQL creates livewire_presence, the presences of D37 (PresenceStore), and its triggers: a join and a leave notify on Channel with the tenant as partition, an update only when a column other than updated_at changed, so a heartbeat reaches no subscription.

View Source
const TriggerFunctionSQL = `CREATE OR REPLACE FUNCTION livewire_notify() RETURNS trigger LANGUAGE plpgsql AS $$
DECLARE
	old_row jsonb := CASE WHEN TG_OP IN ('UPDATE', 'DELETE') THEN to_jsonb(OLD) END;
	new_row jsonb := CASE WHEN TG_OP IN ('INSERT', 'UPDATE') THEN to_jsonb(NEW) END;
	partition_column text := CASE WHEN TG_NARGS > 0 THEN TG_ARGV[0] END;
	table_name text := (SELECT relname FROM pg_class WHERE oid = coalesce(pg_partition_root(TG_RELID), TG_RELID));
	operation text := CASE WHEN TG_OP = 'TRUNCATE' THEN 'delete' ELSE lower(TG_OP) END;
	row_id text := coalesce(coalesce(new_row, old_row)->>'id', '');
	old_id text := coalesce(old_row->>'id', '');
	row_partition text := coalesce(coalesce(new_row, old_row)->>partition_column, '');
	old_partition text := coalesce(old_row->>partition_column, '');
BEGIN
	PERFORM pg_notify('` + Channel + `', json_build_object('table', table_name, 'op', operation,
		'id', CASE WHEN octet_length(row_id) <= 500 THEN row_id ELSE '' END,
		'partition', CASE WHEN octet_length(row_partition) <= 500 THEN row_partition ELSE '' END)::text);
	IF TG_OP = 'UPDATE' AND old_partition <> row_partition THEN
		PERFORM pg_notify('` + Channel + `', json_build_object('table', table_name, 'op', 'update',
			'id', CASE WHEN octet_length(old_id) <= 500 THEN old_id ELSE '' END,
			'partition', CASE WHEN octet_length(old_partition) <= 500 THEN old_partition ELSE '' END)::text);
	END IF;
	RETURN NULL;
END
$$;
`

TriggerFunctionSQL creates livewire_notify(), the trigger function of TriggerSQL, PartitionedTriggerSQL and PresenceSQL. It sends one notification per row change on Channel: `{"table", "op", "id", "partition"}` with the operation in lower case (insert, update, delete), the row's id column as text ("" without one) and the partition column's value ("" without one, D25). An update that changes the partition column notifies the new and the old value. The table is the root of a natively partitioned table (PARTITION BY), never the partition the row lives in, and its id must be unique across the partitions; an update that moves a row to another native partition runs as a delete and an insert and notifies those two. A TRUNCATE notifies one delete without id and partition, which reaches every subscription of the table. An id or partition value longer than 500 bytes is sent as "", so the payload stays below pg_notify's 8000 bytes: such a change reaches every subscription of the table, never fewer than it should.

Variables

View Source
var ErrMalformedNotification = errors.New("postgresadapter: malformed notification")

ErrMalformedNotification marks a notification on Channel that is not a change the triggers send; the Listener drops it and reports it to OnError.

Functions

func Distinct

func Distinct(ctx context.Context, database DBTX, specification *query.ResourceSQL, listQuery *query.ListQuery, field query.Field) ([]livewire.DistinctValue, error)

Distinct is the distinct read of D15: the values of field under the list's WHERE with their counts, most frequent first, at most listQuery.Limit of them (the resource's DistinctMaximum, which the engine puts there). It runs in the engine's transaction inside an engine unit of work, on database outside one. Field must be one of the resource's filterable scalar fields, else it is a *query.Error.

func HistoryPage

func HistoryPage[Item any](ctx context.Context, database DBTX, table string, listQuery *query.ListQuery, decode func(row []byte) (*Item, error)) (*livewire.Page[livewire.Version[Item]], error)

HistoryPage reads one page of an item's versions from table's `<table>_history` (HistorySQL) as a resource's History loader answers it (D34, D56): listQuery is the engine's history query (HistoryFields, scoped to the item's id), over the versions of the id's current life only, so an item re-created under a deleted id never shows the deleted one's versions; it is read with its page query and its capped count on one snapshot like ListPage, in the engine's transaction inside an engine unit of work. decode maps a version's stored row, the jsonb of the table's columns, to the item's JSON shape. The page has no cursor: a history pages by offset.

History: func(ctx context.Context, listQuery *query.ListQuery) (*livewire.Page[livewire.Version[part.Item]], error) {
	return postgresadapter.HistoryPage(ctx, r.pool, "parts", listQuery, decodePartRow)
},

func HistorySQL

func HistorySQL(table string) (string, error)

HistorySQL creates `<table>_history` and the trigger that appends a version on every insert, update and delete of table (D34): the row as jsonb in data, the version (1, 2, ... per id), the operation, valid_from and valid_to (NULL for the current version; a delete is a closed version of the last row), and the actor, real actor, mutation and request id the Transactor set in livewire.actor (D31; NULL outside an engine transaction). An update that changes nothing appends nothing, a TRUNCATE appends nothing either; an update that changes the id closes the old id's version and starts the new id's history. Table needs a uuid column id. For a natively partitioned table the history is the table's, whichever partition the row lives in; the id must be unique across the partitions, which PostgreSQL cannot enforce there and livewire assumes everywhere (Change.ID, item routes). An update that moves a row to another partition runs as a delete and an insert, so it appends a delete and an insert version, not an update. The trigger function livewire_history() is created or replaced with it. Applications only insert into history through the trigger; the trigger itself closes the previous version. A table name whose `_history` would pass PostgreSQL's 63 bytes is refused.

Every version carries the life of its id (life, from 1) and the transaction that wrote it (transaction_id). A row that arrives under an id, by an insert or by an update that changes its id onto it, starts that id's next life: a re-create after a delete or a TRUNCATE, a ClientID create of an old id (D18) and an id change onto an id that lived before never continue the old item's history. Only the insert half of a partition move, whose id's last version is a delete of the same transaction, continues the life, so a moved row keeps its whole history; a delete and a re-create of one id inside one transaction cannot be told from a move and stay one life too. An id change starts the new id's history at the change; the old versions stay under the old id. Every other version continues the life of the one before. HistoryPage reads the current life only (D56).

func ListPage

func ListPage[Item any](ctx context.Context, database DBTX, specification *query.ResourceSQL, listQuery *query.ListQuery, hydrate Hydrate[Item]) (*livewire.Page[Item], error)

ListPage reads one page of a filterable resource (section 8, D53, D55) as the resource's List loader answers it: the page query, the capped count and the hydration of the page's rows, all on one snapshot; the items are in page order (never nil) and Next is set when the page is full. Inside an engine unit of work (TransactionFrom(ctx) is not nil) they run in the engine's transaction, whatever database is, so the read sees the engine's snapshot and livewire.tenant; outside one, on a pool or a connection, ListPage opens a REPEATABLE READ READ ONLY transaction of its own. A cursor value the column refuses (SQLSTATE 22P02, a forged enum value) is a *query.Error with query.CodeInvalidCursor; an id the hydration did not return is an error.

Example

A resource's List loader: the page on the engine's transaction (ListPage prefers it over the pool), hydrated by id; a cursor-mode resource encodes page.Next.

package main

import (
	"context"

	"github.com/google/uuid"
	"github.com/jackc/pgx/v5/pgxpool"

	"github.com/govengers/livewire"
	"github.com/govengers/livewire/postgresadapter"
	"github.com/govengers/livewire/query"
)

func main() {
	type Part struct {
		ID   uuid.UUID
		Name string
	}
	partSQL := &query.ResourceSQL{From: "parts", IDColumn: "parts.id"}
	hydrate := func(ctx context.Context, database postgresadapter.DBTX, rows []postgresadapter.PageRow) (map[uuid.UUID]Part, error) {
		parts := map[uuid.UUID]Part{}
		result, err := database.Query(ctx, "SELECT id, name FROM parts WHERE id = ANY($1)", postgresadapter.PageIDs(rows))
		if err != nil {
			return nil, err
		}
		defer result.Close()
		for result.Next() {
			var part Part
			if err := result.Scan(&part.ID, &part.Name); err != nil {
				return nil, err
			}
			parts[part.ID] = part
		}
		return parts, result.Err()
	}
	var pool *pgxpool.Pool // the repository's pool
	listParts := func(ctx context.Context, listQuery *query.ListQuery) (*livewire.Page[Part], error) {
		return postgresadapter.ListPage(ctx, pool, partSQL, listQuery, hydrate) // ResourceOptions.List
	}
	_ = listParts
}

func PageIDs

func PageIDs(rows []PageRow) []uuid.UUID

PageIDs is the ids of rows in page order, the argument of a hydration query.

func PartitionedTriggerSQL

func PartitionedTriggerSQL(table, column string) string

PartitionedTriggerSQL is TriggerSQL for one table whose changes carry the value of column as their partition (D25), so a change re-runs only the subscriptions of that partition and those without one.

func TriggerSQL

func TriggerSQL(tables ...string) string

TriggerSQL makes every table notify on each row change and on TRUNCATE (D22); the changes carry no partition, so they reach the subscriptions of every partition. A table name is one identifier in the search path, quoted as it is. On a natively partitioned table the row trigger reaches every partition and the changes name the table itself; the TRUNCATE trigger is installed on the table and on the partitions it has now, so a partition attached later notifies its own TRUNCATE only once TriggerSQL runs again, and a DETACH PARTITION notifies nothing.

func WriteCSV

func WriteCSV(ctx context.Context, database DBTX, export *Export, writer io.Writer) (int, error)

WriteCSV streams every row under export.Query's WHERE, in its order, as CSV to writer: a header of the column names, then one record per row, read in chunks by keyset so no offset grows; Limit and Offset of the query are ignored. A column whose Read check refuses the identity in ctx is left out, header included (D39). It runs on one snapshot like ListPage, in the engine's transaction inside an engine unit of work, and returns how many rows it wrote. Values are written as text: times in RFC 3339 UTC, uuids hyphenated, NULL as an empty field, arrays and json as JSON. A text value a spreadsheet would run as a formula, one that starts with =, +, -, @, a tab or a carriage return, is written with a single quote in front (D69), so opening the export runs nothing, and so is one that starts with a quote, so a reader drops exactly one leading quote of every text cell that starts with one; numbers, times and every other value are written by their type and keep their form.

Types

type AuditStore

type AuditStore struct {
	// contains filtered or unexported fields
}

AuditStore writes livewire_audit (AuditSQL) and implements livewire.AuditStore: the engine writes one row per call (D32) through EngineOptions.AuditStore. It is safe for concurrent use.

func NewAuditStore

func NewAuditStore(options *AuditStoreOptions) (*AuditStore, error)

NewAuditStore validates and copies options.

func (*AuditStore) Maintain

func (s *AuditStore) Maintain(ctx context.Context) error

Maintain runs livewire_audit_maintain: it creates the partitions of this and the next UTC month, drops the partitions whose whole month is older than Retention and deletes the default partition's rows older than that, so rows go by the month, up to a month after their Retention. Audit writes and reads wait while it creates or drops a partition. The application runs it periodically, at least monthly; without it every row stays in the default partition.

func (*AuditStore) Write

func (s *AuditStore) Write(ctx context.Context, entry *livewire.AuditEntry) error

Write inserts the row of entry in the engine's transaction in ctx, so it commits and rolls back with the mutation (D31); outside one, a failed call's row after the rollback, it writes on the pool. Parameters are stored as their JSON, the IdempotencyKey in any spelling uuid.Parse reads as its canonical uuid.

type AuditStoreOptions

type AuditStoreOptions struct {
	Pool      *pgxpool.Pool
	Retention time.Duration
}

AuditStoreOptions configures an AuditStore. Pool is required; Retention (default 365 days, not negative) is how long Maintain keeps rows at least: it drops a month's partition once the whole month is older than Retention, so a row stays up to a month longer.

type DBTX

type DBTX interface {
	Exec(ctx context.Context, statement string, arguments ...any) (pgconn.CommandTag, error)
	Query(ctx context.Context, statement string, arguments ...any) (pgx.Rows, error)
	QueryRow(ctx context.Context, statement string, arguments ...any) pgx.Row
	CopyFrom(ctx context.Context, table pgx.Identifier, columns []string, source pgx.CopyFromSource) (int64, error)
	SendBatch(ctx context.Context, batch *pgx.Batch) pgx.BatchResults
}

DBTX is the query method set of a pgx connection, pool or transaction: a superset of sqlc's pgx/v5 DBTX (with :copyfrom and :batch queries included), so `sqlc.New(handle)` takes it, and of govengers/jobs' postgresadapter.Executor. *pgxpool.Pool, *pgx.Conn and pgx.Tx satisfy it; the engine's query-only handle (TransactionFrom) does too and has no Commit or Rollback (D45). It types the handle, it does not sandbox it: a statement like COMMIT still reaches the server.

func TransactionFrom

func TransactionFrom(ctx context.Context) DBTX

TransactionFrom is the query handle of the engine transaction in ctx (D45), nil outside an engine unit of work, where a repository falls back to its pool:

database := postgresadapter.TransactionFrom(ctx)
if database == nil {
	database = r.pool
}
row, err := sqlc.New(database).InsertPart(ctx, parameters)

type Export

type Export struct {
	Specification *query.ResourceSQL
	Query         *query.ListQuery
	Columns       query.Fields
	ChunkSize     int
}

Export is one CSV export of D35: the columns are the fields it writes, by Name in the header and by SQL per row; ChunkSize (default 1000) is how many rows one keyset query reads.

type FilterStore

type FilterStore struct {
	// contains filtered or unexported fields
}

FilterStore keeps the stored filters of D13 in livewire_filters (FiltersSQL) and implements livewire.FilterStore. Its calls run on the pool, outside the engine's read transaction (D50). It is safe for concurrent use.

func NewFilterStore

func NewFilterStore(options *FilterStoreOptions) (*FilterStore, error)

NewFilterStore validates and copies options.

func (*FilterStore) Delete

func (s *FilterStore) Delete(ctx context.Context, owner livewire.Owner, id string) error

Delete removes the owner's filter; a filter that is not there, or not the owner's, is no error.

func (*FilterStore) Get

Get returns the owner's filter and marks it used at most once per hour; another owner's filter, an unknown id and an id that is not a uuid are livewire.NotFound.

func (*FilterStore) List

func (s *FilterStore) List(ctx context.Context, owner livewire.Owner, resource string) ([]livewire.StoredFilter, error)

List returns the owner's named filters of a resource, of every resource when resource is empty, by resource and name; unnamed filters are registrations, not the owner's list. It returns at most MaximumNamedPerOwner filters.

func (*FilterStore) Save

Save stores filter and returns the stored row; the Saves of one owner run one after the other, so the maximums hold exactly. An unnamed filter is deduplicated on the owner, resource, context and hash, and marked used; a new one beyond MaximumUnnamedPerOwner replaces the owner's least recently used unnamed filter, because unnamed filters are registrations a client can send again (D54): a 404 on an unnamed filterId means register the filter inline again. A named filter beyond MaximumNamedPerOwner is refused with livewire.Invalid at "filter", a name the owner already gave a filter of the resource with livewire.Conflict, a Name or Context longer than 200 characters with livewire.Invalid.

func (*FilterStore) Sweep

func (s *FilterStore) Sweep(ctx context.Context) (int, error)

Sweep removes the unnamed filters unused for 30 days (D13) and returns how many; the application runs it periodically, a job of govengers/jobs for instance.

type FilterStoreOptions

type FilterStoreOptions struct {
	Pool                   *pgxpool.Pool
	MaximumUnnamedPerOwner int
	MaximumNamedPerOwner   int
}

FilterStoreOptions configures a FilterStore. Pool is required. MaximumUnnamedPerOwner (default 1000) and MaximumNamedPerOwner (default 200) bound the filters one owner (key and tenant) keeps, so neither inline subscriptions nor POST /filters grow the table without bound (D54); neither may be negative.

type Hydrate

type Hydrate[Item any] func(ctx context.Context, database DBTX, rows []PageRow) (map[uuid.UUID]Item, error)

Hydrate reads the items of a page's rows on database, the list's transaction, keyed by id; a sqlc query `WHERE id = ANY(@ids::uuid[])` serves it. ListPage puts them in page order.

type IdempotencyStore

type IdempotencyStore struct {
	// contains filtered or unexported fields
}

IdempotencyStore keeps the records of D16 in livewire_idempotency (IdempotencySQL) and implements livewire.IdempotencyStore. Reserve and Record run in the engine's transaction (TransactionFrom), so a rolled-back call frees its key. It is safe for concurrent use.

func NewIdempotencyStore

func NewIdempotencyStore(options *IdempotencyStoreOptions) (*IdempotencyStore, error)

NewIdempotencyStore validates and copies options.

func (*IdempotencyStore) Record

Record stores the status and response of a key Reserve claimed in the same transaction; record.Key is the key in any spelling Reserve takes.

func (*IdempotencyStore) Reserve

func (s *IdempotencyStore) Reserve(ctx context.Context, owner livewire.Owner, key, mutation string) (*livewire.IdempotencyRecord, bool, error)

Reserve claims key for owner and mutation without blocking: it takes pg_try_advisory_xact_lock over a hash of owner and key, so a key another transaction holds is livewire.InProgress at once; a committed record is replayed (livewire.Conflict when it was recorded for another mutation); a fresh key is inserted as in flight. The lock and the reservation end with the transaction. Key is a uuid in any spelling uuid.Parse reads; the record carries its canonical form, so every spelling of one key is the same key.

func (*IdempotencyStore) Sweep

func (s *IdempotencyStore) Sweep(ctx context.Context) (int, error)

Sweep removes the records older than Window and returns how many; the application runs it periodically.

type IdempotencyStoreOptions

type IdempotencyStoreOptions struct {
	Pool   *pgxpool.Pool
	Window time.Duration
}

IdempotencyStoreOptions configures an IdempotencyStore. Pool is required and serves Sweep only; Window (default 24 h, not negative) is how long Sweep keeps a record.

type Listener

type Listener struct {
	// contains filtered or unexported fields
}

Listener is the change feed of one replica (section 5, livewire.ChangeFeed): one dedicated connection that LISTENs on Channel, reconnects with backoff and calls onResync after every successful LISTEN, the first included. Run it once at a time.

func NewListener

func NewListener(options *ListenerOptions) (*Listener, error)

NewListener validates and copies options; it opens no connection.

func (*Listener) Run

func (l *Listener) Run(ctx context.Context, onChange func(livewire.Change), onResync func()) error

Run listens until ctx ends and returns ctx.Err() then; it implements livewire.ChangeFeed. A lost connection is reported to OnError and opened again after a backoff that starts over once a LISTEN succeeded.

type ListenerOptions

type ListenerOptions struct {
	DSN           string
	Connect       func(ctx context.Context) (*pgx.Conn, error)
	RetryInterval time.Duration
	PingInterval  time.Duration
	OnError       func(err error)
}

ListenerOptions configures a Listener. Exactly one of DSN and Connect is required: DSN is parsed by pgx.ParseConfig, Connect opens the dedicated connection itself (a pool's configuration, a token-based login). RetryInterval is the first delay after a failed or lost connection (default 1 s), doubling up to 32 times it, with up to 20 % jitter. PingInterval (default 30 s) is how long the connection may stay silent before a ping proves it alive; it also bounds a connect and a ping. OnError receives every connection failure and every dropped notification (errors.Is ErrMalformedNotification), from Run's goroutine; nil reports nothing. No value may be negative.

type PageRow

type PageRow struct {
	ID      uuid.UUID
	Derived map[string]any
}

PageRow is one row of the page query: the id and the value of every derived column of the ResourceSQL by its name, as pgx returns it.

type PresenceStore

type PresenceStore struct {
	// contains filtered or unexported fields
}

PresenceStore keeps the presences of D37 in livewire_presence (PresenceSQL), whose triggers carry every join, leave and state change to the engine through the Listener; a heartbeat changes updated_at only and notifies nobody. It implements livewire.PresenceStore; its calls run on the pool, outside any engine transaction (D50). The database clock stamps updated_at. It is safe for concurrent use.

func NewPresenceStore

func NewPresenceStore(options *PresenceStoreOptions) (*PresenceStore, error)

NewPresenceStore validates and copies options.

func (*PresenceStore) Heartbeat

func (s *PresenceStore) Heartbeat(ctx context.Context, socket string) error

Heartbeat refreshes updated_at of every presence of the socket.

func (*PresenceStore) Join

func (s *PresenceStore) Join(ctx context.Context, presence *livewire.Presence) error

Join inserts the socket's presence on the record of presence.Owner.Tenant, or updates its owner, display and state; presence.UpdatedAt is ignored.

func (*PresenceStore) Leave

func (s *PresenceStore) Leave(ctx context.Context, tenant, socket, target, key string) error

Leave removes the socket's presence on the tenant's record; an absent one is no error.

func (*PresenceStore) List

func (s *PresenceStore) List(ctx context.Context, tenant, target, key string) ([]livewire.Presence, error)

List returns at most 1000 presences on the tenant's record, by socket.

func (*PresenceStore) Sweep

func (s *PresenceStore) Sweep(ctx context.Context, maximumAge time.Duration) (int, error)

Sweep removes every presence not refreshed for maximumAge and returns how many.

type PresenceStoreOptions

type PresenceStoreOptions struct {
	Pool *pgxpool.Pool
}

PresenceStoreOptions configures a PresenceStore; Pool is required.

type Transactor

type Transactor struct {
	// contains filtered or unexported fields
}

Transactor begins the engine's transactions on a pool (D31, D42, D45): a read is REPEATABLE READ READ ONLY, a write READ COMMITTED READ WRITE, whatever the server's or role's default_transaction_isolation is. Both set livewire.tenant (the tenant, "" with a single tenant) and a write sets livewire.actor, a JSON object `{"actor", "realActor", "mutation", "requestId"}` (owner keys "provider:subject", absent members omitted) with SET LOCAL semantics, so triggers and Row Level Security policies read them with current_setting('livewire.tenant', true) and both are gone after the transaction. It is safe for concurrent use.

func NewTransactor

func NewTransactor(options *TransactorOptions) (*Transactor, error)

NewTransactor validates and copies options.

func (*Transactor) Begin

func (t *Transactor) Begin(ctx context.Context, transactionDetails livewire.TransactionDetails) (livewire.Transaction, error)

Begin begins a transaction for transactionDetails and sets its settings; it implements livewire.Transactor.

type TransactorOptions

type TransactorOptions struct {
	Pool *pgxpool.Pool
}

TransactorOptions configures a Transactor; Pool is required.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL