@kairos-es/feed-postgres 0.0.0-stage → 1.0.0-alpha.6
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,342 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.pooledConnectionFault = exports.makeAcquireLease = exports.leaseRow = exports.advisoryLockName = exports.advisoryLock = exports.DEFAULT_LEASE_TTL = exports.DEFAULT_LEASE_HEARTBEAT = void 0;
|
|
7
|
+
var _feed = require("@kairos-es/feed");
|
|
8
|
+
var _effect = require("effect");
|
|
9
|
+
var _context = require("./internal/context.js");
|
|
10
|
+
/**
|
|
11
|
+
* The sync worker's lease on Postgres, in the two kinds `FeedSyncStore`'s doc
|
|
12
|
+
* describes, chosen in config to suit the infrastructure in front of the
|
|
13
|
+
* database.
|
|
14
|
+
*
|
|
15
|
+
* ## Why there are two
|
|
16
|
+
*
|
|
17
|
+
* A worker lease is what keeps a feed's sends to a sink in order: a sink has no
|
|
18
|
+
* transaction, so a rival worker's stale send lands before any compare-and-set can
|
|
19
|
+
* catch it (ADR-0019 §5). The two kinds trade the same two things against each
|
|
20
|
+
* other — what the connection must be, and how long a lapsed worker can go on
|
|
21
|
+
* sending — and neither wins on both.
|
|
22
|
+
*
|
|
23
|
+
* - **`leaseRow`, the default.** The holder and an expiry live on the sink's
|
|
24
|
+
* cursor row, taken with a guarded upsert (only when free, expired, or held by
|
|
25
|
+
* an advisory worker whose session is gone; see below) and renewed by a
|
|
26
|
+
* heartbeat forked into the lease's scope. Every statement is an ordinary short
|
|
27
|
+
* one, and the take one short transaction, so it works through a TRANSACTION
|
|
28
|
+
* POOLER, which drops session state and so rules out a session lock, and it
|
|
29
|
+
* needs no reserved connection. Its cost is a window. `ensureHeld` fails a
|
|
30
|
+
* MARGIN before the stored expiry: once the last successful renewal is
|
|
31
|
+
* `ttl - margin` old by the worker's own `Clock`, that renewal's expiry, which is
|
|
32
|
+
* when a rival may take the row, is at most `margin` away. So a call to the sink
|
|
33
|
+
* that `ensureHeld` let through, and that lands within `margin` of that check,
|
|
34
|
+
* pauses included, lands before any rival can hold the lease, however its
|
|
35
|
+
* renewals have been failing meanwhile, as long as the two workers' clocks
|
|
36
|
+
* agree. A call that outlasts the margin, whether it is slow or its worker
|
|
37
|
+
* pauses (a long GC pause, a network stall), can land after a rival took over:
|
|
38
|
+
* ONE stale call, since the next `ensureHeld` fails; `FeedSyncStore.ts` owns what
|
|
39
|
+
* that call costs. The margin is `ttl / 3` unless configured, and must leave the
|
|
40
|
+
* heartbeat room under the lease (`heartbeat + margin < ttl`), or `ensureHeld`
|
|
41
|
+
* would fail between two renewals that both landed at once. Each renewal is
|
|
42
|
+
* bounded by the heartbeat and the next starts on the next tick, and the age
|
|
43
|
+
* `ensureHeld` judges runs from when a renewal STARTED. So a renewal that lands
|
|
44
|
+
* is at most `heartbeat` late, and the worst age `ensureHeld` meets is
|
|
45
|
+
* `heartbeat` plus that latency with every renewal landing, and `2 * heartbeat`
|
|
46
|
+
* plus it with one lost. A worker therefore goes on sending, without a single
|
|
47
|
+
* failed `ensureHeld`, through renewals that are slow when
|
|
48
|
+
* `2 * heartbeat + margin <= ttl`, and through one that fails or hangs when
|
|
49
|
+
* `3 * heartbeat + margin <= ttl`. The defaults satisfy the second; the
|
|
50
|
+
* construction check enforces only the first's zero-latency form, so a tighter
|
|
51
|
+
* config is accepted and trades those guarantees for a shorter `ttl`.
|
|
52
|
+
* - **`advisoryLock`.** A session `pg_try_advisory_lock` on a connection RESERVED
|
|
53
|
+
* from a client the consumer dedicates to it, keyed by a hash of the prefix, the
|
|
54
|
+
* feed and the sink. The lock goes when the session goes, and `ensureHeld` asks
|
|
55
|
+
* that same session, before every call that changes the sink, whether it still
|
|
56
|
+
* holds the lock; any failure to answer is a lost lease. So a process pause
|
|
57
|
+
* opens no stale window, since the lock survives it. What a check cannot see is
|
|
58
|
+
* the session dying AFTER it: the call it let through is already under way and
|
|
59
|
+
* runs to its end, so it can land as late as its whole duration after the session
|
|
60
|
+
* died, and after a rival has taken the lock and sent. That is still ONE stale
|
|
61
|
+
* call, since the next `ensureHeld` asks a session that is gone, but a sink call
|
|
62
|
+
* can be long — a resync's `publish` most of all. The price is a connection per
|
|
63
|
+
* running worker, and a connection that is genuinely a SESSION: a transaction
|
|
64
|
+
* pooler would hand the lock's session to somebody else between statements. A
|
|
65
|
+
* client this package can recognise as one is refused at construction; see
|
|
66
|
+
* {@link pooledConnectionFault} for what it can and cannot recognise.
|
|
67
|
+
*
|
|
68
|
+
* ## Fencing is the same for both
|
|
69
|
+
*
|
|
70
|
+
* Both kinds record the holder on the cursor row, and the cursor compare-and-set
|
|
71
|
+
* requires it, so a worker whose lease has gone cannot move the cursor whichever
|
|
72
|
+
* kind it held. Mixing the two kinds on one feed and sink fails safe, because
|
|
73
|
+
* neither takes the row from a LIVE holder of the other. A lease-row worker takes
|
|
74
|
+
* over an advisory holder (one with no expiry to lapse) only while it holds that
|
|
75
|
+
* sink's advisory lock for its own transaction, which a live advisory session
|
|
76
|
+
* refuses; so a crashed advisory worker, whose scope never closed to clear the row,
|
|
77
|
+
* does not leave the sink unleasable by every lease-row worker after it. An
|
|
78
|
+
* advisory worker that wins the lock but finds a lease row unexpired on the row lets
|
|
79
|
+
* the lock go and fails `LeaseHeld`. Whichever took the row first keeps it while it
|
|
80
|
+
* lives. The advisory worker likewise takes over an ADVISORY holder recorded on the
|
|
81
|
+
* row, because holding the lock proves that session is gone; it records itself
|
|
82
|
+
* from the session that holds the lock, and only while it still does, so a session
|
|
83
|
+
* that died after its try-lock cannot overwrite the successor that took the lock
|
|
84
|
+
* from it.
|
|
85
|
+
*/
|
|
86
|
+
|
|
87
|
+
/** The default lease length. */
|
|
88
|
+
const DEFAULT_LEASE_TTL = exports.DEFAULT_LEASE_TTL = /*#__PURE__*/_effect.Duration.seconds(30);
|
|
89
|
+
/**
|
|
90
|
+
* The default renewal interval: a fifth of the lease. With the default margin, a
|
|
91
|
+
* third, `3 * heartbeat + margin` is under the `ttl`, so a worker goes on sending,
|
|
92
|
+
* with no failed `ensureHeld`, through one renewal that fails or hangs. However
|
|
93
|
+
* many fail, it stops sending a margin before its lease could lapse. The module
|
|
94
|
+
* doc has the arithmetic.
|
|
95
|
+
*/
|
|
96
|
+
const DEFAULT_LEASE_HEARTBEAT = exports.DEFAULT_LEASE_HEARTBEAT = /*#__PURE__*/_effect.Duration.seconds(6);
|
|
97
|
+
/** A lease row, the default; see the module doc. */
|
|
98
|
+
const leaseRow = (options = {}) => ({
|
|
99
|
+
_tag: 'LeaseRow',
|
|
100
|
+
ttl: options.ttl ?? DEFAULT_LEASE_TTL,
|
|
101
|
+
heartbeat: options.heartbeat ?? DEFAULT_LEASE_HEARTBEAT,
|
|
102
|
+
...(options.margin === undefined ? {} : {
|
|
103
|
+
margin: options.margin
|
|
104
|
+
})
|
|
105
|
+
});
|
|
106
|
+
/** A session advisory lock on a dedicated connection; see the module doc. */
|
|
107
|
+
exports.leaseRow = leaseRow;
|
|
108
|
+
const advisoryLock = options => ({
|
|
109
|
+
_tag: 'AdvisoryLock',
|
|
110
|
+
connection: options.connection
|
|
111
|
+
});
|
|
112
|
+
/**
|
|
113
|
+
* Why `client` cannot carry a session lock, when this package can tell, or
|
|
114
|
+
* `undefined`.
|
|
115
|
+
*
|
|
116
|
+
* What it can tell is limited to what a client says about where it connects,
|
|
117
|
+
* which `@effect/sql-pg`'s client exposes as `config` and a generic `SqlClient`
|
|
118
|
+
* does not. It recognises the two pooler conventions that show in an address: a
|
|
119
|
+
* Neon pooled endpoint (a host containing `-pooler`), and PgBouncer's default
|
|
120
|
+
* port, 6432, or a `pgbouncer=true` URL parameter. A pooler on any other address,
|
|
121
|
+
* or behind a client that exposes nothing, passes; that is a wiring the consumer
|
|
122
|
+
* owns. The URL is read only to judge it and is never repeated in the message.
|
|
123
|
+
*/
|
|
124
|
+
exports.advisoryLock = advisoryLock;
|
|
125
|
+
const pooledConnectionFault = client => {
|
|
126
|
+
const config = client.config;
|
|
127
|
+
if (config === null || typeof config !== 'object') {
|
|
128
|
+
return undefined;
|
|
129
|
+
}
|
|
130
|
+
const {
|
|
131
|
+
host,
|
|
132
|
+
port,
|
|
133
|
+
url
|
|
134
|
+
} = config;
|
|
135
|
+
const hosts = typeof host === 'string' ? [host] : [];
|
|
136
|
+
const ports = typeof port === 'number' ? [port] : [];
|
|
137
|
+
let pgbouncer = false;
|
|
138
|
+
const rawUrl = _effect.Redacted.isRedacted(url) ? _effect.Redacted.value(url) : typeof url === 'string' ? url : undefined;
|
|
139
|
+
if (typeof rawUrl === 'string') {
|
|
140
|
+
try {
|
|
141
|
+
const parsed = new URL(rawUrl);
|
|
142
|
+
hosts.push(parsed.hostname);
|
|
143
|
+
if (parsed.port !== '') {
|
|
144
|
+
ports.push(Number(parsed.port));
|
|
145
|
+
}
|
|
146
|
+
pgbouncer = parsed.searchParams.get('pgbouncer') === 'true';
|
|
147
|
+
} catch {
|
|
148
|
+
// Not a URL this check can read; the driver will say so itself.
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
const signs = [...(hosts.some(h => h.includes('-pooler')) ? ['a "-pooler" host, which is a pooled endpoint'] : []), ...(ports.includes(6432) ? ["port 6432, PgBouncer's default"] : []), ...(pgbouncer ? ['a pgbouncer=true parameter'] : [])];
|
|
152
|
+
return signs.length === 0 ? undefined : `the advisoryLock connection looks like a transaction pooler (${signs.join('; ')}), which hands a session to other clients between statements and so cannot hold a session lock; give it a direct connection, or use leaseRow`;
|
|
153
|
+
};
|
|
154
|
+
/** A positive, finite duration, or a construction-time throw naming it. */
|
|
155
|
+
exports.pooledConnectionFault = pooledConnectionFault;
|
|
156
|
+
const positive = (label, input) => {
|
|
157
|
+
const decoded = _effect.Duration.fromInputUnsafe(input);
|
|
158
|
+
if (!_effect.Duration.isFinite(decoded) || _effect.Duration.toMillis(decoded) <= 0) {
|
|
159
|
+
throw new Error(`feed-postgres: ${label} must be a positive, finite duration`);
|
|
160
|
+
}
|
|
161
|
+
return decoded;
|
|
162
|
+
};
|
|
163
|
+
/** A fresh holder id, from `Random`, never the host. */
|
|
164
|
+
const newHolder = kind => _effect.Effect.gen(function* () {
|
|
165
|
+
const high = yield* _effect.Random.nextIntBetween(0, 2 ** 31);
|
|
166
|
+
const low = yield* _effect.Random.nextIntBetween(0, 2 ** 31);
|
|
167
|
+
return _feed.LeaseHolder.make(`pg-${kind}:${high.toString(16)}${low.toString(16).padStart(8, '0')}`);
|
|
168
|
+
});
|
|
169
|
+
/**
|
|
170
|
+
* The advisory lock's name for one feed and sink, hashed server-side into its
|
|
171
|
+
* key. Package-internal, and the one home of the name: `removeSink` tries the
|
|
172
|
+
* same lock to tell whether an advisory worker is live, so the two must agree.
|
|
173
|
+
*
|
|
174
|
+
* @internal
|
|
175
|
+
*/
|
|
176
|
+
const advisoryLockName = (context, id) => JSON.stringify(['kairos-es/feed', context.names.schema, context.names.tablePrefix, id.feed, id.sink]);
|
|
177
|
+
/**
|
|
178
|
+
* The lease-row kind; see the module doc. Throws on a heartbeat not under its ttl,
|
|
179
|
+
* or a margin that leaves the heartbeat no room under it.
|
|
180
|
+
*/
|
|
181
|
+
exports.advisoryLockName = advisoryLockName;
|
|
182
|
+
const leaseRowAcquirer = (config, sql, context) => {
|
|
183
|
+
const {
|
|
184
|
+
statements
|
|
185
|
+
} = context;
|
|
186
|
+
const ttl = _effect.Duration.toMillis(positive('lease ttl', config.ttl));
|
|
187
|
+
const heartbeat = positive('lease heartbeat', config.heartbeat);
|
|
188
|
+
if (_effect.Duration.toMillis(heartbeat) >= ttl) {
|
|
189
|
+
throw new Error('feed-postgres: the lease heartbeat must be shorter than its ttl, or the lease lapses between renewals');
|
|
190
|
+
}
|
|
191
|
+
const margin = config.margin === undefined ? ttl / 3 : _effect.Duration.toMillis(positive('lease margin', config.margin));
|
|
192
|
+
if (_effect.Duration.toMillis(heartbeat) + margin >= ttl) {
|
|
193
|
+
throw new Error('feed-postgres: the lease heartbeat plus its margin must be shorter than its ttl, or ensureHeld fails between two renewals that both landed');
|
|
194
|
+
}
|
|
195
|
+
// `ensureHeld` fails once the last renewal is this old: its stored expiry is
|
|
196
|
+
// then at most `margin` away.
|
|
197
|
+
const usable = ttl - margin;
|
|
198
|
+
return id => _effect.Effect.gen(function* () {
|
|
199
|
+
const holder = yield* newHolder('row');
|
|
200
|
+
const now = yield* _effect.Clock.currentTimeMillis;
|
|
201
|
+
const taken = yield* sql.withTransaction(_effect.Effect.gen(function* () {
|
|
202
|
+
// Held to this transaction's end, so an ADVISORY holder on the row can
|
|
203
|
+
// be taken over only while no advisory session holds its lock, and no
|
|
204
|
+
// advisory worker can take the lock before this commits.
|
|
205
|
+
const lock = yield* sql.unsafe(statements.tryAdvisoryXactLock, [advisoryLockName(context, id)]);
|
|
206
|
+
return yield* sql.unsafe(statements.takeLeaseRow, [id.feed, id.sink, holder, String(now + ttl), String(now), lock[0]?.taken === true]);
|
|
207
|
+
})).pipe(_effect.Effect.mapError((0, _context.writeFailed)('taking the feed lease')));
|
|
208
|
+
if (taken.length === 0) {
|
|
209
|
+
return yield* new _feed.LeaseHeld(id);
|
|
210
|
+
}
|
|
211
|
+
const state = yield* _effect.Ref.make({
|
|
212
|
+
renewedAt: now,
|
|
213
|
+
lost: false
|
|
214
|
+
});
|
|
215
|
+
yield* _effect.Effect.addFinalizer(() => _effect.Effect.ignore(sql.unsafe(statements.releaseLease, [id.feed, id.sink, holder])));
|
|
216
|
+
const renew = _effect.Effect.gen(function* () {
|
|
217
|
+
const at = yield* _effect.Clock.currentTimeMillis;
|
|
218
|
+
const renewed = yield* sql.unsafe(statements.renewLeaseRow, [id.feed, id.sink, holder, String(at + ttl)]);
|
|
219
|
+
if (renewed.length === 0) {
|
|
220
|
+
yield* _effect.Ref.update(state, s => ({
|
|
221
|
+
...s,
|
|
222
|
+
lost: true
|
|
223
|
+
}));
|
|
224
|
+
} else {
|
|
225
|
+
yield* _effect.Ref.update(state, s => ({
|
|
226
|
+
...s,
|
|
227
|
+
renewedAt: at
|
|
228
|
+
}));
|
|
229
|
+
}
|
|
230
|
+
}).pipe(
|
|
231
|
+
// Bounded by the heartbeat, so a renewal that hangs is abandoned in time
|
|
232
|
+
// for the next tick rather than holding every later one behind it. An
|
|
233
|
+
// abandoned renewal whose cancel comes too late can still land: it touches
|
|
234
|
+
// only this holder's own row, never moves its expiry back
|
|
235
|
+
// (`renewLeaseRow`), and leaves `renewedAt` at the last renewal this fibre
|
|
236
|
+
// saw land, so `ensureHeld` errs early, never late.
|
|
237
|
+
_effect.Effect.timeoutOption(heartbeat), _effect.Effect.flatMap(done => _effect.Option.isSome(done) ? _effect.Effect.void : _effect.Effect.logWarning('feed lease renewal timed out', {
|
|
238
|
+
feed: id.feed,
|
|
239
|
+
sink: id.sink
|
|
240
|
+
})),
|
|
241
|
+
// A renewal that could not be made is not yet a lost lease: the age
|
|
242
|
+
// check in `ensureHeld` decides that, a margin before the lease lapses.
|
|
243
|
+
_effect.Effect.catchTag('SqlError', cause => _effect.Effect.logWarning('feed lease renewal failed', {
|
|
244
|
+
feed: id.feed,
|
|
245
|
+
sink: id.sink,
|
|
246
|
+
reason: cause.message
|
|
247
|
+
})));
|
|
248
|
+
// On a fixed cadence from the take: a renewal never outlasts the interval,
|
|
249
|
+
// so each one starts on its own tick whatever became of the one before. A
|
|
250
|
+
// worker that falls behind (a pause) renews once at once, and does not
|
|
251
|
+
// replay the ticks it missed.
|
|
252
|
+
const interval = _effect.Duration.toMillis(heartbeat);
|
|
253
|
+
yield* _effect.Effect.forkScoped(_effect.Effect.gen(function* () {
|
|
254
|
+
let next = now + interval;
|
|
255
|
+
while (true) {
|
|
256
|
+
const at = yield* _effect.Clock.currentTimeMillis;
|
|
257
|
+
if (next > at) {
|
|
258
|
+
yield* _effect.Effect.sleep(_effect.Duration.millis(next - at));
|
|
259
|
+
}
|
|
260
|
+
yield* renew;
|
|
261
|
+
next = Math.max(next + interval, yield* _effect.Clock.currentTimeMillis);
|
|
262
|
+
}
|
|
263
|
+
}));
|
|
264
|
+
const lease = {
|
|
265
|
+
holder,
|
|
266
|
+
ensureHeld: _effect.Effect.gen(function* () {
|
|
267
|
+
const s = yield* _effect.Ref.get(state);
|
|
268
|
+
const at = yield* _effect.Clock.currentTimeMillis;
|
|
269
|
+
if (s.lost || at - s.renewedAt >= usable) {
|
|
270
|
+
return yield* new _feed.LeaseLost(id);
|
|
271
|
+
}
|
|
272
|
+
})
|
|
273
|
+
};
|
|
274
|
+
return lease;
|
|
275
|
+
});
|
|
276
|
+
};
|
|
277
|
+
/** The advisory-lock kind; see the module doc. Throws on a recognisably pooled connection. */
|
|
278
|
+
const advisoryLockAcquirer = (config, sql, context) => {
|
|
279
|
+
const fault = pooledConnectionFault(config.connection);
|
|
280
|
+
if (fault !== undefined) {
|
|
281
|
+
throw new Error(`feed-postgres: ${fault}`);
|
|
282
|
+
}
|
|
283
|
+
const {
|
|
284
|
+
connection
|
|
285
|
+
} = config;
|
|
286
|
+
const {
|
|
287
|
+
statements
|
|
288
|
+
} = context;
|
|
289
|
+
return id => _effect.Effect.gen(function* () {
|
|
290
|
+
const lockName = advisoryLockName(context, id);
|
|
291
|
+
const session = yield* connection.reserve.pipe(_effect.Effect.mapError((0, _context.unreadable)('reserving the feed lease connection')));
|
|
292
|
+
const taken = yield* session.execute(statements.tryAdvisoryLock, [lockName], undefined).pipe(_effect.Effect.mapError((0, _context.unreadable)('taking the feed advisory lock')));
|
|
293
|
+
if (taken[0]?.taken !== true) {
|
|
294
|
+
return yield* new _feed.LeaseHeld(id);
|
|
295
|
+
}
|
|
296
|
+
yield* _effect.Effect.addFinalizer(() => _effect.Effect.ignore(session.execute(statements.advisoryUnlock, [lockName], undefined)));
|
|
297
|
+
const holder = yield* newHolder('advisory');
|
|
298
|
+
const now = yield* _effect.Clock.currentTimeMillis;
|
|
299
|
+
// On the lock's own session, and conditional on it still holding the lock:
|
|
300
|
+
// a session that died since the try-lock cannot run it, and one that lost
|
|
301
|
+
// the lock lands nothing, so neither overwrites a successor's claim.
|
|
302
|
+
const claimed = yield* session.execute(statements.claimAdvisoryHolder, [id.feed, id.sink, holder, String(now), lockName], undefined).pipe(_effect.Effect.mapError((0, _context.writeFailed)('recording the feed lease holder')));
|
|
303
|
+
if (claimed.length === 0) {
|
|
304
|
+
// A live lease row holds the sink, or this session no longer holds the
|
|
305
|
+
// lock, so this worker must not send. The lock
|
|
306
|
+
// guards nothing now, so it is let go at once rather than when the
|
|
307
|
+
// caller's scope closes, and another advisory worker may try the moment
|
|
308
|
+
// that lease row lapses. The finalizer's second unlock then finds nothing
|
|
309
|
+
// held, which is harmless.
|
|
310
|
+
yield* _effect.Effect.ignore(session.execute(statements.advisoryUnlock, [lockName], undefined));
|
|
311
|
+
return yield* new _feed.LeaseHeld(id);
|
|
312
|
+
}
|
|
313
|
+
yield* _effect.Effect.addFinalizer(() => _effect.Effect.ignore(sql.unsafe(statements.releaseLease, [id.feed, id.sink, holder])));
|
|
314
|
+
const held = session.execute(statements.holdsAdvisoryLock, [lockName], undefined).pipe(_effect.Effect.map(rows => rows[0]?.held === true), _effect.Effect.catchTag('SqlError', () => _effect.Effect.succeed(false)));
|
|
315
|
+
const lease = {
|
|
316
|
+
holder,
|
|
317
|
+
// Asked of the session that holds the lock, so a dead session cannot
|
|
318
|
+
// answer, and an unanswered question is a lost lease.
|
|
319
|
+
ensureHeld: _effect.Effect.flatMap(held, yes => yes ? _effect.Effect.void : _effect.Effect.fail(new _feed.LeaseLost(id)))
|
|
320
|
+
};
|
|
321
|
+
return lease;
|
|
322
|
+
});
|
|
323
|
+
};
|
|
324
|
+
/**
|
|
325
|
+
* Build the acquirer for `config`, checking it first: a `heartbeat` that is not
|
|
326
|
+
* shorter than its `ttl`, a `margin` that leaves the heartbeat no room under the
|
|
327
|
+
* `ttl`, or an `advisoryLock` connection recognisably pooled,
|
|
328
|
+
* throws here, which the store's factory turns into a defect at construction.
|
|
329
|
+
* The one place the lease kind is dispatched on.
|
|
330
|
+
*
|
|
331
|
+
* @internal
|
|
332
|
+
*/
|
|
333
|
+
const makeAcquireLease = (config, sql, context) => {
|
|
334
|
+
switch (config._tag) {
|
|
335
|
+
case 'LeaseRow':
|
|
336
|
+
return leaseRowAcquirer(config, sql, context);
|
|
337
|
+
case 'AdvisoryLock':
|
|
338
|
+
return advisoryLockAcquirer(config, sql, context);
|
|
339
|
+
}
|
|
340
|
+
};
|
|
341
|
+
exports.makeAcquireLease = makeAcquireLease;
|
|
342
|
+
//# sourceMappingURL=lease.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lease.js","names":["_feed","require","_effect","_context","DEFAULT_LEASE_TTL","exports","Duration","seconds","DEFAULT_LEASE_HEARTBEAT","leaseRow","options","_tag","ttl","heartbeat","margin","undefined","advisoryLock","connection","pooledConnectionFault","client","config","host","port","url","hosts","ports","pgbouncer","rawUrl","Redacted","isRedacted","value","parsed","URL","push","hostname","Number","searchParams","get","signs","some","h","includes","length","join","positive","label","input","decoded","fromInputUnsafe","isFinite","toMillis","Error","newHolder","kind","Effect","gen","high","Random","nextIntBetween","low","LeaseHolder","make","toString","padStart","advisoryLockName","context","id","JSON","stringify","names","schema","tablePrefix","feed","sink","leaseRowAcquirer","sql","statements","usable","holder","now","Clock","currentTimeMillis","taken","withTransaction","lock","unsafe","tryAdvisoryXactLock","takeLeaseRow","String","pipe","mapError","writeFailed","LeaseHeld","state","Ref","renewedAt","lost","addFinalizer","ignore","releaseLease","renew","at","renewed","renewLeaseRow","update","s","timeoutOption","flatMap","done","Option","isSome","void","logWarning","catchTag","cause","reason","message","interval","forkScoped","next","sleep","millis","Math","max","lease","ensureHeld","LeaseLost","advisoryLockAcquirer","fault","lockName","session","reserve","unreadable","execute","tryAdvisoryLock","advisoryUnlock","claimed","claimAdvisoryHolder","held","holdsAdvisoryLock","map","rows","succeed","yes","fail","makeAcquireLease"],"sources":["../../src/lease.ts"],"sourcesContent":[null],"mappings":"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","ignoreList":[]}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.makePostgresFeedSyncStore = void 0;
|
|
7
|
+
var _feed = require("@kairos-es/feed");
|
|
8
|
+
var _effect = require("effect");
|
|
9
|
+
var _sql = require("effect/sql");
|
|
10
|
+
var _context = require("./internal/context.js");
|
|
11
|
+
var _lease = require("./lease.js");
|
|
12
|
+
/**
|
|
13
|
+
* The Postgres `FeedSyncStore`: every sink's cursor, its parked list and its
|
|
14
|
+
* worker lease, kept in the source database beside the feed rows (ADR-0019 §10).
|
|
15
|
+
*
|
|
16
|
+
* `@kairos-es/feed`'s `FeedSyncStore.ts` owns the contract, and the in-memory feed
|
|
17
|
+
* is the oracle this store is held to; what is here is how each atomic step the
|
|
18
|
+
* contract names becomes one transaction or one statement.
|
|
19
|
+
*
|
|
20
|
+
* - `casCursor` is ONE transaction: the guarded, holder-fenced cursor update,
|
|
21
|
+
* stamped with its `lastAdvance` from this process's `Clock`, and the parked
|
|
22
|
+
* list moved with it. A guard that loses fails `CursorSuperseded` inside the
|
|
23
|
+
* transaction, which rolls back the parked changes with it, so a park can never
|
|
24
|
+
* outlive a refused advance.
|
|
25
|
+
* - `retryParked` is one transaction that takes the feed lock exclusively before it
|
|
26
|
+
* re-stamps the row, so its new `seq` cannot commit out of order with the
|
|
27
|
+
* materialisation's own writes (`internal/statements.ts` owns that argument).
|
|
28
|
+
* - `collectTombstones` is one statement that reads every registered sink's cursor
|
|
29
|
+
* and deletes below the lowest, or nothing while any of them has no cursor, so
|
|
30
|
+
* no cursor can move between the two.
|
|
31
|
+
* - `removeSink` is one transaction that first takes the sink's advisory lock for
|
|
32
|
+
* its own length, which an advisory worker's session lock refuses, then deletes
|
|
33
|
+
* the cursor row only when no live lease row holds it, in the same statement as
|
|
34
|
+
* that test, and the parks after it. Both tests are therefore made whichever
|
|
35
|
+
* lease kind this store was built with: a sink can be leased by either kind
|
|
36
|
+
* (`lease.ts` owns how the two coexist), and the row alone cannot say whether
|
|
37
|
+
* an advisory holder's session is alive.
|
|
38
|
+
* - `describe` is three reads, each its own snapshot, so a figure in it can be a
|
|
39
|
+
* batch older or newer than the next. It is a report for an operator, and no
|
|
40
|
+
* decision is taken on it.
|
|
41
|
+
*
|
|
42
|
+
* One store serves every feed in its prefix: unlike the in-memory feed, whose
|
|
43
|
+
* store belongs to one feed, a SQL store's state is keyed by feed name in shared
|
|
44
|
+
* tables, so a single store value is what every worker over the prefix takes.
|
|
45
|
+
*
|
|
46
|
+
* Infrastructure faults are `FeedUnreadable` on a read and `FeedWriteFailed` on a
|
|
47
|
+
* write, never defects, so the driver's supervision backs off and tries again.
|
|
48
|
+
*/
|
|
49
|
+
|
|
50
|
+
/** A timestamp column read back as epoch milliseconds, or `None` when NULL. */
|
|
51
|
+
const instantOf = ms => ms === null ? _effect.Option.none() : _effect.Option.some(_effect.DateTime.makeUnsafe(Number(ms)));
|
|
52
|
+
const cursorOf = row => row === undefined || row.generation === null || row.seq === null ? _effect.Option.none() : _effect.Option.some({
|
|
53
|
+
generation: _feed.Generation.make((0, _context.bigintOf)(row.generation)),
|
|
54
|
+
seq: _feed.FeedSeq.make((0, _context.bigintOf)(row.seq))
|
|
55
|
+
});
|
|
56
|
+
const parkedOf = row => ({
|
|
57
|
+
key: _feed.FeedKey.make(row.key),
|
|
58
|
+
seq: _feed.FeedSeq.make((0, _context.bigintOf)(row.seq)),
|
|
59
|
+
reason: row.reason,
|
|
60
|
+
parkedAt: _effect.DateTime.makeUnsafe(Number(row.parked_at_ms))
|
|
61
|
+
});
|
|
62
|
+
/** Park entries as the `jsonb` parameter the park statement takes; last per key wins. */
|
|
63
|
+
const parkJson = park => {
|
|
64
|
+
const byKey = new Map();
|
|
65
|
+
for (const record of park) {
|
|
66
|
+
byKey.delete(record.key);
|
|
67
|
+
byKey.set(record.key, record);
|
|
68
|
+
}
|
|
69
|
+
return JSON.stringify(Array.from(byKey.values(), record => ({
|
|
70
|
+
key: record.key,
|
|
71
|
+
seq: record.seq.toString(),
|
|
72
|
+
reason: record.reason,
|
|
73
|
+
parked_at_ms: String(_effect.DateTime.toEpochMillis(record.parkedAt))
|
|
74
|
+
})));
|
|
75
|
+
};
|
|
76
|
+
/**
|
|
77
|
+
* Build the sync store over the ambient `SqlClient`. Dies at construction on a
|
|
78
|
+
* malformed config, a lease whose heartbeat is not shorter than its ttl or leaves
|
|
79
|
+
* no room for its margin, or an `advisoryLock` connection recognisably pooled.
|
|
80
|
+
* `ensureFeedSchema` must have run.
|
|
81
|
+
*/
|
|
82
|
+
const makePostgresFeedSyncStore = (options = {}) => _effect.Effect.gen(function* () {
|
|
83
|
+
const sql = (yield* _sql.SqlClient.SqlClient).withoutTransforms();
|
|
84
|
+
const context = (0, _context.feedContext)(options.config ?? {});
|
|
85
|
+
const acquireLease = (0, _lease.makeAcquireLease)(options.lease ?? (0, _lease.leaseRow)(), sql, context);
|
|
86
|
+
const {
|
|
87
|
+
statements
|
|
88
|
+
} = context;
|
|
89
|
+
const readCursor = id => sql.unsafe(statements.selectCursor, [id.feed, id.sink]).pipe(_effect.Effect.mapError((0, _context.unreadable)(`reading the cursor of sink '${id.sink}'`)), _effect.Effect.map(rows => cursorOf(rows[0])));
|
|
90
|
+
const casCursor = args => sql.withTransaction(_effect.Effect.gen(function* () {
|
|
91
|
+
const expected = _effect.Option.getOrUndefined(args.expected);
|
|
92
|
+
const now = yield* _effect.DateTime.now;
|
|
93
|
+
const landed = yield* sql.unsafe(statements.casCursor, [args.feed, args.sink, args.holder, args.next.generation.toString(), args.next.seq.toString(), expected === undefined ? null : expected.generation.toString(), expected === undefined ? null : expected.seq.toString(), String(_effect.DateTime.toEpochMillis(now))]);
|
|
94
|
+
if (landed.length === 0) {
|
|
95
|
+
return yield* new _feed.CursorSuperseded({
|
|
96
|
+
feed: args.feed,
|
|
97
|
+
sink: args.sink
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
const change = args.parked;
|
|
101
|
+
if (change._tag === 'Replace') {
|
|
102
|
+
yield* sql.unsafe(statements.clearParked, [args.feed, args.sink]);
|
|
103
|
+
} else if (change.unpark.length > 0) {
|
|
104
|
+
yield* sql.unsafe(statements.unparkKeys, [args.feed, args.sink, JSON.stringify(change.unpark)]);
|
|
105
|
+
}
|
|
106
|
+
if (change.park.length > 0) {
|
|
107
|
+
yield* sql.unsafe(statements.parkKeys, [args.feed, args.sink, parkJson(change.park)]);
|
|
108
|
+
}
|
|
109
|
+
})).pipe(_effect.Effect.catchTag('SqlError', cause => _effect.Effect.fail((0, _context.writeFailed)(`moving the cursor of sink '${args.sink}'`)(cause))));
|
|
110
|
+
const readParked = id => sql.unsafe(statements.selectParked, [id.feed, id.sink]).pipe(_effect.Effect.mapError((0, _context.unreadable)(`reading the parked records of sink '${id.sink}'`)), _effect.Effect.map(rows => rows.map(parkedOf)));
|
|
111
|
+
const retryParked = args => sql.withTransaction(_effect.Effect.gen(function* () {
|
|
112
|
+
yield* sql.unsafe(statements.lockFeedExclusive, [args.feed]);
|
|
113
|
+
const unparked = yield* sql.unsafe(statements.unparkOne, [args.feed, args.sink, args.key]);
|
|
114
|
+
if (unparked.length === 0) {
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
yield* sql.unsafe(statements.restampRow, [args.feed, args.key]);
|
|
118
|
+
return true;
|
|
119
|
+
})).pipe(_effect.Effect.mapError((0, _context.writeFailed)(`retrying parked record '${args.key}' of sink '${args.sink}'`)));
|
|
120
|
+
const collectTombstones = feed => sql.unsafe(statements.collectTombstones, [feed]).pipe(_effect.Effect.mapError((0, _context.writeFailed)(`collecting the tombstones of feed '${feed}'`)), _effect.Effect.map(rows => rows.length));
|
|
121
|
+
const removeSink = id => sql.withTransaction(_effect.Effect.gen(function* () {
|
|
122
|
+
const lock = yield* sql.unsafe(statements.tryAdvisoryXactLock, [(0, _lease.advisoryLockName)(context, id)]);
|
|
123
|
+
if (lock[0]?.taken !== true) {
|
|
124
|
+
return yield* new _feed.LeaseHeld(id);
|
|
125
|
+
}
|
|
126
|
+
const now = yield* _effect.Clock.currentTimeMillis;
|
|
127
|
+
const cursor = yield* sql.unsafe(statements.deleteCursorUnlessLeased, [id.feed, id.sink, String(now)]);
|
|
128
|
+
if (cursor.length === 0) {
|
|
129
|
+
const left = yield* sql.unsafe(statements.cursorExists, [id.feed, id.sink]);
|
|
130
|
+
if (left.length > 0) {
|
|
131
|
+
return yield* new _feed.LeaseHeld(id);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
const parked = yield* sql.unsafe(statements.deleteSinkParked, [id.feed, id.sink]);
|
|
135
|
+
return cursor.length > 0 || parked.length > 0;
|
|
136
|
+
})).pipe(_effect.Effect.catchTag('SqlError', cause => _effect.Effect.fail((0, _context.writeFailed)(`removing sink '${id.sink}'`)(cause))));
|
|
137
|
+
const describe = feed => _effect.Effect.gen(function* () {
|
|
138
|
+
const head = yield* sql.unsafe(statements.selectHead, [feed]);
|
|
139
|
+
const sinks = yield* sql.unsafe(statements.describeSinks, [feed]);
|
|
140
|
+
const parked = yield* sql.unsafe(statements.describeParked, [feed]);
|
|
141
|
+
const parkedBySink = new Map();
|
|
142
|
+
for (const record of parked) {
|
|
143
|
+
const list = parkedBySink.get(record.sink);
|
|
144
|
+
if (list === undefined) {
|
|
145
|
+
parkedBySink.set(record.sink, [parkedOf(record)]);
|
|
146
|
+
} else {
|
|
147
|
+
list.push(parkedOf(record));
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
const description = {
|
|
151
|
+
head: _feed.FeedSeq.make((0, _context.bigintOf)(head[0]?.head ?? '0')),
|
|
152
|
+
sinks: sinks.map(row => ({
|
|
153
|
+
sink: _feed.SinkName.make(row.sink),
|
|
154
|
+
cursor: cursorOf(row),
|
|
155
|
+
lastAdvance: instantOf(row.last_advance_at_ms),
|
|
156
|
+
lease: row.lease_holder === null ? _effect.Option.none() : _effect.Option.some({
|
|
157
|
+
holder: _feed.LeaseHolder.make(row.lease_holder),
|
|
158
|
+
expiresAt: instantOf(row.lease_expires_at_ms)
|
|
159
|
+
}),
|
|
160
|
+
parked: parkedBySink.get(row.sink) ?? [],
|
|
161
|
+
pending: Number(row.pending)
|
|
162
|
+
}))
|
|
163
|
+
};
|
|
164
|
+
return description;
|
|
165
|
+
}).pipe(_effect.Effect.mapError((0, _context.unreadable)(`describing feed '${feed}'`)));
|
|
166
|
+
const store = {
|
|
167
|
+
acquireLease,
|
|
168
|
+
readCursor,
|
|
169
|
+
casCursor,
|
|
170
|
+
readParked,
|
|
171
|
+
retryParked,
|
|
172
|
+
collectTombstones,
|
|
173
|
+
removeSink,
|
|
174
|
+
describe
|
|
175
|
+
};
|
|
176
|
+
return store;
|
|
177
|
+
});
|
|
178
|
+
exports.makePostgresFeedSyncStore = makePostgresFeedSyncStore;
|
|
179
|
+
//# sourceMappingURL=syncStore.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"syncStore.js","names":["_feed","require","_effect","_sql","_context","_lease","instantOf","ms","Option","none","some","DateTime","makeUnsafe","Number","cursorOf","row","undefined","generation","seq","Generation","make","bigintOf","FeedSeq","parkedOf","key","FeedKey","reason","parkedAt","parked_at_ms","parkJson","park","byKey","Map","record","delete","set","JSON","stringify","Array","from","values","toString","String","toEpochMillis","makePostgresFeedSyncStore","options","Effect","gen","sql","SqlClient","withoutTransforms","context","feedContext","config","acquireLease","makeAcquireLease","lease","leaseRow","statements","readCursor","id","unsafe","selectCursor","feed","sink","pipe","mapError","unreadable","map","rows","casCursor","args","withTransaction","expected","getOrUndefined","now","landed","holder","next","length","CursorSuperseded","change","parked","_tag","clearParked","unpark","unparkKeys","parkKeys","catchTag","cause","fail","writeFailed","readParked","selectParked","retryParked","lockFeedExclusive","unparked","unparkOne","restampRow","collectTombstones","removeSink","lock","tryAdvisoryXactLock","advisoryLockName","taken","LeaseHeld","Clock","currentTimeMillis","cursor","deleteCursorUnlessLeased","left","cursorExists","deleteSinkParked","describe","head","selectHead","sinks","describeSinks","describeParked","parkedBySink","list","get","push","description","SinkName","lastAdvance","last_advance_at_ms","lease_holder","LeaseHolder","expiresAt","lease_expires_at_ms","pending","store","exports"],"sources":["../../src/syncStore.ts"],"sourcesContent":[null],"mappings":"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","ignoreList":[]}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.withFeed = void 0;
|
|
7
|
+
var _read = require("@kairos-es/read");
|
|
8
|
+
var _effect = require("effect");
|
|
9
|
+
var _sql = require("effect/sql");
|
|
10
|
+
var _feed = require("./feed.js");
|
|
11
|
+
var _context = require("./internal/context.js");
|
|
12
|
+
const sameKey = (left, right) => left.projection === right.projection && left.partition === right.partition;
|
|
13
|
+
/**
|
|
14
|
+
* `store`, with `resetCheckpoint` for `key` also emptying `feed` and moving its
|
|
15
|
+
* generation on; see the module doc. Every other key passes through untouched.
|
|
16
|
+
*/
|
|
17
|
+
const withFeed = (store, feed, key) => _effect.Effect.gen(function* () {
|
|
18
|
+
const context = (0, _feed.contextOf)(feed);
|
|
19
|
+
if (context === undefined) {
|
|
20
|
+
return yield* _effect.Effect.die(new Error(`withFeed: feed '${feed.feed}' was not made by makePostgresFeed`));
|
|
21
|
+
}
|
|
22
|
+
if (store.durability !== 'durable') {
|
|
23
|
+
return yield* _effect.Effect.die(new Error(`withFeed: feed '${feed.feed}' is written inside the store's commit transaction, and an ephemeral store opens none`));
|
|
24
|
+
}
|
|
25
|
+
const {
|
|
26
|
+
statements
|
|
27
|
+
} = context;
|
|
28
|
+
const sql = (yield* _sql.SqlClient.SqlClient).withoutTransforms();
|
|
29
|
+
const bound = yield* sql.unsafe(statements.bindFeed, [feed.feed, key.projection, key.partition]).pipe(_effect.Effect.mapError((0, _context.writeFailed)(`binding feed '${feed.feed}'`)));
|
|
30
|
+
const binding = bound[0];
|
|
31
|
+
if (binding !== undefined && (binding.projection !== key.projection || binding.partition !== key.partition)) {
|
|
32
|
+
return yield* _effect.Effect.die(new Error(`withFeed: feed '${feed.feed}' is already bound to projection '${binding.projection}' (partition '${binding.partition}'), and one feed serves one materialisation`));
|
|
33
|
+
}
|
|
34
|
+
const resetBound = reset => sql.withTransaction(_effect.Effect.gen(function* () {
|
|
35
|
+
yield* sql.unsafe(statements.bumpGeneration, [feed.feed]);
|
|
36
|
+
yield* store.resetCheckpoint(reset);
|
|
37
|
+
yield* sql.unsafe(statements.deleteFeedRows, [feed.feed]);
|
|
38
|
+
})).pipe(_effect.Effect.catchTag('SqlError', cause => _effect.Effect.fail(new _read.ProjectionStoreError({
|
|
39
|
+
reason: `resetting projection '${reset.projection}' partition '${reset.partition}' and its feed '${feed.feed}' failed: ${cause.message}`,
|
|
40
|
+
cause
|
|
41
|
+
}))));
|
|
42
|
+
const decorated = {
|
|
43
|
+
durability: store.durability,
|
|
44
|
+
readCheckpoint: store.readCheckpoint,
|
|
45
|
+
commit: store.commit,
|
|
46
|
+
resetCheckpoint: reset => sameKey(reset, key) ? resetBound(reset) : store.resetCheckpoint(reset)
|
|
47
|
+
};
|
|
48
|
+
return decorated;
|
|
49
|
+
});
|
|
50
|
+
exports.withFeed = withFeed;
|
|
51
|
+
//# sourceMappingURL=withFeed.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"withFeed.js","names":["_read","require","_effect","_sql","_feed","_context","sameKey","left","right","projection","partition","withFeed","store","feed","key","Effect","gen","context","contextOf","undefined","die","Error","durability","statements","sql","SqlClient","withoutTransforms","bound","unsafe","bindFeed","pipe","mapError","writeFailed","binding","resetBound","reset","withTransaction","bumpGeneration","resetCheckpoint","deleteFeedRows","catchTag","cause","fail","ProjectionStoreError","reason","message","decorated","readCheckpoint","commit","exports"],"sources":["../../src/withFeed.ts"],"sourcesContent":[null],"mappings":"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","ignoreList":[]}
|