@braidlabs/data 0.1.1
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/package.json +41 -0
- package/src/index.d.ts +34 -0
- package/src/index.js +23 -0
- package/src/index.js.map +1 -0
- package/src/lib/adapters.d.ts +74 -0
- package/src/lib/adapters.js +10 -0
- package/src/lib/adapters.js.map +1 -0
- package/src/lib/events.d.ts +141 -0
- package/src/lib/events.js +337 -0
- package/src/lib/events.js.map +1 -0
- package/src/lib/flush.d.ts +49 -0
- package/src/lib/flush.js +60 -0
- package/src/lib/flush.js.map +1 -0
- package/src/lib/indexeddb-driver.d.ts +29 -0
- package/src/lib/indexeddb-driver.js +160 -0
- package/src/lib/indexeddb-driver.js.map +1 -0
- package/src/lib/intents.d.ts +83 -0
- package/src/lib/intents.js +136 -0
- package/src/lib/intents.js.map +1 -0
- package/src/lib/invalidation.d.ts +63 -0
- package/src/lib/invalidation.js +136 -0
- package/src/lib/invalidation.js.map +1 -0
- package/src/lib/locks.d.ts +43 -0
- package/src/lib/locks.js +78 -0
- package/src/lib/locks.js.map +1 -0
- package/src/lib/memory-driver.d.ts +10 -0
- package/src/lib/memory-driver.js +53 -0
- package/src/lib/memory-driver.js.map +1 -0
- package/src/lib/outbox.d.ts +146 -0
- package/src/lib/outbox.js +89 -0
- package/src/lib/outbox.js.map +1 -0
- package/src/lib/partitions.d.ts +70 -0
- package/src/lib/partitions.js +62 -0
- package/src/lib/partitions.js.map +1 -0
- package/src/lib/query.d.ts +235 -0
- package/src/lib/query.js +430 -0
- package/src/lib/query.js.map +1 -0
- package/src/lib/record-store.d.ts +125 -0
- package/src/lib/record-store.js +85 -0
- package/src/lib/record-store.js.map +1 -0
- package/src/lib/tenancy.d.ts +121 -0
- package/src/lib/tenancy.js +220 -0
- package/src/lib/tenancy.js.map +1 -0
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
import { drainOutbox } from './flush.js';
|
|
2
|
+
import { sharedInvalidator } from './invalidation.js';
|
|
3
|
+
import { createOutbox } from './outbox.js';
|
|
4
|
+
import { createIntentRegistry, } from './intents.js';
|
|
5
|
+
const EVENT_COLLECTION = 'skew-events';
|
|
6
|
+
const DEFAULT_TTL_MS = 60 * 60 * 1000;
|
|
7
|
+
const DEFAULT_MAX_DEPTH = 500;
|
|
8
|
+
/** The queue owner for a consumer's events, kept clear of its mutation outbox's lock and records. */
|
|
9
|
+
const queueOwner = (consumer) => `events:${consumer}`;
|
|
10
|
+
/** The tag that says "your event queue moved" — the same wake-up trick the overlay uses. */
|
|
11
|
+
const wakeTag = (consumer) => `skew:events#${consumer}`;
|
|
12
|
+
/** One mutation id per channel+type, so `drainOutbox` can find the handler by name after a reload. */
|
|
13
|
+
const runnerId = (channel, type) => `event:${channel}:${type}`;
|
|
14
|
+
export function createEventBus(options) {
|
|
15
|
+
const partition = options.partition ?? (() => 'default');
|
|
16
|
+
const invalidator = options.invalidator ?? sharedInvalidator({ partition: partition() });
|
|
17
|
+
const schemas = new Map();
|
|
18
|
+
const channels = new Map();
|
|
19
|
+
const handlers = new Map();
|
|
20
|
+
const maxAttempts = options.maxAttempts ?? 5;
|
|
21
|
+
/** This consumer's own durable queue. One outbox, every channel's events. */
|
|
22
|
+
const queue = options.driver
|
|
23
|
+
? createOutbox({
|
|
24
|
+
driver: options.driver,
|
|
25
|
+
owner: queueOwner(options.consumer),
|
|
26
|
+
partition: partition(),
|
|
27
|
+
collection: EVENT_COLLECTION,
|
|
28
|
+
})
|
|
29
|
+
: null;
|
|
30
|
+
/** Projects a value to one reader's version, exactly as the context bus does. */
|
|
31
|
+
function project(type, value, as) {
|
|
32
|
+
const schema = schemas.get(type);
|
|
33
|
+
if (!schema)
|
|
34
|
+
return structuredClone(value);
|
|
35
|
+
const envelope = as === undefined || as === schema.version ? schema.write(value) : schema.write(value, { as });
|
|
36
|
+
return structuredClone(envelope.payload);
|
|
37
|
+
}
|
|
38
|
+
function openWire(name) {
|
|
39
|
+
const wireName = `skew:events:${partition()}:${name}`;
|
|
40
|
+
if (options.broadcastChannel)
|
|
41
|
+
return options.broadcastChannel(wireName);
|
|
42
|
+
return typeof BroadcastChannel === 'undefined'
|
|
43
|
+
? undefined
|
|
44
|
+
: new BroadcastChannel(wireName);
|
|
45
|
+
}
|
|
46
|
+
function channelState(name, channelOptions) {
|
|
47
|
+
// One entity's channel is not another's, so the identity carries it — while the contract stays
|
|
48
|
+
// registered under the logical name, because every fund's selection has the same shape.
|
|
49
|
+
const key = channelOptions.entity ? `${name}@${channelOptions.entity}` : name;
|
|
50
|
+
const existing = channels.get(key);
|
|
51
|
+
if (existing)
|
|
52
|
+
return existing;
|
|
53
|
+
const scope = channelOptions.scope ?? 'page';
|
|
54
|
+
const wire = scope === 'origin' ? openWire(key) : undefined;
|
|
55
|
+
const created = {
|
|
56
|
+
name,
|
|
57
|
+
key,
|
|
58
|
+
options: channelOptions,
|
|
59
|
+
state: undefined,
|
|
60
|
+
conflict: null,
|
|
61
|
+
stateListeners: new Set(),
|
|
62
|
+
wire,
|
|
63
|
+
};
|
|
64
|
+
// A BroadcastChannel never delivers to its own sender, so a message arriving here is always from
|
|
65
|
+
// another context — no de-duplication needed, which is the same property invalidation relies on.
|
|
66
|
+
wire?.addEventListener('message', (event) => {
|
|
67
|
+
const message = event.data;
|
|
68
|
+
if (message?.kind === 'state')
|
|
69
|
+
applyState(created, message.value, { local: false });
|
|
70
|
+
if (message?.kind === 'event' && message.event)
|
|
71
|
+
deliverLive(message.event);
|
|
72
|
+
});
|
|
73
|
+
channels.set(key, created);
|
|
74
|
+
return created;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Applies a new state value and tells the subscribers.
|
|
78
|
+
*
|
|
79
|
+
* Conflict is compared only against a value this context published: another context's broadcast is
|
|
80
|
+
* news, not a disagreement. Two publishers racing end in last-write-wins, and the policy decides
|
|
81
|
+
* whether anyone is told — the same contract, and the same words, as a mutation's confirmation.
|
|
82
|
+
*/
|
|
83
|
+
function applyState(channel, value, { local }) {
|
|
84
|
+
const previous = channel.state;
|
|
85
|
+
let stored = value;
|
|
86
|
+
if (local && previous !== undefined && !same(previous, value)) {
|
|
87
|
+
const policy = channel.options.onConflict ?? 'raise';
|
|
88
|
+
const conflict = {
|
|
89
|
+
expected: previous,
|
|
90
|
+
actual: value,
|
|
91
|
+
paths: differingKeys(previous, value),
|
|
92
|
+
};
|
|
93
|
+
if (typeof policy === 'function')
|
|
94
|
+
stored = policy(conflict);
|
|
95
|
+
else if (policy === 'raise')
|
|
96
|
+
channel.conflict = conflict;
|
|
97
|
+
}
|
|
98
|
+
channel.state = stored;
|
|
99
|
+
for (const subscription of [...channel.stateListeners]) {
|
|
100
|
+
// Isolated per subscriber: one application's bad deploy must not present as a different
|
|
101
|
+
// application silently failing to update.
|
|
102
|
+
try {
|
|
103
|
+
subscription.listener(project(channel.name, stored, subscription.as));
|
|
104
|
+
}
|
|
105
|
+
catch (error) {
|
|
106
|
+
options.onEventError?.(`[skew/data] a "${channel.name}" state subscriber threw`, error);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
/** Fan-out to handlers in this context. Used by at-most-once, and by the queue drain. */
|
|
111
|
+
function deliverLive(event) {
|
|
112
|
+
for (const handler of [...(handlers.get(runnerId(event.channel, event.type)) ?? [])]) {
|
|
113
|
+
try {
|
|
114
|
+
void handler(event);
|
|
115
|
+
}
|
|
116
|
+
catch (error) {
|
|
117
|
+
options.onEventError?.(`[skew/data] a "${event.type}" handler threw`, error);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Writes one entry per registered consumer, then wakes them.
|
|
123
|
+
*
|
|
124
|
+
* The emitting application is included: its own handlers are served by draining, not by a direct
|
|
125
|
+
* call, so a durable event is delivered by exactly one path no matter who is listening. Two paths
|
|
126
|
+
* would mean a mounted consumer handled the event twice.
|
|
127
|
+
*/
|
|
128
|
+
async function enqueue(event, channel) {
|
|
129
|
+
if (!options.driver) {
|
|
130
|
+
throw new TypeError('[skew/data] at-least-once delivery needs a `driver` on the bus: an event that must survive ' +
|
|
131
|
+
'a reload has to be written somewhere. Pass one, or emit at-most-once.');
|
|
132
|
+
}
|
|
133
|
+
const recipients = options.consumers?.length ? options.consumers : [options.consumer];
|
|
134
|
+
const ttl = channel.options.ttlMs ?? DEFAULT_TTL_MS;
|
|
135
|
+
const depth = channel.options.maxDepth ?? DEFAULT_MAX_DEPTH;
|
|
136
|
+
for (const consumer of recipients) {
|
|
137
|
+
const consumerQueue = createOutbox({
|
|
138
|
+
driver: options.driver,
|
|
139
|
+
owner: queueOwner(consumer),
|
|
140
|
+
partition: partition(),
|
|
141
|
+
collection: EVENT_COLLECTION,
|
|
142
|
+
});
|
|
143
|
+
await trim(consumerQueue, consumer, channel.key, ttl, depth);
|
|
144
|
+
await consumerQueue.enqueue({ mutationId: runnerId(event.channel, event.type), input: event });
|
|
145
|
+
}
|
|
146
|
+
// One tag for every consumer: the wake-up crosses realms and, with cross-context invalidation on,
|
|
147
|
+
// tabs — which is the whole reason a queued event reaches an application that was not running.
|
|
148
|
+
invalidator.invalidate(...recipients.map(wakeTag));
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Enforces the retention bounds before adding to a queue.
|
|
152
|
+
*
|
|
153
|
+
* Both bounds are needed and both are reported. Fan-out retention is unbounded by nature —
|
|
154
|
+
* consumers × events × until-handled — so a queue with no policy is a storage quota that dies
|
|
155
|
+
* unattended, and one that drops silently is worse than one that fills up.
|
|
156
|
+
*/
|
|
157
|
+
async function trim(consumerQueue, consumer, channelKey, ttlMs, maxDepth) {
|
|
158
|
+
// Scoped to this channel: one consumer's queue holds every channel's events, and a busy channel
|
|
159
|
+
// must not evict a quiet one's — least of all one entity's evicting another's.
|
|
160
|
+
const mine = (await consumerQueue.mine()).filter((entry) => entry.input.channel === channelKey);
|
|
161
|
+
const cutoff = Date.now() - ttlMs;
|
|
162
|
+
for (const entry of mine) {
|
|
163
|
+
const event = entry.input;
|
|
164
|
+
if (Date.parse(event.at) >= cutoff)
|
|
165
|
+
continue;
|
|
166
|
+
await consumerQueue.remove(entry.id);
|
|
167
|
+
options.onEventError?.(`[skew/data] dropped "${event.type}" for "${consumer}" — it sat in the queue past its ${ttlMs}ms window`, event);
|
|
168
|
+
}
|
|
169
|
+
const remaining = (await consumerQueue.mine()).filter((entry) => entry.input.channel === channelKey);
|
|
170
|
+
const excess = remaining.length + 1 - maxDepth;
|
|
171
|
+
for (const entry of remaining.slice(0, Math.max(0, excess))) {
|
|
172
|
+
await consumerQueue.remove(entry.id);
|
|
173
|
+
options.onEventError?.(`[skew/data] dropped "${entry.input.type}" for "${consumer}" — the queue is at its ${maxDepth} limit`, entry.input);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Drains this consumer's queue through `drainOutbox`.
|
|
178
|
+
*
|
|
179
|
+
* Everything about *how* a queue drains — one at a time across tabs, oldest first, stop at the
|
|
180
|
+
* first failure, abandon loudly after `maxAttempts` — is the same code that replays mutations. A
|
|
181
|
+
* second implementation of those rules would be a second set of bugs, and this package already
|
|
182
|
+
* paid for that lesson once.
|
|
183
|
+
*/
|
|
184
|
+
/**
|
|
185
|
+
* Serializes this bus's own flushes, for the reason the data client had to.
|
|
186
|
+
*
|
|
187
|
+
* The cross-context lock declines a drain another context is already running, which is right —
|
|
188
|
+
* waiting would drain a queue that context has already emptied. Applied to *one* bus's overlapping
|
|
189
|
+
* calls that rule is a trap: an emit wakes us, the wake-up flush is still finishing, and the
|
|
190
|
+
* caller's own `flush()` reports `skipped` and quietly does nothing. Within one bus, a second
|
|
191
|
+
* flush waits for the first and then runs, picking up whatever arrived in between.
|
|
192
|
+
*/
|
|
193
|
+
let flushing = null;
|
|
194
|
+
async function flush() {
|
|
195
|
+
if (flushing)
|
|
196
|
+
await flushing.catch(() => undefined);
|
|
197
|
+
const run = drain();
|
|
198
|
+
flushing = run;
|
|
199
|
+
try {
|
|
200
|
+
return await run;
|
|
201
|
+
}
|
|
202
|
+
finally {
|
|
203
|
+
if (flushing === run)
|
|
204
|
+
flushing = null;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
async function drain() {
|
|
208
|
+
if (!queue)
|
|
209
|
+
return { sent: 0, failed: 0, remaining: 0, skipped: false };
|
|
210
|
+
return drainOutbox({
|
|
211
|
+
outbox: queue,
|
|
212
|
+
owner: queueOwner(options.consumer),
|
|
213
|
+
runnerFor: (mutationId) => {
|
|
214
|
+
const registered = handlers.get(mutationId);
|
|
215
|
+
if (!registered?.size)
|
|
216
|
+
return undefined;
|
|
217
|
+
return async (input) => {
|
|
218
|
+
const event = input;
|
|
219
|
+
// Sequentially: handlers for one event type on one consumer are ordered by registration,
|
|
220
|
+
// and a throwing handler must fail the entry so it is retried rather than lost.
|
|
221
|
+
for (const handler of [...registered])
|
|
222
|
+
await handler(event);
|
|
223
|
+
};
|
|
224
|
+
},
|
|
225
|
+
maxAttempts,
|
|
226
|
+
...(options.onEventError === undefined ? {} : { onError: options.onEventError }),
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
const intents = createIntentRegistry({
|
|
230
|
+
consumer: options.consumer,
|
|
231
|
+
// Intents project through the same registered contracts state and events do, so an application
|
|
232
|
+
// declares a shape once and every path respects it.
|
|
233
|
+
schemaFor: (intent) => schemas.get(intent),
|
|
234
|
+
...(options.onEventError === undefined ? {} : { onEventError: options.onEventError }),
|
|
235
|
+
});
|
|
236
|
+
// Woken by any context that queued something for us — including our own emit.
|
|
237
|
+
const stopWaking = invalidator.subscribe(() => [wakeTag(options.consumer)], () => void flush().catch(() => undefined));
|
|
238
|
+
return {
|
|
239
|
+
channel(name, channelOptions = {}) {
|
|
240
|
+
const channel = channelState(name, channelOptions);
|
|
241
|
+
return {
|
|
242
|
+
name,
|
|
243
|
+
broadcast(value) {
|
|
244
|
+
applyState(channel, value, { local: true });
|
|
245
|
+
channel.wire?.postMessage({ kind: 'state', value: channel.state });
|
|
246
|
+
},
|
|
247
|
+
state(readOptions) {
|
|
248
|
+
return channel.state === undefined ? undefined : project(name, channel.state, readOptions?.as);
|
|
249
|
+
},
|
|
250
|
+
subscribe(listener, subscribeOptions) {
|
|
251
|
+
const subscription = {
|
|
252
|
+
listener: listener,
|
|
253
|
+
...(subscribeOptions?.as === undefined ? {} : { as: subscribeOptions.as }),
|
|
254
|
+
};
|
|
255
|
+
channel.stateListeners.add(subscription);
|
|
256
|
+
// Called immediately with the current value: a fragment that mounts late still needs to
|
|
257
|
+
// know what is true, which is the half of FDC3's broadcast that *is* state.
|
|
258
|
+
if (channel.state !== undefined) {
|
|
259
|
+
try {
|
|
260
|
+
subscription.listener(project(name, channel.state, subscription.as));
|
|
261
|
+
}
|
|
262
|
+
catch (error) {
|
|
263
|
+
options.onEventError?.(`[skew/data] a "${name}" state subscriber threw on subscribe`, error);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
const unsubscribe = () => void channel.stateListeners.delete(subscription);
|
|
267
|
+
subscribeOptions?.signal?.addEventListener('abort', unsubscribe, { once: true });
|
|
268
|
+
return unsubscribe;
|
|
269
|
+
},
|
|
270
|
+
async emit(type, payload, emitOptions) {
|
|
271
|
+
const event = {
|
|
272
|
+
eventId: crypto.randomUUID(),
|
|
273
|
+
// The instance, not the logical name: a handler on fund f1 must not be handed f2's
|
|
274
|
+
// event, and retention must not let one entity evict another's queue.
|
|
275
|
+
channel: channel.key,
|
|
276
|
+
type,
|
|
277
|
+
payload,
|
|
278
|
+
at: new Date().toISOString(),
|
|
279
|
+
source: options.consumer,
|
|
280
|
+
};
|
|
281
|
+
if ((emitOptions?.delivery ?? 'at-most-once') === 'at-least-once') {
|
|
282
|
+
await enqueue(event, channel);
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
deliverLive(event);
|
|
286
|
+
channel.wire?.postMessage({ kind: 'event', event });
|
|
287
|
+
},
|
|
288
|
+
addEventListener(type, handler, subscribeOptions) {
|
|
289
|
+
const id = runnerId(channel.key, type);
|
|
290
|
+
let registered = handlers.get(id);
|
|
291
|
+
if (!registered) {
|
|
292
|
+
registered = new Set();
|
|
293
|
+
handlers.set(id, registered);
|
|
294
|
+
}
|
|
295
|
+
const wrapped = async (event) => {
|
|
296
|
+
await handler({
|
|
297
|
+
...event,
|
|
298
|
+
payload: project(type, event.payload, subscribeOptions?.as),
|
|
299
|
+
});
|
|
300
|
+
};
|
|
301
|
+
registered.add(wrapped);
|
|
302
|
+
const unsubscribe = () => void registered.delete(wrapped);
|
|
303
|
+
subscribeOptions?.signal?.addEventListener('abort', unsubscribe, { once: true });
|
|
304
|
+
return unsubscribe;
|
|
305
|
+
},
|
|
306
|
+
conflict: () => channel.conflict,
|
|
307
|
+
acknowledgeConflict: () => void (channel.conflict = null),
|
|
308
|
+
};
|
|
309
|
+
},
|
|
310
|
+
register(type, schema) {
|
|
311
|
+
schemas.set(type, schema);
|
|
312
|
+
},
|
|
313
|
+
addIntentListener: intents.addIntentListener,
|
|
314
|
+
raiseIntent: intents.raiseIntent,
|
|
315
|
+
candidates: intents.candidates,
|
|
316
|
+
flush,
|
|
317
|
+
close() {
|
|
318
|
+
stopWaking();
|
|
319
|
+
for (const channel of channels.values())
|
|
320
|
+
channel.wire?.close();
|
|
321
|
+
channels.clear();
|
|
322
|
+
handlers.clear();
|
|
323
|
+
invalidator.close();
|
|
324
|
+
},
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
/** Which top-level keys two states disagree about. Same rule as a mutation's conflict. */
|
|
328
|
+
function differingKeys(previous, next) {
|
|
329
|
+
const before = (previous ?? {});
|
|
330
|
+
const after = (next ?? {});
|
|
331
|
+
const keys = new Set([...Object.keys(before), ...Object.keys(after)]);
|
|
332
|
+
return [...keys].filter((key) => !same(before[key], after[key]));
|
|
333
|
+
}
|
|
334
|
+
function same(a, b) {
|
|
335
|
+
return a === b || JSON.stringify(a) === JSON.stringify(b);
|
|
336
|
+
}
|
|
337
|
+
//# sourceMappingURL=events.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"events.js","sourceRoot":"","sources":["../../../../../libs/data/src/lib/events.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { Outbox, QueuedEntry } from './outbox.js';
|
|
2
|
+
/**
|
|
3
|
+
* Draining a queue: one implementation, used by the framework-neutral client and by every binding.
|
|
4
|
+
*
|
|
5
|
+
* This lived in two places once — here and in Angular's `OutboxService` — and the copies had already
|
|
6
|
+
* drifted apart on what to do with an entry nobody can replay. Ordering, locking, retry, and give-up
|
|
7
|
+
* are exactly the rules that must not have two answers, so they have one.
|
|
8
|
+
*/
|
|
9
|
+
export interface FlushResult {
|
|
10
|
+
sent: number;
|
|
11
|
+
failed: number;
|
|
12
|
+
/** Entries still queued when the drain stopped. */
|
|
13
|
+
remaining: number;
|
|
14
|
+
/** True when another context held the lock and this call did no work. */
|
|
15
|
+
skipped: boolean;
|
|
16
|
+
}
|
|
17
|
+
/** Replays one queued write. Receives the entry's stored input, never a closure. */
|
|
18
|
+
export type MutationRunner = (input: unknown, entry: QueuedEntry) => Promise<unknown>;
|
|
19
|
+
export interface DrainOptions {
|
|
20
|
+
outbox: Outbox;
|
|
21
|
+
/** Which application's queue this is. The flush lock is per owner. */
|
|
22
|
+
owner: string;
|
|
23
|
+
/** How this caller finds the runner for a mutation kind. */
|
|
24
|
+
runnerFor: (mutationId: string) => MutationRunner | undefined;
|
|
25
|
+
/** Attempts before an entry is abandoned. */
|
|
26
|
+
maxAttempts?: number;
|
|
27
|
+
/** Reports an entry that could not be replayed. Never silent. */
|
|
28
|
+
onError?: (message: string, detail?: unknown) => void;
|
|
29
|
+
/** Called after each successful send, so a caller can invalidate what the write touched. */
|
|
30
|
+
onSent?: (entry: QueuedEntry) => void;
|
|
31
|
+
/** Called after each abandoned entry, for the same reason. */
|
|
32
|
+
onAbandoned?: (entry: QueuedEntry) => void;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Drains one owner's queue, oldest first, under a lock held across tabs and realms.
|
|
36
|
+
*
|
|
37
|
+
* Four rules, each with a failure behind it:
|
|
38
|
+
*
|
|
39
|
+
* - **Strictly sequential.** Queued writes routinely depend on each other — create a thing, then
|
|
40
|
+
* rename the thing — and replaying them in parallel races them into the wrong order.
|
|
41
|
+
* - **A failure stops the drain** rather than skipping ahead, for the same reason.
|
|
42
|
+
* - **A permanently failing entry is abandoned loudly.** The user navigated away believing it saved,
|
|
43
|
+
* so its disappearance has to be reported to someone who can act on it.
|
|
44
|
+
* - **An entry with no runner is kept, not dropped.** It means the app did not re-register that
|
|
45
|
+
* mutation kind at start-up, or renamed it between deploys — and dropping it discards a write the
|
|
46
|
+
* user was told had succeeded. Keeping it means a later build (or a rollback) can still send it,
|
|
47
|
+
* and `outbox.remove` is there for an operator who decides otherwise.
|
|
48
|
+
*/
|
|
49
|
+
export declare function drainOutbox(options: DrainOptions): Promise<FlushResult>;
|
package/src/lib/flush.js
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { outboxFlushLock, withLock } from './locks.js';
|
|
2
|
+
/**
|
|
3
|
+
* Drains one owner's queue, oldest first, under a lock held across tabs and realms.
|
|
4
|
+
*
|
|
5
|
+
* Four rules, each with a failure behind it:
|
|
6
|
+
*
|
|
7
|
+
* - **Strictly sequential.** Queued writes routinely depend on each other — create a thing, then
|
|
8
|
+
* rename the thing — and replaying them in parallel races them into the wrong order.
|
|
9
|
+
* - **A failure stops the drain** rather than skipping ahead, for the same reason.
|
|
10
|
+
* - **A permanently failing entry is abandoned loudly.** The user navigated away believing it saved,
|
|
11
|
+
* so its disappearance has to be reported to someone who can act on it.
|
|
12
|
+
* - **An entry with no runner is kept, not dropped.** It means the app did not re-register that
|
|
13
|
+
* mutation kind at start-up, or renamed it between deploys — and dropping it discards a write the
|
|
14
|
+
* user was told had succeeded. Keeping it means a later build (or a rollback) can still send it,
|
|
15
|
+
* and `outbox.remove` is there for an operator who decides otherwise.
|
|
16
|
+
*/
|
|
17
|
+
export async function drainOutbox(options) {
|
|
18
|
+
const maxAttempts = options.maxAttempts ?? 5;
|
|
19
|
+
// Declined rather than queued: a flush waiting behind another tab's would run against a queue that
|
|
20
|
+
// tab had already drained. Per owner, so an unrelated application's disjoint queue does not wait.
|
|
21
|
+
const outcome = await withLock(outboxFlushLock(options.owner), async () => {
|
|
22
|
+
let sent = 0;
|
|
23
|
+
let failed = 0;
|
|
24
|
+
// Snapshot: entries queued mid-drain wait for the next pass.
|
|
25
|
+
for (const entry of await options.outbox.mine()) {
|
|
26
|
+
const runner = options.runnerFor(entry.mutationId);
|
|
27
|
+
if (!runner) {
|
|
28
|
+
options.onError?.(`[skew/data] no registered mutation for "${entry.mutationId}" — the entry stays queued. ` +
|
|
29
|
+
'Register this mutation kind during start-up so a queue rehydrated from storage has ' +
|
|
30
|
+
'somewhere to go.', entry);
|
|
31
|
+
failed += 1;
|
|
32
|
+
break;
|
|
33
|
+
}
|
|
34
|
+
try {
|
|
35
|
+
await runner(entry.input, entry);
|
|
36
|
+
await options.outbox.remove(entry.id);
|
|
37
|
+
options.onSent?.(entry);
|
|
38
|
+
sent += 1;
|
|
39
|
+
}
|
|
40
|
+
catch (error) {
|
|
41
|
+
failed += 1;
|
|
42
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
43
|
+
const attempts = await options.outbox.recordFailure(entry.id, message);
|
|
44
|
+
if (attempts >= maxAttempts) {
|
|
45
|
+
options.onError?.(`[skew/data] giving up on "${entry.mutationId}" after ${attempts} attempts`, error);
|
|
46
|
+
await options.outbox.remove(entry.id);
|
|
47
|
+
options.onAbandoned?.(entry);
|
|
48
|
+
}
|
|
49
|
+
break;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return { sent, failed, remaining: (await options.outbox.mine()).length };
|
|
53
|
+
});
|
|
54
|
+
return outcome.acquired
|
|
55
|
+
? { ...outcome.value, skipped: false }
|
|
56
|
+
: // Reported rather than folded into `sent: 0`, which also happens when a drain ran and
|
|
57
|
+
// everything failed — conflating them makes a dead server look like a busy tab.
|
|
58
|
+
{ sent: 0, failed: 0, remaining: (await options.outbox.mine()).length, skipped: true };
|
|
59
|
+
}
|
|
60
|
+
//# sourceMappingURL=flush.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"flush.js","sourceRoot":"","sources":["../../../../../libs/data/src/lib/flush.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAuCvD;;;;;;;;;;;;;;GAcG;AACH,MAAM,CAAC,KAAK,UAAU,WAAW,CAAC,OAAqB;IACrD,MAAM,WAAW,GAAG,OAAO,CAAC,WAAW,IAAI,CAAC,CAAC;IAE7C,mGAAmG;IACnG,kGAAkG;IAClG,MAAM,OAAO,GAAG,MAAM,QAAQ,CAAC,eAAe,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,KAAK,IAAI,EAAE;QACxE,IAAI,IAAI,GAAG,CAAC,CAAC;QACb,IAAI,MAAM,GAAG,CAAC,CAAC;QAEf,6DAA6D;QAC7D,KAAK,MAAM,KAAK,IAAI,MAAM,OAAO,CAAC,MAAM,CAAC,IAAI,EAAE,EAAE,CAAC;YAChD,MAAM,MAAM,GAAG,OAAO,CAAC,SAAS,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC;YAEnD,IAAI,CAAC,MAAM,EAAE,CAAC;gBACZ,OAAO,CAAC,OAAO,EAAE,CACf,2CAA2C,KAAK,CAAC,UAAU,8BAA8B;oBACvF,qFAAqF;oBACrF,kBAAkB,EACpB,KAAK,CACN,CAAC;gBACF,MAAM,IAAI,CAAC,CAAC;gBACZ,MAAM;YACR,CAAC;YAED,IAAI,CAAC;gBACH,MAAM,MAAM,CAAC,KAAK,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC;gBACjC,MAAM,OAAO,CAAC,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;gBACtC,OAAO,CAAC,MAAM,EAAE,CAAC,KAAK,CAAC,CAAC;gBACxB,IAAI,IAAI,CAAC,CAAC;YACZ,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBACf,MAAM,IAAI,CAAC,CAAC;gBACZ,MAAM,OAAO,GAAG,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;gBACvE,MAAM,QAAQ,GAAG,MAAM,OAAO,CAAC,MAAM,CAAC,aAAa,CAAC,KAAK,CAAC,EAAE,EAAE,OAAO,CAAC,CAAC;gBAEvE,IAAI,QAAQ,IAAI,WAAW,EAAE,CAAC;oBAC5B,OAAO,CAAC,OAAO,EAAE,CAAC,6BAA6B,KAAK,CAAC,UAAU,WAAW,QAAQ,WAAW,EAAE,KAAK,CAAC,CAAC;oBACtG,MAAM,OAAO,CAAC,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;oBACtC,OAAO,CAAC,WAAW,EAAE,CAAC,KAAK,CAAC,CAAC;gBAC/B,CAAC;gBACD,MAAM;YACR,CAAC;QACH,CAAC;QAED,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,CAAC,MAAM,OAAO,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC;IAC3E,CAAC,CAAC,CAAC;IAEH,OAAO,OAAO,CAAC,QAAQ;QACrB,CAAC,CAAC,EAAE,GAAG,OAAO,CAAC,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE;QACtC,CAAC,CAAC,sFAAsF;YACtF,gFAAgF;YAChF,EAAE,IAAI,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,SAAS,EAAE,CAAC,MAAM,OAAO,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;AAC7F,CAAC"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { RecordDriver } from './record-store.js';
|
|
2
|
+
export interface IndexedDbDriverOptions {
|
|
3
|
+
/** Database name. One per application is normal; collections are object stores within it. */
|
|
4
|
+
database?: string;
|
|
5
|
+
/** Collections to create. IndexedDB can only create stores during a version upgrade. */
|
|
6
|
+
collections: readonly string[];
|
|
7
|
+
/** Injectable for tests. */
|
|
8
|
+
indexedDB?: IDBFactory;
|
|
9
|
+
/**
|
|
10
|
+
* Injectable for tests, alongside `indexedDB`.
|
|
11
|
+
*
|
|
12
|
+
* Both are needed to substitute the implementation: `IDBKeyRange` is a separate global rather than
|
|
13
|
+
* something reachable from the factory, so injecting only the factory leaves the range queries
|
|
14
|
+
* reaching for whatever the environment happens to have — which is nothing, outside a browser.
|
|
15
|
+
*/
|
|
16
|
+
keyRange?: typeof IDBKeyRange;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* IndexedDB, as the source of truth.
|
|
20
|
+
*
|
|
21
|
+
* Why this rather than a KV shim over the same database: every operation below is **one
|
|
22
|
+
* transaction over one record**. There is no read-modify-write anywhere, so two realms or two tabs
|
|
23
|
+
* writing concurrently cannot lose each other's work — which a `get`-then-`set` shim absolutely can,
|
|
24
|
+
* whatever storage sits underneath it.
|
|
25
|
+
*
|
|
26
|
+
* The `[partition, seq]` compound index is what makes `list` a range scan rather than a full read
|
|
27
|
+
* plus filter, and it is why partitions stay cheap as the store grows.
|
|
28
|
+
*/
|
|
29
|
+
export declare function indexedDbRecordDriver(options: IndexedDbDriverOptions): RecordDriver;
|