@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/queue/db.js
ADDED
|
@@ -0,0 +1,304 @@
|
|
|
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 { RECORD_VERSION, } from "./types.js";
|
|
20
|
+
/** Bumped only for a real schema migration; see {@link QueueDb.open}. */
|
|
21
|
+
const DB_VERSION = 1;
|
|
22
|
+
const OUTBOX = "outbox";
|
|
23
|
+
const DEADLETTER = "deadletter";
|
|
24
|
+
const LOCAL_ID_INDEX = "byLocalId";
|
|
25
|
+
/**
|
|
26
|
+
* Thrown when the stored database was created by a NEWER SDK.
|
|
27
|
+
*
|
|
28
|
+
* IndexedDB refuses to open a database at a lower version than the one on disk,
|
|
29
|
+
* which is exactly the signal we want: a rollback, or another tab on a newer
|
|
30
|
+
* build. The queue disables delivery rather than touching data it cannot read.
|
|
31
|
+
*/
|
|
32
|
+
export class QueueVersionError extends Error {
|
|
33
|
+
constructor(message) {
|
|
34
|
+
super(message);
|
|
35
|
+
this.name = "QueueVersionError";
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
/** Thrown when a write cannot be stored because the origin is out of quota. */
|
|
39
|
+
export class QueueQuotaError extends Error {
|
|
40
|
+
constructor(message) {
|
|
41
|
+
super(message);
|
|
42
|
+
this.name = "QueueQuotaError";
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
function isQuotaError(error) {
|
|
46
|
+
return (error instanceof DOMException &&
|
|
47
|
+
(error.name === "QuotaExceededError" ||
|
|
48
|
+
// Firefox's legacy spelling.
|
|
49
|
+
error.name === "NS_ERROR_DOM_QUOTA_REACHED"));
|
|
50
|
+
}
|
|
51
|
+
/** Promisify an IDBRequest. */
|
|
52
|
+
function promisify(request) {
|
|
53
|
+
return new Promise((resolve, reject) => {
|
|
54
|
+
request.onsuccess = () => resolve(request.result);
|
|
55
|
+
request.onerror = () => reject(request.error);
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
/** Resolve when a transaction commits, reject if it aborts or errors. */
|
|
59
|
+
function txDone(tx) {
|
|
60
|
+
return new Promise((resolve, reject) => {
|
|
61
|
+
tx.oncomplete = () => resolve();
|
|
62
|
+
tx.onabort = () => reject(tx.error);
|
|
63
|
+
tx.onerror = () => reject(tx.error);
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
export class QueueDb {
|
|
67
|
+
db;
|
|
68
|
+
name;
|
|
69
|
+
constructor(db, name) {
|
|
70
|
+
this.db = db;
|
|
71
|
+
this.name = name;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Open (creating if needed) the queue database.
|
|
75
|
+
*
|
|
76
|
+
* @throws {QueueVersionError} when the stored database is newer than this SDK.
|
|
77
|
+
*/
|
|
78
|
+
static async open(name) {
|
|
79
|
+
if (typeof indexedDB === "undefined") {
|
|
80
|
+
throw new Error("IndexedDB is unavailable in this environment");
|
|
81
|
+
}
|
|
82
|
+
const request = indexedDB.open(name, DB_VERSION);
|
|
83
|
+
request.onupgradeneeded = () => {
|
|
84
|
+
const db = request.result;
|
|
85
|
+
if (!db.objectStoreNames.contains(OUTBOX)) {
|
|
86
|
+
const outbox = db.createObjectStore(OUTBOX, {
|
|
87
|
+
keyPath: "seq",
|
|
88
|
+
autoIncrement: true,
|
|
89
|
+
});
|
|
90
|
+
// Lets watch()/discard() find a record without scanning the queue.
|
|
91
|
+
outbox.createIndex(LOCAL_ID_INDEX, "localId", { unique: true });
|
|
92
|
+
}
|
|
93
|
+
if (!db.objectStoreNames.contains(DEADLETTER)) {
|
|
94
|
+
db.createObjectStore(DEADLETTER, { keyPath: "write.localId" });
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
// A newer SDK cannot upgrade while an older tab holds the database open.
|
|
98
|
+
// Without this the upgrade blocks indefinitely and that tab never notices.
|
|
99
|
+
request.onblocked = () => {
|
|
100
|
+
// eslint-disable-next-line no-console
|
|
101
|
+
console.warn(`[ironflow] offline queue database "${name}" is blocked by another tab ` +
|
|
102
|
+
`holding an older version open. Close other tabs to let it upgrade.`);
|
|
103
|
+
};
|
|
104
|
+
try {
|
|
105
|
+
const db = await promisify(request);
|
|
106
|
+
// Yield the connection when another context needs to upgrade, rather than
|
|
107
|
+
// blocking it forever.
|
|
108
|
+
db.onversionchange = () => db.close();
|
|
109
|
+
return new QueueDb(db, name);
|
|
110
|
+
}
|
|
111
|
+
catch (error) {
|
|
112
|
+
if (error instanceof DOMException && error.name === "VersionError") {
|
|
113
|
+
throw new QueueVersionError(`Offline queue database "${name}" was written by a newer Ironflow SDK. ` +
|
|
114
|
+
`Delivery is paused so the existing writes are not destroyed; upgrade to drain them.`);
|
|
115
|
+
}
|
|
116
|
+
throw error;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
close() {
|
|
120
|
+
this.db.close();
|
|
121
|
+
}
|
|
122
|
+
// --------------------------------------------------------------------------
|
|
123
|
+
// Outbox
|
|
124
|
+
// --------------------------------------------------------------------------
|
|
125
|
+
/**
|
|
126
|
+
* Append a write and return the FIFO key IndexedDB assigned it.
|
|
127
|
+
*
|
|
128
|
+
* @throws {QueueQuotaError} when the origin is out of storage.
|
|
129
|
+
*/
|
|
130
|
+
async append(write) {
|
|
131
|
+
try {
|
|
132
|
+
const tx = this.db.transaction(OUTBOX, "readwrite");
|
|
133
|
+
const key = promisify(tx.objectStore(OUTBOX).add(write));
|
|
134
|
+
await txDone(tx);
|
|
135
|
+
return (await key);
|
|
136
|
+
}
|
|
137
|
+
catch (error) {
|
|
138
|
+
if (isQuotaError(error)) {
|
|
139
|
+
throw new QueueQuotaError("Browser storage quota exceeded; the write was not queued");
|
|
140
|
+
}
|
|
141
|
+
throw error;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* The next write to send, or `undefined` when the outbox is empty.
|
|
146
|
+
*
|
|
147
|
+
* Strict FIFO: always the lowest key. A write that fails stays here, which is
|
|
148
|
+
* what makes the queue blocking — item N+1 cannot pass item N.
|
|
149
|
+
*/
|
|
150
|
+
async head() {
|
|
151
|
+
const tx = this.db.transaction(OUTBOX, "readonly");
|
|
152
|
+
const cursor = await promisify(tx.objectStore(OUTBOX).openCursor());
|
|
153
|
+
return cursor?.value;
|
|
154
|
+
}
|
|
155
|
+
async remove(seq) {
|
|
156
|
+
const tx = this.db.transaction(OUTBOX, "readwrite");
|
|
157
|
+
tx.objectStore(OUTBOX).delete(seq);
|
|
158
|
+
await txDone(tx);
|
|
159
|
+
}
|
|
160
|
+
async findByLocalId(localId) {
|
|
161
|
+
const tx = this.db.transaction(OUTBOX, "readonly");
|
|
162
|
+
const found = await promisify(tx.objectStore(OUTBOX).index(LOCAL_ID_INDEX).get(localId));
|
|
163
|
+
return found;
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Item count and total bytes, for the caps and the pending badge.
|
|
167
|
+
*
|
|
168
|
+
* Read once at open; the queue keeps counters in memory afterwards rather
|
|
169
|
+
* than paying a store scan per write.
|
|
170
|
+
*/
|
|
171
|
+
async usage() {
|
|
172
|
+
const tx = this.db.transaction(OUTBOX, "readonly");
|
|
173
|
+
const all = (await promisify(tx.objectStore(OUTBOX).getAll()));
|
|
174
|
+
return {
|
|
175
|
+
count: all.length,
|
|
176
|
+
bytes: all.reduce((sum, w) => sum + w.bytes, 0),
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Whether the outbox holds records this SDK does not understand.
|
|
181
|
+
*
|
|
182
|
+
* Such records are left untouched and stop delivery entirely: skipping past
|
|
183
|
+
* them would break FIFO, and dead-lettering them would destroy valid data
|
|
184
|
+
* written by a newer build.
|
|
185
|
+
*/
|
|
186
|
+
async hasFutureRecords() {
|
|
187
|
+
const tx = this.db.transaction(OUTBOX, "readonly");
|
|
188
|
+
const all = (await promisify(tx.objectStore(OUTBOX).getAll()));
|
|
189
|
+
return all.some((w) => w.v > RECORD_VERSION);
|
|
190
|
+
}
|
|
191
|
+
// --------------------------------------------------------------------------
|
|
192
|
+
// Dead letter
|
|
193
|
+
// --------------------------------------------------------------------------
|
|
194
|
+
/** Move a write out of the outbox and into the dead-letter store atomically. */
|
|
195
|
+
async deadLetter(entry) {
|
|
196
|
+
const tx = this.db.transaction([OUTBOX, DEADLETTER], "readwrite");
|
|
197
|
+
tx.objectStore(OUTBOX).delete(entry.write.seq);
|
|
198
|
+
tx.objectStore(DEADLETTER).put(entry);
|
|
199
|
+
await txDone(tx);
|
|
200
|
+
}
|
|
201
|
+
async listDeadLettered() {
|
|
202
|
+
const tx = this.db.transaction(DEADLETTER, "readonly");
|
|
203
|
+
const all = (await promisify(tx.objectStore(DEADLETTER).getAll()));
|
|
204
|
+
return all.sort((a, b) => a.deadLetteredAt - b.deadLetteredAt);
|
|
205
|
+
}
|
|
206
|
+
async countDeadLettered() {
|
|
207
|
+
const tx = this.db.transaction(DEADLETTER, "readonly");
|
|
208
|
+
return promisify(tx.objectStore(DEADLETTER).count());
|
|
209
|
+
}
|
|
210
|
+
async discardDeadLettered(localId) {
|
|
211
|
+
const tx = this.db.transaction(DEADLETTER, "readwrite");
|
|
212
|
+
const store = tx.objectStore(DEADLETTER);
|
|
213
|
+
const existing = await promisify(store.get(localId));
|
|
214
|
+
if (existing === undefined) {
|
|
215
|
+
await txDone(tx);
|
|
216
|
+
return false;
|
|
217
|
+
}
|
|
218
|
+
store.delete(localId);
|
|
219
|
+
await txDone(tx);
|
|
220
|
+
return true;
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Move a dead-lettered write back onto the outbox in ONE transaction.
|
|
224
|
+
*
|
|
225
|
+
* Two transactions would leave both copies alive if the second failed, and the
|
|
226
|
+
* surviving dead letter could then be retried again — a duplicate that the
|
|
227
|
+
* engine only dedupes because the idempotency key is preserved.
|
|
228
|
+
*/
|
|
229
|
+
async requeueDeadLettered(localId, enqueuedAt) {
|
|
230
|
+
const tx = this.db.transaction([OUTBOX, DEADLETTER], "readwrite");
|
|
231
|
+
const store = tx.objectStore(DEADLETTER);
|
|
232
|
+
const entry = (await promisify(store.get(localId)));
|
|
233
|
+
if (!entry) {
|
|
234
|
+
await txDone(tx);
|
|
235
|
+
return undefined;
|
|
236
|
+
}
|
|
237
|
+
const { seq: _seq, ...rest } = entry.write;
|
|
238
|
+
tx.objectStore(OUTBOX).add({ ...rest, enqueuedAt });
|
|
239
|
+
store.delete(localId);
|
|
240
|
+
await txDone(tx);
|
|
241
|
+
return entry.write;
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* Where one write currently lives, read from BOTH stores in ONE transaction.
|
|
245
|
+
*
|
|
246
|
+
* Two separate reads can interleave with `deadLetter()`, which moves a record
|
|
247
|
+
* between the stores atomically: read the dead-letter store (miss), the move
|
|
248
|
+
* commits, read the outbox (miss) — and the write reports as delivered when
|
|
249
|
+
* it was actually lost. A single readonly transaction spanning both stores
|
|
250
|
+
* cannot observe that intermediate state.
|
|
251
|
+
*
|
|
252
|
+
* Both requests are issued before the first await on purpose. Awaiting one
|
|
253
|
+
* before opening the other would let the transaction auto-close, putting the
|
|
254
|
+
* second read back in a transaction of its own and reinstating the race.
|
|
255
|
+
*/
|
|
256
|
+
async statusOf(localId) {
|
|
257
|
+
const tx = this.db.transaction([OUTBOX, DEADLETTER], "readonly");
|
|
258
|
+
const deadReq = promisify(tx.objectStore(DEADLETTER).get(localId));
|
|
259
|
+
const queuedReq = promisify(tx.objectStore(OUTBOX).index(LOCAL_ID_INDEX).get(localId));
|
|
260
|
+
const [dead, queued] = await Promise.all([deadReq, queuedReq]);
|
|
261
|
+
if (dead)
|
|
262
|
+
return { kind: "dead", entry: dead };
|
|
263
|
+
if (queued)
|
|
264
|
+
return { kind: "queued" };
|
|
265
|
+
return { kind: "absent" };
|
|
266
|
+
}
|
|
267
|
+
async getDeadLettered(localId) {
|
|
268
|
+
const tx = this.db.transaction(DEADLETTER, "readonly");
|
|
269
|
+
return (await promisify(tx.objectStore(DEADLETTER).get(localId)));
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* Trim the dead-letter store to its cap, oldest first.
|
|
273
|
+
*
|
|
274
|
+
* Drop-oldest is fine here, unlike the outbox: ordering does not matter for
|
|
275
|
+
* writes that are already never going to be sent.
|
|
276
|
+
*/
|
|
277
|
+
async trimDeadLettered(max) {
|
|
278
|
+
const all = await this.listDeadLettered();
|
|
279
|
+
if (all.length <= max)
|
|
280
|
+
return;
|
|
281
|
+
// Never evict a record written by a NEWER SDK: this build cannot read it,
|
|
282
|
+
// so deleting it destroys data that a rollback would have delivered fine.
|
|
283
|
+
// The cap is still measured against the WHOLE store — counting only the
|
|
284
|
+
// evictable subset would keep `max` evictable records plus every future
|
|
285
|
+
// one, which is no upper bound at all once a future record exists.
|
|
286
|
+
const evictable = all.filter((e) => e.write.v <= RECORD_VERSION);
|
|
287
|
+
const excess = evictable.slice(0, Math.min(all.length - max, evictable.length));
|
|
288
|
+
if (all.length - excess.length > max) {
|
|
289
|
+
// eslint-disable-next-line no-console
|
|
290
|
+
console.warn(`[ironflow] dead-letter store holds ${all.length - excess.length} records, ` +
|
|
291
|
+
`above the cap of ${max}: the excess was written by a newer SDK and ` +
|
|
292
|
+
`cannot be evicted. Upgrade to drain it.`);
|
|
293
|
+
}
|
|
294
|
+
if (excess.length === 0)
|
|
295
|
+
return;
|
|
296
|
+
const tx = this.db.transaction(DEADLETTER, "readwrite");
|
|
297
|
+
const store = tx.objectStore(DEADLETTER);
|
|
298
|
+
for (const entry of excess) {
|
|
299
|
+
store.delete(entry.write.localId);
|
|
300
|
+
}
|
|
301
|
+
await txDone(tx);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
//# sourceMappingURL=db.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"db.js","sourceRoot":"","sources":["../../src/queue/db.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Identifier generation for queued writes (ADR 0052).
|
|
3
|
+
*
|
|
4
|
+
* Every queued write carries an idempotency key generated at ENQUEUE time, not
|
|
5
|
+
* at send time. That ordering is the whole trick: a retry after a page reload
|
|
6
|
+
* reuses the same key, so the engine's dedup lookup recognises it and returns
|
|
7
|
+
* the original event instead of creating a second one.
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* A random UUID v4.
|
|
11
|
+
*
|
|
12
|
+
* `crypto.randomUUID()` is the obvious implementation and is used when present,
|
|
13
|
+
* but it lands in Safari 15.4 while this package documents a Safari 13.1
|
|
14
|
+
* baseline (README "Browser Compatibility"). `crypto.getRandomValues()` goes
|
|
15
|
+
* back much further, so the fallback keeps the baseline honest rather than
|
|
16
|
+
* quietly raising it.
|
|
17
|
+
*
|
|
18
|
+
* Both branches are cryptographically random; the fallback only differs in
|
|
19
|
+
* doing the RFC 4122 bit-twiddling and hex formatting by hand.
|
|
20
|
+
*/
|
|
21
|
+
export declare function newUuid(): string;
|
|
22
|
+
//# sourceMappingURL=ids.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ids.d.ts","sourceRoot":"","sources":["../../src/queue/ids.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH;;;;;;;;;;;GAWG;AACH,wBAAgB,OAAO,IAAI,MAAM,CA8BhC"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Identifier generation for queued writes (ADR 0052).
|
|
3
|
+
*
|
|
4
|
+
* Every queued write carries an idempotency key generated at ENQUEUE time, not
|
|
5
|
+
* at send time. That ordering is the whole trick: a retry after a page reload
|
|
6
|
+
* reuses the same key, so the engine's dedup lookup recognises it and returns
|
|
7
|
+
* the original event instead of creating a second one.
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* A random UUID v4.
|
|
11
|
+
*
|
|
12
|
+
* `crypto.randomUUID()` is the obvious implementation and is used when present,
|
|
13
|
+
* but it lands in Safari 15.4 while this package documents a Safari 13.1
|
|
14
|
+
* baseline (README "Browser Compatibility"). `crypto.getRandomValues()` goes
|
|
15
|
+
* back much further, so the fallback keeps the baseline honest rather than
|
|
16
|
+
* quietly raising it.
|
|
17
|
+
*
|
|
18
|
+
* Both branches are cryptographically random; the fallback only differs in
|
|
19
|
+
* doing the RFC 4122 bit-twiddling and hex formatting by hand.
|
|
20
|
+
*/
|
|
21
|
+
export function newUuid() {
|
|
22
|
+
const c = globalThis.crypto;
|
|
23
|
+
if (typeof c?.randomUUID === "function") {
|
|
24
|
+
return c.randomUUID();
|
|
25
|
+
}
|
|
26
|
+
if (typeof c?.getRandomValues !== "function") {
|
|
27
|
+
throw new Error("Ironflow offline queue requires Web Crypto (crypto.randomUUID or crypto.getRandomValues)");
|
|
28
|
+
}
|
|
29
|
+
const bytes = c.getRandomValues(new Uint8Array(16));
|
|
30
|
+
// Version 4 (random) and RFC 4122 variant, per §4.4.
|
|
31
|
+
bytes[6] = (bytes[6] & 0x0f) | 0x40;
|
|
32
|
+
bytes[8] = (bytes[8] & 0x3f) | 0x80;
|
|
33
|
+
const hex = [];
|
|
34
|
+
for (const b of bytes) {
|
|
35
|
+
hex.push(b.toString(16).padStart(2, "0"));
|
|
36
|
+
}
|
|
37
|
+
return [
|
|
38
|
+
hex.slice(0, 4).join(""),
|
|
39
|
+
hex.slice(4, 6).join(""),
|
|
40
|
+
hex.slice(6, 8).join(""),
|
|
41
|
+
hex.slice(8, 10).join(""),
|
|
42
|
+
hex.slice(10, 16).join(""),
|
|
43
|
+
].join("-");
|
|
44
|
+
}
|
|
45
|
+
//# sourceMappingURL=ids.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ids.js","sourceRoot":"","sources":["../../src/queue/ids.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,OAAO;IACrB,MAAM,CAAC,GAAG,UAAU,CAAC,MAAM,CAAC;IAE5B,IAAI,OAAO,CAAC,EAAE,UAAU,KAAK,UAAU,EAAE,CAAC;QACxC,OAAO,CAAC,CAAC,UAAU,EAAE,CAAC;IACxB,CAAC;IAED,IAAI,OAAO,CAAC,EAAE,eAAe,KAAK,UAAU,EAAE,CAAC;QAC7C,MAAM,IAAI,KAAK,CACb,0FAA0F,CAC3F,CAAC;IACJ,CAAC;IAED,MAAM,KAAK,GAAG,CAAC,CAAC,eAAe,CAAC,IAAI,UAAU,CAAC,EAAE,CAAC,CAAC,CAAC;IACpD,qDAAqD;IACrD,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,CAAE,GAAG,IAAI,CAAC,GAAG,IAAI,CAAC;IACrC,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,CAAE,GAAG,IAAI,CAAC,GAAG,IAAI,CAAC;IAErC,MAAM,GAAG,GAAa,EAAE,CAAC;IACzB,KAAK,MAAM,CAAC,IAAI,KAAK,EAAE,CAAC;QACtB,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC;IAC5C,CAAC;IAED,OAAO;QACL,GAAG,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC;QACxB,GAAG,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC;QACxB,GAAG,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC;QACxB,GAAG,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC;QACzB,GAAG,CAAC,KAAK,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC;KAC3B,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AACd,CAAC"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cross-tab flush coordination for the offline write queue (ADR 0052).
|
|
3
|
+
*
|
|
4
|
+
* IndexedDB is shared per-origin, so every open tab sees the same outbox. Two
|
|
5
|
+
* tabs draining at once stay *correct* — the engine deduplicates by idempotency
|
|
6
|
+
* key — but strict FIFO does not survive it, and FIFO is the property the whole
|
|
7
|
+
* design is built around. One drainer at a time is the fix.
|
|
8
|
+
*
|
|
9
|
+
* The Web Locks API does this natively and, crucially, releases the lock when a
|
|
10
|
+
* tab crashes. A hand-rolled lease row in IndexedDB would need its own expiry
|
|
11
|
+
* logic and would get it wrong.
|
|
12
|
+
*
|
|
13
|
+
* Everything routes through `withFlushLock` so the no-locks fallback is a real
|
|
14
|
+
* code path rather than a dead branch: `navigator.locks` is Safari 16+, and the
|
|
15
|
+
* package supports 13.1+. The test environment deliberately leaves the global
|
|
16
|
+
* undefined, so the default test run exercises the fallback.
|
|
17
|
+
*/
|
|
18
|
+
/**
|
|
19
|
+
* Returned when another context already holds the lock.
|
|
20
|
+
*
|
|
21
|
+
* A busy lock is not an error — it means another tab is draining, which is
|
|
22
|
+
* exactly what we wanted. The caller skips this cycle and tries later.
|
|
23
|
+
*/
|
|
24
|
+
export declare const LOCK_BUSY: unique symbol;
|
|
25
|
+
/** Whether this environment can coordinate drains across tabs. */
|
|
26
|
+
export declare function hasWebLocks(): boolean;
|
|
27
|
+
/**
|
|
28
|
+
* Run `fn` while holding the named lock, or return {@link LOCK_BUSY} if another
|
|
29
|
+
* context holds it.
|
|
30
|
+
*
|
|
31
|
+
* `ifAvailable` rather than waiting: a queued drain that fires the moment the
|
|
32
|
+
* other tab finishes would just re-drain an empty queue. Skipping and retrying
|
|
33
|
+
* on the next cycle is both simpler and cheaper.
|
|
34
|
+
*
|
|
35
|
+
* The lock is held only for the duration of `fn`. That is what lets the drainer
|
|
36
|
+
* release it while paused on re-authentication — it returns from `fn` instead of
|
|
37
|
+
* awaiting inside it. Holding a lock across an unresolved `onAuthRequired` (a
|
|
38
|
+
* user who walked away from a login prompt) would block every other tab
|
|
39
|
+
* indefinitely.
|
|
40
|
+
*/
|
|
41
|
+
export declare function withFlushLock<T>(name: string, fn: () => Promise<T>): Promise<T | typeof LOCK_BUSY>;
|
|
42
|
+
//# sourceMappingURL=lock.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lock.d.ts","sourceRoot":"","sources":["../../src/queue/lock.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH;;;;;GAKG;AACH,eAAO,MAAM,SAAS,eAAqC,CAAC;AAgB5D,kEAAkE;AAClE,wBAAgB,WAAW,IAAI,OAAO,CAErC;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAsB,aAAa,CAAC,CAAC,EACnC,IAAI,EAAE,MAAM,EACZ,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GACnB,OAAO,CAAC,CAAC,GAAG,OAAO,SAAS,CAAC,CAmB/B"}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cross-tab flush coordination for the offline write queue (ADR 0052).
|
|
3
|
+
*
|
|
4
|
+
* IndexedDB is shared per-origin, so every open tab sees the same outbox. Two
|
|
5
|
+
* tabs draining at once stay *correct* — the engine deduplicates by idempotency
|
|
6
|
+
* key — but strict FIFO does not survive it, and FIFO is the property the whole
|
|
7
|
+
* design is built around. One drainer at a time is the fix.
|
|
8
|
+
*
|
|
9
|
+
* The Web Locks API does this natively and, crucially, releases the lock when a
|
|
10
|
+
* tab crashes. A hand-rolled lease row in IndexedDB would need its own expiry
|
|
11
|
+
* logic and would get it wrong.
|
|
12
|
+
*
|
|
13
|
+
* Everything routes through `withFlushLock` so the no-locks fallback is a real
|
|
14
|
+
* code path rather than a dead branch: `navigator.locks` is Safari 16+, and the
|
|
15
|
+
* package supports 13.1+. The test environment deliberately leaves the global
|
|
16
|
+
* undefined, so the default test run exercises the fallback.
|
|
17
|
+
*/
|
|
18
|
+
/**
|
|
19
|
+
* Returned when another context already holds the lock.
|
|
20
|
+
*
|
|
21
|
+
* A busy lock is not an error — it means another tab is draining, which is
|
|
22
|
+
* exactly what we wanted. The caller skips this cycle and tries later.
|
|
23
|
+
*/
|
|
24
|
+
export const LOCK_BUSY = Symbol("ironflow.queue.lock-busy");
|
|
25
|
+
function lockManager() {
|
|
26
|
+
const locks = globalThis.navigator
|
|
27
|
+
?.locks;
|
|
28
|
+
return typeof locks?.request === "function" ? locks : undefined;
|
|
29
|
+
}
|
|
30
|
+
/** Whether this environment can coordinate drains across tabs. */
|
|
31
|
+
export function hasWebLocks() {
|
|
32
|
+
return lockManager() !== undefined;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Run `fn` while holding the named lock, or return {@link LOCK_BUSY} if another
|
|
36
|
+
* context holds it.
|
|
37
|
+
*
|
|
38
|
+
* `ifAvailable` rather than waiting: a queued drain that fires the moment the
|
|
39
|
+
* other tab finishes would just re-drain an empty queue. Skipping and retrying
|
|
40
|
+
* on the next cycle is both simpler and cheaper.
|
|
41
|
+
*
|
|
42
|
+
* The lock is held only for the duration of `fn`. That is what lets the drainer
|
|
43
|
+
* release it while paused on re-authentication — it returns from `fn` instead of
|
|
44
|
+
* awaiting inside it. Holding a lock across an unresolved `onAuthRequired` (a
|
|
45
|
+
* user who walked away from a login prompt) would block every other tab
|
|
46
|
+
* indefinitely.
|
|
47
|
+
*/
|
|
48
|
+
export async function withFlushLock(name, fn) {
|
|
49
|
+
const locks = lockManager();
|
|
50
|
+
// No Web Locks (Safari < 16, SSR, older engines): this tab is the only
|
|
51
|
+
// drainer it can know about. Single-tab correctness is preserved; concurrent
|
|
52
|
+
// tabs fall back to dedup-keeps-it-correct, order-may-interleave.
|
|
53
|
+
if (!locks) {
|
|
54
|
+
return fn();
|
|
55
|
+
}
|
|
56
|
+
let result = LOCK_BUSY;
|
|
57
|
+
await locks.request(name, { ifAvailable: true, mode: "exclusive" }, async (lock) => {
|
|
58
|
+
// A null lock means `ifAvailable` declined it — someone else is draining.
|
|
59
|
+
if (lock === null)
|
|
60
|
+
return;
|
|
61
|
+
result = await fn();
|
|
62
|
+
});
|
|
63
|
+
return result;
|
|
64
|
+
}
|
|
65
|
+
//# sourceMappingURL=lock.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lock.js","sourceRoot":"","sources":["../../src/queue/lock.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH;;;;;GAKG;AACH,MAAM,CAAC,MAAM,SAAS,GAAG,MAAM,CAAC,0BAA0B,CAAC,CAAC;AAU5D,SAAS,WAAW;IAClB,MAAM,KAAK,GAAI,UAAU,CAAC,SAAqD;QAC7E,EAAE,KAAK,CAAC;IACV,OAAO,OAAO,KAAK,EAAE,OAAO,KAAK,UAAU,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,SAAS,CAAC;AAClE,CAAC;AAED,kEAAkE;AAClE,MAAM,UAAU,WAAW;IACzB,OAAO,WAAW,EAAE,KAAK,SAAS,CAAC;AACrC,CAAC;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,CAAC,KAAK,UAAU,aAAa,CACjC,IAAY,EACZ,EAAoB;IAEpB,MAAM,KAAK,GAAG,WAAW,EAAE,CAAC;IAE5B,uEAAuE;IACvE,6EAA6E;IAC7E,kEAAkE;IAClE,IAAI,CAAC,KAAK,EAAE,CAAC;QACX,OAAO,EAAE,EAAE,CAAC;IACd,CAAC;IAED,IAAI,MAAM,GAAyB,SAAS,CAAC;IAE7C,MAAM,KAAK,CAAC,OAAO,CAAC,IAAI,EAAE,EAAE,WAAW,EAAE,IAAI,EAAE,IAAI,EAAE,WAAW,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,EAAE;QACjF,0EAA0E;QAC1E,IAAI,IAAI,KAAK,IAAI;YAAE,OAAO;QAC1B,MAAM,GAAG,MAAM,EAAE,EAAE,CAAC;IACtB,CAAC,CAAC,CAAC;IAEH,OAAO,MAAM,CAAC;AAChB,CAAC"}
|