tablinum 0.6.4 → 0.8.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/dist/index.js CHANGED
@@ -224,6 +224,257 @@ function resolveRuntimeConfig(source) {
224
224
  );
225
225
  }
226
226
 
227
+ // src/storage/idb.ts
228
+ import { Effect as Effect2 } from "effect";
229
+ import { openDB, deleteDB } from "idb";
230
+
231
+ // src/sync/compact-event.ts
232
+ import { bytesToHex as bytesToHex2, hexToBytes as hexToBytes2 } from "@noble/hashes/utils.js";
233
+ var VERSION = 1;
234
+ var HEADER_SIZE = 133;
235
+ function base64ToBytes(base64) {
236
+ const binary = atob(base64);
237
+ const bytes = new Uint8Array(binary.length);
238
+ for (let i = 0; i < binary.length; i++) {
239
+ bytes[i] = binary.charCodeAt(i);
240
+ }
241
+ return bytes;
242
+ }
243
+ function bytesToBase64(bytes) {
244
+ let binary = "";
245
+ for (let i = 0; i < bytes.length; i++) {
246
+ binary += String.fromCharCode(bytes[i]);
247
+ }
248
+ return btoa(binary);
249
+ }
250
+ function packEvent(event) {
251
+ const pubkey = hexToBytes2(event.pubkey);
252
+ const sig = hexToBytes2(event.sig);
253
+ const recipientTag = event.tags.find((t) => t[0] === "p");
254
+ if (!recipientTag) throw new Error("Gift wrap missing #p tag");
255
+ if (event.tags.some((t) => t[0] !== "p")) {
256
+ throw new Error("Gift wrap has unexpected non-p tags; compact encoding would lose them");
257
+ }
258
+ const recipient = hexToBytes2(recipientTag[1]);
259
+ const createdAtBuf = new Uint8Array(4);
260
+ new DataView(createdAtBuf.buffer).setUint32(0, event.created_at, false);
261
+ const content = base64ToBytes(event.content);
262
+ const result = new Uint8Array(HEADER_SIZE + content.length);
263
+ result[0] = VERSION;
264
+ result.set(pubkey, 1);
265
+ result.set(sig, 33);
266
+ result.set(recipient, 97);
267
+ result.set(createdAtBuf, 129);
268
+ result.set(content, HEADER_SIZE);
269
+ return result;
270
+ }
271
+ function unpackEvent(id, compact) {
272
+ const version = compact[0];
273
+ if (version !== VERSION) throw new Error(`Unknown compact event version: ${version}`);
274
+ const pubkey = bytesToHex2(compact.slice(1, 33));
275
+ const sig = bytesToHex2(compact.slice(33, 97));
276
+ const recipient = bytesToHex2(compact.slice(97, 129));
277
+ const dv = new DataView(compact.buffer, compact.byteOffset + 129, 4);
278
+ const createdAt = dv.getUint32(0, false);
279
+ const content = bytesToBase64(compact.slice(HEADER_SIZE));
280
+ return {
281
+ id,
282
+ pubkey,
283
+ sig,
284
+ created_at: createdAt,
285
+ kind: 1059,
286
+ tags: [["p", recipient]],
287
+ content
288
+ };
289
+ }
290
+
291
+ // src/storage/idb.ts
292
+ var DB_NAME = "tablinum";
293
+ function storeName(collection2) {
294
+ return `col_${collection2}`;
295
+ }
296
+ function computeSchemaSig(schema) {
297
+ return Object.entries(schema).sort(([a], [b]) => a.localeCompare(b)).map(([name, def]) => {
298
+ const indices = [...def.indices ?? []].sort().join(",");
299
+ return `${name}:${indices}`;
300
+ }).join("|");
301
+ }
302
+ function wrap(label, fn) {
303
+ return Effect2.tryPromise({
304
+ try: fn,
305
+ catch: (e) => new StorageError({
306
+ message: `IndexedDB ${label} failed: ${e instanceof Error ? e.message : String(e)}`,
307
+ cause: e
308
+ })
309
+ });
310
+ }
311
+ function upgradeSchema(database, schema, tx) {
312
+ if (!database.objectStoreNames.contains("_meta")) {
313
+ database.createObjectStore("_meta");
314
+ }
315
+ if (!database.objectStoreNames.contains("events")) {
316
+ const events = database.createObjectStore("events", { keyPath: "id" });
317
+ events.createIndex("by-record", ["collection", "recordId"]);
318
+ }
319
+ if (!database.objectStoreNames.contains("giftwraps")) {
320
+ database.createObjectStore("giftwraps", { keyPath: "id" });
321
+ }
322
+ const expectedStores = /* @__PURE__ */ new Set();
323
+ for (const [, def] of Object.entries(schema)) {
324
+ const sn = storeName(def.name);
325
+ expectedStores.add(sn);
326
+ if (!database.objectStoreNames.contains(sn)) {
327
+ const store = database.createObjectStore(sn, { keyPath: "id" });
328
+ for (const idx of def.indices ?? []) {
329
+ store.createIndex(idx, idx);
330
+ }
331
+ } else {
332
+ const store = tx.objectStore(sn);
333
+ const existingIndices = new Set(Array.from(store.indexNames));
334
+ const wantedIndices = new Set(def.indices ?? []);
335
+ for (const idx of existingIndices) {
336
+ if (!wantedIndices.has(idx)) store.deleteIndex(idx);
337
+ }
338
+ for (const idx of wantedIndices) {
339
+ if (!existingIndices.has(idx)) store.createIndex(idx, idx);
340
+ }
341
+ }
342
+ }
343
+ for (const existing of Array.from(database.objectStoreNames)) {
344
+ if (existing.startsWith("col_") && !expectedStores.has(existing)) {
345
+ database.deleteObjectStore(existing);
346
+ }
347
+ }
348
+ tx.objectStore("_meta").put(computeSchemaSig(schema), "schema_sig");
349
+ }
350
+ function deleteIDBStorage(dbName) {
351
+ if (typeof indexedDB === "undefined") {
352
+ return Effect2.fail(
353
+ new StorageError({
354
+ message: "IndexedDB is not available in this environment"
355
+ })
356
+ );
357
+ }
358
+ return wrap("deleteDatabase", () => deleteDB(dbName));
359
+ }
360
+ function openIDBStorage(dbName, schema) {
361
+ return Effect2.gen(function* () {
362
+ const name = dbName ?? DB_NAME;
363
+ const schemaSig = computeSchemaSig(schema);
364
+ if (typeof indexedDB === "undefined") {
365
+ return yield* Effect2.fail(
366
+ new StorageError({
367
+ message: "IndexedDB is not available in this environment"
368
+ })
369
+ );
370
+ }
371
+ const probeDb = yield* Effect2.tryPromise({
372
+ try: () => openDB(name),
373
+ catch: (e) => new StorageError({ message: "Failed to open IndexedDB", cause: e })
374
+ });
375
+ const currentVersion = probeDb.version;
376
+ let needsUpgrade = true;
377
+ if (probeDb.objectStoreNames.contains("_meta")) {
378
+ const storedSig = yield* Effect2.tryPromise({
379
+ try: () => probeDb.get("_meta", "schema_sig"),
380
+ catch: () => new StorageError({ message: "Failed to read schema meta" })
381
+ }).pipe(Effect2.catch(() => Effect2.succeed(void 0)));
382
+ needsUpgrade = storedSig !== schemaSig;
383
+ }
384
+ probeDb.close();
385
+ const db = needsUpgrade ? yield* Effect2.tryPromise({
386
+ try: () => openDB(name, currentVersion + 1, {
387
+ upgrade(database, _oldVersion, _newVersion, transaction) {
388
+ upgradeSchema(database, schema, transaction);
389
+ }
390
+ }),
391
+ catch: (e) => new StorageError({ message: "Failed to open IndexedDB", cause: e })
392
+ }) : yield* Effect2.tryPromise({
393
+ try: () => openDB(name),
394
+ catch: (e) => new StorageError({ message: "Failed to open IndexedDB", cause: e })
395
+ });
396
+ yield* Effect2.addFinalizer(() => Effect2.sync(() => db.close()));
397
+ const handle = {
398
+ putRecord: (collection2, record) => wrap("putRecord", () => db.put(storeName(collection2), record).then(() => void 0)),
399
+ getRecord: (collection2, id) => wrap("getRecord", () => db.get(storeName(collection2), id)),
400
+ getAllRecords: (collection2) => wrap("getAllRecords", () => db.getAll(storeName(collection2))),
401
+ countRecords: (collection2) => wrap("countRecords", () => db.count(storeName(collection2))),
402
+ clearRecords: (collection2) => wrap("clearRecords", () => db.clear(storeName(collection2))),
403
+ getByIndex: (collection2, indexName, value) => wrap("getByIndex", () => db.getAllFromIndex(storeName(collection2), indexName, value)),
404
+ getByIndexRange: (collection2, indexName, range) => wrap("getByIndexRange", () => db.getAllFromIndex(storeName(collection2), indexName, range)),
405
+ getAllSorted: (collection2, indexName, direction) => wrap("getAllSorted", async () => {
406
+ const sn = storeName(collection2);
407
+ const tx = db.transaction(sn, "readonly");
408
+ const store = tx.objectStore(sn);
409
+ const index = store.index(indexName);
410
+ const results = [];
411
+ let cursor = await index.openCursor(null, direction ?? "next");
412
+ while (cursor) {
413
+ results.push(cursor.value);
414
+ cursor = await cursor.continue();
415
+ }
416
+ return results;
417
+ }),
418
+ putEvent: (event) => wrap("putEvent", () => db.put("events", event).then(() => void 0)),
419
+ getEvent: (id) => wrap("getEvent", () => db.get("events", id)),
420
+ getAllEvents: () => wrap("getAllEvents", () => db.getAll("events")),
421
+ getEventsByRecord: (collection2, recordId) => wrap(
422
+ "getEventsByRecord",
423
+ () => db.getAllFromIndex("events", "by-record", [collection2, recordId])
424
+ ),
425
+ putGiftWrap: (gw) => wrap("putGiftWrap", async () => {
426
+ if (gw.event) {
427
+ const compact = packEvent(gw.event);
428
+ await db.put("giftwraps", { id: gw.id, compact, createdAt: gw.createdAt });
429
+ } else {
430
+ await db.put("giftwraps", { id: gw.id, createdAt: gw.createdAt });
431
+ }
432
+ }),
433
+ getGiftWrap: (id) => wrap("getGiftWrap", async () => {
434
+ const raw = await db.get("giftwraps", id);
435
+ if (!raw) return void 0;
436
+ if (raw.compact) {
437
+ return { id: raw.id, event: unpackEvent(raw.id, raw.compact), createdAt: raw.createdAt };
438
+ }
439
+ if (raw.event) {
440
+ const compact = packEvent(raw.event);
441
+ await db.put("giftwraps", { id: raw.id, compact, createdAt: raw.createdAt });
442
+ return { id: raw.id, event: raw.event, createdAt: raw.createdAt };
443
+ }
444
+ return { id: raw.id, createdAt: raw.createdAt };
445
+ }),
446
+ getAllGiftWraps: () => wrap("getAllGiftWraps", async () => {
447
+ const raws = await db.getAll("giftwraps");
448
+ const results = [];
449
+ for (const raw of raws) {
450
+ if (raw.compact) {
451
+ results.push({ id: raw.id, event: unpackEvent(raw.id, raw.compact), createdAt: raw.createdAt });
452
+ } else if (raw.event) {
453
+ const compact = packEvent(raw.event);
454
+ await db.put("giftwraps", { id: raw.id, compact, createdAt: raw.createdAt });
455
+ results.push({ id: raw.id, event: raw.event, createdAt: raw.createdAt });
456
+ } else {
457
+ results.push({ id: raw.id, createdAt: raw.createdAt });
458
+ }
459
+ }
460
+ return results;
461
+ }),
462
+ deleteGiftWrap: (id) => wrap("deleteGiftWrap", () => db.delete("giftwraps", id).then(() => void 0)),
463
+ deleteEvent: (id) => wrap("deleteEvent", () => db.delete("events", id).then(() => void 0)),
464
+ stripEventData: (id) => wrap("stripEventData", async () => {
465
+ const existing = await db.get("events", id);
466
+ if (existing) {
467
+ await db.put("events", { ...existing, data: null });
468
+ }
469
+ }),
470
+ getMeta: (key) => wrap("getMeta", () => db.get("_meta", key)),
471
+ putMeta: (key, value) => wrap("putMeta", () => db.put("_meta", value, key).then(() => void 0)),
472
+ close: () => Effect2.sync(() => db.close())
473
+ };
474
+ return handle;
475
+ });
476
+ }
477
+
227
478
  // src/services/Config.ts
