@ironflow/browser 0.26.0 → 0.28.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/README.md +179 -13
- package/dist/client.d.ts +94 -6
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +191 -122
- package/dist/client.js.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10 -0
- package/dist/index.js.map +1 -1
- package/dist/offline.d.ts +166 -0
- package/dist/offline.d.ts.map +1 -0
- package/dist/offline.js +306 -0
- package/dist/offline.js.map +1 -0
- package/dist/queue/db.d.ts +123 -0
- package/dist/queue/db.d.ts.map +1 -0
- package/dist/queue/db.js +304 -0
- package/dist/queue/db.js.map +1 -0
- package/dist/queue/ids.d.ts +22 -0
- package/dist/queue/ids.d.ts.map +1 -0
- package/dist/queue/ids.js +45 -0
- package/dist/queue/ids.js.map +1 -0
- package/dist/queue/lock.d.ts +42 -0
- package/dist/queue/lock.d.ts.map +1 -0
- package/dist/queue/lock.js +65 -0
- package/dist/queue/lock.js.map +1 -0
- package/dist/queue/queue.d.ts +281 -0
- package/dist/queue/queue.d.ts.map +1 -0
- package/dist/queue/queue.js +894 -0
- package/dist/queue/queue.js.map +1 -0
- package/dist/queue/types.d.ts +145 -0
- package/dist/queue/types.d.ts.map +1 -0
- package/dist/queue/types.js +6 -0
- package/dist/queue/types.js.map +1 -0
- package/package.json +5 -3
package/dist/offline.js
ADDED
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The offline-capable client (ADR 0052).
|
|
3
|
+
*
|
|
4
|
+
* Why this is a separate factory rather than a flag on the singleton:
|
|
5
|
+
* `configure()` returns `void`, so TypeScript cannot refine `emit()`'s static
|
|
6
|
+
* return type from a runtime option. Putting the queued-result type on the
|
|
7
|
+
* shared `ironflow` singleton would force every existing caller to handle a
|
|
8
|
+
* case that can only occur for apps that opted in — a package-wide breaking
|
|
9
|
+
* change dressed up as an opt-in. A factory confines the new type to clients
|
|
10
|
+
* that asked for it and leaves `ironflow` exactly as it was.
|
|
11
|
+
*
|
|
12
|
+
* createClient({ offlineQueue: { identity } })
|
|
13
|
+
* │
|
|
14
|
+
* ├─ no IndexedDB (SSR, Node) ──> queue disabled, writes go direct
|
|
15
|
+
* └─ IndexedDB available ──> OfflineQueue over emit + append
|
|
16
|
+
*
|
|
17
|
+
* The underlying full client stays reachable as `.client`, so everything this
|
|
18
|
+
* surface does not wrap (runs, projections, subscriptions, admin) is one
|
|
19
|
+
* property away rather than re-declared here.
|
|
20
|
+
*/
|
|
21
|
+
import { IronflowError } from "@ironflow/core";
|
|
22
|
+
import { IronflowClient, REPLAY_QUEUED_WRITE } from "./client.js";
|
|
23
|
+
import { newUuid } from "./queue/ids.js";
|
|
24
|
+
import { OfflineQueue } from "./queue/queue.js";
|
|
25
|
+
/**
|
|
26
|
+
* Endpoint per write kind.
|
|
27
|
+
*
|
|
28
|
+
* The drainer resolves the path through this at send time and never reads one
|
|
29
|
+
* out of storage, so a record injected into IndexedDB cannot aim an
|
|
30
|
+
* authenticated POST at an arbitrary endpoint.
|
|
31
|
+
*/
|
|
32
|
+
const PATHS = {
|
|
33
|
+
emit: "/ironflow.v1.PubSubService/Emit",
|
|
34
|
+
"stream-append": "/ironflow.v1.EntityStreamService/AppendEvent",
|
|
35
|
+
};
|
|
36
|
+
/**
|
|
37
|
+
* A client whose `emit` and `streams.append` persist before they send.
|
|
38
|
+
*
|
|
39
|
+
* Note that neither returns `runIds`. A run id cannot exist before the server
|
|
40
|
+
* creates the run, and the whole point here is to answer before contacting the
|
|
41
|
+
* server. Subscribe for the outcome instead.
|
|
42
|
+
*/
|
|
43
|
+
export class OfflineClient {
|
|
44
|
+
/** The full client. Everything not wrapped here lives on it. */
|
|
45
|
+
client;
|
|
46
|
+
offlineQueue;
|
|
47
|
+
/** @internal Use {@link createClient}. */
|
|
48
|
+
constructor(client, queue) {
|
|
49
|
+
this.client = client;
|
|
50
|
+
this.offlineQueue = queue;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Emit an event, persisting it first.
|
|
54
|
+
*
|
|
55
|
+
* Always write-through, even when the network is fine. Sending directly
|
|
56
|
+
* whenever the queue happens to be empty would let a new write overtake
|
|
57
|
+
* queued ones the moment it is not, which is the FIFO violation the design
|
|
58
|
+
* exists to prevent.
|
|
59
|
+
*/
|
|
60
|
+
async emit(eventName, data, options) {
|
|
61
|
+
// Fix the key HERE, before serialising, so the stored bytes already carry
|
|
62
|
+
// it. If the body went out without one, every retry would create a fresh
|
|
63
|
+
// event server-side and the queue's whole exactly-once story would be
|
|
64
|
+
// fiction — the engine dedups on this field and nothing else.
|
|
65
|
+
//
|
|
66
|
+
// `||`, not `??`: an empty string is a key the engine will ignore, so
|
|
67
|
+
// honouring it verbatim would report a dedup key that does not dedup.
|
|
68
|
+
if (!this.offlineQueue) {
|
|
69
|
+
const direct = directIdempotencyKey(options?.idempotencyKey);
|
|
70
|
+
const result = await this.client.emit(eventName, data, {
|
|
71
|
+
...options,
|
|
72
|
+
idempotencyKey: direct,
|
|
73
|
+
});
|
|
74
|
+
return {
|
|
75
|
+
queued: false,
|
|
76
|
+
localId: result.eventId ?? "",
|
|
77
|
+
idempotencyKey: direct ?? "",
|
|
78
|
+
pending: false,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
const key = options?.idempotencyKey || newUuid();
|
|
82
|
+
const { localId, idempotencyKey } = await this.offlineQueue.enqueue({
|
|
83
|
+
kind: "emit",
|
|
84
|
+
body: {
|
|
85
|
+
event: eventName,
|
|
86
|
+
data,
|
|
87
|
+
...(options?.version ? { version: options.version } : {}),
|
|
88
|
+
idempotency_key: key,
|
|
89
|
+
metadata: options?.metadata,
|
|
90
|
+
namespace: options?.namespace,
|
|
91
|
+
},
|
|
92
|
+
idempotencyKey: key,
|
|
93
|
+
});
|
|
94
|
+
return { queued: true, localId, idempotencyKey, pending: true };
|
|
95
|
+
}
|
|
96
|
+
streams = {
|
|
97
|
+
/**
|
|
98
|
+
* Append to an entity stream, persisting first.
|
|
99
|
+
*
|
|
100
|
+
* `expectedVersion` is refused outright. A version read before the write
|
|
101
|
+
* was queued is stale by definition, so the append is guaranteed to lose
|
|
102
|
+
* its optimistic-concurrency check — the server answers `CodeAborted`/409,
|
|
103
|
+
* which this queue treats as permanent and dead-letters. Failing loudly at
|
|
104
|
+
* call time beats discovering it in the dead-letter store later.
|
|
105
|
+
* Merge-on-conflict is a separate feature.
|
|
106
|
+
*/
|
|
107
|
+
append: async (entityId, input, options) => {
|
|
108
|
+
if (options?.expectedVersion !== undefined &&
|
|
109
|
+
options.expectedVersion !== -1) {
|
|
110
|
+
throw new IronflowError("streams.append cannot be queued with expectedVersion set: the version " +
|
|
111
|
+
"is stale by the time the write is sent, so the append would always " +
|
|
112
|
+
"conflict. Use expectedVersion: -1, or write through client.streams.append.", { code: "QUEUE_EXPECTED_VERSION_UNSUPPORTED", retryable: false });
|
|
113
|
+
}
|
|
114
|
+
// Same reasoning as emit(): the key has to be in the stored bytes, not
|
|
115
|
+
// just alongside them, or a retry duplicates the event — and the direct
|
|
116
|
+
// path gets one too, so the reported key is always the one the engine saw.
|
|
117
|
+
if (!this.offlineQueue) {
|
|
118
|
+
const direct = directIdempotencyKey(options?.idempotencyKey);
|
|
119
|
+
const result = await this.client.streams.append(entityId, input, {
|
|
120
|
+
...options,
|
|
121
|
+
idempotencyKey: direct,
|
|
122
|
+
});
|
|
123
|
+
return {
|
|
124
|
+
queued: false,
|
|
125
|
+
localId: result.eventId,
|
|
126
|
+
idempotencyKey: direct ?? "",
|
|
127
|
+
pending: false,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
const key = options?.idempotencyKey || newUuid();
|
|
131
|
+
const body = {
|
|
132
|
+
entity_id: entityId,
|
|
133
|
+
entity_type: input.entityType,
|
|
134
|
+
event_name: input.name,
|
|
135
|
+
data: input.data,
|
|
136
|
+
expected_version: -1,
|
|
137
|
+
idempotency_key: key,
|
|
138
|
+
version: options?.version ?? 1,
|
|
139
|
+
};
|
|
140
|
+
if (options?.metadata !== undefined) {
|
|
141
|
+
body.metadata = options.metadata;
|
|
142
|
+
}
|
|
143
|
+
const { localId, idempotencyKey } = await this.offlineQueue.enqueue({
|
|
144
|
+
kind: "stream-append",
|
|
145
|
+
body,
|
|
146
|
+
idempotencyKey: key,
|
|
147
|
+
});
|
|
148
|
+
return { queued: true, localId, idempotencyKey, pending: true };
|
|
149
|
+
},
|
|
150
|
+
};
|
|
151
|
+
/** Stable across reads, so `useEffect(..., [app.queue])` does not thrash. */
|
|
152
|
+
queueApi = null;
|
|
153
|
+
get queue() {
|
|
154
|
+
if (this.queueApi)
|
|
155
|
+
return this.queueApi;
|
|
156
|
+
const q = this.offlineQueue;
|
|
157
|
+
if (!q) {
|
|
158
|
+
// No IndexedDB: the surface still exists so callers need no branching,
|
|
159
|
+
// it just reports an empty, disabled queue.
|
|
160
|
+
const empty = {
|
|
161
|
+
pending: 0,
|
|
162
|
+
inFlight: 0,
|
|
163
|
+
total: 0,
|
|
164
|
+
deadLettered: 0,
|
|
165
|
+
state: "idle",
|
|
166
|
+
};
|
|
167
|
+
return (this.queueApi = {
|
|
168
|
+
enabled: false,
|
|
169
|
+
stats: () => empty,
|
|
170
|
+
subscribe: (cb) => {
|
|
171
|
+
cb(empty);
|
|
172
|
+
return () => { };
|
|
173
|
+
},
|
|
174
|
+
watch: (_id, cb) => {
|
|
175
|
+
cb({ status: "sent" });
|
|
176
|
+
return () => { };
|
|
177
|
+
},
|
|
178
|
+
flush: async () => { },
|
|
179
|
+
deadLetter: async () => [],
|
|
180
|
+
discard: async () => false,
|
|
181
|
+
retry: async () => false,
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
return (this.queueApi = {
|
|
185
|
+
enabled: true,
|
|
186
|
+
stats: () => q.stats(),
|
|
187
|
+
subscribe: (cb) => q.subscribe(cb),
|
|
188
|
+
watch: (id, cb) => q.watch(id, cb),
|
|
189
|
+
flush: () => q.flush(),
|
|
190
|
+
deadLetter: () => q.deadLetter(),
|
|
191
|
+
discard: (id) => q.discard(id),
|
|
192
|
+
retry: (id) => q.retry(id),
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Stop draining and release the database.
|
|
197
|
+
*
|
|
198
|
+
* Drain-then-swap: an in-flight write is allowed to finish rather than being
|
|
199
|
+
* abandoned with its fate unknown.
|
|
200
|
+
*/
|
|
201
|
+
async close() {
|
|
202
|
+
await this.offlineQueue?.stop();
|
|
203
|
+
this.client.disconnect();
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* The dedup key to report and send on the direct (queue-disabled) path.
|
|
208
|
+
*
|
|
209
|
+
* An empty string counts as absent. The engine skips dedup entirely for an
|
|
210
|
+
* empty key, so passing one straight through would make
|
|
211
|
+
* `QueuedWriteResult.idempotencyKey` — documented as "the dedup key the engine
|
|
212
|
+
* will see" — false exactly where a caller might trust it to retry safely.
|
|
213
|
+
*
|
|
214
|
+
* Generation is allowed to FAIL rather than throw. This path exists for
|
|
215
|
+
* environments missing browser APIs (SSR, Node, a locked-down WebView), and
|
|
216
|
+
* `newUuid()` throws without Web Crypto — so generating unconditionally would
|
|
217
|
+
* turn the documented degradation path into a crash on the very environments it
|
|
218
|
+
* exists to tolerate. Reporting no key there is honest: the engine saw none.
|
|
219
|
+
*/
|
|
220
|
+
function directIdempotencyKey(supplied) {
|
|
221
|
+
if (supplied)
|
|
222
|
+
return supplied;
|
|
223
|
+
try {
|
|
224
|
+
return newUuid();
|
|
225
|
+
}
|
|
226
|
+
catch {
|
|
227
|
+
return undefined;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
/** Default database name; scoped so staging writes can never flush into prod. */
|
|
231
|
+
export function queueDbName(serverUrl, environment) {
|
|
232
|
+
return `ironflow-queue:${serverUrl}:${environment}`;
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Create a client that persists writes before sending them.
|
|
236
|
+
*
|
|
237
|
+
* Async because the outbox has to be opened and its counters hydrated before
|
|
238
|
+
* the first `emit` can be honestly answered.
|
|
239
|
+
*
|
|
240
|
+
* Degrades rather than throwing when the environment has no IndexedDB (SSR, a
|
|
241
|
+
* Node import, a locked-down browser): the queue disables itself, writes go
|
|
242
|
+
* straight out, and the app keeps working. Throwing here would crash a server
|
|
243
|
+
* render for anyone who put the option in shared config.
|
|
244
|
+
*/
|
|
245
|
+
export async function createClient(options) {
|
|
246
|
+
const { offlineQueue: queueConfig, ...clientOptions } = options;
|
|
247
|
+
const client = new IronflowClient();
|
|
248
|
+
client.configure(clientOptions);
|
|
249
|
+
const config = client.getConfig();
|
|
250
|
+
if (typeof indexedDB === "undefined") {
|
|
251
|
+
// Through the client's logger, so an app that quieted SDK logging is not
|
|
252
|
+
// shouted at on every SSR render — the case this branch exists to tolerate.
|
|
253
|
+
client.getLogger().warn("IndexedDB is unavailable; the offline write queue is disabled and writes will be sent directly.");
|
|
254
|
+
return new OfflineClient(client, null);
|
|
255
|
+
}
|
|
256
|
+
const queue = new OfflineQueue({
|
|
257
|
+
dbName: queueConfig.dbName ?? queueDbName(config.serverUrl, config.environment),
|
|
258
|
+
identity: queueConfig.identity,
|
|
259
|
+
maxItems: queueConfig.maxItems,
|
|
260
|
+
maxBytes: queueConfig.maxBytes,
|
|
261
|
+
maxRetentionMs: queueConfig.maxRetentionMs,
|
|
262
|
+
maxDeadLettered: queueConfig.maxDeadLettered,
|
|
263
|
+
onWriteLost: queueConfig.onWriteLost
|
|
264
|
+
? (write, reason, message) => queueConfig.onWriteLost({ localId: write.localId, kind: write.kind, body: write.body }, reason, message)
|
|
265
|
+
: undefined,
|
|
266
|
+
onAuthRequired: queueConfig.onAuthRequired
|
|
267
|
+
? async () => {
|
|
268
|
+
// Applied through setAuth(), never configure(): configure() is a full
|
|
269
|
+
// teardown and would destroy the transport, the subscriptions and the
|
|
270
|
+
// drainer that is mid-recovery.
|
|
271
|
+
const { identity, ...auth } = await queueConfig.onAuthRequired();
|
|
272
|
+
// Only when a credential actually came back. Both `AuthConfig` fields
|
|
273
|
+
// are optional, so returning `{ identity }` alone typechecks — and
|
|
274
|
+
// `setAuth({})` would then clear the credential on the SHARED client,
|
|
275
|
+
// taking subscriptions and every non-queue call down with it, and
|
|
276
|
+
// guaranteeing the next attempt 401s again.
|
|
277
|
+
if (auth.apiKey || auth.token) {
|
|
278
|
+
client.setAuth(auth);
|
|
279
|
+
}
|
|
280
|
+
// Returning the identity lets the queue rebind. If a DIFFERENT user
|
|
281
|
+
// signed in at the prompt, the pending writes then fail the identity
|
|
282
|
+
// check and are quarantined instead of being sent as that new user.
|
|
283
|
+
return { identity };
|
|
284
|
+
}
|
|
285
|
+
: undefined,
|
|
286
|
+
resolvePath: (kind) => PATHS[kind] ?? null,
|
|
287
|
+
// Read live, not captured: the guard exists to catch a client that was
|
|
288
|
+
// reconfigured to another server or environment while writes were queued.
|
|
289
|
+
currentDestination: () => {
|
|
290
|
+
const live = client.getConfig();
|
|
291
|
+
return { serverUrl: live.serverUrl, environment: live.environment };
|
|
292
|
+
},
|
|
293
|
+
send: (path, write) => client[REPLAY_QUEUED_WRITE](path, write.body),
|
|
294
|
+
});
|
|
295
|
+
try {
|
|
296
|
+
await queue.start();
|
|
297
|
+
}
|
|
298
|
+
catch (error) {
|
|
299
|
+
// A database written by a newer SDK, or storage that refuses to open. The
|
|
300
|
+
// app is more useful with direct writes than with a hard failure at import.
|
|
301
|
+
client.getLogger().warn(`offline write queue could not start; writes will be sent directly. ${error instanceof Error ? error.message : String(error)}`);
|
|
302
|
+
return new OfflineClient(client, null);
|
|
303
|
+
}
|
|
304
|
+
return new OfflineClient(client, queue);
|
|
305
|
+
}
|
|
306
|
+
//# sourceMappingURL=offline.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"offline.js","sourceRoot":"","sources":["../src/offline.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* IndexedDB persistence for the offline write queue (ADR 0052).
|
|
3
|
+
*
|
|
4
|
+
* Two object stores:
|
|
5
|
+
*
|
|
6
|
+
* outbox keyPath "seq", autoIncrement index: localId
|
|
7
|
+
* Insertion order IS send order — autoIncrement gives a
|
|
8
|
+
* monotonic FIFO key for free, so "next to send" is just the
|
|
9
|
+
* first record by key.
|
|
10
|
+
*
|
|
11
|
+
* deadletter keyPath "write.localId"
|
|
12
|
+
* Writes that will never be sent, kept so an app can list,
|
|
13
|
+
* discard, or replay them. Without a surface they would be
|
|
14
|
+
* inaccessible data growing until quota pressure.
|
|
15
|
+
*
|
|
16
|
+
* Raw IndexedDB, no wrapper dependency: two stores and one upgrade handler do
|
|
17
|
+
* not justify adding to a package with four runtime deps.
|
|
18
|
+
*/
|
|
19
|
+
import { type DeadLetteredWrite, type QueuedWrite } from "./types.js";
|
|
20
|
+
/** A write with its FIFO key not yet assigned by IndexedDB. */
|
|
21
|
+
export type NewQueuedWrite = Omit<QueuedWrite, "seq">;
|
|
22
|
+
/**
|
|
23
|
+
* Thrown when the stored database was created by a NEWER SDK.
|
|
24
|
+
*
|
|
25
|
+
* IndexedDB refuses to open a database at a lower version than the one on disk,
|
|
26
|
+
* which is exactly the signal we want: a rollback, or another tab on a newer
|
|
27
|
+
* build. The queue disables delivery rather than touching data it cannot read.
|
|
28
|
+
*/
|
|
29
|
+
export declare class QueueVersionError extends Error {
|
|
30
|
+
constructor(message: string);
|
|
31
|
+
}
|
|
32
|
+
/** Thrown when a write cannot be stored because the origin is out of quota. */
|
|
33
|
+
export declare class QueueQuotaError extends Error {
|
|
34
|
+
constructor(message: string);
|
|
35
|
+
}
|
|
36
|
+
export declare class QueueDb {
|
|
37
|
+
private readonly db;
|
|
38
|
+
readonly name: string;
|
|
39
|
+
private constructor();
|
|
40
|
+
/**
|
|
41
|
+
* Open (creating if needed) the queue database.
|
|
42
|
+
*
|
|
43
|
+
* @throws {QueueVersionError} when the stored database is newer than this SDK.
|
|
44
|
+
*/
|
|
45
|
+
static open(name: string): Promise<QueueDb>;
|
|
46
|
+
close(): void;
|
|
47
|
+
/**
|
|
48
|
+
* Append a write and return the FIFO key IndexedDB assigned it.
|
|
49
|
+
*
|
|
50
|
+
* @throws {QueueQuotaError} when the origin is out of storage.
|
|
51
|
+
*/
|
|
52
|
+
append(write: NewQueuedWrite): Promise<number>;
|
|
53
|
+
/**
|
|
54
|
+
* The next write to send, or `undefined` when the outbox is empty.
|
|
55
|
+
*
|
|
56
|
+
* Strict FIFO: always the lowest key. A write that fails stays here, which is
|
|
57
|
+
* what makes the queue blocking — item N+1 cannot pass item N.
|
|
58
|
+
*/
|
|
59
|
+
head(): Promise<QueuedWrite | undefined>;
|
|
60
|
+
remove(seq: number): Promise<void>;
|
|
61
|
+
findByLocalId(localId: string): Promise<QueuedWrite | undefined>;
|
|
62
|
+
/**
|
|
63
|
+
* Item count and total bytes, for the caps and the pending badge.
|
|
64
|
+
*
|
|
65
|
+
* Read once at open; the queue keeps counters in memory afterwards rather
|
|
66
|
+
* than paying a store scan per write.
|
|
67
|
+
*/
|
|
68
|
+
usage(): Promise<{
|
|
69
|
+
count: number;
|
|
70
|
+
bytes: number;
|
|
71
|
+
}>;
|
|
72
|
+
/**
|
|
73
|
+
* Whether the outbox holds records this SDK does not understand.
|
|
74
|
+
*
|
|
75
|
+
* Such records are left untouched and stop delivery entirely: skipping past
|
|
76
|
+
* them would break FIFO, and dead-lettering them would destroy valid data
|
|
77
|
+
* written by a newer build.
|
|
78
|
+
*/
|
|
79
|
+
hasFutureRecords(): Promise<boolean>;
|
|
80
|
+
/** Move a write out of the outbox and into the dead-letter store atomically. */
|
|
81
|
+
deadLetter(entry: DeadLetteredWrite): Promise<void>;
|
|
82
|
+
listDeadLettered(): Promise<DeadLetteredWrite[]>;
|
|
83
|
+
countDeadLettered(): Promise<number>;
|
|
84
|
+
discardDeadLettered(localId: string): Promise<boolean>;
|
|
85
|
+
/**
|
|
86
|
+
* Move a dead-lettered write back onto the outbox in ONE transaction.
|
|
87
|
+
*
|
|
88
|
+
* Two transactions would leave both copies alive if the second failed, and the
|
|
89
|
+
* surviving dead letter could then be retried again — a duplicate that the
|
|
90
|
+
* engine only dedupes because the idempotency key is preserved.
|
|
91
|
+
*/
|
|
92
|
+
requeueDeadLettered(localId: string, enqueuedAt: number): Promise<QueuedWrite | undefined>;
|
|
93
|
+
/**
|
|
94
|
+
* Where one write currently lives, read from BOTH stores in ONE transaction.
|
|
95
|
+
*
|
|
96
|
+
* Two separate reads can interleave with `deadLetter()`, which moves a record
|
|
97
|
+
* between the stores atomically: read the dead-letter store (miss), the move
|
|
98
|
+
* commits, read the outbox (miss) — and the write reports as delivered when
|
|
99
|
+
* it was actually lost. A single readonly transaction spanning both stores
|
|
100
|
+
* cannot observe that intermediate state.
|
|
101
|
+
*
|
|
102
|
+
* Both requests are issued before the first await on purpose. Awaiting one
|
|
103
|
+
* before opening the other would let the transaction auto-close, putting the
|
|
104
|
+
* second read back in a transaction of its own and reinstating the race.
|
|
105
|
+
*/
|
|
106
|
+
statusOf(localId: string): Promise<{
|
|
107
|
+
kind: "queued";
|
|
108
|
+
} | {
|
|
109
|
+
kind: "dead";
|
|
110
|
+
entry: DeadLetteredWrite;
|
|
111
|
+
} | {
|
|
112
|
+
kind: "absent";
|
|
113
|
+
}>;
|
|
114
|
+
getDeadLettered(localId: string): Promise<DeadLetteredWrite | undefined>;
|
|
115
|
+
/**
|
|
116
|
+
* Trim the dead-letter store to its cap, oldest first.
|
|
117
|
+
*
|
|
118
|
+
* Drop-oldest is fine here, unlike the outbox: ordering does not matter for
|
|
119
|
+
* writes that are already never going to be sent.
|
|
120
|
+
*/
|
|
121
|
+
trimDeadLettered(max: number): Promise<void>;
|
|
122
|
+
}
|
|
123
|
+
//# sourceMappingURL=db.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"db.d.ts","sourceRoot":"","sources":["../../src/queue/db.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AACH,OAAO,EAEL,KAAK,iBAAiB,EACtB,KAAK,WAAW,EACjB,MAAM,YAAY,CAAC;AASpB,+DAA+D;AAC/D,MAAM,MAAM,cAAc,GAAG,IAAI,CAAC,WAAW,EAAE,KAAK,CAAC,CAAC;AAEtD;;;;;;GAMG;AACH,qBAAa,iBAAkB,SAAQ,KAAK;gBAC9B,OAAO,EAAE,MAAM;CAI5B;AAED,+EAA+E;AAC/E,qBAAa,eAAgB,SAAQ,KAAK;gBAC5B,OAAO,EAAE,MAAM;CAI5B;AA4BD,qBAAa,OAAO;IAEhB,OAAO,CAAC,QAAQ,CAAC,EAAE;IACnB,QAAQ,CAAC,IAAI,EAAE,MAAM;IAFvB,OAAO;IAKP;;;;OAIG;WACU,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAmDjD,KAAK,IAAI,IAAI;IAQb;;;;OAIG;IACG,MAAM,CAAC,KAAK,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC;IAgBpD;;;;;OAKG;IACG,IAAI,IAAI,OAAO,CAAC,WAAW,GAAG,SAAS,CAAC;IAMxC,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAMlC,aAAa,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,GAAG,SAAS,CAAC;IAQtE;;;;;OAKG;IACG,KAAK,IAAI,OAAO,CAAC;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC;IAWxD;;;;;;OAMG;IACG,gBAAgB,IAAI,OAAO,CAAC,OAAO,CAAC;IAY1C,gFAAgF;IAC1E,UAAU,CAAC,KAAK,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC;IAOnD,gBAAgB,IAAI,OAAO,CAAC,iBAAiB,EAAE,CAAC;IAQhD,iBAAiB,IAAI,OAAO,CAAC,MAAM,CAAC;IAKpC,mBAAmB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAa5D;;;;;;OAMG;IACG,mBAAmB,CACvB,OAAO,EAAE,MAAM,EACf,UAAU,EAAE,MAAM,GACjB,OAAO,CAAC,WAAW,GAAG,SAAS,CAAC;IAoBnC;;;;;;;;;;;;OAYG;IACG,QAAQ,CACZ,OAAO,EAAE,MAAM,GACd,OAAO,CACN;QAAE,IAAI,EAAE,QAAQ,CAAA;KAAE,GAClB;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,iBAAiB,CAAA;KAAE,GAC1C;QAAE,IAAI,EAAE,QAAQ,CAAA;KAAE,CACrB;IAaK,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,iBAAiB,GAAG,SAAS,CAAC;IAO9E;;;;;OAKG;IACG,gBAAgB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;CA4BnD"}
|