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.
@@ -230,9 +230,6 @@ var NotFoundError = class extends Data.TaggedError("NotFoundError") {
230
230
  var ClosedError = class extends Data.TaggedError("ClosedError") {
231
231
  };
232
232
 
233
- // src/svelte/tablinum.svelte.ts
234
- import { Effect as Effect25, Exit as Exit2, References as References6, Scope as Scope6 } from "effect";
235
-
236
233
  // src/db/create-tablinum.ts
237
234
  import { Effect as Effect22, Layer as Layer10, References as References4, ServiceMap as ServiceMap10 } from "effect";
238
235
 
@@ -261,6 +258,257 @@ function resolveRuntimeConfig(source) {
261
258
  );
262
259
  }
263
260
 
261
+ // src/storage/idb.ts
262
+ import { Effect as Effect2 } from "effect";
263
+ import { openDB, deleteDB } from "idb";
264
+
265
+ // src/sync/compact-event.ts
266
+ import { bytesToHex as bytesToHex2, hexToBytes as hexToBytes2 } from "@noble/hashes/utils.js";
267
+ var VERSION = 1;
268
+ var HEADER_SIZE = 133;
269
+ function base64ToBytes(base64) {
270
+ const binary = atob(base64);
271
+ const bytes = new Uint8Array(binary.length);
272
+ for (let i = 0; i < binary.length; i++) {
273
+ bytes[i] = binary.charCodeAt(i);
274
+ }
275
+ return bytes;
276
+ }
277
+ function bytesToBase64(bytes) {
278
+ let binary = "";
279
+ for (let i = 0; i < bytes.length; i++) {
280
+ binary += String.fromCharCode(bytes[i]);
281
+ }
282
+ return btoa(binary);
283
+ }
284
+ function packEvent(event) {
285
+ const pubkey = hexToBytes2(event.pubkey);
286
+ const sig = hexToBytes2(event.sig);
287
+ const recipientTag = event.tags.find((t) => t[0] === "p");
288
+ if (!recipientTag) throw new Error("Gift wrap missing #p tag");
289
+ if (event.tags.some((t) => t[0] !== "p")) {
290
+ throw new Error("Gift wrap has unexpected non-p tags; compact encoding would lose them");
291
+ }
292
+ const recipient = hexToBytes2(recipientTag[1]);
293
+ const createdAtBuf = new Uint8Array(4);
294
+ new DataView(createdAtBuf.buffer).setUint32(0, event.created_at, false);
295
+ const content = base64ToBytes(event.content);
296
+ const result = new Uint8Array(HEADER_SIZE + content.length);
297
+ result[0] = VERSION;
298
+ result.set(pubkey, 1);
299
+ result.set(sig, 33);
300
+ result.set(recipient, 97);
301
+ result.set(createdAtBuf, 129);
302
+ result.set(content, HEADER_SIZE);
303
+ return result;
304
+ }
305
+ function unpackEvent(id, compact) {
306
+ const version = compact[0];
307
+ if (version !== VERSION) throw new Error(`Unknown compact event version: ${version}`);
308
+ const pubkey = bytesToHex2(compact.slice(1, 33));
309
+ const sig = bytesToHex2(compact.slice(33, 97));
310
+ const recipient = bytesToHex2(compact.slice(97, 129));
311
+ const dv = new DataView(compact.buffer, compact.byteOffset + 129, 4);
312
+ const createdAt = dv.getUint32(0, false);
313
+ const content = bytesToBase64(compact.slice(HEADER_SIZE));
314
+ return {
315
+ id,
316
+ pubkey,
317
+ sig,
318
+ created_at: createdAt,
319
+ kind: 1059,
320
+ tags: [["p", recipient]],
321
+ content
322
+ };
323
+ }
324
+
325
+ // src/storage/idb.ts
326
+ var DB_NAME = "tablinum";
327
+ function storeName(collection2) {
328
+ return `col_${collection2}`;
329
+ }
330
+ function computeSchemaSig(schema) {
331
+ return Object.entries(schema).sort(([a], [b]) => a.localeCompare(b)).map(([name, def]) => {
332
+ const indices = [...def.indices ?? []].sort().join(",");
333
+ return `${name}:${indices}`;
334
+ }).join("|");
335
+ }
336
+ function wrap(label, fn) {
337
+ return Effect2.tryPromise({
338
+ try: fn,
339
+ catch: (e) => new StorageError({
340
+ message: `IndexedDB ${label} failed: ${e instanceof Error ? e.message : String(e)}`,
341
+ cause: e
342
+ })
343
+ });
344
+ }
345
+ function upgradeSchema(database, schema, tx) {
346
+ if (!database.objectStoreNames.contains("_meta")) {
347
+ database.createObjectStore("_meta");
348
+ }
349
+ if (!database.objectStoreNames.contains("events")) {
350
+ const events = database.createObjectStore("events", { keyPath: "id" });
351
+ events.createIndex("by-record", ["collection", "recordId"]);
352
+ }
353
+ if (!database.objectStoreNames.contains("giftwraps")) {
354
+ database.createObjectStore("giftwraps", { keyPath: "id" });
355
+ }
356
+ const expectedStores = /* @__PURE__ */ new Set();
357
+ for (const [, def] of Object.entries(schema)) {
358
+ const sn = storeName(def.name);
359
+ expectedStores.add(sn);
360
+ if (!database.objectStoreNames.contains(sn)) {
361
+ const store = database.createObjectStore(sn, { keyPath: "id" });
362
+ for (const idx of def.indices ?? []) {
363
+ store.createIndex(idx, idx);
364
+ }
365
+ } else {
366
+ const store = tx.objectStore(sn);
367
+ const existingIndices = new Set(Array.from(store.indexNames));
368
+ const wantedIndices = new Set(def.indices ?? []);
369
+ for (const idx of existingIndices) {
370
+ if (!wantedIndices.has(idx)) store.deleteIndex(idx);
371
+ }
372
+ for (const idx of wantedIndices) {
373
+ if (!existingIndices.has(idx)) store.createIndex(idx, idx);
374
+ }
375
+ }
376
+ }
377
+ for (const existing of Array.from(database.objectStoreNames)) {
378
+ if (existing.startsWith("col_") && !expectedStores.has(existing)) {
379
+ database.deleteObjectStore(existing);
380
+ }
381
+ }
382
+ tx.objectStore("_meta").put(computeSchemaSig(schema), "schema_sig");
383
+ }
384
+ function deleteIDBStorage(dbName) {
385
+ if (typeof indexedDB === "undefined") {
386
+ return Effect2.fail(
387
+ new StorageError({
388
+ message: "IndexedDB is not available in this environment"
389
+ })
390
+ );
391
+ }
392
+ return wrap("deleteDatabase", () => deleteDB(dbName));
393
+ }
394
+ function openIDBStorage(dbName, schema) {
395
+ return Effect2.gen(function* () {
396
+ const name = dbName ?? DB_NAME;
397
+ const schemaSig = computeSchemaSig(schema);
398
+ if (typeof indexedDB === "undefined") {
399
+ return yield* Effect2.fail(
400
+ new StorageError({
401
+ message: "IndexedDB is not available in this environment"
402
+ })
403
+ );
404
+ }
405
+ const probeDb = yield* Effect2.tryPromise({
406
+ try: () => openDB(name),
407
+ catch: (e) => new StorageError({ message: "Failed to open IndexedDB", cause: e })
408
+ });
409
+ const currentVersion = probeDb.version;
410
+ let needsUpgrade = true;
411
+ if (probeDb.objectStoreNames.contains("_meta")) {
412
+ const storedSig = yield* Effect2.tryPromise({
413
+ try: () => probeDb.get("_meta", "schema_sig"),
414
+ catch: () => new StorageError({ message: "Failed to read schema meta" })
415
+ }).pipe(Effect2.catch(() => Effect2.succeed(void 0)));
416
+ needsUpgrade = storedSig !== schemaSig;
417
+ }
418
+ probeDb.close();
419
+ const db = needsUpgrade ? yield* Effect2.tryPromise({
420
+ try: () => openDB(name, currentVersion + 1, {
421
+ upgrade(database, _oldVersion, _newVersion, transaction) {
422
+ upgradeSchema(database, schema, transaction);
423
+ }
424
+ }),
425
+ catch: (e) => new StorageError({ message: "Failed to open IndexedDB", cause: e })
426
+ }) : yield* Effect2.tryPromise({
427
+ try: () => openDB(name),
428
+ catch: (e) => new StorageError({ message: "Failed to open IndexedDB", cause: e })
429
+ });
430
+ yield* Effect2.addFinalizer(() => Effect2.sync(() => db.close()));
431
+ const handle = {
432
+ putRecord: (collection2, record) => wrap("putRecord", () => db.put(storeName(collection2), record).then(() => void 0)),
433
+ getRecord: (collection2, id) => wrap("getRecord", () => db.get(storeName(collection2), id)),
434
+ getAllRecords: (collection2) => wrap("getAllRecords", () => db.getAll(storeName(collection2))),
435
+ countRecords: (collection2) => wrap("countRecords", () => db.count(storeName(collection2))),
436
+ clearRecords: (collection2) => wrap("clearRecords", () => db.clear(storeName(collection2))),
437
+ getByIndex: (collection2, indexName, value) => wrap("getByIndex", () => db.getAllFromIndex(storeName(collection2), indexName, value)),
438
+ getByIndexRange: (collection2, indexName, range) => wrap("getByIndexRange", () => db.getAllFromIndex(storeName(collection2), indexName, range)),
439
+ getAllSorted: (collection2, indexName, direction) => wrap("getAllSorted", async () => {
440
+ const sn = storeName(collection2);
441
+ const tx = db.transaction(sn, "readonly");
442
+ const store = tx.objectStore(sn);
443
+ const index = store.index(indexName);
444
+ const results = [];
445
+ let cursor = await index.openCursor(null, direction ?? "next");
446
+ while (cursor) {
447
+ results.push(cursor.value);
448
+ cursor = await cursor.continue();
449
+ }
450
+ return results;
451
+ }),
452
+ putEvent: (event) => wrap("putEvent", () => db.put("events", event).then(() => void 0)),
453
+ getEvent: (id) => wrap("getEvent", () => db.get("events", id)),
454
+ getAllEvents: () => wrap("getAllEvents", () => db.getAll("events")),
455
+ getEventsByRecord: (collection2, recordId) => wrap(
456
+ "getEventsByRecord",
457
+ () => db.getAllFromIndex("events", "by-record", [collection2, recordId])
458
+ ),
459
+ putGiftWrap: (gw) => wrap("putGiftWrap", async () => {
460
+ if (gw.event) {
461
+ const compact = packEvent(gw.event);
462
+ await db.put("giftwraps", { id: gw.id, compact, createdAt: gw.createdAt });
463
+ } else {
464
+ await db.put("giftwraps", { id: gw.id, createdAt: gw.createdAt });
465
+ }
466
+ }),
467
+ getGiftWrap: (id) => wrap("getGiftWrap", async () => {
468
+ const raw = await db.get("giftwraps", id);
469
+ if (!raw) return void 0;
470
+ if (raw.compact) {
471
+ return { id: raw.id, event: unpackEvent(raw.id, raw.compact), createdAt: raw.createdAt };
472
+ }
473
+ if (raw.event) {
474
+ const compact = packEvent(raw.event);
475
+ await db.put("giftwraps", { id: raw.id, compact, createdAt: raw.createdAt });
476
+ return { id: raw.id, event: raw.event, createdAt: raw.createdAt };
477
+ }
478
+ return { id: raw.id, createdAt: raw.createdAt };
479
+ }),
480
+ getAllGiftWraps: () => wrap("getAllGiftWraps", async () => {
481
+ const raws = await db.getAll("giftwraps");
482
+ const results = [];
483
+ for (const raw of raws) {
484
+ if (raw.compact) {
485
+ results.push({ id: raw.id, event: unpackEvent(raw.id, raw.compact), createdAt: raw.createdAt });
486
+ } else if (raw.event) {
487
+ const compact = packEvent(raw.event);
488
+ await db.put("giftwraps", { id: raw.id, compact, createdAt: raw.createdAt });
489
+ results.push({ id: raw.id, event: raw.event, createdAt: raw.createdAt });
490
+ } else {
491
+ results.push({ id: raw.id, createdAt: raw.createdAt });
492
+ }
493
+ }
494
+ return results;
495
+ }),
496
+ deleteGiftWrap: (id) => wrap("deleteGiftWrap", () => db.delete("giftwraps", id).then(() => void 0)),
497
+ deleteEvent: (id) => wrap("deleteEvent", () => db.delete("events", id).then(() => void 0)),
498
+ stripEventData: (id) => wrap("stripEventData", async () => {
499
+ const existing = await db.get("events", id);
500
+ if (existing) {
501
+ await db.put("events", { ...existing, data: null });
502
+ }
503
+ }),
504
+ getMeta: (key) => wrap("getMeta", () => db.get("_meta", key)),
505
+ putMeta: (key, value) => wrap("putMeta", () => db.put("_meta", value, key).then(() => void 0)),
506
+ close: () => Effect2.sync(() => db.close())
507
+ };
508
+ return handle;
509
+ });
510
+ }
511
+
264
512
  // src/services/Config.ts
