tablinum 0.5.0 → 0.6.1

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.
@@ -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,6 +55,7 @@ function collection(name, fields, options) {
53
55
  }
54
56
  return { _tag: "CollectionDef", name, fields, indices, eventRetention };
55
57
  }
58
+
56
59
  // src/db/invite.ts
57
60
  import { Schema as Schema2 } from "effect";
58
61
 
@@ -82,15 +85,17 @@ var PersistedEpochStoreSchema = Schema.Struct({
82
85
  epochs: Schema.Array(PersistedEpochSchema),
83
86
  currentEpochId: Schema.String
84
87
  });
85
- var decodePersistedEpochStore = Schema.decodeUnknownSync(Schema.fromJsonString(PersistedEpochStoreSchema));
88
+ var decodePersistedEpochStore = Schema.decodeUnknownSync(
89
+ Schema.fromJsonString(PersistedEpochStoreSchema)
90
+ );
86
91
  function createEpochKey(id, privateKeyHex, createdBy, parentEpoch) {
87
92
  const publicKey = getPublicKey(hexToBytes(privateKeyHex));
88
93
  const base = { id, privateKey: privateKeyHex, publicKey, createdBy };
89
- return parentEpoch !== undefined ? { ...base, parentEpoch } : base;
94
+ return parentEpoch !== void 0 ? { ...base, parentEpoch } : base;
90
95
  }
91
96
  function createEpochStore(initialEpoch) {
92
- const epochs = new Map;
93
- const keysByPublicKey = new Map;
97
+ const epochs = /* @__PURE__ */ new Map();
98
+ const keysByPublicKey = /* @__PURE__ */ new Map();
94
99
  epochs.set(initialEpoch.id, initialEpoch);
95
100
  keysByPublicKey.set(initialEpoch.publicKey, hexToBytes(initialEpoch.privateKey));
96
101
  return { epochs, keysByPublicKey, currentEpochId: initialEpoch.id };
@@ -100,7 +105,14 @@ function addEpoch(store, epoch) {
100
105
  store.keysByPublicKey.set(epoch.publicKey, hexToBytes(epoch.privateKey));
101
106
  }
102
107
  function hydrateEpochStore(snapshot) {
103
- const [firstEpoch, ...remainingEpochs] = snapshot.epochs.map((epoch) => createEpochKey(EpochId(epoch.id), epoch.privateKey, epoch.createdBy, epoch.parentEpoch !== undefined ? EpochId(epoch.parentEpoch) : undefined));
108
+ const [firstEpoch, ...remainingEpochs] = snapshot.epochs.map(
109
+ (epoch) => createEpochKey(
110
+ EpochId(epoch.id),
111
+ epoch.privateKey,
112
+ epoch.createdBy,
113
+ epoch.parentEpoch !== void 0 ? EpochId(epoch.parentEpoch) : void 0
114
+ )
115
+ );
104
116
  if (!firstEpoch) {
105
117
  throw new Error("Epoch snapshot must contain at least one epoch");
106
118
  }
@@ -116,9 +128,16 @@ function createEpochStoreFromInputs(epochKeys, options = {}) {
116
128
  throw new Error("Epoch input must contain at least one key");
117
129
  }
118
130
  const createdBy = options.createdBy ?? "";
119
- const epochs = epochKeys.map((epochKey, index) => createEpochKey(epochKey.epochId, epochKey.key, createdBy, index > 0 ? epochKeys[index - 1].epochId : undefined));
131
+ const epochs = epochKeys.map(
132
+ (epochKey, index) => createEpochKey(
133
+ epochKey.epochId,
134
+ epochKey.key,
135
+ createdBy,
136
+ index > 0 ? epochKeys[index - 1].epochId : void 0
137
+ )
138
+ );
120
139
  const store = createEpochStore(epochs[0]);
121
- for (let i = 1;i < epochs.length; i++) {
140
+ for (let i = 1; i < epochs.length; i++) {
122
141
  addEpoch(store, epochs[i]);
123
142
  }
124
143
  store.currentEpochId = epochs[epochs.length - 1].id;
@@ -145,7 +164,7 @@ function serializeEpochStore(store) {
145
164
  id: epoch.id,
146
165
  privateKey: epoch.privateKey,
147
166
  createdBy: epoch.createdBy,
148
- ...epoch.parentEpoch !== undefined ? { parentEpoch: epoch.parentEpoch } : {}
167
+ ...epoch.parentEpoch !== void 0 ? { parentEpoch: epoch.parentEpoch } : {}
149
168
  })),
150
169
  currentEpochId: store.currentEpochId
151
170
  };
@@ -193,29 +212,24 @@ function decodeInvite(encoded) {
193
212
  throw new Error("Invalid invite: unexpected shape");
194
213
  }
195
214
  }
215
+
196
216
  // src/errors.ts
197
217
  import { Data } from "effect";
218
+ var ValidationError = class extends Data.TaggedError("ValidationError") {
219
+ };
220
+ var StorageError = class extends Data.TaggedError("StorageError") {
221
+ };
222
+ var CryptoError = class extends Data.TaggedError("CryptoError") {
223
+ };
224
+ var RelayError = class extends Data.TaggedError("RelayError") {
225
+ };
226
+ var SyncError = class extends Data.TaggedError("SyncError") {
227
+ };
228
+ var NotFoundError = class extends Data.TaggedError("NotFoundError") {
229
+ };
230
+ var ClosedError = class extends Data.TaggedError("ClosedError") {
231
+ };
198
232
 
199
- class ValidationError extends Data.TaggedError("ValidationError") {
200
- }
201
-
202
- class StorageError extends Data.TaggedError("StorageError") {
203
- }
204
-
205
- class CryptoError extends Data.TaggedError("CryptoError") {
206
- }
207
-
208
- class RelayError extends Data.TaggedError("RelayError") {
209
- }
210
-
211
- class SyncError extends Data.TaggedError("SyncError") {
212
- }
213
-
214
- class NotFoundError extends Data.TaggedError("NotFoundError") {
215
- }
216
-
217
- class ClosedError extends Data.TaggedError("ClosedError") {
218
- }
219
233
  // src/svelte/tablinum.svelte.ts
220
234
  import { Effect as Effect25, Exit as Exit2, References as References6, Scope as Scope6 } from "effect";
221
235
 
@@ -232,49 +246,61 @@ var RuntimeConfigSchema = Schema3.Struct({
232
246
  epochKeys: Schema3.optional(Schema3.Array(EpochKeyInputSchema))
233
247
  });
234
248
  function resolveRuntimeConfig(source) {
235
- return Schema3.decodeUnknownEffect(RuntimeConfigSchema)(source).pipe(Effect.map((config) => ({
236
- relays: [...config.relays],
237
- privateKey: config.privateKey,
238
- epochKeys: config.epochKeys?.map((ek) => ({ epochId: EpochId(ek.epochId), key: ek.key })),
239
- dbName: DatabaseName(config.dbName ?? "tablinum")
240
- })), Effect.mapError((error) => new ValidationError({
241
- message: `Invalid Tablinum configuration: ${error.message}`
242
- })));
249
+ return Schema3.decodeUnknownEffect(RuntimeConfigSchema)(source).pipe(
250
+ Effect.map((config) => ({
251
+ relays: [...config.relays],
252
+ privateKey: config.privateKey,
253
+ epochKeys: config.epochKeys?.map((ek) => ({ epochId: EpochId(ek.epochId), key: ek.key })),
254
+ dbName: DatabaseName(config.dbName ?? "tablinum")
255
+ })),
256
+ Effect.mapError(
257
+ (error) => new ValidationError({
258
+ message: `Invalid Tablinum configuration: ${error.message}`
259
+ })
260
+ )
261
+ );
243
262
  }
244
263
 
245
264
  // src/services/Config.ts
246
265
  import { ServiceMap } from "effect";
247
-
248
- class Config extends ServiceMap.Service()("tablinum/Config") {
249
- }
266
+ var Config = class extends ServiceMap.Service()("tablinum/Config") {
267
+ };
250
268
 
251
269
  // src/services/Tablinum.ts
252
270
  import { ServiceMap as ServiceMap2 } from "effect";
253
-
254
- class Tablinum extends ServiceMap2.Service()("tablinum/Tablinum") {
255
- }
271
+ var Tablinum = class extends ServiceMap2.Service()(
272
+ "tablinum/Tablinum"
273
+ ) {
274
+ };
256
275
 
257
276
  // src/layers/TablinumLive.ts
258
277
  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";
259
278
 
260
279
  // src/crud/watch.ts
261
280
  import { Effect as Effect2, PubSub, Ref, Stream } from "effect";
262
- function watchCollection(ctx, storage, collectionName, filter, mapRecord) {
281
+ function watchCollection(ctx, storage, collectionName, filter, mapRecord2) {
263
282
  const query = () => Effect2.map(storage.getAllRecords(collectionName), (all) => {
264
283
  const filtered = all.filter((r) => !r._d && (filter ? filter(r) : true));
265
- return mapRecord ? filtered.map(mapRecord) : filtered;
284
+ return mapRecord2 ? filtered.map(mapRecord2) : filtered;
266
285
  });
267
- const changes = Stream.fromPubSub(ctx.pubsub).pipe(Stream.filter((event) => event.collection === collectionName), Stream.mapEffect(() => Effect2.gen(function* () {
268
- const replaying = yield* Ref.get(ctx.replayingRef);
269
- if (replaying)
270
- return;
271
- return yield* query();
272
- })), Stream.filter((result) => result !== undefined));
273
- return Stream.unwrap(Effect2.gen(function* () {
274
- yield* Effect2.sleep(0);
275
- const initial = yield* query();
276
- return Stream.concat(Stream.make(initial), changes);
277
- }));
286
+ const changes = Stream.fromPubSub(ctx.pubsub).pipe(
287
+ Stream.filter((event) => event.collection === collectionName),
288
+ Stream.mapEffect(
289
+ () => Effect2.gen(function* () {
290
+ const replaying = yield* Ref.get(ctx.replayingRef);
291
+ if (replaying) return void 0;
292
+ return yield* query();
293
+ })
294
+ ),
295
+ Stream.filter((result) => result !== void 0)
296
+ );
297
+ return Stream.unwrap(
298
+ Effect2.gen(function* () {
299
+ yield* Effect2.sleep(0);
300
+ const initial = yield* query();
301
+ return Stream.concat(Stream.make(initial), changes);
302
+ })
303
+ );
278
304
  }
279
305
  function notifyChange(ctx, event) {
280
306
  return PubSub.publish(ctx.pubsub, event).pipe(Effect2.asVoid);
@@ -297,12 +323,9 @@ import { Effect as Effect3 } from "effect";
297
323
 
298
324
  // src/storage/lww.ts
299
325
  function resolveWinner(existing, incoming) {
300
- if (existing === null)
301
- return incoming;
302
- if (incoming.createdAt > existing.createdAt)
303
- return incoming;
304
- if (incoming.createdAt < existing.createdAt)
305
- return existing;
326
+ if (existing === null) return incoming;
327
+ if (incoming.createdAt > existing.createdAt) return incoming;
328
+ if (incoming.createdAt < existing.createdAt) return existing;
306
329
  return incoming.id < existing.id ? incoming : existing;
307
330
  }
308
331
 
@@ -369,8 +392,7 @@ function applyEvent(storage, event) {
369
392
  };
370
393
  const incomingMeta = { id: event.id, createdAt: event.createdAt };
371
394
  const winner = resolveWinner(existingMeta, incomingMeta);
372
- if (winner.id !== event.id)
373
- return false;
395
+ if (winner.id !== event.id) return false;
374
396
  }
375
397
  if (existing && event.kind === "u") {
376
398
  yield* storage.putRecord(event.collection, deepMerge(existing, buildRecord(event)));
@@ -386,10 +408,11 @@ function rebuild(storage, collections) {
386
408
  yield* storage.clearRecords(col);
387
409
  }
388
410
  const allEvents = yield* storage.getAllEvents();
389
- const sorted = [...allEvents].sort((a, b) => a.createdAt - b.createdAt || (a.id < b.id ? -1 : 1));
411
+ const sorted = [...allEvents].sort(
412
+ (a, b) => a.createdAt - b.createdAt || (a.id < b.id ? -1 : 1)
413
+ );
390
414
  for (const event of sorted) {
391
- if (event.data === null && event.kind !== "d")
392
- continue;
415
+ if (event.data === null && event.kind !== "d") continue;
393
416
  yield* applyEvent(storage, event);
394
417
  }
395
418
  });
@@ -436,15 +459,22 @@ function buildStructSchema(def, options = {}) {
436
459
  }
437
460
  for (const [name, fieldDef] of Object.entries(def.fields)) {
438
461
  const fieldSchema = fieldDefToSchema(fieldDef);
439
- schemaFields[name] = options.allOptional ? Schema4.optionalKey(fieldSchema) : fieldSchema;
462
+ schemaFields[name] = options.allOptional || fieldDef.isOptional ? Schema4.optionalKey(fieldSchema) : fieldSchema;
440
463
  }
441
464
  return Schema4.Struct(schemaFields);
442
465
  }
443
466
  function buildValidator(collectionName, def) {
444
467
  const decode = Schema4.decodeUnknownEffect(buildStructSchema(def, { includeId: true }));
445
- return (input) => decode(input).pipe(Effect4.map((result) => result), Effect4.mapError((e) => new ValidationError({
446
- message: `Validation failed for collection "${collectionName}": ${e.message}`
447
- })));
468
+ return (input) => decode(input).pipe(
469
+ Effect4.map(
470
+ (result) => result
471
+ ),
472
+ Effect4.mapError(
473
+ (e) => new ValidationError({
474
+ message: `Validation failed for collection "${collectionName}": ${e.message}`
475
+ })
476
+ )
477
+ );
448
478
  }
449
479
  function buildPartialValidator(collectionName, def) {
450
480
  const decode = Schema4.decodeUnknownEffect(buildStructSchema(def, { allOptional: true }));
@@ -456,15 +486,20 @@ function buildPartialValidator(collectionName, def) {
456
486
  }
457
487
  const record = input;
458
488
  const unknownField = Object.keys(record).find((key) => !Object.hasOwn(def.fields, key));
459
- if (unknownField !== undefined) {
489
+ if (unknownField !== void 0) {
460
490
  return yield* new ValidationError({
461
491
  message: `Unknown field "${unknownField}" in collection "${collectionName}"`,
462
492
  field: unknownField
463
493
  });
464
494
  }
465
- return yield* decode(record).pipe(Effect4.map((result) => result), Effect4.mapError((e) => new ValidationError({
466
- message: `Validation failed for collection "${collectionName}": ${e.message}`
467
- })));
495
+ return yield* decode(record).pipe(
496
+ Effect4.map((result) => result),
497
+ Effect4.mapError(
498
+ (e) => new ValidationError({
499
+ message: `Validation failed for collection "${collectionName}": ${e.message}`
500
+ })
501
+ )
502
+ );
468
503
  });
469
504
  }
470
505
 
@@ -475,16 +510,14 @@ import { Effect as Effect6, Option as Option3, References } from "effect";
475
510
  var alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
