tablinum 0.6.3 → 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 +453 -386
- package/dist/storage/idb.d.ts +1 -0
- package/dist/svelte/index.svelte.d.ts +1 -0
- package/dist/svelte/index.svelte.js +466 -389
- 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) {
|
|
@@ -438,7 +686,8 @@ function fieldDefToSchema(fd) {
|
|
|
438
686
|
case "object": {
|
|
439
687
|
const nested = {};
|
|
440
688
|
for (const [k, v] of Object.entries(fd.fields)) {
|
|
441
|
-
|
|
689
|
+
const fieldSchema = fieldDefToSchema(v);
|
|
690
|
+
nested[k] = v.isOptional ? Schema4.optionalKey(fieldSchema) : fieldSchema;
|
|
442
691
|
}
|
|
443
692
|
base = Schema4.Struct(nested);
|
|
444
693
|
break;
|
|
@@ -466,10 +715,10 @@ function buildStructSchema(def, options = {}) {
|
|
|
466
715
|
function buildValidator(collectionName, def) {
|
|
467
716
|
const decode = Schema4.decodeUnknownEffect(buildStructSchema(def, { includeId: true }));
|
|
468
717
|
return (input) => decode(input).pipe(
|
|
469
|
-
|
|
718
|
+
Effect5.map(
|
|
470
719
|
(result) => result
|
|
471
720
|
),
|
|
472
|
-
|
|
721
|
+
Effect5.mapError(
|
|
473
722
|
(e) => new ValidationError({
|
|
474
723
|
message: `Validation failed for collection "${collectionName}": ${e.message}`
|
|
475
724
|
})
|
|
@@ -478,7 +727,7 @@ function buildValidator(collectionName, def) {
|
|
|
478
727
|
}
|
|
479
728
|
function buildPartialValidator(collectionName, def) {
|
|
480
729
|
const decode = Schema4.decodeUnknownEffect(buildStructSchema(def, { allOptional: true }));
|
|
481
|
-
return (input) =>
|
|
730
|
+
return (input) => Effect5.gen(function* () {
|
|
482
731
|
if (typeof input !== "object" || input === null) {
|
|
483
732
|
return yield* new ValidationError({
|
|
484
733
|
message: `Validation failed for collection "${collectionName}": expected an object`
|
|
@@ -493,8 +742,8 @@ function buildPartialValidator(collectionName, def) {
|
|
|
493
742
|
});
|
|
494
743
|
}
|
|
495
744
|
return yield* decode(record).pipe(
|
|
496
|
-
|
|
497
|
-
|
|
745
|
+
Effect5.map((result) => result),
|
|
746
|
+
Effect5.mapError(
|
|
498
747
|
(e) => new ValidationError({
|
|
499
748
|
message: `Validation failed for collection "${collectionName}": ${e.message}`
|
|
500
749
|
})
|
|
@@ -504,7 +753,7 @@ function buildPartialValidator(collectionName, def) {
|
|
|
504
753
|
}
|
|
505
754
|
|
|
506
755
|
// src/crud/collection-handle.ts
|
|
507
|
-
import { Effect as
|
|
756
|
+
import { Effect as Effect7, Option as Option3, References } from "effect";
|
|
508
757
|
|
|
509
758
|
// src/utils/uuid.ts
|
|
510
759
|
var alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
|
|
@@ -537,12 +786,12 @@ function uuidv7() {
|
|
|
537
786
|
}
|
|
538
787
|
|
|
539
788
|
// src/crud/query-builder.ts
|
|
540
|
-
import { Effect as
|
|
789
|
+
import { Effect as Effect6, Option as Option2, Ref as Ref2, Stream as Stream2 } from "effect";
|
|
541
790
|
function emptyPlan() {
|
|
542
791
|
return { filters: [] };
|
|
543
792
|
}
|
|
544
793
|
function executeQuery(ctx, plan) {
|
|
545
|
-
return
|
|
794
|
+
return Effect6.gen(function* () {
|
|
546
795
|
if (plan.fieldName) {
|
|
547
796
|
const fieldDef = ctx.def.fields[plan.fieldName];
|
|
548
797
|
if (!fieldDef) {
|
|
@@ -618,7 +867,7 @@ function watchQuery(ctx, plan) {
|
|
|
618
867
|
const changes = Stream2.fromPubSub(ctx.watchCtx.pubsub).pipe(
|
|
619
868
|
Stream2.filter((event) => event.collection === ctx.collectionName),
|
|
620
869
|
Stream2.mapEffect(
|
|
621
|
-
() =>
|
|
870
|
+
() => Effect6.gen(function* () {
|
|
622
871
|
const replaying = yield* Ref2.get(ctx.watchCtx.replayingRef);
|
|
623
872
|
if (replaying) return void 0;
|
|
624
873
|
return yield* query();
|
|
@@ -627,7 +876,7 @@ function watchQuery(ctx, plan) {
|
|
|
627
876
|
Stream2.filter((result) => result !== void 0)
|
|
628
877
|
);
|
|
629
878
|
return Stream2.unwrap(
|
|
630
|
-
|
|
879
|
+
Effect6.gen(function* () {
|
|
631
880
|
const initial = yield* query();
|
|
632
881
|
return Stream2.concat(Stream2.make(initial), changes);
|
|
633
882
|
})
|
|
@@ -653,11 +902,11 @@ function makeQueryBuilder(ctx, plan) {
|
|
|
653
902
|
offset: (n) => makeQueryBuilder(ctx, { ...plan, offset: n }),
|
|
654
903
|
limit: (n) => makeQueryBuilder(ctx, { ...plan, limit: n }),
|
|
655
904
|
get: () => executeQuery(ctx, plan),
|
|
656
|
-
first: () =>
|
|
905
|
+
first: () => Effect6.map(
|
|
657
906
|
executeQuery(ctx, { ...plan, limit: 1 }),
|
|
658
907
|
(results) => results.length > 0 ? Option2.some(results[0]) : Option2.none()
|
|
659
908
|
),
|
|
660
|
-
count: () =>
|
|
909
|
+
count: () => Effect6.map(executeQuery(ctx, plan), (results) => results.length),
|
|
661
910
|
watch: () => watchQuery(ctx, plan)
|
|
662
911
|
};
|
|
663
912
|
}
|
|
@@ -763,7 +1012,7 @@ function replayState(recordId, events, stopAtId) {
|
|
|
763
1012
|
return state;
|
|
764
1013
|
}
|
|
765
1014
|
function promoteToSnapshot(storage, collection2, recordId, target, allSorted) {
|
|
766
|
-
return
|
|
1015
|
+
return Effect7.gen(function* () {
|
|
767
1016
|
const chronological = sortChronologically(allSorted);
|
|
768
1017
|
const state = replayState(recordId, chronological, target.id);
|
|
769
1018
|
if (state) {
|
|
@@ -772,7 +1021,7 @@ function promoteToSnapshot(storage, collection2, recordId, target, allSorted) {
|
|
|
772
1021
|
});
|
|
773
1022
|
}
|
|
774
1023
|
function pruneEvents(storage, collection2, recordId, retention) {
|
|
775
|
-
return
|
|
1024
|
+
return Effect7.gen(function* () {
|
|
776
1025
|
const events = yield* storage.getEventsByRecord(collection2, recordId);
|
|
777
1026
|
if (events.length <= retention) return;
|
|
778
1027
|
const sorted = [...events].sort((a, b) => b.createdAt - a.createdAt || (a.id < b.id ? 1 : -1));
|
|
@@ -793,8 +1042,8 @@ function mapRecord(record) {
|
|
|
793
1042
|
}
|
|
794
1043
|
function createCollectionHandle(def, storage, watchCtx, validator, partialValidator, makeEventId, localAuthor, onWrite, logLevel = "None") {
|
|
795
1044
|
const collectionName = def.name;
|
|
796
|
-
const withLog = (effect) =>
|
|
797
|
-
const commitEvent = (event) =>
|
|
1045
|
+
const withLog = (effect) => Effect7.provideService(effect, References.MinimumLogLevel, logLevel);
|
|
1046
|
+
const commitEvent = (event) => Effect7.gen(function* () {
|
|
798
1047
|
yield* storage.putEvent(event);
|
|
799
1048
|
yield* applyEvent(storage, event);
|
|
800
1049
|
if (onWrite) yield* onWrite(event);
|
|
@@ -806,7 +1055,7 @@ function createCollectionHandle(def, storage, watchCtx, validator, partialValida
|
|
|
806
1055
|
});
|
|
807
1056
|
const handle = {
|
|
808
1057
|
add: (data) => withLog(
|
|
809
|
-
|
|
1058
|
+
Effect7.gen(function* () {
|
|
810
1059
|
const id = uuidv7();
|
|
811
1060
|
const fullRecord = { id, ...data };
|
|
812
1061
|
yield* validator(fullRecord);
|
|
@@ -820,7 +1069,7 @@ function createCollectionHandle(def, storage, watchCtx, validator, partialValida
|
|
|
820
1069
|
author: localAuthor
|
|
821
1070
|
};
|
|
822
1071
|
yield* commitEvent(event);
|
|
823
|
-
yield*
|
|
1072
|
+
yield* Effect7.logDebug("Record added", {
|
|
824
1073
|
collection: collectionName,
|
|
825
1074
|
recordId: id,
|
|
826
1075
|
data: fullRecord
|
|
@@ -829,7 +1078,7 @@ function createCollectionHandle(def, storage, watchCtx, validator, partialValida
|
|
|
829
1078
|
})
|
|
830
1079
|
),
|
|
831
1080
|
update: (id, data) => withLog(
|
|
832
|
-
|
|
1081
|
+
Effect7.gen(function* () {
|
|
833
1082
|
const existing = yield* storage.getRecord(collectionName, id);
|
|
834
1083
|
if (!existing || existing._d) {
|
|
835
1084
|
return yield* new NotFoundError({
|
|
@@ -852,7 +1101,7 @@ function createCollectionHandle(def, storage, watchCtx, validator, partialValida
|
|
|
852
1101
|
author: localAuthor
|
|
853
1102
|
};
|
|
854
1103
|
yield* commitEvent(event);
|
|
855
|
-
yield*
|
|
1104
|
+
yield* Effect7.logDebug("Record updated", {
|
|
856
1105
|
collection: collectionName,
|
|
857
1106
|
recordId: id,
|
|
858
1107
|
data: diff
|
|
@@ -861,7 +1110,7 @@ function createCollectionHandle(def, storage, watchCtx, validator, partialValida
|
|
|
861
1110
|
})
|
|
862
1111
|
),
|
|
863
1112
|
delete: (id) => withLog(
|
|
864
|
-
|
|
1113
|
+
Effect7.gen(function* () {
|
|
865
1114
|
const existing = yield* storage.getRecord(collectionName, id);
|
|
866
1115
|
if (!existing || existing._d) {
|
|
867
1116
|
return yield* new NotFoundError({
|
|
@@ -879,11 +1128,11 @@ function createCollectionHandle(def, storage, watchCtx, validator, partialValida
|
|
|
879
1128
|
author: localAuthor
|
|
880
1129
|
};
|
|
881
1130
|
yield* commitEvent(event);
|
|
882
|
-
yield*
|
|
1131
|
+
yield* Effect7.logDebug("Record deleted", { collection: collectionName, recordId: id });
|
|
883
1132
|
yield* pruneEvents(storage, collectionName, id, def.eventRetention);
|
|
884
1133
|
})
|
|
885
1134
|
),
|
|
886
|
-
undo: (id) =>
|
|
1135
|
+
undo: (id) => Effect7.gen(function* () {
|
|
887
1136
|
const existing = yield* storage.getRecord(collectionName, id);
|
|
888
1137
|
if (!existing) {
|
|
889
1138
|
return yield* new NotFoundError({ collection: collectionName, id });
|
|
@@ -908,7 +1157,7 @@ function createCollectionHandle(def, storage, watchCtx, validator, partialValida
|
|
|
908
1157
|
yield* commitEvent(event);
|
|
909
1158
|
yield* pruneEvents(storage, collectionName, id, def.eventRetention);
|
|
910
1159
|
}),
|
|
911
|
-
get: (id) =>
|
|
1160
|
+
get: (id) => Effect7.gen(function* () {
|
|
912
1161
|
const record = yield* storage.getRecord(collectionName, id);
|
|
913
1162
|
if (!record || record._d) {
|
|
914
1163
|
return yield* new NotFoundError({
|
|
@@ -918,11 +1167,11 @@ function createCollectionHandle(def, storage, watchCtx, validator, partialValida
|
|
|
918
1167
|
}
|
|
919
1168
|
return mapRecord(record);
|
|
920
1169
|
}),
|
|
921
|
-
first: () =>
|
|
1170
|
+
first: () => Effect7.map(storage.getAllRecords(collectionName), (all) => {
|
|
922
1171
|
const found = all.find((r) => !r._d);
|
|
923
1172
|
return found ? Option3.some(mapRecord(found)) : Option3.none();
|
|
924
1173
|
}),
|
|
925
|
-
count: () =>
|
|
1174
|
+
count: () => Effect7.map(storage.getAllRecords(collectionName), (all) => all.filter((r) => !r._d).length),
|
|
926
1175
|
watch: () => watchCollection(watchCtx, storage, collectionName, void 0, mapRecord),
|
|
927
1176
|
where: (fieldName) => createWhereClause(
|
|
928
1177
|
storage,
|
|
@@ -945,12 +1194,12 @@ function createCollectionHandle(def, storage, watchCtx, validator, partialValida
|
|
|
945
1194
|
}
|
|
946
1195
|
|
|
947
1196
|
// src/sync/sync-service.ts
|
|
948
|
-
import { Duration, Effect as
|
|
1197
|
+
import { Duration, Effect as Effect9, Layer, Option as Option5, References as References2, Ref as Ref3, Schedule } from "effect";
|
|
949
1198
|
import { unwrapEvent } from "nostr-tools/nip59";
|
|
950
1199
|
import { GiftWrap as GiftWrap2 } from "nostr-tools/kinds";
|
|
951
1200
|
|
|
952
1201
|
// src/sync/negentropy.ts
|
|
953
|
-
import { Effect as
|
|
1202
|
+
import { Effect as Effect8 } from "effect";
|
|
954
1203
|
|
|
955
1204
|
// src/vendor/negentropy.js
|
|
956
1205
|
var PROTOCOL_VERSION = 97;
|
|
@@ -1437,14 +1686,14 @@ function itemCompare(a, b) {
|
|
|
1437
1686
|
}
|
|
1438
1687
|
|
|
1439
1688
|
// src/sync/negentropy.ts
|
|
1440
|
-
import { hexToBytes as
|
|
1689
|
+
import { hexToBytes as hexToBytes3 } from "@noble/hashes/utils.js";
|
|
1441
1690
|
import { GiftWrap } from "nostr-tools/kinds";
|
|
1442
1691
|
function reconcileWithRelay(storage, relay, relayUrl, publicKeys) {
|
|
1443
|
-
return
|
|
1692
|
+
return Effect8.gen(function* () {
|
|
1444
1693
|
const allGiftWraps = yield* storage.getAllGiftWraps();
|
|
1445
1694
|
const storageVector = new NegentropyStorageVector();
|
|
1446
1695
|
for (const gw of allGiftWraps) {
|
|
1447
|
-
storageVector.insert(gw.createdAt,
|
|
1696
|
+
storageVector.insert(gw.createdAt, hexToBytes3(gw.id));
|
|
1448
1697
|
}
|
|
1449
1698
|
storageVector.seal();
|
|
1450
1699
|
const neg = new Negentropy(storageVector, 0);
|
|
@@ -1455,7 +1704,7 @@ function reconcileWithRelay(storage, relay, relayUrl, publicKeys) {
|
|
|
1455
1704
|
const allHaveIds = [];
|
|
1456
1705
|
const allNeedIds = [];
|
|
1457
1706
|
const subId = `neg-${Date.now()}`;
|
|
1458
|
-
const initialMsg = yield*
|
|
1707
|
+
const initialMsg = yield* Effect8.tryPromise({
|
|
1459
1708
|
try: () => neg.initiate(),
|
|
1460
1709
|
catch: (e) => new SyncError({
|
|
1461
1710
|
message: `Negentropy initiate failed: ${e instanceof Error ? e.message : String(e)}`,
|
|
@@ -1467,7 +1716,7 @@ function reconcileWithRelay(storage, relay, relayUrl, publicKeys) {
|
|
|
1467
1716
|
while (currentMsg !== null) {
|
|
1468
1717
|
const response = yield* relay.sendNegMsg(relayUrl, subId, filter, currentMsg);
|
|
1469
1718
|
if (response.msgHex === null) break;
|
|
1470
|
-
const reconcileResult = yield*
|
|
1719
|
+
const reconcileResult = yield* Effect8.tryPromise({
|
|
1471
1720
|
try: () => neg.reconcile(response.msgHex),
|
|
1472
1721
|
catch: (e) => new SyncError({
|
|
1473
1722
|
message: `Negentropy reconcile failed: ${e instanceof Error ? e.message : String(e)}`,
|
|
@@ -1480,13 +1729,13 @@ function reconcileWithRelay(storage, relay, relayUrl, publicKeys) {
|
|
|
1480
1729
|
for (const id of needIds) allNeedIds.push(id);
|
|
1481
1730
|
currentMsg = nextMsg;
|
|
1482
1731
|
}
|
|
1483
|
-
yield*
|
|
1732
|
+
yield* Effect8.logDebug("Negentropy reconciliation complete", {
|
|
1484
1733
|
relay: relayUrl,
|
|
1485
1734
|
have: allHaveIds.length,
|
|
1486
1735
|
need: allNeedIds.length
|
|
1487
1736
|
});
|
|
1488
1737
|
return { haveIds: allHaveIds, needIds: allNeedIds };
|
|
1489
|
-
}).pipe(
|
|
1738
|
+
}).pipe(Effect8.withLogSpan("tablinum.negentropy"));
|
|
1490
1739
|
}
|
|
1491
1740
|
|
|
1492
1741
|
// src/db/key-rotation.ts
|
|
@@ -1580,52 +1829,52 @@ function createSyncHandle(storage, giftWrapHandle, relay, publishQueue, syncStat
|
|
|
1580
1829
|
kind: "create"
|
|
1581
1830
|
});
|
|
1582
1831
|
const forkHandled = (effect) => {
|
|
1583
|
-
|
|
1832
|
+
Effect9.runFork(
|
|
1584
1833
|
effect.pipe(
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1834
|
+
Effect9.tapError((e) => Effect9.sync(() => onSyncError?.(e))),
|
|
1835
|
+
Effect9.ignore,
|
|
1836
|
+
Effect9.provide(logLayer),
|
|
1837
|
+
Effect9.forkIn(scope)
|
|
1589
1838
|
)
|
|
1590
1839
|
);
|
|
1591
1840
|
};
|
|
1592
1841
|
let autoFlushActive = false;
|
|
1593
|
-
const autoFlushEffect =
|
|
1842
|
+
const autoFlushEffect = Effect9.gen(function* () {
|
|
1594
1843
|
const size = yield* publishQueue.size();
|
|
1595
1844
|
if (size === 0) return;
|
|
1596
1845
|
yield* syncStatus.set("syncing");
|
|
1597
1846
|
yield* publishQueue.flush(relayUrls);
|
|
1598
1847
|
const remaining = yield* publishQueue.size();
|
|
1599
|
-
if (remaining > 0) yield*
|
|
1848
|
+
if (remaining > 0) yield* Effect9.fail("pending");
|
|
1600
1849
|
}).pipe(
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1850
|
+
Effect9.ensuring(syncStatus.set("idle")),
|
|
1851
|
+
Effect9.retry({ schedule: Schedule.exponential(5e3).pipe(Schedule.jittered), times: 10 }),
|
|
1852
|
+
Effect9.ignore
|
|
1604
1853
|
);
|
|
1605
1854
|
const scheduleAutoFlush = () => {
|
|
1606
1855
|
if (autoFlushActive) return;
|
|
1607
1856
|
autoFlushActive = true;
|
|
1608
1857
|
forkHandled(
|
|
1609
1858
|
autoFlushEffect.pipe(
|
|
1610
|
-
|
|
1611
|
-
|
|
1859
|
+
Effect9.ensuring(
|
|
1860
|
+
Effect9.sync(() => {
|
|
1612
1861
|
autoFlushActive = false;
|
|
1613
1862
|
})
|
|
1614
1863
|
)
|
|
1615
1864
|
)
|
|
1616
1865
|
);
|
|
1617
1866
|
};
|
|
1618
|
-
const shouldRejectWrite = (authorPubkey) =>
|
|
1867
|
+
const shouldRejectWrite = (authorPubkey) => Effect9.gen(function* () {
|
|
1619
1868
|
const memberRecord = yield* storage.getRecord("_members", authorPubkey);
|
|
1620
1869
|
if (!memberRecord) return false;
|
|
1621
1870
|
return !!memberRecord.removedAt;
|
|
1622
1871
|
});
|
|
1623
1872
|
const storeGiftWrapShell = (gw) => storage.putGiftWrap({ id: gw.id, event: gw, createdAt: gw.created_at });
|
|
1624
|
-
const unwrapGiftWrap = (remoteGw) =>
|
|
1873
|
+
const unwrapGiftWrap = (remoteGw) => Effect9.gen(function* () {
|
|
1625
1874
|
const existing = yield* storage.getGiftWrap(remoteGw.id);
|
|
1626
1875
|
if (existing) return null;
|
|
1627
1876
|
const rumor = yield* giftWrapHandle.unwrap(remoteGw).pipe(
|
|
1628
|
-
|
|
1877
|
+
Effect9.orElseSucceed(() => null)
|
|
1629
1878
|
);
|
|
1630
1879
|
if (!rumor) {
|
|
1631
1880
|
yield* storeGiftWrapShell(remoteGw);
|
|
@@ -1653,7 +1902,7 @@ function createSyncHandle(storage, giftWrapHandle, relay, publishQueue, syncStat
|
|
|
1653
1902
|
recordId: dTag.substring(colonIdx + 1)
|
|
1654
1903
|
};
|
|
1655
1904
|
});
|
|
1656
|
-
const applyUnwrappedEvent = (uw) =>
|
|
1905
|
+
const applyUnwrappedEvent = (uw) => Effect9.gen(function* () {
|
|
1657
1906
|
const { giftWrap: remoteGw, rumor, collection: collectionName, recordId } = uw;
|
|
1658
1907
|
const retention = knownCollections.get(collectionName);
|
|
1659
1908
|
if (retention === void 0) {
|
|
@@ -1663,17 +1912,17 @@ function createSyncHandle(storage, giftWrapHandle, relay, publishQueue, syncStat
|
|
|
1663
1912
|
if (rumor.pubkey) {
|
|
1664
1913
|
const reject = yield* shouldRejectWrite(rumor.pubkey);
|
|
1665
1914
|
if (reject) {
|
|
1666
|
-
yield*
|
|
1915
|
+
yield* Effect9.logWarning("Rejected write from removed member", {
|
|
1667
1916
|
author: rumor.pubkey.slice(0, 12)
|
|
1668
1917
|
});
|
|
1669
1918
|
yield* storeGiftWrapShell(remoteGw);
|
|
1670
1919
|
return null;
|
|
1671
1920
|
}
|
|
1672
1921
|
}
|
|
1673
|
-
const parsed = yield*
|
|
1922
|
+
const parsed = yield* Effect9.try({
|
|
1674
1923
|
try: () => JSON.parse(rumor.content),
|
|
1675
1924
|
catch: () => void 0
|
|
1676
|
-
}).pipe(
|
|
1925
|
+
}).pipe(Effect9.orElseSucceed(() => void 0));
|
|
1677
1926
|
if (parsed === void 0) {
|
|
1678
1927
|
yield* storeGiftWrapShell(remoteGw);
|
|
1679
1928
|
return null;
|
|
@@ -1705,7 +1954,7 @@ function createSyncHandle(storage, giftWrapHandle, relay, publishQueue, syncStat
|
|
|
1705
1954
|
if (didApply && (kind === "u" || kind === "d")) {
|
|
1706
1955
|
yield* pruneEvents(storage, collectionName, recordId, retention);
|
|
1707
1956
|
}
|
|
1708
|
-
yield*
|
|
1957
|
+
yield* Effect9.logDebug("Processed gift wrap", {
|
|
1709
1958
|
collection: collectionName,
|
|
1710
1959
|
recordId,
|
|
1711
1960
|
kind,
|
|
@@ -1716,33 +1965,33 @@ function createSyncHandle(storage, giftWrapHandle, relay, publishQueue, syncStat
|
|
|
1716
1965
|
}
|
|
1717
1966
|
return collectionName;
|
|
1718
1967
|
});
|
|
1719
|
-
const reconcileRelay = (url, pubKeys) =>
|
|
1720
|
-
yield*
|
|
1968
|
+
const reconcileRelay = (url, pubKeys) => Effect9.gen(function* () {
|
|
1969
|
+
yield* Effect9.logDebug("Syncing relay", { relay: url });
|
|
1721
1970
|
const { haveIds, needIds } = yield* reconcileWithRelay(
|
|
1722
1971
|
storage,
|
|
1723
1972
|
relay,
|
|
1724
1973
|
url,
|
|
1725
1974
|
Array.from(pubKeys)
|
|
1726
1975
|
).pipe(
|
|
1727
|
-
|
|
1728
|
-
|
|
1976
|
+
Effect9.tapError((err) => Effect9.sync(() => onSyncError?.(err))),
|
|
1977
|
+
Effect9.orElseSucceed(() => ({ haveIds: [], needIds: [] }))
|
|
1729
1978
|
);
|
|
1730
|
-
yield*
|
|
1979
|
+
yield* Effect9.logDebug("Relay reconciliation result", {
|
|
1731
1980
|
relay: url,
|
|
1732
1981
|
need: needIds.length,
|
|
1733
1982
|
have: haveIds.length
|
|
1734
1983
|
});
|
|
1735
1984
|
const events = needIds.length > 0 ? yield* relay.fetchEvents(needIds, url).pipe(
|
|
1736
|
-
|
|
1737
|
-
|
|
1985
|
+
Effect9.tapError((err) => Effect9.sync(() => onSyncError?.(err))),
|
|
1986
|
+
Effect9.orElseSucceed(() => [])
|
|
1738
1987
|
) : [];
|
|
1739
1988
|
return {
|
|
1740
1989
|
events,
|
|
1741
1990
|
haveIds: haveIds.map((id) => ({ id, url }))
|
|
1742
1991
|
};
|
|
1743
|
-
}).pipe(
|
|
1744
|
-
const syncAllRelays = (pubKeys, changedCollections) =>
|
|
1745
|
-
const results = yield*
|
|
1992
|
+
}).pipe(Effect9.withLogSpan("tablinum.reconcileRelay"));
|
|
1993
|
+
const syncAllRelays = (pubKeys, changedCollections) => Effect9.gen(function* () {
|
|
1994
|
+
const results = yield* Effect9.forEach(
|
|
1746
1995
|
relayUrls,
|
|
1747
1996
|
(url) => reconcileRelay(url, pubKeys),
|
|
1748
1997
|
{ concurrency: "unbounded" }
|
|
@@ -1759,44 +2008,44 @@ function createSyncHandle(storage, giftWrapHandle, relay, publishQueue, syncStat
|
|
|
1759
2008
|
}
|
|
1760
2009
|
const unwrapped = [];
|
|
1761
2010
|
for (const gw of allGiftWraps) {
|
|
1762
|
-
const result = yield* unwrapGiftWrap(gw).pipe(
|
|
2011
|
+
const result = yield* unwrapGiftWrap(gw).pipe(Effect9.orElseSucceed(() => null));
|
|
1763
2012
|
if (result) unwrapped.push(result);
|
|
1764
2013
|
}
|
|
1765
2014
|
unwrapped.sort((a, b) => a.rumor.created_at - b.rumor.created_at || (a.rumor.id < b.rumor.id ? -1 : 1));
|
|
1766
2015
|
for (const event of unwrapped) {
|
|
1767
2016
|
const collection2 = yield* applyUnwrappedEvent(event).pipe(
|
|
1768
|
-
|
|
2017
|
+
Effect9.orElseSucceed(() => null)
|
|
1769
2018
|
);
|
|
1770
2019
|
if (collection2) changedCollections.add(collection2);
|
|
1771
2020
|
}
|
|
1772
2021
|
const allHaveIds = results.flatMap((r) => r.haveIds);
|
|
1773
|
-
yield*
|
|
2022
|
+
yield* Effect9.forEach(
|
|
1774
2023
|
allHaveIds,
|
|
1775
|
-
({ id, url }) =>
|
|
2024
|
+
({ id, url }) => Effect9.gen(function* () {
|
|
1776
2025
|
const gw = yield* storage.getGiftWrap(id);
|
|
1777
2026
|
if (!gw?.event) return;
|
|
1778
2027
|
yield* relay.publish(gw.event, [url]).pipe(
|
|
1779
|
-
|
|
1780
|
-
|
|
2028
|
+
Effect9.tapError((err) => Effect9.sync(() => onSyncError?.(err))),
|
|
2029
|
+
Effect9.ignore
|
|
1781
2030
|
);
|
|
1782
2031
|
}),
|
|
1783
2032
|
{ concurrency: "unbounded", discard: true }
|
|
1784
2033
|
);
|
|
1785
|
-
}).pipe(
|
|
1786
|
-
const processGiftWrap = (remoteGw) =>
|
|
2034
|
+
}).pipe(Effect9.withLogSpan("tablinum.syncAllRelays"));
|
|
2035
|
+
const processGiftWrap = (remoteGw) => Effect9.gen(function* () {
|
|
1787
2036
|
const uw = yield* unwrapGiftWrap(remoteGw);
|
|
1788
2037
|
if (!uw) return null;
|
|
1789
2038
|
return yield* applyUnwrappedEvent(uw);
|
|
1790
2039
|
});
|
|
1791
|
-
const processRealtimeGiftWrap = (remoteGw) =>
|
|
1792
|
-
const collection2 = yield* processGiftWrap(remoteGw).pipe(
|
|
2040
|
+
const processRealtimeGiftWrap = (remoteGw) => Effect9.gen(function* () {
|
|
2041
|
+
const collection2 = yield* processGiftWrap(remoteGw).pipe(Effect9.orElseSucceed(() => null));
|
|
1793
2042
|
if (collection2) {
|
|
1794
2043
|
yield* notifyCollectionUpdated(collection2);
|
|
1795
2044
|
}
|
|
1796
2045
|
});
|
|
1797
|
-
const processRotationGiftWrap = (remoteGw) =>
|
|
1798
|
-
const unwrapResult = yield*
|
|
1799
|
-
|
|
2046
|
+
const processRotationGiftWrap = (remoteGw) => Effect9.gen(function* () {
|
|
2047
|
+
const unwrapResult = yield* Effect9.result(
|
|
2048
|
+
Effect9.try({
|
|
1800
2049
|
try: () => unwrapEvent(remoteGw, personalPrivateKey),
|
|
1801
2050
|
catch: (e) => new CryptoError({
|
|
1802
2051
|
message: `Rotation unwrap failed: ${e instanceof Error ? e.message : String(e)}`,
|
|
@@ -1846,73 +2095,73 @@ function createSyncHandle(storage, giftWrapHandle, relay, publishQueue, syncStat
|
|
|
1846
2095
|
yield* handle.addEpochSubscription(epoch.publicKey);
|
|
1847
2096
|
return true;
|
|
1848
2097
|
});
|
|
1849
|
-
const subscribeAcrossRelays = (filter, onEvent) =>
|
|
2098
|
+
const subscribeAcrossRelays = (filter, onEvent) => Effect9.forEach(
|
|
1850
2099
|
relayUrls,
|
|
1851
|
-
(url) =>
|
|
2100
|
+
(url) => Effect9.gen(function* () {
|
|
1852
2101
|
yield* relay.subscribe(filter, url, (event) => {
|
|
1853
2102
|
forkHandled(onEvent(event));
|
|
1854
2103
|
}).pipe(
|
|
1855
|
-
|
|
1856
|
-
|
|
2104
|
+
Effect9.tapError((err) => Effect9.sync(() => onSyncError?.(err))),
|
|
2105
|
+
Effect9.ignore
|
|
1857
2106
|
);
|
|
1858
2107
|
}),
|
|
1859
2108
|
{ concurrency: "unbounded", discard: true }
|
|
1860
2109
|
);
|
|
1861
2110
|
let healingActive = false;
|
|
1862
|
-
const healingEffect =
|
|
2111
|
+
const healingEffect = Effect9.gen(function* () {
|
|
1863
2112
|
if (!healingActive) return;
|
|
1864
2113
|
const status = yield* syncStatus.get();
|
|
1865
2114
|
if (status === "syncing") return;
|
|
1866
2115
|
yield* syncStatus.set("syncing");
|
|
1867
|
-
yield*
|
|
2116
|
+
yield* Effect9.gen(function* () {
|
|
1868
2117
|
const pubKeys = getSubscriptionPubKeys();
|
|
1869
2118
|
const changedCollections = /* @__PURE__ */ new Set();
|
|
1870
2119
|
yield* syncAllRelays(pubKeys, changedCollections);
|
|
1871
2120
|
if (changedCollections.size > 0) {
|
|
1872
2121
|
yield* notifyReplayComplete(watchCtx, [...changedCollections]);
|
|
1873
2122
|
}
|
|
1874
|
-
}).pipe(
|
|
1875
|
-
}).pipe(
|
|
2123
|
+
}).pipe(Effect9.ensuring(syncStatus.set("idle")));
|
|
2124
|
+
}).pipe(Effect9.ignore);
|
|
1876
2125
|
const handle = {
|
|
1877
|
-
sync: () =>
|
|
1878
|
-
yield*
|
|
2126
|
+
sync: () => Effect9.gen(function* () {
|
|
2127
|
+
yield* Effect9.logInfo("Sync started");
|
|
1879
2128
|
yield* syncStatus.set("syncing");
|
|
1880
2129
|
yield* Ref3.set(watchCtx.replayingRef, true);
|
|
1881
2130
|
const changedCollections = /* @__PURE__ */ new Set();
|
|
1882
|
-
yield*
|
|
2131
|
+
yield* Effect9.gen(function* () {
|
|
1883
2132
|
const pubKeys = getSubscriptionPubKeys();
|
|
1884
2133
|
yield* syncAllRelays(pubKeys, changedCollections);
|
|
1885
|
-
yield* publishQueue.flush(relayUrls).pipe(
|
|
2134
|
+
yield* publishQueue.flush(relayUrls).pipe(Effect9.ignore);
|
|
1886
2135
|
}).pipe(
|
|
1887
|
-
|
|
1888
|
-
|
|
2136
|
+
Effect9.ensuring(
|
|
2137
|
+
Effect9.gen(function* () {
|
|
1889
2138
|
yield* notifyReplayComplete(watchCtx, [...changedCollections]);
|
|
1890
2139
|
yield* syncStatus.set("idle");
|
|
1891
2140
|
})
|
|
1892
2141
|
)
|
|
1893
2142
|
);
|
|
1894
|
-
yield*
|
|
1895
|
-
}).pipe(
|
|
1896
|
-
publishLocal: (giftWrap) =>
|
|
2143
|
+
yield* Effect9.logInfo("Sync complete", { changed: [...changedCollections] });
|
|
2144
|
+
}).pipe(Effect9.withLogSpan("tablinum.sync")),
|
|
2145
|
+
publishLocal: (giftWrap) => Effect9.gen(function* () {
|
|
1897
2146
|
if (!giftWrap.event) return;
|
|
1898
2147
|
yield* relay.publish(giftWrap.event, relayUrls).pipe(
|
|
1899
|
-
|
|
2148
|
+
Effect9.tapError(
|
|
1900
2149
|
() => storage.putGiftWrap(giftWrap).pipe(
|
|
1901
|
-
|
|
1902
|
-
|
|
2150
|
+
Effect9.andThen(publishQueue.enqueue(giftWrap.id)),
|
|
2151
|
+
Effect9.andThen(Effect9.sync(() => scheduleAutoFlush()))
|
|
1903
2152
|
)
|
|
1904
2153
|
),
|
|
1905
|
-
|
|
1906
|
-
|
|
2154
|
+
Effect9.tapError((err) => Effect9.sync(() => onSyncError?.(err))),
|
|
2155
|
+
Effect9.ignore
|
|
1907
2156
|
);
|
|
1908
2157
|
}),
|
|
1909
|
-
startSubscription: () =>
|
|
2158
|
+
startSubscription: () => Effect9.gen(function* () {
|
|
1910
2159
|
const pubKeys = getSubscriptionPubKeys();
|
|
1911
2160
|
yield* subscribeAcrossRelays({ kinds: [GiftWrap2], "#p": pubKeys }, processRealtimeGiftWrap);
|
|
1912
2161
|
if (!pubKeys.includes(personalPublicKey)) {
|
|
1913
2162
|
yield* subscribeAcrossRelays(
|
|
1914
2163
|
{ kinds: [GiftWrap2], "#p": [personalPublicKey] },
|
|
1915
|
-
(event) =>
|
|
2164
|
+
(event) => Effect9.result(processRotationGiftWrap(event)).pipe(Effect9.asVoid)
|
|
1916
2165
|
);
|
|
1917
2166
|
}
|
|
1918
2167
|
}),
|
|
@@ -1921,10 +2170,10 @@ function createSyncHandle(storage, giftWrapHandle, relay, publishQueue, syncStat
|
|
|
1921
2170
|
if (healingActive) return;
|
|
1922
2171
|
healingActive = true;
|
|
1923
2172
|
forkHandled(
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
2173
|
+
Effect9.sleep(Duration.minutes(5)).pipe(
|
|
2174
|
+
Effect9.andThen(healingEffect),
|
|
2175
|
+
Effect9.repeat(Schedule.spaced(Duration.minutes(5))),
|
|
2176
|
+
Effect9.ensuring(Effect9.sync(() => {
|
|
1928
2177
|
healingActive = false;
|
|
1929
2178
|
}))
|
|
1930
2179
|
)
|
|
@@ -1936,8 +2185,8 @@ function createSyncHandle(storage, giftWrapHandle, relay, publishQueue, syncStat
|
|
|
1936
2185
|
};
|
|
1937
2186
|
forkHandled(
|
|
1938
2187
|
publishQueue.size().pipe(
|
|
1939
|
-
|
|
1940
|
-
(size) =>
|
|
2188
|
+
Effect9.flatMap(
|
|
2189
|
+
(size) => Effect9.sync(() => {
|
|
1941
2190
|
if (size > 0) scheduleAutoFlush();
|
|
1942
2191
|
})
|
|
1943
2192
|
)
|
|
@@ -1947,7 +2196,7 @@ function createSyncHandle(storage, giftWrapHandle, relay, publishQueue, syncStat
|
|
|
1947
2196
|
}
|
|
1948
2197
|
|
|
1949
2198
|
// src/db/members.ts
|
|
1950
|
-
import { Effect as
|
|
2199
|
+
import { Effect as Effect10, Option as Option6, Schema as Schema6 } from "effect";
|
|
1951
2200
|
var optionalString = {
|
|
1952
2201
|
_tag: "FieldDef",
|
|
1953
2202
|
kind: "string",
|
|
@@ -1996,15 +2245,15 @@ var AuthorProfileSchema = Schema6.Struct({
|
|
|
1996
2245
|
});
|
|
1997
2246
|
var decodeAuthorProfile = Schema6.decodeUnknownEffect(Schema6.fromJsonString(AuthorProfileSchema));
|
|
1998
2247
|
function fetchAuthorProfile(relay, relayUrls, pubkey) {
|
|
1999
|
-
return
|
|
2248
|
+
return Effect10.gen(function* () {
|
|
2000
2249
|
for (const url of relayUrls) {
|
|
2001
|
-
const result = yield*
|
|
2250
|
+
const result = yield* Effect10.result(
|
|
2002
2251
|
relay.fetchByFilter({ kinds: [0], authors: [pubkey], limit: 1 }, url)
|
|
2003
2252
|
);
|
|
2004
2253
|
if (result._tag === "Success" && result.success.length > 0) {
|
|
2005
2254
|
return yield* decodeAuthorProfile(result.success[0].content).pipe(
|
|
2006
|
-
|
|
2007
|
-
|
|
2255
|
+
Effect10.map(Option6.some),
|
|
2256
|
+
Effect10.orElseSucceed(() => Option6.none())
|
|
2008
2257
|
);
|
|
2009
2258
|
}
|
|
2010
2259
|
}
|
|
@@ -2054,15 +2303,15 @@ var SyncStatus = class extends ServiceMap9.Service()(
|
|
|
2054
2303
|
};
|
|
2055
2304
|
|
|
2056
2305
|
// src/layers/IdentityLive.ts
|
|
2057
|
-
import { Effect as
|
|
2058
|
-
import { hexToBytes as
|
|
2306
|
+
import { Effect as Effect12, Layer as Layer2 } from "effect";
|
|
2307
|
+
import { hexToBytes as hexToBytes4 } from "@noble/hashes/utils.js";
|
|
2059
2308
|
|
|
2060
2309
|
// src/db/identity.ts
|
|
2061
|
-
import { Effect as
|
|
2310
|
+
import { Effect as Effect11 } from "effect";
|
|
2062
2311
|
import { getPublicKey as getPublicKey2 } from "nostr-tools/pure";
|
|
2063
|
-
import { bytesToHex as
|
|
2312
|
+
import { bytesToHex as bytesToHex3 } from "@noble/hashes/utils.js";
|
|
2064
2313
|
function createIdentity(suppliedKey) {
|
|
2065
|
-
return
|
|
2314
|
+
return Effect11.gen(function* () {
|
|
2066
2315
|
let privateKey;
|
|
2067
2316
|
if (suppliedKey) {
|
|
2068
2317
|
if (suppliedKey.length !== 32) {
|
|
@@ -2075,8 +2324,8 @@ function createIdentity(suppliedKey) {
|
|
|
2075
2324
|
privateKey = new Uint8Array(32);
|
|
2076
2325
|
crypto.getRandomValues(privateKey);
|
|
2077
2326
|
}
|
|
2078
|
-
const privateKeyHex =
|
|
2079
|
-
const publicKey = yield*
|
|
2327
|
+
const privateKeyHex = bytesToHex3(privateKey);
|
|
2328
|
+
const publicKey = yield* Effect11.try({
|
|
2080
2329
|
try: () => getPublicKey2(privateKey),
|
|
2081
2330
|
catch: (e) => new CryptoError({
|
|
2082
2331
|
message: `Failed to derive public key: ${e instanceof Error ? e.message : String(e)}`,
|
|
@@ -2094,14 +2343,14 @@ function createIdentity(suppliedKey) {
|
|
|
2094
2343
|
// src/layers/IdentityLive.ts
|
|
2095
2344
|
var IdentityLive = Layer2.effect(
|
|
2096
2345
|
Identity,
|
|
2097
|
-
|
|
2346
|
+
Effect12.gen(function* () {
|
|
2098
2347
|
const config = yield* Config;
|
|
2099
2348
|
const storage = yield* Storage;
|
|
2100
2349
|
const idbKey = yield* storage.getMeta("identity_key");
|
|
2101
|
-
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);
|
|
2102
2351
|
const identity = yield* createIdentity(resolvedKey);
|
|
2103
2352
|
yield* storage.putMeta("identity_key", identity.exportKey());
|
|
2104
|
-
yield*
|
|
2353
|
+
yield* Effect12.logInfo("Identity loaded", {
|
|
2105
2354
|
publicKey: identity.publicKey.slice(0, 12) + "...",
|
|
2106
2355
|
source: config.privateKey ? "config" : resolvedKey ? "storage" : "generated"
|
|
2107
2356
|
});
|
|
@@ -2110,12 +2359,12 @@ var IdentityLive = Layer2.effect(
|
|
|
2110
2359
|
);
|
|
2111
2360
|
|
|
2112
2361
|
// src/layers/EpochStoreLive.ts
|
|
2113
|
-
import { Effect as
|
|
2362
|
+
import { Effect as Effect13, Layer as Layer3, Option as Option7 } from "effect";
|
|
2114
2363
|
import { generateSecretKey as generateSecretKey2 } from "nostr-tools/pure";
|
|
2115
|
-
import { bytesToHex as
|
|
2364
|
+
import { bytesToHex as bytesToHex4 } from "@noble/hashes/utils.js";
|
|
2116
2365
|
var EpochStoreLive = Layer3.effect(
|
|
2117
2366
|
EpochStore,
|
|
2118
|
-
|
|
2367
|
+
Effect13.gen(function* () {
|
|
2119
2368
|
const config = yield* Config;
|
|
2120
2369
|
const identity = yield* Identity;
|
|
2121
2370
|
const storage = yield* Storage;
|
|
@@ -2134,7 +2383,7 @@ var EpochStoreLive = Layer3.effect(
|
|
|
2134
2383
|
return existing !== void 0 && existing.privateKey === ek.key;
|
|
2135
2384
|
});
|
|
2136
2385
|
if (configIsSubset) {
|
|
2137
|
-
yield*
|
|
2386
|
+
yield* Effect13.logInfo("Epoch store loaded", {
|
|
2138
2387
|
source: "storage",
|
|
2139
2388
|
epochs: idbStore.epochs.size
|
|
2140
2389
|
});
|
|
@@ -2143,271 +2392,28 @@ var EpochStoreLive = Layer3.effect(
|
|
|
2143
2392
|
}
|
|
2144
2393
|
const store2 = createEpochStoreFromInputs(config.epochKeys);
|
|
2145
2394
|
yield* storage.putMeta("epochs", stringifyEpochStore(store2));
|
|
2146
|
-
yield*
|
|
2395
|
+
yield* Effect13.logInfo("Epoch store loaded", { source: "config", epochs: store2.epochs.size });
|
|
2147
2396
|
return store2;
|
|
2148
2397
|
}
|
|
2149
2398
|
if (idbStore) {
|
|
2150
|
-
yield*
|
|
2399
|
+
yield* Effect13.logInfo("Epoch store loaded", {
|
|
2151
2400
|
source: "storage",
|
|
2152
2401
|
epochs: idbStore.epochs.size
|
|
2153
2402
|
});
|
|
2154
2403
|
return idbStore;
|
|
2155
2404
|
}
|
|
2156
2405
|
const store = createEpochStoreFromInputs(
|
|
2157
|
-
[{ epochId: EpochId("epoch-0"), key:
|
|
2406
|
+
[{ epochId: EpochId("epoch-0"), key: bytesToHex4(generateSecretKey2()) }],
|
|
2158
2407
|
{ createdBy: identity.publicKey }
|
|
2159
2408
|
);
|
|
2160
2409
|
yield* storage.putMeta("epochs", stringifyEpochStore(store));
|
|
2161
|
-
yield*
|
|
2410
|
+
yield* Effect13.logInfo("Epoch store loaded", { source: "generated", epochs: store.epochs.size });
|
|
2162
2411
|
return store;
|
|
2163
2412
|
})
|
|
2164
2413
|
);
|
|
2165
2414
|
|
|
2166
2415
|
// src/layers/StorageLive.ts
|
|
2167
2416
|
import { Effect as Effect14, Layer as Layer4 } from "effect";
|
|
2168
|
-
|
|
2169
|
-
// src/storage/idb.ts
|
|
2170
|
-
import { Effect as Effect13 } from "effect";
|
|
2171
|
-
import { openDB } from "idb";
|
|
2172
|
-
|
|
2173
|
-
// src/sync/compact-event.ts
|
|
2174
|
-
import { bytesToHex as bytesToHex4, hexToBytes as hexToBytes4 } from "@noble/hashes/utils.js";
|
|
2175
|
-
var VERSION = 1;
|
|
2176
|
-
var HEADER_SIZE = 133;
|
|
2177
|
-
function base64ToBytes(base64) {
|
|
2178
|
-
const binary = atob(base64);
|
|
2179
|
-
const bytes = new Uint8Array(binary.length);
|
|
2180
|
-
for (let i = 0; i < binary.length; i++) {
|
|
2181
|
-
bytes[i] = binary.charCodeAt(i);
|
|
2182
|
-
}
|
|
2183
|
-
return bytes;
|
|
2184
|
-
}
|
|
2185
|
-
function bytesToBase64(bytes) {
|
|
2186
|
-
let binary = "";
|
|
2187
|
-
for (let i = 0; i < bytes.length; i++) {
|
|
2188
|
-
binary += String.fromCharCode(bytes[i]);
|
|
2189
|
-
}
|
|
2190
|
-
return btoa(binary);
|
|
2191
|
-
}
|
|
2192
|
-
function packEvent(event) {
|
|
2193
|
-
const pubkey = hexToBytes4(event.pubkey);
|
|
2194
|
-
const sig = hexToBytes4(event.sig);
|
|
2195
|
-
const recipientTag = event.tags.find((t) => t[0] === "p");
|
|
2196
|
-
if (!recipientTag) throw new Error("Gift wrap missing #p tag");
|
|
2197
|
-
if (event.tags.some((t) => t[0] !== "p")) {
|
|
2198
|
-
throw new Error("Gift wrap has unexpected non-p tags; compact encoding would lose them");
|
|
2199
|
-
}
|
|
2200
|
-
const recipient = hexToBytes4(recipientTag[1]);
|
|
2201
|
-
const createdAtBuf = new Uint8Array(4);
|
|
2202
|
-
new DataView(createdAtBuf.buffer).setUint32(0, event.created_at, false);
|
|
2203
|
-
const content = base64ToBytes(event.content);
|
|
2204
|
-
const result = new Uint8Array(HEADER_SIZE + content.length);
|
|
2205
|
-
result[0] = VERSION;
|
|
2206
|
-
result.set(pubkey, 1);
|
|
2207
|
-
result.set(sig, 33);
|
|
2208
|
-
result.set(recipient, 97);
|
|
2209
|
-
result.set(createdAtBuf, 129);
|
|
2210
|
-
result.set(content, HEADER_SIZE);
|
|
2211
|
-
return result;
|
|
2212
|
-
}
|
|
2213
|
-
function unpackEvent(id, compact) {
|
|
2214
|
-
const version = compact[0];
|
|
2215
|
-
if (version !== VERSION) throw new Error(`Unknown compact event version: ${version}`);
|
|
2216
|
-
const pubkey = bytesToHex4(compact.slice(1, 33));
|
|
2217
|
-
const sig = bytesToHex4(compact.slice(33, 97));
|
|
2218
|
-
const recipient = bytesToHex4(compact.slice(97, 129));
|
|
2219
|
-
const dv = new DataView(compact.buffer, compact.byteOffset + 129, 4);
|
|
2220
|
-
const createdAt = dv.getUint32(0, false);
|
|
2221
|
-
const content = bytesToBase64(compact.slice(HEADER_SIZE));
|
|
2222
|
-
return {
|
|
2223
|
-
id,
|
|
2224
|
-
pubkey,
|
|
2225
|
-
sig,
|
|
2226
|
-
created_at: createdAt,
|
|
2227
|
-
kind: 1059,
|
|
2228
|
-
tags: [["p", recipient]],
|
|
2229
|
-
content
|
|
2230
|
-
};
|
|
2231
|
-
}
|
|
2232
|
-
|
|
2233
|
-
// src/storage/idb.ts
|
|
2234
|
-
var DB_NAME = "tablinum";
|
|
2235
|
-
function storeName(collection2) {
|
|
2236
|
-
return `col_${collection2}`;
|
|
2237
|
-
}
|
|
2238
|
-
function computeSchemaSig(schema) {
|
|
2239
|
-
return Object.entries(schema).sort(([a], [b]) => a.localeCompare(b)).map(([name, def]) => {
|
|
2240
|
-
const indices = [...def.indices ?? []].sort().join(",");
|
|
2241
|
-
return `${name}:${indices}`;
|
|
2242
|
-
}).join("|");
|
|
2243
|
-
}
|
|
2244
|
-
function wrap(label, fn) {
|
|
2245
|
-
return Effect13.tryPromise({
|
|
2246
|
-
try: fn,
|
|
2247
|
-
catch: (e) => new StorageError({
|
|
2248
|
-
message: `IndexedDB ${label} failed: ${e instanceof Error ? e.message : String(e)}`,
|
|
2249
|
-
cause: e
|
|
2250
|
-
})
|
|
2251
|
-
});
|
|
2252
|
-
}
|
|
2253
|
-
function upgradeSchema(database, schema, tx) {
|
|
2254
|
-
if (!database.objectStoreNames.contains("_meta")) {
|
|
2255
|
-
database.createObjectStore("_meta");
|
|
2256
|
-
}
|
|
2257
|
-
if (!database.objectStoreNames.contains("events")) {
|
|
2258
|
-
const events = database.createObjectStore("events", { keyPath: "id" });
|
|
2259
|
-
events.createIndex("by-record", ["collection", "recordId"]);
|
|
2260
|
-
}
|
|
2261
|
-
if (!database.objectStoreNames.contains("giftwraps")) {
|
|
2262
|
-
database.createObjectStore("giftwraps", { keyPath: "id" });
|
|
2263
|
-
}
|
|
2264
|
-
const expectedStores = /* @__PURE__ */ new Set();
|
|
2265
|
-
for (const [, def] of Object.entries(schema)) {
|
|
2266
|
-
const sn = storeName(def.name);
|
|
2267
|
-
expectedStores.add(sn);
|
|
2268
|
-
if (!database.objectStoreNames.contains(sn)) {
|
|
2269
|
-
const store = database.createObjectStore(sn, { keyPath: "id" });
|
|
2270
|
-
for (const idx of def.indices ?? []) {
|
|
2271
|
-
store.createIndex(idx, idx);
|
|
2272
|
-
}
|
|
2273
|
-
} else {
|
|
2274
|
-
const store = tx.objectStore(sn);
|
|
2275
|
-
const existingIndices = new Set(Array.from(store.indexNames));
|
|
2276
|
-
const wantedIndices = new Set(def.indices ?? []);
|
|
2277
|
-
for (const idx of existingIndices) {
|
|
2278
|
-
if (!wantedIndices.has(idx)) store.deleteIndex(idx);
|
|
2279
|
-
}
|
|
2280
|
-
for (const idx of wantedIndices) {
|
|
2281
|
-
if (!existingIndices.has(idx)) store.createIndex(idx, idx);
|
|
2282
|
-
}
|
|
2283
|
-
}
|
|
2284
|
-
}
|
|
2285
|
-
for (const existing of Array.from(database.objectStoreNames)) {
|
|
2286
|
-
if (existing.startsWith("col_") && !expectedStores.has(existing)) {
|
|
2287
|
-
database.deleteObjectStore(existing);
|
|
2288
|
-
}
|
|
2289
|
-
}
|
|
2290
|
-
tx.objectStore("_meta").put(computeSchemaSig(schema), "schema_sig");
|
|
2291
|
-
}
|
|
2292
|
-
function openIDBStorage(dbName, schema) {
|
|
2293
|
-
return Effect13.gen(function* () {
|
|
2294
|
-
const name = dbName ?? DB_NAME;
|
|
2295
|
-
const schemaSig = computeSchemaSig(schema);
|
|
2296
|
-
if (typeof indexedDB === "undefined") {
|
|
2297
|
-
return yield* Effect13.fail(
|
|
2298
|
-
new StorageError({
|
|
2299
|
-
message: "IndexedDB is not available in this environment"
|
|
2300
|
-
})
|
|
2301
|
-
);
|
|
2302
|
-
}
|
|
2303
|
-
const probeDb = yield* Effect13.tryPromise({
|
|
2304
|
-
try: () => openDB(name),
|
|
2305
|
-
catch: (e) => new StorageError({ message: "Failed to open IndexedDB", cause: e })
|
|
2306
|
-
});
|
|
2307
|
-
const currentVersion = probeDb.version;
|
|
2308
|
-
let needsUpgrade = true;
|
|
2309
|
-
if (probeDb.objectStoreNames.contains("_meta")) {
|
|
2310
|
-
const storedSig = yield* Effect13.tryPromise({
|
|
2311
|
-
try: () => probeDb.get("_meta", "schema_sig"),
|
|
2312
|
-
catch: () => new StorageError({ message: "Failed to read schema meta" })
|
|
2313
|
-
}).pipe(Effect13.catch(() => Effect13.succeed(void 0)));
|
|
2314
|
-
needsUpgrade = storedSig !== schemaSig;
|
|
2315
|
-
}
|
|
2316
|
-
probeDb.close();
|
|
2317
|
-
const db = needsUpgrade ? yield* Effect13.tryPromise({
|
|
2318
|
-
try: () => openDB(name, currentVersion + 1, {
|
|
2319
|
-
upgrade(database, _oldVersion, _newVersion, transaction) {
|
|
2320
|
-
upgradeSchema(database, schema, transaction);
|
|
2321
|
-
}
|
|
2322
|
-
}),
|
|
2323
|
-
catch: (e) => new StorageError({ message: "Failed to open IndexedDB", cause: e })
|
|
2324
|
-
}) : yield* Effect13.tryPromise({
|
|
2325
|
-
try: () => openDB(name),
|
|
2326
|
-
catch: (e) => new StorageError({ message: "Failed to open IndexedDB", cause: e })
|
|
2327
|
-
});
|
|
2328
|
-
yield* Effect13.addFinalizer(() => Effect13.sync(() => db.close()));
|
|
2329
|
-
const handle = {
|
|
2330
|
-
putRecord: (collection2, record) => wrap("putRecord", () => db.put(storeName(collection2), record).then(() => void 0)),
|
|
2331
|
-
getRecord: (collection2, id) => wrap("getRecord", () => db.get(storeName(collection2), id)),
|
|
2332
|
-
getAllRecords: (collection2) => wrap("getAllRecords", () => db.getAll(storeName(collection2))),
|
|
2333
|
-
countRecords: (collection2) => wrap("countRecords", () => db.count(storeName(collection2))),
|
|
2334
|
-
clearRecords: (collection2) => wrap("clearRecords", () => db.clear(storeName(collection2))),
|
|
2335
|
-
getByIndex: (collection2, indexName, value) => wrap("getByIndex", () => db.getAllFromIndex(storeName(collection2), indexName, value)),
|
|
2336
|
-
getByIndexRange: (collection2, indexName, range) => wrap("getByIndexRange", () => db.getAllFromIndex(storeName(collection2), indexName, range)),
|
|
2337
|
-
getAllSorted: (collection2, indexName, direction) => wrap("getAllSorted", async () => {
|
|
2338
|
-
const sn = storeName(collection2);
|
|
2339
|
-
const tx = db.transaction(sn, "readonly");
|
|
2340
|
-
const store = tx.objectStore(sn);
|
|
2341
|
-
const index = store.index(indexName);
|
|
2342
|
-
const results = [];
|
|
2343
|
-
let cursor = await index.openCursor(null, direction ?? "next");
|
|
2344
|
-
while (cursor) {
|
|
2345
|
-
results.push(cursor.value);
|
|
2346
|
-
cursor = await cursor.continue();
|
|
2347
|
-
}
|
|
2348
|
-
return results;
|
|
2349
|
-
}),
|
|
2350
|
-
putEvent: (event) => wrap("putEvent", () => db.put("events", event).then(() => void 0)),
|
|
2351
|
-
getEvent: (id) => wrap("getEvent", () => db.get("events", id)),
|
|
2352
|
-
getAllEvents: () => wrap("getAllEvents", () => db.getAll("events")),
|
|
2353
|
-
getEventsByRecord: (collection2, recordId) => wrap(
|
|
2354
|
-
"getEventsByRecord",
|
|
2355
|
-
() => db.getAllFromIndex("events", "by-record", [collection2, recordId])
|
|
2356
|
-
),
|
|
2357
|
-
putGiftWrap: (gw) => wrap("putGiftWrap", async () => {
|
|
2358
|
-
if (gw.event) {
|
|
2359
|
-
const compact = packEvent(gw.event);
|
|
2360
|
-
await db.put("giftwraps", { id: gw.id, compact, createdAt: gw.createdAt });
|
|
2361
|
-
} else {
|
|
2362
|
-
await db.put("giftwraps", { id: gw.id, createdAt: gw.createdAt });
|
|
2363
|
-
}
|
|
2364
|
-
}),
|
|
2365
|
-
getGiftWrap: (id) => wrap("getGiftWrap", async () => {
|
|
2366
|
-
const raw = await db.get("giftwraps", id);
|
|
2367
|
-
if (!raw) return void 0;
|
|
2368
|
-
if (raw.compact) {
|
|
2369
|
-
return { id: raw.id, event: unpackEvent(raw.id, raw.compact), createdAt: raw.createdAt };
|
|
2370
|
-
}
|
|
2371
|
-
if (raw.event) {
|
|
2372
|
-
const compact = packEvent(raw.event);
|
|
2373
|
-
await db.put("giftwraps", { id: raw.id, compact, createdAt: raw.createdAt });
|
|
2374
|
-
return { id: raw.id, event: raw.event, createdAt: raw.createdAt };
|
|
2375
|
-
}
|
|
2376
|
-
return { id: raw.id, createdAt: raw.createdAt };
|
|
2377
|
-
}),
|
|
2378
|
-
getAllGiftWraps: () => wrap("getAllGiftWraps", async () => {
|
|
2379
|
-
const raws = await db.getAll("giftwraps");
|
|
2380
|
-
const results = [];
|
|
2381
|
-
for (const raw of raws) {
|
|
2382
|
-
if (raw.compact) {
|
|
2383
|
-
results.push({ id: raw.id, event: unpackEvent(raw.id, raw.compact), createdAt: raw.createdAt });
|
|
2384
|
-
} else if (raw.event) {
|
|
2385
|
-
const compact = packEvent(raw.event);
|
|
2386
|
-
await db.put("giftwraps", { id: raw.id, compact, createdAt: raw.createdAt });
|
|
2387
|
-
results.push({ id: raw.id, event: raw.event, createdAt: raw.createdAt });
|
|
2388
|
-
} else {
|
|
2389
|
-
results.push({ id: raw.id, createdAt: raw.createdAt });
|
|
2390
|
-
}
|
|
2391
|
-
}
|
|
2392
|
-
return results;
|
|
2393
|
-
}),
|
|
2394
|
-
deleteGiftWrap: (id) => wrap("deleteGiftWrap", () => db.delete("giftwraps", id).then(() => void 0)),
|
|
2395
|
-
deleteEvent: (id) => wrap("deleteEvent", () => db.delete("events", id).then(() => void 0)),
|
|
2396
|
-
stripEventData: (id) => wrap("stripEventData", async () => {
|
|
2397
|
-
const existing = await db.get("events", id);
|
|
2398
|
-
if (existing) {
|
|
2399
|
-
await db.put("events", { ...existing, data: null });
|
|
2400
|
-
}
|
|
2401
|
-
}),
|
|
2402
|
-
getMeta: (key) => wrap("getMeta", () => db.get("_meta", key)),
|
|
2403
|
-
putMeta: (key, value) => wrap("putMeta", () => db.put("_meta", value, key).then(() => void 0)),
|
|
2404
|
-
close: () => Effect13.sync(() => db.close())
|
|
2405
|
-
};
|
|
2406
|
-
return handle;
|
|
2407
|
-
});
|
|
2408
|
-
}
|
|
2409
|
-
|
|
2410
|
-
// src/layers/StorageLive.ts
|
|
2411
2417
|
var StorageLive = Layer4.effect(
|
|
2412
2418
|
Storage,
|
|
2413
2419
|
Effect14.gen(function* () {
|
|
@@ -3086,6 +3092,60 @@ var TablinumLive = Layer9.effect(
|
|
|
3086
3092
|
yield* Scope4.close(scope, Exit.void);
|
|
3087
3093
|
})
|
|
3088
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
|
+
),
|
|
3089
3149
|
rebuild: () => ensureOpen(
|
|
3090
3150
|
rebuild(
|
|
3091
3151
|
storage,
|
|
@@ -3223,6 +3283,9 @@ function validateConfig(config) {
|
|
|
3223
3283
|
}
|
|
3224
3284
|
});
|
|
3225
3285
|
}
|
|
3286
|
+
function deleteDatabase(dbName) {
|
|
3287
|
+
return deleteIDBStorage(DatabaseName(dbName ?? "tablinum"));
|
|
3288
|
+
}
|
|
3226
3289
|
function createTablinum(config) {
|
|
3227
3290
|
return Effect22.gen(function* () {
|
|
3228
3291
|
yield* validateConfig(config);
|
|
@@ -3244,6 +3307,9 @@ function createTablinum(config) {
|
|
|
3244
3307
|
});
|
|
3245
3308
|
}
|
|
3246
3309
|
|
|
3310
|
+
// src/svelte/tablinum.svelte.ts
|
|
3311
|
+
import { Effect as Effect25, Exit as Exit2, References as References6, Scope as Scope6 } from "effect";
|
|
3312
|
+
|
|
3247
3313
|
// src/svelte/collection.svelte.ts
|
|
3248
3314
|
import { Effect as Effect24, Fiber, Option as Option11, References as References5, Stream as Stream4 } from "effect";
|
|
3249
3315
|
|
|
@@ -3478,9 +3544,11 @@ var Tablinum2 = class {
|
|
|
3478
3544
|
#closed = false;
|
|
3479
3545
|
#readyState = createDeferred();
|
|
3480
3546
|
#logLevel;
|
|
3547
|
+
#dbName;
|
|
3481
3548
|
constructor(config) {
|
|
3482
3549
|
this.ready = this.#readyState.promise;
|
|
3483
3550
|
this.#logLevel = resolveLogLevel(config.logLevel);
|
|
3551
|
+
this.#dbName = config.dbName ?? "tablinum";
|
|
3484
3552
|
this.#init(config);
|
|
3485
3553
|
}
|
|
3486
3554
|
#settleReady(err) {
|
|
@@ -3615,6 +3683,14 @@ var Tablinum2 = class {
|
|
|
3615
3683
|
}
|
|
3616
3684
|
this.status = "closed";
|
|
3617
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
|
+
};
|
|
3618
3694
|
sync = async () => this.#runHandleEffect((handle) => handle.sync());
|
|
3619
3695
|
rebuild = async () => this.#runHandleEffect((handle) => handle.rebuild());
|
|
3620
3696
|
addMember = async (pubkey) => this.#runHandleEffect((handle) => handle.addMember(pubkey));
|
|
@@ -3635,6 +3711,7 @@ export {
|
|
|
3635
3711
|
ValidationError,
|
|
3636
3712
|
collection,
|
|
3637
3713
|
decodeInvite,
|
|
3714
|
+
deleteDatabase,
|
|
3638
3715
|
encodeInvite,
|
|
3639
3716
|
field
|
|
3640
3717
|
};
|