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.
package/dist/index.js CHANGED
@@ -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,16 +41,20 @@ 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/create-tablinum.ts
52
- import { Effect as Effect22, Layer as Layer9, ServiceMap as ServiceMap10 } from "effect";
57
+ import { Effect as Effect22, Layer as Layer10, References as References4, ServiceMap as ServiceMap10 } from "effect";
53
58
 
54
59
  // src/db/runtime-config.ts
55
60
  import { Effect, Schema as Schema2 } from "effect";
@@ -215,13 +220,13 @@ class Tablinum extends ServiceMap2.Service()("tablinum/Tablinum") {
215
220
  }
216
221
 
217
222
  // src/layers/TablinumLive.ts
218
- import { Effect as Effect21, Exit, Layer as Layer8, Option as Option9, PubSub as PubSub2, Ref as Ref5, Scope as Scope4 } from "effect";
223
+ 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";
219
224
 
220
225
  // src/crud/watch.ts
221
226
  import { Effect as Effect2, PubSub, Ref, Stream } from "effect";
222
227
  function watchCollection(ctx, storage, collectionName, filter, mapRecord) {
223
228
  const query = () => Effect2.map(storage.getAllRecords(collectionName), (all) => {
224
- const filtered = all.filter((r) => !r._deleted && (filter ? filter(r) : true));
229
+ const filtered = all.filter((r) => !r._d && (filter ? filter(r) : true));
225
230
  return mapRecord ? filtered.map(mapRecord) : filtered;
226
231
  });
227
232
  const changes = Stream.fromPubSub(ctx.pubsub).pipe(Stream.filter((event) => event.collection === collectionName), Stream.mapEffect(() => Effect2.gen(function* () {
@@ -266,14 +271,56 @@ function resolveWinner(existing, incoming) {
266
271
  return incoming.id < existing.id ? incoming : existing;
267
272
  }
268
273
 
274
+ // src/utils/diff.ts
275
+ function isPlainObject(value) {
276
+ return value !== null && typeof value === "object" && !Array.isArray(value);
277
+ }
278
+ function deepDiff(before, after) {
279
+ const result = {};
280
+ let hasChanges = false;
281
+ for (const key of Object.keys(after)) {
282
+ const a = before[key];
283
+ const b = after[key];
284
+ if (isPlainObject(a) && isPlainObject(b)) {
285
+ const nested = deepDiff(a, b);
286
+ if (nested !== null) {
287
+ result[key] = nested;
288
+ hasChanges = true;
289
+ }
290
+ } else if (Array.isArray(a) && Array.isArray(b)) {
291
+ if (JSON.stringify(a) !== JSON.stringify(b)) {
292
+ result[key] = b;
293
+ hasChanges = true;
294
+ }
295
+ } else if (a !== b) {
296
+ result[key] = b;
297
+ hasChanges = true;
298
+ }
299
+ }
300
+ return hasChanges ? result : null;
301
+ }
302
+ function deepMerge(target, source) {
303
+ const result = { ...target };
304
+ for (const key of Object.keys(source)) {
305
+ const t = target[key];
306
+ const s = source[key];
307
+ if (isPlainObject(t) && isPlainObject(s)) {
308
+ result[key] = deepMerge(t, s);
309
+ } else {
310
+ result[key] = s;
311
+ }
312
+ }
313
+ return result;
314
+ }
315
+
269
316
  // src/storage/records-store.ts
270
317
  function buildRecord(event) {
271
318
  return {
272
319
  id: event.recordId,
273
- _deleted: event.kind === "delete",
274
- _updatedAt: event.createdAt,
275
- _eventId: event.id,
276
- _author: event.author,
320
+ _d: event.kind === "d",
321
+ _u: event.createdAt,
322
+ _e: event.id,
323
+ _a: event.author,
277
324
  ...event.data ?? {}
278
325
  };
279
326
  }
@@ -282,15 +329,19 @@ function applyEvent(storage, event) {
282
329
  const existing = yield* storage.getRecord(event.collection, event.recordId);
283
330
  if (existing) {
284
331
  const existingMeta = {
285
- id: existing._eventId,
286
- createdAt: existing._updatedAt
332
+ id: existing._e,
333
+ createdAt: existing._u
287
334
  };
288
335
  const incomingMeta = { id: event.id, createdAt: event.createdAt };
289
336
  const winner = resolveWinner(existingMeta, incomingMeta);
290
337
  if (winner.id !== event.id)
291
338
  return false;
292
339
  }
293
- yield* storage.putRecord(event.collection, buildRecord(event));
340
+ if (existing && event.kind === "u") {
341
+ yield* storage.putRecord(event.collection, deepMerge(existing, buildRecord(event)));
342
+ } else {
343
+ yield* storage.putRecord(event.collection, buildRecord(event));
344
+ }
294
345
  return true;
295
346
  });
296
347
  }
@@ -300,15 +351,11 @@ function rebuild(storage, collections) {
300
351
  yield* storage.clearRecords(col);
301
352
  }
302
353
  const allEvents = yield* storage.getAllEvents();
303
- const sorted = [...allEvents].sort((a, b) => a.createdAt - b.createdAt);
304
- const winners = new Map;
354
+ const sorted = [...allEvents].sort((a, b) => a.createdAt - b.createdAt || (a.id < b.id ? -1 : 1));
305
355
  for (const event of sorted) {
306
- const key = `${event.collection}:${event.recordId}`;
307
- const current = winners.get(key) ?? null;
308
- winners.set(key, resolveWinner(current, event));
309
- }
310
- for (const event of winners.values()) {
311
- yield* storage.putRecord(event.collection, buildRecord(event));
356
+ if (event.data === null && event.kind !== "d")
357
+ continue;
358
+ yield* applyEvent(storage, event);
312
359
  }
313
360
  });
314
361
  }
@@ -330,6 +377,14 @@ function fieldDefToSchema(fd) {
330
377
  case "json":
331
378
  base = Schema3.Unknown;
332
379
  break;
380
+ case "object": {
381
+ const nested = {};
382
+ for (const [k, v] of Object.entries(fd.fields)) {
383
+ nested[k] = fieldDefToSchema(v);
384
+ }
385
+ base = Schema3.Struct(nested);
386
+ break;
387
+ }
333
388
  }
