tablinum 0.6.4 → 0.7.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 +451 -385
- package/dist/storage/idb.d.ts +1 -0
- package/dist/svelte/index.svelte.d.ts +1 -0
- package/dist/svelte/index.svelte.js +464 -388
- package/dist/svelte/tablinum.svelte.d.ts +2 -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) {
|
|
@@ -383,7 +631,7 @@ function buildRecord(event) {
|
|
|
383
631
|
};
|
|
384
632
|
}
|
|
385
633
|
function applyEvent(storage, event) {
|
|
386
|
-
return
|
|
634
|
+
return Effect4.gen(function* () {
|
|
387
635
|
const existing = yield* storage.getRecord(event.collection, event.recordId);
|
|
388
636
|
if (existing) {
|
|
389
637
|
const existingMeta = {
|
|
@@ -403,7 +651,7 @@ function applyEvent(storage, event) {
|
|
|
403
651
|
});
|
|
404
652
|
}
|
|
405
653
|
function rebuild(storage, collections) {
|
|
406
|
-
return
|
|
654
|
+
return Effect4.gen(function* () {
|
|
407
655
|
for (const col of collections) {
|
|
408
656
|
yield* storage.clearRecords(col);
|
|
409
657
|
}
|
|
@@ -419,7 +667,7 @@ function rebuild(storage, collections) {
|
|
|
419
667
|
}
|
|
420
668
|
|
|
421
669
|
// src/schema/validate.ts
|
|
422
|
-
import { Effect as
|
|
670
|
+
import { Effect as Effect5, Schema as Schema4 } from "effect";
|
|
423
671
|
function fieldDefToSchema(fd) {
|
|
424
672
|
let base;
|
|
425
673
|
switch (fd.kind) {
|
|
@@ -467,10 +715,10 @@ function buildStructSchema(def, options = {}) {
|
|
|
467
715
|
function buildValidator(collectionName, def) {
|
|
468
716
|
const decode = Schema4.decodeUnknownEffect(buildStructSchema(def, { includeId: true }));
|
|
469
717
|
return (input) => decode(input).pipe(
|
|
470
|
-
|
|
718
|
+
Effect5.map(
|
|
471
719
|
(result) => result
|
|
472
720
|
),
|
|
473
|
-
|
|
721
|
+
Effect5.mapError(
|
|
474
722
|
(e) => new ValidationError({
|
|
475
723
|
message: `Validation failed for collection "${collectionName}": ${e.message}`
|
|
476
724
|
})
|
|
@@ -479,7 +727,7 @@ function buildValidator(collectionName, def) {
|
|
|
479
727
|
}
|
|
480
728
|
function buildPartialValidator(collectionName, def) {
|
|
481
729
|
const decode = Schema4.decodeUnknownEffect(buildStructSchema(def, { allOptional: true }));
|
|
482
|
-
return (input) =>
|
|
730
|
+
return (input) => Effect5.gen(function* () {
|
|
483
731
|
if (typeof input !== "object" || input === null) {
|
|
484
732
|
return yield* new ValidationError({
|
|
485
733
|
message: `Validation failed for collection "${collectionName}": expected an object`
|
|
@@ -494,8 +742,8 @@ function buildPartialValidator(collectionName, def) {
|
|
|
494
742
|
});
|
|
495
743
|
}
|
|
496
744
|
return yield* decode(record).pipe(
|
|
497
|
-
|
|
498
|
-
|
|
745
|
+
Effect5.map((result) => result),
|
|
746
|
+
Effect5.mapError(
|
|
499
747
|
(e) => new ValidationError({
|
|
500
748
|
message: `Validation failed for collection "${collectionName}": ${e.message}`
|
|
501
749
|
})
|
|
@@ -505,7 +753,7 @@ function buildPartialValidator(collectionName, def) {
|
|
|
505
753
|
}
|
|
506
754
|
|
|
507
755
|
// src/crud/collection-handle.ts
|
|
508
|
-
import { Effect as
|
|
756
|
+
import { Effect as Effect7, Option as Option3, References } from "effect";
|
|
509
757
|
|
|
510
758
|
// src/utils/uuid.ts
|
|
511
759
|
var alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
|
|
@@ -538,12 +786,12 @@ function uuidv7() {
|
|
|
538
786
|
}
|
|
539
787
|
|
|
540
788
|
// src/crud/query-builder.ts
|
|
541
|
-
import { Effect as
|
|
789
|
+
import { Effect as Effect6, Option as Option2, Ref as Ref2, Stream as Stream2 } from "effect";
|
|
542
790
|
function emptyPlan() {
|
|
543
791
|
return { filters: [] };
|
|
544
792
|
}
|
|
545
793
|
function executeQuery(ctx, plan) {
|
|
546
|
-
return
|
|
794
|
+
return Effect6.gen(function* () {
|
|
547
795
|
if (plan.fieldName) {
|
|
548
796
|
const fieldDef = ctx.def.fields[plan.fieldName];
|
|
549
797
|
if (!fieldDef) {
|
|
@@ -619,7 +867,7 @@ function watchQuery(ctx, plan) {
|
|
|
619
867
|
const changes = Stream2.fromPubSub(ctx.watchCtx.pubsub).pipe(
|
|
620
868
|
Stream2.filter((event) => event.collection === ctx.collectionName),
|
|
621
869
|
Stream2.mapEffect(
|
|
622
|
-
() =>
|
|
870
|
+
() => Effect6.gen(function* () {
|
|
623
871
|
const replaying = yield* Ref2.get(ctx.watchCtx.replayingRef);
|
|
624
872
|
if (replaying) return void 0;
|
|
625
873
|
return yield* query();
|
|
@@ -628,7 +876,7 @@ function watchQuery(ctx, plan) {
|
|
|
628
876
|
Stream2.filter((result) => result !== void 0)
|
|
629
877
|
);
|
|
630
878
|
return Stream2.unwrap(
|
|
631
|
-
|
|
879
|
+
Effect6.gen(function* () {
|
|
632
880
|
const initial = yield* query();
|
|
633
881
|
return Stream2.concat(Stream2.make(initial), changes);
|
|
634
882
|
})
|
|
@@ -654,11 +902,11 @@ function makeQueryBuilder(ctx, plan) {
|
|
|
654
902
|
offset: (n) => makeQueryBuilder(ctx, { ...plan, offset: n }),
|
|
655
903
|
limit: (n) => makeQueryBuilder(ctx, { ...plan, limit: n }),
|
|
656
904
|
get: () => executeQuery(ctx, plan),
|
|
657
|
-
first: () =>
|
|
905
|
+
first: () => Effect6.map(
|
|
658
906
|
executeQuery(ctx, { ...plan, limit: 1 }),
|
|
659
907
|
(results) => results.length > 0 ? Option2.some(results[0]) : Option2.none()
|
|
660
908
|
),
|
|
661
|
-
count: () =>
|
|
909
|
+
count: () => Effect6.map(executeQuery(ctx, plan), (results) => results.length),
|
|
662
910
|
watch: () => watchQuery(ctx, plan)
|
|
663
911
|
};
|
|
664
912
|
}
|
|
@@ -764,7 +1012,7 @@ function replayState(recordId, events, stopAtId) {
|
|
|
764
1012
|
return state;
|
|
765
1013
|
}
|
|
766
1014
|
function promoteToSnapshot(storage, collection2, recordId, target, allSorted) {
|
|
767
|
-
return
|
|
1015
|
+
return Effect7.gen(function* () {
|
|
768
1016
|
const chronological = sortChronologically(allSorted);
|
|
769
1017
|
const state = replayState(recordId, chronological, target.id);
|
|
770
1018
|
if (state) {
|
|
@@ -773,7 +1021,7 @@ function promoteToSnapshot(storage, collection2, recordId, target, allSorted) {
|
|
|
773
1021
|
});
|
|
774
1022
|
}
|
|
775
1023
|
function pruneEvents(storage, collection2, recordId, retention) {
|
|
776
|
-
return
|
|
1024
|
+
return Effect7.gen(function* () {
|
|
777
1025
|
const events = yield* storage.getEventsByRecord(collection2, recordId);
|
|
778
1026
|
if (events.length <= retention) return;
|
|
779
1027
|
const sorted = [...events].sort((a, b) => b.createdAt - a.createdAt || (a.id < b.id ? 1 : -1));
|
|
@@ -794,8 +1042,8 @@ function mapRecord(record) {
|
|
|
794
1042
|
}
|
|
795
1043
|
function createCollectionHandle(def, storage, watchCtx, validator, partialValidator, makeEventId, localAuthor, onWrite, logLevel = "None") {
|
|
796
1044
|
const collectionName = def.name;
|
|
797
|
-
const withLog = (effect) =>
|
|
798
|
-
const commitEvent = (event) =>
|
|
1045
|
+
const withLog = (effect) => Effect7.provideService(effect, References.MinimumLogLevel, logLevel);
|
|
1046
|
+
const commitEvent = (event) => Effect7.gen(function* () {
|
|
799
1047
|
yield* storage.putEvent(event);
|
|
800
1048
|
yield* applyEvent(storage, event);
|
|
801
1049
|
if (onWrite) yield* onWrite(event);
|
|
@@ -807,7 +1055,7 @@ function createCollectionHandle(def, storage, watchCtx, validator, partialValida
|
|
|
807
1055
|
});
|
|
808
1056
|
const handle = {
|
|
809
1057
|
add: (data) => withLog(
|
|
810
|
-
|
|
1058
|
+
Effect7.gen(function* () {
|
|
811
1059
|
const id = uuidv7();
|
|
812
1060
|
const fullRecord = { id, ...data };
|
|
813
1061
|
yield* validator(fullRecord);
|
|
@@ -821,7 +1069,7 @@ function createCollectionHandle(def, storage, watchCtx, validator, partialValida
|
|
|
821
1069
|
author: localAuthor
|
|
822
1070
|
};
|
|
823
1071
|
yield* commitEvent(event);
|
|
824
|
-
yield*
|
|
1072
|
+
yield* Effect7.logDebug("Record added", {
|
|
825
1073
|
collection: collectionName,
|
|
826
1074
|
recordId: id,
|
|
827
1075
|
data: fullRecord
|
|
@@ -830,7 +1078,7 @@ function createCollectionHandle(def, storage, watchCtx, validator, partialValida
|
|
|
830
1078
|
})
|
|
831
1079
|
),
|
|
832
1080
|
update: (id, data) => withLog(
|
|
833
|
-
|
|
1081
|
+
Effect7.gen(function* () {
|
|
834
1082
|
const existing = yield* storage.getRecord(collectionName, id);
|
|
835
1083
|
if (!existing || existing._d) {
|
|
836
1084
|
return yield* new NotFoundError({
|
|
@@ -853,7 +1101,7 @@ function createCollectionHandle(def, storage, watchCtx, validator, partialValida
|
|
|
853
1101
|
author: localAuthor
|
|
854
1102
|
};
|
|
855
1103
|
yield* commitEvent(event);
|
|
856
|
-
yield*
|
|
1104
|
+
yield* Effect7.logDebug("Record updated", {
|
|
857
1105
|
collection: collectionName,
|
|
858
1106
|
recordId: id,
|
|
859
1107
|
data: diff
|
|
@@ -862,7 +1110,7 @@ function createCollectionHandle(def, storage, watchCtx, validator, partialValida
|
|
|
862
1110
|
})
|
|
863
1111
|
),
|
|
864
1112
|
delete: (id) => withLog(
|
|
865
|
-
|
|
1113
|
+
Effect7.gen(function* () {
|
|
866
1114
|
const existing = yield* storage.getRecord(collectionName, id);
|
|
867
1115
|
if (!existing || existing._d) {
|
|
868
1116
|
return yield* new NotFoundError({
|
|
@@ -880,11 +1128,11 @@ function createCollectionHandle(def, storage, watchCtx, validator, partialValida
|
|
|
880
1128
|
author: localAuthor
|
|
881
1129
|
};
|
|
882
1130
|
yield* commitEvent(event);
|
|
883
|
-
yield*
|
|
1131
|
+
yield* Effect7.logDebug("Record deleted", { collection: collectionName, recordId: id });
|
|
884
1132
|
yield* pruneEvents(storage, collectionName, id, def.eventRetention);
|
|
885
1133
|
})
|
|
886
1134
|
),
|
|
887
|
-
undo: (id) =>
|
|
1135
|
+
undo: (id) => Effect7.gen(function* () {
|
|
888
1136
|
const existing = yield* storage.getRecord(collectionName, id);
|
|
889
1137
|
if (!existing) {
|
|
890
1138
|
return yield* new NotFoundError({ collection: collectionName, id });
|
|
@@ -909,7 +1157,7 @@ function createCollectionHandle(def, storage, watchCtx, validator, partialValida
|
|
|
909
1157
|
yield* commitEvent(event);
|
|
910
1158
|
yield* pruneEvents(storage, collectionName, id, def.eventRetention);
|
|
911
1159
|
}),
|
|
912
|
-
get: (id) =>
|
|
1160
|
+
get: (id) => Effect7.gen(function* () {
|
|
913
1161
|
const record = yield* storage.getRecord(collectionName, id);
|
|
914
1162
|
if (!record || record._d) {
|
|
915
1163
|
return yield* new NotFoundError({
|
|
@@ -919,11 +1167,11 @@ function createCollectionHandle(def, storage, watchCtx, validator, partialValida
|
|
|
919
1167
|
}
|
|
920
1168
|
return mapRecord(record);
|
|
921
1169
|
}),
|
|
922
|
-
first: () =>
|
|
1170
|
+
first: () => Effect7.map(storage.getAllRecords(collectionName), (all) => {
|
|
923
1171
|
const found = all.find((r) => !r._d);
|
|
924
1172
|
return found ? Option3.some(mapRecord(found)) : Option3.none();
|
|
925
1173
|
}),
|
|
926
|
-
count: () =>
|
|
1174
|
+
count: () => Effect7.map(storage.getAllRecords(collectionName), (all) => all.filter((r) => !r._d).length),
|
|
927
1175
|
watch: () => watchCollection(watchCtx, storage, collectionName, void 0, mapRecord),
|
|
928
1176
|
where: (fieldName) => createWhereClause(
|
|
929
1177
|
storage,
|
|
@@ -946,12 +1194,12 @@ function createCollectionHandle(def, storage, watchCtx, validator, partialValida
|
|
|
946
1194
|
}
|
|
947
1195
|
|
|
948
1196
|
// src/sync/sync-service.ts
|
|
949
|
-
import { Duration, Effect as
|
|
1197
|
+
import { Duration, Effect as Effect9, Layer, Option as Option5, References as References2, Ref as Ref3, Schedule } from "effect";
|
|
950
1198
|
import { unwrapEvent } from "nostr-tools/nip59";
|
|
951
1199
|
import { GiftWrap as GiftWrap2 } from "nostr-tools/kinds";
|
|
952
1200
|
|
|
953
1201
|
// src/sync/negentropy.ts
|
|
954
|
-
import { Effect as
|
|
1202
|
+
import { Effect as Effect8 } from "effect";
|
|
955
1203
|
|
|
956
1204
|
// src/vendor/negentropy.js
|
|
957
1205
|
var PROTOCOL_VERSION = 97;
|
|
@@ -1438,14 +1686,14 @@ function itemCompare(a, b) {
|
|
|
1438
1686
|
}
|
|
1439
1687
|
|
|
1440
1688
|
// src/sync/negentropy.ts
|
|
1441
|
-
import { hexToBytes as
|
|
1689
|
+
import { hexToBytes as hexToBytes3 } from "@noble/hashes/utils.js";
|
|
1442
1690
|
import { GiftWrap } from "nostr-tools/kinds";
|
|
1443
1691
|
function reconcileWithRelay(storage, relay, relayUrl, publicKeys) {
|
|
1444
|
-
return
|
|
1692
|
+
return Effect8.gen(function* () {
|
|
1445
1693
|
const allGiftWraps = yield* storage.getAllGiftWraps();
|
|
1446
1694
|
const storageVector = new NegentropyStorageVector();
|
|
1447
1695
|
for (const gw of allGiftWraps) {
|
|
1448
|
-
storageVector.insert(gw.createdAt,
|
|
1696
|
+
storageVector.insert(gw.createdAt, hexToBytes3(gw.id));
|
|
1449
1697
|
}
|
|
1450
1698
|
storageVector.seal();
|
|
1451
1699
|
const neg = new Negentropy(storageVector, 0);
|
|
@@ -1456,7 +1704,7 @@ function reconcileWithRelay(storage, relay, relayUrl, publicKeys) {
|
|
|
1456
1704
|
const allHaveIds = [];
|
|
1457
1705
|
const allNeedIds = [];
|
|
1458
1706
|
const subId = `neg-${Date.now()}`;
|
|
1459
|
-
const initialMsg = yield*
|
|
1707
|
+
const initialMsg = yield* Effect8.tryPromise({
|
|
1460
1708
|
try: () => neg.initiate(),
|
|
1461
1709
|
catch: (e) => new SyncError({
|
|
1462
1710
|
message: `Negentropy initiate failed: ${e instanceof Error ? e.message : String(e)}`,
|
|
@@ -1468,7 +1716,7 @@ function reconcileWithRelay(storage, relay, relayUrl, publicKeys) {
|
|
|
1468
1716
|
while (currentMsg !== null) {
|
|
1469
1717
|
const response = yield* relay.sendNegMsg(relayUrl, subId, filter, currentMsg);
|
|
1470
1718
|
if (response.msgHex === null) break;
|
|
1471
|
-
const reconcileResult = yield*
|
|
1719
|
+
const reconcileResult = yield* Effect8.tryPromise({
|
|
1472
1720
|
try: () => neg.reconcile(response.msgHex),
|
|
1473
1721
|
catch: (e) => new SyncError({
|
|
1474
1722
|
message: `Negentropy reconcile failed: ${e instanceof Error ? e.message : String(e)}`,
|
|
@@ -1481,13 +1729,13 @@ function reconcileWithRelay(storage, relay, relayUrl, publicKeys) {
|
|
|
1481
1729
|
for (const id of needIds) allNeedIds.push(id);
|
|
1482
1730
|
currentMsg = nextMsg;
|
|
1483
1731
|
}
|
|
1484
|
-
yield*
|
|
1732
|
+
yield* Effect8.logDebug("Negentropy reconciliation complete", {
|
|
1485
1733
|
relay: relayUrl,
|
|
1486
1734
|
have: allHaveIds.length,
|
|
1487
1735
|
need: allNeedIds.length
|
|
1488
1736
|
});
|
|
1489
1737
|
return { haveIds: allHaveIds, needIds: allNeedIds };
|
|
1490
|
-
}).pipe(
|
|
1738
|
+
}).pipe(Effect8.withLogSpan("tablinum.negentropy"));
|
|
1491
1739
|
}
|
|
1492
1740
|
|
|
1493
1741
|
// src/db/key-rotation.ts
|
|
@@ -1581,52 +1829,52 @@ function createSyncHandle(storage, giftWrapHandle, relay, publishQueue, syncStat
|
|
|
1581
1829
|
kind: "create"
|
|
1582
1830
|
});
|
|
1583
1831
|
const forkHandled = (effect) => {
|
|
1584
|
-
|
|
1832
|
+
Effect9.runFork(
|
|
1585
1833
|
effect.pipe(
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1834
|
+
Effect9.tapError((e) => Effect9.sync(() => onSyncError?.(e))),
|
|
1835
|
+
Effect9.ignore,
|
|
1836
|
+
Effect9.provide(logLayer),
|
|
1837
|
+
Effect9.forkIn(scope)
|
|
1590
1838
|
)
|
|
1591
1839
|
);
|
|
1592
1840
|
};
|
|
1593
1841
|
let autoFlushActive = false;
|
|
1594
|
-
const autoFlushEffect =
|
|
1842
|
+
const autoFlushEffect = Effect9.gen(function* () {
|
|
1595
1843
|
const size = yield* publishQueue.size();
|
|
1596
1844
|
if (size === 0) return;
|
|
1597
1845
|
yield* syncStatus.set("syncing");
|
|
1598
1846
|
yield* publishQueue.flush(relayUrls);
|
|
1599
1847
|
const remaining = yield* publishQueue.size();
|
|
1600
|
-
if (remaining > 0) yield*
|
|
1848
|
+
if (remaining > 0) yield* Effect9.fail("pending");
|
|
1601
1849
|
}).pipe(
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1850
|
+
Effect9.ensuring(syncStatus.set("idle")),
|
|
1851
|
+
Effect9.retry({ schedule: Schedule.exponential(5e3).pipe(Schedule.jittered), times: 10 }),
|
|
1852
|
+
Effect9.ignore
|
|
1605
1853
|
);
|
|
1606
1854
|
const scheduleAutoFlush = () => {
|
|
1607
1855
|
if (autoFlushActive) return;
|
|
1608
1856
|
autoFlushActive = true;
|
|
1609
1857
|
forkHandled(
|
|
1610
1858
|
autoFlushEffect.pipe(
|
|
1611
|
-
|
|
1612
|
-
|
|
1859
|
+
Effect9.ensuring(
|
|
1860
|
+
Effect9.sync(() => {
|
|
1613
1861
|
autoFlushActive = false;
|
|
1614
1862
|
})
|
|
1615
1863
|
)
|
|
1616
1864
|
)
|
|
1617
1865
|
);
|
|
1618
1866
|
};
|
|
1619
|
-
const shouldRejectWrite = (authorPubkey) =>
|
|
1867
|
+
const shouldRejectWrite = (authorPubkey) => Effect9.gen(function* () {
|
|
1620
1868
|
const memberRecord = yield* storage.getRecord("_members", authorPubkey);
|
|
1621
1869
|
if (!memberRecord) return false;
|
|
1622
1870
|
return !!memberRecord.removedAt;
|
|
1623
1871
|
});
|
|
1624
1872
|
const storeGiftWrapShell = (gw) => storage.putGiftWrap({ id: gw.id, event: gw, createdAt: gw.created_at });
|
|
1625
|
-
const unwrapGiftWrap = (remoteGw) =>
|
|
1873
|
+
const unwrapGiftWrap = (remoteGw) => Effect9.gen(function* () {
|
|
1626
1874
|
const existing = yield* storage.getGiftWrap(remoteGw.id);
|
|
1627
1875
|
if (existing) return null;
|
|
1628
1876
|
const rumor = yield* giftWrapHandle.unwrap(remoteGw).pipe(
|
|
1629
|
-
|
|
1877
|
+
Effect9.orElseSucceed(() => null)
|
|
1630
1878
|
);
|
|
1631
1879
|
if (!rumor) {
|
|
1632
1880
|
yield* storeGiftWrapShell(remoteGw);
|
|
@@ -1654,7 +1902,7 @@ function createSyncHandle(storage, giftWrapHandle, relay, publishQueue, syncStat
|
|
|
1654
1902
|
recordId: dTag.substring(colonIdx + 1)
|
|
1655
1903
|
};
|
|
1656
1904
|
});
|
|
1657
|
-
const applyUnwrappedEvent = (uw) =>
|
|
1905
|
+
const applyUnwrappedEvent = (uw) => Effect9.gen(function* () {
|
|
1658
1906
|
const { giftWrap: remoteGw, rumor, collection: collectionName, recordId } = uw;
|
|
1659
1907
|
const retention = knownCollections.get(collectionName);
|
|
1660
1908
|
if (retention === void 0) {
|
|
@@ -1664,17 +1912,17 @@ function createSyncHandle(storage, giftWrapHandle, relay, publishQueue, syncStat
|
|
|
1664
1912
|
if (rumor.pubkey) {
|
|
1665
1913
|
const reject = yield* shouldRejectWrite(rumor.pubkey);
|
|
1666
1914
|
if (reject) {
|
|
1667
|
-
yield*
|
|
1915
|
+
yield* Effect9.logWarning("Rejected write from removed member", {
|
|
1668
1916
|
author: rumor.pubkey.slice(0, 12)
|
|
1669
1917
|
});
|
|
1670
1918
|
yield* storeGiftWrapShell(remoteGw);
|
|
1671
1919
|
return null;
|
|
1672
1920
|
}
|
|
1673
1921
|
}
|
|
1674
|
-
const parsed = yield*
|
|
1922
|
+
const parsed = yield* Effect9.try({
|
|
1675
1923
|
try: () => JSON.parse(rumor.content),
|
|
1676
1924
|
catch: () => void 0
|
|
1677
|
-
}).pipe(
|
|
1925
|
+
}).pipe(Effect9.orElseSucceed(() => void 0));
|
|
1678
1926
|
if (parsed === void 0) {
|
|
1679
1927
|
yield* storeGiftWrapShell(remoteGw);
|
|
1680
1928
|
return null;
|
|
@@ -1706,7 +1954,7 @@ function createSyncHandle(storage, giftWrapHandle, relay, publishQueue, syncStat
|
|
|
1706
1954
|
if (didApply && (kind === "u" || kind === "d")) {
|
|
1707
1955
|
yield* pruneEvents(storage, collectionName, recordId, retention);
|
|
1708
1956
|
}
|
|
1709
|
-
yield*
|
|
1957
|
+
yield* Effect9.logDebug("Processed gift wrap", {
|
|
1710
1958
|
collection: collectionName,
|
|
1711
1959
|
recordId,
|
|
1712
1960
|
kind,
|
|
@@ -1717,33 +1965,33 @@ function createSyncHandle(storage, giftWrapHandle, relay, publishQueue, syncStat
|
|
|
1717
1965
|
}
|
|
1718
1966
|
return collectionName;
|
|
1719
1967
|
});
|
|
1720
|
-
const reconcileRelay = (url, pubKeys) =>
|
|
1721
|
-
yield*
|
|
1968
|
+
const reconcileRelay = (url, pubKeys) => Effect9.gen(function* () {
|
|
1969
|
+
yield* Effect9.logDebug("Syncing relay", { relay: url });
|
|
1722
1970
|
const { haveIds, needIds } = yield* reconcileWithRelay(
|
|
1723
1971
|
storage,
|
|
1724
1972
|
relay,
|
|
1725
1973
|
url,
|
|
1726
1974
|
Array.from(pubKeys)
|
|
1727
1975
|
).pipe(
|
|
1728
|
-
|
|
1729
|
-
|
|
1976
|
+
Effect9.tapError((err) => Effect9.sync(() => onSyncError?.(err))),
|
|
1977
|
+
Effect9.orElseSucceed(() => ({ haveIds: [], needIds: [] }))
|
|
1730
1978
|
);
|
|
1731
|
-
yield*
|
|
1979
|
+
yield* Effect9.logDebug("Relay reconciliation result", {
|
|
1732
1980
|
relay: url,
|
|
1733
1981
|
need: needIds.length,
|
|
1734
1982
|
have: haveIds.length
|
|
1735
1983
|
});
|
|
1736
1984
|
const events = needIds.length > 0 ? yield* relay.fetchEvents(needIds, url).pipe(
|
|
1737
|
-
|
|
1738
|
-
|
|
1985
|
+
Effect9.tapError((err) => Effect9.sync(() => onSyncError?.(err))),
|
|
1986
|
+
Effect9.orElseSucceed(() => [])
|
|
1739
1987
|
) : [];
|
|
1740
1988
|
return {
|
|
1741
1989
|
events,
|
|
1742
1990
|
haveIds: haveIds.map((id) => ({ id, url }))
|
|
1743
1991
|
};
|
|
1744
|
-
}).pipe(
|
|
1745
|
-
const syncAllRelays = (pubKeys, changedCollections) =>
|
|
1746
|
-
const results = yield*
|
|
1992
|
+
}).pipe(Effect9.withLogSpan("tablinum.reconcileRelay"));
|
|
1993
|
+
const syncAllRelays = (pubKeys, changedCollections) => Effect9.gen(function* () {
|
|
1994
|
+
const results = yield* Effect9.forEach(
|
|
1747
1995
|
relayUrls,
|
|
1748
1996
|
(url) => reconcileRelay(url, pubKeys),
|
|
1749
1997
|
{ concurrency: "unbounded" }
|
|
@@ -1760,44 +2008,44 @@ function createSyncHandle(storage, giftWrapHandle, relay, publishQueue, syncStat
|
|
|
1760
2008
|
}
|
|
1761
2009
|
const unwrapped = [];
|
|
1762
2010
|
for (const gw of allGiftWraps) {
|
|
1763
|
-
const result = yield* unwrapGiftWrap(gw).pipe(
|
|
2011
|
+
const result = yield* unwrapGiftWrap(gw).pipe(Effect9.orElseSucceed(() => null));
|
|
1764
2012
|
if (result) unwrapped.push(result);
|
|
1765
2013
|
}
|
|
1766
2014
|
unwrapped.sort((a, b) => a.rumor.created_at - b.rumor.created_at || (a.rumor.id < b.rumor.id ? -1 : 1));
|
|
1767
2015
|
for (const event of unwrapped) {
|
|
1768
2016
|
const collection2 = yield* applyUnwrappedEvent(event).pipe(
|
|
1769
|
-
|
|
2017
|
+
Effect9.orElseSucceed(() => null)
|
|
1770
2018
|
);
|
|
1771
2019
|
if (collection2) changedCollections.add(collection2);
|
|
1772
2020
|
}
|
|
1773
2021
|
const allHaveIds = results.flatMap((r) => r.haveIds);
|
|
1774
|
-
yield*
|
|
2022
|
+
yield* Effect9.forEach(
|
|
1775
2023
|
allHaveIds,
|
|
1776
|
-
({ id, url }) =>
|
|
2024
|
+
({ id, url }) => Effect9.gen(function* () {
|
|
1777
2025
|
const gw = yield* storage.getGiftWrap(id);
|
|
1778
2026
|
if (!gw?.event) return;
|
|
1779
2027
|
yield* relay.publish(gw.event, [url]).pipe(
|
|
1780
|
-
|
|
1781
|
-
|
|
2028
|
+
Effect9.tapError((err) => Effect9.sync(() => onSyncError?.(err))),
|
|
2029
|
+
Effect9.ignore
|
|
1782
2030
|
);
|
|
1783
2031
|
}),
|
|
1784
2032
|
{ concurrency: "unbounded", discard: true }
|
|
1785
2033
|
);
|
|
1786
|
-
}).pipe(
|
|
1787
|
-
const processGiftWrap = (remoteGw) =>
|
|
2034
|
+
}).pipe(Effect9.withLogSpan("tablinum.syncAllRelays"));
|
|
2035
|
+
const processGiftWrap = (remoteGw) => Effect9.gen(function* () {
|
|
1788
2036
|
const uw = yield* unwrapGiftWrap(remoteGw);
|
|
1789
2037
|
if (!uw) return null;
|
|
1790
2038
|
return yield* applyUnwrappedEvent(uw);
|
|
1791
2039
|
});
|
|
1792
|
-
const processRealtimeGiftWrap = (remoteGw) =>
|
|
1793
|
-
const collection2 = yield* processGiftWrap(remoteGw).pipe(
|
|
2040
|
+
const processRealtimeGiftWrap = (remoteGw) => Effect9.gen(function* () {
|
|
2041
|
+
const collection2 = yield* processGiftWrap(remoteGw).pipe(Effect9.orElseSucceed(() => null));
|
|
1794
2042
|
if (collection2) {
|
|
1795
2043
|
yield* notifyCollectionUpdated(collection2);
|
|
1796
2044
|
}
|
|
1797
2045
|
});
|
|
1798
|
-
const processRotationGiftWrap = (remoteGw) =>
|
|
1799
|
-
const unwrapResult = yield*
|
|
1800
|
-
|
|
2046
|
+
const processRotationGiftWrap = (remoteGw) => Effect9.gen(function* () {
|
|
2047
|
+
const unwrapResult = yield* Effect9.result(
|
|
2048
|
+
Effect9.try({
|
|
1801
2049
|
try: () => unwrapEvent(remoteGw, personalPrivateKey),
|
|
1802
2050
|
catch: (e) => new CryptoError({
|
|
1803
2051
|
message: `Rotation unwrap failed: ${e instanceof Error ? e.message : String(e)}`,
|
|
@@ -1847,73 +2095,73 @@ function createSyncHandle(storage, giftWrapHandle, relay, publishQueue, syncStat
|
|
|
1847
2095
|
yield* handle.addEpochSubscription(epoch.publicKey);
|
|
1848
2096
|
return true;
|
|
1849
2097
|
});
|
|
1850
|
-
const subscribeAcrossRelays = (filter, onEvent) =>
|
|
2098
|
+
const subscribeAcrossRelays = (filter, onEvent) => Effect9.forEach(
|
|
1851
2099
|
relayUrls,
|
|
1852
|
-
(url) =>
|
|
2100
|
+
(url) => Effect9.gen(function* () {
|
|
1853
2101
|
yield* relay.subscribe(filter, url, (event) => {
|
|
1854
2102
|
forkHandled(onEvent(event));
|
|
1855
2103
|
}).pipe(
|
|
1856
|
-
|
|
1857
|
-
|
|
2104
|
+
Effect9.tapError((err) => Effect9.sync(() => onSyncError?.(err))),
|
|
2105
|
+
Effect9.ignore
|
|
1858
2106
|
);
|
|
1859
2107
|
}),
|
|
1860
2108
|
{ concurrency: "unbounded", discard: true }
|
|
1861
2109
|
);
|
|
1862
2110
|
let healingActive = false;
|
|
1863
|
-
const healingEffect =
|
|
2111
|
+
const healingEffect = Effect9.gen(function* () {
|
|
1864
2112
|
if (!healingActive) return;
|
|
1865
2113
|
const status = yield* syncStatus.get();
|
|
1866
2114
|
if (status === "syncing") return;
|
|
1867
2115
|
yield* syncStatus.set("syncing");
|
|
1868
|
-
yield*
|
|
2116
|
+
yield* Effect9.gen(function* () {
|
|
1869
2117
|
const pubKeys = getSubscriptionPubKeys();
|
|
1870
2118
|
const changedCollections = /* @__PURE__ */ new Set();
|
|
1871
2119
|
yield* syncAllRelays(pubKeys, changedCollections);
|
|
1872
2120
|
if (changedCollections.size > 0) {
|
|
1873
2121
|
yield* notifyReplayComplete(watchCtx, [...changedCollections]);
|
|
1874
2122
|
}
|
|
1875
|
-
}).pipe(
|
|
1876
|
-
}).pipe(
|
|
2123
|
+
}).pipe(Effect9.ensuring(syncStatus.set("idle")));
|
|
2124
|
+
}).pipe(Effect9.ignore);
|
|
1877
2125
|
const handle = {
|
|
1878
|
-
sync: () =>
|
|
1879
|
-
yield*
|
|
2126
|
+
sync: () => Effect9.gen(function* () {
|
|
2127
|
+
yield* Effect9.logInfo("Sync started");
|
|
1880
2128
|
yield* syncStatus.set("syncing");
|
|
1881
2129
|
yield* Ref3.set(watchCtx.replayingRef, true);
|
|
1882
2130
|
const changedCollections = /* @__PURE__ */ new Set();
|
|
1883
|
-
yield*
|
|
2131
|
+
yield* Effect9.gen(function* () {
|
|
1884
2132
|
const pubKeys = getSubscriptionPubKeys();
|
|
1885
2133
|
yield* syncAllRelays(pubKeys, changedCollections);
|
|
1886
|
-
yield* publishQueue.flush(relayUrls).pipe(
|
|
2134
|
+
yield* publishQueue.flush(relayUrls).pipe(Effect9.ignore);
|
|
1887
2135
|
}).pipe(
|
|
1888
|
-
|
|
1889
|
-
|
|
2136
|
+
Effect9.ensuring(
|
|
2137
|
+
Effect9.gen(function* () {
|
|
1890
2138
|
yield* notifyReplayComplete(watchCtx, [...changedCollections]);
|
|
1891
2139
|
yield* syncStatus.set("idle");
|
|
1892
2140
|
})
|
|
1893
2141
|
)
|
|
1894
2142
|
);
|
|
1895
|
-
yield*
|
|
1896
|
-
}).pipe(
|
|
1897
|
-
publishLocal: (giftWrap) =>
|
|
2143
|
+
yield* Effect9.logInfo("Sync complete", { changed: [...changedCollections] });
|
|
2144
|
+
}).pipe(Effect9.withLogSpan("tablinum.sync")),
|
|
2145
|
+
publishLocal: (giftWrap) => Effect9.gen(function* () {
|
|
1898
2146
|
if (!giftWrap.event) return;
|
|
1899
2147
|
yield* relay.publish(giftWrap.event, relayUrls).pipe(
|
|
1900
|
-
|
|
2148
|
+
Effect9.tapError(
|
|
1901
2149
|
() => storage.putGiftWrap(giftWrap).pipe(
|
|
1902
|
-
|
|
1903
|
-
|
|
2150
|
+
Effect9.andThen(publishQueue.enqueue(giftWrap.id)),
|
|
2151
|
+
Effect9.andThen(Effect9.sync(() => scheduleAutoFlush()))
|
|
1904
2152
|
)
|
|
1905
2153
|
),
|
|
1906
|
-
|
|
1907
|
-
|
|
2154
|
+
Effect9.tapError((err) => Effect9.sync(() => onSyncError?.(err))),
|
|
2155
|
+
Effect9.ignore
|
|
1908
2156
|
);
|
|
1909
2157
|
}),
|
|
1910
|
-
startSubscription: () =>
|
|
2158
|
+
startSubscription: () => Effect9.gen(function* () {
|
|
1911
2159
|
const pubKeys = getSubscriptionPubKeys();
|
|
1912
2160
|
yield* subscribeAcrossRelays({ kinds: [GiftWrap2], "#p": pubKeys }, processRealtimeGiftWrap);
|
|
1913
2161
|
if (!pubKeys.includes(personalPublicKey)) {
|
|
1914
2162
|
yield* subscribeAcrossRelays(
|
|
1915
2163
|
{ kinds: [GiftWrap2], "#p": [personalPublicKey] },
|
|
1916
|
-
(event) =>
|
|
2164
|
+
(event) => Effect9.result(processRotationGiftWrap(event)).pipe(Effect9.asVoid)
|
|
1917
2165
|
);
|
|
1918
2166
|
}
|
|
1919
2167
|
}),
|
|
@@ -1922,10 +2170,10 @@ function createSyncHandle(storage, giftWrapHandle, relay, publishQueue, syncStat
|
|
|
1922
2170
|
if (healingActive) return;
|
|
1923
2171
|
healingActive = true;
|
|
1924
2172
|
forkHandled(
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
2173
|
+
Effect9.sleep(Duration.minutes(5)).pipe(
|
|
2174
|
+
Effect9.andThen(healingEffect),
|
|
2175
|
+
Effect9.repeat(Schedule.spaced(Duration.minutes(5))),
|
|
2176
|
+
Effect9.ensuring(Effect9.sync(() => {
|
|
1929
2177
|
healingActive = false;
|
|
1930
2178
|
}))
|
|
1931
2179
|
)
|
|
@@ -1937,8 +2185,8 @@ function createSyncHandle(storage, giftWrapHandle, relay, publishQueue, syncStat
|
|
|
1937
2185
|
};
|
|
1938
2186
|
forkHandled(
|
|
1939
2187
|
publishQueue.size().pipe(
|
|
1940
|
-
|
|
1941
|
-
(size) =>
|
|
2188
|
+
Effect9.flatMap(
|
|
2189
|
+
(size) => Effect9.sync(() => {
|
|
1942
2190
|
if (size > 0) scheduleAutoFlush();
|
|
1943
2191
|
})
|
|
1944
2192
|
)
|
|
@@ -1948,7 +2196,7 @@ function createSyncHandle(storage, giftWrapHandle, relay, publishQueue, syncStat
|
|
|
1948
2196
|
}
|
|
1949
2197
|
|
|
1950
2198
|
// src/db/members.ts
|
|
1951
|
-
import { Effect as
|
|
2199
|
+
import { Effect as Effect10, Option as Option6, Schema as Schema6 } from "effect";
|
|
1952
2200
|
var optionalString = {
|
|
1953
2201
|
_tag: "FieldDef",
|
|
1954
2202
|
kind: "string",
|
|
@@ -1997,15 +2245,15 @@ var AuthorProfileSchema = Schema6.Struct({
|
|
|
1997
2245
|
});
|
|
1998
2246
|
var decodeAuthorProfile = Schema6.decodeUnknownEffect(Schema6.fromJsonString(AuthorProfileSchema));
|
|
1999
2247
|
function fetchAuthorProfile(relay, relayUrls, pubkey) {
|
|
2000
|
-
return
|
|
2248
|
+
return Effect10.gen(function* () {
|
|
2001
2249
|
for (const url of relayUrls) {
|
|
2002
|
-
const result = yield*
|
|
2250
|
+
const result = yield* Effect10.result(
|
|
2003
2251
|
relay.fetchByFilter({ kinds: [0], authors: [pubkey], limit: 1 }, url)
|
|
2004
2252
|
);
|
|
2005
2253
|
if (result._tag === "Success" && result.success.length > 0) {
|
|
2006
2254
|
return yield* decodeAuthorProfile(result.success[0].content).pipe(
|
|
2007
|
-
|
|
2008
|
-
|
|
2255
|
+
Effect10.map(Option6.some),
|
|
2256
|
+
Effect10.orElseSucceed(() => Option6.none())
|
|
2009
2257
|
);
|
|
2010
2258
|
}
|
|
2011
2259
|
}
|
|
@@ -2055,15 +2303,15 @@ var SyncStatus = class extends ServiceMap9.Service()(
|
|
|
2055
2303
|
};
|
|
2056
2304
|
|
|
2057
2305
|
// src/layers/IdentityLive.ts
|
|
2058
|
-
import { Effect as
|
|
2059
|
-
import { hexToBytes as
|
|
2306
|
+
import { Effect as Effect12, Layer as Layer2 } from "effect";
|
|
2307
|
+
import { hexToBytes as hexToBytes4 } from "@noble/hashes/utils.js";
|
|
2060
2308
|
|
|
2061
2309
|
// src/db/identity.ts
|
|
2062
|
-
import { Effect as
|
|
2310
|
+
import { Effect as Effect11 } from "effect";
|
|
2063
2311
|
import { getPublicKey as getPublicKey2 } from "nostr-tools/pure";
|
|
2064
|
-
import { bytesToHex as
|
|
2312
|
+
import { bytesToHex as bytesToHex3 } from "@noble/hashes/utils.js";
|
|
2065
2313
|
function createIdentity(suppliedKey) {
|
|
2066
|
-
return
|
|
2314
|
+
return Effect11.gen(function* () {
|
|
2067
2315
|
let privateKey;
|
|
2068
2316
|
if (suppliedKey) {
|
|
2069
2317
|
if (suppliedKey.length !== 32) {
|
|
@@ -2076,8 +2324,8 @@ function createIdentity(suppliedKey) {
|
|
|
2076
2324
|
privateKey = new Uint8Array(32);
|
|
2077
2325
|
crypto.getRandomValues(privateKey);
|
|
2078
2326
|
}
|
|
2079
|
-
const privateKeyHex =
|
|
2080
|
-
const publicKey = yield*
|
|
2327
|
+
const privateKeyHex = bytesToHex3(privateKey);
|
|
2328
|
+
const publicKey = yield* Effect11.try({
|
|
2081
2329
|
try: () => getPublicKey2(privateKey),
|
|
2082
2330
|
catch: (e) => new CryptoError({
|
|
2083
2331
|
message: `Failed to derive public key: ${e instanceof Error ? e.message : String(e)}`,
|
|
@@ -2095,14 +2343,14 @@ function createIdentity(suppliedKey) {
|
|
|
2095
2343
|
// src/layers/IdentityLive.ts
|
|
2096
2344
|
var IdentityLive = Layer2.effect(
|
|
2097
2345
|
Identity,
|
|
2098
|
-
|
|
2346
|
+
Effect12.gen(function* () {
|
|
2099
2347
|
const config = yield* Config;
|
|
2100
2348
|
const storage = yield* Storage;
|
|
2101
2349
|
const idbKey = yield* storage.getMeta("identity_key");
|
|
2102
|
-
const resolvedKey = config.privateKey ?? (typeof idbKey === "string" && idbKey.length === 64 ?
|
|
2350
|
+
const resolvedKey = config.privateKey ?? (typeof idbKey === "string" && idbKey.length === 64 ? hexToBytes4(idbKey) : void 0);
|
|
2103
2351
|
const identity = yield* createIdentity(resolvedKey);
|
|
2104
2352
|
yield* storage.putMeta("identity_key", identity.exportKey());
|
|
2105
|
-
yield*
|
|
2353
|
+
yield* Effect12.logInfo("Identity loaded", {
|
|
2106
2354
|
publicKey: identity.publicKey.slice(0, 12) + "...",
|
|
2107
2355
|
source: config.privateKey ? "config" : resolvedKey ? "storage" : "generated"
|
|
2108
2356
|
});
|
|
@@ -2111,12 +2359,12 @@ var IdentityLive = Layer2.effect(
|
|
|
2111
2359
|
);
|
|
2112
2360
|
|
|
2113
2361
|
// src/layers/EpochStoreLive.ts
|
|
2114
|
-
import { Effect as
|
|
2362
|
+
import { Effect as Effect13, Layer as Layer3, Option as Option7 } from "effect";
|
|
2115
2363
|
import { generateSecretKey as generateSecretKey2 } from "nostr-tools/pure";
|
|
2116
|
-
import { bytesToHex as
|
|
2364
|
+
import { bytesToHex as bytesToHex4 } from "@noble/hashes/utils.js";
|
|
2117
2365
|
var EpochStoreLive = Layer3.effect(
|
|
2118
2366
|
EpochStore,
|
|
2119
|
-
|
|
2367
|
+
Effect13.gen(function* () {
|
|
2120
2368
|
const config = yield* Config;
|
|
2121
2369
|
const identity = yield* Identity;
|
|
2122
2370
|
const storage = yield* Storage;
|
|
@@ -2135,7 +2383,7 @@ var EpochStoreLive = Layer3.effect(
|
|
|
2135
2383
|
return existing !== void 0 && existing.privateKey === ek.key;
|
|
2136
2384
|
});
|
|
2137
2385
|
if (configIsSubset) {
|
|
2138
|
-
yield*
|
|
2386
|
+
yield* Effect13.logInfo("Epoch store loaded", {
|
|
2139
2387
|
source: "storage",
|
|
2140
2388
|
epochs: idbStore.epochs.size
|
|
2141
2389
|
});
|
|
@@ -2144,271 +2392,28 @@ var EpochStoreLive = Layer3.effect(
|
|
|
2144
2392
|
}
|
|
2145
2393
|
const store2 = createEpochStoreFromInputs(config.epochKeys);
|
|
2146
2394
|
yield* storage.putMeta("epochs", stringifyEpochStore(store2));
|
|
2147
|
-
yield*
|
|
2395
|
+
yield* Effect13.logInfo("Epoch store loaded", { source: "config", epochs: store2.epochs.size });
|
|
2148
2396
|
return store2;
|
|
2149
2397
|
}
|
|
2150
2398
|
if (idbStore) {
|
|
2151
|
-
yield*
|
|
2399
|
+
yield* Effect13.logInfo("Epoch store loaded", {
|
|
2152
2400
|
source: "storage",
|
|
2153
2401
|
epochs: idbStore.epochs.size
|
|
2154
2402
|
});
|
|
2155
2403
|
return idbStore;
|
|
2156
2404
|
}
|
|
2157
2405
|
const store = createEpochStoreFromInputs(
|
|
2158
|
-
[{ epochId: EpochId("epoch-0"), key:
|
|
2406
|
+
[{ epochId: EpochId("epoch-0"), key: bytesToHex4(generateSecretKey2()) }],
|
|
2159
2407
|
{ createdBy: identity.publicKey }
|
|
2160
2408
|
);
|
|
2161
2409
|
yield* storage.putMeta("epochs", stringifyEpochStore(store));
|
|
2162
|
-
yield*
|
|
2410
|
+
yield* Effect13.logInfo("Epoch store loaded", { source: "generated", epochs: store.epochs.size });
|
|
2163
2411
|
return store;
|
|
2164
2412
|
})
|
|
2165
2413
|
);
|
|
2166
2414
|
|
|
2167
2415
|
// src/layers/StorageLive.ts
|
|
2168
2416
|
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
2417
|
var StorageLive = Layer4.effect(
|
|
2413
2418
|
Storage,
|
|
2414
2419
|
Effect14.gen(function* () {
|
|
@@ -3087,6 +3092,60 @@ var TablinumLive = Layer9.effect(
|
|
|
3087
3092
|
yield* Scope4.close(scope, Exit.void);
|
|
3088
3093
|
})
|
|
3089
3094
|
),
|
|
3095
|
+
destroy: () => withLog(
|
|
3096
|
+
Effect21.gen(function* () {
|
|
3097
|
+
if (!(yield* Ref5.get(closedRef))) {
|
|
3098
|
+
yield* Ref5.set(closedRef, true);
|
|
3099
|
+
syncHandle.stopHealing();
|
|
3100
|
+
yield* Scope4.close(scope, Exit.void);
|
|
3101
|
+
}
|
|
3102
|
+
yield* deleteIDBStorage(config.dbName);
|
|
3103
|
+
})
|
|
3104
|
+
),
|
|
3105
|
+
leave: () => withLog(
|
|
3106
|
+
Effect21.gen(function* () {
|
|
3107
|
+
if (yield* Ref5.get(closedRef)) {
|
|
3108
|
+
return yield* new SyncError({ message: "Database is closed", phase: "leave" });
|
|
3109
|
+
}
|
|
3110
|
+
const allMembers = yield* storage.getAllRecords("_members");
|
|
3111
|
+
const activeMembers = allMembers.filter(
|
|
3112
|
+
(member) => !member.removedAt && member.id !== identity.publicKey
|
|
3113
|
+
);
|
|
3114
|
+
const activePubkeys = activeMembers.map((member) => member.id);
|
|
3115
|
+
const result = createRotation(
|
|
3116
|
+
epochStore,
|
|
3117
|
+
identity.privateKey,
|
|
3118
|
+
identity.publicKey,
|
|
3119
|
+
activePubkeys,
|
|
3120
|
+
[identity.publicKey]
|
|
3121
|
+
);
|
|
3122
|
+
addEpoch(epochStore, result.epoch);
|
|
3123
|
+
epochStore.currentEpochId = result.epoch.id;
|
|
3124
|
+
yield* storage.putMeta("epochs", stringifyEpochStore(epochStore));
|
|
3125
|
+
const memberRecord = yield* storage.getRecord("_members", identity.publicKey);
|
|
3126
|
+
yield* putMemberRecord({
|
|
3127
|
+
...memberRecord ?? {
|
|
3128
|
+
id: identity.publicKey,
|
|
3129
|
+
addedAt: 0,
|
|
3130
|
+
addedInEpoch: EpochId("epoch-0")
|
|
3131
|
+
},
|
|
3132
|
+
removedAt: Date.now(),
|
|
3133
|
+
removedInEpoch: result.epoch.id
|
|
3134
|
+
});
|
|
3135
|
+
yield* Effect21.forEach(
|
|
3136
|
+
result.wrappedEvents,
|
|
3137
|
+
(wrappedEvent) => relay.publish(wrappedEvent, [...config.relays]).pipe(
|
|
3138
|
+
Effect21.tapError((e) => Effect21.sync(() => reportSyncError(config.onSyncError, e))),
|
|
3139
|
+
Effect21.ignore
|
|
3140
|
+
),
|
|
3141
|
+
{ discard: true }
|
|
3142
|
+
);
|
|
3143
|
+
yield* Ref5.set(closedRef, true);
|
|
3144
|
+
syncHandle.stopHealing();
|
|
3145
|
+
yield* Scope4.close(scope, Exit.void);
|
|
3146
|
+
yield* deleteIDBStorage(config.dbName);
|
|
3147
|
+
})
|
|
3148
|
+
),
|
|
3090
3149
|
rebuild: () => ensureOpen(
|
|
3091
3150
|
rebuild(
|
|
3092
3151
|
storage,
|
|
@@ -3224,6 +3283,9 @@ function validateConfig(config) {
|
|
|
3224
3283
|
}
|
|
3225
3284
|
});
|
|
3226
3285
|
}
|
|
3286
|
+
function deleteDatabase(dbName) {
|
|
3287
|
+
return deleteIDBStorage(DatabaseName(dbName ?? "tablinum"));
|
|
3288
|
+
}
|
|
3227
3289
|
function createTablinum(config) {
|
|
3228
3290
|
return Effect22.gen(function* () {
|
|
3229
3291
|
yield* validateConfig(config);
|
|
@@ -3245,6 +3307,9 @@ function createTablinum(config) {
|
|
|
3245
3307
|
});
|
|
3246
3308
|
}
|
|
3247
3309
|
|
|
3310
|
+
// src/svelte/tablinum.svelte.ts
|
|
3311
|
+
import { Effect as Effect25, Exit as Exit2, References as References6, Scope as Scope6 } from "effect";
|
|
3312
|
+
|
|
3248
3313
|
// src/svelte/collection.svelte.ts
|
|
3249
3314
|
import { Effect as Effect24, Fiber, Option as Option11, References as References5, Stream as Stream4 } from "effect";
|
|
3250
3315
|
|
|
@@ -3479,9 +3544,11 @@ var Tablinum2 = class {
|
|
|
3479
3544
|
#closed = false;
|
|
3480
3545
|
#readyState = createDeferred();
|
|
3481
3546
|
#logLevel;
|
|
3547
|
+
#dbName;
|
|
3482
3548
|
constructor(config) {
|
|
3483
3549
|
this.ready = this.#readyState.promise;
|
|
3484
3550
|
this.#logLevel = resolveLogLevel(config.logLevel);
|
|
3551
|
+
this.#dbName = config.dbName ?? "tablinum";
|
|
3485
3552
|
this.#init(config);
|
|
3486
3553
|
}
|
|
3487
3554
|
#settleReady(err) {
|
|
@@ -3616,6 +3683,14 @@ var Tablinum2 = class {
|
|
|
3616
3683
|
}
|
|
3617
3684
|
this.status = "closed";
|
|
3618
3685
|
};
|
|
3686
|
+
destroy = async () => {
|
|
3687
|
+
await this.close();
|
|
3688
|
+
await Effect25.runPromise(deleteDatabase(this.#dbName));
|
|
3689
|
+
};
|
|
3690
|
+
leave = async () => {
|
|
3691
|
+
await this.#runHandleEffect((handle) => handle.leave());
|
|
3692
|
+
await Effect25.runPromise(deleteDatabase(this.#dbName));
|
|
3693
|
+
};
|
|
3619
3694
|
sync = async () => this.#runHandleEffect((handle) => handle.sync());
|
|
3620
3695
|
rebuild = async () => this.#runHandleEffect((handle) => handle.rebuild());
|
|
3621
3696
|
addMember = async (pubkey) => this.#runHandleEffect((handle) => handle.addMember(pubkey));
|
|
@@ -3636,6 +3711,7 @@ export {
|
|
|
3636
3711
|
ValidationError,
|
|
3637
3712
|
collection,
|
|
3638
3713
|
decodeInvite,
|
|
3714
|
+
deleteDatabase,
|
|
3639
3715
|
encodeInvite,
|
|
3640
3716
|
field
|
|
3641
3717
|
};
|