tablinum 0.4.0 → 0.6.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
@@ -1,14 +1,13 @@
1
1
  var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
2
2
  get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
3
3
  }) : x)(function(x) {
4
- if (typeof require !== "undefined")
5
- return require.apply(this, arguments);
4
+ if (typeof require !== "undefined") return require.apply(this, arguments);
6
5
  throw Error('Dynamic require of "' + x + '" is not supported');
7
6
  });
8
7
 
9
8
  // src/schema/field.ts
10
9
  function make(kind, isOptional, isArray, fields) {
11
- return { _tag: "FieldDef", kind, isOptional, isArray, ...fields !== undefined && { fields } };
10
+ return { _tag: "FieldDef", kind, isOptional, isArray, ...fields !== void 0 && { fields } };
12
11
  }
13
12
  var field = {
14
13
  string: () => make("string", false, false),
@@ -19,8 +18,9 @@ var field = {
19
18
  optional: (inner) => make(inner.kind, true, inner.isArray, inner.fields),
20
19
  array: (inner) => make(inner.kind, inner.isOptional, true, inner.fields)
21
20
  };
21
+
22
22
  // src/schema/collection.ts
23
- var RESERVED_NAMES = new Set(["id", "_d", "_u", "_e", "_a"]);
23
+ var RESERVED_NAMES = /* @__PURE__ */ new Set(["id", "_d", "_u", "_e", "_a"]);
24
24
  function collection(name, fields, options) {
25
25
  if (!name || name.trim().length === 0) {
26
26
  throw new Error("Collection name must not be empty");
@@ -42,7 +42,9 @@ function collection(name, fields, options) {
42
42
  throw new Error(`Index field "${idx}" does not exist in collection "${name}"`);
43
43
  }
44
44
  if (fieldDef.kind === "json" || fieldDef.kind === "object" || fieldDef.isArray) {
45
- throw new Error(`Field "${idx}" cannot be indexed (type: ${fieldDef.kind}${fieldDef.isArray ? "[]" : ""})`);
45
+ throw new Error(
46
+ `Field "${idx}" cannot be indexed (type: ${fieldDef.kind}${fieldDef.isArray ? "[]" : ""})`
47
+ );
46
48
  }
47
49
  indices.push(idx);
48
50
  }
@@ -53,35 +55,29 @@ function collection(name, fields, options) {
53
55
  }
54
56
  return { _tag: "CollectionDef", name, fields, indices, eventRetention };
55
57
  }
58
+
56
59
  // src/db/create-tablinum.ts
57
- import { Effect as Effect22, Layer as Layer9, ServiceMap as ServiceMap10 } from "effect";
60
+ import { Effect as Effect22, Layer as Layer10, References as References4, ServiceMap as ServiceMap10 } from "effect";
58
61
 
59
62
  // src/db/runtime-config.ts
60
63
  import { Effect, Schema as Schema2 } from "effect";
61
64
 
62
65
  // src/errors.ts
63
66
  import { Data } from "effect";
64
-
65
- class ValidationError extends Data.TaggedError("ValidationError") {
66
- }
67
-
68
- class StorageError extends Data.TaggedError("StorageError") {
69
- }
70
-
71
- class CryptoError extends Data.TaggedError("CryptoError") {
72
- }
73
-
74
- class RelayError extends Data.TaggedError("RelayError") {
75
- }
76
-
77
- class SyncError extends Data.TaggedError("SyncError") {
78
- }
79
-
80
- class NotFoundError extends Data.TaggedError("NotFoundError") {
81
- }
82
-
83
- class ClosedError extends Data.TaggedError("ClosedError") {
84
- }
67
+ var ValidationError = class extends Data.TaggedError("ValidationError") {
68
+ };
69
+ var StorageError = class extends Data.TaggedError("StorageError") {
70
+ };
71
+ var CryptoError = class extends Data.TaggedError("CryptoError") {
72
+ };
73
+ var RelayError = class extends Data.TaggedError("RelayError") {
74
+ };
75
+ var SyncError = class extends Data.TaggedError("SyncError") {
76
+ };
77
+ var NotFoundError = class extends Data.TaggedError("NotFoundError") {
78
+ };
79
+ var ClosedError = class extends Data.TaggedError("ClosedError") {
80
+ };
85
81
 
86
82
  // src/db/epoch.ts
87
83
  import { Option, Schema } from "effect";
@@ -109,15 +105,17 @@ var PersistedEpochStoreSchema = Schema.Struct({
109
105
  epochs: Schema.Array(PersistedEpochSchema),
110
106
  currentEpochId: Schema.String
111
107
  });
112
- var decodePersistedEpochStore = Schema.decodeUnknownSync(Schema.fromJsonString(PersistedEpochStoreSchema));
108
+ var decodePersistedEpochStore = Schema.decodeUnknownSync(
109
+ Schema.fromJsonString(PersistedEpochStoreSchema)
110
+ );
113
111
  function createEpochKey(id, privateKeyHex, createdBy, parentEpoch) {
114
112
  const publicKey = getPublicKey(hexToBytes(privateKeyHex));
115
113
  const base = { id, privateKey: privateKeyHex, publicKey, createdBy };
116
- return parentEpoch !== undefined ? { ...base, parentEpoch } : base;
114
+ return parentEpoch !== void 0 ? { ...base, parentEpoch } : base;
117
115
  }
118
116
  function createEpochStore(initialEpoch) {
119
- const epochs = new Map;
120
- const keysByPublicKey = new Map;
117
+ const epochs = /* @__PURE__ */ new Map();
118
+ const keysByPublicKey = /* @__PURE__ */ new Map();
121
119
  epochs.set(initialEpoch.id, initialEpoch);
122
120
  keysByPublicKey.set(initialEpoch.publicKey, hexToBytes(initialEpoch.privateKey));
123
121
  return { epochs, keysByPublicKey, currentEpochId: initialEpoch.id };
@@ -127,7 +125,14 @@ function addEpoch(store, epoch) {
127
125
  store.keysByPublicKey.set(epoch.publicKey, hexToBytes(epoch.privateKey));
128
126
  }
129
127
  function hydrateEpochStore(snapshot) {
130
- const [firstEpoch, ...remainingEpochs] = snapshot.epochs.map((epoch) => createEpochKey(EpochId(epoch.id), epoch.privateKey, epoch.createdBy, epoch.parentEpoch !== undefined ? EpochId(epoch.parentEpoch) : undefined));
128
+ const [firstEpoch, ...remainingEpochs] = snapshot.epochs.map(
129
+ (epoch) => createEpochKey(
130
+ EpochId(epoch.id),
131
+ epoch.privateKey,
132
+ epoch.createdBy,
133
+ epoch.parentEpoch !== void 0 ? EpochId(epoch.parentEpoch) : void 0
134
+ )
135
+ );
131
136
  if (!firstEpoch) {
132
137
  throw new Error("Epoch snapshot must contain at least one epoch");
133
138
  }
@@ -143,9 +148,16 @@ function createEpochStoreFromInputs(epochKeys, options = {}) {
143
148
  throw new Error("Epoch input must contain at least one key");
144
149
  }
145
150
  const createdBy = options.createdBy ?? "";
146
- const epochs = epochKeys.map((epochKey, index) => createEpochKey(epochKey.epochId, epochKey.key, createdBy, index > 0 ? epochKeys[index - 1].epochId : undefined));
151
+ const epochs = epochKeys.map(
152
+ (epochKey, index) => createEpochKey(
153
+ epochKey.epochId,
154
+ epochKey.key,
155
+ createdBy,
156
+ index > 0 ? epochKeys[index - 1].epochId : void 0
157
+ )
158
+ );
147
159
  const store = createEpochStore(epochs[0]);
148
- for (let i = 1;i < epochs.length; i++) {
160
+ for (let i = 1; i < epochs.length; i++) {
149
161
  addEpoch(store, epochs[i]);
150
162
  }
151
163
  store.currentEpochId = epochs[epochs.length - 1].id;
@@ -172,7 +184,7 @@ function serializeEpochStore(store) {
172
184
  id: epoch.id,
173
185
  privateKey: epoch.privateKey,
174
186
  createdBy: epoch.createdBy,
175
- ...epoch.parentEpoch !== undefined ? { parentEpoch: epoch.parentEpoch } : {}
187
+ ...epoch.parentEpoch !== void 0 ? { parentEpoch: epoch.parentEpoch } : {}
176
188
  })),
177
189
  currentEpochId: store.currentEpochId
178
190
  };
@@ -197,49 +209,61 @@ var RuntimeConfigSchema = Schema2.Struct({
197
209
  epochKeys: Schema2.optional(Schema2.Array(EpochKeyInputSchema))
198
210
  });
199
211
  function resolveRuntimeConfig(source) {
200
- return Schema2.decodeUnknownEffect(RuntimeConfigSchema)(source).pipe(Effect.map((config) => ({
201
- relays: [...config.relays],
202
- privateKey: config.privateKey,
203
- epochKeys: config.epochKeys?.map((ek) => ({ epochId: EpochId(ek.epochId), key: ek.key })),
204
- dbName: DatabaseName(config.dbName ?? "tablinum")
205
- })), Effect.mapError((error) => new ValidationError({
206
- message: `Invalid Tablinum configuration: ${error.message}`
207
- })));
212
+ return Schema2.decodeUnknownEffect(RuntimeConfigSchema)(source).pipe(
213
+ Effect.map((config) => ({
214
+ relays: [...config.relays],
215
+ privateKey: config.privateKey,
216
+ epochKeys: config.epochKeys?.map((ek) => ({ epochId: EpochId(ek.epochId), key: ek.key })),
217
+ dbName: DatabaseName(config.dbName ?? "tablinum")
218
+ })),
219
+ Effect.mapError(
220
+ (error) => new ValidationError({
221
+ message: `Invalid Tablinum configuration: ${error.message}`
222
+ })
223
+ )
224
+ );
208
225
  }
209
226
 
210
227
  // src/services/Config.ts
211
228
  import { ServiceMap } from "effect";
212
-
213
- class Config extends ServiceMap.Service()("tablinum/Config") {
214
- }
229
+ var Config = class extends ServiceMap.Service()("tablinum/Config") {
230
+ };
215
231
 
216
232
  // src/services/Tablinum.ts
217
233
  import { ServiceMap as ServiceMap2 } from "effect";
218
-
219
- class Tablinum extends ServiceMap2.Service()("tablinum/Tablinum") {
220
- }
234
+ var Tablinum = class extends ServiceMap2.Service()(
235
+ "tablinum/Tablinum"
236
+ ) {
237
+ };
221
238
 
222
239
  // src/layers/TablinumLive.ts
223
- import { Effect as Effect21, Exit, Layer as Layer8, Option as Option9, PubSub as PubSub2, Ref as Ref5, Scope as Scope4 } from "effect";
240
+ 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";
224
241
 
225
242
  // src/crud/watch.ts
226
243
  import { Effect as Effect2, PubSub, Ref, Stream } from "effect";
227
- function watchCollection(ctx, storage, collectionName, filter, mapRecord) {
244
+ function watchCollection(ctx, storage, collectionName, filter, mapRecord2) {
228
245
  const query = () => Effect2.map(storage.getAllRecords(collectionName), (all) => {
229
246
  const filtered = all.filter((r) => !r._d && (filter ? filter(r) : true));
230
- return mapRecord ? filtered.map(mapRecord) : filtered;
247
+ return mapRecord2 ? filtered.map(mapRecord2) : filtered;
231
248
  });
232
- const changes = Stream.fromPubSub(ctx.pubsub).pipe(Stream.filter((event) => event.collection === collectionName), Stream.mapEffect(() => Effect2.gen(function* () {
233
- const replaying = yield* Ref.get(ctx.replayingRef);
234
- if (replaying)
235
- return;
236
- return yield* query();
237
- })), Stream.filter((result) => result !== undefined));
238
- return Stream.unwrap(Effect2.gen(function* () {
239
- yield* Effect2.sleep(0);
240
- const initial = yield* query();
241
- return Stream.concat(Stream.make(initial), changes);
242
- }));
249
+ const changes = Stream.fromPubSub(ctx.pubsub).pipe(
250
+ Stream.filter((event) => event.collection === collectionName),
251
+ Stream.mapEffect(
252
+ () => Effect2.gen(function* () {
253
+ const replaying = yield* Ref.get(ctx.replayingRef);
254
+ if (replaying) return void 0;
255
+ return yield* query();
256
+ })
257
+ ),
258
+ Stream.filter((result) => result !== void 0)
259
+ );
260
+ return Stream.unwrap(
261
+ Effect2.gen(function* () {
262
+ yield* Effect2.sleep(0);
263
+ const initial = yield* query();
264
+ return Stream.concat(Stream.make(initial), changes);
265
+ })
266
+ );
243
267
  }
244
268
  function notifyChange(ctx, event) {
245
269
  return PubSub.publish(ctx.pubsub, event).pipe(Effect2.asVoid);
@@ -262,12 +286,9 @@ import { Effect as Effect3 } from "effect";
262
286
 
263
287
  // src/storage/lww.ts
264
288
  function resolveWinner(existing, incoming) {
265
- if (existing === null)
266
- return incoming;
267
- if (incoming.createdAt > existing.createdAt)
268
- return incoming;
269
- if (incoming.createdAt < existing.createdAt)
270
- return existing;
289
+ if (existing === null) return incoming;
290
+ if (incoming.createdAt > existing.createdAt) return incoming;
291
+ if (incoming.createdAt < existing.createdAt) return existing;
271
292
  return incoming.id < existing.id ? incoming : existing;
272
293
  }
273
294
 
@@ -334,8 +355,7 @@ function applyEvent(storage, event) {
334
355
  };
335
356
  const incomingMeta = { id: event.id, createdAt: event.createdAt };
336
357
  const winner = resolveWinner(existingMeta, incomingMeta);
337
- if (winner.id !== event.id)
338
- return false;
358
+ if (winner.id !== event.id) return false;
339
359
  }
340
360
  if (existing && event.kind === "u") {
341
361
  yield* storage.putRecord(event.collection, deepMerge(existing, buildRecord(event)));
@@ -351,10 +371,11 @@ function rebuild(storage, collections) {
351
371
  yield* storage.clearRecords(col);
352
372
  }
353
373
  const allEvents = yield* storage.getAllEvents();
354
- const sorted = [...allEvents].sort((a, b) => a.createdAt - b.createdAt || (a.id < b.id ? -1 : 1));
374
+ const sorted = [...allEvents].sort(
375
+ (a, b) => a.createdAt - b.createdAt || (a.id < b.id ? -1 : 1)
376
+ );
355
377
  for (const event of sorted) {
356
- if (event.data === null && event.kind !== "d")
357
- continue;
378
+ if (event.data === null && event.kind !== "d") continue;
358
379
  yield* applyEvent(storage, event);
359
380
  }
360
381
  });
@@ -407,9 +428,16 @@ function buildStructSchema(def, options = {}) {
407
428
  }
408
429
  function buildValidator(collectionName, def) {
409
430
  const decode = Schema3.decodeUnknownEffect(buildStructSchema(def, { includeId: true }));
410
- return (input) => decode(input).pipe(Effect4.map((result) => result), Effect4.mapError((e) => new ValidationError({
411
- message: `Validation failed for collection "${collectionName}": ${e.message}`
412
- })));
431
+ return (input) => decode(input).pipe(
432
+ Effect4.map(
433
+ (result) => result
434
+ ),
435
+ Effect4.mapError(
436
+ (e) => new ValidationError({
437
+ message: `Validation failed for collection "${collectionName}": ${e.message}`
438
+ })
439
+ )
440
+ );
413
441
  }
414
442
  function buildPartialValidator(collectionName, def) {
415
443
  const decode = Schema3.decodeUnknownEffect(buildStructSchema(def, { allOptional: true }));
@@ -421,35 +449,38 @@ function buildPartialValidator(collectionName, def) {
421
449
  }
422
450
  const record = input;
423
451
  const unknownField = Object.keys(record).find((key) => !Object.hasOwn(def.fields, key));
424
- if (unknownField !== undefined) {
452
+ if (unknownField !== void 0) {
425
453
  return yield* new ValidationError({
426
454
  message: `Unknown field "${unknownField}" in collection "${collectionName}"`,
427
455
  field: unknownField
428
456
  });
429
457
  }
430
- return yield* decode(record).pipe(Effect4.map((result) => result), Effect4.mapError((e) => new ValidationError({
431
- message: `Validation failed for collection "${collectionName}": ${e.message}`
432
- })));
458
+ return yield* decode(record).pipe(
459
+ Effect4.map((result) => result),
460
+ Effect4.mapError(
461
+ (e) => new ValidationError({
462
+ message: `Validation failed for collection "${collectionName}": ${e.message}`
463
+ })
464
+ )
465
+ );
433
466
  });
434
467
  }
435
468
 
436
469
  // src/crud/collection-handle.ts
437
- import { Effect as Effect6, Option as Option3 } from "effect";
470
+ import { Effect as Effect6, Option as Option3, References } from "effect";
438
471
 
439
472
  // src/utils/uuid.ts
440
473
  var alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
441
474
  function toBase64url(bytes) {
442
475
  let result = "";
443
- for (let i = 0;i < bytes.length; i += 3) {
476
+ for (let i = 0; i < bytes.length; i += 3) {
444
477
  const b0 = bytes[i];
445
478
  const b1 = bytes[i + 1] ?? 0;
446
479
  const b2 = bytes[i + 2] ?? 0;
447
480
  result += alphabet[b0 >> 2];
448
481
  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];
482
+ if (i + 1 < bytes.length) result += alphabet[(b1 & 15) << 2 | b2 >> 6];
483
+ if (i + 2 < bytes.length) result += alphabet[b2 & 63];
453
484
  }
454
485
  return result;
455
486
  }
