@kairos-es/feed-postgres 0.0.0-stage → 0.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +28 -0
- package/README.md +84 -2
- package/dist/cjs/config.js +46 -0
- package/dist/cjs/config.js.map +1 -0
- package/dist/cjs/ensureFeedSchema.js +39 -0
- package/dist/cjs/ensureFeedSchema.js.map +1 -0
- package/dist/cjs/feed.js +176 -0
- package/dist/cjs/feed.js.map +1 -0
- package/dist/cjs/index.js +84 -0
- package/dist/cjs/index.js.map +1 -0
- package/dist/cjs/internal/context.js +45 -0
- package/dist/cjs/internal/context.js.map +1 -0
- package/dist/cjs/internal/ddl.js +149 -0
- package/dist/cjs/internal/ddl.js.map +1 -0
- package/dist/cjs/internal/statements.js +270 -0
- package/dist/cjs/internal/statements.js.map +1 -0
- package/dist/cjs/lease.js +342 -0
- package/dist/cjs/lease.js.map +1 -0
- package/dist/cjs/syncStore.js +179 -0
- package/dist/cjs/syncStore.js.map +1 -0
- package/dist/cjs/withFeed.js +51 -0
- package/dist/cjs/withFeed.js.map +1 -0
- package/dist/dts-cjs/config.d.ts +37 -0
- package/dist/dts-cjs/config.d.ts.map +1 -0
- package/dist/dts-cjs/ensureFeedSchema.d.ts +24 -0
- package/dist/dts-cjs/ensureFeedSchema.d.ts.map +1 -0
- package/dist/dts-cjs/feed.d.ts +81 -0
- package/dist/dts-cjs/feed.d.ts.map +1 -0
- package/dist/dts-cjs/index.d.ts +50 -0
- package/dist/dts-cjs/index.d.ts.map +1 -0
- package/dist/dts-cjs/internal/context.d.ts +28 -0
- package/dist/dts-cjs/internal/context.d.ts.map +1 -0
- package/dist/dts-cjs/internal/ddl.d.ts +99 -0
- package/dist/dts-cjs/internal/ddl.d.ts.map +1 -0
- package/dist/dts-cjs/internal/statements.d.ts +127 -0
- package/dist/dts-cjs/internal/statements.d.ts.map +1 -0
- package/dist/dts-cjs/lease.d.ts +138 -0
- package/dist/dts-cjs/lease.d.ts.map +1 -0
- package/dist/dts-cjs/package.json +3 -0
- package/dist/dts-cjs/syncStore.d.ts +57 -0
- package/dist/dts-cjs/syncStore.d.ts.map +1 -0
- package/dist/dts-cjs/withFeed.d.ts +69 -0
- package/dist/dts-cjs/withFeed.d.ts.map +1 -0
- package/dist/dts-esm/config.d.ts +37 -0
- package/dist/dts-esm/config.d.ts.map +1 -0
- package/dist/dts-esm/ensureFeedSchema.d.ts +24 -0
- package/dist/dts-esm/ensureFeedSchema.d.ts.map +1 -0
- package/dist/dts-esm/feed.d.ts +81 -0
- package/dist/dts-esm/feed.d.ts.map +1 -0
- package/dist/dts-esm/index.d.ts +50 -0
- package/dist/dts-esm/index.d.ts.map +1 -0
- package/dist/dts-esm/internal/context.d.ts +28 -0
- package/dist/dts-esm/internal/context.d.ts.map +1 -0
- package/dist/dts-esm/internal/ddl.d.ts +99 -0
- package/dist/dts-esm/internal/ddl.d.ts.map +1 -0
- package/dist/dts-esm/internal/statements.d.ts +127 -0
- package/dist/dts-esm/internal/statements.d.ts.map +1 -0
- package/dist/dts-esm/lease.d.ts +138 -0
- package/dist/dts-esm/lease.d.ts.map +1 -0
- package/dist/dts-esm/package.json +3 -0
- package/dist/dts-esm/syncStore.d.ts +57 -0
- package/dist/dts-esm/syncStore.d.ts.map +1 -0
- package/dist/dts-esm/withFeed.d.ts +69 -0
- package/dist/dts-esm/withFeed.d.ts.map +1 -0
- package/dist/esm/config.js +39 -0
- package/dist/esm/config.js.map +1 -0
- package/dist/esm/ensureFeedSchema.js +31 -0
- package/dist/esm/ensureFeedSchema.js.map +1 -0
- package/dist/esm/feed.js +166 -0
- package/dist/esm/feed.js.map +1 -0
- package/dist/esm/index.js +50 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/internal/context.js +34 -0
- package/dist/esm/internal/context.js.map +1 -0
- package/dist/esm/internal/ddl.js +136 -0
- package/dist/esm/internal/ddl.js.map +1 -0
- package/dist/esm/internal/statements.js +262 -0
- package/dist/esm/internal/statements.js.map +1 -0
- package/dist/esm/lease.js +330 -0
- package/dist/esm/lease.js.map +1 -0
- package/dist/esm/package.json +4 -0
- package/dist/esm/syncStore.js +171 -0
- package/dist/esm/syncStore.js.map +1 -0
- package/dist/esm/withFeed.js +44 -0
- package/dist/esm/withFeed.js.map +1 -0
- package/package.json +35 -3
- package/src/config.ts +46 -0
- package/src/ensureFeedSchema.ts +32 -0
- package/src/feed.ts +284 -0
- package/src/index.ts +67 -0
- package/src/internal/context.ts +44 -0
- package/src/internal/ddl.ts +233 -0
- package/src/internal/statements.ts +387 -0
- package/src/lease.ts +503 -0
- package/src/syncStore.ts +340 -0
- package/src/withFeed.ts +148 -0
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Postgres `FeedSyncStore`: every sink's cursor, its parked list and its
|
|
3
|
+
* worker lease, kept in the source database beside the feed rows (ADR-0019 §10).
|
|
4
|
+
*
|
|
5
|
+
* `@kairos-es/feed`'s `FeedSyncStore.ts` owns the contract, and the in-memory feed
|
|
6
|
+
* is the oracle this store is held to; what is here is how each atomic step the
|
|
7
|
+
* contract names becomes one transaction or one statement.
|
|
8
|
+
*
|
|
9
|
+
* - `casCursor` is ONE transaction: the guarded, holder-fenced cursor update,
|
|
10
|
+
* stamped with its `lastAdvance` from this process's `Clock`, and the parked
|
|
11
|
+
* list moved with it. A guard that loses fails `CursorSuperseded` inside the
|
|
12
|
+
* transaction, which rolls back the parked changes with it, so a park can never
|
|
13
|
+
* outlive a refused advance.
|
|
14
|
+
* - `retryParked` is one transaction that takes the feed lock exclusively before it
|
|
15
|
+
* re-stamps the row, so its new `seq` cannot commit out of order with the
|
|
16
|
+
* materialisation's own writes (`internal/statements.ts` owns that argument).
|
|
17
|
+
* - `collectTombstones` is one statement that reads every registered sink's cursor
|
|
18
|
+
* and deletes below the lowest, or nothing while any of them has no cursor, so
|
|
19
|
+
* no cursor can move between the two.
|
|
20
|
+
* - `removeSink` is one transaction that first takes the sink's advisory lock for
|
|
21
|
+
* its own length, which an advisory worker's session lock refuses, then deletes
|
|
22
|
+
* the cursor row only when no live lease row holds it, in the same statement as
|
|
23
|
+
* that test, and the parks after it. Both tests are therefore made whichever
|
|
24
|
+
* lease kind this store was built with: a sink can be leased by either kind
|
|
25
|
+
* (`lease.ts` owns how the two coexist), and the row alone cannot say whether
|
|
26
|
+
* an advisory holder's session is alive.
|
|
27
|
+
* - `describe` is three reads, each its own snapshot, so a figure in it can be a
|
|
28
|
+
* batch older or newer than the next. It is a report for an operator, and no
|
|
29
|
+
* decision is taken on it.
|
|
30
|
+
*
|
|
31
|
+
* One store serves every feed in its prefix: unlike the in-memory feed, whose
|
|
32
|
+
* store belongs to one feed, a SQL store's state is keyed by feed name in shared
|
|
33
|
+
* tables, so a single store value is what every worker over the prefix takes.
|
|
34
|
+
*
|
|
35
|
+
* Infrastructure faults are `FeedUnreadable` on a read and `FeedWriteFailed` on a
|
|
36
|
+
* write, never defects, so the driver's supervision backs off and tries again.
|
|
37
|
+
*/
|
|
38
|
+
import { type FeedSyncStore } from '@kairos-es/feed';
|
|
39
|
+
import { Effect } from 'effect';
|
|
40
|
+
import { SqlClient } from 'effect/sql';
|
|
41
|
+
import type { FeedPostgresConfig } from './config.js';
|
|
42
|
+
import { type FeedLeaseConfig } from './lease.js';
|
|
43
|
+
/** Options for {@link makePostgresFeedSyncStore}. */
|
|
44
|
+
export interface PostgresFeedSyncStoreOptions {
|
|
45
|
+
/** The `{ schema, tablePrefix }` pair the feeds were written under. */
|
|
46
|
+
readonly config?: FeedPostgresConfig;
|
|
47
|
+
/** The lease kind; `leaseRow()` with its defaults when absent. */
|
|
48
|
+
readonly lease?: FeedLeaseConfig;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Build the sync store over the ambient `SqlClient`. Dies at construction on a
|
|
52
|
+
* malformed config, a lease whose heartbeat is not shorter than its ttl or leaves
|
|
53
|
+
* no room for its margin, or an `advisoryLock` connection recognisably pooled.
|
|
54
|
+
* `ensureFeedSchema` must have run.
|
|
55
|
+
*/
|
|
56
|
+
export declare const makePostgresFeedSyncStore: (options?: PostgresFeedSyncStoreOptions) => Effect.Effect<FeedSyncStore, never, SqlClient.SqlClient>;
|
|
57
|
+
//# sourceMappingURL=syncStore.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"syncStore.d.ts","sourceRoot":"","sources":["../../src/syncStore.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AACH,OAAO,EAML,KAAK,aAAa,EAOnB,MAAM,iBAAiB,CAAA;AACxB,OAAO,EAAmB,MAAM,EAAU,MAAM,QAAQ,CAAA;AACxD,OAAO,EAAE,SAAS,EAAE,MAAM,YAAY,CAAA;AACtC,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,UAAU,CAAA;AAOlD,OAAO,EAEL,KAAK,eAAe,EAGrB,MAAM,SAAS,CAAA;AAEhB,qDAAqD;AACrD,MAAM,WAAW,4BAA4B;IAC3C,uEAAuE;IACvE,QAAQ,CAAC,MAAM,CAAC,EAAE,kBAAkB,CAAA;IACpC,kEAAkE;IAClE,QAAQ,CAAC,KAAK,CAAC,EAAE,eAAe,CAAA;CACjC;AA0DD;;;;;GAKG;AACH,eAAO,MAAM,yBAAyB,GACpC,UAAS,4BAAiC,KACzC,MAAM,CAAC,MAAM,CAAC,aAAa,EAAE,KAAK,EAAE,SAAS,CAAC,SAAS,CAwMtD,CAAA"}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `withFeed` — a `ProjectionStore` decorated so that resetting the checkpoint a
|
|
3
|
+
* feed is bound to also empties the feed and moves its generation on, in ONE
|
|
4
|
+
* transaction.
|
|
5
|
+
*
|
|
6
|
+
* ## Why a decorator
|
|
7
|
+
*
|
|
8
|
+
* Nothing in the library rebuilds a read model; a rebuild is an application
|
|
9
|
+
* resetting a checkpoint and letting the runner replay. A feed has to know when
|
|
10
|
+
* that happens, because a sink holding the old copy must be rebuilt from the
|
|
11
|
+
* replay rather than patched by it: a record the replay never writes again would
|
|
12
|
+
* otherwise survive in the sink. The reset is the one moment both facts are true,
|
|
13
|
+
* so the generation bump rides it.
|
|
14
|
+
*
|
|
15
|
+
* What it adds is the FEED's half of a rebuild and nothing more. A rebuild resets
|
|
16
|
+
* the checkpoint, drops the view, and starts again from `ORIGIN`; this resets the
|
|
17
|
+
* checkpoint and the feed, and the view is still the consumer's to drop. So
|
|
18
|
+
* rebuilding a DURABLE view means emptying it yourself before the replay, exactly
|
|
19
|
+
* as the port's own `resetCheckpoint` leaves it. Skip that and a row the replay
|
|
20
|
+
* never writes again survives in the view, and a read model whose `apply` reads
|
|
21
|
+
* its view back (a running count, say) builds on the old rows, which the feed
|
|
22
|
+
* then mirrors to every sink.
|
|
23
|
+
*
|
|
24
|
+
* The decorator keeps the port's three methods and replaces only
|
|
25
|
+
* `resetCheckpoint`, so it works through `forKey` exactly as the store does, and
|
|
26
|
+
* nothing about the read side has to know a feed exists.
|
|
27
|
+
*
|
|
28
|
+
* ## One transaction, and the client it needs
|
|
29
|
+
*
|
|
30
|
+
* The reset bumps the generation, deletes the checkpoint row through the store's
|
|
31
|
+
* own `resetCheckpoint`, and deletes the feed's rows, all inside one
|
|
32
|
+
* `withTransaction` on the client this decorator was built over. The store's own
|
|
33
|
+
* statement joins that transaction only when the store was built from the SAME
|
|
34
|
+
* client instance (ADR-0018 §3), which `@kairos-es/read-postgres`'s
|
|
35
|
+
* `makeSqlProjectionStore` is when both are built under one `SqlClient`. Build
|
|
36
|
+
* them apart and the checkpoint delete commits on its own: the reset still
|
|
37
|
+
* happens, but not atomically with the bump.
|
|
38
|
+
*
|
|
39
|
+
* The generation is bumped FIRST. That takes the feed lock exclusively, so a
|
|
40
|
+
* materialisation commit in flight finishes before the reset proceeds, and one
|
|
41
|
+
* that starts after it waits; and it takes the locks in the same order a commit
|
|
42
|
+
* does (the feed, then the checkpoint), so the two cannot deadlock. A commit that
|
|
43
|
+
* was mid-flight with the old checkpoint then loses its guard and rolls its feed
|
|
44
|
+
* rows back with its view.
|
|
45
|
+
*
|
|
46
|
+
* ## The binding
|
|
47
|
+
*
|
|
48
|
+
* A feed serves ONE materialisation, because one writer is what keeps its `seq`
|
|
49
|
+
* ascending in commit order. The binding is recorded on the feed's generations
|
|
50
|
+
* row the first time the feed is decorated, and checked at every construction
|
|
51
|
+
* after that, so a feed bound to another checkpoint key is refused across
|
|
52
|
+
* processes, not only within one. A refusal is a defect, as the read side's
|
|
53
|
+
* construction gate's are.
|
|
54
|
+
*
|
|
55
|
+
* The decorated store must be DURABLE. This feed is, and the feed's rows are
|
|
56
|
+
* written inside the store's commit transaction: an ephemeral store opens no
|
|
57
|
+
* transaction for them to join.
|
|
58
|
+
*/
|
|
59
|
+
import type { FeedWriteFailed } from '@kairos-es/feed';
|
|
60
|
+
import { type CheckpointKey, type ProjectionStore } from '@kairos-es/read';
|
|
61
|
+
import { Effect } from 'effect';
|
|
62
|
+
import { SqlClient } from 'effect/sql';
|
|
63
|
+
import { type PostgresFeed } from './feed.js';
|
|
64
|
+
/**
|
|
65
|
+
* `store`, with `resetCheckpoint` for `key` also emptying `feed` and moving its
|
|
66
|
+
* generation on; see the module doc. Every other key passes through untouched.
|
|
67
|
+
*/
|
|
68
|
+
export declare const withFeed: <D>(store: ProjectionStore, feed: PostgresFeed<D>, key: CheckpointKey) => Effect.Effect<ProjectionStore, FeedWriteFailed, SqlClient.SqlClient>;
|
|
69
|
+
//# sourceMappingURL=withFeed.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"withFeed.d.ts","sourceRoot":"","sources":["../../src/withFeed.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyDG;AACH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAA;AACtD,OAAO,EACL,KAAK,aAAa,EAClB,KAAK,eAAe,EAErB,MAAM,iBAAiB,CAAA;AACxB,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAA;AAC/B,OAAO,EAAE,SAAS,EAAE,MAAM,YAAY,CAAA;AACtC,OAAO,EAAa,KAAK,YAAY,EAAE,MAAM,QAAQ,CAAA;AAMrD;;;GAGG;AACH,eAAO,MAAM,QAAQ,GAAI,CAAC,EACxB,OAAO,eAAe,EACtB,MAAM,YAAY,CAAC,CAAC,CAAC,EACrB,KAAK,aAAa,KACjB,MAAM,CAAC,MAAM,CAAC,eAAe,EAAE,eAAe,EAAE,SAAS,CAAC,SAAS,CAmElE,CAAA"}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public configuration for the Postgres change feed.
|
|
3
|
+
*
|
|
4
|
+
* A feed backend is defined by its `{ schema, tablePrefix }` naming pair alone:
|
|
5
|
+
* every object it owns is DERIVED from it (`internal/ddl.ts`), so one config
|
|
6
|
+
* value is one isolated set of feed tables, cohabiting per prefix beside the
|
|
7
|
+
* event store's objects and `@kairos-es/read-postgres`'s checkpoint table. Pass
|
|
8
|
+
* the same pair to all three and one prefix names one complete world: the log,
|
|
9
|
+
* the checkpoints, and the feeds the read models write.
|
|
10
|
+
*
|
|
11
|
+
* It is a `Schema` rather than a bare interface so that a malformed value fails
|
|
12
|
+
* LOUDLY at construction rather than obscurely at the first statement: an empty or
|
|
13
|
+
* dotted atom (a dot would break the qualified-name split), or a pair whose
|
|
14
|
+
* derived names would exceed Postgres's 63-byte identifier limit and be silently
|
|
15
|
+
* truncated. The shape and its checks are `@kairos-es/read-postgres`'s, written
|
|
16
|
+
* again here rather than imported, because each backend measures only the names it
|
|
17
|
+
* creates and a sibling backend's internals are not a public surface.
|
|
18
|
+
*/
|
|
19
|
+
import { Schema } from 'effect';
|
|
20
|
+
/**
|
|
21
|
+
* Configuration for the feed's tables.
|
|
22
|
+
*
|
|
23
|
+
* - `schema` — the Postgres schema that owns them (default `public`).
|
|
24
|
+
* - `tablePrefix` — the stem every name derives from (default `dcb_events`,
|
|
25
|
+
* giving `dcb_events_feed_rows` and its siblings).
|
|
26
|
+
*/
|
|
27
|
+
export declare const FeedPostgresConfig: Schema.Struct<{
|
|
28
|
+
readonly schema: Schema.optionalKey<Schema.String>;
|
|
29
|
+
readonly tablePrefix: Schema.optionalKey<Schema.String>;
|
|
30
|
+
}>;
|
|
31
|
+
export type FeedPostgresConfig = typeof FeedPostgresConfig.Type;
|
|
32
|
+
/** Decode a config, throwing a `SchemaError` on any violation. */
|
|
33
|
+
export declare const decodeFeedPostgresConfig: (input: unknown, options?: import("effect/SchemaAST").ParseOptions) => {
|
|
34
|
+
readonly schema?: string;
|
|
35
|
+
readonly tablePrefix?: string;
|
|
36
|
+
};
|
|
37
|
+
//# sourceMappingURL=config.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/config.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AACH,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAA;AAY/B;;;;;;GAMG;AACH,eAAO,MAAM,kBAAkB;;;EAGqC,CAAA;AACpE,MAAM,MAAM,kBAAkB,GAAG,OAAO,kBAAkB,CAAC,IAAI,CAAA;AAE/D,kEAAkE;AAClE,eAAO,MAAM,wBAAwB;;;CACS,CAAA"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `ensureFeedSchema` — the feed's idempotent DDL, as a payload for the consumer's
|
|
3
|
+
* migrator rather than a migrator of its own, exactly as `ensureCheckpointSchema`
|
|
4
|
+
* is for the read side and `ensureSchema` for the event store.
|
|
5
|
+
*
|
|
6
|
+
* It requires the GENERIC `SqlClient`, so it drops into an existing migrator:
|
|
7
|
+
*
|
|
8
|
+
* ```ts
|
|
9
|
+
* Migrator.fromRecord({
|
|
10
|
+
* '0003_kairos_feed': ensureFeedSchema(config),
|
|
11
|
+
* })
|
|
12
|
+
* ```
|
|
13
|
+
*
|
|
14
|
+
* or runs once at boot. Every statement is `IF NOT EXISTS`, so running it again
|
|
15
|
+
* against existing tables keeps every row. Nothing in this package performs DDL at
|
|
16
|
+
* runtime: a feed cannot conjure its own tables, so schema ownership stays with
|
|
17
|
+
* whoever owns the migrations.
|
|
18
|
+
*/
|
|
19
|
+
import { Effect } from 'effect';
|
|
20
|
+
import { SqlClient, type SqlError } from 'effect/sql';
|
|
21
|
+
import { type FeedPostgresConfig } from './config.js';
|
|
22
|
+
/** The feed tables' migration payload for `config`. */
|
|
23
|
+
export declare const ensureFeedSchema: (config?: FeedPostgresConfig) => Effect.Effect<void, SqlError.SqlError, SqlClient.SqlClient>;
|
|
24
|
+
//# sourceMappingURL=ensureFeedSchema.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ensureFeedSchema.d.ts","sourceRoot":"","sources":["../../src/ensureFeedSchema.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AACH,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAA;AAC/B,OAAO,EAAE,SAAS,EAAE,KAAK,QAAQ,EAAE,MAAM,YAAY,CAAA;AACrD,OAAO,EAA4B,KAAK,kBAAkB,EAAE,MAAM,UAAU,CAAA;AAG5E,uDAAuD;AACvD,eAAO,MAAM,gBAAgB,GAC3B,SAAQ,kBAAuB,KAC9B,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,QAAQ,CAAC,QAAQ,EAAE,SAAS,CAAC,SAAS,CAKzD,CAAA"}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Postgres feed's two faces: the `BatchedFeedWriter` a read model's `apply`
|
|
3
|
+
* writes through, and the `FeedSource` a sync worker tails.
|
|
4
|
+
*
|
|
5
|
+
* ## The writer rides the read model's own client
|
|
6
|
+
*
|
|
7
|
+
* A feed row must exist if and only if the view write and the checkpoint advance
|
|
8
|
+
* that produced it landed (ADR-0019 §2). `@kairos-es/read-postgres`'s `commit` is
|
|
9
|
+
* one transaction over the client the store was built from, and `effect/sql`
|
|
10
|
+
* carries that transaction in the Effect CONTEXT under a key that belongs to that
|
|
11
|
+
* one client instance (ADR-0018 §3). So the writer does not capture a client: it
|
|
12
|
+
* reads `SqlClient` from the context AT WRITE TIME, which inside `commit`'s
|
|
13
|
+
* `viewWrites` is the read model's own, and its statements join the open
|
|
14
|
+
* transaction and roll back with it on a lost guard. That is why `SqlClient` sits
|
|
15
|
+
* in the writer's `RW` and not in the factory's requirements alone. A write
|
|
16
|
+
* through any OTHER client instance is not bracketed, and the exactly-once
|
|
17
|
+
* property does not extend to it.
|
|
18
|
+
*
|
|
19
|
+
* ## What one `write` sends
|
|
20
|
+
*
|
|
21
|
+
* The body's upserts and removes are buffered, last write per key winning, and
|
|
22
|
+
* land as at most three statements whatever the batch size: the feed lock (see
|
|
23
|
+
* `internal/statements.ts` for why the writer takes it), one upsert of every
|
|
24
|
+
* changed document and one tombstone of every removed key. The upsert writes
|
|
25
|
+
* nothing for a document equal to the stored one, compared as `jsonb`, so key
|
|
26
|
+
* order is no change; the tombstone writes nothing for a key that is absent or
|
|
27
|
+
* already a tombstone.
|
|
28
|
+
*
|
|
29
|
+
* Each document is encoded through its `Schema` before anything is sent. A
|
|
30
|
+
* document that fails to encode is a DEFECT in the read model, not a fault of the
|
|
31
|
+
* feed and not something a sink should ever be asked to refuse.
|
|
32
|
+
*
|
|
33
|
+
* ## The source
|
|
34
|
+
*
|
|
35
|
+
* `changesAfter` pages the feed's rows by `seq`, tombstones included, and decodes
|
|
36
|
+
* each document through the same `Schema`. A stored document that no longer
|
|
37
|
+
* decodes is a defect too: it means the schema changed without the feed being
|
|
38
|
+
* rebuilt, which retrying cannot mend.
|
|
39
|
+
*
|
|
40
|
+
* `listen` is optional and is a WAKE only. This package takes no reserved
|
|
41
|
+
* connection for a `LISTEN` of its own, as `@kairos-es/read-postgres` takes none,
|
|
42
|
+
* so the poll is the guarantee. {@link eventStoreWake} offers the event store's
|
|
43
|
+
* `subscribe` as a wake: it fires when an event is appended, which is BEFORE the
|
|
44
|
+
* projection has applied it and written the feed, so a woken read may find nothing
|
|
45
|
+
* new and the poll picks the row up. It shortens the wait; it never decides
|
|
46
|
+
* correctness.
|
|
47
|
+
*/
|
|
48
|
+
import { DcbEventStore, type Query } from '@kairos-es/core';
|
|
49
|
+
import { type BatchedFeedWriter, type FeedName, type FeedSource, type FeedUnreadable, type FeedWriteFailed } from '@kairos-es/feed';
|
|
50
|
+
import { Effect, Schema, Stream } from 'effect';
|
|
51
|
+
import { SqlClient } from 'effect/sql';
|
|
52
|
+
import type { FeedPostgresConfig } from './config.js';
|
|
53
|
+
/** A Postgres feed: its writer, and the source a sync worker reads. */
|
|
54
|
+
export interface PostgresFeed<D> extends BatchedFeedWriter<D, SqlClient.SqlClient> {
|
|
55
|
+
readonly source: FeedSource<D>;
|
|
56
|
+
}
|
|
57
|
+
/** Options for {@link makePostgresFeed}. */
|
|
58
|
+
export interface PostgresFeedOptions<D, I> {
|
|
59
|
+
/** The `{ schema, tablePrefix }` pair; the same one the event store and read side take. */
|
|
60
|
+
readonly config?: FeedPostgresConfig;
|
|
61
|
+
readonly feed: FeedName;
|
|
62
|
+
/** The document's schema: what the writer encodes through and the source decodes through. */
|
|
63
|
+
readonly doc: Schema.Codec<D, I>;
|
|
64
|
+
/** A wake for the sync worker, such as {@link eventStoreWake}'s. The poll is the guarantee. */
|
|
65
|
+
readonly listen?: Stream.Stream<unknown, FeedUnreadable>;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Build a feed over the ambient `SqlClient`. Creates the feed's generations row
|
|
69
|
+
* if it is absent, at generation 0, so the writer's feed lock always has a row to
|
|
70
|
+
* take; that is the only write it makes. `ensureFeedSchema` must have run first.
|
|
71
|
+
*/
|
|
72
|
+
export declare const makePostgresFeed: <D, I>(options: PostgresFeedOptions<D, I>) => Effect.Effect<PostgresFeed<D>, FeedWriteFailed, SqlClient.SqlClient>;
|
|
73
|
+
/**
|
|
74
|
+
* A wake from the event store: one element per event matching `query` appended
|
|
75
|
+
* after the moment the stream is subscribed, and nothing else. Pass it as
|
|
76
|
+
* `listen`. Early by the projection's own lag, as the module doc says, so the
|
|
77
|
+
* worker's poll still bounds the latency; `query` should be the read model's own,
|
|
78
|
+
* so events it never applies wake nothing.
|
|
79
|
+
*/
|
|
80
|
+
export declare const eventStoreWake: (query: Query) => Effect.Effect<Stream.Stream<unknown>, never, DcbEventStore>;
|
|
81
|
+
//# sourceMappingURL=feed.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"feed.d.ts","sourceRoot":"","sources":["../../src/feed.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8CG;AACH,OAAO,EACL,aAAa,EAEb,KAAK,KAAK,EAEX,MAAM,iBAAiB,CAAA;AACxB,OAAO,EACL,KAAK,iBAAiB,EAEtB,KAAK,QAAQ,EAGb,KAAK,UAAU,EACf,KAAK,cAAc,EACnB,KAAK,eAAe,EAGrB,MAAM,iBAAiB,CAAA;AACxB,OAAO,EAAE,MAAM,EAAU,MAAM,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAA;AACvD,OAAO,EAAE,SAAS,EAAE,MAAM,YAAY,CAAA;AACtC,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,UAAU,CAAA;AASlD,uEAAuE;AACvE,MAAM,WAAW,YAAY,CAAC,CAAC,CAC7B,SAAQ,iBAAiB,CAAC,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC;IACjD,QAAQ,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC,CAAC,CAAA;CAC/B;AAED,4CAA4C;AAC5C,MAAM,WAAW,mBAAmB,CAAC,CAAC,EAAE,CAAC;IACvC,2FAA2F;IAC3F,QAAQ,CAAC,MAAM,CAAC,EAAE,kBAAkB,CAAA;IACpC,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAA;IACvB,6FAA6F;IAC7F,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAA;IAChC,+FAA+F;IAC/F,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,CAAC,OAAO,EAAE,cAAc,CAAC,CAAA;CACzD;AAiBD;;;;GAIG;AACH,eAAO,MAAM,gBAAgB,GAAI,CAAC,EAAE,CAAC,EACnC,SAAS,mBAAmB,CAAC,CAAC,EAAE,CAAC,CAAC,KACjC,MAAM,CAAC,MAAM,CAAC,YAAY,CAAC,CAAC,CAAC,EAAE,eAAe,EAAE,SAAS,CAAC,SAAS,CAoJlE,CAAA;AAEJ;;;;;;GAMG;AACH,eAAO,MAAM,cAAc,GACzB,OAAO,KAAK,KACX,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,EAAE,KAAK,EAAE,aAAa,CASzD,CAAA"}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @kairos-es/feed-postgres — the Postgres change feed: the feed tables and their
|
|
3
|
+
* idempotent DDL, the `FeedWriter` that rides a read model's own `SqlClient`
|
|
4
|
+
* inside its commit, the `FeedSource`, the `FeedSyncStore` with its worker lease,
|
|
5
|
+
* and the store decorator that moves the feed's generation on a reset
|
|
6
|
+
* (ADR-0019).
|
|
7
|
+
*
|
|
8
|
+
* A package of its own rather than part of `@kairos-es/read-postgres`, so that a
|
|
9
|
+
* read model with no feed gains no `@kairos-es/feed` peer (ADR-0003). It needs
|
|
10
|
+
* only the GENERIC `SqlClient`, and takes no reserved connection unless the
|
|
11
|
+
* consumer chooses the advisory-lock lease and dedicates one to it.
|
|
12
|
+
*
|
|
13
|
+
* Where each part's argument lives:
|
|
14
|
+
*
|
|
15
|
+
* - `config.ts` and `internal/ddl.ts` — the `{ schema, tablePrefix }` pair and the
|
|
16
|
+
* five objects derived from it; `ensureFeedSchema.ts` — the migration payload.
|
|
17
|
+
* - `internal/statements.ts` — every statement the package sends, and the feed
|
|
18
|
+
* lock that keeps `seq` ascending in commit order.
|
|
19
|
+
* - `feed.ts` — the writer, the source and the wake.
|
|
20
|
+
* - `syncStore.ts` — the cursor, the parked list and their atomic steps.
|
|
21
|
+
* - `lease.ts` — the two lease kinds, and what each costs.
|
|
22
|
+
* - `withFeed.ts` — the reset, the generation and the binding.
|
|
23
|
+
*
|
|
24
|
+
* Wiring, end to end:
|
|
25
|
+
*
|
|
26
|
+
* ```ts
|
|
27
|
+
* const config = { schema: 'public', tablePrefix: 'dcb_events' }
|
|
28
|
+
* const key = checkpointKey(ProjectionId.make('course-search'))
|
|
29
|
+
*
|
|
30
|
+
* // 1. In YOUR migrator, beside `ensureSchema` and `ensureCheckpointSchema`:
|
|
31
|
+
* // ensureFeedSchema(config)
|
|
32
|
+
*
|
|
33
|
+
* // 2. Where the read model is wired, under ONE SqlClient:
|
|
34
|
+
* const feed = yield* makePostgresFeed({ config, feed: FeedName.make('course-search'), doc: CourseDoc })
|
|
35
|
+
* const store = yield* withFeed(yield* makeSqlProjectionStore(config), feed, key)
|
|
36
|
+
* // …and in the read model's `apply`, inside `commit`:
|
|
37
|
+
* // feed.write(lastPosition, (writer) => writer.upsert(FeedKey.make(id), toDoc(course)))
|
|
38
|
+
*
|
|
39
|
+
* // 3. Beside the projections, one worker per feed and sink:
|
|
40
|
+
* const syncStore = yield* makePostgresFeedSyncStore({ config })
|
|
41
|
+
* yield* Effect.forkScoped(runFeedSync({ source: feed.source, sink, store: syncStore }))
|
|
42
|
+
* ```
|
|
43
|
+
*/
|
|
44
|
+
export { decodeFeedPostgresConfig, FeedPostgresConfig, } from './config.js';
|
|
45
|
+
export { ensureFeedSchema } from './ensureFeedSchema.js';
|
|
46
|
+
export { eventStoreWake, makePostgresFeed, type PostgresFeed, type PostgresFeedOptions, } from './feed.js';
|
|
47
|
+
export { advisoryLock, DEFAULT_LEASE_HEARTBEAT, DEFAULT_LEASE_TTL, type FeedLeaseConfig, leaseRow, pooledConnectionFault, } from './lease.js';
|
|
48
|
+
export { makePostgresFeedSyncStore, type PostgresFeedSyncStoreOptions, } from './syncStore.js';
|
|
49
|
+
export { withFeed } from './withFeed.js';
|
|
50
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0CG;AACH,OAAO,EACL,wBAAwB,EACxB,kBAAkB,GACnB,MAAM,UAAU,CAAA;AACjB,OAAO,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAA;AACrD,OAAO,EACL,cAAc,EACd,gBAAgB,EAChB,KAAK,YAAY,EACjB,KAAK,mBAAmB,GACzB,MAAM,QAAQ,CAAA;AACf,OAAO,EACL,YAAY,EACZ,uBAAuB,EACvB,iBAAiB,EACjB,KAAK,eAAe,EACpB,QAAQ,EACR,qBAAqB,GACtB,MAAM,SAAS,CAAA;AAChB,OAAO,EACL,yBAAyB,EACzB,KAAK,4BAA4B,GAClC,MAAM,aAAa,CAAA;AACpB,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAA"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What every part of this package shares about one config: the resolved names,
|
|
3
|
+
* the compiled statements, and the error mapping from `SqlError` to the feed's own
|
|
4
|
+
* two infrastructure errors.
|
|
5
|
+
*
|
|
6
|
+
* A `PostgresFeed` carries its context privately, in a `WeakMap` keyed by the
|
|
7
|
+
* feed value, so `withFeed` can reach the feed's names and statements without the
|
|
8
|
+
* public type growing a field no consumer should touch.
|
|
9
|
+
*/
|
|
10
|
+
import { FeedUnreadable, FeedWriteFailed } from '@kairos-es/feed';
|
|
11
|
+
import type { SqlError } from 'effect/sql';
|
|
12
|
+
import { type FeedPostgresConfig } from '../config.js';
|
|
13
|
+
import { type ResolvedFeedNames } from './ddl.js';
|
|
14
|
+
import { type FeedStatements } from './statements.js';
|
|
15
|
+
/** One config's names and statements. */
|
|
16
|
+
export interface FeedContext {
|
|
17
|
+
readonly names: ResolvedFeedNames;
|
|
18
|
+
readonly statements: FeedStatements;
|
|
19
|
+
}
|
|
20
|
+
/** Decode `config` and compile its statements; throws on a malformed config. */
|
|
21
|
+
export declare const feedContext: (config: FeedPostgresConfig) => FeedContext;
|
|
22
|
+
/** A read that failed, named for what was being read. */
|
|
23
|
+
export declare const unreadable: (attempt: string) => (cause: SqlError.SqlError) => FeedUnreadable;
|
|
24
|
+
/** A write that failed, named for what was being written. */
|
|
25
|
+
export declare const writeFailed: (attempt: string) => (cause: SqlError.SqlError) => FeedWriteFailed;
|
|
26
|
+
/** A 64-bit column read back as text, as a `bigint`; see `statements.ts`. */
|
|
27
|
+
export declare const bigintOf: (text: string | bigint | number) => bigint;
|
|
28
|
+
//# sourceMappingURL=context.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"context.d.ts","sourceRoot":"","sources":["../../../src/internal/context.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,EAAE,cAAc,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAA;AACjE,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAA;AAC1C,OAAO,EAA4B,KAAK,kBAAkB,EAAE,MAAM,WAAW,CAAA;AAC7E,OAAO,EAAE,KAAK,iBAAiB,EAAoB,MAAM,OAAO,CAAA;AAChE,OAAO,EAAyB,KAAK,cAAc,EAAE,MAAM,cAAc,CAAA;AAEzE,yCAAyC;AACzC,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,KAAK,EAAE,iBAAiB,CAAA;IACjC,QAAQ,CAAC,UAAU,EAAE,cAAc,CAAA;CACpC;AAED,gFAAgF;AAChF,eAAO,MAAM,WAAW,GAAI,QAAQ,kBAAkB,KAAG,WAGxD,CAAA;AAED,yDAAyD;AACzD,eAAO,MAAM,UAAU,GACpB,SAAS,MAAM,MACf,OAAO,QAAQ,CAAC,QAAQ,KAAG,cACkD,CAAA;AAEhF,6DAA6D;AAC7D,eAAO,MAAM,WAAW,GACrB,SAAS,MAAM,MACf,OAAO,QAAQ,CAAC,QAAQ,KAAG,eAIxB,CAAA;AAEN,6EAA6E;AAC7E,eAAO,MAAM,QAAQ,GAAI,MAAM,MAAM,GAAG,MAAM,GAAG,MAAM,KAAG,MAAsB,CAAA"}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Internal object naming and DDL for the Postgres change feed.
|
|
3
|
+
*
|
|
4
|
+
* The single source of truth for the FIVE objects this package owns — the feed
|
|
5
|
+
* rows, the sequence they share, the generations, the sync cursors and the parked
|
|
6
|
+
* list — and for the idempotent DDL that creates them. Every name is derived ONCE
|
|
7
|
+
* from `{ schema, tablePrefix }` and reused by `ensureFeedSchema` (the migration
|
|
8
|
+
* payload) and by every runtime statement in `statements.ts`, so a migration and a
|
|
9
|
+
* query can never disagree about what an object is called.
|
|
10
|
+
*
|
|
11
|
+
* The pair is the SAME one the event store and `@kairos-es/read-postgres` take, so
|
|
12
|
+
* the feed's tables land beside the log and the checkpoints they belong with, and
|
|
13
|
+
* one prefix keeps naming one complete, isolated world (ADR-0002).
|
|
14
|
+
*
|
|
15
|
+
* The identifier discipline is `@kairos-es/read-postgres`'s, duplicated rather
|
|
16
|
+
* than imported for the reason that package gives for duplicating the event
|
|
17
|
+
* store's: each package measures and quotes only the names IT materialises, and
|
|
18
|
+
* importing a sibling backend's internals would couple two packages that must
|
|
19
|
+
* stay independently publishable. Identifiers are pre-quoted here and embedded as
|
|
20
|
+
* literal text into `sql.unsafe`; values are always bound `$n` parameters. Names
|
|
21
|
+
* never pass through `sql(identifier)`, because the consumer's client may carry a
|
|
22
|
+
* query-name transform that would rewrite them.
|
|
23
|
+
*/
|
|
24
|
+
import type { SqlClient, Statement } from 'effect/sql';
|
|
25
|
+
/** Every object a feed config owns, as qualified (unquoted) names. */
|
|
26
|
+
export interface ResolvedFeedNames {
|
|
27
|
+
readonly schema: string;
|
|
28
|
+
readonly tablePrefix: string;
|
|
29
|
+
/** `<schema>.<prefix>_feed_rows`: one row per feed and key, the newest state. */
|
|
30
|
+
readonly rowsTable: string;
|
|
31
|
+
/** `<prefix>_feed_rows_seq_idx`: the `(feed, seq)` index the tail reads by. */
|
|
32
|
+
readonly rowsSeqIndex: string;
|
|
33
|
+
/** `<schema>.<prefix>_feed_seq`: the one sequence every feed in the prefix shares. */
|
|
34
|
+
readonly sequence: string;
|
|
35
|
+
/** `<schema>.<prefix>_feed_generations`: each feed's generation and binding. */
|
|
36
|
+
readonly generationsTable: string;
|
|
37
|
+
/** `<schema>.<prefix>_feed_cursors`: one cursor and lease per feed and sink. */
|
|
38
|
+
readonly cursorsTable: string;
|
|
39
|
+
/** `<schema>.<prefix>_feed_parked`: the records a sink refused. */
|
|
40
|
+
readonly parkedTable: string;
|
|
41
|
+
}
|
|
42
|
+
/** The default schema, identical to the event store's and the read side's. */
|
|
43
|
+
export declare const DEFAULT_SCHEMA = "public";
|
|
44
|
+
/** The default table prefix, identical to the event store's and the read side's. */
|
|
45
|
+
export declare const DEFAULT_TABLE_PREFIX = "dcb_events";
|
|
46
|
+
/**
|
|
47
|
+
* The first derived identifier over Postgres's 63-byte limit, as a message, or
|
|
48
|
+
* `undefined` when every name fits. Postgres truncates an over-long identifier
|
|
49
|
+
* with only a notice, so two prefixes sharing a 63-byte stem would silently share
|
|
50
|
+
* one feed table; refusing at the boundary beats that.
|
|
51
|
+
*/
|
|
52
|
+
export declare const oversizedIdentifier: (config: {
|
|
53
|
+
readonly schema?: string;
|
|
54
|
+
readonly tablePrefix?: string;
|
|
55
|
+
}) => string | undefined;
|
|
56
|
+
/** Derive every name from the pair; throws on an over-long identifier. */
|
|
57
|
+
export declare const resolveFeedNames: (config: {
|
|
58
|
+
readonly schema?: string;
|
|
59
|
+
readonly tablePrefix?: string;
|
|
60
|
+
}) => ResolvedFeedNames;
|
|
61
|
+
/** Quote a possibly qualified identifier, atom by atom, for literal SQL text. */
|
|
62
|
+
export declare const quoteQualified: (name: string) => string;
|
|
63
|
+
/** Quote one identifier atom, doubling any embedded `"`. */
|
|
64
|
+
export declare const quoteAtom: (name: string) => string;
|
|
65
|
+
/**
|
|
66
|
+
* The quoted sequence name as a SQL string LITERAL, for `nextval('…')`. The
|
|
67
|
+
* literal is parsed as a `regclass`, which honours the double quotes inside it, so
|
|
68
|
+
* an awkward prefix resolves to exactly the object the DDL created; any `'` in it
|
|
69
|
+
* is doubled so the literal cannot be closed early.
|
|
70
|
+
*/
|
|
71
|
+
export declare const sequenceLiteral: (names: ResolvedFeedNames) => string;
|
|
72
|
+
/**
|
|
73
|
+
* The ordered, idempotent DDL. Every statement is `IF NOT EXISTS`, so a second
|
|
74
|
+
* run against existing objects is a no-op that keeps every row.
|
|
75
|
+
*
|
|
76
|
+
* Shapes that are load-bearing:
|
|
77
|
+
*
|
|
78
|
+
* - `_feed_rows` is keyed `(feed, key)`: one row per record, which is what
|
|
79
|
+
* "coalesced, latest wins" means in storage, and the arbiter the writer's
|
|
80
|
+
* `ON CONFLICT` names. `doc` is NULL for a tombstone; a document that encodes to
|
|
81
|
+
* JSON `null` is the jsonb value `null`, which is not SQL NULL, so the two
|
|
82
|
+
* cannot be confused.
|
|
83
|
+
* - The `(feed, seq)` index is what `changesAfter` pages by.
|
|
84
|
+
* - ONE sequence serves every feed in the prefix. A feed's `seq` values therefore
|
|
85
|
+
* have gaps, which the cursor never minds: it compares, never counts.
|
|
86
|
+
* - `_feed_cursors` holds the cursor AND the lease, so the cursor's
|
|
87
|
+
* compare-and-set can fence on the lease holder in the same row it moves. The
|
|
88
|
+
* cursor is `(generation, seq)`, both NULL before a sink's first sync and never
|
|
89
|
+
* one without the other, which the `CHECK` holds. `last_advance_at` is when
|
|
90
|
+
* the cursor last moved, NULL until it first does; taking a lease leaves it
|
|
91
|
+
* alone, so a sink registered and never run shows as one.
|
|
92
|
+
* - `_feed_generations` holds the checkpoint key a feed is bound to, so binding
|
|
93
|
+
* one feed to two materialisations is refused across processes, not only within
|
|
94
|
+
* one.
|
|
95
|
+
*/
|
|
96
|
+
export declare const feedDdlStatements: (names: ResolvedFeedNames) => ReadonlyArray<string>;
|
|
97
|
+
/** The DDL as statements on `sql`, one `sql.unsafe` each, in order. */
|
|
98
|
+
export declare const runFeedDdl: (sql: SqlClient.SqlClient, names: ResolvedFeedNames) => ReadonlyArray<Statement.Statement<unknown>>;
|
|
99
|
+
//# sourceMappingURL=ddl.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ddl.d.ts","sourceRoot":"","sources":["../../../src/internal/ddl.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,OAAO,KAAK,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,YAAY,CAAA;AAEtD,sEAAsE;AACtE,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAA;IAC5B,iFAAiF;IACjF,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;IAC1B,+EAA+E;IAC/E,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAA;IAC7B,sFAAsF;IACtF,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;IACzB,gFAAgF;IAChF,QAAQ,CAAC,gBAAgB,EAAE,MAAM,CAAA;IACjC,gFAAgF;IAChF,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAA;IAC7B,mEAAmE;IACnE,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAA;CAC7B;AAED,8EAA8E;AAC9E,eAAO,MAAM,cAAc,WAAW,CAAA;AACtC,oFAAoF;AACpF,eAAO,MAAM,oBAAoB,eAAe,CAAA;AAuChD;;;;;GAKG;AACH,eAAO,MAAM,mBAAmB,GAAI,QAAQ;IAC1C,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAA;CAC9B,KAAG,MAAM,GAAG,SAQZ,CAAA;AAED,0EAA0E;AAC1E,eAAO,MAAM,gBAAgB,GAAI,QAAQ;IACvC,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAA;CAC9B,KAAG,iBAkBH,CAAA;AAED,iFAAiF;AACjF,eAAO,MAAM,cAAc,GAAI,MAAM,MAAM,KAAG,MAIhC,CAAA;AAEd,4DAA4D;AAC5D,eAAO,MAAM,SAAS,GAAI,MAAM,MAAM,KAAG,MACR,CAAA;AAEjC;;;;;GAKG;AACH,eAAO,MAAM,eAAe,GAAI,OAAO,iBAAiB,KAAG,MACA,CAAA;AAE3D;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,eAAO,MAAM,iBAAiB,GAC5B,OAAO,iBAAiB,KACvB,aAAa,CAAC,MAAM,CAmDtB,CAAA;AAED,uEAAuE;AACvE,eAAO,MAAM,UAAU,GACrB,KAAK,SAAS,CAAC,SAAS,EACxB,OAAO,iBAAiB,KACvB,aAAa,CAAC,SAAS,CAAC,SAAS,CAAC,OAAO,CAAC,CACuB,CAAA"}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Every runtime statement text the Postgres change feed issues, compiled once per
|
|
3
|
+
* name set, and the conventions their parameters follow.
|
|
4
|
+
*
|
|
5
|
+
* One module, so what the feed sends to the database is auditable in one file.
|
|
6
|
+
* The Effect wiring that issues them lives in `feed.ts`, `syncStore.ts`, `lease.ts`
|
|
7
|
+
* and `withFeed.ts`; none of those writes SQL of its own.
|
|
8
|
+
*
|
|
9
|
+
* ## Parameter conventions
|
|
10
|
+
*
|
|
11
|
+
* - Every 64-bit value (`seq`, `generation`, `event_position`) is bound as a
|
|
12
|
+
* decimal STRING and cast (`$n::bigint`), and read back as text and turned into
|
|
13
|
+
* a `bigint`, so no value ever passes through a JS `number` and no driver's
|
|
14
|
+
* integer handling can round one.
|
|
15
|
+
* - A batch of rows is bound as ONE `jsonb` parameter and unpacked server-side
|
|
16
|
+
* (`jsonb_to_recordset`, `jsonb_array_elements_text`), so a batch is one
|
|
17
|
+
* statement whatever its size and no array-binding support is asked of the
|
|
18
|
+
* driver.
|
|
19
|
+
* - A timestamp is bound as epoch MILLISECONDS from Effect's `Clock`, and read back
|
|
20
|
+
* the same way. Lease expiry is judged against the worker's Clock rather than the
|
|
21
|
+
* database's `now()`, so a `TestClock` governs it in tests as it governs every
|
|
22
|
+
* other time in this repository; the cost is that workers on several hosts must
|
|
23
|
+
* keep their clocks within a small fraction of the lease length of each other.
|
|
24
|
+
* - A document is read back as `doc::text` and parsed here, so a consumer's JSON
|
|
25
|
+
* parsing or name transforms cannot reshape it on the way.
|
|
26
|
+
* - A column read back as text is ALIASED to its own name, so every `ORDER BY`
|
|
27
|
+
* qualifies the column with its table alias: an unqualified name in `ORDER BY`
|
|
28
|
+
* resolves to the output column first, and `seq` would then sort as text, with
|
|
29
|
+
* `10` before `4`.
|
|
30
|
+
*
|
|
31
|
+
* ## The feed lock
|
|
32
|
+
*
|
|
33
|
+
* The generations row of a feed doubles as its WRITE LOCK. A feed's `seq` is
|
|
34
|
+
* ascending in commit order only while one transaction at a time takes `seq`
|
|
35
|
+
* values for that feed, and the subscribe machine silently loses a row whose `seq`
|
|
36
|
+
* commits below one already read (ADR-0009). The materialisation is one writer, but
|
|
37
|
+
* two other things take `seq` values for a feed outside its commit: `retryParked`
|
|
38
|
+
* re-stamps a row, and a reset empties the feed. So the writer takes the row
|
|
39
|
+
* `FOR SHARE` before it takes any `seq`, `retryParked` takes it `FOR UPDATE`, and
|
|
40
|
+
* the reset UPDATEs it; each waits for the others to commit, and `seq` order is
|
|
41
|
+
* commit order for all three.
|
|
42
|
+
*/
|
|
43
|
+
import { type ResolvedFeedNames } from './ddl.js';
|
|
44
|
+
/** Every statement text, compiled for one name set. */
|
|
45
|
+
export interface FeedStatements {
|
|
46
|
+
/** `$1` feed. Creates the generations row at generation 0 if absent. */
|
|
47
|
+
readonly ensureGeneration: string;
|
|
48
|
+
/** `$1` feed. The writer's share of the feed lock. */
|
|
49
|
+
readonly lockFeedShared: string;
|
|
50
|
+
/** `$1` feed, `$2` event position, `$3` jsonb `[{ key, doc }]`. */
|
|
51
|
+
readonly upsertRows: string;
|
|
52
|
+
/** `$1` feed, `$2` event position, `$3` jsonb `[key]`. */
|
|
53
|
+
readonly tombstoneRows: string;
|
|
54
|
+
/** `$1` feed. */
|
|
55
|
+
readonly selectGeneration: string;
|
|
56
|
+
/** `$1` feed, `$2` after seq, `$3` limit. */
|
|
57
|
+
readonly changesAfter: string;
|
|
58
|
+
/** `$1` feed, `$2` sink. */
|
|
59
|
+
readonly selectCursor: string;
|
|
60
|
+
/**
|
|
61
|
+
* `$1` feed, `$2` sink, `$3` holder, `$4` next generation, `$5` next seq,
|
|
62
|
+
* `$6` expected generation (NULL for none), `$7` expected seq (NULL for none),
|
|
63
|
+
* `$8` now ms, recorded as the sink's last advance.
|
|
64
|
+
*/
|
|
65
|
+
readonly casCursor: string;
|
|
66
|
+
/** `$1` feed, `$2` sink. */
|
|
67
|
+
readonly clearParked: string;
|
|
68
|
+
/** `$1` feed, `$2` sink, `$3` jsonb `[key]`. */
|
|
69
|
+
readonly unparkKeys: string;
|
|
70
|
+
/** `$1` feed, `$2` sink, `$3` jsonb `[{ key, seq, reason, parked_at_ms }]`. */
|
|
71
|
+
readonly parkKeys: string;
|
|
72
|
+
/** `$1` feed, `$2` sink. */
|
|
73
|
+
readonly selectParked: string;
|
|
74
|
+
/** `$1` feed. The feed lock, exclusively, for `retryParked`. */
|
|
75
|
+
readonly lockFeedExclusive: string;
|
|
76
|
+
/** `$1` feed, `$2` sink, `$3` key. */
|
|
77
|
+
readonly unparkOne: string;
|
|
78
|
+
/** `$1` feed, `$2` key. */
|
|
79
|
+
readonly restampRow: string;
|
|
80
|
+
/** `$1` feed. */
|
|
81
|
+
readonly collectTombstones: string;
|
|
82
|
+
/** `$1` lock name. The advisory lock, held until the transaction ends. */
|
|
83
|
+
readonly tryAdvisoryXactLock: string;
|
|
84
|
+
/** `$1` feed, `$2` sink, `$3` now ms. Deletes the cursor row unless a live lease row holds it. */
|
|
85
|
+
readonly deleteCursorUnlessLeased: string;
|
|
86
|
+
/** `$1` feed, `$2` sink. Whether a cursor row is still there. */
|
|
87
|
+
readonly cursorExists: string;
|
|
88
|
+
/** `$1` feed, `$2` sink. Every park of the sink, one row back per park. */
|
|
89
|
+
readonly deleteSinkParked: string;
|
|
90
|
+
/** `$1` feed. */
|
|
91
|
+
readonly selectHead: string;
|
|
92
|
+
/** `$1` feed. */
|
|
93
|
+
readonly describeSinks: string;
|
|
94
|
+
/** `$1` feed. */
|
|
95
|
+
readonly describeParked: string;
|
|
96
|
+
/**
|
|
97
|
+
* `$1` feed, `$2` sink, `$3` holder, `$4` expiry ms, `$5` now ms, `$6` whether
|
|
98
|
+
* the caller holds the sink's advisory lock for its transaction.
|
|
99
|
+
*/
|
|
100
|
+
readonly takeLeaseRow: string;
|
|
101
|
+
/** `$1` feed, `$2` sink, `$3` holder, `$4` expiry ms. */
|
|
102
|
+
readonly renewLeaseRow: string;
|
|
103
|
+
/** `$1` feed, `$2` sink, `$3` holder. */
|
|
104
|
+
readonly releaseLease: string;
|
|
105
|
+
/**
|
|
106
|
+
* `$1` feed, `$2` sink, `$3` holder, `$4` now ms, `$5` lock name. On the
|
|
107
|
+
* dedicated connection. Records an advisory holder unless a live lease row
|
|
108
|
+
* holds the row or this session no longer holds the lock; zero rows means one
|
|
109
|
+
* of the two.
|
|
110
|
+
*/
|
|
111
|
+
readonly claimAdvisoryHolder: string;
|
|
112
|
+
/** `$1` lock name. On the dedicated connection. */
|
|
113
|
+
readonly tryAdvisoryLock: string;
|
|
114
|
+
/** `$1` lock name. On the dedicated connection. */
|
|
115
|
+
readonly holdsAdvisoryLock: string;
|
|
116
|
+
/** `$1` lock name. On the dedicated connection. */
|
|
117
|
+
readonly advisoryUnlock: string;
|
|
118
|
+
/** `$1` feed, `$2` projection, `$3` partition. */
|
|
119
|
+
readonly bindFeed: string;
|
|
120
|
+
/** `$1` feed. */
|
|
121
|
+
readonly bumpGeneration: string;
|
|
122
|
+
/** `$1` feed. */
|
|
123
|
+
readonly deleteFeedRows: string;
|
|
124
|
+
}
|
|
125
|
+
/** Compile every statement for `names`. */
|
|
126
|
+
export declare const compileFeedStatements: (names: ResolvedFeedNames) => FeedStatements;
|
|
127
|
+
//# sourceMappingURL=statements.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"statements.d.ts","sourceRoot":"","sources":["../../../src/internal/statements.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyCG;AACH,OAAO,EAAkB,KAAK,iBAAiB,EAAmB,MAAM,OAAO,CAAA;AAE/E,uDAAuD;AACvD,MAAM,WAAW,cAAc;IAE7B,wEAAwE;IACxE,QAAQ,CAAC,gBAAgB,EAAE,MAAM,CAAA;IACjC,sDAAsD;IACtD,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAA;IAC/B,mEAAmE;IACnE,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAA;IAC3B,0DAA0D;IAC1D,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAA;IAE9B,iBAAiB;IACjB,QAAQ,CAAC,gBAAgB,EAAE,MAAM,CAAA;IACjC,6CAA6C;IAC7C,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAA;IAE7B,4BAA4B;IAC5B,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAA;IAC7B;;;;OAIG;IACH,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;IAC1B,4BAA4B;IAC5B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAA;IAC5B,gDAAgD;IAChD,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAA;IAC3B,+EAA+E;IAC/E,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;IACzB,4BAA4B;IAC5B,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAA;IAC7B,gEAAgE;IAChE,QAAQ,CAAC,iBAAiB,EAAE,MAAM,CAAA;IAClC,sCAAsC;IACtC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;IAC1B,2BAA2B;IAC3B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAA;IAC3B,iBAAiB;IACjB,QAAQ,CAAC,iBAAiB,EAAE,MAAM,CAAA;IAElC,0EAA0E;IAC1E,QAAQ,CAAC,mBAAmB,EAAE,MAAM,CAAA;IACpC,kGAAkG;IAClG,QAAQ,CAAC,wBAAwB,EAAE,MAAM,CAAA;IACzC,iEAAiE;IACjE,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAA;IAC7B,2EAA2E;IAC3E,QAAQ,CAAC,gBAAgB,EAAE,MAAM,CAAA;IACjC,iBAAiB;IACjB,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAA;IAC3B,iBAAiB;IACjB,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAA;IAC9B,iBAAiB;IACjB,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAA;IAE/B;;;OAGG;IACH,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAA;IAC7B,yDAAyD;IACzD,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAA;IAC9B,yCAAyC;IACzC,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAA;IAC7B;;;;;OAKG;IACH,QAAQ,CAAC,mBAAmB,EAAE,MAAM,CAAA;IACpC,mDAAmD;IACnD,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAA;IAChC,mDAAmD;IACnD,QAAQ,CAAC,iBAAiB,EAAE,MAAM,CAAA;IAClC,mDAAmD;IACnD,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAA;IAE/B,kDAAkD;IAClD,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;IACzB,iBAAiB;IACjB,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAA;IAC/B,iBAAiB;IACjB,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAA;CAChC;AAED,2CAA2C;AAC3C,eAAO,MAAM,qBAAqB,GAChC,OAAO,iBAAiB,KACvB,cA2PF,CAAA"}
|