tablinum 0.2.0 → 0.5.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.
@@ -7,19 +7,20 @@ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require
7
7
  });
8
8
 
9
9
  // src/schema/field.ts
10
- function make(kind, isOptional, isArray) {
11
- return { _tag: "FieldDef", kind, isOptional, isArray };
10
+ function make(kind, isOptional, isArray, fields) {
11
+ return { _tag: "FieldDef", kind, isOptional, isArray, ...fields !== undefined && { fields } };
12
12
  }
13
13
  var field = {
14
14
  string: () => make("string", false, false),
15
15
  number: () => make("number", false, false),
16
16
  boolean: () => make("boolean", false, false),
17
17
  json: () => make("json", false, false),
18
- optional: (inner) => make(inner.kind, true, inner.isArray),
19
- array: (inner) => make(inner.kind, inner.isOptional, true)
18
+ object: (fields) => make("object", false, false, fields),
19
+ optional: (inner) => make(inner.kind, true, inner.isArray, inner.fields),
20
+ array: (inner) => make(inner.kind, inner.isOptional, true, inner.fields)
20
21
  };
21
22
  // src/schema/collection.ts
22
- var RESERVED_NAMES = new Set(["id", "_deleted", "_createdAt", "_updatedAt"]);
23
+ var RESERVED_NAMES = new Set(["id", "_d", "_u", "_e", "_a"]);
23
24
  function collection(name, fields, options) {
24
25
  if (!name || name.trim().length === 0) {
25
26
  throw new Error("Collection name must not be empty");
@@ -40,13 +41,17 @@ function collection(name, fields, options) {
40
41
  if (!fieldDef) {
41
42
  throw new Error(`Index field "${idx}" does not exist in collection "${name}"`);
42
43
  }
43
- if (fieldDef.kind === "json" || fieldDef.isArray) {
44
+ if (fieldDef.kind === "json" || fieldDef.kind === "object" || fieldDef.isArray) {
44
45
  throw new Error(`Field "${idx}" cannot be indexed (type: ${fieldDef.kind}${fieldDef.isArray ? "[]" : ""})`);
45
46
  }
46
47
  indices.push(idx);
47
48
  }
48
49
  }
49
- return { _tag: "CollectionDef", name, fields, indices };
50
+ const eventRetention = options?.eventRetention ?? 1;
51
+ if (eventRetention < 0 || !Number.isInteger(eventRetention)) {
52
+ throw new Error(`eventRetention must be a non-negative integer, got ${eventRetention}`);
53
+ }
54
+ return { _tag: "CollectionDef", name, fields, indices, eventRetention };
50
55
  }
51
56
  // src/db/invite.ts
52
57
  import { Schema as Schema2 } from "effect";
@@ -212,10 +217,10 @@ class NotFoundError extends Data.TaggedError("NotFoundError") {
212
217
  class ClosedError extends Data.TaggedError("ClosedError") {
213
218
  }
214
219
  // src/svelte/tablinum.svelte.ts
215
- import { Effect as Effect25, Exit as Exit2, Scope as Scope6 } from "effect";
220
+ import { Effect as Effect25, Exit as Exit2, References as References6, Scope as Scope6 } from "effect";
216
221
 
217
222
  // src/db/create-tablinum.ts
218
- import { Effect as Effect22, Layer as Layer9, ServiceMap as ServiceMap10 } from "effect";
223
+ import { Effect as Effect22, Layer as Layer10, References as References4, ServiceMap as ServiceMap10 } from "effect";
219
224
 
220
225
  // src/db/runtime-config.ts
221
226
  import { Effect, Schema as Schema3 } from "effect";
@@ -250,13 +255,13 @@ class Tablinum extends ServiceMap2.Service()("tablinum/Tablinum") {
250
255
  }
251
256
 
252
257
  // src/layers/TablinumLive.ts
253
- import { Effect as Effect21, Exit, Layer as Layer8, Option as Option9, PubSub as PubSub2, Ref as Ref5, Scope as Scope4 } from "effect";
258
+ 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";
254
259
 
255
260
  // src/crud/watch.ts
256
261
  import { Effect as Effect2, PubSub, Ref, Stream } from "effect";
257
262
  function watchCollection(ctx, storage, collectionName, filter, mapRecord) {
258
263
  const query = () => Effect2.map(storage.getAllRecords(collectionName), (all) => {
259
- const filtered = all.filter((r) => !r._deleted && (filter ? filter(r) : true));
264
+ const filtered = all.filter((r) => !r._d && (filter ? filter(r) : true));
260
265
  return mapRecord ? filtered.map(mapRecord) : filtered;
261
266
  });
262
267
  const changes = Stream.fromPubSub(ctx.pubsub).pipe(Stream.filter((event) => event.collection === collectionName), Stream.mapEffect(() => Effect2.gen(function* () {
@@ -301,14 +306,56 @@ function resolveWinner(existing, incoming) {
301
306
  return incoming.id < existing.id ? incoming : existing;
302
307
  }
303
308
 
309
+ // src/utils/diff.ts
310
+ function isPlainObject(value) {
311
+ return value !== null && typeof value === "object" && !Array.isArray(value);
312
+ }
313
+ function deepDiff(before, after) {
314
+ const result = {};
315
+ let hasChanges = false;
316
+ for (const key of Object.keys(after)) {
317
+ const a = before[key];
318
+ const b = after[key];
319
+ if (isPlainObject(a) && isPlainObject(b)) {
320
+ const nested = deepDiff(a, b);
321
+ if (nested !== null) {
322
+ result[key] = nested;
323
+ hasChanges = true;
324
+ }
325
+ } else if (Array.isArray(a) && Array.isArray(b)) {
326
+ if (JSON.stringify(a) !== JSON.stringify(b)) {
327
+ result[key] = b;
328
+ hasChanges = true;
329
+ }
330
+ } else if (a !== b) {
331
+ result[key] = b;
332
+ hasChanges = true;
333
+ }
334
+ }
335
+ return hasChanges ? result : null;
336
+ }
337
+ function deepMerge(target, source) {
338
+ const result = { ...target };
339
+ for (const key of Object.keys(source)) {
340
+ const t = target[key];
341
+ const s = source[key];
342
+ if (isPlainObject(t) && isPlainObject(s)) {
343
+ result[key] = deepMerge(t, s);
344
+ } else {
345
+ result[key] = s;
346
+ }
347
+ }
348
+ return result;
349
+ }
350
+
304
351
  // src/storage/records-store.ts
305
352
  function buildRecord(event) {
306
353
  return {
307
354
  id: event.recordId,
308
- _deleted: event.kind === "delete",
309
- _updatedAt: event.createdAt,
310
- _eventId: event.id,
311
- _author: event.author,
355
+ _d: event.kind === "d",
356
+ _u: event.createdAt,
357
+ _e: event.id,
358
+ _a: event.author,
312
359
  ...event.data ?? {}
313
360
  };
314
361
  }
@@ -317,15 +364,19 @@ function applyEvent(storage, event) {
317
364
  const existing = yield* storage.getRecord(event.collection, event.recordId);
318
365
  if (existing) {
319
366
  const existingMeta = {
320
- id: existing._eventId,
321
- createdAt: existing._updatedAt
367
+ id: existing._e,
368
+ createdAt: existing._u
322
369
  };
323
370
  const incomingMeta = { id: event.id, createdAt: event.createdAt };
324
371
  const winner = resolveWinner(existingMeta, incomingMeta);
325
372
  if (winner.id !== event.id)
326
373
  return false;
327
374
  }
328
- yield* storage.putRecord(event.collection, buildRecord(event));
375
+ if (existing && event.kind === "u") {
376
+ yield* storage.putRecord(event.collection, deepMerge(existing, buildRecord(event)));
377
+ } else {
378
+ yield* storage.putRecord(event.collection, buildRecord(event));
379
+ }
329
380
  return true;
330
381
  });
331
382
  }
@@ -335,15 +386,11 @@ function rebuild(storage, collections) {
335
386
  yield* storage.clearRecords(col);
336
387
  }
337
388
  const allEvents = yield* storage.getAllEvents();
338
- const sorted = [...allEvents].sort((a, b) => a.createdAt - b.createdAt);
339
- const winners = new Map;
389
+ const sorted = [...allEvents].sort((a, b) => a.createdAt - b.createdAt || (a.id < b.id ? -1 : 1));
340
390
  for (const event of sorted) {
341
- const key = `${event.collection}:${event.recordId}`;
342
- const current = winners.get(key) ?? null;
343
- winners.set(key, resolveWinner(current, event));
344
- }
345
- for (const event of winners.values()) {
346
- yield* storage.putRecord(event.collection, buildRecord(event));
391
+ if (event.data === null && event.kind !== "d")
392
+ continue;
393
+ yield* applyEvent(storage, event);
347
394
  }
348
395
  });
349
396
  }
@@ -365,6 +412,14 @@ function fieldDefToSchema(fd) {
365
412
  case "json":
366
413
  base = Schema4.Unknown;
367
414
  break;
415
+ case "object": {
416
+ const nested = {};
417
+ for (const [k, v] of Object.entries(fd.fields)) {
418
+ nested[k] = fieldDefToSchema(v);
419
+ }
420
+ base = Schema4.Struct(nested);
421
+ break;
422
+ }
368
423
  }
369
424
  if (fd.isArray) {
370
425
  base = Schema4.Array(base);
@@ -414,9 +469,25 @@ function buildPartialValidator(collectionName, def) {
414
469
  }
415
470
 
416
471
  // src/crud/collection-handle.ts
417
- import { Effect as Effect6, Option as Option3 } from "effect";
472
+ import { Effect as Effect6, Option as Option3, References } from "effect";
418
473
 
419
474
  // src/utils/uuid.ts
475
+ var alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
476
+ function toBase64url(bytes) {
477
+ let result = "";
478
+ for (let i = 0;i < bytes.length; i += 3) {
479
+ const b0 = bytes[i];
480
+ const b1 = bytes[i + 1] ?? 0;
481
+ const b2 = bytes[i + 2] ?? 0;
482
+ result += alphabet[b0 >> 2];
483
+ result += alphabet[(b0 & 3) << 4 | b1 >> 4];
484
+ if (i + 1 < bytes.length)
485
+ result += alphabet[(b1 & 15) << 2 | b2 >> 6];
486
+ if (i + 2 < bytes.length)
487
+ result += alphabet[b2 & 63];
488
+ }
489
+ return result;
490
+ }
420
491
  function uuidv7() {
421
492
  const now = Date.now();
422
493
  const bytes = new Uint8Array(16);
@@ -429,8 +500,7 @@ function uuidv7() {
429
500
  bytes[5] = now & 255;
430
501
  bytes[6] = bytes[6] & 15 | 112;
431
502
  bytes[8] = bytes[8] & 63 | 128;
432
- const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
433
- return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
503
+ return toBase64url(bytes);
434
504
  }
435
505
 
436
506
  // src/crud/query-builder.ts
@@ -448,7 +518,7 @@ function executeQuery(ctx, plan) {
448
518
  field: plan.fieldName
449
519
  });
450
520
  }
451
- if (fieldDef.kind === "json" || fieldDef.isArray) {
521
+ if (fieldDef.kind === "json" || fieldDef.kind === "object" || fieldDef.isArray) {
452
522
  return yield* new ValidationError({
453
523
  message: `Field "${plan.fieldName}" does not support filtering (type: ${fieldDef.kind}${fieldDef.isArray ? "[]" : ""})`,
454
524
  field: plan.fieldName
@@ -473,7 +543,7 @@ function executeQuery(ctx, plan) {
473
543
  } else {
474
544
  results = [...yield* ctx.storage.getAllRecords(ctx.collectionName)];
475
545
  }
476
- results = results.filter((r) => !r._deleted);
546
+ results = results.filter((r) => !r._d);
477
547
  for (const f of plan.filters) {
478
548
  results = results.filter(f);
479
549
  }
@@ -594,12 +664,62 @@ function createOrderByBuilder(storage, watchCtx, collectionName, def, fieldName,
594
664
  }
595
665
 
596
666
  // src/crud/collection-handle.ts
667
+ var KIND_FULL = { c: "create", u: "update", d: "delete" };
668
+ function sortChronologically(events) {
669
+ return [...events].sort((a, b) => a.createdAt - b.createdAt || (a.id < b.id ? -1 : 1));
670
+ }
671
+ function replayState(recordId, events, stopAtId) {
672
+ let state = null;
673
+ for (const e of events) {
674
+ if (e.kind === "d") {
675
+ state = null;
676
+ } else if (e.data !== null) {
677
+ if (state === null) {
678
+ state = { id: recordId, ...e.data };
679
+ } else {
680
+ state = deepMerge(state, e.data);
681
+ }
682
+ }
683
+ if (stopAtId !== undefined && e.id === stopAtId) {
684
+ break;
685
+ }
686
+ }
687
+ return state;
688
+ }
689
+ function promoteToSnapshot(storage, collection2, recordId, target, allSorted) {
690
+ return Effect6.gen(function* () {
691
+ const chronological = sortChronologically(allSorted);
692
+ const state = replayState(recordId, chronological, target.id);
693
+ if (state) {
694
+ yield* storage.putEvent({ ...target, data: state });
695
+ }
696
+ });
697
+ }
698
+ function pruneEvents(storage, collection2, recordId, retention) {
699
+ return Effect6.gen(function* () {
700
+ const events = yield* storage.getEventsByRecord(collection2, recordId);
701
+ if (events.length <= retention)
702
+ return;
703
+ const sorted = [...events].sort((a, b) => b.createdAt - a.createdAt || (a.id < b.id ? 1 : -1));
704
+ const retained = sorted.slice(0, retention);
705
+ const toStrip = sorted.slice(retention);
706
+ const oldestRetained = retained[retained.length - 1];
707
+ if (retention > 0 && oldestRetained?.kind === "u" && oldestRetained.data !== null) {
708
+ yield* promoteToSnapshot(storage, collection2, recordId, oldestRetained, sorted);
709
+ }
710
+ for (const e of toStrip) {
711
+ if (e.data !== null)
712
+ yield* storage.stripEventData(e.id);
713
+ }
714
+ });
715
+ }
597
716
  function mapRecord(record) {
598
- const { _deleted, _updatedAt, _author, ...fields } = record;
717
+ const { _d, _u, _a, _e, ...fields } = record;
599
718
  return fields;
600
719
  }
601
- function createCollectionHandle(def, storage, watchCtx, validator, partialValidator, makeEventId, onWrite) {
720
+ function createCollectionHandle(def, storage, watchCtx, validator, partialValidator, makeEventId, localAuthor, onWrite, logLevel = "None") {
602
721
  const collectionName = def.name;
722
+ const withLog = (effect) => Effect6.provideService(effect, References.MinimumLogLevel, logLevel);
603
723
  const commitEvent = (event) => Effect6.gen(function* () {
604
724
  yield* storage.putEvent(event);
605
725
  yield* applyEvent(storage, event);
@@ -608,11 +728,11 @@ function createCollectionHandle(def, storage, watchCtx, validator, partialValida
608
728
  yield* notifyChange(watchCtx, {
609
729
  collection: collectionName,
610
730
  recordId: event.recordId,
611
- kind: event.kind
731
+ kind: KIND_FULL[event.kind]
612
732
  });
613
733
  });
614
734
  const handle = {
615
- add: (data) => Effect6.gen(function* () {
735
+ add: (data) => withLog(Effect6.gen(function* () {
616
736
  const id = uuidv7();
617
737
  const fullRecord = { id, ...data };
618
738
  yield* validator(fullRecord);
@@ -620,38 +740,44 @@ function createCollectionHandle(def, storage, watchCtx, validator, partialValida
620
740
  id: makeEventId(),
621
741
  collection: collectionName,
622
742
  recordId: id,
623
- kind: "create",
743
+ kind: "c",
624
744
  data: fullRecord,
625
- createdAt: Date.now()
745
+ createdAt: Date.now(),
746
+ author: localAuthor
626
747
  };
627
748
  yield* commitEvent(event);
749
+ yield* Effect6.logDebug("Record added", { collection: collectionName, recordId: id, data: fullRecord });
628
750
  return id;
629
- }),
630
- update: (id, data) => Effect6.gen(function* () {
751
+ })),
752
+ update: (id, data) => withLog(Effect6.gen(function* () {
631
753
  const existing = yield* storage.getRecord(collectionName, id);
632
- if (!existing || existing._deleted) {
754
+ if (!existing || existing._d) {
633
755
  return yield* new NotFoundError({
634
756
  collection: collectionName,
635
757
  id
636
758
  });
637
759
  }
638
760
  yield* partialValidator(data);
639
- const { _deleted, _updatedAt, _author, ...existingFields } = existing;
761
+ const { _d, _u, _a, _e, ...existingFields } = existing;
640
762
  const merged = { ...existingFields, ...data, id };
641
763
  yield* validator(merged);
764
+ const diff = deepDiff(existingFields, merged);
642
765
  const event = {
643
766
  id: makeEventId(),
644
767
  collection: collectionName,
645
768
  recordId: id,
646
- kind: "update",
647
- data: merged,
648
- createdAt: Date.now()
769
+ kind: "u",
770
+ data: diff ?? { id },
771
+ createdAt: Date.now(),
772
+ author: localAuthor
649
773
  };
650
774
  yield* commitEvent(event);
651
- }),
652
- delete: (id) => Effect6.gen(function* () {
775
+ yield* Effect6.logDebug("Record updated", { collection: collectionName, recordId: id, data: diff });
776
+ yield* pruneEvents(storage, collectionName, id, def.eventRetention);
777
+ })),
778
+ delete: (id) => withLog(Effect6.gen(function* () {
653
779
  const existing = yield* storage.getRecord(collectionName, id);
654
- if (!existing || existing._deleted) {
780
+ if (!existing || existing._d) {
655
781
  return yield* new NotFoundError({
656
782
  collection: collectionName,
657
783
  id
@@ -661,17 +787,43 @@ function createCollectionHandle(def, storage, watchCtx, validator, partialValida
661
787
  id: makeEventId(),
662
788
  collection: collectionName,
663
789
  recordId: id,
664
- kind: "delete",
790
+ kind: "d",
665
791
  data: null,
666
- createdAt: Date.now()
792
+ createdAt: Date.now(),
793
+ author: localAuthor
794
+ };
795
+ yield* commitEvent(event);
796
+ yield* Effect6.logDebug("Record deleted", { collection: collectionName, recordId: id });
797
+ yield* pruneEvents(storage, collectionName, id, def.eventRetention);
798
+ })),
799
+ undo: (id) => Effect6.gen(function* () {
800
+ const existing = yield* storage.getRecord(collectionName, id);
801
+ if (!existing) {
802
+ return yield* new NotFoundError({ collection: collectionName, id });
803
+ }
804
+ const events = sortChronologically(yield* storage.getEventsByRecord(collectionName, id));
805
+ if (events.length < 2) {
806
+ return yield* new NotFoundError({ collection: collectionName, id });
807
+ }
808
+ const state = replayState(id, events.slice(0, -1));
809
+ if (!state) {
810
+ return yield* new NotFoundError({ collection: collectionName, id });
811
+ }
812
+ const event = {
813
+ id: makeEventId(),
814
+ collection: collectionName,
815
+ recordId: id,
816
+ kind: "u",
817
+ data: state,
818
+ createdAt: Date.now(),
819
+ author: localAuthor
667
820
  };
668
821
  yield* commitEvent(event);
669
- const oldEvents = yield* storage.getEventsByRecord(collectionName, id);
670
- yield* Effect6.forEach(oldEvents.filter((e) => e.id !== event.id), (e) => storage.deleteEvent(e.id), { discard: true });
822
+ yield* pruneEvents(storage, collectionName, id, def.eventRetention);
671
823
  }),
672
824
  get: (id) => Effect6.gen(function* () {
673
825
  const record = yield* storage.getRecord(collectionName, id);
674
- if (!record || record._deleted) {
826
+ if (!record || record._d) {
675
827
  return yield* new NotFoundError({
676
828
  collection: collectionName,
677
829
  id
@@ -680,10 +832,10 @@ function createCollectionHandle(def, storage, watchCtx, validator, partialValida
680
832
  return mapRecord(record);
681
833
  }),
682
834
  first: () => Effect6.map(storage.getAllRecords(collectionName), (all) => {
683
- const found = all.find((r) => !r._deleted);
835
+ const found = all.find((r) => !r._d);
684
836
  return found ? Option3.some(mapRecord(found)) : Option3.none();
685
837
  }),
686
- count: () => Effect6.map(storage.getAllRecords(collectionName), (all) => all.filter((r) => !r._deleted).length),
838
+ count: () => Effect6.map(storage.getAllRecords(collectionName), (all) => all.filter((r) => !r._d).length),
687
839
  watch: () => watchCollection(watchCtx, storage, collectionName, undefined, mapRecord),
688
840
  where: (fieldName) => createWhereClause(storage, watchCtx, collectionName, def, fieldName, mapRecord),
689
841
  orderBy: (fieldName) => createOrderByBuilder(storage, watchCtx, collectionName, def, fieldName, mapRecord)
@@ -692,7 +844,7 @@ function createCollectionHandle(def, storage, watchCtx, validator, partialValida
692
844
  }
693
845
 
694
846
  // src/sync/sync-service.ts
695
- import { Effect as Effect8, Option as Option5, Ref as Ref3, Schedule } from "effect";
847
+ import { Effect as Effect8, Layer, Option as Option5, References as References2, Ref as Ref3, Schedule } from "effect";
696
848
  import { unwrapEvent } from "nostr-tools/nip59";
697
849
  import { GiftWrap as GiftWrap2 } from "nostr-tools/kinds";
698
850
 
@@ -1263,8 +1415,13 @@ function reconcileWithRelay(storage, relay, relayUrl, publicKeys) {
1263
1415
  allNeedIds.push(id);
1264
1416
  currentMsg = nextMsg;
1265
1417
  }
1418
+ yield* Effect7.logDebug("Negentropy reconciliation complete", {
1419
+ relay: relayUrl,
1420
+ have: allHaveIds.length,
1421
+ need: allNeedIds.length
1422
+ });
1266
1423
  return { haveIds: allHaveIds, needIds: allNeedIds };
1267
- });
1424
+ }).pipe(Effect7.withLogSpan("tablinum.negentropy"));
1268
1425
  }
1269
1426
 
1270
1427
  // src/db/key-rotation.ts
@@ -1350,7 +1507,8 @@ function parseRemovalNotice(content, dTag) {
1350
1507
  }
1351
1508
 
1352
1509
  // src/sync/sync-service.ts
1353
- function createSyncHandle(storage, giftWrapHandle, relay, publishQueue, syncStatus, watchCtx, relayUrls, knownCollections, epochStore, personalPrivateKey, personalPublicKey, scope, onSyncError, onNewAuthor, onRemoved, onMembersChanged) {
1510
+ function createSyncHandle(storage, giftWrapHandle, relay, publishQueue, syncStatus, watchCtx, relayUrls, knownCollections, epochStore, personalPrivateKey, personalPublicKey, scope, logLevel, onSyncError, onNewAuthor, onRemoved, onMembersChanged) {
1511
+ const logLayer = Layer.succeed(References2.MinimumLogLevel, logLevel);
1354
1512
  const getSubscriptionPubKeys = () => {
1355
1513
  return getAllPublicKeys(epochStore);
1356
1514
  };
@@ -1360,7 +1518,7 @@ function createSyncHandle(storage, giftWrapHandle, relay, publishQueue, syncStat
1360
1518
  kind: "create"
1361
1519
  });
1362
1520
  const forkHandled = (effect) => {
1363
- Effect8.runFork(effect.pipe(Effect8.tapError((e) => Effect8.sync(() => onSyncError?.(e))), Effect8.ignore, Effect8.forkIn(scope)));
1521
+ Effect8.runFork(effect.pipe(Effect8.tapError((e) => Effect8.sync(() => onSyncError?.(e))), Effect8.ignore, Effect8.provide(logLayer), Effect8.forkIn(scope)));
1364
1522
  };
1365
1523
  let autoFlushActive = false;
1366
1524
  const autoFlushEffect = Effect8.gen(function* () {
@@ -1409,19 +1567,21 @@ function createSyncHandle(storage, giftWrapHandle, relay, publishQueue, syncStat
1409
1567
  }
1410
1568
  const collectionName = dTag.substring(0, colonIdx);
1411
1569
  const recordId = dTag.substring(colonIdx + 1);
1412
- if (!knownCollections.has(collectionName)) {
1570
+ const retention = knownCollections.get(collectionName);
1571
+ if (retention === undefined) {
1413
1572
  yield* storage.putGiftWrap({ id: remoteGw.id, createdAt: remoteGw.created_at });
1414
1573
  return null;
1415
1574
  }
1416
1575
  if (rumor.pubkey) {
1417
1576
  const reject = yield* shouldRejectWrite(rumor.pubkey);
1418
1577
  if (reject) {
1578
+ yield* Effect8.logWarning("Rejected write from removed member", { author: rumor.pubkey.slice(0, 12) });
1419
1579
  yield* storage.putGiftWrap({ id: remoteGw.id, createdAt: remoteGw.created_at });
1420
1580
  return null;
1421
1581
  }
1422
1582
  }
1423
1583
  let data = null;
1424
- let kind = "update";
1584
+ let kind = "u";
1425
1585
  const parsed = yield* Effect8.try({
1426
1586
  try: () => JSON.parse(rumor.content),
1427
1587
  catch: () => {
@@ -1435,7 +1595,7 @@ function createSyncHandle(storage, giftWrapHandle, relay, publishQueue, syncStat
1435
1595
  return null;
1436
1596
  }
1437
1597
  if (parsed === null || parsed._deleted) {
1438
- kind = "delete";
1598
+ kind = "d";
1439
1599
  } else {
1440
1600
  data = parsed;
1441
1601
  }
@@ -1451,15 +1611,14 @@ function createSyncHandle(storage, giftWrapHandle, relay, publishQueue, syncStat
1451
1611
  };
1452
1612
  yield* storage.putGiftWrap({
1453
1613
  id: remoteGw.id,
1454
- eventId: event.id,
1455
1614
  createdAt: remoteGw.created_at
1456
1615
  });
1457
1616
  yield* storage.putEvent(event);
1458
1617
  const didApply = yield* applyEvent(storage, event);
1459
- if (kind === "delete" && didApply) {
1460
- const oldEvents = yield* storage.getEventsByRecord(collectionName, recordId);
1461
- yield* Effect8.forEach(oldEvents.filter((e) => e.id !== event.id), (e) => storage.deleteEvent(e.id), { discard: true });
1618
+ if (didApply && (kind === "u" || kind === "d")) {
1619
+ yield* pruneEvents(storage, collectionName, recordId, retention);
1462
1620
  }
1621
+ yield* Effect8.logDebug("Processed gift wrap", { collection: collectionName, recordId, kind, author: author?.slice(0, 12) });
1463
1622
  if (author && onNewAuthor) {
1464
1623
  onNewAuthor(author);
1465
1624
  }
@@ -1528,15 +1687,18 @@ function createSyncHandle(storage, giftWrapHandle, relay, publishQueue, syncStat
1528
1687
  }).pipe(Effect8.tapError((err) => Effect8.sync(() => onSyncError?.(err))), Effect8.ignore);
1529
1688
  }), { discard: true });
1530
1689
  const syncRelay = (url, pubKeys, changedCollections) => Effect8.gen(function* () {
1690
+ yield* Effect8.logDebug("Syncing relay", { relay: url });
1531
1691
  const reconcileResult = yield* Effect8.result(reconcileWithRelay(storage, relay, url, Array.from(pubKeys)));
1532
1692
  if (reconcileResult._tag === "Failure") {
1533
1693
  onSyncError?.(reconcileResult.failure);
1534
1694
  return;
1535
1695
  }
1536
1696
  const { haveIds, needIds } = reconcileResult.success;
1697
+ yield* Effect8.logDebug("Relay reconciliation result", { relay: url, need: needIds.length, have: haveIds.length });
1537
1698
  if (needIds.length > 0) {
1538
1699
  const fetched = yield* relay.fetchEvents(needIds, url).pipe(Effect8.tapError((err) => Effect8.sync(() => onSyncError?.(err))), Effect8.orElseSucceed(() => []));
1539
- yield* Effect8.forEach(fetched, (remoteGw) => Effect8.gen(function* () {
1700
+ const sorted = [...fetched].sort((a, b) => a.created_at - b.created_at);
1701
+ yield* Effect8.forEach(sorted, (remoteGw) => Effect8.gen(function* () {
1540
1702
  const collection2 = yield* processGiftWrap(remoteGw).pipe(Effect8.orElseSucceed(() => null));
1541
1703
  if (collection2)
1542
1704
  changedCollections.add(collection2);
@@ -1550,9 +1712,10 @@ function createSyncHandle(storage, giftWrapHandle, relay, publishQueue, syncStat
1550
1712
  yield* relay.publish(gw.event, [url]).pipe(Effect8.andThen(storage.stripGiftWrapBlob(id)), Effect8.tapError((err) => Effect8.sync(() => onSyncError?.(err))), Effect8.ignore);
1551
1713
  }), { discard: true });
1552
1714
  }
1553
- });
1715
+ }).pipe(Effect8.withLogSpan("tablinum.syncRelay"));
1554
1716
  const handle = {
1555
1717
  sync: () => Effect8.gen(function* () {
1718
+ yield* Effect8.logInfo("Sync started");
1556
1719
  yield* syncStatus.set("syncing");
1557
1720
  yield* Ref3.set(watchCtx.replayingRef, true);
1558
1721
  const changedCollections = new Set;
@@ -1566,7 +1729,8 @@ function createSyncHandle(storage, giftWrapHandle, relay, publishQueue, syncStat
1566
1729
  yield* notifyReplayComplete(watchCtx, [...changedCollections]);
1567
1730
  yield* syncStatus.set("idle");
1568
1731
  })));
1569
- }),
1732
+ yield* Effect8.logInfo("Sync complete", { changed: [...changedCollections] });
1733
+ }).pipe(Effect8.withLogSpan("tablinum.sync")),
1570
1734
  publishLocal: (giftWrap) => Effect8.gen(function* () {
1571
1735
  if (!giftWrap.event)
1572
1736
  return;
@@ -1627,7 +1791,8 @@ var membersCollectionDef = {
1627
1791
  removedAt: optionalNumber,
1628
1792
  removedInEpoch: optionalString
1629
1793
  },
1630
- indices: []
1794
+ indices: [],
1795
+ eventRetention: 1
1631
1796
  };
1632
1797
  var AuthorProfileSchema = Schema6.Struct({
1633
1798
  name: Schema6.optionalKey(Schema6.String),
@@ -1691,7 +1856,7 @@ class SyncStatus extends ServiceMap9.Service()("tablinum/SyncStatus") {
1691
1856
  }
1692
1857
 
1693
1858
  // src/layers/IdentityLive.ts
1694
- import { Effect as Effect11, Layer } from "effect";
1859
+ import { Effect as Effect11, Layer as Layer2 } from "effect";
1695
1860
  import { hexToBytes as hexToBytes3 } from "@noble/hashes/utils.js";
1696
1861
 
1697
1862
  // src/db/identity.ts
@@ -1729,21 +1894,25 @@ function createIdentity(suppliedKey) {
1729
1894
  }
1730
1895
 
1731
1896
  // src/layers/IdentityLive.ts
1732
- var IdentityLive = Layer.effect(Identity, Effect11.gen(function* () {
1897
+ var IdentityLive = Layer2.effect(Identity, Effect11.gen(function* () {
1733
1898
  const config = yield* Config;
1734
1899
  const storage = yield* Storage;
1735
1900
  const idbKey = yield* storage.getMeta("identity_key");
1736
1901
  const resolvedKey = config.privateKey ?? (typeof idbKey === "string" && idbKey.length === 64 ? hexToBytes3(idbKey) : undefined);
1737
1902
  const identity = yield* createIdentity(resolvedKey);
1738
1903
  yield* storage.putMeta("identity_key", identity.exportKey());
1904
+ yield* Effect11.logInfo("Identity loaded", {
1905
+ publicKey: identity.publicKey.slice(0, 12) + "...",
1906
+ source: config.privateKey ? "config" : resolvedKey ? "storage" : "generated"
1907
+ });
1739
1908
  return identity;
1740
1909
  }));
1741
1910
 
1742
1911
  // src/layers/EpochStoreLive.ts
1743
- import { Effect as Effect12, Layer as Layer2, Option as Option7 } from "effect";
1912
+ import { Effect as Effect12, Layer as Layer3, Option as Option7 } from "effect";
1744
1913
  import { generateSecretKey as generateSecretKey2 } from "nostr-tools/pure";
1745
1914
  import { bytesToHex as bytesToHex3 } from "@noble/hashes/utils.js";
1746
- var EpochStoreLive = Layer2.effect(EpochStore, Effect12.gen(function* () {
1915
+ var EpochStoreLive = Layer3.effect(EpochStore, Effect12.gen(function* () {
1747
1916
  const config = yield* Config;
1748
1917
  const identity = yield* Identity;
1749
1918
  const storage = yield* Storage;
@@ -1751,21 +1920,24 @@ var EpochStoreLive = Layer2.effect(EpochStore, Effect12.gen(function* () {
1751
1920
  if (typeof idbRaw === "string") {
1752
1921
  const idbStore = deserializeEpochStore(idbRaw);
1753
1922
  if (Option7.isSome(idbStore)) {
1923
+ yield* Effect12.logInfo("Epoch store loaded", { source: "storage", epochs: idbStore.value.epochs.size });
1754
1924
  return idbStore.value;
1755
1925
  }
1756
1926
  }
1757
1927
  if (config.epochKeys && config.epochKeys.length > 0) {
1758
1928
  const store2 = createEpochStoreFromInputs(config.epochKeys);
1759
1929
  yield* storage.putMeta("epochs", stringifyEpochStore(store2));
1930
+ yield* Effect12.logInfo("Epoch store loaded", { source: "config", epochs: store2.epochs.size });
1760
1931
  return store2;
1761
1932
  }
1762
1933
  const store = createEpochStoreFromInputs([{ epochId: EpochId("epoch-0"), key: bytesToHex3(generateSecretKey2()) }], { createdBy: identity.publicKey });
1763
1934
  yield* storage.putMeta("epochs", stringifyEpochStore(store));
1935
+ yield* Effect12.logInfo("Epoch store loaded", { source: "generated", epochs: store.epochs.size });
1764
1936
  return store;
1765
1937
  }));
1766
1938
 
1767
1939
  // src/layers/StorageLive.ts
1768
- import { Effect as Effect14, Layer as Layer3 } from "effect";
1940
+ import { Effect as Effect14, Layer as Layer4 } from "effect";
1769
1941
 
1770
1942
  // src/storage/idb.ts
1771
1943
  import { Effect as Effect13 } from "effect";
@@ -1900,13 +2072,18 @@ function openIDBStorage(dbName, schema) {
1900
2072
  stripGiftWrapBlob: (id) => wrap("stripGiftWrapBlob", async () => {
1901
2073
  const existing = await db.get("giftwraps", id);
1902
2074
  if (existing) {
1903
- const { event: _, ...tombstone } = existing;
1904
- await db.put("giftwraps", tombstone);
2075
+ await db.put("giftwraps", { id: existing.id, createdAt: existing.createdAt });
1905
2076
  }
1906
2077
  }),
1907
2078
  deleteEvent: (id) => wrap("deleteEvent", () => db.delete("events", id).then(() => {
1908
2079
  return;
1909
2080
  })),
2081
+ stripEventData: (id) => wrap("stripEventData", async () => {
2082
+ const existing = await db.get("events", id);
2083
+ if (existing) {
2084
+ await db.put("events", { ...existing, data: null });
2085
+ }
2086
+ }),
1910
2087
  getMeta: (key) => wrap("getMeta", () => db.get("_meta", key)),
1911
2088
  putMeta: (key, value) => wrap("putMeta", () => db.put("_meta", value, key).then(() => {
1912
2089
  return;
@@ -1918,16 +2095,18 @@ function openIDBStorage(dbName, schema) {
1918
2095
  }
1919
2096
 
1920
2097
  // src/layers/StorageLive.ts
1921
- var StorageLive = Layer3.effect(Storage, Effect14.gen(function* () {
2098
+ var StorageLive = Layer4.effect(Storage, Effect14.gen(function* () {
1922
2099
  const config = yield* Config;
1923
- return yield* openIDBStorage(config.dbName, {
2100
+ const handle = yield* openIDBStorage(config.dbName, {
1924
2101
  ...config.schema,
1925
2102
  _members: membersCollectionDef
1926
2103
  });
2104
+ yield* Effect14.logInfo("Storage opened", { dbName: config.dbName });
2105
+ return handle;
1927
2106
  }));
1928
2107
 
1929
2108
  // src/layers/RelayLive.ts
1930
- import { Layer as Layer4 } from "effect";
2109
+ import { Layer as Layer5 } from "effect";
1931
2110
 
1932
2111
  // src/sync/relay.ts
1933
2112
  import { Effect as Effect15, Option as Option8, Schema as Schema7, ScopedCache, Scope as Scope3 } from "effect";
@@ -2140,10 +2319,10 @@ function createRelayHandle() {
2140
2319
  }
2141
2320
 
2142
2321
  // src/layers/RelayLive.ts
2143
- var RelayLive = Layer4.effect(Relay, createRelayHandle());
2322
+ var RelayLive = Layer5.effect(Relay, createRelayHandle());
2144
2323
 
2145
2324
  // src/layers/GiftWrapLive.ts
2146
- import { Effect as Effect17, Layer as Layer5 } from "effect";
2325
+ import { Effect as Effect17, Layer as Layer6 } from "effect";
2147
2326
 
2148
2327
  // src/sync/gift-wrap.ts
2149
2328
  import { Effect as Effect16 } from "effect";
@@ -2180,14 +2359,14 @@ function createEpochGiftWrapHandle(senderPrivateKey, epochStore) {
2180
2359
  }
2181
2360
 
2182
2361
  // src/layers/GiftWrapLive.ts
2183
- var GiftWrapLive = Layer5.effect(GiftWrap3, Effect17.gen(function* () {
2362
+ var GiftWrapLive = Layer6.effect(GiftWrap3, Effect17.gen(function* () {
2184
2363
  const identity = yield* Identity;
2185
2364
  const epochStore = yield* EpochStore;
2186
2365
  return createEpochGiftWrapHandle(identity.privateKey, epochStore);
2187
2366
  }));
2188
2367
 
2189
2368
  // src/layers/PublishQueueLive.ts
2190
- import { Effect as Effect19, Layer as Layer6 } from "effect";
2369
+ import { Effect as Effect19, Layer as Layer7 } from "effect";
2191
2370
 
2192
2371
  // src/sync/publish-queue.ts
2193
2372
  import { Effect as Effect18, Ref as Ref4 } from "effect";
@@ -2261,14 +2440,14 @@ function createPublishQueue(storage, relay) {
2261
2440
  }
2262
2441
 
2263
2442
  // src/layers/PublishQueueLive.ts
2264
- var PublishQueueLive = Layer6.effect(PublishQueue, Effect19.gen(function* () {
2443
+ var PublishQueueLive = Layer7.effect(PublishQueue, Effect19.gen(function* () {
2265
2444
  const storage = yield* Storage;
2266
2445
  const relay = yield* Relay;
2267
2446
  return yield* createPublishQueue(storage, relay);
2268
2447
  }));
2269
2448
 
2270
2449
  // src/layers/SyncStatusLive.ts
2271
- import { Layer as Layer7 } from "effect";
2450
+ import { Layer as Layer8 } from "effect";
2272
2451
 
2273
2452
  // src/sync/sync-status.ts
2274
2453
  import { Effect as Effect20, SubscriptionRef } from "effect";
@@ -2292,7 +2471,7 @@ function createSyncStatusHandle() {
2292
2471
  }
2293
2472
 
2294
2473
  // src/layers/SyncStatusLive.ts
2295
- var SyncStatusLive = Layer7.effect(SyncStatus, createSyncStatusHandle());
2474
+ var SyncStatusLive = Layer8.effect(SyncStatus, createSyncStatusHandle());
2296
2475
 
2297
2476
  // src/layers/TablinumLive.ts
2298
2477
  function reportSyncError(onSyncError, error) {
@@ -2313,12 +2492,12 @@ function mapMemberRecord(record) {
2313
2492
  ...record.removedInEpoch !== undefined ? { removedInEpoch: record.removedInEpoch } : {}
2314
2493
  };
2315
2494
  }
2316
- var IdentityWithDeps = IdentityLive.pipe(Layer8.provide(StorageLive));
2317
- var EpochStoreWithDeps = EpochStoreLive.pipe(Layer8.provide(IdentityWithDeps), Layer8.provide(StorageLive));
2318
- var GiftWrapWithDeps = GiftWrapLive.pipe(Layer8.provide(IdentityWithDeps), Layer8.provide(EpochStoreWithDeps));
2319
- var PublishQueueWithDeps = PublishQueueLive.pipe(Layer8.provide(StorageLive), Layer8.provide(RelayLive));
2320
- var AllServicesLive = Layer8.mergeAll(IdentityWithDeps, EpochStoreWithDeps, StorageLive, RelayLive, GiftWrapWithDeps, PublishQueueWithDeps, SyncStatusLive);
2321
- var TablinumLive = Layer8.effect(Tablinum, Effect21.gen(function* () {
2495
+ var IdentityWithDeps = IdentityLive.pipe(Layer9.provide(StorageLive));
2496
+ var EpochStoreWithDeps = EpochStoreLive.pipe(Layer9.provide(IdentityWithDeps), Layer9.provide(StorageLive));
2497
+ var GiftWrapWithDeps = GiftWrapLive.pipe(Layer9.provide(IdentityWithDeps), Layer9.provide(EpochStoreWithDeps));
2498
+ var PublishQueueWithDeps = PublishQueueLive.pipe(Layer9.provide(StorageLive), Layer9.provide(RelayLive));
2499
+ var AllServicesLive = Layer9.mergeAll(IdentityWithDeps, EpochStoreWithDeps, StorageLive, RelayLive, GiftWrapWithDeps, PublishQueueWithDeps, SyncStatusLive);
2500
+ var TablinumLive = Layer9.effect(Tablinum, Effect21.gen(function* () {
2322
2501
  const config = yield* Config;
2323
2502
  const identity = yield* Identity;
2324
2503
  const epochStore = yield* EpochStore;
@@ -2328,17 +2507,18 @@ var TablinumLive = Layer8.effect(Tablinum, Effect21.gen(function* () {
2328
2507
  const publishQueue = yield* PublishQueue;
2329
2508
  const syncStatus = yield* SyncStatus;
2330
2509
  const scope = yield* Effect21.scope;
2510
+ const logLayer = Layer9.succeed(References3.MinimumLogLevel, config.logLevel);
2331
2511
  const pubsub = yield* PubSub2.unbounded();
2332
2512
  const replayingRef = yield* Ref5.make(false);
2333
2513
  const closedRef = yield* Ref5.make(false);
2334
2514
  const watchCtx = { pubsub, replayingRef };
2335
2515
  const schemaEntries = Object.entries(config.schema);
2336
2516
  const allSchemaEntries = [...schemaEntries, ["_members", membersCollectionDef]];
2337
- const knownCollections = new Set(allSchemaEntries.map(([, def]) => def.name));
2517
+ const knownCollections = new Map(allSchemaEntries.map(([, def]) => [def.name, def.eventRetention]));
2338
2518
  let notifyAuthor;
2339
- const syncHandle = createSyncHandle(storage, giftWrap, relay, publishQueue, syncStatus, watchCtx, config.relays, knownCollections, epochStore, identity.privateKey, identity.publicKey, scope, config.onSyncError ? (error) => reportSyncError(config.onSyncError, error) : undefined, (pubkey) => notifyAuthor?.(pubkey), config.onRemoved, config.onMembersChanged);
2519
+ const syncHandle = createSyncHandle(storage, giftWrap, relay, publishQueue, syncStatus, watchCtx, config.relays, knownCollections, epochStore, identity.privateKey, identity.publicKey, scope, config.logLevel, config.onSyncError ? (error) => reportSyncError(config.onSyncError, error) : undefined, (pubkey) => notifyAuthor?.(pubkey), config.onRemoved, config.onMembersChanged);
2340
2520
  const onWrite = (event) => Effect21.gen(function* () {
2341
- const content = event.kind === "delete" ? JSON.stringify({ _deleted: true }) : JSON.stringify(event.data);
2521
+ const content = event.kind === "d" ? JSON.stringify(null) : JSON.stringify(event.data);
2342
2522
  const dTag = `${event.collection}:${event.recordId}`;
2343
2523
  const wrapResult = yield* Effect21.result(giftWrap.wrap({
2344
2524
  kind: 1,
@@ -2351,11 +2531,10 @@ var TablinumLive = Layer8.effect(Tablinum, Effect21.gen(function* () {
2351
2531
  return;
2352
2532
  }
2353
2533
  const gw = wrapResult.success;
2354
- yield* storage.putGiftWrap({ id: gw.id, eventId: event.id, createdAt: gw.created_at });
2534
+ yield* storage.putGiftWrap({ id: gw.id, createdAt: gw.created_at });
2355
2535
  yield* Effect21.forkIn(Effect21.gen(function* () {
2356
2536
  const publishResult = yield* Effect21.result(syncHandle.publishLocal({
2357
2537
  id: gw.id,
2358
- eventId: event.id,
2359
2538
  event: gw,
2360
2539
  createdAt: gw.created_at
2361
2540
  }));
@@ -2371,9 +2550,10 @@ var TablinumLive = Layer8.effect(Tablinum, Effect21.gen(function* () {
2371
2550
  id: uuidv7(),
2372
2551
  collection: "_members",
2373
2552
  recordId: record.id,
2374
- kind: existing ? "update" : "create",
2553
+ kind: existing ? "u" : "c",
2375
2554
  data: record,
2376
- createdAt: Date.now()
2555
+ createdAt: Date.now(),
2556
+ author: identity.publicKey
2377
2557
  };
2378
2558
  yield* storage.putEvent(event);
2379
2559
  yield* applyEvent(storage, event);
@@ -2414,16 +2594,21 @@ var TablinumLive = Layer8.effect(Tablinum, Effect21.gen(function* () {
2414
2594
  config.onMembersChanged?.();
2415
2595
  }
2416
2596
  }
2417
- }).pipe(Effect21.ignore, Effect21.forkIn(scope)));
2597
+ }).pipe(Effect21.ignore, Effect21.provide(logLayer), Effect21.forkIn(scope)));
2418
2598
  };
2419
2599
  const handles = new Map;
2420
2600
  for (const [, def] of allSchemaEntries) {
2421
2601
  const validator = buildValidator(def.name, def);
2422
2602
  const partialValidator = buildPartialValidator(def.name, def);
2423
- const handle = createCollectionHandle(def, storage, watchCtx, validator, partialValidator, uuidv7, onWrite);
2603
+ const handle = createCollectionHandle(def, storage, watchCtx, validator, partialValidator, uuidv7, identity.publicKey, onWrite, config.logLevel);
2424
2604
  handles.set(def.name, handle);
2425
2605
  }
2426
2606
  yield* syncHandle.startSubscription();
2607
+ yield* Effect21.logInfo("Tablinum ready", {
2608
+ dbName: config.dbName,
2609
+ collections: schemaEntries.map(([name]) => name),
2610
+ relays: config.relays
2611
+ });
2427
2612
  const selfMember = yield* storage.getRecord("_members", identity.publicKey);
2428
2613
  if (!selfMember) {
2429
2614
  yield* putMemberRecord({
@@ -2432,18 +2617,19 @@ var TablinumLive = Layer8.effect(Tablinum, Effect21.gen(function* () {
2432
2617
  addedInEpoch: getCurrentEpoch(epochStore).id
2433
2618
  });
2434
2619
  }
2435
- const ensureOpen = (effect) => Effect21.gen(function* () {
2620
+ const withLog = (effect) => Effect21.provideService(effect, References3.MinimumLogLevel, config.logLevel);
2621
+ const ensureOpen = (effect) => withLog(Effect21.gen(function* () {
2436
2622
  if (yield* Ref5.get(closedRef)) {
2437
2623
  return yield* new StorageError({ message: "Database is closed" });
2438
2624
  }
2439
2625
  return yield* effect;
2440
- });
2441
- const ensureSyncOpen = (effect) => Effect21.gen(function* () {
2626
+ }));
2627
+ const ensureSyncOpen = (effect) => withLog(Effect21.gen(function* () {
2442
2628
  if (yield* Ref5.get(closedRef)) {
2443
2629
  return yield* new SyncError({ message: "Database is closed", phase: "init" });
2444
2630
  }
2445
2631
  return yield* effect;
2446
- });
2632
+ }));
2447
2633
  const dbHandle = {
2448
2634
  collection: (name) => {
2449
2635
  const handle = handles.get(name);
@@ -2459,12 +2645,12 @@ var TablinumLive = Layer8.effect(Tablinum, Effect21.gen(function* () {
2459
2645
  relays: [...config.relays],
2460
2646
  dbName: config.dbName
2461
2647
  }),
2462
- close: () => Effect21.gen(function* () {
2648
+ close: () => withLog(Effect21.gen(function* () {
2463
2649
  if (yield* Ref5.get(closedRef))
2464
2650
  return;
2465
2651
  yield* Ref5.set(closedRef, true);
2466
2652
  yield* Scope4.close(scope, Exit.void);
2467
- }),
2653
+ })),
2468
2654
  rebuild: () => ensureOpen(rebuild(storage, allSchemaEntries.map(([, def]) => def.name))),
2469
2655
  sync: () => ensureSyncOpen(syncHandle.sync()),
2470
2656
  getSyncStatus: () => syncStatus.get(),
@@ -2508,20 +2694,52 @@ var TablinumLive = Layer8.effect(Tablinum, Effect21.gen(function* () {
2508
2694
  })),
2509
2695
  getMembers: () => ensureOpen(Effect21.gen(function* () {
2510
2696
  const allRecords = yield* storage.getAllRecords("_members");
2511
- return allRecords.filter((record) => !record._deleted).map(mapMemberRecord);
2697
+ return allRecords.filter((record) => !record._d).map(mapMemberRecord);
2698
+ })),
2699
+ getProfile: () => ensureOpen(Effect21.gen(function* () {
2700
+ const record = yield* storage.getRecord("_members", identity.publicKey);
2701
+ if (!record)
2702
+ return {};
2703
+ const profile = {};
2704
+ if (record.name !== undefined)
2705
+ profile.name = record.name;
2706
+ if (record.picture !== undefined)
2707
+ profile.picture = record.picture;
2708
+ if (record.about !== undefined)
2709
+ profile.about = record.about;
2710
+ if (record.nip05 !== undefined)
2711
+ profile.nip05 = record.nip05;
2712
+ return profile;
2512
2713
  })),
2513
2714
  setProfile: (profile) => ensureOpen(Effect21.gen(function* () {
2514
2715
  const existing = yield* storage.getRecord("_members", identity.publicKey);
2515
2716
  if (!existing) {
2516
2717
  return yield* new ValidationError({ message: "Current user is not a member" });
2517
2718
  }
2518
- yield* putMemberRecord({ ...existing, ...profile });
2719
+ const { _d, _u, _a, _e, ...memberFields } = existing;
2720
+ yield* putMemberRecord({ ...memberFields, ...profile });
2519
2721
  }))
2520
2722
  };
2521
2723
  return dbHandle;
2522
- })).pipe(Layer8.provide(AllServicesLive));
2724
+ }).pipe(Effect21.withLogSpan("tablinum.init"))).pipe(Layer9.provide(AllServicesLive));
2523
2725
 
2524
2726
  // src/db/create-tablinum.ts
2727
+ function resolveLogLevel(input) {
2728
+ if (input === undefined || input === "none")
2729
+ return "None";
2730
+ switch (input) {
2731
+ case "debug":
2732
+ return "Debug";
2733
+ case "info":
2734
+ return "Info";
2735
+ case "warning":
2736
+ return "Warn";
2737
+ case "error":
2738
+ return "Error";
2739
+ default:
2740
+ return input;
2741
+ }
2742
+ }
2525
2743
  function validateConfig(config) {
2526
2744
  return Effect22.gen(function* () {
2527
2745
  if (Object.keys(config.schema).length === 0) {
@@ -2535,22 +2753,25 @@ function createTablinum(config) {
2535
2753
  return Effect22.gen(function* () {
2536
2754
  yield* validateConfig(config);
2537
2755
  const runtimeConfig = yield* resolveRuntimeConfig(config);
2756
+ const logLevel = resolveLogLevel(config.logLevel);
2538
2757
  const configValue = {
2539
2758
  ...runtimeConfig,
2540
2759
  schema: config.schema,
2760
+ logLevel,
2541
2761
  onSyncError: config.onSyncError,
2542
2762
  onRemoved: config.onRemoved,
2543
2763
  onMembersChanged: config.onMembersChanged
2544
2764
  };
2545
- const configLayer = Layer9.succeed(Config, configValue);
2546
- const fullLayer = TablinumLive.pipe(Layer9.provide(configLayer));
2547
- const ctx = yield* Layer9.build(fullLayer);
2765
+ const configLayer = Layer10.succeed(Config, configValue);
2766
+ const logLayer = Layer10.succeed(References4.MinimumLogLevel, logLevel);
2767
+ const fullLayer = TablinumLive.pipe(Layer10.provide(configLayer), Layer10.provide(logLayer));
2768
+ const ctx = yield* Layer10.build(fullLayer);
2548
2769
  return ServiceMap10.get(ctx, Tablinum);
2549
2770
  });
2550
2771
  }
2551
2772
 
2552
2773
  // src/svelte/collection.svelte.ts
2553
- import { Effect as Effect24, Fiber, Option as Option11, Stream as Stream4 } from "effect";
2774
+ import { Effect as Effect24, Fiber, Option as Option11, References as References5, Stream as Stream4 } from "effect";
2554
2775
 
2555
2776
  // src/svelte/deferred.ts
2556
2777
  function createDeferred() {
@@ -2643,10 +2864,12 @@ class Collection {
2643
2864
  #version = $state(0);
2644
2865
  #watchAbort = null;
2645
2866
  #watchFiber = null;
2646
- _bind(handle) {
2867
+ #logLevel = "None";
2868
+ _bind(handle, logLevel = "None") {
2647
2869
  if (this.#handle)
2648
2870
  return;
2649
2871
  this.#handle = handle;
2872
+ this.#logLevel = logLevel;
2650
2873
  this.error = null;
2651
2874
  this.#settleReady();
2652
2875
  this.#startWatch();
@@ -2675,7 +2898,7 @@ class Collection {
2675
2898
  if (!abort.signal.aborted) {
2676
2899
  this.error = e instanceof Error ? e : new Error(String(e));
2677
2900
  }
2678
- }))));
2901
+ })), Effect24.provideService(References5.MinimumLogLevel, this.#logLevel)));
2679
2902
  }
2680
2903
  #touchVersion = () => {
2681
2904
  this.#version;
@@ -2687,7 +2910,7 @@ class Collection {
2687
2910
  };
2688
2911
  #run = async (getEffect) => {
2689
2912
  await this.#ready.promise;
2690
- return Effect24.runPromise(getEffect());
2913
+ return Effect24.runPromise(getEffect().pipe(Effect24.provideService(References5.MinimumLogLevel, this.#logLevel)));
2691
2914
  };
2692
2915
  add = (data) => {
2693
2916
  return this.#run(() => this.#handleOrThrow().add(data));
@@ -2698,6 +2921,9 @@ class Collection {
2698
2921
  delete = (id) => {
2699
2922
  return this.#run(() => this.#handleOrThrow().delete(id));
2700
2923
  };
2924
+ undo = (id) => {
2925
+ return this.#run(() => this.#handleOrThrow().undo(id));
2926
+ };
2701
2927
  get(id) {
2702
2928
  if (typeof id === "string") {
2703
2929
  return this.#run(() => this.#handleOrThrow().get(id));
@@ -2751,8 +2977,10 @@ class Tablinum2 {
2751
2977
  #unsubscribeRelayStatus = null;
2752
2978
  #closed = false;
2753
2979
  #readyState = createDeferred();
2980
+ #logLevel;
2754
2981
  constructor(config) {
2755
2982
  this.ready = this.#readyState.promise;
2983
+ this.#logLevel = resolveLogLevel(config.logLevel);
2756
2984
  this.#init(config);
2757
2985
  }
2758
2986
  #settleReady(err) {
@@ -2763,16 +2991,16 @@ class Tablinum2 {
2763
2991
  }
2764
2992
  }
2765
2993
  #bindCollections(handle) {
2766
- this.#members._bind(handle.members);
2994
+ this.#members._bind(handle.members, this.#logLevel);
2767
2995
  for (const [name, collection2] of this.#collections) {
2768
- collection2._bind(handle.collection(name));
2996
+ collection2._bind(handle.collection(name), this.#logLevel);
2769
2997
  }
2770
2998
  }
2771
2999
  #runHandleEffect = async (run) => {
2772
3000
  const handle = this.#requireReady();
2773
3001
  try {
2774
3002
  this.error = null;
2775
- return await Effect25.runPromise(run(handle));
3003
+ return await Effect25.runPromise(run(handle).pipe(Effect25.provideService(References6.MinimumLogLevel, this.#logLevel)));
2776
3004
  } catch (e) {
2777
3005
  this.error = e instanceof Error ? e : new Error(String(e));
2778
3006
  throw this.error;
@@ -2830,7 +3058,7 @@ class Tablinum2 {
2830
3058
  col = new Collection;
2831
3059
  this.#collections.set(name, col);
2832
3060
  if (this.#handle) {
2833
- col._bind(this.#handle.collection(name));
3061
+ col._bind(this.#handle.collection(name), this.#logLevel);
2834
3062
  }
2835
3063
  }
2836
3064
  return col;
@@ -2889,6 +3117,7 @@ class Tablinum2 {
2889
3117
  addMember = async (pubkey) => this.#runHandleEffect((handle) => handle.addMember(pubkey));
2890
3118
  removeMember = async (pubkey) => this.#runHandleEffect((handle) => handle.removeMember(pubkey));
2891
3119
  getMembers = async () => this.#runHandleEffect((handle) => handle.getMembers());
3120
+ getProfile = async () => this.#runHandleEffect((handle) => handle.getProfile());
2892
3121
  setProfile = async (profile) => this.#runHandleEffect((handle) => handle.setProfile(profile));
2893
3122
  }
2894
3123
  export {