@@ -494,16 +525,28 @@ function executeQuery(ctx, plan) {
494
525
  if (plan.indexQuery && ctx.def.indices.includes(plan.indexQuery.field)) {
495
526
  if (plan.indexQuery.type === "value") {
496
527
  results = [
497
- ...yield* ctx.storage.getByIndex(ctx.collectionName, plan.indexQuery.field, plan.indexQuery.range)
528
+ ...yield* ctx.storage.getByIndex(
529
+ ctx.collectionName,
530
+ plan.indexQuery.field,
531
+ plan.indexQuery.range
532
+ )
498
533
  ];
499
534
  } else {
500
535
  results = [
501
- ...yield* ctx.storage.getByIndexRange(ctx.collectionName, plan.indexQuery.field, plan.indexQuery.range)
536
+ ...yield* ctx.storage.getByIndexRange(
537
+ ctx.collectionName,
538
+ plan.indexQuery.field,
539
+ plan.indexQuery.range
540
+ )
502
541
  ];
503
542
  }
504
543
  } else if (plan.orderBy && ctx.def.indices.includes(plan.orderBy.field) && plan.filters.length === 0) {
505
544
  results = [
506
- ...yield* ctx.storage.getAllSorted(ctx.collectionName, plan.orderBy.field, plan.orderBy.direction === "desc" ? "prev" : "next")
545
+ ...yield* ctx.storage.getAllSorted(
546
+ ctx.collectionName,
547
+ plan.orderBy.field,
548
+ plan.orderBy.direction === "desc" ? "prev" : "next"
549
+ )
507
550
  ];
508
551
  } else {
509
552
  results = [...yield* ctx.storage.getAllRecords(ctx.collectionName)];
@@ -527,7 +570,7 @@ function executeQuery(ctx, plan) {
527
570
  if (plan.offset) {
528
571
  results = results.slice(plan.offset);
529
572
  }
530
- if (plan.limit !== null && plan.limit !== undefined) {
573
+ if (plan.limit !== null && plan.limit !== void 0) {
531
574
  results = results.slice(0, plan.limit);
532
575
  }
533
576
  return results.map(ctx.mapRecord);
@@ -535,16 +578,23 @@ function executeQuery(ctx, plan) {
535
578
  }
536
579
  function watchQuery(ctx, plan) {
537
580
  const query = () => executeQuery(ctx, plan);
538
- const changes = Stream2.fromPubSub(ctx.watchCtx.pubsub).pipe(Stream2.filter((event) => event.collection === ctx.collectionName), Stream2.mapEffect(() => Effect5.gen(function* () {
539
- const replaying = yield* Ref2.get(ctx.watchCtx.replayingRef);
540
- if (replaying)
541
- return;
542
- return yield* query();
543
- })), Stream2.filter((result) => result !== undefined));
544
- return Stream2.unwrap(Effect5.gen(function* () {
545
- const initial = yield* query();
546
- return Stream2.concat(Stream2.make(initial), changes);
547
- }));
581
+ const changes = Stream2.fromPubSub(ctx.watchCtx.pubsub).pipe(
582
+ Stream2.filter((event) => event.collection === ctx.collectionName),
583
+ Stream2.mapEffect(
584
+ () => Effect5.gen(function* () {
585
+ const replaying = yield* Ref2.get(ctx.watchCtx.replayingRef);
586
+ if (replaying) return void 0;
587
+ return yield* query();
588
+ })
589
+ ),
590
+ Stream2.filter((result) => result !== void 0)
591
+ );
592
+ return Stream2.unwrap(
593
+ Effect5.gen(function* () {
594
+ const initial = yield* query();
595
+ return Stream2.concat(Stream2.make(initial), changes);
596
+ })
597
+ );
548
598
  }
549
599
  function makeQueryBuilder(ctx, plan) {
550
600
  return {
@@ -566,15 +616,18 @@ function makeQueryBuilder(ctx, plan) {
566
616
  offset: (n) => makeQueryBuilder(ctx, { ...plan, offset: n }),
567
617
  limit: (n) => makeQueryBuilder(ctx, { ...plan, limit: n }),
568
618
  get: () => executeQuery(ctx, plan),
569
- first: () => Effect5.map(executeQuery(ctx, { ...plan, limit: 1 }), (results) => results.length > 0 ? Option2.some(results[0]) : Option2.none()),
619
+ first: () => Effect5.map(
620
+ executeQuery(ctx, { ...plan, limit: 1 }),
621
+ (results) => results.length > 0 ? Option2.some(results[0]) : Option2.none()
622
+ ),
570
623
  count: () => Effect5.map(executeQuery(ctx, plan), (results) => results.length),
571
624
  watch: () => watchQuery(ctx, plan)
572
625
  };
573
626
  }
574
- function createWhereClause(storage, watchCtx, collectionName, def, fieldName, mapRecord) {
575
- const ctx = { storage, watchCtx, collectionName, def, mapRecord };
627
+ function createWhereClause(storage, watchCtx, collectionName, def, fieldName, mapRecord2) {
628
+ const ctx = { storage, watchCtx, collectionName, def, mapRecord: mapRecord2 };
576
629
  const fieldDef = def.fields[fieldName];
577
- const isIndexed = def.indices.includes(fieldName) && fieldDef !== null && fieldDef !== undefined && fieldDef.kind !== "boolean";
630
+ const isIndexed = def.indices.includes(fieldName) && fieldDef !== null && fieldDef !== void 0 && fieldDef.kind !== "boolean";
578
631
  const withFilter = (filterFn, indexQuery) => {
579
632
  const plan = {
580
633
  ...emptyPlan(),
@@ -585,30 +638,51 @@ function createWhereClause(storage, watchCtx, collectionName, def, fieldName, ma
585
638
  return makeQueryBuilder(ctx, plan);
586
639
  };
587
640
  return {
588
- equals: (value) => withFilter((r) => r[fieldName] === value, isIndexed ? { field: fieldName, range: value, type: "value" } : undefined),
589
- above: (value) => withFilter((r) => r[fieldName] > value, isIndexed ? { field: fieldName, range: IDBKeyRange.lowerBound(value, true), type: "range" } : undefined),
590
- aboveOrEqual: (value) => withFilter((r) => r[fieldName] >= value, isIndexed ? { field: fieldName, range: IDBKeyRange.lowerBound(value, false), type: "range" } : undefined),
591
- below: (value) => withFilter((r) => r[fieldName] < value, isIndexed ? { field: fieldName, range: IDBKeyRange.upperBound(value, true), type: "range" } : undefined),
592
- belowOrEqual: (value) => withFilter((r) => r[fieldName] <= value, isIndexed ? { field: fieldName, range: IDBKeyRange.upperBound(value, false), type: "range" } : undefined),
641
+ equals: (value) => withFilter(
642
+ (r) => r[fieldName] === value,
643
+ isIndexed ? { field: fieldName, range: value, type: "value" } : void 0
644
+ ),
645
+ above: (value) => withFilter(
646
+ (r) => r[fieldName] > value,
647
+ isIndexed ? { field: fieldName, range: IDBKeyRange.lowerBound(value, true), type: "range" } : void 0
648
+ ),
649
+ aboveOrEqual: (value) => withFilter(
650
+ (r) => r[fieldName] >= value,
651
+ isIndexed ? { field: fieldName, range: IDBKeyRange.lowerBound(value, false), type: "range" } : void 0
652
+ ),
653
+ below: (value) => withFilter(
654
+ (r) => r[fieldName] < value,
655
+ isIndexed ? { field: fieldName, range: IDBKeyRange.upperBound(value, true), type: "range" } : void 0
656
+ ),
657
+ belowOrEqual: (value) => withFilter(
658
+ (r) => r[fieldName] <= value,
659
+ isIndexed ? { field: fieldName, range: IDBKeyRange.upperBound(value, false), type: "range" } : void 0
660
+ ),
593
661
  between: (lower, upper, options) => {
594
662
  const includeLower = options?.includeLower ?? true;
595
663
  const includeUpper = options?.includeUpper ?? true;
596
- return withFilter((r) => {
597
- const v = r[fieldName];
598
- const aboveLower = includeLower ? v >= lower : v > lower;
599
- const belowUpper = includeUpper ? v <= upper : v < upper;
600
- return aboveLower && belowUpper;
601
- }, isIndexed ? {
664
+ return withFilter(
665
+ (r) => {
666
+ const v = r[fieldName];
667
+ const aboveLower = includeLower ? v >= lower : v > lower;
668
+ const belowUpper = includeUpper ? v <= upper : v < upper;
669
+ return aboveLower && belowUpper;
670
+ },
671
+ isIndexed ? {
672
+ field: fieldName,
673
+ range: IDBKeyRange.bound(lower, upper, !includeLower, !includeUpper),
674
+ type: "range"
675
+ } : void 0
676
+ );
677
+ },
678
+ startsWith: (prefix) => withFilter(
679
+ (r) => typeof r[fieldName] === "string" && r[fieldName].startsWith(prefix),
680
+ isIndexed ? {
602
681
  field: fieldName,
603
- range: IDBKeyRange.bound(lower, upper, !includeLower, !includeUpper),
682
+ range: IDBKeyRange.bound(prefix, prefix + "\uFFFF", false, false),
604
683
  type: "range"
605
- } : undefined);
606
- },
607
- startsWith: (prefix) => withFilter((r) => typeof r[fieldName] === "string" && r[fieldName].startsWith(prefix), isIndexed ? {
608
- field: fieldName,
609
- range: IDBKeyRange.bound(prefix, prefix + "￿", false, false),
610
- type: "range"
611
- } : undefined),
684
+ } : void 0
685
+ ),
612
686
  anyOf: (values) => {
613
687
  const set = new Set(values);
614
688
  return withFilter((r) => set.has(r[fieldName]));
@@ -619,8 +693,8 @@ function createWhereClause(storage, watchCtx, collectionName, def, fieldName, ma
619
693
  }
620
694
  };
621
695
  }
622
- function createOrderByBuilder(storage, watchCtx, collectionName, def, fieldName, mapRecord) {
623
- const ctx = { storage, watchCtx, collectionName, def, mapRecord };
696
+ function createOrderByBuilder(storage, watchCtx, collectionName, def, fieldName, mapRecord2) {
697
+ const ctx = { storage, watchCtx, collectionName, def, mapRecord: mapRecord2 };
624
698
  const plan = {
625
699
  ...emptyPlan(),
626
700
  orderBy: { field: fieldName, direction: "asc" }
@@ -645,7 +719,7 @@ function replayState(recordId, events, stopAtId) {
645
719
  state = deepMerge(state, e.data);
646
720
  }
647
721
  }
648
- if (stopAtId !== undefined && e.id === stopAtId) {
722
+ if (stopAtId !== void 0 && e.id === stopAtId) {
649
723
  break;
650
724
  }
651
725
  }
@@ -663,8 +737,7 @@ function promoteToSnapshot(storage, collection2, recordId, target, allSorted) {
663
737
  function pruneEvents(storage, collection2, recordId, retention) {
664
738
  return Effect6.gen(function* () {
665
739
  const events = yield* storage.getEventsByRecord(collection2, recordId);
666
- if (events.length <= retention)
667
- return;
740
+ if (events.length <= retention) return;
668
741
  const sorted = [...events].sort((a, b) => b.createdAt - a.createdAt || (a.id < b.id ? 1 : -1));
669
742
  const retained = sorted.slice(0, retention);
670
743
  const toStrip = sorted.slice(retention);
@@ -673,8 +746,7 @@ function pruneEvents(storage, collection2, recordId, retention) {
673
746
  yield* promoteToSnapshot(storage, collection2, recordId, oldestRetained, sorted);
674
747
  }
675
748
  for (const e of toStrip) {
676
- if (e.data !== null)
677
- yield* storage.stripEventData(e.id);
749
+ if (e.data !== null) yield* storage.stripEventData(e.id);
678
750
  }
679
751
  });
680
752
  }
@@ -682,13 +754,13 @@ function mapRecord(record) {
682
754
  const { _d, _u, _a, _e, ...fields } = record;
683
755
  return fields;
684
756
  }
685
- function createCollectionHandle(def, storage, watchCtx, validator, partialValidator, makeEventId, localAuthor, onWrite) {
757
+ function createCollectionHandle(def, storage, watchCtx, validator, partialValidator, makeEventId, localAuthor, onWrite, logLevel = "None") {
686
758
  const collectionName = def.name;
759
+ const withLog = (effect) => Effect6.provideService(effect, References.MinimumLogLevel, logLevel);
687
760
  const commitEvent = (event) => Effect6.gen(function* () {
688
761
  yield* storage.putEvent(event);
689
762
  yield* applyEvent(storage, event);
690
- if (onWrite)
691
- yield* onWrite(event);
763
+ if (onWrite) yield* onWrite(event);
692
764
  yield* notifyChange(watchCtx, {
693
765
  collection: collectionName,
694
766
  recordId: event.recordId,
@@ -696,67 +768,84 @@ function createCollectionHandle(def, storage, watchCtx, validator, partialValida
696
768
  });
697
769
  });
698
770
  const handle = {
699
- add: (data) => Effect6.gen(function* () {
700
- const id = uuidv7();
701
- const fullRecord = { id, ...data };
702
- yield* validator(fullRecord);
703
- const event = {
704
- id: makeEventId(),
705
- collection: collectionName,
706
- recordId: id,
707
- kind: "c",
708
- data: fullRecord,
709
- createdAt: Date.now(),
710
- author: localAuthor
711
- };
712
- yield* commitEvent(event);
713
- return id;
714
- }),
715
- update: (id, data) => Effect6.gen(function* () {
716
- const existing = yield* storage.getRecord(collectionName, id);
717
- if (!existing || existing._d) {
718
- return yield* new NotFoundError({
771
+ add: (data) => withLog(
772
+ Effect6.gen(function* () {
773
+ const id = uuidv7();
774
+ const fullRecord = { id, ...data };
775
+ yield* validator(fullRecord);
776
+ const event = {
777
+ id: makeEventId(),
719
778
  collection: collectionName,
720
- id
779
+ recordId: id,
780
+ kind: "c",
781
+ data: fullRecord,
782
+ createdAt: Date.now(),
783
+ author: localAuthor
784
+ };
785
+ yield* commitEvent(event);
786
+ yield* Effect6.logDebug("Record added", {
787
+ collection: collectionName,
788
+ recordId: id,
789
+ data: fullRecord
721
790
  });
722
- }
723
- yield* partialValidator(data);
724
- const { _d, _u, _a, _e, ...existingFields } = existing;
725
- const merged = { ...existingFields, ...data, id };
726
- yield* validator(merged);
727
- const diff = deepDiff(existingFields, merged);
728
- const event = {
729
- id: makeEventId(),
730
- collection: collectionName,
731
- recordId: id,
732
- kind: "u",
733
- data: diff ?? { id },
734
- createdAt: Date.now(),
735
- author: localAuthor
736
- };
737
- yield* commitEvent(event);
738
- yield* pruneEvents(storage, collectionName, id, def.eventRetention);
739
- }),
740
- delete: (id) => Effect6.gen(function* () {
741
- const existing = yield* storage.getRecord(collectionName, id);
742
- if (!existing || existing._d) {
743
- return yield* new NotFoundError({
791
+ return id;
792
+ })
793
+ ),
794
+ update: (id, data) => withLog(
795
+ Effect6.gen(function* () {
796
+ const existing = yield* storage.getRecord(collectionName, id);
797
+ if (!existing || existing._d) {
798
+ return yield* new NotFoundError({
799
+ collection: collectionName,
800
+ id
801
+ });
802
+ }
803
+ yield* partialValidator(data);
804
+ const { _d, _u, _a, _e, ...existingFields } = existing;
805
+ const merged = { ...existingFields, ...data, id };
806
+ yield* validator(merged);
807
+ const diff = deepDiff(existingFields, merged);
808
+ const event = {
809
+ id: makeEventId(),
744
810
  collection: collectionName,
745
- id
811
+ recordId: id,
812
+ kind: "u",
813
+ data: diff ?? { id },
814
+ createdAt: Date.now(),
815
+ author: localAuthor
816
+ };
817
+ yield* commitEvent(event);
818
+ yield* Effect6.logDebug("Record updated", {
819
+ collection: collectionName,
820
+ recordId: id,
821
+ data: diff
746
822
  });
747
- }
748
- const event = {
749
- id: makeEventId(),
750
- collection: collectionName,
751
- recordId: id,
752
- kind: "d",
753
- data: null,
754
- createdAt: Date.now(),
755
- author: localAuthor
756
- };
757
- yield* commitEvent(event);
758
- yield* pruneEvents(storage, collectionName, id, def.eventRetention);
759
- }),
823
+ yield* pruneEvents(storage, collectionName, id, def.eventRetention);
824
+ })
825
+ ),
826
+ delete: (id) => withLog(
827
+ Effect6.gen(function* () {
828
+ const existing = yield* storage.getRecord(collectionName, id);
829
+ if (!existing || existing._d) {
830
+ return yield* new NotFoundError({
831
+ collection: collectionName,
832
+ id
833
+ });
834
+ }
835
+ const event = {
836
+ id: makeEventId(),
837
+ collection: collectionName,
838
+ recordId: id,
839
+ kind: "d",
840
+ data: null,
841
+ createdAt: Date.now(),
842
+ author: localAuthor
843
+ };
844
+ yield* commitEvent(event);
845
+ yield* Effect6.logDebug("Record deleted", { collection: collectionName, recordId: id });
846
+ yield* pruneEvents(storage, collectionName, id, def.eventRetention);
847
+ })
848
+ ),
760
849
  undo: (id) => Effect6.gen(function* () {
761
850
  const existing = yield* storage.getRecord(collectionName, id);
762
851
  if (!existing) {
@@ -797,15 +886,29 @@ function createCollectionHandle(def, storage, watchCtx, validator, partialValida
797
886
  return found ? Option3.some(mapRecord(found)) : Option3.none();
798
887
  }),
799
888
  count: () => Effect6.map(storage.getAllRecords(collectionName), (all) => all.filter((r) => !r._d).length),
800
- watch: () => watchCollection(watchCtx, storage, collectionName, undefined, mapRecord),
801
- where: (fieldName) => createWhereClause(storage, watchCtx, collectionName, def, fieldName, mapRecord),
802
- orderBy: (fieldName) => createOrderByBuilder(storage, watchCtx, collectionName, def, fieldName, mapRecord)
889
+ watch: () => watchCollection(watchCtx, storage, collectionName, void 0, mapRecord),
890
+ where: (fieldName) => createWhereClause(
891
+ storage,
892
+ watchCtx,
893
+ collectionName,
894
+ def,
895
+ fieldName,
896
+ mapRecord
897
+ ),
898
+ orderBy: (fieldName) => createOrderByBuilder(
899
+ storage,
900
+ watchCtx,
901
+ collectionName,
902
+ def,
903
+ fieldName,
904
+ mapRecord
905
+ )
803
906
  };
804
907
  return handle;
805
908
  }
806
909
 
807
910
  // src/sync/sync-service.ts
808
- import { Effect as Effect8, Option as Option5, Ref as Ref3, Schedule } from "effect";
911
+ import { Duration, Effect as Effect8, Layer, Option as Option5, References as References2, Ref as Ref3, Schedule } from "effect";
809
912
  import { unwrapEvent } from "nostr-tools/nip59";
810
913
  import { GiftWrap as GiftWrap2 } from "nostr-tools/kinds";
811
914
 
@@ -821,8 +924,7 @@ var Mode = {
821
924
  Fingerprint: 1,
822
925
  IdList: 2
823
926
  };
824
-
825
- class WrappedBuffer {
927
+ var WrappedBuffer = class {
826
928
  constructor(buffer) {
827
929
  this._raw = new Uint8Array(buffer || 512);
828
930
  this.length = buffer ? buffer.length : 0;
@@ -834,10 +936,8 @@ class WrappedBuffer {
834
936
  return this._raw.byteLength;
835
937
  }
836
938
  extend(buf) {
837
- if (buf._raw)
838
- buf = buf.unwrap();
839
- if (typeof buf.length !== "number")
840
- throw Error("bad length");
939
+ if (buf._raw) buf = buf.unwrap();
940
+ if (typeof buf.length !== "number") throw Error("bad length");
841
941
  const targetSize = buf.length + this.length;
842
942
  if (this.capacity < targetSize) {
843
943
  const oldRaw = this._raw;
@@ -860,42 +960,36 @@ class WrappedBuffer {
860
960
  this.length -= n;
861
961
  return firstSubarray;
862
962
  }
863
- }
963
+ };
864
964
  function decodeVarInt(buf) {
865
965
  let res = 0;
866
- while (true) {
867
- if (buf.length === 0)
868
- throw Error("parse ends prematurely");
966
+ while (1) {
967
+ if (buf.length === 0) throw Error("parse ends prematurely");
869
968
  let byte = buf.shift();
870
969
  res = res << 7 | byte & 127;
871
- if ((byte & 128) === 0)
872
- break;
970
+ if ((byte & 128) === 0) break;
873
971
  }
874
972
  return res;
875
973
  }
876
974
  function encodeVarInt(n) {
877
- if (n === 0)
878
- return new WrappedBuffer([0]);
975
+ if (n === 0) return new WrappedBuffer([0]);
879
976
  let o = [];
880
977
  while (n !== 0) {
881
978
  o.push(n & 127);
882
979
  n >>>= 7;
883
980
  }
884
981
  o.reverse();
885
- for (let i = 0;i < o.length - 1; i++)
886
- o[i] |= 128;
982
+ for (let i = 0; i < o.length - 1; i++) o[i] |= 128;
887
983
  return new WrappedBuffer(o);
888
984
  }
889
985
  function getByte(buf) {
890
986
  return getBytes(buf, 1)[0];
891
987
  }
892
988
  function getBytes(buf, n) {
893
- if (buf.length < n)
894
- throw Error("parse ends prematurely");
989
+ if (buf.length < n) throw Error("parse ends prematurely");
895
990
  return buf.shiftN(n);
896
991
  }
897
-
898
- class Accumulator {
992
+ var Accumulator = class {
899
993
  constructor() {
900
994
  this.setToZero();
901
995
  if (typeof window === "undefined") {
@@ -912,15 +1006,14 @@ class Accumulator {
912
1006
  let currCarry = 0, nextCarry = 0;
913
1007
  let p = new DataView(this.buf.buffer);
914
1008
  let po = new DataView(otherBuf.buffer);
915
- for (let i = 0;i < 8; i++) {
1009
+ for (let i = 0; i < 8; i++) {
916
1010
  let offset = i * 4;
917
1011
  let orig = p.getUint32(offset, true);
918
1012
  let otherV = po.getUint32(offset, true);
919
1013
  let next = orig;
920
1014
  next += currCarry;
921
1015
  next += otherV;
922
- if (next > 4294967295)
923
- nextCarry = 1;
1016
+ if (next > 4294967295) nextCarry = 1;
924
1017
  p.setUint32(offset, next & 4294967295, true);
925
1018
  currCarry = nextCarry;
926
1019
  nextCarry = 0;
@@ -928,7 +1021,7 @@ class Accumulator {
928
1021
  }
929
1022
  negate() {
930
1023
  let p = new DataView(this.buf.buffer);
931
- for (let i = 0;i < 8; i++) {
1024
+ for (let i = 0; i < 8; i++) {
932
1025
  let offset = i * 4;
933
1026
  p.setUint32(offset, ~p.getUint32(offset, true));
934
1027
  }
@@ -937,33 +1030,29 @@ class Accumulator {
937
1030
  this.add(one);
938
1031
  }
939
1032
  async getFingerprint(n) {
940
- let input = new WrappedBuffer;
1033
+ let input = new WrappedBuffer();
941
1034
  input.extend(this.buf);
942
1035
  input.extend(encodeVarInt(n));
943
1036
  let hash = await this.sha256(input.unwrap());
944
1037
  return hash.subarray(0, FINGERPRINT_SIZE);
945
1038
  }
946
- }
947
-
948
- class NegentropyStorageVector {
1039
+ };
1040
+ var NegentropyStorageVector = class {
949
1041
  constructor() {
950
1042
  this.items = [];
951
1043
  this.sealed = false;
952
1044
  }
953
1045
  insert(timestamp, id) {
954
- if (this.sealed)
955
- throw Error("already sealed");
1046
+ if (this.sealed) throw Error("already sealed");
956
1047
  id = loadInputBuffer(id);
957
- if (id.byteLength !== ID_SIZE)
958
- throw Error("bad id size for added item");
1048
+ if (id.byteLength !== ID_SIZE) throw Error("bad id size for added item");
959
1049
  this.items.push({ timestamp, id });
960
1050
  }
961
1051
  seal() {
962
- if (this.sealed)
963
- throw Error("already sealed");
1052
+ if (this.sealed) throw Error("already sealed");
964
1053
  this.sealed = true;
965
1054
  this.items.sort(itemCompare);
966
- for (let i = 1;i < this.items.length; i++) {
1055
+ for (let i = 1; i < this.items.length; i++) {
967
1056
  if (itemCompare(this.items[i - 1], this.items[i]) === 0)
968
1057
  throw Error("duplicate item inserted");
969
1058
  }
@@ -977,16 +1066,14 @@ class NegentropyStorageVector {
977
1066
  }
978
1067
  getItem(i) {
979
1068
  this._checkSealed();
980
- if (i >= this.items.length)
981
- throw Error("out of range");
1069
+ if (i >= this.items.length) throw Error("out of range");
982
1070
  return this.items[i];
983
1071
  }
984
1072
  iterate(begin, end, cb) {
985
1073
  this._checkSealed();
986
1074
  this._checkBounds(begin, end);
987
- for (let i = begin;i < end; ++i) {
988
- if (!cb(this.items[i], i))
989
- break;
1075
+ for (let i = begin; i < end; ++i) {
1076
+ if (!cb(this.items[i], i)) break;
990
1077
  }
991
1078
  }
992
1079
  findLowerBound(begin, end, bound) {
@@ -995,7 +1082,7 @@ class NegentropyStorageVector {
995
1082
  return this._binarySearch(this.items, begin, end, (a) => itemCompare(a, bound) < 0);
996
1083
  }
997
1084
  async fingerprint(begin, end) {
998
- let out = new Accumulator;
1085
+ let out = new Accumulator();
999
1086
  out.setToZero();
1000
1087
  this.iterate(begin, end, (item, i) => {
1001
1088
  out.add(item.id);
@@ -1004,12 +1091,10 @@ class NegentropyStorageVector {
1004
1091
  return await out.getFingerprint(end - begin);
1005
1092
  }
1006
1093
  _checkSealed() {
1007
- if (!this.sealed)
1008
- throw Error("not sealed");
1094
+ if (!this.sealed) throw Error("not sealed");
1009
1095
  }
1010
1096
  _checkBounds(begin, end) {
1011
- if (begin > end || end > this.items.length)
1012
- throw Error("bad range");
1097
+ if (begin > end || end > this.items.length) throw Error("bad range");
1013
1098
  }
1014
1099
  _binarySearch(arr, first, last, cmp) {
1015
1100
  let count = last - first;
@@ -1026,12 +1111,10 @@ class NegentropyStorageVector {
1026
1111
  }
1027
1112
  return first;
1028
1113
  }
1029
- }
1030
-
1031
- class Negentropy {
1114
+ };
1115
+ var Negentropy = class {
1032
1116
  constructor(storage, frameSizeLimit = 0) {
1033
- if (frameSizeLimit !== 0 && frameSizeLimit < 4096)
1034
- throw Error("frameSizeLimit too small");
1117
+ if (frameSizeLimit !== 0 && frameSizeLimit < 4096) throw Error("frameSizeLimit too small");
1035
1118
  this.storage = storage;
1036
1119
  this.frameSizeLimit = frameSizeLimit;
1037
1120
  this.lastTimestampIn = 0;
@@ -1041,10 +1124,9 @@ class Negentropy {
1041
1124
  return { timestamp, id: id ? id : new Uint8Array(0) };
1042
1125
  }
1043
1126
  async initiate() {
1044
- if (this.isInitiator)
1045
- throw Error("already initiated");
1127
+ if (this.isInitiator) throw Error("already initiated");
1046
1128
  this.isInitiator = true;
1047
- let output = new WrappedBuffer;
1129
+ let output = new WrappedBuffer();
1048
1130
  output.extend([PROTOCOL_VERSION]);
1049
1131
  await this.splitRange(0, this.storage.size(), this._bound(Number.MAX_VALUE), output);
1050
1132
  return this._renderOutput(output);
@@ -1056,23 +1138,24 @@ class Negentropy {
1056
1138
  let haveIds = [], needIds = [];
1057
1139
  query = new WrappedBuffer(loadInputBuffer(query));
1058
1140
  this.lastTimestampIn = this.lastTimestampOut = 0;
1059
- let fullOutput = new WrappedBuffer;
1141
+ let fullOutput = new WrappedBuffer();
1060
1142
  fullOutput.extend([PROTOCOL_VERSION]);
1061
1143
  let protocolVersion = getByte(query);
1062
1144
  if (protocolVersion < 96 || protocolVersion > 111)
1063
1145
  throw Error("invalid negentropy protocol version byte");
1064
1146
  if (protocolVersion !== PROTOCOL_VERSION) {
1065
1147
  if (this.isInitiator)
1066
- throw Error("unsupported negentropy protocol version requested: " + (protocolVersion - 96));
1067
- else
1068
- return [this._renderOutput(fullOutput), haveIds, needIds];
1148
+ throw Error(
1149
+ "unsupported negentropy protocol version requested: " + (protocolVersion - 96)
1150
+ );
1151
+ else return [this._renderOutput(fullOutput), haveIds, needIds];
1069
1152
  }
1070
1153
  let storageSize = this.storage.size();
1071
1154
  let prevBound = this._bound(0);
1072
1155
  let prevIndex = 0;
1073
1156
  let skip = false;
1074
1157
  while (query.length !== 0) {
1075
- let o = new WrappedBuffer;
1158
+ let o = new WrappedBuffer();
1076
1159
  let doSkip = () => {
1077
1160
  if (skip) {
1078
1161
  skip = false;
@@ -1098,10 +1181,9 @@ class Negentropy {
1098
1181
  } else if (mode === Mode.IdList) {
1099
1182
  let numIds = decodeVarInt(query);
1100
1183
  let theirElems = {};
1101
- for (let i = 0;i < numIds; i++) {
1184
+ for (let i = 0; i < numIds; i++) {
1102
1185
  let e = getBytes(query, ID_SIZE);
1103
- if (this.isInitiator)
1104
- theirElems[e] = e;
1186
+ if (this.isInitiator) theirElems[e] = e;
1105
1187
  }
1106
1188
  if (this.isInitiator) {
1107
1189
  skip = true;
@@ -1120,7 +1202,7 @@ class Negentropy {
1120
1202
  }
1121
1203
  } else {
1122
1204
  doSkip();
1123
- let responseIds = new WrappedBuffer;
1205
+ let responseIds = new WrappedBuffer();
1124
1206
  let numResponseIds = 0;
1125
1207
  let endBound = currBound;
1126
1208
  this.storage.iterate(lower, upper, (item, index) => {
@@ -1138,7 +1220,7 @@ class Negentropy {
1138
1220
  o.extend(encodeVarInt(numResponseIds));
1139
1221
  o.extend(responseIds);
1140
1222
  fullOutput.extend(o);
1141
- o = new WrappedBuffer;
1223
+ o = new WrappedBuffer();
1142
1224
  }
1143
1225
  } else {
1144
1226
  throw Error("unexpected mode");
@@ -1176,7 +1258,7 @@ class Negentropy {
1176
1258
  let itemsPerBucket = Math.floor(numElems / buckets);
1177
1259
  let bucketsWithExtra = numElems % buckets;
1178
1260
  let curr = lower;
1179
- for (let i = 0;i < buckets; i++) {
1261
+ for (let i = 0; i < buckets; i++) {
1180
1262
  let bucketSize = itemsPerBucket + (i < bucketsWithExtra ? 1 : 0);
1181
1263
  let ourFingerprint = await this.storage.fingerprint(curr, curr + bucketSize);
1182
1264
  curr += bucketSize;
@@ -1186,10 +1268,8 @@ class Negentropy {
1186
1268
  } else {
1187
1269
  let prevItem, currItem;
1188
1270
  this.storage.iterate(curr - 1, curr + 1, (item, index) => {
1189
- if (index === curr - 1)
1190
- prevItem = item;
1191
- else
1192
- currItem = item;
1271
+ if (index === curr - 1) prevItem = item;
1272
+ else currItem = item;
1193
1273
  return true;
1194
1274
  });
1195
1275
  nextBound = this.getMinimalBound(prevItem, currItem);
@@ -1202,13 +1282,13 @@ class Negentropy {
1202
1282
  }
1203
1283
  _renderOutput(o) {
1204
1284
  o = o.unwrap();
1205
- if (!this.wantUint8ArrayOutput)
1206
- o = uint8ArrayToHex(o);
1285
+ if (!this.wantUint8ArrayOutput) o = uint8ArrayToHex(o);
1207
1286
  return o;
1208
1287
  }
1209
1288
  exceededFrameSizeLimit(n) {
1210
1289
  return this.frameSizeLimit && n > this.frameSizeLimit - 200;
1211
1290
  }
1291
+ // Decoding
1212
1292
  decodeTimestampIn(encoded) {
1213
1293
  let timestamp = decodeVarInt(encoded);
1214
1294
  timestamp = timestamp === 0 ? Number.MAX_VALUE : timestamp - 1;
@@ -1223,11 +1303,11 @@ class Negentropy {
1223
1303
  decodeBound(encoded) {
1224
1304
  let timestamp = this.decodeTimestampIn(encoded);
1225
1305
  let len = decodeVarInt(encoded);
1226
- if (len > ID_SIZE)
1227
- throw Error("bound key too long");
1306
+ if (len > ID_SIZE) throw Error("bound key too long");
1228
1307
  let id = getBytes(encoded, len);
1229
1308
  return { timestamp, id };
1230
1309
  }
1310
+ // Encoding
1231
1311
  encodeTimestampOut(timestamp) {
1232
1312
  if (timestamp === Number.MAX_VALUE) {
1233
1313
  this.lastTimestampOut = Number.MAX_VALUE;
@@ -1239,7 +1319,7 @@ class Negentropy {
1239
1319
  return encodeVarInt(timestamp + 1);
1240
1320
  }
1241
1321
  encodeBound(key) {
1242
- let output = new WrappedBuffer;
1322
+ let output = new WrappedBuffer();
1243
1323
  output.extend(this.encodeTimestampOut(key.timestamp));
1244
1324
  output.extend(encodeVarInt(key.id.length));
1245
1325
  output.extend(key.id);
@@ -1252,30 +1332,24 @@ class Negentropy {
1252
1332
  let sharedPrefixBytes = 0;
1253
1333
  let currKey = curr.id;
1254
1334
  let prevKey = prev.id;
1255
- for (let i = 0;i < ID_SIZE; i++) {
1256
- if (currKey[i] !== prevKey[i])
1257
- break;
1335
+ for (let i = 0; i < ID_SIZE; i++) {
1336
+ if (currKey[i] !== prevKey[i]) break;
1258
1337
  sharedPrefixBytes++;
1259
1338
  }
1260
1339
  return this._bound(curr.timestamp, curr.id.subarray(0, sharedPrefixBytes + 1));
1261
1340
  }
1262
1341
  }
1263
- }
1342
+ };
1264
1343
  function loadInputBuffer(inp) {
1265
- if (typeof inp === "string")
1266
- inp = hexToUint8Array(inp);
1267
- else if (__proto__ !== Uint8Array.prototype)
1268
- inp = new Uint8Array(inp);
1344
+ if (typeof inp === "string") inp = hexToUint8Array(inp);
1345
+ else if (__proto__ !== Uint8Array.prototype) inp = new Uint8Array(inp);
1269
1346
  return inp;
1270
1347
  }
1271
1348
  function hexToUint8Array(h) {
1272
- if (h.startsWith("0x"))
1273
- h = h.substr(2);
1274
- if (h.length % 2 === 1)
1275
- throw Error("odd length of hex string");
1349
+ if (h.startsWith("0x")) h = h.substr(2);
1350
+ if (h.length % 2 === 1) throw Error("odd length of hex string");
1276
1351
  let arr = new Uint8Array(h.length / 2);
1277
- for (let i = 0;i < arr.length; i++)
1278
- arr[i] = parseInt(h.substr(i * 2, 2), 16);
1352
+ for (let i = 0; i < arr.length; i++) arr[i] = parseInt(h.substr(i * 2, 2), 16);
1279
1353
  return arr;
1280
1354
  }
1281
1355
  var uint8ArrayToHexLookupTable = new Array(256);
@@ -1298,28 +1372,24 @@ var uint8ArrayToHexLookupTable = new Array(256);
1298
1372
  "e",
1299
1373
  "f"
1300
1374
  ];
1301
- for (let i = 0;i < 256; i++) {
1375
+ for (let i = 0; i < 256; i++) {
1302
1376
  uint8ArrayToHexLookupTable[i] = hexAlphabet[i >>> 4 & 15] + hexAlphabet[i & 15];
1303
1377
  }
1304
1378
  }
1305
1379
  function uint8ArrayToHex(arr) {
1306
1380
  let out = "";
1307
- for (let i = 0, edx = arr.length;i < edx; i++) {
1381
+ for (let i = 0, edx = arr.length; i < edx; i++) {
1308
1382
  out += uint8ArrayToHexLookupTable[arr[i]];
1309
1383
  }
1310
1384
  return out;
1311
1385
  }
1312
1386
  function compareUint8Array(a, b) {
1313
- for (let i = 0;i < a.byteLength; i++) {
1314
- if (a[i] < b[i])
1315
- return -1;
1316
- if (a[i] > b[i])
1317
- return 1;
1318
- }
1319
- if (a.byteLength > b.byteLength)
1320
- return 1;
1321
- if (a.byteLength < b.byteLength)
1322
- return -1;
1387
+ for (let i = 0; i < a.byteLength; i++) {
1388
+ if (a[i] < b[i]) return -1;
1389
+ if (a[i] > b[i]) return 1;
1390
+ }
1391
+ if (a.byteLength > b.byteLength) return 1;
1392
+ if (a.byteLength < b.byteLength) return -1;
1323
1393
  return 0;
1324
1394
  }
1325
1395
  function itemCompare(a, b) {
@@ -1335,7 +1405,7 @@ import { GiftWrap } from "nostr-tools/kinds";
1335
1405
  function reconcileWithRelay(storage, relay, relayUrl, publicKeys) {
1336
1406
  return Effect7.gen(function* () {
1337
1407
  const allGiftWraps = yield* storage.getAllGiftWraps();
1338
- const storageVector = new NegentropyStorageVector;
1408
+ const storageVector = new NegentropyStorageVector();
1339
1409
  for (const gw of allGiftWraps) {
1340
1410
  storageVector.insert(gw.createdAt, hexToBytes2(gw.id));
1341
1411
  }
@@ -1359,8 +1429,7 @@ function reconcileWithRelay(storage, relay, relayUrl, publicKeys) {
1359
1429
  let currentMsg = initialMsg;
1360
1430
  while (currentMsg !== null) {
1361
1431
  const response = yield* relay.sendNegMsg(relayUrl, subId, filter, currentMsg);
1362
- if (response.msgHex === null)
1363
- break;
1432
+ if (response.msgHex === null) break;
1364
1433
  const reconcileResult = yield* Effect7.try({
1365
1434
  try: () => neg.reconcile(response.msgHex),
1366
1435
  catch: (e) => new SyncError({
@@ -1370,14 +1439,17 @@ function reconcileWithRelay(storage, relay, relayUrl, publicKeys) {
1370
1439
  })
1371
1440
  });
1372
1441
  const [nextMsg, haveIds, needIds] = reconcileResult;
1373
- for (const id of haveIds)
1374
- allHaveIds.push(id);
1375
- for (const id of needIds)
1376
- allNeedIds.push(id);
1442
+ for (const id of haveIds) allHaveIds.push(id);
1443
+ for (const id of needIds) allNeedIds.push(id);
1377
1444
  currentMsg = nextMsg;
1378
1445
  }
1446
+ yield* Effect7.logDebug("Negentropy reconciliation complete", {
1447
+ relay: relayUrl,
1448
+ have: allHaveIds.length,
1449
+ need: allNeedIds.length
1450
+ });
1379
1451
  return { haveIds: allHaveIds, needIds: allNeedIds };
1380
- });
1452
+ }).pipe(Effect7.withLogSpan("tablinum.negentropy"));
1381
1453
  }
1382
1454
 
1383
1455
  // src/db/key-rotation.ts
@@ -1416,12 +1488,11 @@ function createRotation(epochStore, senderPrivateKey, senderPublicKey, remaining
1416
1488
  kind: 1,
1417
1489
  content: JSON.stringify(rotationData),
1418
1490
  tags: [["d", `_system:rotation:${epochId}`]],
1419
- created_at: Math.floor(Date.now() / 1000)
1491
+ created_at: Math.floor(Date.now() / 1e3)
1420
1492
  };
1421
1493
  const wrappedEvents = [];
1422
1494
  for (const memberPubkey of remainingMemberPubkeys) {
1423
- if (memberPubkey === senderPublicKey)
1424
- continue;
1495
+ if (memberPubkey === senderPublicKey) continue;
1425
1496
  const wrapped = wrapEvent(rumor, senderPrivateKey, memberPubkey);
1426
1497
  wrappedEvents.push(wrapped);
1427
1498
  }
@@ -1434,7 +1505,7 @@ function createRotation(epochStore, senderPrivateKey, senderPublicKey, remaining
1434
1505
  kind: 1,
1435
1506
  content: JSON.stringify(removalData),
1436
1507
  tags: [["d", `_system:removed:${epochId}`]],
1437
- created_at: Math.floor(Date.now() / 1000)
1508
+ created_at: Math.floor(Date.now() / 1e3)
1438
1509
  };
1439
1510
  const removalNotices = [];
1440
1511
  for (const removedPubkey of removedMemberPubkeys) {
@@ -1444,8 +1515,7 @@ function createRotation(epochStore, senderPrivateKey, senderPublicKey, remaining
1444
1515
  return { epoch, wrappedEvents, removalNotices };
1445
1516
  }
1446
1517
  function parseRotationEvent(content, dTag) {
1447
- if (!dTag.startsWith("_system:rotation:"))
1448
- return Option4.none();
1518
+ if (!dTag.startsWith("_system:rotation:")) return Option4.none();
1449
1519
  try {
1450
1520
  return Option4.some(decodeRotationData(content));
1451
1521
  } catch {
@@ -1453,8 +1523,7 @@ function parseRotationEvent(content, dTag) {
1453
1523
  }
1454
1524
  }
1455
1525
  function parseRemovalNotice(content, dTag) {
1456
- if (!dTag.startsWith("_system:removed:"))
1457
- return Option4.none();
1526
+ if (!dTag.startsWith("_system:removed:")) return Option4.none();
1458
1527
  try {
1459
1528
  return Option4.some(decodeRemovalNotice(content));
1460
1529
  } catch {
@@ -1463,7 +1532,8 @@ function parseRemovalNotice(content, dTag) {
1463
1532
  }
1464
1533
 
1465
1534
  // src/sync/sync-service.ts
1466
- function createSyncHandle(storage, giftWrapHandle, relay, publishQueue, syncStatus, watchCtx, relayUrls, knownCollections, epochStore, personalPrivateKey, personalPublicKey, scope, onSyncError, onNewAuthor, onRemoved, onMembersChanged) {
1535
+ function createSyncHandle(storage, giftWrapHandle, relay, publishQueue, syncStatus, watchCtx, relayUrls, knownCollections, epochStore, personalPrivateKey, personalPublicKey, scope, logLevel, onSyncError, onNewAuthor, onRemoved, onMembersChanged) {
1536
+ const logLayer = Layer.succeed(References2.MinimumLogLevel, logLevel);
1467
1537
  const getSubscriptionPubKeys = () => {
1468
1538
  return getAllPublicKeys(epochStore);
1469
1539
  };
@@ -1473,64 +1543,79 @@ function createSyncHandle(storage, giftWrapHandle, relay, publishQueue, syncStat
1473
1543
  kind: "create"
1474
1544
  });
1475
1545
  const forkHandled = (effect) => {
1476
- Effect8.runFork(effect.pipe(Effect8.tapError((e) => Effect8.sync(() => onSyncError?.(e))), Effect8.ignore, Effect8.forkIn(scope)));
1546
+ Effect8.runFork(
1547
+ effect.pipe(
1548
+ Effect8.tapError((e) => Effect8.sync(() => onSyncError?.(e))),
1549
+ Effect8.ignore,
1550
+ Effect8.provide(logLayer),
1551
+ Effect8.forkIn(scope)
1552
+ )
1553
+ );
1477
1554
  };
1478
1555
  let autoFlushActive = false;
1479
1556
  const autoFlushEffect = Effect8.gen(function* () {
1480
1557
  const size = yield* publishQueue.size();
1481
- if (size === 0)
1482
- return;
1558
+ if (size === 0) return;
1483
1559
  yield* syncStatus.set("syncing");
1484
1560
  yield* publishQueue.flush(relayUrls);
1485
1561
  const remaining = yield* publishQueue.size();
1486
- if (remaining > 0)
1487
- yield* Effect8.fail("pending");
1488
- }).pipe(Effect8.ensuring(syncStatus.set("idle")), Effect8.retry({ schedule: Schedule.exponential(5000).pipe(Schedule.jittered), times: 10 }), Effect8.ignore);
1562
+ if (remaining > 0) yield* Effect8.fail("pending");
1563
+ }).pipe(
1564
+ Effect8.ensuring(syncStatus.set("idle")),
1565
+ Effect8.retry({ schedule: Schedule.exponential(5e3).pipe(Schedule.jittered), times: 10 }),
1566
+ Effect8.ignore
1567
+ );
1489
1568
  const scheduleAutoFlush = () => {
1490
- if (autoFlushActive)
1491
- return;
1569
+ if (autoFlushActive) return;
1492
1570
  autoFlushActive = true;
1493
- forkHandled(autoFlushEffect.pipe(Effect8.ensuring(Effect8.sync(() => {
1494
- autoFlushActive = false;
1495
- }))));
1571
+ forkHandled(
1572
+ autoFlushEffect.pipe(
1573
+ Effect8.ensuring(
1574
+ Effect8.sync(() => {
1575
+ autoFlushActive = false;
1576
+ })
1577
+ )
1578
+ )
1579
+ );
1496
1580
  };
1497
1581
  const shouldRejectWrite = (authorPubkey) => Effect8.gen(function* () {
1498
1582
  const memberRecord = yield* storage.getRecord("_members", authorPubkey);
1499
- if (!memberRecord)
1500
- return false;
1583
+ if (!memberRecord) return false;
1501
1584
  return !!memberRecord.removedAt;
1502
1585
  });
1503
1586
  const processGiftWrap = (remoteGw) => Effect8.gen(function* () {
1504
1587
  const existing = yield* storage.getGiftWrap(remoteGw.id);
1505
- if (existing)
1506
- return null;
1588
+ if (existing) return null;
1507
1589
  const unwrapResult = yield* Effect8.result(giftWrapHandle.unwrap(remoteGw));
1508
1590
  if (unwrapResult._tag === "Failure") {
1509
- yield* storage.putGiftWrap({ id: remoteGw.id, createdAt: remoteGw.created_at });
1591
+ yield* storage.putGiftWrap({ id: remoteGw.id, event: remoteGw, createdAt: remoteGw.created_at });
1510
1592
  return null;
1511
1593
  }
1512
1594
  const rumor = unwrapResult.success;
1513
1595
  const dTag = rumor.tags.find((t) => t[0] === "d")?.[1];
1514
1596
  if (!dTag) {
1515
- yield* storage.putGiftWrap({ id: remoteGw.id, createdAt: remoteGw.created_at });
1597
+ yield* storage.putGiftWrap({ id: remoteGw.id, event: remoteGw, createdAt: remoteGw.created_at });
1516
1598
  return null;
1517
1599
  }
1518
1600
  const colonIdx = dTag.indexOf(":");
1519
1601
  if (colonIdx === -1) {
1520
- yield* storage.putGiftWrap({ id: remoteGw.id, createdAt: remoteGw.created_at });
1602
+ yield* storage.putGiftWrap({ id: remoteGw.id, event: remoteGw, createdAt: remoteGw.created_at });
1521
1603
  return null;
1522
1604
  }
1523
1605
  const collectionName = dTag.substring(0, colonIdx);
1524
1606
  const recordId = dTag.substring(colonIdx + 1);
1525
1607
  const retention = knownCollections.get(collectionName);
1526
- if (retention === undefined) {
1527
- yield* storage.putGiftWrap({ id: remoteGw.id, createdAt: remoteGw.created_at });
1608
+ if (retention === void 0) {
1609
+ yield* storage.putGiftWrap({ id: remoteGw.id, event: remoteGw, createdAt: remoteGw.created_at });
1528
1610
  return null;
1529
1611
  }
1530
1612
  if (rumor.pubkey) {
1531
1613
  const reject = yield* shouldRejectWrite(rumor.pubkey);
1532
1614
  if (reject) {
1533
- yield* storage.putGiftWrap({ id: remoteGw.id, createdAt: remoteGw.created_at });
1615
+ yield* Effect8.logWarning("Rejected write from removed member", {
1616
+ author: rumor.pubkey.slice(0, 12)
1617
+ });
1618
+ yield* storage.putGiftWrap({ id: remoteGw.id, event: remoteGw, createdAt: remoteGw.created_at });
1534
1619
  return null;
1535
1620
  }
1536
1621
  }
@@ -1538,14 +1623,10 @@ function createSyncHandle(storage, giftWrapHandle, relay, publishQueue, syncStat
1538
1623
  let kind = "u";
1539
1624
  const parsed = yield* Effect8.try({
1540
1625
  try: () => JSON.parse(rumor.content),
1541
- catch: () => {
1542
- return;
1543
- }
1544
- }).pipe(Effect8.orElseSucceed(() => {
1545
- return;
1546
- }));
1547
- if (parsed === undefined) {
1548
- yield* storage.putGiftWrap({ id: remoteGw.id, createdAt: remoteGw.created_at });
1626
+ catch: () => void 0
1627
+ }).pipe(Effect8.orElseSucceed(() => void 0));
1628
+ if (parsed === void 0) {
1629
+ yield* storage.putGiftWrap({ id: remoteGw.id, event: remoteGw, createdAt: remoteGw.created_at });
1549
1630
  return null;
1550
1631
  }
1551
1632
  if (parsed === null || parsed._deleted) {
@@ -1553,18 +1634,19 @@ function createSyncHandle(storage, giftWrapHandle, relay, publishQueue, syncStat
1553
1634
  } else {
1554
1635
  data = parsed;
1555
1636
  }
1556
- const author = rumor.pubkey || undefined;
1637
+ const author = rumor.pubkey || void 0;
1557
1638
  const event = {
1558
1639
  id: rumor.id,
1559
1640
  collection: collectionName,
1560
1641
  recordId,
1561
1642
  kind,
1562
1643
  data,
1563
- createdAt: rumor.created_at * 1000,
1644
+ createdAt: rumor.created_at * 1e3,
1564
1645
  author
1565
1646
  };
1566
1647
  yield* storage.putGiftWrap({
1567
1648
  id: remoteGw.id,
1649
+ event: remoteGw,
1568
1650
  createdAt: remoteGw.created_at
1569
1651
  });
1570
1652
  yield* storage.putEvent(event);
@@ -1572,6 +1654,12 @@ function createSyncHandle(storage, giftWrapHandle, relay, publishQueue, syncStat
1572
1654
  if (didApply && (kind === "u" || kind === "d")) {
1573
1655
  yield* pruneEvents(storage, collectionName, recordId, retention);
1574
1656
  }
1657
+ yield* Effect8.logDebug("Processed gift wrap", {
1658
+ collection: collectionName,
1659
+ recordId,
1660
+ kind,
1661
+ author: author?.slice(0, 12)
1662
+ });
1575
1663
  if (author && onNewAuthor) {
1576
1664
  onNewAuthor(author);
1577
1665
  }
@@ -1584,32 +1672,34 @@ function createSyncHandle(storage, giftWrapHandle, relay, publishQueue, syncStat
1584
1672
  }
1585
1673
  });
1586
1674
  const processRotationGiftWrap = (remoteGw) => Effect8.gen(function* () {
1587
- const unwrapResult = yield* Effect8.result(Effect8.try({
1588
- try: () => unwrapEvent(remoteGw, personalPrivateKey),
1589
- catch: (e) => new CryptoError({
1590
- message: `Rotation unwrap failed: ${e instanceof Error ? e.message : String(e)}`,
1591
- cause: e
1675
+ const unwrapResult = yield* Effect8.result(
1676
+ Effect8.try({
1677
+ try: () => unwrapEvent(remoteGw, personalPrivateKey),
1678
+ catch: (e) => new CryptoError({
1679
+ message: `Rotation unwrap failed: ${e instanceof Error ? e.message : String(e)}`,
1680
+ cause: e
1681
+ })
1592
1682
  })
1593
- }));
1594
- if (unwrapResult._tag === "Failure")
1595
- return false;
1683
+ );
1684
+ if (unwrapResult._tag === "Failure") return false;
1596
1685
  const rumor = unwrapResult.success;
1597
1686
  const dTag = rumor.tags.find((t) => t[0] === "d")?.[1];
1598
- if (!dTag)
1599
- return false;
1687
+ if (!dTag) return false;
1600
1688
  const removalNoticeOpt = parseRemovalNotice(rumor.content, dTag);
1601
1689
  if (Option5.isSome(removalNoticeOpt)) {
1602
- if (onRemoved)
1603
- onRemoved(removalNoticeOpt.value);
1690
+ if (onRemoved) onRemoved(removalNoticeOpt.value);
1604
1691
  return true;
1605
1692
  }
1606
1693
  const rotationDataOpt = parseRotationEvent(rumor.content, dTag);
1607
- if (Option5.isNone(rotationDataOpt))
1608
- return false;
1694
+ if (Option5.isNone(rotationDataOpt)) return false;
1609
1695
  const rotationData = rotationDataOpt.value;
1610
- if (epochStore.epochs.has(rotationData.epochId))
1611
- return false;
1612
- const epoch = createEpochKey(rotationData.epochId, rotationData.epochKey, rumor.pubkey || "", rotationData.parentEpoch);
1696
+ if (epochStore.epochs.has(rotationData.epochId)) return false;
1697
+ const epoch = createEpochKey(
1698
+ rotationData.epochId,
1699
+ rotationData.epochKey,
1700
+ rumor.pubkey || "",
1701
+ rotationData.parentEpoch
1702
+ );
1613
1703
  addEpoch(epochStore, epoch);
1614
1704
  epochStore.currentEpochId = epoch.id;
1615
1705
  let membersChanged = false;
@@ -1629,75 +1719,158 @@ function createSyncHandle(storage, giftWrapHandle, relay, publishQueue, syncStat
1629
1719
  membersChanged = true;
1630
1720
  }
1631
1721
  }
1632
- if (membersChanged && onMembersChanged)
1633
- onMembersChanged();
1722
+ if (membersChanged && onMembersChanged) onMembersChanged();
1634
1723
  yield* handle.addEpochSubscription(epoch.publicKey);
1635
1724
  return true;
1636
1725
  });
1637
- const subscribeAcrossRelays = (filter, onEvent) => Effect8.forEach(relayUrls, (url) => Effect8.gen(function* () {
1638
- yield* relay.subscribe(filter, url, (event) => {
1639
- forkHandled(onEvent(event));
1640
- }).pipe(Effect8.tapError((err) => Effect8.sync(() => onSyncError?.(err))), Effect8.ignore);
1641
- }), { discard: true });
1726
+ const subscribeAcrossRelays = (filter, onEvent) => Effect8.forEach(
1727
+ relayUrls,
1728
+ (url) => Effect8.gen(function* () {
1729
+ yield* relay.subscribe(filter, url, (event) => {
1730
+ forkHandled(onEvent(event));
1731
+ }).pipe(
1732
+ Effect8.tapError((err) => Effect8.sync(() => onSyncError?.(err))),
1733
+ Effect8.ignore
1734
+ );
1735
+ }),
1736
+ { discard: true }
1737
+ );
1642
1738
  const syncRelay = (url, pubKeys, changedCollections) => Effect8.gen(function* () {
1643
- const reconcileResult = yield* Effect8.result(reconcileWithRelay(storage, relay, url, Array.from(pubKeys)));
1739
+ yield* Effect8.logDebug("Syncing relay", { relay: url });
1740
+ const reconcileResult = yield* Effect8.result(
1741
+ reconcileWithRelay(storage, relay, url, Array.from(pubKeys))
1742
+ );
1644
1743
  if (reconcileResult._tag === "Failure") {
1645
1744
  onSyncError?.(reconcileResult.failure);
1646
1745
  return;
1647
1746
  }
1648
1747
  const { haveIds, needIds } = reconcileResult.success;
1748
+ yield* Effect8.logDebug("Relay reconciliation result", {
1749
+ relay: url,
1750
+ need: needIds.length,
1751
+ have: haveIds.length
1752
+ });
1649
1753
  if (needIds.length > 0) {
1650
- const fetched = yield* relay.fetchEvents(needIds, url).pipe(Effect8.tapError((err) => Effect8.sync(() => onSyncError?.(err))), Effect8.orElseSucceed(() => []));
1754
+ const fetched = yield* relay.fetchEvents(needIds, url).pipe(
1755
+ Effect8.tapError((err) => Effect8.sync(() => onSyncError?.(err))),
1756
+ Effect8.orElseSucceed(() => [])
1757
+ );
1651
1758
  const sorted = [...fetched].sort((a, b) => a.created_at - b.created_at);
1652
- yield* Effect8.forEach(sorted, (remoteGw) => Effect8.gen(function* () {
1653
- const collection2 = yield* processGiftWrap(remoteGw).pipe(Effect8.orElseSucceed(() => null));
1654
- if (collection2)
1655
- changedCollections.add(collection2);
1656
- }), { discard: true });
1759
+ yield* Effect8.forEach(
1760
+ sorted,
1761
+ (remoteGw) => Effect8.gen(function* () {
1762
+ const collection2 = yield* processGiftWrap(remoteGw).pipe(
1763
+ Effect8.orElseSucceed(() => null)
1764
+ );
1765
+ if (collection2) changedCollections.add(collection2);
1766
+ }),
1767
+ { discard: true }
1768
+ );
1657
1769
  }
1658
1770
  if (haveIds.length > 0) {
1659
- yield* Effect8.forEach(haveIds, (id) => Effect8.gen(function* () {
1660
- const gw = yield* storage.getGiftWrap(id);
1661
- if (!gw?.event)
1662
- return;
1663
- yield* relay.publish(gw.event, [url]).pipe(Effect8.andThen(storage.stripGiftWrapBlob(id)), Effect8.tapError((err) => Effect8.sync(() => onSyncError?.(err))), Effect8.ignore);
1664
- }), { discard: true });
1665
- }
1666
- });
1771
+ yield* Effect8.forEach(
1772
+ haveIds,
1773
+ (id) => Effect8.gen(function* () {
1774
+ const gw = yield* storage.getGiftWrap(id);
1775
+ if (!gw?.event) return;
1776
+ yield* relay.publish(gw.event, [url]).pipe(
1777
+ Effect8.tapError((err) => Effect8.sync(() => onSyncError?.(err))),
1778
+ Effect8.ignore
1779
+ );
1780
+ }),
1781
+ { discard: true }
1782
+ );
1783
+ }
1784
+ }).pipe(Effect8.withLogSpan("tablinum.syncRelay"));
1785
+ let healingActive = false;
1786
+ const healingEffect = Effect8.gen(function* () {
1787
+ if (!healingActive) return;
1788
+ const status = yield* syncStatus.get();
1789
+ if (status === "syncing") return;
1790
+ yield* syncStatus.set("syncing");
1791
+ yield* Effect8.gen(function* () {
1792
+ const pubKeys = getSubscriptionPubKeys();
1793
+ const changedCollections = /* @__PURE__ */ new Set();
1794
+ yield* Effect8.forEach(relayUrls, (url) => syncRelay(url, pubKeys, changedCollections), {
1795
+ discard: true
1796
+ });
1797
+ if (changedCollections.size > 0) {
1798
+ yield* notifyReplayComplete(watchCtx, [...changedCollections]);
1799
+ }
1800
+ }).pipe(Effect8.ensuring(syncStatus.set("idle")));
1801
+ }).pipe(Effect8.ignore);
1667
1802
  const handle = {
1668
1803
  sync: () => Effect8.gen(function* () {
1804
+ yield* Effect8.logInfo("Sync started");
1669
1805
  yield* syncStatus.set("syncing");
1670
1806
  yield* Ref3.set(watchCtx.replayingRef, true);
1671
- const changedCollections = new Set;
1807
+ const changedCollections = /* @__PURE__ */ new Set();
1672
1808
  yield* Effect8.gen(function* () {
1673
1809
  const pubKeys = getSubscriptionPubKeys();
1674
1810
  yield* Effect8.forEach(relayUrls, (url) => syncRelay(url, pubKeys, changedCollections), {
1675
1811
  discard: true
1676
1812
  });
1677
1813
  yield* publishQueue.flush(relayUrls).pipe(Effect8.ignore);
1678
- }).pipe(Effect8.ensuring(Effect8.gen(function* () {
1679
- yield* notifyReplayComplete(watchCtx, [...changedCollections]);
1680
- yield* syncStatus.set("idle");
1681
- })));
1682
- }),
1814
+ }).pipe(
1815
+ Effect8.ensuring(
1816
+ Effect8.gen(function* () {
1817
+ yield* notifyReplayComplete(watchCtx, [...changedCollections]);
1818
+ yield* syncStatus.set("idle");
1819
+ })
1820
+ )
1821
+ );
1822
+ yield* Effect8.logInfo("Sync complete", { changed: [...changedCollections] });
1823
+ }).pipe(Effect8.withLogSpan("tablinum.sync")),
1683
1824
  publishLocal: (giftWrap) => Effect8.gen(function* () {
1684
- if (!giftWrap.event)
1685
- return;
1686
- yield* relay.publish(giftWrap.event, relayUrls).pipe(Effect8.tapError(() => storage.putGiftWrap(giftWrap).pipe(Effect8.andThen(publishQueue.enqueue(giftWrap.id)), Effect8.andThen(Effect8.sync(() => scheduleAutoFlush())))), Effect8.tapError((err) => Effect8.sync(() => onSyncError?.(err))), Effect8.ignore);
1825
+ if (!giftWrap.event) return;
1826
+ yield* relay.publish(giftWrap.event, relayUrls).pipe(
1827
+ Effect8.tapError(
1828
+ () => storage.putGiftWrap(giftWrap).pipe(
1829
+ Effect8.andThen(publishQueue.enqueue(giftWrap.id)),
1830
+ Effect8.andThen(Effect8.sync(() => scheduleAutoFlush()))
1831
+ )
1832
+ ),
1833
+ Effect8.tapError((err) => Effect8.sync(() => onSyncError?.(err))),
1834
+ Effect8.ignore
1835
+ );
1687
1836
  }),
1688
1837
  startSubscription: () => Effect8.gen(function* () {
1689
1838
  const pubKeys = getSubscriptionPubKeys();
1690
1839
  yield* subscribeAcrossRelays({ kinds: [GiftWrap2], "#p": pubKeys }, processRealtimeGiftWrap);
1691
1840
  if (!pubKeys.includes(personalPublicKey)) {
1692
- yield* subscribeAcrossRelays({ kinds: [GiftWrap2], "#p": [personalPublicKey] }, (event) => Effect8.result(processRotationGiftWrap(event)).pipe(Effect8.asVoid));
1841
+ yield* subscribeAcrossRelays(
1842
+ { kinds: [GiftWrap2], "#p": [personalPublicKey] },
1843
+ (event) => Effect8.result(processRotationGiftWrap(event)).pipe(Effect8.asVoid)
1844
+ );
1693
1845
  }
1694
1846
  }),
1695
- addEpochSubscription: (publicKey) => subscribeAcrossRelays({ kinds: [GiftWrap2], "#p": [publicKey] }, processRealtimeGiftWrap)
1847
+ addEpochSubscription: (publicKey) => subscribeAcrossRelays({ kinds: [GiftWrap2], "#p": [publicKey] }, processRealtimeGiftWrap),
1848
+ startHealing: () => {
1849
+ if (healingActive) return;
1850
+ healingActive = true;
1851
+ forkHandled(
1852
+ Effect8.sleep(Duration.minutes(5)).pipe(
1853
+ Effect8.andThen(healingEffect),
1854
+ Effect8.repeat(Schedule.spaced(Duration.minutes(5))),
1855
+ Effect8.ensuring(Effect8.sync(() => {
1856
+ healingActive = false;
1857
+ }))
1858
+ )
1859
+ );
1860
+ },
1861
+ stopHealing: () => {
1862
+ healingActive = false;
1863
+ }
1696
1864
  };
1697
- forkHandled(publishQueue.size().pipe(Effect8.flatMap((size) => Effect8.sync(() => {
1698
- if (size > 0)
1699
- scheduleAutoFlush();
1700
- }))));
1865
+ forkHandled(
1866
+ publishQueue.size().pipe(
1867
+ Effect8.flatMap(
1868
+ (size) => Effect8.sync(() => {
1869
+ if (size > 0) scheduleAutoFlush();
1870
+ })
1871
+ )
1872
+ )
1873
+ );
1701
1874
  return handle;
1702
1875
  }
1703
1876
 
@@ -1753,9 +1926,14 @@ var decodeAuthorProfile = Schema5.decodeUnknownEffect(Schema5.fromJsonString(Aut
1753
1926
  function fetchAuthorProfile(relay, relayUrls, pubkey) {
1754
1927
  return Effect9.gen(function* () {
1755
1928
  for (const url of relayUrls) {
1756
- const result = yield* Effect9.result(relay.fetchByFilter({ kinds: [0], authors: [pubkey], limit: 1 }, url));
1929
+ const result = yield* Effect9.result(
1930
+ relay.fetchByFilter({ kinds: [0], authors: [pubkey], limit: 1 }, url)
1931
+ );
1757
1932
  if (result._tag === "Success" && result.success.length > 0) {
1758
- return yield* decodeAuthorProfile(result.success[0].content).pipe(Effect9.map(Option6.some), Effect9.orElseSucceed(() => Option6.none()));
1933
+ return yield* decodeAuthorProfile(result.success[0].content).pipe(
1934
+ Effect9.map(Option6.some),
1935
+ Effect9.orElseSucceed(() => Option6.none())
1936
+ );
1759
1937
  }
1760
1938
  }
1761
1939
  return Option6.none();
@@ -1764,48 +1942,47 @@ function fetchAuthorProfile(relay, relayUrls, pubkey) {
1764
1942
 
1765
1943
  // src/services/Identity.ts
1766
1944
  import { ServiceMap as ServiceMap3 } from "effect";
1767
-
1768
- class Identity extends ServiceMap3.Service()("tablinum/Identity") {
1769
- }
1945
+ var Identity = class extends ServiceMap3.Service()("tablinum/Identity") {
1946
+ };
1770
1947
 
1771
1948
  // src/services/EpochStore.ts
1772
1949
  import { ServiceMap as ServiceMap4 } from "effect";
1773
-
1774
- class EpochStore extends ServiceMap4.Service()("tablinum/EpochStore") {
1775
- }
1950
+ var EpochStore = class extends ServiceMap4.Service()(
1951
+ "tablinum/EpochStore"
1952
+ ) {
1953
+ };
1776
1954
 
1777
1955
  // src/services/Storage.ts
1778
1956
  import { ServiceMap as ServiceMap5 } from "effect";
1779
-
1780
- class Storage extends ServiceMap5.Service()("tablinum/Storage") {
1781
- }
1957
+ var Storage = class extends ServiceMap5.Service()("tablinum/Storage") {
1958
+ };
1782
1959
 
1783
1960
  // src/services/Relay.ts
1784
1961
  import { ServiceMap as ServiceMap6 } from "effect";
1785
-
1786
- class Relay extends ServiceMap6.Service()("tablinum/Relay") {
1787
- }
1962
+ var Relay = class extends ServiceMap6.Service()("tablinum/Relay") {
1963
+ };
1788
1964
 
1789
1965
  // src/services/GiftWrap.ts
1790
1966
  import { ServiceMap as ServiceMap7 } from "effect";
1791
-
1792
- class GiftWrap3 extends ServiceMap7.Service()("tablinum/GiftWrap") {
1793
- }
1967
+ var GiftWrap3 = class extends ServiceMap7.Service()("tablinum/GiftWrap") {
1968
+ };
1794
1969
 
1795
1970
  // src/services/PublishQueue.ts
1796
1971
  import { ServiceMap as ServiceMap8 } from "effect";
1797
-
1798
- class PublishQueue extends ServiceMap8.Service()("tablinum/PublishQueue") {
1799
- }
1972
+ var PublishQueue = class extends ServiceMap8.Service()(
1973
+ "tablinum/PublishQueue"
1974
+ ) {
1975
+ };
1800
1976
 
1801
1977
  // src/services/SyncStatus.ts
1802
1978
  import { ServiceMap as ServiceMap9 } from "effect";
1803
-
1804
- class SyncStatus extends ServiceMap9.Service()("tablinum/SyncStatus") {
1805
- }
1979
+ var SyncStatus = class extends ServiceMap9.Service()(
1980
+ "tablinum/SyncStatus"
1981
+ ) {
1982
+ };
1806
1983
 
1807
1984
  // src/layers/IdentityLive.ts
1808
- import { Effect as Effect11, Layer } from "effect";
1985
+ import { Effect as Effect11, Layer as Layer2 } from "effect";
1809
1986
  import { hexToBytes as hexToBytes3 } from "@noble/hashes/utils.js";
1810
1987
 
1811
1988
  // src/db/identity.ts
@@ -1843,47 +2020,128 @@ function createIdentity(suppliedKey) {
1843
2020
  }
1844
2021
 
1845
2022
  // src/layers/IdentityLive.ts
1846
- var IdentityLive = Layer.effect(Identity, Effect11.gen(function* () {
1847
- const config = yield* Config;
1848
- const storage = yield* Storage;
1849
- const idbKey = yield* storage.getMeta("identity_key");
1850
- const resolvedKey = config.privateKey ?? (typeof idbKey === "string" && idbKey.length === 64 ? hexToBytes3(idbKey) : undefined);
1851
- const identity = yield* createIdentity(resolvedKey);
1852
- yield* storage.putMeta("identity_key", identity.exportKey());
1853
- return identity;
1854
- }));
2023
+ var IdentityLive = Layer2.effect(
2024
+ Identity,
2025
+ Effect11.gen(function* () {
2026
+ const config = yield* Config;
2027
+ const storage = yield* Storage;
2028
+ const idbKey = yield* storage.getMeta("identity_key");
2029
+ const resolvedKey = config.privateKey ?? (typeof idbKey === "string" && idbKey.length === 64 ? hexToBytes3(idbKey) : void 0);
2030
+ const identity = yield* createIdentity(resolvedKey);
2031
+ yield* storage.putMeta("identity_key", identity.exportKey());
2032
+ yield* Effect11.logInfo("Identity loaded", {
2033
+ publicKey: identity.publicKey.slice(0, 12) + "...",
2034
+ source: config.privateKey ? "config" : resolvedKey ? "storage" : "generated"
2035
+ });
2036
+ return identity;
2037
+ })
2038
+ );
1855
2039
 
1856
2040
  // src/layers/EpochStoreLive.ts
1857
- import { Effect as Effect12, Layer as Layer2, Option as Option7 } from "effect";
2041
+ import { Effect as Effect12, Layer as Layer3, Option as Option7 } from "effect";
1858
2042
  import { generateSecretKey as generateSecretKey2 } from "nostr-tools/pure";
1859
2043
  import { bytesToHex as bytesToHex3 } from "@noble/hashes/utils.js";
1860
- var EpochStoreLive = Layer2.effect(EpochStore, Effect12.gen(function* () {
1861
- const config = yield* Config;
1862
- const identity = yield* Identity;
1863
- const storage = yield* Storage;
1864
- const idbRaw = yield* storage.getMeta("epochs");
1865
- if (typeof idbRaw === "string") {
1866
- const idbStore = deserializeEpochStore(idbRaw);
1867
- if (Option7.isSome(idbStore)) {
1868
- return idbStore.value;
1869
- }
1870
- }
1871
- if (config.epochKeys && config.epochKeys.length > 0) {
1872
- const store2 = createEpochStoreFromInputs(config.epochKeys);
1873
- yield* storage.putMeta("epochs", stringifyEpochStore(store2));
1874
- return store2;
1875
- }
1876
- const store = createEpochStoreFromInputs([{ epochId: EpochId("epoch-0"), key: bytesToHex3(generateSecretKey2()) }], { createdBy: identity.publicKey });
1877
- yield* storage.putMeta("epochs", stringifyEpochStore(store));
1878
- return store;
1879
- }));
2044
+ var EpochStoreLive = Layer3.effect(
2045
+ EpochStore,
2046
+ Effect12.gen(function* () {
2047
+ const config = yield* Config;
2048
+ const identity = yield* Identity;
2049
+ const storage = yield* Storage;
2050
+ const idbRaw = yield* storage.getMeta("epochs");
2051
+ if (typeof idbRaw === "string") {
2052
+ const idbStore = deserializeEpochStore(idbRaw);
2053
+ if (Option7.isSome(idbStore)) {
2054
+ yield* Effect12.logInfo("Epoch store loaded", {
2055
+ source: "storage",
2056
+ epochs: idbStore.value.epochs.size
2057
+ });
2058
+ return idbStore.value;
2059
+ }
2060
+ }
2061
+ if (config.epochKeys && config.epochKeys.length > 0) {
2062
+ const store2 = createEpochStoreFromInputs(config.epochKeys);
2063
+ yield* storage.putMeta("epochs", stringifyEpochStore(store2));
2064
+ yield* Effect12.logInfo("Epoch store loaded", { source: "config", epochs: store2.epochs.size });
2065
+ return store2;
2066
+ }
2067
+ const store = createEpochStoreFromInputs(
2068
+ [{ epochId: EpochId("epoch-0"), key: bytesToHex3(generateSecretKey2()) }],
2069
+ { createdBy: identity.publicKey }
2070
+ );
2071
+ yield* storage.putMeta("epochs", stringifyEpochStore(store));
2072
+ yield* Effect12.logInfo("Epoch store loaded", { source: "generated", epochs: store.epochs.size });
2073
+ return store;
2074
+ })
2075
+ );
1880
2076
 
1881
2077
  // src/layers/StorageLive.ts
1882
- import { Effect as Effect14, Layer as Layer3 } from "effect";
2078
+ import { Effect as Effect14, Layer as Layer4 } from "effect";
1883
2079
 
1884
2080
  // src/storage/idb.ts
1885
2081
  import { Effect as Effect13 } from "effect";
1886
2082
  import { openDB } from "idb";
2083
+
2084
+ // src/sync/compact-event.ts
2085
+ import { bytesToHex as bytesToHex4, hexToBytes as hexToBytes4 } from "@noble/hashes/utils.js";
2086
+ var VERSION = 1;
2087
+ var HEADER_SIZE = 133;
2088
+ function base64ToBytes(base64) {
2089
+ const binary = atob(base64);
2090
+ const bytes = new Uint8Array(binary.length);
2091
+ for (let i = 0; i < binary.length; i++) {
2092
+ bytes[i] = binary.charCodeAt(i);
2093
+ }
2094
+ return bytes;
2095
+ }
2096
+ function bytesToBase64(bytes) {
2097
+ let binary = "";
2098
+ for (let i = 0; i < bytes.length; i++) {
2099
+ binary += String.fromCharCode(bytes[i]);
2100
+ }
2101
+ return btoa(binary);
2102
+ }
2103
+ function packEvent(event) {
2104
+ const pubkey = hexToBytes4(event.pubkey);
2105
+ const sig = hexToBytes4(event.sig);
2106
+ const recipientTag = event.tags.find((t) => t[0] === "p");
2107
+ if (!recipientTag) throw new Error("Gift wrap missing #p tag");
2108
+ if (event.tags.some((t) => t[0] !== "p")) {
2109
+ throw new Error("Gift wrap has unexpected non-p tags; compact encoding would lose them");
2110
+ }
2111
+ const recipient = hexToBytes4(recipientTag[1]);
2112
+ const createdAtBuf = new Uint8Array(4);
2113
+ new DataView(createdAtBuf.buffer).setUint32(0, event.created_at, false);
2114
+ const content = base64ToBytes(event.content);
2115
+ const result = new Uint8Array(HEADER_SIZE + content.length);
2116
+ result[0] = VERSION;
2117
+ result.set(pubkey, 1);
2118
+ result.set(sig, 33);
2119
+ result.set(recipient, 97);
2120
+ result.set(createdAtBuf, 129);
2121
+ result.set(content, HEADER_SIZE);
2122
+ return result;
2123
+ }
2124
+ function unpackEvent(id, compact) {
2125
+ const version = compact[0];
2126
+ if (version !== VERSION) throw new Error(`Unknown compact event version: ${version}`);
2127
+ const pubkey = bytesToHex4(compact.slice(1, 33));
2128
+ const sig = bytesToHex4(compact.slice(33, 97));
2129
+ const recipient = bytesToHex4(compact.slice(97, 129));
2130
+ const dv = new DataView(compact.buffer, compact.byteOffset + 129, 4);
2131
+ const createdAt = dv.getUint32(0, false);
2132
+ const content = bytesToBase64(compact.slice(HEADER_SIZE));
2133
+ return {
2134
+ id,
2135
+ pubkey,
2136
+ sig,
2137
+ created_at: createdAt,
2138
+ kind: 1059,
2139
+ tags: [["p", recipient]],
2140
+ content
2141
+ };
2142
+ }
2143
+
2144
+ // src/storage/idb.ts
1887
2145
  var DB_NAME = "tablinum";
1888
2146
  function storeName(collection2) {
1889
2147
  return `col_${collection2}`;
@@ -1914,7 +2172,7 @@ function upgradeSchema(database, schema, tx) {
1914
2172
  if (!database.objectStoreNames.contains("giftwraps")) {
1915
2173
  database.createObjectStore("giftwraps", { keyPath: "id" });
1916
2174
  }
1917
- const expectedStores = new Set;
2175
+ const expectedStores = /* @__PURE__ */ new Set();
1918
2176
  for (const [, def] of Object.entries(schema)) {
1919
2177
  const sn = storeName(def.name);
1920
2178
  expectedStores.add(sn);
@@ -1928,12 +2186,10 @@ function upgradeSchema(database, schema, tx) {
1928
2186
  const existingIndices = new Set(Array.from(store.indexNames));
1929
2187
  const wantedIndices = new Set(def.indices ?? []);
1930
2188
  for (const idx of existingIndices) {
1931
- if (!wantedIndices.has(idx))
1932
- store.deleteIndex(idx);
2189
+ if (!wantedIndices.has(idx)) store.deleteIndex(idx);
1933
2190
  }
1934
2191
  for (const idx of wantedIndices) {
1935
- if (!existingIndices.has(idx))
1936
- store.createIndex(idx, idx);
2192
+ if (!existingIndices.has(idx)) store.createIndex(idx, idx);
1937
2193
  }
1938
2194
  }
1939
2195
  }
@@ -1948,6 +2204,13 @@ function openIDBStorage(dbName, schema) {
1948
2204
  return Effect13.gen(function* () {
1949
2205
  const name = dbName ?? DB_NAME;
1950
2206
  const schemaSig = computeSchemaSig(schema);
2207
+ if (typeof indexedDB === "undefined") {
2208
+ return yield* Effect13.fail(
2209
+ new StorageError({
2210
+ message: "IndexedDB is not available in this environment"
2211
+ })
2212
+ );
2213
+ }
1951
2214
  const probeDb = yield* Effect13.tryPromise({
1952
2215
  try: () => openDB(name),
1953
2216
  catch: (e) => new StorageError({ message: "Failed to open IndexedDB", cause: e })
@@ -1958,7 +2221,7 @@ function openIDBStorage(dbName, schema) {
1958
2221
  const storedSig = yield* Effect13.tryPromise({
1959
2222
  try: () => probeDb.get("_meta", "schema_sig"),
1960
2223
  catch: () => new StorageError({ message: "Failed to read schema meta" })
1961
- }).pipe(Effect13.catch(() => Effect13.succeed(undefined)));
2224
+ }).pipe(Effect13.catch(() => Effect13.succeed(void 0)));
1962
2225
  needsUpgrade = storedSig !== schemaSig;
1963
2226
  }
1964
2227
  probeDb.close();
@@ -1975,9 +2238,7 @@ function openIDBStorage(dbName, schema) {
1975
2238
  });
1976
2239
  yield* Effect13.addFinalizer(() => Effect13.sync(() => db.close()));
1977
2240
  const handle = {
1978
- putRecord: (collection2, record) => wrap("putRecord", () => db.put(storeName(collection2), record).then(() => {
1979
- return;
1980
- })),
2241
+ putRecord: (collection2, record) => wrap("putRecord", () => db.put(storeName(collection2), record).then(() => void 0)),
1981
2242
  getRecord: (collection2, id) => wrap("getRecord", () => db.get(storeName(collection2), id)),
1982
2243
  getAllRecords: (collection2) => wrap("getAllRecords", () => db.getAll(storeName(collection2))),
1983
2244
  countRecords: (collection2) => wrap("countRecords", () => db.count(storeName(collection2))),
@@ -1997,29 +2258,52 @@ function openIDBStorage(dbName, schema) {
1997
2258
  }
1998
2259
  return results;
1999
2260
  }),
2000
- putEvent: (event) => wrap("putEvent", () => db.put("events", event).then(() => {
2001
- return;
2002
- })),
2261
+ putEvent: (event) => wrap("putEvent", () => db.put("events", event).then(() => void 0)),
2003
2262
  getEvent: (id) => wrap("getEvent", () => db.get("events", id)),
2004
2263
  getAllEvents: () => wrap("getAllEvents", () => db.getAll("events")),
2005
- getEventsByRecord: (collection2, recordId) => wrap("getEventsByRecord", () => db.getAllFromIndex("events", "by-record", [collection2, recordId])),
2006
- putGiftWrap: (gw) => wrap("putGiftWrap", () => db.put("giftwraps", gw).then(() => {
2007
- return;
2008
- })),
2009
- getGiftWrap: (id) => wrap("getGiftWrap", () => db.get("giftwraps", id)),
2010
- getAllGiftWraps: () => wrap("getAllGiftWraps", () => db.getAll("giftwraps")),
2011
- deleteGiftWrap: (id) => wrap("deleteGiftWrap", () => db.delete("giftwraps", id).then(() => {
2012
- return;
2013
- })),
2014
- stripGiftWrapBlob: (id) => wrap("stripGiftWrapBlob", async () => {
2015
- const existing = await db.get("giftwraps", id);
2016
- if (existing) {
2017
- await db.put("giftwraps", { id: existing.id, createdAt: existing.createdAt });
2264
+ getEventsByRecord: (collection2, recordId) => wrap(
2265
+ "getEventsByRecord",
2266
+ () => db.getAllFromIndex("events", "by-record", [collection2, recordId])
2267
+ ),
2268
+ putGiftWrap: (gw) => wrap("putGiftWrap", async () => {
2269
+ if (gw.event) {
2270
+ const compact = packEvent(gw.event);
2271
+ await db.put("giftwraps", { id: gw.id, compact, createdAt: gw.createdAt });
2272
+ } else {
2273
+ await db.put("giftwraps", { id: gw.id, createdAt: gw.createdAt });
2018
2274
  }
2019
2275
  }),
2020
- deleteEvent: (id) => wrap("deleteEvent", () => db.delete("events", id).then(() => {
2021
- return;
2022
- })),
2276
+ getGiftWrap: (id) => wrap("getGiftWrap", async () => {
2277
+ const raw = await db.get("giftwraps", id);
2278
+ if (!raw) return void 0;
2279
+ if (raw.compact) {
2280
+ return { id: raw.id, event: unpackEvent(raw.id, raw.compact), createdAt: raw.createdAt };
2281
+ }
2282
+ if (raw.event) {
2283
+ const compact = packEvent(raw.event);
2284
+ await db.put("giftwraps", { id: raw.id, compact, createdAt: raw.createdAt });
2285
+ return { id: raw.id, event: raw.event, createdAt: raw.createdAt };
2286
+ }
2287
+ return { id: raw.id, createdAt: raw.createdAt };
2288
+ }),
2289
+ getAllGiftWraps: () => wrap("getAllGiftWraps", async () => {
2290
+ const raws = await db.getAll("giftwraps");
2291
+ const results = [];
2292
+ for (const raw of raws) {
2293
+ if (raw.compact) {
2294
+ results.push({ id: raw.id, event: unpackEvent(raw.id, raw.compact), createdAt: raw.createdAt });
2295
+ } else if (raw.event) {
2296
+ const compact = packEvent(raw.event);
2297
+ await db.put("giftwraps", { id: raw.id, compact, createdAt: raw.createdAt });
2298
+ results.push({ id: raw.id, event: raw.event, createdAt: raw.createdAt });
2299
+ } else {
2300
+ results.push({ id: raw.id, createdAt: raw.createdAt });
2301
+ }
2302
+ }
2303
+ return results;
2304
+ }),
2305
+ deleteGiftWrap: (id) => wrap("deleteGiftWrap", () => db.delete("giftwraps", id).then(() => void 0)),
2306
+ deleteEvent: (id) => wrap("deleteEvent", () => db.delete("events", id).then(() => void 0)),
2023
2307
  stripEventData: (id) => wrap("stripEventData", async () => {
2024
2308
  const existing = await db.get("events", id);
2025
2309
  if (existing) {
@@ -2027,9 +2311,7 @@ function openIDBStorage(dbName, schema) {
2027
2311
  }
2028
2312
  }),
2029
2313
  getMeta: (key) => wrap("getMeta", () => db.get("_meta", key)),
2030
- putMeta: (key, value) => wrap("putMeta", () => db.put("_meta", value, key).then(() => {
2031
- return;
2032
- })),
2314
+ putMeta: (key, value) => wrap("putMeta", () => db.put("_meta", value, key).then(() => void 0)),
2033
2315
  close: () => Effect13.sync(() => db.close())
2034
2316
  };
2035
2317
  return handle;
@@ -2037,16 +2319,21 @@ function openIDBStorage(dbName, schema) {
2037
2319
  }
2038
2320
 
2039
2321
  // src/layers/StorageLive.ts
2040
- var StorageLive = Layer3.effect(Storage, Effect14.gen(function* () {
2041
- const config = yield* Config;
2042
- return yield* openIDBStorage(config.dbName, {
2043
- ...config.schema,
2044
- _members: membersCollectionDef
2045
- });
2046
- }));
2322
+ var StorageLive = Layer4.effect(
2323
+ Storage,
2324
+ Effect14.gen(function* () {
2325
+ const config = yield* Config;
2326
+ const handle = yield* openIDBStorage(config.dbName, {
2327
+ ...config.schema,
2328
+ _members: membersCollectionDef
2329
+ });
2330
+ yield* Effect14.logInfo("Storage opened", { dbName: config.dbName });
2331
+ return handle;
2332
+ })
2333
+ );
2047
2334
 
2048
2335
  // src/layers/RelayLive.ts
2049
- import { Layer as Layer4 } from "effect";
2336
+ import { Layer as Layer5 } from "effect";
2050
2337
 
2051
2338
  // src/sync/relay.ts
2052
2339
  import { Effect as Effect15, Option as Option8, Schema as Schema6, ScopedCache, Scope as Scope3 } from "effect";
@@ -2057,32 +2344,41 @@ var NegMessageFrameSchema = Schema6.Tuple([
2057
2344
  Schema6.String
2058
2345
  ]);
2059
2346
  var NegErrorFrameSchema = Schema6.Tuple([Schema6.Literal("NEG-ERR"), Schema6.String, Schema6.String]);
2060
- var decodeNegFrame = Schema6.decodeUnknownEffect(Schema6.fromJsonString(Schema6.Union([NegMessageFrameSchema, NegErrorFrameSchema])));
2347
+ var decodeNegFrame = Schema6.decodeUnknownEffect(
2348
+ Schema6.fromJsonString(Schema6.Union([NegMessageFrameSchema, NegErrorFrameSchema]))
2349
+ );
2061
2350
  function parseNegMessageFrame(data) {
2062
- return Effect15.runSync(decodeNegFrame(data).pipe(Effect15.map(Option8.some), Effect15.orElseSucceed(() => Option8.none())));
2351
+ return Effect15.runSync(
2352
+ decodeNegFrame(data).pipe(
2353
+ Effect15.map(Option8.some),
2354
+ Effect15.orElseSucceed(() => Option8.none())
2355
+ )
2356
+ );
2063
2357
  }
2064
2358
  function createRelayHandle() {
2065
2359
  return Effect15.gen(function* () {
2066
2360
  const relayScope = yield* Effect15.scope;
2067
2361
  const connections = yield* ScopedCache.make({
2068
2362
  capacity: 64,
2069
- lookup: (url) => Effect15.acquireRelease(Effect15.tryPromise({
2070
- try: () => Relay2.connect(url),
2071
- catch: (e) => new RelayError({
2072
- message: `Connect to ${url} failed: ${e instanceof Error ? e.message : String(e)}`,
2073
- url,
2074
- cause: e
2363
+ lookup: (url) => Effect15.acquireRelease(
2364
+ Effect15.tryPromise({
2365
+ try: () => Relay2.connect(url),
2366
+ catch: (e) => new RelayError({
2367
+ message: `Connect to ${url} failed: ${e instanceof Error ? e.message : String(e)}`,
2368
+ url,
2369
+ cause: e
2370
+ })
2371
+ }),
2372
+ (relay) => Effect15.sync(() => {
2373
+ relay.close();
2075
2374
  })
2076
- }), (relay) => Effect15.sync(() => {
2077
- relay.close();
2078
- }))
2375
+ )
2079
2376
  });
2080
- const connectedUrls = new Set;
2081
- const statusListeners = new Set;
2377
+ const connectedUrls = /* @__PURE__ */ new Set();
2378
+ const statusListeners = /* @__PURE__ */ new Set();
2082
2379
  const notifyStatus = () => {
2083
2380
  const status = { connectedUrls: [...connectedUrls] };
2084
- for (const listener of statusListeners)
2085
- listener(status);
2381
+ for (const listener of statusListeners) listener(status);
2086
2382
  };
2087
2383
  const markConnected = (url) => {
2088
2384
  if (!connectedUrls.has(url)) {
@@ -2096,66 +2392,98 @@ function createRelayHandle() {
2096
2392
  notifyStatus();
2097
2393
  }
2098
2394
  };
2099
- const getRelay = (url) => ScopedCache.get(connections, url).pipe(Effect15.flatMap((relay) => relay.connected === false ? ScopedCache.invalidate(connections, url).pipe(Effect15.andThen(ScopedCache.get(connections, url))) : Effect15.succeed(relay)));
2100
- const withRelay = (url, run) => getRelay(url).pipe(Effect15.tap(() => Effect15.sync(() => markConnected(url))), Effect15.flatMap((relay) => run(relay)), Effect15.tapError(() => ScopedCache.invalidate(connections, url).pipe(Effect15.tap(() => Effect15.sync(() => markDisconnected(url))))));
2101
- const collectEvents = (url, filters) => withRelay(url, (relay) => Effect15.callback((resume) => {
2102
- const events = [];
2103
- let settled = false;
2104
- let timer;
2105
- let sub;
2106
- const cleanup = () => {
2107
- settled = true;
2108
- if (timer !== undefined) {
2109
- clearTimeout(timer);
2110
- timer = undefined;
2111
- }
2112
- sub?.close();
2113
- sub = undefined;
2114
- };
2115
- const fail = (e) => resume(Effect15.fail(new RelayError({
2116
- message: `Fetch from ${url} failed: ${e instanceof Error ? e.message : String(e)}`,
2117
- url,
2118
- cause: e
2119
- })));
2120
- try {
2121
- sub = relay.subscribe([...filters], {
2122
- onevent(evt) {
2123
- if (!settled) {
2124
- events.push(evt);
2395
+ const getRelay = (url) => ScopedCache.get(connections, url).pipe(
2396
+ Effect15.flatMap(
2397
+ (relay) => relay.connected === false ? ScopedCache.invalidate(connections, url).pipe(
2398
+ Effect15.andThen(ScopedCache.get(connections, url))
2399
+ ) : Effect15.succeed(relay)
2400
+ )
2401
+ );
2402
+ const withRelay = (url, run) => getRelay(url).pipe(
2403
+ Effect15.tap(() => Effect15.sync(() => markConnected(url))),
2404
+ Effect15.flatMap((relay) => run(relay)),
2405
+ Effect15.tapError(
2406
+ () => ScopedCache.invalidate(connections, url).pipe(
2407
+ Effect15.tap(() => Effect15.sync(() => markDisconnected(url)))
2408
+ )
2409
+ )
2410
+ );
2411
+ const collectEvents = (url, filters) => withRelay(
2412
+ url,
2413
+ (relay) => Effect15.callback((resume) => {
2414
+ const events = [];
2415
+ let settled = false;
2416
+ let timer;
2417
+ let sub;
2418
+ const cleanup = () => {
2419
+ settled = true;
2420
+ if (timer !== void 0) {
2421
+ clearTimeout(timer);
2422
+ timer = void 0;
2423
+ }
2424
+ sub?.close();
2425
+ sub = void 0;
2426
+ };
2427
+ const fail = (e) => resume(
2428
+ Effect15.fail(
2429
+ new RelayError({
2430
+ message: `Fetch from ${url} failed: ${e instanceof Error ? e.message : String(e)}`,
2431
+ url,
2432
+ cause: e
2433
+ })
2434
+ )
2435
+ );
2436
+ try {
2437
+ sub = relay.subscribe([...filters], {
2438
+ onevent(evt) {
2439
+ if (!settled) {
2440
+ events.push(evt);
2441
+ }
2442
+ },
2443
+ oneose() {
2444
+ if (settled) return;
2445
+ cleanup();
2446
+ resume(Effect15.succeed(events));
2125
2447
  }
2126
- },
2127
- oneose() {
2128
- if (settled)
2129
- return;
2448
+ });
2449
+ timer = setTimeout(() => {
2450
+ if (settled) return;
2130
2451
  cleanup();
2131
2452
  resume(Effect15.succeed(events));
2132
- }
2133
- });
2134
- timer = setTimeout(() => {
2135
- if (settled)
2136
- return;
2453
+ }, 1e4);
2454
+ } catch (e) {
2137
2455
  cleanup();
2138
- resume(Effect15.succeed(events));
2139
- }, 1e4);
2140
- } catch (e) {
2141
- cleanup();
2142
- fail(e);
2143
- }
2144
- return Effect15.sync(cleanup);
2145
- }));
2456
+ fail(e);
2457
+ }
2458
+ return Effect15.sync(cleanup);
2459
+ })
2460
+ );
2146
2461
  return {
2147
2462
  publish: (event, urls) => Effect15.gen(function* () {
2148
- const results = yield* Effect15.forEach(urls, (url) => Effect15.result(withRelay(url, (relay) => Effect15.tryPromise({
2149
- try: () => relay.publish(event),
2150
- catch: (e) => new RelayError({
2151
- message: `Publish to ${url} failed: ${e instanceof Error ? e.message : String(e)}`,
2152
- url,
2153
- cause: e
2154
- })
2155
- }).pipe(Effect15.timeoutOrElse({
2156
- duration: "10 seconds",
2157
- onTimeout: () => Effect15.fail(new RelayError({ message: `Publish to ${url} timed out`, url }))
2158
- })))), { concurrency: "unbounded" });
2463
+ const results = yield* Effect15.forEach(
2464
+ urls,
2465
+ (url) => Effect15.result(
2466
+ withRelay(
2467
+ url,
2468
+ (relay) => Effect15.tryPromise({
2469
+ try: () => relay.publish(event),
2470
+ catch: (e) => new RelayError({
2471
+ message: `Publish to ${url} failed: ${e instanceof Error ? e.message : String(e)}`,
2472
+ url,
2473
+ cause: e
2474
+ })
2475
+ }).pipe(
2476
+ Effect15.timeoutOrElse({
2477
+ duration: "10 seconds",
2478
+ onTimeout: () => Effect15.fail(
2479
+ new RelayError({ message: `Publish to ${url} timed out`, url })
2480
+ )
2481
+ })
2482
+ )
2483
+ )
2484
+ ),
2485
+ { concurrency: "unbounded" }
2486
+ );
2159
2487
  const failures = results.filter((r) => r._tag === "Failure");
2160
2488
  if (failures.length === urls.length && urls.length > 0) {
2161
2489
  return yield* new RelayError({
@@ -2164,90 +2492,104 @@ function createRelayHandle() {
2164
2492
  }
2165
2493
  }),
2166
2494
  fetchEvents: (ids, url) => Effect15.gen(function* () {
2167
- if (ids.length === 0)
2168
- return [];
2495
+ if (ids.length === 0) return [];
2169
2496
  return yield* collectEvents(url, [{ ids }]);
2170
2497
  }),
2171
2498
  fetchByFilter: (filter, url) => collectEvents(url, [filter]),
2172
- subscribe: (filter, url, onEvent) => withRelay(url, (relay) => Effect15.acquireRelease(Effect15.try({
2173
- try: () => relay.subscribe([filter], {
2174
- onevent(evt) {
2175
- onEvent(evt);
2176
- },
2177
- oneose() {}
2178
- }),
2179
- catch: (e) => new RelayError({
2180
- message: `Subscribe to ${url} failed: ${e instanceof Error ? e.message : String(e)}`,
2181
- url,
2182
- cause: e
2183
- })
2184
- }), (sub) => Effect15.sync(() => {
2185
- sub.close();
2186
- })).pipe(Effect15.provideService(Scope3.Scope, relayScope), Effect15.asVoid)),
2187
- sendNegMsg: (url, subId, filter, msgHex) => withRelay(url, (relay) => Effect15.callback((resume) => {
2188
- let settled = false;
2189
- let timer;
2190
- let sub;
2191
- let ws;
2192
- const cleanup = () => {
2193
- settled = true;
2194
- if (timer !== undefined) {
2195
- clearTimeout(timer);
2196
- timer = undefined;
2197
- }
2198
- sub?.close();
2199
- sub = undefined;
2200
- ws?.removeEventListener("message", handler);
2201
- ws = undefined;
2202
- };
2203
- const fail = (e) => resume(Effect15.fail(new RelayError({
2204
- message: `NIP-77 negotiation with ${url} failed: ${e instanceof Error ? e.message : String(e)}`,
2205
- url,
2206
- cause: e
2207
- })));
2208
- const handler = (msg) => {
2209
- if (settled || typeof msg.data !== "string")
2210
- return;
2211
- const frameOpt = parseNegMessageFrame(msg.data);
2212
- if (Option8.isNone(frameOpt) || frameOpt.value[1] !== subId)
2213
- return;
2214
- const frame = frameOpt.value;
2215
- cleanup();
2216
- if (frame[0] === "NEG-MSG") {
2217
- resume(Effect15.succeed({
2218
- msgHex: frame[2],
2219
- haveIds: [],
2220
- needIds: []
2221
- }));
2222
- return;
2223
- }
2224
- fail(new Error(`NEG-ERR: ${frame[2]}`));
2225
- };
2226
- try {
2227
- sub = relay.subscribe([filter], {
2228
- onevent() {},
2229
- oneose() {}
2230
- });
2231
- ws = relay._ws || relay.ws;
2232
- if (!ws) {
2499
+ subscribe: (filter, url, onEvent) => withRelay(
2500
+ url,
2501
+ (relay) => Effect15.acquireRelease(
2502
+ Effect15.try({
2503
+ try: () => relay.subscribe([filter], {
2504
+ onevent(evt) {
2505
+ onEvent(evt);
2506
+ },
2507
+ oneose() {
2508
+ }
2509
+ }),
2510
+ catch: (e) => new RelayError({
2511
+ message: `Subscribe to ${url} failed: ${e instanceof Error ? e.message : String(e)}`,
2512
+ url,
2513
+ cause: e
2514
+ })
2515
+ }),
2516
+ (sub) => Effect15.sync(() => {
2517
+ sub.close();
2518
+ })
2519
+ ).pipe(Effect15.provideService(Scope3.Scope, relayScope), Effect15.asVoid)
2520
+ ),
2521
+ sendNegMsg: (url, subId, filter, msgHex) => withRelay(
2522
+ url,
2523
+ (relay) => Effect15.callback((resume) => {
2524
+ let settled = false;
2525
+ let timer;
2526
+ let sub;
2527
+ let ws;
2528
+ const cleanup = () => {
2529
+ settled = true;
2530
+ if (timer !== void 0) {
2531
+ clearTimeout(timer);
2532
+ timer = void 0;
2533
+ }
2534
+ sub?.close();
2535
+ sub = void 0;
2536
+ ws?.removeEventListener("message", handler);
2537
+ ws = void 0;
2538
+ };
2539
+ const fail = (e) => resume(
2540
+ Effect15.fail(
2541
+ new RelayError({
2542
+ message: `NIP-77 negotiation with ${url} failed: ${e instanceof Error ? e.message : String(e)}`,
2543
+ url,
2544
+ cause: e
2545
+ })
2546
+ )
2547
+ );
2548
+ const handler = (msg) => {
2549
+ if (settled || typeof msg.data !== "string") return;
2550
+ const frameOpt = parseNegMessageFrame(msg.data);
2551
+ if (Option8.isNone(frameOpt) || frameOpt.value[1] !== subId) return;
2552
+ const frame = frameOpt.value;
2233
2553
  cleanup();
2234
- fail(new Error("Cannot access relay WebSocket"));
2235
- return Effect15.succeed(undefined);
2236
- }
2237
- timer = setTimeout(() => {
2238
- if (settled)
2554
+ if (frame[0] === "NEG-MSG") {
2555
+ resume(
2556
+ Effect15.succeed({
2557
+ msgHex: frame[2],
2558
+ haveIds: [],
2559
+ needIds: []
2560
+ })
2561
+ );
2239
2562
  return;
2563
+ }
2564
+ fail(new Error(`NEG-ERR: ${frame[2]}`));
2565
+ };
2566
+ try {
2567
+ sub = relay.subscribe([filter], {
2568
+ onevent() {
2569
+ },
2570
+ oneose() {
2571
+ }
2572
+ });
2573
+ ws = relay._ws || relay.ws;
2574
+ if (!ws) {
2575
+ cleanup();
2576
+ fail(new Error("Cannot access relay WebSocket"));
2577
+ return Effect15.succeed(void 0);
2578
+ }
2579
+ timer = setTimeout(() => {
2580
+ if (settled) return;
2581
+ cleanup();
2582
+ fail(new Error("NIP-77 negotiation timeout"));
2583
+ }, 3e4);
2584
+ ws.addEventListener("message", handler);
2585
+ ws.send(JSON.stringify(["NEG-OPEN", subId, filter, msgHex]));
2586
+ } catch (e) {
2240
2587
  cleanup();
2241
- fail(new Error("NIP-77 negotiation timeout"));
2242
- }, 30000);
2243
- ws.addEventListener("message", handler);
2244
- ws.send(JSON.stringify(["NEG-OPEN", subId, filter, msgHex]));
2245
- } catch (e) {
2246
- cleanup();
2247
- fail(e);
2248
- }
2249
- return Effect15.sync(cleanup);
2250
- })),
2588
+ fail(e);
2589
+ }
2590
+ return Effect15.sync(cleanup);
2591
+ })
2592
+ ),
2251
2593
  closeAll: () => ScopedCache.invalidateAll(connections),
2252
2594
  getStatus: () => ({ connectedUrls: [...connectedUrls] }),
2253
2595
  subscribeStatus: (callback) => {
@@ -2259,10 +2601,10 @@ function createRelayHandle() {
2259
2601
  }
2260
2602
 
2261
2603
  // src/layers/RelayLive.ts
2262
- var RelayLive = Layer4.effect(Relay, createRelayHandle());
2604
+ var RelayLive = Layer5.effect(Relay, createRelayHandle());
2263
2605
 
2264
2606
  // src/layers/GiftWrapLive.ts
2265
- import { Effect as Effect17, Layer as Layer5 } from "effect";
2607
+ import { Effect as Effect17, Layer as Layer6 } from "effect";
2266
2608
 
2267
2609
  // src/sync/gift-wrap.ts
2268
2610
  import { Effect as Effect16 } from "effect";
@@ -2299,14 +2641,17 @@ function createEpochGiftWrapHandle(senderPrivateKey, epochStore) {
2299
2641
  }
2300
2642
 
2301
2643
  // src/layers/GiftWrapLive.ts
2302
- var GiftWrapLive = Layer5.effect(GiftWrap3, Effect17.gen(function* () {
2303
- const identity = yield* Identity;
2304
- const epochStore = yield* EpochStore;
2305
- return createEpochGiftWrapHandle(identity.privateKey, epochStore);
2306
- }));
2644
+ var GiftWrapLive = Layer6.effect(
2645
+ GiftWrap3,
2646
+ Effect17.gen(function* () {
2647
+ const identity = yield* Identity;
2648
+ const epochStore = yield* EpochStore;
2649
+ return createEpochGiftWrapHandle(identity.privateKey, epochStore);
2650
+ })
2651
+ );
2307
2652
 
2308
2653
  // src/layers/PublishQueueLive.ts
2309
- import { Effect as Effect19, Layer as Layer6 } from "effect";
2654
+ import { Effect as Effect19, Layer as Layer7 } from "effect";
2310
2655
 
2311
2656
  // src/sync/publish-queue.ts
2312
2657
  import { Effect as Effect18, Ref as Ref4 } from "effect";
@@ -2317,12 +2662,11 @@ function persist(storage, pending) {
2317
2662
  function createPublishQueue(storage, relay) {
2318
2663
  return Effect18.gen(function* () {
2319
2664
  const stored = yield* storage.getMeta(META_KEY);
2320
- const initial = Array.isArray(stored) ? new Set(stored) : new Set;
2665
+ const initial = Array.isArray(stored) ? new Set(stored) : /* @__PURE__ */ new Set();
2321
2666
  const pendingRef = yield* Ref4.make(initial);
2322
- const listeners = new Set;
2667
+ const listeners = /* @__PURE__ */ new Set();
2323
2668
  const notify = (pending) => {
2324
- for (const listener of listeners)
2325
- listener(pending.size);
2669
+ for (const listener of listeners) listener(pending.size);
2326
2670
  };
2327
2671
  return {
2328
2672
  enqueue: (eventId) => Effect18.gen(function* () {
@@ -2336,13 +2680,11 @@ function createPublishQueue(storage, relay) {
2336
2680
  }),
2337
2681
  flush: (relayUrls) => Effect18.gen(function* () {
2338
2682
  const pending = yield* Ref4.get(pendingRef);
2339
- if (pending.size === 0)
2340
- return;
2341
- const succeeded = new Set;
2683
+ if (pending.size === 0) return;
2684
+ const succeeded = /* @__PURE__ */ new Set();
2342
2685
  let consecutiveFailures = 0;
2343
2686
  for (const eventId of pending) {
2344
- if (consecutiveFailures >= 3)
2345
- break;
2687
+ if (consecutiveFailures >= 3) break;
2346
2688
  const gw = yield* storage.getGiftWrap(eventId);
2347
2689
  if (!gw || !gw.event) {
2348
2690
  succeeded.add(eventId);
@@ -2352,7 +2694,6 @@ function createPublishQueue(storage, relay) {
2352
2694
  const result = yield* Effect18.result(relay.publish(gw.event, relayUrls));
2353
2695
  if (result._tag === "Success") {
2354
2696
  succeeded.add(eventId);
2355
- yield* storage.stripGiftWrapBlob(eventId);
2356
2697
  consecutiveFailures = 0;
2357
2698
  } else {
2358
2699
  consecutiveFailures++;
@@ -2380,27 +2721,29 @@ function createPublishQueue(storage, relay) {
2380
2721
  }
2381
2722
 
2382
2723
  // src/layers/PublishQueueLive.ts
2383
- var PublishQueueLive = Layer6.effect(PublishQueue, Effect19.gen(function* () {
2384
- const storage = yield* Storage;
2385
- const relay = yield* Relay;
2386
- return yield* createPublishQueue(storage, relay);
2387
- }));
2724
+ var PublishQueueLive = Layer7.effect(
2725
+ PublishQueue,
2726
+ Effect19.gen(function* () {
2727
+ const storage = yield* Storage;
2728
+ const relay = yield* Relay;
2729
+ return yield* createPublishQueue(storage, relay);
2730
+ })
2731
+ );
2388
2732
 
2389
2733
  // src/layers/SyncStatusLive.ts
2390
- import { Layer as Layer7 } from "effect";
2734
+ import { Layer as Layer8 } from "effect";
2391
2735
 
2392
2736
  // src/sync/sync-status.ts
2393
2737
  import { Effect as Effect20, SubscriptionRef } from "effect";
2394
2738
  function createSyncStatusHandle() {
2395
2739
  return Effect20.gen(function* () {
2396
2740
  const ref = yield* SubscriptionRef.make("idle");
2397
- const listeners = new Set;
2741
+ const listeners = /* @__PURE__ */ new Set();
2398
2742
  return {
2399
2743
  get: () => SubscriptionRef.get(ref),
2400
2744
  set: (status) => Effect20.gen(function* () {
2401
2745
  yield* SubscriptionRef.set(ref, status);
2402
- for (const listener of listeners)
2403
- listener(status);
2746
+ for (const listener of listeners) listener(status);
2404
2747
  }),
2405
2748
  subscribe: (callback) => {
2406
2749
  listeners.add(callback);
@@ -2411,12 +2754,11 @@ function createSyncStatusHandle() {
2411
2754
  }
2412
2755
 
2413
2756
  // src/layers/SyncStatusLive.ts
2414
- var SyncStatusLive = Layer7.effect(SyncStatus, createSyncStatusHandle());
2757
+ var SyncStatusLive = Layer8.effect(SyncStatus, createSyncStatusHandle());
2415
2758
 
2416
2759
  // src/layers/TablinumLive.ts
2417
2760
  function reportSyncError(onSyncError, error) {
2418
- if (!onSyncError)
2419
- return;
2761
+ if (!onSyncError) return;
2420
2762
  onSyncError(error instanceof Error ? error : new Error(String(error)));
2421
2763
  }
2422
2764
  function mapMemberRecord(record) {
@@ -2424,239 +2766,365 @@ function mapMemberRecord(record) {
2424
2766
  id: record.id,
2425
2767
  addedAt: record.addedAt,
2426
2768
  addedInEpoch: record.addedInEpoch,
2427
- ...record.name !== undefined ? { name: record.name } : {},
2428
- ...record.picture !== undefined ? { picture: record.picture } : {},
2429
- ...record.about !== undefined ? { about: record.about } : {},
2430
- ...record.nip05 !== undefined ? { nip05: record.nip05 } : {},
2431
- ...record.removedAt !== undefined ? { removedAt: record.removedAt } : {},
2432
- ...record.removedInEpoch !== undefined ? { removedInEpoch: record.removedInEpoch } : {}
2769
+ ...record.name !== void 0 ? { name: record.name } : {},
2770
+ ...record.picture !== void 0 ? { picture: record.picture } : {},
2771
+ ...record.about !== void 0 ? { about: record.about } : {},
2772
+ ...record.nip05 !== void 0 ? { nip05: record.nip05 } : {},
2773
+ ...record.removedAt !== void 0 ? { removedAt: record.removedAt } : {},
2774
+ ...record.removedInEpoch !== void 0 ? { removedInEpoch: record.removedInEpoch } : {}
2433
2775
  };
2434
2776
  }
2435
- var IdentityWithDeps = IdentityLive.pipe(Layer8.provide(StorageLive));
2436
- var EpochStoreWithDeps = EpochStoreLive.pipe(Layer8.provide(IdentityWithDeps), Layer8.provide(StorageLive));
2437
- var GiftWrapWithDeps = GiftWrapLive.pipe(Layer8.provide(IdentityWithDeps), Layer8.provide(EpochStoreWithDeps));
2438
- var PublishQueueWithDeps = PublishQueueLive.pipe(Layer8.provide(StorageLive), Layer8.provide(RelayLive));
2439
- var AllServicesLive = Layer8.mergeAll(IdentityWithDeps, EpochStoreWithDeps, StorageLive, RelayLive, GiftWrapWithDeps, PublishQueueWithDeps, SyncStatusLive);
2440
- var TablinumLive = Layer8.effect(Tablinum, Effect21.gen(function* () {
2441
- const config = yield* Config;
2442
- const identity = yield* Identity;
2443
- const epochStore = yield* EpochStore;
2444
- const storage = yield* Storage;
2445
- const relay = yield* Relay;
2446
- const giftWrap = yield* GiftWrap3;
2447
- const publishQueue = yield* PublishQueue;
2448
- const syncStatus = yield* SyncStatus;
2449
- const scope = yield* Effect21.scope;
2450
- const pubsub = yield* PubSub2.unbounded();
2451
- const replayingRef = yield* Ref5.make(false);
2452
- const closedRef = yield* Ref5.make(false);
2453
- const watchCtx = { pubsub, replayingRef };
2454
- const schemaEntries = Object.entries(config.schema);
2455
- const allSchemaEntries = [...schemaEntries, ["_members", membersCollectionDef]];
2456
- const knownCollections = new Map(allSchemaEntries.map(([, def]) => [def.name, def.eventRetention]));
2457
- let notifyAuthor;
2458
- 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);
2459
- const onWrite = (event) => Effect21.gen(function* () {
2460
- const content = event.kind === "d" ? JSON.stringify(null) : JSON.stringify(event.data);
2461
- const dTag = `${event.collection}:${event.recordId}`;
2462
- const wrapResult = yield* Effect21.result(giftWrap.wrap({
2463
- kind: 1,
2464
- content,
2465
- tags: [["d", dTag]],
2466
- created_at: Math.floor(event.createdAt / 1000)
2467
- }));
2468
- if (wrapResult._tag === "Failure") {
2469
- reportSyncError(config.onSyncError, wrapResult.failure);
2470
- return;
2471
- }
2472
- const gw = wrapResult.success;
2473
- yield* storage.putGiftWrap({ id: gw.id, createdAt: gw.created_at });
2474
- yield* Effect21.forkIn(Effect21.gen(function* () {
2475
- const publishResult = yield* Effect21.result(syncHandle.publishLocal({
2476
- id: gw.id,
2477
- event: gw,
2478
- createdAt: gw.created_at
2479
- }));
2480
- if (publishResult._tag === "Failure") {
2481
- reportSyncError(config.onSyncError, publishResult.failure);
2777
+ var IdentityWithDeps = IdentityLive.pipe(Layer9.provide(StorageLive));
2778
+ var EpochStoreWithDeps = EpochStoreLive.pipe(
2779
+ Layer9.provide(IdentityWithDeps),
2780
+ Layer9.provide(StorageLive)
2781
+ );
2782
+ var GiftWrapWithDeps = GiftWrapLive.pipe(
2783
+ Layer9.provide(IdentityWithDeps),
2784
+ Layer9.provide(EpochStoreWithDeps)
2785
+ );
2786
+ var PublishQueueWithDeps = PublishQueueLive.pipe(
2787
+ Layer9.provide(StorageLive),
2788
+ Layer9.provide(RelayLive)
2789
+ );
2790
+ var AllServicesLive = Layer9.mergeAll(
2791
+ IdentityWithDeps,
2792
+ EpochStoreWithDeps,
2793
+ StorageLive,
2794
+ RelayLive,
2795
+ GiftWrapWithDeps,
2796
+ PublishQueueWithDeps,
2797
+ SyncStatusLive
2798
+ );
2799
+ var TablinumLive = Layer9.effect(
2800
+ Tablinum,
2801
+ Effect21.gen(function* () {
2802
+ const config = yield* Config;
2803
+ const identity = yield* Identity;
2804
+ const epochStore = yield* EpochStore;
2805
+ const storage = yield* Storage;
2806
+ const relay = yield* Relay;
2807
+ const giftWrap = yield* GiftWrap3;
2808
+ const publishQueue = yield* PublishQueue;
2809
+ const syncStatus = yield* SyncStatus;
2810
+ const scope = yield* Effect21.scope;
2811
+ const logLayer = Layer9.succeed(References3.MinimumLogLevel, config.logLevel);
2812
+ const pubsub = yield* PubSub2.unbounded();
2813
+ const replayingRef = yield* Ref5.make(false);
2814
+ const closedRef = yield* Ref5.make(false);
2815
+ const watchCtx = { pubsub, replayingRef };
2816
+ const schemaEntries = Object.entries(config.schema);
2817
+ const allSchemaEntries = [...schemaEntries, ["_members", membersCollectionDef]];
2818
+ const knownCollections = new Map(
2819
+ allSchemaEntries.map(([, def]) => [def.name, def.eventRetention])
2820
+ );
2821
+ let notifyAuthor;
2822
+ const syncHandle = createSyncHandle(
2823
+ storage,
2824
+ giftWrap,
2825
+ relay,
2826
+ publishQueue,
2827
+ syncStatus,
2828
+ watchCtx,
2829
+ config.relays,
2830
+ knownCollections,
2831
+ epochStore,
2832
+ identity.privateKey,
2833
+ identity.publicKey,
2834
+ scope,
2835
+ config.logLevel,
2836
+ config.onSyncError ? (error) => reportSyncError(config.onSyncError, error) : void 0,
2837
+ (pubkey) => notifyAuthor?.(pubkey),
2838
+ config.onRemoved,
2839
+ config.onMembersChanged
2840
+ );
2841
+ const onWrite = (event) => Effect21.gen(function* () {
2842
+ const content = event.kind === "d" ? JSON.stringify(null) : JSON.stringify(event.data);
2843
+ const dTag = `${event.collection}:${event.recordId}`;
2844
+ const wrapResult = yield* Effect21.result(
2845
+ giftWrap.wrap({
2846
+ kind: 1,
2847
+ content,
2848
+ tags: [["d", dTag]],
2849
+ created_at: Math.floor(event.createdAt / 1e3)
2850
+ })
2851
+ );
2852
+ if (wrapResult._tag === "Failure") {
2853
+ reportSyncError(config.onSyncError, wrapResult.failure);
2854
+ return;
2482
2855
  }
2483
- }), scope);
2484
- });
2485
- const knownAuthors = new Set;
2486
- const putMemberRecord = (record) => Effect21.gen(function* () {
2487
- const existing = yield* storage.getRecord("_members", record.id);
2488
- const event = {
2489
- id: uuidv7(),
2490
- collection: "_members",
2491
- recordId: record.id,
2492
- kind: existing ? "u" : "c",
2493
- data: record,
2494
- createdAt: Date.now(),
2495
- author: identity.publicKey
2496
- };
2497
- yield* storage.putEvent(event);
2498
- yield* applyEvent(storage, event);
2499
- yield* onWrite(event);
2500
- yield* notifyChange(watchCtx, {
2501
- collection: "_members",
2502
- recordId: record.id,
2503
- kind: existing ? "update" : "create"
2856
+ const gw = wrapResult.success;
2857
+ yield* storage.putGiftWrap({ id: gw.id, event: gw, createdAt: gw.created_at });
2858
+ yield* Effect21.forkIn(
2859
+ Effect21.gen(function* () {
2860
+ const publishResult = yield* Effect21.result(
2861
+ syncHandle.publishLocal({
2862
+ id: gw.id,
2863
+ event: gw,
2864
+ createdAt: gw.created_at
2865
+ })
2866
+ );
2867
+ if (publishResult._tag === "Failure") {
2868
+ reportSyncError(config.onSyncError, publishResult.failure);
2869
+ }
2870
+ }),
2871
+ scope
2872
+ );
2504
2873
  });
2505
- config.onMembersChanged?.();
2506
- });
2507
- notifyAuthor = (pubkey) => {
2508
- if (knownAuthors.has(pubkey))
2509
- return;
2510
- knownAuthors.add(pubkey);
2511
- Effect21.runFork(Effect21.gen(function* () {
2512
- const existing = yield* storage.getRecord("_members", pubkey);
2513
- if (!existing) {
2514
- yield* putMemberRecord({
2515
- id: pubkey,
2516
- addedAt: Date.now(),
2517
- addedInEpoch: getCurrentEpoch(epochStore).id
2518
- });
2519
- }
2520
- const profileOpt = yield* fetchAuthorProfile(relay, config.relays, pubkey).pipe(Effect21.catchTag("RelayError", () => Effect21.succeed(Option9.none())));
2521
- if (Option9.isSome(profileOpt)) {
2522
- const current = yield* storage.getRecord("_members", pubkey);
2523
- if (current) {
2524
- yield* storage.putRecord("_members", {
2525
- ...current,
2526
- ...profileOpt.value
2527
- });
2528
- yield* notifyChange(watchCtx, {
2529
- collection: "_members",
2530
- recordId: pubkey,
2531
- kind: "update"
2532
- });
2533
- config.onMembersChanged?.();
2534
- }
2535
- }
2536
- }).pipe(Effect21.ignore, Effect21.forkIn(scope)));
2537
- };
2538
- const handles = new Map;
2539
- for (const [, def] of allSchemaEntries) {
2540
- const validator = buildValidator(def.name, def);
2541
- const partialValidator = buildPartialValidator(def.name, def);
2542
- const handle = createCollectionHandle(def, storage, watchCtx, validator, partialValidator, uuidv7, identity.publicKey, onWrite);
2543
- handles.set(def.name, handle);
2544
- }
2545
- yield* syncHandle.startSubscription();
2546
- const selfMember = yield* storage.getRecord("_members", identity.publicKey);
2547
- if (!selfMember) {
2548
- yield* putMemberRecord({
2549
- id: identity.publicKey,
2550
- addedAt: Date.now(),
2551
- addedInEpoch: getCurrentEpoch(epochStore).id
2874
+ const knownAuthors = /* @__PURE__ */ new Set();
2875
+ const putMemberRecord = (record) => Effect21.gen(function* () {
2876
+ const existing = yield* storage.getRecord("_members", record.id);
2877
+ const event = {
2878
+ id: uuidv7(),
2879
+ collection: "_members",
2880
+ recordId: record.id,
2881
+ kind: existing ? "u" : "c",
2882
+ data: record,
2883
+ createdAt: Date.now(),
2884
+ author: identity.publicKey
2885
+ };
2886
+ yield* storage.putEvent(event);
2887
+ yield* applyEvent(storage, event);
2888
+ yield* onWrite(event);
2889
+ yield* notifyChange(watchCtx, {
2890
+ collection: "_members",
2891
+ recordId: record.id,
2892
+ kind: existing ? "update" : "create"
2893
+ });
2894
+ config.onMembersChanged?.();
2552
2895
  });
2553
- }
2554
- const ensureOpen = (effect) => Effect21.gen(function* () {
2555
- if (yield* Ref5.get(closedRef)) {
2556
- return yield* new StorageError({ message: "Database is closed" });
2557
- }
2558
- return yield* effect;
2559
- });
2560
- const ensureSyncOpen = (effect) => Effect21.gen(function* () {
2561
- if (yield* Ref5.get(closedRef)) {
2562
- return yield* new SyncError({ message: "Database is closed", phase: "init" });
2563
- }
2564
- return yield* effect;
2565
- });
2566
- const dbHandle = {
2567
- collection: (name) => {
2568
- const handle = handles.get(name);
2569
- if (!handle)
2570
- throw new Error(`Collection "${name}" not found in schema`);
2571
- return handle;
2572
- },
2573
- publicKey: identity.publicKey,
2574
- members: handles.get("_members"),
2575
- exportKey: () => identity.exportKey(),
2576
- exportInvite: () => ({
2577
- epochKeys: [...exportEpochKeys(epochStore)],
2578
- relays: [...config.relays],
2579
- dbName: config.dbName
2580
- }),
2581
- close: () => Effect21.gen(function* () {
2582
- if (yield* Ref5.get(closedRef))
2583
- return;
2584
- yield* Ref5.set(closedRef, true);
2585
- yield* Scope4.close(scope, Exit.void);
2586
- }),
2587
- rebuild: () => ensureOpen(rebuild(storage, allSchemaEntries.map(([, def]) => def.name))),
2588
- sync: () => ensureSyncOpen(syncHandle.sync()),
2589
- getSyncStatus: () => syncStatus.get(),
2590
- subscribeSyncStatus: (callback) => syncStatus.subscribe(callback),
2591
- pendingCount: () => publishQueue.size(),
2592
- subscribePendingCount: (callback) => publishQueue.subscribe(callback),
2593
- getRelayStatus: () => relay.getStatus(),
2594
- subscribeRelayStatus: (callback) => relay.subscribeStatus(callback),
2595
- addMember: (pubkey) => ensureOpen(Effect21.gen(function* () {
2596
- const existing = yield* storage.getRecord("_members", pubkey);
2597
- if (existing && !existing.removedAt)
2598
- return;
2896
+ notifyAuthor = (pubkey) => {
2897
+ if (knownAuthors.has(pubkey)) return;
2898
+ knownAuthors.add(pubkey);
2899
+ Effect21.runFork(
2900
+ Effect21.gen(function* () {
2901
+ const existing = yield* storage.getRecord("_members", pubkey);
2902
+ if (!existing) {
2903
+ yield* putMemberRecord({
2904
+ id: pubkey,
2905
+ addedAt: Date.now(),
2906
+ addedInEpoch: getCurrentEpoch(epochStore).id
2907
+ });
2908
+ }
2909
+ const profileOpt = yield* fetchAuthorProfile(relay, config.relays, pubkey).pipe(
2910
+ Effect21.catchTag("RelayError", () => Effect21.succeed(Option9.none()))
2911
+ );
2912
+ if (Option9.isSome(profileOpt)) {
2913
+ const current = yield* storage.getRecord("_members", pubkey);
2914
+ if (current) {
2915
+ yield* storage.putRecord("_members", {
2916
+ ...current,
2917
+ ...profileOpt.value
2918
+ });
2919
+ yield* notifyChange(watchCtx, {
2920
+ collection: "_members",
2921
+ recordId: pubkey,
2922
+ kind: "update"
2923
+ });
2924
+ config.onMembersChanged?.();
2925
+ }
2926
+ }
2927
+ }).pipe(Effect21.ignore, Effect21.provide(logLayer), Effect21.forkIn(scope))
2928
+ );
2929
+ };
2930
+ const handles = /* @__PURE__ */ new Map();
2931
+ for (const [, def] of allSchemaEntries) {
2932
+ const validator = buildValidator(def.name, def);
2933
+ const partialValidator = buildPartialValidator(def.name, def);
2934
+ const handle = createCollectionHandle(
2935
+ def,
2936
+ storage,
2937
+ watchCtx,
2938
+ validator,
2939
+ partialValidator,
2940
+ uuidv7,
2941
+ identity.publicKey,
2942
+ onWrite,
2943
+ config.logLevel
2944
+ );
2945
+ handles.set(def.name, handle);
2946
+ }
2947
+ yield* syncHandle.startSubscription();
2948
+ yield* Effect21.logInfo("Tablinum ready", {
2949
+ dbName: config.dbName,
2950
+ collections: schemaEntries.map(([name]) => name),
2951
+ relays: config.relays
2952
+ });
2953
+ const selfMember = yield* storage.getRecord("_members", identity.publicKey);
2954
+ if (!selfMember) {
2599
2955
  yield* putMemberRecord({
2600
- id: pubkey,
2956
+ id: identity.publicKey,
2601
2957
  addedAt: Date.now(),
2602
- addedInEpoch: getCurrentEpoch(epochStore).id,
2603
- ...existing ? { removedAt: undefined, removedInEpoch: undefined } : {}
2604
- });
2605
- })),
2606
- removeMember: (pubkey) => ensureOpen(Effect21.gen(function* () {
2607
- const allMembers = yield* storage.getAllRecords("_members");
2608
- const activeMembers = allMembers.filter((member) => !member.removedAt && member.id !== pubkey);
2609
- const activePubkeys = activeMembers.map((member) => member.id);
2610
- const result = createRotation(epochStore, identity.privateKey, identity.publicKey, activePubkeys, [pubkey]);
2611
- addEpoch(epochStore, result.epoch);
2612
- epochStore.currentEpochId = result.epoch.id;
2613
- yield* storage.putMeta("epochs", stringifyEpochStore(epochStore));
2614
- const memberRecord = yield* storage.getRecord("_members", pubkey);
2615
- yield* putMemberRecord({
2616
- ...memberRecord ?? {
2617
- id: pubkey,
2618
- addedAt: 0,
2619
- addedInEpoch: EpochId("epoch-0")
2620
- },
2621
- removedAt: Date.now(),
2622
- removedInEpoch: result.epoch.id
2958
+ addedInEpoch: getCurrentEpoch(epochStore).id
2623
2959
  });
2624
- yield* Effect21.forEach(result.wrappedEvents, (wrappedEvent) => relay.publish(wrappedEvent, [...config.relays]).pipe(Effect21.tapError((e) => Effect21.sync(() => reportSyncError(config.onSyncError, e))), Effect21.ignore), { discard: true });
2625
- yield* Effect21.forEach(result.removalNotices, (notice) => relay.publish(notice, [...config.relays]).pipe(Effect21.tapError((e) => Effect21.sync(() => reportSyncError(config.onSyncError, e))), Effect21.ignore), { discard: true });
2626
- yield* syncHandle.addEpochSubscription(result.epoch.publicKey);
2627
- })),
2628
- getMembers: () => ensureOpen(Effect21.gen(function* () {
2629
- const allRecords = yield* storage.getAllRecords("_members");
2630
- return allRecords.filter((record) => !record._d).map(mapMemberRecord);
2631
- })),
2632
- getProfile: () => ensureOpen(Effect21.gen(function* () {
2633
- const record = yield* storage.getRecord("_members", identity.publicKey);
2634
- if (!record)
2635
- return {};
2636
- const profile = {};
2637
- if (record.name !== undefined)
2638
- profile.name = record.name;
2639
- if (record.picture !== undefined)
2640
- profile.picture = record.picture;
2641
- if (record.about !== undefined)
2642
- profile.about = record.about;
2643
- if (record.nip05 !== undefined)
2644
- profile.nip05 = record.nip05;
2645
- return profile;
2646
- })),
2647
- setProfile: (profile) => ensureOpen(Effect21.gen(function* () {
2648
- const existing = yield* storage.getRecord("_members", identity.publicKey);
2649
- if (!existing) {
2650
- return yield* new ValidationError({ message: "Current user is not a member" });
2651
- }
2652
- const { _d, _u, _a, _e, ...memberFields } = existing;
2653
- yield* putMemberRecord({ ...memberFields, ...profile });
2654
- }))
2655
- };
2656
- return dbHandle;
2657
- })).pipe(Layer8.provide(AllServicesLive));
2960
+ }
2961
+ const withLog = (effect) => Effect21.provideService(effect, References3.MinimumLogLevel, config.logLevel);
2962
+ const ensureOpen = (effect) => withLog(
2963
+ Effect21.gen(function* () {
2964
+ if (yield* Ref5.get(closedRef)) {
2965
+ return yield* new StorageError({ message: "Database is closed" });
2966
+ }
2967
+ return yield* effect;
2968
+ })
2969
+ );
2970
+ const ensureSyncOpen = (effect) => withLog(
2971
+ Effect21.gen(function* () {
2972
+ if (yield* Ref5.get(closedRef)) {
2973
+ return yield* new SyncError({ message: "Database is closed", phase: "init" });
2974
+ }
2975
+ return yield* effect;
2976
+ })
2977
+ );
2978
+ const dbHandle = {
2979
+ collection: (name) => {
2980
+ const handle = handles.get(name);
2981
+ if (!handle) throw new Error(`Collection "${name}" not found in schema`);
2982
+ return handle;
2983
+ },
2984
+ publicKey: identity.publicKey,
2985
+ members: handles.get("_members"),
2986
+ exportKey: () => identity.exportKey(),
2987
+ exportInvite: () => ({
2988
+ epochKeys: [...exportEpochKeys(epochStore)],
2989
+ relays: [...config.relays],
2990
+ dbName: config.dbName
2991
+ }),
2992
+ close: () => withLog(
2993
+ Effect21.gen(function* () {
2994
+ if (yield* Ref5.get(closedRef)) return;
2995
+ yield* Ref5.set(closedRef, true);
2996
+ syncHandle.stopHealing();
2997
+ yield* Scope4.close(scope, Exit.void);
2998
+ })
2999
+ ),
3000
+ rebuild: () => ensureOpen(
3001
+ rebuild(
3002
+ storage,
3003
+ allSchemaEntries.map(([, def]) => def.name)
3004
+ )
3005
+ ),
3006
+ sync: () => ensureSyncOpen(
3007
+ syncHandle.sync().pipe(
3008
+ Effect21.tap(
3009
+ () => Effect21.sync(() => {
3010
+ syncHandle.startHealing();
3011
+ })
3012
+ )
3013
+ )
3014
+ ),
3015
+ getSyncStatus: () => syncStatus.get(),
3016
+ subscribeSyncStatus: (callback) => syncStatus.subscribe(callback),
3017
+ pendingCount: () => publishQueue.size(),
3018
+ subscribePendingCount: (callback) => publishQueue.subscribe(callback),
3019
+ getRelayStatus: () => relay.getStatus(),
3020
+ subscribeRelayStatus: (callback) => relay.subscribeStatus(callback),
3021
+ addMember: (pubkey) => ensureOpen(
3022
+ Effect21.gen(function* () {
3023
+ const existing = yield* storage.getRecord("_members", pubkey);
3024
+ if (existing && !existing.removedAt) return;
3025
+ yield* putMemberRecord({
3026
+ id: pubkey,
3027
+ addedAt: Date.now(),
3028
+ addedInEpoch: getCurrentEpoch(epochStore).id,
3029
+ ...existing ? { removedAt: void 0, removedInEpoch: void 0 } : {}
3030
+ });
3031
+ })
3032
+ ),
3033
+ removeMember: (pubkey) => ensureOpen(
3034
+ Effect21.gen(function* () {
3035
+ const allMembers = yield* storage.getAllRecords("_members");
3036
+ const activeMembers = allMembers.filter(
3037
+ (member) => !member.removedAt && member.id !== pubkey
3038
+ );
3039
+ const activePubkeys = activeMembers.map((member) => member.id);
3040
+ const result = createRotation(
3041
+ epochStore,
3042
+ identity.privateKey,
3043
+ identity.publicKey,
3044
+ activePubkeys,
3045
+ [pubkey]
3046
+ );
3047
+ addEpoch(epochStore, result.epoch);
3048
+ epochStore.currentEpochId = result.epoch.id;
3049
+ yield* storage.putMeta("epochs", stringifyEpochStore(epochStore));
3050
+ const memberRecord = yield* storage.getRecord("_members", pubkey);
3051
+ yield* putMemberRecord({
3052
+ ...memberRecord ?? {
3053
+ id: pubkey,
3054
+ addedAt: 0,
3055
+ addedInEpoch: EpochId("epoch-0")
3056
+ },
3057
+ removedAt: Date.now(),
3058
+ removedInEpoch: result.epoch.id
3059
+ });
3060
+ yield* Effect21.forEach(
3061
+ result.wrappedEvents,
3062
+ (wrappedEvent) => relay.publish(wrappedEvent, [...config.relays]).pipe(
3063
+ Effect21.tapError((e) => Effect21.sync(() => reportSyncError(config.onSyncError, e))),
3064
+ Effect21.ignore
3065
+ ),
3066
+ { discard: true }
3067
+ );
3068
+ yield* Effect21.forEach(
3069
+ result.removalNotices,
3070
+ (notice) => relay.publish(notice, [...config.relays]).pipe(
3071
+ Effect21.tapError((e) => Effect21.sync(() => reportSyncError(config.onSyncError, e))),
3072
+ Effect21.ignore
3073
+ ),
3074
+ { discard: true }
3075
+ );
3076
+ yield* syncHandle.addEpochSubscription(result.epoch.publicKey);
3077
+ })
3078
+ ),
3079
+ getMembers: () => ensureOpen(
3080
+ Effect21.gen(function* () {
3081
+ const allRecords = yield* storage.getAllRecords("_members");
3082
+ return allRecords.filter((record) => !record._d).map(mapMemberRecord);
3083
+ })
3084
+ ),
3085
+ getProfile: () => ensureOpen(
3086
+ Effect21.gen(function* () {
3087
+ const record = yield* storage.getRecord("_members", identity.publicKey);
3088
+ if (!record) return {};
3089
+ const profile = {};
3090
+ if (record.name !== void 0) profile.name = record.name;
3091
+ if (record.picture !== void 0) profile.picture = record.picture;
3092
+ if (record.about !== void 0) profile.about = record.about;
3093
+ if (record.nip05 !== void 0) profile.nip05 = record.nip05;
3094
+ return profile;
3095
+ })
3096
+ ),
3097
+ setProfile: (profile) => ensureOpen(
3098
+ Effect21.gen(function* () {
3099
+ const existing = yield* storage.getRecord("_members", identity.publicKey);
3100
+ if (!existing) {
3101
+ return yield* new ValidationError({ message: "Current user is not a member" });
3102
+ }
3103
+ const { _d, _u, _a, _e, ...memberFields } = existing;
3104
+ yield* putMemberRecord({ ...memberFields, ...profile });
3105
+ })
3106
+ )
3107
+ };
3108
+ return dbHandle;
3109
+ }).pipe(Effect21.withLogSpan("tablinum.init"))
3110
+ ).pipe(Layer9.provide(AllServicesLive));
2658
3111
 
2659
3112
  // src/db/create-tablinum.ts
3113
+ function resolveLogLevel(input) {
3114
+ if (input === void 0 || input === "none") return "None";
3115
+ switch (input) {
3116
+ case "debug":
3117
+ return "Debug";
3118
+ case "info":
3119
+ return "Info";
3120
+ case "warning":
3121
+ return "Warn";
3122
+ case "error":
3123
+ return "Error";
3124
+ default:
3125
+ return input;
3126
+ }
3127
+ }
2660
3128
  function validateConfig(config) {
2661
3129
  return Effect22.gen(function* () {
2662
3130
  if (Object.keys(config.schema).length === 0) {
@@ -2670,19 +3138,26 @@ function createTablinum(config) {
2670
3138
  return Effect22.gen(function* () {
2671
3139
  yield* validateConfig(config);
2672
3140
  const runtimeConfig = yield* resolveRuntimeConfig(config);
3141
+ const logLevel = resolveLogLevel(config.logLevel);
2673
3142
  const configValue = {
2674
3143
  ...runtimeConfig,
2675
3144
  schema: config.schema,
3145
+ logLevel,
2676
3146
  onSyncError: config.onSyncError,
2677
3147
  onRemoved: config.onRemoved,
2678
3148
  onMembersChanged: config.onMembersChanged
2679
3149
  };
2680
- const configLayer = Layer9.succeed(Config, configValue);
2681
- const fullLayer = TablinumLive.pipe(Layer9.provide(configLayer));
2682
- const ctx = yield* Layer9.build(fullLayer);
3150
+ const configLayer = Layer10.succeed(Config, configValue);
3151
+ const logLayer = Layer10.succeed(References4.MinimumLogLevel, logLevel);
3152
+ const fullLayer = TablinumLive.pipe(Layer10.provide(configLayer), Layer10.provide(logLayer));
3153
+ const ctx = yield* Layer10.build(fullLayer);
2683
3154
  return ServiceMap10.get(ctx, Tablinum);
2684
3155
  });
2685
3156
  }
3157
+
3158
+ // src/index.ts
3159
+ import { LogLevel } from "effect";
3160
+
2686
3161
  // src/db/invite.ts
2687
3162
  import { Schema as Schema7 } from "effect";
2688
3163
  var InviteSchema = Schema7.Struct({
@@ -2717,19 +3192,20 @@ function decodeInvite(encoded) {
2717
3192
  }
2718
3193
  }
2719
3194
  export {
2720
- field,
2721
- encodeInvite,
2722
- decodeInvite,
2723
- createTablinum,
2724
- collection,
2725
- ValidationError,
2726
- TablinumLive,
2727
- SyncError,
2728
- StorageError,
2729
- RelayError,
2730
- NotFoundError,
2731
- EpochId,
2732
- DatabaseName,
3195
+ ClosedError,
2733
3196
  CryptoError,
2734
- ClosedError
3197
+ DatabaseName,
3198
+ EpochId,
3199
+ LogLevel,
3200
+ NotFoundError,
3201
+ RelayError,
3202
+ StorageError,
3203
+ SyncError,
3204
+ TablinumLive,
3205
+ ValidationError,
3206
+ collection,
3207
+ createTablinum,
3208
+ decodeInvite,
3209
+ encodeInvite,
3210
+ field
2735
3211
  };