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
package/dist/index.js
CHANGED
|
@@ -224,6 +224,257 @@ function resolveRuntimeConfig(source) {
|
|
|
224
224
|
);
|
|
225
225
|
}
|
|
226
226
|
|
|
227
|
+
// src/storage/idb.ts
|
|
228
|
+
import { Effect as Effect2 } from "effect";
|
|
229
|
+
import { openDB, deleteDB } from "idb";
|
|
230
|
+
|
|
231
|
+
// src/sync/compact-event.ts
|
|
232
|
+
import { bytesToHex as bytesToHex2, hexToBytes as hexToBytes2 } from "@noble/hashes/utils.js";
|
|
233
|
+
var VERSION = 1;
|
|
234
|
+
var HEADER_SIZE = 133;
|
|
235
|
+
function base64ToBytes(base64) {
|
|
236
|
+
const binary = atob(base64);
|
|
237
|
+
const bytes = new Uint8Array(binary.length);
|
|
238
|
+
for (let i = 0; i < binary.length; i++) {
|
|
239
|
+
bytes[i] = binary.charCodeAt(i);
|
|
240
|
+
}
|
|
241
|
+
return bytes;
|
|
242
|
+
}
|
|
243
|
+
function bytesToBase64(bytes) {
|
|
244
|
+
let binary = "";
|
|
245
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
246
|
+
binary += String.fromCharCode(bytes[i]);
|
|
247
|
+
}
|
|
248
|
+
return btoa(binary);
|
|
249
|
+
}
|
|
250
|
+
function packEvent(event) {
|
|
251
|
+
const pubkey = hexToBytes2(event.pubkey);
|
|
252
|
+
const sig = hexToBytes2(event.sig);
|
|
253
|
+
const recipientTag = event.tags.find((t) => t[0] === "p");
|
|
254
|
+
if (!recipientTag) throw new Error("Gift wrap missing #p tag");
|
|
255
|
+
if (event.tags.some((t) => t[0] !== "p")) {
|
|
256
|
+
throw new Error("Gift wrap has unexpected non-p tags; compact encoding would lose them");
|
|
257
|
+
}
|
|
258
|
+
const recipient = hexToBytes2(recipientTag[1]);
|
|
259
|
+
const createdAtBuf = new Uint8Array(4);
|
|
260
|
+
new DataView(createdAtBuf.buffer).setUint32(0, event.created_at, false);
|
|
261
|
+
const content = base64ToBytes(event.content);
|
|
262
|
+
const result = new Uint8Array(HEADER_SIZE + content.length);
|
|
263
|
+
result[0] = VERSION;
|
|
264
|
+
result.set(pubkey, 1);
|
|
265
|
+
result.set(sig, 33);
|
|
266
|
+
result.set(recipient, 97);
|
|
267
|
+
result.set(createdAtBuf, 129);
|
|
268
|
+
result.set(content, HEADER_SIZE);
|
|
269
|
+
return result;
|
|
270
|
+
}
|
|
271
|
+
function unpackEvent(id, compact) {
|
|
272
|
+
const version = compact[0];
|
|
273
|
+
if (version !== VERSION) throw new Error(`Unknown compact event version: ${version}`);
|
|
274
|
+
const pubkey = bytesToHex2(compact.slice(1, 33));
|
|
275
|
+
const sig = bytesToHex2(compact.slice(33, 97));
|
|
276
|
+
const recipient = bytesToHex2(compact.slice(97, 129));
|
|
277
|
+
const dv = new DataView(compact.buffer, compact.byteOffset + 129, 4);
|
|
278
|
+
const createdAt = dv.getUint32(0, false);
|
|
279
|
+
const content = bytesToBase64(compact.slice(HEADER_SIZE));
|
|
280
|
+
return {
|
|
281
|
+
id,
|
|
282
|
+
pubkey,
|
|
283
|
+
sig,
|
|
284
|
+
created_at: createdAt,
|
|
285
|
+
kind: 1059,
|
|
286
|
+
tags: [["p", recipient]],
|
|
287
|
+
content
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// src/storage/idb.ts
|
|
292
|
+
var DB_NAME = "tablinum";
|
|
293
|
+
function storeName(collection2) {
|
|
294
|
+
return `col_${collection2}`;
|
|
295
|
+
}
|
|
296
|
+
function computeSchemaSig(schema) {
|
|
297
|
+
return Object.entries(schema).sort(([a], [b]) => a.localeCompare(b)).map(([name, def]) => {
|
|
298
|
+
const indices = [...def.indices ?? []].sort().join(",");
|
|
299
|
+
return `${name}:${indices}`;
|
|
300
|
+
}).join("|");
|
|
301
|
+
}
|
|
302
|
+
function wrap(label, fn) {
|
|
303
|
+
return Effect2.tryPromise({
|
|
304
|
+
try: fn,
|
|
305
|
+
catch: (e) => new StorageError({
|
|
306
|
+
message: `IndexedDB ${label} failed: ${e instanceof Error ? e.message : String(e)}`,
|
|
307
|
+
cause: e
|
|
308
|
+
})
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
function upgradeSchema(database, schema, tx) {
|
|
312
|
+
if (!database.objectStoreNames.contains("_meta")) {
|
|
313
|
+
database.createObjectStore("_meta");
|
|
314
|
+
}
|
|
315
|
+
if (!database.objectStoreNames.contains("events")) {
|
|
316
|
+
const events = database.createObjectStore("events", { keyPath: "id" });
|
|
317
|
+
events.createIndex("by-record", ["collection", "recordId"]);
|
|
318
|
+
}
|
|
319
|
+
if (!database.objectStoreNames.contains("giftwraps")) {
|
|
320
|
+
database.createObjectStore("giftwraps", { keyPath: "id" });
|
|
321
|
+
}
|
|
322
|
+
const expectedStores = /* @__PURE__ */ new Set();
|
|
323
|
+
for (const [, def] of Object.entries(schema)) {
|
|
324
|
+
const sn = storeName(def.name);
|
|
325
|
+
expectedStores.add(sn);
|
|
326
|
+
if (!database.objectStoreNames.contains(sn)) {
|
|
327
|
+
const store = database.createObjectStore(sn, { keyPath: "id" });
|
|
328
|
+
for (const idx of def.indices ?? []) {
|
|
329
|
+
store.createIndex(idx, idx);
|
|
330
|
+
}
|
|
331
|
+
} else {
|
|
332
|
+
const store = tx.objectStore(sn);
|
|
333
|
+
const existingIndices = new Set(Array.from(store.indexNames));
|
|
334
|
+
const wantedIndices = new Set(def.indices ?? []);
|
|
335
|
+
for (const idx of existingIndices) {
|
|
336
|
+
if (!wantedIndices.has(idx)) store.deleteIndex(idx);
|
|
337
|
+
}
|
|
338
|
+
for (const idx of wantedIndices) {
|
|
339
|
+
if (!existingIndices.has(idx)) store.createIndex(idx, idx);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
for (const existing of Array.from(database.objectStoreNames)) {
|
|
344
|
+
if (existing.startsWith("col_") && !expectedStores.has(existing)) {
|
|
345
|
+
database.deleteObjectStore(existing);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
tx.objectStore("_meta").put(computeSchemaSig(schema), "schema_sig");
|
|
349
|
+
}
|
|
350
|
+
function deleteIDBStorage(dbName) {
|
|
351
|
+
if (typeof indexedDB === "undefined") {
|
|
352
|
+
return Effect2.fail(
|
|
353
|
+
new StorageError({
|
|
354
|
+
message: "IndexedDB is not available in this environment"
|
|
355
|
+
})
|
|
356
|
+
);
|
|
357
|
+
}
|
|
358
|
+
return wrap("deleteDatabase", () => deleteDB(dbName));
|
|
359
|
+
}
|
|
360
|
+
function openIDBStorage(dbName, schema) {
|
|
361
|
+
return Effect2.gen(function* () {
|
|
362
|
+
const name = dbName ?? DB_NAME;
|
|
363
|
+
const schemaSig = computeSchemaSig(schema);
|
|
364
|
+
if (typeof indexedDB === "undefined") {
|
|
365
|
+
return yield* Effect2.fail(
|
|
366
|
+
new StorageError({
|
|
367
|
+
message: "IndexedDB is not available in this environment"
|
|
368
|
+
})
|
|
369
|
+
);
|
|
370
|
+
}
|
|
371
|
+
const probeDb = yield* Effect2.tryPromise({
|
|
372
|
+
try: () => openDB(name),
|
|
373
|
+
catch: (e) => new StorageError({ message: "Failed to open IndexedDB", cause: e })
|
|
374
|
+
});
|
|
375
|
+
const currentVersion = probeDb.version;
|
|
376
|
+
let needsUpgrade = true;
|
|
377
|
+
if (probeDb.objectStoreNames.contains("_meta")) {
|
|
378
|
+
const storedSig = yield* Effect2.tryPromise({
|
|
379
|
+
try: () => probeDb.get("_meta", "schema_sig"),
|
|
380
|
+
catch: () => new StorageError({ message: "Failed to read schema meta" })
|
|
381
|
+
}).pipe(Effect2.catch(() => Effect2.succeed(void 0)));
|
|
382
|
+
needsUpgrade = storedSig !== schemaSig;
|
|
383
|
+
}
|
|
384
|
+
probeDb.close();
|
|
385
|
+
const db = needsUpgrade ? yield* Effect2.tryPromise({
|
|
386
|
+
try: () => openDB(name, currentVersion + 1, {
|
|
387
|
+
upgrade(database, _oldVersion, _newVersion, transaction) {
|
|
388
|
+
upgradeSchema(database, schema, transaction);
|
|
389
|
+
}
|
|
390
|
+
}),
|
|
391
|
+
catch: (e) => new StorageError({ message: "Failed to open IndexedDB", cause: e })
|
|
392
|
+
}) : yield* Effect2.tryPromise({
|
|
393
|
+
try: () => openDB(name),
|
|
394
|
+
catch: (e) => new StorageError({ message: "Failed to open IndexedDB", cause: e })
|
|
395
|
+
});
|
|
396
|
+
yield* Effect2.addFinalizer(() => Effect2.sync(() => db.close()));
|
|
397
|
+
const handle = {
|
|
398
|
+
putRecord: (collection2, record) => wrap("putRecord", () => db.put(storeName(collection2), record).then(() => void 0)),
|
|
399
|
+
getRecord: (collection2, id) => wrap("getRecord", () => db.get(storeName(collection2), id)),
|
|
400
|
+
getAllRecords: (collection2) => wrap("getAllRecords", () => db.getAll(storeName(collection2))),
|
|
401
|
+
countRecords: (collection2) => wrap("countRecords", () => db.count(storeName(collection2))),
|
|
402
|
+
clearRecords: (collection2) => wrap("clearRecords", () => db.clear(storeName(collection2))),
|
|
403
|
+
getByIndex: (collection2, indexName, value) => wrap("getByIndex", () => db.getAllFromIndex(storeName(collection2), indexName, value)),
|
|
404
|
+
getByIndexRange: (collection2, indexName, range) => wrap("getByIndexRange", () => db.getAllFromIndex(storeName(collection2), indexName, range)),
|
|
405
|
+
getAllSorted: (collection2, indexName, direction) => wrap("getAllSorted", async () => {
|
|
406
|
+
const sn = storeName(collection2);
|
|
407
|
+
const tx = db.transaction(sn, "readonly");
|
|
408
|
+
const store = tx.objectStore(sn);
|
|
409
|
+
const index = store.index(indexName);
|
|
410
|
+
const results = [];
|
|
411
|
+
let cursor = await index.openCursor(null, direction ?? "next");
|
|
412
|
+
while (cursor) {
|
|
413
|
+
results.push(cursor.value);
|
|
414
|
+
cursor = await cursor.continue();
|
|
415
|
+
}
|
|
416
|
+
return results;
|
|
417
|
+
}),
|
|
418
|
+
putEvent: (event) => wrap("putEvent", () => db.put("events", event).then(() => void 0)),
|
|
419
|
+
getEvent: (id) => wrap("getEvent", () => db.get("events", id)),
|
|
420
|
+
getAllEvents: () => wrap("getAllEvents", () => db.getAll("events")),
|
|
421
|
+
getEventsByRecord: (collection2, recordId) => wrap(
|
|
422
|
+
"getEventsByRecord",
|
|
423
|
+
() => db.getAllFromIndex("events", "by-record", [collection2, recordId])
|
|
424
|
+
),
|
|
425
|
+
putGiftWrap: (gw) => wrap("putGiftWrap", async () => {
|
|
426
|
+
if (gw.event) {
|
|
427
|
+
const compact = packEvent(gw.event);
|
|
428
|
+
await db.put("giftwraps", { id: gw.id, compact, createdAt: gw.createdAt });
|
|
429
|
+
} else {
|
|
430
|
+
await db.put("giftwraps", { id: gw.id, createdAt: gw.createdAt });
|
|
431
|
+
}
|
|
432
|
+
}),
|
|
433
|
+
getGiftWrap: (id) => wrap("getGiftWrap", async () => {
|
|
434
|
+
const raw = await db.get("giftwraps", id);
|
|
435
|
+
if (!raw) return void 0;
|
|
436
|
+
if (raw.compact) {
|
|
437
|
+
return { id: raw.id, event: unpackEvent(raw.id, raw.compact), createdAt: raw.createdAt };
|
|
438
|
+
}
|
|
439
|
+
if (raw.event) {
|
|
440
|
+
const compact = packEvent(raw.event);
|
|
441
|
+
await db.put("giftwraps", { id: raw.id, compact, createdAt: raw.createdAt });
|
|
442
|
+
return { id: raw.id, event: raw.event, createdAt: raw.createdAt };
|
|
443
|
+
}
|
|
444
|
+
return { id: raw.id, createdAt: raw.createdAt };
|
|
445
|
+
}),
|
|
446
|
+
getAllGiftWraps: () => wrap("getAllGiftWraps", async () => {
|
|
447
|
+
const raws = await db.getAll("giftwraps");
|
|
448
|
+
const results = [];
|
|
449
|
+
for (const raw of raws) {
|
|
450
|
+
if (raw.compact) {
|
|
451
|
+
results.push({ id: raw.id, event: unpackEvent(raw.id, raw.compact), createdAt: raw.createdAt });
|
|
452
|
+
} else if (raw.event) {
|
|
453
|
+
const compact = packEvent(raw.event);
|
|
454
|
+
await db.put("giftwraps", { id: raw.id, compact, createdAt: raw.createdAt });
|
|
455
|
+
results.push({ id: raw.id, event: raw.event, createdAt: raw.createdAt });
|
|
456
|
+
} else {
|
|
457
|
+
results.push({ id: raw.id, createdAt: raw.createdAt });
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
return results;
|
|
461
|
+
}),
|
|
462
|
+
deleteGiftWrap: (id) => wrap("deleteGiftWrap", () => db.delete("giftwraps", id).then(() => void 0)),
|
|
463
|
+
deleteEvent: (id) => wrap("deleteEvent", () => db.delete("events", id).then(() => void 0)),
|
|
464
|
+
stripEventData: (id) => wrap("stripEventData", async () => {
|
|
465
|
+
const existing = await db.get("events", id);
|
|
466
|
+
if (existing) {
|
|
467
|
+
await db.put("events", { ...existing, data: null });
|
|
468
|
+
}
|
|
469
|
+
}),
|
|
470
|
+
getMeta: (key) => wrap("getMeta", () => db.get("_meta", key)),
|
|
471
|
+
putMeta: (key, value) => wrap("putMeta", () => db.put("_meta", value, key).then(() => void 0)),
|
|
472
|
+
close: () => Effect2.sync(() => db.close())
|
|
473
|
+
};
|
|
474
|
+
return handle;
|
|
475
|
+
});
|
|
476
|
+
}
|
|
477
|
+
|
|
227
478
|
// src/services/Config.ts
|
|
228
479
|
import { ServiceMap } from "effect";
|
|
229
480
|
var Config = class extends ServiceMap.Service()("tablinum/Config") {
|
|
@@ -240,16 +491,16 @@ var Tablinum = class extends ServiceMap2.Service()(
|
|
|
240
491
|
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";
|
|
241
492
|
|
|
242
493
|
// src/crud/watch.ts
|
|
243
|
-
import { Effect as
|
|
494
|
+
import { Effect as Effect3, PubSub, Ref, Stream } from "effect";
|
|
244
495
|
function watchCollection(ctx, storage, collectionName, filter, mapRecord2) {
|
|
245
|
-
const query = () =>
|
|
496
|
+
const query = () => Effect3.map(storage.getAllRecords(collectionName), (all) => {
|
|
246
497
|
const filtered = all.filter((r) => !r._d && (filter ? filter(r) : true));
|
|
247
498
|
return mapRecord2 ? filtered.map(mapRecord2) : filtered;
|
|
248
499
|
});
|
|
249
500
|
const changes = Stream.fromPubSub(ctx.pubsub).pipe(
|
|
250
501
|
Stream.filter((event) => event.collection === collectionName),
|
|
251
502
|
Stream.mapEffect(
|
|
252
|
-
() =>
|
|
503
|
+
() => Effect3.gen(function* () {
|
|
253
504
|
const replaying = yield* Ref.get(ctx.replayingRef);
|
|
254
505
|
if (replaying) return void 0;
|
|
255
506
|
return yield* query();
|
|
@@ -258,18 +509,18 @@ function watchCollection(ctx, storage, collectionName, filter, mapRecord2) {
|
|
|
258
509
|
Stream.filter((result) => result !== void 0)
|
|
259
510
|
);
|
|
260
511
|
return Stream.unwrap(
|
|
261
|
-
|
|
262
|
-
yield*
|
|
512
|
+
Effect3.gen(function* () {
|
|
513
|
+
yield* Effect3.sleep(0);
|
|
263
514
|
const initial = yield* query();
|
|
264
515
|
return Stream.concat(Stream.make(initial), changes);
|
|
265
516
|
})
|
|
266
517
|
);
|
|
267
518
|
}
|
|
268
519
|
function notifyChange(ctx, event) {
|
|
269
|
-
return PubSub.publish(ctx.pubsub, event).pipe(
|
|
520
|
+
return PubSub.publish(ctx.pubsub, event).pipe(Effect3.asVoid);
|
|
270
521
|
}
|
|
271
522
|
function notifyReplayComplete(ctx, collections) {
|
|
272
|
-
return
|
|
523
|
+
return Effect3.gen(function* () {
|
|
273
524
|
yield* Ref.set(ctx.replayingRef, false);
|
|
274
525
|
for (const collection2 of collections) {
|
|
275
526
|
yield* notifyChange(ctx, {
|
|
@@ -282,7 +533,7 @@ function notifyReplayComplete(ctx, collections) {
|
|
|
282
533
|
}
|
|
283
534
|
|
|
284
535
|
// src/storage/records-store.ts
|
|
285
|
-
import { Effect as
|
|
536
|
+
import { Effect as Effect4 } from "effect";
|
|
286
537
|
|
|
287
538
|
// src/storage/lww.ts
|
|
288
539
|
function resolveWinner(existing, incoming) {
|
|
@@ -296,30 +547,6 @@ function resolveWinner(existing, incoming) {
|
|
|
296
547
|
function isPlainObject(value) {
|
|
297
548
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
298
549
|
}
|
|
299
|
-
function deepDiff(before, after) {
|
|
300
|
-
const result = {};
|
|
301
|
-
let hasChanges = false;
|
|
302
|
-
for (const key of Object.keys(after)) {
|
|
303
|
-
const a = before[key];
|
|
304
|
-
const b = after[key];
|
|
305
|
-
if (isPlainObject(a) && isPlainObject(b)) {
|
|
306
|
-
const nested = deepDiff(a, b);
|
|
307
|
-
if (nested !== null) {
|
|
308
|
-
result[key] = nested;
|
|
309
|
-
hasChanges = true;
|
|
310
|
-
}
|
|
311
|
-
} else if (Array.isArray(a) && Array.isArray(b)) {
|
|
312
|
-
if (JSON.stringify(a) !== JSON.stringify(b)) {
|
|
313
|
-
result[key] = b;
|
|
314
|
-
hasChanges = true;
|
|
315
|
-
}
|
|
316
|
-
} else if (a !== b) {
|
|
317
|
-
result[key] = b;
|
|
318
|
-
hasChanges = true;
|
|
319
|
-
}
|
|
320
|
-
}
|
|
321
|
-
return hasChanges ? result : null;
|
|
322
|
-
}
|
|
323
550
|
function deepMerge(target, source) {
|
|
324
551
|
const result = { ...target };
|
|
325
552
|
for (const key of Object.keys(source)) {
|
|
@@ -346,7 +573,7 @@ function buildRecord(event) {
|
|
|
346
573
|
};
|
|
347
574
|
}
|
|
348
575
|
function applyEvent(storage, event) {
|
|
349
|
-
return
|
|
576
|
+
return Effect4.gen(function* () {
|
|
350
577
|
const existing = yield* storage.getRecord(event.collection, event.recordId);
|
|
351
578
|
if (existing) {
|
|
352
579
|
const existingMeta = {
|
|
@@ -366,7 +593,7 @@ function applyEvent(storage, event) {
|
|
|
366
593
|
});
|
|
367
594
|
}
|
|
368
595
|
function rebuild(storage, collections) {
|
|
369
|
-
return
|
|
596
|
+
return Effect4.gen(function* () {
|
|
370
597
|
for (const col of collections) {
|
|
371
598
|
yield* storage.clearRecords(col);
|
|
372
599
|
}
|
|
@@ -382,7 +609,7 @@ function rebuild(storage, collections) {
|
|
|
382
609
|
}
|
|
383
610
|
|
|
384
611
|
// src/schema/validate.ts
|
|
385
|
-
import { Effect as
|
|
612
|
+
import { Effect as Effect5, Schema as Schema3 } from "effect";
|
|
386
613
|
function fieldDefToSchema(fd) {
|
|
387
614
|
let base;
|
|
388
615
|
switch (fd.kind) {
|
|
@@ -430,10 +657,10 @@ function buildStructSchema(def, options = {}) {
|
|
|
430
657
|
function buildValidator(collectionName, def) {
|
|
431
658
|
const decode = Schema3.decodeUnknownEffect(buildStructSchema(def, { includeId: true }));
|
|
432
659
|
return (input) => decode(input).pipe(
|
|
433
|
-
|
|
660
|
+
Effect5.map(
|
|
434
661
|
(result) => result
|
|
435
662
|
),
|
|
436
|
-
|
|
663
|
+
Effect5.mapError(
|
|
437
664
|
(e) => new ValidationError({
|
|
438
665
|
message: `Validation failed for collection "${collectionName}": ${e.message}`
|
|
439
666
|
})
|
|
@@ -442,7 +669,7 @@ function buildValidator(collectionName, def) {
|
|
|
442
669
|
}
|
|
443
670
|
function buildPartialValidator(collectionName, def) {
|
|
444
671
|
const decode = Schema3.decodeUnknownEffect(buildStructSchema(def, { allOptional: true }));
|
|
445
|
-
return (input) =>
|
|
672
|
+
return (input) => Effect5.gen(function* () {
|
|
446
673
|
if (typeof input !== "object" || input === null) {
|
|
447
674
|
return yield* new ValidationError({
|
|
448
675
|
message: `Validation failed for collection "${collectionName}": expected an object`
|
|
@@ -457,8 +684,8 @@ function buildPartialValidator(collectionName, def) {
|
|
|
457
684
|
});
|
|
458
685
|
}
|
|
459
686
|
return yield* decode(record).pipe(
|
|
460
|
-
|
|
461
|
-
|
|
687
|
+
Effect5.map((result) => result),
|
|
688
|
+
Effect5.mapError(
|
|
462
689
|
(e) => new ValidationError({
|
|
463
690
|
message: `Validation failed for collection "${collectionName}": ${e.message}`
|
|
464
691
|
})
|
|
@@ -468,7 +695,7 @@ function buildPartialValidator(collectionName, def) {
|
|
|
468
695
|
}
|
|
469
696
|
|
|
470
697
|
// src/crud/collection-handle.ts
|
|
471
|
-
import { Effect as
|
|
698
|
+
import { Effect as Effect7, Option as Option3, References } from "effect";
|
|
472
699
|
|
|
473
700
|
// src/utils/uuid.ts
|
|
474
701
|
var alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
|
|
@@ -501,12 +728,12 @@ function uuidv7() {
|
|
|
501
728
|
}
|
|
502
729
|
|
|
503
730
|
// src/crud/query-builder.ts
|
|
504
|
-
import { Effect as
|
|
731
|
+
import { Effect as Effect6, Option as Option2, Ref as Ref2, Stream as Stream2 } from "effect";
|
|
505
732
|
function emptyPlan() {
|
|
506
733
|
return { filters: [] };
|
|
507
734
|
}
|
|
508
735
|
function executeQuery(ctx, plan) {
|
|
509
|
-
return
|
|
736
|
+
return Effect6.gen(function* () {
|
|
510
737
|
if (plan.fieldName) {
|
|
511
738
|
const fieldDef = ctx.def.fields[plan.fieldName];
|
|
512
739
|
if (!fieldDef) {
|
|
@@ -582,7 +809,7 @@ function watchQuery(ctx, plan) {
|
|
|
582
809
|
const changes = Stream2.fromPubSub(ctx.watchCtx.pubsub).pipe(
|
|
583
810
|
Stream2.filter((event) => event.collection === ctx.collectionName),
|
|
584
811
|
Stream2.mapEffect(
|
|
585
|
-
() =>
|
|
812
|
+
() => Effect6.gen(function* () {
|
|
586
813
|
const replaying = yield* Ref2.get(ctx.watchCtx.replayingRef);
|
|
587
814
|
if (replaying) return void 0;
|
|
588
815
|
return yield* query();
|
|
@@ -591,7 +818,7 @@ function watchQuery(ctx, plan) {
|
|
|
591
818
|
Stream2.filter((result) => result !== void 0)
|
|
592
819
|
);
|
|
593
820
|
return Stream2.unwrap(
|
|
594
|
-
|
|
821
|
+
Effect6.gen(function* () {
|
|
595
822
|
const initial = yield* query();
|
|
596
823
|
return Stream2.concat(Stream2.make(initial), changes);
|
|
597
824
|
})
|
|
@@ -617,11 +844,11 @@ function makeQueryBuilder(ctx, plan) {
|
|
|
617
844
|
offset: (n) => makeQueryBuilder(ctx, { ...plan, offset: n }),
|
|
618
845
|
limit: (n) => makeQueryBuilder(ctx, { ...plan, limit: n }),
|
|
619
846
|
get: () => executeQuery(ctx, plan),
|
|
620
|
-
first: () =>
|
|
847
|
+
first: () => Effect6.map(
|
|
621
848
|
executeQuery(ctx, { ...plan, limit: 1 }),
|
|
622
849
|
(results) => results.length > 0 ? Option2.some(results[0]) : Option2.none()
|
|
623
850
|
),
|
|
624
|
-
count: () =>
|
|
851
|
+
count: () => Effect6.map(executeQuery(ctx, plan), (results) => results.length),
|
|
625
852
|
watch: () => watchQuery(ctx, plan)
|
|
626
853
|
};
|
|
627
854
|
}
|
|
@@ -727,7 +954,7 @@ function replayState(recordId, events, stopAtId) {
|
|
|
727
954
|
return state;
|
|
728
955
|
}
|
|
729
956
|
function promoteToSnapshot(storage, collection2, recordId, target, allSorted) {
|
|
730
|
-
return
|
|
957
|
+
return Effect7.gen(function* () {
|
|
731
958
|
const chronological = sortChronologically(allSorted);
|
|
732
959
|
const state = replayState(recordId, chronological, target.id);
|
|
733
960
|
if (state) {
|
|
@@ -736,7 +963,7 @@ function promoteToSnapshot(storage, collection2, recordId, target, allSorted) {
|
|
|
736
963
|
});
|
|
737
964
|
}
|
|
738
965
|
function pruneEvents(storage, collection2, recordId, retention) {
|
|
739
|
-
return
|
|
966
|
+
return Effect7.gen(function* () {
|
|
740
967
|
const events = yield* storage.getEventsByRecord(collection2, recordId);
|
|
741
968
|
if (events.length <= retention) return;
|
|
742
969
|
const sorted = [...events].sort((a, b) => b.createdAt - a.createdAt || (a.id < b.id ? 1 : -1));
|
|
@@ -757,8 +984,8 @@ function mapRecord(record) {
|
|
|
757
984
|
}
|
|
758
985
|
function createCollectionHandle(def, storage, watchCtx, validator, partialValidator, makeEventId, localAuthor, onWrite, logLevel = "None") {
|
|
759
986
|
const collectionName = def.name;
|
|
760
|
-
const withLog = (effect) =>
|
|
761
|
-
const commitEvent = (event) =>
|
|
987
|
+
const withLog = (effect) => Effect7.provideService(effect, References.MinimumLogLevel, logLevel);
|
|
988
|
+
const commitEvent = (event) => Effect7.gen(function* () {
|
|
762
989
|
yield* storage.putEvent(event);
|
|
763
990
|
yield* applyEvent(storage, event);
|
|
764
991
|
if (onWrite) yield* onWrite(event);
|
|
@@ -770,7 +997,7 @@ function createCollectionHandle(def, storage, watchCtx, validator, partialValida
|
|
|
770
997
|
});
|
|
771
998
|
const handle = {
|
|
772
999
|
add: (data) => withLog(
|
|
773
|
-
|
|
1000
|
+
Effect7.gen(function* () {
|
|
774
1001
|
const id = uuidv7();
|
|
775
1002
|
const fullRecord = { id, ...data };
|
|
776
1003
|
yield* validator(fullRecord);
|
|
@@ -784,7 +1011,7 @@ function createCollectionHandle(def, storage, watchCtx, validator, partialValida
|
|
|
784
1011
|
author: localAuthor
|
|
785
1012
|
};
|
|
786
1013
|
yield* commitEvent(event);
|
|
787
|
-
yield*
|
|
1014
|
+
yield* Effect7.logDebug("Record added", {
|
|
788
1015
|
collection: collectionName,
|
|
789
1016
|
recordId: id,
|
|
790
1017
|
data: fullRecord
|
|
@@ -793,7 +1020,7 @@ function createCollectionHandle(def, storage, watchCtx, validator, partialValida
|
|
|
793
1020
|
})
|
|
794
1021
|
),
|
|
795
1022
|
update: (id, data) => withLog(
|
|
796
|
-
|
|
1023
|
+
Effect7.gen(function* () {
|
|
797
1024
|
const existing = yield* storage.getRecord(collectionName, id);
|
|
798
1025
|
if (!existing || existing._d) {
|
|
799
1026
|
return yield* new NotFoundError({
|
|
@@ -805,27 +1032,26 @@ function createCollectionHandle(def, storage, watchCtx, validator, partialValida
|
|
|
805
1032
|
const { _d, _u, _a, _e, ...existingFields } = existing;
|
|
806
1033
|
const merged = { ...existingFields, ...data, id };
|
|
807
1034
|
yield* validator(merged);
|
|
808
|
-
const diff = deepDiff(existingFields, merged);
|
|
809
1035
|
const event = {
|
|
810
1036
|
id: makeEventId(),
|
|
811
1037
|
collection: collectionName,
|
|
812
1038
|
recordId: id,
|
|
813
1039
|
kind: "u",
|
|
814
|
-
data:
|
|
1040
|
+
data: merged,
|
|
815
1041
|
createdAt: Date.now(),
|
|
816
1042
|
author: localAuthor
|
|
817
1043
|
};
|
|
818
1044
|
yield* commitEvent(event);
|
|
819
|
-
yield*
|
|
1045
|
+
yield* Effect7.logDebug("Record updated", {
|
|
820
1046
|
collection: collectionName,
|
|
821
1047
|
recordId: id,
|
|
822
|
-
data:
|
|
1048
|
+
data: merged
|
|
823
1049
|
});
|
|
824
1050
|
yield* pruneEvents(storage, collectionName, id, def.eventRetention);
|
|
825
1051
|
})
|
|
826
1052
|
),
|
|
827
1053
|
delete: (id) => withLog(
|
|
828
|
-
|
|
1054
|
+
Effect7.gen(function* () {
|
|
829
1055
|
const existing = yield* storage.getRecord(collectionName, id);
|
|
830
1056
|
if (!existing || existing._d) {
|
|
831
1057
|
return yield* new NotFoundError({
|
|
@@ -843,11 +1069,11 @@ function createCollectionHandle(def, storage, watchCtx, validator, partialValida
|
|
|
843
1069
|
author: localAuthor
|
|
844
1070
|
};
|
|
845
1071
|
yield* commitEvent(event);
|
|
846
|
-
yield*
|
|
1072
|
+
yield* Effect7.logDebug("Record deleted", { collection: collectionName, recordId: id });
|
|
847
1073
|
yield* pruneEvents(storage, collectionName, id, def.eventRetention);
|
|
848
1074
|
})
|
|
849
1075
|
),
|
|
850
|
-
undo: (id) =>
|
|
1076
|
+
undo: (id) => Effect7.gen(function* () {
|
|
851
1077
|
const existing = yield* storage.getRecord(collectionName, id);
|
|
852
1078
|
if (!existing) {
|
|
853
1079
|
return yield* new NotFoundError({ collection: collectionName, id });
|
|
@@ -872,7 +1098,7 @@ function createCollectionHandle(def, storage, watchCtx, validator, partialValida
|
|
|
872
1098
|
yield* commitEvent(event);
|
|
873
1099
|
yield* pruneEvents(storage, collectionName, id, def.eventRetention);
|
|
874
1100
|
}),
|
|
875
|
-
get: (id) =>
|
|
1101
|
+
get: (id) => Effect7.gen(function* () {
|
|
876
1102
|
const record = yield* storage.getRecord(collectionName, id);
|
|
877
1103
|
if (!record || record._d) {
|
|
878
1104
|
return yield* new NotFoundError({
|
|
@@ -882,11 +1108,11 @@ function createCollectionHandle(def, storage, watchCtx, validator, partialValida
|
|
|
882
1108
|
}
|
|
883
1109
|
return mapRecord(record);
|
|
884
1110
|
}),
|
|
885
|
-
first: () =>
|
|
1111
|
+
first: () => Effect7.map(storage.getAllRecords(collectionName), (all) => {
|
|
886
1112
|
const found = all.find((r) => !r._d);
|
|
887
1113
|
return found ? Option3.some(mapRecord(found)) : Option3.none();
|
|
888
1114
|
}),
|
|
889
|
-
count: () =>
|
|
1115
|
+
count: () => Effect7.map(storage.getAllRecords(collectionName), (all) => all.filter((r) => !r._d).length),
|
|
890
1116
|
watch: () => watchCollection(watchCtx, storage, collectionName, void 0, mapRecord),
|
|
891
1117
|
where: (fieldName) => createWhereClause(
|
|
892
1118
|
storage,
|
|
@@ -909,12 +1135,12 @@ function createCollectionHandle(def, storage, watchCtx, validator, partialValida
|
|
|
909
1135
|
}
|
|
910
1136
|
|
|
911
1137
|
// src/sync/sync-service.ts
|
|
912
|
-
import { Duration, Effect as
|
|
1138
|
+
import { Duration, Effect as Effect9, Layer, Option as Option5, References as References2, Ref as Ref3, Schedule } from "effect";
|
|
913
1139
|
import { unwrapEvent } from "nostr-tools/nip59";
|
|
914
1140
|
import { GiftWrap as GiftWrap2 } from "nostr-tools/kinds";
|
|
915
1141
|
|
|
916
1142
|
// src/sync/negentropy.ts
|
|
917
|
-
import { Effect as
|
|
1143
|
+
import { Effect as Effect8 } from "effect";
|
|
918
1144
|
|
|
919
1145
|
// src/vendor/negentropy.js
|
|
920
1146
|
var PROTOCOL_VERSION = 97;
|
|
@@ -1401,14 +1627,14 @@ function itemCompare(a, b) {
|
|
|
1401
1627
|
}
|
|
1402
1628
|
|
|
1403
1629
|
// src/sync/negentropy.ts
|
|
1404
|
-
import { hexToBytes as
|
|
1630
|
+
import { hexToBytes as hexToBytes3 } from "@noble/hashes/utils.js";
|
|
1405
1631
|
import { GiftWrap } from "nostr-tools/kinds";
|
|
1406
1632
|
function reconcileWithRelay(storage, relay, relayUrl, publicKeys) {
|
|
1407
|
-
return
|
|
1633
|
+
return Effect8.gen(function* () {
|
|
1408
1634
|
const allGiftWraps = yield* storage.getAllGiftWraps();
|
|
1409
1635
|
const storageVector = new NegentropyStorageVector();
|
|
1410
1636
|
for (const gw of allGiftWraps) {
|
|
1411
|
-
storageVector.insert(gw.createdAt,
|
|
1637
|
+
storageVector.insert(gw.createdAt, hexToBytes3(gw.id));
|
|
1412
1638
|
}
|
|
1413
1639
|
storageVector.seal();
|
|
1414
1640
|
const neg = new Negentropy(storageVector, 0);
|
|
@@ -1419,7 +1645,7 @@ function reconcileWithRelay(storage, relay, relayUrl, publicKeys) {
|
|
|
1419
1645
|
const allHaveIds = [];
|
|
1420
1646
|
const allNeedIds = [];
|
|
1421
1647
|
const subId = `neg-${Date.now()}`;
|
|
1422
|
-
const initialMsg = yield*
|
|
1648
|
+
const initialMsg = yield* Effect8.tryPromise({
|
|
1423
1649
|
try: () => neg.initiate(),
|
|
1424
1650
|
catch: (e) => new SyncError({
|
|
1425
1651
|
message: `Negentropy initiate failed: ${e instanceof Error ? e.message : String(e)}`,
|
|
@@ -1431,7 +1657,7 @@ function reconcileWithRelay(storage, relay, relayUrl, publicKeys) {
|
|
|
1431
1657
|
while (currentMsg !== null) {
|
|
1432
1658
|
const response = yield* relay.sendNegMsg(relayUrl, subId, filter, currentMsg);
|
|
1433
1659
|
if (response.msgHex === null) break;
|
|
1434
|
-
const reconcileResult = yield*
|
|
1660
|
+
const reconcileResult = yield* Effect8.tryPromise({
|
|
1435
1661
|
try: () => neg.reconcile(response.msgHex),
|
|
1436
1662
|
catch: (e) => new SyncError({
|
|
1437
1663
|
message: `Negentropy reconcile failed: ${e instanceof Error ? e.message : String(e)}`,
|
|
@@ -1444,13 +1670,13 @@ function reconcileWithRelay(storage, relay, relayUrl, publicKeys) {
|
|
|
1444
1670
|
for (const id of needIds) allNeedIds.push(id);
|
|
1445
1671
|
currentMsg = nextMsg;
|
|
1446
1672
|
}
|
|
1447
|
-
yield*
|
|
1673
|
+
yield* Effect8.logDebug("Negentropy reconciliation complete", {
|
|
1448
1674
|
relay: relayUrl,
|
|
1449
1675
|
have: allHaveIds.length,
|
|
1450
1676
|
need: allNeedIds.length
|
|
1451
1677
|
});
|
|
1452
1678
|
return { haveIds: allHaveIds, needIds: allNeedIds };
|
|
1453
|
-
}).pipe(
|
|
1679
|
+
}).pipe(Effect8.withLogSpan("tablinum.negentropy"));
|
|
1454
1680
|
}
|
|
1455
1681
|
|
|
1456
1682
|
// src/db/key-rotation.ts
|
|
@@ -1544,52 +1770,52 @@ function createSyncHandle(storage, giftWrapHandle, relay, publishQueue, syncStat
|
|
|
1544
1770
|
kind: "create"
|
|
1545
1771
|
});
|
|
1546
1772
|
const forkHandled = (effect) => {
|
|
1547
|
-
|
|
1773
|
+
Effect9.runFork(
|
|
1548
1774
|
effect.pipe(
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1775
|
+
Effect9.tapError((e) => Effect9.sync(() => onSyncError?.(e))),
|
|
1776
|
+
Effect9.ignore,
|
|
1777
|
+
Effect9.provide(logLayer),
|
|
1778
|
+
Effect9.forkIn(scope)
|
|
1553
1779
|
)
|
|
1554
1780
|
);
|
|
1555
1781
|
};
|
|
1556
1782
|
let autoFlushActive = false;
|
|
1557
|
-
const autoFlushEffect =
|
|
1783
|
+
const autoFlushEffect = Effect9.gen(function* () {
|
|
1558
1784
|
const size = yield* publishQueue.size();
|
|
1559
1785
|
if (size === 0) return;
|
|
1560
1786
|
yield* syncStatus.set("syncing");
|
|
1561
1787
|
yield* publishQueue.flush(relayUrls);
|
|
1562
1788
|
const remaining = yield* publishQueue.size();
|
|
1563
|
-
if (remaining > 0) yield*
|
|
1789
|
+
if (remaining > 0) yield* Effect9.fail("pending");
|
|
1564
1790
|
}).pipe(
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1791
|
+
Effect9.ensuring(syncStatus.set("idle")),
|
|
1792
|
+
Effect9.retry({ schedule: Schedule.exponential(5e3).pipe(Schedule.jittered), times: 10 }),
|
|
1793
|
+
Effect9.ignore
|
|
1568
1794
|
);
|
|
1569
1795
|
const scheduleAutoFlush = () => {
|
|
1570
1796
|
if (autoFlushActive) return;
|
|
1571
1797
|
autoFlushActive = true;
|
|
1572
1798
|
forkHandled(
|
|
1573
1799
|
autoFlushEffect.pipe(
|
|
1574
|
-
|
|
1575
|
-
|
|
1800
|
+
Effect9.ensuring(
|
|
1801
|
+
Effect9.sync(() => {
|
|
1576
1802
|
autoFlushActive = false;
|
|
1577
1803
|
})
|
|
1578
1804
|
)
|
|
1579
1805
|
)
|
|
1580
1806
|
);
|
|
1581
1807
|
};
|
|
1582
|
-
const shouldRejectWrite = (authorPubkey) =>
|
|
1808
|
+
const shouldRejectWrite = (authorPubkey) => Effect9.gen(function* () {
|
|
1583
1809
|
const memberRecord = yield* storage.getRecord("_members", authorPubkey);
|
|
1584
1810
|
if (!memberRecord) return false;
|
|
1585
1811
|
return !!memberRecord.removedAt;
|
|
1586
1812
|
});
|
|
1587
1813
|
const storeGiftWrapShell = (gw) => storage.putGiftWrap({ id: gw.id, event: gw, createdAt: gw.created_at });
|
|
1588
|
-
const unwrapGiftWrap = (remoteGw) =>
|
|
1814
|
+
const unwrapGiftWrap = (remoteGw) => Effect9.gen(function* () {
|
|
1589
1815
|
const existing = yield* storage.getGiftWrap(remoteGw.id);
|
|
1590
1816
|
if (existing) return null;
|
|
1591
1817
|
const rumor = yield* giftWrapHandle.unwrap(remoteGw).pipe(
|
|
1592
|
-
|
|
1818
|
+
Effect9.orElseSucceed(() => null)
|
|
1593
1819
|
);
|
|
1594
1820
|
if (!rumor) {
|
|
1595
1821
|
yield* storeGiftWrapShell(remoteGw);
|
|
@@ -1617,7 +1843,7 @@ function createSyncHandle(storage, giftWrapHandle, relay, publishQueue, syncStat
|
|
|
1617
1843
|
recordId: dTag.substring(colonIdx + 1)
|
|
1618
1844
|
};
|
|
1619
1845
|
});
|
|
1620
|
-
const applyUnwrappedEvent = (uw) =>
|
|
1846
|
+
const applyUnwrappedEvent = (uw) => Effect9.gen(function* () {
|
|
1621
1847
|
const { giftWrap: remoteGw, rumor, collection: collectionName, recordId } = uw;
|
|
1622
1848
|
const retention = knownCollections.get(collectionName);
|
|
1623
1849
|
if (retention === void 0) {
|
|
@@ -1627,17 +1853,17 @@ function createSyncHandle(storage, giftWrapHandle, relay, publishQueue, syncStat
|
|
|
1627
1853
|
if (rumor.pubkey) {
|
|
1628
1854
|
const reject = yield* shouldRejectWrite(rumor.pubkey);
|
|
1629
1855
|
if (reject) {
|
|
1630
|
-
yield*
|
|
1856
|
+
yield* Effect9.logWarning("Rejected write from removed member", {
|
|
1631
1857
|
author: rumor.pubkey.slice(0, 12)
|
|
1632
1858
|
});
|
|
1633
1859
|
yield* storeGiftWrapShell(remoteGw);
|
|
1634
1860
|
return null;
|
|
1635
1861
|
}
|
|
1636
1862
|
}
|
|
1637
|
-
const parsed = yield*
|
|
1863
|
+
const parsed = yield* Effect9.try({
|
|
1638
1864
|
try: () => JSON.parse(rumor.content),
|
|
1639
1865
|
catch: () => void 0
|
|
1640
|
-
}).pipe(
|
|
1866
|
+
}).pipe(Effect9.orElseSucceed(() => void 0));
|
|
1641
1867
|
if (parsed === void 0) {
|
|
1642
1868
|
yield* storeGiftWrapShell(remoteGw);
|
|
1643
1869
|
return null;
|
|
@@ -1669,7 +1895,7 @@ function createSyncHandle(storage, giftWrapHandle, relay, publishQueue, syncStat
|
|
|
1669
1895
|
if (didApply && (kind === "u" || kind === "d")) {
|
|
1670
1896
|
yield* pruneEvents(storage, collectionName, recordId, retention);
|
|
1671
1897
|
}
|
|
1672
|
-
yield*
|
|
1898
|
+
yield* Effect9.logDebug("Processed gift wrap", {
|
|
1673
1899
|
collection: collectionName,
|
|
1674
1900
|
recordId,
|
|
1675
1901
|
kind,
|
|
@@ -1680,33 +1906,33 @@ function createSyncHandle(storage, giftWrapHandle, relay, publishQueue, syncStat
|
|
|
1680
1906
|
}
|
|
1681
1907
|
return collectionName;
|
|
1682
1908
|
});
|
|
1683
|
-
const reconcileRelay = (url, pubKeys) =>
|
|
1684
|
-
yield*
|
|
1909
|
+
const reconcileRelay = (url, pubKeys) => Effect9.gen(function* () {
|
|
1910
|
+
yield* Effect9.logDebug("Syncing relay", { relay: url });
|
|
1685
1911
|
const { haveIds, needIds } = yield* reconcileWithRelay(
|
|
1686
1912
|
storage,
|
|
1687
1913
|
relay,
|
|
1688
1914
|
url,
|
|
1689
1915
|
Array.from(pubKeys)
|
|
1690
1916
|
).pipe(
|
|
1691
|
-
|
|
1692
|
-
|
|
1917
|
+
Effect9.tapError((err) => Effect9.sync(() => onSyncError?.(err))),
|
|
1918
|
+
Effect9.orElseSucceed(() => ({ haveIds: [], needIds: [] }))
|
|
1693
1919
|
);
|
|
1694
|
-
yield*
|
|
1920
|
+
yield* Effect9.logDebug("Relay reconciliation result", {
|
|
1695
1921
|
relay: url,
|
|
1696
1922
|
need: needIds.length,
|
|
1697
1923
|
have: haveIds.length
|
|
1698
1924
|
});
|
|
1699
1925
|
const events = needIds.length > 0 ? yield* relay.fetchEvents(needIds, url).pipe(
|
|
1700
|
-
|
|
1701
|
-
|
|
1926
|
+
Effect9.tapError((err) => Effect9.sync(() => onSyncError?.(err))),
|
|
1927
|
+
Effect9.orElseSucceed(() => [])
|
|
1702
1928
|
) : [];
|
|
1703
1929
|
return {
|
|
1704
1930
|
events,
|
|
1705
1931
|
haveIds: haveIds.map((id) => ({ id, url }))
|
|
1706
1932
|
};
|
|
1707
|
-
}).pipe(
|
|
1708
|
-
const syncAllRelays = (pubKeys, changedCollections) =>
|
|
1709
|
-
const results = yield*
|
|
1933
|
+
}).pipe(Effect9.withLogSpan("tablinum.reconcileRelay"));
|
|
1934
|
+
const syncAllRelays = (pubKeys, changedCollections) => Effect9.gen(function* () {
|
|
1935
|
+
const results = yield* Effect9.forEach(
|
|
1710
1936
|
relayUrls,
|
|
1711
1937
|
(url) => reconcileRelay(url, pubKeys),
|
|
1712
1938
|
{ concurrency: "unbounded" }
|
|
@@ -1723,44 +1949,44 @@ function createSyncHandle(storage, giftWrapHandle, relay, publishQueue, syncStat
|
|
|
1723
1949
|
}
|
|
1724
1950
|
const unwrapped = [];
|
|
1725
1951
|
for (const gw of allGiftWraps) {
|
|
1726
|
-
const result = yield* unwrapGiftWrap(gw).pipe(
|
|
1952
|
+
const result = yield* unwrapGiftWrap(gw).pipe(Effect9.orElseSucceed(() => null));
|
|
1727
1953
|
if (result) unwrapped.push(result);
|
|
1728
1954
|
}
|
|
1729
1955
|
unwrapped.sort((a, b) => a.rumor.created_at - b.rumor.created_at || (a.rumor.id < b.rumor.id ? -1 : 1));
|
|
1730
1956
|
for (const event of unwrapped) {
|
|
1731
1957
|
const collection2 = yield* applyUnwrappedEvent(event).pipe(
|
|
1732
|
-
|
|
1958
|
+
Effect9.orElseSucceed(() => null)
|
|
1733
1959
|
);
|
|
1734
1960
|
if (collection2) changedCollections.add(collection2);
|
|
1735
1961
|
}
|
|
1736
1962
|
const allHaveIds = results.flatMap((r) => r.haveIds);
|
|
1737
|
-
yield*
|
|
1963
|
+
yield* Effect9.forEach(
|
|
1738
1964
|
allHaveIds,
|
|
1739
|
-
({ id, url }) =>
|
|
1965
|
+
({ id, url }) => Effect9.gen(function* () {
|
|
1740
1966
|
const gw = yield* storage.getGiftWrap(id);
|
|
1741
1967
|
if (!gw?.event) return;
|
|
1742
1968
|
yield* relay.publish(gw.event, [url]).pipe(
|
|
1743
|
-
|
|
1744
|
-
|
|
1969
|
+
Effect9.tapError((err) => Effect9.sync(() => onSyncError?.(err))),
|
|
1970
|
+
Effect9.ignore
|
|
1745
1971
|
);
|
|
1746
1972
|
}),
|
|
1747
1973
|
{ concurrency: "unbounded", discard: true }
|
|
1748
1974
|
);
|
|
1749
|
-
}).pipe(
|
|
1750
|
-
const processGiftWrap = (remoteGw) =>
|
|
1975
|
+
}).pipe(Effect9.withLogSpan("tablinum.syncAllRelays"));
|
|
1976
|
+
const processGiftWrap = (remoteGw) => Effect9.gen(function* () {
|
|
1751
1977
|
const uw = yield* unwrapGiftWrap(remoteGw);
|
|
1752
1978
|
if (!uw) return null;
|
|
1753
1979
|
return yield* applyUnwrappedEvent(uw);
|
|
1754
1980
|
});
|
|
1755
|
-
const processRealtimeGiftWrap = (remoteGw) =>
|
|
1756
|
-
const collection2 = yield* processGiftWrap(remoteGw).pipe(
|
|
1981
|
+
const processRealtimeGiftWrap = (remoteGw) => Effect9.gen(function* () {
|
|
1982
|
+
const collection2 = yield* processGiftWrap(remoteGw).pipe(Effect9.orElseSucceed(() => null));
|
|
1757
1983
|
if (collection2) {
|
|
1758
1984
|
yield* notifyCollectionUpdated(collection2);
|
|
1759
1985
|
}
|
|
1760
1986
|
});
|
|
1761
|
-
const processRotationGiftWrap = (remoteGw) =>
|
|
1762
|
-
const unwrapResult = yield*
|
|
1763
|
-
|
|
1987
|
+
const processRotationGiftWrap = (remoteGw) => Effect9.gen(function* () {
|
|
1988
|
+
const unwrapResult = yield* Effect9.result(
|
|
1989
|
+
Effect9.try({
|
|
1764
1990
|
try: () => unwrapEvent(remoteGw, personalPrivateKey),
|
|
1765
1991
|
catch: (e) => new CryptoError({
|
|
1766
1992
|
message: `Rotation unwrap failed: ${e instanceof Error ? e.message : String(e)}`,
|
|
@@ -1810,73 +2036,73 @@ function createSyncHandle(storage, giftWrapHandle, relay, publishQueue, syncStat
|
|
|
1810
2036
|
yield* handle.addEpochSubscription(epoch.publicKey);
|
|
1811
2037
|
return true;
|
|
1812
2038
|
});
|
|
1813
|
-
const subscribeAcrossRelays = (filter, onEvent) =>
|
|
2039
|
+
const subscribeAcrossRelays = (filter, onEvent) => Effect9.forEach(
|
|
1814
2040
|
relayUrls,
|
|
1815
|
-
(url) =>
|
|
2041
|
+
(url) => Effect9.gen(function* () {
|
|
1816
2042
|
yield* relay.subscribe(filter, url, (event) => {
|
|
1817
2043
|
forkHandled(onEvent(event));
|
|
1818
2044
|
}).pipe(
|
|
1819
|
-
|
|
1820
|
-
|
|
2045
|
+
Effect9.tapError((err) => Effect9.sync(() => onSyncError?.(err))),
|
|
2046
|
+
Effect9.ignore
|
|
1821
2047
|
);
|
|
1822
2048
|
}),
|
|
1823
2049
|
{ concurrency: "unbounded", discard: true }
|
|
1824
2050
|
);
|
|
1825
2051
|
let healingActive = false;
|
|
1826
|
-
const healingEffect =
|
|
2052
|
+
const healingEffect = Effect9.gen(function* () {
|
|
1827
2053
|
if (!healingActive) return;
|
|
1828
2054
|
const status = yield* syncStatus.get();
|
|
1829
2055
|
if (status === "syncing") return;
|
|
1830
2056
|
yield* syncStatus.set("syncing");
|
|
1831
|
-
yield*
|
|
2057
|
+
yield* Effect9.gen(function* () {
|
|
1832
2058
|
const pubKeys = getSubscriptionPubKeys();
|
|
1833
2059
|
const changedCollections = /* @__PURE__ */ new Set();
|
|
1834
2060
|
yield* syncAllRelays(pubKeys, changedCollections);
|
|
1835
2061
|
if (changedCollections.size > 0) {
|
|
1836
2062
|
yield* notifyReplayComplete(watchCtx, [...changedCollections]);
|
|
1837
2063
|
}
|
|
1838
|
-
}).pipe(
|
|
1839
|
-
}).pipe(
|
|
2064
|
+
}).pipe(Effect9.ensuring(syncStatus.set("idle")));
|
|
2065
|
+
}).pipe(Effect9.ignore);
|
|
1840
2066
|
const handle = {
|
|
1841
|
-
sync: () =>
|
|
1842
|
-
yield*
|
|
2067
|
+
sync: () => Effect9.gen(function* () {
|
|
2068
|
+
yield* Effect9.logInfo("Sync started");
|
|
1843
2069
|
yield* syncStatus.set("syncing");
|
|
1844
2070
|
yield* Ref3.set(watchCtx.replayingRef, true);
|
|
1845
2071
|
const changedCollections = /* @__PURE__ */ new Set();
|
|
1846
|
-
yield*
|
|
2072
|
+
yield* Effect9.gen(function* () {
|
|
1847
2073
|
const pubKeys = getSubscriptionPubKeys();
|
|
1848
2074
|
yield* syncAllRelays(pubKeys, changedCollections);
|
|
1849
|
-
yield* publishQueue.flush(relayUrls).pipe(
|
|
2075
|
+
yield* publishQueue.flush(relayUrls).pipe(Effect9.ignore);
|
|
1850
2076
|
}).pipe(
|
|
1851
|
-
|
|
1852
|
-
|
|
2077
|
+
Effect9.ensuring(
|
|
2078
|
+
Effect9.gen(function* () {
|
|
1853
2079
|
yield* notifyReplayComplete(watchCtx, [...changedCollections]);
|
|
1854
2080
|
yield* syncStatus.set("idle");
|
|
1855
2081
|
})
|
|
1856
2082
|
)
|
|
1857
2083
|
);
|
|
1858
|
-
yield*
|
|
1859
|
-
}).pipe(
|
|
1860
|
-
publishLocal: (giftWrap) =>
|
|
2084
|
+
yield* Effect9.logInfo("Sync complete", { changed: [...changedCollections] });
|
|
2085
|
+
}).pipe(Effect9.withLogSpan("tablinum.sync")),
|
|
2086
|
+
publishLocal: (giftWrap) => Effect9.gen(function* () {
|
|
1861
2087
|
if (!giftWrap.event) return;
|
|
1862
2088
|
yield* relay.publish(giftWrap.event, relayUrls).pipe(
|
|
1863
|
-
|
|
2089
|
+
Effect9.tapError(
|
|
1864
2090
|
() => storage.putGiftWrap(giftWrap).pipe(
|
|
1865
|
-
|
|
1866
|
-
|
|
2091
|
+
Effect9.andThen(publishQueue.enqueue(giftWrap.id)),
|
|
2092
|
+
Effect9.andThen(Effect9.sync(() => scheduleAutoFlush()))
|
|
1867
2093
|
)
|
|
1868
2094
|
),
|
|
1869
|
-
|
|
1870
|
-
|
|
2095
|
+
Effect9.tapError((err) => Effect9.sync(() => onSyncError?.(err))),
|
|
2096
|
+
Effect9.ignore
|
|
1871
2097
|
);
|
|
1872
2098
|
}),
|
|
1873
|
-
startSubscription: () =>
|
|
2099
|
+
startSubscription: () => Effect9.gen(function* () {
|
|
1874
2100
|
const pubKeys = getSubscriptionPubKeys();
|
|
1875
2101
|
yield* subscribeAcrossRelays({ kinds: [GiftWrap2], "#p": pubKeys }, processRealtimeGiftWrap);
|
|
1876
2102
|
if (!pubKeys.includes(personalPublicKey)) {
|
|
1877
2103
|
yield* subscribeAcrossRelays(
|
|
1878
2104
|
{ kinds: [GiftWrap2], "#p": [personalPublicKey] },
|
|
1879
|
-
(event) =>
|
|
2105
|
+
(event) => Effect9.result(processRotationGiftWrap(event)).pipe(Effect9.asVoid)
|
|
1880
2106
|
);
|
|
1881
2107
|
}
|
|
1882
2108
|
}),
|
|
@@ -1885,10 +2111,10 @@ function createSyncHandle(storage, giftWrapHandle, relay, publishQueue, syncStat
|
|
|
1885
2111
|
if (healingActive) return;
|
|
1886
2112
|
healingActive = true;
|
|
1887
2113
|
forkHandled(
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
2114
|
+
Effect9.sleep(Duration.minutes(5)).pipe(
|
|
2115
|
+
Effect9.andThen(healingEffect),
|
|
2116
|
+
Effect9.repeat(Schedule.spaced(Duration.minutes(5))),
|
|
2117
|
+
Effect9.ensuring(Effect9.sync(() => {
|
|
1892
2118
|
healingActive = false;
|
|
1893
2119
|
}))
|
|
1894
2120
|
)
|
|
@@ -1900,8 +2126,8 @@ function createSyncHandle(storage, giftWrapHandle, relay, publishQueue, syncStat
|
|
|
1900
2126
|
};
|
|
1901
2127
|
forkHandled(
|
|
1902
2128
|
publishQueue.size().pipe(
|
|
1903
|
-
|
|
1904
|
-
(size) =>
|
|
2129
|
+
Effect9.flatMap(
|
|
2130
|
+
(size) => Effect9.sync(() => {
|
|
1905
2131
|
if (size > 0) scheduleAutoFlush();
|
|
1906
2132
|
})
|
|
1907
2133
|
)
|
|
@@ -1911,7 +2137,7 @@ function createSyncHandle(storage, giftWrapHandle, relay, publishQueue, syncStat
|
|
|
1911
2137
|
}
|
|
1912
2138
|
|
|
1913
2139
|
// src/db/members.ts
|
|
1914
|
-
import { Effect as
|
|
2140
|
+
import { Effect as Effect10, Option as Option6, Schema as Schema5 } from "effect";
|
|
1915
2141
|
var optionalString = {
|
|
1916
2142
|
_tag: "FieldDef",
|
|
1917
2143
|
kind: "string",
|
|
@@ -1960,15 +2186,15 @@ var AuthorProfileSchema = Schema5.Struct({
|
|
|
1960
2186
|
});
|
|
1961
2187
|
var decodeAuthorProfile = Schema5.decodeUnknownEffect(Schema5.fromJsonString(AuthorProfileSchema));
|
|
1962
2188
|
function fetchAuthorProfile(relay, relayUrls, pubkey) {
|
|
1963
|
-
return
|
|
2189
|
+
return Effect10.gen(function* () {
|
|
1964
2190
|
for (const url of relayUrls) {
|
|
1965
|
-
const result = yield*
|
|
2191
|
+
const result = yield* Effect10.result(
|
|
1966
2192
|
relay.fetchByFilter({ kinds: [0], authors: [pubkey], limit: 1 }, url)
|
|
1967
2193
|
);
|
|
1968
2194
|
if (result._tag === "Success" && result.success.length > 0) {
|
|
1969
2195
|
return yield* decodeAuthorProfile(result.success[0].content).pipe(
|
|
1970
|
-
|
|
1971
|
-
|
|
2196
|
+
Effect10.map(Option6.some),
|
|
2197
|
+
Effect10.orElseSucceed(() => Option6.none())
|
|
1972
2198
|
);
|
|
1973
2199
|
}
|
|
1974
2200
|
}
|
|
@@ -1976,6 +2202,20 @@ function fetchAuthorProfile(relay, relayUrls, pubkey) {
|
|
|
1976
2202
|
});
|
|
1977
2203
|
}
|
|
1978
2204
|
|
|
2205
|
+
// src/sync/deletion.ts
|
|
2206
|
+
import { finalizeEvent } from "nostr-tools/pure";
|
|
2207
|
+
function createDeletionEvent(targetEventIds, signingKey) {
|
|
2208
|
+
return finalizeEvent(
|
|
2209
|
+
{
|
|
2210
|
+
kind: 5,
|
|
2211
|
+
content: "",
|
|
2212
|
+
tags: targetEventIds.map((id) => ["e", id]),
|
|
2213
|
+
created_at: Math.floor(Date.now() / 1e3)
|
|
2214
|
+
},
|
|
2215
|
+
signingKey
|
|
2216
|
+
);
|
|
2217
|
+
}
|
|
2218
|
+
|
|
1979
2219
|
// src/services/Identity.ts
|
|
1980
2220
|
import { ServiceMap as ServiceMap3 } from "effect";
|
|
1981
2221
|
var Identity = class extends ServiceMap3.Service()("tablinum/Identity") {
|
|
@@ -2018,15 +2258,15 @@ var SyncStatus = class extends ServiceMap9.Service()(
|
|
|
2018
2258
|
};
|
|
2019
2259
|
|
|
2020
2260
|
// src/layers/IdentityLive.ts
|
|
2021
|
-
import { Effect as
|
|
2022
|
-
import { hexToBytes as
|
|
2261
|
+
import { Effect as Effect12, Layer as Layer2 } from "effect";
|
|
2262
|
+
import { hexToBytes as hexToBytes4 } from "@noble/hashes/utils.js";
|
|
2023
2263
|
|
|
2024
2264
|
// src/db/identity.ts
|
|
2025
|
-
import { Effect as
|
|
2265
|
+
import { Effect as Effect11 } from "effect";
|
|
2026
2266
|
import { getPublicKey as getPublicKey2 } from "nostr-tools/pure";
|
|
2027
|
-
import { bytesToHex as
|
|
2267
|
+
import { bytesToHex as bytesToHex3 } from "@noble/hashes/utils.js";
|
|
2028
2268
|
function createIdentity(suppliedKey) {
|
|
2029
|
-
return
|
|
2269
|
+
return Effect11.gen(function* () {
|
|
2030
2270
|
let privateKey;
|
|
2031
2271
|
if (suppliedKey) {
|
|
2032
2272
|
if (suppliedKey.length !== 32) {
|
|
@@ -2039,8 +2279,8 @@ function createIdentity(suppliedKey) {
|
|
|
2039
2279
|
privateKey = new Uint8Array(32);
|
|
2040
2280
|
crypto.getRandomValues(privateKey);
|
|
2041
2281
|
}
|
|
2042
|
-
const privateKeyHex =
|
|
2043
|
-
const publicKey = yield*
|
|
2282
|
+
const privateKeyHex = bytesToHex3(privateKey);
|
|
2283
|
+
const publicKey = yield* Effect11.try({
|
|
2044
2284
|
try: () => getPublicKey2(privateKey),
|
|
2045
2285
|
catch: (e) => new CryptoError({
|
|
2046
2286
|
message: `Failed to derive public key: ${e instanceof Error ? e.message : String(e)}`,
|
|
@@ -2058,14 +2298,14 @@ function createIdentity(suppliedKey) {
|
|
|
2058
2298
|
// src/layers/IdentityLive.ts
|
|
2059
2299
|
var IdentityLive = Layer2.effect(
|
|
2060
2300
|
Identity,
|
|
2061
|
-
|
|
2301
|
+
Effect12.gen(function* () {
|
|
2062
2302
|
const config = yield* Config;
|
|
2063
2303
|
const storage = yield* Storage;
|
|
2064
2304
|
const idbKey = yield* storage.getMeta("identity_key");
|
|
2065
|
-
const resolvedKey = config.privateKey ?? (typeof idbKey === "string" && idbKey.length === 64 ?
|
|
2305
|
+
const resolvedKey = config.privateKey ?? (typeof idbKey === "string" && idbKey.length === 64 ? hexToBytes4(idbKey) : void 0);
|
|
2066
2306
|
const identity = yield* createIdentity(resolvedKey);
|
|
2067
2307
|
yield* storage.putMeta("identity_key", identity.exportKey());
|
|
2068
|
-
yield*
|
|
2308
|
+
yield* Effect12.logInfo("Identity loaded", {
|
|
2069
2309
|
publicKey: identity.publicKey.slice(0, 12) + "...",
|
|
2070
2310
|
source: config.privateKey ? "config" : resolvedKey ? "storage" : "generated"
|
|
2071
2311
|
});
|
|
@@ -2074,12 +2314,12 @@ var IdentityLive = Layer2.effect(
|
|
|
2074
2314
|
);
|
|
2075
2315
|
|
|
2076
2316
|
// src/layers/EpochStoreLive.ts
|
|
2077
|
-
import { Effect as
|
|
2317
|
+
import { Effect as Effect13, Layer as Layer3, Option as Option7 } from "effect";
|
|
2078
2318
|
import { generateSecretKey as generateSecretKey2 } from "nostr-tools/pure";
|
|
2079
|
-
import { bytesToHex as
|
|
2319
|
+
import { bytesToHex as bytesToHex4 } from "@noble/hashes/utils.js";
|
|
2080
2320
|
var EpochStoreLive = Layer3.effect(
|
|
2081
2321
|
EpochStore,
|
|
2082
|
-
|
|
2322
|
+
Effect13.gen(function* () {
|
|
2083
2323
|
const config = yield* Config;
|
|
2084
2324
|
const identity = yield* Identity;
|
|
2085
2325
|
const storage = yield* Storage;
|
|
@@ -2098,7 +2338,7 @@ var EpochStoreLive = Layer3.effect(
|
|
|
2098
2338
|
return existing !== void 0 && existing.privateKey === ek.key;
|
|
2099
2339
|
});
|
|
2100
2340
|
if (configIsSubset) {
|
|
2101
|
-
yield*
|
|
2341
|
+
yield* Effect13.logInfo("Epoch store loaded", {
|
|
2102
2342
|
source: "storage",
|
|
2103
2343
|
epochs: idbStore.epochs.size
|
|
2104
2344
|
});
|
|
@@ -2107,271 +2347,28 @@ var EpochStoreLive = Layer3.effect(
|
|
|
2107
2347
|
}
|
|
2108
2348
|
const store2 = createEpochStoreFromInputs(config.epochKeys);
|
|
2109
2349
|
yield* storage.putMeta("epochs", stringifyEpochStore(store2));
|
|
2110
|
-
yield*
|
|
2350
|
+
yield* Effect13.logInfo("Epoch store loaded", { source: "config", epochs: store2.epochs.size });
|
|
2111
2351
|
return store2;
|
|
2112
2352
|
}
|
|
2113
2353
|
if (idbStore) {
|
|
2114
|
-
yield*
|
|
2354
|
+
yield* Effect13.logInfo("Epoch store loaded", {
|
|
2115
2355
|
source: "storage",
|
|
2116
2356
|
epochs: idbStore.epochs.size
|
|
2117
2357
|
});
|
|
2118
2358
|
return idbStore;
|
|
2119
2359
|
}
|
|
2120
2360
|
const store = createEpochStoreFromInputs(
|
|
2121
|
-
[{ epochId: EpochId("epoch-0"), key:
|
|
2361
|
+
[{ epochId: EpochId("epoch-0"), key: bytesToHex4(generateSecretKey2()) }],
|
|
2122
2362
|
{ createdBy: identity.publicKey }
|
|
2123
2363
|
);
|
|
2124
2364
|
yield* storage.putMeta("epochs", stringifyEpochStore(store));
|
|
2125
|
-
yield*
|
|
2365
|
+
yield* Effect13.logInfo("Epoch store loaded", { source: "generated", epochs: store.epochs.size });
|
|
2126
2366
|
return store;
|
|
2127
2367
|
})
|
|
2128
2368
|
);
|
|
2129
2369
|
|
|
2130
2370
|
// src/layers/StorageLive.ts
|
|
2131
2371
|
import { Effect as Effect14, Layer as Layer4 } from "effect";
|
|
2132
|
-
|
|
2133
|
-
// src/storage/idb.ts
|
|
2134
|
-
import { Effect as Effect13 } from "effect";
|
|
2135
|
-
import { openDB } from "idb";
|
|
2136
|
-
|
|
2137
|
-
// src/sync/compact-event.ts
|
|
2138
|
-
import { bytesToHex as bytesToHex4, hexToBytes as hexToBytes4 } from "@noble/hashes/utils.js";
|
|
2139
|
-
var VERSION = 1;
|
|
2140
|
-
var HEADER_SIZE = 133;
|
|
2141
|
-
function base64ToBytes(base64) {
|
|
2142
|
-
const binary = atob(base64);
|
|
2143
|
-
const bytes = new Uint8Array(binary.length);
|
|
2144
|
-
for (let i = 0; i < binary.length; i++) {
|
|
2145
|
-
bytes[i] = binary.charCodeAt(i);
|
|
2146
|
-
}
|
|
2147
|
-
return bytes;
|
|
2148
|
-
}
|
|
2149
|
-
function bytesToBase64(bytes) {
|
|
2150
|
-
let binary = "";
|
|
2151
|
-
for (let i = 0; i < bytes.length; i++) {
|
|
2152
|
-
binary += String.fromCharCode(bytes[i]);
|
|
2153
|
-
}
|
|
2154
|
-
return btoa(binary);
|
|
2155
|
-
}
|
|
2156
|
-
function packEvent(event) {
|
|
2157
|
-
const pubkey = hexToBytes4(event.pubkey);
|
|
2158
|
-
const sig = hexToBytes4(event.sig);
|
|
2159
|
-
const recipientTag = event.tags.find((t) => t[0] === "p");
|
|
2160
|
-
if (!recipientTag) throw new Error("Gift wrap missing #p tag");
|
|
2161
|
-
if (event.tags.some((t) => t[0] !== "p")) {
|
|
2162
|
-
throw new Error("Gift wrap has unexpected non-p tags; compact encoding would lose them");
|
|
2163
|
-
}
|
|
2164
|
-
const recipient = hexToBytes4(recipientTag[1]);
|
|
2165
|
-
const createdAtBuf = new Uint8Array(4);
|
|
2166
|
-
new DataView(createdAtBuf.buffer).setUint32(0, event.created_at, false);
|
|
2167
|
-
const content = base64ToBytes(event.content);
|
|
2168
|
-
const result = new Uint8Array(HEADER_SIZE + content.length);
|
|
2169
|
-
result[0] = VERSION;
|
|
2170
|
-
result.set(pubkey, 1);
|
|
2171
|
-
result.set(sig, 33);
|
|
2172
|
-
result.set(recipient, 97);
|
|
2173
|
-
result.set(createdAtBuf, 129);
|
|
2174
|
-
result.set(content, HEADER_SIZE);
|
|
2175
|
-
return result;
|
|
2176
|
-
}
|
|
2177
|
-
function unpackEvent(id, compact) {
|
|
2178
|
-
const version = compact[0];
|
|
2179
|
-
if (version !== VERSION) throw new Error(`Unknown compact event version: ${version}`);
|
|
2180
|
-
const pubkey = bytesToHex4(compact.slice(1, 33));
|
|
2181
|
-
const sig = bytesToHex4(compact.slice(33, 97));
|
|
2182
|
-
const recipient = bytesToHex4(compact.slice(97, 129));
|
|
2183
|
-
const dv = new DataView(compact.buffer, compact.byteOffset + 129, 4);
|
|
2184
|
-
const createdAt = dv.getUint32(0, false);
|
|
2185
|
-
const content = bytesToBase64(compact.slice(HEADER_SIZE));
|
|
2186
|
-
return {
|
|
2187
|
-
id,
|
|
2188
|
-
pubkey,
|
|
2189
|
-
sig,
|
|
2190
|
-
created_at: createdAt,
|
|
2191
|
-
kind: 1059,
|
|
2192
|
-
tags: [["p", recipient]],
|
|
2193
|
-
content
|
|
2194
|
-
};
|
|
2195
|
-
}
|
|
2196
|
-
|
|
2197
|
-
// src/storage/idb.ts
|
|
2198
|
-
var DB_NAME = "tablinum";
|
|
2199
|
-
function storeName(collection2) {
|
|
2200
|
-
return `col_${collection2}`;
|
|
2201
|
-
}
|
|
2202
|
-
function computeSchemaSig(schema) {
|
|
2203
|
-
return Object.entries(schema).sort(([a], [b]) => a.localeCompare(b)).map(([name, def]) => {
|
|
2204
|
-
const indices = [...def.indices ?? []].sort().join(",");
|
|
2205
|
-
return `${name}:${indices}`;
|
|
2206
|
-
}).join("|");
|
|
2207
|
-
}
|
|
2208
|
-
function wrap(label, fn) {
|
|
2209
|
-
return Effect13.tryPromise({
|
|
2210
|
-
try: fn,
|
|
2211
|
-
catch: (e) => new StorageError({
|
|
2212
|
-
message: `IndexedDB ${label} failed: ${e instanceof Error ? e.message : String(e)}`,
|
|
2213
|
-
cause: e
|
|
2214
|
-
})
|
|
2215
|
-
});
|
|
2216
|
-
}
|
|
2217
|
-
function upgradeSchema(database, schema, tx) {
|
|
2218
|
-
if (!database.objectStoreNames.contains("_meta")) {
|
|
2219
|
-
database.createObjectStore("_meta");
|
|
2220
|
-
}
|
|
2221
|
-
if (!database.objectStoreNames.contains("events")) {
|
|
2222
|
-
const events = database.createObjectStore("events", { keyPath: "id" });
|
|
2223
|
-
events.createIndex("by-record", ["collection", "recordId"]);
|
|
2224
|
-
}
|
|
2225
|
-
if (!database.objectStoreNames.contains("giftwraps")) {
|
|
2226
|
-
database.createObjectStore("giftwraps", { keyPath: "id" });
|
|
2227
|
-
}
|
|
2228
|
-
const expectedStores = /* @__PURE__ */ new Set();
|
|
2229
|
-
for (const [, def] of Object.entries(schema)) {
|
|
2230
|
-
const sn = storeName(def.name);
|
|
2231
|
-
expectedStores.add(sn);
|
|
2232
|
-
if (!database.objectStoreNames.contains(sn)) {
|
|
2233
|
-
const store = database.createObjectStore(sn, { keyPath: "id" });
|
|
2234
|
-
for (const idx of def.indices ?? []) {
|
|
2235
|
-
store.createIndex(idx, idx);
|
|
2236
|
-
}
|
|
2237
|
-
} else {
|
|
2238
|
-
const store = tx.objectStore(sn);
|
|
2239
|
-
const existingIndices = new Set(Array.from(store.indexNames));
|
|
2240
|
-
const wantedIndices = new Set(def.indices ?? []);
|
|
2241
|
-
for (const idx of existingIndices) {
|
|
2242
|
-
if (!wantedIndices.has(idx)) store.deleteIndex(idx);
|
|
2243
|
-
}
|
|
2244
|
-
for (const idx of wantedIndices) {
|
|
2245
|
-
if (!existingIndices.has(idx)) store.createIndex(idx, idx);
|
|
2246
|
-
}
|
|
2247
|
-
}
|
|
2248
|
-
}
|
|
2249
|
-
for (const existing of Array.from(database.objectStoreNames)) {
|
|
2250
|
-
if (existing.startsWith("col_") && !expectedStores.has(existing)) {
|
|
2251
|
-
database.deleteObjectStore(existing);
|
|
2252
|
-
}
|
|
2253
|
-
}
|
|
2254
|
-
tx.objectStore("_meta").put(computeSchemaSig(schema), "schema_sig");
|
|
2255
|
-
}
|
|
2256
|
-
function openIDBStorage(dbName, schema) {
|
|
2257
|
-
return Effect13.gen(function* () {
|
|
2258
|
-
const name = dbName ?? DB_NAME;
|
|
2259
|
-
const schemaSig = computeSchemaSig(schema);
|
|
2260
|
-
if (typeof indexedDB === "undefined") {
|
|
2261
|
-
return yield* Effect13.fail(
|
|
2262
|
-
new StorageError({
|
|
2263
|
-
message: "IndexedDB is not available in this environment"
|
|
2264
|
-
})
|
|
2265
|
-
);
|
|
2266
|
-
}
|
|
2267
|
-
const probeDb = yield* Effect13.tryPromise({
|
|
2268
|
-
try: () => openDB(name),
|
|
2269
|
-
catch: (e) => new StorageError({ message: "Failed to open IndexedDB", cause: e })
|
|
2270
|
-
});
|
|
2271
|
-
const currentVersion = probeDb.version;
|
|
2272
|
-
let needsUpgrade = true;
|
|
2273
|
-
if (probeDb.objectStoreNames.contains("_meta")) {
|
|
2274
|
-
const storedSig = yield* Effect13.tryPromise({
|
|
2275
|
-
try: () => probeDb.get("_meta", "schema_sig"),
|
|
2276
|
-
catch: () => new StorageError({ message: "Failed to read schema meta" })
|
|
2277
|
-
}).pipe(Effect13.catch(() => Effect13.succeed(void 0)));
|
|
2278
|
-
needsUpgrade = storedSig !== schemaSig;
|
|
2279
|
-
}
|
|
2280
|
-
probeDb.close();
|
|
2281
|
-
const db = needsUpgrade ? yield* Effect13.tryPromise({
|
|
2282
|
-
try: () => openDB(name, currentVersion + 1, {
|
|
2283
|
-
upgrade(database, _oldVersion, _newVersion, transaction) {
|
|
2284
|
-
upgradeSchema(database, schema, transaction);
|
|
2285
|
-
}
|
|
2286
|
-
}),
|
|
2287
|
-
catch: (e) => new StorageError({ message: "Failed to open IndexedDB", cause: e })
|
|
2288
|
-
}) : yield* Effect13.tryPromise({
|
|
2289
|
-
try: () => openDB(name),
|
|
2290
|
-
catch: (e) => new StorageError({ message: "Failed to open IndexedDB", cause: e })
|
|
2291
|
-
});
|
|
2292
|
-
yield* Effect13.addFinalizer(() => Effect13.sync(() => db.close()));
|
|
2293
|
-
const handle = {
|
|
2294
|
-
putRecord: (collection2, record) => wrap("putRecord", () => db.put(storeName(collection2), record).then(() => void 0)),
|
|
2295
|
-
getRecord: (collection2, id) => wrap("getRecord", () => db.get(storeName(collection2), id)),
|
|
2296
|
-
getAllRecords: (collection2) => wrap("getAllRecords", () => db.getAll(storeName(collection2))),
|
|
2297
|
-
countRecords: (collection2) => wrap("countRecords", () => db.count(storeName(collection2))),
|
|
2298
|
-
clearRecords: (collection2) => wrap("clearRecords", () => db.clear(storeName(collection2))),
|
|
2299
|
-
getByIndex: (collection2, indexName, value) => wrap("getByIndex", () => db.getAllFromIndex(storeName(collection2), indexName, value)),
|
|
2300
|
-
getByIndexRange: (collection2, indexName, range) => wrap("getByIndexRange", () => db.getAllFromIndex(storeName(collection2), indexName, range)),
|
|
2301
|
-
getAllSorted: (collection2, indexName, direction) => wrap("getAllSorted", async () => {
|
|
2302
|
-
const sn = storeName(collection2);
|
|
2303
|
-
const tx = db.transaction(sn, "readonly");
|
|
2304
|
-
const store = tx.objectStore(sn);
|
|
2305
|
-
const index = store.index(indexName);
|
|
2306
|
-
const results = [];
|
|
2307
|
-
let cursor = await index.openCursor(null, direction ?? "next");
|
|
2308
|
-
while (cursor) {
|
|
2309
|
-
results.push(cursor.value);
|
|
2310
|
-
cursor = await cursor.continue();
|
|
2311
|
-
}
|
|
2312
|
-
return results;
|
|
2313
|
-
}),
|
|
2314
|
-
putEvent: (event) => wrap("putEvent", () => db.put("events", event).then(() => void 0)),
|
|
2315
|
-
getEvent: (id) => wrap("getEvent", () => db.get("events", id)),
|
|
2316
|
-
getAllEvents: () => wrap("getAllEvents", () => db.getAll("events")),
|
|
2317
|
-
getEventsByRecord: (collection2, recordId) => wrap(
|
|
2318
|
-
"getEventsByRecord",
|
|
2319
|
-
() => db.getAllFromIndex("events", "by-record", [collection2, recordId])
|
|
2320
|
-
),
|
|
2321
|
-
putGiftWrap: (gw) => wrap("putGiftWrap", async () => {
|
|
2322
|
-
if (gw.event) {
|
|
2323
|
-
const compact = packEvent(gw.event);
|
|
2324
|
-
await db.put("giftwraps", { id: gw.id, compact, createdAt: gw.createdAt });
|
|
2325
|
-
} else {
|
|
2326
|
-
await db.put("giftwraps", { id: gw.id, createdAt: gw.createdAt });
|
|
2327
|
-
}
|
|
2328
|
-
}),
|
|
2329
|
-
getGiftWrap: (id) => wrap("getGiftWrap", async () => {
|
|
2330
|
-
const raw = await db.get("giftwraps", id);
|
|
2331
|
-
if (!raw) return void 0;
|
|
2332
|
-
if (raw.compact) {
|
|
2333
|
-
return { id: raw.id, event: unpackEvent(raw.id, raw.compact), createdAt: raw.createdAt };
|
|
2334
|
-
}
|
|
2335
|
-
if (raw.event) {
|
|
2336
|
-
const compact = packEvent(raw.event);
|
|
2337
|
-
await db.put("giftwraps", { id: raw.id, compact, createdAt: raw.createdAt });
|
|
2338
|
-
return { id: raw.id, event: raw.event, createdAt: raw.createdAt };
|
|
2339
|
-
}
|
|
2340
|
-
return { id: raw.id, createdAt: raw.createdAt };
|
|
2341
|
-
}),
|
|
2342
|
-
getAllGiftWraps: () => wrap("getAllGiftWraps", async () => {
|
|
2343
|
-
const raws = await db.getAll("giftwraps");
|
|
2344
|
-
const results = [];
|
|
2345
|
-
for (const raw of raws) {
|
|
2346
|
-
if (raw.compact) {
|
|
2347
|
-
results.push({ id: raw.id, event: unpackEvent(raw.id, raw.compact), createdAt: raw.createdAt });
|
|
2348
|
-
} else if (raw.event) {
|
|
2349
|
-
const compact = packEvent(raw.event);
|
|
2350
|
-
await db.put("giftwraps", { id: raw.id, compact, createdAt: raw.createdAt });
|
|
2351
|
-
results.push({ id: raw.id, event: raw.event, createdAt: raw.createdAt });
|
|
2352
|
-
} else {
|
|
2353
|
-
results.push({ id: raw.id, createdAt: raw.createdAt });
|
|
2354
|
-
}
|
|
2355
|
-
}
|
|
2356
|
-
return results;
|
|
2357
|
-
}),
|
|
2358
|
-
deleteGiftWrap: (id) => wrap("deleteGiftWrap", () => db.delete("giftwraps", id).then(() => void 0)),
|
|
2359
|
-
deleteEvent: (id) => wrap("deleteEvent", () => db.delete("events", id).then(() => void 0)),
|
|
2360
|
-
stripEventData: (id) => wrap("stripEventData", async () => {
|
|
2361
|
-
const existing = await db.get("events", id);
|
|
2362
|
-
if (existing) {
|
|
2363
|
-
await db.put("events", { ...existing, data: null });
|
|
2364
|
-
}
|
|
2365
|
-
}),
|
|
2366
|
-
getMeta: (key) => wrap("getMeta", () => db.get("_meta", key)),
|
|
2367
|
-
putMeta: (key, value) => wrap("putMeta", () => db.put("_meta", value, key).then(() => void 0)),
|
|
2368
|
-
close: () => Effect13.sync(() => db.close())
|
|
2369
|
-
};
|
|
2370
|
-
return handle;
|
|
2371
|
-
});
|
|
2372
|
-
}
|
|
2373
|
-
|
|
2374
|
-
// src/layers/StorageLive.ts
|
|
2375
2372
|
var StorageLive = Layer4.effect(
|
|
2376
2373
|
Storage,
|
|
2377
2374
|
Effect14.gen(function* () {
|
|
@@ -2908,6 +2905,12 @@ var TablinumLive = Layer9.effect(
|
|
|
2908
2905
|
}
|
|
2909
2906
|
const gw = wrapResult.success;
|
|
2910
2907
|
yield* storage.putGiftWrap({ id: gw.id, event: gw, createdAt: gw.created_at });
|
|
2908
|
+
const metaKey = `gw_record:${event.collection}:${event.recordId}`;
|
|
2909
|
+
const prevMapping = yield* storage.getMeta(metaKey).pipe(
|
|
2910
|
+
Effect21.orElseSucceed(() => void 0)
|
|
2911
|
+
);
|
|
2912
|
+
const epochPubKey = gw.tags.find((t) => t[0] === "p")?.[1];
|
|
2913
|
+
yield* storage.putMeta(metaKey, { gwId: gw.id, epochPubKey });
|
|
2911
2914
|
yield* Effect21.forkIn(
|
|
2912
2915
|
Effect21.gen(function* () {
|
|
2913
2916
|
const publishResult = yield* Effect21.result(
|
|
@@ -2920,10 +2923,80 @@ var TablinumLive = Layer9.effect(
|
|
|
2920
2923
|
if (publishResult._tag === "Failure") {
|
|
2921
2924
|
reportSyncError(config.onSyncError, publishResult.failure);
|
|
2922
2925
|
}
|
|
2926
|
+
if (prevMapping?.epochPubKey) {
|
|
2927
|
+
const signingKey = getDecryptionKey(epochStore, prevMapping.epochPubKey);
|
|
2928
|
+
if (signingKey) {
|
|
2929
|
+
const deletionEvent = createDeletionEvent(
|
|
2930
|
+
[prevMapping.gwId],
|
|
2931
|
+
signingKey
|
|
2932
|
+
);
|
|
2933
|
+
yield* relay.publish(deletionEvent, [...config.relays]).pipe(
|
|
2934
|
+
Effect21.tapError(
|
|
2935
|
+
(e) => Effect21.sync(() => reportSyncError(config.onSyncError, e))
|
|
2936
|
+
),
|
|
2937
|
+
Effect21.ignore
|
|
2938
|
+
);
|
|
2939
|
+
}
|
|
2940
|
+
yield* storage.deleteGiftWrap(prevMapping.gwId).pipe(Effect21.ignore);
|
|
2941
|
+
}
|
|
2923
2942
|
}),
|
|
2924
2943
|
scope
|
|
2925
2944
|
);
|
|
2926
2945
|
});
|
|
2946
|
+
const republishAllUnderCurrentEpoch = () => Effect21.gen(function* () {
|
|
2947
|
+
const oldGwDeletions = [];
|
|
2948
|
+
for (const [, def] of allSchemaEntries) {
|
|
2949
|
+
const collectionName = def.name;
|
|
2950
|
+
const allRecords = yield* storage.getAllRecords(collectionName);
|
|
2951
|
+
for (const record of allRecords) {
|
|
2952
|
+
const recordId = record.id;
|
|
2953
|
+
const { _d, _u, _a, _e, ...fields } = record;
|
|
2954
|
+
const content = _d ? JSON.stringify(null) : JSON.stringify(fields);
|
|
2955
|
+
const dTag = `${collectionName}:${recordId}`;
|
|
2956
|
+
const wrapResult = yield* Effect21.result(
|
|
2957
|
+
giftWrap.wrap({
|
|
2958
|
+
kind: 1,
|
|
2959
|
+
content,
|
|
2960
|
+
tags: [["d", dTag]],
|
|
2961
|
+
created_at: Math.floor(Date.now() / 1e3)
|
|
2962
|
+
})
|
|
2963
|
+
);
|
|
2964
|
+
if (wrapResult._tag === "Failure") continue;
|
|
2965
|
+
const gw = wrapResult.success;
|
|
2966
|
+
yield* storage.putGiftWrap({ id: gw.id, event: gw, createdAt: gw.created_at });
|
|
2967
|
+
const metaKey = `gw_record:${collectionName}:${recordId}`;
|
|
2968
|
+
const prevMapping = yield* storage.getMeta(metaKey).pipe(
|
|
2969
|
+
Effect21.orElseSucceed(() => void 0)
|
|
2970
|
+
);
|
|
2971
|
+
if (prevMapping) oldGwDeletions.push(prevMapping);
|
|
2972
|
+
const epochPubKey = gw.tags.find((t) => t[0] === "p")?.[1];
|
|
2973
|
+
yield* storage.putMeta(metaKey, { gwId: gw.id, epochPubKey });
|
|
2974
|
+
yield* relay.publish(gw, [...config.relays]).pipe(
|
|
2975
|
+
Effect21.tapError((e) => Effect21.sync(() => reportSyncError(config.onSyncError, e))),
|
|
2976
|
+
Effect21.ignore
|
|
2977
|
+
);
|
|
2978
|
+
}
|
|
2979
|
+
}
|
|
2980
|
+
const byEpoch = /* @__PURE__ */ new Map();
|
|
2981
|
+
for (const { gwId, epochPubKey } of oldGwDeletions) {
|
|
2982
|
+
const ids = byEpoch.get(epochPubKey) ?? [];
|
|
2983
|
+
ids.push(gwId);
|
|
2984
|
+
byEpoch.set(epochPubKey, ids);
|
|
2985
|
+
}
|
|
2986
|
+
for (const [epochPubKey, gwIds] of byEpoch) {
|
|
2987
|
+
const signingKey = getDecryptionKey(epochStore, epochPubKey);
|
|
2988
|
+
if (signingKey) {
|
|
2989
|
+
const deletionEvent = createDeletionEvent(gwIds, signingKey);
|
|
2990
|
+
yield* relay.publish(deletionEvent, [...config.relays]).pipe(
|
|
2991
|
+
Effect21.tapError((e) => Effect21.sync(() => reportSyncError(config.onSyncError, e))),
|
|
2992
|
+
Effect21.ignore
|
|
2993
|
+
);
|
|
2994
|
+
}
|
|
2995
|
+
for (const gwId of gwIds) {
|
|
2996
|
+
yield* storage.deleteGiftWrap(gwId).pipe(Effect21.ignore);
|
|
2997
|
+
}
|
|
2998
|
+
}
|
|
2999
|
+
});
|
|
2927
3000
|
const knownAuthors = /* @__PURE__ */ new Set();
|
|
2928
3001
|
const putMemberRecord = (record) => Effect21.gen(function* () {
|
|
2929
3002
|
const existing = yield* storage.getRecord("_members", record.id);
|
|
@@ -3011,6 +3084,13 @@ var TablinumLive = Layer9.effect(
|
|
|
3011
3084
|
addedInEpoch: getCurrentEpoch(epochStore).id
|
|
3012
3085
|
});
|
|
3013
3086
|
}
|
|
3087
|
+
const migrated = yield* storage.getMeta("migration_gw_republish").pipe(
|
|
3088
|
+
Effect21.orElseSucceed(() => void 0)
|
|
3089
|
+
);
|
|
3090
|
+
if (!migrated) {
|
|
3091
|
+
yield* republishAllUnderCurrentEpoch().pipe(Effect21.ignore);
|
|
3092
|
+
yield* storage.putMeta("migration_gw_republish", true);
|
|
3093
|
+
}
|
|
3014
3094
|
const withLog = (effect) => Effect21.provideService(effect, References3.MinimumLogLevel, config.logLevel);
|
|
3015
3095
|
const ensureOpen = (effect) => withLog(
|
|
3016
3096
|
Effect21.gen(function* () {
|
|
@@ -3050,6 +3130,61 @@ var TablinumLive = Layer9.effect(
|
|
|
3050
3130
|
yield* Scope4.close(scope, Exit.void);
|
|
3051
3131
|
})
|
|
3052
3132
|
),
|
|
3133
|
+
destroy: () => withLog(
|
|
3134
|
+
Effect21.gen(function* () {
|
|
3135
|
+
if (!(yield* Ref5.get(closedRef))) {
|
|
3136
|
+
yield* Ref5.set(closedRef, true);
|
|
3137
|
+
syncHandle.stopHealing();
|
|
3138
|
+
yield* Scope4.close(scope, Exit.void);
|
|
3139
|
+
}
|
|
3140
|
+
yield* deleteIDBStorage(config.dbName);
|
|
3141
|
+
})
|
|
3142
|
+
),
|
|
3143
|
+
leave: () => withLog(
|
|
3144
|
+
Effect21.gen(function* () {
|
|
3145
|
+
if (yield* Ref5.get(closedRef)) {
|
|
3146
|
+
return yield* new SyncError({ message: "Database is closed", phase: "leave" });
|
|
3147
|
+
}
|
|
3148
|
+
const allMembers = yield* storage.getAllRecords("_members");
|
|
3149
|
+
const activeMembers = allMembers.filter(
|
|
3150
|
+
(member) => !member.removedAt && member.id !== identity.publicKey
|
|
3151
|
+
);
|
|
3152
|
+
const activePubkeys = activeMembers.map((member) => member.id);
|
|
3153
|
+
const result = createRotation(
|
|
3154
|
+
epochStore,
|
|
3155
|
+
identity.privateKey,
|
|
3156
|
+
identity.publicKey,
|
|
3157
|
+
activePubkeys,
|
|
3158
|
+
[identity.publicKey]
|
|
3159
|
+
);
|
|
3160
|
+
addEpoch(epochStore, result.epoch);
|
|
3161
|
+
epochStore.currentEpochId = result.epoch.id;
|
|
3162
|
+
yield* storage.putMeta("epochs", stringifyEpochStore(epochStore));
|
|
3163
|
+
const memberRecord = yield* storage.getRecord("_members", identity.publicKey);
|
|
3164
|
+
yield* putMemberRecord({
|
|
3165
|
+
...memberRecord ?? {
|
|
3166
|
+
id: identity.publicKey,
|
|
3167
|
+
addedAt: 0,
|
|
3168
|
+
addedInEpoch: EpochId("epoch-0")
|
|
3169
|
+
},
|
|
3170
|
+
removedAt: Date.now(),
|
|
3171
|
+
removedInEpoch: result.epoch.id
|
|
3172
|
+
});
|
|
3173
|
+
yield* republishAllUnderCurrentEpoch();
|
|
3174
|
+
yield* Effect21.forEach(
|
|
3175
|
+
result.wrappedEvents,
|
|
3176
|
+
(wrappedEvent) => relay.publish(wrappedEvent, [...config.relays]).pipe(
|
|
3177
|
+
Effect21.tapError((e) => Effect21.sync(() => reportSyncError(config.onSyncError, e))),
|
|
3178
|
+
Effect21.ignore
|
|
3179
|
+
),
|
|
3180
|
+
{ discard: true }
|
|
3181
|
+
);
|
|
3182
|
+
yield* Ref5.set(closedRef, true);
|
|
3183
|
+
syncHandle.stopHealing();
|
|
3184
|
+
yield* Scope4.close(scope, Exit.void);
|
|
3185
|
+
yield* deleteIDBStorage(config.dbName);
|
|
3186
|
+
})
|
|
3187
|
+
),
|
|
3053
3188
|
rebuild: () => ensureOpen(
|
|
3054
3189
|
rebuild(
|
|
3055
3190
|
storage,
|
|
@@ -3110,6 +3245,7 @@ var TablinumLive = Layer9.effect(
|
|
|
3110
3245
|
removedAt: Date.now(),
|
|
3111
3246
|
removedInEpoch: result.epoch.id
|
|
3112
3247
|
});
|
|
3248
|
+
yield* republishAllUnderCurrentEpoch();
|
|
3113
3249
|
yield* Effect21.forEach(
|
|
3114
3250
|
result.wrappedEvents,
|
|
3115
3251
|
(wrappedEvent) => relay.publish(wrappedEvent, [...config.relays]).pipe(
|
|
@@ -3187,6 +3323,9 @@ function validateConfig(config) {
|
|
|
3187
3323
|
}
|
|
3188
3324
|
});
|
|
3189
3325
|
}
|
|
3326
|
+
function deleteDatabase(dbName) {
|
|
3327
|
+
return deleteIDBStorage(DatabaseName(dbName ?? "tablinum"));
|
|
3328
|
+
}
|
|
3190
3329
|
function createTablinum(config) {
|
|
3191
3330
|
return Effect22.gen(function* () {
|
|
3192
3331
|
yield* validateConfig(config);
|
|
@@ -3259,6 +3398,7 @@ export {
|
|
|
3259
3398
|
collection,
|
|
3260
3399
|
createTablinum,
|
|
3261
3400
|
decodeInvite,
|
|
3401
|
+
deleteDatabase,
|
|
3262
3402
|
encodeInvite,
|
|
3263
3403
|
field
|
|
3264
3404
|
};
|