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