265
513
  import { ServiceMap } from "effect";
266
514
  var Config = class extends ServiceMap.Service()("tablinum/Config") {
@@ -277,16 +525,16 @@ var Tablinum = class extends ServiceMap2.Service()(
277
525
  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";
278
526
 
279
527
  // src/crud/watch.ts
280
- import { Effect as Effect2, PubSub, Ref, Stream } from "effect";
528
+ import { Effect as Effect3, PubSub, Ref, Stream } from "effect";
281
529
  function watchCollection(ctx, storage, collectionName, filter, mapRecord2) {
282
- const query = () => Effect2.map(storage.getAllRecords(collectionName), (all) => {
530
+ const query = () => Effect3.map(storage.getAllRecords(collectionName), (all) => {
283
531
  const filtered = all.filter((r) => !r._d && (filter ? filter(r) : true));
284
532
  return mapRecord2 ? filtered.map(mapRecord2) : filtered;
285
533
  });
286
534
  const changes = Stream.fromPubSub(ctx.pubsub).pipe(
287
535
  Stream.filter((event) => event.collection === collectionName),
288
536
  Stream.mapEffect(
289
- () => Effect2.gen(function* () {
537
+ () => Effect3.gen(function* () {
290
538
  const replaying = yield* Ref.get(ctx.replayingRef);
291
539
  if (replaying) return void 0;
292
540
  return yield* query();
@@ -295,18 +543,18 @@ function watchCollection(ctx, storage, collectionName, filter, mapRecord2) {
295
543
  Stream.filter((result) => result !== void 0)
296
544
  );
297
545
  return Stream.unwrap(
298
- Effect2.gen(function* () {
299
- yield* Effect2.sleep(0);
546
+ Effect3.gen(function* () {
547
+ yield* Effect3.sleep(0);
300
548
  const initial = yield* query();
301
549
  return Stream.concat(Stream.make(initial), changes);
302
550
  })
303
551
  );
304
552
  }
305
553
  function notifyChange(ctx, event) {
306
- return PubSub.publish(ctx.pubsub, event).pipe(Effect2.asVoid);
554
+ return PubSub.publish(ctx.pubsub, event).pipe(Effect3.asVoid);
307
555
  }
308
556
  function notifyReplayComplete(ctx, collections) {
309
- return Effect2.gen(function* () {
557
+ return Effect3.gen(function* () {
310
558
  yield* Ref.set(ctx.replayingRef, false);
311
559
  for (const collection2 of collections) {
312
560
  yield* notifyChange(ctx, {
@@ -319,7 +567,7 @@ function notifyReplayComplete(ctx, collections) {
319
567
  }
320
568
 
321
569
  // src/storage/records-store.ts
322
- import { Effect as Effect3 } from "effect";
570
+ import { Effect as Effect4 } from "effect";
323
571
 
324
572
  // src/storage/lww.ts
325
573
  function resolveWinner(existing, incoming) {
@@ -333,30 +581,6 @@ function resolveWinner(existing, incoming) {
333
581
  function isPlainObject(value) {
334
582
  return value !== null && typeof value === "object" && !Array.isArray(value);
335
583
  }
336
- function deepDiff(before, after) {
337
- const result = {};
338
- let hasChanges = false;
339
- for (const key of Object.keys(after)) {
340
- const a = before[key];
341
- const b = after[key];
342
- if (isPlainObject(a) && isPlainObject(b)) {
343
- const nested = deepDiff(a, b);
344
- if (nested !== null) {
345
- result[key] = nested;
346
- hasChanges = true;
347
- }
348
- } else if (Array.isArray(a) && Array.isArray(b)) {
349
- if (JSON.stringify(a) !== JSON.stringify(b)) {
350
- result[key] = b;
351
- hasChanges = true;
352
- }
353
- } else if (a !== b) {
354
- result[key] = b;
355
- hasChanges = true;
356
- }
357
- }
358
- return hasChanges ? result : null;
359
- }
360
584
  function deepMerge(target, source) {
361
585
  const result = { ...target };
362
586
  for (const key of Object.keys(source)) {
@@ -383,7 +607,7 @@ function buildRecord(event) {
383
607
  };
384
608
  }
385
609
  function applyEvent(storage, event) {
386
- return Effect3.gen(function* () {
610
+ return Effect4.gen(function* () {
387
611
  const existing = yield* storage.getRecord(event.collection, event.recordId);
388
612
  if (existing) {
389
613
  const existingMeta = {
@@ -403,7 +627,7 @@ function applyEvent(storage, event) {
403
627
  });
404
628
  }
405
629
  function rebuild(storage, collections) {
406
- return Effect3.gen(function* () {
630
+ return Effect4.gen(function* () {
407
631
  for (const col of collections) {
408
632
  yield* storage.clearRecords(col);
409
633
  }
@@ -419,7 +643,7 @@ function rebuild(storage, collections) {
419
643
  }
420
644
 
421
645
  // src/schema/validate.ts
422
- import { Effect as Effect4, Schema as Schema4 } from "effect";
646
+ import { Effect as Effect5, Schema as Schema4 } from "effect";
423
647
  function fieldDefToSchema(fd) {
424
648
  let base;
425
649
  switch (fd.kind) {
@@ -467,10 +691,10 @@ function buildStructSchema(def, options = {}) {
467
691
  function buildValidator(collectionName, def) {
468
692
  const decode = Schema4.decodeUnknownEffect(buildStructSchema(def, { includeId: true }));
469
693
  return (input) => decode(input).pipe(
470
- Effect4.map(
694
+ Effect5.map(
471
695
  (result) => result
472
696
  ),
473
- Effect4.mapError(
697
+ Effect5.mapError(
474
698
  (e) => new ValidationError({
475
699
  message: `Validation failed for collection "${collectionName}": ${e.message}`
476
700
  })
@@ -479,7 +703,7 @@ function buildValidator(collectionName, def) {
479
703
  }
480
704
  function buildPartialValidator(collectionName, def) {
481
705
  const decode = Schema4.decodeUnknownEffect(buildStructSchema(def, { allOptional: true }));
482
- return (input) => Effect4.gen(function* () {
706
+ return (input) => Effect5.gen(function* () {
483
707
  if (typeof input !== "object" || input === null) {
484
708
  return yield* new ValidationError({
485
709
  message: `Validation failed for collection "${collectionName}": expected an object`
@@ -494,8 +718,8 @@ function buildPartialValidator(collectionName, def) {
494
718
  });
495
719
  }
496
720
  return yield* decode(record).pipe(
497
- Effect4.map((result) => result),
498
- Effect4.mapError(
721
+ Effect5.map((result) => result),
722
+ Effect5.mapError(
499
723
  (e) => new ValidationError({
500
724
  message: `Validation failed for collection "${collectionName}": ${e.message}`
501
725
  })
@@ -505,7 +729,7 @@ function buildPartialValidator(collectionName, def) {
505
729
  }
506
730
 
507
731
  // src/crud/collection-handle.ts
508
- import { Effect as Effect6, Option as Option3, References } from "effect";
732
+ import { Effect as Effect7, Option as Option3, References } from "effect";
509
733
 
510
734
  // src/utils/uuid.ts
511
735
  var alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
@@ -538,12 +762,12 @@ function uuidv7() {
538
762
  }
539
763
 
540
764
  // src/crud/query-builder.ts
541
- import { Effect as Effect5, Option as Option2, Ref as Ref2, Stream as Stream2 } from "effect";
765
+ import { Effect as Effect6, Option as Option2, Ref as Ref2, Stream as Stream2 } from "effect";
542
766
  function emptyPlan() {
543
767
  return { filters: [] };
544
768
  }
545
769
  function executeQuery(ctx, plan) {
546
- return Effect5.gen(function* () {
770
+ return Effect6.gen(function* () {
547
771
  if (plan.fieldName) {
548
772
  const fieldDef = ctx.def.fields[plan.fieldName];
549
773
  if (!fieldDef) {
@@ -619,7 +843,7 @@ function watchQuery(ctx, plan) {
619
843
  const changes = Stream2.fromPubSub(ctx.watchCtx.pubsub).pipe(
620
844
  Stream2.filter((event) => event.collection === ctx.collectionName),
621
845
  Stream2.mapEffect(
622
- () => Effect5.gen(function* () {
846
+ () => Effect6.gen(function* () {
623
847
  const replaying = yield* Ref2.get(ctx.watchCtx.replayingRef);
624
848
  if (replaying) return void 0;
625
849
  return yield* query();
@@ -628,7 +852,7 @@ function watchQuery(ctx, plan) {
628
852
  Stream2.filter((result) => result !== void 0)
629
853
  );
630
854
  return Stream2.unwrap(
631
- Effect5.gen(function* () {
855
+ Effect6.gen(function* () {
632
856
  const initial = yield* query();
633
857
  return Stream2.concat(Stream2.make(initial), changes);
634
858
  })
@@ -654,11 +878,11 @@ function makeQueryBuilder(ctx, plan) {
654
878
  offset: (n) => makeQueryBuilder(ctx, { ...plan, offset: n }),
655
879
  limit: (n) => makeQueryBuilder(ctx, { ...plan, limit: n }),
656
880
  get: () => executeQuery(ctx, plan),
657
- first: () => Effect5.map(
881
+ first: () => Effect6.map(
658
882
  executeQuery(ctx, { ...plan, limit: 1 }),
659
883
  (results) => results.length > 0 ? Option2.some(results[0]) : Option2.none()
660
884
  ),
661
- count: () => Effect5.map(executeQuery(ctx, plan), (results) => results.length),
885
+ count: () => Effect6.map(executeQuery(ctx, plan), (results) => results.length),
662
886
  watch: () => watchQuery(ctx, plan)
663
887
  };
664
888
  }
@@ -764,7 +988,7 @@ function replayState(recordId, events, stopAtId) {
764
988
  return state;
765
989
  }
766
990
  function promoteToSnapshot(storage, collection2, recordId, target, allSorted) {
767
- return Effect6.gen(function* () {
991
+ return Effect7.gen(function* () {
768
992
  const chronological = sortChronologically(allSorted);
769
993
  const state = replayState(recordId, chronological, target.id);
770
994
  if (state) {
@@ -773,7 +997,7 @@ function promoteToSnapshot(storage, collection2, recordId, target, allSorted) {
773
997
  });
774
998
  }
775
999
  function pruneEvents(storage, collection2, recordId, retention) {
776
- return Effect6.gen(function* () {
1000
+ return Effect7.gen(function* () {
777
1001
  const events = yield* storage.getEventsByRecord(collection2, recordId);
778
1002
  if (events.length <= retention) return;
779
1003
  const sorted = [...events].sort((a, b) => b.createdAt - a.createdAt || (a.id < b.id ? 1 : -1));
@@ -794,8 +1018,8 @@ function mapRecord(record) {
794
1018
  }
795
1019
  function createCollectionHandle(def, storage, watchCtx, validator, partialValidator, makeEventId, localAuthor, onWrite, logLevel = "None") {
796
1020
  const collectionName = def.name;
797
- const withLog = (effect) => Effect6.provideService(effect, References.MinimumLogLevel, logLevel);
798
- const commitEvent = (event) => Effect6.gen(function* () {
1021
+ const withLog = (effect) => Effect7.provideService(effect, References.MinimumLogLevel, logLevel);
1022
+ const commitEvent = (event) => Effect7.gen(function* () {
799
1023
  yield* storage.putEvent(event);
800
1024
  yield* applyEvent(storage, event);
801
1025
  if (onWrite) yield* onWrite(event);
@@ -807,7 +1031,7 @@ function createCollectionHandle(def, storage, watchCtx, validator, partialValida
807
1031
  });
808
1032
  const handle = {
809
1033
  add: (data) => withLog(
810
- Effect6.gen(function* () {
1034
+ Effect7.gen(function* () {
811
1035
  const id = uuidv7();
812
1036
  const fullRecord = { id, ...data };
813
1037
  yield* validator(fullRecord);
@@ -821,7 +1045,7 @@ function createCollectionHandle(def, storage, watchCtx, validator, partialValida
821
1045
  author: localAuthor
822
1046
  };
823
1047
  yield* commitEvent(event);
824
- yield* Effect6.logDebug("Record added", {
1048
+ yield* Effect7.logDebug("Record added", {
825
1049
  collection: collectionName,
826
1050
  recordId: id,
827
1051
  data: fullRecord
@@ -830,7 +1054,7 @@ function createCollectionHandle(def, storage, watchCtx, validator, partialValida
830
1054
  })
831
1055
  ),
832
1056
  update: (id, data) => withLog(
833
- Effect6.gen(function* () {
1057
+ Effect7.gen(function* () {
834
1058
  const existing = yield* storage.getRecord(collectionName, id);
835
1059
  if (!existing || existing._d) {
836
1060
  return yield* new NotFoundError({
@@ -842,27 +1066,26 @@ function createCollectionHandle(def, storage, watchCtx, validator, partialValida
842
1066
  const { _d, _u, _a, _e, ...existingFields } = existing;
843
1067
  const merged = { ...existingFields, ...data, id };
844
1068
  yield* validator(merged);
845
- const diff = deepDiff(existingFields, merged);
846
1069
  const event = {
847
1070
  id: makeEventId(),
848
1071
  collection: collectionName,
849
1072
  recordId: id,
850
1073
  kind: "u",
851
- data: diff ?? { id },
1074
+ data: merged,
852
1075
  createdAt: Date.now(),
853
1076
  author: localAuthor
854
1077
  };
855
1078
  yield* commitEvent(event);
856
- yield* Effect6.logDebug("Record updated", {
1079
+ yield* Effect7.logDebug("Record updated", {
857
1080
  collection: collectionName,
858
1081
  recordId: id,
859
- data: diff
1082
+ data: merged
860
1083
  });
861
1084
  yield* pruneEvents(storage, collectionName, id, def.eventRetention);
862
1085
  })
863
1086
  ),
864
1087
  delete: (id) => withLog(
865
- Effect6.gen(function* () {
1088
+ Effect7.gen(function* () {
866
1089
  const existing = yield* storage.getRecord(collectionName, id);
867
1090
  if (!existing || existing._d) {
868
1091
  return yield* new NotFoundError({
@@ -880,11 +1103,11 @@ function createCollectionHandle(def, storage, watchCtx, validator, partialValida
880
1103
  author: localAuthor
881
1104
  };
882
1105
  yield* commitEvent(event);
883
- yield* Effect6.logDebug("Record deleted", { collection: collectionName, recordId: id });
1106
+ yield* Effect7.logDebug("Record deleted", { collection: collectionName, recordId: id });
884
1107
  yield* pruneEvents(storage, collectionName, id, def.eventRetention);
885
1108
  })
886
1109
  ),
887
- undo: (id) => Effect6.gen(function* () {
1110
+ undo: (id) => Effect7.gen(function* () {
888
1111
  const existing = yield* storage.getRecord(collectionName, id);
889
1112
  if (!existing) {
890
1113
  return yield* new NotFoundError({ collection: collectionName, id });
@@ -909,7 +1132,7 @@ function createCollectionHandle(def, storage, watchCtx, validator, partialValida
909
1132
  yield* commitEvent(event);
910
1133
  yield* pruneEvents(storage, collectionName, id, def.eventRetention);
911
1134
  }),
912
- get: (id) => Effect6.gen(function* () {
1135
+ get: (id) => Effect7.gen(function* () {
913
1136
  const record = yield* storage.getRecord(collectionName, id);
914
1137
  if (!record || record._d) {
915
1138
  return yield* new NotFoundError({
@@ -919,11 +1142,11 @@ function createCollectionHandle(def, storage, watchCtx, validator, partialValida
919
1142
  }
920
1143
  return mapRecord(record);
921
1144
  }),
922
- first: () => Effect6.map(storage.getAllRecords(collectionName), (all) => {
1145
+ first: () => Effect7.map(storage.getAllRecords(collectionName), (all) => {
923
1146
  const found = all.find((r) => !r._d);
924
1147
  return found ? Option3.some(mapRecord(found)) : Option3.none();
925
1148
  }),
926
- count: () => Effect6.map(storage.getAllRecords(collectionName), (all) => all.filter((r) => !r._d).length),
1149
+ count: () => Effect7.map(storage.getAllRecords(collectionName), (all) => all.filter((r) => !r._d).length),
927
1150
  watch: () => watchCollection(watchCtx, storage, collectionName, void 0, mapRecord),
928
1151
  where: (fieldName) => createWhereClause(
929
1152
  storage,
@@ -946,12 +1169,12 @@ function createCollectionHandle(def, storage, watchCtx, validator, partialValida
946
1169
  }
947
1170
 
948
1171
  // src/sync/sync-service.ts
949
- import { Duration, Effect as Effect8, Layer, Option as Option5, References as References2, Ref as Ref3, Schedule } from "effect";
1172
+ import { Duration, Effect as Effect9, Layer, Option as Option5, References as References2, Ref as Ref3, Schedule } from "effect";
950
1173
  import { unwrapEvent } from "nostr-tools/nip59";
951
1174
  import { GiftWrap as GiftWrap2 } from "nostr-tools/kinds";
952
1175
 
953
1176
  // src/sync/negentropy.ts
954
- import { Effect as Effect7 } from "effect";
1177
+ import { Effect as Effect8 } from "effect";
955
1178
 
956
1179
  // src/vendor/negentropy.js
957
1180
  var PROTOCOL_VERSION = 97;
@@ -1438,14 +1661,14 @@ function itemCompare(a, b) {
1438
1661
  }
1439
1662
 
1440
1663
  // src/sync/negentropy.ts
1441
- import { hexToBytes as hexToBytes2 } from "@noble/hashes/utils.js";
1664
+ import { hexToBytes as hexToBytes3 } from "@noble/hashes/utils.js";
1442
1665
  import { GiftWrap } from "nostr-tools/kinds";
1443
1666
  function reconcileWithRelay(storage, relay, relayUrl, publicKeys) {
1444
- return Effect7.gen(function* () {
1667
+ return Effect8.gen(function* () {
1445
1668
  const allGiftWraps = yield* storage.getAllGiftWraps();
1446
1669
  const storageVector = new NegentropyStorageVector();
1447
1670
  for (const gw of allGiftWraps) {
1448
- storageVector.insert(gw.createdAt, hexToBytes2(gw.id));
1671
+ storageVector.insert(gw.createdAt, hexToBytes3(gw.id));
1449
1672
  }
1450
1673
  storageVector.seal();
1451
1674
  const neg = new Negentropy(storageVector, 0);
@@ -1456,7 +1679,7 @@ function reconcileWithRelay(storage, relay, relayUrl, publicKeys) {
1456
1679
  const allHaveIds = [];
1457
1680
  const allNeedIds = [];
1458
1681
  const subId = `neg-${Date.now()}`;
1459
- const initialMsg = yield* Effect7.tryPromise({
1682
+ const initialMsg = yield* Effect8.tryPromise({
1460
1683
  try: () => neg.initiate(),
1461
1684
  catch: (e) => new SyncError({
1462
1685
  message: `Negentropy initiate failed: ${e instanceof Error ? e.message : String(e)}`,
@@ -1468,7 +1691,7 @@ function reconcileWithRelay(storage, relay, relayUrl, publicKeys) {
1468
1691
  while (currentMsg !== null) {
1469
1692
  const response = yield* relay.sendNegMsg(relayUrl, subId, filter, currentMsg);
1470
1693
  if (response.msgHex === null) break;
1471
- const reconcileResult = yield* Effect7.tryPromise({
1694
+ const reconcileResult = yield* Effect8.tryPromise({
1472
1695
  try: () => neg.reconcile(response.msgHex),
1473
1696
  catch: (e) => new SyncError({
1474
1697
  message: `Negentropy reconcile failed: ${e instanceof Error ? e.message : String(e)}`,
@@ -1481,13 +1704,13 @@ function reconcileWithRelay(storage, relay, relayUrl, publicKeys) {
1481
1704
  for (const id of needIds) allNeedIds.push(id);
1482
1705
  currentMsg = nextMsg;
1483
1706
  }
1484
- yield* Effect7.logDebug("Negentropy reconciliation complete", {
1707
+ yield* Effect8.logDebug("Negentropy reconciliation complete", {
1485
1708
  relay: relayUrl,
1486
1709
  have: allHaveIds.length,
1487
1710
  need: allNeedIds.length
1488
1711
  });
1489
1712
  return { haveIds: allHaveIds, needIds: allNeedIds };
1490
- }).pipe(Effect7.withLogSpan("tablinum.negentropy"));
1713
+ }).pipe(Effect8.withLogSpan("tablinum.negentropy"));
1491
1714
  }
1492
1715
 
1493
1716
  // src/db/key-rotation.ts
@@ -1581,52 +1804,52 @@ function createSyncHandle(storage, giftWrapHandle, relay, publishQueue, syncStat
1581
1804
  kind: "create"
1582
1805
  });
1583
1806
  const forkHandled = (effect) => {
1584
- Effect8.runFork(
1807
+ Effect9.runFork(
1585
1808
  effect.pipe(
1586
- Effect8.tapError((e) => Effect8.sync(() => onSyncError?.(e))),
1587
- Effect8.ignore,
1588
- Effect8.provide(logLayer),
1589
- Effect8.forkIn(scope)
1809
+ Effect9.tapError((e) => Effect9.sync(() => onSyncError?.(e))),
1810
+ Effect9.ignore,
1811
+ Effect9.provide(logLayer),
1812
+ Effect9.forkIn(scope)
1590
1813
  )
1591
1814
  );
1592
1815
  };
1593
1816
  let autoFlushActive = false;
1594
- const autoFlushEffect = Effect8.gen(function* () {
1817
+ const autoFlushEffect = Effect9.gen(function* () {
1595
1818
  const size = yield* publishQueue.size();
1596
1819
  if (size === 0) return;
1597
1820
  yield* syncStatus.set("syncing");
1598
1821
  yield* publishQueue.flush(relayUrls);
1599
1822
  const remaining = yield* publishQueue.size();
1600
- if (remaining > 0) yield* Effect8.fail("pending");
1823
+ if (remaining > 0) yield* Effect9.fail("pending");
1601
1824
  }).pipe(
1602
- Effect8.ensuring(syncStatus.set("idle")),
1603
- Effect8.retry({ schedule: Schedule.exponential(5e3).pipe(Schedule.jittered), times: 10 }),
1604
- Effect8.ignore
1825
+ Effect9.ensuring(syncStatus.set("idle")),
1826
+ Effect9.retry({ schedule: Schedule.exponential(5e3).pipe(Schedule.jittered), times: 10 }),
1827
+ Effect9.ignore
1605
1828
  );
1606
1829
  const scheduleAutoFlush = () => {
1607
1830
  if (autoFlushActive) return;
1608
1831
  autoFlushActive = true;
1609
1832
  forkHandled(
1610
1833
  autoFlushEffect.pipe(
1611
- Effect8.ensuring(
1612
- Effect8.sync(() => {
1834
+ Effect9.ensuring(
1835
+ Effect9.sync(() => {
1613
1836
  autoFlushActive = false;
1614
1837
  })
1615
1838
  )
1616
1839
  )
1617
1840
  );
1618
1841
  };
1619
- const shouldRejectWrite = (authorPubkey) => Effect8.gen(function* () {
1842
+ const shouldRejectWrite = (authorPubkey) => Effect9.gen(function* () {
1620
1843
  const memberRecord = yield* storage.getRecord("_members", authorPubkey);
1621
1844
  if (!memberRecord) return false;
1622
1845
  return !!memberRecord.removedAt;
1623
1846
  });
1624
1847
  const storeGiftWrapShell = (gw) => storage.putGiftWrap({ id: gw.id, event: gw, createdAt: gw.created_at });
1625
- const unwrapGiftWrap = (remoteGw) => Effect8.gen(function* () {
1848
+ const unwrapGiftWrap = (remoteGw) => Effect9.gen(function* () {
1626
1849
  const existing = yield* storage.getGiftWrap(remoteGw.id);
1627
1850
  if (existing) return null;
1628
1851
  const rumor = yield* giftWrapHandle.unwrap(remoteGw).pipe(
1629
- Effect8.orElseSucceed(() => null)
1852
+ Effect9.orElseSucceed(() => null)
1630
1853
  );
1631
1854
  if (!rumor) {
1632
1855
  yield* storeGiftWrapShell(remoteGw);
@@ -1654,7 +1877,7 @@ function createSyncHandle(storage, giftWrapHandle, relay, publishQueue, syncStat
1654
1877
  recordId: dTag.substring(colonIdx + 1)
1655
1878
  };
1656
1879
  });
1657
- const applyUnwrappedEvent = (uw) => Effect8.gen(function* () {
1880
+ const applyUnwrappedEvent = (uw) => Effect9.gen(function* () {
1658
1881
  const { giftWrap: remoteGw, rumor, collection: collectionName, recordId } = uw;
1659
1882
  const retention = knownCollections.get(collectionName);
1660
1883
  if (retention === void 0) {
@@ -1664,17 +1887,17 @@ function createSyncHandle(storage, giftWrapHandle, relay, publishQueue, syncStat
1664
1887
  if (rumor.pubkey) {
1665
1888
  const reject = yield* shouldRejectWrite(rumor.pubkey);
1666
1889
  if (reject) {
1667
- yield* Effect8.logWarning("Rejected write from removed member", {
1890
+ yield* Effect9.logWarning("Rejected write from removed member", {
1668
1891
  author: rumor.pubkey.slice(0, 12)
1669
1892
  });
1670
1893
  yield* storeGiftWrapShell(remoteGw);
1671
1894
  return null;
1672
1895
  }
1673
1896
  }
1674
- const parsed = yield* Effect8.try({
1897
+ const parsed = yield* Effect9.try({
1675
1898
  try: () => JSON.parse(rumor.content),
1676
1899
  catch: () => void 0
1677
- }).pipe(Effect8.orElseSucceed(() => void 0));
1900
+ }).pipe(Effect9.orElseSucceed(() => void 0));
1678
1901
  if (parsed === void 0) {
1679
1902
  yield* storeGiftWrapShell(remoteGw);
1680
1903
  return null;
@@ -1706,7 +1929,7 @@ function createSyncHandle(storage, giftWrapHandle, relay, publishQueue, syncStat
1706
1929
  if (didApply && (kind === "u" || kind === "d")) {
1707
1930
  yield* pruneEvents(storage, collectionName, recordId, retention);
1708
1931
  }
1709
- yield* Effect8.logDebug("Processed gift wrap", {
1932
+ yield* Effect9.logDebug("Processed gift wrap", {
1710
1933
  collection: collectionName,
1711
1934
  recordId,
1712
1935
  kind,
@@ -1717,33 +1940,33 @@ function createSyncHandle(storage, giftWrapHandle, relay, publishQueue, syncStat
1717
1940
  }
1718
1941
  return collectionName;
1719
1942
  });
1720
- const reconcileRelay = (url, pubKeys) => Effect8.gen(function* () {
1721
- yield* Effect8.logDebug("Syncing relay", { relay: url });
1943
+ const reconcileRelay = (url, pubKeys) => Effect9.gen(function* () {
1944
+ yield* Effect9.logDebug("Syncing relay", { relay: url });
1722
1945
  const { haveIds, needIds } = yield* reconcileWithRelay(
1723
1946
  storage,
1724
1947
  relay,
1725
1948
  url,
1726
1949
  Array.from(pubKeys)
1727
1950
  ).pipe(
1728
- Effect8.tapError((err) => Effect8.sync(() => onSyncError?.(err))),
1729
- Effect8.orElseSucceed(() => ({ haveIds: [], needIds: [] }))
1951
+ Effect9.tapError((err) => Effect9.sync(() => onSyncError?.(err))),
1952
+ Effect9.orElseSucceed(() => ({ haveIds: [], needIds: [] }))
1730
1953
  );
1731
- yield* Effect8.logDebug("Relay reconciliation result", {
1954
+ yield* Effect9.logDebug("Relay reconciliation result", {
1732
1955
  relay: url,
1733
1956
  need: needIds.length,
1734
1957
  have: haveIds.length
1735
1958
  });
1736
1959
  const events = needIds.length > 0 ? yield* relay.fetchEvents(needIds, url).pipe(
1737
- Effect8.tapError((err) => Effect8.sync(() => onSyncError?.(err))),
1738
- Effect8.orElseSucceed(() => [])
1960
+ Effect9.tapError((err) => Effect9.sync(() => onSyncError?.(err))),
1961
+ Effect9.orElseSucceed(() => [])
1739
1962
  ) : [];
1740
1963
  return {
1741
1964
  events,
1742
1965
  haveIds: haveIds.map((id) => ({ id, url }))
1743
1966
  };
1744
- }).pipe(Effect8.withLogSpan("tablinum.reconcileRelay"));
1745
- const syncAllRelays = (pubKeys, changedCollections) => Effect8.gen(function* () {
1746
- const results = yield* Effect8.forEach(
1967
+ }).pipe(Effect9.withLogSpan("tablinum.reconcileRelay"));
1968
+ const syncAllRelays = (pubKeys, changedCollections) => Effect9.gen(function* () {
1969
+ const results = yield* Effect9.forEach(
1747
1970
  relayUrls,
1748
1971
  (url) => reconcileRelay(url, pubKeys),
1749
1972
  { concurrency: "unbounded" }
@@ -1760,44 +1983,44 @@ function createSyncHandle(storage, giftWrapHandle, relay, publishQueue, syncStat
1760
1983
  }
1761
1984
  const unwrapped = [];
1762
1985
  for (const gw of allGiftWraps) {
1763
- const result = yield* unwrapGiftWrap(gw).pipe(Effect8.orElseSucceed(() => null));
1986
+ const result = yield* unwrapGiftWrap(gw).pipe(Effect9.orElseSucceed(() => null));
1764
1987
  if (result) unwrapped.push(result);
1765
1988
  }
1766
1989
  unwrapped.sort((a, b) => a.rumor.created_at - b.rumor.created_at || (a.rumor.id < b.rumor.id ? -1 : 1));
1767
1990
  for (const event of unwrapped) {
1768
1991
  const collection2 = yield* applyUnwrappedEvent(event).pipe(
1769
- Effect8.orElseSucceed(() => null)
1992
+ Effect9.orElseSucceed(() => null)
1770
1993
  );
1771
1994
  if (collection2) changedCollections.add(collection2);
1772
1995
  }
1773
1996
  const allHaveIds = results.flatMap((r) => r.haveIds);
1774
- yield* Effect8.forEach(
1997
+ yield* Effect9.forEach(
1775
1998
  allHaveIds,
1776
- ({ id, url }) => Effect8.gen(function* () {
1999
+ ({ id, url }) => Effect9.gen(function* () {
1777
2000
  const gw = yield* storage.getGiftWrap(id);
1778
2001
  if (!gw?.event) return;
1779
2002
  yield* relay.publish(gw.event, [url]).pipe(
1780
- Effect8.tapError((err) => Effect8.sync(() => onSyncError?.(err))),
1781
- Effect8.ignore
2003
+ Effect9.tapError((err) => Effect9.sync(() => onSyncError?.(err))),
2004
+ Effect9.ignore
1782
2005
  );
1783
2006
  }),
1784
2007
  { concurrency: "unbounded", discard: true }
1785
2008
  );
1786
- }).pipe(Effect8.withLogSpan("tablinum.syncAllRelays"));
1787
- const processGiftWrap = (remoteGw) => Effect8.gen(function* () {
2009
+ }).pipe(Effect9.withLogSpan("tablinum.syncAllRelays"));
2010
+ const processGiftWrap = (remoteGw) => Effect9.gen(function* () {
1788
2011
  const uw = yield* unwrapGiftWrap(remoteGw);
1789
2012
  if (!uw) return null;
1790
2013
  return yield* applyUnwrappedEvent(uw);
1791
2014
  });
1792
- const processRealtimeGiftWrap = (remoteGw) => Effect8.gen(function* () {
1793
- const collection2 = yield* processGiftWrap(remoteGw).pipe(Effect8.orElseSucceed(() => null));
2015
+ const processRealtimeGiftWrap = (remoteGw) => Effect9.gen(function* () {
2016
+ const collection2 = yield* processGiftWrap(remoteGw).pipe(Effect9.orElseSucceed(() => null));
1794
2017
  if (collection2) {
1795
2018
  yield* notifyCollectionUpdated(collection2);
1796
2019
  }
1797
2020
  });
1798
- const processRotationGiftWrap = (remoteGw) => Effect8.gen(function* () {
1799
- const unwrapResult = yield* Effect8.result(
1800
- Effect8.try({
2021
+ const processRotationGiftWrap = (remoteGw) => Effect9.gen(function* () {
2022
+ const unwrapResult = yield* Effect9.result(
2023
+ Effect9.try({
1801
2024
  try: () => unwrapEvent(remoteGw, personalPrivateKey),
1802
2025
  catch: (e) => new CryptoError({
1803
2026
  message: `Rotation unwrap failed: ${e instanceof Error ? e.message : String(e)}`,
@@ -1847,73 +2070,73 @@ function createSyncHandle(storage, giftWrapHandle, relay, publishQueue, syncStat
1847
2070
  yield* handle.addEpochSubscription(epoch.publicKey);
1848
2071
  return true;
1849
2072
  });
1850
- const subscribeAcrossRelays = (filter, onEvent) => Effect8.forEach(
2073
+ const subscribeAcrossRelays = (filter, onEvent) => Effect9.forEach(
1851
2074
  relayUrls,
1852
- (url) => Effect8.gen(function* () {
2075
+ (url) => Effect9.gen(function* () {
1853
2076
  yield* relay.subscribe(filter, url, (event) => {
1854
2077
  forkHandled(onEvent(event));
1855
2078
  }).pipe(
1856
- Effect8.tapError((err) => Effect8.sync(() => onSyncError?.(err))),
1857
- Effect8.ignore
2079
+ Effect9.tapError((err) => Effect9.sync(() => onSyncError?.(err))),
2080
+ Effect9.ignore
1858
2081
  );
1859
2082
  }),
1860
2083
  { concurrency: "unbounded", discard: true }
1861
2084
  );
1862
2085
  let healingActive = false;
1863
- const healingEffect = Effect8.gen(function* () {
2086
+ const healingEffect = Effect9.gen(function* () {
1864
2087
  if (!healingActive) return;
1865
2088
  const status = yield* syncStatus.get();
1866
2089
  if (status === "syncing") return;
1867
2090
  yield* syncStatus.set("syncing");
1868
- yield* Effect8.gen(function* () {
2091
+ yield* Effect9.gen(function* () {
1869
2092
  const pubKeys = getSubscriptionPubKeys();
1870
2093
  const changedCollections = /* @__PURE__ */ new Set();
1871
2094
  yield* syncAllRelays(pubKeys, changedCollections);
1872
2095
  if (changedCollections.size > 0) {
1873
2096
  yield* notifyReplayComplete(watchCtx, [...changedCollections]);
1874
2097
  }
1875
- }).pipe(Effect8.ensuring(syncStatus.set("idle")));
1876
- }).pipe(Effect8.ignore);
2098
+ }).pipe(Effect9.ensuring(syncStatus.set("idle")));
2099
+ }).pipe(Effect9.ignore);
1877
2100
  const handle = {
1878
- sync: () => Effect8.gen(function* () {
1879
- yield* Effect8.logInfo("Sync started");
2101
+ sync: () => Effect9.gen(function* () {
2102
+ yield* Effect9.logInfo("Sync started");
1880
2103
  yield* syncStatus.set("syncing");
1881
2104
  yield* Ref3.set(watchCtx.replayingRef, true);
1882
2105
  const changedCollections = /* @__PURE__ */ new Set();
1883
- yield* Effect8.gen(function* () {
2106
+ yield* Effect9.gen(function* () {
1884
2107
  const pubKeys = getSubscriptionPubKeys();
1885
2108
  yield* syncAllRelays(pubKeys, changedCollections);
1886
- yield* publishQueue.flush(relayUrls).pipe(Effect8.ignore);
2109
+ yield* publishQueue.flush(relayUrls).pipe(Effect9.ignore);
1887
2110
  }).pipe(
1888
- Effect8.ensuring(
1889
- Effect8.gen(function* () {
2111
+ Effect9.ensuring(
2112
+ Effect9.gen(function* () {
1890
2113
  yield* notifyReplayComplete(watchCtx, [...changedCollections]);
1891
2114
  yield* syncStatus.set("idle");
1892
2115
  })
1893
2116
  )
1894
2117
  );
1895
- yield* Effect8.logInfo("Sync complete", { changed: [...changedCollections] });
1896
- }).pipe(Effect8.withLogSpan("tablinum.sync")),
1897
- publishLocal: (giftWrap) => Effect8.gen(function* () {
2118
+ yield* Effect9.logInfo("Sync complete", { changed: [...changedCollections] });
2119
+ }).pipe(Effect9.withLogSpan("tablinum.sync")),
2120
+ publishLocal: (giftWrap) => Effect9.gen(function* () {
1898
2121
  if (!giftWrap.event) return;
1899
2122
  yield* relay.publish(giftWrap.event, relayUrls).pipe(
1900
- Effect8.tapError(
2123
+ Effect9.tapError(
1901
2124
  () => storage.putGiftWrap(giftWrap).pipe(
1902
- Effect8.andThen(publishQueue.enqueue(giftWrap.id)),
1903
- Effect8.andThen(Effect8.sync(() => scheduleAutoFlush()))
2125
+ Effect9.andThen(publishQueue.enqueue(giftWrap.id)),
2126
+ Effect9.andThen(Effect9.sync(() => scheduleAutoFlush()))
1904
2127
  )
1905
2128
  ),
1906
- Effect8.tapError((err) => Effect8.sync(() => onSyncError?.(err))),
1907
- Effect8.ignore
2129
+ Effect9.tapError((err) => Effect9.sync(() => onSyncError?.(err))),
2130
+ Effect9.ignore
1908
2131
  );
1909
2132
  }),
1910
- startSubscription: () => Effect8.gen(function* () {
2133
+ startSubscription: () => Effect9.gen(function* () {
1911
2134
  const pubKeys = getSubscriptionPubKeys();
1912
2135
  yield* subscribeAcrossRelays({ kinds: [GiftWrap2], "#p": pubKeys }, processRealtimeGiftWrap);
1913
2136
  if (!pubKeys.includes(personalPublicKey)) {
1914
2137
  yield* subscribeAcrossRelays(
1915
2138
  { kinds: [GiftWrap2], "#p": [personalPublicKey] },
1916
- (event) => Effect8.result(processRotationGiftWrap(event)).pipe(Effect8.asVoid)
2139
+ (event) => Effect9.result(processRotationGiftWrap(event)).pipe(Effect9.asVoid)
1917
2140
  );
1918
2141
  }
1919
2142
  }),
@@ -1922,10 +2145,10 @@ function createSyncHandle(storage, giftWrapHandle, relay, publishQueue, syncStat
1922
2145
  if (healingActive) return;
1923
2146
  healingActive = true;
1924
2147
  forkHandled(
1925
- Effect8.sleep(Duration.minutes(5)).pipe(
1926
- Effect8.andThen(healingEffect),
1927
- Effect8.repeat(Schedule.spaced(Duration.minutes(5))),
1928
- Effect8.ensuring(Effect8.sync(() => {
2148
+ Effect9.sleep(Duration.minutes(5)).pipe(
2149
+ Effect9.andThen(healingEffect),
2150
+ Effect9.repeat(Schedule.spaced(Duration.minutes(5))),
2151
+ Effect9.ensuring(Effect9.sync(() => {
1929
2152
  healingActive = false;
1930
2153
  }))
1931
2154
  )
@@ -1937,8 +2160,8 @@ function createSyncHandle(storage, giftWrapHandle, relay, publishQueue, syncStat
1937
2160
  };
1938
2161
  forkHandled(
1939
2162
  publishQueue.size().pipe(
1940
- Effect8.flatMap(
1941
- (size) => Effect8.sync(() => {
2163
+ Effect9.flatMap(
2164
+ (size) => Effect9.sync(() => {
1942
2165
  if (size > 0) scheduleAutoFlush();
1943
2166
  })
1944
2167
  )
@@ -1948,7 +2171,7 @@ function createSyncHandle(storage, giftWrapHandle, relay, publishQueue, syncStat
1948
2171
  }
1949
2172
 
1950
2173
  // src/db/members.ts
1951
- import { Effect as Effect9, Option as Option6, Schema as Schema6 } from "effect";
2174
+ import { Effect as Effect10, Option as Option6, Schema as Schema6 } from "effect";
1952
2175
  var optionalString = {
1953
2176
  _tag: "FieldDef",
1954
2177
  kind: "string",
@@ -1997,15 +2220,15 @@ var AuthorProfileSchema = Schema6.Struct({
1997
2220
  });
1998
2221
  var decodeAuthorProfile = Schema6.decodeUnknownEffect(Schema6.fromJsonString(AuthorProfileSchema));
1999
2222
  function fetchAuthorProfile(relay, relayUrls, pubkey) {
2000
- return Effect9.gen(function* () {
2223
+ return Effect10.gen(function* () {
2001
2224
  for (const url of relayUrls) {
2002
- const result = yield* Effect9.result(
2225
+ const result = yield* Effect10.result(
2003
2226
  relay.fetchByFilter({ kinds: [0], authors: [pubkey], limit: 1 }, url)
2004
2227
  );
2005
2228
  if (result._tag === "Success" && result.success.length > 0) {
2006
2229
  return yield* decodeAuthorProfile(result.success[0].content).pipe(
2007
- Effect9.map(Option6.some),
2008
- Effect9.orElseSucceed(() => Option6.none())
2230
+ Effect10.map(Option6.some),
2231
+ Effect10.orElseSucceed(() => Option6.none())
2009
2232
  );
2010
2233
  }
2011
2234
  }
@@ -2013,6 +2236,20 @@ function fetchAuthorProfile(relay, relayUrls, pubkey) {
2013
2236
  });
2014
2237
  }
2015
2238
 
2239
+ // src/sync/deletion.ts
2240
+ import { finalizeEvent } from "nostr-tools/pure";
2241
+ function createDeletionEvent(targetEventIds, signingKey) {
2242
+ return finalizeEvent(
2243
+ {
2244
+ kind: 5,
2245
+ content: "",
2246
+ tags: targetEventIds.map((id) => ["e", id]),
2247
+ created_at: Math.floor(Date.now() / 1e3)
2248
+ },
2249
+ signingKey
2250
+ );
2251
+ }
2252
+
2016
2253
  // src/services/Identity.ts
2017
2254
  import { ServiceMap as ServiceMap3 } from "effect";
2018
2255
  var Identity = class extends ServiceMap3.Service()("tablinum/Identity") {
@@ -2055,15 +2292,15 @@ var SyncStatus = class extends ServiceMap9.Service()(
2055
2292
  };
2056
2293
 
2057
2294
  // src/layers/IdentityLive.ts
2058
- import { Effect as Effect11, Layer as Layer2 } from "effect";
2059
- import { hexToBytes as hexToBytes3 } from "@noble/hashes/utils.js";
2295
+ import { Effect as Effect12, Layer as Layer2 } from "effect";
2296
+ import { hexToBytes as hexToBytes4 } from "@noble/hashes/utils.js";
2060
2297
 
2061
2298
  // src/db/identity.ts
2062
- import { Effect as Effect10 } from "effect";
2299
+ import { Effect as Effect11 } from "effect";
2063
2300
  import { getPublicKey as getPublicKey2 } from "nostr-tools/pure";
2064
- import { bytesToHex as bytesToHex2 } from "@noble/hashes/utils.js";
2301
+ import { bytesToHex as bytesToHex3 } from "@noble/hashes/utils.js";
2065
2302
  function createIdentity(suppliedKey) {
2066
- return Effect10.gen(function* () {
2303
+ return Effect11.gen(function* () {
2067
2304
  let privateKey;
2068
2305
  if (suppliedKey) {
2069
2306
  if (suppliedKey.length !== 32) {
@@ -2076,8 +2313,8 @@ function createIdentity(suppliedKey) {
2076
2313
  privateKey = new Uint8Array(32);
2077
2314
  crypto.getRandomValues(privateKey);
2078
2315
  }
2079
- const privateKeyHex = bytesToHex2(privateKey);
2080
- const publicKey = yield* Effect10.try({
2316
+ const privateKeyHex = bytesToHex3(privateKey);
2317
+ const publicKey = yield* Effect11.try({
2081
2318
  try: () => getPublicKey2(privateKey),
2082
2319
  catch: (e) => new CryptoError({
2083
2320
  message: `Failed to derive public key: ${e instanceof Error ? e.message : String(e)}`,
@@ -2095,14 +2332,14 @@ function createIdentity(suppliedKey) {
2095
2332
  // src/layers/IdentityLive.ts
2096
2333
  var IdentityLive = Layer2.effect(
2097
2334
  Identity,
2098
- Effect11.gen(function* () {
2335
+ Effect12.gen(function* () {
2099
2336
  const config = yield* Config;
2100
2337
  const storage = yield* Storage;
2101
2338
  const idbKey = yield* storage.getMeta("identity_key");
2102
- const resolvedKey = config.privateKey ?? (typeof idbKey === "string" && idbKey.length === 64 ? hexToBytes3(idbKey) : void 0);
2339
+ const resolvedKey = config.privateKey ?? (typeof idbKey === "string" && idbKey.length === 64 ? hexToBytes4(idbKey) : void 0);
2103
2340
  const identity = yield* createIdentity(resolvedKey);
2104
2341
  yield* storage.putMeta("identity_key", identity.exportKey());
2105
- yield* Effect11.logInfo("Identity loaded", {
2342
+ yield* Effect12.logInfo("Identity loaded", {
2106
2343
  publicKey: identity.publicKey.slice(0, 12) + "...",
2107
2344
  source: config.privateKey ? "config" : resolvedKey ? "storage" : "generated"
2108
2345
  });
@@ -2111,12 +2348,12 @@ var IdentityLive = Layer2.effect(
2111
2348
  );
2112
2349
 
2113
2350
  // src/layers/EpochStoreLive.ts
2114
- import { Effect as Effect12, Layer as Layer3, Option as Option7 } from "effect";
2351
+ import { Effect as Effect13, Layer as Layer3, Option as Option7 } from "effect";
2115
2352
  import { generateSecretKey as generateSecretKey2 } from "nostr-tools/pure";
2116
- import { bytesToHex as bytesToHex3 } from "@noble/hashes/utils.js";
2353
+ import { bytesToHex as bytesToHex4 } from "@noble/hashes/utils.js";
2117
2354
  var EpochStoreLive = Layer3.effect(
2118
2355
  EpochStore,
2119
- Effect12.gen(function* () {
2356
+ Effect13.gen(function* () {
2120
2357
  const config = yield* Config;
2121
2358
  const identity = yield* Identity;
2122
2359
  const storage = yield* Storage;
@@ -2135,7 +2372,7 @@ var EpochStoreLive = Layer3.effect(
2135
2372
  return existing !== void 0 && existing.privateKey === ek.key;
2136
2373
  });
2137
2374
  if (configIsSubset) {
2138
- yield* Effect12.logInfo("Epoch store loaded", {
2375
+ yield* Effect13.logInfo("Epoch store loaded", {
2139
2376
  source: "storage",
2140
2377
  epochs: idbStore.epochs.size
2141
2378
  });
@@ -2144,271 +2381,28 @@ var EpochStoreLive = Layer3.effect(
2144
2381
  }
2145
2382
  const store2 = createEpochStoreFromInputs(config.epochKeys);
2146
2383
  yield* storage.putMeta("epochs", stringifyEpochStore(store2));
2147
- yield* Effect12.logInfo("Epoch store loaded", { source: "config", epochs: store2.epochs.size });
2384
+ yield* Effect13.logInfo("Epoch store loaded", { source: "config", epochs: store2.epochs.size });
2148
2385
  return store2;
2149
2386
  }
2150
2387
  if (idbStore) {
2151
- yield* Effect12.logInfo("Epoch store loaded", {
2388
+ yield* Effect13.logInfo("Epoch store loaded", {
2152
2389
  source: "storage",
2153
2390
  epochs: idbStore.epochs.size
2154
2391
  });
2155
2392
  return idbStore;
2156
2393
  }
2157
2394
  const store = createEpochStoreFromInputs(
2158
- [{ epochId: EpochId("epoch-0"), key: bytesToHex3(generateSecretKey2()) }],
2395
+ [{ epochId: EpochId("epoch-0"), key: bytesToHex4(generateSecretKey2()) }],
2159
2396
  { createdBy: identity.publicKey }
2160
2397
  );
2161
2398
  yield* storage.putMeta("epochs", stringifyEpochStore(store));
2162
- yield* Effect12.logInfo("Epoch store loaded", { source: "generated", epochs: store.epochs.size });
2399
+ yield* Effect13.logInfo("Epoch store loaded", { source: "generated", epochs: store.epochs.size });
2163
2400
  return store;
2164
2401
  })
2165
2402
  );
2166
2403
 
2167
2404
  // src/layers/StorageLive.ts
2168
2405
  import { Effect as Effect14, Layer as Layer4 } from "effect";
2169
-
2170
- // src/storage/idb.ts
2171
- import { Effect as Effect13 } from "effect";
2172
- import { openDB } from "idb";
2173
-
2174
- // src/sync/compact-event.ts
2175
- import { bytesToHex as bytesToHex4, hexToBytes as hexToBytes4 } from "@noble/hashes/utils.js";
2176
- var VERSION = 1;
2177
- var HEADER_SIZE = 133;
2178
- function base64ToBytes(base64) {
2179
- const binary = atob(base64);
2180
- const bytes = new Uint8Array(binary.length);
2181
- for (let i = 0; i < binary.length; i++) {
2182
- bytes[i] = binary.charCodeAt(i);
2183
- }
2184
- return bytes;
2185
- }
2186
- function bytesToBase64(bytes) {
2187
- let binary = "";
2188
- for (let i = 0; i < bytes.length; i++) {
2189
- binary += String.fromCharCode(bytes[i]);
2190
- }
2191
- return btoa(binary);
2192
- }
2193
- function packEvent(event) {
2194
- const pubkey = hexToBytes4(event.pubkey);
2195
- const sig = hexToBytes4(event.sig);
2196
- const recipientTag = event.tags.find((t) => t[0] === "p");
2197
- if (!recipientTag) throw new Error("Gift wrap missing #p tag");
2198
- if (event.tags.some((t) => t[0] !== "p")) {
2199
- throw new Error("Gift wrap has unexpected non-p tags; compact encoding would lose them");
2200
- }
2201
- const recipient = hexToBytes4(recipientTag[1]);
2202
- const createdAtBuf = new Uint8Array(4);
2203
- new DataView(createdAtBuf.buffer).setUint32(0, event.created_at, false);
2204
- const content = base64ToBytes(event.content);
2205
- const result = new Uint8Array(HEADER_SIZE + content.length);
2206
- result[0] = VERSION;
2207
- result.set(pubkey, 1);
2208
- result.set(sig, 33);
2209
- result.set(recipient, 97);
2210
- result.set(createdAtBuf, 129);
2211
- result.set(content, HEADER_SIZE);
2212
- return result;
2213
- }
2214
- function unpackEvent(id, compact) {
2215
- const version = compact[0];
2216
- if (version !== VERSION) throw new Error(`Unknown compact event version: ${version}`);
2217
- const pubkey = bytesToHex4(compact.slice(1, 33));
2218
- const sig = bytesToHex4(compact.slice(33, 97));
2219
- const recipient = bytesToHex4(compact.slice(97, 129));
2220
- const dv = new DataView(compact.buffer, compact.byteOffset + 129, 4);
2221
- const createdAt = dv.getUint32(0, false);
2222
- const content = bytesToBase64(compact.slice(HEADER_SIZE));
2223
- return {
2224
- id,
2225
- pubkey,
2226
- sig,
2227
- created_at: createdAt,
2228
- kind: 1059,
2229
- tags: [["p", recipient]],
2230
- content
2231
- };
2232
- }
2233
-
2234
- // src/storage/idb.ts
2235
- var DB_NAME = "tablinum";
2236
- function storeName(collection2) {
2237
- return `col_${collection2}`;
2238
- }
2239
- function computeSchemaSig(schema) {
2240
- return Object.entries(schema).sort(([a], [b]) => a.localeCompare(b)).map(([name, def]) => {
2241
- const indices = [...def.indices ?? []].sort().join(",");
2242
- return `${name}:${indices}`;
2243
- }).join("|");
2244
- }
2245
- function wrap(label, fn) {
2246
- return Effect13.tryPromise({
2247
- try: fn,
2248
- catch: (e) => new StorageError({
2249
- message: `IndexedDB ${label} failed: ${e instanceof Error ? e.message : String(e)}`,
2250
- cause: e
2251
- })
2252
- });
2253
- }
2254
- function upgradeSchema(database, schema, tx) {
2255
- if (!database.objectStoreNames.contains("_meta")) {
2256
- database.createObjectStore("_meta");
2257
- }
2258
- if (!database.objectStoreNames.contains("events")) {
2259
- const events = database.createObjectStore("events", { keyPath: "id" });
2260
- events.createIndex("by-record", ["collection", "recordId"]);
2261
- }
2262
- if (!database.objectStoreNames.contains("giftwraps")) {
2263
- database.createObjectStore("giftwraps", { keyPath: "id" });
2264
- }
2265
- const expectedStores = /* @__PURE__ */ new Set();
2266
- for (const [, def] of Object.entries(schema)) {
2267
- const sn = storeName(def.name);
2268
- expectedStores.add(sn);
2269
- if (!database.objectStoreNames.contains(sn)) {
2270
- const store = database.createObjectStore(sn, { keyPath: "id" });
2271
- for (const idx of def.indices ?? []) {
2272
- store.createIndex(idx, idx);
2273
- }
2274
- } else {
2275
- const store = tx.objectStore(sn);
2276
- const existingIndices = new Set(Array.from(store.indexNames));
2277
- const wantedIndices = new Set(def.indices ?? []);
2278
- for (const idx of existingIndices) {
2279
- if (!wantedIndices.has(idx)) store.deleteIndex(idx);
2280
- }
2281
- for (const idx of wantedIndices) {
2282
- if (!existingIndices.has(idx)) store.createIndex(idx, idx);
2283
- }
2284
- }
2285
- }
2286
- for (const existing of Array.from(database.objectStoreNames)) {
2287
- if (existing.startsWith("col_") && !expectedStores.has(existing)) {
2288
- database.deleteObjectStore(existing);
2289
- }
2290
- }
2291
- tx.objectStore("_meta").put(computeSchemaSig(schema), "schema_sig");
2292
- }
2293
- function openIDBStorage(dbName, schema) {
2294
- return Effect13.gen(function* () {
2295
- const name = dbName ?? DB_NAME;
2296
- const schemaSig = computeSchemaSig(schema);
2297
- if (typeof indexedDB === "undefined") {
2298
- return yield* Effect13.fail(
2299
- new StorageError({
2300
- message: "IndexedDB is not available in this environment"
2301
- })
2302
- );
2303
- }
2304
- const probeDb = yield* Effect13.tryPromise({
2305
- try: () => openDB(name),
2306
- catch: (e) => new StorageError({ message: "Failed to open IndexedDB", cause: e })
2307
- });
2308
- const currentVersion = probeDb.version;
2309
- let needsUpgrade = true;
2310
- if (probeDb.objectStoreNames.contains("_meta")) {
2311
- const storedSig = yield* Effect13.tryPromise({
2312
- try: () => probeDb.get("_meta", "schema_sig"),
2313
- catch: () => new StorageError({ message: "Failed to read schema meta" })
2314
- }).pipe(Effect13.catch(() => Effect13.succeed(void 0)));
2315
- needsUpgrade = storedSig !== schemaSig;
2316
- }
2317
- probeDb.close();
2318
- const db = needsUpgrade ? yield* Effect13.tryPromise({
2319
- try: () => openDB(name, currentVersion + 1, {
2320
- upgrade(database, _oldVersion, _newVersion, transaction) {
2321
- upgradeSchema(database, schema, transaction);
2322
- }
2323
- }),
2324
- catch: (e) => new StorageError({ message: "Failed to open IndexedDB", cause: e })
2325
- }) : yield* Effect13.tryPromise({
2326
- try: () => openDB(name),
2327
- catch: (e) => new StorageError({ message: "Failed to open IndexedDB", cause: e })
2328
- });
2329
- yield* Effect13.addFinalizer(() => Effect13.sync(() => db.close()));
2330
- const handle = {
2331
- putRecord: (collection2, record) => wrap("putRecord", () => db.put(storeName(collection2), record).then(() => void 0)),
2332
- getRecord: (collection2, id) => wrap("getRecord", () => db.get(storeName(collection2), id)),
2333
- getAllRecords: (collection2) => wrap("getAllRecords", () => db.getAll(storeName(collection2))),
2334
- countRecords: (collection2) => wrap("countRecords", () => db.count(storeName(collection2))),
2335
- clearRecords: (collection2) => wrap("clearRecords", () => db.clear(storeName(collection2))),
2336
- getByIndex: (collection2, indexName, value) => wrap("getByIndex", () => db.getAllFromIndex(storeName(collection2), indexName, value)),
2337
- getByIndexRange: (collection2, indexName, range) => wrap("getByIndexRange", () => db.getAllFromIndex(storeName(collection2), indexName, range)),
2338
- getAllSorted: (collection2, indexName, direction) => wrap("getAllSorted", async () => {
2339
- const sn = storeName(collection2);
2340
- const tx = db.transaction(sn, "readonly");
2341
- const store = tx.objectStore(sn);
2342
- const index = store.index(indexName);
2343
- const results = [];
2344
- let cursor = await index.openCursor(null, direction ?? "next");
2345
- while (cursor) {
2346
- results.push(cursor.value);
2347
- cursor = await cursor.continue();
2348
- }
2349
- return results;
2350
- }),
2351
- putEvent: (event) => wrap("putEvent", () => db.put("events", event).then(() => void 0)),
2352
- getEvent: (id) => wrap("getEvent", () => db.get("events", id)),
2353
- getAllEvents: () => wrap("getAllEvents", () => db.getAll("events")),
2354
- getEventsByRecord: (collection2, recordId) => wrap(
2355
- "getEventsByRecord",
2356
- () => db.getAllFromIndex("events", "by-record", [collection2, recordId])
2357
- ),
2358
- putGiftWrap: (gw) => wrap("putGiftWrap", async () => {
2359
- if (gw.event) {
2360
- const compact = packEvent(gw.event);
2361
- await db.put("giftwraps", { id: gw.id, compact, createdAt: gw.createdAt });
2362
- } else {
2363
- await db.put("giftwraps", { id: gw.id, createdAt: gw.createdAt });
2364
- }
2365
- }),
2366
- getGiftWrap: (id) => wrap("getGiftWrap", async () => {
2367
- const raw = await db.get("giftwraps", id);
2368
- if (!raw) return void 0;
2369
- if (raw.compact) {
2370
- return { id: raw.id, event: unpackEvent(raw.id, raw.compact), createdAt: raw.createdAt };
2371
- }
2372
- if (raw.event) {
2373
- const compact = packEvent(raw.event);
2374
- await db.put("giftwraps", { id: raw.id, compact, createdAt: raw.createdAt });
2375
- return { id: raw.id, event: raw.event, createdAt: raw.createdAt };
2376
- }
2377
- return { id: raw.id, createdAt: raw.createdAt };
2378
- }),
2379
- getAllGiftWraps: () => wrap("getAllGiftWraps", async () => {
2380
- const raws = await db.getAll("giftwraps");
2381
- const results = [];
2382
- for (const raw of raws) {
2383
- if (raw.compact) {
2384
- results.push({ id: raw.id, event: unpackEvent(raw.id, raw.compact), createdAt: raw.createdAt });
2385
- } else if (raw.event) {
2386
- const compact = packEvent(raw.event);
2387
- await db.put("giftwraps", { id: raw.id, compact, createdAt: raw.createdAt });
2388
- results.push({ id: raw.id, event: raw.event, createdAt: raw.createdAt });
2389
- } else {
2390
- results.push({ id: raw.id, createdAt: raw.createdAt });
2391
- }
2392
- }
2393
- return results;
2394
- }),
2395
- deleteGiftWrap: (id) => wrap("deleteGiftWrap", () => db.delete("giftwraps", id).then(() => void 0)),
2396
- deleteEvent: (id) => wrap("deleteEvent", () => db.delete("events", id).then(() => void 0)),
2397
- stripEventData: (id) => wrap("stripEventData", async () => {
2398
- const existing = await db.get("events", id);
2399
- if (existing) {
2400
- await db.put("events", { ...existing, data: null });
2401
- }
2402
- }),
2403
- getMeta: (key) => wrap("getMeta", () => db.get("_meta", key)),
2404
- putMeta: (key, value) => wrap("putMeta", () => db.put("_meta", value, key).then(() => void 0)),
2405
- close: () => Effect13.sync(() => db.close())
2406
- };
2407
- return handle;
2408
- });
2409
- }
2410
-
2411
- // src/layers/StorageLive.ts
2412
2406
  var StorageLive = Layer4.effect(
2413
2407
  Storage,
2414
2408
  Effect14.gen(function* () {
@@ -2945,6 +2939,12 @@ var TablinumLive = Layer9.effect(
2945
2939
  }
2946
2940
  const gw = wrapResult.success;
2947
2941
  yield* storage.putGiftWrap({ id: gw.id, event: gw, createdAt: gw.created_at });
2942
+ const metaKey = `gw_record:${event.collection}:${event.recordId}`;
2943
+ const prevMapping = yield* storage.getMeta(metaKey).pipe(
2944
+ Effect21.orElseSucceed(() => void 0)
2945
+ );
2946
+ const epochPubKey = gw.tags.find((t) => t[0] === "p")?.[1];
2947
+ yield* storage.putMeta(metaKey, { gwId: gw.id, epochPubKey });
2948
2948
  yield* Effect21.forkIn(
2949
2949
  Effect21.gen(function* () {
2950
2950
  const publishResult = yield* Effect21.result(
@@ -2957,10 +2957,80 @@ var TablinumLive = Layer9.effect(
2957
2957
  if (publishResult._tag === "Failure") {
2958
2958
  reportSyncError(config.onSyncError, publishResult.failure);
2959
2959
  }
2960
+ if (prevMapping?.epochPubKey) {
2961
+ const signingKey = getDecryptionKey(epochStore, prevMapping.epochPubKey);
2962
+ if (signingKey) {
2963
+ const deletionEvent = createDeletionEvent(
2964
+ [prevMapping.gwId],
2965
+ signingKey
2966
+ );
2967
+ yield* relay.publish(deletionEvent, [...config.relays]).pipe(
2968
+ Effect21.tapError(
2969
+ (e) => Effect21.sync(() => reportSyncError(config.onSyncError, e))
2970
+ ),
2971
+ Effect21.ignore
2972
+ );
2973
+ }
2974
+ yield* storage.deleteGiftWrap(prevMapping.gwId).pipe(Effect21.ignore);
2975
+ }
2960
2976
  }),
2961
2977
  scope
2962
2978
  );
2963
2979
  });
2980
+ const republishAllUnderCurrentEpoch = () => Effect21.gen(function* () {
2981
+ const oldGwDeletions = [];
2982
+ for (const [, def] of allSchemaEntries) {
2983
+ const collectionName = def.name;
2984
+ const allRecords = yield* storage.getAllRecords(collectionName);
2985
+ for (const record of allRecords) {
2986
+ const recordId = record.id;
2987
+ const { _d, _u, _a, _e, ...fields } = record;
2988
+ const content = _d ? JSON.stringify(null) : JSON.stringify(fields);
2989
+ const dTag = `${collectionName}:${recordId}`;
2990
+ const wrapResult = yield* Effect21.result(
2991
+ giftWrap.wrap({
2992
+ kind: 1,
2993
+ content,
2994
+ tags: [["d", dTag]],
2995
+ created_at: Math.floor(Date.now() / 1e3)
2996
+ })
2997
+ );
2998
+ if (wrapResult._tag === "Failure") continue;
2999
+ const gw = wrapResult.success;
3000
+ yield* storage.putGiftWrap({ id: gw.id, event: gw, createdAt: gw.created_at });
3001
+ const metaKey = `gw_record:${collectionName}:${recordId}`;
3002
+ const prevMapping = yield* storage.getMeta(metaKey).pipe(
3003
+ Effect21.orElseSucceed(() => void 0)
3004
+ );
3005
+ if (prevMapping) oldGwDeletions.push(prevMapping);
3006
+ const epochPubKey = gw.tags.find((t) => t[0] === "p")?.[1];
3007
+ yield* storage.putMeta(metaKey, { gwId: gw.id, epochPubKey });
3008
+ yield* relay.publish(gw, [...config.relays]).pipe(
3009
+ Effect21.tapError((e) => Effect21.sync(() => reportSyncError(config.onSyncError, e))),
3010
+ Effect21.ignore
3011
+ );
3012
+ }
3013
+ }
3014
+ const byEpoch = /* @__PURE__ */ new Map();
3015
+ for (const { gwId, epochPubKey } of oldGwDeletions) {
3016
+ const ids = byEpoch.get(epochPubKey) ?? [];
3017
+ ids.push(gwId);
3018
+ byEpoch.set(epochPubKey, ids);
3019
+ }
3020
+ for (const [epochPubKey, gwIds] of byEpoch) {
3021
+ const signingKey = getDecryptionKey(epochStore, epochPubKey);
3022
+ if (signingKey) {
3023
+ const deletionEvent = createDeletionEvent(gwIds, signingKey);
3024
+ yield* relay.publish(deletionEvent, [...config.relays]).pipe(
3025
+ Effect21.tapError((e) => Effect21.sync(() => reportSyncError(config.onSyncError, e))),
3026
+ Effect21.ignore
3027
+ );
3028
+ }
3029
+ for (const gwId of gwIds) {
3030
+ yield* storage.deleteGiftWrap(gwId).pipe(Effect21.ignore);
3031
+ }
3032
+ }
3033
+ });
2964
3034
  const knownAuthors = /* @__PURE__ */ new Set();
2965
3035
  const putMemberRecord = (record) => Effect21.gen(function* () {
2966
3036
  const existing = yield* storage.getRecord("_members", record.id);
@@ -3048,6 +3118,13 @@ var TablinumLive = Layer9.effect(
3048
3118
  addedInEpoch: getCurrentEpoch(epochStore).id
3049
3119
  });
3050
3120
  }
3121
+ const migrated = yield* storage.getMeta("migration_gw_republish").pipe(
3122
+ Effect21.orElseSucceed(() => void 0)
3123
+ );
3124
+ if (!migrated) {
3125
+ yield* republishAllUnderCurrentEpoch().pipe(Effect21.ignore);
3126
+ yield* storage.putMeta("migration_gw_republish", true);
3127
+ }
3051
3128
  const withLog = (effect) => Effect21.provideService(effect, References3.MinimumLogLevel, config.logLevel);
3052
3129
  const ensureOpen = (effect) => withLog(
3053
3130
  Effect21.gen(function* () {
@@ -3087,6 +3164,61 @@ var TablinumLive = Layer9.effect(
3087
3164
  yield* Scope4.close(scope, Exit.void);
3088
3165
  })
3089
3166
  ),
3167
+ destroy: () => withLog(
3168
+ Effect21.gen(function* () {
3169
+ if (!(yield* Ref5.get(closedRef))) {
3170
+ yield* Ref5.set(closedRef, true);
3171
+ syncHandle.stopHealing();
3172
+ yield* Scope4.close(scope, Exit.void);
3173
+ }
3174
+ yield* deleteIDBStorage(config.dbName);
3175
+ })
3176
+ ),
3177
+ leave: () => withLog(
3178
+ Effect21.gen(function* () {
3179
+ if (yield* Ref5.get(closedRef)) {
3180
+ return yield* new SyncError({ message: "Database is closed", phase: "leave" });
3181
+ }
3182
+ const allMembers = yield* storage.getAllRecords("_members");
3183
+ const activeMembers = allMembers.filter(
3184
+ (member) => !member.removedAt && member.id !== identity.publicKey
3185
+ );
3186
+ const activePubkeys = activeMembers.map((member) => member.id);
3187
+ const result = createRotation(
3188
+ epochStore,
3189
+ identity.privateKey,
3190
+ identity.publicKey,
3191
+ activePubkeys,
3192
+ [identity.publicKey]
3193
+ );
3194
+ addEpoch(epochStore, result.epoch);
3195
+ epochStore.currentEpochId = result.epoch.id;
3196
+ yield* storage.putMeta("epochs", stringifyEpochStore(epochStore));
3197
+ const memberRecord = yield* storage.getRecord("_members", identity.publicKey);
3198
+ yield* putMemberRecord({
3199
+ ...memberRecord ?? {
3200
+ id: identity.publicKey,
3201
+ addedAt: 0,
3202
+ addedInEpoch: EpochId("epoch-0")
3203
+ },
3204
+ removedAt: Date.now(),
3205
+ removedInEpoch: result.epoch.id
3206
+ });
3207
+ yield* republishAllUnderCurrentEpoch();
3208
+ yield* Effect21.forEach(
3209
+ result.wrappedEvents,
3210
+ (wrappedEvent) => relay.publish(wrappedEvent, [...config.relays]).pipe(
3211
+ Effect21.tapError((e) => Effect21.sync(() => reportSyncError(config.onSyncError, e))),
3212
+ Effect21.ignore
3213
+ ),
3214
+ { discard: true }
3215
+ );
3216
+ yield* Ref5.set(closedRef, true);
3217
+ syncHandle.stopHealing();
3218
+ yield* Scope4.close(scope, Exit.void);
3219
+ yield* deleteIDBStorage(config.dbName);
3220
+ })
3221
+ ),
3090
3222
  rebuild: () => ensureOpen(
3091
3223
  rebuild(
3092
3224
  storage,
@@ -3147,6 +3279,7 @@ var TablinumLive = Layer9.effect(
3147
3279
  removedAt: Date.now(),
3148
3280
  removedInEpoch: result.epoch.id
3149
3281
  });
3282
+ yield* republishAllUnderCurrentEpoch();
3150
3283
  yield* Effect21.forEach(
3151
3284
  result.wrappedEvents,
3152
3285
  (wrappedEvent) => relay.publish(wrappedEvent, [...config.relays]).pipe(
@@ -3224,6 +3357,9 @@ function validateConfig(config) {
3224
3357
  }
3225
3358
  });
3226
3359
  }
3360
+ function deleteDatabase(dbName) {
3361
+ return deleteIDBStorage(DatabaseName(dbName ?? "tablinum"));
3362
+ }
3227
3363
  function createTablinum(config) {
3228
3364
  return Effect22.gen(function* () {
3229
3365
  yield* validateConfig(config);
@@ -3245,6 +3381,9 @@ function createTablinum(config) {
3245
3381
  });
3246
3382
  }
3247
3383
 
3384
+ // src/svelte/tablinum.svelte.ts
3385
+ import { Effect as Effect25, Exit as Exit2, References as References6, Scope as Scope6 } from "effect";
3386
+
3248
3387
  // src/svelte/collection.svelte.ts
3249
3388
  import { Effect as Effect24, Fiber, Option as Option11, References as References5, Stream as Stream4 } from "effect";
3250
3389
 
@@ -3479,9 +3618,11 @@ var Tablinum2 = class {
3479
3618
  #closed = false;
3480
3619
  #readyState = createDeferred();
3481
3620
  #logLevel;
3621
+ #dbName;
3482
3622
  constructor(config) {
3483
3623
  this.ready = this.#readyState.promise;
3484
3624
  this.#logLevel = resolveLogLevel(config.logLevel);
3625
+ this.#dbName = config.dbName ?? "tablinum";
3485
3626
  this.#init(config);
3486
3627
  }
3487
3628
  #settleReady(err) {
@@ -3616,6 +3757,14 @@ var Tablinum2 = class {
3616
3757
  }
3617
3758
  this.status = "closed";
3618
3759
  };
3760
+ destroy = async () => {
3761
+ await this.close();
3762
+ await Effect25.runPromise(deleteDatabase(this.#dbName));
3763
+ };
3764
+ leave = async () => {
3765
+ await this.#runHandleEffect((handle) => handle.leave());
3766
+ await Effect25.runPromise(deleteDatabase(this.#dbName));
3767
+ };
3619
3768
  sync = async () => this.#runHandleEffect((handle) => handle.sync());
3620
3769
  rebuild = async () => this.#runHandleEffect((handle) => handle.rebuild());
3621
3770
  addMember = async (pubkey) => this.#runHandleEffect((handle) => handle.addMember(pubkey));
@@ -3636,6 +3785,7 @@ export {
3636
3785
  ValidationError,
3637
3786
  collection,
3638
3787
  decodeInvite,
3788
+ deleteDatabase,
3639
3789
  encodeInvite,
3640
3790
  field
3641
3791
  };