334
389
  if (fd.isArray) {
335
390
  base = Schema3.Array(base);
@@ -379,9 +434,25 @@ function buildPartialValidator(collectionName, def) {
379
434
  }
380
435
 
381
436
  // src/crud/collection-handle.ts
382
- import { Effect as Effect6, Option as Option3 } from "effect";
437
+ import { Effect as Effect6, Option as Option3, References } from "effect";
383
438
 
384
439
  // src/utils/uuid.ts
440
+ var alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
441
+ function toBase64url(bytes) {
442
+ let result = "";
443
+ for (let i = 0;i < bytes.length; i += 3) {
444
+ const b0 = bytes[i];
445
+ const b1 = bytes[i + 1] ?? 0;
446
+ const b2 = bytes[i + 2] ?? 0;
447
+ result += alphabet[b0 >> 2];
448
+ result += alphabet[(b0 & 3) << 4 | b1 >> 4];
449
+ if (i + 1 < bytes.length)
450
+ result += alphabet[(b1 & 15) << 2 | b2 >> 6];
451
+ if (i + 2 < bytes.length)
452
+ result += alphabet[b2 & 63];
453
+ }
454
+ return result;
455
+ }
385
456
  function uuidv7() {
386
457
  const now = Date.now();
387
458
  const bytes = new Uint8Array(16);
@@ -394,8 +465,7 @@ function uuidv7() {
394
465
  bytes[5] = now & 255;
395
466
  bytes[6] = bytes[6] & 15 | 112;
396
467
  bytes[8] = bytes[8] & 63 | 128;
397
- const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
398
- return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
468
+ return toBase64url(bytes);
399
469
  }
400
470
 
401
471
  // src/crud/query-builder.ts
@@ -413,7 +483,7 @@ function executeQuery(ctx, plan) {
413
483
  field: plan.fieldName
414
484
  });
415
485
  }
