@vibes.diy/vibe-runtime 9.4.3 → 10.0.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/firefly-database.d.ts +0 -61
- package/firefly-database.js +25 -441
- package/firefly-database.js.map +1 -1
- package/indexeddb-store.d.ts +0 -1
- package/indexeddb-store.js +16 -5
- package/indexeddb-store.js.map +1 -1
- package/package.json +3 -3
- package/register-dependencies.js +1 -1
- package/register-dependencies.js.map +1 -1
- package/safe-local-storage.d.ts +1 -0
- package/safe-local-storage.js +14 -0
- package/safe-local-storage.js.map +1 -0
- package/use-firefly.d.ts +0 -50
- package/use-firefly.js +0 -521
- package/use-firefly.js.map +1 -1
- package/use-fireproof-local.d.ts +1 -2
- package/use-fireproof-local.js.map +1 -1
- package/viewer-identity-cache.d.ts +12 -0
- package/viewer-identity-cache.js +87 -0
- package/viewer-identity-cache.js.map +1 -0
- package/localfly.d.ts +0 -79
- package/localfly.js +0 -226
- package/localfly.js.map +0 -1
- package/read-cache.d.ts +0 -26
- package/read-cache.js +0 -230
- package/read-cache.js.map +0 -1
package/firefly-database.d.ts
CHANGED
|
@@ -36,18 +36,6 @@ export interface ChangeMeta {
|
|
|
36
36
|
readonly ephemeral?: boolean;
|
|
37
37
|
}
|
|
38
38
|
export type ListenerFn<T extends DocTypes = DocTypes> = (changes: DocWithId<T>[], meta?: ChangeMeta) => void;
|
|
39
|
-
export interface OfflineQueueStore {
|
|
40
|
-
put(doc: Record<string, unknown> & {
|
|
41
|
-
_id?: string;
|
|
42
|
-
}): Promise<DocResponse>;
|
|
43
|
-
del(id: string): Promise<DocResponse>;
|
|
44
|
-
snapshot<T extends DocTypes = DocTypes>(): DocWithId<T>[];
|
|
45
|
-
}
|
|
46
|
-
export interface AnonLedgerSource extends OfflineQueueStore {
|
|
47
|
-
clear(): void;
|
|
48
|
-
readonly size: number;
|
|
49
|
-
}
|
|
50
|
-
export type DrainMigrateFn = (doc: DocWithId, userHandle: string) => Record<string, unknown> | null | undefined | false;
|
|
51
39
|
export interface IndexRow<T extends DocTypes = DocTypes> {
|
|
52
40
|
key: string;
|
|
53
41
|
value: DocWithId<T>;
|
|
@@ -114,46 +102,16 @@ export declare class FireflyDatabase {
|
|
|
114
102
|
private readonly vibeApp;
|
|
115
103
|
private readonly listeners;
|
|
116
104
|
private readonly updateListeners;
|
|
117
|
-
private optimisticEnabled;
|
|
118
|
-
private readonly overlay;
|
|
119
|
-
private writeSeq;
|
|
120
|
-
private offlineQueue;
|
|
121
|
-
private anonFallback;
|
|
122
|
-
private queueGeneration;
|
|
123
|
-
private activeIdentity;
|
|
124
|
-
private readonly queueTokens;
|
|
125
|
-
private readonly liveWriteTokens;
|
|
126
|
-
private draining;
|
|
127
|
-
private drainTimer;
|
|
128
|
-
private drainBackoff;
|
|
129
|
-
private onlineHandler;
|
|
130
|
-
private readonly idChains;
|
|
131
105
|
private readonly ephemeralOverlay;
|
|
132
106
|
private readonly peerDocs;
|
|
133
107
|
private ephemeralSeq;
|
|
134
108
|
private ephemeralSweep;
|
|
135
109
|
private grantedChannels;
|
|
136
110
|
seedDocs?: DocWithId<DocTypes>[];
|
|
137
|
-
private retrySleep;
|
|
138
111
|
constructor(name: string, vibeApi: FireflyTransport, acl?: DbAcl);
|
|
139
112
|
resubscribe(): void;
|
|
140
113
|
broadcastEphemeral(docId: string, doc: Record<string, unknown>): void;
|
|
141
114
|
applyAcl(acl: DbAcl): void;
|
|
142
|
-
setOptimistic(enabled: boolean): void;
|
|
143
|
-
setOfflineQueue(store: OfflineQueueStore | undefined): void;
|
|
144
|
-
setAnonFallback(enabled: boolean): void;
|
|
145
|
-
setActiveIdentity(handle: string | undefined): void;
|
|
146
|
-
setRetrySleep(fn: (ms: number) => Promise<void>): void;
|
|
147
|
-
handoffAnonymousLedger(args: {
|
|
148
|
-
anonLedger: AnonLedgerSource;
|
|
149
|
-
handleLedger: OfflineQueueStore;
|
|
150
|
-
userHandle: string;
|
|
151
|
-
migrate?: DrainMigrateFn;
|
|
152
|
-
}): Promise<{
|
|
153
|
-
migrated: number;
|
|
154
|
-
dropped: number;
|
|
155
|
-
}>;
|
|
156
|
-
private foldQueueIntoOverlay;
|
|
157
115
|
ready(): Promise<void>;
|
|
158
116
|
close(): Promise<void>;
|
|
159
117
|
destroy(): Promise<void>;
|
|
@@ -163,23 +121,6 @@ export declare class FireflyDatabase {
|
|
|
163
121
|
_id?: string;
|
|
164
122
|
}): Promise<DocResponse>;
|
|
165
123
|
del(id: string): Promise<DocResponse>;
|
|
166
|
-
private clearOverlay;
|
|
167
|
-
private clearLiveWriteToken;
|
|
168
|
-
private rollback;
|
|
169
|
-
private classifyWriteError;
|
|
170
|
-
private retryNetworkWrite;
|
|
171
|
-
private canQueue;
|
|
172
|
-
private queueWrite;
|
|
173
|
-
private dropQueued;
|
|
174
|
-
private drainAfterSuccess;
|
|
175
|
-
private issueWrite;
|
|
176
|
-
private runExclusive;
|
|
177
|
-
private scheduleDrain;
|
|
178
|
-
private scheduleDrainBackoff;
|
|
179
|
-
private drain;
|
|
180
|
-
private settleDrained;
|
|
181
|
-
private installOnlineListener;
|
|
182
|
-
private removeOnlineListener;
|
|
183
124
|
remove(id: string): Promise<DocResponse>;
|
|
184
125
|
bulk<T extends DocTypes>(docs: (T & {
|
|
185
126
|
_id?: string;
|
|
@@ -192,10 +133,8 @@ export declare class FireflyDatabase {
|
|
|
192
133
|
serverDocs: DocWithId<T>[];
|
|
193
134
|
}>;
|
|
194
135
|
materializeLive<T extends DocTypes>(serverDocs: DocWithId<T>[], mapFn: MapFnArg<T>, opts?: QueryOpts): QueryResponse<T>;
|
|
195
|
-
private foldOverlays;
|
|
196
136
|
private queryHint;
|
|
197
137
|
private fetchServerDocs;
|
|
198
|
-
private applyOverlay;
|
|
199
138
|
allDocs<T extends DocTypes>(opts?: {
|
|
200
139
|
limit?: number;
|
|
201
140
|
offset?: number;
|
package/firefly-database.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { exception2Result, Result } from "@adviser/cement";
|
|
2
1
|
import { isResPutDoc, isResGetDoc, isResQueryDocs, isResDeleteDoc, isEvtDocChanged, isEvtDocEphemeral, isEvtDocEphemeralDrop, } from "@vibes.diy/vibe-types";
|
|
3
2
|
import { decorateFiles } from "./firefly-files-read.js";
|
|
4
3
|
import { uploadFiles } from "./firefly-files-write.js";
|
|
@@ -43,21 +42,6 @@ export function newOptimisticId(now = Date.now()) {
|
|
|
43
42
|
const hex = randomHex(10);
|
|
44
43
|
return `${t.slice(0, 8)}-${t.slice(8)}-7${hex.slice(0, 3)}-${hex.slice(3, 7)}-${hex.slice(7, 19)}`;
|
|
45
44
|
}
|
|
46
|
-
const NETWORK_RETRY_LIMIT = 4;
|
|
47
|
-
function hasRawFiles(doc) {
|
|
48
|
-
const files = doc._files;
|
|
49
|
-
if (files === null || typeof files !== "object")
|
|
50
|
-
return false;
|
|
51
|
-
const hasFile = typeof File !== "undefined";
|
|
52
|
-
const hasBlob = typeof Blob !== "undefined";
|
|
53
|
-
for (const v of Object.values(files)) {
|
|
54
|
-
if (hasFile && v instanceof File)
|
|
55
|
-
return true;
|
|
56
|
-
if (hasBlob && v instanceof Blob)
|
|
57
|
-
return true;
|
|
58
|
-
}
|
|
59
|
-
return false;
|
|
60
|
-
}
|
|
61
45
|
export function materializeQuery(allDocs, mapFn, opts) {
|
|
62
46
|
let encodedRows;
|
|
63
47
|
if (typeof mapFn === "string") {
|
|
@@ -184,30 +168,12 @@ export class FireflyDatabase {
|
|
|
184
168
|
vibeApp;
|
|
185
169
|
listeners = new Set();
|
|
186
170
|
updateListeners = new Set();
|
|
187
|
-
optimisticEnabled = false;
|
|
188
|
-
overlay = new Map();
|
|
189
|
-
writeSeq = 0;
|
|
190
|
-
offlineQueue;
|
|
191
|
-
anonFallback = false;
|
|
192
|
-
queueGeneration = 0;
|
|
193
|
-
activeIdentity = undefined;
|
|
194
|
-
queueTokens = new Map();
|
|
195
|
-
liveWriteTokens = new Map();
|
|
196
|
-
draining = false;
|
|
197
|
-
drainTimer;
|
|
198
|
-
drainBackoff = 0;
|
|
199
|
-
onlineHandler;
|
|
200
|
-
idChains = new Map();
|
|
201
171
|
ephemeralOverlay = new Map();
|
|
202
172
|
peerDocs = new Map();
|
|
203
173
|
ephemeralSeq = 0;
|
|
204
174
|
ephemeralSweep;
|
|
205
175
|
grantedChannels;
|
|
206
176
|
seedDocs;
|
|
207
|
-
retrySleep = (ms) => new Promise((resolve) => {
|
|
208
|
-
const t = setTimeout(resolve, ms);
|
|
209
|
-
t.unref?.();
|
|
210
|
-
});
|
|
211
177
|
constructor(name, vibeApi, acl) {
|
|
212
178
|
this.name = name;
|
|
213
179
|
this.vibeApi = vibeApi;
|
|
@@ -263,86 +229,6 @@ export class FireflyDatabase {
|
|
|
263
229
|
}
|
|
264
230
|
});
|
|
265
231
|
}
|
|
266
|
-
setOptimistic(enabled) {
|
|
267
|
-
this.optimisticEnabled = enabled;
|
|
268
|
-
if (!enabled)
|
|
269
|
-
this.overlay.clear();
|
|
270
|
-
}
|
|
271
|
-
setOfflineQueue(store) {
|
|
272
|
-
if (store === this.offlineQueue)
|
|
273
|
-
return;
|
|
274
|
-
this.queueGeneration++;
|
|
275
|
-
for (const [id, token] of this.queueTokens)
|
|
276
|
-
this.rollback(id, token);
|
|
277
|
-
this.queueTokens.clear();
|
|
278
|
-
if (this.drainTimer !== undefined) {
|
|
279
|
-
clearTimeout(this.drainTimer);
|
|
280
|
-
this.drainTimer = undefined;
|
|
281
|
-
}
|
|
282
|
-
this.offlineQueue = store;
|
|
283
|
-
if (store !== undefined) {
|
|
284
|
-
this.foldQueueIntoOverlay(store);
|
|
285
|
-
this.installOnlineListener();
|
|
286
|
-
if (store.snapshot().length > 0)
|
|
287
|
-
this.scheduleDrain();
|
|
288
|
-
}
|
|
289
|
-
else {
|
|
290
|
-
this.removeOnlineListener();
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
setAnonFallback(enabled) {
|
|
294
|
-
this.anonFallback = enabled;
|
|
295
|
-
}
|
|
296
|
-
setActiveIdentity(handle) {
|
|
297
|
-
if (this.activeIdentity === handle)
|
|
298
|
-
return;
|
|
299
|
-
this.activeIdentity = handle;
|
|
300
|
-
this.queueGeneration++;
|
|
301
|
-
}
|
|
302
|
-
setRetrySleep(fn) {
|
|
303
|
-
this.retrySleep = fn;
|
|
304
|
-
}
|
|
305
|
-
async handoffAnonymousLedger(args) {
|
|
306
|
-
const { anonLedger, handleLedger, userHandle, migrate } = args;
|
|
307
|
-
const snap = anonLedger.snapshot();
|
|
308
|
-
let migrated = 0;
|
|
309
|
-
let dropped = 0;
|
|
310
|
-
for (const doc of snap) {
|
|
311
|
-
const isDel = doc._deleted === true;
|
|
312
|
-
const out = isDel || migrate === undefined ? doc : migrate(doc, userHandle);
|
|
313
|
-
if (out === false || out === null || out === undefined) {
|
|
314
|
-
dropped++;
|
|
315
|
-
continue;
|
|
316
|
-
}
|
|
317
|
-
await handleLedger.put({ ...out, _id: doc._id });
|
|
318
|
-
migrated++;
|
|
319
|
-
}
|
|
320
|
-
this.anonFallback = false;
|
|
321
|
-
this.setOfflineQueue(handleLedger);
|
|
322
|
-
anonLedger.clear();
|
|
323
|
-
return { migrated, dropped };
|
|
324
|
-
}
|
|
325
|
-
foldQueueIntoOverlay(store) {
|
|
326
|
-
const snap = store.snapshot();
|
|
327
|
-
if (snap.length === 0)
|
|
328
|
-
return;
|
|
329
|
-
const affected = [];
|
|
330
|
-
for (const doc of snap) {
|
|
331
|
-
const id = doc._id;
|
|
332
|
-
const token = ++this.writeSeq;
|
|
333
|
-
this.queueTokens.set(id, token);
|
|
334
|
-
if (doc._deleted === true) {
|
|
335
|
-
this.overlay.set(id, { kind: "del", token });
|
|
336
|
-
affected.push({ _id: id, _deleted: true });
|
|
337
|
-
}
|
|
338
|
-
else {
|
|
339
|
-
const optimisticDoc = decorateFiles({ ...doc });
|
|
340
|
-
this.overlay.set(id, { kind: "put", doc: optimisticDoc, token });
|
|
341
|
-
affected.push(optimisticDoc);
|
|
342
|
-
}
|
|
343
|
-
}
|
|
344
|
-
this.notifyListeners(affected, { optimistic: true });
|
|
345
|
-
}
|
|
346
232
|
async ready() {
|
|
347
233
|
return;
|
|
348
234
|
}
|
|
@@ -351,11 +237,6 @@ export class FireflyDatabase {
|
|
|
351
237
|
clearInterval(this.ephemeralSweep);
|
|
352
238
|
this.ephemeralSweep = undefined;
|
|
353
239
|
}
|
|
354
|
-
if (this.drainTimer !== undefined) {
|
|
355
|
-
clearTimeout(this.drainTimer);
|
|
356
|
-
this.drainTimer = undefined;
|
|
357
|
-
}
|
|
358
|
-
this.removeOnlineListener();
|
|
359
240
|
return;
|
|
360
241
|
}
|
|
361
242
|
async destroy() {
|
|
@@ -365,12 +246,6 @@ export class FireflyDatabase {
|
|
|
365
246
|
return;
|
|
366
247
|
}
|
|
367
248
|
async get(id) {
|
|
368
|
-
const pending = this.overlay.get(id);
|
|
369
|
-
if (pending) {
|
|
370
|
-
if (pending.kind === "del")
|
|
371
|
-
throw new Error(`Failed to get document: ${id} (optimistically deleted)`);
|
|
372
|
-
return pending.doc;
|
|
373
|
-
}
|
|
374
249
|
this.pruneEphemeral();
|
|
375
250
|
const eph = this.ephemeralOverlay.get(id);
|
|
376
251
|
const rRes = await this.vibeApi.getDoc(id, this.name);
|
|
@@ -389,311 +264,35 @@ export class FireflyDatabase {
|
|
|
389
264
|
throw new Error(`Failed to get document: ${JSON.stringify(res)}`);
|
|
390
265
|
}
|
|
391
266
|
async put(doc) {
|
|
392
|
-
const
|
|
393
|
-
const
|
|
394
|
-
const
|
|
395
|
-
if (
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
const optimisticDoc = decorateFiles({ ...doc, _id: id });
|
|
400
|
-
this.overlay.set(id, { kind: "put", doc: optimisticDoc, token });
|
|
401
|
-
this.notifyListeners([optimisticDoc], { optimistic: true });
|
|
402
|
-
}
|
|
403
|
-
try {
|
|
404
|
-
const docToPut = await uploadFiles(doc, this.vibeApi);
|
|
405
|
-
const issue = () => this.vibeApi.putDoc(docToPut, id, this.name);
|
|
406
|
-
const first = await this.issueWrite(id, issue);
|
|
407
|
-
const err0 = this.classifyWriteError(first, isResPutDoc, "put");
|
|
408
|
-
const settled = err0?.errClass === "network" && this.anonFallback === false
|
|
409
|
-
? await this.retryNetworkWrite({ id, issue, isOk: isResPutDoc, label: "put", queueGen, token })
|
|
410
|
-
: { rRes: first, err: err0 };
|
|
411
|
-
const rRes = settled.rRes;
|
|
412
|
-
const err = settled.err;
|
|
413
|
-
if (err === undefined) {
|
|
414
|
-
const res = rRes.Ok();
|
|
415
|
-
if (id !== undefined)
|
|
416
|
-
this.clearOverlay(id, token);
|
|
417
|
-
this.clearLiveWriteToken(id, token);
|
|
418
|
-
this.dropQueued(res.id);
|
|
419
|
-
const savedDoc = { ...docToPut, _id: res.id };
|
|
420
|
-
this.notifyListeners([savedDoc]);
|
|
421
|
-
this.drainAfterSuccess();
|
|
422
|
-
return { id: res.id, ok: true };
|
|
423
|
-
}
|
|
424
|
-
if (settled.superseded === true)
|
|
425
|
-
throw new Error("superseded by a newer write to the same document");
|
|
426
|
-
const queueable = this.anonFallback && (err.errClass === "network" || err.errClass === "access");
|
|
427
|
-
if (queueable && queueGen === this.queueGeneration) {
|
|
428
|
-
if (this.canQueue(id, doc)) {
|
|
429
|
-
const res = await this.queueWrite(id, { ...docToPut, _id: id }, token);
|
|
430
|
-
if (res.isErr())
|
|
431
|
-
throw res.Err();
|
|
432
|
-
this.vibeApi.notifyWriteOutcome?.({ outcome: "queued", docId: id });
|
|
433
|
-
return res.Ok();
|
|
434
|
-
}
|
|
435
|
-
this.vibeApi.notifyWriteOutcome?.({ outcome: "failed", docId: id ?? "", message: err.message });
|
|
436
|
-
}
|
|
437
|
-
else if (err.errClass === "network" && queueGen === this.queueGeneration) {
|
|
438
|
-
this.vibeApi.notifyWriteOutcome?.({ outcome: "failed", docId: id ?? "", message: err.message });
|
|
439
|
-
}
|
|
440
|
-
throw new Error(err.message);
|
|
267
|
+
const id = doc._id;
|
|
268
|
+
const docToPut = await uploadFiles(doc, this.vibeApi);
|
|
269
|
+
const rRes = await this.vibeApi.putDoc(docToPut, id, this.name);
|
|
270
|
+
if (rRes.isErr()) {
|
|
271
|
+
const message = `Failed to put document: ${errMsg(rRes.Err())}`;
|
|
272
|
+
this.vibeApi.notifyWriteOutcome?.({ outcome: "failed", docId: id ?? "", message });
|
|
273
|
+
throw new Error(message);
|
|
441
274
|
}
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
275
|
+
const res = rRes.Ok();
|
|
276
|
+
if (!isResPutDoc(res)) {
|
|
277
|
+
const message = `Failed to put document: ${res.message ?? JSON.stringify(res)}`;
|
|
278
|
+
this.vibeApi.notifyWriteOutcome?.({ outcome: "failed", docId: id ?? "", message });
|
|
279
|
+
throw new Error(message);
|
|
446
280
|
}
|
|
281
|
+
const savedDoc = { ...docToPut, _id: res.id };
|
|
282
|
+
this.notifyListeners([savedDoc]);
|
|
283
|
+
return { id: res.id, ok: true };
|
|
447
284
|
}
|
|
448
285
|
async del(id) {
|
|
449
|
-
const
|
|
450
|
-
const token = ++this.writeSeq;
|
|
451
|
-
this.liveWriteTokens.set(id, token);
|
|
452
|
-
const queueGen = this.queueGeneration;
|
|
453
|
-
if (optimistic) {
|
|
454
|
-
this.overlay.set(id, { kind: "del", token });
|
|
455
|
-
this.notifyListeners([{ _id: id, _deleted: true }], { optimistic: true });
|
|
456
|
-
}
|
|
457
|
-
try {
|
|
458
|
-
const issue = () => this.vibeApi.deleteDoc(id, this.name);
|
|
459
|
-
const first = await this.issueWrite(id, issue);
|
|
460
|
-
const err0 = this.classifyWriteError(first, isResDeleteDoc, "delete");
|
|
461
|
-
const settled = err0?.errClass === "network" && this.anonFallback === false
|
|
462
|
-
? await this.retryNetworkWrite({ id, issue, isOk: isResDeleteDoc, label: "delete", queueGen, token })
|
|
463
|
-
: { rRes: first, err: err0 };
|
|
464
|
-
const rRes = settled.rRes;
|
|
465
|
-
const err = settled.err;
|
|
466
|
-
if (err === undefined) {
|
|
467
|
-
const res = rRes.Ok();
|
|
468
|
-
if (optimistic)
|
|
469
|
-
this.clearOverlay(id, token);
|
|
470
|
-
this.clearLiveWriteToken(id, token);
|
|
471
|
-
this.dropQueued(res.id);
|
|
472
|
-
this.notifyListeners([{ _id: res.id, _deleted: true }]);
|
|
473
|
-
this.drainAfterSuccess();
|
|
474
|
-
return { id: res.id, ok: true };
|
|
475
|
-
}
|
|
476
|
-
if (settled.superseded === true)
|
|
477
|
-
throw new Error("superseded by a newer write to the same document");
|
|
478
|
-
const queueable = this.anonFallback && (err.errClass === "network" || err.errClass === "access");
|
|
479
|
-
if (queueable && queueGen === this.queueGeneration && this.canQueue(id, {})) {
|
|
480
|
-
const res = await this.queueWrite(id, { _id: id, _deleted: true }, token);
|
|
481
|
-
if (res.isErr())
|
|
482
|
-
throw res.Err();
|
|
483
|
-
return res.Ok();
|
|
484
|
-
}
|
|
485
|
-
throw new Error(err.message);
|
|
486
|
-
}
|
|
487
|
-
catch (err) {
|
|
488
|
-
this.rollback(id, token);
|
|
489
|
-
this.clearLiveWriteToken(id, token);
|
|
490
|
-
throw err;
|
|
491
|
-
}
|
|
492
|
-
}
|
|
493
|
-
clearOverlay(id, token) {
|
|
494
|
-
if (this.overlay.get(id)?.token !== token)
|
|
495
|
-
return false;
|
|
496
|
-
this.overlay.delete(id);
|
|
497
|
-
return true;
|
|
498
|
-
}
|
|
499
|
-
clearLiveWriteToken(id, token) {
|
|
500
|
-
if (id !== undefined && this.liveWriteTokens.get(id) === token)
|
|
501
|
-
this.liveWriteTokens.delete(id);
|
|
502
|
-
}
|
|
503
|
-
rollback(id, token) {
|
|
504
|
-
if (id === undefined || !this.clearOverlay(id, token))
|
|
505
|
-
return;
|
|
506
|
-
this.notifyListeners([{ _id: id }], { optimistic: true });
|
|
507
|
-
}
|
|
508
|
-
classifyWriteError(rRes, isOk, label) {
|
|
286
|
+
const rRes = await this.vibeApi.deleteDoc(id, this.name);
|
|
509
287
|
if (rRes.isErr()) {
|
|
510
|
-
|
|
288
|
+
throw new Error(`Failed to delete document: ${errMsg(rRes.Err())}`);
|
|
511
289
|
}
|
|
512
290
|
const res = rRes.Ok();
|
|
513
|
-
if (
|
|
514
|
-
|
|
515
|
-
const frame = res;
|
|
516
|
-
if (frame.code === "network") {
|
|
517
|
-
return { errClass: "network", message: `Failed to ${label} document: ${frame.message ?? "network error"}` };
|
|
518
|
-
}
|
|
519
|
-
if (frame.code === "access-denied") {
|
|
520
|
-
return { errClass: "access", message: `Failed to ${label} document: ${frame.message ?? "access denied"}` };
|
|
521
|
-
}
|
|
522
|
-
if (frame.code === "conflict") {
|
|
523
|
-
return { errClass: "conflict", message: `Failed to ${label} document: ${frame.message ?? "write conflict"}` };
|
|
291
|
+
if (!isResDeleteDoc(res)) {
|
|
292
|
+
throw new Error(`Failed to delete document: ${res.message ?? JSON.stringify(res)}`);
|
|
524
293
|
}
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
async retryNetworkWrite(args) {
|
|
528
|
-
const { id, issue, isOk, label, queueGen, token } = args;
|
|
529
|
-
let last = {
|
|
530
|
-
rRes: Result.Err(`Failed to ${label} document: retry abandoned`),
|
|
531
|
-
err: { errClass: "network", message: `Failed to ${label} document: retry abandoned` },
|
|
532
|
-
};
|
|
533
|
-
let backoff = 0;
|
|
534
|
-
for (let attempt = 0; attempt < NETWORK_RETRY_LIMIT; attempt++) {
|
|
535
|
-
backoff = backoff === 0 ? 500 : Math.min(backoff * 2, 4000);
|
|
536
|
-
await this.retrySleep(backoff);
|
|
537
|
-
if (this.queueGeneration !== queueGen)
|
|
538
|
-
return last;
|
|
539
|
-
if (id !== undefined && this.liveWriteTokens.get(id) !== token)
|
|
540
|
-
return { ...last, superseded: true };
|
|
541
|
-
const rRes = await this.issueWrite(id, issue);
|
|
542
|
-
if (this.queueGeneration !== queueGen)
|
|
543
|
-
return last;
|
|
544
|
-
const err = this.classifyWriteError(rRes, isOk, label);
|
|
545
|
-
last = { rRes, err };
|
|
546
|
-
if (err === undefined || err.errClass !== "network")
|
|
547
|
-
return last;
|
|
548
|
-
}
|
|
549
|
-
return last;
|
|
550
|
-
}
|
|
551
|
-
canQueue(id, doc) {
|
|
552
|
-
return this.offlineQueue !== undefined && id !== undefined && !hasRawFiles(doc);
|
|
553
|
-
}
|
|
554
|
-
async queueWrite(id, doc, token) {
|
|
555
|
-
const persisted = await exception2Result(async () => {
|
|
556
|
-
await this.offlineQueue?.put(doc);
|
|
557
|
-
});
|
|
558
|
-
if (persisted.isErr()) {
|
|
559
|
-
this.queueTokens.delete(id);
|
|
560
|
-
return Result.Err(persisted.Err());
|
|
561
|
-
}
|
|
562
|
-
this.queueTokens.set(id, token);
|
|
563
|
-
this.scheduleDrain();
|
|
564
|
-
return Result.Ok({ id, ok: true });
|
|
565
|
-
}
|
|
566
|
-
dropQueued(id) {
|
|
567
|
-
if (this.offlineQueue === undefined)
|
|
568
|
-
return;
|
|
569
|
-
this.queueTokens.delete(id);
|
|
570
|
-
void this.offlineQueue.del(id);
|
|
571
|
-
}
|
|
572
|
-
drainAfterSuccess() {
|
|
573
|
-
if (this.offlineQueue !== undefined && this.offlineQueue.snapshot().length > 0)
|
|
574
|
-
this.scheduleDrain();
|
|
575
|
-
}
|
|
576
|
-
issueWrite(id, fn) {
|
|
577
|
-
return id === undefined ? fn() : this.runExclusive(id, fn);
|
|
578
|
-
}
|
|
579
|
-
runExclusive(id, fn) {
|
|
580
|
-
const prev = this.idChains.get(id) ?? Promise.resolve();
|
|
581
|
-
const run = prev.then(() => fn());
|
|
582
|
-
const tail = run.then(() => undefined, () => undefined);
|
|
583
|
-
this.idChains.set(id, tail);
|
|
584
|
-
void tail.then(() => {
|
|
585
|
-
if (this.idChains.get(id) === tail)
|
|
586
|
-
this.idChains.delete(id);
|
|
587
|
-
});
|
|
588
|
-
return run;
|
|
589
|
-
}
|
|
590
|
-
scheduleDrain(delay = 0) {
|
|
591
|
-
if (this.anonFallback)
|
|
592
|
-
return;
|
|
593
|
-
if (this.drainTimer !== undefined)
|
|
594
|
-
return;
|
|
595
|
-
this.drainTimer = setTimeout(() => {
|
|
596
|
-
this.drainTimer = undefined;
|
|
597
|
-
void this.drain();
|
|
598
|
-
}, delay);
|
|
599
|
-
this.drainTimer.unref?.();
|
|
600
|
-
}
|
|
601
|
-
scheduleDrainBackoff() {
|
|
602
|
-
const delay = this.drainBackoff === 0 ? 500 : Math.min(this.drainBackoff * 2, 4000);
|
|
603
|
-
this.drainBackoff = delay;
|
|
604
|
-
if (this.drainTimer !== undefined) {
|
|
605
|
-
clearTimeout(this.drainTimer);
|
|
606
|
-
this.drainTimer = undefined;
|
|
607
|
-
}
|
|
608
|
-
this.scheduleDrain(delay);
|
|
609
|
-
}
|
|
610
|
-
async drain() {
|
|
611
|
-
const store = this.offlineQueue;
|
|
612
|
-
if (this.draining || store === undefined)
|
|
613
|
-
return;
|
|
614
|
-
this.draining = true;
|
|
615
|
-
const gen = this.queueGeneration;
|
|
616
|
-
let networkStalled = false;
|
|
617
|
-
try {
|
|
618
|
-
const snap = [...store.snapshot()].sort((a, b) => (a._id < b._id ? -1 : a._id > b._id ? 1 : 0));
|
|
619
|
-
for (const doc of snap) {
|
|
620
|
-
if (this.queueGeneration !== gen)
|
|
621
|
-
break;
|
|
622
|
-
const id = doc._id;
|
|
623
|
-
const token = this.queueTokens.get(id);
|
|
624
|
-
const isDel = doc._deleted === true;
|
|
625
|
-
const outcome = await this.runExclusive(id, () => {
|
|
626
|
-
if (token === undefined || this.queueTokens.get(id) !== token) {
|
|
627
|
-
return Promise.resolve({ skipped: true });
|
|
628
|
-
}
|
|
629
|
-
const rReq = isDel
|
|
630
|
-
? this.vibeApi.deleteDoc(id, this.name)
|
|
631
|
-
: this.vibeApi.putDoc(doc, id, this.name);
|
|
632
|
-
return rReq.then((rRes) => ({ skipped: false, rRes }));
|
|
633
|
-
});
|
|
634
|
-
if (this.queueGeneration !== gen)
|
|
635
|
-
break;
|
|
636
|
-
if (outcome.skipped)
|
|
637
|
-
continue;
|
|
638
|
-
const err = this.classifyWriteError(outcome.rRes, isDel ? isResDeleteDoc : isResPutDoc, isDel ? "delete" : "put");
|
|
639
|
-
if (err === undefined) {
|
|
640
|
-
await this.settleDrained({ store, id, token, isDel, denied: false, gen });
|
|
641
|
-
}
|
|
642
|
-
else if (err.errClass === "rejection" || err.errClass === "access") {
|
|
643
|
-
await this.settleDrained({ store, id, token, isDel, denied: true, gen });
|
|
644
|
-
this.vibeApi.notifyWriteOutcome?.({ outcome: "rejected", docId: id, message: err.message });
|
|
645
|
-
console.error(`Offline queue: dropping rejected write "${id}" in db "${this.name}": ${err.message}`);
|
|
646
|
-
}
|
|
647
|
-
else {
|
|
648
|
-
networkStalled = true;
|
|
649
|
-
break;
|
|
650
|
-
}
|
|
651
|
-
}
|
|
652
|
-
}
|
|
653
|
-
finally {
|
|
654
|
-
this.draining = false;
|
|
655
|
-
}
|
|
656
|
-
if (networkStalled) {
|
|
657
|
-
this.scheduleDrainBackoff();
|
|
658
|
-
}
|
|
659
|
-
else {
|
|
660
|
-
this.drainBackoff = 0;
|
|
661
|
-
if (this.offlineQueue !== undefined && this.offlineQueue.snapshot().length > 0)
|
|
662
|
-
this.scheduleDrain();
|
|
663
|
-
}
|
|
664
|
-
}
|
|
665
|
-
async settleDrained(args) {
|
|
666
|
-
const { store, id, token, isDel, denied, gen } = args;
|
|
667
|
-
if (gen !== this.queueGeneration)
|
|
668
|
-
return;
|
|
669
|
-
if (token !== undefined && this.queueTokens.get(id) !== token)
|
|
670
|
-
return;
|
|
671
|
-
this.queueTokens.delete(id);
|
|
672
|
-
await store.del(id);
|
|
673
|
-
if (denied) {
|
|
674
|
-
if (token !== undefined)
|
|
675
|
-
this.rollback(id, token);
|
|
676
|
-
return;
|
|
677
|
-
}
|
|
678
|
-
if (token !== undefined)
|
|
679
|
-
this.clearOverlay(id, token);
|
|
680
|
-
this.notifyListeners([isDel ? { _id: id, _deleted: true } : { _id: id }]);
|
|
681
|
-
}
|
|
682
|
-
installOnlineListener() {
|
|
683
|
-
if (this.onlineHandler !== undefined)
|
|
684
|
-
return;
|
|
685
|
-
const g = globalThis;
|
|
686
|
-
if (typeof g.addEventListener !== "function")
|
|
687
|
-
return;
|
|
688
|
-
this.onlineHandler = () => this.scheduleDrain();
|
|
689
|
-
g.addEventListener("online", this.onlineHandler);
|
|
690
|
-
}
|
|
691
|
-
removeOnlineListener() {
|
|
692
|
-
if (this.onlineHandler === undefined)
|
|
693
|
-
return;
|
|
694
|
-
const g = globalThis;
|
|
695
|
-
g.removeEventListener?.("online", this.onlineHandler);
|
|
696
|
-
this.onlineHandler = undefined;
|
|
294
|
+
this.notifyListeners([{ _id: res.id, _deleted: true }]);
|
|
295
|
+
return { id: res.id, ok: true };
|
|
697
296
|
}
|
|
698
297
|
async remove(id) {
|
|
699
298
|
return this.del(id);
|
|
@@ -708,17 +307,14 @@ export class FireflyDatabase {
|
|
|
708
307
|
}
|
|
709
308
|
async query(mapFn, opts = {}) {
|
|
710
309
|
const serverDocs = await this.fetchServerDocs(this.queryHint(mapFn, opts));
|
|
711
|
-
return materializeQuery(this.
|
|
310
|
+
return materializeQuery(this.mergeOverlayDocs(serverDocs), mapFn, opts);
|
|
712
311
|
}
|
|
713
312
|
async queryLive(mapFn, opts = {}) {
|
|
714
313
|
const serverDocs = await this.fetchServerDocs(this.queryHint(mapFn, opts));
|
|
715
|
-
return { result: materializeQuery(this.
|
|
314
|
+
return { result: materializeQuery(this.mergeOverlayDocs(serverDocs), mapFn, opts), serverDocs };
|
|
716
315
|
}
|
|
717
316
|
materializeLive(serverDocs, mapFn, opts = {}) {
|
|
718
|
-
return materializeQuery(this.
|
|
719
|
-
}
|
|
720
|
-
foldOverlays(serverDocs) {
|
|
721
|
-
return this.applyOverlay(this.mergeOverlayDocs(serverDocs));
|
|
317
|
+
return materializeQuery(this.mergeOverlayDocs(serverDocs), mapFn, opts);
|
|
722
318
|
}
|
|
723
319
|
queryHint(mapFn, opts) {
|
|
724
320
|
const isPrimitive = (v) => v === null || typeof v === "string" || typeof v === "number" || typeof v === "boolean";
|
|
@@ -745,18 +341,6 @@ export class FireflyDatabase {
|
|
|
745
341
|
}
|
|
746
342
|
return res.docs.map((d) => decorateFiles({ ...d, _id: d._id }));
|
|
747
343
|
}
|
|
748
|
-
applyOverlay(serverDocs) {
|
|
749
|
-
if (this.overlay.size === 0)
|
|
750
|
-
return serverDocs;
|
|
751
|
-
const byId = new Map(serverDocs.map((d) => [d._id, d]));
|
|
752
|
-
for (const [id, entry] of this.overlay) {
|
|
753
|
-
if (entry.kind === "del")
|
|
754
|
-
byId.delete(id);
|
|
755
|
-
else
|
|
756
|
-
byId.set(id, entry.doc);
|
|
757
|
-
}
|
|
758
|
-
return [...byId.values()];
|
|
759
|
-
}
|
|
760
344
|
async allDocs(opts = {}) {
|
|
761
345
|
const rRes = await this.vibeApi.queryDocs(this.name);
|
|
762
346
|
if (rRes.isErr()) {
|
|
@@ -769,7 +353,7 @@ export class FireflyDatabase {
|
|
|
769
353
|
return this.materializeAllDocs(res.docs.map((d) => decorateFiles({ ...d, _id: d._id })), opts);
|
|
770
354
|
}
|
|
771
355
|
materializeAllDocs(serverDocs, opts = {}) {
|
|
772
|
-
let docs = this.
|
|
356
|
+
let docs = this.mergeOverlayDocs(serverDocs);
|
|
773
357
|
docs.sort((a, b) => (a._id < b._id ? -1 : a._id > b._id ? 1 : 0));
|
|
774
358
|
if (opts.descending)
|
|
775
359
|
docs.reverse();
|