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
package/dist/index.js
CHANGED
|
@@ -224,6 +224,257 @@ function resolveRuntimeConfig(source) {
|
|
|
224
224
|
);
|
|
225
225
|
}
|
|
226
226
|
|
|
227
|
+
// src/storage/idb.ts
|
|
228
|
+
import { Effect as Effect2 } from "effect";
|
|
229
|
+
import { openDB, deleteDB } from "idb";
|
|
230
|
+
|
|
231
|
+
// src/sync/compact-event.ts
|
|
232
|
+
import { bytesToHex as bytesToHex2, hexToBytes as hexToBytes2 } from "@noble/hashes/utils.js";
|
|
233
|
+
var VERSION = 1;
|
|
234
|
+
var HEADER_SIZE = 133;
|
|
235
|
+
function base64ToBytes(base64) {
|
|
236
|
+
const binary = atob(base64);
|
|
237
|
+
const bytes = new Uint8Array(binary.length);
|
|
238
|
+
for (let i = 0; i < binary.length; i++) {
|
|
239
|
+
bytes[i] = binary.charCodeAt(i);
|
|
240
|
+
}
|
|
241
|
+
return bytes;
|
|
242
|
+
}
|
|
243
|
+
function bytesToBase64(bytes) {
|
|
244
|
+
let binary = "";
|
|
245
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
246
|
+
binary += String.fromCharCode(bytes[i]);
|
|
247
|
+
}
|
|
248
|
+
return btoa(binary);
|
|
249
|
+
}
|
|
250
|
+
function packEvent(event) {
|
|
251
|
+
const pubkey = hexToBytes2(event.pubkey);
|
|
252
|
+
const sig = hexToBytes2(event.sig);
|
|
253
|
+
const recipientTag = event.tags.find((t) => t[0] === "p");
|
|
254
|
+
if (!recipientTag) throw new Error("Gift wrap missing #p tag");
|
|
255
|
+
if (event.tags.some((t) => t[0] !== "p")) {
|
|
256
|
+
throw new Error("Gift wrap has unexpected non-p tags; compact encoding would lose them");
|
|
257
|
+
}
|
|
258
|
+
const recipient = hexToBytes2(recipientTag[1]);
|
|
259
|
+
const createdAtBuf = new Uint8Array(4);
|
|
260
|
+
new DataView(createdAtBuf.buffer).setUint32(0, event.created_at, false);
|
|
261
|
+
const content = base64ToBytes(event.content);
|
|
262
|
+
const result = new Uint8Array(HEADER_SIZE + content.length);
|
|
263
|
+
result[0] = VERSION;
|
|
264
|
+
result.set(pubkey, 1);
|
|
265
|
+
result.set(sig, 33);
|
|
266
|
+
result.set(recipient, 97);
|
|
267
|
+
result.set(createdAtBuf, 129);
|
|
268
|
+
result.set(content, HEADER_SIZE);
|
|
269
|
+
return result;
|
|
270
|
+
}
|
|
271
|
+
function unpackEvent(id, compact) {
|
|
272
|
+
const version = compact[0];
|
|
273
|
+
if (version !== VERSION) throw new Error(`Unknown compact event version: ${version}`);
|
|
274
|
+
const pubkey = bytesToHex2(compact.slice(1, 33));
|
|
275
|
+
const sig = bytesToHex2(compact.slice(33, 97));
|
|
276
|
+
const recipient = bytesToHex2(compact.slice(97, 129));
|
|
277
|
+
const dv = new DataView(compact.buffer, compact.byteOffset + 129, 4);
|
|
278
|
+
const createdAt = dv.getUint32(0, false);
|
|
279
|
+
const content = bytesToBase64(compact.slice(HEADER_SIZE));
|
|
280
|
+
return {
|
|
281
|
+
id,
|
|
282
|
+
pubkey,
|
|
283
|
+
sig,
|
|
284
|
+
created_at: createdAt,
|
|
285
|
+
kind: 1059,
|
|
286
|
+
tags: [["p", recipient]],
|
|
287
|
+
content
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// src/storage/idb.ts
|
|
292
|
+
var DB_NAME = "tablinum";
|
|
293
|
+
function storeName(collection2) {
|
|
294
|
+
return `col_${collection2}`;
|
|
295
|
+
}
|
|
296
|
+
function computeSchemaSig(schema) {
|
|
297
|
+
return Object.entries(schema).sort(([a], [b]) => a.localeCompare(b)).map(([name, def]) => {
|
|
298
|
+
const indices = [...def.indices ?? []].sort().join(",");
|
|
299
|
+
return `${name}:${indices}`;
|
|
300
|
+
}).join("|");
|
|
301
|
+
}
|
|
302
|
+
function wrap(label, fn) {
|
|
303
|
+
return Effect2.tryPromise({
|
|
304
|
+
try: fn,
|
|
305
|
+
catch: (e) => new StorageError({
|
|
306
|
+
message: `IndexedDB ${label} failed: ${e instanceof Error ? e.message : String(e)}`,
|
|
307
|
+
cause: e
|
|
308
|
+
})
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
function upgradeSchema(database, schema, tx) {
|
|
312
|
+
if (!database.objectStoreNames.contains("_meta")) {
|
|
313
|
+
database.createObjectStore("_meta");
|
|
314
|
+
}
|
|
315
|
+
if (!database.objectStoreNames.contains("events")) {
|
|
316
|
+
const events = database.createObjectStore("events", { keyPath: "id" });
|
|
317
|
+
events.createIndex("by-record", ["collection", "recordId"]);
|
|
318
|
+
}
|
|
319
|
+
if (!database.objectStoreNames.contains("giftwraps")) {
|
|
320
|
+
database.createObjectStore("giftwraps", { keyPath: "id" });
|
|
321
|
+
}
|
|
322
|
+
const expectedStores = /* @__PURE__ */ new Set();
|
|
323
|
+
for (const [, def] of Object.entries(schema)) {
|
|
324
|
+
const sn = storeName(def.name);
|
|
325
|
+
expectedStores.add(sn);
|
|
326
|
+
if (!database.objectStoreNames.contains(sn)) {
|
|
327
|
+
const store = database.createObjectStore(sn, { keyPath: "id" });
|
|
328
|
+
for (const idx of def.indices ?? []) {
|
|
329
|
+
store.createIndex(idx, idx);
|
|
330
|
+
}
|
|
331
|
+
} else {
|
|
332
|
+
const store = tx.objectStore(sn);
|
|
333
|
+
const existingIndices = new Set(Array.from(store.indexNames));
|
|
334
|
+
const wantedIndices = new Set(def.indices ?? []);
|
|
335
|
+
for (const idx of existingIndices) {
|
|
336
|
+
if (!wantedIndices.has(idx)) store.deleteIndex(idx);
|
|
337
|
+
}
|
|
338
|
+
for (const idx of wantedIndices) {
|
|
339
|
+
if (!existingIndices.has(idx)) store.createIndex(idx, idx);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
for (const existing of Array.from(database.objectStoreNames)) {
|
|
344
|
+
if (existing.startsWith("col_") && !expectedStores.has(existing)) {
|
|
345
|
+
database.deleteObjectStore(existing);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
tx.objectStore("_meta").put(computeSchemaSig(schema), "schema_sig");
|
|
349
|
+
}
|
|
350
|
+
function deleteIDBStorage(dbName) {
|
|
351
|
+
if (typeof indexedDB === "undefined") {
|
|
352
|
+
return Effect2.fail(
|
|
353
|
+
new StorageError({
|
|
354
|
+
message: "IndexedDB is not available in this environment"
|
|
355
|
+
})
|
|
356
|
+
);
|
|
357
|
+
}
|
|
358
|
+
return wrap("deleteDatabase", () => deleteDB(dbName));
|
|
359
|
+
}
|
|
360
|
+
function openIDBStorage(dbName, schema) {
|
|
361
|
+
return Effect2.gen(function* () {
|
|
362
|
+
const name = dbName ?? DB_NAME;
|
|
363
|
+
const schemaSig = computeSchemaSig(schema);
|
|
364
|
+
if (typeof indexedDB === "undefined") {
|
|
365
|
+
return yield* Effect2.fail(
|
|
366
|
+
new StorageError({
|
|
367
|
+
message: "IndexedDB is not available in this environment"
|
|
368
|
+
})
|
|
369
|
+
);
|
|
370
|
+
}
|
|
371
|
+
const probeDb = yield* Effect2.tryPromise({
|
|
372
|
+
try: () => openDB(name),
|
|
373
|
+
catch: (e) => new StorageError({ message: "Failed to open IndexedDB", cause: e })
|
|
374
|
+
});
|
|
375
|
+
const currentVersion = probeDb.version;
|
|
376
|
+
let needsUpgrade = true;
|
|
377
|
+
if (probeDb.objectStoreNames.contains("_meta")) {
|
|
378
|
+
const storedSig = yield* Effect2.tryPromise({
|
|
379
|
+
try: () => probeDb.get("_meta", "schema_sig"),
|
|
380
|
+
catch: () => new StorageError({ message: "Failed to read schema meta" })
|
|
381
|
+
}).pipe(Effect2.catch(() => Effect2.succeed(void 0)));
|
|
382
|
+
needsUpgrade = storedSig !== schemaSig;
|
|
383
|
+
}
|
|
384
|
+
probeDb.close();
|
|
385
|
+
const db = needsUpgrade ? yield* Effect2.tryPromise({
|
|
386
|
+
try: () => openDB(name, currentVersion + 1, {
|
|
387
|
+
upgrade(database, _oldVersion, _newVersion, transaction) {
|
|
388
|
+
upgradeSchema(database, schema, transaction);
|
|
389
|
+
}
|
|
390
|
+
}),
|
|
391
|
+
catch: (e) => new StorageError({ message: "Failed to open IndexedDB", cause: e })
|
|
392
|
+
}) : yield* Effect2.tryPromise({
|
|
393
|
+
try: () => openDB(name),
|
|
394
|
+
catch: (e) => new StorageError({ message: "Failed to open IndexedDB", cause: e })
|
|
395
|
+
});
|
|
396
|
+
yield* Effect2.addFinalizer(() => Effect2.sync(() => db.close()));
|
|
397
|
+
const handle = {
|
|
398
|
+
putRecord: (collection2, record) => wrap("putRecord", () => db.put(storeName(collection2), record).then(() => void 0)),
|
|
399
|
+
getRecord: (collection2, id) => wrap("getRecord", () => db.get(storeName(collection2), id)),
|
|
400
|
+
getAllRecords: (collection2) => wrap("getAllRecords", () => db.getAll(storeName(collection2))),
|
|
401
|
+
countRecords: (collection2) => wrap("countRecords", () => db.count(storeName(collection2))),
|
|
402
|
+
clearRecords: (collection2) => wrap("clearRecords", () => db.clear(storeName(collection2))),
|
|
403
|
+
getByIndex: (collection2, indexName, value) => wrap("getByIndex", () => db.getAllFromIndex(storeName(collection2), indexName, value)),
|
|
404
|
+
getByIndexRange: (collection2, indexName, range) => wrap("getByIndexRange", () => db.getAllFromIndex(storeName(collection2), indexName, range)),
|
|
405
|
+
getAllSorted: (collection2, indexName, direction) => wrap("getAllSorted", async () => {
|
|
406
|
+
const sn = storeName(collection2);
|
|
407
|
+
const tx = db.transaction(sn, "readonly");
|
|
408
|
+
const store = tx.objectStore(sn);
|
|
409
|
+
const index = store.index(indexName);
|
|
410
|
+
const results = [];
|
|
411
|
+
let cursor = await index.openCursor(null, direction ?? "next");
|
|
412
|
+
while (cursor) {
|
|
413
|
+
results.push(cursor.value);
|
|
414
|
+
cursor = await cursor.continue();
|
|
415
|
+
}
|
|
416
|
+
return results;
|
|
417
|
+
}),
|
|
418
|
+
putEvent: (event) => wrap("putEvent", () => db.put("events", event).then(() => void 0)),
|
|
419
|
+
getEvent: (id) => wrap("getEvent", () => db.get("events", id)),
|
|
420
|
+
getAllEvents: () => wrap("getAllEvents", () => db.getAll("events")),
|
|
421
|
+
getEventsByRecord: (collection2, recordId) => wrap(
|
|
422
|
+
"getEventsByRecord",
|
|
423
|
+
() => db.getAllFromIndex("events", "by-record", [collection2, recordId])
|
|
424
|
+
),
|
|
425
|
+
putGiftWrap: (gw) => wrap("putGiftWrap", async () => {
|
|
426
|
+
if (gw.event) {
|
|
427
|
+
const compact = packEvent(gw.event);
|
|
428
|
+
await db.put("giftwraps", { id: gw.id, compact, createdAt: gw.createdAt });
|
|
429
|
+
} else {
|
|
430
|
+
await db.put("giftwraps", { id: gw.id, createdAt: gw.createdAt });
|
|
431
|
+
}
|
|
432
|
+
}),
|
|
433
|
+
getGiftWrap: (id) => wrap("getGiftWrap", async () => {
|
|
434
|
+
const raw = await db.get("giftwraps", id);
|
|
435
|
+
if (!raw) return void 0;
|
|
436
|
+
if (raw.compact) {
|
|
437
|
+
return { id: raw.id, event: unpackEvent(raw.id, raw.compact), createdAt: raw.createdAt };
|
|
438
|
+
}
|
|
439
|
+
if (raw.event) {
|
|
440
|
+
const compact = packEvent(raw.event);
|
|
441
|
+
await db.put("giftwraps", { id: raw.id, compact, createdAt: raw.createdAt });
|
|
442
|
+
return { id: raw.id, event: raw.event, createdAt: raw.createdAt };
|
|
443
|
+
}
|
|
444
|
+
return { id: raw.id, createdAt: raw.createdAt };
|
|
445
|
+
}),
|
|
446
|
+
getAllGiftWraps: () => wrap("getAllGiftWraps", async () => {
|
|
447
|
+
const raws = await db.getAll("giftwraps");
|
|
448
|
+
const results = [];
|
|
449
|
+
for (const raw of raws) {
|
|
450
|
+
if (raw.compact) {
|
|
451
|
+
results.push({ id: raw.id, event: unpackEvent(raw.id, raw.compact), createdAt: raw.createdAt });
|
|
452
|
+
} else if (raw.event) {
|
|
453
|
+
const compact = packEvent(raw.event);
|
|
454
|
+
await db.put("giftwraps", { id: raw.id, compact, createdAt: raw.createdAt });
|
|
455
|
+
results.push({ id: raw.id, event: raw.event, createdAt: raw.createdAt });
|
|
456
|
+
} else {
|
|
457
|
+
results.push({ id: raw.id, createdAt: raw.createdAt });
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
return results;
|
|
461
|
+
}),
|
|
462
|
+
deleteGiftWrap: (id) => wrap("deleteGiftWrap", () => db.delete("giftwraps", id).then(() => void 0)),
|
|
463
|
+
deleteEvent: (id) => wrap("deleteEvent", () => db.delete("events", id).then(() => void 0)),
|
|
464
|
+
stripEventData: (id) => wrap("stripEventData", async () => {
|
|
465
|
+
const existing = await db.get("events", id);
|
|
466
|
+
if (existing) {
|
|
467
|
+
await db.put("events", { ...existing, data: null });
|
|
468
|
+
}
|
|
469
|
+
}),
|
|
470
|
+
getMeta: (key) => wrap("getMeta", () => db.get("_meta", key)),
|
|
471
|
+
putMeta: (key, value) => wrap("putMeta", () => db.put("_meta", value, key).then(() => void 0)),
|
|
472
|
+
close: () => Effect2.sync(() => db.close())
|
|
473
|
+
};
|
|
474
|
+
return handle;
|
|
475
|
+
});
|
|
476
|
+
}
|
|
477
|
+
|
|
227
478
|
// src/services/Config.ts
|
|
228
479
|
import { ServiceMap } from "effect";
|
|
229
480
|
var Config = class extends ServiceMap.Service()("tablinum/Config") {
|
|
@@ -240,16 +491,16 @@ var Tablinum = class extends ServiceMap2.Service()(
|
|
|
240
491
|
import { Effect as Effect21, Exit, Layer as Layer9, Option as Option9, PubSub as PubSub2, References as References3, Ref as Ref5, Scope as Scope4 } from "effect";
|
|
241
492
|
|
|
242
493
|
// src/crud/watch.ts
|
|
243
|
-
import { Effect as
|
|
494
|
+
import { Effect as Effect3, PubSub, Ref, Stream } from "effect";
|
|
244
495
|
function watchCollection(ctx, storage, collectionName, filter, mapRecord2) {
|
|
245
|
-
const query = () =>
|
|
496
|
+
const query = () => Effect3.map(storage.getAllRecords(collectionName), (all) => {
|
|
246
497
|
const filtered = all.filter((r) => !r._d && (filter ? filter(r) : true));
|
|
247
498
|
return mapRecord2 ? filtered.map(mapRecord2) : filtered;
|
|
248
499
|
});
|
|
249
500
|
const changes = Stream.fromPubSub(ctx.pubsub).pipe(
|
|
250
501
|
Stream.filter((event) => event.collection === collectionName),
|
|
251
502
|
Stream.mapEffect(
|
|
252
|
-
() =>
|
|
503
|
+
() => Effect3.gen(function* () {
|
|
253
504
|
const replaying = yield* Ref.get(ctx.replayingRef);
|
|
254
505
|
if (replaying) return void 0;
|
|
255
506
|
return yield* query();
|
|
@@ -258,18 +509,18 @@ function watchCollection(ctx, storage, collectionName, filter, mapRecord2) {
|
|
|
258
509
|
Stream.filter((result) => result !== void 0)
|
|
259
510
|
);
|
|
260
511
|
return Stream.unwrap(
|
|
261
|
-
|
|
262
|
-
yield*
|
|
512
|
+
Effect3.gen(function* () {
|
|
513
|
+
yield* Effect3.sleep(0);
|
|
263
514
|
const initial = yield* query();
|
|
264
515
|
return Stream.concat(Stream.make(initial), changes);
|
|
265
516
|
})
|
|
266
517
|
);
|
|
267
518
|
}
|
|
268
519
|
function notifyChange(ctx, event) {
|
|
269
|
-
return PubSub.publish(ctx.pubsub, event).pipe(
|
|
520
|
+
return PubSub.publish(ctx.pubsub, event).pipe(Effect3.asVoid);
|
|
270
521
|
}
|
|
271
522
|
function notifyReplayComplete(ctx, collections) {
|
|
272
|
-
return
|
|
523
|
+
return Effect3.gen(function* () {
|
|
273
524
|
yield* Ref.set(ctx.replayingRef, false);
|
|
274
525
|
for (const collection2 of collections) {
|
|
275
526
|
yield* notifyChange(ctx, {
|
|
@@ -282,7 +533,7 @@ function notifyReplayComplete(ctx, collections) {
|
|
|
282
533
|
}
|
|
283
534
|
|
|
284
535
|
// src/storage/records-store.ts
|
|
285
|
-
import { Effect as
|
|
536
|
+
import { Effect as Effect4 } from "effect";
|
|
286
537
|
|
|
287
538
|
// src/storage/lww.ts
|
|
288
539
|
function resolveWinner(existing, incoming) {
|
|
@@ -346,7 +597,7 @@ function buildRecord(event) {
|
|
|
346
597
|
};
|
|
347
598
|
}
|
|
348
599
|
function applyEvent(storage, event) {
|
|
349
|
-
return
|
|
600
|
+
return Effect4.gen(function* () {
|
|
350
601
|
const existing = yield* storage.getRecord(event.collection, event.recordId);
|
|
351
602
|
if (existing) {
|
|
352
603
|
const existingMeta = {
|
|
@@ -366,7 +617,7 @@ function applyEvent(storage, event) {
|
|
|
366
617
|
});
|
|
367
618
|
}
|
|
368
619
|
function rebuild(storage, collections) {
|
|
369
|
-
return
|
|
620
|
+
return Effect4.gen(function* () {
|
|
370
621
|
for (const col of collections) {
|
|
371
622
|
yield* storage.clearRecords(col);
|
|
372
623
|
}
|
|
@@ -382,7 +633,7 @@ function rebuild(storage, collections) {
|
|
|
382
633
|
}
|
|
383
634
|
|
|
384
635
|
// src/schema/validate.ts
|
|
385
|
-
import { Effect as
|
|
636
|
+
import { Effect as Effect5, Schema as Schema3 } from "effect";
|
|
386
637
|
function fieldDefToSchema(fd) {
|
|
387
638
|
let base;
|
|
388
639
|
switch (fd.kind) {
|
|
@@ -401,7 +652,8 @@ function fieldDefToSchema(fd) {
|
|
|
401
652
|
case "object": {
|
|
402
653
|
const nested = {};
|
|
403
654
|
for (const [k, v] of Object.entries(fd.fields)) {
|
|
404
|
-
|
|
655
|
+
const fieldSchema = fieldDefToSchema(v);
|
|
656
|
+
nested[k] = v.isOptional ? Schema3.optionalKey(fieldSchema) : fieldSchema;
|
|
405
657
|
}
|
|
406
658
|
base = Schema3.Struct(nested);
|
|
407
659
|
break;
|
|
@@ -429,10 +681,10 @@ function buildStructSchema(def, options = {}) {
|
|
|
429
681
|
function buildValidator(collectionName, def) {
|
|
430
682
|
const decode = Schema3.decodeUnknownEffect(buildStructSchema(def, { includeId: true }));
|
|
431
683
|
return (input) => decode(input).pipe(
|
|
432
|
-
|
|
684
|
+
Effect5.map(
|
|
433
685
|
(result) => result
|
|
434
686
|
),
|
|
435
|
-
|
|
687
|
+
Effect5.mapError(
|
|
436
688
|
(e) => new ValidationError({
|
|
437
689
|
message: `Validation failed for collection "${collectionName}": ${e.message}`
|
|
438
690
|
})
|
|
@@ -441,7 +693,7 @@ function buildValidator(collectionName, def) {
|
|
|
441
693
|
}
|
|
442
694
|
function buildPartialValidator(collectionName, def) {
|
|
443
695
|
const decode = Schema3.decodeUnknownEffect(buildStructSchema(def, { allOptional: true }));
|
|
444
|
-
return (input) =>
|
|
696
|
+
return (input) => Effect5.gen(function* () {
|
|
445
697
|
if (typeof input !== "object" || input === null) {
|
|
446
698
|
return yield* new ValidationError({
|
|
447
699
|
message: `Validation failed for collection "${collectionName}": expected an object`
|
|
@@ -456,8 +708,8 @@ function buildPartialValidator(collectionName, def) {
|
|
|
456
708
|
});
|
|
457
709
|
}
|
|
458
710
|
return yield* decode(record).pipe(
|
|
459
|
-
|
|
460
|
-
|
|
711
|
+
Effect5.map((result) => result),
|
|
712
|
+
Effect5.mapError(
|
|
461
713
|
(e) => new ValidationError({
|
|
462
714
|
message: `Validation failed for collection "${collectionName}": ${e.message}`
|
|
463
715
|
})
|
|
@@ -467,7 +719,7 @@ function buildPartialValidator(collectionName, def) {
|
|
|
467
719
|
}
|
|
468
720
|
|
|
469
721
|
// src/crud/collection-handle.ts
|
|
470
|
-
import { Effect as
|
|
722
|
+
import { Effect as Effect7, Option as Option3, References } from "effect";
|
|
471
723
|
|
|
472
724
|
// src/utils/uuid.ts
|
|
473
725
|
var alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
|
|
@@ -500,12 +752,12 @@ function uuidv7() {
|
|
|
500
752
|
}
|
|
501
753
|
|
|
502
754
|
// src/crud/query-builder.ts
|
|
503
|
-
import { Effect as
|
|
755
|
+
import { Effect as Effect6, Option as Option2, Ref as Ref2, Stream as Stream2 } from "effect";
|
|
504
756
|
function emptyPlan() {
|
|
505
757
|
return { filters: [] };
|
|
506
758
|
}
|
|
507
759
|
function executeQuery(ctx, plan) {
|
|
508
|
-
return
|
|
760
|
+
return Effect6.gen(function* () {
|
|
509
761
|
if (plan.fieldName) {
|
|
510
762
|
const fieldDef = ctx.def.fields[plan.fieldName];
|
|
511
763
|
if (!fieldDef) {
|
|
@@ -581,7 +833,7 @@ function watchQuery(ctx, plan) {
|
|
|
581
833
|
const changes = Stream2.fromPubSub(ctx.watchCtx.pubsub).pipe(
|
|
582
834
|
Stream2.filter((event) => event.collection === ctx.collectionName),
|
|
583
835
|
Stream2.mapEffect(
|
|
584
|
-
() =>
|
|
836
|
+
() => Effect6.gen(function* () {
|
|
585
837
|
const replaying = yield* Ref2.get(ctx.watchCtx.replayingRef);
|
|
586
838
|
if (replaying) return void 0;
|
|
587
839
|
return yield* query();
|
|
@@ -590,7 +842,7 @@ function watchQuery(ctx, plan) {
|
|
|
590
842
|
Stream2.filter((result) => result !== void 0)
|
|
591
843
|
);
|
|
592
844
|
return Stream2.unwrap(
|
|
593
|
-
|
|
845
|
+
Effect6.gen(function* () {
|
|
594
846
|
const initial = yield* query();
|
|
595
847
|
return Stream2.concat(Stream2.make(initial), changes);
|
|
596
848
|
})
|
|
@@ -616,11 +868,11 @@ function makeQueryBuilder(ctx, plan) {
|
|
|
616
868
|
offset: (n) => makeQueryBuilder(ctx, { ...plan, offset: n }),
|
|
617
869
|
limit: (n) => makeQueryBuilder(ctx, { ...plan, limit: n }),
|
|
618
870
|
get: () => executeQuery(ctx, plan),
|
|
619
|
-
first: () =>
|
|
871
|
+
first: () => Effect6.map(
|
|
620
872
|
executeQuery(ctx, { ...plan, limit: 1 }),
|
|
621
873
|
(results) => results.length > 0 ? Option2.some(results[0]) : Option2.none()
|
|
622
874
|
),
|
|
623
|
-
count: () =>
|
|
875
|
+
count: () => Effect6.map(executeQuery(ctx, plan), (results) => results.length),
|
|
624
876
|
watch: () => watchQuery(ctx, plan)
|
|
625
877
|
};
|
|
626
878
|
}
|
|
@@ -726,7 +978,7 @@ function replayState(recordId, events, stopAtId) {
|
|
|
726
978
|
return state;
|
|
727
979
|
}
|
|
728
980
|
function promoteToSnapshot(storage, collection2, recordId, target, allSorted) {
|
|
729
|
-
return
|
|
981
|
+
return Effect7.gen(function* () {
|
|
730
982
|
const chronological = sortChronologically(allSorted);
|
|
731
983
|
const state = replayState(recordId, chronological, target.id);
|
|
732
984
|
if (state) {
|
|
@@ -735,7 +987,7 @@ function promoteToSnapshot(storage, collection2, recordId, target, allSorted) {
|
|
|
735
987
|
});
|
|
736
988
|
}
|
|
737
989
|
function pruneEvents(storage, collection2, recordId, retention) {
|
|
738
|
-
return
|
|
990
|
+
return Effect7.gen(function* () {
|
|
739
991
|
const events = yield* storage.getEventsByRecord(collection2, recordId);
|
|
740
992
|
if (events.length <= retention) return;
|
|
741
993
|
const sorted = [...events].sort((a, b) => b.createdAt - a.createdAt || (a.id < b.id ? 1 : -1));
|
|
@@ -756,8 +1008,8 @@ function mapRecord(record) {
|
|
|
756
1008
|
}
|
|
757
1009
|
function createCollectionHandle(def, storage, watchCtx, validator, partialValidator, makeEventId, localAuthor, onWrite, logLevel = "None") {
|
|
758
1010
|
const collectionName = def.name;
|
|
759
|
-
const withLog = (effect) =>
|
|
760
|
-
const commitEvent = (event) =>
|
|
1011
|
+
const withLog = (effect) => Effect7.provideService(effect, References.MinimumLogLevel, logLevel);
|
|
1012
|
+
const commitEvent = (event) => Effect7.gen(function* () {
|
|
761
1013
|
yield* storage.putEvent(event);
|
|
762
1014
|
yield* applyEvent(storage, event);
|
|
763
1015
|
if (onWrite) yield* onWrite(event);
|
|
@@ -769,7 +1021,7 @@ function createCollectionHandle(def, storage, watchCtx, validator, partialValida
|
|
|
769
1021
|
});
|
|
770
1022
|
const handle = {
|
|
771
1023
|
add: (data) => withLog(
|
|
772
|
-
|
|
1024
|
+
Effect7.gen(function* () {
|
|
773
1025
|
const id = uuidv7();
|
|
774
1026
|
const fullRecord = { id, ...data };
|
|
775
1027
|
yield* validator(fullRecord);
|
|
@@ -783,7 +1035,7 @@ function createCollectionHandle(def, storage, watchCtx, validator, partialValida
|
|
|
783
1035
|
author: localAuthor
|
|
784
1036
|
};
|
|
785
1037
|
yield* commitEvent(event);
|
|
786
|
-
yield*
|
|
1038
|
+
yield* Effect7.logDebug("Record added", {
|
|
787
1039
|
collection: collectionName,
|
|
788
1040
|
recordId: id,
|
|
789
1041
|
data: fullRecord
|
|
@@ -792,7 +1044,7 @@ function createCollectionHandle(def, storage, watchCtx, validator, partialValida
|
|
|
792
1044
|
})
|
|
793
1045
|
),
|
|
794
1046
|
update: (id, data) => withLog(
|
|
795
|
-
|
|
1047
|
+
Effect7.gen(function* () {
|
|
796
1048
|
const existing = yield* storage.getRecord(collectionName, id);
|
|
797
1049
|
if (!existing || existing._d) {
|
|
798
1050
|
return yield* new NotFoundError({
|
|
@@ -815,7 +1067,7 @@ function createCollectionHandle(def, storage, watchCtx, validator, partialValida
|
|
|
815
1067
|
author: localAuthor
|
|
816
1068
|
};
|
|
817
1069
|
yield* commitEvent(event);
|
|
818
|
-
yield*
|
|
1070
|
+
yield* Effect7.logDebug("Record updated", {
|
|
819
1071
|
collection: collectionName,
|
|
820
1072
|
recordId: id,
|
|
821
1073
|
data: diff
|
|
@@ -824,7 +1076,7 @@ function createCollectionHandle(def, storage, watchCtx, validator, partialValida
|
|
|
824
1076
|
})
|
|
825
1077
|
),
|
|
826
1078
|
delete: (id) => withLog(
|
|
827
|
-
|
|
1079
|
+
Effect7.gen(function* () {
|
|
828
1080
|
const existing = yield* storage.getRecord(collectionName, id);
|
|
829
1081
|
if (!existing || existing._d) {
|
|
830
1082
|
return yield* new NotFoundError({
|
|
@@ -842,11 +1094,11 @@ function createCollectionHandle(def, storage, watchCtx, validator, partialValida
|
|
|
842
1094
|
author: localAuthor
|
|
843
1095
|
};
|
|
844
1096
|
yield* commitEvent(event);
|
|
845
|
-
yield*
|
|
1097
|
+
yield* Effect7.logDebug("Record deleted", { collection: collectionName, recordId: id });
|
|
846
1098
|
yield* pruneEvents(storage, collectionName, id, def.eventRetention);
|
|
847
1099
|
})
|
|
848
1100
|
),
|
|
849
|
-
undo: (id) =>
|
|
1101
|
+
undo: (id) => Effect7.gen(function* () {
|
|
850
1102
|
const existing = yield* storage.getRecord(collectionName, id);
|
|
851
1103
|
if (!existing) {
|
|
852
1104
|
return yield* new NotFoundError({ collection: collectionName, id });
|
|
@@ -871,7 +1123,7 @@ function createCollectionHandle(def, storage, watchCtx, validator, partialValida
|
|
|
871
1123
|
yield* commitEvent(event);
|
|
872
1124
|
yield* pruneEvents(storage, collectionName, id, def.eventRetention);
|
|
873
1125
|
}),
|
|
874
|
-
get: (id) =>
|
|
1126
|
+
get: (id) => Effect7.gen(function* () {
|
|
875
1127
|
const record = yield* storage.getRecord(collectionName, id);
|
|
876
1128
|
if (!record || record._d) {
|
|
877
1129
|
return yield* new NotFoundError({
|
|
@@ -881,11 +1133,11 @@ function createCollectionHandle(def, storage, watchCtx, validator, partialValida
|
|
|
881
1133
|
}
|
|
882
1134
|
return mapRecord(record);
|
|
883
1135
|
}),
|
|
884
|
-
first: () =>
|
|
1136
|
+
first: () => Effect7.map(storage.getAllRecords(collectionName), (all) => {
|
|
885
1137
|
const found = all.find((r) => !r._d);
|
|
886
1138
|
return found ? Option3.some(mapRecord(found)) : Option3.none();
|
|
887
1139
|
}),
|
|
888
|
-
count: () =>
|
|
1140
|
+
count: () => Effect7.map(storage.getAllRecords(collectionName), (all) => all.filter((r) => !r._d).length),
|
|
889
1141
|
watch: () => watchCollection(watchCtx, storage, collectionName, void 0, mapRecord),
|
|
890
1142
|
where: (fieldName) => createWhereClause(
|
|
891
1143
|
storage,
|
|
@@ -908,12 +1160,12 @@ function createCollectionHandle(def, storage, watchCtx, validator, partialValida
|
|
|
908
1160
|
}
|
|
909
1161
|
|
|
910
1162
|
// src/sync/sync-service.ts
|
|
911
|
-
import { Duration, Effect as
|
|
1163
|
+
import { Duration, Effect as Effect9, Layer, Option as Option5, References as References2, Ref as Ref3, Schedule } from "effect";
|
|
912
1164
|
import { unwrapEvent } from "nostr-tools/nip59";
|
|
913
1165
|
import { GiftWrap as GiftWrap2 } from "nostr-tools/kinds";
|
|
914
1166
|
|
|
915
1167
|
// src/sync/negentropy.ts
|
|
916
|
-
import { Effect as
|
|
1168
|
+
import { Effect as Effect8 } from "effect";
|
|
917
1169
|
|
|
918
1170
|
// src/vendor/negentropy.js
|
|
919
1171
|
var PROTOCOL_VERSION = 97;
|
|
@@ -1400,14 +1652,14 @@ function itemCompare(a, b) {
|
|
|
1400
1652
|
}
|
|
1401
1653
|
|
|
1402
1654
|
// src/sync/negentropy.ts
|
|
1403
|
-
import { hexToBytes as
|
|
1655
|
+
import { hexToBytes as hexToBytes3 } from "@noble/hashes/utils.js";
|
|
1404
1656
|
import { GiftWrap } from "nostr-tools/kinds";
|
|
1405
1657
|
function reconcileWithRelay(storage, relay, relayUrl, publicKeys) {
|
|
1406
|
-
return
|
|
1658
|
+
return Effect8.gen(function* () {
|
|
1407
1659
|
const allGiftWraps = yield* storage.getAllGiftWraps();
|
|
1408
1660
|
const storageVector = new NegentropyStorageVector();
|
|
1409
1661
|
for (const gw of allGiftWraps) {
|
|
1410
|
-
storageVector.insert(gw.createdAt,
|
|
1662
|
+
storageVector.insert(gw.createdAt, hexToBytes3(gw.id));
|
|
1411
1663
|
}
|
|
1412
1664
|
storageVector.seal();
|
|
1413
1665
|
const neg = new Negentropy(storageVector, 0);
|
|
@@ -1418,7 +1670,7 @@ function reconcileWithRelay(storage, relay, relayUrl, publicKeys) {
|
|
|
1418
1670
|
const allHaveIds = [];
|
|
1419
1671
|
const allNeedIds = [];
|
|
1420
1672
|
const subId = `neg-${Date.now()}`;
|
|
1421
|
-
const initialMsg = yield*
|
|
1673
|
+
const initialMsg = yield* Effect8.tryPromise({
|
|
1422
1674
|
try: () => neg.initiate(),
|
|
1423
1675
|
catch: (e) => new SyncError({
|
|
1424
1676
|
message: `Negentropy initiate failed: ${e instanceof Error ? e.message : String(e)}`,
|
|
@@ -1430,7 +1682,7 @@ function reconcileWithRelay(storage, relay, relayUrl, publicKeys) {
|
|
|
1430
1682
|
while (currentMsg !== null) {
|
|
1431
1683
|
const response = yield* relay.sendNegMsg(relayUrl, subId, filter, currentMsg);
|
|
1432
1684
|
if (response.msgHex === null) break;
|
|
1433
|
-
const reconcileResult = yield*
|
|
1685
|
+
const reconcileResult = yield* Effect8.tryPromise({
|
|
1434
1686
|
try: () => neg.reconcile(response.msgHex),
|
|
1435
1687
|
catch: (e) => new SyncError({
|
|
1436
1688
|
message: `Negentropy reconcile failed: ${e instanceof Error ? e.message : String(e)}`,
|
|
@@ -1443,13 +1695,13 @@ function reconcileWithRelay(storage, relay, relayUrl, publicKeys) {
|
|
|
1443
1695
|
for (const id of needIds) allNeedIds.push(id);
|
|
1444
1696
|
currentMsg = nextMsg;
|
|
1445
1697
|
}
|
|
1446
|
-
yield*
|
|
1698
|
+
yield* Effect8.logDebug("Negentropy reconciliation complete", {
|
|
1447
1699
|
relay: relayUrl,
|
|
1448
1700
|
have: allHaveIds.length,
|
|
1449
1701
|
need: allNeedIds.length
|
|
1450
1702
|
});
|
|
1451
1703
|
return { haveIds: allHaveIds, needIds: allNeedIds };
|
|
1452
|
-
}).pipe(
|
|
1704
|
+
}).pipe(Effect8.withLogSpan("tablinum.negentropy"));
|
|
1453
1705
|
}
|
|
1454
1706
|
|
|
1455
1707
|
// src/db/key-rotation.ts
|
|
@@ -1543,52 +1795,52 @@ function createSyncHandle(storage, giftWrapHandle, relay, publishQueue, syncStat
|
|
|
1543
1795
|
kind: "create"
|
|
1544
1796
|
});
|
|
1545
1797
|
const forkHandled = (effect) => {
|
|
1546
|
-
|
|
1798
|
+
Effect9.runFork(
|
|
1547
1799
|
effect.pipe(
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1800
|
+
Effect9.tapError((e) => Effect9.sync(() => onSyncError?.(e))),
|
|
1801
|
+
Effect9.ignore,
|
|
1802
|
+
Effect9.provide(logLayer),
|
|
1803
|
+
Effect9.forkIn(scope)
|
|
1552
1804
|
)
|
|
1553
1805
|
);
|
|
1554
1806
|
};
|
|
1555
1807
|
let autoFlushActive = false;
|
|
1556
|
-
const autoFlushEffect =
|
|
1808
|
+
const autoFlushEffect = Effect9.gen(function* () {
|
|
1557
1809
|
const size = yield* publishQueue.size();
|
|
1558
1810
|
if (size === 0) return;
|
|
1559
1811
|
yield* syncStatus.set("syncing");
|
|
1560
1812
|
yield* publishQueue.flush(relayUrls);
|
|
1561
1813
|
const remaining = yield* publishQueue.size();
|
|
1562
|
-
if (remaining > 0) yield*
|
|
1814
|
+
if (remaining > 0) yield* Effect9.fail("pending");
|
|
1563
1815
|
}).pipe(
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1816
|
+
Effect9.ensuring(syncStatus.set("idle")),
|
|
1817
|
+
Effect9.retry({ schedule: Schedule.exponential(5e3).pipe(Schedule.jittered), times: 10 }),
|
|
1818
|
+
Effect9.ignore
|
|
1567
1819
|
);
|
|
1568
1820
|
const scheduleAutoFlush = () => {
|
|
1569
1821
|
if (autoFlushActive) return;
|
|
1570
1822
|
autoFlushActive = true;
|
|
1571
1823
|
forkHandled(
|
|
1572
1824
|
autoFlushEffect.pipe(
|
|
1573
|
-
|
|
1574
|
-
|
|
1825
|
+
Effect9.ensuring(
|
|
1826
|
+
Effect9.sync(() => {
|
|
1575
1827
|
autoFlushActive = false;
|
|
1576
1828
|
})
|
|
1577
1829
|
)
|
|
1578
1830
|
)
|
|
1579
1831
|
);
|
|
1580
1832
|
};
|
|
1581
|
-
const shouldRejectWrite = (authorPubkey) =>
|
|
1833
|
+
const shouldRejectWrite = (authorPubkey) => Effect9.gen(function* () {
|
|
1582
1834
|
const memberRecord = yield* storage.getRecord("_members", authorPubkey);
|
|
1583
1835
|
if (!memberRecord) return false;
|
|
1584
1836
|
return !!memberRecord.removedAt;
|
|
1585
1837
|
});
|
|
1586
1838
|
const storeGiftWrapShell = (gw) => storage.putGiftWrap({ id: gw.id, event: gw, createdAt: gw.created_at });
|
|
1587
|
-
const unwrapGiftWrap = (remoteGw) =>
|
|
1839
|
+
const unwrapGiftWrap = (remoteGw) => Effect9.gen(function* () {
|
|
1588
1840
|
const existing = yield* storage.getGiftWrap(remoteGw.id);
|
|
1589
1841
|
if (existing) return null;
|
|
1590
1842
|
const rumor = yield* giftWrapHandle.unwrap(remoteGw).pipe(
|
|
1591
|
-
|
|
1843
|
+
Effect9.orElseSucceed(() => null)
|
|
1592
1844
|
);
|
|
1593
1845
|
if (!rumor) {
|
|
1594
1846
|
yield* storeGiftWrapShell(remoteGw);
|
|
@@ -1616,7 +1868,7 @@ function createSyncHandle(storage, giftWrapHandle, relay, publishQueue, syncStat
|
|
|
1616
1868
|
recordId: dTag.substring(colonIdx + 1)
|
|
1617
1869
|
};
|
|
1618
1870
|
});
|
|
1619
|
-
const applyUnwrappedEvent = (uw) =>
|
|
1871
|
+
const applyUnwrappedEvent = (uw) => Effect9.gen(function* () {
|
|
1620
1872
|
const { giftWrap: remoteGw, rumor, collection: collectionName, recordId } = uw;
|
|
1621
1873
|
const retention = knownCollections.get(collectionName);
|
|
1622
1874
|
if (retention === void 0) {
|
|
@@ -1626,17 +1878,17 @@ function createSyncHandle(storage, giftWrapHandle, relay, publishQueue, syncStat
|
|
|
1626
1878
|
if (rumor.pubkey) {
|
|
1627
1879
|
const reject = yield* shouldRejectWrite(rumor.pubkey);
|
|
1628
1880
|
if (reject) {
|
|
1629
|
-
yield*
|
|
1881
|
+
yield* Effect9.logWarning("Rejected write from removed member", {
|
|
1630
1882
|
author: rumor.pubkey.slice(0, 12)
|
|
1631
1883
|
});
|
|
1632
1884
|
yield* storeGiftWrapShell(remoteGw);
|
|
1633
1885
|
return null;
|
|
1634
1886
|
}
|
|
1635
1887
|
}
|
|
1636
|
-
const parsed = yield*
|
|
1888
|
+
const parsed = yield* Effect9.try({
|
|
1637
1889
|
try: () => JSON.parse(rumor.content),
|
|
1638
1890
|
catch: () => void 0
|
|
1639
|
-
}).pipe(
|
|
1891
|
+
}).pipe(Effect9.orElseSucceed(() => void 0));
|
|
1640
1892
|
if (parsed === void 0) {
|
|
1641
1893
|
yield* storeGiftWrapShell(remoteGw);
|
|
1642
1894
|
return null;
|
|
@@ -1668,7 +1920,7 @@ function createSyncHandle(storage, giftWrapHandle, relay, publishQueue, syncStat
|
|
|
1668
1920
|
if (didApply && (kind === "u" || kind === "d")) {
|
|
1669
1921
|
yield* pruneEvents(storage, collectionName, recordId, retention);
|
|
1670
1922
|
}
|
|
1671
|
-
yield*
|
|
1923
|
+
yield* Effect9.logDebug("Processed gift wrap", {
|
|
1672
1924
|
collection: collectionName,
|
|
1673
1925
|
recordId,
|
|
1674
1926
|
kind,
|
|
@@ -1679,33 +1931,33 @@ function createSyncHandle(storage, giftWrapHandle, relay, publishQueue, syncStat
|
|
|
1679
1931
|
}
|
|
1680
1932
|
return collectionName;
|
|
1681
1933
|
});
|
|
1682
|
-
const reconcileRelay = (url, pubKeys) =>
|
|
1683
|
-
yield*
|
|
1934
|
+
const reconcileRelay = (url, pubKeys) => Effect9.gen(function* () {
|
|
1935
|
+
yield* Effect9.logDebug("Syncing relay", { relay: url });
|
|
1684
1936
|
const { haveIds, needIds } = yield* reconcileWithRelay(
|
|
1685
1937
|
storage,
|
|
1686
1938
|
relay,
|
|
1687
1939
|
url,
|
|
1688
1940
|
Array.from(pubKeys)
|
|
1689
1941
|
).pipe(
|
|
1690
|
-
|
|
1691
|
-
|
|
1942
|
+
Effect9.tapError((err) => Effect9.sync(() => onSyncError?.(err))),
|
|
1943
|
+
Effect9.orElseSucceed(() => ({ haveIds: [], needIds: [] }))
|
|
1692
1944
|
);
|
|
1693
|
-
yield*
|
|
1945
|
+
yield* Effect9.logDebug("Relay reconciliation result", {
|
|
1694
1946
|
relay: url,
|
|
1695
1947
|
need: needIds.length,
|
|
1696
1948
|
have: haveIds.length
|
|
1697
1949
|
});
|
|
1698
1950
|
const events = needIds.length > 0 ? yield* relay.fetchEvents(needIds, url).pipe(
|
|
1699
|
-
|
|
1700
|
-
|
|
1951
|
+
Effect9.tapError((err) => Effect9.sync(() => onSyncError?.(err))),
|
|
1952
|
+
Effect9.orElseSucceed(() => [])
|
|
1701
1953
|
) : [];
|
|
1702
1954
|
return {
|
|
1703
1955
|
events,
|
|
1704
1956
|
haveIds: haveIds.map((id) => ({ id, url }))
|
|
1705
1957
|
};
|
|
1706
|
-
}).pipe(
|
|
1707
|
-
const syncAllRelays = (pubKeys, changedCollections) =>
|
|
1708
|
-
const results = yield*
|
|
1958
|
+
}).pipe(Effect9.withLogSpan("tablinum.reconcileRelay"));
|
|
1959
|
+
const syncAllRelays = (pubKeys, changedCollections) => Effect9.gen(function* () {
|
|
1960
|
+
const results = yield* Effect9.forEach(
|
|
1709
1961
|
relayUrls,
|
|
1710
1962
|
(url) => reconcileRelay(url, pubKeys),
|
|
1711
1963
|
{ concurrency: "unbounded" }
|
|
@@ -1722,44 +1974,44 @@ function createSyncHandle(storage, giftWrapHandle, relay, publishQueue, syncStat
|
|
|
1722
1974
|
}
|
|
1723
1975
|
const unwrapped = [];
|
|
1724
1976
|
for (const gw of allGiftWraps) {
|
|
1725
|
-
const result = yield* unwrapGiftWrap(gw).pipe(
|
|
1977
|
+
const result = yield* unwrapGiftWrap(gw).pipe(Effect9.orElseSucceed(() => null));
|
|
1726
1978
|
if (result) unwrapped.push(result);
|
|
1727
1979
|
}
|
|
1728
1980
|
unwrapped.sort((a, b) => a.rumor.created_at - b.rumor.created_at || (a.rumor.id < b.rumor.id ? -1 : 1));
|
|
1729
1981
|
for (const event of unwrapped) {
|
|
1730
1982
|
const collection2 = yield* applyUnwrappedEvent(event).pipe(
|
|
1731
|
-
|
|
1983
|
+
Effect9.orElseSucceed(() => null)
|
|
1732
1984
|
);
|
|
1733
1985
|
if (collection2) changedCollections.add(collection2);
|
|
1734
1986
|
}
|
|
1735
1987
|
const allHaveIds = results.flatMap((r) => r.haveIds);
|
|
1736
|
-
yield*
|
|
1988
|
+
yield* Effect9.forEach(
|
|
1737
1989
|
allHaveIds,
|
|
1738
|
-
({ id, url }) =>
|
|
1990
|
+
({ id, url }) => Effect9.gen(function* () {
|
|
1739
1991
|
const gw = yield* storage.getGiftWrap(id);
|
|
1740
1992
|
if (!gw?.event) return;
|
|
1741
1993
|
yield* relay.publish(gw.event, [url]).pipe(
|
|
1742
|
-
|
|
1743
|
-
|
|
1994
|
+
Effect9.tapError((err) => Effect9.sync(() => onSyncError?.(err))),
|
|
1995
|
+
Effect9.ignore
|
|
1744
1996
|
);
|
|
1745
1997
|
}),
|
|
1746
1998
|
{ concurrency: "unbounded", discard: true }
|
|
1747
1999
|
);
|
|
1748
|
-
}).pipe(
|
|
1749
|
-
const processGiftWrap = (remoteGw) =>
|
|
2000
|
+
}).pipe(Effect9.withLogSpan("tablinum.syncAllRelays"));
|
|
2001
|
+
const processGiftWrap = (remoteGw) => Effect9.gen(function* () {
|
|
1750
2002
|
const uw = yield* unwrapGiftWrap(remoteGw);
|
|
1751
2003
|
if (!uw) return null;
|
|
1752
2004
|
return yield* applyUnwrappedEvent(uw);
|
|
1753
2005
|
});
|
|
1754
|
-
const processRealtimeGiftWrap = (remoteGw) =>
|
|
1755
|
-
const collection2 = yield* processGiftWrap(remoteGw).pipe(
|
|
2006
|
+
const processRealtimeGiftWrap = (remoteGw) => Effect9.gen(function* () {
|
|
2007
|
+
const collection2 = yield* processGiftWrap(remoteGw).pipe(Effect9.orElseSucceed(() => null));
|
|
1756
2008
|
if (collection2) {
|
|
1757
2009
|
yield* notifyCollectionUpdated(collection2);
|
|
1758
2010
|
}
|
|
1759
2011
|
});
|
|
1760
|
-
const processRotationGiftWrap = (remoteGw) =>
|
|
1761
|
-
const unwrapResult = yield*
|
|
1762
|
-
|
|
2012
|
+
const processRotationGiftWrap = (remoteGw) => Effect9.gen(function* () {
|
|
2013
|
+
const unwrapResult = yield* Effect9.result(
|
|
2014
|
+
Effect9.try({
|
|
1763
2015
|
try: () => unwrapEvent(remoteGw, personalPrivateKey),
|
|
1764
2016
|
catch: (e) => new CryptoError({
|
|
1765
2017
|
message: `Rotation unwrap failed: ${e instanceof Error ? e.message : String(e)}`,
|
|
@@ -1809,73 +2061,73 @@ function createSyncHandle(storage, giftWrapHandle, relay, publishQueue, syncStat
|
|
|
1809
2061
|
yield* handle.addEpochSubscription(epoch.publicKey);
|
|
1810
2062
|
return true;
|
|
1811
2063
|
});
|
|
1812
|
-
const subscribeAcrossRelays = (filter, onEvent) =>
|
|
2064
|
+
const subscribeAcrossRelays = (filter, onEvent) => Effect9.forEach(
|
|
1813
2065
|
relayUrls,
|
|
1814
|
-
(url) =>
|
|
2066
|
+
(url) => Effect9.gen(function* () {
|
|
1815
2067
|
yield* relay.subscribe(filter, url, (event) => {
|
|
1816
2068
|
forkHandled(onEvent(event));
|
|
1817
2069
|
}).pipe(
|
|
1818
|
-
|
|
1819
|
-
|
|
2070
|
+
Effect9.tapError((err) => Effect9.sync(() => onSyncError?.(err))),
|
|
2071
|
+
Effect9.ignore
|
|
1820
2072
|
);
|
|
1821
2073
|
}),
|
|
1822
2074
|
{ concurrency: "unbounded", discard: true }
|
|
1823
2075
|
);
|
|
1824
2076
|
let healingActive = false;
|
|
1825
|
-
const healingEffect =
|
|
2077
|
+
const healingEffect = Effect9.gen(function* () {
|
|
1826
2078
|
if (!healingActive) return;
|
|
1827
2079
|
const status = yield* syncStatus.get();
|
|
1828
2080
|
if (status === "syncing") return;
|
|
1829
2081
|
yield* syncStatus.set("syncing");
|
|
1830
|
-
yield*
|
|
2082
|
+
yield* Effect9.gen(function* () {
|
|
1831
2083
|
const pubKeys = getSubscriptionPubKeys();
|
|
1832
2084
|
const changedCollections = /* @__PURE__ */ new Set();
|
|
1833
2085
|
yield* syncAllRelays(pubKeys, changedCollections);
|
|
1834
2086
|
if (changedCollections.size > 0) {
|
|
1835
2087
|
yield* notifyReplayComplete(watchCtx, [...changedCollections]);
|
|
1836
2088
|
}
|
|
1837
|
-
}).pipe(
|
|
1838
|
-
}).pipe(
|
|
2089
|
+
}).pipe(Effect9.ensuring(syncStatus.set("idle")));
|
|
2090
|
+
}).pipe(Effect9.ignore);
|
|
1839
2091
|
const handle = {
|
|
1840
|
-
sync: () =>
|
|
1841
|
-
yield*
|
|
2092
|
+
sync: () => Effect9.gen(function* () {
|
|
2093
|
+
yield* Effect9.logInfo("Sync started");
|
|
1842
2094
|
yield* syncStatus.set("syncing");
|
|
1843
2095
|
yield* Ref3.set(watchCtx.replayingRef, true);
|
|
1844
2096
|
const changedCollections = /* @__PURE__ */ new Set();
|
|
1845
|
-
yield*
|
|
2097
|
+
yield* Effect9.gen(function* () {
|
|
1846
2098
|
const pubKeys = getSubscriptionPubKeys();
|
|
1847
2099
|
yield* syncAllRelays(pubKeys, changedCollections);
|
|
1848
|
-
yield* publishQueue.flush(relayUrls).pipe(
|
|
2100
|
+
yield* publishQueue.flush(relayUrls).pipe(Effect9.ignore);
|
|
1849
2101
|
}).pipe(
|
|
1850
|
-
|
|
1851
|
-
|
|
2102
|
+
Effect9.ensuring(
|
|
2103
|
+
Effect9.gen(function* () {
|
|
1852
2104
|
yield* notifyReplayComplete(watchCtx, [...changedCollections]);
|
|
1853
2105
|
yield* syncStatus.set("idle");
|
|
1854
2106
|
})
|
|
1855
2107
|
)
|
|
1856
2108
|
);
|
|
1857
|
-
yield*
|
|
1858
|
-
}).pipe(
|
|
1859
|
-
publishLocal: (giftWrap) =>
|
|
2109
|
+
yield* Effect9.logInfo("Sync complete", { changed: [...changedCollections] });
|
|
2110
|
+
}).pipe(Effect9.withLogSpan("tablinum.sync")),
|
|
2111
|
+
publishLocal: (giftWrap) => Effect9.gen(function* () {
|
|
1860
2112
|
if (!giftWrap.event) return;
|
|
1861
2113
|
yield* relay.publish(giftWrap.event, relayUrls).pipe(
|
|
1862
|
-
|
|
2114
|
+
Effect9.tapError(
|
|
1863
2115
|
() => storage.putGiftWrap(giftWrap).pipe(
|
|
1864
|
-
|
|
1865
|
-
|
|
2116
|
+
Effect9.andThen(publishQueue.enqueue(giftWrap.id)),
|
|
2117
|
+
Effect9.andThen(Effect9.sync(() => scheduleAutoFlush()))
|
|
1866
2118
|
)
|
|
1867
2119
|
),
|
|
1868
|
-
|
|
1869
|
-
|
|
2120
|
+
Effect9.tapError((err) => Effect9.sync(() => onSyncError?.(err))),
|
|
2121
|
+
Effect9.ignore
|
|
1870
2122
|
);
|
|
1871
2123
|
}),
|
|
1872
|
-
startSubscription: () =>
|
|
2124
|
+
startSubscription: () => Effect9.gen(function* () {
|
|
1873
2125
|
const pubKeys = getSubscriptionPubKeys();
|
|
1874
2126
|
yield* subscribeAcrossRelays({ kinds: [GiftWrap2], "#p": pubKeys }, processRealtimeGiftWrap);
|
|
1875
2127
|
if (!pubKeys.includes(personalPublicKey)) {
|
|
1876
2128
|
yield* subscribeAcrossRelays(
|
|
1877
2129
|
{ kinds: [GiftWrap2], "#p": [personalPublicKey] },
|
|
1878
|
-
(event) =>
|
|
2130
|
+
(event) => Effect9.result(processRotationGiftWrap(event)).pipe(Effect9.asVoid)
|
|
1879
2131
|
);
|
|
1880
2132
|
}
|
|
1881
2133
|
}),
|
|
@@ -1884,10 +2136,10 @@ function createSyncHandle(storage, giftWrapHandle, relay, publishQueue, syncStat
|
|
|
1884
2136
|
if (healingActive) return;
|
|
1885
2137
|
healingActive = true;
|
|
1886
2138
|
forkHandled(
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
2139
|
+
Effect9.sleep(Duration.minutes(5)).pipe(
|
|
2140
|
+
Effect9.andThen(healingEffect),
|
|
2141
|
+
Effect9.repeat(Schedule.spaced(Duration.minutes(5))),
|
|
2142
|
+
Effect9.ensuring(Effect9.sync(() => {
|
|
1891
2143
|
healingActive = false;
|
|
1892
2144
|
}))
|
|
1893
2145
|
)
|
|
@@ -1899,8 +2151,8 @@ function createSyncHandle(storage, giftWrapHandle, relay, publishQueue, syncStat
|
|
|
1899
2151
|
};
|
|
1900
2152
|
forkHandled(
|
|
1901
2153
|
publishQueue.size().pipe(
|
|
1902
|
-
|
|
1903
|
-
(size) =>
|
|
2154
|
+
Effect9.flatMap(
|
|
2155
|
+
(size) => Effect9.sync(() => {
|
|
1904
2156
|
if (size > 0) scheduleAutoFlush();
|
|
1905
2157
|
})
|
|
1906
2158
|
)
|
|
@@ -1910,7 +2162,7 @@ function createSyncHandle(storage, giftWrapHandle, relay, publishQueue, syncStat
|
|
|
1910
2162
|
}
|
|
1911
2163
|
|
|
1912
2164
|
// src/db/members.ts
|
|
1913
|
-
import { Effect as
|
|
2165
|
+
import { Effect as Effect10, Option as Option6, Schema as Schema5 } from "effect";
|
|
1914
2166
|
var optionalString = {
|
|
1915
2167
|
_tag: "FieldDef",
|
|
1916
2168
|
kind: "string",
|
|
@@ -1959,15 +2211,15 @@ var AuthorProfileSchema = Schema5.Struct({
|
|
|
1959
2211
|
});
|
|
1960
2212
|
var decodeAuthorProfile = Schema5.decodeUnknownEffect(Schema5.fromJsonString(AuthorProfileSchema));
|
|
1961
2213
|
function fetchAuthorProfile(relay, relayUrls, pubkey) {
|
|
1962
|
-
return
|
|
2214
|
+
return Effect10.gen(function* () {
|
|
1963
2215
|
for (const url of relayUrls) {
|
|
1964
|
-
const result = yield*
|
|
2216
|
+
const result = yield* Effect10.result(
|
|
1965
2217
|
relay.fetchByFilter({ kinds: [0], authors: [pubkey], limit: 1 }, url)
|
|
1966
2218
|
);
|
|
1967
2219
|
if (result._tag === "Success" && result.success.length > 0) {
|
|
1968
2220
|
return yield* decodeAuthorProfile(result.success[0].content).pipe(
|
|
1969
|
-
|
|
1970
|
-
|
|
2221
|
+
Effect10.map(Option6.some),
|
|
2222
|
+
Effect10.orElseSucceed(() => Option6.none())
|
|
1971
2223
|
);
|
|
1972
2224
|
}
|
|
1973
2225
|
}
|
|
@@ -2017,15 +2269,15 @@ var SyncStatus = class extends ServiceMap9.Service()(
|
|
|
2017
2269
|
};
|
|
2018
2270
|
|
|
2019
2271
|
// src/layers/IdentityLive.ts
|
|
2020
|
-
import { Effect as
|
|
2021
|
-
import { hexToBytes as
|
|
2272
|
+
import { Effect as Effect12, Layer as Layer2 } from "effect";
|
|
2273
|
+
import { hexToBytes as hexToBytes4 } from "@noble/hashes/utils.js";
|
|
2022
2274
|
|
|
2023
2275
|
// src/db/identity.ts
|
|
2024
|
-
import { Effect as
|
|
2276
|
+
import { Effect as Effect11 } from "effect";
|
|
2025
2277
|
import { getPublicKey as getPublicKey2 } from "nostr-tools/pure";
|
|
2026
|
-
import { bytesToHex as
|
|
2278
|
+
import { bytesToHex as bytesToHex3 } from "@noble/hashes/utils.js";
|
|
2027
2279
|
function createIdentity(suppliedKey) {
|
|
2028
|
-
return
|
|
2280
|
+
return Effect11.gen(function* () {
|
|
2029
2281
|
let privateKey;
|
|
2030
2282
|
if (suppliedKey) {
|
|
2031
2283
|
if (suppliedKey.length !== 32) {
|
|
@@ -2038,8 +2290,8 @@ function createIdentity(suppliedKey) {
|
|
|
2038
2290
|
privateKey = new Uint8Array(32);
|
|
2039
2291
|
crypto.getRandomValues(privateKey);
|
|
2040
2292
|
}
|
|
2041
|
-
const privateKeyHex =
|
|
2042
|
-
const publicKey = yield*
|
|
2293
|
+
const privateKeyHex = bytesToHex3(privateKey);
|
|
2294
|
+
const publicKey = yield* Effect11.try({
|
|
2043
2295
|
try: () => getPublicKey2(privateKey),
|
|
2044
2296
|
catch: (e) => new CryptoError({
|
|
2045
2297
|
message: `Failed to derive public key: ${e instanceof Error ? e.message : String(e)}`,
|
|
@@ -2057,14 +2309,14 @@ function createIdentity(suppliedKey) {
|
|
|
2057
2309
|
// src/layers/IdentityLive.ts
|
|
2058
2310
|
var IdentityLive = Layer2.effect(
|
|
2059
2311
|
Identity,
|
|
2060
|
-
|
|
2312
|
+
Effect12.gen(function* () {
|
|
2061
2313
|
const config = yield* Config;
|
|
2062
2314
|
const storage = yield* Storage;
|
|
2063
2315
|
const idbKey = yield* storage.getMeta("identity_key");
|
|
2064
|
-
const resolvedKey = config.privateKey ?? (typeof idbKey === "string" && idbKey.length === 64 ?
|
|
2316
|
+
const resolvedKey = config.privateKey ?? (typeof idbKey === "string" && idbKey.length === 64 ? hexToBytes4(idbKey) : void 0);
|
|
2065
2317
|
const identity = yield* createIdentity(resolvedKey);
|
|
2066
2318
|
yield* storage.putMeta("identity_key", identity.exportKey());
|
|
2067
|
-
yield*
|
|
2319
|
+
yield* Effect12.logInfo("Identity loaded", {
|
|
2068
2320
|
publicKey: identity.publicKey.slice(0, 12) + "...",
|
|
2069
2321
|
source: config.privateKey ? "config" : resolvedKey ? "storage" : "generated"
|
|
2070
2322
|
});
|
|
@@ -2073,12 +2325,12 @@ var IdentityLive = Layer2.effect(
|
|
|
2073
2325
|
);
|
|
2074
2326
|
|
|
2075
2327
|
// src/layers/EpochStoreLive.ts
|
|
2076
|
-
import { Effect as
|
|
2328
|
+
import { Effect as Effect13, Layer as Layer3, Option as Option7 } from "effect";
|
|
2077
2329
|
import { generateSecretKey as generateSecretKey2 } from "nostr-tools/pure";
|
|
2078
|
-
import { bytesToHex as
|
|
2330
|
+
import { bytesToHex as bytesToHex4 } from "@noble/hashes/utils.js";
|
|
2079
2331
|
var EpochStoreLive = Layer3.effect(
|
|
2080
2332
|
EpochStore,
|
|
2081
|
-
|
|
2333
|
+
Effect13.gen(function* () {
|
|
2082
2334
|
const config = yield* Config;
|
|
2083
2335
|
const identity = yield* Identity;
|
|
2084
2336
|
const storage = yield* Storage;
|
|
@@ -2097,7 +2349,7 @@ var EpochStoreLive = Layer3.effect(
|
|
|
2097
2349
|
return existing !== void 0 && existing.privateKey === ek.key;
|
|
2098
2350
|
});
|
|
2099
2351
|
if (configIsSubset) {
|
|
2100
|
-
yield*
|
|
2352
|
+
yield* Effect13.logInfo("Epoch store loaded", {
|
|
2101
2353
|
source: "storage",
|
|
2102
2354
|
epochs: idbStore.epochs.size
|
|
2103
2355
|
});
|
|
@@ -2106,271 +2358,28 @@ var EpochStoreLive = Layer3.effect(
|
|
|
2106
2358
|
}
|
|
2107
2359
|
const store2 = createEpochStoreFromInputs(config.epochKeys);
|
|
2108
2360
|
yield* storage.putMeta("epochs", stringifyEpochStore(store2));
|
|
2109
|
-
yield*
|
|
2361
|
+
yield* Effect13.logInfo("Epoch store loaded", { source: "config", epochs: store2.epochs.size });
|
|
2110
2362
|
return store2;
|
|
2111
2363
|
}
|
|
2112
2364
|
if (idbStore) {
|
|
2113
|
-
yield*
|
|
2365
|
+
yield* Effect13.logInfo("Epoch store loaded", {
|
|
2114
2366
|
source: "storage",
|
|
2115
2367
|
epochs: idbStore.epochs.size
|
|
2116
2368
|
});
|
|
2117
2369
|
return idbStore;
|
|
2118
2370
|
}
|
|
2119
2371
|
const store = createEpochStoreFromInputs(
|
|
2120
|
-
[{ epochId: EpochId("epoch-0"), key:
|
|
2372
|
+
[{ epochId: EpochId("epoch-0"), key: bytesToHex4(generateSecretKey2()) }],
|
|
2121
2373
|
{ createdBy: identity.publicKey }
|
|
2122
2374
|
);
|
|
2123
2375
|
yield* storage.putMeta("epochs", stringifyEpochStore(store));
|
|
2124
|
-
yield*
|
|
2376
|
+
yield* Effect13.logInfo("Epoch store loaded", { source: "generated", epochs: store.epochs.size });
|
|
2125
2377
|
return store;
|
|
2126
2378
|
})
|
|
2127
2379
|
);
|
|
2128
2380
|
|
|
2129
2381
|
// src/layers/StorageLive.ts
|
|
2130
2382
|
import { Effect as Effect14, Layer as Layer4 } from "effect";
|
|
2131
|
-
|
|
2132
|
-
// src/storage/idb.ts
|
|
2133
|
-
import { Effect as Effect13 } from "effect";
|
|
2134
|
-
import { openDB } from "idb";
|
|
2135
|
-
|
|
2136
|
-
// src/sync/compact-event.ts
|
|
2137
|
-
import { bytesToHex as bytesToHex4, hexToBytes as hexToBytes4 } from "@noble/hashes/utils.js";
|
|
2138
|
-
var VERSION = 1;
|
|
2139
|
-
var HEADER_SIZE = 133;
|
|
2140
|
-
function base64ToBytes(base64) {
|
|
2141
|
-
const binary = atob(base64);
|
|
2142
|
-
const bytes = new Uint8Array(binary.length);
|
|
2143
|
-
for (let i = 0; i < binary.length; i++) {
|
|
2144
|
-
bytes[i] = binary.charCodeAt(i);
|
|
2145
|
-
}
|
|
2146
|
-
return bytes;
|
|
2147
|
-
}
|
|
2148
|
-
function bytesToBase64(bytes) {
|
|
2149
|
-
let binary = "";
|
|
2150
|
-
for (let i = 0; i < bytes.length; i++) {
|
|
2151
|
-
binary += String.fromCharCode(bytes[i]);
|
|
2152
|
-
}
|
|
2153
|
-
return btoa(binary);
|
|
2154
|
-
}
|
|
2155
|
-
function packEvent(event) {
|
|
2156
|
-
const pubkey = hexToBytes4(event.pubkey);
|
|
2157
|
-
const sig = hexToBytes4(event.sig);
|
|
2158
|
-
const recipientTag = event.tags.find((t) => t[0] === "p");
|
|
2159
|
-
if (!recipientTag) throw new Error("Gift wrap missing #p tag");
|
|
2160
|
-
if (event.tags.some((t) => t[0] !== "p")) {
|
|
2161
|
-
throw new Error("Gift wrap has unexpected non-p tags; compact encoding would lose them");
|
|
2162
|
-
}
|
|
2163
|
-
const recipient = hexToBytes4(recipientTag[1]);
|
|
2164
|
-
const createdAtBuf = new Uint8Array(4);
|
|
2165
|
-
new DataView(createdAtBuf.buffer).setUint32(0, event.created_at, false);
|
|
2166
|
-
const content = base64ToBytes(event.content);
|
|
2167
|
-
const result = new Uint8Array(HEADER_SIZE + content.length);
|
|
2168
|
-
result[0] = VERSION;
|
|
2169
|
-
result.set(pubkey, 1);
|
|
2170
|
-
result.set(sig, 33);
|
|
2171
|
-
result.set(recipient, 97);
|
|
2172
|
-
result.set(createdAtBuf, 129);
|
|
2173
|
-
result.set(content, HEADER_SIZE);
|
|
2174
|
-
return result;
|
|
2175
|
-
}
|
|
2176
|
-
function unpackEvent(id, compact) {
|
|
2177
|
-
const version = compact[0];
|
|
2178
|
-
if (version !== VERSION) throw new Error(`Unknown compact event version: ${version}`);
|
|
2179
|
-
const pubkey = bytesToHex4(compact.slice(1, 33));
|
|
2180
|
-
const sig = bytesToHex4(compact.slice(33, 97));
|
|
2181
|
-
const recipient = bytesToHex4(compact.slice(97, 129));
|
|
2182
|
-
const dv = new DataView(compact.buffer, compact.byteOffset + 129, 4);
|
|
2183
|
-
const createdAt = dv.getUint32(0, false);
|
|
2184
|
-
const content = bytesToBase64(compact.slice(HEADER_SIZE));
|
|
2185
|
-
return {
|
|
2186
|
-
id,
|
|
2187
|
-
pubkey,
|
|
2188
|
-
sig,
|
|
2189
|
-
created_at: createdAt,
|
|
2190
|
-
kind: 1059,
|
|
2191
|
-
tags: [["p", recipient]],
|
|
2192
|
-
content
|
|
2193
|
-
};
|
|
2194
|
-
}
|
|
2195
|
-
|
|
2196
|
-
// src/storage/idb.ts
|
|
2197
|
-
var DB_NAME = "tablinum";
|
|
2198
|
-
function storeName(collection2) {
|
|
2199
|
-
return `col_${collection2}`;
|
|
2200
|
-
}
|
|
2201
|
-
function computeSchemaSig(schema) {
|
|
2202
|
-
return Object.entries(schema).sort(([a], [b]) => a.localeCompare(b)).map(([name, def]) => {
|
|
2203
|
-
const indices = [...def.indices ?? []].sort().join(",");
|
|
2204
|
-
return `${name}:${indices}`;
|
|
2205
|
-
}).join("|");
|
|
2206
|
-
}
|
|
2207
|
-
function wrap(label, fn) {
|
|
2208
|
-
return Effect13.tryPromise({
|
|
2209
|
-
try: fn,
|
|
2210
|
-
catch: (e) => new StorageError({
|
|
2211
|
-
message: `IndexedDB ${label} failed: ${e instanceof Error ? e.message : String(e)}`,
|
|
2212
|
-
cause: e
|
|
2213
|
-
})
|
|
2214
|
-
});
|
|
2215
|
-
}
|
|
2216
|
-
function upgradeSchema(database, schema, tx) {
|
|
2217
|
-
if (!database.objectStoreNames.contains("_meta")) {
|
|
2218
|
-
database.createObjectStore("_meta");
|
|
2219
|
-
}
|
|
2220
|
-
if (!database.objectStoreNames.contains("events")) {
|
|
2221
|
-
const events = database.createObjectStore("events", { keyPath: "id" });
|
|
2222
|
-
events.createIndex("by-record", ["collection", "recordId"]);
|
|
2223
|
-
}
|
|
2224
|
-
if (!database.objectStoreNames.contains("giftwraps")) {
|
|
2225
|
-
database.createObjectStore("giftwraps", { keyPath: "id" });
|
|
2226
|
-
}
|
|
2227
|
-
const expectedStores = /* @__PURE__ */ new Set();
|
|
2228
|
-
for (const [, def] of Object.entries(schema)) {
|
|
2229
|
-
const sn = storeName(def.name);
|
|
2230
|
-
expectedStores.add(sn);
|
|
2231
|
-
if (!database.objectStoreNames.contains(sn)) {
|
|
2232
|
-
const store = database.createObjectStore(sn, { keyPath: "id" });
|
|
2233
|
-
for (const idx of def.indices ?? []) {
|
|
2234
|
-
store.createIndex(idx, idx);
|
|
2235
|
-
}
|
|
2236
|
-
} else {
|
|
2237
|
-
const store = tx.objectStore(sn);
|
|
2238
|
-
const existingIndices = new Set(Array.from(store.indexNames));
|
|
2239
|
-
const wantedIndices = new Set(def.indices ?? []);
|
|
2240
|
-
for (const idx of existingIndices) {
|
|
2241
|
-
if (!wantedIndices.has(idx)) store.deleteIndex(idx);
|
|
2242
|
-
}
|
|
2243
|
-
for (const idx of wantedIndices) {
|
|
2244
|
-
if (!existingIndices.has(idx)) store.createIndex(idx, idx);
|
|
2245
|
-
}
|
|
2246
|
-
}
|
|
2247
|
-
}
|
|
2248
|
-
for (const existing of Array.from(database.objectStoreNames)) {
|
|
2249
|
-
if (existing.startsWith("col_") && !expectedStores.has(existing)) {
|
|
2250
|
-
database.deleteObjectStore(existing);
|
|
2251
|
-
}
|
|
2252
|
-
}
|
|
2253
|
-
tx.objectStore("_meta").put(computeSchemaSig(schema), "schema_sig");
|
|
2254
|
-
}
|
|
2255
|
-
function openIDBStorage(dbName, schema) {
|
|
2256
|
-
return Effect13.gen(function* () {
|
|
2257
|
-
const name = dbName ?? DB_NAME;
|
|
2258
|
-
const schemaSig = computeSchemaSig(schema);
|
|
2259
|
-
if (typeof indexedDB === "undefined") {
|
|
2260
|
-
return yield* Effect13.fail(
|
|
2261
|
-
new StorageError({
|
|
2262
|
-
message: "IndexedDB is not available in this environment"
|
|
2263
|
-
})
|
|
2264
|
-
);
|
|
2265
|
-
}
|
|
2266
|
-
const probeDb = yield* Effect13.tryPromise({
|
|
2267
|
-
try: () => openDB(name),
|
|
2268
|
-
catch: (e) => new StorageError({ message: "Failed to open IndexedDB", cause: e })
|
|
2269
|
-
});
|
|
2270
|
-
const currentVersion = probeDb.version;
|
|
2271
|
-
let needsUpgrade = true;
|
|
2272
|
-
if (probeDb.objectStoreNames.contains("_meta")) {
|
|
2273
|
-
const storedSig = yield* Effect13.tryPromise({
|
|
2274
|
-
try: () => probeDb.get("_meta", "schema_sig"),
|
|
2275
|
-
catch: () => new StorageError({ message: "Failed to read schema meta" })
|
|
2276
|
-
}).pipe(Effect13.catch(() => Effect13.succeed(void 0)));
|
|
2277
|
-
needsUpgrade = storedSig !== schemaSig;
|
|
2278
|
-
}
|
|
2279
|
-
probeDb.close();
|
|
2280
|
-
const db = needsUpgrade ? yield* Effect13.tryPromise({
|
|
2281
|
-
try: () => openDB(name, currentVersion + 1, {
|
|
2282
|
-
upgrade(database, _oldVersion, _newVersion, transaction) {
|
|
2283
|
-
upgradeSchema(database, schema, transaction);
|
|
2284
|
-
}
|
|
2285
|
-
}),
|
|
2286
|
-
catch: (e) => new StorageError({ message: "Failed to open IndexedDB", cause: e })
|
|
2287
|
-
}) : yield* Effect13.tryPromise({
|
|
2288
|
-
try: () => openDB(name),
|
|
2289
|
-
catch: (e) => new StorageError({ message: "Failed to open IndexedDB", cause: e })
|
|
2290
|
-
});
|
|
2291
|
-
yield* Effect13.addFinalizer(() => Effect13.sync(() => db.close()));
|
|
2292
|
-
const handle = {
|
|
2293
|
-
putRecord: (collection2, record) => wrap("putRecord", () => db.put(storeName(collection2), record).then(() => void 0)),
|
|
2294
|
-
getRecord: (collection2, id) => wrap("getRecord", () => db.get(storeName(collection2), id)),
|
|
2295
|
-
getAllRecords: (collection2) => wrap("getAllRecords", () => db.getAll(storeName(collection2))),
|
|
2296
|
-
countRecords: (collection2) => wrap("countRecords", () => db.count(storeName(collection2))),
|
|
2297
|
-
clearRecords: (collection2) => wrap("clearRecords", () => db.clear(storeName(collection2))),
|
|
2298
|
-
getByIndex: (collection2, indexName, value) => wrap("getByIndex", () => db.getAllFromIndex(storeName(collection2), indexName, value)),
|
|
2299
|
-
getByIndexRange: (collection2, indexName, range) => wrap("getByIndexRange", () => db.getAllFromIndex(storeName(collection2), indexName, range)),
|
|
2300
|
-
getAllSorted: (collection2, indexName, direction) => wrap("getAllSorted", async () => {
|
|
2301
|
-
const sn = storeName(collection2);
|
|
2302
|
-
const tx = db.transaction(sn, "readonly");
|
|
2303
|
-
const store = tx.objectStore(sn);
|
|
2304
|
-
const index = store.index(indexName);
|
|
2305
|
-
const results = [];
|
|
2306
|
-
let cursor = await index.openCursor(null, direction ?? "next");
|
|
2307
|
-
while (cursor) {
|
|
2308
|
-
results.push(cursor.value);
|
|
2309
|
-
cursor = await cursor.continue();
|
|
2310
|
-
}
|
|
2311
|
-
return results;
|
|
2312
|
-
}),
|
|
2313
|
-
putEvent: (event) => wrap("putEvent", () => db.put("events", event).then(() => void 0)),
|
|
2314
|
-
getEvent: (id) => wrap("getEvent", () => db.get("events", id)),
|
|
2315
|
-
getAllEvents: () => wrap("getAllEvents", () => db.getAll("events")),
|
|
2316
|
-
getEventsByRecord: (collection2, recordId) => wrap(
|
|
2317
|
-
"getEventsByRecord",
|
|
2318
|
-
() => db.getAllFromIndex("events", "by-record", [collection2, recordId])
|
|
2319
|
-
),
|
|
2320
|
-
putGiftWrap: (gw) => wrap("putGiftWrap", async () => {
|
|
2321
|
-
if (gw.event) {
|
|
2322
|
-
const compact = packEvent(gw.event);
|
|
2323
|
-
await db.put("giftwraps", { id: gw.id, compact, createdAt: gw.createdAt });
|
|
2324
|
-
} else {
|
|
2325
|
-
await db.put("giftwraps", { id: gw.id, createdAt: gw.createdAt });
|
|
2326
|
-
}
|
|
2327
|
-
}),
|
|
2328
|
-
getGiftWrap: (id) => wrap("getGiftWrap", async () => {
|
|
2329
|
-
const raw = await db.get("giftwraps", id);
|
|
2330
|
-
if (!raw) return void 0;
|
|
2331
|
-
if (raw.compact) {
|
|
2332
|
-
return { id: raw.id, event: unpackEvent(raw.id, raw.compact), createdAt: raw.createdAt };
|
|
2333
|
-
}
|
|
2334
|
-
if (raw.event) {
|
|
2335
|
-
const compact = packEvent(raw.event);
|
|
2336
|
-
await db.put("giftwraps", { id: raw.id, compact, createdAt: raw.createdAt });
|
|
2337
|
-
return { id: raw.id, event: raw.event, createdAt: raw.createdAt };
|
|
2338
|
-
}
|
|
2339
|
-
return { id: raw.id, createdAt: raw.createdAt };
|
|
2340
|
-
}),
|
|
2341
|
-
getAllGiftWraps: () => wrap("getAllGiftWraps", async () => {
|
|
2342
|
-
const raws = await db.getAll("giftwraps");
|
|
2343
|
-
const results = [];
|
|
2344
|
-
for (const raw of raws) {
|
|
2345
|
-
if (raw.compact) {
|
|
2346
|
-
results.push({ id: raw.id, event: unpackEvent(raw.id, raw.compact), createdAt: raw.createdAt });
|
|
2347
|
-
} else if (raw.event) {
|
|
2348
|
-
const compact = packEvent(raw.event);
|
|
2349
|
-
await db.put("giftwraps", { id: raw.id, compact, createdAt: raw.createdAt });
|
|
2350
|
-
results.push({ id: raw.id, event: raw.event, createdAt: raw.createdAt });
|
|
2351
|
-
} else {
|
|
2352
|
-
results.push({ id: raw.id, createdAt: raw.createdAt });
|
|
2353
|
-
}
|
|
2354
|
-
}
|
|
2355
|
-
return results;
|
|
2356
|
-
}),
|
|
2357
|
-
deleteGiftWrap: (id) => wrap("deleteGiftWrap", () => db.delete("giftwraps", id).then(() => void 0)),
|
|
2358
|
-
deleteEvent: (id) => wrap("deleteEvent", () => db.delete("events", id).then(() => void 0)),
|
|
2359
|
-
stripEventData: (id) => wrap("stripEventData", async () => {
|
|
2360
|
-
const existing = await db.get("events", id);
|
|
2361
|
-
if (existing) {
|
|
2362
|
-
await db.put("events", { ...existing, data: null });
|
|
2363
|
-
}
|
|
2364
|
-
}),
|
|
2365
|
-
getMeta: (key) => wrap("getMeta", () => db.get("_meta", key)),
|
|
2366
|
-
putMeta: (key, value) => wrap("putMeta", () => db.put("_meta", value, key).then(() => void 0)),
|
|
2367
|
-
close: () => Effect13.sync(() => db.close())
|
|
2368
|
-
};
|
|
2369
|
-
return handle;
|
|
2370
|
-
});
|
|
2371
|
-
}
|
|
2372
|
-
|
|
2373
|
-
// src/layers/StorageLive.ts
|
|
2374
2383
|
var StorageLive = Layer4.effect(
|
|
2375
2384
|
Storage,
|
|
2376
2385
|
Effect14.gen(function* () {
|
|
@@ -3049,6 +3058,60 @@ var TablinumLive = Layer9.effect(
|
|
|
3049
3058
|
yield* Scope4.close(scope, Exit.void);
|
|
3050
3059
|
})
|
|
3051
3060
|
),
|
|
3061
|
+
destroy: () => withLog(
|
|
3062
|
+
Effect21.gen(function* () {
|
|
3063
|
+
if (!(yield* Ref5.get(closedRef))) {
|
|
3064
|
+
yield* Ref5.set(closedRef, true);
|
|
3065
|
+
syncHandle.stopHealing();
|
|
3066
|
+
yield* Scope4.close(scope, Exit.void);
|
|
3067
|
+
}
|
|
3068
|
+
yield* deleteIDBStorage(config.dbName);
|
|
3069
|
+
})
|
|
3070
|
+
),
|
|
3071
|
+
leave: () => withLog(
|
|
3072
|
+
Effect21.gen(function* () {
|
|
3073
|
+
if (yield* Ref5.get(closedRef)) {
|
|
3074
|
+
return yield* new SyncError({ message: "Database is closed", phase: "leave" });
|
|
3075
|
+
}
|
|
3076
|
+
const allMembers = yield* storage.getAllRecords("_members");
|
|
3077
|
+
const activeMembers = allMembers.filter(
|
|
3078
|
+
(member) => !member.removedAt && member.id !== identity.publicKey
|
|
3079
|
+
);
|
|
3080
|
+
const activePubkeys = activeMembers.map((member) => member.id);
|
|
3081
|
+
const result = createRotation(
|
|
3082
|
+
epochStore,
|
|
3083
|
+
identity.privateKey,
|
|
3084
|
+
identity.publicKey,
|
|
3085
|
+
activePubkeys,
|
|
3086
|
+
[identity.publicKey]
|
|
3087
|
+
);
|
|
3088
|
+
addEpoch(epochStore, result.epoch);
|
|
3089
|
+
epochStore.currentEpochId = result.epoch.id;
|
|
3090
|
+
yield* storage.putMeta("epochs", stringifyEpochStore(epochStore));
|
|
3091
|
+
const memberRecord = yield* storage.getRecord("_members", identity.publicKey);
|
|
3092
|
+
yield* putMemberRecord({
|
|
3093
|
+
...memberRecord ?? {
|
|
3094
|
+
id: identity.publicKey,
|
|
3095
|
+
addedAt: 0,
|
|
3096
|
+
addedInEpoch: EpochId("epoch-0")
|
|
3097
|
+
},
|
|
3098
|
+
removedAt: Date.now(),
|
|
3099
|
+
removedInEpoch: result.epoch.id
|
|
3100
|
+
});
|
|
3101
|
+
yield* Effect21.forEach(
|
|
3102
|
+
result.wrappedEvents,
|
|
3103
|
+
(wrappedEvent) => relay.publish(wrappedEvent, [...config.relays]).pipe(
|
|
3104
|
+
Effect21.tapError((e) => Effect21.sync(() => reportSyncError(config.onSyncError, e))),
|
|
3105
|
+
Effect21.ignore
|
|
3106
|
+
),
|
|
3107
|
+
{ discard: true }
|
|
3108
|
+
);
|
|
3109
|
+
yield* Ref5.set(closedRef, true);
|
|
3110
|
+
syncHandle.stopHealing();
|
|
3111
|
+
yield* Scope4.close(scope, Exit.void);
|
|
3112
|
+
yield* deleteIDBStorage(config.dbName);
|
|
3113
|
+
})
|
|
3114
|
+
),
|
|
3052
3115
|
rebuild: () => ensureOpen(
|
|
3053
3116
|
rebuild(
|
|
3054
3117
|
storage,
|
|
@@ -3186,6 +3249,9 @@ function validateConfig(config) {
|
|
|
3186
3249
|
}
|
|
3187
3250
|
});
|
|
3188
3251
|
}
|
|
3252
|
+
function deleteDatabase(dbName) {
|
|
3253
|
+
return deleteIDBStorage(DatabaseName(dbName ?? "tablinum"));
|
|
3254
|
+
}
|
|
3189
3255
|
function createTablinum(config) {
|
|
3190
3256
|
return Effect22.gen(function* () {
|
|
3191
3257
|
yield* validateConfig(config);
|
|
@@ -3258,6 +3324,7 @@ export {
|
|
|
3258
3324
|
collection,
|
|
3259
3325
|
createTablinum,
|
|
3260
3326
|
decodeInvite,
|
|
3327
|
+
deleteDatabase,
|
|
3261
3328
|
encodeInvite,
|
|
3262
3329
|
field
|
|
3263
3330
|
};
|