tablinum 0.6.3 → 0.7.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) {
@@ -383,7 +631,7 @@ function buildRecord(event) {
383
631
  };
384
632
  }
385
633
  function applyEvent(storage, event) {
386
- return Effect3.gen(function* () {
634
+ return Effect4.gen(function* () {
387
635
  const existing = yield* storage.getRecord(event.collection, event.recordId);
388
636
  if (existing) {
389
637
  const existingMeta = {
@@ -403,7 +651,7 @@ function applyEvent(storage, event) {
403
651
  });
404
652
  }
405
653
  function rebuild(storage, collections) {
406
- return Effect3.gen(function* () {
654
+ return Effect4.gen(function* () {
407
655
  for (const col of collections) {
408
656
  yield* storage.clearRecords(col);
409
657
  }
@@ -419,7 +667,7 @@ function rebuild(storage, collections) {
419
667
  }
420
668
 
421
669
  // src/schema/validate.ts
422
- import { Effect as Effect4, Schema as Schema4 } from "effect";
670
+ import { Effect as Effect5, Schema as Schema4 } from "effect";
423
671
  function fieldDefToSchema(fd) {
424
672
  let base;
425
673
  switch (fd.kind) {
@@ -438,7 +686,8 @@ function fieldDefToSchema(fd) {
438
686
  case "object": {
439
687
  const nested = {};
440
688
  for (const [k, v] of Object.entries(fd.fields)) {
441
- nested[k] = fieldDefToSchema(v);
689
+ const fieldSchema = fieldDefToSchema(v);
690
+ nested[k] = v.isOptional ? Schema4.optionalKey(fieldSchema) : fieldSchema;
442
691
  }
443
692
  base = Schema4.Struct(nested);
444
693
  break;
@@ -466,10 +715,10 @@ function buildStructSchema(def, options = {}) {
466
715
  function buildValidator(collectionName, def) {
467
716
  const decode = Schema4.decodeUnknownEffect(buildStructSchema(def, { includeId: true }));
468
717
  return (input) => decode(input).pipe(
469
- Effect4.map(
718
+ Effect5.map(
470
719
  (result) => result
471
720
  ),
472
- Effect4.mapError(
721
+ Effect5.mapError(
473
722
  (e) => new ValidationError({
474
723
  message: `Validation failed for collection "${collectionName}": ${e.message}`
475
724
  })
@@ -478,7 +727,7 @@ function buildValidator(collectionName, def) {
478
727
  }
479
728
  function buildPartialValidator(collectionName, def) {
480
729
  const decode = Schema4.decodeUnknownEffect(buildStructSchema(def, { allOptional: true }));
481
- return (input) => Effect4.gen(function* () {
730
+ return (input) => Effect5.gen(function* () {
482
731
  if (typeof input !== "object" || input === null) {
483
732
  return yield* new ValidationError({
484
733
  message: `Validation failed for collection "${collectionName}": expected an object`
@@ -493,8 +742,8 @@ function buildPartialValidator(collectionName, def) {
493
742
  });
494
743
  }
495
744
  return yield* decode(record).pipe(
496
- Effect4.map((result) => result),
497
- Effect4.mapError(
745
+ Effect5.map((result) => result),
746
+ Effect5.mapError(
498
747
  (e) => new ValidationError({
499
748
  message: `Validation failed for collection "${collectionName}": ${e.message}`
500
749
  })
@@ -504,7 +753,7 @@ function buildPartialValidator(collectionName, def) {
504
753
  }
505
754
 
506
755
  // src/crud/collection-handle.ts
507
- import { Effect as Effect6, Option as Option3, References } from "effect";
756
+ import { Effect as Effect7, Option as Option3, References } from "effect";
508
757
 
509
758
  // src/utils/uuid.ts
510
759
  var alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
@@ -537,12 +786,12 @@ function uuidv7() {
537
786
  }
538
787
 
539
788
  // src/crud/query-builder.ts
540
- import { Effect as Effect5, Option as Option2, Ref as Ref2, Stream as Stream2 } from "effect";
789
+ import { Effect as Effect6, Option as Option2, Ref as Ref2, Stream as Stream2 } from "effect";
541
790
  function emptyPlan() {
542
791
  return { filters: [] };
543
792
  }
544
793
  function executeQuery(ctx, plan) {
545
- return Effect5.gen(function* () {
794
+ return Effect6.gen(function* () {
546
795
  if (plan.fieldName) {
547
796
  const fieldDef = ctx.def.fields[plan.fieldName];
548
797
  if (!fieldDef) {
@@ -618,7 +867,7 @@ function watchQuery(ctx, plan) {
618
867
  const changes = Stream2.fromPubSub(ctx.watchCtx.pubsub).pipe(
619
868
  Stream2.filter((event) => event.collection === ctx.collectionName),
620
869
  Stream2.mapEffect(
621
- () => Effect5.gen(function* () {
870
+ () => Effect6.gen(function* () {
622
871
  const replaying = yield* Ref2.get(ctx.watchCtx.replayingRef);
623
872
  if (replaying) return void 0;
624
873
  return yield* query();
@@ -627,7 +876,7 @@ function watchQuery(ctx, plan) {
627
876
  Stream2.filter((result) => result !== void 0)
628
877
  );
629
878
  return Stream2.unwrap(
630
- Effect5.gen(function* () {
879
+ Effect6.gen(function* () {
631
880
  const initial = yield* query();
632
881
  return Stream2.concat(Stream2.make(initial), changes);
633
882
  })
@@ -653,11 +902,11 @@ function makeQueryBuilder(ctx, plan) {
653
902
  offset: (n) => makeQueryBuilder(ctx, { ...plan, offset: n }),
654
903
  limit: (n) => makeQueryBuilder(ctx, { ...plan, limit: n }),
655
904
  get: () => executeQuery(ctx, plan),
656
- first: () => Effect5.map(
905
+ first: () => Effect6.map(
657
906
  executeQuery(ctx, { ...plan, limit: 1 }),
658
907
  (results) => results.length > 0 ? Option2.some(results[0]) : Option2.none()
659
908
  ),
660
- count: () => Effect5.map(executeQuery(ctx, plan), (results) => results.length),
909
+ count: () => Effect6.map(executeQuery(ctx, plan), (results) => results.length),
661
910
  watch: () => watchQuery(ctx, plan)
662
911
  };
663
912
  }
@@ -763,7 +1012,7 @@ function replayState(recordId, events, stopAtId) {
763
1012
  return state;
764
1013
  }
765
1014
  function promoteToSnapshot(storage, collection2, recordId, target, allSorted) {
766
- return Effect6.gen(function* () {
1015
+ return Effect7.gen(function* () {
767
1016
  const chronological = sortChronologically(allSorted);
768
1017
  const state = replayState(recordId, chronological, target.id);
769
1018
  if (state) {
@@ -772,7 +1021,7 @@ function promoteToSnapshot(storage, collection2, recordId, target, allSorted) {
772
1021
  });
773
1022
  }
774
1023
  function pruneEvents(storage, collection2, recordId, retention) {
775
- return Effect6.gen(function* () {
1024
+ return Effect7.gen(function* () {
776
1025
  const events = yield* storage.getEventsByRecord(collection2, recordId);
777
1026
  if (events.length <= retention) return;
778
1027
  const sorted = [...events].sort((a, b) => b.createdAt - a.createdAt || (a.id < b.id ? 1 : -1));
@@ -793,8 +1042,8 @@ function mapRecord(record) {
793
1042
  }
794
1043
  function createCollectionHandle(def, storage, watchCtx, validator, partialValidator, makeEventId, localAuthor, onWrite, logLevel = "None") {
795
1044
  const collectionName = def.name;
796
- const withLog = (effect) => Effect6.provideService(effect, References.MinimumLogLevel, logLevel);
797
- const commitEvent = (event) => Effect6.gen(function* () {
1045
+ const withLog = (effect) => Effect7.provideService(effect, References.MinimumLogLevel, logLevel);
1046
+ const commitEvent = (event) => Effect7.gen(function* () {
798
1047
  yield* storage.putEvent(event);
799
1048
  yield* applyEvent(storage, event);
800
1049
  if (onWrite) yield* onWrite(event);
@@ -806,7 +1055,7 @@ function createCollectionHandle(def, storage, watchCtx, validator, partialValida
806
1055
  });
807
1056
  const handle = {
808
1057
  add: (data) => withLog(
809
- Effect6.gen(function* () {
1058
+ Effect7.gen(function* () {
810
1059
  const id = uuidv7();
811
1060
  const fullRecord = { id, ...data };
812
1061
  yield* validator(fullRecord);
@@ -820,7 +1069,7 @@ function createCollectionHandle(def, storage, watchCtx, validator, partialValida
820
1069
  author: localAuthor
821
1070
  };
822
1071
  yield* commitEvent(event);
823
- yield* Effect6.logDebug("Record added", {
1072
+ yield* Effect7.logDebug("Record added", {
824
1073
  collection: collectionName,
825
1074
  recordId: id,
826
1075
  data: fullRecord
@@ -829,7 +1078,7 @@ function createCollectionHandle(def, storage, watchCtx, validator, partialValida
829
1078
  })
830
1079
  ),
831
1080
  update: (id, data) => withLog(
832
- Effect6.gen(function* () {
1081
+ Effect7.gen(function* () {
833
1082
  const existing = yield* storage.getRecord(collectionName, id);
834
1083
  if (!existing || existing._d) {
835
1084
  return yield* new NotFoundError({
@@ -852,7 +1101,7 @@ function createCollectionHandle(def, storage, watchCtx, validator, partialValida
852
1101
  author: localAuthor
853
1102
  };
854
1103
  yield* commitEvent(event);
855
- yield* Effect6.logDebug("Record updated", {
1104
+ yield* Effect7.logDebug("Record updated", {
856
1105
  collection: collectionName,
857
1106
  recordId: id,
858
1107
  data: diff
@@ -861,7 +1110,7 @@ function createCollectionHandle(def, storage, watchCtx, validator, partialValida
861
1110
  })
862
1111
  ),
863
1112
  delete: (id) => withLog(
864
- Effect6.gen(function* () {
1113
+ Effect7.gen(function* () {
865
1114
  const existing = yield* storage.getRecord(collectionName, id);
866
1115
  if (!existing || existing._d) {
867
1116
  return yield* new NotFoundError({
@@ -879,11 +1128,11 @@ function createCollectionHandle(def, storage, watchCtx, validator, partialValida
879
1128
  author: localAuthor
880
1129
  };
881
1130
  yield* commitEvent(event);
882
- yield* Effect6.logDebug("Record deleted", { collection: collectionName, recordId: id });
1131
+ yield* Effect7.logDebug("Record deleted", { collection: collectionName, recordId: id });
883
1132
  yield* pruneEvents(storage, collectionName, id, def.eventRetention);
884
1133
  })
885
1134
  ),
886
- undo: (id) => Effect6.gen(function* () {
1135
+ undo: (id) => Effect7.gen(function* () {
887
1136
  const existing = yield* storage.getRecord(collectionName, id);
888
1137
  if (!existing) {
889
1138
  return yield* new NotFoundError({ collection: collectionName, id });
@@ -908,7 +1157,7 @@ function createCollectionHandle(def, storage, watchCtx, validator, partialValida
908
1157
  yield* commitEvent(event);
909
1158
  yield* pruneEvents(storage, collectionName, id, def.eventRetention);
910
1159
  }),
911
- get: (id) => Effect6.gen(function* () {
1160
+ get: (id) => Effect7.gen(function* () {
912
1161
  const record = yield* storage.getRecord(collectionName, id);
913
1162
  if (!record || record._d) {
914
1163
  return yield* new NotFoundError({
@@ -918,11 +1167,11 @@ function createCollectionHandle(def, storage, watchCtx, validator, partialValida
918
1167
  }
919
1168
  return mapRecord(record);
920
1169
  }),
921
- first: () => Effect6.map(storage.getAllRecords(collectionName), (all) => {
1170
+ first: () => Effect7.map(storage.getAllRecords(collectionName), (all) => {
922
1171
  const found = all.find((r) => !r._d);
923
1172
  return found ? Option3.some(mapRecord(found)) : Option3.none();
924
1173
  }),
925
- count: () => Effect6.map(storage.getAllRecords(collectionName), (all) => all.filter((r) => !r._d).length),
1174
+ count: () => Effect7.map(storage.getAllRecords(collectionName), (all) => all.filter((r) => !r._d).length),
926
1175
  watch: () => watchCollection(watchCtx, storage, collectionName, void 0, mapRecord),
927
1176
  where: (fieldName) => createWhereClause(
928
1177
  storage,
@@ -945,12 +1194,12 @@ function createCollectionHandle(def, storage, watchCtx, validator, partialValida
945
1194
  }
946
1195
 
947
1196
  // src/sync/sync-service.ts
948
- import { Duration, Effect as Effect8, Layer, Option as Option5, References as References2, Ref as Ref3, Schedule } from "effect";
1197
+ import { Duration, Effect as Effect9, Layer, Option as Option5, References as References2, Ref as Ref3, Schedule } from "effect";
949
1198
  import { unwrapEvent } from "nostr-tools/nip59";
950
1199
  import { GiftWrap as GiftWrap2 } from "nostr-tools/kinds";
951
1200
 
952
1201
  // src/sync/negentropy.ts
953
- import { Effect as Effect7 } from "effect";
1202
+ import { Effect as Effect8 } from "effect";
954
1203
 
955
1204
  // src/vendor/negentropy.js
956
1205
  var PROTOCOL_VERSION = 97;
@@ -1437,14 +1686,14 @@ function itemCompare(a, b) {
1437
1686
  }
1438
1687
 
1439
1688
  // src/sync/negentropy.ts
1440
- import { hexToBytes as hexToBytes2 } from "@noble/hashes/utils.js";
1689
+ import { hexToBytes as hexToBytes3 } from "@noble/hashes/utils.js";
1441
1690
  import { GiftWrap } from "nostr-tools/kinds";
1442
1691
  function reconcileWithRelay(storage, relay, relayUrl, publicKeys) {
1443
- return Effect7.gen(function* () {
1692
+ return Effect8.gen(function* () {
1444
1693
  const allGiftWraps = yield* storage.getAllGiftWraps();
1445
1694
  const storageVector = new NegentropyStorageVector();
1446
1695
  for (const gw of allGiftWraps) {
1447
- storageVector.insert(gw.createdAt, hexToBytes2(gw.id));
1696
+ storageVector.insert(gw.createdAt, hexToBytes3(gw.id));
1448
1697
  }
1449
1698
  storageVector.seal();
1450
1699
  const neg = new Negentropy(storageVector, 0);
@@ -1455,7 +1704,7 @@ function reconcileWithRelay(storage, relay, relayUrl, publicKeys) {
1455
1704
  const allHaveIds = [];
1456
1705
  const allNeedIds = [];
1457
1706
  const subId = `neg-${Date.now()}`;
1458
- const initialMsg = yield* Effect7.tryPromise({
1707
+ const initialMsg = yield* Effect8.tryPromise({
1459
1708
  try: () => neg.initiate(),
1460
1709
  catch: (e) => new SyncError({
1461
1710
  message: `Negentropy initiate failed: ${e instanceof Error ? e.message : String(e)}`,
@@ -1467,7 +1716,7 @@ function reconcileWithRelay(storage, relay, relayUrl, publicKeys) {
1467
1716
  while (currentMsg !== null) {
1468
1717
  const response = yield* relay.sendNegMsg(relayUrl, subId, filter, currentMsg);
1469
1718
  if (response.msgHex === null) break;
1470
- const reconcileResult = yield* Effect7.tryPromise({
1719
+ const reconcileResult = yield* Effect8.tryPromise({
1471
1720
  try: () => neg.reconcile(response.msgHex),
1472
1721
  catch: (e) => new SyncError({
1473
1722
  message: `Negentropy reconcile failed: ${e instanceof Error ? e.message : String(e)}`,
@@ -1480,13 +1729,13 @@ function reconcileWithRelay(storage, relay, relayUrl, publicKeys) {
1480
1729
  for (const id of needIds) allNeedIds.push(id);
1481
1730
  currentMsg = nextMsg;
1482
1731
  }
1483
- yield* Effect7.logDebug("Negentropy reconciliation complete", {
1732
+ yield* Effect8.logDebug("Negentropy reconciliation complete", {
1484
1733
  relay: relayUrl,
1485
1734
  have: allHaveIds.length,
1486
1735
  need: allNeedIds.length
1487
1736
  });
1488
1737
  return { haveIds: allHaveIds, needIds: allNeedIds };
1489
- }).pipe(Effect7.withLogSpan("tablinum.negentropy"));
1738
+ }).pipe(Effect8.withLogSpan("tablinum.negentropy"));
1490
1739
  }
1491
1740
 
1492
1741
  // src/db/key-rotation.ts
@@ -1580,52 +1829,52 @@ function createSyncHandle(storage, giftWrapHandle, relay, publishQueue, syncStat
1580
1829
  kind: "create"
1581
1830
  });
1582
1831
  const forkHandled = (effect) => {
1583
- Effect8.runFork(
1832
+ Effect9.runFork(
1584
1833
  effect.pipe(
1585
- Effect8.tapError((e) => Effect8.sync(() => onSyncError?.(e))),
1586
- Effect8.ignore,
1587
- Effect8.provide(logLayer),
1588
- Effect8.forkIn(scope)
1834
+ Effect9.tapError((e) => Effect9.sync(() => onSyncError?.(e))),
1835
+ Effect9.ignore,
1836
+ Effect9.provide(logLayer),
1837
+ Effect9.forkIn(scope)
1589
1838
  )
1590
1839
  );
1591
1840
  };
1592
1841
  let autoFlushActive = false;
1593
- const autoFlushEffect = Effect8.gen(function* () {
1842
+ const autoFlushEffect = Effect9.gen(function* () {
1594
1843
  const size = yield* publishQueue.size();
1595
1844
  if (size === 0) return;
1596
1845
  yield* syncStatus.set("syncing");
1597
1846
  yield* publishQueue.flush(relayUrls);
1598
1847
  const remaining = yield* publishQueue.size();
1599
- if (remaining > 0) yield* Effect8.fail("pending");
1848
+ if (remaining > 0) yield* Effect9.fail("pending");
1600
1849
  }).pipe(
1601
- Effect8.ensuring(syncStatus.set("idle")),
1602
- Effect8.retry({ schedule: Schedule.exponential(5e3).pipe(Schedule.jittered), times: 10 }),
1603
- Effect8.ignore
1850
+ Effect9.ensuring(syncStatus.set("idle")),
1851
+ Effect9.retry({ schedule: Schedule.exponential(5e3).pipe(Schedule.jittered), times: 10 }),
1852
+ Effect9.ignore
1604
1853
  );
1605
1854
  const scheduleAutoFlush = () => {
1606
1855
  if (autoFlushActive) return;
1607
1856
  autoFlushActive = true;
1608
1857
  forkHandled(
1609
1858
  autoFlushEffect.pipe(
1610
- Effect8.ensuring(
1611
- Effect8.sync(() => {
1859
+ Effect9.ensuring(
1860
+ Effect9.sync(() => {
1612
1861
  autoFlushActive = false;
1613
1862
  })
1614
1863
  )
1615
1864
  )
1616
1865
  );
1617
1866
  };
1618
- const shouldRejectWrite = (authorPubkey) => Effect8.gen(function* () {
1867
+ const shouldRejectWrite = (authorPubkey) => Effect9.gen(function* () {
1619
1868
  const memberRecord = yield* storage.getRecord("_members", authorPubkey);
1620
1869
  if (!memberRecord) return false;
1621
1870
  return !!memberRecord.removedAt;
1622
1871
  });
1623
1872
  const storeGiftWrapShell = (gw) => storage.putGiftWrap({ id: gw.id, event: gw, createdAt: gw.created_at });
1624
- const unwrapGiftWrap = (remoteGw) => Effect8.gen(function* () {
1873
+ const unwrapGiftWrap = (remoteGw) => Effect9.gen(function* () {
1625
1874
  const existing = yield* storage.getGiftWrap(remoteGw.id);
1626
1875
  if (existing) return null;
1627
1876
  const rumor = yield* giftWrapHandle.unwrap(remoteGw).pipe(
1628
- Effect8.orElseSucceed(() => null)
1877
+ Effect9.orElseSucceed(() => null)
1629
1878
  );
1630
1879
  if (!rumor) {
1631
1880
  yield* storeGiftWrapShell(remoteGw);
@@ -1653,7 +1902,7 @@ function createSyncHandle(storage, giftWrapHandle, relay, publishQueue, syncStat
1653
1902
  recordId: dTag.substring(colonIdx + 1)
1654
1903
  };
1655
1904
  });
1656
- const applyUnwrappedEvent = (uw) => Effect8.gen(function* () {
1905
+ const applyUnwrappedEvent = (uw) => Effect9.gen(function* () {
1657
1906
  const { giftWrap: remoteGw, rumor, collection: collectionName, recordId } = uw;
1658
1907
  const retention = knownCollections.get(collectionName);
1659
1908
  if (retention === void 0) {
@@ -1663,17 +1912,17 @@ function createSyncHandle(storage, giftWrapHandle, relay, publishQueue, syncStat
1663
1912
  if (rumor.pubkey) {
1664
1913
  const reject = yield* shouldRejectWrite(rumor.pubkey);
1665
1914
  if (reject) {
1666
- yield* Effect8.logWarning("Rejected write from removed member", {
1915
+ yield* Effect9.logWarning("Rejected write from removed member", {
1667
1916
  author: rumor.pubkey.slice(0, 12)
1668
1917
  });
1669
1918
  yield* storeGiftWrapShell(remoteGw);
1670
1919
  return null;
1671
1920
  }
1672
1921
  }
1673
- const parsed = yield* Effect8.try({
1922
+ const parsed = yield* Effect9.try({
1674
1923
  try: () => JSON.parse(rumor.content),
1675
1924
  catch: () => void 0
1676
- }).pipe(Effect8.orElseSucceed(() => void 0));
1925
+ }).pipe(Effect9.orElseSucceed(() => void 0));
1677
1926
  if (parsed === void 0) {
1678
1927
  yield* storeGiftWrapShell(remoteGw);
1679
1928
  return null;
@@ -1705,7 +1954,7 @@ function createSyncHandle(storage, giftWrapHandle, relay, publishQueue, syncStat
1705
1954
  if (didApply && (kind === "u" || kind === "d")) {
1706
1955
  yield* pruneEvents(storage, collectionName, recordId, retention);
1707
1956
  }
1708
- yield* Effect8.logDebug("Processed gift wrap", {
1957
+ yield* Effect9.logDebug("Processed gift wrap", {
1709
1958
  collection: collectionName,
1710
1959
  recordId,
1711
1960
  kind,
@@ -1716,33 +1965,33 @@ function createSyncHandle(storage, giftWrapHandle, relay, publishQueue, syncStat
1716
1965
  }
1717
1966
  return collectionName;
1718
1967
  });
1719
- const reconcileRelay = (url, pubKeys) => Effect8.gen(function* () {
1720
- yield* Effect8.logDebug("Syncing relay", { relay: url });
1968
+ const reconcileRelay = (url, pubKeys) => Effect9.gen(function* () {
1969
+ yield* Effect9.logDebug("Syncing relay", { relay: url });
1721
1970
  const { haveIds, needIds } = yield* reconcileWithRelay(
1722
1971
  storage,
1723
1972
  relay,
1724
1973
  url,
1725
1974
  Array.from(pubKeys)
1726
1975
  ).pipe(
1727
- Effect8.tapError((err) => Effect8.sync(() => onSyncError?.(err))),
1728
- Effect8.orElseSucceed(() => ({ haveIds: [], needIds: [] }))
1976
+ Effect9.tapError((err) => Effect9.sync(() => onSyncError?.(err))),
1977
+ Effect9.orElseSucceed(() => ({ haveIds: [], needIds: [] }))
1729
1978
  );
1730
- yield* Effect8.logDebug("Relay reconciliation result", {
1979
+ yield* Effect9.logDebug("Relay reconciliation result", {
1731
1980
  relay: url,
1732
1981
  need: needIds.length,
1733
1982
  have: haveIds.length
1734
1983
  });
1735
1984
  const events = needIds.length > 0 ? yield* relay.fetchEvents(needIds, url).pipe(
1736
- Effect8.tapError((err) => Effect8.sync(() => onSyncError?.(err))),
1737
- Effect8.orElseSucceed(() => [])
1985
+ Effect9.tapError((err) => Effect9.sync(() => onSyncError?.(err))),
1986
+ Effect9.orElseSucceed(() => [])
1738
1987
  ) : [];
1739
1988
  return {
1740
1989
  events,
1741
1990
  haveIds: haveIds.map((id) => ({ id, url }))
1742
1991
  };
1743
- }).pipe(Effect8.withLogSpan("tablinum.reconcileRelay"));
1744
- const syncAllRelays = (pubKeys, changedCollections) => Effect8.gen(function* () {
1745
- const results = yield* Effect8.forEach(
1992
+ }).pipe(Effect9.withLogSpan("tablinum.reconcileRelay"));
1993
+ const syncAllRelays = (pubKeys, changedCollections) => Effect9.gen(function* () {
1994
+ const results = yield* Effect9.forEach(
1746
1995
  relayUrls,
1747
1996
  (url) => reconcileRelay(url, pubKeys),
1748
1997
  { concurrency: "unbounded" }
@@ -1759,44 +2008,44 @@ function createSyncHandle(storage, giftWrapHandle, relay, publishQueue, syncStat
1759
2008
  }
1760
2009
  const unwrapped = [];
1761
2010
  for (const gw of allGiftWraps) {
1762
- const result = yield* unwrapGiftWrap(gw).pipe(Effect8.orElseSucceed(() => null));
2011
+ const result = yield* unwrapGiftWrap(gw).pipe(Effect9.orElseSucceed(() => null));
1763
2012
  if (result) unwrapped.push(result);
1764
2013
  }
1765
2014
  unwrapped.sort((a, b) => a.rumor.created_at - b.rumor.created_at || (a.rumor.id < b.rumor.id ? -1 : 1));
1766
2015
  for (const event of unwrapped) {
1767
2016
  const collection2 = yield* applyUnwrappedEvent(event).pipe(
1768
- Effect8.orElseSucceed(() => null)
2017
+ Effect9.orElseSucceed(() => null)
1769
2018
  );
1770
2019
  if (collection2) changedCollections.add(collection2);
1771
2020
  }
1772
2021
  const allHaveIds = results.flatMap((r) => r.haveIds);
1773
- yield* Effect8.forEach(
2022
+ yield* Effect9.forEach(
1774
2023
  allHaveIds,
1775
- ({ id, url }) => Effect8.gen(function* () {
2024
+ ({ id, url }) => Effect9.gen(function* () {
1776
2025
  const gw = yield* storage.getGiftWrap(id);
1777
2026
  if (!gw?.event) return;
1778
2027
  yield* relay.publish(gw.event, [url]).pipe(
1779
- Effect8.tapError((err) => Effect8.sync(() => onSyncError?.(err))),
1780
- Effect8.ignore
2028
+ Effect9.tapError((err) => Effect9.sync(() => onSyncError?.(err))),
2029
+ Effect9.ignore
1781
2030
  );
1782
2031
  }),
1783
2032
  { concurrency: "unbounded", discard: true }
1784
2033
  );
1785
- }).pipe(Effect8.withLogSpan("tablinum.syncAllRelays"));
1786
- const processGiftWrap = (remoteGw) => Effect8.gen(function* () {
2034
+ }).pipe(Effect9.withLogSpan("tablinum.syncAllRelays"));
2035
+ const processGiftWrap = (remoteGw) => Effect9.gen(function* () {
1787
2036
  const uw = yield* unwrapGiftWrap(remoteGw);
1788
2037
  if (!uw) return null;
1789
2038
  return yield* applyUnwrappedEvent(uw);
1790
2039
  });
1791
- const processRealtimeGiftWrap = (remoteGw) => Effect8.gen(function* () {
1792
- const collection2 = yield* processGiftWrap(remoteGw).pipe(Effect8.orElseSucceed(() => null));
2040
+ const processRealtimeGiftWrap = (remoteGw) => Effect9.gen(function* () {
2041
+ const collection2 = yield* processGiftWrap(remoteGw).pipe(Effect9.orElseSucceed(() => null));
1793
2042
  if (collection2) {
1794
2043
  yield* notifyCollectionUpdated(collection2);
1795
2044
  }
1796
2045
  });
1797
- const processRotationGiftWrap = (remoteGw) => Effect8.gen(function* () {
1798
- const unwrapResult = yield* Effect8.result(
1799
- Effect8.try({
2046
+ const processRotationGiftWrap = (remoteGw) => Effect9.gen(function* () {
2047
+ const unwrapResult = yield* Effect9.result(
2048
+ Effect9.try({
1800
2049
  try: () => unwrapEvent(remoteGw, personalPrivateKey),
1801
2050
  catch: (e) => new CryptoError({
1802
2051
  message: `Rotation unwrap failed: ${e instanceof Error ? e.message : String(e)}`,
@@ -1846,73 +2095,73 @@ function createSyncHandle(storage, giftWrapHandle, relay, publishQueue, syncStat
1846
2095
  yield* handle.addEpochSubscription(epoch.publicKey);
1847
2096
  return true;
1848
2097
  });
1849
- const subscribeAcrossRelays = (filter, onEvent) => Effect8.forEach(
2098
+ const subscribeAcrossRelays = (filter, onEvent) => Effect9.forEach(
1850
2099
  relayUrls,
1851
- (url) => Effect8.gen(function* () {
2100
+ (url) => Effect9.gen(function* () {
1852
2101
  yield* relay.subscribe(filter, url, (event) => {
1853
2102
  forkHandled(onEvent(event));
1854
2103
  }).pipe(
1855
- Effect8.tapError((err) => Effect8.sync(() => onSyncError?.(err))),
1856
- Effect8.ignore
2104
+ Effect9.tapError((err) => Effect9.sync(() => onSyncError?.(err))),
2105
+ Effect9.ignore
1857
2106
  );
1858
2107
  }),
1859
2108
  { concurrency: "unbounded", discard: true }
1860
2109
  );
1861
2110
  let healingActive = false;
1862
- const healingEffect = Effect8.gen(function* () {
2111
+ const healingEffect = Effect9.gen(function* () {
1863
2112
  if (!healingActive) return;
1864
2113
  const status = yield* syncStatus.get();
1865
2114
  if (status === "syncing") return;
1866
2115
  yield* syncStatus.set("syncing");
1867
- yield* Effect8.gen(function* () {
2116
+ yield* Effect9.gen(function* () {
1868
2117
  const pubKeys = getSubscriptionPubKeys();
1869
2118
  const changedCollections = /* @__PURE__ */ new Set();
1870
2119
  yield* syncAllRelays(pubKeys, changedCollections);
1871
2120
  if (changedCollections.size > 0) {
1872
2121
  yield* notifyReplayComplete(watchCtx, [...changedCollections]);
1873
2122
  }
1874
- }).pipe(Effect8.ensuring(syncStatus.set("idle")));
1875
- }).pipe(Effect8.ignore);
2123
+ }).pipe(Effect9.ensuring(syncStatus.set("idle")));
2124
+ }).pipe(Effect9.ignore);
1876
2125
  const handle = {
1877
- sync: () => Effect8.gen(function* () {
1878
- yield* Effect8.logInfo("Sync started");
2126
+ sync: () => Effect9.gen(function* () {
2127
+ yield* Effect9.logInfo("Sync started");
1879
2128
  yield* syncStatus.set("syncing");
1880
2129
  yield* Ref3.set(watchCtx.replayingRef, true);
1881
2130
  const changedCollections = /* @__PURE__ */ new Set();
1882
- yield* Effect8.gen(function* () {
2131
+ yield* Effect9.gen(function* () {
1883
2132
  const pubKeys = getSubscriptionPubKeys();
1884
2133
  yield* syncAllRelays(pubKeys, changedCollections);
1885
- yield* publishQueue.flush(relayUrls).pipe(Effect8.ignore);
2134
+ yield* publishQueue.flush(relayUrls).pipe(Effect9.ignore);
1886
2135
  }).pipe(
1887
- Effect8.ensuring(
1888
- Effect8.gen(function* () {
2136
+ Effect9.ensuring(
2137
+ Effect9.gen(function* () {
1889
2138
  yield* notifyReplayComplete(watchCtx, [...changedCollections]);
1890
2139
  yield* syncStatus.set("idle");
1891
2140
  })
1892
2141
  )
1893
2142
  );
1894
- yield* Effect8.logInfo("Sync complete", { changed: [...changedCollections] });
1895
- }).pipe(Effect8.withLogSpan("tablinum.sync")),
1896
- publishLocal: (giftWrap) => Effect8.gen(function* () {
2143
+ yield* Effect9.logInfo("Sync complete", { changed: [...changedCollections] });
2144
+ }).pipe(Effect9.withLogSpan("tablinum.sync")),
2145
+ publishLocal: (giftWrap) => Effect9.gen(function* () {
1897
2146
  if (!giftWrap.event) return;
1898
2147
  yield* relay.publish(giftWrap.event, relayUrls).pipe(
1899
- Effect8.tapError(
2148
+ Effect9.tapError(
1900
2149
  () => storage.putGiftWrap(giftWrap).pipe(
1901
- Effect8.andThen(publishQueue.enqueue(giftWrap.id)),
1902
- Effect8.andThen(Effect8.sync(() => scheduleAutoFlush()))
2150
+ Effect9.andThen(publishQueue.enqueue(giftWrap.id)),
2151
+ Effect9.andThen(Effect9.sync(() => scheduleAutoFlush()))
1903
2152
  )
1904
2153
  ),
1905
- Effect8.tapError((err) => Effect8.sync(() => onSyncError?.(err))),
1906
- Effect8.ignore
2154
+ Effect9.tapError((err) => Effect9.sync(() => onSyncError?.(err))),
2155
+ Effect9.ignore
1907
2156
  );
1908
2157
  }),
1909
- startSubscription: () => Effect8.gen(function* () {
2158
+ startSubscription: () => Effect9.gen(function* () {
1910
2159
  const pubKeys = getSubscriptionPubKeys();
1911
2160
  yield* subscribeAcrossRelays({ kinds: [GiftWrap2], "#p": pubKeys }, processRealtimeGiftWrap);
1912
2161
  if (!pubKeys.includes(personalPublicKey)) {
1913
2162
  yield* subscribeAcrossRelays(
1914
2163
  { kinds: [GiftWrap2], "#p": [personalPublicKey] },
1915
- (event) => Effect8.result(processRotationGiftWrap(event)).pipe(Effect8.asVoid)
2164
+ (event) => Effect9.result(processRotationGiftWrap(event)).pipe(Effect9.asVoid)
1916
2165
  );
1917
2166
  }
1918
2167
  }),
@@ -1921,10 +2170,10 @@ function createSyncHandle(storage, giftWrapHandle, relay, publishQueue, syncStat
1921
2170
  if (healingActive) return;
1922
2171
  healingActive = true;
1923
2172
  forkHandled(
1924
- Effect8.sleep(Duration.minutes(5)).pipe(
1925
- Effect8.andThen(healingEffect),
1926
- Effect8.repeat(Schedule.spaced(Duration.minutes(5))),
1927
- Effect8.ensuring(Effect8.sync(() => {
2173
+ Effect9.sleep(Duration.minutes(5)).pipe(
2174
+ Effect9.andThen(healingEffect),
2175
+ Effect9.repeat(Schedule.spaced(Duration.minutes(5))),
2176
+ Effect9.ensuring(Effect9.sync(() => {
1928
2177
  healingActive = false;
1929
2178
  }))
1930
2179
  )
@@ -1936,8 +2185,8 @@ function createSyncHandle(storage, giftWrapHandle, relay, publishQueue, syncStat
1936
2185
  };
1937
2186
  forkHandled(
1938
2187
  publishQueue.size().pipe(
1939
- Effect8.flatMap(
1940
- (size) => Effect8.sync(() => {
2188
+ Effect9.flatMap(
2189
+ (size) => Effect9.sync(() => {
1941
2190
  if (size > 0) scheduleAutoFlush();
1942
2191
  })
1943
2192
  )
@@ -1947,7 +2196,7 @@ function createSyncHandle(storage, giftWrapHandle, relay, publishQueue, syncStat
1947
2196
  }
1948
2197
 
1949
2198
  // src/db/members.ts
1950
- import { Effect as Effect9, Option as Option6, Schema as Schema6 } from "effect";
2199
+ import { Effect as Effect10, Option as Option6, Schema as Schema6 } from "effect";
1951
2200
  var optionalString = {
1952
2201
  _tag: "FieldDef",
1953
2202
  kind: "string",
@@ -1996,15 +2245,15 @@ var AuthorProfileSchema = Schema6.Struct({
1996
2245
  });
1997
2246
  var decodeAuthorProfile = Schema6.decodeUnknownEffect(Schema6.fromJsonString(AuthorProfileSchema));
1998
2247
  function fetchAuthorProfile(relay, relayUrls, pubkey) {
1999
- return Effect9.gen(function* () {
2248
+ return Effect10.gen(function* () {
2000
2249
  for (const url of relayUrls) {
2001
- const result = yield* Effect9.result(
2250
+ const result = yield* Effect10.result(
2002
2251
  relay.fetchByFilter({ kinds: [0], authors: [pubkey], limit: 1 }, url)
2003
2252
  );
2004
2253
  if (result._tag === "Success" && result.success.length > 0) {
2005
2254
  return yield* decodeAuthorProfile(result.success[0].content).pipe(
2006
- Effect9.map(Option6.some),
2007
- Effect9.orElseSucceed(() => Option6.none())
2255
+ Effect10.map(Option6.some),
2256
+ Effect10.orElseSucceed(() => Option6.none())
2008
2257
  );
2009
2258
  }
2010
2259
  }
@@ -2054,15 +2303,15 @@ var SyncStatus = class extends ServiceMap9.Service()(
2054
2303
  };
2055
2304
 
2056
2305
  // src/layers/IdentityLive.ts
2057
- import { Effect as Effect11, Layer as Layer2 } from "effect";
2058
- import { hexToBytes as hexToBytes3 } from "@noble/hashes/utils.js";
2306
+ import { Effect as Effect12, Layer as Layer2 } from "effect";
2307
+ import { hexToBytes as hexToBytes4 } from "@noble/hashes/utils.js";
2059
2308
 
2060
2309
  // src/db/identity.ts
2061
- import { Effect as Effect10 } from "effect";
2310
+ import { Effect as Effect11 } from "effect";
2062
2311
  import { getPublicKey as getPublicKey2 } from "nostr-tools/pure";
2063
- import { bytesToHex as bytesToHex2 } from "@noble/hashes/utils.js";
2312
+ import { bytesToHex as bytesToHex3 } from "@noble/hashes/utils.js";
2064
2313
  function createIdentity(suppliedKey) {
2065
- return Effect10.gen(function* () {
2314
+ return Effect11.gen(function* () {
2066
2315
  let privateKey;
2067
2316
  if (suppliedKey) {
2068
2317
  if (suppliedKey.length !== 32) {
@@ -2075,8 +2324,8 @@ function createIdentity(suppliedKey) {
2075
2324
  privateKey = new Uint8Array(32);
2076
2325
  crypto.getRandomValues(privateKey);
2077
2326
  }
2078
- const privateKeyHex = bytesToHex2(privateKey);
2079
- const publicKey = yield* Effect10.try({
2327
+ const privateKeyHex = bytesToHex3(privateKey);
2328
+ const publicKey = yield* Effect11.try({
2080
2329
  try: () => getPublicKey2(privateKey),
2081
2330
  catch: (e) => new CryptoError({
2082
2331
  message: `Failed to derive public key: ${e instanceof Error ? e.message : String(e)}`,
@@ -2094,14 +2343,14 @@ function createIdentity(suppliedKey) {
2094
2343
  // src/layers/IdentityLive.ts
2095
2344
  var IdentityLive = Layer2.effect(
2096
2345
  Identity,
2097
- Effect11.gen(function* () {
2346
+ Effect12.gen(function* () {
2098
2347
  const config = yield* Config;
2099
2348
  const storage = yield* Storage;
2100
2349
  const idbKey = yield* storage.getMeta("identity_key");
2101
- const resolvedKey = config.privateKey ?? (typeof idbKey === "string" && idbKey.length === 64 ? hexToBytes3(idbKey) : void 0);
2350
+ const resolvedKey = config.privateKey ?? (typeof idbKey === "string" && idbKey.length === 64 ? hexToBytes4(idbKey) : void 0);
2102
2351
  const identity = yield* createIdentity(resolvedKey);
2103
2352
  yield* storage.putMeta("identity_key", identity.exportKey());
2104
- yield* Effect11.logInfo("Identity loaded", {
2353
+ yield* Effect12.logInfo("Identity loaded", {
2105
2354
  publicKey: identity.publicKey.slice(0, 12) + "...",
2106
2355
  source: config.privateKey ? "config" : resolvedKey ? "storage" : "generated"
2107
2356
  });
@@ -2110,12 +2359,12 @@ var IdentityLive = Layer2.effect(
2110
2359
  );
2111
2360
 
2112
2361
  // src/layers/EpochStoreLive.ts
2113
- import { Effect as Effect12, Layer as Layer3, Option as Option7 } from "effect";
2362
+ import { Effect as Effect13, Layer as Layer3, Option as Option7 } from "effect";
2114
2363
  import { generateSecretKey as generateSecretKey2 } from "nostr-tools/pure";
2115
- import { bytesToHex as bytesToHex3 } from "@noble/hashes/utils.js";
2364
+ import { bytesToHex as bytesToHex4 } from "@noble/hashes/utils.js";
2116
2365
  var EpochStoreLive = Layer3.effect(
2117
2366
  EpochStore,
2118
- Effect12.gen(function* () {
2367
+ Effect13.gen(function* () {
2119
2368
  const config = yield* Config;
2120
2369
  const identity = yield* Identity;
2121
2370
  const storage = yield* Storage;
@@ -2134,7 +2383,7 @@ var EpochStoreLive = Layer3.effect(
2134
2383
  return existing !== void 0 && existing.privateKey === ek.key;
2135
2384
  });
2136
2385
  if (configIsSubset) {
2137
- yield* Effect12.logInfo("Epoch store loaded", {
2386
+ yield* Effect13.logInfo("Epoch store loaded", {
2138
2387
  source: "storage",
2139
2388
  epochs: idbStore.epochs.size
2140
2389
  });
@@ -2143,271 +2392,28 @@ var EpochStoreLive = Layer3.effect(
2143
2392
  }
2144
2393
  const store2 = createEpochStoreFromInputs(config.epochKeys);
2145
2394
  yield* storage.putMeta("epochs", stringifyEpochStore(store2));
2146
- yield* Effect12.logInfo("Epoch store loaded", { source: "config", epochs: store2.epochs.size });
2395
+ yield* Effect13.logInfo("Epoch store loaded", { source: "config", epochs: store2.epochs.size });
2147
2396
  return store2;
2148
2397
  }
2149
2398
  if (idbStore) {
2150
- yield* Effect12.logInfo("Epoch store loaded", {
2399
+ yield* Effect13.logInfo("Epoch store loaded", {
2151
2400
  source: "storage",
2152
2401
  epochs: idbStore.epochs.size
2153
2402
  });
2154
2403
  return idbStore;
2155
2404
  }
2156
2405
  const store = createEpochStoreFromInputs(
2157
- [{ epochId: EpochId("epoch-0"), key: bytesToHex3(generateSecretKey2()) }],
2406
+ [{ epochId: EpochId("epoch-0"), key: bytesToHex4(generateSecretKey2()) }],
2158
2407
  { createdBy: identity.publicKey }
2159
2408
  );
2160
2409
  yield* storage.putMeta("epochs", stringifyEpochStore(store));
2161
- yield* Effect12.logInfo("Epoch store loaded", { source: "generated", epochs: store.epochs.size });
2410
+ yield* Effect13.logInfo("Epoch store loaded", { source: "generated", epochs: store.epochs.size });
2162
2411
  return store;
2163
2412
  })
2164
2413
  );
2165
2414
 
2166
2415
  // src/layers/StorageLive.ts
2167
2416
  import { Effect as Effect14, Layer as Layer4 } from "effect";
2168
-
2169
- // src/storage/idb.ts
2170
- import { Effect as Effect13 } from "effect";
2171
- import { openDB } from "idb";
2172
-
2173
- // src/sync/compact-event.ts
2174
- import { bytesToHex as bytesToHex4, hexToBytes as hexToBytes4 } from "@noble/hashes/utils.js";
2175
- var VERSION = 1;
2176
- var HEADER_SIZE = 133;
2177
- function base64ToBytes(base64) {
2178
- const binary = atob(base64);
2179
- const bytes = new Uint8Array(binary.length);
2180
- for (let i = 0; i < binary.length; i++) {
2181
- bytes[i] = binary.charCodeAt(i);
2182
- }
2183
- return bytes;
2184
- }
2185
- function bytesToBase64(bytes) {
2186
- let binary = "";
2187
- for (let i = 0; i < bytes.length; i++) {
2188
- binary += String.fromCharCode(bytes[i]);
2189
- }
2190
- return btoa(binary);
2191
- }
2192
- function packEvent(event) {
2193
- const pubkey = hexToBytes4(event.pubkey);
2194
- const sig = hexToBytes4(event.sig);
2195
- const recipientTag = event.tags.find((t) => t[0] === "p");
2196
- if (!recipientTag) throw new Error("Gift wrap missing #p tag");
2197
- if (event.tags.some((t) => t[0] !== "p")) {
2198
- throw new Error("Gift wrap has unexpected non-p tags; compact encoding would lose them");
2199
- }
2200
- const recipient = hexToBytes4(recipientTag[1]);
2201
- const createdAtBuf = new Uint8Array(4);
2202
- new DataView(createdAtBuf.buffer).setUint32(0, event.created_at, false);
2203
- const content = base64ToBytes(event.content);
2204
- const result = new Uint8Array(HEADER_SIZE + content.length);
2205
- result[0] = VERSION;
2206
- result.set(pubkey, 1);
2207
- result.set(sig, 33);
2208
- result.set(recipient, 97);
2209
- result.set(createdAtBuf, 129);
2210
- result.set(content, HEADER_SIZE);
2211
- return result;
2212
- }
2213
- function unpackEvent(id, compact) {
2214
- const version = compact[0];
2215
- if (version !== VERSION) throw new Error(`Unknown compact event version: ${version}`);
2216
- const pubkey = bytesToHex4(compact.slice(1, 33));
2217
- const sig = bytesToHex4(compact.slice(33, 97));
2218
- const recipient = bytesToHex4(compact.slice(97, 129));
2219
- const dv = new DataView(compact.buffer, compact.byteOffset + 129, 4);
2220
- const createdAt = dv.getUint32(0, false);
2221
- const content = bytesToBase64(compact.slice(HEADER_SIZE));
2222
- return {
2223
- id,
2224
- pubkey,
2225
- sig,
2226
- created_at: createdAt,
2227
- kind: 1059,
2228
- tags: [["p", recipient]],
2229
- content
2230
- };
2231
- }
2232
-
2233
- // src/storage/idb.ts
2234
- var DB_NAME = "tablinum";
2235
- function storeName(collection2) {
2236
- return `col_${collection2}`;
2237
- }
2238
- function computeSchemaSig(schema) {
2239
- return Object.entries(schema).sort(([a], [b]) => a.localeCompare(b)).map(([name, def]) => {
2240
- const indices = [...def.indices ?? []].sort().join(",");
2241
- return `${name}:${indices}`;
2242
- }).join("|");
2243
- }
2244
- function wrap(label, fn) {
2245
- return Effect13.tryPromise({
2246
- try: fn,
2247
- catch: (e) => new StorageError({
2248
- message: `IndexedDB ${label} failed: ${e instanceof Error ? e.message : String(e)}`,
2249
- cause: e
2250
- })
2251
- });
2252
- }
2253
- function upgradeSchema(database, schema, tx) {
2254
- if (!database.objectStoreNames.contains("_meta")) {
2255
- database.createObjectStore("_meta");
2256
- }
2257
- if (!database.objectStoreNames.contains("events")) {
2258
- const events = database.createObjectStore("events", { keyPath: "id" });
2259
- events.createIndex("by-record", ["collection", "recordId"]);
2260
- }
2261
- if (!database.objectStoreNames.contains("giftwraps")) {
2262
- database.createObjectStore("giftwraps", { keyPath: "id" });
2263
- }
2264
- const expectedStores = /* @__PURE__ */ new Set();
2265
- for (const [, def] of Object.entries(schema)) {
2266
- const sn = storeName(def.name);
2267
- expectedStores.add(sn);
2268
- if (!database.objectStoreNames.contains(sn)) {
2269
- const store = database.createObjectStore(sn, { keyPath: "id" });
2270
- for (const idx of def.indices ?? []) {
2271
- store.createIndex(idx, idx);
2272
- }
2273
- } else {
2274
- const store = tx.objectStore(sn);
2275
- const existingIndices = new Set(Array.from(store.indexNames));
2276
- const wantedIndices = new Set(def.indices ?? []);
2277
- for (const idx of existingIndices) {
2278
- if (!wantedIndices.has(idx)) store.deleteIndex(idx);
2279
- }
2280
- for (const idx of wantedIndices) {
2281
- if (!existingIndices.has(idx)) store.createIndex(idx, idx);
2282
- }
2283
- }
2284
- }
2285
- for (const existing of Array.from(database.objectStoreNames)) {
2286
- if (existing.startsWith("col_") && !expectedStores.has(existing)) {
2287
- database.deleteObjectStore(existing);
2288
- }
2289
- }
2290
- tx.objectStore("_meta").put(computeSchemaSig(schema), "schema_sig");
2291
- }
2292
- function openIDBStorage(dbName, schema) {
2293
- return Effect13.gen(function* () {
2294
- const name = dbName ?? DB_NAME;
2295
- const schemaSig = computeSchemaSig(schema);
2296
- if (typeof indexedDB === "undefined") {
2297
- return yield* Effect13.fail(
2298
- new StorageError({
2299
- message: "IndexedDB is not available in this environment"
2300
- })
2301
- );
2302
- }
2303
- const probeDb = yield* Effect13.tryPromise({
2304
- try: () => openDB(name),
2305
- catch: (e) => new StorageError({ message: "Failed to open IndexedDB", cause: e })
2306
- });
2307
- const currentVersion = probeDb.version;
2308
- let needsUpgrade = true;
2309
- if (probeDb.objectStoreNames.contains("_meta")) {
2310
- const storedSig = yield* Effect13.tryPromise({
2311
- try: () => probeDb.get("_meta", "schema_sig"),
2312
- catch: () => new StorageError({ message: "Failed to read schema meta" })
2313
- }).pipe(Effect13.catch(() => Effect13.succeed(void 0)));
2314
- needsUpgrade = storedSig !== schemaSig;
2315
- }
2316
- probeDb.close();
2317
- const db = needsUpgrade ? yield* Effect13.tryPromise({
2318
- try: () => openDB(name, currentVersion + 1, {
2319
- upgrade(database, _oldVersion, _newVersion, transaction) {
2320
- upgradeSchema(database, schema, transaction);
2321
- }
2322
- }),
2323
- catch: (e) => new StorageError({ message: "Failed to open IndexedDB", cause: e })
2324
- }) : yield* Effect13.tryPromise({
2325
- try: () => openDB(name),
2326
- catch: (e) => new StorageError({ message: "Failed to open IndexedDB", cause: e })
2327
- });
2328
- yield* Effect13.addFinalizer(() => Effect13.sync(() => db.close()));
2329
- const handle = {
2330
- putRecord: (collection2, record) => wrap("putRecord", () => db.put(storeName(collection2), record).then(() => void 0)),
2331
- getRecord: (collection2, id) => wrap("getRecord", () => db.get(storeName(collection2), id)),
2332
- getAllRecords: (collection2) => wrap("getAllRecords", () => db.getAll(storeName(collection2))),
2333
- countRecords: (collection2) => wrap("countRecords", () => db.count(storeName(collection2))),
2334
- clearRecords: (collection2) => wrap("clearRecords", () => db.clear(storeName(collection2))),
2335
- getByIndex: (collection2, indexName, value) => wrap("getByIndex", () => db.getAllFromIndex(storeName(collection2), indexName, value)),
2336
- getByIndexRange: (collection2, indexName, range) => wrap("getByIndexRange", () => db.getAllFromIndex(storeName(collection2), indexName, range)),
2337
- getAllSorted: (collection2, indexName, direction) => wrap("getAllSorted", async () => {
2338
- const sn = storeName(collection2);
2339
- const tx = db.transaction(sn, "readonly");
2340
- const store = tx.objectStore(sn);
2341
- const index = store.index(indexName);
2342
- const results = [];
2343
- let cursor = await index.openCursor(null, direction ?? "next");
2344
- while (cursor) {
2345
- results.push(cursor.value);
2346
- cursor = await cursor.continue();
2347
- }
2348
- return results;
2349
- }),
2350
- putEvent: (event) => wrap("putEvent", () => db.put("events", event).then(() => void 0)),
2351
- getEvent: (id) => wrap("getEvent", () => db.get("events", id)),
2352
- getAllEvents: () => wrap("getAllEvents", () => db.getAll("events")),
2353
- getEventsByRecord: (collection2, recordId) => wrap(
2354
- "getEventsByRecord",
2355
- () => db.getAllFromIndex("events", "by-record", [collection2, recordId])
2356
- ),
2357
- putGiftWrap: (gw) => wrap("putGiftWrap", async () => {
2358
- if (gw.event) {
2359
- const compact = packEvent(gw.event);
2360
- await db.put("giftwraps", { id: gw.id, compact, createdAt: gw.createdAt });
2361
- } else {
2362
- await db.put("giftwraps", { id: gw.id, createdAt: gw.createdAt });
2363
- }
2364
- }),
2365
- getGiftWrap: (id) => wrap("getGiftWrap", async () => {
2366
- const raw = await db.get("giftwraps", id);
2367
- if (!raw) return void 0;
2368
- if (raw.compact) {
2369
- return { id: raw.id, event: unpackEvent(raw.id, raw.compact), createdAt: raw.createdAt };
2370
- }
2371
- if (raw.event) {
2372
- const compact = packEvent(raw.event);
2373
- await db.put("giftwraps", { id: raw.id, compact, createdAt: raw.createdAt });
2374
- return { id: raw.id, event: raw.event, createdAt: raw.createdAt };
2375
- }
2376
- return { id: raw.id, createdAt: raw.createdAt };
2377
- }),
2378
- getAllGiftWraps: () => wrap("getAllGiftWraps", async () => {
2379
- const raws = await db.getAll("giftwraps");
2380
- const results = [];
2381
- for (const raw of raws) {
2382
- if (raw.compact) {
2383
- results.push({ id: raw.id, event: unpackEvent(raw.id, raw.compact), createdAt: raw.createdAt });
2384
- } else if (raw.event) {
2385
- const compact = packEvent(raw.event);
2386
- await db.put("giftwraps", { id: raw.id, compact, createdAt: raw.createdAt });
2387
- results.push({ id: raw.id, event: raw.event, createdAt: raw.createdAt });
2388
- } else {
2389
- results.push({ id: raw.id, createdAt: raw.createdAt });
2390
- }
2391
- }
2392
- return results;
2393
- }),
2394
- deleteGiftWrap: (id) => wrap("deleteGiftWrap", () => db.delete("giftwraps", id).then(() => void 0)),
2395
- deleteEvent: (id) => wrap("deleteEvent", () => db.delete("events", id).then(() => void 0)),
2396
- stripEventData: (id) => wrap("stripEventData", async () => {
2397
- const existing = await db.get("events", id);
2398
- if (existing) {
2399
- await db.put("events", { ...existing, data: null });
2400
- }
2401
- }),
2402
- getMeta: (key) => wrap("getMeta", () => db.get("_meta", key)),
2403
- putMeta: (key, value) => wrap("putMeta", () => db.put("_meta", value, key).then(() => void 0)),
2404
- close: () => Effect13.sync(() => db.close())
2405
- };
2406
- return handle;
2407
- });
2408
- }
2409
-
2410
- // src/layers/StorageLive.ts
2411
2417
  var StorageLive = Layer4.effect(
2412
2418
  Storage,
2413
2419
  Effect14.gen(function* () {
@@ -3086,6 +3092,60 @@ var TablinumLive = Layer9.effect(
3086
3092
  yield* Scope4.close(scope, Exit.void);
3087
3093
  })
3088
3094
  ),
3095
+ destroy: () => withLog(
3096
+ Effect21.gen(function* () {
3097
+ if (!(yield* Ref5.get(closedRef))) {
3098
+ yield* Ref5.set(closedRef, true);
3099
+ syncHandle.stopHealing();
3100
+ yield* Scope4.close(scope, Exit.void);
3101
+ }
3102
+ yield* deleteIDBStorage(config.dbName);
3103
+ })
3104
+ ),
3105
+ leave: () => withLog(
3106
+ Effect21.gen(function* () {
3107
+ if (yield* Ref5.get(closedRef)) {
3108
+ return yield* new SyncError({ message: "Database is closed", phase: "leave" });
3109
+ }
3110
+ const allMembers = yield* storage.getAllRecords("_members");
3111
+ const activeMembers = allMembers.filter(
3112
+ (member) => !member.removedAt && member.id !== identity.publicKey
3113
+ );
3114
+ const activePubkeys = activeMembers.map((member) => member.id);
3115
+ const result = createRotation(
3116
+ epochStore,
3117
+ identity.privateKey,
3118
+ identity.publicKey,
3119
+ activePubkeys,
3120
+ [identity.publicKey]
3121
+ );
3122
+ addEpoch(epochStore, result.epoch);
3123
+ epochStore.currentEpochId = result.epoch.id;
3124
+ yield* storage.putMeta("epochs", stringifyEpochStore(epochStore));
3125
+ const memberRecord = yield* storage.getRecord("_members", identity.publicKey);
3126
+ yield* putMemberRecord({
3127
+ ...memberRecord ?? {
3128
+ id: identity.publicKey,
3129
+ addedAt: 0,
3130
+ addedInEpoch: EpochId("epoch-0")
3131
+ },
3132
+ removedAt: Date.now(),
3133
+ removedInEpoch: result.epoch.id
3134
+ });
3135
+ yield* Effect21.forEach(
3136
+ result.wrappedEvents,
3137
+ (wrappedEvent) => relay.publish(wrappedEvent, [...config.relays]).pipe(
3138
+ Effect21.tapError((e) => Effect21.sync(() => reportSyncError(config.onSyncError, e))),
3139
+ Effect21.ignore
3140
+ ),
3141
+ { discard: true }
3142
+ );
3143
+ yield* Ref5.set(closedRef, true);
3144
+ syncHandle.stopHealing();
3145
+ yield* Scope4.close(scope, Exit.void);
3146
+ yield* deleteIDBStorage(config.dbName);
3147
+ })
3148
+ ),
3089
3149
  rebuild: () => ensureOpen(
3090
3150
  rebuild(
3091
3151
  storage,
@@ -3223,6 +3283,9 @@ function validateConfig(config) {
3223
3283
  }
3224
3284
  });
3225
3285
  }
3286
+ function deleteDatabase(dbName) {
3287
+ return deleteIDBStorage(DatabaseName(dbName ?? "tablinum"));
3288
+ }
3226
3289
  function createTablinum(config) {
3227
3290
  return Effect22.gen(function* () {
3228
3291
  yield* validateConfig(config);
@@ -3244,6 +3307,9 @@ function createTablinum(config) {
3244
3307
  });
3245
3308
  }
3246
3309
 
3310
+ // src/svelte/tablinum.svelte.ts
3311
+ import { Effect as Effect25, Exit as Exit2, References as References6, Scope as Scope6 } from "effect";
3312
+
3247
3313
  // src/svelte/collection.svelte.ts
3248
3314
  import { Effect as Effect24, Fiber, Option as Option11, References as References5, Stream as Stream4 } from "effect";
3249
3315
 
@@ -3478,9 +3544,11 @@ var Tablinum2 = class {
3478
3544
  #closed = false;
3479
3545
  #readyState = createDeferred();
3480
3546
  #logLevel;
3547
+ #dbName;
3481
3548
  constructor(config) {
3482
3549
  this.ready = this.#readyState.promise;
3483
3550
  this.#logLevel = resolveLogLevel(config.logLevel);
3551
+ this.#dbName = config.dbName ?? "tablinum";
3484
3552
  this.#init(config);
3485
3553
  }
3486
3554
  #settleReady(err) {
@@ -3615,6 +3683,14 @@ var Tablinum2 = class {
3615
3683
  }
3616
3684
  this.status = "closed";
3617
3685
  };
3686
+ destroy = async () => {
3687
+ await this.close();
3688
+ await Effect25.runPromise(deleteDatabase(this.#dbName));
3689
+ };
3690
+ leave = async () => {
3691
+ await this.#runHandleEffect((handle) => handle.leave());
3692
+ await Effect25.runPromise(deleteDatabase(this.#dbName));
3693
+ };
3618
3694
  sync = async () => this.#runHandleEffect((handle) => handle.sync());
3619
3695
  rebuild = async () => this.#runHandleEffect((handle) => handle.rebuild());
3620
3696
  addMember = async (pubkey) => this.#runHandleEffect((handle) => handle.addMember(pubkey));
@@ -3635,6 +3711,7 @@ export {
3635
3711
  ValidationError,
3636
3712
  collection,
3637
3713
  decodeInvite,
3714
+ deleteDatabase,
3638
3715
  encodeInvite,
3639
3716
  field
3640
3717
  };