476
511
  function toBase64url(bytes) {
477
512
  let result = "";
478
- for (let i = 0;i < bytes.length; i += 3) {
513
+ for (let i = 0; i < bytes.length; i += 3) {
479
514
  const b0 = bytes[i];
480
515
  const b1 = bytes[i + 1] ?? 0;
481
516
  const b2 = bytes[i + 2] ?? 0;
482
517
  result += alphabet[b0 >> 2];
483
518
  result += alphabet[(b0 & 3) << 4 | b1 >> 4];
484
- if (i + 1 < bytes.length)
485
- result += alphabet[(b1 & 15) << 2 | b2 >> 6];
486
- if (i + 2 < bytes.length)
487
- result += alphabet[b2 & 63];
519
+ if (i + 1 < bytes.length) result += alphabet[(b1 & 15) << 2 | b2 >> 6];
520
+ if (i + 2 < bytes.length) result += alphabet[b2 & 63];
488
521
  }
489
522
  return result;
490
523
  }
@@ -529,16 +562,28 @@ function executeQuery(ctx, plan) {
529
562
  if (plan.indexQuery && ctx.def.indices.includes(plan.indexQuery.field)) {
530
563
  if (plan.indexQuery.type === "value") {
531
564
  results = [
532
- ...yield* ctx.storage.getByIndex(ctx.collectionName, plan.indexQuery.field, plan.indexQuery.range)
565
+ ...yield* ctx.storage.getByIndex(
566
+ ctx.collectionName,
567
+ plan.indexQuery.field,
568
+ plan.indexQuery.range
569
+ )
533
570
  ];
534
571
  } else {
535
572
  results = [
536
- ...yield* ctx.storage.getByIndexRange(ctx.collectionName, plan.indexQuery.field, plan.indexQuery.range)
573
+ ...yield* ctx.storage.getByIndexRange(
574
+ ctx.collectionName,
575
+ plan.indexQuery.field,
576
+ plan.indexQuery.range
577
+ )
537
578
  ];
538
579
  }
539
580
  } else if (plan.orderBy && ctx.def.indices.includes(plan.orderBy.field) && plan.filters.length === 0) {
540
581
  results = [
541
- ...yield* ctx.storage.getAllSorted(ctx.collectionName, plan.orderBy.field, plan.orderBy.direction === "desc" ? "prev" : "next")
582
+ ...yield* ctx.storage.getAllSorted(
583
+ ctx.collectionName,
584
+ plan.orderBy.field,
585
+ plan.orderBy.direction === "desc" ? "prev" : "next"
586
+ )
542
587
  ];
543
588
  } else {
544
589
  results = [...yield* ctx.storage.getAllRecords(ctx.collectionName)];
@@ -562,7 +607,7 @@ function executeQuery(ctx, plan) {
562
607
  if (plan.offset) {
563
608
  results = results.slice(plan.offset);
564
609
  }
565
- if (plan.limit !== null && plan.limit !== undefined) {
610
+ if (plan.limit !== null && plan.limit !== void 0) {
566
611
  results = results.slice(0, plan.limit);
567
612
  }
568
613
  return results.map(ctx.mapRecord);
@@ -570,16 +615,23 @@ function executeQuery(ctx, plan) {
570
615
  }
571
616
  function watchQuery(ctx, plan) {
572
617
  const query = () => executeQuery(ctx, plan);
573
- const changes = Stream2.fromPubSub(ctx.watchCtx.pubsub).pipe(Stream2.filter((event) => event.collection === ctx.collectionName), Stream2.mapEffect(() => Effect5.gen(function* () {
574
- const replaying = yield* Ref2.get(ctx.watchCtx.replayingRef);
575
- if (replaying)
576
- return;
577
- return yield* query();
578
- })), Stream2.filter((result) => result !== undefined));
579
- return Stream2.unwrap(Effect5.gen(function* () {
580
- const initial = yield* query();
581
- return Stream2.concat(Stream2.make(initial), changes);
582
- }));
618
+ const changes = Stream2.fromPubSub(ctx.watchCtx.pubsub).pipe(
619
+ Stream2.filter((event) => event.collection === ctx.collectionName),
620
+ Stream2.mapEffect(
621
+ () => Effect5.gen(function* () {
622
+ const replaying = yield* Ref2.get(ctx.watchCtx.replayingRef);
623
+ if (replaying) return void 0;
624
+ return yield* query();
625
+ })
626
+ ),
627
+ Stream2.filter((result) => result !== void 0)
628
+ );
629
+ return Stream2.unwrap(
630
+ Effect5.gen(function* () {
631
+ const initial = yield* query();
632
+ return Stream2.concat(Stream2.make(initial), changes);
633
+ })
634
+ );
583
635
  }
584
636
  function makeQueryBuilder(ctx, plan) {
585
637
  return {
@@ -601,15 +653,18 @@ function makeQueryBuilder(ctx, plan) {
601
653
  offset: (n) => makeQueryBuilder(ctx, { ...plan, offset: n }),
602
654
  limit: (n) => makeQueryBuilder(ctx, { ...plan, limit: n }),
603
655
  get: () => executeQuery(ctx, plan),
604
- first: () => Effect5.map(executeQuery(ctx, { ...plan, limit: 1 }), (results) => results.length > 0 ? Option2.some(results[0]) : Option2.none()),
656
+ first: () => Effect5.map(
657
+ executeQuery(ctx, { ...plan, limit: 1 }),
658
+ (results) => results.length > 0 ? Option2.some(results[0]) : Option2.none()
659
+ ),
605
660
  count: () => Effect5.map(executeQuery(ctx, plan), (results) => results.length),
606
661
  watch: () => watchQuery(ctx, plan)
607
662
  };
608
663
  }
609
- function createWhereClause(storage, watchCtx, collectionName, def, fieldName, mapRecord) {
610
- const ctx = { storage, watchCtx, collectionName, def, mapRecord };
664
+ function createWhereClause(storage, watchCtx, collectionName, def, fieldName, mapRecord2) {
665
+ const ctx = { storage, watchCtx, collectionName, def, mapRecord: mapRecord2 };
611
666
  const fieldDef = def.fields[fieldName];
612
- const isIndexed = def.indices.includes(fieldName) && fieldDef !== null && fieldDef !== undefined && fieldDef.kind !== "boolean";
667
+ const isIndexed = def.indices.includes(fieldName) && fieldDef !== null && fieldDef !== void 0 && fieldDef.kind !== "boolean";
613
668
  const withFilter = (filterFn, indexQuery) => {
614
669
  const plan = {
615
670
  ...emptyPlan(),
@@ -620,30 +675,51 @@ function createWhereClause(storage, watchCtx, collectionName, def, fieldName, ma
620
675
  return makeQueryBuilder(ctx, plan);
621
676
  };
622
677
  return {
623
- equals: (value) => withFilter((r) => r[fieldName] === value, isIndexed ? { field: fieldName, range: value, type: "value" } : undefined),
624
- above: (value) => withFilter((r) => r[fieldName] > value, isIndexed ? { field: fieldName, range: IDBKeyRange.lowerBound(value, true), type: "range" } : undefined),
625
- aboveOrEqual: (value) => withFilter((r) => r[fieldName] >= value, isIndexed ? { field: fieldName, range: IDBKeyRange.lowerBound(value, false), type: "range" } : undefined),
626
- below: (value) => withFilter((r) => r[fieldName] < value, isIndexed ? { field: fieldName, range: IDBKeyRange.upperBound(value, true), type: "range" } : undefined),
627
- belowOrEqual: (value) => withFilter((r) => r[fieldName] <= value, isIndexed ? { field: fieldName, range: IDBKeyRange.upperBound(value, false), type: "range" } : undefined),
678
+ equals: (value) => withFilter(
679
+ (r) => r[fieldName] === value,
680
+ isIndexed ? { field: fieldName, range: value, type: "value" } : void 0
681
+ ),
682
+ above: (value) => withFilter(
683
+ (r) => r[fieldName] > value,
684
+ isIndexed ? { field: fieldName, range: IDBKeyRange.lowerBound(value, true), type: "range" } : void 0
685
+ ),
686
+ aboveOrEqual: (value) => withFilter(
687
+ (r) => r[fieldName] >= value,
688
+ isIndexed ? { field: fieldName, range: IDBKeyRange.lowerBound(value, false), type: "range" } : void 0
689
+ ),
690
+ below: (value) => withFilter(
691
+ (r) => r[fieldName] < value,
692
+ isIndexed ? { field: fieldName, range: IDBKeyRange.upperBound(value, true), type: "range" } : void 0
693
+ ),
694
+ belowOrEqual: (value) => withFilter(
695
+ (r) => r[fieldName] <= value,
696
+ isIndexed ? { field: fieldName, range: IDBKeyRange.upperBound(value, false), type: "range" } : void 0
697
+ ),
628
698
  between: (lower, upper, options) => {
629
699
  const includeLower = options?.includeLower ?? true;
630
700
  const includeUpper = options?.includeUpper ?? true;
631
- return withFilter((r) => {
632
- const v = r[fieldName];
633
- const aboveLower = includeLower ? v >= lower : v > lower;
634
- const belowUpper = includeUpper ? v <= upper : v < upper;
635
- return aboveLower && belowUpper;
636
- }, isIndexed ? {
701
+ return withFilter(
702
+ (r) => {
703
+ const v = r[fieldName];
704
+ const aboveLower = includeLower ? v >= lower : v > lower;
705
+ const belowUpper = includeUpper ? v <= upper : v < upper;
706
+ return aboveLower && belowUpper;
707
+ },
708
+ isIndexed ? {
709
+ field: fieldName,
710
+ range: IDBKeyRange.bound(lower, upper, !includeLower, !includeUpper),
711
+ type: "range"
712
+ } : void 0
713
+ );
714
+ },
715
+ startsWith: (prefix) => withFilter(
716
+ (r) => typeof r[fieldName] === "string" && r[fieldName].startsWith(prefix),
717
+ isIndexed ? {
637
718
  field: fieldName,
638
- range: IDBKeyRange.bound(lower, upper, !includeLower, !includeUpper),
719
+ range: IDBKeyRange.bound(prefix, prefix + "\uFFFF", false, false),
639
720
  type: "range"
640
- } : undefined);
641
- },
642
- startsWith: (prefix) => withFilter((r) => typeof r[fieldName] === "string" && r[fieldName].startsWith(prefix), isIndexed ? {
643
- field: fieldName,
644
- range: IDBKeyRange.bound(prefix, prefix + "￿", false, false),
645
- type: "range"
646
- } : undefined),
721
+ } : void 0
722
+ ),
647
723
  anyOf: (values) => {
648
724
  const set = new Set(values);
649
725
  return withFilter((r) => set.has(r[fieldName]));
@@ -654,8 +730,8 @@ function createWhereClause(storage, watchCtx, collectionName, def, fieldName, ma
654
730
  }
655
731
  };
656
732
  }
657
- function createOrderByBuilder(storage, watchCtx, collectionName, def, fieldName, mapRecord) {
658
- const ctx = { storage, watchCtx, collectionName, def, mapRecord };
733
+ function createOrderByBuilder(storage, watchCtx, collectionName, def, fieldName, mapRecord2) {
734
+ const ctx = { storage, watchCtx, collectionName, def, mapRecord: mapRecord2 };
659
735
  const plan = {
660
736
  ...emptyPlan(),
661
737
  orderBy: { field: fieldName, direction: "asc" }
@@ -680,7 +756,7 @@ function replayState(recordId, events, stopAtId) {
680
756
  state = deepMerge(state, e.data);
681
757
  }
682
758
  }
683
- if (stopAtId !== undefined && e.id === stopAtId) {
759
+ if (stopAtId !== void 0 && e.id === stopAtId) {
684
760
  break;
685
761
  }
686
762
  }
@@ -698,8 +774,7 @@ function promoteToSnapshot(storage, collection2, recordId, target, allSorted) {
698
774
  function pruneEvents(storage, collection2, recordId, retention) {
699
775
  return Effect6.gen(function* () {
700
776
  const events = yield* storage.getEventsByRecord(collection2, recordId);
701
- if (events.length <= retention)
702
- return;
777
+ if (events.length <= retention) return;
703
778
  const sorted = [...events].sort((a, b) => b.createdAt - a.createdAt || (a.id < b.id ? 1 : -1));
704
779
  const retained = sorted.slice(0, retention);
705
780
  const toStrip = sorted.slice(retention);
@@ -708,8 +783,7 @@ function pruneEvents(storage, collection2, recordId, retention) {
708
783
  yield* promoteToSnapshot(storage, collection2, recordId, oldestRetained, sorted);
709
784
  }
710
785
  for (const e of toStrip) {
711
- if (e.data !== null)
712
- yield* storage.stripEventData(e.id);
786
+ if (e.data !== null) yield* storage.stripEventData(e.id);
713
787
  }
714
788
  });
715
789
  }
@@ -723,8 +797,7 @@ function createCollectionHandle(def, storage, watchCtx, validator, partialValida
723
797
  const commitEvent = (event) => Effect6.gen(function* () {
724
798
  yield* storage.putEvent(event);
725
799
  yield* applyEvent(storage, event);
726
- if (onWrite)
727
- yield* onWrite(event);
800
+ if (onWrite) yield* onWrite(event);
728
801
  yield* notifyChange(watchCtx, {
729
802
  collection: collectionName,
730
803
  recordId: event.recordId,
@@ -732,70 +805,84 @@ function createCollectionHandle(def, storage, watchCtx, validator, partialValida
732
805
  });
733
806
  });
734
807
  const handle = {
735
- add: (data) => withLog(Effect6.gen(function* () {
736
- const id = uuidv7();
737
- const fullRecord = { id, ...data };
738
- yield* validator(fullRecord);
739
- const event = {
740
- id: makeEventId(),
741
- collection: collectionName,
742
- recordId: id,
743
- kind: "c",
744
- data: fullRecord,
745
- createdAt: Date.now(),
746
- author: localAuthor
747
- };
748
- yield* commitEvent(event);
749
- yield* Effect6.logDebug("Record added", { collection: collectionName, recordId: id, data: fullRecord });
750
- return id;
751
- })),
752
- update: (id, data) => withLog(Effect6.gen(function* () {
753
- const existing = yield* storage.getRecord(collectionName, id);
754
- if (!existing || existing._d) {
755
- return yield* new NotFoundError({
808
+ add: (data) => withLog(
809
+ Effect6.gen(function* () {
810
+ const id = uuidv7();
811
+ const fullRecord = { id, ...data };
812
+ yield* validator(fullRecord);
813
+ const event = {
814
+ id: makeEventId(),
756
815
  collection: collectionName,
757
- id
816
+ recordId: id,
817
+ kind: "c",
818
+ data: fullRecord,
819
+ createdAt: Date.now(),
820
+ author: localAuthor
821
+ };
822
+ yield* commitEvent(event);
823
+ yield* Effect6.logDebug("Record added", {
824
+ collection: collectionName,
825
+ recordId: id,
826
+ data: fullRecord
758
827
  });
759
- }
760
- yield* partialValidator(data);
761
- const { _d, _u, _a, _e, ...existingFields } = existing;
762
- const merged = { ...existingFields, ...data, id };
763
- yield* validator(merged);
764
- const diff = deepDiff(existingFields, merged);
765
- const event = {
766
- id: makeEventId(),
767
- collection: collectionName,
768
- recordId: id,
769
- kind: "u",
770
- data: diff ?? { id },
771
- createdAt: Date.now(),
772
- author: localAuthor
773
- };
774
- yield* commitEvent(event);
775
- yield* Effect6.logDebug("Record updated", { collection: collectionName, recordId: id, data: diff });
776
- yield* pruneEvents(storage, collectionName, id, def.eventRetention);
777
- })),
778
- delete: (id) => withLog(Effect6.gen(function* () {
779
- const existing = yield* storage.getRecord(collectionName, id);
780
- if (!existing || existing._d) {
781
- return yield* new NotFoundError({
828
+ return id;
829
+ })
830
+ ),
831
+ update: (id, data) => withLog(
832
+ Effect6.gen(function* () {
833
+ const existing = yield* storage.getRecord(collectionName, id);
834
+ if (!existing || existing._d) {
835
+ return yield* new NotFoundError({
836
+ collection: collectionName,
837
+ id
838
+ });
839
+ }
840
+ yield* partialValidator(data);
841
+ const { _d, _u, _a, _e, ...existingFields } = existing;
842
+ const merged = { ...existingFields, ...data, id };
843
+ yield* validator(merged);
844
+ const diff = deepDiff(existingFields, merged);
845
+ const event = {
846
+ id: makeEventId(),
782
847
  collection: collectionName,
783
- id
848
+ recordId: id,
849
+ kind: "u",
850
+ data: diff ?? { id },
851
+ createdAt: Date.now(),
852
+ author: localAuthor
853
+ };
854
+ yield* commitEvent(event);
855
+ yield* Effect6.logDebug("Record updated", {
856
+ collection: collectionName,
857
+ recordId: id,
858
+ data: diff
784
859
  });
785
- }
786
- const event = {
787
- id: makeEventId(),
788
- collection: collectionName,
789
- recordId: id,
790
- kind: "d",
791
- data: null,
792
- createdAt: Date.now(),
793
- author: localAuthor
794
- };
795
- yield* commitEvent(event);
796
- yield* Effect6.logDebug("Record deleted", { collection: collectionName, recordId: id });
797
- yield* pruneEvents(storage, collectionName, id, def.eventRetention);
798
- })),
860
+ yield* pruneEvents(storage, collectionName, id, def.eventRetention);
861
+ })
862
+ ),
863
+ delete: (id) => withLog(
864
+ Effect6.gen(function* () {
865
+ const existing = yield* storage.getRecord(collectionName, id);
866
+ if (!existing || existing._d) {
867
+ return yield* new NotFoundError({
868
+ collection: collectionName,
869
+ id
870
+ });
871
+ }
872
+ const event = {
873
+ id: makeEventId(),
874
+ collection: collectionName,
875
+ recordId: id,
876
+ kind: "d",
877
+ data: null,
878
+ createdAt: Date.now(),
879
+ author: localAuthor
880
+ };
881
+ yield* commitEvent(event);
882
+ yield* Effect6.logDebug("Record deleted", { collection: collectionName, recordId: id });
883
+ yield* pruneEvents(storage, collectionName, id, def.eventRetention);
884
+ })
885
+ ),
799
886
  undo: (id) => Effect6.gen(function* () {
800
887
  const existing = yield* storage.getRecord(collectionName, id);
801
888
  if (!existing) {
@@ -836,15 +923,29 @@ function createCollectionHandle(def, storage, watchCtx, validator, partialValida
836
923
  return found ? Option3.some(mapRecord(found)) : Option3.none();
837
924
  }),
838
925
  count: () => Effect6.map(storage.getAllRecords(collectionName), (all) => all.filter((r) => !r._d).length),
839
- watch: () => watchCollection(watchCtx, storage, collectionName, undefined, mapRecord),
840
- where: (fieldName) => createWhereClause(storage, watchCtx, collectionName, def, fieldName, mapRecord),
841
- orderBy: (fieldName) => createOrderByBuilder(storage, watchCtx, collectionName, def, fieldName, mapRecord)
926
+ watch: () => watchCollection(watchCtx, storage, collectionName, void 0, mapRecord),
927
+ where: (fieldName) => createWhereClause(
928
+ storage,
929
+ watchCtx,
930
+ collectionName,
931
+ def,
932
+ fieldName,
933
+ mapRecord
934
+ ),
935
+ orderBy: (fieldName) => createOrderByBuilder(
936
+ storage,
937
+ watchCtx,
938
+ collectionName,
939
+ def,
940
+ fieldName,
941
+ mapRecord
942
+ )
842
943
  };
843
944
  return handle;
844
945
  }
845
946
 
846
947
  // src/sync/sync-service.ts
847
- import { Effect as Effect8, Layer, Option as Option5, References as References2, Ref as Ref3, Schedule } from "effect";
948
+ import { Duration, Effect as Effect8, Layer, Option as Option5, References as References2, Ref as Ref3, Schedule } from "effect";
848
949
  import { unwrapEvent } from "nostr-tools/nip59";
849
950
  import { GiftWrap as GiftWrap2 } from "nostr-tools/kinds";
850
951
 
@@ -860,8 +961,7 @@ var Mode = {
860
961
  Fingerprint: 1,
861
962
  IdList: 2
862
963
  };
863
-
864
- class WrappedBuffer {
964
+ var WrappedBuffer = class {
865
965
  constructor(buffer) {
866
966
  this._raw = new Uint8Array(buffer || 512);
867
967
  this.length = buffer ? buffer.length : 0;
@@ -873,10 +973,8 @@ class WrappedBuffer {
873
973
  return this._raw.byteLength;
874
974
  }
875
975
  extend(buf) {
876
- if (buf._raw)
877
- buf = buf.unwrap();
878
- if (typeof buf.length !== "number")
879
- throw Error("bad length");
976
+ if (buf._raw) buf = buf.unwrap();
977
+ if (typeof buf.length !== "number") throw Error("bad length");
880
978
  const targetSize = buf.length + this.length;
881
979
  if (this.capacity < targetSize) {
882
980
  const oldRaw = this._raw;
@@ -899,42 +997,36 @@ class WrappedBuffer {
899
997
  this.length -= n;
900
998
  return firstSubarray;
901
999
  }
902
- }
1000
+ };
903
1001
  function decodeVarInt(buf) {
904
1002
  let res = 0;
905
- while (true) {
906
- if (buf.length === 0)
907
- throw Error("parse ends prematurely");
1003
+ while (1) {
1004
+ if (buf.length === 0) throw Error("parse ends prematurely");
908
1005
  let byte = buf.shift();
909
1006
  res = res << 7 | byte & 127;
910
- if ((byte & 128) === 0)
911
- break;
1007
+ if ((byte & 128) === 0) break;
912
1008
  }
913
1009
  return res;
914
1010
  }
915
1011
  function encodeVarInt(n) {
916
- if (n === 0)
917
- return new WrappedBuffer([0]);
1012
+ if (n === 0) return new WrappedBuffer([0]);
918
1013
  let o = [];
919
1014
  while (n !== 0) {
920
1015
  o.push(n & 127);
921
1016
  n >>>= 7;
922
1017
  }
923
1018
  o.reverse();
924
- for (let i = 0;i < o.length - 1; i++)
925
- o[i] |= 128;
1019
+ for (let i = 0; i < o.length - 1; i++) o[i] |= 128;
926
1020
  return new WrappedBuffer(o);
927
1021
  }
928
1022
  function getByte(buf) {
929
1023
  return getBytes(buf, 1)[0];
930
1024
  }
931
1025
  function getBytes(buf, n) {
932
- if (buf.length < n)
933
- throw Error("parse ends prematurely");
1026
+ if (buf.length < n) throw Error("parse ends prematurely");
934
1027
  return buf.shiftN(n);
935
1028
  }
936
-
937
- class Accumulator {
1029
+ var Accumulator = class {
938
1030
  constructor() {
939
1031
  this.setToZero();
940
1032
  if (typeof window === "undefined") {
@@ -951,15 +1043,14 @@ class Accumulator {
951
1043
  let currCarry = 0, nextCarry = 0;
952
1044
  let p = new DataView(this.buf.buffer);
953
1045
  let po = new DataView(otherBuf.buffer);
954
- for (let i = 0;i < 8; i++) {
1046
+ for (let i = 0; i < 8; i++) {
955
1047
  let offset = i * 4;
956
1048
  let orig = p.getUint32(offset, true);
957
1049
  let otherV = po.getUint32(offset, true);
958
1050
  let next = orig;
959
1051
  next += currCarry;
960
1052
  next += otherV;
961
- if (next > 4294967295)
962
- nextCarry = 1;
1053
+ if (next > 4294967295) nextCarry = 1;
963
1054
  p.setUint32(offset, next & 4294967295, true);
964
1055
  currCarry = nextCarry;
965
1056
  nextCarry = 0;
@@ -967,7 +1058,7 @@ class Accumulator {
967
1058
  }
968
1059
  negate() {
969
1060
  let p = new DataView(this.buf.buffer);
970
- for (let i = 0;i < 8; i++) {
1061
+ for (let i = 0; i < 8; i++) {
971
1062
  let offset = i * 4;
972
1063
  p.setUint32(offset, ~p.getUint32(offset, true));
973
1064
  }
@@ -976,33 +1067,29 @@ class Accumulator {
976
1067
  this.add(one);
977
1068
  }
978
1069
  async getFingerprint(n) {
979
- let input = new WrappedBuffer;
1070
+ let input = new WrappedBuffer();
980
1071
  input.extend(this.buf);
981
1072
  input.extend(encodeVarInt(n));
982
1073
  let hash = await this.sha256(input.unwrap());
983
1074
  return hash.subarray(0, FINGERPRINT_SIZE);
984
1075
  }
985
- }
986
-
987
- class NegentropyStorageVector {
1076
+ };
1077
+ var NegentropyStorageVector = class {
988
1078
  constructor() {
989
1079
  this.items = [];
990
1080
  this.sealed = false;
991
1081
  }
992
1082
  insert(timestamp, id) {
993
- if (this.sealed)
994
- throw Error("already sealed");
1083
+ if (this.sealed) throw Error("already sealed");
995
1084
  id = loadInputBuffer(id);
996
- if (id.byteLength !== ID_SIZE)
997
- throw Error("bad id size for added item");
1085
+ if (id.byteLength !== ID_SIZE) throw Error("bad id size for added item");
998
1086
  this.items.push({ timestamp, id });
999
1087
  }
1000
1088
  seal() {
1001
- if (this.sealed)
1002
- throw Error("already sealed");
1089
+ if (this.sealed) throw Error("already sealed");
1003
1090
  this.sealed = true;
1004
1091
  this.items.sort(itemCompare);
1005
- for (let i = 1;i < this.items.length; i++) {
1092
+ for (let i = 1; i < this.items.length; i++) {
1006
1093
  if (itemCompare(this.items[i - 1], this.items[i]) === 0)
1007
1094
  throw Error("duplicate item inserted");
1008
1095
  }
@@ -1016,16 +1103,14 @@ class NegentropyStorageVector {
1016
1103
  }
1017
1104
  getItem(i) {
1018
1105
  this._checkSealed();
1019
- if (i >= this.items.length)
1020
- throw Error("out of range");
1106
+ if (i >= this.items.length) throw Error("out of range");
1021
1107
  return this.items[i];
1022
1108
  }
1023
1109
  iterate(begin, end, cb) {
1024
1110
  this._checkSealed();
1025
1111
  this._checkBounds(begin, end);
1026
- for (let i = begin;i < end; ++i) {
1027
- if (!cb(this.items[i], i))
1028
- break;
1112
+ for (let i = begin; i < end; ++i) {
1113
+ if (!cb(this.items[i], i)) break;
1029
1114
  }
1030
1115
  }
1031
1116
  findLowerBound(begin, end, bound) {
@@ -1034,7 +1119,7 @@ class NegentropyStorageVector {
1034
1119
  return this._binarySearch(this.items, begin, end, (a) => itemCompare(a, bound) < 0);
1035
1120
  }
1036
1121
  async fingerprint(begin, end) {
1037
- let out = new Accumulator;
1122
+ let out = new Accumulator();
1038
1123
  out.setToZero();
1039
1124
  this.iterate(begin, end, (item, i) => {
1040
1125
  out.add(item.id);
@@ -1043,12 +1128,10 @@ class NegentropyStorageVector {
1043
1128
  return await out.getFingerprint(end - begin);
1044
1129
  }
1045
1130
  _checkSealed() {
1046
- if (!this.sealed)
1047
- throw Error("not sealed");
1131
+ if (!this.sealed) throw Error("not sealed");
1048
1132
  }
1049
1133
  _checkBounds(begin, end) {
1050
- if (begin > end || end > this.items.length)
1051
- throw Error("bad range");
1134
+ if (begin > end || end > this.items.length) throw Error("bad range");
1052
1135
  }
1053
1136
  _binarySearch(arr, first, last, cmp) {
1054
1137
  let count = last - first;
@@ -1065,12 +1148,10 @@ class NegentropyStorageVector {
1065
1148
  }
1066
1149
  return first;
1067
1150
  }
1068
- }
1069
-
1070
- class Negentropy {
1151
+ };
1152
+ var Negentropy = class {
1071
1153
  constructor(storage, frameSizeLimit = 0) {
1072
- if (frameSizeLimit !== 0 && frameSizeLimit < 4096)
1073
- throw Error("frameSizeLimit too small");
1154
+ if (frameSizeLimit !== 0 && frameSizeLimit < 4096) throw Error("frameSizeLimit too small");
1074
1155
  this.storage = storage;
1075
1156
  this.frameSizeLimit = frameSizeLimit;
1076
1157
  this.lastTimestampIn = 0;
@@ -1080,10 +1161,9 @@ class Negentropy {
1080
1161
  return { timestamp, id: id ? id : new Uint8Array(0) };
1081
1162
  }
1082
1163
  async initiate() {
1083
- if (this.isInitiator)
1084
- throw Error("already initiated");
1164
+ if (this.isInitiator) throw Error("already initiated");
1085
1165
  this.isInitiator = true;
1086
- let output = new WrappedBuffer;
1166
+ let output = new WrappedBuffer();
1087
1167
  output.extend([PROTOCOL_VERSION]);
1088
1168
  await this.splitRange(0, this.storage.size(), this._bound(Number.MAX_VALUE), output);
1089
1169
  return this._renderOutput(output);
@@ -1095,23 +1175,24 @@ class Negentropy {
1095
1175
  let haveIds = [], needIds = [];
1096
1176
  query = new WrappedBuffer(loadInputBuffer(query));
1097
1177
  this.lastTimestampIn = this.lastTimestampOut = 0;
1098
- let fullOutput = new WrappedBuffer;
1178
+ let fullOutput = new WrappedBuffer();
1099
1179
  fullOutput.extend([PROTOCOL_VERSION]);
1100
1180
  let protocolVersion = getByte(query);
1101
1181
  if (protocolVersion < 96 || protocolVersion > 111)
1102
1182
  throw Error("invalid negentropy protocol version byte");
1103
1183
  if (protocolVersion !== PROTOCOL_VERSION) {
1104
1184
  if (this.isInitiator)
1105
- throw Error("unsupported negentropy protocol version requested: " + (protocolVersion - 96));
1106
- else
1107
- return [this._renderOutput(fullOutput), haveIds, needIds];
1185
+ throw Error(
1186
+ "unsupported negentropy protocol version requested: " + (protocolVersion - 96)
1187
+ );
1188
+ else return [this._renderOutput(fullOutput), haveIds, needIds];
1108
1189
  }
1109
1190
  let storageSize = this.storage.size();
1110
1191
  let prevBound = this._bound(0);
1111
1192
  let prevIndex = 0;
1112
1193
  let skip = false;
1113
1194
  while (query.length !== 0) {
1114
- let o = new WrappedBuffer;
1195
+ let o = new WrappedBuffer();
1115
1196
  let doSkip = () => {
1116
1197
  if (skip) {
1117
1198
  skip = false;
@@ -1137,10 +1218,9 @@ class Negentropy {
1137
1218
  } else if (mode === Mode.IdList) {
1138
1219
  let numIds = decodeVarInt(query);
1139
1220
  let theirElems = {};
1140
- for (let i = 0;i < numIds; i++) {
1221
+ for (let i = 0; i < numIds; i++) {
1141
1222
  let e = getBytes(query, ID_SIZE);
1142
- if (this.isInitiator)
1143
- theirElems[e] = e;
1223
+ if (this.isInitiator) theirElems[e] = e;
1144
1224
  }
1145
1225
  if (this.isInitiator) {
1146
1226
  skip = true;
@@ -1159,7 +1239,7 @@ class Negentropy {
1159
1239
  }
1160
1240
  } else {
1161
1241
  doSkip();
1162
- let responseIds = new WrappedBuffer;
1242
+ let responseIds = new WrappedBuffer();
1163
1243
  let numResponseIds = 0;
1164
1244
  let endBound = currBound;
1165
1245
  this.storage.iterate(lower, upper, (item, index) => {
@@ -1177,7 +1257,7 @@ class Negentropy {
1177
1257
  o.extend(encodeVarInt(numResponseIds));
1178
1258
  o.extend(responseIds);
1179
1259
  fullOutput.extend(o);
1180
- o = new WrappedBuffer;
1260
+ o = new WrappedBuffer();
1181
1261
  }
1182
1262
  } else {
1183
1263
  throw Error("unexpected mode");
@@ -1215,7 +1295,7 @@ class Negentropy {
1215
1295
  let itemsPerBucket = Math.floor(numElems / buckets);
1216
1296
  let bucketsWithExtra = numElems % buckets;
1217
1297
  let curr = lower;
1218
- for (let i = 0;i < buckets; i++) {
1298
+ for (let i = 0; i < buckets; i++) {
1219
1299
  let bucketSize = itemsPerBucket + (i < bucketsWithExtra ? 1 : 0);
1220
1300
  let ourFingerprint = await this.storage.fingerprint(curr, curr + bucketSize);
1221
1301
  curr += bucketSize;
@@ -1225,10 +1305,8 @@ class Negentropy {
1225
1305
  } else {
1226
1306
  let prevItem, currItem;
1227
1307
  this.storage.iterate(curr - 1, curr + 1, (item, index) => {
1228
- if (index === curr - 1)
1229
- prevItem = item;
1230
- else
1231
- currItem = item;
1308
+ if (index === curr - 1) prevItem = item;
1309
+ else currItem = item;
1232
1310
  return true;
1233
1311
  });
1234
1312
  nextBound = this.getMinimalBound(prevItem, currItem);
@@ -1241,13 +1319,13 @@ class Negentropy {
1241
1319
  }
1242
1320
  _renderOutput(o) {
1243
1321
  o = o.unwrap();
1244
- if (!this.wantUint8ArrayOutput)
1245
- o = uint8ArrayToHex(o);
1322
+ if (!this.wantUint8ArrayOutput) o = uint8ArrayToHex(o);
1246
1323
  return o;
1247
1324
  }
1248
1325
  exceededFrameSizeLimit(n) {
1249
1326
  return this.frameSizeLimit && n > this.frameSizeLimit - 200;
1250
1327
  }
1328
+ // Decoding
1251
1329
  decodeTimestampIn(encoded) {
1252
1330
  let timestamp = decodeVarInt(encoded);
1253
1331
  timestamp = timestamp === 0 ? Number.MAX_VALUE : timestamp - 1;
@@ -1262,11 +1340,11 @@ class Negentropy {
1262
1340
  decodeBound(encoded) {
1263
1341
  let timestamp = this.decodeTimestampIn(encoded);
1264
1342
  let len = decodeVarInt(encoded);
1265
- if (len > ID_SIZE)
1266
- throw Error("bound key too long");
1343
+ if (len > ID_SIZE) throw Error("bound key too long");
1267
1344
  let id = getBytes(encoded, len);
1268
1345
  return { timestamp, id };
1269
1346
  }
1347
+ // Encoding
1270
1348
  encodeTimestampOut(timestamp) {
1271
1349
  if (timestamp === Number.MAX_VALUE) {
1272
1350
  this.lastTimestampOut = Number.MAX_VALUE;
@@ -1278,7 +1356,7 @@ class Negentropy {
1278
1356
  return encodeVarInt(timestamp + 1);
1279
1357
  }
1280
1358
  encodeBound(key) {
1281
- let output = new WrappedBuffer;
1359
+ let output = new WrappedBuffer();
1282
1360
  output.extend(this.encodeTimestampOut(key.timestamp));
1283
1361
  output.extend(encodeVarInt(key.id.length));
1284
1362
  output.extend(key.id);
@@ -1291,30 +1369,24 @@ class Negentropy {
1291
1369
  let sharedPrefixBytes = 0;
1292
1370
  let currKey = curr.id;
1293
1371
  let prevKey = prev.id;
1294
- for (let i = 0;i < ID_SIZE; i++) {
1295
- if (currKey[i] !== prevKey[i])
1296
- break;
1372
+ for (let i = 0; i < ID_SIZE; i++) {
1373
+ if (currKey[i] !== prevKey[i]) break;
1297
1374
  sharedPrefixBytes++;
1298
1375
  }
1299
1376
  return this._bound(curr.timestamp, curr.id.subarray(0, sharedPrefixBytes + 1));
1300
1377
  }
1301
1378
  }
1302
- }
1379
+ };
1303
1380
  function loadInputBuffer(inp) {
1304
- if (typeof inp === "string")
1305
- inp = hexToUint8Array(inp);
1306
- else if (__proto__ !== Uint8Array.prototype)
1307
- inp = new Uint8Array(inp);
1381
+ if (typeof inp === "string") inp = hexToUint8Array(inp);
1382
+ else if (__proto__ !== Uint8Array.prototype) inp = new Uint8Array(inp);
1308
1383
  return inp;
1309
1384
  }
1310
1385
  function hexToUint8Array(h) {
1311
- if (h.startsWith("0x"))
1312
- h = h.substr(2);
1313
- if (h.length % 2 === 1)
1314
- throw Error("odd length of hex string");
1386
+ if (h.startsWith("0x")) h = h.substr(2);
1387
+ if (h.length % 2 === 1) throw Error("odd length of hex string");
1315
1388
  let arr = new Uint8Array(h.length / 2);
1316
- for (let i = 0;i < arr.length; i++)
1317
- arr[i] = parseInt(h.substr(i * 2, 2), 16);
1389
+ for (let i = 0; i < arr.length; i++) arr[i] = parseInt(h.substr(i * 2, 2), 16);
1318
1390
  return arr;
1319
1391
  }
1320
1392
  var uint8ArrayToHexLookupTable = new Array(256);
@@ -1337,28 +1409,24 @@ var uint8ArrayToHexLookupTable = new Array(256);
1337
1409
  "e",
1338
1410
  "f"
1339
1411
  ];
1340
- for (let i = 0;i < 256; i++) {
1412
+ for (let i = 0; i < 256; i++) {
1341
1413
  uint8ArrayToHexLookupTable[i] = hexAlphabet[i >>> 4 & 15] + hexAlphabet[i & 15];
1342
1414
  }
1343
1415
  }
1344
1416
  function uint8ArrayToHex(arr) {
1345
1417
  let out = "";
1346
- for (let i = 0, edx = arr.length;i < edx; i++) {
1418
+ for (let i = 0, edx = arr.length; i < edx; i++) {
1347
1419
  out += uint8ArrayToHexLookupTable[arr[i]];
1348
1420
  }
1349
1421
  return out;
1350
1422
  }
1351
1423
  function compareUint8Array(a, b) {
1352
- for (let i = 0;i < a.byteLength; i++) {
1353
- if (a[i] < b[i])
1354
- return -1;
1355
- if (a[i] > b[i])
1356
- return 1;
1357
- }
1358
- if (a.byteLength > b.byteLength)
1359
- return 1;
1360
- if (a.byteLength < b.byteLength)
1361
- return -1;
1424
+ for (let i = 0; i < a.byteLength; i++) {
1425
+ if (a[i] < b[i]) return -1;
1426
+ if (a[i] > b[i]) return 1;
1427
+ }
1428
+ if (a.byteLength > b.byteLength) return 1;
1429
+ if (a.byteLength < b.byteLength) return -1;
1362
1430
  return 0;
1363
1431
  }
1364
1432
  function itemCompare(a, b) {
@@ -1374,7 +1442,7 @@ import { GiftWrap } from "nostr-tools/kinds";
1374
1442
  function reconcileWithRelay(storage, relay, relayUrl, publicKeys) {
1375
1443
  return Effect7.gen(function* () {
1376
1444
  const allGiftWraps = yield* storage.getAllGiftWraps();
1377
- const storageVector = new NegentropyStorageVector;
1445
+ const storageVector = new NegentropyStorageVector();
1378
1446
  for (const gw of allGiftWraps) {
1379
1447
  storageVector.insert(gw.createdAt, hexToBytes2(gw.id));
1380
1448
  }
@@ -1387,7 +1455,7 @@ function reconcileWithRelay(storage, relay, relayUrl, publicKeys) {
1387
1455
  const allHaveIds = [];
1388
1456
  const allNeedIds = [];
1389
1457
  const subId = `neg-${Date.now()}`;
1390
- const initialMsg = yield* Effect7.try({
1458
+ const initialMsg = yield* Effect7.tryPromise({
1391
1459
  try: () => neg.initiate(),
1392
1460
  catch: (e) => new SyncError({
1393
1461
  message: `Negentropy initiate failed: ${e instanceof Error ? e.message : String(e)}`,
@@ -1398,9 +1466,8 @@ function reconcileWithRelay(storage, relay, relayUrl, publicKeys) {
1398
1466
  let currentMsg = initialMsg;
1399
1467
  while (currentMsg !== null) {
1400
1468
  const response = yield* relay.sendNegMsg(relayUrl, subId, filter, currentMsg);
1401
- if (response.msgHex === null)
1402
- break;
1403
- const reconcileResult = yield* Effect7.try({
1469
+ if (response.msgHex === null) break;
1470
+ const reconcileResult = yield* Effect7.tryPromise({
1404
1471
  try: () => neg.reconcile(response.msgHex),
1405
1472
  catch: (e) => new SyncError({
1406
1473
  message: `Negentropy reconcile failed: ${e instanceof Error ? e.message : String(e)}`,
@@ -1409,10 +1476,8 @@ function reconcileWithRelay(storage, relay, relayUrl, publicKeys) {
1409
1476
  })
1410
1477
  });
1411
1478
  const [nextMsg, haveIds, needIds] = reconcileResult;
1412
- for (const id of haveIds)
1413
- allHaveIds.push(id);
1414
- for (const id of needIds)
1415
- allNeedIds.push(id);
1479
+ for (const id of haveIds) allHaveIds.push(id);
1480
+ for (const id of needIds) allNeedIds.push(id);
1416
1481
  currentMsg = nextMsg;
1417
1482
  }
1418
1483
  yield* Effect7.logDebug("Negentropy reconciliation complete", {
@@ -1460,12 +1525,11 @@ function createRotation(epochStore, senderPrivateKey, senderPublicKey, remaining
1460
1525
  kind: 1,
1461
1526
  content: JSON.stringify(rotationData),
1462
1527
  tags: [["d", `_system:rotation:${epochId}`]],
1463
- created_at: Math.floor(Date.now() / 1000)
1528
+ created_at: Math.floor(Date.now() / 1e3)
1464
1529
  };
1465
1530
  const wrappedEvents = [];
1466
1531
  for (const memberPubkey of remainingMemberPubkeys) {
1467
- if (memberPubkey === senderPublicKey)
1468
- continue;
1532
+ if (memberPubkey === senderPublicKey) continue;
1469
1533
  const wrapped = wrapEvent(rumor, senderPrivateKey, memberPubkey);
1470
1534
  wrappedEvents.push(wrapped);
1471
1535
  }
@@ -1478,7 +1542,7 @@ function createRotation(epochStore, senderPrivateKey, senderPublicKey, remaining
1478
1542
  kind: 1,
1479
1543
  content: JSON.stringify(removalData),
1480
1544
  tags: [["d", `_system:removed:${epochId}`]],
1481
- created_at: Math.floor(Date.now() / 1000)
1545
+ created_at: Math.floor(Date.now() / 1e3)
1482
1546
  };
1483
1547
  const removalNotices = [];
1484
1548
  for (const removedPubkey of removedMemberPubkeys) {
@@ -1488,8 +1552,7 @@ function createRotation(epochStore, senderPrivateKey, senderPublicKey, remaining
1488
1552
  return { epoch, wrappedEvents, removalNotices };
1489
1553
  }
1490
1554
  function parseRotationEvent(content, dTag) {
1491
- if (!dTag.startsWith("_system:rotation:"))
1492
- return Option4.none();
1555
+ if (!dTag.startsWith("_system:rotation:")) return Option4.none();
1493
1556
  try {
1494
1557
  return Option4.some(decodeRotationData(content));
1495
1558
  } catch {
@@ -1497,8 +1560,7 @@ function parseRotationEvent(content, dTag) {
1497
1560
  }
1498
1561
  }
1499
1562
  function parseRemovalNotice(content, dTag) {
1500
- if (!dTag.startsWith("_system:removed:"))
1501
- return Option4.none();
1563
+ if (!dTag.startsWith("_system:removed:")) return Option4.none();
1502
1564
  try {
1503
1565
  return Option4.some(decodeRemovalNotice(content));
1504
1566
  } catch {
@@ -1518,99 +1580,124 @@ function createSyncHandle(storage, giftWrapHandle, relay, publishQueue, syncStat
1518
1580
  kind: "create"
1519
1581
  });
1520
1582
  const forkHandled = (effect) => {
1521
- Effect8.runFork(effect.pipe(Effect8.tapError((e) => Effect8.sync(() => onSyncError?.(e))), Effect8.ignore, Effect8.provide(logLayer), Effect8.forkIn(scope)));
1583
+ Effect8.runFork(
1584
+ effect.pipe(
1585
+ Effect8.tapError((e) => Effect8.sync(() => onSyncError?.(e))),
1586
+ Effect8.ignore,
1587
+ Effect8.provide(logLayer),
1588
+ Effect8.forkIn(scope)
1589
+ )
1590
+ );
1522
1591
  };
1523
1592
  let autoFlushActive = false;
1524
1593
  const autoFlushEffect = Effect8.gen(function* () {
1525
1594
  const size = yield* publishQueue.size();
1526
- if (size === 0)
1527
- return;
1595
+ if (size === 0) return;
1528
1596
  yield* syncStatus.set("syncing");
1529
1597
  yield* publishQueue.flush(relayUrls);
1530
1598
  const remaining = yield* publishQueue.size();
1531
- if (remaining > 0)
1532
- yield* Effect8.fail("pending");
1533
- }).pipe(Effect8.ensuring(syncStatus.set("idle")), Effect8.retry({ schedule: Schedule.exponential(5000).pipe(Schedule.jittered), times: 10 }), Effect8.ignore);
1599
+ if (remaining > 0) yield* Effect8.fail("pending");
1600
+ }).pipe(
1601
+ Effect8.ensuring(syncStatus.set("idle")),
1602
+ Effect8.retry({ schedule: Schedule.exponential(5e3).pipe(Schedule.jittered), times: 10 }),
1603
+ Effect8.ignore
1604
+ );
1534
1605
  const scheduleAutoFlush = () => {
1535
- if (autoFlushActive)
1536
- return;
1606
+ if (autoFlushActive) return;
1537
1607
  autoFlushActive = true;
1538
- forkHandled(autoFlushEffect.pipe(Effect8.ensuring(Effect8.sync(() => {
1539
- autoFlushActive = false;
1540
- }))));
1608
+ forkHandled(
1609
+ autoFlushEffect.pipe(
1610
+ Effect8.ensuring(
1611
+ Effect8.sync(() => {
1612
+ autoFlushActive = false;
1613
+ })
1614
+ )
1615
+ )
1616
+ );
1541
1617
  };
1542
1618
  const shouldRejectWrite = (authorPubkey) => Effect8.gen(function* () {
1543
1619
  const memberRecord = yield* storage.getRecord("_members", authorPubkey);
1544
- if (!memberRecord)
1545
- return false;
1620
+ if (!memberRecord) return false;
1546
1621
  return !!memberRecord.removedAt;
1547
1622
  });
1548
- const processGiftWrap = (remoteGw) => Effect8.gen(function* () {
1623
+ const storeGiftWrapShell = (gw) => storage.putGiftWrap({ id: gw.id, event: gw, createdAt: gw.created_at });
1624
+ const unwrapGiftWrap = (remoteGw) => Effect8.gen(function* () {
1549
1625
  const existing = yield* storage.getGiftWrap(remoteGw.id);
1550
- if (existing)
1551
- return null;
1552
- const unwrapResult = yield* Effect8.result(giftWrapHandle.unwrap(remoteGw));
1553
- if (unwrapResult._tag === "Failure") {
1554
- yield* storage.putGiftWrap({ id: remoteGw.id, createdAt: remoteGw.created_at });
1626
+ if (existing) return null;
1627
+ const rumor = yield* giftWrapHandle.unwrap(remoteGw).pipe(
1628
+ Effect8.orElseSucceed(() => null)
1629
+ );
1630
+ if (!rumor) {
1631
+ yield* storeGiftWrapShell(remoteGw);
1555
1632
  return null;
1556
1633
  }
1557
- const rumor = unwrapResult.success;
1558
1634
  const dTag = rumor.tags.find((t) => t[0] === "d")?.[1];
1559
1635
  if (!dTag) {
1560
- yield* storage.putGiftWrap({ id: remoteGw.id, createdAt: remoteGw.created_at });
1636
+ yield* storeGiftWrapShell(remoteGw);
1561
1637
  return null;
1562
1638
  }
1563
1639
  const colonIdx = dTag.indexOf(":");
1564
1640
  if (colonIdx === -1) {
1565
- yield* storage.putGiftWrap({ id: remoteGw.id, createdAt: remoteGw.created_at });
1641
+ yield* storeGiftWrapShell(remoteGw);
1642
+ return null;
1643
+ }
1644
+ const collection2 = dTag.substring(0, colonIdx);
1645
+ if (!knownCollections.has(collection2)) {
1646
+ yield* storeGiftWrapShell(remoteGw);
1566
1647
  return null;
1567
1648
  }
1568
- const collectionName = dTag.substring(0, colonIdx);
1569
- const recordId = dTag.substring(colonIdx + 1);
1649
+ return {
1650
+ giftWrap: remoteGw,
1651
+ rumor,
1652
+ collection: collection2,
1653
+ recordId: dTag.substring(colonIdx + 1)
1654
+ };
1655
+ });
1656
+ const applyUnwrappedEvent = (uw) => Effect8.gen(function* () {
1657
+ const { giftWrap: remoteGw, rumor, collection: collectionName, recordId } = uw;
1570
1658
  const retention = knownCollections.get(collectionName);
1571
- if (retention === undefined) {
1572
- yield* storage.putGiftWrap({ id: remoteGw.id, createdAt: remoteGw.created_at });
1659
+ if (retention === void 0) {
1660
+ yield* storeGiftWrapShell(remoteGw);
1573
1661
  return null;
1574
1662
  }
1575
1663
  if (rumor.pubkey) {
1576
1664
  const reject = yield* shouldRejectWrite(rumor.pubkey);
1577
1665
  if (reject) {
1578
- yield* Effect8.logWarning("Rejected write from removed member", { author: rumor.pubkey.slice(0, 12) });
1579
- yield* storage.putGiftWrap({ id: remoteGw.id, createdAt: remoteGw.created_at });
1666
+ yield* Effect8.logWarning("Rejected write from removed member", {
1667
+ author: rumor.pubkey.slice(0, 12)
1668
+ });
1669
+ yield* storeGiftWrapShell(remoteGw);
1580
1670
  return null;
1581
1671
  }
1582
1672
  }
1583
- let data = null;
1584
- let kind = "u";
1585
1673
  const parsed = yield* Effect8.try({
1586
1674
  try: () => JSON.parse(rumor.content),
1587
- catch: () => {
1588
- return;
1589
- }
1590
- }).pipe(Effect8.orElseSucceed(() => {
1591
- return;
1592
- }));
1593
- if (parsed === undefined) {
1594
- yield* storage.putGiftWrap({ id: remoteGw.id, createdAt: remoteGw.created_at });
1675
+ catch: () => void 0
1676
+ }).pipe(Effect8.orElseSucceed(() => void 0));
1677
+ if (parsed === void 0) {
1678
+ yield* storeGiftWrapShell(remoteGw);
1595
1679
  return null;
1596
1680
  }
1681
+ let data = null;
1682
+ let kind = "u";
1597
1683
  if (parsed === null || parsed._deleted) {
1598
1684
  kind = "d";
1599
1685
  } else {
1600
1686
  data = parsed;
1601
1687
  }
1602
- const author = rumor.pubkey || undefined;
1688
+ const author = rumor.pubkey || void 0;
1603
1689
  const event = {
1604
1690
  id: rumor.id,
1605
1691
  collection: collectionName,
1606
1692
  recordId,
1607
1693
  kind,
1608
1694
  data,
1609
- createdAt: rumor.created_at * 1000,
1695
+ createdAt: rumor.created_at * 1e3,
1610
1696
  author
1611
1697
  };
1612
1698
  yield* storage.putGiftWrap({
1613
1699
  id: remoteGw.id,
1700
+ event: remoteGw,
1614
1701
  createdAt: remoteGw.created_at
1615
1702
  });
1616
1703
  yield* storage.putEvent(event);
@@ -1618,12 +1705,89 @@ function createSyncHandle(storage, giftWrapHandle, relay, publishQueue, syncStat
1618
1705
  if (didApply && (kind === "u" || kind === "d")) {
1619
1706
  yield* pruneEvents(storage, collectionName, recordId, retention);
1620
1707
  }
1621
- yield* Effect8.logDebug("Processed gift wrap", { collection: collectionName, recordId, kind, author: author?.slice(0, 12) });
1708
+ yield* Effect8.logDebug("Processed gift wrap", {
1709
+ collection: collectionName,
1710
+ recordId,
1711
+ kind,
1712
+ author: author?.slice(0, 12)
1713
+ });
1622
1714
  if (author && onNewAuthor) {
1623
1715
  onNewAuthor(author);
1624
1716
  }
1625
1717
  return collectionName;
1626
1718
  });
1719
+ const reconcileRelay = (url, pubKeys) => Effect8.gen(function* () {
1720
+ yield* Effect8.logDebug("Syncing relay", { relay: url });
1721
+ const { haveIds, needIds } = yield* reconcileWithRelay(
1722
+ storage,
1723
+ relay,
1724
+ url,
1725
+ Array.from(pubKeys)
1726
+ ).pipe(
1727
+ Effect8.tapError((err) => Effect8.sync(() => onSyncError?.(err))),
1728
+ Effect8.orElseSucceed(() => ({ haveIds: [], needIds: [] }))
1729
+ );
1730
+ yield* Effect8.logDebug("Relay reconciliation result", {
1731
+ relay: url,
1732
+ need: needIds.length,
1733
+ have: haveIds.length
1734
+ });
1735
+ const events = needIds.length > 0 ? yield* relay.fetchEvents(needIds, url).pipe(
1736
+ Effect8.tapError((err) => Effect8.sync(() => onSyncError?.(err))),
1737
+ Effect8.orElseSucceed(() => [])
1738
+ ) : [];
1739
+ return {
1740
+ events,
1741
+ haveIds: haveIds.map((id) => ({ id, url }))
1742
+ };
1743
+ }).pipe(Effect8.withLogSpan("tablinum.reconcileRelay"));
1744
+ const syncAllRelays = (pubKeys, changedCollections) => Effect8.gen(function* () {
1745
+ const results = yield* Effect8.forEach(
1746
+ relayUrls,
1747
+ (url) => reconcileRelay(url, pubKeys),
1748
+ { concurrency: "unbounded" }
1749
+ );
1750
+ const seen = /* @__PURE__ */ new Set();
1751
+ const allGiftWraps = [];
1752
+ for (const { events } of results) {
1753
+ for (const ev of events) {
1754
+ if (!seen.has(ev.id)) {
1755
+ seen.add(ev.id);
1756
+ allGiftWraps.push(ev);
1757
+ }
1758
+ }
1759
+ }
1760
+ const unwrapped = [];
1761
+ for (const gw of allGiftWraps) {
1762
+ const result = yield* unwrapGiftWrap(gw).pipe(Effect8.orElseSucceed(() => null));
1763
+ if (result) unwrapped.push(result);
1764
+ }
1765
+ unwrapped.sort((a, b) => a.rumor.created_at - b.rumor.created_at || (a.rumor.id < b.rumor.id ? -1 : 1));
1766
+ for (const event of unwrapped) {
1767
+ const collection2 = yield* applyUnwrappedEvent(event).pipe(
1768
+ Effect8.orElseSucceed(() => null)
1769
+ );
1770
+ if (collection2) changedCollections.add(collection2);
1771
+ }
1772
+ const allHaveIds = results.flatMap((r) => r.haveIds);
1773
+ yield* Effect8.forEach(
1774
+ allHaveIds,
1775
+ ({ id, url }) => Effect8.gen(function* () {
1776
+ const gw = yield* storage.getGiftWrap(id);
1777
+ if (!gw?.event) return;
1778
+ yield* relay.publish(gw.event, [url]).pipe(
1779
+ Effect8.tapError((err) => Effect8.sync(() => onSyncError?.(err))),
1780
+ Effect8.ignore
1781
+ );
1782
+ }),
1783
+ { concurrency: "unbounded", discard: true }
1784
+ );
1785
+ }).pipe(Effect8.withLogSpan("tablinum.syncAllRelays"));
1786
+ const processGiftWrap = (remoteGw) => Effect8.gen(function* () {
1787
+ const uw = yield* unwrapGiftWrap(remoteGw);
1788
+ if (!uw) return null;
1789
+ return yield* applyUnwrappedEvent(uw);
1790
+ });
1627
1791
  const processRealtimeGiftWrap = (remoteGw) => Effect8.gen(function* () {
1628
1792
  const collection2 = yield* processGiftWrap(remoteGw).pipe(Effect8.orElseSucceed(() => null));
1629
1793
  if (collection2) {
@@ -1631,32 +1795,34 @@ function createSyncHandle(storage, giftWrapHandle, relay, publishQueue, syncStat
1631
1795
  }
1632
1796
  });
1633
1797
  const processRotationGiftWrap = (remoteGw) => Effect8.gen(function* () {
1634
- const unwrapResult = yield* Effect8.result(Effect8.try({
1635
- try: () => unwrapEvent(remoteGw, personalPrivateKey),
1636
- catch: (e) => new CryptoError({
1637
- message: `Rotation unwrap failed: ${e instanceof Error ? e.message : String(e)}`,
1638
- cause: e
1798
+ const unwrapResult = yield* Effect8.result(
1799
+ Effect8.try({
1800
+ try: () => unwrapEvent(remoteGw, personalPrivateKey),
1801
+ catch: (e) => new CryptoError({
1802
+ message: `Rotation unwrap failed: ${e instanceof Error ? e.message : String(e)}`,
1803
+ cause: e
1804
+ })
1639
1805
  })
1640
- }));
1641
- if (unwrapResult._tag === "Failure")
1642
- return false;
1806
+ );
1807
+ if (unwrapResult._tag === "Failure") return false;
1643
1808
  const rumor = unwrapResult.success;
1644
1809
  const dTag = rumor.tags.find((t) => t[0] === "d")?.[1];
1645
- if (!dTag)
1646
- return false;
1810
+ if (!dTag) return false;
1647
1811
  const removalNoticeOpt = parseRemovalNotice(rumor.content, dTag);
1648
1812
  if (Option5.isSome(removalNoticeOpt)) {
1649
- if (onRemoved)
1650
- onRemoved(removalNoticeOpt.value);
1813
+ if (onRemoved) onRemoved(removalNoticeOpt.value);
1651
1814
  return true;
1652
1815
  }
1653
1816
  const rotationDataOpt = parseRotationEvent(rumor.content, dTag);
1654
- if (Option5.isNone(rotationDataOpt))
1655
- return false;
1817
+ if (Option5.isNone(rotationDataOpt)) return false;
1656
1818
  const rotationData = rotationDataOpt.value;
1657
- if (epochStore.epochs.has(rotationData.epochId))
1658
- return false;
1659
- const epoch = createEpochKey(rotationData.epochId, rotationData.epochKey, rumor.pubkey || "", rotationData.parentEpoch);
1819
+ if (epochStore.epochs.has(rotationData.epochId)) return false;
1820
+ const epoch = createEpochKey(
1821
+ rotationData.epochId,
1822
+ rotationData.epochKey,
1823
+ rumor.pubkey || "",
1824
+ rotationData.parentEpoch
1825
+ );
1660
1826
  addEpoch(epochStore, epoch);
1661
1827
  epochStore.currentEpochId = epoch.id;
1662
1828
  let membersChanged = false;
@@ -1676,79 +1842,107 @@ function createSyncHandle(storage, giftWrapHandle, relay, publishQueue, syncStat
1676
1842
  membersChanged = true;
1677
1843
  }
1678
1844
  }
1679
- if (membersChanged && onMembersChanged)
1680
- onMembersChanged();
1845
+ if (membersChanged && onMembersChanged) onMembersChanged();
1681
1846
  yield* handle.addEpochSubscription(epoch.publicKey);
1682
1847
  return true;
1683
1848
  });
1684
- const subscribeAcrossRelays = (filter, onEvent) => Effect8.forEach(relayUrls, (url) => Effect8.gen(function* () {
1685
- yield* relay.subscribe(filter, url, (event) => {
1686
- forkHandled(onEvent(event));
1687
- }).pipe(Effect8.tapError((err) => Effect8.sync(() => onSyncError?.(err))), Effect8.ignore);
1688
- }), { discard: true });
1689
- const syncRelay = (url, pubKeys, changedCollections) => Effect8.gen(function* () {
1690
- yield* Effect8.logDebug("Syncing relay", { relay: url });
1691
- const reconcileResult = yield* Effect8.result(reconcileWithRelay(storage, relay, url, Array.from(pubKeys)));
1692
- if (reconcileResult._tag === "Failure") {
1693
- onSyncError?.(reconcileResult.failure);
1694
- return;
1695
- }
1696
- const { haveIds, needIds } = reconcileResult.success;
1697
- yield* Effect8.logDebug("Relay reconciliation result", { relay: url, need: needIds.length, have: haveIds.length });
1698
- if (needIds.length > 0) {
1699
- const fetched = yield* relay.fetchEvents(needIds, url).pipe(Effect8.tapError((err) => Effect8.sync(() => onSyncError?.(err))), Effect8.orElseSucceed(() => []));
1700
- const sorted = [...fetched].sort((a, b) => a.created_at - b.created_at);
1701
- yield* Effect8.forEach(sorted, (remoteGw) => Effect8.gen(function* () {
1702
- const collection2 = yield* processGiftWrap(remoteGw).pipe(Effect8.orElseSucceed(() => null));
1703
- if (collection2)
1704
- changedCollections.add(collection2);
1705
- }), { discard: true });
1706
- }
1707
- if (haveIds.length > 0) {
1708
- yield* Effect8.forEach(haveIds, (id) => Effect8.gen(function* () {
1709
- const gw = yield* storage.getGiftWrap(id);
1710
- if (!gw?.event)
1711
- return;
1712
- yield* relay.publish(gw.event, [url]).pipe(Effect8.andThen(storage.stripGiftWrapBlob(id)), Effect8.tapError((err) => Effect8.sync(() => onSyncError?.(err))), Effect8.ignore);
1713
- }), { discard: true });
1714
- }
1715
- }).pipe(Effect8.withLogSpan("tablinum.syncRelay"));
1849
+ const subscribeAcrossRelays = (filter, onEvent) => Effect8.forEach(
1850
+ relayUrls,
1851
+ (url) => Effect8.gen(function* () {
1852
+ yield* relay.subscribe(filter, url, (event) => {
1853
+ forkHandled(onEvent(event));
1854
+ }).pipe(
1855
+ Effect8.tapError((err) => Effect8.sync(() => onSyncError?.(err))),
1856
+ Effect8.ignore
1857
+ );
1858
+ }),
1859
+ { concurrency: "unbounded", discard: true }
1860
+ );
1861
+ let healingActive = false;
1862
+ const healingEffect = Effect8.gen(function* () {
1863
+ if (!healingActive) return;
1864
+ const status = yield* syncStatus.get();
1865
+ if (status === "syncing") return;
1866
+ yield* syncStatus.set("syncing");
1867
+ yield* Effect8.gen(function* () {
1868
+ const pubKeys = getSubscriptionPubKeys();
1869
+ const changedCollections = /* @__PURE__ */ new Set();
1870
+ yield* syncAllRelays(pubKeys, changedCollections);
1871
+ if (changedCollections.size > 0) {
1872
+ yield* notifyReplayComplete(watchCtx, [...changedCollections]);
1873
+ }
1874
+ }).pipe(Effect8.ensuring(syncStatus.set("idle")));
1875
+ }).pipe(Effect8.ignore);
1716
1876
  const handle = {
1717
1877
  sync: () => Effect8.gen(function* () {
1718
1878
  yield* Effect8.logInfo("Sync started");
1719
1879
  yield* syncStatus.set("syncing");
1720
1880
  yield* Ref3.set(watchCtx.replayingRef, true);
1721
- const changedCollections = new Set;
1881
+ const changedCollections = /* @__PURE__ */ new Set();
1722
1882
  yield* Effect8.gen(function* () {
1723
1883
  const pubKeys = getSubscriptionPubKeys();
1724
- yield* Effect8.forEach(relayUrls, (url) => syncRelay(url, pubKeys, changedCollections), {
1725
- discard: true
1726
- });
1884
+ yield* syncAllRelays(pubKeys, changedCollections);
1727
1885
  yield* publishQueue.flush(relayUrls).pipe(Effect8.ignore);
1728
- }).pipe(Effect8.ensuring(Effect8.gen(function* () {
1729
- yield* notifyReplayComplete(watchCtx, [...changedCollections]);
1730
- yield* syncStatus.set("idle");
1731
- })));
1886
+ }).pipe(
1887
+ Effect8.ensuring(
1888
+ Effect8.gen(function* () {
1889
+ yield* notifyReplayComplete(watchCtx, [...changedCollections]);
1890
+ yield* syncStatus.set("idle");
1891
+ })
1892
+ )
1893
+ );
1732
1894
  yield* Effect8.logInfo("Sync complete", { changed: [...changedCollections] });
1733
1895
  }).pipe(Effect8.withLogSpan("tablinum.sync")),
1734
1896
  publishLocal: (giftWrap) => Effect8.gen(function* () {
1735
- if (!giftWrap.event)
1736
- return;
1737
- 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);
1897
+ if (!giftWrap.event) return;
1898
+ yield* relay.publish(giftWrap.event, relayUrls).pipe(
1899
+ Effect8.tapError(
1900
+ () => storage.putGiftWrap(giftWrap).pipe(
1901
+ Effect8.andThen(publishQueue.enqueue(giftWrap.id)),
1902
+ Effect8.andThen(Effect8.sync(() => scheduleAutoFlush()))
1903
+ )
1904
+ ),
1905
+ Effect8.tapError((err) => Effect8.sync(() => onSyncError?.(err))),
1906
+ Effect8.ignore
1907
+ );
1738
1908
  }),
1739
1909
  startSubscription: () => Effect8.gen(function* () {
1740
1910
  const pubKeys = getSubscriptionPubKeys();
1741
1911
  yield* subscribeAcrossRelays({ kinds: [GiftWrap2], "#p": pubKeys }, processRealtimeGiftWrap);
1742
1912
  if (!pubKeys.includes(personalPublicKey)) {
1743
- yield* subscribeAcrossRelays({ kinds: [GiftWrap2], "#p": [personalPublicKey] }, (event) => Effect8.result(processRotationGiftWrap(event)).pipe(Effect8.asVoid));
1913
+ yield* subscribeAcrossRelays(
1914
+ { kinds: [GiftWrap2], "#p": [personalPublicKey] },
1915
+ (event) => Effect8.result(processRotationGiftWrap(event)).pipe(Effect8.asVoid)
1916
+ );
1744
1917
  }
1745
1918
  }),
1746
- addEpochSubscription: (publicKey) => subscribeAcrossRelays({ kinds: [GiftWrap2], "#p": [publicKey] }, processRealtimeGiftWrap)
1919
+ addEpochSubscription: (publicKey) => subscribeAcrossRelays({ kinds: [GiftWrap2], "#p": [publicKey] }, processRealtimeGiftWrap),
1920
+ startHealing: () => {
1921
+ if (healingActive) return;
1922
+ healingActive = true;
1923
+ forkHandled(
1924
+ Effect8.sleep(Duration.minutes(5)).pipe(
1925
+ Effect8.andThen(healingEffect),
1926
+ Effect8.repeat(Schedule.spaced(Duration.minutes(5))),
1927
+ Effect8.ensuring(Effect8.sync(() => {
1928
+ healingActive = false;
1929
+ }))
1930
+ )
1931
+ );
1932
+ },
1933
+ stopHealing: () => {
1934
+ healingActive = false;
1935
+ }
1747
1936
  };
1748
- forkHandled(publishQueue.size().pipe(Effect8.flatMap((size) => Effect8.sync(() => {
1749
- if (size > 0)
1750
- scheduleAutoFlush();
1751
- }))));
1937
+ forkHandled(
1938
+ publishQueue.size().pipe(
1939
+ Effect8.flatMap(
1940
+ (size) => Effect8.sync(() => {
1941
+ if (size > 0) scheduleAutoFlush();
1942
+ })
1943
+ )
1944
+ )
1945
+ );
1752
1946
  return handle;
1753
1947
  }
1754
1948
 
@@ -1804,9 +1998,14 @@ var decodeAuthorProfile = Schema6.decodeUnknownEffect(Schema6.fromJsonString(Aut
1804
1998
  function fetchAuthorProfile(relay, relayUrls, pubkey) {
1805
1999
  return Effect9.gen(function* () {
1806
2000
  for (const url of relayUrls) {
1807
- const result = yield* Effect9.result(relay.fetchByFilter({ kinds: [0], authors: [pubkey], limit: 1 }, url));
2001
+ const result = yield* Effect9.result(
2002
+ relay.fetchByFilter({ kinds: [0], authors: [pubkey], limit: 1 }, url)
2003
+ );
1808
2004
  if (result._tag === "Success" && result.success.length > 0) {
1809
- return yield* decodeAuthorProfile(result.success[0].content).pipe(Effect9.map(Option6.some), Effect9.orElseSucceed(() => Option6.none()));
2005
+ return yield* decodeAuthorProfile(result.success[0].content).pipe(
2006
+ Effect9.map(Option6.some),
2007
+ Effect9.orElseSucceed(() => Option6.none())
2008
+ );
1810
2009
  }
1811
2010
  }
1812
2011
  return Option6.none();
@@ -1815,45 +2014,44 @@ function fetchAuthorProfile(relay, relayUrls, pubkey) {
1815
2014
 
1816
2015
  // src/services/Identity.ts
1817
2016
  import { ServiceMap as ServiceMap3 } from "effect";
1818
-
1819
- class Identity extends ServiceMap3.Service()("tablinum/Identity") {
1820
- }
2017
+ var Identity = class extends ServiceMap3.Service()("tablinum/Identity") {
2018
+ };
1821
2019
 
1822
2020
  // src/services/EpochStore.ts
1823
2021
  import { ServiceMap as ServiceMap4 } from "effect";
1824
-
1825
- class EpochStore extends ServiceMap4.Service()("tablinum/EpochStore") {
1826
- }
2022
+ var EpochStore = class extends ServiceMap4.Service()(
2023
+ "tablinum/EpochStore"
2024
+ ) {
2025
+ };
1827
2026
 
1828
2027
  // src/services/Storage.ts
1829
2028
  import { ServiceMap as ServiceMap5 } from "effect";
1830
-
1831
- class Storage extends ServiceMap5.Service()("tablinum/Storage") {
1832
- }
2029
+ var Storage = class extends ServiceMap5.Service()("tablinum/Storage") {
2030
+ };
1833
2031
 
1834
2032
  // src/services/Relay.ts
1835
2033
  import { ServiceMap as ServiceMap6 } from "effect";
1836
-
1837
- class Relay extends ServiceMap6.Service()("tablinum/Relay") {
1838
- }
2034
+ var Relay = class extends ServiceMap6.Service()("tablinum/Relay") {
2035
+ };
1839
2036
 
1840
2037
  // src/services/GiftWrap.ts
1841
2038
  import { ServiceMap as ServiceMap7 } from "effect";
1842
-
1843
- class GiftWrap3 extends ServiceMap7.Service()("tablinum/GiftWrap") {
1844
- }
2039
+ var GiftWrap3 = class extends ServiceMap7.Service()("tablinum/GiftWrap") {
2040
+ };
1845
2041
 
1846
2042
  // src/services/PublishQueue.ts
1847
2043
  import { ServiceMap as ServiceMap8 } from "effect";
1848
-
1849
- class PublishQueue extends ServiceMap8.Service()("tablinum/PublishQueue") {
1850
- }
2044
+ var PublishQueue = class extends ServiceMap8.Service()(
2045
+ "tablinum/PublishQueue"
2046
+ ) {
2047
+ };
1851
2048
 
1852
2049
  // src/services/SyncStatus.ts
1853
2050
  import { ServiceMap as ServiceMap9 } from "effect";
1854
-
1855
- class SyncStatus extends ServiceMap9.Service()("tablinum/SyncStatus") {
1856
- }
2051
+ var SyncStatus = class extends ServiceMap9.Service()(
2052
+ "tablinum/SyncStatus"
2053
+ ) {
2054
+ };
1857
2055
 
1858
2056
  // src/layers/IdentityLive.ts
1859
2057
  import { Effect as Effect11, Layer as Layer2 } from "effect";
@@ -1894,47 +2092,59 @@ function createIdentity(suppliedKey) {
1894
2092
  }
1895
2093
 
1896
2094
  // src/layers/IdentityLive.ts
1897
- var IdentityLive = Layer2.effect(Identity, Effect11.gen(function* () {
1898
- const config = yield* Config;
1899
- const storage = yield* Storage;
1900
- const idbKey = yield* storage.getMeta("identity_key");
1901
- const resolvedKey = config.privateKey ?? (typeof idbKey === "string" && idbKey.length === 64 ? hexToBytes3(idbKey) : undefined);
1902
- const identity = yield* createIdentity(resolvedKey);
1903
- yield* storage.putMeta("identity_key", identity.exportKey());
1904
- yield* Effect11.logInfo("Identity loaded", {
1905
- publicKey: identity.publicKey.slice(0, 12) + "...",
1906
- source: config.privateKey ? "config" : resolvedKey ? "storage" : "generated"
1907
- });
1908
- return identity;
1909
- }));
2095
+ var IdentityLive = Layer2.effect(
2096
+ Identity,
2097
+ Effect11.gen(function* () {
2098
+ const config = yield* Config;
2099
+ const storage = yield* Storage;
2100
+ const idbKey = yield* storage.getMeta("identity_key");
2101
+ const resolvedKey = config.privateKey ?? (typeof idbKey === "string" && idbKey.length === 64 ? hexToBytes3(idbKey) : void 0);
2102
+ const identity = yield* createIdentity(resolvedKey);
2103
+ yield* storage.putMeta("identity_key", identity.exportKey());
2104
+ yield* Effect11.logInfo("Identity loaded", {
2105
+ publicKey: identity.publicKey.slice(0, 12) + "...",
2106
+ source: config.privateKey ? "config" : resolvedKey ? "storage" : "generated"
2107
+ });
2108
+ return identity;
2109
+ })
2110
+ );
1910
2111
 
1911
2112
  // src/layers/EpochStoreLive.ts
1912
2113
  import { Effect as Effect12, Layer as Layer3, Option as Option7 } from "effect";
1913
2114
  import { generateSecretKey as generateSecretKey2 } from "nostr-tools/pure";
1914
2115
  import { bytesToHex as bytesToHex3 } from "@noble/hashes/utils.js";
1915
- var EpochStoreLive = Layer3.effect(EpochStore, Effect12.gen(function* () {
1916
- const config = yield* Config;
1917
- const identity = yield* Identity;
1918
- const storage = yield* Storage;
1919
- const idbRaw = yield* storage.getMeta("epochs");
1920
- if (typeof idbRaw === "string") {
1921
- const idbStore = deserializeEpochStore(idbRaw);
1922
- if (Option7.isSome(idbStore)) {
1923
- yield* Effect12.logInfo("Epoch store loaded", { source: "storage", epochs: idbStore.value.epochs.size });
1924
- return idbStore.value;
1925
- }
1926
- }
1927
- if (config.epochKeys && config.epochKeys.length > 0) {
1928
- const store2 = createEpochStoreFromInputs(config.epochKeys);
1929
- yield* storage.putMeta("epochs", stringifyEpochStore(store2));
1930
- yield* Effect12.logInfo("Epoch store loaded", { source: "config", epochs: store2.epochs.size });
1931
- return store2;
1932
- }
1933
- const store = createEpochStoreFromInputs([{ epochId: EpochId("epoch-0"), key: bytesToHex3(generateSecretKey2()) }], { createdBy: identity.publicKey });
1934
- yield* storage.putMeta("epochs", stringifyEpochStore(store));
1935
- yield* Effect12.logInfo("Epoch store loaded", { source: "generated", epochs: store.epochs.size });
1936
- return store;
1937
- }));
2116
+ var EpochStoreLive = Layer3.effect(
2117
+ EpochStore,
2118
+ Effect12.gen(function* () {
2119
+ const config = yield* Config;
2120
+ const identity = yield* Identity;
2121
+ const storage = yield* Storage;
2122
+ const idbRaw = yield* storage.getMeta("epochs");
2123
+ if (typeof idbRaw === "string") {
2124
+ const idbStore = deserializeEpochStore(idbRaw);
2125
+ if (Option7.isSome(idbStore)) {
2126
+ yield* Effect12.logInfo("Epoch store loaded", {
2127
+ source: "storage",
2128
+ epochs: idbStore.value.epochs.size
2129
+ });
2130
+ return idbStore.value;
2131
+ }
2132
+ }
2133
+ if (config.epochKeys && config.epochKeys.length > 0) {
2134
+ const store2 = createEpochStoreFromInputs(config.epochKeys);
2135
+ yield* storage.putMeta("epochs", stringifyEpochStore(store2));
2136
+ yield* Effect12.logInfo("Epoch store loaded", { source: "config", epochs: store2.epochs.size });
2137
+ return store2;
2138
+ }
2139
+ const store = createEpochStoreFromInputs(
2140
+ [{ epochId: EpochId("epoch-0"), key: bytesToHex3(generateSecretKey2()) }],
2141
+ { createdBy: identity.publicKey }
2142
+ );
2143
+ yield* storage.putMeta("epochs", stringifyEpochStore(store));
2144
+ yield* Effect12.logInfo("Epoch store loaded", { source: "generated", epochs: store.epochs.size });
2145
+ return store;
2146
+ })
2147
+ );
1938
2148
 
1939
2149
  // src/layers/StorageLive.ts
1940
2150
  import { Effect as Effect14, Layer as Layer4 } from "effect";
@@ -1942,6 +2152,68 @@ import { Effect as Effect14, Layer as Layer4 } from "effect";
1942
2152
  // src/storage/idb.ts
1943
2153
  import { Effect as Effect13 } from "effect";
1944
2154
  import { openDB } from "idb";
2155
+
2156
+ // src/sync/compact-event.ts
2157
+ import { bytesToHex as bytesToHex4, hexToBytes as hexToBytes4 } from "@noble/hashes/utils.js";
2158
+ var VERSION = 1;
2159
+ var HEADER_SIZE = 133;
2160
+ function base64ToBytes(base64) {
2161
+ const binary = atob(base64);
2162
+ const bytes = new Uint8Array(binary.length);
2163
+ for (let i = 0; i < binary.length; i++) {
2164
+ bytes[i] = binary.charCodeAt(i);
2165
+ }
2166
+ return bytes;
2167
+ }
2168
+ function bytesToBase64(bytes) {
2169
+ let binary = "";
2170
+ for (let i = 0; i < bytes.length; i++) {
2171
+ binary += String.fromCharCode(bytes[i]);
2172
+ }
2173
+ return btoa(binary);
2174
+ }
2175
+ function packEvent(event) {
2176
+ const pubkey = hexToBytes4(event.pubkey);
2177
+ const sig = hexToBytes4(event.sig);
2178
+ const recipientTag = event.tags.find((t) => t[0] === "p");
2179
+ if (!recipientTag) throw new Error("Gift wrap missing #p tag");
2180
+ if (event.tags.some((t) => t[0] !== "p")) {
2181
+ throw new Error("Gift wrap has unexpected non-p tags; compact encoding would lose them");
2182
+ }
2183
+ const recipient = hexToBytes4(recipientTag[1]);
2184
+ const createdAtBuf = new Uint8Array(4);
2185
+ new DataView(createdAtBuf.buffer).setUint32(0, event.created_at, false);
2186
+ const content = base64ToBytes(event.content);
2187
+ const result = new Uint8Array(HEADER_SIZE + content.length);
2188
+ result[0] = VERSION;
2189
+ result.set(pubkey, 1);
2190
+ result.set(sig, 33);
2191
+ result.set(recipient, 97);
2192
+ result.set(createdAtBuf, 129);
2193
+ result.set(content, HEADER_SIZE);
2194
+ return result;
2195
+ }
2196
+ function unpackEvent(id, compact) {
2197
+ const version = compact[0];
2198
+ if (version !== VERSION) throw new Error(`Unknown compact event version: ${version}`);
2199
+ const pubkey = bytesToHex4(compact.slice(1, 33));
2200
+ const sig = bytesToHex4(compact.slice(33, 97));
2201
+ const recipient = bytesToHex4(compact.slice(97, 129));
2202
+ const dv = new DataView(compact.buffer, compact.byteOffset + 129, 4);
2203
+ const createdAt = dv.getUint32(0, false);
2204
+ const content = bytesToBase64(compact.slice(HEADER_SIZE));
2205
+ return {
2206
+ id,
2207
+ pubkey,
2208
+ sig,
2209
+ created_at: createdAt,
2210
+ kind: 1059,
2211
+ tags: [["p", recipient]],
2212
+ content
2213
+ };
2214
+ }
2215
+
2216
+ // src/storage/idb.ts
1945
2217
  var DB_NAME = "tablinum";
1946
2218
  function storeName(collection2) {
1947
2219
  return `col_${collection2}`;
@@ -1972,7 +2244,7 @@ function upgradeSchema(database, schema, tx) {
1972
2244
  if (!database.objectStoreNames.contains("giftwraps")) {
1973
2245
  database.createObjectStore("giftwraps", { keyPath: "id" });
1974
2246
  }
1975
- const expectedStores = new Set;
2247
+ const expectedStores = /* @__PURE__ */ new Set();
1976
2248
  for (const [, def] of Object.entries(schema)) {
1977
2249
  const sn = storeName(def.name);
1978
2250
  expectedStores.add(sn);
@@ -1986,12 +2258,10 @@ function upgradeSchema(database, schema, tx) {
1986
2258
  const existingIndices = new Set(Array.from(store.indexNames));
1987
2259
  const wantedIndices = new Set(def.indices ?? []);
1988
2260
  for (const idx of existingIndices) {
1989
- if (!wantedIndices.has(idx))
1990
- store.deleteIndex(idx);
2261
+ if (!wantedIndices.has(idx)) store.deleteIndex(idx);
1991
2262
  }
1992
2263
  for (const idx of wantedIndices) {
1993
- if (!existingIndices.has(idx))
1994
- store.createIndex(idx, idx);
2264
+ if (!existingIndices.has(idx)) store.createIndex(idx, idx);
1995
2265
  }
1996
2266
  }
1997
2267
  }
@@ -2006,6 +2276,13 @@ function openIDBStorage(dbName, schema) {
2006
2276
  return Effect13.gen(function* () {
2007
2277
  const name = dbName ?? DB_NAME;
2008
2278
  const schemaSig = computeSchemaSig(schema);
2279
+ if (typeof indexedDB === "undefined") {
2280
+ return yield* Effect13.fail(
2281
+ new StorageError({
2282
+ message: "IndexedDB is not available in this environment"
2283
+ })
2284
+ );
2285
+ }
2009
2286
  const probeDb = yield* Effect13.tryPromise({
2010
2287
  try: () => openDB(name),
2011
2288
  catch: (e) => new StorageError({ message: "Failed to open IndexedDB", cause: e })
@@ -2016,7 +2293,7 @@ function openIDBStorage(dbName, schema) {
2016
2293
  const storedSig = yield* Effect13.tryPromise({
2017
2294
  try: () => probeDb.get("_meta", "schema_sig"),
2018
2295
  catch: () => new StorageError({ message: "Failed to read schema meta" })
2019
- }).pipe(Effect13.catch(() => Effect13.succeed(undefined)));
2296
+ }).pipe(Effect13.catch(() => Effect13.succeed(void 0)));
2020
2297
  needsUpgrade = storedSig !== schemaSig;
2021
2298
  }
2022
2299
  probeDb.close();
@@ -2033,9 +2310,7 @@ function openIDBStorage(dbName, schema) {
2033
2310
  });
2034
2311
  yield* Effect13.addFinalizer(() => Effect13.sync(() => db.close()));
2035
2312
  const handle = {
2036
- putRecord: (collection2, record) => wrap("putRecord", () => db.put(storeName(collection2), record).then(() => {
2037
- return;
2038
- })),
2313
+ putRecord: (collection2, record) => wrap("putRecord", () => db.put(storeName(collection2), record).then(() => void 0)),
2039
2314
  getRecord: (collection2, id) => wrap("getRecord", () => db.get(storeName(collection2), id)),
2040
2315
  getAllRecords: (collection2) => wrap("getAllRecords", () => db.getAll(storeName(collection2))),
2041
2316
  countRecords: (collection2) => wrap("countRecords", () => db.count(storeName(collection2))),
@@ -2055,29 +2330,52 @@ function openIDBStorage(dbName, schema) {
2055
2330
  }
2056
2331
  return results;
2057
2332
  }),
2058
- putEvent: (event) => wrap("putEvent", () => db.put("events", event).then(() => {
2059
- return;
2060
- })),
2333
+ putEvent: (event) => wrap("putEvent", () => db.put("events", event).then(() => void 0)),
2061
2334
  getEvent: (id) => wrap("getEvent", () => db.get("events", id)),
2062
2335
  getAllEvents: () => wrap("getAllEvents", () => db.getAll("events")),
2063
- getEventsByRecord: (collection2, recordId) => wrap("getEventsByRecord", () => db.getAllFromIndex("events", "by-record", [collection2, recordId])),
2064
- putGiftWrap: (gw) => wrap("putGiftWrap", () => db.put("giftwraps", gw).then(() => {
2065
- return;
2066
- })),
2067
- getGiftWrap: (id) => wrap("getGiftWrap", () => db.get("giftwraps", id)),
2068
- getAllGiftWraps: () => wrap("getAllGiftWraps", () => db.getAll("giftwraps")),
2069
- deleteGiftWrap: (id) => wrap("deleteGiftWrap", () => db.delete("giftwraps", id).then(() => {
2070
- return;
2071
- })),
2072
- stripGiftWrapBlob: (id) => wrap("stripGiftWrapBlob", async () => {
2073
- const existing = await db.get("giftwraps", id);
2074
- if (existing) {
2075
- await db.put("giftwraps", { id: existing.id, createdAt: existing.createdAt });
2336
+ getEventsByRecord: (collection2, recordId) => wrap(
2337
+ "getEventsByRecord",
2338
+ () => db.getAllFromIndex("events", "by-record", [collection2, recordId])
2339
+ ),
2340
+ putGiftWrap: (gw) => wrap("putGiftWrap", async () => {
2341
+ if (gw.event) {
2342
+ const compact = packEvent(gw.event);
2343
+ await db.put("giftwraps", { id: gw.id, compact, createdAt: gw.createdAt });
2344
+ } else {
2345
+ await db.put("giftwraps", { id: gw.id, createdAt: gw.createdAt });
2076
2346
  }
2077
2347
  }),
2078
- deleteEvent: (id) => wrap("deleteEvent", () => db.delete("events", id).then(() => {
2079
- return;
2080
- })),
2348
+ getGiftWrap: (id) => wrap("getGiftWrap", async () => {
2349
+ const raw = await db.get("giftwraps", id);
2350
+ if (!raw) return void 0;
2351
+ if (raw.compact) {
2352
+ return { id: raw.id, event: unpackEvent(raw.id, raw.compact), createdAt: raw.createdAt };
2353
+ }
2354
+ if (raw.event) {
2355
+ const compact = packEvent(raw.event);
2356
+ await db.put("giftwraps", { id: raw.id, compact, createdAt: raw.createdAt });
2357
+ return { id: raw.id, event: raw.event, createdAt: raw.createdAt };
2358
+ }
2359
+ return { id: raw.id, createdAt: raw.createdAt };
2360
+ }),
2361
+ getAllGiftWraps: () => wrap("getAllGiftWraps", async () => {
2362
+ const raws = await db.getAll("giftwraps");
2363
+ const results = [];
2364
+ for (const raw of raws) {
2365
+ if (raw.compact) {
2366
+ results.push({ id: raw.id, event: unpackEvent(raw.id, raw.compact), createdAt: raw.createdAt });
2367
+ } else if (raw.event) {
2368
+ const compact = packEvent(raw.event);
2369
+ await db.put("giftwraps", { id: raw.id, compact, createdAt: raw.createdAt });
2370
+ results.push({ id: raw.id, event: raw.event, createdAt: raw.createdAt });
2371
+ } else {
2372
+ results.push({ id: raw.id, createdAt: raw.createdAt });
2373
+ }
2374
+ }
2375
+ return results;
2376
+ }),
2377
+ deleteGiftWrap: (id) => wrap("deleteGiftWrap", () => db.delete("giftwraps", id).then(() => void 0)),
2378
+ deleteEvent: (id) => wrap("deleteEvent", () => db.delete("events", id).then(() => void 0)),
2081
2379
  stripEventData: (id) => wrap("stripEventData", async () => {
2082
2380
  const existing = await db.get("events", id);
2083
2381
  if (existing) {
@@ -2085,9 +2383,7 @@ function openIDBStorage(dbName, schema) {
2085
2383
  }
2086
2384
  }),
2087
2385
  getMeta: (key) => wrap("getMeta", () => db.get("_meta", key)),
2088
- putMeta: (key, value) => wrap("putMeta", () => db.put("_meta", value, key).then(() => {
2089
- return;
2090
- })),
2386
+ putMeta: (key, value) => wrap("putMeta", () => db.put("_meta", value, key).then(() => void 0)),
2091
2387
  close: () => Effect13.sync(() => db.close())
2092
2388
  };
2093
2389
  return handle;
@@ -2095,15 +2391,18 @@ function openIDBStorage(dbName, schema) {
2095
2391
  }
2096
2392
 
2097
2393
  // src/layers/StorageLive.ts
2098
- var StorageLive = Layer4.effect(Storage, Effect14.gen(function* () {
2099
- const config = yield* Config;
2100
- const handle = yield* openIDBStorage(config.dbName, {
2101
- ...config.schema,
2102
- _members: membersCollectionDef
2103
- });
2104
- yield* Effect14.logInfo("Storage opened", { dbName: config.dbName });
2105
- return handle;
2106
- }));
2394
+ var StorageLive = Layer4.effect(
2395
+ Storage,
2396
+ Effect14.gen(function* () {
2397
+ const config = yield* Config;
2398
+ const handle = yield* openIDBStorage(config.dbName, {
2399
+ ...config.schema,
2400
+ _members: membersCollectionDef
2401
+ });
2402
+ yield* Effect14.logInfo("Storage opened", { dbName: config.dbName });
2403
+ return handle;
2404
+ })
2405
+ );
2107
2406
 
2108
2407
  // src/layers/RelayLive.ts
2109
2408
  import { Layer as Layer5 } from "effect";
@@ -2117,32 +2416,41 @@ var NegMessageFrameSchema = Schema7.Tuple([
2117
2416
  Schema7.String
2118
2417
  ]);
2119
2418
  var NegErrorFrameSchema = Schema7.Tuple([Schema7.Literal("NEG-ERR"), Schema7.String, Schema7.String]);
2120
- var decodeNegFrame = Schema7.decodeUnknownEffect(Schema7.fromJsonString(Schema7.Union([NegMessageFrameSchema, NegErrorFrameSchema])));
2419
+ var decodeNegFrame = Schema7.decodeUnknownEffect(
2420
+ Schema7.fromJsonString(Schema7.Union([NegMessageFrameSchema, NegErrorFrameSchema]))
2421
+ );
2121
2422
  function parseNegMessageFrame(data) {
2122
- return Effect15.runSync(decodeNegFrame(data).pipe(Effect15.map(Option8.some), Effect15.orElseSucceed(() => Option8.none())));
2423
+ return Effect15.runSync(
2424
+ decodeNegFrame(data).pipe(
2425
+ Effect15.map(Option8.some),
2426
+ Effect15.orElseSucceed(() => Option8.none())
2427
+ )
2428
+ );
2123
2429
  }
2124
2430
  function createRelayHandle() {
2125
2431
  return Effect15.gen(function* () {
2126
2432
  const relayScope = yield* Effect15.scope;
2127
2433
  const connections = yield* ScopedCache.make({
2128
2434
  capacity: 64,
2129
- lookup: (url) => Effect15.acquireRelease(Effect15.tryPromise({
2130
- try: () => Relay2.connect(url),
2131
- catch: (e) => new RelayError({
2132
- message: `Connect to ${url} failed: ${e instanceof Error ? e.message : String(e)}`,
2133
- url,
2134
- cause: e
2435
+ lookup: (url) => Effect15.acquireRelease(
2436
+ Effect15.tryPromise({
2437
+ try: () => Relay2.connect(url),
2438
+ catch: (e) => new RelayError({
2439
+ message: `Connect to ${url} failed: ${e instanceof Error ? e.message : String(e)}`,
2440
+ url,
2441
+ cause: e
2442
+ })
2443
+ }),
2444
+ (relay) => Effect15.sync(() => {
2445
+ relay.close();
2135
2446
  })
2136
- }), (relay) => Effect15.sync(() => {
2137
- relay.close();
2138
- }))
2447
+ )
2139
2448
  });
2140
- const connectedUrls = new Set;
2141
- const statusListeners = new Set;
2449
+ const connectedUrls = /* @__PURE__ */ new Set();
2450
+ const statusListeners = /* @__PURE__ */ new Set();
2142
2451
  const notifyStatus = () => {
2143
2452
  const status = { connectedUrls: [...connectedUrls] };
2144
- for (const listener of statusListeners)
2145
- listener(status);
2453
+ for (const listener of statusListeners) listener(status);
2146
2454
  };
2147
2455
  const markConnected = (url) => {
2148
2456
  if (!connectedUrls.has(url)) {
@@ -2156,66 +2464,98 @@ function createRelayHandle() {
2156
2464
  notifyStatus();
2157
2465
  }
2158
2466
  };
2159
- 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)));
2160
- 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))))));
2161
- const collectEvents = (url, filters) => withRelay(url, (relay) => Effect15.callback((resume) => {
2162
- const events = [];
2163
- let settled = false;
2164
- let timer;
2165
- let sub;
2166
- const cleanup = () => {
2167
- settled = true;
2168
- if (timer !== undefined) {
2169
- clearTimeout(timer);
2170
- timer = undefined;
2171
- }
2172
- sub?.close();
2173
- sub = undefined;
2174
- };
2175
- const fail = (e) => resume(Effect15.fail(new RelayError({
2176
- message: `Fetch from ${url} failed: ${e instanceof Error ? e.message : String(e)}`,
2177
- url,
2178
- cause: e
2179
- })));
2180
- try {
2181
- sub = relay.subscribe([...filters], {
2182
- onevent(evt) {
2183
- if (!settled) {
2184
- events.push(evt);
2467
+ const getRelay = (url) => ScopedCache.get(connections, url).pipe(
2468
+ Effect15.flatMap(
2469
+ (relay) => relay.connected === false ? ScopedCache.invalidate(connections, url).pipe(
2470
+ Effect15.andThen(ScopedCache.get(connections, url))
2471
+ ) : Effect15.succeed(relay)
2472
+ )
2473
+ );
2474
+ const withRelay = (url, run) => getRelay(url).pipe(
2475
+ Effect15.tap(() => Effect15.sync(() => markConnected(url))),
2476
+ Effect15.flatMap((relay) => run(relay)),
2477
+ Effect15.tapError(
2478
+ () => ScopedCache.invalidate(connections, url).pipe(
2479
+ Effect15.tap(() => Effect15.sync(() => markDisconnected(url)))
2480
+ )
2481
+ )
2482
+ );
2483
+ const collectEvents = (url, filters) => withRelay(
2484
+ url,
2485
+ (relay) => Effect15.callback((resume) => {
2486
+ const events = [];
2487
+ let settled = false;
2488
+ let timer;
2489
+ let sub;
2490
+ const cleanup = () => {
2491
+ settled = true;
2492
+ if (timer !== void 0) {
2493
+ clearTimeout(timer);
2494
+ timer = void 0;
2495
+ }
2496
+ sub?.close();
2497
+ sub = void 0;
2498
+ };
2499
+ const fail = (e) => resume(
2500
+ Effect15.fail(
2501
+ new RelayError({
2502
+ message: `Fetch from ${url} failed: ${e instanceof Error ? e.message : String(e)}`,
2503
+ url,
2504
+ cause: e
2505
+ })
2506
+ )
2507
+ );
2508
+ try {
2509
+ sub = relay.subscribe([...filters], {
2510
+ onevent(evt) {
2511
+ if (!settled) {
2512
+ events.push(evt);
2513
+ }
2514
+ },
2515
+ oneose() {
2516
+ if (settled) return;
2517
+ cleanup();
2518
+ resume(Effect15.succeed(events));
2185
2519
  }
2186
- },
2187
- oneose() {
2188
- if (settled)
2189
- return;
2520
+ });
2521
+ timer = setTimeout(() => {
2522
+ if (settled) return;
2190
2523
  cleanup();
2191
2524
  resume(Effect15.succeed(events));
2192
- }
2193
- });
2194
- timer = setTimeout(() => {
2195
- if (settled)
2196
- return;
2525
+ }, 1e4);
2526
+ } catch (e) {
2197
2527
  cleanup();
2198
- resume(Effect15.succeed(events));
2199
- }, 1e4);
2200
- } catch (e) {
2201
- cleanup();
2202
- fail(e);
2203
- }
2204
- return Effect15.sync(cleanup);
2205
- }));
2528
+ fail(e);
2529
+ }
2530
+ return Effect15.sync(cleanup);
2531
+ })
2532
+ );
2206
2533
  return {
2207
2534
  publish: (event, urls) => Effect15.gen(function* () {
2208
- const results = yield* Effect15.forEach(urls, (url) => Effect15.result(withRelay(url, (relay) => Effect15.tryPromise({
2209
- try: () => relay.publish(event),
2210
- catch: (e) => new RelayError({
2211
- message: `Publish to ${url} failed: ${e instanceof Error ? e.message : String(e)}`,
2212
- url,
2213
- cause: e
2214
- })
2215
- }).pipe(Effect15.timeoutOrElse({
2216
- duration: "10 seconds",
2217
- onTimeout: () => Effect15.fail(new RelayError({ message: `Publish to ${url} timed out`, url }))
2218
- })))), { concurrency: "unbounded" });
2535
+ const results = yield* Effect15.forEach(
2536
+ urls,
2537
+ (url) => Effect15.result(
2538
+ withRelay(
2539
+ url,
2540
+ (relay) => Effect15.tryPromise({
2541
+ try: () => relay.publish(event),
2542
+ catch: (e) => new RelayError({
2543
+ message: `Publish to ${url} failed: ${e instanceof Error ? e.message : String(e)}`,
2544
+ url,
2545
+ cause: e
2546
+ })
2547
+ }).pipe(
2548
+ Effect15.timeoutOrElse({
2549
+ duration: "10 seconds",
2550
+ onTimeout: () => Effect15.fail(
2551
+ new RelayError({ message: `Publish to ${url} timed out`, url })
2552
+ )
2553
+ })
2554
+ )
2555
+ )
2556
+ ),
2557
+ { concurrency: "unbounded" }
2558
+ );
2219
2559
  const failures = results.filter((r) => r._tag === "Failure");
2220
2560
  if (failures.length === urls.length && urls.length > 0) {
2221
2561
  return yield* new RelayError({
@@ -2224,90 +2564,104 @@ function createRelayHandle() {
2224
2564
  }
2225
2565
  }),
2226
2566
  fetchEvents: (ids, url) => Effect15.gen(function* () {
2227
- if (ids.length === 0)
2228
- return [];
2567
+ if (ids.length === 0) return [];
2229
2568
  return yield* collectEvents(url, [{ ids }]);
2230
2569
  }),
2231
2570
  fetchByFilter: (filter, url) => collectEvents(url, [filter]),
2232
- subscribe: (filter, url, onEvent) => withRelay(url, (relay) => Effect15.acquireRelease(Effect15.try({
2233
- try: () => relay.subscribe([filter], {
2234
- onevent(evt) {
2235
- onEvent(evt);
2236
- },
2237
- oneose() {}
2238
- }),
2239
- catch: (e) => new RelayError({
2240
- message: `Subscribe to ${url} failed: ${e instanceof Error ? e.message : String(e)}`,
2241
- url,
2242
- cause: e
2243
- })
2244
- }), (sub) => Effect15.sync(() => {
2245
- sub.close();
2246
- })).pipe(Effect15.provideService(Scope3.Scope, relayScope), Effect15.asVoid)),
2247
- sendNegMsg: (url, subId, filter, msgHex) => withRelay(url, (relay) => Effect15.callback((resume) => {
2248
- let settled = false;
2249
- let timer;
2250
- let sub;
2251
- let ws;
2252
- const cleanup = () => {
2253
- settled = true;
2254
- if (timer !== undefined) {
2255
- clearTimeout(timer);
2256
- timer = undefined;
2257
- }
2258
- sub?.close();
2259
- sub = undefined;
2260
- ws?.removeEventListener("message", handler);
2261
- ws = undefined;
2262
- };
2263
- const fail = (e) => resume(Effect15.fail(new RelayError({
2264
- message: `NIP-77 negotiation with ${url} failed: ${e instanceof Error ? e.message : String(e)}`,
2265
- url,
2266
- cause: e
2267
- })));
2268
- const handler = (msg) => {
2269
- if (settled || typeof msg.data !== "string")
2270
- return;
2271
- const frameOpt = parseNegMessageFrame(msg.data);
2272
- if (Option8.isNone(frameOpt) || frameOpt.value[1] !== subId)
2273
- return;
2274
- const frame = frameOpt.value;
2275
- cleanup();
2276
- if (frame[0] === "NEG-MSG") {
2277
- resume(Effect15.succeed({
2278
- msgHex: frame[2],
2279
- haveIds: [],
2280
- needIds: []
2281
- }));
2282
- return;
2283
- }
2284
- fail(new Error(`NEG-ERR: ${frame[2]}`));
2285
- };
2286
- try {
2287
- sub = relay.subscribe([filter], {
2288
- onevent() {},
2289
- oneose() {}
2290
- });
2291
- ws = relay._ws || relay.ws;
2292
- if (!ws) {
2571
+ subscribe: (filter, url, onEvent) => withRelay(
2572
+ url,
2573
+ (relay) => Effect15.acquireRelease(
2574
+ Effect15.try({
2575
+ try: () => relay.subscribe([filter], {
2576
+ onevent(evt) {
2577
+ onEvent(evt);
2578
+ },
2579
+ oneose() {
2580
+ }
2581
+ }),
2582
+ catch: (e) => new RelayError({
2583
+ message: `Subscribe to ${url} failed: ${e instanceof Error ? e.message : String(e)}`,
2584
+ url,
2585
+ cause: e
2586
+ })
2587
+ }),
2588
+ (sub) => Effect15.sync(() => {
2589
+ sub.close();
2590
+ })
2591
+ ).pipe(Effect15.provideService(Scope3.Scope, relayScope), Effect15.asVoid)
2592
+ ),
2593
+ sendNegMsg: (url, subId, filter, msgHex) => withRelay(
2594
+ url,
2595
+ (relay) => Effect15.callback((resume) => {
2596
+ let settled = false;
2597
+ let timer;
2598
+ let sub;
2599
+ let ws;
2600
+ const cleanup = () => {
2601
+ settled = true;
2602
+ if (timer !== void 0) {
2603
+ clearTimeout(timer);
2604
+ timer = void 0;
2605
+ }
2606
+ sub?.close();
2607
+ sub = void 0;
2608
+ ws?.removeEventListener("message", handler);
2609
+ ws = void 0;
2610
+ };
2611
+ const fail = (e) => resume(
2612
+ Effect15.fail(
2613
+ new RelayError({
2614
+ message: `NIP-77 negotiation with ${url} failed: ${e instanceof Error ? e.message : String(e)}`,
2615
+ url,
2616
+ cause: e
2617
+ })
2618
+ )
2619
+ );
2620
+ const handler = (msg) => {
2621
+ if (settled || typeof msg.data !== "string") return;
2622
+ const frameOpt = parseNegMessageFrame(msg.data);
2623
+ if (Option8.isNone(frameOpt) || frameOpt.value[1] !== subId) return;
2624
+ const frame = frameOpt.value;
2293
2625
  cleanup();
2294
- fail(new Error("Cannot access relay WebSocket"));
2295
- return Effect15.succeed(undefined);
2296
- }
2297
- timer = setTimeout(() => {
2298
- if (settled)
2626
+ if (frame[0] === "NEG-MSG") {
2627
+ resume(
2628
+ Effect15.succeed({
2629
+ msgHex: frame[2],
2630
+ haveIds: [],
2631
+ needIds: []
2632
+ })
2633
+ );
2299
2634
  return;
2635
+ }
2636
+ fail(new Error(`NEG-ERR: ${frame[2]}`));
2637
+ };
2638
+ try {
2639
+ sub = relay.subscribe([filter], {
2640
+ onevent() {
2641
+ },
2642
+ oneose() {
2643
+ }
2644
+ });
2645
+ ws = relay._ws || relay.ws;
2646
+ if (!ws) {
2647
+ cleanup();
2648
+ fail(new Error("Cannot access relay WebSocket"));
2649
+ return Effect15.succeed(void 0);
2650
+ }
2651
+ timer = setTimeout(() => {
2652
+ if (settled) return;
2653
+ cleanup();
2654
+ fail(new Error("NIP-77 negotiation timeout"));
2655
+ }, 3e4);
2656
+ ws.addEventListener("message", handler);
2657
+ ws.send(JSON.stringify(["NEG-OPEN", subId, filter, msgHex]));
2658
+ } catch (e) {
2300
2659
  cleanup();
2301
- fail(new Error("NIP-77 negotiation timeout"));
2302
- }, 30000);
2303
- ws.addEventListener("message", handler);
2304
- ws.send(JSON.stringify(["NEG-OPEN", subId, filter, msgHex]));
2305
- } catch (e) {
2306
- cleanup();
2307
- fail(e);
2308
- }
2309
- return Effect15.sync(cleanup);
2310
- })),
2660
+ fail(e);
2661
+ }
2662
+ return Effect15.sync(cleanup);
2663
+ })
2664
+ ),
2311
2665
  closeAll: () => ScopedCache.invalidateAll(connections),
2312
2666
  getStatus: () => ({ connectedUrls: [...connectedUrls] }),
2313
2667
  subscribeStatus: (callback) => {
@@ -2359,11 +2713,14 @@ function createEpochGiftWrapHandle(senderPrivateKey, epochStore) {
2359
2713
  }
2360
2714
 
2361
2715
  // src/layers/GiftWrapLive.ts
2362
- var GiftWrapLive = Layer6.effect(GiftWrap3, Effect17.gen(function* () {
2363
- const identity = yield* Identity;
2364
- const epochStore = yield* EpochStore;
2365
- return createEpochGiftWrapHandle(identity.privateKey, epochStore);
2366
- }));
2716
+ var GiftWrapLive = Layer6.effect(
2717
+ GiftWrap3,
2718
+ Effect17.gen(function* () {
2719
+ const identity = yield* Identity;
2720
+ const epochStore = yield* EpochStore;
2721
+ return createEpochGiftWrapHandle(identity.privateKey, epochStore);
2722
+ })
2723
+ );
2367
2724
 
2368
2725
  // src/layers/PublishQueueLive.ts
2369
2726
  import { Effect as Effect19, Layer as Layer7 } from "effect";
@@ -2377,12 +2734,11 @@ function persist(storage, pending) {
2377
2734
  function createPublishQueue(storage, relay) {
2378
2735
  return Effect18.gen(function* () {
2379
2736
  const stored = yield* storage.getMeta(META_KEY);
2380
- const initial = Array.isArray(stored) ? new Set(stored) : new Set;
2737
+ const initial = Array.isArray(stored) ? new Set(stored) : /* @__PURE__ */ new Set();
2381
2738
  const pendingRef = yield* Ref4.make(initial);
2382
- const listeners = new Set;
2739
+ const listeners = /* @__PURE__ */ new Set();
2383
2740
  const notify = (pending) => {
2384
- for (const listener of listeners)
2385
- listener(pending.size);
2741
+ for (const listener of listeners) listener(pending.size);
2386
2742
  };
2387
2743
  return {
2388
2744
  enqueue: (eventId) => Effect18.gen(function* () {
@@ -2396,13 +2752,11 @@ function createPublishQueue(storage, relay) {
2396
2752
  }),
2397
2753
  flush: (relayUrls) => Effect18.gen(function* () {
2398
2754
  const pending = yield* Ref4.get(pendingRef);
2399
- if (pending.size === 0)
2400
- return;
2401
- const succeeded = new Set;
2755
+ if (pending.size === 0) return;
2756
+ const succeeded = /* @__PURE__ */ new Set();
2402
2757
  let consecutiveFailures = 0;
2403
2758
  for (const eventId of pending) {
2404
- if (consecutiveFailures >= 3)
2405
- break;
2759
+ if (consecutiveFailures >= 3) break;
2406
2760
  const gw = yield* storage.getGiftWrap(eventId);
2407
2761
  if (!gw || !gw.event) {
2408
2762
  succeeded.add(eventId);
@@ -2412,7 +2766,6 @@ function createPublishQueue(storage, relay) {
2412
2766
  const result = yield* Effect18.result(relay.publish(gw.event, relayUrls));
2413
2767
  if (result._tag === "Success") {
2414
2768
  succeeded.add(eventId);
2415
- yield* storage.stripGiftWrapBlob(eventId);
2416
2769
  consecutiveFailures = 0;
2417
2770
  } else {
2418
2771
  consecutiveFailures++;
@@ -2440,11 +2793,14 @@ function createPublishQueue(storage, relay) {
2440
2793
  }
2441
2794
 
2442
2795
  // src/layers/PublishQueueLive.ts
2443
- var PublishQueueLive = Layer7.effect(PublishQueue, Effect19.gen(function* () {
2444
- const storage = yield* Storage;
2445
- const relay = yield* Relay;
2446
- return yield* createPublishQueue(storage, relay);
2447
- }));
2796
+ var PublishQueueLive = Layer7.effect(
2797
+ PublishQueue,
2798
+ Effect19.gen(function* () {
2799
+ const storage = yield* Storage;
2800
+ const relay = yield* Relay;
2801
+ return yield* createPublishQueue(storage, relay);
2802
+ })
2803
+ );
2448
2804
 
2449
2805
  // src/layers/SyncStatusLive.ts
2450
2806
  import { Layer as Layer8 } from "effect";
@@ -2454,13 +2810,12 @@ import { Effect as Effect20, SubscriptionRef } from "effect";
2454
2810
  function createSyncStatusHandle() {
2455
2811
  return Effect20.gen(function* () {
2456
2812
  const ref = yield* SubscriptionRef.make("idle");
2457
- const listeners = new Set;
2813
+ const listeners = /* @__PURE__ */ new Set();
2458
2814
  return {
2459
2815
  get: () => SubscriptionRef.get(ref),
2460
2816
  set: (status) => Effect20.gen(function* () {
2461
2817
  yield* SubscriptionRef.set(ref, status);
2462
- for (const listener of listeners)
2463
- listener(status);
2818
+ for (const listener of listeners) listener(status);
2464
2819
  }),
2465
2820
  subscribe: (callback) => {
2466
2821
  listeners.add(callback);
@@ -2475,8 +2830,7 @@ var SyncStatusLive = Layer8.effect(SyncStatus, createSyncStatusHandle());
2475
2830
 
2476
2831
  // src/layers/TablinumLive.ts
2477
2832
  function reportSyncError(onSyncError, error) {
2478
- if (!onSyncError)
2479
- return;
2833
+ if (!onSyncError) return;
2480
2834
  onSyncError(error instanceof Error ? error : new Error(String(error)));
2481
2835
  }
2482
2836
  function mapMemberRecord(record) {
@@ -2484,249 +2838,352 @@ function mapMemberRecord(record) {
2484
2838
  id: record.id,
2485
2839
  addedAt: record.addedAt,
2486
2840
  addedInEpoch: record.addedInEpoch,
2487
- ...record.name !== undefined ? { name: record.name } : {},
2488
- ...record.picture !== undefined ? { picture: record.picture } : {},
2489
- ...record.about !== undefined ? { about: record.about } : {},
2490
- ...record.nip05 !== undefined ? { nip05: record.nip05 } : {},
2491
- ...record.removedAt !== undefined ? { removedAt: record.removedAt } : {},
2492
- ...record.removedInEpoch !== undefined ? { removedInEpoch: record.removedInEpoch } : {}
2841
+ ...record.name !== void 0 ? { name: record.name } : {},
2842
+ ...record.picture !== void 0 ? { picture: record.picture } : {},
2843
+ ...record.about !== void 0 ? { about: record.about } : {},
2844
+ ...record.nip05 !== void 0 ? { nip05: record.nip05 } : {},
2845
+ ...record.removedAt !== void 0 ? { removedAt: record.removedAt } : {},
2846
+ ...record.removedInEpoch !== void 0 ? { removedInEpoch: record.removedInEpoch } : {}
2493
2847
  };
2494
2848
  }
2495
2849
  var IdentityWithDeps = IdentityLive.pipe(Layer9.provide(StorageLive));
2496
- var EpochStoreWithDeps = EpochStoreLive.pipe(Layer9.provide(IdentityWithDeps), Layer9.provide(StorageLive));
2497
- var GiftWrapWithDeps = GiftWrapLive.pipe(Layer9.provide(IdentityWithDeps), Layer9.provide(EpochStoreWithDeps));
2498
- var PublishQueueWithDeps = PublishQueueLive.pipe(Layer9.provide(StorageLive), Layer9.provide(RelayLive));
2499
- var AllServicesLive = Layer9.mergeAll(IdentityWithDeps, EpochStoreWithDeps, StorageLive, RelayLive, GiftWrapWithDeps, PublishQueueWithDeps, SyncStatusLive);
2500
- var TablinumLive = Layer9.effect(Tablinum, Effect21.gen(function* () {
2501
- const config = yield* Config;
2502
- const identity = yield* Identity;
2503
- const epochStore = yield* EpochStore;
2504
- const storage = yield* Storage;
2505
- const relay = yield* Relay;
2506
- const giftWrap = yield* GiftWrap3;
2507
- const publishQueue = yield* PublishQueue;
2508
- const syncStatus = yield* SyncStatus;
2509
- const scope = yield* Effect21.scope;
2510
- const logLayer = Layer9.succeed(References3.MinimumLogLevel, config.logLevel);
2511
- const pubsub = yield* PubSub2.unbounded();
2512
- const replayingRef = yield* Ref5.make(false);
2513
- const closedRef = yield* Ref5.make(false);
2514
- const watchCtx = { pubsub, replayingRef };
2515
- const schemaEntries = Object.entries(config.schema);
2516
- const allSchemaEntries = [...schemaEntries, ["_members", membersCollectionDef]];
2517
- const knownCollections = new Map(allSchemaEntries.map(([, def]) => [def.name, def.eventRetention]));
2518
- let notifyAuthor;
2519
- const syncHandle = createSyncHandle(storage, giftWrap, relay, publishQueue, syncStatus, watchCtx, config.relays, knownCollections, epochStore, identity.privateKey, identity.publicKey, scope, config.logLevel, config.onSyncError ? (error) => reportSyncError(config.onSyncError, error) : undefined, (pubkey) => notifyAuthor?.(pubkey), config.onRemoved, config.onMembersChanged);
2520
- const onWrite = (event) => Effect21.gen(function* () {
2521
- const content = event.kind === "d" ? JSON.stringify(null) : JSON.stringify(event.data);
2522
- const dTag = `${event.collection}:${event.recordId}`;
2523
- const wrapResult = yield* Effect21.result(giftWrap.wrap({
2524
- kind: 1,
2525
- content,
2526
- tags: [["d", dTag]],
2527
- created_at: Math.floor(event.createdAt / 1000)
2528
- }));
2529
- if (wrapResult._tag === "Failure") {
2530
- reportSyncError(config.onSyncError, wrapResult.failure);
2531
- return;
2532
- }
2533
- const gw = wrapResult.success;
2534
- yield* storage.putGiftWrap({ id: gw.id, createdAt: gw.created_at });
2535
- yield* Effect21.forkIn(Effect21.gen(function* () {
2536
- const publishResult = yield* Effect21.result(syncHandle.publishLocal({
2537
- id: gw.id,
2538
- event: gw,
2539
- createdAt: gw.created_at
2540
- }));
2541
- if (publishResult._tag === "Failure") {
2542
- reportSyncError(config.onSyncError, publishResult.failure);
2850
+ var EpochStoreWithDeps = EpochStoreLive.pipe(
2851
+ Layer9.provide(IdentityWithDeps),
2852
+ Layer9.provide(StorageLive)
2853
+ );
2854
+ var GiftWrapWithDeps = GiftWrapLive.pipe(
2855
+ Layer9.provide(IdentityWithDeps),
2856
+ Layer9.provide(EpochStoreWithDeps)
2857
+ );
2858
+ var PublishQueueWithDeps = PublishQueueLive.pipe(
2859
+ Layer9.provide(StorageLive),
2860
+ Layer9.provide(RelayLive)
2861
+ );
2862
+ var AllServicesLive = Layer9.mergeAll(
2863
+ IdentityWithDeps,
2864
+ EpochStoreWithDeps,
2865
+ StorageLive,
2866
+ RelayLive,
2867
+ GiftWrapWithDeps,
2868
+ PublishQueueWithDeps,
2869
+ SyncStatusLive
2870
+ );
2871
+ var TablinumLive = Layer9.effect(
2872
+ Tablinum,
2873
+ Effect21.gen(function* () {
2874
+ const config = yield* Config;
2875
+ const identity = yield* Identity;
2876
+ const epochStore = yield* EpochStore;
2877
+ const storage = yield* Storage;
2878
+ const relay = yield* Relay;
2879
+ const giftWrap = yield* GiftWrap3;
2880
+ const publishQueue = yield* PublishQueue;
2881
+ const syncStatus = yield* SyncStatus;
2882
+ const scope = yield* Effect21.scope;
2883
+ const logLayer = Layer9.succeed(References3.MinimumLogLevel, config.logLevel);
2884
+ const pubsub = yield* PubSub2.unbounded();
2885
+ const replayingRef = yield* Ref5.make(false);
2886
+ const closedRef = yield* Ref5.make(false);
2887
+ const watchCtx = { pubsub, replayingRef };
2888
+ const schemaEntries = Object.entries(config.schema);
2889
+ const allSchemaEntries = [...schemaEntries, ["_members", membersCollectionDef]];
2890
+ const knownCollections = new Map(
2891
+ allSchemaEntries.map(([, def]) => [def.name, def.eventRetention])
2892
+ );
2893
+ let notifyAuthor;
2894
+ const syncHandle = createSyncHandle(
2895
+ storage,
2896
+ giftWrap,
2897
+ relay,
2898
+ publishQueue,
2899
+ syncStatus,
2900
+ watchCtx,
2901
+ config.relays,
2902
+ knownCollections,
2903
+ epochStore,
2904
+ identity.privateKey,
2905
+ identity.publicKey,
2906
+ scope,
2907
+ config.logLevel,
2908
+ config.onSyncError ? (error) => reportSyncError(config.onSyncError, error) : void 0,
2909
+ (pubkey) => notifyAuthor?.(pubkey),
2910
+ config.onRemoved,
2911
+ config.onMembersChanged
2912
+ );
2913
+ const onWrite = (event) => Effect21.gen(function* () {
2914
+ const content = event.kind === "d" ? JSON.stringify(null) : JSON.stringify(event.data);
2915
+ const dTag = `${event.collection}:${event.recordId}`;
2916
+ const wrapResult = yield* Effect21.result(
2917
+ giftWrap.wrap({
2918
+ kind: 1,
2919
+ content,
2920
+ tags: [["d", dTag]],
2921
+ created_at: Math.floor(event.createdAt / 1e3)
2922
+ })
2923
+ );
2924
+ if (wrapResult._tag === "Failure") {
2925
+ reportSyncError(config.onSyncError, wrapResult.failure);
2926
+ return;
2543
2927
  }
2544
- }), scope);
2545
- });
2546
- const knownAuthors = new Set;
2547
- const putMemberRecord = (record) => Effect21.gen(function* () {
2548
- const existing = yield* storage.getRecord("_members", record.id);
2549
- const event = {
2550
- id: uuidv7(),
2551
- collection: "_members",
2552
- recordId: record.id,
2553
- kind: existing ? "u" : "c",
2554
- data: record,
2555
- createdAt: Date.now(),
2556
- author: identity.publicKey
2557
- };
2558
- yield* storage.putEvent(event);
2559
- yield* applyEvent(storage, event);
2560
- yield* onWrite(event);
2561
- yield* notifyChange(watchCtx, {
2562
- collection: "_members",
2563
- recordId: record.id,
2564
- kind: existing ? "update" : "create"
2928
+ const gw = wrapResult.success;
2929
+ yield* storage.putGiftWrap({ id: gw.id, event: gw, createdAt: gw.created_at });
2930
+ yield* Effect21.forkIn(
2931
+ Effect21.gen(function* () {
2932
+ const publishResult = yield* Effect21.result(
2933
+ syncHandle.publishLocal({
2934
+ id: gw.id,
2935
+ event: gw,
2936
+ createdAt: gw.created_at
2937
+ })
2938
+ );
2939
+ if (publishResult._tag === "Failure") {
2940
+ reportSyncError(config.onSyncError, publishResult.failure);
2941
+ }
2942
+ }),
2943
+ scope
2944
+ );
2565
2945
  });
2566
- config.onMembersChanged?.();
2567
- });
2568
- notifyAuthor = (pubkey) => {
2569
- if (knownAuthors.has(pubkey))
2570
- return;
2571
- knownAuthors.add(pubkey);
2572
- Effect21.runFork(Effect21.gen(function* () {
2573
- const existing = yield* storage.getRecord("_members", pubkey);
2574
- if (!existing) {
2575
- yield* putMemberRecord({
2576
- id: pubkey,
2577
- addedAt: Date.now(),
2578
- addedInEpoch: getCurrentEpoch(epochStore).id
2579
- });
2580
- }
2581
- const profileOpt = yield* fetchAuthorProfile(relay, config.relays, pubkey).pipe(Effect21.catchTag("RelayError", () => Effect21.succeed(Option9.none())));
2582
- if (Option9.isSome(profileOpt)) {
2583
- const current = yield* storage.getRecord("_members", pubkey);
2584
- if (current) {
2585
- yield* storage.putRecord("_members", {
2586
- ...current,
2587
- ...profileOpt.value
2588
- });
2589
- yield* notifyChange(watchCtx, {
2590
- collection: "_members",
2591
- recordId: pubkey,
2592
- kind: "update"
2593
- });
2594
- config.onMembersChanged?.();
2595
- }
2596
- }
2597
- }).pipe(Effect21.ignore, Effect21.provide(logLayer), Effect21.forkIn(scope)));
2598
- };
2599
- const handles = new Map;
2600
- for (const [, def] of allSchemaEntries) {
2601
- const validator = buildValidator(def.name, def);
2602
- const partialValidator = buildPartialValidator(def.name, def);
2603
- const handle = createCollectionHandle(def, storage, watchCtx, validator, partialValidator, uuidv7, identity.publicKey, onWrite, config.logLevel);
2604
- handles.set(def.name, handle);
2605
- }
2606
- yield* syncHandle.startSubscription();
2607
- yield* Effect21.logInfo("Tablinum ready", {
2608
- dbName: config.dbName,
2609
- collections: schemaEntries.map(([name]) => name),
2610
- relays: config.relays
2611
- });
2612
- const selfMember = yield* storage.getRecord("_members", identity.publicKey);
2613
- if (!selfMember) {
2614
- yield* putMemberRecord({
2615
- id: identity.publicKey,
2616
- addedAt: Date.now(),
2617
- addedInEpoch: getCurrentEpoch(epochStore).id
2946
+ const knownAuthors = /* @__PURE__ */ new Set();
2947
+ const putMemberRecord = (record) => Effect21.gen(function* () {
2948
+ const existing = yield* storage.getRecord("_members", record.id);
2949
+ const event = {
2950
+ id: uuidv7(),
2951
+ collection: "_members",
2952
+ recordId: record.id,
2953
+ kind: existing ? "u" : "c",
2954
+ data: record,
2955
+ createdAt: Date.now(),
2956
+ author: identity.publicKey
2957
+ };
2958
+ yield* storage.putEvent(event);
2959
+ yield* applyEvent(storage, event);
2960
+ yield* onWrite(event);
2961
+ yield* notifyChange(watchCtx, {
2962
+ collection: "_members",
2963
+ recordId: record.id,
2964
+ kind: existing ? "update" : "create"
2965
+ });
2966
+ config.onMembersChanged?.();
2618
2967
  });
2619
- }
2620
- const withLog = (effect) => Effect21.provideService(effect, References3.MinimumLogLevel, config.logLevel);
2621
- const ensureOpen = (effect) => withLog(Effect21.gen(function* () {
2622
- if (yield* Ref5.get(closedRef)) {
2623
- return yield* new StorageError({ message: "Database is closed" });
2624
- }
2625
- return yield* effect;
2626
- }));
2627
- const ensureSyncOpen = (effect) => withLog(Effect21.gen(function* () {
2628
- if (yield* Ref5.get(closedRef)) {
2629
- return yield* new SyncError({ message: "Database is closed", phase: "init" });
2630
- }
2631
- return yield* effect;
2632
- }));
2633
- const dbHandle = {
2634
- collection: (name) => {
2635
- const handle = handles.get(name);
2636
- if (!handle)
2637
- throw new Error(`Collection "${name}" not found in schema`);
2638
- return handle;
2639
- },
2640
- publicKey: identity.publicKey,
2641
- members: handles.get("_members"),
2642
- exportKey: () => identity.exportKey(),
2643
- exportInvite: () => ({
2644
- epochKeys: [...exportEpochKeys(epochStore)],
2645
- relays: [...config.relays],
2646
- dbName: config.dbName
2647
- }),
2648
- close: () => withLog(Effect21.gen(function* () {
2649
- if (yield* Ref5.get(closedRef))
2650
- return;
2651
- yield* Ref5.set(closedRef, true);
2652
- yield* Scope4.close(scope, Exit.void);
2653
- })),
2654
- rebuild: () => ensureOpen(rebuild(storage, allSchemaEntries.map(([, def]) => def.name))),
2655
- sync: () => ensureSyncOpen(syncHandle.sync()),
2656
- getSyncStatus: () => syncStatus.get(),
2657
- subscribeSyncStatus: (callback) => syncStatus.subscribe(callback),
2658
- pendingCount: () => publishQueue.size(),
2659
- subscribePendingCount: (callback) => publishQueue.subscribe(callback),
2660
- getRelayStatus: () => relay.getStatus(),
2661
- subscribeRelayStatus: (callback) => relay.subscribeStatus(callback),
2662
- addMember: (pubkey) => ensureOpen(Effect21.gen(function* () {
2663
- const existing = yield* storage.getRecord("_members", pubkey);
2664
- if (existing && !existing.removedAt)
2665
- return;
2968
+ notifyAuthor = (pubkey) => {
2969
+ if (knownAuthors.has(pubkey)) return;
2970
+ knownAuthors.add(pubkey);
2971
+ Effect21.runFork(
2972
+ Effect21.gen(function* () {
2973
+ const existing = yield* storage.getRecord("_members", pubkey);
2974
+ if (!existing) {
2975
+ yield* putMemberRecord({
2976
+ id: pubkey,
2977
+ addedAt: Date.now(),
2978
+ addedInEpoch: getCurrentEpoch(epochStore).id
2979
+ });
2980
+ }
2981
+ const profileOpt = yield* fetchAuthorProfile(relay, config.relays, pubkey).pipe(
2982
+ Effect21.catchTag("RelayError", () => Effect21.succeed(Option9.none()))
2983
+ );
2984
+ if (Option9.isSome(profileOpt)) {
2985
+ const current = yield* storage.getRecord("_members", pubkey);
2986
+ if (current) {
2987
+ yield* storage.putRecord("_members", {
2988
+ ...current,
2989
+ ...profileOpt.value
2990
+ });
2991
+ yield* notifyChange(watchCtx, {
2992
+ collection: "_members",
2993
+ recordId: pubkey,
2994
+ kind: "update"
2995
+ });
2996
+ config.onMembersChanged?.();
2997
+ }
2998
+ }
2999
+ }).pipe(Effect21.ignore, Effect21.provide(logLayer), Effect21.forkIn(scope))
3000
+ );
3001
+ };
3002
+ const handles = /* @__PURE__ */ new Map();
3003
+ for (const [, def] of allSchemaEntries) {
3004
+ const validator = buildValidator(def.name, def);
3005
+ const partialValidator = buildPartialValidator(def.name, def);
3006
+ const handle = createCollectionHandle(
3007
+ def,
3008
+ storage,
3009
+ watchCtx,
3010
+ validator,
3011
+ partialValidator,
3012
+ uuidv7,
3013
+ identity.publicKey,
3014
+ onWrite,
3015
+ config.logLevel
3016
+ );
3017
+ handles.set(def.name, handle);
3018
+ }
3019
+ yield* syncHandle.startSubscription();
3020
+ yield* Effect21.logInfo("Tablinum ready", {
3021
+ dbName: config.dbName,
3022
+ collections: schemaEntries.map(([name]) => name),
3023
+ relays: config.relays
3024
+ });
3025
+ const selfMember = yield* storage.getRecord("_members", identity.publicKey);
3026
+ if (!selfMember) {
2666
3027
  yield* putMemberRecord({
2667
- id: pubkey,
3028
+ id: identity.publicKey,
2668
3029
  addedAt: Date.now(),
2669
- addedInEpoch: getCurrentEpoch(epochStore).id,
2670
- ...existing ? { removedAt: undefined, removedInEpoch: undefined } : {}
2671
- });
2672
- })),
2673
- removeMember: (pubkey) => ensureOpen(Effect21.gen(function* () {
2674
- const allMembers = yield* storage.getAllRecords("_members");
2675
- const activeMembers = allMembers.filter((member) => !member.removedAt && member.id !== pubkey);
2676
- const activePubkeys = activeMembers.map((member) => member.id);
2677
- const result = createRotation(epochStore, identity.privateKey, identity.publicKey, activePubkeys, [pubkey]);
2678
- addEpoch(epochStore, result.epoch);
2679
- epochStore.currentEpochId = result.epoch.id;
2680
- yield* storage.putMeta("epochs", stringifyEpochStore(epochStore));
2681
- const memberRecord = yield* storage.getRecord("_members", pubkey);
2682
- yield* putMemberRecord({
2683
- ...memberRecord ?? {
2684
- id: pubkey,
2685
- addedAt: 0,
2686
- addedInEpoch: EpochId("epoch-0")
2687
- },
2688
- removedAt: Date.now(),
2689
- removedInEpoch: result.epoch.id
3030
+ addedInEpoch: getCurrentEpoch(epochStore).id
2690
3031
  });
2691
- 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 });
2692
- 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 });
2693
- yield* syncHandle.addEpochSubscription(result.epoch.publicKey);
2694
- })),
2695
- getMembers: () => ensureOpen(Effect21.gen(function* () {
2696
- const allRecords = yield* storage.getAllRecords("_members");
2697
- return allRecords.filter((record) => !record._d).map(mapMemberRecord);
2698
- })),
2699
- getProfile: () => ensureOpen(Effect21.gen(function* () {
2700
- const record = yield* storage.getRecord("_members", identity.publicKey);
2701
- if (!record)
2702
- return {};
2703
- const profile = {};
2704
- if (record.name !== undefined)
2705
- profile.name = record.name;
2706
- if (record.picture !== undefined)
2707
- profile.picture = record.picture;
2708
- if (record.about !== undefined)
2709
- profile.about = record.about;
2710
- if (record.nip05 !== undefined)
2711
- profile.nip05 = record.nip05;
2712
- return profile;
2713
- })),
2714
- setProfile: (profile) => ensureOpen(Effect21.gen(function* () {
2715
- const existing = yield* storage.getRecord("_members", identity.publicKey);
2716
- if (!existing) {
2717
- return yield* new ValidationError({ message: "Current user is not a member" });
2718
- }
2719
- const { _d, _u, _a, _e, ...memberFields } = existing;
2720
- yield* putMemberRecord({ ...memberFields, ...profile });
2721
- }))
2722
- };
2723
- return dbHandle;
2724
- }).pipe(Effect21.withLogSpan("tablinum.init"))).pipe(Layer9.provide(AllServicesLive));
3032
+ }
3033
+ const withLog = (effect) => Effect21.provideService(effect, References3.MinimumLogLevel, config.logLevel);
3034
+ const ensureOpen = (effect) => withLog(
3035
+ Effect21.gen(function* () {
3036
+ if (yield* Ref5.get(closedRef)) {
3037
+ return yield* new StorageError({ message: "Database is closed" });
3038
+ }
3039
+ return yield* effect;
3040
+ })
3041
+ );
3042
+ const ensureSyncOpen = (effect) => withLog(
3043
+ Effect21.gen(function* () {
3044
+ if (yield* Ref5.get(closedRef)) {
3045
+ return yield* new SyncError({ message: "Database is closed", phase: "init" });
3046
+ }
3047
+ return yield* effect;
3048
+ })
3049
+ );
3050
+ const dbHandle = {
3051
+ collection: (name) => {
3052
+ const handle = handles.get(name);
3053
+ if (!handle) throw new Error(`Collection "${name}" not found in schema`);
3054
+ return handle;
3055
+ },
3056
+ publicKey: identity.publicKey,
3057
+ members: handles.get("_members"),
3058
+ exportKey: () => identity.exportKey(),
3059
+ exportInvite: () => ({
3060
+ epochKeys: [...exportEpochKeys(epochStore)],
3061
+ relays: [...config.relays],
3062
+ dbName: config.dbName
3063
+ }),
3064
+ close: () => withLog(
3065
+ Effect21.gen(function* () {
3066
+ if (yield* Ref5.get(closedRef)) return;
3067
+ yield* Ref5.set(closedRef, true);
3068
+ syncHandle.stopHealing();
3069
+ yield* Scope4.close(scope, Exit.void);
3070
+ })
3071
+ ),
3072
+ rebuild: () => ensureOpen(
3073
+ rebuild(
3074
+ storage,
3075
+ allSchemaEntries.map(([, def]) => def.name)
3076
+ )
3077
+ ),
3078
+ sync: () => ensureSyncOpen(
3079
+ syncHandle.sync().pipe(
3080
+ Effect21.tap(
3081
+ () => Effect21.sync(() => {
3082
+ syncHandle.startHealing();
3083
+ })
3084
+ )
3085
+ )
3086
+ ),
3087
+ getSyncStatus: () => syncStatus.get(),
3088
+ subscribeSyncStatus: (callback) => syncStatus.subscribe(callback),
3089
+ pendingCount: () => publishQueue.size(),
3090
+ subscribePendingCount: (callback) => publishQueue.subscribe(callback),
3091
+ getRelayStatus: () => relay.getStatus(),
3092
+ subscribeRelayStatus: (callback) => relay.subscribeStatus(callback),
3093
+ addMember: (pubkey) => ensureOpen(
3094
+ Effect21.gen(function* () {
3095
+ const existing = yield* storage.getRecord("_members", pubkey);
3096
+ if (existing && !existing.removedAt) return;
3097
+ yield* putMemberRecord({
3098
+ id: pubkey,
3099
+ addedAt: Date.now(),
3100
+ addedInEpoch: getCurrentEpoch(epochStore).id,
3101
+ ...existing ? { removedAt: void 0, removedInEpoch: void 0 } : {}
3102
+ });
3103
+ })
3104
+ ),
3105
+ removeMember: (pubkey) => ensureOpen(
3106
+ Effect21.gen(function* () {
3107
+ const allMembers = yield* storage.getAllRecords("_members");
3108
+ const activeMembers = allMembers.filter(
3109
+ (member) => !member.removedAt && member.id !== pubkey
3110
+ );
3111
+ const activePubkeys = activeMembers.map((member) => member.id);
3112
+ const result = createRotation(
3113
+ epochStore,
3114
+ identity.privateKey,
3115
+ identity.publicKey,
3116
+ activePubkeys,
3117
+ [pubkey]
3118
+ );
3119
+ addEpoch(epochStore, result.epoch);
3120
+ epochStore.currentEpochId = result.epoch.id;
3121
+ yield* storage.putMeta("epochs", stringifyEpochStore(epochStore));
3122
+ const memberRecord = yield* storage.getRecord("_members", pubkey);
3123
+ yield* putMemberRecord({
3124
+ ...memberRecord ?? {
3125
+ id: pubkey,
3126
+ addedAt: 0,
3127
+ addedInEpoch: EpochId("epoch-0")
3128
+ },
3129
+ removedAt: Date.now(),
3130
+ removedInEpoch: result.epoch.id
3131
+ });
3132
+ yield* Effect21.forEach(
3133
+ result.wrappedEvents,
3134
+ (wrappedEvent) => relay.publish(wrappedEvent, [...config.relays]).pipe(
3135
+ Effect21.tapError((e) => Effect21.sync(() => reportSyncError(config.onSyncError, e))),
3136
+ Effect21.ignore
3137
+ ),
3138
+ { discard: true }
3139
+ );
3140
+ yield* Effect21.forEach(
3141
+ result.removalNotices,
3142
+ (notice) => relay.publish(notice, [...config.relays]).pipe(
3143
+ Effect21.tapError((e) => Effect21.sync(() => reportSyncError(config.onSyncError, e))),
3144
+ Effect21.ignore
3145
+ ),
3146
+ { discard: true }
3147
+ );
3148
+ yield* syncHandle.addEpochSubscription(result.epoch.publicKey);
3149
+ })
3150
+ ),
3151
+ getMembers: () => ensureOpen(
3152
+ Effect21.gen(function* () {
3153
+ const allRecords = yield* storage.getAllRecords("_members");
3154
+ return allRecords.filter((record) => !record._d).map(mapMemberRecord);
3155
+ })
3156
+ ),
3157
+ getProfile: () => ensureOpen(
3158
+ Effect21.gen(function* () {
3159
+ const record = yield* storage.getRecord("_members", identity.publicKey);
3160
+ if (!record) return {};
3161
+ const profile = {};
3162
+ if (record.name !== void 0) profile.name = record.name;
3163
+ if (record.picture !== void 0) profile.picture = record.picture;
3164
+ if (record.about !== void 0) profile.about = record.about;
3165
+ if (record.nip05 !== void 0) profile.nip05 = record.nip05;
3166
+ return profile;
3167
+ })
3168
+ ),
3169
+ setProfile: (profile) => ensureOpen(
3170
+ Effect21.gen(function* () {
3171
+ const existing = yield* storage.getRecord("_members", identity.publicKey);
3172
+ if (!existing) {
3173
+ return yield* new ValidationError({ message: "Current user is not a member" });
3174
+ }
3175
+ const { _d, _u, _a, _e, ...memberFields } = existing;
3176
+ yield* putMemberRecord({ ...memberFields, ...profile });
3177
+ })
3178
+ )
3179
+ };
3180
+ return dbHandle;
3181
+ }).pipe(Effect21.withLogSpan("tablinum.init"))
3182
+ ).pipe(Layer9.provide(AllServicesLive));
2725
3183
 
2726
3184
  // src/db/create-tablinum.ts
2727
3185
  function resolveLogLevel(input) {
2728
- if (input === undefined || input === "none")
2729
- return "None";
3186
+ if (input === void 0 || input === "none") return "None";
2730
3187
  switch (input) {
2731
3188
  case "debug":
2732
3189
  return "Debug";
@@ -2786,14 +3243,12 @@ function createDeferred() {
2786
3243
  promise,
2787
3244
  settled: () => settled,
2788
3245
  resolve: (value) => {
2789
- if (settled)
2790
- return;
3246
+ if (settled) return;
2791
3247
  settled = true;
2792
3248
  resolvePromise(value);
2793
3249
  },
2794
3250
  reject: (reason) => {
2795
- if (settled)
2796
- return;
3251
+ if (settled) return;
2797
3252
  settled = true;
2798
3253
  rejectPromise(reason);
2799
3254
  }
@@ -2815,7 +3270,9 @@ function wrapQueryBuilder(getBuilder, touchVersion, ready) {
2815
3270
  },
2816
3271
  first: () => {
2817
3272
  touchVersion();
2818
- return ready.then(() => Effect23.runPromise(Effect23.map(getBuilder().first(), Option10.getOrNull)));
3273
+ return ready.then(
3274
+ () => Effect23.runPromise(Effect23.map(getBuilder().first(), Option10.getOrNull))
3275
+ );
2819
3276
  },
2820
3277
  count: () => {
2821
3278
  touchVersion();
@@ -2847,7 +3304,9 @@ function wrapOrderByBuilder(getBuilder, touchVersion, ready) {
2847
3304
  },
2848
3305
  first: () => {
2849
3306
  touchVersion();
2850
- return ready.then(() => Effect23.runPromise(Effect23.map(getBuilder().first(), Option10.getOrNull)));
3307
+ return ready.then(
3308
+ () => Effect23.runPromise(Effect23.map(getBuilder().first(), Option10.getOrNull))
3309
+ );
2851
3310
  },
2852
3311
  count: () => {
2853
3312
  touchVersion();
@@ -2857,7 +3316,7 @@ function wrapOrderByBuilder(getBuilder, touchVersion, ready) {
2857
3316
  }
2858
3317
 
2859
3318
  // src/svelte/collection.svelte.ts
2860
- class Collection {
3319
+ var Collection = class {
2861
3320
  error = $state(null);
2862
3321
  #handle = null;
2863
3322
  #ready = createDeferred();
@@ -2865,15 +3324,16 @@ class Collection {
2865
3324
  #watchAbort = null;
2866
3325
  #watchFiber = null;
2867
3326
  #logLevel = "None";
3327
+ /** @internal */
2868
3328
  _bind(handle, logLevel = "None") {
2869
- if (this.#handle)
2870
- return;
3329
+ if (this.#handle) return;
2871
3330
  this.#handle = handle;
2872
3331
  this.#logLevel = logLevel;
2873
3332
  this.error = null;
2874
3333
  this.#settleReady();
2875
3334
  this.#startWatch();
2876
3335
  }
3336
+ /** @internal */
2877
3337
  _fail(err) {
2878
3338
  this.error = err;
2879
3339
  this.#settleReady(err);
@@ -2886,31 +3346,41 @@ class Collection {
2886
3346
  }
2887
3347
  }
2888
3348
  #startWatch() {
2889
- if (!this.#handle)
2890
- return;
2891
- const abort = new AbortController;
3349
+ if (!this.#handle) return;
3350
+ const abort = new AbortController();
2892
3351
  this.#watchAbort = abort;
2893
- this.#watchFiber = Effect24.runFork(Stream4.runForEach(this.#handle.watch(), (_records) => Effect24.sync(() => {
2894
- if (!abort.signal.aborted) {
2895
- this.#version++;
2896
- }
2897
- })).pipe(Effect24.catch((e) => Effect24.sync(() => {
2898
- if (!abort.signal.aborted) {
2899
- this.error = e instanceof Error ? e : new Error(String(e));
2900
- }
2901
- })), Effect24.provideService(References5.MinimumLogLevel, this.#logLevel)));
3352
+ this.#watchFiber = Effect24.runFork(
3353
+ Stream4.runForEach(
3354
+ this.#handle.watch(),
3355
+ (_records) => Effect24.sync(() => {
3356
+ if (!abort.signal.aborted) {
3357
+ this.#version++;
3358
+ }
3359
+ })
3360
+ ).pipe(
3361
+ Effect24.catch(
3362
+ (e) => Effect24.sync(() => {
3363
+ if (!abort.signal.aborted) {
3364
+ this.error = e instanceof Error ? e : new Error(String(e));
3365
+ }
3366
+ })
3367
+ ),
3368
+ Effect24.provideService(References5.MinimumLogLevel, this.#logLevel)
3369
+ )
3370
+ );
2902
3371
  }
2903
3372
  #touchVersion = () => {
2904
- this.#version;
3373
+ void this.#version;
2905
3374
  };
2906
3375
  #handleOrThrow = () => {
2907
- if (this.#handle)
2908
- return this.#handle;
3376
+ if (this.#handle) return this.#handle;
2909
3377
  throw this.error ?? new ClosedError({ message: "Collection is not ready" });
2910
3378
  };
2911
3379
  #run = async (getEffect) => {
2912
3380
  await this.#ready.promise;
2913
- return Effect24.runPromise(getEffect().pipe(Effect24.provideService(References5.MinimumLogLevel, this.#logLevel)));
3381
+ return Effect24.runPromise(
3382
+ getEffect().pipe(Effect24.provideService(References5.MinimumLogLevel, this.#logLevel))
3383
+ );
2914
3384
  };
2915
3385
  add = (data) => {
2916
3386
  return this.#run(() => this.#handleOrThrow().add(data));
@@ -2929,7 +3399,11 @@ class Collection {
2929
3399
  return this.#run(() => this.#handleOrThrow().get(id));
2930
3400
  }
2931
3401
  this.#touchVersion();
2932
- return this.#run(() => Stream4.runHead(this.#handleOrThrow().watch()).pipe(Effect24.map((opt) => Option11.getOrElse(opt, () => []))));
3402
+ return this.#run(
3403
+ () => Stream4.runHead(this.#handleOrThrow().watch()).pipe(
3404
+ Effect24.map((opt) => Option11.getOrElse(opt, () => []))
3405
+ )
3406
+ );
2933
3407
  }
2934
3408
  first = () => {
2935
3409
  this.#touchVersion();
@@ -2940,11 +3414,20 @@ class Collection {
2940
3414
  return this.#run(() => this.#handleOrThrow().count());
2941
3415
  };
2942
3416
  where = (field2) => {
2943
- return wrapWhereClause(() => this.#handleOrThrow().where(field2), this.#touchVersion, this.#ready.promise);
3417
+ return wrapWhereClause(
3418
+ () => this.#handleOrThrow().where(field2),
3419
+ this.#touchVersion,
3420
+ this.#ready.promise
3421
+ );
2944
3422
  };
2945
3423
  orderBy = (field2) => {
2946
- return wrapOrderByBuilder(() => this.#handleOrThrow().orderBy(field2), this.#touchVersion, this.#ready.promise);
3424
+ return wrapOrderByBuilder(
3425
+ () => this.#handleOrThrow().orderBy(field2),
3426
+ this.#touchVersion,
3427
+ this.#ready.promise
3428
+ );
2947
3429
  };
3430
+ /** @internal */
2948
3431
  _destroy(reason = new ClosedError({ message: "Collection is closed" })) {
2949
3432
  if (this.#watchAbort) {
2950
3433
  this.#watchAbort.abort();
@@ -2958,10 +3441,10 @@ class Collection {
2958
3441
  this.error ??= reason;
2959
3442
  this.#settleReady(this.error);
2960
3443
  }
2961
- }
3444
+ };
2962
3445
 
2963
3446
  // src/svelte/tablinum.svelte.ts
2964
- class Tablinum2 {
3447
+ var Tablinum2 = class {
2965
3448
  status = $state("initializing");
2966
3449
  syncStatus = $state("idle");
2967
3450
  pendingCount = $state(0);
@@ -2970,8 +3453,8 @@ class Tablinum2 {
2970
3453
  ready;
2971
3454
  #handle = null;
2972
3455
  #scope = null;
2973
- #collections = new Map;
2974
- #members = new Collection;
3456
+ #collections = /* @__PURE__ */ new Map();
3457
+ #members = new Collection();
2975
3458
  #unsubscribeSyncStatus = null;
2976
3459
  #unsubscribePendingCount = null;
2977
3460
  #unsubscribeRelayStatus = null;
@@ -3000,16 +3483,20 @@ class Tablinum2 {
3000
3483
  const handle = this.#requireReady();
3001
3484
  try {
3002
3485
  this.error = null;
3003
- return await Effect25.runPromise(run(handle).pipe(Effect25.provideService(References6.MinimumLogLevel, this.#logLevel)));
3486
+ return await Effect25.runPromise(
3487
+ run(handle).pipe(Effect25.provideService(References6.MinimumLogLevel, this.#logLevel))
3488
+ );
3004
3489
  } catch (e) {
3005
3490
  this.error = e instanceof Error ? e : new Error(String(e));
3006
3491
  throw this.error;
3007
3492
  }
3008
3493
  };
3009
- async#init(config) {
3494
+ async #init(config) {
3010
3495
  const scope = Effect25.runSync(Scope6.make());
3011
3496
  try {
3012
- const handle = await Effect25.runPromise(createTablinum(config).pipe(Effect25.provideService(Scope6.Scope, scope)));
3497
+ const handle = await Effect25.runPromise(
3498
+ createTablinum(config).pipe(Effect25.provideService(Scope6.Scope, scope))
3499
+ );
3013
3500
  if (this.#closed) {
3014
3501
  await Effect25.runPromise(Scope6.close(scope, Exit2.void));
3015
3502
  this.#scope = null;
@@ -3032,7 +3519,8 @@ class Tablinum2 {
3032
3519
  this.status = "ready";
3033
3520
  this.#settleReady();
3034
3521
  } catch (e) {
3035
- await Effect25.runPromise(Scope6.close(scope, Exit2.fail(e))).catch(() => {});
3522
+ await Effect25.runPromise(Scope6.close(scope, Exit2.fail(e))).catch(() => {
3523
+ });
3036
3524
  const err = e instanceof Error ? e : new Error(String(e));
3037
3525
  this.error = err;
3038
3526
  this.status = "error";
@@ -3055,7 +3543,7 @@ class Tablinum2 {
3055
3543
  }
3056
3544
  let col = this.#collections.get(name);
3057
3545
  if (!col) {
3058
- col = new Collection;
3546
+ col = new Collection();
3059
3547
  this.#collections.set(name, col);
3060
3548
  if (this.#handle) {
3061
3549
  col._bind(this.#handle.collection(name), this.#logLevel);
@@ -3076,8 +3564,7 @@ class Tablinum2 {
3076
3564
  return this.#requireReady().exportInvite();
3077
3565
  }
3078
3566
  close = async () => {
3079
- if (this.#closed)
3080
- return;
3567
+ if (this.#closed) return;
3081
3568
  this.#closed = true;
3082
3569
  const closeError = new ClosedError({
3083
3570
  message: this.#readyState.settled() ? "Tablinum is closed" : "Tablinum was closed before initialization completed"
@@ -3095,8 +3582,7 @@ class Tablinum2 {
3095
3582
  this.#unsubscribeRelayStatus = null;
3096
3583
  }
3097
3584
  this.#members._destroy(closeError);
3098
- for (const col of this.#collections.values())
3099
- col._destroy(closeError);
3585
+ for (const col of this.#collections.values()) col._destroy(closeError);
3100
3586
  this.#collections.clear();
3101
3587
  const handle = this.#handle;
3102
3588
  const scope = this.#scope;
@@ -3119,19 +3605,19 @@ class Tablinum2 {
3119
3605
  getMembers = async () => this.#runHandleEffect((handle) => handle.getMembers());
3120
3606
  getProfile = async () => this.#runHandleEffect((handle) => handle.getProfile());
3121
3607
  setProfile = async (profile) => this.#runHandleEffect((handle) => handle.setProfile(profile));
3122
- }
3608
+ };
3123
3609
  export {
3124
- field,
3125
- encodeInvite,
3126
- decodeInvite,
3127
- collection,
3128
- ValidationError,
3129
- Tablinum2 as Tablinum,
3130
- SyncError,
3131
- StorageError,
3132
- RelayError,
3133
- NotFoundError,
3134
- CryptoError,
3610
+ ClosedError,
3135
3611
  Collection,
3136
- ClosedError
3612
+ CryptoError,
3613
+ NotFoundError,
3614
+ RelayError,
3615
+ StorageError,
3616
+ SyncError,
3617
+ Tablinum2 as Tablinum,
3618
+ ValidationError,
3619
+ collection,
3620
+ decodeInvite,
3621
+ encodeInvite,
3622
+ field
3137
3623
  };