416
- if (fieldDef.kind === "json" || fieldDef.isArray) {
486
+ if (fieldDef.kind === "json" || fieldDef.kind === "object" || fieldDef.isArray) {
417
487
  return yield* new ValidationError({
418
488
  message: `Field "${plan.fieldName}" does not support filtering (type: ${fieldDef.kind}${fieldDef.isArray ? "[]" : ""})`,
419
489
  field: plan.fieldName
@@ -438,7 +508,7 @@ function executeQuery(ctx, plan) {
438
508
  } else {
439
509
  results = [...yield* ctx.storage.getAllRecords(ctx.collectionName)];
440
510
  }
441
- results = results.filter((r) => !r._deleted);
511
+ results = results.filter((r) => !r._d);
442
512
  for (const f of plan.filters) {
443
513
  results = results.filter(f);
444
514
  }
@@ -559,12 +629,62 @@ function createOrderByBuilder(storage, watchCtx, collectionName, def, fieldName,
559
629
  }
560
630
 
561
631
  // src/crud/collection-handle.ts
632
+ var KIND_FULL = { c: "create", u: "update", d: "delete" };
633
+ function sortChronologically(events) {
634
+ return [...events].sort((a, b) => a.createdAt - b.createdAt || (a.id < b.id ? -1 : 1));
635
+ }
636
+ function replayState(recordId, events, stopAtId) {
637
+ let state = null;
638
+ for (const e of events) {
639
+ if (e.kind === "d") {
640
+ state = null;
641
+ } else if (e.data !== null) {
642
+ if (state === null) {
643
+ state = { id: recordId, ...e.data };
644
+ } else {
645
+ state = deepMerge(state, e.data);
646
+ }
647
+ }
648
+ if (stopAtId !== undefined && e.id === stopAtId) {
649
+ break;
650
+ }
651
+ }
652
+ return state;
653
+ }
654
+ function promoteToSnapshot(storage, collection2, recordId, target, allSorted) {
655
+ return Effect6.gen(function* () {
656
+ const chronological = sortChronologically(allSorted);
657
+ const state = replayState(recordId, chronological, target.id);
658
+ if (state) {
659
+ yield* storage.putEvent({ ...target, data: state });
660
+ }
661
+ });
662
+ }
663
+ function pruneEvents(storage, collection2, recordId, retention) {
664
+ return Effect6.gen(function* () {
665
+ const events = yield* storage.getEventsByRecord(collection2, recordId);
666
+ if (events.length <= retention)
667
+ return;
668
+ const sorted = [...events].sort((a, b) => b.createdAt - a.createdAt || (a.id < b.id ? 1 : -1));
669
+ const retained = sorted.slice(0, retention);
670
+ const toStrip = sorted.slice(retention);
671
+ const oldestRetained = retained[retained.length - 1];
672
+ if (retention > 0 && oldestRetained?.kind === "u" && oldestRetained.data !== null) {
673
+ yield* promoteToSnapshot(storage, collection2, recordId, oldestRetained, sorted);
674
+ }
675
+ for (const e of toStrip) {
676
+ if (e.data !== null)
677
+ yield* storage.stripEventData(e.id);
678
+ }
679
+ });
680
+ }
562
681
  function mapRecord(record) {
563
- const { _deleted, _updatedAt, _author, ...fields } = record;
682
+ const { _d, _u, _a, _e, ...fields } = record;
564
683
  return fields;
565
684
  }
566
- function createCollectionHandle(def, storage, watchCtx, validator, partialValidator, makeEventId, onWrite) {
685
+ function createCollectionHandle(def, storage, watchCtx, validator, partialValidator, makeEventId, localAuthor, onWrite, logLevel = "None") {
567
686
  const collectionName = def.name;
687
+ const withLog = (effect) => Effect6.provideService(effect, References.MinimumLogLevel, logLevel);
568
688
  const commitEvent = (event) => Effect6.gen(function* () {
569
689
  yield* storage.putEvent(event);
570
690
  yield* applyEvent(storage, event);
@@ -573,11 +693,11 @@ function createCollectionHandle(def, storage, watchCtx, validator, partialValida
573
693
  yield* notifyChange(watchCtx, {
574
694
  collection: collectionName,
575
695
  recordId: event.recordId,
576
- kind: event.kind
696
+ kind: KIND_FULL[event.kind]
577
697
  });
578
698
  });
579
699
  const handle = {
580
- add: (data) => Effect6.gen(function* () {
700
+ add: (data) => withLog(Effect6.gen(function* () {
581
701
  const id = uuidv7();
582
702
  const fullRecord = { id, ...data };
583
703
  yield* validator(fullRecord);
@@ -585,38 +705,44 @@ function createCollectionHandle(def, storage, watchCtx, validator, partialValida
585
705
  id: makeEventId(),
586
706
  collection: collectionName,
587
707
  recordId: id,
588
- kind: "create",
708
+ kind: "c",
589
709
  data: fullRecord,
590
- createdAt: Date.now()
710
+ createdAt: Date.now(),
711
+ author: localAuthor
591
712
  };
592
713
  yield* commitEvent(event);
714
+ yield* Effect6.logDebug("Record added", { collection: collectionName, recordId: id, data: fullRecord });
593
715
  return id;
594
- }),
595
- update: (id, data) => Effect6.gen(function* () {
716
+ })),
717
+ update: (id, data) => withLog(Effect6.gen(function* () {
596
718
  const existing = yield* storage.getRecord(collectionName, id);
597
- if (!existing || existing._deleted) {
719
+ if (!existing || existing._d) {
598
720
  return yield* new NotFoundError({
599
721
  collection: collectionName,
600
722
  id
601
723
  });
602
724
  }
603
725
  yield* partialValidator(data);
604
- const { _deleted, _updatedAt, _author, ...existingFields } = existing;
726
+ const { _d, _u, _a, _e, ...existingFields } = existing;
605
727
  const merged = { ...existingFields, ...data, id };
606
728
  yield* validator(merged);
729
+ const diff = deepDiff(existingFields, merged);
607
730
  const event = {
608
731
  id: makeEventId(),
609
732
  collection: collectionName,
610
733
  recordId: id,
611
- kind: "update",
612
- data: merged,
613
- createdAt: Date.now()
734
+ kind: "u",
735
+ data: diff ?? { id },
736
+ createdAt: Date.now(),
737
+ author: localAuthor
614
738
  };
615
739
  yield* commitEvent(event);
616
- }),
617
- delete: (id) => Effect6.gen(function* () {
740
+ yield* Effect6.logDebug("Record updated", { collection: collectionName, recordId: id, data: diff });
741
+ yield* pruneEvents(storage, collectionName, id, def.eventRetention);
742
+ })),
743
+ delete: (id) => withLog(Effect6.gen(function* () {
618
744
  const existing = yield* storage.getRecord(collectionName, id);
619
- if (!existing || existing._deleted) {
745
+ if (!existing || existing._d) {
620
746
  return yield* new NotFoundError({
621
747
  collection: collectionName,
622
748
  id
@@ -626,17 +752,43 @@ function createCollectionHandle(def, storage, watchCtx, validator, partialValida
626
752
  id: makeEventId(),
627
753
  collection: collectionName,
628
754
  recordId: id,
629
- kind: "delete",
755
+ kind: "d",
630
756
  data: null,
631
- createdAt: Date.now()
757
+ createdAt: Date.now(),
758
+ author: localAuthor
759
+ };
760
+ yield* commitEvent(event);
761
+ yield* Effect6.logDebug("Record deleted", { collection: collectionName, recordId: id });
762
+ yield* pruneEvents(storage, collectionName, id, def.eventRetention);
763
+ })),
764
+ undo: (id) => Effect6.gen(function* () {
765
+ const existing = yield* storage.getRecord(collectionName, id);
766
+ if (!existing) {
767
+ return yield* new NotFoundError({ collection: collectionName, id });
768
+ }
769
+ const events = sortChronologically(yield* storage.getEventsByRecord(collectionName, id));
770
+ if (events.length < 2) {
771
+ return yield* new NotFoundError({ collection: collectionName, id });
772
+ }
773
+ const state = replayState(id, events.slice(0, -1));
774
+ if (!state) {
775
+ return yield* new NotFoundError({ collection: collectionName, id });
776
+ }
777
+ const event = {
778
+ id: makeEventId(),
779
+ collection: collectionName,
780
+ recordId: id,
781
+ kind: "u",
782
+ data: state,
783
+ createdAt: Date.now(),
784
+ author: localAuthor
632
785
  };
633
786
  yield* commitEvent(event);
634
- const oldEvents = yield* storage.getEventsByRecord(collectionName, id);
635
- yield* Effect6.forEach(oldEvents.filter((e) => e.id !== event.id), (e) => storage.deleteEvent(e.id), { discard: true });
787
+ yield* pruneEvents(storage, collectionName, id, def.eventRetention);
636
788
  }),
637
789
  get: (id) => Effect6.gen(function* () {
638
790
  const record = yield* storage.getRecord(collectionName, id);
639
- if (!record || record._deleted) {
791
+ if (!record || record._d) {
640
792
  return yield* new NotFoundError({
641
793
  collection: collectionName,
642
794
  id
@@ -645,10 +797,10 @@ function createCollectionHandle(def, storage, watchCtx, validator, partialValida
645
797
  return mapRecord(record);
646
798
  }),
647
799
  first: () => Effect6.map(storage.getAllRecords(collectionName), (all) => {
648
- const found = all.find((r) => !r._deleted);
800
+ const found = all.find((r) => !r._d);
649
801
  return found ? Option3.some(mapRecord(found)) : Option3.none();
650
802
  }),
651
- count: () => Effect6.map(storage.getAllRecords(collectionName), (all) => all.filter((r) => !r._deleted).length),
803
+ count: () => Effect6.map(storage.getAllRecords(collectionName), (all) => all.filter((r) => !r._d).length),
652
804
  watch: () => watchCollection(watchCtx, storage, collectionName, undefined, mapRecord),
653
805
  where: (fieldName) => createWhereClause(storage, watchCtx, collectionName, def, fieldName, mapRecord),
654
806
  orderBy: (fieldName) => createOrderByBuilder(storage, watchCtx, collectionName, def, fieldName, mapRecord)
@@ -657,7 +809,7 @@ function createCollectionHandle(def, storage, watchCtx, validator, partialValida
657
809
  }
658
810
 
659
811
  // src/sync/sync-service.ts
660
- import { Effect as Effect8, Option as Option5, Ref as Ref3, Schedule } from "effect";
812
+ import { Effect as Effect8, Layer, Option as Option5, References as References2, Ref as Ref3, Schedule } from "effect";
661
813
  import { unwrapEvent } from "nostr-tools/nip59";
662
814
  import { GiftWrap as GiftWrap2 } from "nostr-tools/kinds";
663
815
 
@@ -1228,8 +1380,13 @@ function reconcileWithRelay(storage, relay, relayUrl, publicKeys) {
1228
1380
  allNeedIds.push(id);
1229
1381
  currentMsg = nextMsg;
1230
1382
  }
1383
+ yield* Effect7.logDebug("Negentropy reconciliation complete", {
1384
+ relay: relayUrl,
1385
+ have: allHaveIds.length,
1386
+ need: allNeedIds.length
1387
+ });
1231
1388
  return { haveIds: allHaveIds, needIds: allNeedIds };
1232
- });
1389
+ }).pipe(Effect7.withLogSpan("tablinum.negentropy"));
1233
1390
  }
1234
1391
 
1235
1392
  // src/db/key-rotation.ts
@@ -1315,7 +1472,8 @@ function parseRemovalNotice(content, dTag) {
1315
1472
  }
1316
1473
 
1317
1474
  // src/sync/sync-service.ts
1318
- function createSyncHandle(storage, giftWrapHandle, relay, publishQueue, syncStatus, watchCtx, relayUrls, knownCollections, epochStore, personalPrivateKey, personalPublicKey, scope, onSyncError, onNewAuthor, onRemoved, onMembersChanged) {
1475
+ function createSyncHandle(storage, giftWrapHandle, relay, publishQueue, syncStatus, watchCtx, relayUrls, knownCollections, epochStore, personalPrivateKey, personalPublicKey, scope, logLevel, onSyncError, onNewAuthor, onRemoved, onMembersChanged) {
1476
+ const logLayer = Layer.succeed(References2.MinimumLogLevel, logLevel);
1319
1477
  const getSubscriptionPubKeys = () => {
1320
1478
  return getAllPublicKeys(epochStore);
1321
1479
  };
@@ -1325,7 +1483,7 @@ function createSyncHandle(storage, giftWrapHandle, relay, publishQueue, syncStat
1325
1483
  kind: "create"
1326
1484
  });
1327
1485
  const forkHandled = (effect) => {
1328
- Effect8.runFork(effect.pipe(Effect8.tapError((e) => Effect8.sync(() => onSyncError?.(e))), Effect8.ignore, Effect8.forkIn(scope)));
1486
+ Effect8.runFork(effect.pipe(Effect8.tapError((e) => Effect8.sync(() => onSyncError?.(e))), Effect8.ignore, Effect8.provide(logLayer), Effect8.forkIn(scope)));
1329
1487
  };
1330
1488
  let autoFlushActive = false;
1331
1489
  const autoFlushEffect = Effect8.gen(function* () {
@@ -1374,19 +1532,21 @@ function createSyncHandle(storage, giftWrapHandle, relay, publishQueue, syncStat
1374
1532
  }
1375
1533
  const collectionName = dTag.substring(0, colonIdx);
1376
1534
  const recordId = dTag.substring(colonIdx + 1);
1377
- if (!knownCollections.has(collectionName)) {
1535
+ const retention = knownCollections.get(collectionName);
1536
+ if (retention === undefined) {
1378
1537
  yield* storage.putGiftWrap({ id: remoteGw.id, createdAt: remoteGw.created_at });
1379
1538
  return null;
1380
1539
  }
1381
1540
  if (rumor.pubkey) {
1382
1541
  const reject = yield* shouldRejectWrite(rumor.pubkey);
1383
1542
  if (reject) {
1543
+ yield* Effect8.logWarning("Rejected write from removed member", { author: rumor.pubkey.slice(0, 12) });
1384
1544
  yield* storage.putGiftWrap({ id: remoteGw.id, createdAt: remoteGw.created_at });
1385
1545
  return null;
1386
1546
  }
1387
1547
  }
1388
1548
  let data = null;
1389
- let kind = "update";
1549
+ let kind = "u";
1390
1550
  const parsed = yield* Effect8.try({
1391
1551
  try: () => JSON.parse(rumor.content),
1392
1552
  catch: () => {
@@ -1400,7 +1560,7 @@ function createSyncHandle(storage, giftWrapHandle, relay, publishQueue, syncStat
1400
1560
  return null;
1401
1561
  }
1402
1562
  if (parsed === null || parsed._deleted) {
1403
- kind = "delete";
1563
+ kind = "d";
1404
1564
  } else {
1405
1565
  data = parsed;
1406
1566
  }
@@ -1416,15 +1576,14 @@ function createSyncHandle(storage, giftWrapHandle, relay, publishQueue, syncStat
1416
1576
  };
1417
1577
  yield* storage.putGiftWrap({
1418
1578
  id: remoteGw.id,
1419
- eventId: event.id,
1420
1579
  createdAt: remoteGw.created_at
1421
1580
  });
1422
1581
  yield* storage.putEvent(event);
1423
1582
  const didApply = yield* applyEvent(storage, event);
1424
- if (kind === "delete" && didApply) {
1425
- const oldEvents = yield* storage.getEventsByRecord(collectionName, recordId);
1426
- yield* Effect8.forEach(oldEvents.filter((e) => e.id !== event.id), (e) => storage.deleteEvent(e.id), { discard: true });
1583
+ if (didApply && (kind === "u" || kind === "d")) {
1584
+ yield* pruneEvents(storage, collectionName, recordId, retention);
1427
1585
  }
1586
+ yield* Effect8.logDebug("Processed gift wrap", { collection: collectionName, recordId, kind, author: author?.slice(0, 12) });
1428
1587
  if (author && onNewAuthor) {
1429
1588
  onNewAuthor(author);
1430
1589
  }
@@ -1493,15 +1652,18 @@ function createSyncHandle(storage, giftWrapHandle, relay, publishQueue, syncStat
1493
1652
  }).pipe(Effect8.tapError((err) => Effect8.sync(() => onSyncError?.(err))), Effect8.ignore);
1494
1653
  }), { discard: true });
1495
1654
  const syncRelay = (url, pubKeys, changedCollections) => Effect8.gen(function* () {
1655
+ yield* Effect8.logDebug("Syncing relay", { relay: url });
1496
1656
  const reconcileResult = yield* Effect8.result(reconcileWithRelay(storage, relay, url, Array.from(pubKeys)));
1497
1657
  if (reconcileResult._tag === "Failure") {
1498
1658
  onSyncError?.(reconcileResult.failure);
1499
1659
  return;
1500
1660
  }
1501
1661
  const { haveIds, needIds } = reconcileResult.success;
1662
+ yield* Effect8.logDebug("Relay reconciliation result", { relay: url, need: needIds.length, have: haveIds.length });
1502
1663
  if (needIds.length > 0) {
1503
1664
  const fetched = yield* relay.fetchEvents(needIds, url).pipe(Effect8.tapError((err) => Effect8.sync(() => onSyncError?.(err))), Effect8.orElseSucceed(() => []));
1504
- yield* Effect8.forEach(fetched, (remoteGw) => Effect8.gen(function* () {
1665
+ const sorted = [...fetched].sort((a, b) => a.created_at - b.created_at);
1666
+ yield* Effect8.forEach(sorted, (remoteGw) => Effect8.gen(function* () {
1505
1667
  const collection2 = yield* processGiftWrap(remoteGw).pipe(Effect8.orElseSucceed(() => null));
1506
1668
  if (collection2)
1507
1669
  changedCollections.add(collection2);
@@ -1515,9 +1677,10 @@ function createSyncHandle(storage, giftWrapHandle, relay, publishQueue, syncStat
1515
1677
  yield* relay.publish(gw.event, [url]).pipe(Effect8.andThen(storage.stripGiftWrapBlob(id)), Effect8.tapError((err) => Effect8.sync(() => onSyncError?.(err))), Effect8.ignore);
1516
1678
  }), { discard: true });
1517
1679
  }
1518
- });
1680
+ }).pipe(Effect8.withLogSpan("tablinum.syncRelay"));
1519
1681
  const handle = {
1520
1682
  sync: () => Effect8.gen(function* () {
1683
+ yield* Effect8.logInfo("Sync started");
1521
1684
  yield* syncStatus.set("syncing");
1522
1685
  yield* Ref3.set(watchCtx.replayingRef, true);
1523
1686
  const changedCollections = new Set;
@@ -1531,7 +1694,8 @@ function createSyncHandle(storage, giftWrapHandle, relay, publishQueue, syncStat
1531
1694
  yield* notifyReplayComplete(watchCtx, [...changedCollections]);
1532
1695
  yield* syncStatus.set("idle");
1533
1696
  })));
1534
- }),
1697
+ yield* Effect8.logInfo("Sync complete", { changed: [...changedCollections] });
1698
+ }).pipe(Effect8.withLogSpan("tablinum.sync")),
1535
1699
  publishLocal: (giftWrap) => Effect8.gen(function* () {
1536
1700
  if (!giftWrap.event)
1537
1701
  return;
@@ -1592,7 +1756,8 @@ var membersCollectionDef = {
1592
1756
  removedAt: optionalNumber,
1593
1757
  removedInEpoch: optionalString
1594
1758
  },
1595
- indices: []
1759
+ indices: [],
1760
+ eventRetention: 1
1596
1761
  };
1597
1762
  var AuthorProfileSchema = Schema5.Struct({
1598
1763
  name: Schema5.optionalKey(Schema5.String),
@@ -1656,7 +1821,7 @@ class SyncStatus extends ServiceMap9.Service()("tablinum/SyncStatus") {
1656
1821
  }
1657
1822
 
1658
1823
  // src/layers/IdentityLive.ts
1659
- import { Effect as Effect11, Layer } from "effect";
1824
+ import { Effect as Effect11, Layer as Layer2 } from "effect";
1660
1825
  import { hexToBytes as hexToBytes3 } from "@noble/hashes/utils.js";
1661
1826
 
1662
1827
  // src/db/identity.ts
@@ -1694,21 +1859,25 @@ function createIdentity(suppliedKey) {
1694
1859
  }
1695
1860
 
1696
1861
  // src/layers/IdentityLive.ts
1697
- var IdentityLive = Layer.effect(Identity, Effect11.gen(function* () {
1862
+ var IdentityLive = Layer2.effect(Identity, Effect11.gen(function* () {
1698
1863
  const config = yield* Config;
1699
1864
  const storage = yield* Storage;
1700
1865
  const idbKey = yield* storage.getMeta("identity_key");
1701
1866
  const resolvedKey = config.privateKey ?? (typeof idbKey === "string" && idbKey.length === 64 ? hexToBytes3(idbKey) : undefined);
1702
1867
  const identity = yield* createIdentity(resolvedKey);
1703
1868
  yield* storage.putMeta("identity_key", identity.exportKey());
1869
+ yield* Effect11.logInfo("Identity loaded", {
1870
+ publicKey: identity.publicKey.slice(0, 12) + "...",
1871
+ source: config.privateKey ? "config" : resolvedKey ? "storage" : "generated"
1872
+ });
1704
1873
  return identity;
1705
1874
  }));
1706
1875
 
1707
1876
  // src/layers/EpochStoreLive.ts
1708
- import { Effect as Effect12, Layer as Layer2, Option as Option7 } from "effect";
1877
+ import { Effect as Effect12, Layer as Layer3, Option as Option7 } from "effect";
1709
1878
  import { generateSecretKey as generateSecretKey2 } from "nostr-tools/pure";
1710
1879
  import { bytesToHex as bytesToHex3 } from "@noble/hashes/utils.js";
1711
- var EpochStoreLive = Layer2.effect(EpochStore, Effect12.gen(function* () {
1880
+ var EpochStoreLive = Layer3.effect(EpochStore, Effect12.gen(function* () {
1712
1881
  const config = yield* Config;
1713
1882
  const identity = yield* Identity;
1714
1883
  const storage = yield* Storage;
@@ -1716,21 +1885,24 @@ var EpochStoreLive = Layer2.effect(EpochStore, Effect12.gen(function* () {
1716
1885
  if (typeof idbRaw === "string") {
1717
1886
  const idbStore = deserializeEpochStore(idbRaw);
1718
1887
  if (Option7.isSome(idbStore)) {
1888
+ yield* Effect12.logInfo("Epoch store loaded", { source: "storage", epochs: idbStore.value.epochs.size });
1719
1889
  return idbStore.value;
1720
1890
  }
1721
1891
  }
1722
1892
  if (config.epochKeys && config.epochKeys.length > 0) {
1723
1893
  const store2 = createEpochStoreFromInputs(config.epochKeys);
1724
1894
  yield* storage.putMeta("epochs", stringifyEpochStore(store2));
1895
+ yield* Effect12.logInfo("Epoch store loaded", { source: "config", epochs: store2.epochs.size });
1725
1896
  return store2;
1726
1897
  }
1727
1898
  const store = createEpochStoreFromInputs([{ epochId: EpochId("epoch-0"), key: bytesToHex3(generateSecretKey2()) }], { createdBy: identity.publicKey });
1728
1899
  yield* storage.putMeta("epochs", stringifyEpochStore(store));
1900
+ yield* Effect12.logInfo("Epoch store loaded", { source: "generated", epochs: store.epochs.size });
1729
1901
  return store;
1730
1902
  }));
1731
1903
 
1732
1904
  // src/layers/StorageLive.ts
1733
- import { Effect as Effect14, Layer as Layer3 } from "effect";
1905
+ import { Effect as Effect14, Layer as Layer4 } from "effect";
1734
1906
 
1735
1907
  // src/storage/idb.ts
1736
1908
  import { Effect as Effect13 } from "effect";
@@ -1865,13 +2037,18 @@ function openIDBStorage(dbName, schema) {
1865
2037
  stripGiftWrapBlob: (id) => wrap("stripGiftWrapBlob", async () => {
1866
2038
  const existing = await db.get("giftwraps", id);
1867
2039
  if (existing) {
1868
- const { event: _, ...tombstone } = existing;
1869
- await db.put("giftwraps", tombstone);
2040
+ await db.put("giftwraps", { id: existing.id, createdAt: existing.createdAt });
1870
2041
  }
1871
2042
  }),
1872
2043
  deleteEvent: (id) => wrap("deleteEvent", () => db.delete("events", id).then(() => {
1873
2044
  return;
1874
2045
  })),
2046
+ stripEventData: (id) => wrap("stripEventData", async () => {
2047
+ const existing = await db.get("events", id);
2048
+ if (existing) {
2049
+ await db.put("events", { ...existing, data: null });
2050
+ }
2051
+ }),
1875
2052
  getMeta: (key) => wrap("getMeta", () => db.get("_meta", key)),
1876
2053
  putMeta: (key, value) => wrap("putMeta", () => db.put("_meta", value, key).then(() => {
1877
2054
  return;
@@ -1883,16 +2060,18 @@ function openIDBStorage(dbName, schema) {
1883
2060
  }
1884
2061
 
1885
2062
  // src/layers/StorageLive.ts
1886
- var StorageLive = Layer3.effect(Storage, Effect14.gen(function* () {
2063
+ var StorageLive = Layer4.effect(Storage, Effect14.gen(function* () {
1887
2064
  const config = yield* Config;
1888
- return yield* openIDBStorage(config.dbName, {
2065
+ const handle = yield* openIDBStorage(config.dbName, {
1889
2066
  ...config.schema,
1890
2067
  _members: membersCollectionDef
1891
2068
  });
2069
+ yield* Effect14.logInfo("Storage opened", { dbName: config.dbName });
2070
+ return handle;
1892
2071
  }));
1893
2072
 
1894
2073
  // src/layers/RelayLive.ts
1895
- import { Layer as Layer4 } from "effect";
2074
+ import { Layer as Layer5 } from "effect";
1896
2075
 
1897
2076
  // src/sync/relay.ts
1898
2077
  import { Effect as Effect15, Option as Option8, Schema as Schema6, ScopedCache, Scope as Scope3 } from "effect";
@@ -2105,10 +2284,10 @@ function createRelayHandle() {
2105
2284
  }
2106
2285
 
2107
2286
  // src/layers/RelayLive.ts
2108
- var RelayLive = Layer4.effect(Relay, createRelayHandle());
2287
+ var RelayLive = Layer5.effect(Relay, createRelayHandle());
2109
2288
 
2110
2289
  // src/layers/GiftWrapLive.ts
2111
- import { Effect as Effect17, Layer as Layer5 } from "effect";
2290
+ import { Effect as Effect17, Layer as Layer6 } from "effect";
2112
2291
 
2113
2292
  // src/sync/gift-wrap.ts
2114
2293
  import { Effect as Effect16 } from "effect";
@@ -2145,14 +2324,14 @@ function createEpochGiftWrapHandle(senderPrivateKey, epochStore) {
2145
2324
  }
2146
2325
 
2147
2326
  // src/layers/GiftWrapLive.ts
2148
- var GiftWrapLive = Layer5.effect(GiftWrap3, Effect17.gen(function* () {
2327
+ var GiftWrapLive = Layer6.effect(GiftWrap3, Effect17.gen(function* () {
2149
2328
  const identity = yield* Identity;
2150
2329
  const epochStore = yield* EpochStore;
2151
2330
  return createEpochGiftWrapHandle(identity.privateKey, epochStore);
2152
2331
  }));
2153
2332
 
2154
2333
  // src/layers/PublishQueueLive.ts
2155
- import { Effect as Effect19, Layer as Layer6 } from "effect";
2334
+ import { Effect as Effect19, Layer as Layer7 } from "effect";
2156
2335
 
2157
2336
  // src/sync/publish-queue.ts
2158
2337
  import { Effect as Effect18, Ref as Ref4 } from "effect";
@@ -2226,14 +2405,14 @@ function createPublishQueue(storage, relay) {
2226
2405
  }
2227
2406
 
2228
2407
  // src/layers/PublishQueueLive.ts
2229
- var PublishQueueLive = Layer6.effect(PublishQueue, Effect19.gen(function* () {
2408
+ var PublishQueueLive = Layer7.effect(PublishQueue, Effect19.gen(function* () {
2230
2409
  const storage = yield* Storage;
2231
2410
  const relay = yield* Relay;
2232
2411
  return yield* createPublishQueue(storage, relay);
2233
2412
  }));
2234
2413
 
2235
2414
  // src/layers/SyncStatusLive.ts
2236
- import { Layer as Layer7 } from "effect";
2415
+ import { Layer as Layer8 } from "effect";
2237
2416
 
2238
2417
  // src/sync/sync-status.ts
2239
2418
  import { Effect as Effect20, SubscriptionRef } from "effect";
@@ -2257,7 +2436,7 @@ function createSyncStatusHandle() {
2257
2436
  }
2258
2437
 
2259
2438
  // src/layers/SyncStatusLive.ts
2260
- var SyncStatusLive = Layer7.effect(SyncStatus, createSyncStatusHandle());
2439
+ var SyncStatusLive = Layer8.effect(SyncStatus, createSyncStatusHandle());
2261
2440
 
2262
2441
  // src/layers/TablinumLive.ts
2263
2442
  function reportSyncError(onSyncError, error) {
@@ -2278,12 +2457,12 @@ function mapMemberRecord(record) {
2278
2457
  ...record.removedInEpoch !== undefined ? { removedInEpoch: record.removedInEpoch } : {}
2279
2458
  };
2280
2459
  }
2281
- var IdentityWithDeps = IdentityLive.pipe(Layer8.provide(StorageLive));
2282
- var EpochStoreWithDeps = EpochStoreLive.pipe(Layer8.provide(IdentityWithDeps), Layer8.provide(StorageLive));
2283
- var GiftWrapWithDeps = GiftWrapLive.pipe(Layer8.provide(IdentityWithDeps), Layer8.provide(EpochStoreWithDeps));
2284
- var PublishQueueWithDeps = PublishQueueLive.pipe(Layer8.provide(StorageLive), Layer8.provide(RelayLive));
2285
- var AllServicesLive = Layer8.mergeAll(IdentityWithDeps, EpochStoreWithDeps, StorageLive, RelayLive, GiftWrapWithDeps, PublishQueueWithDeps, SyncStatusLive);
2286
- var TablinumLive = Layer8.effect(Tablinum, Effect21.gen(function* () {
2460
+ var IdentityWithDeps = IdentityLive.pipe(Layer9.provide(StorageLive));
2461
+ var EpochStoreWithDeps = EpochStoreLive.pipe(Layer9.provide(IdentityWithDeps), Layer9.provide(StorageLive));
2462
+ var GiftWrapWithDeps = GiftWrapLive.pipe(Layer9.provide(IdentityWithDeps), Layer9.provide(EpochStoreWithDeps));
2463
+ var PublishQueueWithDeps = PublishQueueLive.pipe(Layer9.provide(StorageLive), Layer9.provide(RelayLive));
2464
+ var AllServicesLive = Layer9.mergeAll(IdentityWithDeps, EpochStoreWithDeps, StorageLive, RelayLive, GiftWrapWithDeps, PublishQueueWithDeps, SyncStatusLive);
2465
+ var TablinumLive = Layer9.effect(Tablinum, Effect21.gen(function* () {
2287
2466
  const config = yield* Config;
2288
2467
  const identity = yield* Identity;
2289
2468
  const epochStore = yield* EpochStore;
@@ -2293,17 +2472,18 @@ var TablinumLive = Layer8.effect(Tablinum, Effect21.gen(function* () {
2293
2472
  const publishQueue = yield* PublishQueue;
2294
2473
  const syncStatus = yield* SyncStatus;
2295
2474
  const scope = yield* Effect21.scope;
2475
+ const logLayer = Layer9.succeed(References3.MinimumLogLevel, config.logLevel);
2296
2476
  const pubsub = yield* PubSub2.unbounded();
2297
2477
  const replayingRef = yield* Ref5.make(false);
2298
2478
  const closedRef = yield* Ref5.make(false);
2299
2479
  const watchCtx = { pubsub, replayingRef };
2300
2480
  const schemaEntries = Object.entries(config.schema);
2301
2481
  const allSchemaEntries = [...schemaEntries, ["_members", membersCollectionDef]];
2302
- const knownCollections = new Set(allSchemaEntries.map(([, def]) => def.name));
2482
+ const knownCollections = new Map(allSchemaEntries.map(([, def]) => [def.name, def.eventRetention]));
2303
2483
  let notifyAuthor;
2304
- 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);
2484
+ 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);
2305
2485
  const onWrite = (event) => Effect21.gen(function* () {
2306
- const content = event.kind === "delete" ? JSON.stringify({ _deleted: true }) : JSON.stringify(event.data);
2486
+ const content = event.kind === "d" ? JSON.stringify(null) : JSON.stringify(event.data);
2307
2487
  const dTag = `${event.collection}:${event.recordId}`;
2308
2488
  const wrapResult = yield* Effect21.result(giftWrap.wrap({
2309
2489
  kind: 1,
@@ -2316,11 +2496,10 @@ var TablinumLive = Layer8.effect(Tablinum, Effect21.gen(function* () {
2316
2496
  return;
2317
2497
  }
2318
2498
  const gw = wrapResult.success;
2319
- yield* storage.putGiftWrap({ id: gw.id, eventId: event.id, createdAt: gw.created_at });
2499
+ yield* storage.putGiftWrap({ id: gw.id, createdAt: gw.created_at });
2320
2500
  yield* Effect21.forkIn(Effect21.gen(function* () {
2321
2501
  const publishResult = yield* Effect21.result(syncHandle.publishLocal({
2322
2502
  id: gw.id,
2323
- eventId: event.id,
2324
2503
  event: gw,
2325
2504
  createdAt: gw.created_at
2326
2505
  }));
@@ -2336,9 +2515,10 @@ var TablinumLive = Layer8.effect(Tablinum, Effect21.gen(function* () {
2336
2515
  id: uuidv7(),
2337
2516
  collection: "_members",
2338
2517
  recordId: record.id,
2339
- kind: existing ? "update" : "create",
2518
+ kind: existing ? "u" : "c",
2340
2519
  data: record,
2341
- createdAt: Date.now()
2520
+ createdAt: Date.now(),
2521
+ author: identity.publicKey
2342
2522
  };
2343
2523
  yield* storage.putEvent(event);
2344
2524
  yield* applyEvent(storage, event);
@@ -2379,16 +2559,21 @@ var TablinumLive = Layer8.effect(Tablinum, Effect21.gen(function* () {
2379
2559
  config.onMembersChanged?.();
2380
2560
  }
2381
2561
  }
2382
- }).pipe(Effect21.ignore, Effect21.forkIn(scope)));
2562
+ }).pipe(Effect21.ignore, Effect21.provide(logLayer), Effect21.forkIn(scope)));
2383
2563
  };
2384
2564
  const handles = new Map;
2385
2565
  for (const [, def] of allSchemaEntries) {
2386
2566
  const validator = buildValidator(def.name, def);
2387
2567
  const partialValidator = buildPartialValidator(def.name, def);
2388
- const handle = createCollectionHandle(def, storage, watchCtx, validator, partialValidator, uuidv7, onWrite);
2568
+ const handle = createCollectionHandle(def, storage, watchCtx, validator, partialValidator, uuidv7, identity.publicKey, onWrite, config.logLevel);
2389
2569
  handles.set(def.name, handle);
2390
2570
  }
2391
2571
  yield* syncHandle.startSubscription();
2572
+ yield* Effect21.logInfo("Tablinum ready", {
2573
+ dbName: config.dbName,
2574
+ collections: schemaEntries.map(([name]) => name),
2575
+ relays: config.relays
2576
+ });
2392
2577
  const selfMember = yield* storage.getRecord("_members", identity.publicKey);
2393
2578
  if (!selfMember) {
2394
2579
  yield* putMemberRecord({
@@ -2397,18 +2582,19 @@ var TablinumLive = Layer8.effect(Tablinum, Effect21.gen(function* () {
2397
2582
  addedInEpoch: getCurrentEpoch(epochStore).id
2398
2583
  });
2399
2584
  }
2400
- const ensureOpen = (effect) => Effect21.gen(function* () {
2585
+ const withLog = (effect) => Effect21.provideService(effect, References3.MinimumLogLevel, config.logLevel);
2586
+ const ensureOpen = (effect) => withLog(Effect21.gen(function* () {
2401
2587
  if (yield* Ref5.get(closedRef)) {
2402
2588
  return yield* new StorageError({ message: "Database is closed" });
2403
2589
  }
2404
2590
  return yield* effect;
2405
- });
2406
- const ensureSyncOpen = (effect) => Effect21.gen(function* () {
2591
+ }));
2592
+ const ensureSyncOpen = (effect) => withLog(Effect21.gen(function* () {
2407
2593
  if (yield* Ref5.get(closedRef)) {
2408
2594
  return yield* new SyncError({ message: "Database is closed", phase: "init" });
2409
2595
  }
2410
2596
  return yield* effect;
2411
- });
2597
+ }));
2412
2598
  const dbHandle = {
2413
2599
  collection: (name) => {
2414
2600
  const handle = handles.get(name);
@@ -2424,12 +2610,12 @@ var TablinumLive = Layer8.effect(Tablinum, Effect21.gen(function* () {
2424
2610
  relays: [...config.relays],
2425
2611
  dbName: config.dbName
2426
2612
  }),
2427
- close: () => Effect21.gen(function* () {
2613
+ close: () => withLog(Effect21.gen(function* () {
2428
2614
  if (yield* Ref5.get(closedRef))
2429
2615
  return;
2430
2616
  yield* Ref5.set(closedRef, true);
2431
2617
  yield* Scope4.close(scope, Exit.void);
2432
- }),
2618
+ })),
2433
2619
  rebuild: () => ensureOpen(rebuild(storage, allSchemaEntries.map(([, def]) => def.name))),
2434
2620
  sync: () => ensureSyncOpen(syncHandle.sync()),
2435
2621
  getSyncStatus: () => syncStatus.get(),
@@ -2473,20 +2659,52 @@ var TablinumLive = Layer8.effect(Tablinum, Effect21.gen(function* () {
2473
2659
  })),
2474
2660
  getMembers: () => ensureOpen(Effect21.gen(function* () {
2475
2661
  const allRecords = yield* storage.getAllRecords("_members");
2476
- return allRecords.filter((record) => !record._deleted).map(mapMemberRecord);
2662
+ return allRecords.filter((record) => !record._d).map(mapMemberRecord);
2663
+ })),
2664
+ getProfile: () => ensureOpen(Effect21.gen(function* () {
2665
+ const record = yield* storage.getRecord("_members", identity.publicKey);
2666
+ if (!record)
2667
+ return {};
2668
+ const profile = {};
2669
+ if (record.name !== undefined)
2670
+ profile.name = record.name;
2671
+ if (record.picture !== undefined)
2672
+ profile.picture = record.picture;
2673
+ if (record.about !== undefined)
2674
+ profile.about = record.about;
2675
+ if (record.nip05 !== undefined)
2676
+ profile.nip05 = record.nip05;
2677
+ return profile;
2477
2678
  })),
2478
2679
  setProfile: (profile) => ensureOpen(Effect21.gen(function* () {
2479
2680
  const existing = yield* storage.getRecord("_members", identity.publicKey);
2480
2681
  if (!existing) {
2481
2682
  return yield* new ValidationError({ message: "Current user is not a member" });
2482
2683
  }
2483
- yield* putMemberRecord({ ...existing, ...profile });
2684
+ const { _d, _u, _a, _e, ...memberFields } = existing;
2685
+ yield* putMemberRecord({ ...memberFields, ...profile });
2484
2686
  }))
2485
2687
  };
2486
2688
  return dbHandle;
2487
- })).pipe(Layer8.provide(AllServicesLive));
2689
+ }).pipe(Effect21.withLogSpan("tablinum.init"))).pipe(Layer9.provide(AllServicesLive));
2488
2690
 
2489
2691
  // src/db/create-tablinum.ts
2692
+ function resolveLogLevel(input) {
2693
+ if (input === undefined || input === "none")
2694
+ return "None";
2695
+ switch (input) {
2696
+ case "debug":
2697
+ return "Debug";
2698
+ case "info":
2699
+ return "Info";
2700
+ case "warning":
2701
+ return "Warn";
2702
+ case "error":
2703
+ return "Error";
2704
+ default:
2705
+ return input;
2706
+ }
2707
+ }
2490
2708
  function validateConfig(config) {
2491
2709
  return Effect22.gen(function* () {
2492
2710
  if (Object.keys(config.schema).length === 0) {
@@ -2500,19 +2718,26 @@ function createTablinum(config) {
2500
2718
  return Effect22.gen(function* () {
2501
2719
  yield* validateConfig(config);
2502
2720
  const runtimeConfig = yield* resolveRuntimeConfig(config);
2721
+ const logLevel = resolveLogLevel(config.logLevel);
2503
2722
  const configValue = {
2504
2723
  ...runtimeConfig,
2505
2724
  schema: config.schema,
2725
+ logLevel,
2506
2726
  onSyncError: config.onSyncError,
2507
2727
  onRemoved: config.onRemoved,
2508
2728
  onMembersChanged: config.onMembersChanged
2509
2729
  };
2510
- const configLayer = Layer9.succeed(Config, configValue);
2511
- const fullLayer = TablinumLive.pipe(Layer9.provide(configLayer));
2512
- const ctx = yield* Layer9.build(fullLayer);
2730
+ const configLayer = Layer10.succeed(Config, configValue);
2731
+ const logLayer = Layer10.succeed(References4.MinimumLogLevel, logLevel);
2732
+ const fullLayer = TablinumLive.pipe(Layer10.provide(configLayer), Layer10.provide(logLayer));
2733
+ const ctx = yield* Layer10.build(fullLayer);
2513
2734
  return ServiceMap10.get(ctx, Tablinum);
2514
2735
  });
2515
2736
  }
2737
+
2738
+ // src/index.ts
2739
+ import { LogLevel } from "effect";
2740
+
2516
2741
  // src/db/invite.ts
2517
2742
  import { Schema as Schema7 } from "effect";
2518
2743
  var InviteSchema = Schema7.Struct({
@@ -2558,6 +2783,7 @@ export {
2558
2783
  StorageError,
2559
2784
  RelayError,
2560
2785
  NotFoundError,
2786
+ LogLevel,
2561
2787
  EpochId,
2562
2788
  DatabaseName,
2563
2789
  CryptoError,