tablinum 0.6.4 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/db/create-tablinum.d.ts +1 -0
- package/dist/db/database-handle.d.ts +2 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +451 -385
- package/dist/storage/idb.d.ts +1 -0
- package/dist/svelte/index.svelte.d.ts +1 -0
- package/dist/svelte/index.svelte.js +464 -388
- package/dist/svelte/tablinum.svelte.d.ts +2 -0
- package/package.json +1 -1
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) {
|
|
@@ -430,10 +681,10 @@ function buildStructSchema(def, options = {}) {
|
|
|
430
681
|
function buildValidator(collectionName, def) {
|
|
431
682
|
const decode = Schema3.decodeUnknownEffect(buildStructSchema(def, { includeId: true }));
|
|
432
683
|
return (input) => decode(input).pipe(
|
|
433
|
-
|
|
684
|
+
Effect5.map(
|
|
434
685
|
(result) => result
|
|
435
686
|
),
|
|
436
|
-
|
|
687
|
+
Effect5.mapError(
|
|
437
688
|
(e) => new ValidationError({
|
|
438
689
|
message: `Validation failed for collection "${collectionName}": ${e.message}`
|
|
439
690
|
})
|
|
@@ -442,7 +693,7 @@ function buildValidator(collectionName, def) {
|
|
|
442
693
|
}
|
|
443
694
|
function buildPartialValidator(collectionName, def) {
|
|
444
695
|
const decode = Schema3.decodeUnknownEffect(buildStructSchema(def, { allOptional: true }));
|
|
445
|
-
return (input) =>
|
|
696
|
+
return (input) => Effect5.gen(function* () {
|
|
446
697
|
if (typeof input !== "object" || input === null) {
|
|
447
698
|
return yield* new ValidationError({
|
|
448
699
|
message: `Validation failed for collection "${collectionName}": expected an object`
|
|
@@ -457,8 +708,8 @@ function buildPartialValidator(collectionName, def) {
|
|
|
457
708
|
});
|
|
458
709
|
}
|
|
459
710
|
return yield* decode(record).pipe(
|
|
460
|
-
|
|
461
|
-
|
|
711
|
+
Effect5.map((result) => result),
|
|
712
|
+
Effect5.mapError(
|
|
462
713
|
(e) => new ValidationError({
|
|
463
714
|
message: `Validation failed for collection "${collectionName}": ${e.message}`
|
|
464
715
|
})
|
|
@@ -468,7 +719,7 @@ function buildPartialValidator(collectionName, def) {
|
|
|
468
719
|
}
|
|
469
720
|
|
|
470
721
|
// src/crud/collection-handle.ts
|
|
471
|
-
import { Effect as
|
|
722
|
+
import { Effect as Effect7, Option as Option3, References } from "effect";
|
|
472
723
|
|
|
473
724
|
// src/utils/uuid.ts
|
|
474
725
|
var alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
|
|
@@ -501,12 +752,12 @@ function uuidv7() {
|
|
|
501
752
|
}
|
|
502
753
|
|
|
503
754
|
// src/crud/query-builder.ts
|
|
504
|
-
import { Effect as
|
|
755
|
+
import { Effect as Effect6, Option as Option2, Ref as Ref2, Stream as Stream2 } from "effect";
|
|
505
756
|
function emptyPlan() {
|
|
506
757
|
return { filters: [] };
|
|
507
758
|
}
|
|
508
759
|
function executeQuery(ctx, plan) {
|
|
509
|
-
return
|
|
760
|
+
return Effect6.gen(function* () {
|
|
510
761
|
if (plan.fieldName) {
|
|
511
762
|
const fieldDef = ctx.def.fields[plan.fieldName];
|
|
512
763
|
if (!fieldDef) {
|
|
@@ -582,7 +833,7 @@ function watchQuery(ctx, plan) {
|
|
|
582
833
|
const changes = Stream2.fromPubSub(ctx.watchCtx.pubsub).pipe(
|
|
583
834
|
Stream2.filter((event) => event.collection === ctx.collectionName),
|
|
584
835
|
Stream2.mapEffect(
|
|
585
|
-
() =>
|
|
836
|
+
() => Effect6.gen(function* () {
|
|
586
837
|
const replaying = yield* Ref2.get(ctx.watchCtx.replayingRef);
|
|
587
838
|
if (replaying) return void 0;
|
|
588
839
|
return yield* query();
|
|
@@ -591,7 +842,7 @@ function watchQuery(ctx, plan) {
|
|
|
591
842
|
Stream2.filter((result) => result !== void 0)
|
|
592
843
|
);
|
|
593
844
|
return Stream2.unwrap(
|
|
594
|
-
|
|
845
|
+
Effect6.gen(function* () {
|
|
595
846
|
const initial = yield* query();
|
|
596
847
|
return Stream2.concat(Stream2.make(initial), changes);
|
|
597
848
|
})
|
|
@@ -617,11 +868,11 @@ function makeQueryBuilder(ctx, plan) {
|
|
|
617
868
|
offset: (n) => makeQueryBuilder(ctx, { ...plan, offset: n }),
|
|
618
869
|
limit: (n) => makeQueryBuilder(ctx, { ...plan, limit: n }),
|
|
619
870
|
get: () => executeQuery(ctx, plan),
|
|
620
|
-
first: () =>
|
|
871
|
+
first: () => Effect6.map(
|
|
621
872
|
executeQuery(ctx, { ...plan, limit: 1 }),
|
|
622
873
|
(results) => results.length > 0 ? Option2.some(results[0]) : Option2.none()
|
|
623
874
|
),
|
|
624
|
-
count: () =>
|
|
875
|
+
count: () => Effect6.map(executeQuery(ctx, plan), (results) => results.length),
|
|
625
876
|
watch: () => watchQuery(ctx, plan)
|
|
626
877
|
};
|
|
627
878
|
}
|
|
@@ -727,7 +978,7 @@ function replayState(recordId, events, stopAtId) {
|
|
|
727
978
|
return state;
|
|
728
979
|
}
|
|
729
980
|
function promoteToSnapshot(storage, collection2, recordId, target, allSorted) {
|
|
730
|
-
return
|
|
981
|
+
return Effect7.gen(function* () {
|
|
731
982
|
const chronological = sortChronologically(allSorted);
|
|
732
983
|
const state = replayState(recordId, chronological, target.id);
|
|
733
984
|
if (state) {
|
|
@@ -736,7 +987,7 @@ function promoteToSnapshot(storage, collection2, recordId, target, allSorted) {
|
|
|
736
987
|
});
|
|
737
988
|
}
|
|
738
989
|
function pruneEvents(storage, collection2, recordId, retention) {
|
|
739
|
-
return
|
|
990
|
+
return Effect7.gen(function* () {
|
|
740
991
|
const events = yield* storage.getEventsByRecord(collection2, recordId);
|
|
741
992
|
if (events.length <= retention) return;
|
|
742
993
|
const sorted = [...events].sort((a, b) => b.createdAt - a.createdAt || (a.id < b.id ? 1 : -1));
|
|
@@ -757,8 +1008,8 @@ function mapRecord(record) {
|
|
|
757
1008
|
}
|
|
758
1009
|
function createCollectionHandle(def, storage, watchCtx, validator, partialValidator, makeEventId, localAuthor, onWrite, logLevel = "None") {
|
|
759
1010
|
const collectionName = def.name;
|
|
760
|
-
const withLog = (effect) =>
|
|
761
|
-
const commitEvent = (event) =>
|
|
1011
|
+
const withLog = (effect) => Effect7.provideService(effect, References.MinimumLogLevel, logLevel);
|
|
1012
|
+
const commitEvent = (event) => Effect7.gen(function* () {
|
|
762
1013
|
yield* storage.putEvent(event);
|
|
763
1014
|
yield* applyEvent(storage, event);
|
|
764
1015
|
if (onWrite) yield* onWrite(event);
|
|
@@ -770,7 +1021,7 @@ function createCollectionHandle(def, storage, watchCtx, validator, partialValida
|
|
|
770
1021
|
});
|
|
771
1022
|
const handle = {
|
|
772
1023
|
add: (data) => withLog(
|
|
773
|
-
|
|
1024
|
+
Effect7.gen(function* () {
|
|
774
1025
|
const id = uuidv7();
|
|
775
1026
|
const fullRecord = { id, ...data };
|
|
776
1027
|
yield* validator(fullRecord);
|
|
@@ -784,7 +1035,7 @@ function createCollectionHandle(def, storage, watchCtx, validator, partialValida
|
|
|
784
1035
|
author: localAuthor
|
|
785
1036
|
};
|
|
786
1037
|
yield* commitEvent(event);
|
|
787
|
-
yield*
|
|
1038
|
+
yield* Effect7.logDebug("Record added", {
|
|
788
1039
|
collection: collectionName,
|
|
789
1040
|
recordId: id,
|
|
790
1041
|
data: fullRecord
|
|
@@ -793,7 +1044,7 @@ function createCollectionHandle(def, storage, watchCtx, validator, partialValida
|
|
|
793
1044
|
})
|
|
794
1045
|
),
|
|
795
1046
|
update: (id, data) => withLog(
|
|
796
|
-
|
|
1047
|
+
Effect7.gen(function* () {
|
|
797
1048
|
const existing = yield* storage.getRecord(collectionName, id);
|
|
798
1049
|
if (!existing || existing._d) {
|
|
799
1050
|
return yield* new NotFoundError({
|
|
@@ -816,7 +1067,7 @@ function createCollectionHandle(def, storage, watchCtx, validator, partialValida
|
|
|
816
1067
|
author: localAuthor
|
|
817
1068
|
};
|
|
818
1069
|
yield* commitEvent(event);
|
|
819
|
-
yield*
|
|
1070
|
+
yield* Effect7.logDebug("Record updated", {
|
|
820
1071
|
collection: collectionName,
|
|
821
1072
|
recordId: id,
|
|
822
1073
|
data: diff
|
|
@@ -825,7 +1076,7 @@ function createCollectionHandle(def, storage, watchCtx, validator, partialValida
|
|
|
825
1076
|
})
|
|
826
1077
|
),
|
|
827
1078
|
delete: (id) => withLog(
|
|
828
|
-
|
|
1079
|
+
Effect7.gen(function* () {
|
|
829
1080
|
const existing = yield* storage.getRecord(collectionName, id);
|
|
830
1081
|
if (!existing || existing._d) {
|
|
831
1082
|
return yield* new NotFoundError({
|
|
@@ -843,11 +1094,11 @@ function createCollectionHandle(def, storage, watchCtx, validator, partialValida
|
|
|
843
1094
|
author: localAuthor
|
|
844
1095
|
};
|
|
845
1096
|
yield* commitEvent(event);
|
|
846
|
-
yield*
|
|
1097
|
+
yield* Effect7.logDebug("Record deleted", { collection: collectionName, recordId: id });
|
|
847
1098
|
yield* pruneEvents(storage, collectionName, id, def.eventRetention);
|
|
848
1099
|
})
|
|
849
1100
|
),
|
|
850
|
-
undo: (id) =>
|
|
1101
|
+
undo: (id) => Effect7.gen(function* () {
|
|
851
1102
|
const existing = yield* storage.getRecord(collectionName, id);
|
|
852
1103
|
if (!existing) {
|
|
853
1104
|
return yield* new NotFoundError({ collection: collectionName, id });
|
|
@@ -872,7 +1123,7 @@ function createCollectionHandle(def, storage, watchCtx, validator, partialValida
|
|
|
872
1123
|
yield* commitEvent(event);
|
|
873
1124
|
yield* pruneEvents(storage, collectionName, id, def.eventRetention);
|
|
874
1125
|
}),
|
|
875
|
-
get: (id) =>
|
|
1126
|
+
get: (id) => Effect7.gen(function* () {
|
|
876
1127
|
const record = yield* storage.getRecord(collectionName, id);
|
|
877
1128
|
if (!record || record._d) {
|
|
878
1129
|
return yield* new NotFoundError({
|
|
@@ -882,11 +1133,11 @@ function createCollectionHandle(def, storage, watchCtx, validator, partialValida
|
|
|
882
1133
|
}
|
|
883
1134
|
return mapRecord(record);
|
|
884
1135
|
}),
|
|
885
|
-
first: () =>
|
|
1136
|
+
first: () => Effect7.map(storage.getAllRecords(collectionName), (all) => {
|
|
886
1137
|
const found = all.find((r) => !r._d);
|
|
887
1138
|
return found ? Option3.some(mapRecord(found)) : Option3.none();
|
|
888
1139
|
}),
|
|
889
|
-
count: () =>
|
|
1140
|
+
count: () => Effect7.map(storage.getAllRecords(collectionName), (all) => all.filter((r) => !r._d).length),
|
|
890
1141
|
watch: () => watchCollection(watchCtx, storage, collectionName, void 0, mapRecord),
|
|
891
1142
|
where: (fieldName) => createWhereClause(
|
|
892
1143
|
storage,
|
|
@@ -909,12 +1160,12 @@ function createCollectionHandle(def, storage, watchCtx, validator, partialValida
|
|
|
909
1160
|
}
|
|
910
1161
|
|
|
911
1162
|
// src/sync/sync-service.ts
|
|
912
|
-
import { Duration, Effect as
|
|
1163
|
+
import { Duration, Effect as Effect9, Layer, Option as Option5, References as References2, Ref as Ref3, Schedule } from "effect";
|
|
913
1164
|
import { unwrapEvent } from "nostr-tools/nip59";
|
|
914
1165
|
import { GiftWrap as GiftWrap2 } from "nostr-tools/kinds";
|
|
915
1166
|
|
|
916
1167
|
// src/sync/negentropy.ts
|
|
917
|
-
import { Effect as
|
|
1168
|
+
import { Effect as Effect8 } from "effect";
|
|
918
1169
|
|
|
919
1170
|
// src/vendor/negentropy.js
|
|
920
1171
|
var PROTOCOL_VERSION = 97;
|
|
@@ -1401,14 +1652,14 @@ function itemCompare(a, b) {
|
|
|
1401
1652
|
}
|
|
1402
1653
|
|
|
1403
1654
|
// src/sync/negentropy.ts
|
|
1404
|
-
import { hexToBytes as
|
|
1655
|
+
import { hexToBytes as hexToBytes3 } from "@noble/hashes/utils.js";
|
|
1405
1656
|
import { GiftWrap } from "nostr-tools/kinds";
|
|
1406
1657
|
function reconcileWithRelay(storage, relay, relayUrl, publicKeys) {
|
|
1407
|
-
return
|
|
1658
|
+
return Effect8.gen(function* () {
|
|
1408
1659
|
const allGiftWraps = yield* storage.getAllGiftWraps();
|
|
1409
1660
|
const storageVector = new NegentropyStorageVector();
|
|
1410
1661
|
for (const gw of allGiftWraps) {
|
|
1411
|
-
storageVector.insert(gw.createdAt,
|
|
1662
|
+
storageVector.insert(gw.createdAt, hexToBytes3(gw.id));
|
|
1412
1663
|
}
|
|
1413
1664
|
storageVector.seal();
|
|
1414
1665
|
const neg = new Negentropy(storageVector, 0);
|
|
@@ -1419,7 +1670,7 @@ function reconcileWithRelay(storage, relay, relayUrl, publicKeys) {
|
|
|
1419
1670
|
const allHaveIds = [];
|
|
1420
1671
|
const allNeedIds = [];
|
|
1421
1672
|
const subId = `neg-${Date.now()}`;
|
|
1422
|
-
const initialMsg = yield*
|
|
1673
|
+
const initialMsg = yield* Effect8.tryPromise({
|
|
1423
1674
|
try: () => neg.initiate(),
|
|
1424
1675
|
catch: (e) => new SyncError({
|
|
1425
1676
|
message: `Negentropy initiate failed: ${e instanceof Error ? e.message : String(e)}`,
|
|
@@ -1431,7 +1682,7 @@ function reconcileWithRelay(storage, relay, relayUrl, publicKeys) {
|
|
|
1431
1682
|
while (currentMsg !== null) {
|
|
1432
1683
|
const response = yield* relay.sendNegMsg(relayUrl, subId, filter, currentMsg);
|
|
1433
1684
|
if (response.msgHex === null) break;
|
|
1434
|
-
const reconcileResult = yield*
|
|
1685
|
+
const reconcileResult = yield* Effect8.tryPromise({
|
|
1435
1686
|
try: () => neg.reconcile(response.msgHex),
|
|
1436
1687
|
catch: (e) => new SyncError({
|
|
1437
1688
|
message: `Negentropy reconcile failed: ${e instanceof Error ? e.message : String(e)}`,
|
|
@@ -1444,13 +1695,13 @@ function reconcileWithRelay(storage, relay, relayUrl, publicKeys) {
|
|
|
1444
1695
|
for (const id of needIds) allNeedIds.push(id);
|
|
1445
1696
|
currentMsg = nextMsg;
|
|
1446
1697
|
}
|
|
1447
|
-
yield*
|
|
1698
|
+
yield* Effect8.logDebug("Negentropy reconciliation complete", {
|
|
1448
1699
|
relay: relayUrl,
|
|
1449
1700
|
have: allHaveIds.length,
|
|
1450
1701
|
need: allNeedIds.length
|
|
1451
1702
|
});
|
|
1452
1703
|
return { haveIds: allHaveIds, needIds: allNeedIds };
|
|
1453
|
-
}).pipe(
|
|
1704
|
+
}).pipe(Effect8.withLogSpan("tablinum.negentropy"));
|
|
1454
1705
|
}
|
|
1455
1706
|
|
|
1456
1707
|
// src/db/key-rotation.ts
|
|
@@ -1544,52 +1795,52 @@ function createSyncHandle(storage, giftWrapHandle, relay, publishQueue, syncStat
|
|
|
1544
1795
|
kind: "create"
|
|
1545
1796
|
});
|
|
1546
1797
|
const forkHandled = (effect) => {
|
|
1547
|
-
|
|
1798
|
+
Effect9.runFork(
|
|
1548
1799
|
effect.pipe(
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1800
|
+
Effect9.tapError((e) => Effect9.sync(() => onSyncError?.(e))),
|
|
1801
|
+
Effect9.ignore,
|
|
1802
|
+
Effect9.provide(logLayer),
|
|
1803
|
+
Effect9.forkIn(scope)
|
|
1553
1804
|
)
|
|
1554
1805
|
);
|
|
1555
1806
|
};
|
|
1556
1807
|
let autoFlushActive = false;
|
|
1557
|
-
const autoFlushEffect =
|
|
1808
|
+
const autoFlushEffect = Effect9.gen(function* () {
|
|
1558
1809
|
const size = yield* publishQueue.size();
|
|
1559
1810
|
if (size === 0) return;
|
|
1560
1811
|
yield* syncStatus.set("syncing");
|
|
1561
1812
|
yield* publishQueue.flush(relayUrls);
|
|
1562
1813
|
const remaining = yield* publishQueue.size();
|
|
1563
|
-
if (remaining > 0) yield*
|
|
1814
|
+
if (remaining > 0) yield* Effect9.fail("pending");
|
|
1564
1815
|
}).pipe(
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1816
|
+
Effect9.ensuring(syncStatus.set("idle")),
|
|
1817
|
+
Effect9.retry({ schedule: Schedule.exponential(5e3).pipe(Schedule.jittered), times: 10 }),
|
|
1818
|
+
Effect9.ignore
|
|
1568
1819
|
);
|
|
1569
1820
|
const scheduleAutoFlush = () => {
|
|
1570
1821
|
if (autoFlushActive) return;
|
|
1571
1822
|
autoFlushActive = true;
|
|
1572
1823
|
forkHandled(
|
|
1573
1824
|
autoFlushEffect.pipe(
|
|
1574
|
-
|
|
1575
|
-
|
|
1825
|
+
Effect9.ensuring(
|
|
1826
|
+
Effect9.sync(() => {
|
|
1576
1827
|
autoFlushActive = false;
|
|
1577
1828
|
})
|
|
1578
1829
|
)
|
|
1579
1830
|
)
|
|
1580
1831
|
);
|
|
1581
1832
|
};
|
|
1582
|
-
const shouldRejectWrite = (authorPubkey) =>
|
|
1833
|
+
const shouldRejectWrite = (authorPubkey) => Effect9.gen(function* () {
|
|
1583
1834
|
const memberRecord = yield* storage.getRecord("_members", authorPubkey);
|
|
1584
1835
|
if (!memberRecord) return false;
|
|
1585
1836
|
return !!memberRecord.removedAt;
|
|
1586
1837
|
});
|
|
1587
1838
|
const storeGiftWrapShell = (gw) => storage.putGiftWrap({ id: gw.id, event: gw, createdAt: gw.created_at });
|
|
1588
|
-
const unwrapGiftWrap = (remoteGw) =>
|
|
1839
|
+
const unwrapGiftWrap = (remoteGw) => Effect9.gen(function* () {
|
|
1589
1840
|
const existing = yield* storage.getGiftWrap(remoteGw.id);
|
|
1590
1841
|
if (existing) return null;
|
|
1591
1842
|
const rumor = yield* giftWrapHandle.unwrap(remoteGw).pipe(
|
|
1592
|
-
|
|
1843
|
+
Effect9.orElseSucceed(() => null)
|
|
1593
1844
|
);
|
|
1594
1845
|
if (!rumor) {
|
|
1595
1846
|
yield* storeGiftWrapShell(remoteGw);
|
|
@@ -1617,7 +1868,7 @@ function createSyncHandle(storage, giftWrapHandle, relay, publishQueue, syncStat
|
|
|
1617
1868
|
recordId: dTag.substring(colonIdx + 1)
|
|
1618
1869
|
};
|
|
1619
1870
|
});
|
|
1620
|
-
const applyUnwrappedEvent = (uw) =>
|
|
1871
|
+
const applyUnwrappedEvent = (uw) => Effect9.gen(function* () {
|
|
1621
1872
|
const { giftWrap: remoteGw, rumor, collection: collectionName, recordId } = uw;
|
|
1622
1873
|
const retention = knownCollections.get(collectionName);
|
|
1623
1874
|
if (retention === void 0) {
|
|
@@ -1627,17 +1878,17 @@ function createSyncHandle(storage, giftWrapHandle, relay, publishQueue, syncStat
|
|
|
1627
1878
|
if (rumor.pubkey) {
|
|
1628
1879
|
const reject = yield* shouldRejectWrite(rumor.pubkey);
|
|
1629
1880
|
if (reject) {
|
|
1630
|
-
yield*
|
|
1881
|
+
yield* Effect9.logWarning("Rejected write from removed member", {
|
|
1631
1882
|
author: rumor.pubkey.slice(0, 12)
|
|
1632
1883
|
});
|
|
1633
1884
|
yield* storeGiftWrapShell(remoteGw);
|
|
1634
1885
|
return null;
|
|
1635
1886
|
}
|
|
1636
1887
|
}
|
|
1637
|
-
const parsed = yield*
|
|
1888
|
+
const parsed = yield* Effect9.try({
|
|
1638
1889
|
try: () => JSON.parse(rumor.content),
|
|
1639
1890
|
catch: () => void 0
|
|
1640
|
-
}).pipe(
|
|
1891
|
+
}).pipe(Effect9.orElseSucceed(() => void 0));
|
|
1641
1892
|
if (parsed === void 0) {
|
|
1642
1893
|
yield* storeGiftWrapShell(remoteGw);
|
|
1643
1894
|
return null;
|
|
@@ -1669,7 +1920,7 @@ function createSyncHandle(storage, giftWrapHandle, relay, publishQueue, syncStat
|
|
|
1669
1920
|
if (didApply && (kind === "u" || kind === "d")) {
|
|
1670
1921
|
yield* pruneEvents(storage, collectionName, recordId, retention);
|
|
1671
1922
|
}
|
|
1672
|
-
yield*
|
|
1923
|
+
yield* Effect9.logDebug("Processed gift wrap", {
|
|
1673
1924
|
collection: collectionName,
|
|
1674
1925
|
recordId,
|
|
1675
1926
|
kind,
|
|
@@ -1680,33 +1931,33 @@ function createSyncHandle(storage, giftWrapHandle, relay, publishQueue, syncStat
|
|
|
1680
1931
|
}
|
|
1681
1932
|
return collectionName;
|
|
1682
1933
|
});
|
|
1683
|
-
const reconcileRelay = (url, pubKeys) =>
|
|
1684
|
-
yield*
|
|
1934
|
+
const reconcileRelay = (url, pubKeys) => Effect9.gen(function* () {
|
|
1935
|
+
yield* Effect9.logDebug("Syncing relay", { relay: url });
|
|
1685
1936
|
const { haveIds, needIds } = yield* reconcileWithRelay(
|
|
1686
1937
|
storage,
|
|
1687
1938
|
relay,
|
|
1688
1939
|
url,
|
|
1689
1940
|
Array.from(pubKeys)
|
|
1690
1941
|
).pipe(
|
|
1691
|
-
|
|
1692
|
-
|
|
1942
|
+
Effect9.tapError((err) => Effect9.sync(() => onSyncError?.(err))),
|
|
1943
|
+
Effect9.orElseSucceed(() => ({ haveIds: [], needIds: [] }))
|
|
1693
1944
|
);
|
|
1694
|
-
yield*
|
|
1945
|
+
yield* Effect9.logDebug("Relay reconciliation result", {
|
|
1695
1946
|
relay: url,
|
|
1696
1947
|
need: needIds.length,
|
|
1697
1948
|
have: haveIds.length
|
|
1698
1949
|
});
|
|
1699
1950
|
const events = needIds.length > 0 ? yield* relay.fetchEvents(needIds, url).pipe(
|
|
1700
|
-
|
|
1701
|
-
|
|
1951
|
+
Effect9.tapError((err) => Effect9.sync(() => onSyncError?.(err))),
|
|
1952
|
+
Effect9.orElseSucceed(() => [])
|
|
1702
1953
|
) : [];
|
|
1703
1954
|
return {
|
|
1704
1955
|
events,
|
|
1705
1956
|
haveIds: haveIds.map((id) => ({ id, url }))
|
|
1706
1957
|
};
|
|
1707
|
-
}).pipe(
|
|
1708
|
-
const syncAllRelays = (pubKeys, changedCollections) =>
|
|
1709
|
-
const results = yield*
|
|
1958
|
+
}).pipe(Effect9.withLogSpan("tablinum.reconcileRelay"));
|
|
1959
|
+
const syncAllRelays = (pubKeys, changedCollections) => Effect9.gen(function* () {
|
|
1960
|
+
const results = yield* Effect9.forEach(
|
|
1710
1961
|
relayUrls,
|
|
1711
1962
|
(url) => reconcileRelay(url, pubKeys),
|
|
1712
1963
|
{ concurrency: "unbounded" }
|
|
@@ -1723,44 +1974,44 @@ function createSyncHandle(storage, giftWrapHandle, relay, publishQueue, syncStat
|
|
|
1723
1974
|
}
|
|
1724
1975
|
const unwrapped = [];
|
|
1725
1976
|
for (const gw of allGiftWraps) {
|
|
1726
|
-
const result = yield* unwrapGiftWrap(gw).pipe(
|
|
1977
|
+
const result = yield* unwrapGiftWrap(gw).pipe(Effect9.orElseSucceed(() => null));
|
|
1727
1978
|
if (result) unwrapped.push(result);
|
|
1728
1979
|
}
|
|
1729
1980
|
unwrapped.sort((a, b) => a.rumor.created_at - b.rumor.created_at || (a.rumor.id < b.rumor.id ? -1 : 1));
|
|
1730
1981
|
for (const event of unwrapped) {
|
|
1731
1982
|
const collection2 = yield* applyUnwrappedEvent(event).pipe(
|
|
1732
|
-
|
|
1983
|
+
Effect9.orElseSucceed(() => null)
|
|
1733
1984
|
);
|
|
1734
1985
|
if (collection2) changedCollections.add(collection2);
|
|
1735
1986
|
}
|
|
1736
1987
|
const allHaveIds = results.flatMap((r) => r.haveIds);
|
|
1737
|
-
yield*
|
|
1988
|
+
yield* Effect9.forEach(
|
|
1738
1989
|
allHaveIds,
|
|
1739
|
-
({ id, url }) =>
|
|
1990
|
+
({ id, url }) => Effect9.gen(function* () {
|
|
1740
1991
|
const gw = yield* storage.getGiftWrap(id);
|
|
1741
1992
|
if (!gw?.event) return;
|
|
1742
1993
|
yield* relay.publish(gw.event, [url]).pipe(
|
|
1743
|
-
|
|
1744
|
-
|
|
1994
|
+
Effect9.tapError((err) => Effect9.sync(() => onSyncError?.(err))),
|
|
1995
|
+
Effect9.ignore
|
|
1745
1996
|
);
|
|
1746
1997
|
}),
|
|
1747
1998
|
{ concurrency: "unbounded", discard: true }
|
|
1748
1999
|
);
|
|
1749
|
-
}).pipe(
|
|
1750
|
-
const processGiftWrap = (remoteGw) =>
|
|
2000
|
+
}).pipe(Effect9.withLogSpan("tablinum.syncAllRelays"));
|
|
2001
|
+
const processGiftWrap = (remoteGw) => Effect9.gen(function* () {
|
|
1751
2002
|
const uw = yield* unwrapGiftWrap(remoteGw);
|
|
1752
2003
|
if (!uw) return null;
|
|
1753
2004
|
return yield* applyUnwrappedEvent(uw);
|
|
1754
2005
|
});
|
|
1755
|
-
const processRealtimeGiftWrap = (remoteGw) =>
|
|
1756
|
-
const collection2 = yield* processGiftWrap(remoteGw).pipe(
|
|
2006
|
+
const processRealtimeGiftWrap = (remoteGw) => Effect9.gen(function* () {
|
|
2007
|
+
const collection2 = yield* processGiftWrap(remoteGw).pipe(Effect9.orElseSucceed(() => null));
|
|
1757
2008
|
if (collection2) {
|
|
1758
2009
|
yield* notifyCollectionUpdated(collection2);
|
|
1759
2010
|
}
|
|
1760
2011
|
});
|
|
1761
|
-
const processRotationGiftWrap = (remoteGw) =>
|
|
1762
|
-
const unwrapResult = yield*
|
|
1763
|
-
|
|
2012
|
+
const processRotationGiftWrap = (remoteGw) => Effect9.gen(function* () {
|
|
2013
|
+
const unwrapResult = yield* Effect9.result(
|
|
2014
|
+
Effect9.try({
|
|
1764
2015
|
try: () => unwrapEvent(remoteGw, personalPrivateKey),
|
|
1765
2016
|
catch: (e) => new CryptoError({
|
|
1766
2017
|
message: `Rotation unwrap failed: ${e instanceof Error ? e.message : String(e)}`,
|
|
@@ -1810,73 +2061,73 @@ function createSyncHandle(storage, giftWrapHandle, relay, publishQueue, syncStat
|
|
|
1810
2061
|
yield* handle.addEpochSubscription(epoch.publicKey);
|
|
1811
2062
|
return true;
|
|
1812
2063
|
});
|
|
1813
|
-
const subscribeAcrossRelays = (filter, onEvent) =>
|
|
2064
|
+
const subscribeAcrossRelays = (filter, onEvent) => Effect9.forEach(
|
|
1814
2065
|
relayUrls,
|
|
1815
|
-
(url) =>
|
|
2066
|
+
(url) => Effect9.gen(function* () {
|
|
1816
2067
|
yield* relay.subscribe(filter, url, (event) => {
|
|
1817
2068
|
forkHandled(onEvent(event));
|
|
1818
2069
|
}).pipe(
|
|
1819
|
-
|
|
1820
|
-
|
|
2070
|
+
Effect9.tapError((err) => Effect9.sync(() => onSyncError?.(err))),
|
|
2071
|
+
Effect9.ignore
|
|
1821
2072
|
);
|
|
1822
2073
|
}),
|
|
1823
2074
|
{ concurrency: "unbounded", discard: true }
|
|
1824
2075
|
);
|
|
1825
2076
|
let healingActive = false;
|
|
1826
|
-
const healingEffect =
|
|
2077
|
+
const healingEffect = Effect9.gen(function* () {
|
|
1827
2078
|
if (!healingActive) return;
|
|
1828
2079
|
const status = yield* syncStatus.get();
|
|
1829
2080
|
if (status === "syncing") return;
|
|
1830
2081
|
yield* syncStatus.set("syncing");
|
|
1831
|
-
yield*
|
|
2082
|
+
yield* Effect9.gen(function* () {
|
|
1832
2083
|
const pubKeys = getSubscriptionPubKeys();
|
|
1833
2084
|
const changedCollections = /* @__PURE__ */ new Set();
|
|
1834
2085
|
yield* syncAllRelays(pubKeys, changedCollections);
|
|
1835
2086
|
if (changedCollections.size > 0) {
|
|
1836
2087
|
yield* notifyReplayComplete(watchCtx, [...changedCollections]);
|
|
1837
2088
|
}
|
|
1838
|
-
}).pipe(
|
|
1839
|
-
}).pipe(
|
|
2089
|
+
}).pipe(Effect9.ensuring(syncStatus.set("idle")));
|
|
2090
|
+
}).pipe(Effect9.ignore);
|
|
1840
2091
|
const handle = {
|
|
1841
|
-
sync: () =>
|
|
1842
|
-
yield*
|
|
2092
|
+
sync: () => Effect9.gen(function* () {
|
|
2093
|
+
yield* Effect9.logInfo("Sync started");
|
|
1843
2094
|
yield* syncStatus.set("syncing");
|
|
1844
2095
|
yield* Ref3.set(watchCtx.replayingRef, true);
|
|
1845
2096
|
const changedCollections = /* @__PURE__ */ new Set();
|
|
1846
|
-
yield*
|
|
2097
|
+
yield* Effect9.gen(function* () {
|
|
1847
2098
|
const pubKeys = getSubscriptionPubKeys();
|
|
1848
2099
|
yield* syncAllRelays(pubKeys, changedCollections);
|
|
1849
|
-
yield* publishQueue.flush(relayUrls).pipe(
|
|
2100
|
+
yield* publishQueue.flush(relayUrls).pipe(Effect9.ignore);
|
|
1850
2101
|
}).pipe(
|
|
1851
|
-
|
|
1852
|
-
|
|
2102
|
+
Effect9.ensuring(
|
|
2103
|
+
Effect9.gen(function* () {
|
|
1853
2104
|
yield* notifyReplayComplete(watchCtx, [...changedCollections]);
|
|
1854
2105
|
yield* syncStatus.set("idle");
|
|
1855
2106
|
})
|
|
1856
2107
|
)
|
|
1857
2108
|
);
|
|
1858
|
-
yield*
|
|
1859
|
-
}).pipe(
|
|
1860
|
-
publishLocal: (giftWrap) =>
|
|
2109
|
+
yield* Effect9.logInfo("Sync complete", { changed: [...changedCollections] });
|
|
2110
|
+
}).pipe(Effect9.withLogSpan("tablinum.sync")),
|
|
2111
|
+
publishLocal: (giftWrap) => Effect9.gen(function* () {
|
|
1861
2112
|
if (!giftWrap.event) return;
|
|
1862
2113
|
yield* relay.publish(giftWrap.event, relayUrls).pipe(
|
|
1863
|
-
|
|
2114
|
+
Effect9.tapError(
|
|
1864
2115
|
() => storage.putGiftWrap(giftWrap).pipe(
|
|
1865
|
-
|
|
1866
|
-
|
|
2116
|
+
Effect9.andThen(publishQueue.enqueue(giftWrap.id)),
|
|
2117
|
+
Effect9.andThen(Effect9.sync(() => scheduleAutoFlush()))
|
|
1867
2118
|
)
|
|
1868
2119
|
),
|
|
1869
|
-
|
|
1870
|
-
|
|
2120
|
+
Effect9.tapError((err) => Effect9.sync(() => onSyncError?.(err))),
|
|
2121
|
+
Effect9.ignore
|
|
1871
2122
|
);
|
|
1872
2123
|
}),
|
|
1873
|
-
startSubscription: () =>
|
|
2124
|
+
startSubscription: () => Effect9.gen(function* () {
|
|
1874
2125
|
const pubKeys = getSubscriptionPubKeys();
|
|
1875
2126
|
yield* subscribeAcrossRelays({ kinds: [GiftWrap2], "#p": pubKeys }, processRealtimeGiftWrap);
|
|
1876
2127
|
if (!pubKeys.includes(personalPublicKey)) {
|
|
1877
2128
|
yield* subscribeAcrossRelays(
|
|
1878
2129
|
{ kinds: [GiftWrap2], "#p": [personalPublicKey] },
|
|
1879
|
-
(event) =>
|
|
2130
|
+
(event) => Effect9.result(processRotationGiftWrap(event)).pipe(Effect9.asVoid)
|
|
1880
2131
|
);
|
|
1881
2132
|
}
|
|
1882
2133
|
}),
|
|
@@ -1885,10 +2136,10 @@ function createSyncHandle(storage, giftWrapHandle, relay, publishQueue, syncStat
|
|
|
1885
2136
|
if (healingActive) return;
|
|
1886
2137
|
healingActive = true;
|
|
1887
2138
|
forkHandled(
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
2139
|
+
Effect9.sleep(Duration.minutes(5)).pipe(
|
|
2140
|
+
Effect9.andThen(healingEffect),
|
|
2141
|
+
Effect9.repeat(Schedule.spaced(Duration.minutes(5))),
|
|
2142
|
+
Effect9.ensuring(Effect9.sync(() => {
|
|
1892
2143
|
healingActive = false;
|
|
1893
2144
|
}))
|
|
1894
2145
|
)
|
|
@@ -1900,8 +2151,8 @@ function createSyncHandle(storage, giftWrapHandle, relay, publishQueue, syncStat
|
|
|
1900
2151
|
};
|
|
1901
2152
|
forkHandled(
|
|
1902
2153
|
publishQueue.size().pipe(
|
|
1903
|
-
|
|
1904
|
-
(size) =>
|
|
2154
|
+
Effect9.flatMap(
|
|
2155
|
+
(size) => Effect9.sync(() => {
|
|
1905
2156
|
if (size > 0) scheduleAutoFlush();
|
|
1906
2157
|
})
|
|
1907
2158
|
)
|
|
@@ -1911,7 +2162,7 @@ function createSyncHandle(storage, giftWrapHandle, relay, publishQueue, syncStat
|
|
|
1911
2162
|
}
|
|
1912
2163
|
|
|
1913
2164
|
// src/db/members.ts
|
|
1914
|
-
import { Effect as
|
|
2165
|
+
import { Effect as Effect10, Option as Option6, Schema as Schema5 } from "effect";
|
|
1915
2166
|
var optionalString = {
|
|
1916
2167
|
_tag: "FieldDef",
|
|
1917
2168
|
kind: "string",
|
|
@@ -1960,15 +2211,15 @@ var AuthorProfileSchema = Schema5.Struct({
|
|
|
1960
2211
|
});
|
|
1961
2212
|
var decodeAuthorProfile = Schema5.decodeUnknownEffect(Schema5.fromJsonString(AuthorProfileSchema));
|
|
1962
2213
|
function fetchAuthorProfile(relay, relayUrls, pubkey) {
|
|
1963
|
-
return
|
|
2214
|
+
return Effect10.gen(function* () {
|
|
1964
2215
|
for (const url of relayUrls) {
|
|
1965
|
-
const result = yield*
|
|
2216
|
+
const result = yield* Effect10.result(
|
|
1966
2217
|
relay.fetchByFilter({ kinds: [0], authors: [pubkey], limit: 1 }, url)
|
|
1967
2218
|
);
|
|
1968
2219
|
if (result._tag === "Success" && result.success.length > 0) {
|
|
1969
2220
|
return yield* decodeAuthorProfile(result.success[0].content).pipe(
|
|
1970
|
-
|
|
1971
|
-
|
|
2221
|
+
Effect10.map(Option6.some),
|
|
2222
|
+
Effect10.orElseSucceed(() => Option6.none())
|
|
1972
2223
|
);
|
|
1973
2224
|
}
|
|
1974
2225
|
}
|
|
@@ -2018,15 +2269,15 @@ var SyncStatus = class extends ServiceMap9.Service()(
|
|
|
2018
2269
|
};
|
|
2019
2270
|
|
|
2020
2271
|
// src/layers/IdentityLive.ts
|
|
2021
|
-
import { Effect as
|
|
2022
|
-
import { hexToBytes as
|
|
2272
|
+
import { Effect as Effect12, Layer as Layer2 } from "effect";
|
|
2273
|
+
import { hexToBytes as hexToBytes4 } from "@noble/hashes/utils.js";
|
|
2023
2274
|
|
|
2024
2275
|
// src/db/identity.ts
|
|
2025
|
-
import { Effect as
|
|
2276
|
+
import { Effect as Effect11 } from "effect";
|
|
2026
2277
|
import { getPublicKey as getPublicKey2 } from "nostr-tools/pure";
|
|
2027
|
-
import { bytesToHex as
|
|
2278
|
+
import { bytesToHex as bytesToHex3 } from "@noble/hashes/utils.js";
|
|
2028
2279
|
function createIdentity(suppliedKey) {
|
|
2029
|
-
return
|
|
2280
|
+
return Effect11.gen(function* () {
|
|
2030
2281
|
let privateKey;
|
|
2031
2282
|
if (suppliedKey) {
|
|
2032
2283
|
if (suppliedKey.length !== 32) {
|
|
@@ -2039,8 +2290,8 @@ function createIdentity(suppliedKey) {
|
|
|
2039
2290
|
privateKey = new Uint8Array(32);
|
|
2040
2291
|
crypto.getRandomValues(privateKey);
|
|
2041
2292
|
}
|
|
2042
|
-
const privateKeyHex =
|
|
2043
|
-
const publicKey = yield*
|
|
2293
|
+
const privateKeyHex = bytesToHex3(privateKey);
|
|
2294
|
+
const publicKey = yield* Effect11.try({
|
|
2044
2295
|
try: () => getPublicKey2(privateKey),
|
|
2045
2296
|
catch: (e) => new CryptoError({
|
|
2046
2297
|
message: `Failed to derive public key: ${e instanceof Error ? e.message : String(e)}`,
|
|
@@ -2058,14 +2309,14 @@ function createIdentity(suppliedKey) {
|
|
|
2058
2309
|
// src/layers/IdentityLive.ts
|
|
2059
2310
|
var IdentityLive = Layer2.effect(
|
|
2060
2311
|
Identity,
|
|
2061
|
-
|
|
2312
|
+
Effect12.gen(function* () {
|
|
2062
2313
|
const config = yield* Config;
|
|
2063
2314
|
const storage = yield* Storage;
|
|
2064
2315
|
const idbKey = yield* storage.getMeta("identity_key");
|
|
2065
|
-
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);
|
|
2066
2317
|
const identity = yield* createIdentity(resolvedKey);
|
|
2067
2318
|
yield* storage.putMeta("identity_key", identity.exportKey());
|
|
2068
|
-
yield*
|
|
2319
|
+
yield* Effect12.logInfo("Identity loaded", {
|
|
2069
2320
|
publicKey: identity.publicKey.slice(0, 12) + "...",
|
|
2070
2321
|
source: config.privateKey ? "config" : resolvedKey ? "storage" : "generated"
|
|
2071
2322
|
});
|
|
@@ -2074,12 +2325,12 @@ var IdentityLive = Layer2.effect(
|
|
|
2074
2325
|
);
|
|
2075
2326
|
|
|
2076
2327
|
// src/layers/EpochStoreLive.ts
|
|
2077
|
-
import { Effect as
|
|
2328
|
+
import { Effect as Effect13, Layer as Layer3, Option as Option7 } from "effect";
|
|
2078
2329
|
import { generateSecretKey as generateSecretKey2 } from "nostr-tools/pure";
|
|
2079
|
-
import { bytesToHex as
|
|
2330
|
+
import { bytesToHex as bytesToHex4 } from "@noble/hashes/utils.js";
|
|
2080
2331
|
var EpochStoreLive = Layer3.effect(
|
|
2081
2332
|
EpochStore,
|
|
2082
|
-
|
|
2333
|
+
Effect13.gen(function* () {
|
|
2083
2334
|
const config = yield* Config;
|
|
2084
2335
|
const identity = yield* Identity;
|
|
2085
2336
|
const storage = yield* Storage;
|
|
@@ -2098,7 +2349,7 @@ var EpochStoreLive = Layer3.effect(
|
|
|
2098
2349
|
return existing !== void 0 && existing.privateKey === ek.key;
|
|
2099
2350
|
});
|
|
2100
2351
|
if (configIsSubset) {
|
|
2101
|
-
yield*
|
|
2352
|
+
yield* Effect13.logInfo("Epoch store loaded", {
|
|
2102
2353
|
source: "storage",
|
|
2103
2354
|
epochs: idbStore.epochs.size
|
|
2104
2355
|
});
|
|
@@ -2107,271 +2358,28 @@ var EpochStoreLive = Layer3.effect(
|
|
|
2107
2358
|
}
|
|
2108
2359
|
const store2 = createEpochStoreFromInputs(config.epochKeys);
|
|
2109
2360
|
yield* storage.putMeta("epochs", stringifyEpochStore(store2));
|
|
2110
|
-
yield*
|
|
2361
|
+
yield* Effect13.logInfo("Epoch store loaded", { source: "config", epochs: store2.epochs.size });
|
|
2111
2362
|
return store2;
|
|
2112
2363
|
}
|
|
2113
2364
|
if (idbStore) {
|
|
2114
|
-
yield*
|
|
2365
|
+
yield* Effect13.logInfo("Epoch store loaded", {
|
|
2115
2366
|
source: "storage",
|
|
2116
2367
|
epochs: idbStore.epochs.size
|
|
2117
2368
|
});
|
|
2118
2369
|
return idbStore;
|
|
2119
2370
|
}
|
|
2120
2371
|
const store = createEpochStoreFromInputs(
|
|
2121
|
-
[{ epochId: EpochId("epoch-0"), key:
|
|
2372
|
+
[{ epochId: EpochId("epoch-0"), key: bytesToHex4(generateSecretKey2()) }],
|
|
2122
2373
|
{ createdBy: identity.publicKey }
|
|
2123
2374
|
);
|
|
2124
2375
|
yield* storage.putMeta("epochs", stringifyEpochStore(store));
|
|
2125
|
-
yield*
|
|
2376
|
+
yield* Effect13.logInfo("Epoch store loaded", { source: "generated", epochs: store.epochs.size });
|
|
2126
2377
|
return store;
|
|
2127
2378
|
})
|
|
2128
2379
|
);
|
|
2129
2380
|
|
|
2130
2381
|
// src/layers/StorageLive.ts
|
|
2131
2382
|
import { Effect as Effect14, Layer as Layer4 } from "effect";
|
|
2132
|
-
|
|
2133
|
-
// src/storage/idb.ts
|
|
2134
|
-
import { Effect as Effect13 } from "effect";
|
|
2135
|
-
import { openDB } from "idb";
|
|
2136
|
-
|
|
2137
|
-
// src/sync/compact-event.ts
|
|
2138
|
-
import { bytesToHex as bytesToHex4, hexToBytes as hexToBytes4 } from "@noble/hashes/utils.js";
|
|
2139
|
-
var VERSION = 1;
|
|
2140
|
-
var HEADER_SIZE = 133;
|
|
2141
|
-
function base64ToBytes(base64) {
|
|
2142
|
-
const binary = atob(base64);
|
|
2143
|
-
const bytes = new Uint8Array(binary.length);
|
|
2144
|
-
for (let i = 0; i < binary.length; i++) {
|
|
2145
|
-
bytes[i] = binary.charCodeAt(i);
|
|
2146
|
-
}
|
|
2147
|
-
return bytes;
|
|
2148
|
-
}
|
|
2149
|
-
function bytesToBase64(bytes) {
|
|
2150
|
-
let binary = "";
|
|
2151
|
-
for (let i = 0; i < bytes.length; i++) {
|
|
2152
|
-
binary += String.fromCharCode(bytes[i]);
|
|
2153
|
-
}
|
|
2154
|
-
return btoa(binary);
|
|
2155
|
-
}
|
|
2156
|
-
function packEvent(event) {
|
|
2157
|
-
const pubkey = hexToBytes4(event.pubkey);
|
|
2158
|
-
const sig = hexToBytes4(event.sig);
|
|
2159
|
-
const recipientTag = event.tags.find((t) => t[0] === "p");
|
|
2160
|
-
if (!recipientTag) throw new Error("Gift wrap missing #p tag");
|
|
2161
|
-
if (event.tags.some((t) => t[0] !== "p")) {
|
|
2162
|
-
throw new Error("Gift wrap has unexpected non-p tags; compact encoding would lose them");
|
|
2163
|
-
}
|
|
2164
|
-
const recipient = hexToBytes4(recipientTag[1]);
|
|
2165
|
-
const createdAtBuf = new Uint8Array(4);
|
|
2166
|
-
new DataView(createdAtBuf.buffer).setUint32(0, event.created_at, false);
|
|
2167
|
-
const content = base64ToBytes(event.content);
|
|
2168
|
-
const result = new Uint8Array(HEADER_SIZE + content.length);
|
|
2169
|
-
result[0] = VERSION;
|
|
2170
|
-
result.set(pubkey, 1);
|
|
2171
|
-
result.set(sig, 33);
|
|
2172
|
-
result.set(recipient, 97);
|
|
2173
|
-
result.set(createdAtBuf, 129);
|
|
2174
|
-
result.set(content, HEADER_SIZE);
|
|
2175
|
-
return result;
|
|
2176
|
-
}
|
|
2177
|
-
function unpackEvent(id, compact) {
|
|
2178
|
-
const version = compact[0];
|
|
2179
|
-
if (version !== VERSION) throw new Error(`Unknown compact event version: ${version}`);
|
|
2180
|
-
const pubkey = bytesToHex4(compact.slice(1, 33));
|
|
2181
|
-
const sig = bytesToHex4(compact.slice(33, 97));
|
|
2182
|
-
const recipient = bytesToHex4(compact.slice(97, 129));
|
|
2183
|
-
const dv = new DataView(compact.buffer, compact.byteOffset + 129, 4);
|
|
2184
|
-
const createdAt = dv.getUint32(0, false);
|
|
2185
|
-
const content = bytesToBase64(compact.slice(HEADER_SIZE));
|
|
2186
|
-
return {
|
|
2187
|
-
id,
|
|
2188
|
-
pubkey,
|
|
2189
|
-
sig,
|
|
2190
|
-
created_at: createdAt,
|
|
2191
|
-
kind: 1059,
|
|
2192
|
-
tags: [["p", recipient]],
|
|
2193
|
-
content
|
|
2194
|
-
};
|
|
2195
|
-
}
|
|
2196
|
-
|
|
2197
|
-
// src/storage/idb.ts
|
|
2198
|
-
var DB_NAME = "tablinum";
|
|
2199
|
-
function storeName(collection2) {
|
|
2200
|
-
return `col_${collection2}`;
|
|
2201
|
-
}
|
|
2202
|
-
function computeSchemaSig(schema) {
|
|
2203
|
-
return Object.entries(schema).sort(([a], [b]) => a.localeCompare(b)).map(([name, def]) => {
|
|
2204
|
-
const indices = [...def.indices ?? []].sort().join(",");
|
|
2205
|
-
return `${name}:${indices}`;
|
|
2206
|
-
}).join("|");
|
|
2207
|
-
}
|
|
2208
|
-
function wrap(label, fn) {
|
|
2209
|
-
return Effect13.tryPromise({
|
|
2210
|
-
try: fn,
|
|
2211
|
-
catch: (e) => new StorageError({
|
|
2212
|
-
message: `IndexedDB ${label} failed: ${e instanceof Error ? e.message : String(e)}`,
|
|
2213
|
-
cause: e
|
|
2214
|
-
})
|
|
2215
|
-
});
|
|
2216
|
-
}
|
|
2217
|
-
function upgradeSchema(database, schema, tx) {
|
|
2218
|
-
if (!database.objectStoreNames.contains("_meta")) {
|
|
2219
|
-
database.createObjectStore("_meta");
|
|
2220
|
-
}
|
|
2221
|
-
if (!database.objectStoreNames.contains("events")) {
|
|
2222
|
-
const events = database.createObjectStore("events", { keyPath: "id" });
|
|
2223
|
-
events.createIndex("by-record", ["collection", "recordId"]);
|
|
2224
|
-
}
|
|
2225
|
-
if (!database.objectStoreNames.contains("giftwraps")) {
|
|
2226
|
-
database.createObjectStore("giftwraps", { keyPath: "id" });
|
|
2227
|
-
}
|
|
2228
|
-
const expectedStores = /* @__PURE__ */ new Set();
|
|
2229
|
-
for (const [, def] of Object.entries(schema)) {
|
|
2230
|
-
const sn = storeName(def.name);
|
|
2231
|
-
expectedStores.add(sn);
|
|
2232
|
-
if (!database.objectStoreNames.contains(sn)) {
|
|
2233
|
-
const store = database.createObjectStore(sn, { keyPath: "id" });
|
|
2234
|
-
for (const idx of def.indices ?? []) {
|
|
2235
|
-
store.createIndex(idx, idx);
|
|
2236
|
-
}
|
|
2237
|
-
} else {
|
|
2238
|
-
const store = tx.objectStore(sn);
|
|
2239
|
-
const existingIndices = new Set(Array.from(store.indexNames));
|
|
2240
|
-
const wantedIndices = new Set(def.indices ?? []);
|
|
2241
|
-
for (const idx of existingIndices) {
|
|
2242
|
-
if (!wantedIndices.has(idx)) store.deleteIndex(idx);
|
|
2243
|
-
}
|
|
2244
|
-
for (const idx of wantedIndices) {
|
|
2245
|
-
if (!existingIndices.has(idx)) store.createIndex(idx, idx);
|
|
2246
|
-
}
|
|
2247
|
-
}
|
|
2248
|
-
}
|
|
2249
|
-
for (const existing of Array.from(database.objectStoreNames)) {
|
|
2250
|
-
if (existing.startsWith("col_") && !expectedStores.has(existing)) {
|
|
2251
|
-
database.deleteObjectStore(existing);
|
|
2252
|
-
}
|
|
2253
|
-
}
|
|
2254
|
-
tx.objectStore("_meta").put(computeSchemaSig(schema), "schema_sig");
|
|
2255
|
-
}
|
|
2256
|
-
function openIDBStorage(dbName, schema) {
|
|
2257
|
-
return Effect13.gen(function* () {
|
|
2258
|
-
const name = dbName ?? DB_NAME;
|
|
2259
|
-
const schemaSig = computeSchemaSig(schema);
|
|
2260
|
-
if (typeof indexedDB === "undefined") {
|
|
2261
|
-
return yield* Effect13.fail(
|
|
2262
|
-
new StorageError({
|
|
2263
|
-
message: "IndexedDB is not available in this environment"
|
|
2264
|
-
})
|
|
2265
|
-
);
|
|
2266
|
-
}
|
|
2267
|
-
const probeDb = yield* Effect13.tryPromise({
|
|
2268
|
-
try: () => openDB(name),
|
|
2269
|
-
catch: (e) => new StorageError({ message: "Failed to open IndexedDB", cause: e })
|
|
2270
|
-
});
|
|
2271
|
-
const currentVersion = probeDb.version;
|
|
2272
|
-
let needsUpgrade = true;
|
|
2273
|
-
if (probeDb.objectStoreNames.contains("_meta")) {
|
|
2274
|
-
const storedSig = yield* Effect13.tryPromise({
|
|
2275
|
-
try: () => probeDb.get("_meta", "schema_sig"),
|
|
2276
|
-
catch: () => new StorageError({ message: "Failed to read schema meta" })
|
|
2277
|
-
}).pipe(Effect13.catch(() => Effect13.succeed(void 0)));
|
|
2278
|
-
needsUpgrade = storedSig !== schemaSig;
|
|
2279
|
-
}
|
|
2280
|
-
probeDb.close();
|
|
2281
|
-
const db = needsUpgrade ? yield* Effect13.tryPromise({
|
|
2282
|
-
try: () => openDB(name, currentVersion + 1, {
|
|
2283
|
-
upgrade(database, _oldVersion, _newVersion, transaction) {
|
|
2284
|
-
upgradeSchema(database, schema, transaction);
|
|
2285
|
-
}
|
|
2286
|
-
}),
|
|
2287
|
-
catch: (e) => new StorageError({ message: "Failed to open IndexedDB", cause: e })
|
|
2288
|
-
}) : yield* Effect13.tryPromise({
|
|
2289
|
-
try: () => openDB(name),
|
|
2290
|
-
catch: (e) => new StorageError({ message: "Failed to open IndexedDB", cause: e })
|
|
2291
|
-
});
|
|
2292
|
-
yield* Effect13.addFinalizer(() => Effect13.sync(() => db.close()));
|
|
2293
|
-
const handle = {
|
|
2294
|
-
putRecord: (collection2, record) => wrap("putRecord", () => db.put(storeName(collection2), record).then(() => void 0)),
|
|
2295
|
-
getRecord: (collection2, id) => wrap("getRecord", () => db.get(storeName(collection2), id)),
|
|
2296
|
-
getAllRecords: (collection2) => wrap("getAllRecords", () => db.getAll(storeName(collection2))),
|
|
2297
|
-
countRecords: (collection2) => wrap("countRecords", () => db.count(storeName(collection2))),
|
|
2298
|
-
clearRecords: (collection2) => wrap("clearRecords", () => db.clear(storeName(collection2))),
|
|
2299
|
-
getByIndex: (collection2, indexName, value) => wrap("getByIndex", () => db.getAllFromIndex(storeName(collection2), indexName, value)),
|
|
2300
|
-
getByIndexRange: (collection2, indexName, range) => wrap("getByIndexRange", () => db.getAllFromIndex(storeName(collection2), indexName, range)),
|
|
2301
|
-
getAllSorted: (collection2, indexName, direction) => wrap("getAllSorted", async () => {
|
|
2302
|
-
const sn = storeName(collection2);
|
|
2303
|
-
const tx = db.transaction(sn, "readonly");
|
|
2304
|
-
const store = tx.objectStore(sn);
|
|
2305
|
-
const index = store.index(indexName);
|
|
2306
|
-
const results = [];
|
|
2307
|
-
let cursor = await index.openCursor(null, direction ?? "next");
|
|
2308
|
-
while (cursor) {
|
|
2309
|
-
results.push(cursor.value);
|
|
2310
|
-
cursor = await cursor.continue();
|
|
2311
|
-
}
|
|
2312
|
-
return results;
|
|
2313
|
-
}),
|
|
2314
|
-
putEvent: (event) => wrap("putEvent", () => db.put("events", event).then(() => void 0)),
|
|
2315
|
-
getEvent: (id) => wrap("getEvent", () => db.get("events", id)),
|
|
2316
|
-
getAllEvents: () => wrap("getAllEvents", () => db.getAll("events")),
|
|
2317
|
-
getEventsByRecord: (collection2, recordId) => wrap(
|
|
2318
|
-
"getEventsByRecord",
|
|
2319
|
-
() => db.getAllFromIndex("events", "by-record", [collection2, recordId])
|
|
2320
|
-
),
|
|
2321
|
-
putGiftWrap: (gw) => wrap("putGiftWrap", async () => {
|
|
2322
|
-
if (gw.event) {
|
|
2323
|
-
const compact = packEvent(gw.event);
|
|
2324
|
-
await db.put("giftwraps", { id: gw.id, compact, createdAt: gw.createdAt });
|
|
2325
|
-
} else {
|
|
2326
|
-
await db.put("giftwraps", { id: gw.id, createdAt: gw.createdAt });
|
|
2327
|
-
}
|
|
2328
|
-
}),
|
|
2329
|
-
getGiftWrap: (id) => wrap("getGiftWrap", async () => {
|
|
2330
|
-
const raw = await db.get("giftwraps", id);
|
|
2331
|
-
if (!raw) return void 0;
|
|
2332
|
-
if (raw.compact) {
|
|
2333
|
-
return { id: raw.id, event: unpackEvent(raw.id, raw.compact), createdAt: raw.createdAt };
|
|
2334
|
-
}
|
|
2335
|
-
if (raw.event) {
|
|
2336
|
-
const compact = packEvent(raw.event);
|
|
2337
|
-
await db.put("giftwraps", { id: raw.id, compact, createdAt: raw.createdAt });
|
|
2338
|
-
return { id: raw.id, event: raw.event, createdAt: raw.createdAt };
|
|
2339
|
-
}
|
|
2340
|
-
return { id: raw.id, createdAt: raw.createdAt };
|
|
2341
|
-
}),
|
|
2342
|
-
getAllGiftWraps: () => wrap("getAllGiftWraps", async () => {
|
|
2343
|
-
const raws = await db.getAll("giftwraps");
|
|
2344
|
-
const results = [];
|
|
2345
|
-
for (const raw of raws) {
|
|
2346
|
-
if (raw.compact) {
|
|
2347
|
-
results.push({ id: raw.id, event: unpackEvent(raw.id, raw.compact), createdAt: raw.createdAt });
|
|
2348
|
-
} else if (raw.event) {
|
|
2349
|
-
const compact = packEvent(raw.event);
|
|
2350
|
-
await db.put("giftwraps", { id: raw.id, compact, createdAt: raw.createdAt });
|
|
2351
|
-
results.push({ id: raw.id, event: raw.event, createdAt: raw.createdAt });
|
|
2352
|
-
} else {
|
|
2353
|
-
results.push({ id: raw.id, createdAt: raw.createdAt });
|
|
2354
|
-
}
|
|
2355
|
-
}
|
|
2356
|
-
return results;
|
|
2357
|
-
}),
|
|
2358
|
-
deleteGiftWrap: (id) => wrap("deleteGiftWrap", () => db.delete("giftwraps", id).then(() => void 0)),
|
|
2359
|
-
deleteEvent: (id) => wrap("deleteEvent", () => db.delete("events", id).then(() => void 0)),
|
|
2360
|
-
stripEventData: (id) => wrap("stripEventData", async () => {
|
|
2361
|
-
const existing = await db.get("events", id);
|
|
2362
|
-
if (existing) {
|
|
2363
|
-
await db.put("events", { ...existing, data: null });
|
|
2364
|
-
}
|
|
2365
|
-
}),
|
|
2366
|
-
getMeta: (key) => wrap("getMeta", () => db.get("_meta", key)),
|
|
2367
|
-
putMeta: (key, value) => wrap("putMeta", () => db.put("_meta", value, key).then(() => void 0)),
|
|
2368
|
-
close: () => Effect13.sync(() => db.close())
|
|
2369
|
-
};
|
|
2370
|
-
return handle;
|
|
2371
|
-
});
|
|
2372
|
-
}
|
|
2373
|
-
|
|
2374
|
-
// src/layers/StorageLive.ts
|
|
2375
2383
|
var StorageLive = Layer4.effect(
|
|
2376
2384
|
Storage,
|
|
2377
2385
|
Effect14.gen(function* () {
|
|
@@ -3050,6 +3058,60 @@ var TablinumLive = Layer9.effect(
|
|
|
3050
3058
|
yield* Scope4.close(scope, Exit.void);
|
|
3051
3059
|
})
|
|
3052
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
|
+
),
|
|
3053
3115
|
rebuild: () => ensureOpen(
|
|
3054
3116
|
rebuild(
|
|
3055
3117
|
storage,
|
|
@@ -3187,6 +3249,9 @@ function validateConfig(config) {
|
|
|
3187
3249
|
}
|
|
3188
3250
|
});
|
|
3189
3251
|
}
|
|
3252
|
+
function deleteDatabase(dbName) {
|
|
3253
|
+
return deleteIDBStorage(DatabaseName(dbName ?? "tablinum"));
|
|
3254
|
+
}
|
|
3190
3255
|
function createTablinum(config) {
|
|
3191
3256
|
return Effect22.gen(function* () {
|
|
3192
3257
|
yield* validateConfig(config);
|
|
@@ -3259,6 +3324,7 @@ export {
|
|
|
3259
3324
|
collection,
|
|
3260
3325
|
createTablinum,
|
|
3261
3326
|
decodeInvite,
|
|
3327
|
+
deleteDatabase,
|
|
3262
3328
|
encodeInvite,
|
|
3263
3329
|
field
|
|
3264
3330
|
};
|