tablinum 0.6.4 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/db/create-tablinum.d.ts +1 -0
- package/dist/db/database-handle.d.ts +2 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +552 -412
- package/dist/storage/idb.d.ts +1 -0
- package/dist/svelte/index.svelte.d.ts +1 -0
- package/dist/svelte/index.svelte.js +565 -415
- package/dist/svelte/tablinum.svelte.d.ts +2 -0
- package/dist/sync/deletion.d.ts +10 -0
- package/package.json +1 -1
|
@@ -230,9 +230,6 @@ var NotFoundError = class extends Data.TaggedError("NotFoundError") {
|
|
|
230
230
|
var ClosedError = class extends Data.TaggedError("ClosedError") {
|
|
231
231
|
};
|
|
232
232
|
|
|
233
|
-
// src/svelte/tablinum.svelte.ts
|
|
234
|
-
import { Effect as Effect25, Exit as Exit2, References as References6, Scope as Scope6 } from "effect";
|
|
235
|
-
|
|
236
233
|
// src/db/create-tablinum.ts
|
|
237
234
|
import { Effect as Effect22, Layer as Layer10, References as References4, ServiceMap as ServiceMap10 } from "effect";
|
|
238
235
|
|
|
@@ -261,6 +258,257 @@ function resolveRuntimeConfig(source) {
|
|
|
261
258
|
);
|
|
262
259
|
}
|
|
263
260
|
|
|
261
|
+
// src/storage/idb.ts
|
|
262
|
+
import { Effect as Effect2 } from "effect";
|
|
263
|
+
import { openDB, deleteDB } from "idb";
|
|
264
|
+
|
|
265
|
+
// src/sync/compact-event.ts
|
|
266
|
+
import { bytesToHex as bytesToHex2, hexToBytes as hexToBytes2 } from "@noble/hashes/utils.js";
|
|
267
|
+
var VERSION = 1;
|
|
268
|
+
var HEADER_SIZE = 133;
|
|
269
|
+
function base64ToBytes(base64) {
|
|
270
|
+
const binary = atob(base64);
|
|
271
|
+
const bytes = new Uint8Array(binary.length);
|
|
272
|
+
for (let i = 0; i < binary.length; i++) {
|
|
273
|
+
bytes[i] = binary.charCodeAt(i);
|
|
274
|
+
}
|
|
275
|
+
return bytes;
|
|
276
|
+
}
|
|
277
|
+
function bytesToBase64(bytes) {
|
|
278
|
+
let binary = "";
|
|
279
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
280
|
+
binary += String.fromCharCode(bytes[i]);
|
|
281
|
+
}
|
|
282
|
+
return btoa(binary);
|
|
283
|
+
}
|
|
284
|
+
function packEvent(event) {
|
|
285
|
+
const pubkey = hexToBytes2(event.pubkey);
|
|
286
|
+
const sig = hexToBytes2(event.sig);
|
|
287
|
+
const recipientTag = event.tags.find((t) => t[0] === "p");
|
|
288
|
+
if (!recipientTag) throw new Error("Gift wrap missing #p tag");
|
|
289
|
+
if (event.tags.some((t) => t[0] !== "p")) {
|
|
290
|
+
throw new Error("Gift wrap has unexpected non-p tags; compact encoding would lose them");
|
|
291
|
+
}
|
|
292
|
+
const recipient = hexToBytes2(recipientTag[1]);
|
|
293
|
+
const createdAtBuf = new Uint8Array(4);
|
|
294
|
+
new DataView(createdAtBuf.buffer).setUint32(0, event.created_at, false);
|
|
295
|
+
const content = base64ToBytes(event.content);
|
|
296
|
+
const result = new Uint8Array(HEADER_SIZE + content.length);
|
|
297
|
+
result[0] = VERSION;
|
|
298
|
+
result.set(pubkey, 1);
|
|
299
|
+
result.set(sig, 33);
|
|
300
|
+
result.set(recipient, 97);
|
|
301
|
+
result.set(createdAtBuf, 129);
|
|
302
|
+
result.set(content, HEADER_SIZE);
|
|
303
|
+
return result;
|
|
304
|
+
}
|
|
305
|
+
function unpackEvent(id, compact) {
|
|
306
|
+
const version = compact[0];
|
|
307
|
+
if (version !== VERSION) throw new Error(`Unknown compact event version: ${version}`);
|
|
308
|
+
const pubkey = bytesToHex2(compact.slice(1, 33));
|
|
309
|
+
const sig = bytesToHex2(compact.slice(33, 97));
|
|
310
|
+
const recipient = bytesToHex2(compact.slice(97, 129));
|
|
311
|
+
const dv = new DataView(compact.buffer, compact.byteOffset + 129, 4);
|
|
312
|
+
const createdAt = dv.getUint32(0, false);
|
|
313
|
+
const content = bytesToBase64(compact.slice(HEADER_SIZE));
|
|
314
|
+
return {
|
|
315
|
+
id,
|
|
316
|
+
pubkey,
|
|
317
|
+
sig,
|
|
318
|
+
created_at: createdAt,
|
|
319
|
+
kind: 1059,
|
|
320
|
+
tags: [["p", recipient]],
|
|
321
|
+
content
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// src/storage/idb.ts
|
|
326
|
+
var DB_NAME = "tablinum";
|
|
327
|
+
function storeName(collection2) {
|
|
328
|
+
return `col_${collection2}`;
|
|
329
|
+
}
|
|
330
|
+
function computeSchemaSig(schema) {
|
|
331
|
+
return Object.entries(schema).sort(([a], [b]) => a.localeCompare(b)).map(([name, def]) => {
|
|
332
|
+
const indices = [...def.indices ?? []].sort().join(",");
|
|
333
|
+
return `${name}:${indices}`;
|
|
334
|
+
}).join("|");
|
|
335
|
+
}
|
|
336
|
+
function wrap(label, fn) {
|
|
337
|
+
return Effect2.tryPromise({
|
|
338
|
+
try: fn,
|
|
339
|
+
catch: (e) => new StorageError({
|
|
340
|
+
message: `IndexedDB ${label} failed: ${e instanceof Error ? e.message : String(e)}`,
|
|
341
|
+
cause: e
|
|
342
|
+
})
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
function upgradeSchema(database, schema, tx) {
|
|
346
|
+
if (!database.objectStoreNames.contains("_meta")) {
|
|
347
|
+
database.createObjectStore("_meta");
|
|
348
|
+
}
|
|
349
|
+
if (!database.objectStoreNames.contains("events")) {
|
|
350
|
+
const events = database.createObjectStore("events", { keyPath: "id" });
|
|
351
|
+
events.createIndex("by-record", ["collection", "recordId"]);
|
|
352
|
+
}
|
|
353
|
+
if (!database.objectStoreNames.contains("giftwraps")) {
|
|
354
|
+
database.createObjectStore("giftwraps", { keyPath: "id" });
|
|
355
|
+
}
|
|
356
|
+
const expectedStores = /* @__PURE__ */ new Set();
|
|
357
|
+
for (const [, def] of Object.entries(schema)) {
|
|
358
|
+
const sn = storeName(def.name);
|
|
359
|
+
expectedStores.add(sn);
|
|
360
|
+
if (!database.objectStoreNames.contains(sn)) {
|
|
361
|
+
const store = database.createObjectStore(sn, { keyPath: "id" });
|
|
362
|
+
for (const idx of def.indices ?? []) {
|
|
363
|
+
store.createIndex(idx, idx);
|
|
364
|
+
}
|
|
365
|
+
} else {
|
|
366
|
+
const store = tx.objectStore(sn);
|
|
367
|
+
const existingIndices = new Set(Array.from(store.indexNames));
|
|
368
|
+
const wantedIndices = new Set(def.indices ?? []);
|
|
369
|
+
for (const idx of existingIndices) {
|
|
370
|
+
if (!wantedIndices.has(idx)) store.deleteIndex(idx);
|
|
371
|
+
}
|
|
372
|
+
for (const idx of wantedIndices) {
|
|
373
|
+
if (!existingIndices.has(idx)) store.createIndex(idx, idx);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
for (const existing of Array.from(database.objectStoreNames)) {
|
|
378
|
+
if (existing.startsWith("col_") && !expectedStores.has(existing)) {
|
|
379
|
+
database.deleteObjectStore(existing);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
tx.objectStore("_meta").put(computeSchemaSig(schema), "schema_sig");
|
|
383
|
+
}
|
|
384
|
+
function deleteIDBStorage(dbName) {
|
|
385
|
+
if (typeof indexedDB === "undefined") {
|
|
386
|
+
return Effect2.fail(
|
|
387
|
+
new StorageError({
|
|
388
|
+
message: "IndexedDB is not available in this environment"
|
|
389
|
+
})
|
|
390
|
+
);
|
|
391
|
+
}
|
|
392
|
+
return wrap("deleteDatabase", () => deleteDB(dbName));
|
|
393
|
+
}
|
|
394
|
+
function openIDBStorage(dbName, schema) {
|
|
395
|
+
return Effect2.gen(function* () {
|
|
396
|
+
const name = dbName ?? DB_NAME;
|
|
397
|
+
const schemaSig = computeSchemaSig(schema);
|
|
398
|
+
if (typeof indexedDB === "undefined") {
|
|
399
|
+
return yield* Effect2.fail(
|
|
400
|
+
new StorageError({
|
|
401
|
+
message: "IndexedDB is not available in this environment"
|
|
402
|
+
})
|
|
403
|
+
);
|
|
404
|
+
}
|
|
405
|
+
const probeDb = yield* Effect2.tryPromise({
|
|
406
|
+
try: () => openDB(name),
|
|
407
|
+
catch: (e) => new StorageError({ message: "Failed to open IndexedDB", cause: e })
|
|
408
|
+
});
|
|
409
|
+
const currentVersion = probeDb.version;
|
|
410
|
+
let needsUpgrade = true;
|
|
411
|
+
if (probeDb.objectStoreNames.contains("_meta")) {
|
|
412
|
+
const storedSig = yield* Effect2.tryPromise({
|
|
413
|
+
try: () => probeDb.get("_meta", "schema_sig"),
|
|
414
|
+
catch: () => new StorageError({ message: "Failed to read schema meta" })
|
|
415
|
+
}).pipe(Effect2.catch(() => Effect2.succeed(void 0)));
|
|
416
|
+
needsUpgrade = storedSig !== schemaSig;
|
|
417
|
+
}
|
|
418
|
+
probeDb.close();
|
|
419
|
+
const db = needsUpgrade ? yield* Effect2.tryPromise({
|
|
420
|
+
try: () => openDB(name, currentVersion + 1, {
|
|
421
|
+
upgrade(database, _oldVersion, _newVersion, transaction) {
|
|
422
|
+
upgradeSchema(database, schema, transaction);
|
|
423
|
+
}
|
|
424
|
+
}),
|
|
425
|
+
catch: (e) => new StorageError({ message: "Failed to open IndexedDB", cause: e })
|
|
426
|
+
}) : yield* Effect2.tryPromise({
|
|
427
|
+
try: () => openDB(name),
|
|
428
|
+
catch: (e) => new StorageError({ message: "Failed to open IndexedDB", cause: e })
|
|
429
|
+
});
|
|
430
|
+
yield* Effect2.addFinalizer(() => Effect2.sync(() => db.close()));
|
|
431
|
+
const handle = {
|
|
432
|
+
putRecord: (collection2, record) => wrap("putRecord", () => db.put(storeName(collection2), record).then(() => void 0)),
|
|
433
|
+
getRecord: (collection2, id) => wrap("getRecord", () => db.get(storeName(collection2), id)),
|
|
434
|
+
getAllRecords: (collection2) => wrap("getAllRecords", () => db.getAll(storeName(collection2))),
|
|
435
|
+
countRecords: (collection2) => wrap("countRecords", () => db.count(storeName(collection2))),
|
|
436
|
+
clearRecords: (collection2) => wrap("clearRecords", () => db.clear(storeName(collection2))),
|
|
437
|
+
getByIndex: (collection2, indexName, value) => wrap("getByIndex", () => db.getAllFromIndex(storeName(collection2), indexName, value)),
|
|
438
|
+
getByIndexRange: (collection2, indexName, range) => wrap("getByIndexRange", () => db.getAllFromIndex(storeName(collection2), indexName, range)),
|
|
439
|
+
getAllSorted: (collection2, indexName, direction) => wrap("getAllSorted", async () => {
|
|
440
|
+
const sn = storeName(collection2);
|
|
441
|
+
const tx = db.transaction(sn, "readonly");
|
|
442
|
+
const store = tx.objectStore(sn);
|
|
443
|
+
const index = store.index(indexName);
|
|
444
|
+
const results = [];
|
|
445
|
+
let cursor = await index.openCursor(null, direction ?? "next");
|
|
446
|
+
while (cursor) {
|
|
447
|
+
results.push(cursor.value);
|
|
448
|
+
cursor = await cursor.continue();
|
|
449
|
+
}
|
|
450
|
+
return results;
|
|
451
|
+
}),
|
|
452
|
+
putEvent: (event) => wrap("putEvent", () => db.put("events", event).then(() => void 0)),
|
|
453
|
+
getEvent: (id) => wrap("getEvent", () => db.get("events", id)),
|
|
454
|
+
getAllEvents: () => wrap("getAllEvents", () => db.getAll("events")),
|
|
455
|
+
getEventsByRecord: (collection2, recordId) => wrap(
|
|
456
|
+
"getEventsByRecord",
|
|
457
|
+
() => db.getAllFromIndex("events", "by-record", [collection2, recordId])
|
|
458
|
+
),
|
|
459
|
+
putGiftWrap: (gw) => wrap("putGiftWrap", async () => {
|
|
460
|
+
if (gw.event) {
|
|
461
|
+
const compact = packEvent(gw.event);
|
|
462
|
+
await db.put("giftwraps", { id: gw.id, compact, createdAt: gw.createdAt });
|
|
463
|
+
} else {
|
|
464
|
+
await db.put("giftwraps", { id: gw.id, createdAt: gw.createdAt });
|
|
465
|
+
}
|
|
466
|
+
}),
|
|
467
|
+
getGiftWrap: (id) => wrap("getGiftWrap", async () => {
|
|
468
|
+
const raw = await db.get("giftwraps", id);
|
|
469
|
+
if (!raw) return void 0;
|
|
470
|
+
if (raw.compact) {
|
|
471
|
+
return { id: raw.id, event: unpackEvent(raw.id, raw.compact), createdAt: raw.createdAt };
|
|
472
|
+
}
|
|
473
|
+
if (raw.event) {
|
|
474
|
+
const compact = packEvent(raw.event);
|
|
475
|
+
await db.put("giftwraps", { id: raw.id, compact, createdAt: raw.createdAt });
|
|
476
|
+
return { id: raw.id, event: raw.event, createdAt: raw.createdAt };
|
|
477
|
+
}
|
|
478
|
+
return { id: raw.id, createdAt: raw.createdAt };
|
|
479
|
+
}),
|
|
480
|
+
getAllGiftWraps: () => wrap("getAllGiftWraps", async () => {
|
|
481
|
+
const raws = await db.getAll("giftwraps");
|
|
482
|
+
const results = [];
|
|
483
|
+
for (const raw of raws) {
|
|
484
|
+
if (raw.compact) {
|
|
485
|
+
results.push({ id: raw.id, event: unpackEvent(raw.id, raw.compact), createdAt: raw.createdAt });
|
|
486
|
+
} else if (raw.event) {
|
|
487
|
+
const compact = packEvent(raw.event);
|
|
488
|
+
await db.put("giftwraps", { id: raw.id, compact, createdAt: raw.createdAt });
|
|
489
|
+
results.push({ id: raw.id, event: raw.event, createdAt: raw.createdAt });
|
|
490
|
+
} else {
|
|
491
|
+
results.push({ id: raw.id, createdAt: raw.createdAt });
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
return results;
|
|
495
|
+
}),
|
|
496
|
+
deleteGiftWrap: (id) => wrap("deleteGiftWrap", () => db.delete("giftwraps", id).then(() => void 0)),
|
|
497
|
+
deleteEvent: (id) => wrap("deleteEvent", () => db.delete("events", id).then(() => void 0)),
|
|
498
|
+
stripEventData: (id) => wrap("stripEventData", async () => {
|
|
499
|
+
const existing = await db.get("events", id);
|
|
500
|
+
if (existing) {
|
|
501
|
+
await db.put("events", { ...existing, data: null });
|
|
502
|
+
}
|
|
503
|
+
}),
|
|
504
|
+
getMeta: (key) => wrap("getMeta", () => db.get("_meta", key)),
|
|
505
|
+
putMeta: (key, value) => wrap("putMeta", () => db.put("_meta", value, key).then(() => void 0)),
|
|
506
|
+
close: () => Effect2.sync(() => db.close())
|
|
507
|
+
};
|
|
508
|
+
return handle;
|
|
509
|
+
});
|
|
510
|
+
}
|
|
511
|
+
|
|
264
512
|
// src/services/Config.ts
|
|
265
513
|
import { ServiceMap } from "effect";
|
|
266
514
|
var Config = class extends ServiceMap.Service()("tablinum/Config") {
|
|
@@ -277,16 +525,16 @@ var Tablinum = class extends ServiceMap2.Service()(
|
|
|
277
525
|
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";
|
|
278
526
|
|
|
279
527
|
// src/crud/watch.ts
|
|
280
|
-
import { Effect as
|
|
528
|
+
import { Effect as Effect3, PubSub, Ref, Stream } from "effect";
|
|
281
529
|
function watchCollection(ctx, storage, collectionName, filter, mapRecord2) {
|
|
282
|
-
const query = () =>
|
|
530
|
+
const query = () => Effect3.map(storage.getAllRecords(collectionName), (all) => {
|
|
283
531
|
const filtered = all.filter((r) => !r._d && (filter ? filter(r) : true));
|
|
284
532
|
return mapRecord2 ? filtered.map(mapRecord2) : filtered;
|
|
285
533
|
});
|
|
286
534
|
const changes = Stream.fromPubSub(ctx.pubsub).pipe(
|
|
287
535
|
Stream.filter((event) => event.collection === collectionName),
|
|
288
536
|
Stream.mapEffect(
|
|
289
|
-
() =>
|
|
537
|
+
() => Effect3.gen(function* () {
|
|
290
538
|
const replaying = yield* Ref.get(ctx.replayingRef);
|
|
291
539
|
if (replaying) return void 0;
|
|
292
540
|
return yield* query();
|
|
@@ -295,18 +543,18 @@ function watchCollection(ctx, storage, collectionName, filter, mapRecord2) {
|
|
|
295
543
|
Stream.filter((result) => result !== void 0)
|
|
296
544
|
);
|
|
297
545
|
return Stream.unwrap(
|
|
298
|
-
|
|
299
|
-
yield*
|
|
546
|
+
Effect3.gen(function* () {
|
|
547
|
+
yield* Effect3.sleep(0);
|
|
300
548
|
const initial = yield* query();
|
|
301
549
|
return Stream.concat(Stream.make(initial), changes);
|
|
302
550
|
})
|
|
303
551
|
);
|
|
304
552
|
}
|
|
305
553
|
function notifyChange(ctx, event) {
|
|
306
|
-
return PubSub.publish(ctx.pubsub, event).pipe(
|
|
554
|
+
return PubSub.publish(ctx.pubsub, event).pipe(Effect3.asVoid);
|
|
307
555
|
}
|
|
308
556
|
function notifyReplayComplete(ctx, collections) {
|
|
309
|
-
return
|
|
557
|
+
return Effect3.gen(function* () {
|
|
310
558
|
yield* Ref.set(ctx.replayingRef, false);
|
|
311
559
|
for (const collection2 of collections) {
|
|
312
560
|
yield* notifyChange(ctx, {
|
|
@@ -319,7 +567,7 @@ function notifyReplayComplete(ctx, collections) {
|
|
|
319
567
|
}
|
|
320
568
|
|
|
321
569
|
// src/storage/records-store.ts
|
|
322
|
-
import { Effect as
|
|
570
|
+
import { Effect as Effect4 } from "effect";
|
|
323
571
|
|
|
324
572
|
// src/storage/lww.ts
|
|
325
573
|
function resolveWinner(existing, incoming) {
|
|
@@ -333,30 +581,6 @@ function resolveWinner(existing, incoming) {
|
|
|
333
581
|
function isPlainObject(value) {
|
|
334
582
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
335
583
|
}
|
|
336
|
-
function deepDiff(before, after) {
|
|
337
|
-
const result = {};
|
|
338
|
-
let hasChanges = false;
|
|
339
|
-
for (const key of Object.keys(after)) {
|
|
340
|
-
const a = before[key];
|
|
341
|
-
const b = after[key];
|
|
342
|
-
if (isPlainObject(a) && isPlainObject(b)) {
|
|
343
|
-
const nested = deepDiff(a, b);
|
|
344
|
-
if (nested !== null) {
|
|
345
|
-
result[key] = nested;
|
|
346
|
-
hasChanges = true;
|
|
347
|
-
}
|
|
348
|
-
} else if (Array.isArray(a) && Array.isArray(b)) {
|
|
349
|
-
if (JSON.stringify(a) !== JSON.stringify(b)) {
|
|
350
|
-
result[key] = b;
|
|
351
|
-
hasChanges = true;
|
|
352
|
-
}
|
|
353
|
-
} else if (a !== b) {
|
|
354
|
-
result[key] = b;
|
|
355
|
-
hasChanges = true;
|
|
356
|
-
}
|
|
357
|
-
}
|
|
358
|
-
return hasChanges ? result : null;
|
|
359
|
-
}
|
|
360
584
|
function deepMerge(target, source) {
|
|
361
585
|
const result = { ...target };
|
|
362
586
|
for (const key of Object.keys(source)) {
|
|
@@ -383,7 +607,7 @@ function buildRecord(event) {
|
|
|
383
607
|
};
|
|
384
608
|
}
|
|
385
609
|
function applyEvent(storage, event) {
|
|
386
|
-
return
|
|
610
|
+
return Effect4.gen(function* () {
|
|
387
611
|
const existing = yield* storage.getRecord(event.collection, event.recordId);
|
|
388
612
|
if (existing) {
|
|
389
613
|
const existingMeta = {
|
|
@@ -403,7 +627,7 @@ function applyEvent(storage, event) {
|
|
|
403
627
|
});
|
|
404
628
|
}
|
|
405
629
|
function rebuild(storage, collections) {
|
|
406
|
-
return
|
|
630
|
+
return Effect4.gen(function* () {
|
|
407
631
|
for (const col of collections) {
|
|
408
632
|
yield* storage.clearRecords(col);
|
|
409
633
|
}
|
|
@@ -419,7 +643,7 @@ function rebuild(storage, collections) {
|
|
|
419
643
|
}
|
|
420
644
|
|
|
421
645
|
// src/schema/validate.ts
|
|
422
|
-
import { Effect as
|
|
646
|
+
import { Effect as Effect5, Schema as Schema4 } from "effect";
|
|
423
647
|
function fieldDefToSchema(fd) {
|
|
424
648
|
let base;
|
|
425
649
|
switch (fd.kind) {
|
|
@@ -467,10 +691,10 @@ function buildStructSchema(def, options = {}) {
|
|
|
467
691
|
function buildValidator(collectionName, def) {
|
|
468
692
|
const decode = Schema4.decodeUnknownEffect(buildStructSchema(def, { includeId: true }));
|
|
469
693
|
return (input) => decode(input).pipe(
|
|
470
|
-
|
|
694
|
+
Effect5.map(
|
|
471
695
|
(result) => result
|
|
472
696
|
),
|
|
473
|
-
|
|
697
|
+
Effect5.mapError(
|
|
474
698
|
(e) => new ValidationError({
|
|
475
699
|
message: `Validation failed for collection "${collectionName}": ${e.message}`
|
|
476
700
|
})
|
|
@@ -479,7 +703,7 @@ function buildValidator(collectionName, def) {
|
|
|
479
703
|
}
|
|
480
704
|
function buildPartialValidator(collectionName, def) {
|
|
481
705
|
const decode = Schema4.decodeUnknownEffect(buildStructSchema(def, { allOptional: true }));
|
|
482
|
-
return (input) =>
|
|
706
|
+
return (input) => Effect5.gen(function* () {
|
|
483
707
|
if (typeof input !== "object" || input === null) {
|
|
484
708
|
return yield* new ValidationError({
|
|
485
709
|
message: `Validation failed for collection "${collectionName}": expected an object`
|
|
@@ -494,8 +718,8 @@ function buildPartialValidator(collectionName, def) {
|
|
|
494
718
|
});
|
|
495
719
|
}
|
|
496
720
|
return yield* decode(record).pipe(
|
|
497
|
-
|
|
498
|
-
|
|
721
|
+
Effect5.map((result) => result),
|
|
722
|
+
Effect5.mapError(
|
|
499
723
|
(e) => new ValidationError({
|
|
500
724
|
message: `Validation failed for collection "${collectionName}": ${e.message}`
|
|
501
725
|
})
|
|
@@ -505,7 +729,7 @@ function buildPartialValidator(collectionName, def) {
|
|
|
505
729
|
}
|
|
506
730
|
|
|
507
731
|
// src/crud/collection-handle.ts
|
|
508
|
-
import { Effect as
|
|
732
|
+
import { Effect as Effect7, Option as Option3, References } from "effect";
|
|
509
733
|
|
|
510
734
|
// src/utils/uuid.ts
|
|
511
735
|
var alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
|
|
@@ -538,12 +762,12 @@ function uuidv7() {
|
|
|
538
762
|
}
|
|
539
763
|
|
|
540
764
|
// src/crud/query-builder.ts
|
|
541
|
-
import { Effect as
|
|
765
|
+
import { Effect as Effect6, Option as Option2, Ref as Ref2, Stream as Stream2 } from "effect";
|
|
542
766
|
function emptyPlan() {
|
|
543
767
|
return { filters: [] };
|
|
544
768
|
}
|
|
545
769
|
function executeQuery(ctx, plan) {
|
|
546
|
-
return
|
|
770
|
+
return Effect6.gen(function* () {
|
|
547
771
|
if (plan.fieldName) {
|
|
548
772
|
const fieldDef = ctx.def.fields[plan.fieldName];
|
|
549
773
|
if (!fieldDef) {
|
|
@@ -619,7 +843,7 @@ function watchQuery(ctx, plan) {
|
|
|
619
843
|
const changes = Stream2.fromPubSub(ctx.watchCtx.pubsub).pipe(
|
|
620
844
|
Stream2.filter((event) => event.collection === ctx.collectionName),
|
|
621
845
|
Stream2.mapEffect(
|
|
622
|
-
() =>
|
|
846
|
+
() => Effect6.gen(function* () {
|
|
623
847
|
const replaying = yield* Ref2.get(ctx.watchCtx.replayingRef);
|
|
624
848
|
if (replaying) return void 0;
|
|
625
849
|
return yield* query();
|
|
@@ -628,7 +852,7 @@ function watchQuery(ctx, plan) {
|
|
|
628
852
|
Stream2.filter((result) => result !== void 0)
|
|
629
853
|
);
|
|
630
854
|
return Stream2.unwrap(
|
|
631
|
-
|
|
855
|
+
Effect6.gen(function* () {
|
|
632
856
|
const initial = yield* query();
|
|
633
857
|
return Stream2.concat(Stream2.make(initial), changes);
|
|
634
858
|
})
|
|
@@ -654,11 +878,11 @@ function makeQueryBuilder(ctx, plan) {
|
|
|
654
878
|
offset: (n) => makeQueryBuilder(ctx, { ...plan, offset: n }),
|
|
655
879
|
limit: (n) => makeQueryBuilder(ctx, { ...plan, limit: n }),
|
|
656
880
|
get: () => executeQuery(ctx, plan),
|
|
657
|
-
first: () =>
|
|
881
|
+
first: () => Effect6.map(
|
|
658
882
|
executeQuery(ctx, { ...plan, limit: 1 }),
|
|
659
883
|
(results) => results.length > 0 ? Option2.some(results[0]) : Option2.none()
|
|
660
884
|
),
|
|
661
|
-
count: () =>
|
|
885
|
+
count: () => Effect6.map(executeQuery(ctx, plan), (results) => results.length),
|
|
662
886
|
watch: () => watchQuery(ctx, plan)
|
|
663
887
|
};
|
|
664
888
|
}
|
|
@@ -764,7 +988,7 @@ function replayState(recordId, events, stopAtId) {
|
|
|
764
988
|
return state;
|
|
765
989
|
}
|
|
766
990
|
function promoteToSnapshot(storage, collection2, recordId, target, allSorted) {
|
|
767
|
-
return
|
|
991
|
+
return Effect7.gen(function* () {
|
|
768
992
|
const chronological = sortChronologically(allSorted);
|
|
769
993
|
const state = replayState(recordId, chronological, target.id);
|
|
770
994
|
if (state) {
|
|
@@ -773,7 +997,7 @@ function promoteToSnapshot(storage, collection2, recordId, target, allSorted) {
|
|
|
773
997
|
});
|
|
774
998
|
}
|
|
775
999
|
function pruneEvents(storage, collection2, recordId, retention) {
|
|
776
|
-
return
|
|
1000
|
+
return Effect7.gen(function* () {
|
|
777
1001
|
const events = yield* storage.getEventsByRecord(collection2, recordId);
|
|
778
1002
|
if (events.length <= retention) return;
|
|
779
1003
|
const sorted = [...events].sort((a, b) => b.createdAt - a.createdAt || (a.id < b.id ? 1 : -1));
|
|
@@ -794,8 +1018,8 @@ function mapRecord(record) {
|
|
|
794
1018
|
}
|
|
795
1019
|
function createCollectionHandle(def, storage, watchCtx, validator, partialValidator, makeEventId, localAuthor, onWrite, logLevel = "None") {
|
|
796
1020
|
const collectionName = def.name;
|
|
797
|
-
const withLog = (effect) =>
|
|
798
|
-
const commitEvent = (event) =>
|
|
1021
|
+
const withLog = (effect) => Effect7.provideService(effect, References.MinimumLogLevel, logLevel);
|
|
1022
|
+
const commitEvent = (event) => Effect7.gen(function* () {
|
|
799
1023
|
yield* storage.putEvent(event);
|
|
800
1024
|
yield* applyEvent(storage, event);
|
|
801
1025
|
if (onWrite) yield* onWrite(event);
|
|
@@ -807,7 +1031,7 @@ function createCollectionHandle(def, storage, watchCtx, validator, partialValida
|
|
|
807
1031
|
});
|
|
808
1032
|
const handle = {
|
|
809
1033
|
add: (data) => withLog(
|
|
810
|
-
|
|
1034
|
+
Effect7.gen(function* () {
|
|
811
1035
|
const id = uuidv7();
|
|
812
1036
|
const fullRecord = { id, ...data };
|
|
813
1037
|
yield* validator(fullRecord);
|
|
@@ -821,7 +1045,7 @@ function createCollectionHandle(def, storage, watchCtx, validator, partialValida
|
|
|
821
1045
|
author: localAuthor
|
|
822
1046
|
};
|
|
823
1047
|
yield* commitEvent(event);
|
|
824
|
-
yield*
|
|
1048
|
+
yield* Effect7.logDebug("Record added", {
|
|
825
1049
|
collection: collectionName,
|
|
826
1050
|
recordId: id,
|
|
827
1051
|
data: fullRecord
|
|
@@ -830,7 +1054,7 @@ function createCollectionHandle(def, storage, watchCtx, validator, partialValida
|
|
|
830
1054
|
})
|
|
831
1055
|
),
|
|
832
1056
|
update: (id, data) => withLog(
|
|
833
|
-
|
|
1057
|
+
Effect7.gen(function* () {
|
|
834
1058
|
const existing = yield* storage.getRecord(collectionName, id);
|
|
835
1059
|
if (!existing || existing._d) {
|
|
836
1060
|
return yield* new NotFoundError({
|
|
@@ -842,27 +1066,26 @@ function createCollectionHandle(def, storage, watchCtx, validator, partialValida
|
|
|
842
1066
|
const { _d, _u, _a, _e, ...existingFields } = existing;
|
|
843
1067
|
const merged = { ...existingFields, ...data, id };
|
|
844
1068
|
yield* validator(merged);
|
|
845
|
-
const diff = deepDiff(existingFields, merged);
|
|
846
1069
|
const event = {
|
|
847
1070
|
id: makeEventId(),
|
|
848
1071
|
collection: collectionName,
|
|
849
1072
|
recordId: id,
|
|
850
1073
|
kind: "u",
|
|
851
|
-
data:
|
|
1074
|
+
data: merged,
|
|
852
1075
|
createdAt: Date.now(),
|
|
853
1076
|
author: localAuthor
|
|
854
1077
|
};
|
|
855
1078
|
yield* commitEvent(event);
|
|
856
|
-
yield*
|
|
1079
|
+
yield* Effect7.logDebug("Record updated", {
|
|
857
1080
|
collection: collectionName,
|
|
858
1081
|
recordId: id,
|
|
859
|
-
data:
|
|
1082
|
+
data: merged
|
|
860
1083
|
});
|
|
861
1084
|
yield* pruneEvents(storage, collectionName, id, def.eventRetention);
|
|
862
1085
|
})
|
|
863
1086
|
),
|
|
864
1087
|
delete: (id) => withLog(
|
|
865
|
-
|
|
1088
|
+
Effect7.gen(function* () {
|
|
866
1089
|
const existing = yield* storage.getRecord(collectionName, id);
|
|
867
1090
|
if (!existing || existing._d) {
|
|
868
1091
|
return yield* new NotFoundError({
|
|
@@ -880,11 +1103,11 @@ function createCollectionHandle(def, storage, watchCtx, validator, partialValida
|
|
|
880
1103
|
author: localAuthor
|
|
881
1104
|
};
|
|
882
1105
|
yield* commitEvent(event);
|
|
883
|
-
yield*
|
|
1106
|
+
yield* Effect7.logDebug("Record deleted", { collection: collectionName, recordId: id });
|
|
884
1107
|
yield* pruneEvents(storage, collectionName, id, def.eventRetention);
|
|
885
1108
|
})
|
|
886
1109
|
),
|
|
887
|
-
undo: (id) =>
|
|
1110
|
+
undo: (id) => Effect7.gen(function* () {
|
|
888
1111
|
const existing = yield* storage.getRecord(collectionName, id);
|
|
889
1112
|
if (!existing) {
|
|
890
1113
|
return yield* new NotFoundError({ collection: collectionName, id });
|
|
@@ -909,7 +1132,7 @@ function createCollectionHandle(def, storage, watchCtx, validator, partialValida
|
|
|
909
1132
|
yield* commitEvent(event);
|
|
910
1133
|
yield* pruneEvents(storage, collectionName, id, def.eventRetention);
|
|
911
1134
|
}),
|
|
912
|
-
get: (id) =>
|
|
1135
|
+
get: (id) => Effect7.gen(function* () {
|
|
913
1136
|
const record = yield* storage.getRecord(collectionName, id);
|
|
914
1137
|
if (!record || record._d) {
|
|
915
1138
|
return yield* new NotFoundError({
|
|
@@ -919,11 +1142,11 @@ function createCollectionHandle(def, storage, watchCtx, validator, partialValida
|
|
|
919
1142
|
}
|
|
920
1143
|
return mapRecord(record);
|
|
921
1144
|
}),
|
|
922
|
-
first: () =>
|
|
1145
|
+
first: () => Effect7.map(storage.getAllRecords(collectionName), (all) => {
|
|
923
1146
|
const found = all.find((r) => !r._d);
|
|
924
1147
|
return found ? Option3.some(mapRecord(found)) : Option3.none();
|
|
925
1148
|
}),
|
|
926
|
-
count: () =>
|
|
1149
|
+
count: () => Effect7.map(storage.getAllRecords(collectionName), (all) => all.filter((r) => !r._d).length),
|
|
927
1150
|
watch: () => watchCollection(watchCtx, storage, collectionName, void 0, mapRecord),
|
|
928
1151
|
where: (fieldName) => createWhereClause(
|
|
929
1152
|
storage,
|
|
@@ -946,12 +1169,12 @@ function createCollectionHandle(def, storage, watchCtx, validator, partialValida
|
|
|
946
1169
|
}
|
|
947
1170
|
|
|
948
1171
|
// src/sync/sync-service.ts
|
|
949
|
-
import { Duration, Effect as
|
|
1172
|
+
import { Duration, Effect as Effect9, Layer, Option as Option5, References as References2, Ref as Ref3, Schedule } from "effect";
|
|
950
1173
|
import { unwrapEvent } from "nostr-tools/nip59";
|
|
951
1174
|
import { GiftWrap as GiftWrap2 } from "nostr-tools/kinds";
|
|
952
1175
|
|
|
953
1176
|
// src/sync/negentropy.ts
|
|
954
|
-
import { Effect as
|
|
1177
|
+
import { Effect as Effect8 } from "effect";
|
|
955
1178
|
|
|
956
1179
|
// src/vendor/negentropy.js
|
|
957
1180
|
var PROTOCOL_VERSION = 97;
|
|
@@ -1438,14 +1661,14 @@ function itemCompare(a, b) {
|
|
|
1438
1661
|
}
|
|
1439
1662
|
|
|
1440
1663
|
// src/sync/negentropy.ts
|
|
1441
|
-
import { hexToBytes as
|
|
1664
|
+
import { hexToBytes as hexToBytes3 } from "@noble/hashes/utils.js";
|
|
1442
1665
|
import { GiftWrap } from "nostr-tools/kinds";
|
|
1443
1666
|
function reconcileWithRelay(storage, relay, relayUrl, publicKeys) {
|
|
1444
|
-
return
|
|
1667
|
+
return Effect8.gen(function* () {
|
|
1445
1668
|
const allGiftWraps = yield* storage.getAllGiftWraps();
|
|
1446
1669
|
const storageVector = new NegentropyStorageVector();
|
|
1447
1670
|
for (const gw of allGiftWraps) {
|
|
1448
|
-
storageVector.insert(gw.createdAt,
|
|
1671
|
+
storageVector.insert(gw.createdAt, hexToBytes3(gw.id));
|
|
1449
1672
|
}
|
|
1450
1673
|
storageVector.seal();
|
|
1451
1674
|
const neg = new Negentropy(storageVector, 0);
|
|
@@ -1456,7 +1679,7 @@ function reconcileWithRelay(storage, relay, relayUrl, publicKeys) {
|
|
|
1456
1679
|
const allHaveIds = [];
|
|
1457
1680
|
const allNeedIds = [];
|
|
1458
1681
|
const subId = `neg-${Date.now()}`;
|
|
1459
|
-
const initialMsg = yield*
|
|
1682
|
+
const initialMsg = yield* Effect8.tryPromise({
|
|
1460
1683
|
try: () => neg.initiate(),
|
|
1461
1684
|
catch: (e) => new SyncError({
|
|
1462
1685
|
message: `Negentropy initiate failed: ${e instanceof Error ? e.message : String(e)}`,
|
|
@@ -1468,7 +1691,7 @@ function reconcileWithRelay(storage, relay, relayUrl, publicKeys) {
|
|
|
1468
1691
|
while (currentMsg !== null) {
|
|
1469
1692
|
const response = yield* relay.sendNegMsg(relayUrl, subId, filter, currentMsg);
|
|
1470
1693
|
if (response.msgHex === null) break;
|
|
1471
|
-
const reconcileResult = yield*
|
|
1694
|
+
const reconcileResult = yield* Effect8.tryPromise({
|
|
1472
1695
|
try: () => neg.reconcile(response.msgHex),
|
|
1473
1696
|
catch: (e) => new SyncError({
|
|
1474
1697
|
message: `Negentropy reconcile failed: ${e instanceof Error ? e.message : String(e)}`,
|
|
@@ -1481,13 +1704,13 @@ function reconcileWithRelay(storage, relay, relayUrl, publicKeys) {
|
|
|
1481
1704
|
for (const id of needIds) allNeedIds.push(id);
|
|
1482
1705
|
currentMsg = nextMsg;
|
|
1483
1706
|
}
|
|
1484
|
-
yield*
|
|
1707
|
+
yield* Effect8.logDebug("Negentropy reconciliation complete", {
|
|
1485
1708
|
relay: relayUrl,
|
|
1486
1709
|
have: allHaveIds.length,
|
|
1487
1710
|
need: allNeedIds.length
|
|
1488
1711
|
});
|
|
1489
1712
|
return { haveIds: allHaveIds, needIds: allNeedIds };
|
|
1490
|
-
}).pipe(
|
|
1713
|
+
}).pipe(Effect8.withLogSpan("tablinum.negentropy"));
|
|
1491
1714
|
}
|
|
1492
1715
|
|
|
1493
1716
|
// src/db/key-rotation.ts
|
|
@@ -1581,52 +1804,52 @@ function createSyncHandle(storage, giftWrapHandle, relay, publishQueue, syncStat
|
|
|
1581
1804
|
kind: "create"
|
|
1582
1805
|
});
|
|
1583
1806
|
const forkHandled = (effect) => {
|
|
1584
|
-
|
|
1807
|
+
Effect9.runFork(
|
|
1585
1808
|
effect.pipe(
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1809
|
+
Effect9.tapError((e) => Effect9.sync(() => onSyncError?.(e))),
|
|
1810
|
+
Effect9.ignore,
|
|
1811
|
+
Effect9.provide(logLayer),
|
|
1812
|
+
Effect9.forkIn(scope)
|
|
1590
1813
|
)
|
|
1591
1814
|
);
|
|
1592
1815
|
};
|
|
1593
1816
|
let autoFlushActive = false;
|
|
1594
|
-
const autoFlushEffect =
|
|
1817
|
+
const autoFlushEffect = Effect9.gen(function* () {
|
|
1595
1818
|
const size = yield* publishQueue.size();
|
|
1596
1819
|
if (size === 0) return;
|
|
1597
1820
|
yield* syncStatus.set("syncing");
|
|
1598
1821
|
yield* publishQueue.flush(relayUrls);
|
|
1599
1822
|
const remaining = yield* publishQueue.size();
|
|
1600
|
-
if (remaining > 0) yield*
|
|
1823
|
+
if (remaining > 0) yield* Effect9.fail("pending");
|
|
1601
1824
|
}).pipe(
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1825
|
+
Effect9.ensuring(syncStatus.set("idle")),
|
|
1826
|
+
Effect9.retry({ schedule: Schedule.exponential(5e3).pipe(Schedule.jittered), times: 10 }),
|
|
1827
|
+
Effect9.ignore
|
|
1605
1828
|
);
|
|
1606
1829
|
const scheduleAutoFlush = () => {
|
|
1607
1830
|
if (autoFlushActive) return;
|
|
1608
1831
|
autoFlushActive = true;
|
|
1609
1832
|
forkHandled(
|
|
1610
1833
|
autoFlushEffect.pipe(
|
|
1611
|
-
|
|
1612
|
-
|
|
1834
|
+
Effect9.ensuring(
|
|
1835
|
+
Effect9.sync(() => {
|
|
1613
1836
|
autoFlushActive = false;
|
|
1614
1837
|
})
|
|
1615
1838
|
)
|
|
1616
1839
|
)
|
|
1617
1840
|
);
|
|
1618
1841
|
};
|
|
1619
|
-
const shouldRejectWrite = (authorPubkey) =>
|
|
1842
|
+
const shouldRejectWrite = (authorPubkey) => Effect9.gen(function* () {
|
|
1620
1843
|
const memberRecord = yield* storage.getRecord("_members", authorPubkey);
|
|
1621
1844
|
if (!memberRecord) return false;
|
|
1622
1845
|
return !!memberRecord.removedAt;
|
|
1623
1846
|
});
|
|
1624
1847
|
const storeGiftWrapShell = (gw) => storage.putGiftWrap({ id: gw.id, event: gw, createdAt: gw.created_at });
|
|
1625
|
-
const unwrapGiftWrap = (remoteGw) =>
|
|
1848
|
+
const unwrapGiftWrap = (remoteGw) => Effect9.gen(function* () {
|
|
1626
1849
|
const existing = yield* storage.getGiftWrap(remoteGw.id);
|
|
1627
1850
|
if (existing) return null;
|
|
1628
1851
|
const rumor = yield* giftWrapHandle.unwrap(remoteGw).pipe(
|
|
1629
|
-
|
|
1852
|
+
Effect9.orElseSucceed(() => null)
|
|
1630
1853
|
);
|
|
1631
1854
|
if (!rumor) {
|
|
1632
1855
|
yield* storeGiftWrapShell(remoteGw);
|
|
@@ -1654,7 +1877,7 @@ function createSyncHandle(storage, giftWrapHandle, relay, publishQueue, syncStat
|
|
|
1654
1877
|
recordId: dTag.substring(colonIdx + 1)
|
|
1655
1878
|
};
|
|
1656
1879
|
});
|
|
1657
|
-
const applyUnwrappedEvent = (uw) =>
|
|
1880
|
+
const applyUnwrappedEvent = (uw) => Effect9.gen(function* () {
|
|
1658
1881
|
const { giftWrap: remoteGw, rumor, collection: collectionName, recordId } = uw;
|
|
1659
1882
|
const retention = knownCollections.get(collectionName);
|
|
1660
1883
|
if (retention === void 0) {
|
|
@@ -1664,17 +1887,17 @@ function createSyncHandle(storage, giftWrapHandle, relay, publishQueue, syncStat
|
|
|
1664
1887
|
if (rumor.pubkey) {
|
|
1665
1888
|
const reject = yield* shouldRejectWrite(rumor.pubkey);
|
|
1666
1889
|
if (reject) {
|
|
1667
|
-
yield*
|
|
1890
|
+
yield* Effect9.logWarning("Rejected write from removed member", {
|
|
1668
1891
|
author: rumor.pubkey.slice(0, 12)
|
|
1669
1892
|
});
|
|
1670
1893
|
yield* storeGiftWrapShell(remoteGw);
|
|
1671
1894
|
return null;
|
|
1672
1895
|
}
|
|
1673
1896
|
}
|
|
1674
|
-
const parsed = yield*
|
|
1897
|
+
const parsed = yield* Effect9.try({
|
|
1675
1898
|
try: () => JSON.parse(rumor.content),
|
|
1676
1899
|
catch: () => void 0
|
|
1677
|
-
}).pipe(
|
|
1900
|
+
}).pipe(Effect9.orElseSucceed(() => void 0));
|
|
1678
1901
|
if (parsed === void 0) {
|
|
1679
1902
|
yield* storeGiftWrapShell(remoteGw);
|
|
1680
1903
|
return null;
|
|
@@ -1706,7 +1929,7 @@ function createSyncHandle(storage, giftWrapHandle, relay, publishQueue, syncStat
|
|
|
1706
1929
|
if (didApply && (kind === "u" || kind === "d")) {
|
|
1707
1930
|
yield* pruneEvents(storage, collectionName, recordId, retention);
|
|
1708
1931
|
}
|
|
1709
|
-
yield*
|
|
1932
|
+
yield* Effect9.logDebug("Processed gift wrap", {
|
|
1710
1933
|
collection: collectionName,
|
|
1711
1934
|
recordId,
|
|
1712
1935
|
kind,
|
|
@@ -1717,33 +1940,33 @@ function createSyncHandle(storage, giftWrapHandle, relay, publishQueue, syncStat
|
|
|
1717
1940
|
}
|
|
1718
1941
|
return collectionName;
|
|
1719
1942
|
});
|
|
1720
|
-
const reconcileRelay = (url, pubKeys) =>
|
|
1721
|
-
yield*
|
|
1943
|
+
const reconcileRelay = (url, pubKeys) => Effect9.gen(function* () {
|
|
1944
|
+
yield* Effect9.logDebug("Syncing relay", { relay: url });
|
|
1722
1945
|
const { haveIds, needIds } = yield* reconcileWithRelay(
|
|
1723
1946
|
storage,
|
|
1724
1947
|
relay,
|
|
1725
1948
|
url,
|
|
1726
1949
|
Array.from(pubKeys)
|
|
1727
1950
|
).pipe(
|
|
1728
|
-
|
|
1729
|
-
|
|
1951
|
+
Effect9.tapError((err) => Effect9.sync(() => onSyncError?.(err))),
|
|
1952
|
+
Effect9.orElseSucceed(() => ({ haveIds: [], needIds: [] }))
|
|
1730
1953
|
);
|
|
1731
|
-
yield*
|
|
1954
|
+
yield* Effect9.logDebug("Relay reconciliation result", {
|
|
1732
1955
|
relay: url,
|
|
1733
1956
|
need: needIds.length,
|
|
1734
1957
|
have: haveIds.length
|
|
1735
1958
|
});
|
|
1736
1959
|
const events = needIds.length > 0 ? yield* relay.fetchEvents(needIds, url).pipe(
|
|
1737
|
-
|
|
1738
|
-
|
|
1960
|
+
Effect9.tapError((err) => Effect9.sync(() => onSyncError?.(err))),
|
|
1961
|
+
Effect9.orElseSucceed(() => [])
|
|
1739
1962
|
) : [];
|
|
1740
1963
|
return {
|
|
1741
1964
|
events,
|
|
1742
1965
|
haveIds: haveIds.map((id) => ({ id, url }))
|
|
1743
1966
|
};
|
|
1744
|
-
}).pipe(
|
|
1745
|
-
const syncAllRelays = (pubKeys, changedCollections) =>
|
|
1746
|
-
const results = yield*
|
|
1967
|
+
}).pipe(Effect9.withLogSpan("tablinum.reconcileRelay"));
|
|
1968
|
+
const syncAllRelays = (pubKeys, changedCollections) => Effect9.gen(function* () {
|
|
1969
|
+
const results = yield* Effect9.forEach(
|
|
1747
1970
|
relayUrls,
|
|
1748
1971
|
(url) => reconcileRelay(url, pubKeys),
|
|
1749
1972
|
{ concurrency: "unbounded" }
|
|
@@ -1760,44 +1983,44 @@ function createSyncHandle(storage, giftWrapHandle, relay, publishQueue, syncStat
|
|
|
1760
1983
|
}
|
|
1761
1984
|
const unwrapped = [];
|
|
1762
1985
|
for (const gw of allGiftWraps) {
|
|
1763
|
-
const result = yield* unwrapGiftWrap(gw).pipe(
|
|
1986
|
+
const result = yield* unwrapGiftWrap(gw).pipe(Effect9.orElseSucceed(() => null));
|
|
1764
1987
|
if (result) unwrapped.push(result);
|
|
1765
1988
|
}
|
|
1766
1989
|
unwrapped.sort((a, b) => a.rumor.created_at - b.rumor.created_at || (a.rumor.id < b.rumor.id ? -1 : 1));
|
|
1767
1990
|
for (const event of unwrapped) {
|
|
1768
1991
|
const collection2 = yield* applyUnwrappedEvent(event).pipe(
|
|
1769
|
-
|
|
1992
|
+
Effect9.orElseSucceed(() => null)
|
|
1770
1993
|
);
|
|
1771
1994
|
if (collection2) changedCollections.add(collection2);
|
|
1772
1995
|
}
|
|
1773
1996
|
const allHaveIds = results.flatMap((r) => r.haveIds);
|
|
1774
|
-
yield*
|
|
1997
|
+
yield* Effect9.forEach(
|
|
1775
1998
|
allHaveIds,
|
|
1776
|
-
({ id, url }) =>
|
|
1999
|
+
({ id, url }) => Effect9.gen(function* () {
|
|
1777
2000
|
const gw = yield* storage.getGiftWrap(id);
|
|
1778
2001
|
if (!gw?.event) return;
|
|
1779
2002
|
yield* relay.publish(gw.event, [url]).pipe(
|
|
1780
|
-
|
|
1781
|
-
|
|
2003
|
+
Effect9.tapError((err) => Effect9.sync(() => onSyncError?.(err))),
|
|
2004
|
+
Effect9.ignore
|
|
1782
2005
|
);
|
|
1783
2006
|
}),
|
|
1784
2007
|
{ concurrency: "unbounded", discard: true }
|
|
1785
2008
|
);
|
|
1786
|
-
}).pipe(
|
|
1787
|
-
const processGiftWrap = (remoteGw) =>
|
|
2009
|
+
}).pipe(Effect9.withLogSpan("tablinum.syncAllRelays"));
|
|
2010
|
+
const processGiftWrap = (remoteGw) => Effect9.gen(function* () {
|
|
1788
2011
|
const uw = yield* unwrapGiftWrap(remoteGw);
|
|
1789
2012
|
if (!uw) return null;
|
|
1790
2013
|
return yield* applyUnwrappedEvent(uw);
|
|
1791
2014
|
});
|
|
1792
|
-
const processRealtimeGiftWrap = (remoteGw) =>
|
|
1793
|
-
const collection2 = yield* processGiftWrap(remoteGw).pipe(
|
|
2015
|
+
const processRealtimeGiftWrap = (remoteGw) => Effect9.gen(function* () {
|
|
2016
|
+
const collection2 = yield* processGiftWrap(remoteGw).pipe(Effect9.orElseSucceed(() => null));
|
|
1794
2017
|
if (collection2) {
|
|
1795
2018
|
yield* notifyCollectionUpdated(collection2);
|
|
1796
2019
|
}
|
|
1797
2020
|
});
|
|
1798
|
-
const processRotationGiftWrap = (remoteGw) =>
|
|
1799
|
-
const unwrapResult = yield*
|
|
1800
|
-
|
|
2021
|
+
const processRotationGiftWrap = (remoteGw) => Effect9.gen(function* () {
|
|
2022
|
+
const unwrapResult = yield* Effect9.result(
|
|
2023
|
+
Effect9.try({
|
|
1801
2024
|
try: () => unwrapEvent(remoteGw, personalPrivateKey),
|
|
1802
2025
|
catch: (e) => new CryptoError({
|
|
1803
2026
|
message: `Rotation unwrap failed: ${e instanceof Error ? e.message : String(e)}`,
|
|
@@ -1847,73 +2070,73 @@ function createSyncHandle(storage, giftWrapHandle, relay, publishQueue, syncStat
|
|
|
1847
2070
|
yield* handle.addEpochSubscription(epoch.publicKey);
|
|
1848
2071
|
return true;
|
|
1849
2072
|
});
|
|
1850
|
-
const subscribeAcrossRelays = (filter, onEvent) =>
|
|
2073
|
+
const subscribeAcrossRelays = (filter, onEvent) => Effect9.forEach(
|
|
1851
2074
|
relayUrls,
|
|
1852
|
-
(url) =>
|
|
2075
|
+
(url) => Effect9.gen(function* () {
|
|
1853
2076
|
yield* relay.subscribe(filter, url, (event) => {
|
|
1854
2077
|
forkHandled(onEvent(event));
|
|
1855
2078
|
}).pipe(
|
|
1856
|
-
|
|
1857
|
-
|
|
2079
|
+
Effect9.tapError((err) => Effect9.sync(() => onSyncError?.(err))),
|
|
2080
|
+
Effect9.ignore
|
|
1858
2081
|
);
|
|
1859
2082
|
}),
|
|
1860
2083
|
{ concurrency: "unbounded", discard: true }
|
|
1861
2084
|
);
|
|
1862
2085
|
let healingActive = false;
|
|
1863
|
-
const healingEffect =
|
|
2086
|
+
const healingEffect = Effect9.gen(function* () {
|
|
1864
2087
|
if (!healingActive) return;
|
|
1865
2088
|
const status = yield* syncStatus.get();
|
|
1866
2089
|
if (status === "syncing") return;
|
|
1867
2090
|
yield* syncStatus.set("syncing");
|
|
1868
|
-
yield*
|
|
2091
|
+
yield* Effect9.gen(function* () {
|
|
1869
2092
|
const pubKeys = getSubscriptionPubKeys();
|
|
1870
2093
|
const changedCollections = /* @__PURE__ */ new Set();
|
|
1871
2094
|
yield* syncAllRelays(pubKeys, changedCollections);
|
|
1872
2095
|
if (changedCollections.size > 0) {
|
|
1873
2096
|
yield* notifyReplayComplete(watchCtx, [...changedCollections]);
|
|
1874
2097
|
}
|
|
1875
|
-
}).pipe(
|
|
1876
|
-
}).pipe(
|
|
2098
|
+
}).pipe(Effect9.ensuring(syncStatus.set("idle")));
|
|
2099
|
+
}).pipe(Effect9.ignore);
|
|
1877
2100
|
const handle = {
|
|
1878
|
-
sync: () =>
|
|
1879
|
-
yield*
|
|
2101
|
+
sync: () => Effect9.gen(function* () {
|
|
2102
|
+
yield* Effect9.logInfo("Sync started");
|
|
1880
2103
|
yield* syncStatus.set("syncing");
|
|
1881
2104
|
yield* Ref3.set(watchCtx.replayingRef, true);
|
|
1882
2105
|
const changedCollections = /* @__PURE__ */ new Set();
|
|
1883
|
-
yield*
|
|
2106
|
+
yield* Effect9.gen(function* () {
|
|
1884
2107
|
const pubKeys = getSubscriptionPubKeys();
|
|
1885
2108
|
yield* syncAllRelays(pubKeys, changedCollections);
|
|
1886
|
-
yield* publishQueue.flush(relayUrls).pipe(
|
|
2109
|
+
yield* publishQueue.flush(relayUrls).pipe(Effect9.ignore);
|
|
1887
2110
|
}).pipe(
|
|
1888
|
-
|
|
1889
|
-
|
|
2111
|
+
Effect9.ensuring(
|
|
2112
|
+
Effect9.gen(function* () {
|
|
1890
2113
|
yield* notifyReplayComplete(watchCtx, [...changedCollections]);
|
|
1891
2114
|
yield* syncStatus.set("idle");
|
|
1892
2115
|
})
|
|
1893
2116
|
)
|
|
1894
2117
|
);
|
|
1895
|
-
yield*
|
|
1896
|
-
}).pipe(
|
|
1897
|
-
publishLocal: (giftWrap) =>
|
|
2118
|
+
yield* Effect9.logInfo("Sync complete", { changed: [...changedCollections] });
|
|
2119
|
+
}).pipe(Effect9.withLogSpan("tablinum.sync")),
|
|
2120
|
+
publishLocal: (giftWrap) => Effect9.gen(function* () {
|
|
1898
2121
|
if (!giftWrap.event) return;
|
|
1899
2122
|
yield* relay.publish(giftWrap.event, relayUrls).pipe(
|
|
1900
|
-
|
|
2123
|
+
Effect9.tapError(
|
|
1901
2124
|
() => storage.putGiftWrap(giftWrap).pipe(
|
|
1902
|
-
|
|
1903
|
-
|
|
2125
|
+
Effect9.andThen(publishQueue.enqueue(giftWrap.id)),
|
|
2126
|
+
Effect9.andThen(Effect9.sync(() => scheduleAutoFlush()))
|
|
1904
2127
|
)
|
|
1905
2128
|
),
|
|
1906
|
-
|
|
1907
|
-
|
|
2129
|
+
Effect9.tapError((err) => Effect9.sync(() => onSyncError?.(err))),
|
|
2130
|
+
Effect9.ignore
|
|
1908
2131
|
);
|
|
1909
2132
|
}),
|
|
1910
|
-
startSubscription: () =>
|
|
2133
|
+
startSubscription: () => Effect9.gen(function* () {
|
|
1911
2134
|
const pubKeys = getSubscriptionPubKeys();
|
|
1912
2135
|
yield* subscribeAcrossRelays({ kinds: [GiftWrap2], "#p": pubKeys }, processRealtimeGiftWrap);
|
|
1913
2136
|
if (!pubKeys.includes(personalPublicKey)) {
|
|
1914
2137
|
yield* subscribeAcrossRelays(
|
|
1915
2138
|
{ kinds: [GiftWrap2], "#p": [personalPublicKey] },
|
|
1916
|
-
(event) =>
|
|
2139
|
+
(event) => Effect9.result(processRotationGiftWrap(event)).pipe(Effect9.asVoid)
|
|
1917
2140
|
);
|
|
1918
2141
|
}
|
|
1919
2142
|
}),
|
|
@@ -1922,10 +2145,10 @@ function createSyncHandle(storage, giftWrapHandle, relay, publishQueue, syncStat
|
|
|
1922
2145
|
if (healingActive) return;
|
|
1923
2146
|
healingActive = true;
|
|
1924
2147
|
forkHandled(
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
2148
|
+
Effect9.sleep(Duration.minutes(5)).pipe(
|
|
2149
|
+
Effect9.andThen(healingEffect),
|
|
2150
|
+
Effect9.repeat(Schedule.spaced(Duration.minutes(5))),
|
|
2151
|
+
Effect9.ensuring(Effect9.sync(() => {
|
|
1929
2152
|
healingActive = false;
|
|
1930
2153
|
}))
|
|
1931
2154
|
)
|
|
@@ -1937,8 +2160,8 @@ function createSyncHandle(storage, giftWrapHandle, relay, publishQueue, syncStat
|
|
|
1937
2160
|
};
|
|
1938
2161
|
forkHandled(
|
|
1939
2162
|
publishQueue.size().pipe(
|
|
1940
|
-
|
|
1941
|
-
(size) =>
|
|
2163
|
+
Effect9.flatMap(
|
|
2164
|
+
(size) => Effect9.sync(() => {
|
|
1942
2165
|
if (size > 0) scheduleAutoFlush();
|
|
1943
2166
|
})
|
|
1944
2167
|
)
|
|
@@ -1948,7 +2171,7 @@ function createSyncHandle(storage, giftWrapHandle, relay, publishQueue, syncStat
|
|
|
1948
2171
|
}
|
|
1949
2172
|
|
|
1950
2173
|
// src/db/members.ts
|
|
1951
|
-
import { Effect as
|
|
2174
|
+
import { Effect as Effect10, Option as Option6, Schema as Schema6 } from "effect";
|
|
1952
2175
|
var optionalString = {
|
|
1953
2176
|
_tag: "FieldDef",
|
|
1954
2177
|
kind: "string",
|
|
@@ -1997,15 +2220,15 @@ var AuthorProfileSchema = Schema6.Struct({
|
|
|
1997
2220
|
});
|
|
1998
2221
|
var decodeAuthorProfile = Schema6.decodeUnknownEffect(Schema6.fromJsonString(AuthorProfileSchema));
|
|
1999
2222
|
function fetchAuthorProfile(relay, relayUrls, pubkey) {
|
|
2000
|
-
return
|
|
2223
|
+
return Effect10.gen(function* () {
|
|
2001
2224
|
for (const url of relayUrls) {
|
|
2002
|
-
const result = yield*
|
|
2225
|
+
const result = yield* Effect10.result(
|
|
2003
2226
|
relay.fetchByFilter({ kinds: [0], authors: [pubkey], limit: 1 }, url)
|
|
2004
2227
|
);
|
|
2005
2228
|
if (result._tag === "Success" && result.success.length > 0) {
|
|
2006
2229
|
return yield* decodeAuthorProfile(result.success[0].content).pipe(
|
|
2007
|
-
|
|
2008
|
-
|
|
2230
|
+
Effect10.map(Option6.some),
|
|
2231
|
+
Effect10.orElseSucceed(() => Option6.none())
|
|
2009
2232
|
);
|
|
2010
2233
|
}
|
|
2011
2234
|
}
|
|
@@ -2013,6 +2236,20 @@ function fetchAuthorProfile(relay, relayUrls, pubkey) {
|
|
|
2013
2236
|
});
|
|
2014
2237
|
}
|
|
2015
2238
|
|
|
2239
|
+
// src/sync/deletion.ts
|
|
2240
|
+
import { finalizeEvent } from "nostr-tools/pure";
|
|
2241
|
+
function createDeletionEvent(targetEventIds, signingKey) {
|
|
2242
|
+
return finalizeEvent(
|
|
2243
|
+
{
|
|
2244
|
+
kind: 5,
|
|
2245
|
+
content: "",
|
|
2246
|
+
tags: targetEventIds.map((id) => ["e", id]),
|
|
2247
|
+
created_at: Math.floor(Date.now() / 1e3)
|
|
2248
|
+
},
|
|
2249
|
+
signingKey
|
|
2250
|
+
);
|
|
2251
|
+
}
|
|
2252
|
+
|
|
2016
2253
|
// src/services/Identity.ts
|
|
2017
2254
|
import { ServiceMap as ServiceMap3 } from "effect";
|
|
2018
2255
|
var Identity = class extends ServiceMap3.Service()("tablinum/Identity") {
|
|
@@ -2055,15 +2292,15 @@ var SyncStatus = class extends ServiceMap9.Service()(
|
|
|
2055
2292
|
};
|
|
2056
2293
|
|
|
2057
2294
|
// src/layers/IdentityLive.ts
|
|
2058
|
-
import { Effect as
|
|
2059
|
-
import { hexToBytes as
|
|
2295
|
+
import { Effect as Effect12, Layer as Layer2 } from "effect";
|
|
2296
|
+
import { hexToBytes as hexToBytes4 } from "@noble/hashes/utils.js";
|
|
2060
2297
|
|
|
2061
2298
|
// src/db/identity.ts
|
|
2062
|
-
import { Effect as
|
|
2299
|
+
import { Effect as Effect11 } from "effect";
|
|
2063
2300
|
import { getPublicKey as getPublicKey2 } from "nostr-tools/pure";
|
|
2064
|
-
import { bytesToHex as
|
|
2301
|
+
import { bytesToHex as bytesToHex3 } from "@noble/hashes/utils.js";
|
|
2065
2302
|
function createIdentity(suppliedKey) {
|
|
2066
|
-
return
|
|
2303
|
+
return Effect11.gen(function* () {
|
|
2067
2304
|
let privateKey;
|
|
2068
2305
|
if (suppliedKey) {
|
|
2069
2306
|
if (suppliedKey.length !== 32) {
|
|
@@ -2076,8 +2313,8 @@ function createIdentity(suppliedKey) {
|
|
|
2076
2313
|
privateKey = new Uint8Array(32);
|
|
2077
2314
|
crypto.getRandomValues(privateKey);
|
|
2078
2315
|
}
|
|
2079
|
-
const privateKeyHex =
|
|
2080
|
-
const publicKey = yield*
|
|
2316
|
+
const privateKeyHex = bytesToHex3(privateKey);
|
|
2317
|
+
const publicKey = yield* Effect11.try({
|
|
2081
2318
|
try: () => getPublicKey2(privateKey),
|
|
2082
2319
|
catch: (e) => new CryptoError({
|
|
2083
2320
|
message: `Failed to derive public key: ${e instanceof Error ? e.message : String(e)}`,
|
|
@@ -2095,14 +2332,14 @@ function createIdentity(suppliedKey) {
|
|
|
2095
2332
|
// src/layers/IdentityLive.ts
|
|
2096
2333
|
var IdentityLive = Layer2.effect(
|
|
2097
2334
|
Identity,
|
|
2098
|
-
|
|
2335
|
+
Effect12.gen(function* () {
|
|
2099
2336
|
const config = yield* Config;
|
|
2100
2337
|
const storage = yield* Storage;
|
|
2101
2338
|
const idbKey = yield* storage.getMeta("identity_key");
|
|
2102
|
-
const resolvedKey = config.privateKey ?? (typeof idbKey === "string" && idbKey.length === 64 ?
|
|
2339
|
+
const resolvedKey = config.privateKey ?? (typeof idbKey === "string" && idbKey.length === 64 ? hexToBytes4(idbKey) : void 0);
|
|
2103
2340
|
const identity = yield* createIdentity(resolvedKey);
|
|
2104
2341
|
yield* storage.putMeta("identity_key", identity.exportKey());
|
|
2105
|
-
yield*
|
|
2342
|
+
yield* Effect12.logInfo("Identity loaded", {
|
|
2106
2343
|
publicKey: identity.publicKey.slice(0, 12) + "...",
|
|
2107
2344
|
source: config.privateKey ? "config" : resolvedKey ? "storage" : "generated"
|
|
2108
2345
|
});
|
|
@@ -2111,12 +2348,12 @@ var IdentityLive = Layer2.effect(
|
|
|
2111
2348
|
);
|
|
2112
2349
|
|
|
2113
2350
|
// src/layers/EpochStoreLive.ts
|
|
2114
|
-
import { Effect as
|
|
2351
|
+
import { Effect as Effect13, Layer as Layer3, Option as Option7 } from "effect";
|
|
2115
2352
|
import { generateSecretKey as generateSecretKey2 } from "nostr-tools/pure";
|
|
2116
|
-
import { bytesToHex as
|
|
2353
|
+
import { bytesToHex as bytesToHex4 } from "@noble/hashes/utils.js";
|
|
2117
2354
|
var EpochStoreLive = Layer3.effect(
|
|
2118
2355
|
EpochStore,
|
|
2119
|
-
|
|
2356
|
+
Effect13.gen(function* () {
|
|
2120
2357
|
const config = yield* Config;
|
|
2121
2358
|
const identity = yield* Identity;
|
|
2122
2359
|
const storage = yield* Storage;
|
|
@@ -2135,7 +2372,7 @@ var EpochStoreLive = Layer3.effect(
|
|
|
2135
2372
|
return existing !== void 0 && existing.privateKey === ek.key;
|
|
2136
2373
|
});
|
|
2137
2374
|
if (configIsSubset) {
|
|
2138
|
-
yield*
|
|
2375
|
+
yield* Effect13.logInfo("Epoch store loaded", {
|
|
2139
2376
|
source: "storage",
|
|
2140
2377
|
epochs: idbStore.epochs.size
|
|
2141
2378
|
});
|
|
@@ -2144,271 +2381,28 @@ var EpochStoreLive = Layer3.effect(
|
|
|
2144
2381
|
}
|
|
2145
2382
|
const store2 = createEpochStoreFromInputs(config.epochKeys);
|
|
2146
2383
|
yield* storage.putMeta("epochs", stringifyEpochStore(store2));
|
|
2147
|
-
yield*
|
|
2384
|
+
yield* Effect13.logInfo("Epoch store loaded", { source: "config", epochs: store2.epochs.size });
|
|
2148
2385
|
return store2;
|
|
2149
2386
|
}
|
|
2150
2387
|
if (idbStore) {
|
|
2151
|
-
yield*
|
|
2388
|
+
yield* Effect13.logInfo("Epoch store loaded", {
|
|
2152
2389
|
source: "storage",
|
|
2153
2390
|
epochs: idbStore.epochs.size
|
|
2154
2391
|
});
|
|
2155
2392
|
return idbStore;
|
|
2156
2393
|
}
|
|
2157
2394
|
const store = createEpochStoreFromInputs(
|
|
2158
|
-
[{ epochId: EpochId("epoch-0"), key:
|
|
2395
|
+
[{ epochId: EpochId("epoch-0"), key: bytesToHex4(generateSecretKey2()) }],
|
|
2159
2396
|
{ createdBy: identity.publicKey }
|
|
2160
2397
|
);
|
|
2161
2398
|
yield* storage.putMeta("epochs", stringifyEpochStore(store));
|
|
2162
|
-
yield*
|
|
2399
|
+
yield* Effect13.logInfo("Epoch store loaded", { source: "generated", epochs: store.epochs.size });
|
|
2163
2400
|
return store;
|
|
2164
2401
|
})
|
|
2165
2402
|
);
|
|
2166
2403
|
|
|
2167
2404
|
// src/layers/StorageLive.ts
|
|
2168
2405
|
import { Effect as Effect14, Layer as Layer4 } from "effect";
|
|
2169
|
-
|
|
2170
|
-
// src/storage/idb.ts
|
|
2171
|
-
import { Effect as Effect13 } from "effect";
|
|
2172
|
-
import { openDB } from "idb";
|
|
2173
|
-
|
|
2174
|
-
// src/sync/compact-event.ts
|
|
2175
|
-
import { bytesToHex as bytesToHex4, hexToBytes as hexToBytes4 } from "@noble/hashes/utils.js";
|
|
2176
|
-
var VERSION = 1;
|
|
2177
|
-
var HEADER_SIZE = 133;
|
|
2178
|
-
function base64ToBytes(base64) {
|
|
2179
|
-
const binary = atob(base64);
|
|
2180
|
-
const bytes = new Uint8Array(binary.length);
|
|
2181
|
-
for (let i = 0; i < binary.length; i++) {
|
|
2182
|
-
bytes[i] = binary.charCodeAt(i);
|
|
2183
|
-
}
|
|
2184
|
-
return bytes;
|
|
2185
|
-
}
|
|
2186
|
-
function bytesToBase64(bytes) {
|
|
2187
|
-
let binary = "";
|
|
2188
|
-
for (let i = 0; i < bytes.length; i++) {
|
|
2189
|
-
binary += String.fromCharCode(bytes[i]);
|
|
2190
|
-
}
|
|
2191
|
-
return btoa(binary);
|
|
2192
|
-
}
|
|
2193
|
-
function packEvent(event) {
|
|
2194
|
-
const pubkey = hexToBytes4(event.pubkey);
|
|
2195
|
-
const sig = hexToBytes4(event.sig);
|
|
2196
|
-
const recipientTag = event.tags.find((t) => t[0] === "p");
|
|
2197
|
-
if (!recipientTag) throw new Error("Gift wrap missing #p tag");
|
|
2198
|
-
if (event.tags.some((t) => t[0] !== "p")) {
|
|
2199
|
-
throw new Error("Gift wrap has unexpected non-p tags; compact encoding would lose them");
|
|
2200
|
-
}
|
|
2201
|
-
const recipient = hexToBytes4(recipientTag[1]);
|
|
2202
|
-
const createdAtBuf = new Uint8Array(4);
|
|
2203
|
-
new DataView(createdAtBuf.buffer).setUint32(0, event.created_at, false);
|
|
2204
|
-
const content = base64ToBytes(event.content);
|
|
2205
|
-
const result = new Uint8Array(HEADER_SIZE + content.length);
|
|
2206
|
-
result[0] = VERSION;
|
|
2207
|
-
result.set(pubkey, 1);
|
|
2208
|
-
result.set(sig, 33);
|
|
2209
|
-
result.set(recipient, 97);
|
|
2210
|
-
result.set(createdAtBuf, 129);
|
|
2211
|
-
result.set(content, HEADER_SIZE);
|
|
2212
|
-
return result;
|
|
2213
|
-
}
|
|
2214
|
-
function unpackEvent(id, compact) {
|
|
2215
|
-
const version = compact[0];
|
|
2216
|
-
if (version !== VERSION) throw new Error(`Unknown compact event version: ${version}`);
|
|
2217
|
-
const pubkey = bytesToHex4(compact.slice(1, 33));
|
|
2218
|
-
const sig = bytesToHex4(compact.slice(33, 97));
|
|
2219
|
-
const recipient = bytesToHex4(compact.slice(97, 129));
|
|
2220
|
-
const dv = new DataView(compact.buffer, compact.byteOffset + 129, 4);
|
|
2221
|
-
const createdAt = dv.getUint32(0, false);
|
|
2222
|
-
const content = bytesToBase64(compact.slice(HEADER_SIZE));
|
|
2223
|
-
return {
|
|
2224
|
-
id,
|
|
2225
|
-
pubkey,
|
|
2226
|
-
sig,
|
|
2227
|
-
created_at: createdAt,
|
|
2228
|
-
kind: 1059,
|
|
2229
|
-
tags: [["p", recipient]],
|
|
2230
|
-
content
|
|
2231
|
-
};
|
|
2232
|
-
}
|
|
2233
|
-
|
|
2234
|
-
// src/storage/idb.ts
|
|
2235
|
-
var DB_NAME = "tablinum";
|
|
2236
|
-
function storeName(collection2) {
|
|
2237
|
-
return `col_${collection2}`;
|
|
2238
|
-
}
|
|
2239
|
-
function computeSchemaSig(schema) {
|
|
2240
|
-
return Object.entries(schema).sort(([a], [b]) => a.localeCompare(b)).map(([name, def]) => {
|
|
2241
|
-
const indices = [...def.indices ?? []].sort().join(",");
|
|
2242
|
-
return `${name}:${indices}`;
|
|
2243
|
-
}).join("|");
|
|
2244
|
-
}
|
|
2245
|
-
function wrap(label, fn) {
|
|
2246
|
-
return Effect13.tryPromise({
|
|
2247
|
-
try: fn,
|
|
2248
|
-
catch: (e) => new StorageError({
|
|
2249
|
-
message: `IndexedDB ${label} failed: ${e instanceof Error ? e.message : String(e)}`,
|
|
2250
|
-
cause: e
|
|
2251
|
-
})
|
|
2252
|
-
});
|
|
2253
|
-
}
|
|
2254
|
-
function upgradeSchema(database, schema, tx) {
|
|
2255
|
-
if (!database.objectStoreNames.contains("_meta")) {
|
|
2256
|
-
database.createObjectStore("_meta");
|
|
2257
|
-
}
|
|
2258
|
-
if (!database.objectStoreNames.contains("events")) {
|
|
2259
|
-
const events = database.createObjectStore("events", { keyPath: "id" });
|
|
2260
|
-
events.createIndex("by-record", ["collection", "recordId"]);
|
|
2261
|
-
}
|
|
2262
|
-
if (!database.objectStoreNames.contains("giftwraps")) {
|
|
2263
|
-
database.createObjectStore("giftwraps", { keyPath: "id" });
|
|
2264
|
-
}
|
|
2265
|
-
const expectedStores = /* @__PURE__ */ new Set();
|
|
2266
|
-
for (const [, def] of Object.entries(schema)) {
|
|
2267
|
-
const sn = storeName(def.name);
|
|
2268
|
-
expectedStores.add(sn);
|
|
2269
|
-
if (!database.objectStoreNames.contains(sn)) {
|
|
2270
|
-
const store = database.createObjectStore(sn, { keyPath: "id" });
|
|
2271
|
-
for (const idx of def.indices ?? []) {
|
|
2272
|
-
store.createIndex(idx, idx);
|
|
2273
|
-
}
|
|
2274
|
-
} else {
|
|
2275
|
-
const store = tx.objectStore(sn);
|
|
2276
|
-
const existingIndices = new Set(Array.from(store.indexNames));
|
|
2277
|
-
const wantedIndices = new Set(def.indices ?? []);
|
|
2278
|
-
for (const idx of existingIndices) {
|
|
2279
|
-
if (!wantedIndices.has(idx)) store.deleteIndex(idx);
|
|
2280
|
-
}
|
|
2281
|
-
for (const idx of wantedIndices) {
|
|
2282
|
-
if (!existingIndices.has(idx)) store.createIndex(idx, idx);
|
|
2283
|
-
}
|
|
2284
|
-
}
|
|
2285
|
-
}
|
|
2286
|
-
for (const existing of Array.from(database.objectStoreNames)) {
|
|
2287
|
-
if (existing.startsWith("col_") && !expectedStores.has(existing)) {
|
|
2288
|
-
database.deleteObjectStore(existing);
|
|
2289
|
-
}
|
|
2290
|
-
}
|
|
2291
|
-
tx.objectStore("_meta").put(computeSchemaSig(schema), "schema_sig");
|
|
2292
|
-
}
|
|
2293
|
-
function openIDBStorage(dbName, schema) {
|
|
2294
|
-
return Effect13.gen(function* () {
|
|
2295
|
-
const name = dbName ?? DB_NAME;
|
|
2296
|
-
const schemaSig = computeSchemaSig(schema);
|
|
2297
|
-
if (typeof indexedDB === "undefined") {
|
|
2298
|
-
return yield* Effect13.fail(
|
|
2299
|
-
new StorageError({
|
|
2300
|
-
message: "IndexedDB is not available in this environment"
|
|
2301
|
-
})
|
|
2302
|
-
);
|
|
2303
|
-
}
|
|
2304
|
-
const probeDb = yield* Effect13.tryPromise({
|
|
2305
|
-
try: () => openDB(name),
|
|
2306
|
-
catch: (e) => new StorageError({ message: "Failed to open IndexedDB", cause: e })
|
|
2307
|
-
});
|
|
2308
|
-
const currentVersion = probeDb.version;
|
|
2309
|
-
let needsUpgrade = true;
|
|
2310
|
-
if (probeDb.objectStoreNames.contains("_meta")) {
|
|
2311
|
-
const storedSig = yield* Effect13.tryPromise({
|
|
2312
|
-
try: () => probeDb.get("_meta", "schema_sig"),
|
|
2313
|
-
catch: () => new StorageError({ message: "Failed to read schema meta" })
|
|
2314
|
-
}).pipe(Effect13.catch(() => Effect13.succeed(void 0)));
|
|
2315
|
-
needsUpgrade = storedSig !== schemaSig;
|
|
2316
|
-
}
|
|
2317
|
-
probeDb.close();
|
|
2318
|
-
const db = needsUpgrade ? yield* Effect13.tryPromise({
|
|
2319
|
-
try: () => openDB(name, currentVersion + 1, {
|
|
2320
|
-
upgrade(database, _oldVersion, _newVersion, transaction) {
|
|
2321
|
-
upgradeSchema(database, schema, transaction);
|
|
2322
|
-
}
|
|
2323
|
-
}),
|
|
2324
|
-
catch: (e) => new StorageError({ message: "Failed to open IndexedDB", cause: e })
|
|
2325
|
-
}) : yield* Effect13.tryPromise({
|
|
2326
|
-
try: () => openDB(name),
|
|
2327
|
-
catch: (e) => new StorageError({ message: "Failed to open IndexedDB", cause: e })
|
|
2328
|
-
});
|
|
2329
|
-
yield* Effect13.addFinalizer(() => Effect13.sync(() => db.close()));
|
|
2330
|
-
const handle = {
|
|
2331
|
-
putRecord: (collection2, record) => wrap("putRecord", () => db.put(storeName(collection2), record).then(() => void 0)),
|
|
2332
|
-
getRecord: (collection2, id) => wrap("getRecord", () => db.get(storeName(collection2), id)),
|
|
2333
|
-
getAllRecords: (collection2) => wrap("getAllRecords", () => db.getAll(storeName(collection2))),
|
|
2334
|
-
countRecords: (collection2) => wrap("countRecords", () => db.count(storeName(collection2))),
|
|
2335
|
-
clearRecords: (collection2) => wrap("clearRecords", () => db.clear(storeName(collection2))),
|
|
2336
|
-
getByIndex: (collection2, indexName, value) => wrap("getByIndex", () => db.getAllFromIndex(storeName(collection2), indexName, value)),
|
|
2337
|
-
getByIndexRange: (collection2, indexName, range) => wrap("getByIndexRange", () => db.getAllFromIndex(storeName(collection2), indexName, range)),
|
|
2338
|
-
getAllSorted: (collection2, indexName, direction) => wrap("getAllSorted", async () => {
|
|
2339
|
-
const sn = storeName(collection2);
|
|
2340
|
-
const tx = db.transaction(sn, "readonly");
|
|
2341
|
-
const store = tx.objectStore(sn);
|
|
2342
|
-
const index = store.index(indexName);
|
|
2343
|
-
const results = [];
|
|
2344
|
-
let cursor = await index.openCursor(null, direction ?? "next");
|
|
2345
|
-
while (cursor) {
|
|
2346
|
-
results.push(cursor.value);
|
|
2347
|
-
cursor = await cursor.continue();
|
|
2348
|
-
}
|
|
2349
|
-
return results;
|
|
2350
|
-
}),
|
|
2351
|
-
putEvent: (event) => wrap("putEvent", () => db.put("events", event).then(() => void 0)),
|
|
2352
|
-
getEvent: (id) => wrap("getEvent", () => db.get("events", id)),
|
|
2353
|
-
getAllEvents: () => wrap("getAllEvents", () => db.getAll("events")),
|
|
2354
|
-
getEventsByRecord: (collection2, recordId) => wrap(
|
|
2355
|
-
"getEventsByRecord",
|
|
2356
|
-
() => db.getAllFromIndex("events", "by-record", [collection2, recordId])
|
|
2357
|
-
),
|
|
2358
|
-
putGiftWrap: (gw) => wrap("putGiftWrap", async () => {
|
|
2359
|
-
if (gw.event) {
|
|
2360
|
-
const compact = packEvent(gw.event);
|
|
2361
|
-
await db.put("giftwraps", { id: gw.id, compact, createdAt: gw.createdAt });
|
|
2362
|
-
} else {
|
|
2363
|
-
await db.put("giftwraps", { id: gw.id, createdAt: gw.createdAt });
|
|
2364
|
-
}
|
|
2365
|
-
}),
|
|
2366
|
-
getGiftWrap: (id) => wrap("getGiftWrap", async () => {
|
|
2367
|
-
const raw = await db.get("giftwraps", id);
|
|
2368
|
-
if (!raw) return void 0;
|
|
2369
|
-
if (raw.compact) {
|
|
2370
|
-
return { id: raw.id, event: unpackEvent(raw.id, raw.compact), createdAt: raw.createdAt };
|
|
2371
|
-
}
|
|
2372
|
-
if (raw.event) {
|
|
2373
|
-
const compact = packEvent(raw.event);
|
|
2374
|
-
await db.put("giftwraps", { id: raw.id, compact, createdAt: raw.createdAt });
|
|
2375
|
-
return { id: raw.id, event: raw.event, createdAt: raw.createdAt };
|
|
2376
|
-
}
|
|
2377
|
-
return { id: raw.id, createdAt: raw.createdAt };
|
|
2378
|
-
}),
|
|
2379
|
-
getAllGiftWraps: () => wrap("getAllGiftWraps", async () => {
|
|
2380
|
-
const raws = await db.getAll("giftwraps");
|
|
2381
|
-
const results = [];
|
|
2382
|
-
for (const raw of raws) {
|
|
2383
|
-
if (raw.compact) {
|
|
2384
|
-
results.push({ id: raw.id, event: unpackEvent(raw.id, raw.compact), createdAt: raw.createdAt });
|
|
2385
|
-
} else if (raw.event) {
|
|
2386
|
-
const compact = packEvent(raw.event);
|
|
2387
|
-
await db.put("giftwraps", { id: raw.id, compact, createdAt: raw.createdAt });
|
|
2388
|
-
results.push({ id: raw.id, event: raw.event, createdAt: raw.createdAt });
|
|
2389
|
-
} else {
|
|
2390
|
-
results.push({ id: raw.id, createdAt: raw.createdAt });
|
|
2391
|
-
}
|
|
2392
|
-
}
|
|
2393
|
-
return results;
|
|
2394
|
-
}),
|
|
2395
|
-
deleteGiftWrap: (id) => wrap("deleteGiftWrap", () => db.delete("giftwraps", id).then(() => void 0)),
|
|
2396
|
-
deleteEvent: (id) => wrap("deleteEvent", () => db.delete("events", id).then(() => void 0)),
|
|
2397
|
-
stripEventData: (id) => wrap("stripEventData", async () => {
|
|
2398
|
-
const existing = await db.get("events", id);
|
|
2399
|
-
if (existing) {
|
|
2400
|
-
await db.put("events", { ...existing, data: null });
|
|
2401
|
-
}
|
|
2402
|
-
}),
|
|
2403
|
-
getMeta: (key) => wrap("getMeta", () => db.get("_meta", key)),
|
|
2404
|
-
putMeta: (key, value) => wrap("putMeta", () => db.put("_meta", value, key).then(() => void 0)),
|
|
2405
|
-
close: () => Effect13.sync(() => db.close())
|
|
2406
|
-
};
|
|
2407
|
-
return handle;
|
|
2408
|
-
});
|
|
2409
|
-
}
|
|
2410
|
-
|
|
2411
|
-
// src/layers/StorageLive.ts
|
|
2412
2406
|
var StorageLive = Layer4.effect(
|
|
2413
2407
|
Storage,
|
|
2414
2408
|
Effect14.gen(function* () {
|
|
@@ -2945,6 +2939,12 @@ var TablinumLive = Layer9.effect(
|
|
|
2945
2939
|
}
|
|
2946
2940
|
const gw = wrapResult.success;
|
|
2947
2941
|
yield* storage.putGiftWrap({ id: gw.id, event: gw, createdAt: gw.created_at });
|
|
2942
|
+
const metaKey = `gw_record:${event.collection}:${event.recordId}`;
|
|
2943
|
+
const prevMapping = yield* storage.getMeta(metaKey).pipe(
|
|
2944
|
+
Effect21.orElseSucceed(() => void 0)
|
|
2945
|
+
);
|
|
2946
|
+
const epochPubKey = gw.tags.find((t) => t[0] === "p")?.[1];
|
|
2947
|
+
yield* storage.putMeta(metaKey, { gwId: gw.id, epochPubKey });
|
|
2948
2948
|
yield* Effect21.forkIn(
|
|
2949
2949
|
Effect21.gen(function* () {
|
|
2950
2950
|
const publishResult = yield* Effect21.result(
|
|
@@ -2957,10 +2957,80 @@ var TablinumLive = Layer9.effect(
|
|
|
2957
2957
|
if (publishResult._tag === "Failure") {
|
|
2958
2958
|
reportSyncError(config.onSyncError, publishResult.failure);
|
|
2959
2959
|
}
|
|
2960
|
+
if (prevMapping?.epochPubKey) {
|
|
2961
|
+
const signingKey = getDecryptionKey(epochStore, prevMapping.epochPubKey);
|
|
2962
|
+
if (signingKey) {
|
|
2963
|
+
const deletionEvent = createDeletionEvent(
|
|
2964
|
+
[prevMapping.gwId],
|
|
2965
|
+
signingKey
|
|
2966
|
+
);
|
|
2967
|
+
yield* relay.publish(deletionEvent, [...config.relays]).pipe(
|
|
2968
|
+
Effect21.tapError(
|
|
2969
|
+
(e) => Effect21.sync(() => reportSyncError(config.onSyncError, e))
|
|
2970
|
+
),
|
|
2971
|
+
Effect21.ignore
|
|
2972
|
+
);
|
|
2973
|
+
}
|
|
2974
|
+
yield* storage.deleteGiftWrap(prevMapping.gwId).pipe(Effect21.ignore);
|
|
2975
|
+
}
|
|
2960
2976
|
}),
|
|
2961
2977
|
scope
|
|
2962
2978
|
);
|
|
2963
2979
|
});
|
|
2980
|
+
const republishAllUnderCurrentEpoch = () => Effect21.gen(function* () {
|
|
2981
|
+
const oldGwDeletions = [];
|
|
2982
|
+
for (const [, def] of allSchemaEntries) {
|
|
2983
|
+
const collectionName = def.name;
|
|
2984
|
+
const allRecords = yield* storage.getAllRecords(collectionName);
|
|
2985
|
+
for (const record of allRecords) {
|
|
2986
|
+
const recordId = record.id;
|
|
2987
|
+
const { _d, _u, _a, _e, ...fields } = record;
|
|
2988
|
+
const content = _d ? JSON.stringify(null) : JSON.stringify(fields);
|
|
2989
|
+
const dTag = `${collectionName}:${recordId}`;
|
|
2990
|
+
const wrapResult = yield* Effect21.result(
|
|
2991
|
+
giftWrap.wrap({
|
|
2992
|
+
kind: 1,
|
|
2993
|
+
content,
|
|
2994
|
+
tags: [["d", dTag]],
|
|
2995
|
+
created_at: Math.floor(Date.now() / 1e3)
|
|
2996
|
+
})
|
|
2997
|
+
);
|
|
2998
|
+
if (wrapResult._tag === "Failure") continue;
|
|
2999
|
+
const gw = wrapResult.success;
|
|
3000
|
+
yield* storage.putGiftWrap({ id: gw.id, event: gw, createdAt: gw.created_at });
|
|
3001
|
+
const metaKey = `gw_record:${collectionName}:${recordId}`;
|
|
3002
|
+
const prevMapping = yield* storage.getMeta(metaKey).pipe(
|
|
3003
|
+
Effect21.orElseSucceed(() => void 0)
|
|
3004
|
+
);
|
|
3005
|
+
if (prevMapping) oldGwDeletions.push(prevMapping);
|
|
3006
|
+
const epochPubKey = gw.tags.find((t) => t[0] === "p")?.[1];
|
|
3007
|
+
yield* storage.putMeta(metaKey, { gwId: gw.id, epochPubKey });
|
|
3008
|
+
yield* relay.publish(gw, [...config.relays]).pipe(
|
|
3009
|
+
Effect21.tapError((e) => Effect21.sync(() => reportSyncError(config.onSyncError, e))),
|
|
3010
|
+
Effect21.ignore
|
|
3011
|
+
);
|
|
3012
|
+
}
|
|
3013
|
+
}
|
|
3014
|
+
const byEpoch = /* @__PURE__ */ new Map();
|
|
3015
|
+
for (const { gwId, epochPubKey } of oldGwDeletions) {
|
|
3016
|
+
const ids = byEpoch.get(epochPubKey) ?? [];
|
|
3017
|
+
ids.push(gwId);
|
|
3018
|
+
byEpoch.set(epochPubKey, ids);
|
|
3019
|
+
}
|
|
3020
|
+
for (const [epochPubKey, gwIds] of byEpoch) {
|
|
3021
|
+
const signingKey = getDecryptionKey(epochStore, epochPubKey);
|
|
3022
|
+
if (signingKey) {
|
|
3023
|
+
const deletionEvent = createDeletionEvent(gwIds, signingKey);
|
|
3024
|
+
yield* relay.publish(deletionEvent, [...config.relays]).pipe(
|
|
3025
|
+
Effect21.tapError((e) => Effect21.sync(() => reportSyncError(config.onSyncError, e))),
|
|
3026
|
+
Effect21.ignore
|
|
3027
|
+
);
|
|
3028
|
+
}
|
|
3029
|
+
for (const gwId of gwIds) {
|
|
3030
|
+
yield* storage.deleteGiftWrap(gwId).pipe(Effect21.ignore);
|
|
3031
|
+
}
|
|
3032
|
+
}
|
|
3033
|
+
});
|
|
2964
3034
|
const knownAuthors = /* @__PURE__ */ new Set();
|
|
2965
3035
|
const putMemberRecord = (record) => Effect21.gen(function* () {
|
|
2966
3036
|
const existing = yield* storage.getRecord("_members", record.id);
|
|
@@ -3048,6 +3118,13 @@ var TablinumLive = Layer9.effect(
|
|
|
3048
3118
|
addedInEpoch: getCurrentEpoch(epochStore).id
|
|
3049
3119
|
});
|
|
3050
3120
|
}
|
|
3121
|
+
const migrated = yield* storage.getMeta("migration_gw_republish").pipe(
|
|
3122
|
+
Effect21.orElseSucceed(() => void 0)
|
|
3123
|
+
);
|
|
3124
|
+
if (!migrated) {
|
|
3125
|
+
yield* republishAllUnderCurrentEpoch().pipe(Effect21.ignore);
|
|
3126
|
+
yield* storage.putMeta("migration_gw_republish", true);
|
|
3127
|
+
}
|
|
3051
3128
|
const withLog = (effect) => Effect21.provideService(effect, References3.MinimumLogLevel, config.logLevel);
|
|
3052
3129
|
const ensureOpen = (effect) => withLog(
|
|
3053
3130
|
Effect21.gen(function* () {
|
|
@@ -3087,6 +3164,61 @@ var TablinumLive = Layer9.effect(
|
|
|
3087
3164
|
yield* Scope4.close(scope, Exit.void);
|
|
3088
3165
|
})
|
|
3089
3166
|
),
|
|
3167
|
+
destroy: () => withLog(
|
|
3168
|
+
Effect21.gen(function* () {
|
|
3169
|
+
if (!(yield* Ref5.get(closedRef))) {
|
|
3170
|
+
yield* Ref5.set(closedRef, true);
|
|
3171
|
+
syncHandle.stopHealing();
|
|
3172
|
+
yield* Scope4.close(scope, Exit.void);
|
|
3173
|
+
}
|
|
3174
|
+
yield* deleteIDBStorage(config.dbName);
|
|
3175
|
+
})
|
|
3176
|
+
),
|
|
3177
|
+
leave: () => withLog(
|
|
3178
|
+
Effect21.gen(function* () {
|
|
3179
|
+
if (yield* Ref5.get(closedRef)) {
|
|
3180
|
+
return yield* new SyncError({ message: "Database is closed", phase: "leave" });
|
|
3181
|
+
}
|
|
3182
|
+
const allMembers = yield* storage.getAllRecords("_members");
|
|
3183
|
+
const activeMembers = allMembers.filter(
|
|
3184
|
+
(member) => !member.removedAt && member.id !== identity.publicKey
|
|
3185
|
+
);
|
|
3186
|
+
const activePubkeys = activeMembers.map((member) => member.id);
|
|
3187
|
+
const result = createRotation(
|
|
3188
|
+
epochStore,
|
|
3189
|
+
identity.privateKey,
|
|
3190
|
+
identity.publicKey,
|
|
3191
|
+
activePubkeys,
|
|
3192
|
+
[identity.publicKey]
|
|
3193
|
+
);
|
|
3194
|
+
addEpoch(epochStore, result.epoch);
|
|
3195
|
+
epochStore.currentEpochId = result.epoch.id;
|
|
3196
|
+
yield* storage.putMeta("epochs", stringifyEpochStore(epochStore));
|
|
3197
|
+
const memberRecord = yield* storage.getRecord("_members", identity.publicKey);
|
|
3198
|
+
yield* putMemberRecord({
|
|
3199
|
+
...memberRecord ?? {
|
|
3200
|
+
id: identity.publicKey,
|
|
3201
|
+
addedAt: 0,
|
|
3202
|
+
addedInEpoch: EpochId("epoch-0")
|
|
3203
|
+
},
|
|
3204
|
+
removedAt: Date.now(),
|
|
3205
|
+
removedInEpoch: result.epoch.id
|
|
3206
|
+
});
|
|
3207
|
+
yield* republishAllUnderCurrentEpoch();
|
|
3208
|
+
yield* Effect21.forEach(
|
|
3209
|
+
result.wrappedEvents,
|
|
3210
|
+
(wrappedEvent) => relay.publish(wrappedEvent, [...config.relays]).pipe(
|
|
3211
|
+
Effect21.tapError((e) => Effect21.sync(() => reportSyncError(config.onSyncError, e))),
|
|
3212
|
+
Effect21.ignore
|
|
3213
|
+
),
|
|
3214
|
+
{ discard: true }
|
|
3215
|
+
);
|
|
3216
|
+
yield* Ref5.set(closedRef, true);
|
|
3217
|
+
syncHandle.stopHealing();
|
|
3218
|
+
yield* Scope4.close(scope, Exit.void);
|
|
3219
|
+
yield* deleteIDBStorage(config.dbName);
|
|
3220
|
+
})
|
|
3221
|
+
),
|
|
3090
3222
|
rebuild: () => ensureOpen(
|
|
3091
3223
|
rebuild(
|
|
3092
3224
|
storage,
|
|
@@ -3147,6 +3279,7 @@ var TablinumLive = Layer9.effect(
|
|
|
3147
3279
|
removedAt: Date.now(),
|
|
3148
3280
|
removedInEpoch: result.epoch.id
|
|
3149
3281
|
});
|
|
3282
|
+
yield* republishAllUnderCurrentEpoch();
|
|
3150
3283
|
yield* Effect21.forEach(
|
|
3151
3284
|
result.wrappedEvents,
|
|
3152
3285
|
(wrappedEvent) => relay.publish(wrappedEvent, [...config.relays]).pipe(
|
|
@@ -3224,6 +3357,9 @@ function validateConfig(config) {
|
|
|
3224
3357
|
}
|
|
3225
3358
|
});
|
|
3226
3359
|
}
|
|
3360
|
+
function deleteDatabase(dbName) {
|
|
3361
|
+
return deleteIDBStorage(DatabaseName(dbName ?? "tablinum"));
|
|
3362
|
+
}
|
|
3227
3363
|
function createTablinum(config) {
|
|
3228
3364
|
return Effect22.gen(function* () {
|
|
3229
3365
|
yield* validateConfig(config);
|
|
@@ -3245,6 +3381,9 @@ function createTablinum(config) {
|
|
|
3245
3381
|
});
|
|
3246
3382
|
}
|
|
3247
3383
|
|
|
3384
|
+
// src/svelte/tablinum.svelte.ts
|
|
3385
|
+
import { Effect as Effect25, Exit as Exit2, References as References6, Scope as Scope6 } from "effect";
|
|
3386
|
+
|
|
3248
3387
|
// src/svelte/collection.svelte.ts
|
|
3249
3388
|
import { Effect as Effect24, Fiber, Option as Option11, References as References5, Stream as Stream4 } from "effect";
|
|
3250
3389
|
|
|
@@ -3479,9 +3618,11 @@ var Tablinum2 = class {
|
|
|
3479
3618
|
#closed = false;
|
|
3480
3619
|
#readyState = createDeferred();
|
|
3481
3620
|
#logLevel;
|
|
3621
|
+
#dbName;
|
|
3482
3622
|
constructor(config) {
|
|
3483
3623
|
this.ready = this.#readyState.promise;
|
|
3484
3624
|
this.#logLevel = resolveLogLevel(config.logLevel);
|
|
3625
|
+
this.#dbName = config.dbName ?? "tablinum";
|
|
3485
3626
|
this.#init(config);
|
|
3486
3627
|
}
|
|
3487
3628
|
#settleReady(err) {
|
|
@@ -3616,6 +3757,14 @@ var Tablinum2 = class {
|
|
|
3616
3757
|
}
|
|
3617
3758
|
this.status = "closed";
|
|
3618
3759
|
};
|
|
3760
|
+
destroy = async () => {
|
|
3761
|
+
await this.close();
|
|
3762
|
+
await Effect25.runPromise(deleteDatabase(this.#dbName));
|
|
3763
|
+
};
|
|
3764
|
+
leave = async () => {
|
|
3765
|
+
await this.#runHandleEffect((handle) => handle.leave());
|
|
3766
|
+
await Effect25.runPromise(deleteDatabase(this.#dbName));
|
|
3767
|
+
};
|
|
3619
3768
|
sync = async () => this.#runHandleEffect((handle) => handle.sync());
|
|
3620
3769
|
rebuild = async () => this.#runHandleEffect((handle) => handle.rebuild());
|
|
3621
3770
|
addMember = async (pubkey) => this.#runHandleEffect((handle) => handle.addMember(pubkey));
|
|
@@ -3636,6 +3785,7 @@ export {
|
|
|
3636
3785
|
ValidationError,
|
|
3637
3786
|
collection,
|
|
3638
3787
|
decodeInvite,
|
|
3788
|
+
deleteDatabase,
|
|
3639
3789
|
encodeInvite,
|
|
3640
3790
|
field
|
|
3641
3791
|
};
|