tablinum 0.2.0 → 0.5.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/README.md +126 -28
- package/dist/crud/collection-handle.d.ts +4 -1
- package/dist/db/create-tablinum.d.ts +4 -0
- package/dist/db/database-handle.d.ts +2 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.js +338 -112
- package/dist/schema/collection.d.ts +2 -0
- package/dist/schema/field.d.ts +7 -1
- package/dist/services/Config.d.ts +2 -0
- package/dist/storage/idb.d.ts +2 -1
- package/dist/storage/records-store.d.ts +1 -0
- package/dist/svelte/collection.svelte.d.ts +3 -1
- package/dist/svelte/index.svelte.d.ts +1 -1
- package/dist/svelte/index.svelte.js +350 -121
- package/dist/svelte/tablinum.svelte.d.ts +2 -1
- package/dist/sync/sync-service.d.ts +2 -1
- package/dist/utils/diff.d.ts +2 -0
- package/package.json +10 -7
|
@@ -7,19 +7,20 @@ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require
|
|
|
7
7
|
});
|
|
8
8
|
|
|
9
9
|
// src/schema/field.ts
|
|
10
|
-
function make(kind, isOptional, isArray) {
|
|
11
|
-
return { _tag: "FieldDef", kind, isOptional, isArray };
|
|
10
|
+
function make(kind, isOptional, isArray, fields) {
|
|
11
|
+
return { _tag: "FieldDef", kind, isOptional, isArray, ...fields !== undefined && { fields } };
|
|
12
12
|
}
|
|
13
13
|
var field = {
|
|
14
14
|
string: () => make("string", false, false),
|
|
15
15
|
number: () => make("number", false, false),
|
|
16
16
|
boolean: () => make("boolean", false, false),
|
|
17
17
|
json: () => make("json", false, false),
|
|
18
|
-
|
|
19
|
-
|
|
18
|
+
object: (fields) => make("object", false, false, fields),
|
|
19
|
+
optional: (inner) => make(inner.kind, true, inner.isArray, inner.fields),
|
|
20
|
+
array: (inner) => make(inner.kind, inner.isOptional, true, inner.fields)
|
|
20
21
|
};
|
|
21
22
|
// src/schema/collection.ts
|
|
22
|
-
var RESERVED_NAMES = new Set(["id", "
|
|
23
|
+
var RESERVED_NAMES = new Set(["id", "_d", "_u", "_e", "_a"]);
|
|
23
24
|
function collection(name, fields, options) {
|
|
24
25
|
if (!name || name.trim().length === 0) {
|
|
25
26
|
throw new Error("Collection name must not be empty");
|
|
@@ -40,13 +41,17 @@ function collection(name, fields, options) {
|
|
|
40
41
|
if (!fieldDef) {
|
|
41
42
|
throw new Error(`Index field "${idx}" does not exist in collection "${name}"`);
|
|
42
43
|
}
|
|
43
|
-
if (fieldDef.kind === "json" || fieldDef.isArray) {
|
|
44
|
+
if (fieldDef.kind === "json" || fieldDef.kind === "object" || fieldDef.isArray) {
|
|
44
45
|
throw new Error(`Field "${idx}" cannot be indexed (type: ${fieldDef.kind}${fieldDef.isArray ? "[]" : ""})`);
|
|
45
46
|
}
|
|
46
47
|
indices.push(idx);
|
|
47
48
|
}
|
|
48
49
|
}
|
|
49
|
-
|
|
50
|
+
const eventRetention = options?.eventRetention ?? 1;
|
|
51
|
+
if (eventRetention < 0 || !Number.isInteger(eventRetention)) {
|
|
52
|
+
throw new Error(`eventRetention must be a non-negative integer, got ${eventRetention}`);
|
|
53
|
+
}
|
|
54
|
+
return { _tag: "CollectionDef", name, fields, indices, eventRetention };
|
|
50
55
|
}
|
|
51
56
|
// src/db/invite.ts
|
|
52
57
|
import { Schema as Schema2 } from "effect";
|
|
@@ -212,10 +217,10 @@ class NotFoundError extends Data.TaggedError("NotFoundError") {
|
|
|
212
217
|
class ClosedError extends Data.TaggedError("ClosedError") {
|
|
213
218
|
}
|
|
214
219
|
// src/svelte/tablinum.svelte.ts
|
|
215
|
-
import { Effect as Effect25, Exit as Exit2, Scope as Scope6 } from "effect";
|
|
220
|
+
import { Effect as Effect25, Exit as Exit2, References as References6, Scope as Scope6 } from "effect";
|
|
216
221
|
|
|
217
222
|
// src/db/create-tablinum.ts
|
|
218
|
-
import { Effect as Effect22, Layer as
|
|
223
|
+
import { Effect as Effect22, Layer as Layer10, References as References4, ServiceMap as ServiceMap10 } from "effect";
|
|
219
224
|
|
|
220
225
|
// src/db/runtime-config.ts
|
|
221
226
|
import { Effect, Schema as Schema3 } from "effect";
|
|
@@ -250,13 +255,13 @@ class Tablinum extends ServiceMap2.Service()("tablinum/Tablinum") {
|
|
|
250
255
|
}
|
|
251
256
|
|
|
252
257
|
// src/layers/TablinumLive.ts
|
|
253
|
-
import { Effect as Effect21, Exit, Layer as
|
|
258
|
+
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";
|
|
254
259
|
|
|
255
260
|
// src/crud/watch.ts
|
|
256
261
|
import { Effect as Effect2, PubSub, Ref, Stream } from "effect";
|
|
257
262
|
function watchCollection(ctx, storage, collectionName, filter, mapRecord) {
|
|
258
263
|
const query = () => Effect2.map(storage.getAllRecords(collectionName), (all) => {
|
|
259
|
-
const filtered = all.filter((r) => !r.
|
|
264
|
+
const filtered = all.filter((r) => !r._d && (filter ? filter(r) : true));
|
|
260
265
|
return mapRecord ? filtered.map(mapRecord) : filtered;
|
|
261
266
|
});
|
|
262
267
|
const changes = Stream.fromPubSub(ctx.pubsub).pipe(Stream.filter((event) => event.collection === collectionName), Stream.mapEffect(() => Effect2.gen(function* () {
|
|
@@ -301,14 +306,56 @@ function resolveWinner(existing, incoming) {
|
|
|
301
306
|
return incoming.id < existing.id ? incoming : existing;
|
|
302
307
|
}
|
|
303
308
|
|
|
309
|
+
// src/utils/diff.ts
|
|
310
|
+
function isPlainObject(value) {
|
|
311
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
312
|
+
}
|
|
313
|
+
function deepDiff(before, after) {
|
|
314
|
+
const result = {};
|
|
315
|
+
let hasChanges = false;
|
|
316
|
+
for (const key of Object.keys(after)) {
|
|
317
|
+
const a = before[key];
|
|
318
|
+
const b = after[key];
|
|
319
|
+
if (isPlainObject(a) && isPlainObject(b)) {
|
|
320
|
+
const nested = deepDiff(a, b);
|
|
321
|
+
if (nested !== null) {
|
|
322
|
+
result[key] = nested;
|
|
323
|
+
hasChanges = true;
|
|
324
|
+
}
|
|
325
|
+
} else if (Array.isArray(a) && Array.isArray(b)) {
|
|
326
|
+
if (JSON.stringify(a) !== JSON.stringify(b)) {
|
|
327
|
+
result[key] = b;
|
|
328
|
+
hasChanges = true;
|
|
329
|
+
}
|
|
330
|
+
} else if (a !== b) {
|
|
331
|
+
result[key] = b;
|
|
332
|
+
hasChanges = true;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
return hasChanges ? result : null;
|
|
336
|
+
}
|
|
337
|
+
function deepMerge(target, source) {
|
|
338
|
+
const result = { ...target };
|
|
339
|
+
for (const key of Object.keys(source)) {
|
|
340
|
+
const t = target[key];
|
|
341
|
+
const s = source[key];
|
|
342
|
+
if (isPlainObject(t) && isPlainObject(s)) {
|
|
343
|
+
result[key] = deepMerge(t, s);
|
|
344
|
+
} else {
|
|
345
|
+
result[key] = s;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
return result;
|
|
349
|
+
}
|
|
350
|
+
|
|
304
351
|
// src/storage/records-store.ts
|
|
305
352
|
function buildRecord(event) {
|
|
306
353
|
return {
|
|
307
354
|
id: event.recordId,
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
355
|
+
_d: event.kind === "d",
|
|
356
|
+
_u: event.createdAt,
|
|
357
|
+
_e: event.id,
|
|
358
|
+
_a: event.author,
|
|
312
359
|
...event.data ?? {}
|
|
313
360
|
};
|
|
314
361
|
}
|
|
@@ -317,15 +364,19 @@ function applyEvent(storage, event) {
|
|
|
317
364
|
const existing = yield* storage.getRecord(event.collection, event.recordId);
|
|
318
365
|
if (existing) {
|
|
319
366
|
const existingMeta = {
|
|
320
|
-
id: existing.
|
|
321
|
-
createdAt: existing.
|
|
367
|
+
id: existing._e,
|
|
368
|
+
createdAt: existing._u
|
|
322
369
|
};
|
|
323
370
|
const incomingMeta = { id: event.id, createdAt: event.createdAt };
|
|
324
371
|
const winner = resolveWinner(existingMeta, incomingMeta);
|
|
325
372
|
if (winner.id !== event.id)
|
|
326
373
|
return false;
|
|
327
374
|
}
|
|
328
|
-
|
|
375
|
+
if (existing && event.kind === "u") {
|
|
376
|
+
yield* storage.putRecord(event.collection, deepMerge(existing, buildRecord(event)));
|
|
377
|
+
} else {
|
|
378
|
+
yield* storage.putRecord(event.collection, buildRecord(event));
|
|
379
|
+
}
|
|
329
380
|
return true;
|
|
330
381
|
});
|
|
331
382
|
}
|
|
@@ -335,15 +386,11 @@ function rebuild(storage, collections) {
|
|
|
335
386
|
yield* storage.clearRecords(col);
|
|
336
387
|
}
|
|
337
388
|
const allEvents = yield* storage.getAllEvents();
|
|
338
|
-
const sorted = [...allEvents].sort((a, b) => a.createdAt - b.createdAt);
|
|
339
|
-
const winners = new Map;
|
|
389
|
+
const sorted = [...allEvents].sort((a, b) => a.createdAt - b.createdAt || (a.id < b.id ? -1 : 1));
|
|
340
390
|
for (const event of sorted) {
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
}
|
|
345
|
-
for (const event of winners.values()) {
|
|
346
|
-
yield* storage.putRecord(event.collection, buildRecord(event));
|
|
391
|
+
if (event.data === null && event.kind !== "d")
|
|
392
|
+
continue;
|
|
393
|
+
yield* applyEvent(storage, event);
|
|
347
394
|
}
|
|
348
395
|
});
|
|
349
396
|
}
|
|
@@ -365,6 +412,14 @@ function fieldDefToSchema(fd) {
|
|
|
365
412
|
case "json":
|
|
366
413
|
base = Schema4.Unknown;
|
|
367
414
|
break;
|
|
415
|
+
case "object": {
|
|
416
|
+
const nested = {};
|
|
417
|
+
for (const [k, v] of Object.entries(fd.fields)) {
|
|
418
|
+
nested[k] = fieldDefToSchema(v);
|
|
419
|
+
}
|
|
420
|
+
base = Schema4.Struct(nested);
|
|
421
|
+
break;
|
|
422
|
+
}
|
|
368
423
|
}
|
|
369
424
|
if (fd.isArray) {
|
|
370
425
|
base = Schema4.Array(base);
|
|
@@ -414,9 +469,25 @@ function buildPartialValidator(collectionName, def) {
|
|
|
414
469
|
}
|
|
415
470
|
|
|
416
471
|
// src/crud/collection-handle.ts
|
|
417
|
-
import { Effect as Effect6, Option as Option3 } from "effect";
|
|
472
|
+
import { Effect as Effect6, Option as Option3, References } from "effect";
|
|
418
473
|
|
|
419
474
|
// src/utils/uuid.ts
|
|
475
|
+
var alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
|
|
476
|
+
function toBase64url(bytes) {
|
|
477
|
+
let result = "";
|
|
478
|
+
for (let i = 0;i < bytes.length; i += 3) {
|
|
479
|
+
const b0 = bytes[i];
|
|
480
|
+
const b1 = bytes[i + 1] ?? 0;
|
|
481
|
+
const b2 = bytes[i + 2] ?? 0;
|
|
482
|
+
result += alphabet[b0 >> 2];
|
|
483
|
+
result += alphabet[(b0 & 3) << 4 | b1 >> 4];
|
|
484
|
+
if (i + 1 < bytes.length)
|
|
485
|
+
result += alphabet[(b1 & 15) << 2 | b2 >> 6];
|
|
486
|
+
if (i + 2 < bytes.length)
|
|
487
|
+
result += alphabet[b2 & 63];
|
|
488
|
+
}
|
|
489
|
+
return result;
|
|
490
|
+
}
|
|
420
491
|
function uuidv7() {
|
|
421
492
|
const now = Date.now();
|
|
422
493
|
const bytes = new Uint8Array(16);
|
|
@@ -429,8 +500,7 @@ function uuidv7() {
|
|
|
429
500
|
bytes[5] = now & 255;
|
|
430
501
|
bytes[6] = bytes[6] & 15 | 112;
|
|
431
502
|
bytes[8] = bytes[8] & 63 | 128;
|
|
432
|
-
|
|
433
|
-
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
503
|
+
return toBase64url(bytes);
|
|
434
504
|
}
|
|
435
505
|
|
|
436
506
|
// src/crud/query-builder.ts
|
|
@@ -448,7 +518,7 @@ function executeQuery(ctx, plan) {
|
|
|
448
518
|
field: plan.fieldName
|
|
449
519
|
});
|
|
450
520
|
}
|
|
451
|
-
if (fieldDef.kind === "json" || fieldDef.isArray) {
|
|
521
|
+
if (fieldDef.kind === "json" || fieldDef.kind === "object" || fieldDef.isArray) {
|
|
452
522
|
return yield* new ValidationError({
|
|
453
523
|
message: `Field "${plan.fieldName}" does not support filtering (type: ${fieldDef.kind}${fieldDef.isArray ? "[]" : ""})`,
|
|
454
524
|
field: plan.fieldName
|
|
@@ -473,7 +543,7 @@ function executeQuery(ctx, plan) {
|
|
|
473
543
|
} else {
|
|
474
544
|
results = [...yield* ctx.storage.getAllRecords(ctx.collectionName)];
|
|
475
545
|
}
|
|
476
|
-
results = results.filter((r) => !r.
|
|
546
|
+
results = results.filter((r) => !r._d);
|
|
477
547
|
for (const f of plan.filters) {
|
|
478
548
|
results = results.filter(f);
|
|
479
549
|
}
|
|
@@ -594,12 +664,62 @@ function createOrderByBuilder(storage, watchCtx, collectionName, def, fieldName,
|
|
|
594
664
|
}
|
|
595
665
|
|
|
596
666
|
// src/crud/collection-handle.ts
|
|
667
|
+
var KIND_FULL = { c: "create", u: "update", d: "delete" };
|
|
668
|
+
function sortChronologically(events) {
|
|
669
|
+
return [...events].sort((a, b) => a.createdAt - b.createdAt || (a.id < b.id ? -1 : 1));
|
|
670
|
+
}
|
|
671
|
+
function replayState(recordId, events, stopAtId) {
|
|
672
|
+
let state = null;
|
|
673
|
+
for (const e of events) {
|
|
674
|
+
if (e.kind === "d") {
|
|
675
|
+
state = null;
|
|
676
|
+
} else if (e.data !== null) {
|
|
677
|
+
if (state === null) {
|
|
678
|
+
state = { id: recordId, ...e.data };
|
|
679
|
+
} else {
|
|
680
|
+
state = deepMerge(state, e.data);
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
if (stopAtId !== undefined && e.id === stopAtId) {
|
|
684
|
+
break;
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
return state;
|
|
688
|
+
}
|
|
689
|
+
function promoteToSnapshot(storage, collection2, recordId, target, allSorted) {
|
|
690
|
+
return Effect6.gen(function* () {
|
|
691
|
+
const chronological = sortChronologically(allSorted);
|
|
692
|
+
const state = replayState(recordId, chronological, target.id);
|
|
693
|
+
if (state) {
|
|
694
|
+
yield* storage.putEvent({ ...target, data: state });
|
|
695
|
+
}
|
|
696
|
+
});
|
|
697
|
+
}
|
|
698
|
+
function pruneEvents(storage, collection2, recordId, retention) {
|
|
699
|
+
return Effect6.gen(function* () {
|
|
700
|
+
const events = yield* storage.getEventsByRecord(collection2, recordId);
|
|
701
|
+
if (events.length <= retention)
|
|
702
|
+
return;
|
|
703
|
+
const sorted = [...events].sort((a, b) => b.createdAt - a.createdAt || (a.id < b.id ? 1 : -1));
|
|
704
|
+
const retained = sorted.slice(0, retention);
|
|
705
|
+
const toStrip = sorted.slice(retention);
|
|
706
|
+
const oldestRetained = retained[retained.length - 1];
|
|
707
|
+
if (retention > 0 && oldestRetained?.kind === "u" && oldestRetained.data !== null) {
|
|
708
|
+
yield* promoteToSnapshot(storage, collection2, recordId, oldestRetained, sorted);
|
|
709
|
+
}
|
|
710
|
+
for (const e of toStrip) {
|
|
711
|
+
if (e.data !== null)
|
|
712
|
+
yield* storage.stripEventData(e.id);
|
|
713
|
+
}
|
|
714
|
+
});
|
|
715
|
+
}
|
|
597
716
|
function mapRecord(record) {
|
|
598
|
-
const {
|
|
717
|
+
const { _d, _u, _a, _e, ...fields } = record;
|
|
599
718
|
return fields;
|
|
600
719
|
}
|
|
601
|
-
function createCollectionHandle(def, storage, watchCtx, validator, partialValidator, makeEventId, onWrite) {
|
|
720
|
+
function createCollectionHandle(def, storage, watchCtx, validator, partialValidator, makeEventId, localAuthor, onWrite, logLevel = "None") {
|
|
602
721
|
const collectionName = def.name;
|
|
722
|
+
const withLog = (effect) => Effect6.provideService(effect, References.MinimumLogLevel, logLevel);
|
|
603
723
|
const commitEvent = (event) => Effect6.gen(function* () {
|
|
604
724
|
yield* storage.putEvent(event);
|
|
605
725
|
yield* applyEvent(storage, event);
|
|
@@ -608,11 +728,11 @@ function createCollectionHandle(def, storage, watchCtx, validator, partialValida
|
|
|
608
728
|
yield* notifyChange(watchCtx, {
|
|
609
729
|
collection: collectionName,
|
|
610
730
|
recordId: event.recordId,
|
|
611
|
-
kind: event.kind
|
|
731
|
+
kind: KIND_FULL[event.kind]
|
|
612
732
|
});
|
|
613
733
|
});
|
|
614
734
|
const handle = {
|
|
615
|
-
add: (data) => Effect6.gen(function* () {
|
|
735
|
+
add: (data) => withLog(Effect6.gen(function* () {
|
|
616
736
|
const id = uuidv7();
|
|
617
737
|
const fullRecord = { id, ...data };
|
|
618
738
|
yield* validator(fullRecord);
|
|
@@ -620,38 +740,44 @@ function createCollectionHandle(def, storage, watchCtx, validator, partialValida
|
|
|
620
740
|
id: makeEventId(),
|
|
621
741
|
collection: collectionName,
|
|
622
742
|
recordId: id,
|
|
623
|
-
kind: "
|
|
743
|
+
kind: "c",
|
|
624
744
|
data: fullRecord,
|
|
625
|
-
createdAt: Date.now()
|
|
745
|
+
createdAt: Date.now(),
|
|
746
|
+
author: localAuthor
|
|
626
747
|
};
|
|
627
748
|
yield* commitEvent(event);
|
|
749
|
+
yield* Effect6.logDebug("Record added", { collection: collectionName, recordId: id, data: fullRecord });
|
|
628
750
|
return id;
|
|
629
|
-
}),
|
|
630
|
-
update: (id, data) => Effect6.gen(function* () {
|
|
751
|
+
})),
|
|
752
|
+
update: (id, data) => withLog(Effect6.gen(function* () {
|
|
631
753
|
const existing = yield* storage.getRecord(collectionName, id);
|
|
632
|
-
if (!existing || existing.
|
|
754
|
+
if (!existing || existing._d) {
|
|
633
755
|
return yield* new NotFoundError({
|
|
634
756
|
collection: collectionName,
|
|
635
757
|
id
|
|
636
758
|
});
|
|
637
759
|
}
|
|
638
760
|
yield* partialValidator(data);
|
|
639
|
-
const {
|
|
761
|
+
const { _d, _u, _a, _e, ...existingFields } = existing;
|
|
640
762
|
const merged = { ...existingFields, ...data, id };
|
|
641
763
|
yield* validator(merged);
|
|
764
|
+
const diff = deepDiff(existingFields, merged);
|
|
642
765
|
const event = {
|
|
643
766
|
id: makeEventId(),
|
|
644
767
|
collection: collectionName,
|
|
645
768
|
recordId: id,
|
|
646
|
-
kind: "
|
|
647
|
-
data:
|
|
648
|
-
createdAt: Date.now()
|
|
769
|
+
kind: "u",
|
|
770
|
+
data: diff ?? { id },
|
|
771
|
+
createdAt: Date.now(),
|
|
772
|
+
author: localAuthor
|
|
649
773
|
};
|
|
650
774
|
yield* commitEvent(event);
|
|
651
|
-
|
|
652
|
-
|
|
775
|
+
yield* Effect6.logDebug("Record updated", { collection: collectionName, recordId: id, data: diff });
|
|
776
|
+
yield* pruneEvents(storage, collectionName, id, def.eventRetention);
|
|
777
|
+
})),
|
|
778
|
+
delete: (id) => withLog(Effect6.gen(function* () {
|
|
653
779
|
const existing = yield* storage.getRecord(collectionName, id);
|
|
654
|
-
if (!existing || existing.
|
|
780
|
+
if (!existing || existing._d) {
|
|
655
781
|
return yield* new NotFoundError({
|
|
656
782
|
collection: collectionName,
|
|
657
783
|
id
|
|
@@ -661,17 +787,43 @@ function createCollectionHandle(def, storage, watchCtx, validator, partialValida
|
|
|
661
787
|
id: makeEventId(),
|
|
662
788
|
collection: collectionName,
|
|
663
789
|
recordId: id,
|
|
664
|
-
kind: "
|
|
790
|
+
kind: "d",
|
|
665
791
|
data: null,
|
|
666
|
-
createdAt: Date.now()
|
|
792
|
+
createdAt: Date.now(),
|
|
793
|
+
author: localAuthor
|
|
794
|
+
};
|
|
795
|
+
yield* commitEvent(event);
|
|
796
|
+
yield* Effect6.logDebug("Record deleted", { collection: collectionName, recordId: id });
|
|
797
|
+
yield* pruneEvents(storage, collectionName, id, def.eventRetention);
|
|
798
|
+
})),
|
|
799
|
+
undo: (id) => Effect6.gen(function* () {
|
|
800
|
+
const existing = yield* storage.getRecord(collectionName, id);
|
|
801
|
+
if (!existing) {
|
|
802
|
+
return yield* new NotFoundError({ collection: collectionName, id });
|
|
803
|
+
}
|
|
804
|
+
const events = sortChronologically(yield* storage.getEventsByRecord(collectionName, id));
|
|
805
|
+
if (events.length < 2) {
|
|
806
|
+
return yield* new NotFoundError({ collection: collectionName, id });
|
|
807
|
+
}
|
|
808
|
+
const state = replayState(id, events.slice(0, -1));
|
|
809
|
+
if (!state) {
|
|
810
|
+
return yield* new NotFoundError({ collection: collectionName, id });
|
|
811
|
+
}
|
|
812
|
+
const event = {
|
|
813
|
+
id: makeEventId(),
|
|
814
|
+
collection: collectionName,
|
|
815
|
+
recordId: id,
|
|
816
|
+
kind: "u",
|
|
817
|
+
data: state,
|
|
818
|
+
createdAt: Date.now(),
|
|
819
|
+
author: localAuthor
|
|
667
820
|
};
|
|
668
821
|
yield* commitEvent(event);
|
|
669
|
-
|
|
670
|
-
yield* Effect6.forEach(oldEvents.filter((e) => e.id !== event.id), (e) => storage.deleteEvent(e.id), { discard: true });
|
|
822
|
+
yield* pruneEvents(storage, collectionName, id, def.eventRetention);
|
|
671
823
|
}),
|
|
672
824
|
get: (id) => Effect6.gen(function* () {
|
|
673
825
|
const record = yield* storage.getRecord(collectionName, id);
|
|
674
|
-
if (!record || record.
|
|
826
|
+
if (!record || record._d) {
|
|
675
827
|
return yield* new NotFoundError({
|
|
676
828
|
collection: collectionName,
|
|
677
829
|
id
|
|
@@ -680,10 +832,10 @@ function createCollectionHandle(def, storage, watchCtx, validator, partialValida
|
|
|
680
832
|
return mapRecord(record);
|
|
681
833
|
}),
|
|
682
834
|
first: () => Effect6.map(storage.getAllRecords(collectionName), (all) => {
|
|
683
|
-
const found = all.find((r) => !r.
|
|
835
|
+
const found = all.find((r) => !r._d);
|
|
684
836
|
return found ? Option3.some(mapRecord(found)) : Option3.none();
|
|
685
837
|
}),
|
|
686
|
-
count: () => Effect6.map(storage.getAllRecords(collectionName), (all) => all.filter((r) => !r.
|
|
838
|
+
count: () => Effect6.map(storage.getAllRecords(collectionName), (all) => all.filter((r) => !r._d).length),
|
|
687
839
|
watch: () => watchCollection(watchCtx, storage, collectionName, undefined, mapRecord),
|
|
688
840
|
where: (fieldName) => createWhereClause(storage, watchCtx, collectionName, def, fieldName, mapRecord),
|
|
689
841
|
orderBy: (fieldName) => createOrderByBuilder(storage, watchCtx, collectionName, def, fieldName, mapRecord)
|
|
@@ -692,7 +844,7 @@ function createCollectionHandle(def, storage, watchCtx, validator, partialValida
|
|
|
692
844
|
}
|
|
693
845
|
|
|
694
846
|
// src/sync/sync-service.ts
|
|
695
|
-
import { Effect as Effect8, Option as Option5, Ref as Ref3, Schedule } from "effect";
|
|
847
|
+
import { Effect as Effect8, Layer, Option as Option5, References as References2, Ref as Ref3, Schedule } from "effect";
|
|
696
848
|
import { unwrapEvent } from "nostr-tools/nip59";
|
|
697
849
|
import { GiftWrap as GiftWrap2 } from "nostr-tools/kinds";
|
|
698
850
|
|
|
@@ -1263,8 +1415,13 @@ function reconcileWithRelay(storage, relay, relayUrl, publicKeys) {
|
|
|
1263
1415
|
allNeedIds.push(id);
|
|
1264
1416
|
currentMsg = nextMsg;
|
|
1265
1417
|
}
|
|
1418
|
+
yield* Effect7.logDebug("Negentropy reconciliation complete", {
|
|
1419
|
+
relay: relayUrl,
|
|
1420
|
+
have: allHaveIds.length,
|
|
1421
|
+
need: allNeedIds.length
|
|
1422
|
+
});
|
|
1266
1423
|
return { haveIds: allHaveIds, needIds: allNeedIds };
|
|
1267
|
-
});
|
|
1424
|
+
}).pipe(Effect7.withLogSpan("tablinum.negentropy"));
|
|
1268
1425
|
}
|
|
1269
1426
|
|
|
1270
1427
|
// src/db/key-rotation.ts
|
|
@@ -1350,7 +1507,8 @@ function parseRemovalNotice(content, dTag) {
|
|
|
1350
1507
|
}
|
|
1351
1508
|
|
|
1352
1509
|
// src/sync/sync-service.ts
|
|
1353
|
-
function createSyncHandle(storage, giftWrapHandle, relay, publishQueue, syncStatus, watchCtx, relayUrls, knownCollections, epochStore, personalPrivateKey, personalPublicKey, scope, onSyncError, onNewAuthor, onRemoved, onMembersChanged) {
|
|
1510
|
+
function createSyncHandle(storage, giftWrapHandle, relay, publishQueue, syncStatus, watchCtx, relayUrls, knownCollections, epochStore, personalPrivateKey, personalPublicKey, scope, logLevel, onSyncError, onNewAuthor, onRemoved, onMembersChanged) {
|
|
1511
|
+
const logLayer = Layer.succeed(References2.MinimumLogLevel, logLevel);
|
|
1354
1512
|
const getSubscriptionPubKeys = () => {
|
|
1355
1513
|
return getAllPublicKeys(epochStore);
|
|
1356
1514
|
};
|
|
@@ -1360,7 +1518,7 @@ function createSyncHandle(storage, giftWrapHandle, relay, publishQueue, syncStat
|
|
|
1360
1518
|
kind: "create"
|
|
1361
1519
|
});
|
|
1362
1520
|
const forkHandled = (effect) => {
|
|
1363
|
-
Effect8.runFork(effect.pipe(Effect8.tapError((e) => Effect8.sync(() => onSyncError?.(e))), Effect8.ignore, Effect8.forkIn(scope)));
|
|
1521
|
+
Effect8.runFork(effect.pipe(Effect8.tapError((e) => Effect8.sync(() => onSyncError?.(e))), Effect8.ignore, Effect8.provide(logLayer), Effect8.forkIn(scope)));
|
|
1364
1522
|
};
|
|
1365
1523
|
let autoFlushActive = false;
|
|
1366
1524
|
const autoFlushEffect = Effect8.gen(function* () {
|
|
@@ -1409,19 +1567,21 @@ function createSyncHandle(storage, giftWrapHandle, relay, publishQueue, syncStat
|
|
|
1409
1567
|
}
|
|
1410
1568
|
const collectionName = dTag.substring(0, colonIdx);
|
|
1411
1569
|
const recordId = dTag.substring(colonIdx + 1);
|
|
1412
|
-
|
|
1570
|
+
const retention = knownCollections.get(collectionName);
|
|
1571
|
+
if (retention === undefined) {
|
|
1413
1572
|
yield* storage.putGiftWrap({ id: remoteGw.id, createdAt: remoteGw.created_at });
|
|
1414
1573
|
return null;
|
|
1415
1574
|
}
|
|
1416
1575
|
if (rumor.pubkey) {
|
|
1417
1576
|
const reject = yield* shouldRejectWrite(rumor.pubkey);
|
|
1418
1577
|
if (reject) {
|
|
1578
|
+
yield* Effect8.logWarning("Rejected write from removed member", { author: rumor.pubkey.slice(0, 12) });
|
|
1419
1579
|
yield* storage.putGiftWrap({ id: remoteGw.id, createdAt: remoteGw.created_at });
|
|
1420
1580
|
return null;
|
|
1421
1581
|
}
|
|
1422
1582
|
}
|
|
1423
1583
|
let data = null;
|
|
1424
|
-
let kind = "
|
|
1584
|
+
let kind = "u";
|
|
1425
1585
|
const parsed = yield* Effect8.try({
|
|
1426
1586
|
try: () => JSON.parse(rumor.content),
|
|
1427
1587
|
catch: () => {
|
|
@@ -1435,7 +1595,7 @@ function createSyncHandle(storage, giftWrapHandle, relay, publishQueue, syncStat
|
|
|
1435
1595
|
return null;
|
|
1436
1596
|
}
|
|
1437
1597
|
if (parsed === null || parsed._deleted) {
|
|
1438
|
-
kind = "
|
|
1598
|
+
kind = "d";
|
|
1439
1599
|
} else {
|
|
1440
1600
|
data = parsed;
|
|
1441
1601
|
}
|
|
@@ -1451,15 +1611,14 @@ function createSyncHandle(storage, giftWrapHandle, relay, publishQueue, syncStat
|
|
|
1451
1611
|
};
|
|
1452
1612
|
yield* storage.putGiftWrap({
|
|
1453
1613
|
id: remoteGw.id,
|
|
1454
|
-
eventId: event.id,
|
|
1455
1614
|
createdAt: remoteGw.created_at
|
|
1456
1615
|
});
|
|
1457
1616
|
yield* storage.putEvent(event);
|
|
1458
1617
|
const didApply = yield* applyEvent(storage, event);
|
|
1459
|
-
if (kind === "
|
|
1460
|
-
|
|
1461
|
-
yield* Effect8.forEach(oldEvents.filter((e) => e.id !== event.id), (e) => storage.deleteEvent(e.id), { discard: true });
|
|
1618
|
+
if (didApply && (kind === "u" || kind === "d")) {
|
|
1619
|
+
yield* pruneEvents(storage, collectionName, recordId, retention);
|
|
1462
1620
|
}
|
|
1621
|
+
yield* Effect8.logDebug("Processed gift wrap", { collection: collectionName, recordId, kind, author: author?.slice(0, 12) });
|
|
1463
1622
|
if (author && onNewAuthor) {
|
|
1464
1623
|
onNewAuthor(author);
|
|
1465
1624
|
}
|
|
@@ -1528,15 +1687,18 @@ function createSyncHandle(storage, giftWrapHandle, relay, publishQueue, syncStat
|
|
|
1528
1687
|
}).pipe(Effect8.tapError((err) => Effect8.sync(() => onSyncError?.(err))), Effect8.ignore);
|
|
1529
1688
|
}), { discard: true });
|
|
1530
1689
|
const syncRelay = (url, pubKeys, changedCollections) => Effect8.gen(function* () {
|
|
1690
|
+
yield* Effect8.logDebug("Syncing relay", { relay: url });
|
|
1531
1691
|
const reconcileResult = yield* Effect8.result(reconcileWithRelay(storage, relay, url, Array.from(pubKeys)));
|
|
1532
1692
|
if (reconcileResult._tag === "Failure") {
|
|
1533
1693
|
onSyncError?.(reconcileResult.failure);
|
|
1534
1694
|
return;
|
|
1535
1695
|
}
|
|
1536
1696
|
const { haveIds, needIds } = reconcileResult.success;
|
|
1697
|
+
yield* Effect8.logDebug("Relay reconciliation result", { relay: url, need: needIds.length, have: haveIds.length });
|
|
1537
1698
|
if (needIds.length > 0) {
|
|
1538
1699
|
const fetched = yield* relay.fetchEvents(needIds, url).pipe(Effect8.tapError((err) => Effect8.sync(() => onSyncError?.(err))), Effect8.orElseSucceed(() => []));
|
|
1539
|
-
|
|
1700
|
+
const sorted = [...fetched].sort((a, b) => a.created_at - b.created_at);
|
|
1701
|
+
yield* Effect8.forEach(sorted, (remoteGw) => Effect8.gen(function* () {
|
|
1540
1702
|
const collection2 = yield* processGiftWrap(remoteGw).pipe(Effect8.orElseSucceed(() => null));
|
|
1541
1703
|
if (collection2)
|
|
1542
1704
|
changedCollections.add(collection2);
|
|
@@ -1550,9 +1712,10 @@ function createSyncHandle(storage, giftWrapHandle, relay, publishQueue, syncStat
|
|
|
1550
1712
|
yield* relay.publish(gw.event, [url]).pipe(Effect8.andThen(storage.stripGiftWrapBlob(id)), Effect8.tapError((err) => Effect8.sync(() => onSyncError?.(err))), Effect8.ignore);
|
|
1551
1713
|
}), { discard: true });
|
|
1552
1714
|
}
|
|
1553
|
-
});
|
|
1715
|
+
}).pipe(Effect8.withLogSpan("tablinum.syncRelay"));
|
|
1554
1716
|
const handle = {
|
|
1555
1717
|
sync: () => Effect8.gen(function* () {
|
|
1718
|
+
yield* Effect8.logInfo("Sync started");
|
|
1556
1719
|
yield* syncStatus.set("syncing");
|
|
1557
1720
|
yield* Ref3.set(watchCtx.replayingRef, true);
|
|
1558
1721
|
const changedCollections = new Set;
|
|
@@ -1566,7 +1729,8 @@ function createSyncHandle(storage, giftWrapHandle, relay, publishQueue, syncStat
|
|
|
1566
1729
|
yield* notifyReplayComplete(watchCtx, [...changedCollections]);
|
|
1567
1730
|
yield* syncStatus.set("idle");
|
|
1568
1731
|
})));
|
|
1569
|
-
|
|
1732
|
+
yield* Effect8.logInfo("Sync complete", { changed: [...changedCollections] });
|
|
1733
|
+
}).pipe(Effect8.withLogSpan("tablinum.sync")),
|
|
1570
1734
|
publishLocal: (giftWrap) => Effect8.gen(function* () {
|
|
1571
1735
|
if (!giftWrap.event)
|
|
1572
1736
|
return;
|
|
@@ -1627,7 +1791,8 @@ var membersCollectionDef = {
|
|
|
1627
1791
|
removedAt: optionalNumber,
|
|
1628
1792
|
removedInEpoch: optionalString
|
|
1629
1793
|
},
|
|
1630
|
-
indices: []
|
|
1794
|
+
indices: [],
|
|
1795
|
+
eventRetention: 1
|
|
1631
1796
|
};
|
|
1632
1797
|
var AuthorProfileSchema = Schema6.Struct({
|
|
1633
1798
|
name: Schema6.optionalKey(Schema6.String),
|
|
@@ -1691,7 +1856,7 @@ class SyncStatus extends ServiceMap9.Service()("tablinum/SyncStatus") {
|
|
|
1691
1856
|
}
|
|
1692
1857
|
|
|
1693
1858
|
// src/layers/IdentityLive.ts
|
|
1694
|
-
import { Effect as Effect11, Layer } from "effect";
|
|
1859
|
+
import { Effect as Effect11, Layer as Layer2 } from "effect";
|
|
1695
1860
|
import { hexToBytes as hexToBytes3 } from "@noble/hashes/utils.js";
|
|
1696
1861
|
|
|
1697
1862
|
// src/db/identity.ts
|
|
@@ -1729,21 +1894,25 @@ function createIdentity(suppliedKey) {
|
|
|
1729
1894
|
}
|
|
1730
1895
|
|
|
1731
1896
|
// src/layers/IdentityLive.ts
|
|
1732
|
-
var IdentityLive =
|
|
1897
|
+
var IdentityLive = Layer2.effect(Identity, Effect11.gen(function* () {
|
|
1733
1898
|
const config = yield* Config;
|
|
1734
1899
|
const storage = yield* Storage;
|
|
1735
1900
|
const idbKey = yield* storage.getMeta("identity_key");
|
|
1736
1901
|
const resolvedKey = config.privateKey ?? (typeof idbKey === "string" && idbKey.length === 64 ? hexToBytes3(idbKey) : undefined);
|
|
1737
1902
|
const identity = yield* createIdentity(resolvedKey);
|
|
1738
1903
|
yield* storage.putMeta("identity_key", identity.exportKey());
|
|
1904
|
+
yield* Effect11.logInfo("Identity loaded", {
|
|
1905
|
+
publicKey: identity.publicKey.slice(0, 12) + "...",
|
|
1906
|
+
source: config.privateKey ? "config" : resolvedKey ? "storage" : "generated"
|
|
1907
|
+
});
|
|
1739
1908
|
return identity;
|
|
1740
1909
|
}));
|
|
1741
1910
|
|
|
1742
1911
|
// src/layers/EpochStoreLive.ts
|
|
1743
|
-
import { Effect as Effect12, Layer as
|
|
1912
|
+
import { Effect as Effect12, Layer as Layer3, Option as Option7 } from "effect";
|
|
1744
1913
|
import { generateSecretKey as generateSecretKey2 } from "nostr-tools/pure";
|
|
1745
1914
|
import { bytesToHex as bytesToHex3 } from "@noble/hashes/utils.js";
|
|
1746
|
-
var EpochStoreLive =
|
|
1915
|
+
var EpochStoreLive = Layer3.effect(EpochStore, Effect12.gen(function* () {
|
|
1747
1916
|
const config = yield* Config;
|
|
1748
1917
|
const identity = yield* Identity;
|
|
1749
1918
|
const storage = yield* Storage;
|
|
@@ -1751,21 +1920,24 @@ var EpochStoreLive = Layer2.effect(EpochStore, Effect12.gen(function* () {
|
|
|
1751
1920
|
if (typeof idbRaw === "string") {
|
|
1752
1921
|
const idbStore = deserializeEpochStore(idbRaw);
|
|
1753
1922
|
if (Option7.isSome(idbStore)) {
|
|
1923
|
+
yield* Effect12.logInfo("Epoch store loaded", { source: "storage", epochs: idbStore.value.epochs.size });
|
|
1754
1924
|
return idbStore.value;
|
|
1755
1925
|
}
|
|
1756
1926
|
}
|
|
1757
1927
|
if (config.epochKeys && config.epochKeys.length > 0) {
|
|
1758
1928
|
const store2 = createEpochStoreFromInputs(config.epochKeys);
|
|
1759
1929
|
yield* storage.putMeta("epochs", stringifyEpochStore(store2));
|
|
1930
|
+
yield* Effect12.logInfo("Epoch store loaded", { source: "config", epochs: store2.epochs.size });
|
|
1760
1931
|
return store2;
|
|
1761
1932
|
}
|
|
1762
1933
|
const store = createEpochStoreFromInputs([{ epochId: EpochId("epoch-0"), key: bytesToHex3(generateSecretKey2()) }], { createdBy: identity.publicKey });
|
|
1763
1934
|
yield* storage.putMeta("epochs", stringifyEpochStore(store));
|
|
1935
|
+
yield* Effect12.logInfo("Epoch store loaded", { source: "generated", epochs: store.epochs.size });
|
|
1764
1936
|
return store;
|
|
1765
1937
|
}));
|
|
1766
1938
|
|
|
1767
1939
|
// src/layers/StorageLive.ts
|
|
1768
|
-
import { Effect as Effect14, Layer as
|
|
1940
|
+
import { Effect as Effect14, Layer as Layer4 } from "effect";
|
|
1769
1941
|
|
|
1770
1942
|
// src/storage/idb.ts
|
|
1771
1943
|
import { Effect as Effect13 } from "effect";
|
|
@@ -1900,13 +2072,18 @@ function openIDBStorage(dbName, schema) {
|
|
|
1900
2072
|
stripGiftWrapBlob: (id) => wrap("stripGiftWrapBlob", async () => {
|
|
1901
2073
|
const existing = await db.get("giftwraps", id);
|
|
1902
2074
|
if (existing) {
|
|
1903
|
-
|
|
1904
|
-
await db.put("giftwraps", tombstone);
|
|
2075
|
+
await db.put("giftwraps", { id: existing.id, createdAt: existing.createdAt });
|
|
1905
2076
|
}
|
|
1906
2077
|
}),
|
|
1907
2078
|
deleteEvent: (id) => wrap("deleteEvent", () => db.delete("events", id).then(() => {
|
|
1908
2079
|
return;
|
|
1909
2080
|
})),
|
|
2081
|
+
stripEventData: (id) => wrap("stripEventData", async () => {
|
|
2082
|
+
const existing = await db.get("events", id);
|
|
2083
|
+
if (existing) {
|
|
2084
|
+
await db.put("events", { ...existing, data: null });
|
|
2085
|
+
}
|
|
2086
|
+
}),
|
|
1910
2087
|
getMeta: (key) => wrap("getMeta", () => db.get("_meta", key)),
|
|
1911
2088
|
putMeta: (key, value) => wrap("putMeta", () => db.put("_meta", value, key).then(() => {
|
|
1912
2089
|
return;
|
|
@@ -1918,16 +2095,18 @@ function openIDBStorage(dbName, schema) {
|
|
|
1918
2095
|
}
|
|
1919
2096
|
|
|
1920
2097
|
// src/layers/StorageLive.ts
|
|
1921
|
-
var StorageLive =
|
|
2098
|
+
var StorageLive = Layer4.effect(Storage, Effect14.gen(function* () {
|
|
1922
2099
|
const config = yield* Config;
|
|
1923
|
-
|
|
2100
|
+
const handle = yield* openIDBStorage(config.dbName, {
|
|
1924
2101
|
...config.schema,
|
|
1925
2102
|
_members: membersCollectionDef
|
|
1926
2103
|
});
|
|
2104
|
+
yield* Effect14.logInfo("Storage opened", { dbName: config.dbName });
|
|
2105
|
+
return handle;
|
|
1927
2106
|
}));
|
|
1928
2107
|
|
|
1929
2108
|
// src/layers/RelayLive.ts
|
|
1930
|
-
import { Layer as
|
|
2109
|
+
import { Layer as Layer5 } from "effect";
|
|
1931
2110
|
|
|
1932
2111
|
// src/sync/relay.ts
|
|
1933
2112
|
import { Effect as Effect15, Option as Option8, Schema as Schema7, ScopedCache, Scope as Scope3 } from "effect";
|
|
@@ -2140,10 +2319,10 @@ function createRelayHandle() {
|
|
|
2140
2319
|
}
|
|
2141
2320
|
|
|
2142
2321
|
// src/layers/RelayLive.ts
|
|
2143
|
-
var RelayLive =
|
|
2322
|
+
var RelayLive = Layer5.effect(Relay, createRelayHandle());
|
|
2144
2323
|
|
|
2145
2324
|
// src/layers/GiftWrapLive.ts
|
|
2146
|
-
import { Effect as Effect17, Layer as
|
|
2325
|
+
import { Effect as Effect17, Layer as Layer6 } from "effect";
|
|
2147
2326
|
|
|
2148
2327
|
// src/sync/gift-wrap.ts
|
|
2149
2328
|
import { Effect as Effect16 } from "effect";
|
|
@@ -2180,14 +2359,14 @@ function createEpochGiftWrapHandle(senderPrivateKey, epochStore) {
|
|
|
2180
2359
|
}
|
|
2181
2360
|
|
|
2182
2361
|
// src/layers/GiftWrapLive.ts
|
|
2183
|
-
var GiftWrapLive =
|
|
2362
|
+
var GiftWrapLive = Layer6.effect(GiftWrap3, Effect17.gen(function* () {
|
|
2184
2363
|
const identity = yield* Identity;
|
|
2185
2364
|
const epochStore = yield* EpochStore;
|
|
2186
2365
|
return createEpochGiftWrapHandle(identity.privateKey, epochStore);
|
|
2187
2366
|
}));
|
|
2188
2367
|
|
|
2189
2368
|
// src/layers/PublishQueueLive.ts
|
|
2190
|
-
import { Effect as Effect19, Layer as
|
|
2369
|
+
import { Effect as Effect19, Layer as Layer7 } from "effect";
|
|
2191
2370
|
|
|
2192
2371
|
// src/sync/publish-queue.ts
|
|
2193
2372
|
import { Effect as Effect18, Ref as Ref4 } from "effect";
|
|
@@ -2261,14 +2440,14 @@ function createPublishQueue(storage, relay) {
|
|
|
2261
2440
|
}
|
|
2262
2441
|
|
|
2263
2442
|
// src/layers/PublishQueueLive.ts
|
|
2264
|
-
var PublishQueueLive =
|
|
2443
|
+
var PublishQueueLive = Layer7.effect(PublishQueue, Effect19.gen(function* () {
|
|
2265
2444
|
const storage = yield* Storage;
|
|
2266
2445
|
const relay = yield* Relay;
|
|
2267
2446
|
return yield* createPublishQueue(storage, relay);
|
|
2268
2447
|
}));
|
|
2269
2448
|
|
|
2270
2449
|
// src/layers/SyncStatusLive.ts
|
|
2271
|
-
import { Layer as
|
|
2450
|
+
import { Layer as Layer8 } from "effect";
|
|
2272
2451
|
|
|
2273
2452
|
// src/sync/sync-status.ts
|
|
2274
2453
|
import { Effect as Effect20, SubscriptionRef } from "effect";
|
|
@@ -2292,7 +2471,7 @@ function createSyncStatusHandle() {
|
|
|
2292
2471
|
}
|
|
2293
2472
|
|
|
2294
2473
|
// src/layers/SyncStatusLive.ts
|
|
2295
|
-
var SyncStatusLive =
|
|
2474
|
+
var SyncStatusLive = Layer8.effect(SyncStatus, createSyncStatusHandle());
|
|
2296
2475
|
|
|
2297
2476
|
// src/layers/TablinumLive.ts
|
|
2298
2477
|
function reportSyncError(onSyncError, error) {
|
|
@@ -2313,12 +2492,12 @@ function mapMemberRecord(record) {
|
|
|
2313
2492
|
...record.removedInEpoch !== undefined ? { removedInEpoch: record.removedInEpoch } : {}
|
|
2314
2493
|
};
|
|
2315
2494
|
}
|
|
2316
|
-
var IdentityWithDeps = IdentityLive.pipe(
|
|
2317
|
-
var EpochStoreWithDeps = EpochStoreLive.pipe(
|
|
2318
|
-
var GiftWrapWithDeps = GiftWrapLive.pipe(
|
|
2319
|
-
var PublishQueueWithDeps = PublishQueueLive.pipe(
|
|
2320
|
-
var AllServicesLive =
|
|
2321
|
-
var TablinumLive =
|
|
2495
|
+
var IdentityWithDeps = IdentityLive.pipe(Layer9.provide(StorageLive));
|
|
2496
|
+
var EpochStoreWithDeps = EpochStoreLive.pipe(Layer9.provide(IdentityWithDeps), Layer9.provide(StorageLive));
|
|
2497
|
+
var GiftWrapWithDeps = GiftWrapLive.pipe(Layer9.provide(IdentityWithDeps), Layer9.provide(EpochStoreWithDeps));
|
|
2498
|
+
var PublishQueueWithDeps = PublishQueueLive.pipe(Layer9.provide(StorageLive), Layer9.provide(RelayLive));
|
|
2499
|
+
var AllServicesLive = Layer9.mergeAll(IdentityWithDeps, EpochStoreWithDeps, StorageLive, RelayLive, GiftWrapWithDeps, PublishQueueWithDeps, SyncStatusLive);
|
|
2500
|
+
var TablinumLive = Layer9.effect(Tablinum, Effect21.gen(function* () {
|
|
2322
2501
|
const config = yield* Config;
|
|
2323
2502
|
const identity = yield* Identity;
|
|
2324
2503
|
const epochStore = yield* EpochStore;
|
|
@@ -2328,17 +2507,18 @@ var TablinumLive = Layer8.effect(Tablinum, Effect21.gen(function* () {
|
|
|
2328
2507
|
const publishQueue = yield* PublishQueue;
|
|
2329
2508
|
const syncStatus = yield* SyncStatus;
|
|
2330
2509
|
const scope = yield* Effect21.scope;
|
|
2510
|
+
const logLayer = Layer9.succeed(References3.MinimumLogLevel, config.logLevel);
|
|
2331
2511
|
const pubsub = yield* PubSub2.unbounded();
|
|
2332
2512
|
const replayingRef = yield* Ref5.make(false);
|
|
2333
2513
|
const closedRef = yield* Ref5.make(false);
|
|
2334
2514
|
const watchCtx = { pubsub, replayingRef };
|
|
2335
2515
|
const schemaEntries = Object.entries(config.schema);
|
|
2336
2516
|
const allSchemaEntries = [...schemaEntries, ["_members", membersCollectionDef]];
|
|
2337
|
-
const knownCollections = new
|
|
2517
|
+
const knownCollections = new Map(allSchemaEntries.map(([, def]) => [def.name, def.eventRetention]));
|
|
2338
2518
|
let notifyAuthor;
|
|
2339
|
-
const syncHandle = createSyncHandle(storage, giftWrap, relay, publishQueue, syncStatus, watchCtx, config.relays, knownCollections, epochStore, identity.privateKey, identity.publicKey, scope, config.onSyncError ? (error) => reportSyncError(config.onSyncError, error) : undefined, (pubkey) => notifyAuthor?.(pubkey), config.onRemoved, config.onMembersChanged);
|
|
2519
|
+
const syncHandle = createSyncHandle(storage, giftWrap, relay, publishQueue, syncStatus, watchCtx, config.relays, knownCollections, epochStore, identity.privateKey, identity.publicKey, scope, config.logLevel, config.onSyncError ? (error) => reportSyncError(config.onSyncError, error) : undefined, (pubkey) => notifyAuthor?.(pubkey), config.onRemoved, config.onMembersChanged);
|
|
2340
2520
|
const onWrite = (event) => Effect21.gen(function* () {
|
|
2341
|
-
const content = event.kind === "
|
|
2521
|
+
const content = event.kind === "d" ? JSON.stringify(null) : JSON.stringify(event.data);
|
|
2342
2522
|
const dTag = `${event.collection}:${event.recordId}`;
|
|
2343
2523
|
const wrapResult = yield* Effect21.result(giftWrap.wrap({
|
|
2344
2524
|
kind: 1,
|
|
@@ -2351,11 +2531,10 @@ var TablinumLive = Layer8.effect(Tablinum, Effect21.gen(function* () {
|
|
|
2351
2531
|
return;
|
|
2352
2532
|
}
|
|
2353
2533
|
const gw = wrapResult.success;
|
|
2354
|
-
yield* storage.putGiftWrap({ id: gw.id,
|
|
2534
|
+
yield* storage.putGiftWrap({ id: gw.id, createdAt: gw.created_at });
|
|
2355
2535
|
yield* Effect21.forkIn(Effect21.gen(function* () {
|
|
2356
2536
|
const publishResult = yield* Effect21.result(syncHandle.publishLocal({
|
|
2357
2537
|
id: gw.id,
|
|
2358
|
-
eventId: event.id,
|
|
2359
2538
|
event: gw,
|
|
2360
2539
|
createdAt: gw.created_at
|
|
2361
2540
|
}));
|
|
@@ -2371,9 +2550,10 @@ var TablinumLive = Layer8.effect(Tablinum, Effect21.gen(function* () {
|
|
|
2371
2550
|
id: uuidv7(),
|
|
2372
2551
|
collection: "_members",
|
|
2373
2552
|
recordId: record.id,
|
|
2374
|
-
kind: existing ? "
|
|
2553
|
+
kind: existing ? "u" : "c",
|
|
2375
2554
|
data: record,
|
|
2376
|
-
createdAt: Date.now()
|
|
2555
|
+
createdAt: Date.now(),
|
|
2556
|
+
author: identity.publicKey
|
|
2377
2557
|
};
|
|
2378
2558
|
yield* storage.putEvent(event);
|
|
2379
2559
|
yield* applyEvent(storage, event);
|
|
@@ -2414,16 +2594,21 @@ var TablinumLive = Layer8.effect(Tablinum, Effect21.gen(function* () {
|
|
|
2414
2594
|
config.onMembersChanged?.();
|
|
2415
2595
|
}
|
|
2416
2596
|
}
|
|
2417
|
-
}).pipe(Effect21.ignore, Effect21.forkIn(scope)));
|
|
2597
|
+
}).pipe(Effect21.ignore, Effect21.provide(logLayer), Effect21.forkIn(scope)));
|
|
2418
2598
|
};
|
|
2419
2599
|
const handles = new Map;
|
|
2420
2600
|
for (const [, def] of allSchemaEntries) {
|
|
2421
2601
|
const validator = buildValidator(def.name, def);
|
|
2422
2602
|
const partialValidator = buildPartialValidator(def.name, def);
|
|
2423
|
-
const handle = createCollectionHandle(def, storage, watchCtx, validator, partialValidator, uuidv7, onWrite);
|
|
2603
|
+
const handle = createCollectionHandle(def, storage, watchCtx, validator, partialValidator, uuidv7, identity.publicKey, onWrite, config.logLevel);
|
|
2424
2604
|
handles.set(def.name, handle);
|
|
2425
2605
|
}
|
|
2426
2606
|
yield* syncHandle.startSubscription();
|
|
2607
|
+
yield* Effect21.logInfo("Tablinum ready", {
|
|
2608
|
+
dbName: config.dbName,
|
|
2609
|
+
collections: schemaEntries.map(([name]) => name),
|
|
2610
|
+
relays: config.relays
|
|
2611
|
+
});
|
|
2427
2612
|
const selfMember = yield* storage.getRecord("_members", identity.publicKey);
|
|
2428
2613
|
if (!selfMember) {
|
|
2429
2614
|
yield* putMemberRecord({
|
|
@@ -2432,18 +2617,19 @@ var TablinumLive = Layer8.effect(Tablinum, Effect21.gen(function* () {
|
|
|
2432
2617
|
addedInEpoch: getCurrentEpoch(epochStore).id
|
|
2433
2618
|
});
|
|
2434
2619
|
}
|
|
2435
|
-
const
|
|
2620
|
+
const withLog = (effect) => Effect21.provideService(effect, References3.MinimumLogLevel, config.logLevel);
|
|
2621
|
+
const ensureOpen = (effect) => withLog(Effect21.gen(function* () {
|
|
2436
2622
|
if (yield* Ref5.get(closedRef)) {
|
|
2437
2623
|
return yield* new StorageError({ message: "Database is closed" });
|
|
2438
2624
|
}
|
|
2439
2625
|
return yield* effect;
|
|
2440
|
-
});
|
|
2441
|
-
const ensureSyncOpen = (effect) => Effect21.gen(function* () {
|
|
2626
|
+
}));
|
|
2627
|
+
const ensureSyncOpen = (effect) => withLog(Effect21.gen(function* () {
|
|
2442
2628
|
if (yield* Ref5.get(closedRef)) {
|
|
2443
2629
|
return yield* new SyncError({ message: "Database is closed", phase: "init" });
|
|
2444
2630
|
}
|
|
2445
2631
|
return yield* effect;
|
|
2446
|
-
});
|
|
2632
|
+
}));
|
|
2447
2633
|
const dbHandle = {
|
|
2448
2634
|
collection: (name) => {
|
|
2449
2635
|
const handle = handles.get(name);
|
|
@@ -2459,12 +2645,12 @@ var TablinumLive = Layer8.effect(Tablinum, Effect21.gen(function* () {
|
|
|
2459
2645
|
relays: [...config.relays],
|
|
2460
2646
|
dbName: config.dbName
|
|
2461
2647
|
}),
|
|
2462
|
-
close: () => Effect21.gen(function* () {
|
|
2648
|
+
close: () => withLog(Effect21.gen(function* () {
|
|
2463
2649
|
if (yield* Ref5.get(closedRef))
|
|
2464
2650
|
return;
|
|
2465
2651
|
yield* Ref5.set(closedRef, true);
|
|
2466
2652
|
yield* Scope4.close(scope, Exit.void);
|
|
2467
|
-
}),
|
|
2653
|
+
})),
|
|
2468
2654
|
rebuild: () => ensureOpen(rebuild(storage, allSchemaEntries.map(([, def]) => def.name))),
|
|
2469
2655
|
sync: () => ensureSyncOpen(syncHandle.sync()),
|
|
2470
2656
|
getSyncStatus: () => syncStatus.get(),
|
|
@@ -2508,20 +2694,52 @@ var TablinumLive = Layer8.effect(Tablinum, Effect21.gen(function* () {
|
|
|
2508
2694
|
})),
|
|
2509
2695
|
getMembers: () => ensureOpen(Effect21.gen(function* () {
|
|
2510
2696
|
const allRecords = yield* storage.getAllRecords("_members");
|
|
2511
|
-
return allRecords.filter((record) => !record.
|
|
2697
|
+
return allRecords.filter((record) => !record._d).map(mapMemberRecord);
|
|
2698
|
+
})),
|
|
2699
|
+
getProfile: () => ensureOpen(Effect21.gen(function* () {
|
|
2700
|
+
const record = yield* storage.getRecord("_members", identity.publicKey);
|
|
2701
|
+
if (!record)
|
|
2702
|
+
return {};
|
|
2703
|
+
const profile = {};
|
|
2704
|
+
if (record.name !== undefined)
|
|
2705
|
+
profile.name = record.name;
|
|
2706
|
+
if (record.picture !== undefined)
|
|
2707
|
+
profile.picture = record.picture;
|
|
2708
|
+
if (record.about !== undefined)
|
|
2709
|
+
profile.about = record.about;
|
|
2710
|
+
if (record.nip05 !== undefined)
|
|
2711
|
+
profile.nip05 = record.nip05;
|
|
2712
|
+
return profile;
|
|
2512
2713
|
})),
|
|
2513
2714
|
setProfile: (profile) => ensureOpen(Effect21.gen(function* () {
|
|
2514
2715
|
const existing = yield* storage.getRecord("_members", identity.publicKey);
|
|
2515
2716
|
if (!existing) {
|
|
2516
2717
|
return yield* new ValidationError({ message: "Current user is not a member" });
|
|
2517
2718
|
}
|
|
2518
|
-
|
|
2719
|
+
const { _d, _u, _a, _e, ...memberFields } = existing;
|
|
2720
|
+
yield* putMemberRecord({ ...memberFields, ...profile });
|
|
2519
2721
|
}))
|
|
2520
2722
|
};
|
|
2521
2723
|
return dbHandle;
|
|
2522
|
-
})).pipe(
|
|
2724
|
+
}).pipe(Effect21.withLogSpan("tablinum.init"))).pipe(Layer9.provide(AllServicesLive));
|
|
2523
2725
|
|
|
2524
2726
|
// src/db/create-tablinum.ts
|
|
2727
|
+
function resolveLogLevel(input) {
|
|
2728
|
+
if (input === undefined || input === "none")
|
|
2729
|
+
return "None";
|
|
2730
|
+
switch (input) {
|
|
2731
|
+
case "debug":
|
|
2732
|
+
return "Debug";
|
|
2733
|
+
case "info":
|
|
2734
|
+
return "Info";
|
|
2735
|
+
case "warning":
|
|
2736
|
+
return "Warn";
|
|
2737
|
+
case "error":
|
|
2738
|
+
return "Error";
|
|
2739
|
+
default:
|
|
2740
|
+
return input;
|
|
2741
|
+
}
|
|
2742
|
+
}
|
|
2525
2743
|
function validateConfig(config) {
|
|
2526
2744
|
return Effect22.gen(function* () {
|
|
2527
2745
|
if (Object.keys(config.schema).length === 0) {
|
|
@@ -2535,22 +2753,25 @@ function createTablinum(config) {
|
|
|
2535
2753
|
return Effect22.gen(function* () {
|
|
2536
2754
|
yield* validateConfig(config);
|
|
2537
2755
|
const runtimeConfig = yield* resolveRuntimeConfig(config);
|
|
2756
|
+
const logLevel = resolveLogLevel(config.logLevel);
|
|
2538
2757
|
const configValue = {
|
|
2539
2758
|
...runtimeConfig,
|
|
2540
2759
|
schema: config.schema,
|
|
2760
|
+
logLevel,
|
|
2541
2761
|
onSyncError: config.onSyncError,
|
|
2542
2762
|
onRemoved: config.onRemoved,
|
|
2543
2763
|
onMembersChanged: config.onMembersChanged
|
|
2544
2764
|
};
|
|
2545
|
-
const configLayer =
|
|
2546
|
-
const
|
|
2547
|
-
const
|
|
2765
|
+
const configLayer = Layer10.succeed(Config, configValue);
|
|
2766
|
+
const logLayer = Layer10.succeed(References4.MinimumLogLevel, logLevel);
|
|
2767
|
+
const fullLayer = TablinumLive.pipe(Layer10.provide(configLayer), Layer10.provide(logLayer));
|
|
2768
|
+
const ctx = yield* Layer10.build(fullLayer);
|
|
2548
2769
|
return ServiceMap10.get(ctx, Tablinum);
|
|
2549
2770
|
});
|
|
2550
2771
|
}
|
|
2551
2772
|
|
|
2552
2773
|
// src/svelte/collection.svelte.ts
|
|
2553
|
-
import { Effect as Effect24, Fiber, Option as Option11, Stream as Stream4 } from "effect";
|
|
2774
|
+
import { Effect as Effect24, Fiber, Option as Option11, References as References5, Stream as Stream4 } from "effect";
|
|
2554
2775
|
|
|
2555
2776
|
// src/svelte/deferred.ts
|
|
2556
2777
|
function createDeferred() {
|
|
@@ -2643,10 +2864,12 @@ class Collection {
|
|
|
2643
2864
|
#version = $state(0);
|
|
2644
2865
|
#watchAbort = null;
|
|
2645
2866
|
#watchFiber = null;
|
|
2646
|
-
|
|
2867
|
+
#logLevel = "None";
|
|
2868
|
+
_bind(handle, logLevel = "None") {
|
|
2647
2869
|
if (this.#handle)
|
|
2648
2870
|
return;
|
|
2649
2871
|
this.#handle = handle;
|
|
2872
|
+
this.#logLevel = logLevel;
|
|
2650
2873
|
this.error = null;
|
|
2651
2874
|
this.#settleReady();
|
|
2652
2875
|
this.#startWatch();
|
|
@@ -2675,7 +2898,7 @@ class Collection {
|
|
|
2675
2898
|
if (!abort.signal.aborted) {
|
|
2676
2899
|
this.error = e instanceof Error ? e : new Error(String(e));
|
|
2677
2900
|
}
|
|
2678
|
-
}))));
|
|
2901
|
+
})), Effect24.provideService(References5.MinimumLogLevel, this.#logLevel)));
|
|
2679
2902
|
}
|
|
2680
2903
|
#touchVersion = () => {
|
|
2681
2904
|
this.#version;
|
|
@@ -2687,7 +2910,7 @@ class Collection {
|
|
|
2687
2910
|
};
|
|
2688
2911
|
#run = async (getEffect) => {
|
|
2689
2912
|
await this.#ready.promise;
|
|
2690
|
-
return Effect24.runPromise(getEffect());
|
|
2913
|
+
return Effect24.runPromise(getEffect().pipe(Effect24.provideService(References5.MinimumLogLevel, this.#logLevel)));
|
|
2691
2914
|
};
|
|
2692
2915
|
add = (data) => {
|
|
2693
2916
|
return this.#run(() => this.#handleOrThrow().add(data));
|
|
@@ -2698,6 +2921,9 @@ class Collection {
|
|
|
2698
2921
|
delete = (id) => {
|
|
2699
2922
|
return this.#run(() => this.#handleOrThrow().delete(id));
|
|
2700
2923
|
};
|
|
2924
|
+
undo = (id) => {
|
|
2925
|
+
return this.#run(() => this.#handleOrThrow().undo(id));
|
|
2926
|
+
};
|
|
2701
2927
|
get(id) {
|
|
2702
2928
|
if (typeof id === "string") {
|
|
2703
2929
|
return this.#run(() => this.#handleOrThrow().get(id));
|
|
@@ -2751,8 +2977,10 @@ class Tablinum2 {
|
|
|
2751
2977
|
#unsubscribeRelayStatus = null;
|
|
2752
2978
|
#closed = false;
|
|
2753
2979
|
#readyState = createDeferred();
|
|
2980
|
+
#logLevel;
|
|
2754
2981
|
constructor(config) {
|
|
2755
2982
|
this.ready = this.#readyState.promise;
|
|
2983
|
+
this.#logLevel = resolveLogLevel(config.logLevel);
|
|
2756
2984
|
this.#init(config);
|
|
2757
2985
|
}
|
|
2758
2986
|
#settleReady(err) {
|
|
@@ -2763,16 +2991,16 @@ class Tablinum2 {
|
|
|
2763
2991
|
}
|
|
2764
2992
|
}
|
|
2765
2993
|
#bindCollections(handle) {
|
|
2766
|
-
this.#members._bind(handle.members);
|
|
2994
|
+
this.#members._bind(handle.members, this.#logLevel);
|
|
2767
2995
|
for (const [name, collection2] of this.#collections) {
|
|
2768
|
-
collection2._bind(handle.collection(name));
|
|
2996
|
+
collection2._bind(handle.collection(name), this.#logLevel);
|
|
2769
2997
|
}
|
|
2770
2998
|
}
|
|
2771
2999
|
#runHandleEffect = async (run) => {
|
|
2772
3000
|
const handle = this.#requireReady();
|
|
2773
3001
|
try {
|
|
2774
3002
|
this.error = null;
|
|
2775
|
-
return await Effect25.runPromise(run(handle));
|
|
3003
|
+
return await Effect25.runPromise(run(handle).pipe(Effect25.provideService(References6.MinimumLogLevel, this.#logLevel)));
|
|
2776
3004
|
} catch (e) {
|
|
2777
3005
|
this.error = e instanceof Error ? e : new Error(String(e));
|
|
2778
3006
|
throw this.error;
|
|
@@ -2830,7 +3058,7 @@ class Tablinum2 {
|
|
|
2830
3058
|
col = new Collection;
|
|
2831
3059
|
this.#collections.set(name, col);
|
|
2832
3060
|
if (this.#handle) {
|
|
2833
|
-
col._bind(this.#handle.collection(name));
|
|
3061
|
+
col._bind(this.#handle.collection(name), this.#logLevel);
|
|
2834
3062
|
}
|
|
2835
3063
|
}
|
|
2836
3064
|
return col;
|
|
@@ -2889,6 +3117,7 @@ class Tablinum2 {
|
|
|
2889
3117
|
addMember = async (pubkey) => this.#runHandleEffect((handle) => handle.addMember(pubkey));
|
|
2890
3118
|
removeMember = async (pubkey) => this.#runHandleEffect((handle) => handle.removeMember(pubkey));
|
|
2891
3119
|
getMembers = async () => this.#runHandleEffect((handle) => handle.getMembers());
|
|
3120
|
+
getProfile = async () => this.#runHandleEffect((handle) => handle.getProfile());
|
|
2892
3121
|
setProfile = async (profile) => this.#runHandleEffect((handle) => handle.setProfile(profile));
|
|
2893
3122
|
}
|
|
2894
3123
|
export {
|