228
479
  import { ServiceMap } from "effect";
229
480
  var Config = class extends ServiceMap.Service()("tablinum/Config") {
@@ -240,16 +491,16 @@ var Tablinum = class extends ServiceMap2.Service()(
240
491
  import { Effect as Effect21, Exit, Layer as Layer9, Option as Option9, PubSub as PubSub2, References as References3, Ref as Ref5, Scope as Scope4 } from "effect";
241
492
 
242
493
  // src/crud/watch.ts
243
- import { Effect as Effect2, PubSub, Ref, Stream } from "effect";
494
+ import { Effect as Effect3, PubSub, Ref, Stream } from "effect";
244
495
  function watchCollection(ctx, storage, collectionName, filter, mapRecord2) {
245
- const query = () => Effect2.map(storage.getAllRecords(collectionName), (all) => {
496
+ const query = () => Effect3.map(storage.getAllRecords(collectionName), (all) => {
246
497
  const filtered = all.filter((r) => !r._d && (filter ? filter(r) : true));
247
498
  return mapRecord2 ? filtered.map(mapRecord2) : filtered;
248
499
  });
249
500
  const changes = Stream.fromPubSub(ctx.pubsub).pipe(
250
501
  Stream.filter((event) => event.collection === collectionName),
251
502
  Stream.mapEffect(
252
- () => Effect2.gen(function* () {
503
+ () => Effect3.gen(function* () {
253
504
  const replaying = yield* Ref.get(ctx.replayingRef);
254
505
  if (replaying) return void 0;
255
506
  return yield* query();
@@ -258,18 +509,18 @@ function watchCollection(ctx, storage, collectionName, filter, mapRecord2) {
258
509
  Stream.filter((result) => result !== void 0)
259
510
  );
260
511
  return Stream.unwrap(
261
- Effect2.gen(function* () {
262
- yield* Effect2.sleep(0);
512
+ Effect3.gen(function* () {
513
+ yield* Effect3.sleep(0);
263
514
  const initial = yield* query();
264
515
  return Stream.concat(Stream.make(initial), changes);
265
516
  })
266
517
  );
267
518
  }
268
519
  function notifyChange(ctx, event) {
269
- return PubSub.publish(ctx.pubsub, event).pipe(Effect2.asVoid);
520
+ return PubSub.publish(ctx.pubsub, event).pipe(Effect3.asVoid);
270
521
  }
271
522
  function notifyReplayComplete(ctx, collections) {
272
- return Effect2.gen(function* () {
523
+ return Effect3.gen(function* () {
273
524
  yield* Ref.set(ctx.replayingRef, false);
274
525
  for (const collection2 of collections) {
275
526
  yield* notifyChange(ctx, {
@@ -282,7 +533,7 @@ function notifyReplayComplete(ctx, collections) {
282
533
  }
283
534
 
284
535
  // src/storage/records-store.ts
285
- import { Effect as Effect3 } from "effect";
536
+ import { Effect as Effect4 } from "effect";
286
537
 
287
538
  // src/storage/lww.ts
288
539
  function resolveWinner(existing, incoming) {
@@ -296,30 +547,6 @@ function resolveWinner(existing, incoming) {
296
547
  function isPlainObject(value) {
297
548
  return value !== null && typeof value === "object" && !Array.isArray(value);
298
549
  }
299
- function deepDiff(before, after) {
300
- const result = {};
301
- let hasChanges = false;
302
- for (const key of Object.keys(after)) {
303
- const a = before[key];
304
- const b = after[key];
305
- if (isPlainObject(a) && isPlainObject(b)) {
306
- const nested = deepDiff(a, b);
307
- if (nested !== null) {
308
- result[key] = nested;
309
- hasChanges = true;
310
- }
311
- } else if (Array.isArray(a) && Array.isArray(b)) {
312
- if (JSON.stringify(a) !== JSON.stringify(b)) {
313
- result[key] = b;
314
- hasChanges = true;
315
- }
316
- } else if (a !== b) {
317
- result[key] = b;
318
- hasChanges = true;
319
- }
320
- }
321
- return hasChanges ? result : null;
322
- }
323
550
  function deepMerge(target, source) {
324
551
  const result = { ...target };
325
552
  for (const key of Object.keys(source)) {
@@ -346,7 +573,7 @@ function buildRecord(event) {
346
573
  };
347
574
  }
348
575
  function applyEvent(storage, event) {
349
- return Effect3.gen(function* () {
576
+ return Effect4.gen(function* () {
350
577
  const existing = yield* storage.getRecord(event.collection, event.recordId);
351
578
  if (existing) {
352
579
  const existingMeta = {
@@ -366,7 +593,7 @@ function applyEvent(storage, event) {
366
593
  });
367
594
  }
368
595
  function rebuild(storage, collections) {
369
- return Effect3.gen(function* () {
596
+ return Effect4.gen(function* () {
370
597
  for (const col of collections) {
371
598
  yield* storage.clearRecords(col);
372
599
  }
@@ -382,7 +609,7 @@ function rebuild(storage, collections) {
382
609
  }
383
610
 
384
611
  // src/schema/validate.ts
385
- import { Effect as Effect4, Schema as Schema3 } from "effect";
612
+ import { Effect as Effect5, Schema as Schema3 } from "effect";
386
613
  function fieldDefToSchema(fd) {
387
614
  let base;
388
615
  switch (fd.kind) {
@@ -430,10 +657,10 @@ function buildStructSchema(def, options = {}) {
430
657
  function buildValidator(collectionName, def) {
431
658
  const decode = Schema3.decodeUnknownEffect(buildStructSchema(def, { includeId: true }));
432
659
  return (input) => decode(input).pipe(
433
- Effect4.map(
660
+ Effect5.map(
434
661
  (result) => result
435
662
  ),
436
- Effect4.mapError(
663
+ Effect5.mapError(
437
664
  (e) => new ValidationError({
438
665
  message: `Validation failed for collection "${collectionName}": ${e.message}`
439
666
  })
@@ -442,7 +669,7 @@ function buildValidator(collectionName, def) {
442
669
  }
443
670
  function buildPartialValidator(collectionName, def) {
444
671
  const decode = Schema3.decodeUnknownEffect(buildStructSchema(def, { allOptional: true }));
445
- return (input) => Effect4.gen(function* () {
672
+ return (input) => Effect5.gen(function* () {
446
673
  if (typeof input !== "object" || input === null) {
447
674
  return yield* new ValidationError({
448
675
  message: `Validation failed for collection "${collectionName}": expected an object`
@@ -457,8 +684,8 @@ function buildPartialValidator(collectionName, def) {
457
684
  });
458
685
  }
459
686
  return yield* decode(record).pipe(
460
- Effect4.map((result) => result),
461
- Effect4.mapError(
687
+ Effect5.map((result) => result),
688
+ Effect5.mapError(
462
689
  (e) => new ValidationError({
463
690
  message: `Validation failed for collection "${collectionName}": ${e.message}`
464
691
  })
@@ -468,7 +695,7 @@ function buildPartialValidator(collectionName, def) {
468
695
  }
469
696
 
470
697
  // src/crud/collection-handle.ts
471
- import { Effect as Effect6, Option as Option3, References } from "effect";
698
+ import { Effect as Effect7, Option as Option3, References } from "effect";
472
699
 
473
700
  // src/utils/uuid.ts
474
701
  var alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
@@ -501,12 +728,12 @@ function uuidv7() {
501
728
  }
502
729
 
503
730
  // src/crud/query-builder.ts
504
- import { Effect as Effect5, Option as Option2, Ref as Ref2, Stream as Stream2 } from "effect";
731
+ import { Effect as Effect6, Option as Option2, Ref as Ref2, Stream as Stream2 } from "effect";
505
732
  function emptyPlan() {
506
733
  return { filters: [] };
507
734
  }
508
735
  function executeQuery(ctx, plan) {
509
- return Effect5.gen(function* () {
736
+ return Effect6.gen(function* () {
510
737
  if (plan.fieldName) {
511
738
  const fieldDef = ctx.def.fields[plan.fieldName];
512
739
  if (!fieldDef) {
@@ -582,7 +809,7 @@ function watchQuery(ctx, plan) {
582
809
  const changes = Stream2.fromPubSub(ctx.watchCtx.pubsub).pipe(
583
810
  Stream2.filter((event) => event.collection === ctx.collectionName),
584
811
  Stream2.mapEffect(
585
- () => Effect5.gen(function* () {
812
+ () => Effect6.gen(function* () {
586
813
  const replaying = yield* Ref2.get(ctx.watchCtx.replayingRef);
587
814
  if (replaying) return void 0;
588
815
  return yield* query();
@@ -591,7 +818,7 @@ function watchQuery(ctx, plan) {
591
818
  Stream2.filter((result) => result !== void 0)
592
819
  );
593
820
  return Stream2.unwrap(
594
- Effect5.gen(function* () {
821
+ Effect6.gen(function* () {
595
822
  const initial = yield* query();
596
823
  return Stream2.concat(Stream2.make(initial), changes);
597
824
  })
@@ -617,11 +844,11 @@ function makeQueryBuilder(ctx, plan) {
617
844
  offset: (n) => makeQueryBuilder(ctx, { ...plan, offset: n }),
618
845
  limit: (n) => makeQueryBuilder(ctx, { ...plan, limit: n }),
619
846
  get: () => executeQuery(ctx, plan),
620
- first: () => Effect5.map(
847
+ first: () => Effect6.map(
621
848
  executeQuery(ctx, { ...plan, limit: 1 }),
622
849
  (results) => results.length > 0 ? Option2.some(results[0]) : Option2.none()
623
850
  ),
624
- count: () => Effect5.map(executeQuery(ctx, plan), (results) => results.length),
851
+ count: () => Effect6.map(executeQuery(ctx, plan), (results) => results.length),
625
852
  watch: () => watchQuery(ctx, plan)
626
853
  };
627
854
  }
@@ -727,7 +954,7 @@ function replayState(recordId, events, stopAtId) {
727
954
  return state;
728
955
  }
729
956
  function promoteToSnapshot(storage, collection2, recordId, target, allSorted) {
730
- return Effect6.gen(function* () {
957
+ return Effect7.gen(function* () {
731
958
  const chronological = sortChronologically(allSorted);
732
959
  const state = replayState(recordId, chronological, target.id);
733
960
  if (state) {
@@ -736,7 +963,7 @@ function promoteToSnapshot(storage, collection2, recordId, target, allSorted) {
736
963
  });
737
964
  }
738
965
  function pruneEvents(storage, collection2, recordId, retention) {
739
- return Effect6.gen(function* () {
966
+ return Effect7.gen(function* () {
740
967
  const events = yield* storage.getEventsByRecord(collection2, recordId);
741
968
  if (events.length <= retention) return;
742
969
  const sorted = [...events].sort((a, b) => b.createdAt - a.createdAt || (a.id < b.id ? 1 : -1));
@@ -757,8 +984,8 @@ function mapRecord(record) {
757
984
  }
758
985
  function createCollectionHandle(def, storage, watchCtx, validator, partialValidator, makeEventId, localAuthor, onWrite, logLevel = "None") {
759
986
  const collectionName = def.name;
760
- const withLog = (effect) => Effect6.provideService(effect, References.MinimumLogLevel, logLevel);
761
- const commitEvent = (event) => Effect6.gen(function* () {
987
+ const withLog = (effect) => Effect7.provideService(effect, References.MinimumLogLevel, logLevel);
988
+ const commitEvent = (event) => Effect7.gen(function* () {
762
989
  yield* storage.putEvent(event);
763
990
  yield* applyEvent(storage, event);
764
991
  if (onWrite) yield* onWrite(event);
@@ -770,7 +997,7 @@ function createCollectionHandle(def, storage, watchCtx, validator, partialValida
770
997
  });
771
998
  const handle = {
772
999
  add: (data) => withLog(
773
- Effect6.gen(function* () {
1000
+ Effect7.gen(function* () {
774
1001
  const id = uuidv7();
775
1002
  const fullRecord = { id, ...data };
776
1003
  yield* validator(fullRecord);
@@ -784,7 +1011,7 @@ function createCollectionHandle(def, storage, watchCtx, validator, partialValida
784
1011
  author: localAuthor
785
1012
  };
786
1013
  yield* commitEvent(event);
787
- yield* Effect6.logDebug("Record added", {
1014
+ yield* Effect7.logDebug("Record added", {
788
1015
  collection: collectionName,
789
1016
  recordId: id,
790
1017
  data: fullRecord
@@ -793,7 +1020,7 @@ function createCollectionHandle(def, storage, watchCtx, validator, partialValida
793
1020
  })
794
1021
  ),
795
1022
  update: (id, data) => withLog(
796
- Effect6.gen(function* () {
1023
+ Effect7.gen(function* () {
797
1024
  const existing = yield* storage.getRecord(collectionName, id);
798
1025
  if (!existing || existing._d) {
799
1026
  return yield* new NotFoundError({
@@ -805,27 +1032,26 @@ function createCollectionHandle(def, storage, watchCtx, validator, partialValida
805
1032
  const { _d, _u, _a, _e, ...existingFields } = existing;
806
1033
  const merged = { ...existingFields, ...data, id };
807
1034
  yield* validator(merged);
808
- const diff = deepDiff(existingFields, merged);
809
1035
  const event = {
810
1036
  id: makeEventId(),
811
1037
  collection: collectionName,
812
1038
  recordId: id,
813
1039
  kind: "u",
814
- data: diff ?? { id },
1040
+ data: merged,
815
1041
  createdAt: Date.now(),
816
1042
  author: localAuthor
817
1043
  };
818
1044
  yield* commitEvent(event);
819
- yield* Effect6.logDebug("Record updated", {
1045
+ yield* Effect7.logDebug("Record updated", {
820
1046
  collection: collectionName,
821
1047
  recordId: id,
822
- data: diff
1048
+ data: merged
823
1049
  });
824
1050
  yield* pruneEvents(storage, collectionName, id, def.eventRetention);
825
1051
  })
826
1052
  ),
827
1053
  delete: (id) => withLog(
828
- Effect6.gen(function* () {
1054
+ Effect7.gen(function* () {
829
1055
  const existing = yield* storage.getRecord(collectionName, id);
830
1056
  if (!existing || existing._d) {
831
1057
  return yield* new NotFoundError({
@@ -843,11 +1069,11 @@ function createCollectionHandle(def, storage, watchCtx, validator, partialValida
843
1069
  author: localAuthor
844
1070
  };
845
1071
  yield* commitEvent(event);
846
- yield* Effect6.logDebug("Record deleted", { collection: collectionName, recordId: id });
1072
+ yield* Effect7.logDebug("Record deleted", { collection: collectionName, recordId: id });
847
1073
  yield* pruneEvents(storage, collectionName, id, def.eventRetention);
848
1074
  })
849
1075
  ),
850
- undo: (id) => Effect6.gen(function* () {
1076
+ undo: (id) => Effect7.gen(function* () {
851
1077
  const existing = yield* storage.getRecord(collectionName, id);
852
1078
  if (!existing) {
853
1079
  return yield* new NotFoundError({ collection: collectionName, id });
@@ -872,7 +1098,7 @@ function createCollectionHandle(def, storage, watchCtx, validator, partialValida
872
1098
  yield* commitEvent(event);
873
1099
  yield* pruneEvents(storage, collectionName, id, def.eventRetention);
874
1100
  }),
875
- get: (id) => Effect6.gen(function* () {
1101
+ get: (id) => Effect7.gen(function* () {
876
1102
  const record = yield* storage.getRecord(collectionName, id);
877
1103
  if (!record || record._d) {
878
1104
  return yield* new NotFoundError({
@@ -882,11 +1108,11 @@ function createCollectionHandle(def, storage, watchCtx, validator, partialValida
882
1108
  }
883
1109
  return mapRecord(record);
884
1110
  }),
885
- first: () => Effect6.map(storage.getAllRecords(collectionName), (all) => {
1111
+ first: () => Effect7.map(storage.getAllRecords(collectionName), (all) => {
886
1112
  const found = all.find((r) => !r._d);
887
1113
  return found ? Option3.some(mapRecord(found)) : Option3.none();
888
1114
  }),
889
- count: () => Effect6.map(storage.getAllRecords(collectionName), (all) => all.filter((r) => !r._d).length),
1115
+ count: () => Effect7.map(storage.getAllRecords(collectionName), (all) => all.filter((r) => !r._d).length),
890
1116
  watch: () => watchCollection(watchCtx, storage, collectionName, void 0, mapRecord),
891
1117
  where: (fieldName) => createWhereClause(
892
1118
  storage,
@@ -909,12 +1135,12 @@ function createCollectionHandle(def, storage, watchCtx, validator, partialValida
909
1135
  }
910
1136
 
911
1137
  // src/sync/sync-service.ts
912
- import { Duration, Effect as Effect8, Layer, Option as Option5, References as References2, Ref as Ref3, Schedule } from "effect";
1138
+ import { Duration, Effect as Effect9, Layer, Option as Option5, References as References2, Ref as Ref3, Schedule } from "effect";
913
1139
  import { unwrapEvent } from "nostr-tools/nip59";
914
1140
  import { GiftWrap as GiftWrap2 } from "nostr-tools/kinds";
915
1141
 
916
1142
  // src/sync/negentropy.ts
917
- import { Effect as Effect7 } from "effect";
1143
+ import { Effect as Effect8 } from "effect";
918
1144
 
919
1145
  // src/vendor/negentropy.js
920
1146
  var PROTOCOL_VERSION = 97;
@@ -1401,14 +1627,14 @@ function itemCompare(a, b) {
1401
1627
  }
1402
1628
 
1403
1629
  // src/sync/negentropy.ts
1404
- import { hexToBytes as hexToBytes2 } from "@noble/hashes/utils.js";
1630
+ import { hexToBytes as hexToBytes3 } from "@noble/hashes/utils.js";
1405
1631
  import { GiftWrap } from "nostr-tools/kinds";
1406
1632
  function reconcileWithRelay(storage, relay, relayUrl, publicKeys) {
1407
- return Effect7.gen(function* () {
1633
+ return Effect8.gen(function* () {
1408
1634
  const allGiftWraps = yield* storage.getAllGiftWraps();
1409
1635
  const storageVector = new NegentropyStorageVector();
1410
1636
  for (const gw of allGiftWraps) {
1411
- storageVector.insert(gw.createdAt, hexToBytes2(gw.id));
1637
+ storageVector.insert(gw.createdAt, hexToBytes3(gw.id));
1412
1638
  }
1413
1639
  storageVector.seal();
1414
1640
  const neg = new Negentropy(storageVector, 0);
@@ -1419,7 +1645,7 @@ function reconcileWithRelay(storage, relay, relayUrl, publicKeys) {
1419
1645
  const allHaveIds = [];
1420
1646
  const allNeedIds = [];
1421
1647
  const subId = `neg-${Date.now()}`;
1422
- const initialMsg = yield* Effect7.tryPromise({
1648
+ const initialMsg = yield* Effect8.tryPromise({
1423
1649
  try: () => neg.initiate(),
1424
1650
  catch: (e) => new SyncError({
1425
1651
  message: `Negentropy initiate failed: ${e instanceof Error ? e.message : String(e)}`,
@@ -1431,7 +1657,7 @@ function reconcileWithRelay(storage, relay, relayUrl, publicKeys) {
1431
1657
  while (currentMsg !== null) {
1432
1658
  const response = yield* relay.sendNegMsg(relayUrl, subId, filter, currentMsg);
1433
1659
  if (response.msgHex === null) break;
1434
- const reconcileResult = yield* Effect7.tryPromise({
1660
+ const reconcileResult = yield* Effect8.tryPromise({
1435
1661
  try: () => neg.reconcile(response.msgHex),
1436
1662
  catch: (e) => new SyncError({
1437
1663
  message: `Negentropy reconcile failed: ${e instanceof Error ? e.message : String(e)}`,
@@ -1444,13 +1670,13 @@ function reconcileWithRelay(storage, relay, relayUrl, publicKeys) {
1444
1670
  for (const id of needIds) allNeedIds.push(id);
1445
1671
  currentMsg = nextMsg;
1446
1672
  }
1447
- yield* Effect7.logDebug("Negentropy reconciliation complete", {
1673
+ yield* Effect8.logDebug("Negentropy reconciliation complete", {
1448
1674
  relay: relayUrl,
1449
1675
  have: allHaveIds.length,
1450
1676
  need: allNeedIds.length
1451
1677
  });
1452
1678
  return { haveIds: allHaveIds, needIds: allNeedIds };
1453
- }).pipe(Effect7.withLogSpan("tablinum.negentropy"));
1679
+ }).pipe(Effect8.withLogSpan("tablinum.negentropy"));
1454
1680
  }
1455
1681
 
1456
1682
  // src/db/key-rotation.ts
@@ -1544,52 +1770,52 @@ function createSyncHandle(storage, giftWrapHandle, relay, publishQueue, syncStat
1544
1770
  kind: "create"
1545
1771
  });
1546
1772
  const forkHandled = (effect) => {
1547
- Effect8.runFork(
1773
+ Effect9.runFork(
1548
1774
  effect.pipe(
1549
- Effect8.tapError((e) => Effect8.sync(() => onSyncError?.(e))),
1550
- Effect8.ignore,
1551
- Effect8.provide(logLayer),
1552
- Effect8.forkIn(scope)
1775
+ Effect9.tapError((e) => Effect9.sync(() => onSyncError?.(e))),
1776
+ Effect9.ignore,
1777
+ Effect9.provide(logLayer),
1778
+ Effect9.forkIn(scope)
1553
1779
  )
1554
1780
  );
1555
1781
  };
1556
1782
  let autoFlushActive = false;
1557
- const autoFlushEffect = Effect8.gen(function* () {
1783
+ const autoFlushEffect = Effect9.gen(function* () {
1558
1784
  const size = yield* publishQueue.size();
1559
1785
  if (size === 0) return;
1560
1786
  yield* syncStatus.set("syncing");
1561
1787
  yield* publishQueue.flush(relayUrls);
1562
1788
  const remaining = yield* publishQueue.size();
1563
- if (remaining > 0) yield* Effect8.fail("pending");
1789
+ if (remaining > 0) yield* Effect9.fail("pending");
1564
1790
  }).pipe(
1565
- Effect8.ensuring(syncStatus.set("idle")),
1566
- Effect8.retry({ schedule: Schedule.exponential(5e3).pipe(Schedule.jittered), times: 10 }),
1567
- Effect8.ignore
1791
+ Effect9.ensuring(syncStatus.set("idle")),
1792
+ Effect9.retry({ schedule: Schedule.exponential(5e3).pipe(Schedule.jittered), times: 10 }),
1793
+ Effect9.ignore
1568
1794
  );
1569
1795
  const scheduleAutoFlush = () => {
1570
1796
  if (autoFlushActive) return;
1571
1797
  autoFlushActive = true;
1572
1798
  forkHandled(
1573
1799
  autoFlushEffect.pipe(
1574
- Effect8.ensuring(
1575
- Effect8.sync(() => {
1800
+ Effect9.ensuring(
1801
+ Effect9.sync(() => {
1576
1802
  autoFlushActive = false;
1577
1803
  })
1578
1804
  )
1579
1805
  )
1580
1806
  );
1581
1807
  };
1582
- const shouldRejectWrite = (authorPubkey) => Effect8.gen(function* () {
1808
+ const shouldRejectWrite = (authorPubkey) => Effect9.gen(function* () {
1583
1809
  const memberRecord = yield* storage.getRecord("_members", authorPubkey);
1584
1810
  if (!memberRecord) return false;
1585
1811
  return !!memberRecord.removedAt;
1586
1812
  });
1587
1813
  const storeGiftWrapShell = (gw) => storage.putGiftWrap({ id: gw.id, event: gw, createdAt: gw.created_at });
1588
- const unwrapGiftWrap = (remoteGw) => Effect8.gen(function* () {
1814
+ const unwrapGiftWrap = (remoteGw) => Effect9.gen(function* () {
1589
1815
  const existing = yield* storage.getGiftWrap(remoteGw.id);
1590
1816
  if (existing) return null;
1591
1817
  const rumor = yield* giftWrapHandle.unwrap(remoteGw).pipe(
1592
- Effect8.orElseSucceed(() => null)
1818
+ Effect9.orElseSucceed(() => null)
1593
1819
  );
1594
1820
  if (!rumor) {
1595
1821
  yield* storeGiftWrapShell(remoteGw);
@@ -1617,7 +1843,7 @@ function createSyncHandle(storage, giftWrapHandle, relay, publishQueue, syncStat
1617
1843
  recordId: dTag.substring(colonIdx + 1)
1618
1844
  };
1619
1845
  });
1620
- const applyUnwrappedEvent = (uw) => Effect8.gen(function* () {
1846
+ const applyUnwrappedEvent = (uw) => Effect9.gen(function* () {
1621
1847
  const { giftWrap: remoteGw, rumor, collection: collectionName, recordId } = uw;
1622
1848
  const retention = knownCollections.get(collectionName);
1623
1849
  if (retention === void 0) {
@@ -1627,17 +1853,17 @@ function createSyncHandle(storage, giftWrapHandle, relay, publishQueue, syncStat
1627
1853
  if (rumor.pubkey) {
1628
1854
  const reject = yield* shouldRejectWrite(rumor.pubkey);
1629
1855
  if (reject) {
1630
- yield* Effect8.logWarning("Rejected write from removed member", {
1856
+ yield* Effect9.logWarning("Rejected write from removed member", {
1631
1857
  author: rumor.pubkey.slice(0, 12)
1632
1858
  });
1633
1859
  yield* storeGiftWrapShell(remoteGw);
1634
1860
  return null;
1635
1861
  }
1636
1862
  }
1637
- const parsed = yield* Effect8.try({
1863
+ const parsed = yield* Effect9.try({
1638
1864
  try: () => JSON.parse(rumor.content),
1639
1865
  catch: () => void 0
1640
- }).pipe(Effect8.orElseSucceed(() => void 0));
1866
+ }).pipe(Effect9.orElseSucceed(() => void 0));
1641
1867
  if (parsed === void 0) {
1642
1868
  yield* storeGiftWrapShell(remoteGw);
1643
1869
  return null;
@@ -1669,7 +1895,7 @@ function createSyncHandle(storage, giftWrapHandle, relay, publishQueue, syncStat
1669
1895
  if (didApply && (kind === "u" || kind === "d")) {
1670
1896
  yield* pruneEvents(storage, collectionName, recordId, retention);
1671
1897
  }
1672
- yield* Effect8.logDebug("Processed gift wrap", {
1898
+ yield* Effect9.logDebug("Processed gift wrap", {
1673
1899
  collection: collectionName,
1674
1900
  recordId,
1675
1901
  kind,
@@ -1680,33 +1906,33 @@ function createSyncHandle(storage, giftWrapHandle, relay, publishQueue, syncStat
1680
1906
  }
1681
1907
  return collectionName;
1682
1908
  });
1683
- const reconcileRelay = (url, pubKeys) => Effect8.gen(function* () {
1684
- yield* Effect8.logDebug("Syncing relay", { relay: url });
1909
+ const reconcileRelay = (url, pubKeys) => Effect9.gen(function* () {
1910
+ yield* Effect9.logDebug("Syncing relay", { relay: url });
1685
1911
  const { haveIds, needIds } = yield* reconcileWithRelay(
1686
1912
  storage,
1687
1913
  relay,
1688
1914
  url,
1689
1915
  Array.from(pubKeys)
1690
1916
  ).pipe(
1691
- Effect8.tapError((err) => Effect8.sync(() => onSyncError?.(err))),
1692
- Effect8.orElseSucceed(() => ({ haveIds: [], needIds: [] }))
1917
+ Effect9.tapError((err) => Effect9.sync(() => onSyncError?.(err))),
1918
+ Effect9.orElseSucceed(() => ({ haveIds: [], needIds: [] }))
1693
1919
  );
1694
- yield* Effect8.logDebug("Relay reconciliation result", {
1920
+ yield* Effect9.logDebug("Relay reconciliation result", {
1695
1921
  relay: url,
1696
1922
  need: needIds.length,
1697
1923
  have: haveIds.length
1698
1924
  });
1699
1925
  const events = needIds.length > 0 ? yield* relay.fetchEvents(needIds, url).pipe(
1700
- Effect8.tapError((err) => Effect8.sync(() => onSyncError?.(err))),
1701
- Effect8.orElseSucceed(() => [])
1926
+ Effect9.tapError((err) => Effect9.sync(() => onSyncError?.(err))),
1927
+ Effect9.orElseSucceed(() => [])
1702
1928
  ) : [];
1703
1929
  return {
1704
1930
  events,
1705
1931
  haveIds: haveIds.map((id) => ({ id, url }))
1706
1932
  };
1707
- }).pipe(Effect8.withLogSpan("tablinum.reconcileRelay"));
1708
- const syncAllRelays = (pubKeys, changedCollections) => Effect8.gen(function* () {
1709
- const results = yield* Effect8.forEach(
1933
+ }).pipe(Effect9.withLogSpan("tablinum.reconcileRelay"));
1934
+ const syncAllRelays = (pubKeys, changedCollections) => Effect9.gen(function* () {
1935
+ const results = yield* Effect9.forEach(
1710
1936
  relayUrls,
1711
1937
  (url) => reconcileRelay(url, pubKeys),
1712
1938
  { concurrency: "unbounded" }
@@ -1723,44 +1949,44 @@ function createSyncHandle(storage, giftWrapHandle, relay, publishQueue, syncStat
1723
1949
  }
1724
1950
  const unwrapped = [];
1725
1951
  for (const gw of allGiftWraps) {
1726
- const result = yield* unwrapGiftWrap(gw).pipe(Effect8.orElseSucceed(() => null));
1952
+ const result = yield* unwrapGiftWrap(gw).pipe(Effect9.orElseSucceed(() => null));
1727
1953
  if (result) unwrapped.push(result);
1728
1954
  }
1729
1955
  unwrapped.sort((a, b) => a.rumor.created_at - b.rumor.created_at || (a.rumor.id < b.rumor.id ? -1 : 1));
1730
1956
  for (const event of unwrapped) {
1731
1957
  const collection2 = yield* applyUnwrappedEvent(event).pipe(
1732
- Effect8.orElseSucceed(() => null)
1958
+ Effect9.orElseSucceed(() => null)
1733
1959
  );
1734
1960
  if (collection2) changedCollections.add(collection2);
1735
1961
  }
1736
1962
  const allHaveIds = results.flatMap((r) => r.haveIds);
1737
- yield* Effect8.forEach(
1963
+ yield* Effect9.forEach(
1738
1964
  allHaveIds,
1739
- ({ id, url }) => Effect8.gen(function* () {
1965
+ ({ id, url }) => Effect9.gen(function* () {
1740
1966
  const gw = yield* storage.getGiftWrap(id);
1741
1967
  if (!gw?.event) return;
1742
1968
  yield* relay.publish(gw.event, [url]).pipe(
1743
- Effect8.tapError((err) => Effect8.sync(() => onSyncError?.(err))),
1744
- Effect8.ignore
1969
+ Effect9.tapError((err) => Effect9.sync(() => onSyncError?.(err))),
1970
+ Effect9.ignore
1745
1971
  );
1746
1972
  }),
1747
1973
  { concurrency: "unbounded", discard: true }
1748
1974
  );
1749
- }).pipe(Effect8.withLogSpan("tablinum.syncAllRelays"));
1750
- const processGiftWrap = (remoteGw) => Effect8.gen(function* () {
1975
+ }).pipe(Effect9.withLogSpan("tablinum.syncAllRelays"));
1976
+ const processGiftWrap = (remoteGw) => Effect9.gen(function* () {
1751
1977
  const uw = yield* unwrapGiftWrap(remoteGw);
1752
1978
  if (!uw) return null;
1753
1979
  return yield* applyUnwrappedEvent(uw);
1754
1980
  });
1755
- const processRealtimeGiftWrap = (remoteGw) => Effect8.gen(function* () {
1756
- const collection2 = yield* processGiftWrap(remoteGw).pipe(Effect8.orElseSucceed(() => null));
1981
+ const processRealtimeGiftWrap = (remoteGw) => Effect9.gen(function* () {
1982
+ const collection2 = yield* processGiftWrap(remoteGw).pipe(Effect9.orElseSucceed(() => null));
1757
1983
  if (collection2) {
1758
1984
  yield* notifyCollectionUpdated(collection2);
1759
1985
  }
1760
1986
  });
1761
- const processRotationGiftWrap = (remoteGw) => Effect8.gen(function* () {
1762
- const unwrapResult = yield* Effect8.result(
1763
- Effect8.try({
1987
+ const processRotationGiftWrap = (remoteGw) => Effect9.gen(function* () {
1988
+ const unwrapResult = yield* Effect9.result(
1989
+ Effect9.try({
1764
1990
  try: () => unwrapEvent(remoteGw, personalPrivateKey),
1765
1991
  catch: (e) => new CryptoError({
1766
1992
  message: `Rotation unwrap failed: ${e instanceof Error ? e.message : String(e)}`,
@@ -1810,73 +2036,73 @@ function createSyncHandle(storage, giftWrapHandle, relay, publishQueue, syncStat
1810
2036
  yield* handle.addEpochSubscription(epoch.publicKey);
1811
2037
  return true;
1812
2038
  });
1813
- const subscribeAcrossRelays = (filter, onEvent) => Effect8.forEach(
2039
+ const subscribeAcrossRelays = (filter, onEvent) => Effect9.forEach(
1814
2040
  relayUrls,
1815
- (url) => Effect8.gen(function* () {
2041
+ (url) => Effect9.gen(function* () {
1816
2042
  yield* relay.subscribe(filter, url, (event) => {
1817
2043
  forkHandled(onEvent(event));
1818
2044
  }).pipe(
1819
- Effect8.tapError((err) => Effect8.sync(() => onSyncError?.(err))),
1820
- Effect8.ignore
2045
+ Effect9.tapError((err) => Effect9.sync(() => onSyncError?.(err))),
2046
+ Effect9.ignore
1821
2047
  );
1822
2048
  }),
1823
2049
  { concurrency: "unbounded", discard: true }
1824
2050
  );
1825
2051
  let healingActive = false;
1826
- const healingEffect = Effect8.gen(function* () {
2052
+ const healingEffect = Effect9.gen(function* () {
1827
2053
  if (!healingActive) return;
1828
2054
  const status = yield* syncStatus.get();
1829
2055
  if (status === "syncing") return;
1830
2056
  yield* syncStatus.set("syncing");
1831
- yield* Effect8.gen(function* () {
2057
+ yield* Effect9.gen(function* () {
1832
2058
  const pubKeys = getSubscriptionPubKeys();
1833
2059
  const changedCollections = /* @__PURE__ */ new Set();
1834
2060
  yield* syncAllRelays(pubKeys, changedCollections);
1835
2061
  if (changedCollections.size > 0) {
1836
2062
  yield* notifyReplayComplete(watchCtx, [...changedCollections]);
1837
2063
  }
1838
- }).pipe(Effect8.ensuring(syncStatus.set("idle")));
1839
- }).pipe(Effect8.ignore);
2064
+ }).pipe(Effect9.ensuring(syncStatus.set("idle")));
2065
+ }).pipe(Effect9.ignore);
1840
2066
  const handle = {
1841
- sync: () => Effect8.gen(function* () {
1842
- yield* Effect8.logInfo("Sync started");
2067
+ sync: () => Effect9.gen(function* () {
2068
+ yield* Effect9.logInfo("Sync started");
1843
2069
  yield* syncStatus.set("syncing");
1844
2070
  yield* Ref3.set(watchCtx.replayingRef, true);
1845
2071
  const changedCollections = /* @__PURE__ */ new Set();
1846
- yield* Effect8.gen(function* () {
2072
+ yield* Effect9.gen(function* () {
1847
2073
  const pubKeys = getSubscriptionPubKeys();
1848
2074
  yield* syncAllRelays(pubKeys, changedCollections);
1849
- yield* publishQueue.flush(relayUrls).pipe(Effect8.ignore);
2075
+ yield* publishQueue.flush(relayUrls).pipe(Effect9.ignore);
1850
2076
  }).pipe(
1851
- Effect8.ensuring(
1852
- Effect8.gen(function* () {
2077
+ Effect9.ensuring(
2078
+ Effect9.gen(function* () {
1853
2079
  yield* notifyReplayComplete(watchCtx, [...changedCollections]);
1854
2080
  yield* syncStatus.set("idle");
1855
2081
  })
1856
2082
  )
1857
2083
  );
1858
- yield* Effect8.logInfo("Sync complete", { changed: [...changedCollections] });
1859
- }).pipe(Effect8.withLogSpan("tablinum.sync")),
1860
- publishLocal: (giftWrap) => Effect8.gen(function* () {
2084
+ yield* Effect9.logInfo("Sync complete", { changed: [...changedCollections] });
2085
+ }).pipe(Effect9.withLogSpan("tablinum.sync")),
2086
+ publishLocal: (giftWrap) => Effect9.gen(function* () {
1861
2087
  if (!giftWrap.event) return;
1862
2088
  yield* relay.publish(giftWrap.event, relayUrls).pipe(
1863
- Effect8.tapError(
2089
+ Effect9.tapError(
1864
2090
  () => storage.putGiftWrap(giftWrap).pipe(
1865
- Effect8.andThen(publishQueue.enqueue(giftWrap.id)),
1866
- Effect8.andThen(Effect8.sync(() => scheduleAutoFlush()))
2091
+ Effect9.andThen(publishQueue.enqueue(giftWrap.id)),
2092
+ Effect9.andThen(Effect9.sync(() => scheduleAutoFlush()))
1867
2093
  )
1868
2094
  ),
1869
- Effect8.tapError((err) => Effect8.sync(() => onSyncError?.(err))),
1870
- Effect8.ignore
2095
+ Effect9.tapError((err) => Effect9.sync(() => onSyncError?.(err))),
2096
+ Effect9.ignore
1871
2097
  );
1872
2098
  }),
1873
- startSubscription: () => Effect8.gen(function* () {
2099
+ startSubscription: () => Effect9.gen(function* () {
1874
2100
  const pubKeys = getSubscriptionPubKeys();
1875
2101
  yield* subscribeAcrossRelays({ kinds: [GiftWrap2], "#p": pubKeys }, processRealtimeGiftWrap);
1876
2102
  if (!pubKeys.includes(personalPublicKey)) {
1877
2103
  yield* subscribeAcrossRelays(
1878
2104
  { kinds: [GiftWrap2], "#p": [personalPublicKey] },
1879
- (event) => Effect8.result(processRotationGiftWrap(event)).pipe(Effect8.asVoid)
2105
+ (event) => Effect9.result(processRotationGiftWrap(event)).pipe(Effect9.asVoid)
1880
2106
  );
1881
2107
  }
1882
2108
  }),
@@ -1885,10 +2111,10 @@ function createSyncHandle(storage, giftWrapHandle, relay, publishQueue, syncStat
1885
2111
  if (healingActive) return;
1886
2112
  healingActive = true;
1887
2113
  forkHandled(
1888
- Effect8.sleep(Duration.minutes(5)).pipe(
1889
- Effect8.andThen(healingEffect),
1890
- Effect8.repeat(Schedule.spaced(Duration.minutes(5))),
1891
- Effect8.ensuring(Effect8.sync(() => {
2114
+ Effect9.sleep(Duration.minutes(5)).pipe(
2115
+ Effect9.andThen(healingEffect),
2116
+ Effect9.repeat(Schedule.spaced(Duration.minutes(5))),
2117
+ Effect9.ensuring(Effect9.sync(() => {
1892
2118
  healingActive = false;
1893
2119
  }))
1894
2120
  )
@@ -1900,8 +2126,8 @@ function createSyncHandle(storage, giftWrapHandle, relay, publishQueue, syncStat
1900
2126
  };
1901
2127
  forkHandled(
1902
2128
  publishQueue.size().pipe(
1903
- Effect8.flatMap(
1904
- (size) => Effect8.sync(() => {
2129
+ Effect9.flatMap(
2130
+ (size) => Effect9.sync(() => {
1905
2131
  if (size > 0) scheduleAutoFlush();
1906
2132
  })
1907
2133
  )
@@ -1911,7 +2137,7 @@ function createSyncHandle(storage, giftWrapHandle, relay, publishQueue, syncStat
1911
2137
  }
1912
2138
 
1913
2139
  // src/db/members.ts
1914
- import { Effect as Effect9, Option as Option6, Schema as Schema5 } from "effect";
2140
+ import { Effect as Effect10, Option as Option6, Schema as Schema5 } from "effect";
1915
2141
  var optionalString = {
1916
2142
  _tag: "FieldDef",
1917
2143
  kind: "string",
@@ -1960,15 +2186,15 @@ var AuthorProfileSchema = Schema5.Struct({
1960
2186
  });
1961
2187
  var decodeAuthorProfile = Schema5.decodeUnknownEffect(Schema5.fromJsonString(AuthorProfileSchema));
1962
2188
  function fetchAuthorProfile(relay, relayUrls, pubkey) {
1963
- return Effect9.gen(function* () {
2189
+ return Effect10.gen(function* () {
1964
2190
  for (const url of relayUrls) {
1965
- const result = yield* Effect9.result(
2191
+ const result = yield* Effect10.result(
1966
2192
  relay.fetchByFilter({ kinds: [0], authors: [pubkey], limit: 1 }, url)
1967
2193
  );
1968
2194
  if (result._tag === "Success" && result.success.length > 0) {
1969
2195
  return yield* decodeAuthorProfile(result.success[0].content).pipe(
1970
- Effect9.map(Option6.some),
1971
- Effect9.orElseSucceed(() => Option6.none())
2196
+ Effect10.map(Option6.some),
2197
+ Effect10.orElseSucceed(() => Option6.none())
1972
2198
  );
1973
2199
  }
1974
2200
  }
@@ -1976,6 +2202,20 @@ function fetchAuthorProfile(relay, relayUrls, pubkey) {
1976
2202
  });
1977
2203
  }
1978
2204
 
2205
+ // src/sync/deletion.ts
2206
+ import { finalizeEvent } from "nostr-tools/pure";
2207
+ function createDeletionEvent(targetEventIds, signingKey) {
2208
+ return finalizeEvent(
2209
+ {
2210
+ kind: 5,
2211
+ content: "",
2212
+ tags: targetEventIds.map((id) => ["e", id]),
2213
+ created_at: Math.floor(Date.now() / 1e3)
2214
+ },
2215
+ signingKey
2216
+ );
2217
+ }
2218
+
1979
2219
  // src/services/Identity.ts
1980
2220
  import { ServiceMap as ServiceMap3 } from "effect";
1981
2221
  var Identity = class extends ServiceMap3.Service()("tablinum/Identity") {
@@ -2018,15 +2258,15 @@ var SyncStatus = class extends ServiceMap9.Service()(
2018
2258
  };
2019
2259
 
2020
2260
  // src/layers/IdentityLive.ts
2021
- import { Effect as Effect11, Layer as Layer2 } from "effect";
2022
- import { hexToBytes as hexToBytes3 } from "@noble/hashes/utils.js";
2261
+ import { Effect as Effect12, Layer as Layer2 } from "effect";
2262
+ import { hexToBytes as hexToBytes4 } from "@noble/hashes/utils.js";
2023
2263
 
2024
2264
  // src/db/identity.ts
2025
- import { Effect as Effect10 } from "effect";
2265
+ import { Effect as Effect11 } from "effect";
2026
2266
  import { getPublicKey as getPublicKey2 } from "nostr-tools/pure";
2027
- import { bytesToHex as bytesToHex2 } from "@noble/hashes/utils.js";
2267
+ import { bytesToHex as bytesToHex3 } from "@noble/hashes/utils.js";
2028
2268
  function createIdentity(suppliedKey) {
2029
- return Effect10.gen(function* () {
2269
+ return Effect11.gen(function* () {
2030
2270
  let privateKey;
2031
2271
  if (suppliedKey) {
2032
2272
  if (suppliedKey.length !== 32) {
@@ -2039,8 +2279,8 @@ function createIdentity(suppliedKey) {
2039
2279
  privateKey = new Uint8Array(32);
2040
2280
  crypto.getRandomValues(privateKey);
2041
2281
  }
2042
- const privateKeyHex = bytesToHex2(privateKey);
2043
- const publicKey = yield* Effect10.try({
2282
+ const privateKeyHex = bytesToHex3(privateKey);
2283
+ const publicKey = yield* Effect11.try({
2044
2284
  try: () => getPublicKey2(privateKey),
2045
2285
  catch: (e) => new CryptoError({
2046
2286
  message: `Failed to derive public key: ${e instanceof Error ? e.message : String(e)}`,
@@ -2058,14 +2298,14 @@ function createIdentity(suppliedKey) {
2058
2298
  // src/layers/IdentityLive.ts
2059
2299
  var IdentityLive = Layer2.effect(
2060
2300
  Identity,
2061
- Effect11.gen(function* () {
2301
+ Effect12.gen(function* () {
2062
2302
  const config = yield* Config;
2063
2303
  const storage = yield* Storage;
2064
2304
  const idbKey = yield* storage.getMeta("identity_key");
2065
- const resolvedKey = config.privateKey ?? (typeof idbKey === "string" && idbKey.length === 64 ? hexToBytes3(idbKey) : void 0);
2305
+ const resolvedKey = config.privateKey ?? (typeof idbKey === "string" && idbKey.length === 64 ? hexToBytes4(idbKey) : void 0);
2066
2306
  const identity = yield* createIdentity(resolvedKey);
2067
2307
  yield* storage.putMeta("identity_key", identity.exportKey());
2068
- yield* Effect11.logInfo("Identity loaded", {
2308
+ yield* Effect12.logInfo("Identity loaded", {
2069
2309
  publicKey: identity.publicKey.slice(0, 12) + "...",
2070
2310
  source: config.privateKey ? "config" : resolvedKey ? "storage" : "generated"
2071
2311
  });
@@ -2074,12 +2314,12 @@ var IdentityLive = Layer2.effect(
2074
2314
  );
2075
2315
 
2076
2316
  // src/layers/EpochStoreLive.ts
2077
- import { Effect as Effect12, Layer as Layer3, Option as Option7 } from "effect";
2317
+ import { Effect as Effect13, Layer as Layer3, Option as Option7 } from "effect";
2078
2318
  import { generateSecretKey as generateSecretKey2 } from "nostr-tools/pure";
2079
- import { bytesToHex as bytesToHex3 } from "@noble/hashes/utils.js";
2319
+ import { bytesToHex as bytesToHex4 } from "@noble/hashes/utils.js";
2080
2320
  var EpochStoreLive = Layer3.effect(
2081
2321
  EpochStore,
2082
- Effect12.gen(function* () {
2322
+ Effect13.gen(function* () {
2083
2323
  const config = yield* Config;
2084
2324
  const identity = yield* Identity;
2085
2325
  const storage = yield* Storage;
@@ -2098,7 +2338,7 @@ var EpochStoreLive = Layer3.effect(
2098
2338
  return existing !== void 0 && existing.privateKey === ek.key;
2099
2339
  });
2100
2340
  if (configIsSubset) {
2101
- yield* Effect12.logInfo("Epoch store loaded", {
2341
+ yield* Effect13.logInfo("Epoch store loaded", {
2102
2342
  source: "storage",
2103
2343
  epochs: idbStore.epochs.size
2104
2344
  });
@@ -2107,271 +2347,28 @@ var EpochStoreLive = Layer3.effect(
2107
2347
  }
2108
2348
  const store2 = createEpochStoreFromInputs(config.epochKeys);
2109
2349
  yield* storage.putMeta("epochs", stringifyEpochStore(store2));
2110
- yield* Effect12.logInfo("Epoch store loaded", { source: "config", epochs: store2.epochs.size });
2350
+ yield* Effect13.logInfo("Epoch store loaded", { source: "config", epochs: store2.epochs.size });
2111
2351
  return store2;
2112
2352
  }
2113
2353
  if (idbStore) {
2114
- yield* Effect12.logInfo("Epoch store loaded", {
2354
+ yield* Effect13.logInfo("Epoch store loaded", {
2115
2355
  source: "storage",
2116
2356
  epochs: idbStore.epochs.size
2117
2357
  });
2118
2358
  return idbStore;
2119
2359
  }
2120
2360
  const store = createEpochStoreFromInputs(
2121
- [{ epochId: EpochId("epoch-0"), key: bytesToHex3(generateSecretKey2()) }],
2361
+ [{ epochId: EpochId("epoch-0"), key: bytesToHex4(generateSecretKey2()) }],
2122
2362
  { createdBy: identity.publicKey }
2123
2363
  );
2124
2364
  yield* storage.putMeta("epochs", stringifyEpochStore(store));
2125
- yield* Effect12.logInfo("Epoch store loaded", { source: "generated", epochs: store.epochs.size });
2365
+ yield* Effect13.logInfo("Epoch store loaded", { source: "generated", epochs: store.epochs.size });
2126
2366
  return store;
2127
2367
  })
2128
2368
  );
2129
2369
 
2130
2370
  // src/layers/StorageLive.ts
2131
2371
  import { Effect as Effect14, Layer as Layer4 } from "effect";
2132
-
2133
- // src/storage/idb.ts
2134
- import { Effect as Effect13 } from "effect";
2135
- import { openDB } from "idb";
2136
-
2137
- // src/sync/compact-event.ts
2138
- import { bytesToHex as bytesToHex4, hexToBytes as hexToBytes4 } from "@noble/hashes/utils.js";
2139
- var VERSION = 1;
2140
- var HEADER_SIZE = 133;
2141
- function base64ToBytes(base64) {
2142
- const binary = atob(base64);
2143
- const bytes = new Uint8Array(binary.length);
2144
- for (let i = 0; i < binary.length; i++) {
2145
- bytes[i] = binary.charCodeAt(i);
2146
- }
2147
- return bytes;
2148
- }
2149
- function bytesToBase64(bytes) {
2150
- let binary = "";
2151
- for (let i = 0; i < bytes.length; i++) {
2152
- binary += String.fromCharCode(bytes[i]);
2153
- }
2154
- return btoa(binary);
2155
- }
2156
- function packEvent(event) {
2157
- const pubkey = hexToBytes4(event.pubkey);
2158
- const sig = hexToBytes4(event.sig);
2159
- const recipientTag = event.tags.find((t) => t[0] === "p");
2160
- if (!recipientTag) throw new Error("Gift wrap missing #p tag");
2161
- if (event.tags.some((t) => t[0] !== "p")) {
2162
- throw new Error("Gift wrap has unexpected non-p tags; compact encoding would lose them");
2163
- }
2164
- const recipient = hexToBytes4(recipientTag[1]);
2165
- const createdAtBuf = new Uint8Array(4);
2166
- new DataView(createdAtBuf.buffer).setUint32(0, event.created_at, false);
2167
- const content = base64ToBytes(event.content);
2168
- const result = new Uint8Array(HEADER_SIZE + content.length);
2169
- result[0] = VERSION;
2170
- result.set(pubkey, 1);
2171
- result.set(sig, 33);
2172
- result.set(recipient, 97);
2173
- result.set(createdAtBuf, 129);
2174
- result.set(content, HEADER_SIZE);
2175
- return result;
2176
- }
2177
- function unpackEvent(id, compact) {
2178
- const version = compact[0];
2179
- if (version !== VERSION) throw new Error(`Unknown compact event version: ${version}`);
2180
- const pubkey = bytesToHex4(compact.slice(1, 33));
2181
- const sig = bytesToHex4(compact.slice(33, 97));
2182
- const recipient = bytesToHex4(compact.slice(97, 129));
2183
- const dv = new DataView(compact.buffer, compact.byteOffset + 129, 4);
2184
- const createdAt = dv.getUint32(0, false);
2185
- const content = bytesToBase64(compact.slice(HEADER_SIZE));
2186
- return {
2187
- id,
2188
- pubkey,
2189
- sig,
2190
- created_at: createdAt,
2191
- kind: 1059,
2192
- tags: [["p", recipient]],
2193
- content
2194
- };
2195
- }
2196
-
2197
- // src/storage/idb.ts
2198
- var DB_NAME = "tablinum";
2199
- function storeName(collection2) {
2200
- return `col_${collection2}`;
2201
- }
2202
- function computeSchemaSig(schema) {
2203
- return Object.entries(schema).sort(([a], [b]) => a.localeCompare(b)).map(([name, def]) => {
2204
- const indices = [...def.indices ?? []].sort().join(",");
2205
- return `${name}:${indices}`;
2206
- }).join("|");
2207
- }
2208
- function wrap(label, fn) {
2209
- return Effect13.tryPromise({
2210
- try: fn,
2211
- catch: (e) => new StorageError({
2212
- message: `IndexedDB ${label} failed: ${e instanceof Error ? e.message : String(e)}`,
2213
- cause: e
2214
- })
2215
- });
2216
- }
2217
- function upgradeSchema(database, schema, tx) {
2218
- if (!database.objectStoreNames.contains("_meta")) {
2219
- database.createObjectStore("_meta");
2220
- }
2221
- if (!database.objectStoreNames.contains("events")) {
2222
- const events = database.createObjectStore("events", { keyPath: "id" });
2223
- events.createIndex("by-record", ["collection", "recordId"]);
2224
- }
2225
- if (!database.objectStoreNames.contains("giftwraps")) {
2226
- database.createObjectStore("giftwraps", { keyPath: "id" });
2227
- }
2228
- const expectedStores = /* @__PURE__ */ new Set();
2229
- for (const [, def] of Object.entries(schema)) {
2230
- const sn = storeName(def.name);
2231
- expectedStores.add(sn);
2232
- if (!database.objectStoreNames.contains(sn)) {
2233
- const store = database.createObjectStore(sn, { keyPath: "id" });
2234
- for (const idx of def.indices ?? []) {
2235
- store.createIndex(idx, idx);
2236
- }
2237
- } else {
2238
- const store = tx.objectStore(sn);
2239
- const existingIndices = new Set(Array.from(store.indexNames));
2240
- const wantedIndices = new Set(def.indices ?? []);
2241
- for (const idx of existingIndices) {
2242
- if (!wantedIndices.has(idx)) store.deleteIndex(idx);
2243
- }
2244
- for (const idx of wantedIndices) {
2245
- if (!existingIndices.has(idx)) store.createIndex(idx, idx);
2246
- }
2247
- }
2248
- }
2249
- for (const existing of Array.from(database.objectStoreNames)) {
2250
- if (existing.startsWith("col_") && !expectedStores.has(existing)) {
2251
- database.deleteObjectStore(existing);
2252
- }
2253
- }
2254
- tx.objectStore("_meta").put(computeSchemaSig(schema), "schema_sig");
2255
- }
2256
- function openIDBStorage(dbName, schema) {
2257
- return Effect13.gen(function* () {
2258
- const name = dbName ?? DB_NAME;
2259
- const schemaSig = computeSchemaSig(schema);
2260
- if (typeof indexedDB === "undefined") {
2261
- return yield* Effect13.fail(
2262
- new StorageError({
2263
- message: "IndexedDB is not available in this environment"
2264
- })
2265
- );
2266
- }
2267
- const probeDb = yield* Effect13.tryPromise({
2268
- try: () => openDB(name),
2269
- catch: (e) => new StorageError({ message: "Failed to open IndexedDB", cause: e })
2270
- });
2271
- const currentVersion = probeDb.version;
2272
- let needsUpgrade = true;
2273
- if (probeDb.objectStoreNames.contains("_meta")) {
2274
- const storedSig = yield* Effect13.tryPromise({
2275
- try: () => probeDb.get("_meta", "schema_sig"),
2276
- catch: () => new StorageError({ message: "Failed to read schema meta" })
2277
- }).pipe(Effect13.catch(() => Effect13.succeed(void 0)));
2278
- needsUpgrade = storedSig !== schemaSig;
2279
- }
2280
- probeDb.close();
2281
- const db = needsUpgrade ? yield* Effect13.tryPromise({
2282
- try: () => openDB(name, currentVersion + 1, {
2283
- upgrade(database, _oldVersion, _newVersion, transaction) {
2284
- upgradeSchema(database, schema, transaction);
2285
- }
2286
- }),
2287
- catch: (e) => new StorageError({ message: "Failed to open IndexedDB", cause: e })
2288
- }) : yield* Effect13.tryPromise({
2289
- try: () => openDB(name),
2290
- catch: (e) => new StorageError({ message: "Failed to open IndexedDB", cause: e })
2291
- });
2292
- yield* Effect13.addFinalizer(() => Effect13.sync(() => db.close()));
2293
- const handle = {
2294
- putRecord: (collection2, record) => wrap("putRecord", () => db.put(storeName(collection2), record).then(() => void 0)),
2295
- getRecord: (collection2, id) => wrap("getRecord", () => db.get(storeName(collection2), id)),
2296
- getAllRecords: (collection2) => wrap("getAllRecords", () => db.getAll(storeName(collection2))),
2297
- countRecords: (collection2) => wrap("countRecords", () => db.count(storeName(collection2))),
2298
- clearRecords: (collection2) => wrap("clearRecords", () => db.clear(storeName(collection2))),
2299
- getByIndex: (collection2, indexName, value) => wrap("getByIndex", () => db.getAllFromIndex(storeName(collection2), indexName, value)),
2300
- getByIndexRange: (collection2, indexName, range) => wrap("getByIndexRange", () => db.getAllFromIndex(storeName(collection2), indexName, range)),
2301
- getAllSorted: (collection2, indexName, direction) => wrap("getAllSorted", async () => {
2302
- const sn = storeName(collection2);
2303
- const tx = db.transaction(sn, "readonly");
2304
- const store = tx.objectStore(sn);
2305
- const index = store.index(indexName);
2306
- const results = [];
2307
- let cursor = await index.openCursor(null, direction ?? "next");
2308
- while (cursor) {
2309
- results.push(cursor.value);
2310
- cursor = await cursor.continue();
2311
- }
2312
- return results;
2313
- }),
2314
- putEvent: (event) => wrap("putEvent", () => db.put("events", event).then(() => void 0)),
2315
- getEvent: (id) => wrap("getEvent", () => db.get("events", id)),
2316
- getAllEvents: () => wrap("getAllEvents", () => db.getAll("events")),
2317
- getEventsByRecord: (collection2, recordId) => wrap(
2318
- "getEventsByRecord",
2319
- () => db.getAllFromIndex("events", "by-record", [collection2, recordId])
2320
- ),
2321
- putGiftWrap: (gw) => wrap("putGiftWrap", async () => {
2322
- if (gw.event) {
2323
- const compact = packEvent(gw.event);
2324
- await db.put("giftwraps", { id: gw.id, compact, createdAt: gw.createdAt });
2325
- } else {
2326
- await db.put("giftwraps", { id: gw.id, createdAt: gw.createdAt });
2327
- }
2328
- }),
2329
- getGiftWrap: (id) => wrap("getGiftWrap", async () => {
2330
- const raw = await db.get("giftwraps", id);
2331
- if (!raw) return void 0;
2332
- if (raw.compact) {
2333
- return { id: raw.id, event: unpackEvent(raw.id, raw.compact), createdAt: raw.createdAt };
2334
- }
2335
- if (raw.event) {
2336
- const compact = packEvent(raw.event);
2337
- await db.put("giftwraps", { id: raw.id, compact, createdAt: raw.createdAt });
2338
- return { id: raw.id, event: raw.event, createdAt: raw.createdAt };
2339
- }
2340
- return { id: raw.id, createdAt: raw.createdAt };
2341
- }),
2342
- getAllGiftWraps: () => wrap("getAllGiftWraps", async () => {
2343
- const raws = await db.getAll("giftwraps");
2344
- const results = [];
2345
- for (const raw of raws) {
2346
- if (raw.compact) {
2347
- results.push({ id: raw.id, event: unpackEvent(raw.id, raw.compact), createdAt: raw.createdAt });
2348
- } else if (raw.event) {
2349
- const compact = packEvent(raw.event);
2350
- await db.put("giftwraps", { id: raw.id, compact, createdAt: raw.createdAt });
2351
- results.push({ id: raw.id, event: raw.event, createdAt: raw.createdAt });
2352
- } else {
2353
- results.push({ id: raw.id, createdAt: raw.createdAt });
2354
- }
2355
- }
2356
- return results;
2357
- }),
2358
- deleteGiftWrap: (id) => wrap("deleteGiftWrap", () => db.delete("giftwraps", id).then(() => void 0)),
2359
- deleteEvent: (id) => wrap("deleteEvent", () => db.delete("events", id).then(() => void 0)),
2360
- stripEventData: (id) => wrap("stripEventData", async () => {
2361
- const existing = await db.get("events", id);
2362
- if (existing) {
2363
- await db.put("events", { ...existing, data: null });
2364
- }
2365
- }),
2366
- getMeta: (key) => wrap("getMeta", () => db.get("_meta", key)),
2367
- putMeta: (key, value) => wrap("putMeta", () => db.put("_meta", value, key).then(() => void 0)),
2368
- close: () => Effect13.sync(() => db.close())
2369
- };
2370
- return handle;
2371
- });
2372
- }
2373
-
2374
- // src/layers/StorageLive.ts
2375
2372
  var StorageLive = Layer4.effect(
2376
2373
  Storage,
2377
2374
  Effect14.gen(function* () {
@@ -2908,6 +2905,12 @@ var TablinumLive = Layer9.effect(
2908
2905
  }
2909
2906
  const gw = wrapResult.success;
2910
2907
  yield* storage.putGiftWrap({ id: gw.id, event: gw, createdAt: gw.created_at });
2908
+ const metaKey = `gw_record:${event.collection}:${event.recordId}`;
2909
+ const prevMapping = yield* storage.getMeta(metaKey).pipe(
2910
+ Effect21.orElseSucceed(() => void 0)
2911
+ );
2912
+ const epochPubKey = gw.tags.find((t) => t[0] === "p")?.[1];
2913
+ yield* storage.putMeta(metaKey, { gwId: gw.id, epochPubKey });
2911
2914
  yield* Effect21.forkIn(
2912
2915
  Effect21.gen(function* () {
2913
2916
  const publishResult = yield* Effect21.result(
@@ -2920,10 +2923,80 @@ var TablinumLive = Layer9.effect(
2920
2923
  if (publishResult._tag === "Failure") {
2921
2924
  reportSyncError(config.onSyncError, publishResult.failure);
2922
2925
  }
2926
+ if (prevMapping?.epochPubKey) {
2927
+ const signingKey = getDecryptionKey(epochStore, prevMapping.epochPubKey);
2928
+ if (signingKey) {
2929
+ const deletionEvent = createDeletionEvent(
2930
+ [prevMapping.gwId],
2931
+ signingKey
2932
+ );
2933
+ yield* relay.publish(deletionEvent, [...config.relays]).pipe(
2934
+ Effect21.tapError(
2935
+ (e) => Effect21.sync(() => reportSyncError(config.onSyncError, e))
2936
+ ),
2937
+ Effect21.ignore
2938
+ );
2939
+ }
2940
+ yield* storage.deleteGiftWrap(prevMapping.gwId).pipe(Effect21.ignore);
2941
+ }
2923
2942
  }),
2924
2943
  scope
2925
2944
  );
2926
2945
  });
2946
+ const republishAllUnderCurrentEpoch = () => Effect21.gen(function* () {
2947
+ const oldGwDeletions = [];
2948
+ for (const [, def] of allSchemaEntries) {
2949
+ const collectionName = def.name;
2950
+ const allRecords = yield* storage.getAllRecords(collectionName);
2951
+ for (const record of allRecords) {
2952
+ const recordId = record.id;
2953
+ const { _d, _u, _a, _e, ...fields } = record;
2954
+ const content = _d ? JSON.stringify(null) : JSON.stringify(fields);
2955
+ const dTag = `${collectionName}:${recordId}`;
2956
+ const wrapResult = yield* Effect21.result(
2957
+ giftWrap.wrap({
2958
+ kind: 1,
2959
+ content,
2960
+ tags: [["d", dTag]],
2961
+ created_at: Math.floor(Date.now() / 1e3)
2962
+ })
2963
+ );
2964
+ if (wrapResult._tag === "Failure") continue;
2965
+ const gw = wrapResult.success;
2966
+ yield* storage.putGiftWrap({ id: gw.id, event: gw, createdAt: gw.created_at });
2967
+ const metaKey = `gw_record:${collectionName}:${recordId}`;
2968
+ const prevMapping = yield* storage.getMeta(metaKey).pipe(
2969
+ Effect21.orElseSucceed(() => void 0)
2970
+ );
2971
+ if (prevMapping) oldGwDeletions.push(prevMapping);
2972
+ const epochPubKey = gw.tags.find((t) => t[0] === "p")?.[1];
2973
+ yield* storage.putMeta(metaKey, { gwId: gw.id, epochPubKey });
2974
+ yield* relay.publish(gw, [...config.relays]).pipe(
2975
+ Effect21.tapError((e) => Effect21.sync(() => reportSyncError(config.onSyncError, e))),
2976
+ Effect21.ignore
2977
+ );
2978
+ }
2979
+ }
2980
+ const byEpoch = /* @__PURE__ */ new Map();
2981
+ for (const { gwId, epochPubKey } of oldGwDeletions) {
2982
+ const ids = byEpoch.get(epochPubKey) ?? [];
2983
+ ids.push(gwId);
2984
+ byEpoch.set(epochPubKey, ids);
2985
+ }
2986
+ for (const [epochPubKey, gwIds] of byEpoch) {
2987
+ const signingKey = getDecryptionKey(epochStore, epochPubKey);
2988
+ if (signingKey) {
2989
+ const deletionEvent = createDeletionEvent(gwIds, signingKey);
2990
+ yield* relay.publish(deletionEvent, [...config.relays]).pipe(
2991
+ Effect21.tapError((e) => Effect21.sync(() => reportSyncError(config.onSyncError, e))),
2992
+ Effect21.ignore
2993
+ );
2994
+ }
2995
+ for (const gwId of gwIds) {
2996
+ yield* storage.deleteGiftWrap(gwId).pipe(Effect21.ignore);
2997
+ }
2998
+ }
2999
+ });
2927
3000
  const knownAuthors = /* @__PURE__ */ new Set();
2928
3001
  const putMemberRecord = (record) => Effect21.gen(function* () {
2929
3002
  const existing = yield* storage.getRecord("_members", record.id);
@@ -3011,6 +3084,13 @@ var TablinumLive = Layer9.effect(
3011
3084
  addedInEpoch: getCurrentEpoch(epochStore).id
3012
3085
  });
3013
3086
  }
3087
+ const migrated = yield* storage.getMeta("migration_gw_republish").pipe(
3088
+ Effect21.orElseSucceed(() => void 0)
3089
+ );
3090
+ if (!migrated) {
3091
+ yield* republishAllUnderCurrentEpoch().pipe(Effect21.ignore);
3092
+ yield* storage.putMeta("migration_gw_republish", true);
3093
+ }
3014
3094
  const withLog = (effect) => Effect21.provideService(effect, References3.MinimumLogLevel, config.logLevel);
3015
3095
  const ensureOpen = (effect) => withLog(
3016
3096
  Effect21.gen(function* () {
@@ -3050,6 +3130,61 @@ var TablinumLive = Layer9.effect(
3050
3130
  yield* Scope4.close(scope, Exit.void);
3051
3131
  })
3052
3132
  ),
3133
+ destroy: () => withLog(
3134
+ Effect21.gen(function* () {
3135
+ if (!(yield* Ref5.get(closedRef))) {
3136
+ yield* Ref5.set(closedRef, true);
3137
+ syncHandle.stopHealing();
3138
+ yield* Scope4.close(scope, Exit.void);
3139
+ }
3140
+ yield* deleteIDBStorage(config.dbName);
3141
+ })
3142
+ ),
3143
+ leave: () => withLog(
3144
+ Effect21.gen(function* () {
3145
+ if (yield* Ref5.get(closedRef)) {
3146
+ return yield* new SyncError({ message: "Database is closed", phase: "leave" });
3147
+ }
3148
+ const allMembers = yield* storage.getAllRecords("_members");
3149
+ const activeMembers = allMembers.filter(
3150
+ (member) => !member.removedAt && member.id !== identity.publicKey
3151
+ );
3152
+ const activePubkeys = activeMembers.map((member) => member.id);
3153
+ const result = createRotation(
3154
+ epochStore,
3155
+ identity.privateKey,
3156
+ identity.publicKey,
3157
+ activePubkeys,
3158
+ [identity.publicKey]
3159
+ );
3160
+ addEpoch(epochStore, result.epoch);
3161
+ epochStore.currentEpochId = result.epoch.id;
3162
+ yield* storage.putMeta("epochs", stringifyEpochStore(epochStore));
3163
+ const memberRecord = yield* storage.getRecord("_members", identity.publicKey);
3164
+ yield* putMemberRecord({
3165
+ ...memberRecord ?? {
3166
+ id: identity.publicKey,
3167
+ addedAt: 0,
3168
+ addedInEpoch: EpochId("epoch-0")
3169
+ },
3170
+ removedAt: Date.now(),
3171
+ removedInEpoch: result.epoch.id
3172
+ });
3173
+ yield* republishAllUnderCurrentEpoch();
3174
+ yield* Effect21.forEach(
3175
+ result.wrappedEvents,
3176
+ (wrappedEvent) => relay.publish(wrappedEvent, [...config.relays]).pipe(
3177
+ Effect21.tapError((e) => Effect21.sync(() => reportSyncError(config.onSyncError, e))),
3178
+ Effect21.ignore
3179
+ ),
3180
+ { discard: true }
3181
+ );
3182
+ yield* Ref5.set(closedRef, true);
3183
+ syncHandle.stopHealing();
3184
+ yield* Scope4.close(scope, Exit.void);
3185
+ yield* deleteIDBStorage(config.dbName);
3186
+ })
3187
+ ),
3053
3188
  rebuild: () => ensureOpen(
3054
3189
  rebuild(
3055
3190
  storage,
@@ -3110,6 +3245,7 @@ var TablinumLive = Layer9.effect(
3110
3245
  removedAt: Date.now(),
3111
3246
  removedInEpoch: result.epoch.id
3112
3247
  });
3248
+ yield* republishAllUnderCurrentEpoch();
3113
3249
  yield* Effect21.forEach(
3114
3250
  result.wrappedEvents,
3115
3251
  (wrappedEvent) => relay.publish(wrappedEvent, [...config.relays]).pipe(
@@ -3187,6 +3323,9 @@ function validateConfig(config) {
3187
3323
  }
3188
3324
  });
3189
3325
  }
3326
+ function deleteDatabase(dbName) {
3327
+ return deleteIDBStorage(DatabaseName(dbName ?? "tablinum"));
3328
+ }
3190
3329
  function createTablinum(config) {
3191
3330
  return Effect22.gen(function* () {
3192
3331
  yield* validateConfig(config);
@@ -3259,6 +3398,7 @@ export {
3259
3398
  collection,
3260
3399
  createTablinum,
3261
3400
  decodeInvite,
3401
+ deleteDatabase,
3262
3402
  encodeInvite,
3263
3403
  field
3264
3404
  };