@vibes.diy/vibe-runtime 10.0.0 → 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.
@@ -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;
@@ -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 optimistic = this.optimisticEnabled;
393
- const id = doc._id ?? (optimistic ? newOptimisticId() : undefined);
394
- const token = ++this.writeSeq;
395
- if (id !== undefined)
396
- this.liveWriteTokens.set(id, token);
397
- const queueGen = this.queueGeneration;
398
- if (optimistic && id !== undefined) {
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
- catch (err) {
443
- this.rollback(id, token);
444
- this.clearLiveWriteToken(id, token);
445
- throw err;
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 optimistic = this.optimisticEnabled;
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
- return { errClass: "network", message: `Failed to ${label} document: ${errMsg(rRes.Err())}` };
288
+ throw new Error(`Failed to delete document: ${errMsg(rRes.Err())}`);
511
289
  }
512
290
  const res = rRes.Ok();
513
- if (isOk(res))
514
- return undefined;
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
- return { errClass: "rejection", message: `Failed to ${label} document: ${frame.message ?? JSON.stringify(res)}` };
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.foldOverlays(serverDocs), mapFn, opts);
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.foldOverlays(serverDocs), mapFn, opts), serverDocs };
314
+ return { result: materializeQuery(this.mergeOverlayDocs(serverDocs), mapFn, opts), serverDocs };
716
315
  }
717
316
  materializeLive(serverDocs, mapFn, opts = {}) {
718
- return materializeQuery(this.foldOverlays(serverDocs), mapFn, opts);
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.foldOverlays(serverDocs);
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();