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
package/dist/index.js
CHANGED
|
@@ -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,16 +41,20 @@ 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/create-tablinum.ts
|
|
52
|
-
import { Effect as Effect22, Layer as
|
|
57
|
+
import { Effect as Effect22, Layer as Layer10, References as References4, ServiceMap as ServiceMap10 } from "effect";
|
|
53
58
|
|
|
54
59
|
// src/db/runtime-config.ts
|
|
55
60
|
import { Effect, Schema as Schema2 } from "effect";
|
|
@@ -215,13 +220,13 @@ class Tablinum extends ServiceMap2.Service()("tablinum/Tablinum") {
|
|
|
215
220
|
}
|
|
216
221
|
|
|
217
222
|
// src/layers/TablinumLive.ts
|
|
218
|
-
import { Effect as Effect21, Exit, Layer as
|
|
223
|
+
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";
|
|
219
224
|
|
|
220
225
|
// src/crud/watch.ts
|
|
221
226
|
import { Effect as Effect2, PubSub, Ref, Stream } from "effect";
|
|
222
227
|
function watchCollection(ctx, storage, collectionName, filter, mapRecord) {
|
|
223
228
|
const query = () => Effect2.map(storage.getAllRecords(collectionName), (all) => {
|
|
224
|
-
const filtered = all.filter((r) => !r.
|
|
229
|
+
const filtered = all.filter((r) => !r._d && (filter ? filter(r) : true));
|
|
225
230
|
return mapRecord ? filtered.map(mapRecord) : filtered;
|
|
226
231
|
});
|
|
227
232
|
const changes = Stream.fromPubSub(ctx.pubsub).pipe(Stream.filter((event) => event.collection === collectionName), Stream.mapEffect(() => Effect2.gen(function* () {
|
|
@@ -266,14 +271,56 @@ function resolveWinner(existing, incoming) {
|
|
|
266
271
|
return incoming.id < existing.id ? incoming : existing;
|
|
267
272
|
}
|
|
268
273
|
|
|
274
|
+
// src/utils/diff.ts
|
|
275
|
+
function isPlainObject(value) {
|
|
276
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
277
|
+
}
|
|
278
|
+
function deepDiff(before, after) {
|
|
279
|
+
const result = {};
|
|
280
|
+
let hasChanges = false;
|
|
281
|
+
for (const key of Object.keys(after)) {
|
|
282
|
+
const a = before[key];
|
|
283
|
+
const b = after[key];
|
|
284
|
+
if (isPlainObject(a) && isPlainObject(b)) {
|
|
285
|
+
const nested = deepDiff(a, b);
|
|
286
|
+
if (nested !== null) {
|
|
287
|
+
result[key] = nested;
|
|
288
|
+
hasChanges = true;
|
|
289
|
+
}
|
|
290
|
+
} else if (Array.isArray(a) && Array.isArray(b)) {
|
|
291
|
+
if (JSON.stringify(a) !== JSON.stringify(b)) {
|
|
292
|
+
result[key] = b;
|
|
293
|
+
hasChanges = true;
|
|
294
|
+
}
|
|
295
|
+
} else if (a !== b) {
|
|
296
|
+
result[key] = b;
|
|
297
|
+
hasChanges = true;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
return hasChanges ? result : null;
|
|
301
|
+
}
|
|
302
|
+
function deepMerge(target, source) {
|
|
303
|
+
const result = { ...target };
|
|
304
|
+
for (const key of Object.keys(source)) {
|
|
305
|
+
const t = target[key];
|
|
306
|
+
const s = source[key];
|
|
307
|
+
if (isPlainObject(t) && isPlainObject(s)) {
|
|
308
|
+
result[key] = deepMerge(t, s);
|
|
309
|
+
} else {
|
|
310
|
+
result[key] = s;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
return result;
|
|
314
|
+
}
|
|
315
|
+
|
|
269
316
|
// src/storage/records-store.ts
|
|
270
317
|
function buildRecord(event) {
|
|
271
318
|
return {
|
|
272
319
|
id: event.recordId,
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
320
|
+
_d: event.kind === "d",
|
|
321
|
+
_u: event.createdAt,
|
|
322
|
+
_e: event.id,
|
|
323
|
+
_a: event.author,
|
|
277
324
|
...event.data ?? {}
|
|
278
325
|
};
|
|
279
326
|
}
|
|
@@ -282,15 +329,19 @@ function applyEvent(storage, event) {
|
|
|
282
329
|
const existing = yield* storage.getRecord(event.collection, event.recordId);
|
|
283
330
|
if (existing) {
|
|
284
331
|
const existingMeta = {
|
|
285
|
-
id: existing.
|
|
286
|
-
createdAt: existing.
|
|
332
|
+
id: existing._e,
|
|
333
|
+
createdAt: existing._u
|
|
287
334
|
};
|
|
288
335
|
const incomingMeta = { id: event.id, createdAt: event.createdAt };
|
|
289
336
|
const winner = resolveWinner(existingMeta, incomingMeta);
|
|
290
337
|
if (winner.id !== event.id)
|
|
291
338
|
return false;
|
|
292
339
|
}
|
|
293
|
-
|
|
340
|
+
if (existing && event.kind === "u") {
|
|
341
|
+
yield* storage.putRecord(event.collection, deepMerge(existing, buildRecord(event)));
|
|
342
|
+
} else {
|
|
343
|
+
yield* storage.putRecord(event.collection, buildRecord(event));
|
|
344
|
+
}
|
|
294
345
|
return true;
|
|
295
346
|
});
|
|
296
347
|
}
|
|
@@ -300,15 +351,11 @@ function rebuild(storage, collections) {
|
|
|
300
351
|
yield* storage.clearRecords(col);
|
|
301
352
|
}
|
|
302
353
|
const allEvents = yield* storage.getAllEvents();
|
|
303
|
-
const sorted = [...allEvents].sort((a, b) => a.createdAt - b.createdAt);
|
|
304
|
-
const winners = new Map;
|
|
354
|
+
const sorted = [...allEvents].sort((a, b) => a.createdAt - b.createdAt || (a.id < b.id ? -1 : 1));
|
|
305
355
|
for (const event of sorted) {
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
}
|
|
310
|
-
for (const event of winners.values()) {
|
|
311
|
-
yield* storage.putRecord(event.collection, buildRecord(event));
|
|
356
|
+
if (event.data === null && event.kind !== "d")
|
|
357
|
+
continue;
|
|
358
|
+
yield* applyEvent(storage, event);
|
|
312
359
|
}
|
|
313
360
|
});
|
|
314
361
|
}
|
|
@@ -330,6 +377,14 @@ function fieldDefToSchema(fd) {
|
|
|
330
377
|
case "json":
|
|
331
378
|
base = Schema3.Unknown;
|
|
332
379
|
break;
|
|
380
|
+
case "object": {
|
|
381
|
+
const nested = {};
|
|
382
|
+
for (const [k, v] of Object.entries(fd.fields)) {
|
|
383
|
+
nested[k] = fieldDefToSchema(v);
|
|
384
|
+
}
|
|
385
|
+
base = Schema3.Struct(nested);
|
|
386
|
+
break;
|
|
387
|
+
}
|
|
333
388
|
}
|
|
334
389
|
if (fd.isArray) {
|
|
335
390
|
base = Schema3.Array(base);
|
|
@@ -379,9 +434,25 @@ function buildPartialValidator(collectionName, def) {
|
|
|
379
434
|
}
|
|
380
435
|
|
|
381
436
|
// src/crud/collection-handle.ts
|
|
382
|
-
import { Effect as Effect6, Option as Option3 } from "effect";
|
|
437
|
+
import { Effect as Effect6, Option as Option3, References } from "effect";
|
|
383
438
|
|
|
384
439
|
// src/utils/uuid.ts
|
|
440
|
+
var alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
|
|
441
|
+
function toBase64url(bytes) {
|
|
442
|
+
let result = "";
|
|
443
|
+
for (let i = 0;i < bytes.length; i += 3) {
|
|
444
|
+
const b0 = bytes[i];
|
|
445
|
+
const b1 = bytes[i + 1] ?? 0;
|
|
446
|
+
const b2 = bytes[i + 2] ?? 0;
|
|
447
|
+
result += alphabet[b0 >> 2];
|
|
448
|
+
result += alphabet[(b0 & 3) << 4 | b1 >> 4];
|
|
449
|
+
if (i + 1 < bytes.length)
|
|
450
|
+
result += alphabet[(b1 & 15) << 2 | b2 >> 6];
|
|
451
|
+
if (i + 2 < bytes.length)
|
|
452
|
+
result += alphabet[b2 & 63];
|
|
453
|
+
}
|
|
454
|
+
return result;
|
|
455
|
+
}
|
|
385
456
|
function uuidv7() {
|
|
386
457
|
const now = Date.now();
|
|
387
458
|
const bytes = new Uint8Array(16);
|
|
@@ -394,8 +465,7 @@ function uuidv7() {
|
|
|
394
465
|
bytes[5] = now & 255;
|
|
395
466
|
bytes[6] = bytes[6] & 15 | 112;
|
|
396
467
|
bytes[8] = bytes[8] & 63 | 128;
|
|
397
|
-
|
|
398
|
-
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
468
|
+
return toBase64url(bytes);
|
|
399
469
|
}
|
|
400
470
|
|
|
401
471
|
// src/crud/query-builder.ts
|
|
@@ -413,7 +483,7 @@ function executeQuery(ctx, plan) {
|
|
|
413
483
|
field: plan.fieldName
|
|
414
484
|
});
|
|
415
485
|
}
|
|
416
|
-
if (fieldDef.kind === "json" || fieldDef.isArray) {
|
|
486
|
+
if (fieldDef.kind === "json" || fieldDef.kind === "object" || fieldDef.isArray) {
|
|
417
487
|
return yield* new ValidationError({
|
|
418
488
|
message: `Field "${plan.fieldName}" does not support filtering (type: ${fieldDef.kind}${fieldDef.isArray ? "[]" : ""})`,
|
|
419
489
|
field: plan.fieldName
|
|
@@ -438,7 +508,7 @@ function executeQuery(ctx, plan) {
|
|
|
438
508
|
} else {
|
|
439
509
|
results = [...yield* ctx.storage.getAllRecords(ctx.collectionName)];
|
|
440
510
|
}
|
|
441
|
-
results = results.filter((r) => !r.
|
|
511
|
+
results = results.filter((r) => !r._d);
|
|
442
512
|
for (const f of plan.filters) {
|
|
443
513
|
results = results.filter(f);
|
|
444
514
|
}
|
|
@@ -559,12 +629,62 @@ function createOrderByBuilder(storage, watchCtx, collectionName, def, fieldName,
|
|
|
559
629
|
}
|
|
560
630
|
|
|
561
631
|
// src/crud/collection-handle.ts
|
|
632
|
+
var KIND_FULL = { c: "create", u: "update", d: "delete" };
|
|
633
|
+
function sortChronologically(events) {
|
|
634
|
+
return [...events].sort((a, b) => a.createdAt - b.createdAt || (a.id < b.id ? -1 : 1));
|
|
635
|
+
}
|
|
636
|
+
function replayState(recordId, events, stopAtId) {
|
|
637
|
+
let state = null;
|
|
638
|
+
for (const e of events) {
|
|
639
|
+
if (e.kind === "d") {
|
|
640
|
+
state = null;
|
|
641
|
+
} else if (e.data !== null) {
|
|
642
|
+
if (state === null) {
|
|
643
|
+
state = { id: recordId, ...e.data };
|
|
644
|
+
} else {
|
|
645
|
+
state = deepMerge(state, e.data);
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
if (stopAtId !== undefined && e.id === stopAtId) {
|
|
649
|
+
break;
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
return state;
|
|
653
|
+
}
|
|
654
|
+
function promoteToSnapshot(storage, collection2, recordId, target, allSorted) {
|
|
655
|
+
return Effect6.gen(function* () {
|
|
656
|
+
const chronological = sortChronologically(allSorted);
|
|
657
|
+
const state = replayState(recordId, chronological, target.id);
|
|
658
|
+
if (state) {
|
|
659
|
+
yield* storage.putEvent({ ...target, data: state });
|
|
660
|
+
}
|
|
661
|
+
});
|
|
662
|
+
}
|
|
663
|
+
function pruneEvents(storage, collection2, recordId, retention) {
|
|
664
|
+
return Effect6.gen(function* () {
|
|
665
|
+
const events = yield* storage.getEventsByRecord(collection2, recordId);
|
|
666
|
+
if (events.length <= retention)
|
|
667
|
+
return;
|
|
668
|
+
const sorted = [...events].sort((a, b) => b.createdAt - a.createdAt || (a.id < b.id ? 1 : -1));
|
|
669
|
+
const retained = sorted.slice(0, retention);
|
|
670
|
+
const toStrip = sorted.slice(retention);
|
|
671
|
+
const oldestRetained = retained[retained.length - 1];
|
|
672
|
+
if (retention > 0 && oldestRetained?.kind === "u" && oldestRetained.data !== null) {
|
|
673
|
+
yield* promoteToSnapshot(storage, collection2, recordId, oldestRetained, sorted);
|
|
674
|
+
}
|
|
675
|
+
for (const e of toStrip) {
|
|
676
|
+
if (e.data !== null)
|
|
677
|
+
yield* storage.stripEventData(e.id);
|
|
678
|
+
}
|
|
679
|
+
});
|
|
680
|
+
}
|
|
562
681
|
function mapRecord(record) {
|
|
563
|
-
const {
|
|
682
|
+
const { _d, _u, _a, _e, ...fields } = record;
|
|
564
683
|
return fields;
|
|
565
684
|
}
|
|
566
|
-
function createCollectionHandle(def, storage, watchCtx, validator, partialValidator, makeEventId, onWrite) {
|
|
685
|
+
function createCollectionHandle(def, storage, watchCtx, validator, partialValidator, makeEventId, localAuthor, onWrite, logLevel = "None") {
|
|
567
686
|
const collectionName = def.name;
|
|
687
|
+
const withLog = (effect) => Effect6.provideService(effect, References.MinimumLogLevel, logLevel);
|
|
568
688
|
const commitEvent = (event) => Effect6.gen(function* () {
|
|
569
689
|
yield* storage.putEvent(event);
|
|
570
690
|
yield* applyEvent(storage, event);
|
|
@@ -573,11 +693,11 @@ function createCollectionHandle(def, storage, watchCtx, validator, partialValida
|
|
|
573
693
|
yield* notifyChange(watchCtx, {
|
|
574
694
|
collection: collectionName,
|
|
575
695
|
recordId: event.recordId,
|
|
576
|
-
kind: event.kind
|
|
696
|
+
kind: KIND_FULL[event.kind]
|
|
577
697
|
});
|
|
578
698
|
});
|
|
579
699
|
const handle = {
|
|
580
|
-
add: (data) => Effect6.gen(function* () {
|
|
700
|
+
add: (data) => withLog(Effect6.gen(function* () {
|
|
581
701
|
const id = uuidv7();
|
|
582
702
|
const fullRecord = { id, ...data };
|
|
583
703
|
yield* validator(fullRecord);
|
|
@@ -585,38 +705,44 @@ function createCollectionHandle(def, storage, watchCtx, validator, partialValida
|
|
|
585
705
|
id: makeEventId(),
|
|
586
706
|
collection: collectionName,
|
|
587
707
|
recordId: id,
|
|
588
|
-
kind: "
|
|
708
|
+
kind: "c",
|
|
589
709
|
data: fullRecord,
|
|
590
|
-
createdAt: Date.now()
|
|
710
|
+
createdAt: Date.now(),
|
|
711
|
+
author: localAuthor
|
|
591
712
|
};
|
|
592
713
|
yield* commitEvent(event);
|
|
714
|
+
yield* Effect6.logDebug("Record added", { collection: collectionName, recordId: id, data: fullRecord });
|
|
593
715
|
return id;
|
|
594
|
-
}),
|
|
595
|
-
update: (id, data) => Effect6.gen(function* () {
|
|
716
|
+
})),
|
|
717
|
+
update: (id, data) => withLog(Effect6.gen(function* () {
|
|
596
718
|
const existing = yield* storage.getRecord(collectionName, id);
|
|
597
|
-
if (!existing || existing.
|
|
719
|
+
if (!existing || existing._d) {
|
|
598
720
|
return yield* new NotFoundError({
|
|
599
721
|
collection: collectionName,
|
|
600
722
|
id
|
|
601
723
|
});
|
|
602
724
|
}
|
|
603
725
|
yield* partialValidator(data);
|
|
604
|
-
const {
|
|
726
|
+
const { _d, _u, _a, _e, ...existingFields } = existing;
|
|
605
727
|
const merged = { ...existingFields, ...data, id };
|
|
606
728
|
yield* validator(merged);
|
|
729
|
+
const diff = deepDiff(existingFields, merged);
|
|
607
730
|
const event = {
|
|
608
731
|
id: makeEventId(),
|
|
609
732
|
collection: collectionName,
|
|
610
733
|
recordId: id,
|
|
611
|
-
kind: "
|
|
612
|
-
data:
|
|
613
|
-
createdAt: Date.now()
|
|
734
|
+
kind: "u",
|
|
735
|
+
data: diff ?? { id },
|
|
736
|
+
createdAt: Date.now(),
|
|
737
|
+
author: localAuthor
|
|
614
738
|
};
|
|
615
739
|
yield* commitEvent(event);
|
|
616
|
-
|
|
617
|
-
|
|
740
|
+
yield* Effect6.logDebug("Record updated", { collection: collectionName, recordId: id, data: diff });
|
|
741
|
+
yield* pruneEvents(storage, collectionName, id, def.eventRetention);
|
|
742
|
+
})),
|
|
743
|
+
delete: (id) => withLog(Effect6.gen(function* () {
|
|
618
744
|
const existing = yield* storage.getRecord(collectionName, id);
|
|
619
|
-
if (!existing || existing.
|
|
745
|
+
if (!existing || existing._d) {
|
|
620
746
|
return yield* new NotFoundError({
|
|
621
747
|
collection: collectionName,
|
|
622
748
|
id
|
|
@@ -626,17 +752,43 @@ function createCollectionHandle(def, storage, watchCtx, validator, partialValida
|
|
|
626
752
|
id: makeEventId(),
|
|
627
753
|
collection: collectionName,
|
|
628
754
|
recordId: id,
|
|
629
|
-
kind: "
|
|
755
|
+
kind: "d",
|
|
630
756
|
data: null,
|
|
631
|
-
createdAt: Date.now()
|
|
757
|
+
createdAt: Date.now(),
|
|
758
|
+
author: localAuthor
|
|
759
|
+
};
|
|
760
|
+
yield* commitEvent(event);
|
|
761
|
+
yield* Effect6.logDebug("Record deleted", { collection: collectionName, recordId: id });
|
|
762
|
+
yield* pruneEvents(storage, collectionName, id, def.eventRetention);
|
|
763
|
+
})),
|
|
764
|
+
undo: (id) => Effect6.gen(function* () {
|
|
765
|
+
const existing = yield* storage.getRecord(collectionName, id);
|
|
766
|
+
if (!existing) {
|
|
767
|
+
return yield* new NotFoundError({ collection: collectionName, id });
|
|
768
|
+
}
|
|
769
|
+
const events = sortChronologically(yield* storage.getEventsByRecord(collectionName, id));
|
|
770
|
+
if (events.length < 2) {
|
|
771
|
+
return yield* new NotFoundError({ collection: collectionName, id });
|
|
772
|
+
}
|
|
773
|
+
const state = replayState(id, events.slice(0, -1));
|
|
774
|
+
if (!state) {
|
|
775
|
+
return yield* new NotFoundError({ collection: collectionName, id });
|
|
776
|
+
}
|
|
777
|
+
const event = {
|
|
778
|
+
id: makeEventId(),
|
|
779
|
+
collection: collectionName,
|
|
780
|
+
recordId: id,
|
|
781
|
+
kind: "u",
|
|
782
|
+
data: state,
|
|
783
|
+
createdAt: Date.now(),
|
|
784
|
+
author: localAuthor
|
|
632
785
|
};
|
|
633
786
|
yield* commitEvent(event);
|
|
634
|
-
|
|
635
|
-
yield* Effect6.forEach(oldEvents.filter((e) => e.id !== event.id), (e) => storage.deleteEvent(e.id), { discard: true });
|
|
787
|
+
yield* pruneEvents(storage, collectionName, id, def.eventRetention);
|
|
636
788
|
}),
|
|
637
789
|
get: (id) => Effect6.gen(function* () {
|
|
638
790
|
const record = yield* storage.getRecord(collectionName, id);
|
|
639
|
-
if (!record || record.
|
|
791
|
+
if (!record || record._d) {
|
|
640
792
|
return yield* new NotFoundError({
|
|
641
793
|
collection: collectionName,
|
|
642
794
|
id
|
|
@@ -645,10 +797,10 @@ function createCollectionHandle(def, storage, watchCtx, validator, partialValida
|
|
|
645
797
|
return mapRecord(record);
|
|
646
798
|
}),
|
|
647
799
|
first: () => Effect6.map(storage.getAllRecords(collectionName), (all) => {
|
|
648
|
-
const found = all.find((r) => !r.
|
|
800
|
+
const found = all.find((r) => !r._d);
|
|
649
801
|
return found ? Option3.some(mapRecord(found)) : Option3.none();
|
|
650
802
|
}),
|
|
651
|
-
count: () => Effect6.map(storage.getAllRecords(collectionName), (all) => all.filter((r) => !r.
|
|
803
|
+
count: () => Effect6.map(storage.getAllRecords(collectionName), (all) => all.filter((r) => !r._d).length),
|
|
652
804
|
watch: () => watchCollection(watchCtx, storage, collectionName, undefined, mapRecord),
|
|
653
805
|
where: (fieldName) => createWhereClause(storage, watchCtx, collectionName, def, fieldName, mapRecord),
|
|
654
806
|
orderBy: (fieldName) => createOrderByBuilder(storage, watchCtx, collectionName, def, fieldName, mapRecord)
|
|
@@ -657,7 +809,7 @@ function createCollectionHandle(def, storage, watchCtx, validator, partialValida
|
|
|
657
809
|
}
|
|
658
810
|
|
|
659
811
|
// src/sync/sync-service.ts
|
|
660
|
-
import { Effect as Effect8, Option as Option5, Ref as Ref3, Schedule } from "effect";
|
|
812
|
+
import { Effect as Effect8, Layer, Option as Option5, References as References2, Ref as Ref3, Schedule } from "effect";
|
|
661
813
|
import { unwrapEvent } from "nostr-tools/nip59";
|
|
662
814
|
import { GiftWrap as GiftWrap2 } from "nostr-tools/kinds";
|
|
663
815
|
|
|
@@ -1228,8 +1380,13 @@ function reconcileWithRelay(storage, relay, relayUrl, publicKeys) {
|
|
|
1228
1380
|
allNeedIds.push(id);
|
|
1229
1381
|
currentMsg = nextMsg;
|
|
1230
1382
|
}
|
|
1383
|
+
yield* Effect7.logDebug("Negentropy reconciliation complete", {
|
|
1384
|
+
relay: relayUrl,
|
|
1385
|
+
have: allHaveIds.length,
|
|
1386
|
+
need: allNeedIds.length
|
|
1387
|
+
});
|
|
1231
1388
|
return { haveIds: allHaveIds, needIds: allNeedIds };
|
|
1232
|
-
});
|
|
1389
|
+
}).pipe(Effect7.withLogSpan("tablinum.negentropy"));
|
|
1233
1390
|
}
|
|
1234
1391
|
|
|
1235
1392
|
// src/db/key-rotation.ts
|
|
@@ -1315,7 +1472,8 @@ function parseRemovalNotice(content, dTag) {
|
|
|
1315
1472
|
}
|
|
1316
1473
|
|
|
1317
1474
|
// src/sync/sync-service.ts
|
|
1318
|
-
function createSyncHandle(storage, giftWrapHandle, relay, publishQueue, syncStatus, watchCtx, relayUrls, knownCollections, epochStore, personalPrivateKey, personalPublicKey, scope, onSyncError, onNewAuthor, onRemoved, onMembersChanged) {
|
|
1475
|
+
function createSyncHandle(storage, giftWrapHandle, relay, publishQueue, syncStatus, watchCtx, relayUrls, knownCollections, epochStore, personalPrivateKey, personalPublicKey, scope, logLevel, onSyncError, onNewAuthor, onRemoved, onMembersChanged) {
|
|
1476
|
+
const logLayer = Layer.succeed(References2.MinimumLogLevel, logLevel);
|
|
1319
1477
|
const getSubscriptionPubKeys = () => {
|
|
1320
1478
|
return getAllPublicKeys(epochStore);
|
|
1321
1479
|
};
|
|
@@ -1325,7 +1483,7 @@ function createSyncHandle(storage, giftWrapHandle, relay, publishQueue, syncStat
|
|
|
1325
1483
|
kind: "create"
|
|
1326
1484
|
});
|
|
1327
1485
|
const forkHandled = (effect) => {
|
|
1328
|
-
Effect8.runFork(effect.pipe(Effect8.tapError((e) => Effect8.sync(() => onSyncError?.(e))), Effect8.ignore, Effect8.forkIn(scope)));
|
|
1486
|
+
Effect8.runFork(effect.pipe(Effect8.tapError((e) => Effect8.sync(() => onSyncError?.(e))), Effect8.ignore, Effect8.provide(logLayer), Effect8.forkIn(scope)));
|
|
1329
1487
|
};
|
|
1330
1488
|
let autoFlushActive = false;
|
|
1331
1489
|
const autoFlushEffect = Effect8.gen(function* () {
|
|
@@ -1374,19 +1532,21 @@ function createSyncHandle(storage, giftWrapHandle, relay, publishQueue, syncStat
|
|
|
1374
1532
|
}
|
|
1375
1533
|
const collectionName = dTag.substring(0, colonIdx);
|
|
1376
1534
|
const recordId = dTag.substring(colonIdx + 1);
|
|
1377
|
-
|
|
1535
|
+
const retention = knownCollections.get(collectionName);
|
|
1536
|
+
if (retention === undefined) {
|
|
1378
1537
|
yield* storage.putGiftWrap({ id: remoteGw.id, createdAt: remoteGw.created_at });
|
|
1379
1538
|
return null;
|
|
1380
1539
|
}
|
|
1381
1540
|
if (rumor.pubkey) {
|
|
1382
1541
|
const reject = yield* shouldRejectWrite(rumor.pubkey);
|
|
1383
1542
|
if (reject) {
|
|
1543
|
+
yield* Effect8.logWarning("Rejected write from removed member", { author: rumor.pubkey.slice(0, 12) });
|
|
1384
1544
|
yield* storage.putGiftWrap({ id: remoteGw.id, createdAt: remoteGw.created_at });
|
|
1385
1545
|
return null;
|
|
1386
1546
|
}
|
|
1387
1547
|
}
|
|
1388
1548
|
let data = null;
|
|
1389
|
-
let kind = "
|
|
1549
|
+
let kind = "u";
|
|
1390
1550
|
const parsed = yield* Effect8.try({
|
|
1391
1551
|
try: () => JSON.parse(rumor.content),
|
|
1392
1552
|
catch: () => {
|
|
@@ -1400,7 +1560,7 @@ function createSyncHandle(storage, giftWrapHandle, relay, publishQueue, syncStat
|
|
|
1400
1560
|
return null;
|
|
1401
1561
|
}
|
|
1402
1562
|
if (parsed === null || parsed._deleted) {
|
|
1403
|
-
kind = "
|
|
1563
|
+
kind = "d";
|
|
1404
1564
|
} else {
|
|
1405
1565
|
data = parsed;
|
|
1406
1566
|
}
|
|
@@ -1416,15 +1576,14 @@ function createSyncHandle(storage, giftWrapHandle, relay, publishQueue, syncStat
|
|
|
1416
1576
|
};
|
|
1417
1577
|
yield* storage.putGiftWrap({
|
|
1418
1578
|
id: remoteGw.id,
|
|
1419
|
-
eventId: event.id,
|
|
1420
1579
|
createdAt: remoteGw.created_at
|
|
1421
1580
|
});
|
|
1422
1581
|
yield* storage.putEvent(event);
|
|
1423
1582
|
const didApply = yield* applyEvent(storage, event);
|
|
1424
|
-
if (kind === "
|
|
1425
|
-
|
|
1426
|
-
yield* Effect8.forEach(oldEvents.filter((e) => e.id !== event.id), (e) => storage.deleteEvent(e.id), { discard: true });
|
|
1583
|
+
if (didApply && (kind === "u" || kind === "d")) {
|
|
1584
|
+
yield* pruneEvents(storage, collectionName, recordId, retention);
|
|
1427
1585
|
}
|
|
1586
|
+
yield* Effect8.logDebug("Processed gift wrap", { collection: collectionName, recordId, kind, author: author?.slice(0, 12) });
|
|
1428
1587
|
if (author && onNewAuthor) {
|
|
1429
1588
|
onNewAuthor(author);
|
|
1430
1589
|
}
|
|
@@ -1493,15 +1652,18 @@ function createSyncHandle(storage, giftWrapHandle, relay, publishQueue, syncStat
|
|
|
1493
1652
|
}).pipe(Effect8.tapError((err) => Effect8.sync(() => onSyncError?.(err))), Effect8.ignore);
|
|
1494
1653
|
}), { discard: true });
|
|
1495
1654
|
const syncRelay = (url, pubKeys, changedCollections) => Effect8.gen(function* () {
|
|
1655
|
+
yield* Effect8.logDebug("Syncing relay", { relay: url });
|
|
1496
1656
|
const reconcileResult = yield* Effect8.result(reconcileWithRelay(storage, relay, url, Array.from(pubKeys)));
|
|
1497
1657
|
if (reconcileResult._tag === "Failure") {
|
|
1498
1658
|
onSyncError?.(reconcileResult.failure);
|
|
1499
1659
|
return;
|
|
1500
1660
|
}
|
|
1501
1661
|
const { haveIds, needIds } = reconcileResult.success;
|
|
1662
|
+
yield* Effect8.logDebug("Relay reconciliation result", { relay: url, need: needIds.length, have: haveIds.length });
|
|
1502
1663
|
if (needIds.length > 0) {
|
|
1503
1664
|
const fetched = yield* relay.fetchEvents(needIds, url).pipe(Effect8.tapError((err) => Effect8.sync(() => onSyncError?.(err))), Effect8.orElseSucceed(() => []));
|
|
1504
|
-
|
|
1665
|
+
const sorted = [...fetched].sort((a, b) => a.created_at - b.created_at);
|
|
1666
|
+
yield* Effect8.forEach(sorted, (remoteGw) => Effect8.gen(function* () {
|
|
1505
1667
|
const collection2 = yield* processGiftWrap(remoteGw).pipe(Effect8.orElseSucceed(() => null));
|
|
1506
1668
|
if (collection2)
|
|
1507
1669
|
changedCollections.add(collection2);
|
|
@@ -1515,9 +1677,10 @@ function createSyncHandle(storage, giftWrapHandle, relay, publishQueue, syncStat
|
|
|
1515
1677
|
yield* relay.publish(gw.event, [url]).pipe(Effect8.andThen(storage.stripGiftWrapBlob(id)), Effect8.tapError((err) => Effect8.sync(() => onSyncError?.(err))), Effect8.ignore);
|
|
1516
1678
|
}), { discard: true });
|
|
1517
1679
|
}
|
|
1518
|
-
});
|
|
1680
|
+
}).pipe(Effect8.withLogSpan("tablinum.syncRelay"));
|
|
1519
1681
|
const handle = {
|
|
1520
1682
|
sync: () => Effect8.gen(function* () {
|
|
1683
|
+
yield* Effect8.logInfo("Sync started");
|
|
1521
1684
|
yield* syncStatus.set("syncing");
|
|
1522
1685
|
yield* Ref3.set(watchCtx.replayingRef, true);
|
|
1523
1686
|
const changedCollections = new Set;
|
|
@@ -1531,7 +1694,8 @@ function createSyncHandle(storage, giftWrapHandle, relay, publishQueue, syncStat
|
|
|
1531
1694
|
yield* notifyReplayComplete(watchCtx, [...changedCollections]);
|
|
1532
1695
|
yield* syncStatus.set("idle");
|
|
1533
1696
|
})));
|
|
1534
|
-
|
|
1697
|
+
yield* Effect8.logInfo("Sync complete", { changed: [...changedCollections] });
|
|
1698
|
+
}).pipe(Effect8.withLogSpan("tablinum.sync")),
|
|
1535
1699
|
publishLocal: (giftWrap) => Effect8.gen(function* () {
|
|
1536
1700
|
if (!giftWrap.event)
|
|
1537
1701
|
return;
|
|
@@ -1592,7 +1756,8 @@ var membersCollectionDef = {
|
|
|
1592
1756
|
removedAt: optionalNumber,
|
|
1593
1757
|
removedInEpoch: optionalString
|
|
1594
1758
|
},
|
|
1595
|
-
indices: []
|
|
1759
|
+
indices: [],
|
|
1760
|
+
eventRetention: 1
|
|
1596
1761
|
};
|
|
1597
1762
|
var AuthorProfileSchema = Schema5.Struct({
|
|
1598
1763
|
name: Schema5.optionalKey(Schema5.String),
|
|
@@ -1656,7 +1821,7 @@ class SyncStatus extends ServiceMap9.Service()("tablinum/SyncStatus") {
|
|
|
1656
1821
|
}
|
|
1657
1822
|
|
|
1658
1823
|
// src/layers/IdentityLive.ts
|
|
1659
|
-
import { Effect as Effect11, Layer } from "effect";
|
|
1824
|
+
import { Effect as Effect11, Layer as Layer2 } from "effect";
|
|
1660
1825
|
import { hexToBytes as hexToBytes3 } from "@noble/hashes/utils.js";
|
|
1661
1826
|
|
|
1662
1827
|
// src/db/identity.ts
|
|
@@ -1694,21 +1859,25 @@ function createIdentity(suppliedKey) {
|
|
|
1694
1859
|
}
|
|
1695
1860
|
|
|
1696
1861
|
// src/layers/IdentityLive.ts
|
|
1697
|
-
var IdentityLive =
|
|
1862
|
+
var IdentityLive = Layer2.effect(Identity, Effect11.gen(function* () {
|
|
1698
1863
|
const config = yield* Config;
|
|
1699
1864
|
const storage = yield* Storage;
|
|
1700
1865
|
const idbKey = yield* storage.getMeta("identity_key");
|
|
1701
1866
|
const resolvedKey = config.privateKey ?? (typeof idbKey === "string" && idbKey.length === 64 ? hexToBytes3(idbKey) : undefined);
|
|
1702
1867
|
const identity = yield* createIdentity(resolvedKey);
|
|
1703
1868
|
yield* storage.putMeta("identity_key", identity.exportKey());
|
|
1869
|
+
yield* Effect11.logInfo("Identity loaded", {
|
|
1870
|
+
publicKey: identity.publicKey.slice(0, 12) + "...",
|
|
1871
|
+
source: config.privateKey ? "config" : resolvedKey ? "storage" : "generated"
|
|
1872
|
+
});
|
|
1704
1873
|
return identity;
|
|
1705
1874
|
}));
|
|
1706
1875
|
|
|
1707
1876
|
// src/layers/EpochStoreLive.ts
|
|
1708
|
-
import { Effect as Effect12, Layer as
|
|
1877
|
+
import { Effect as Effect12, Layer as Layer3, Option as Option7 } from "effect";
|
|
1709
1878
|
import { generateSecretKey as generateSecretKey2 } from "nostr-tools/pure";
|
|
1710
1879
|
import { bytesToHex as bytesToHex3 } from "@noble/hashes/utils.js";
|
|
1711
|
-
var EpochStoreLive =
|
|
1880
|
+
var EpochStoreLive = Layer3.effect(EpochStore, Effect12.gen(function* () {
|
|
1712
1881
|
const config = yield* Config;
|
|
1713
1882
|
const identity = yield* Identity;
|
|
1714
1883
|
const storage = yield* Storage;
|
|
@@ -1716,21 +1885,24 @@ var EpochStoreLive = Layer2.effect(EpochStore, Effect12.gen(function* () {
|
|
|
1716
1885
|
if (typeof idbRaw === "string") {
|
|
1717
1886
|
const idbStore = deserializeEpochStore(idbRaw);
|
|
1718
1887
|
if (Option7.isSome(idbStore)) {
|
|
1888
|
+
yield* Effect12.logInfo("Epoch store loaded", { source: "storage", epochs: idbStore.value.epochs.size });
|
|
1719
1889
|
return idbStore.value;
|
|
1720
1890
|
}
|
|
1721
1891
|
}
|
|
1722
1892
|
if (config.epochKeys && config.epochKeys.length > 0) {
|
|
1723
1893
|
const store2 = createEpochStoreFromInputs(config.epochKeys);
|
|
1724
1894
|
yield* storage.putMeta("epochs", stringifyEpochStore(store2));
|
|
1895
|
+
yield* Effect12.logInfo("Epoch store loaded", { source: "config", epochs: store2.epochs.size });
|
|
1725
1896
|
return store2;
|
|
1726
1897
|
}
|
|
1727
1898
|
const store = createEpochStoreFromInputs([{ epochId: EpochId("epoch-0"), key: bytesToHex3(generateSecretKey2()) }], { createdBy: identity.publicKey });
|
|
1728
1899
|
yield* storage.putMeta("epochs", stringifyEpochStore(store));
|
|
1900
|
+
yield* Effect12.logInfo("Epoch store loaded", { source: "generated", epochs: store.epochs.size });
|
|
1729
1901
|
return store;
|
|
1730
1902
|
}));
|
|
1731
1903
|
|
|
1732
1904
|
// src/layers/StorageLive.ts
|
|
1733
|
-
import { Effect as Effect14, Layer as
|
|
1905
|
+
import { Effect as Effect14, Layer as Layer4 } from "effect";
|
|
1734
1906
|
|
|
1735
1907
|
// src/storage/idb.ts
|
|
1736
1908
|
import { Effect as Effect13 } from "effect";
|
|
@@ -1865,13 +2037,18 @@ function openIDBStorage(dbName, schema) {
|
|
|
1865
2037
|
stripGiftWrapBlob: (id) => wrap("stripGiftWrapBlob", async () => {
|
|
1866
2038
|
const existing = await db.get("giftwraps", id);
|
|
1867
2039
|
if (existing) {
|
|
1868
|
-
|
|
1869
|
-
await db.put("giftwraps", tombstone);
|
|
2040
|
+
await db.put("giftwraps", { id: existing.id, createdAt: existing.createdAt });
|
|
1870
2041
|
}
|
|
1871
2042
|
}),
|
|
1872
2043
|
deleteEvent: (id) => wrap("deleteEvent", () => db.delete("events", id).then(() => {
|
|
1873
2044
|
return;
|
|
1874
2045
|
})),
|
|
2046
|
+
stripEventData: (id) => wrap("stripEventData", async () => {
|
|
2047
|
+
const existing = await db.get("events", id);
|
|
2048
|
+
if (existing) {
|
|
2049
|
+
await db.put("events", { ...existing, data: null });
|
|
2050
|
+
}
|
|
2051
|
+
}),
|
|
1875
2052
|
getMeta: (key) => wrap("getMeta", () => db.get("_meta", key)),
|
|
1876
2053
|
putMeta: (key, value) => wrap("putMeta", () => db.put("_meta", value, key).then(() => {
|
|
1877
2054
|
return;
|
|
@@ -1883,16 +2060,18 @@ function openIDBStorage(dbName, schema) {
|
|
|
1883
2060
|
}
|
|
1884
2061
|
|
|
1885
2062
|
// src/layers/StorageLive.ts
|
|
1886
|
-
var StorageLive =
|
|
2063
|
+
var StorageLive = Layer4.effect(Storage, Effect14.gen(function* () {
|
|
1887
2064
|
const config = yield* Config;
|
|
1888
|
-
|
|
2065
|
+
const handle = yield* openIDBStorage(config.dbName, {
|
|
1889
2066
|
...config.schema,
|
|
1890
2067
|
_members: membersCollectionDef
|
|
1891
2068
|
});
|
|
2069
|
+
yield* Effect14.logInfo("Storage opened", { dbName: config.dbName });
|
|
2070
|
+
return handle;
|
|
1892
2071
|
}));
|
|
1893
2072
|
|
|
1894
2073
|
// src/layers/RelayLive.ts
|
|
1895
|
-
import { Layer as
|
|
2074
|
+
import { Layer as Layer5 } from "effect";
|
|
1896
2075
|
|
|
1897
2076
|
// src/sync/relay.ts
|
|
1898
2077
|
import { Effect as Effect15, Option as Option8, Schema as Schema6, ScopedCache, Scope as Scope3 } from "effect";
|
|
@@ -2105,10 +2284,10 @@ function createRelayHandle() {
|
|
|
2105
2284
|
}
|
|
2106
2285
|
|
|
2107
2286
|
// src/layers/RelayLive.ts
|
|
2108
|
-
var RelayLive =
|
|
2287
|
+
var RelayLive = Layer5.effect(Relay, createRelayHandle());
|
|
2109
2288
|
|
|
2110
2289
|
// src/layers/GiftWrapLive.ts
|
|
2111
|
-
import { Effect as Effect17, Layer as
|
|
2290
|
+
import { Effect as Effect17, Layer as Layer6 } from "effect";
|
|
2112
2291
|
|
|
2113
2292
|
// src/sync/gift-wrap.ts
|
|
2114
2293
|
import { Effect as Effect16 } from "effect";
|
|
@@ -2145,14 +2324,14 @@ function createEpochGiftWrapHandle(senderPrivateKey, epochStore) {
|
|
|
2145
2324
|
}
|
|
2146
2325
|
|
|
2147
2326
|
// src/layers/GiftWrapLive.ts
|
|
2148
|
-
var GiftWrapLive =
|
|
2327
|
+
var GiftWrapLive = Layer6.effect(GiftWrap3, Effect17.gen(function* () {
|
|
2149
2328
|
const identity = yield* Identity;
|
|
2150
2329
|
const epochStore = yield* EpochStore;
|
|
2151
2330
|
return createEpochGiftWrapHandle(identity.privateKey, epochStore);
|
|
2152
2331
|
}));
|
|
2153
2332
|
|
|
2154
2333
|
// src/layers/PublishQueueLive.ts
|
|
2155
|
-
import { Effect as Effect19, Layer as
|
|
2334
|
+
import { Effect as Effect19, Layer as Layer7 } from "effect";
|
|
2156
2335
|
|
|
2157
2336
|
// src/sync/publish-queue.ts
|
|
2158
2337
|
import { Effect as Effect18, Ref as Ref4 } from "effect";
|
|
@@ -2226,14 +2405,14 @@ function createPublishQueue(storage, relay) {
|
|
|
2226
2405
|
}
|
|
2227
2406
|
|
|
2228
2407
|
// src/layers/PublishQueueLive.ts
|
|
2229
|
-
var PublishQueueLive =
|
|
2408
|
+
var PublishQueueLive = Layer7.effect(PublishQueue, Effect19.gen(function* () {
|
|
2230
2409
|
const storage = yield* Storage;
|
|
2231
2410
|
const relay = yield* Relay;
|
|
2232
2411
|
return yield* createPublishQueue(storage, relay);
|
|
2233
2412
|
}));
|
|
2234
2413
|
|
|
2235
2414
|
// src/layers/SyncStatusLive.ts
|
|
2236
|
-
import { Layer as
|
|
2415
|
+
import { Layer as Layer8 } from "effect";
|
|
2237
2416
|
|
|
2238
2417
|
// src/sync/sync-status.ts
|
|
2239
2418
|
import { Effect as Effect20, SubscriptionRef } from "effect";
|
|
@@ -2257,7 +2436,7 @@ function createSyncStatusHandle() {
|
|
|
2257
2436
|
}
|
|
2258
2437
|
|
|
2259
2438
|
// src/layers/SyncStatusLive.ts
|
|
2260
|
-
var SyncStatusLive =
|
|
2439
|
+
var SyncStatusLive = Layer8.effect(SyncStatus, createSyncStatusHandle());
|
|
2261
2440
|
|
|
2262
2441
|
// src/layers/TablinumLive.ts
|
|
2263
2442
|
function reportSyncError(onSyncError, error) {
|
|
@@ -2278,12 +2457,12 @@ function mapMemberRecord(record) {
|
|
|
2278
2457
|
...record.removedInEpoch !== undefined ? { removedInEpoch: record.removedInEpoch } : {}
|
|
2279
2458
|
};
|
|
2280
2459
|
}
|
|
2281
|
-
var IdentityWithDeps = IdentityLive.pipe(
|
|
2282
|
-
var EpochStoreWithDeps = EpochStoreLive.pipe(
|
|
2283
|
-
var GiftWrapWithDeps = GiftWrapLive.pipe(
|
|
2284
|
-
var PublishQueueWithDeps = PublishQueueLive.pipe(
|
|
2285
|
-
var AllServicesLive =
|
|
2286
|
-
var TablinumLive =
|
|
2460
|
+
var IdentityWithDeps = IdentityLive.pipe(Layer9.provide(StorageLive));
|
|
2461
|
+
var EpochStoreWithDeps = EpochStoreLive.pipe(Layer9.provide(IdentityWithDeps), Layer9.provide(StorageLive));
|
|
2462
|
+
var GiftWrapWithDeps = GiftWrapLive.pipe(Layer9.provide(IdentityWithDeps), Layer9.provide(EpochStoreWithDeps));
|
|
2463
|
+
var PublishQueueWithDeps = PublishQueueLive.pipe(Layer9.provide(StorageLive), Layer9.provide(RelayLive));
|
|
2464
|
+
var AllServicesLive = Layer9.mergeAll(IdentityWithDeps, EpochStoreWithDeps, StorageLive, RelayLive, GiftWrapWithDeps, PublishQueueWithDeps, SyncStatusLive);
|
|
2465
|
+
var TablinumLive = Layer9.effect(Tablinum, Effect21.gen(function* () {
|
|
2287
2466
|
const config = yield* Config;
|
|
2288
2467
|
const identity = yield* Identity;
|
|
2289
2468
|
const epochStore = yield* EpochStore;
|
|
@@ -2293,17 +2472,18 @@ var TablinumLive = Layer8.effect(Tablinum, Effect21.gen(function* () {
|
|
|
2293
2472
|
const publishQueue = yield* PublishQueue;
|
|
2294
2473
|
const syncStatus = yield* SyncStatus;
|
|
2295
2474
|
const scope = yield* Effect21.scope;
|
|
2475
|
+
const logLayer = Layer9.succeed(References3.MinimumLogLevel, config.logLevel);
|
|
2296
2476
|
const pubsub = yield* PubSub2.unbounded();
|
|
2297
2477
|
const replayingRef = yield* Ref5.make(false);
|
|
2298
2478
|
const closedRef = yield* Ref5.make(false);
|
|
2299
2479
|
const watchCtx = { pubsub, replayingRef };
|
|
2300
2480
|
const schemaEntries = Object.entries(config.schema);
|
|
2301
2481
|
const allSchemaEntries = [...schemaEntries, ["_members", membersCollectionDef]];
|
|
2302
|
-
const knownCollections = new
|
|
2482
|
+
const knownCollections = new Map(allSchemaEntries.map(([, def]) => [def.name, def.eventRetention]));
|
|
2303
2483
|
let notifyAuthor;
|
|
2304
|
-
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);
|
|
2484
|
+
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);
|
|
2305
2485
|
const onWrite = (event) => Effect21.gen(function* () {
|
|
2306
|
-
const content = event.kind === "
|
|
2486
|
+
const content = event.kind === "d" ? JSON.stringify(null) : JSON.stringify(event.data);
|
|
2307
2487
|
const dTag = `${event.collection}:${event.recordId}`;
|
|
2308
2488
|
const wrapResult = yield* Effect21.result(giftWrap.wrap({
|
|
2309
2489
|
kind: 1,
|
|
@@ -2316,11 +2496,10 @@ var TablinumLive = Layer8.effect(Tablinum, Effect21.gen(function* () {
|
|
|
2316
2496
|
return;
|
|
2317
2497
|
}
|
|
2318
2498
|
const gw = wrapResult.success;
|
|
2319
|
-
yield* storage.putGiftWrap({ id: gw.id,
|
|
2499
|
+
yield* storage.putGiftWrap({ id: gw.id, createdAt: gw.created_at });
|
|
2320
2500
|
yield* Effect21.forkIn(Effect21.gen(function* () {
|
|
2321
2501
|
const publishResult = yield* Effect21.result(syncHandle.publishLocal({
|
|
2322
2502
|
id: gw.id,
|
|
2323
|
-
eventId: event.id,
|
|
2324
2503
|
event: gw,
|
|
2325
2504
|
createdAt: gw.created_at
|
|
2326
2505
|
}));
|
|
@@ -2336,9 +2515,10 @@ var TablinumLive = Layer8.effect(Tablinum, Effect21.gen(function* () {
|
|
|
2336
2515
|
id: uuidv7(),
|
|
2337
2516
|
collection: "_members",
|
|
2338
2517
|
recordId: record.id,
|
|
2339
|
-
kind: existing ? "
|
|
2518
|
+
kind: existing ? "u" : "c",
|
|
2340
2519
|
data: record,
|
|
2341
|
-
createdAt: Date.now()
|
|
2520
|
+
createdAt: Date.now(),
|
|
2521
|
+
author: identity.publicKey
|
|
2342
2522
|
};
|
|
2343
2523
|
yield* storage.putEvent(event);
|
|
2344
2524
|
yield* applyEvent(storage, event);
|
|
@@ -2379,16 +2559,21 @@ var TablinumLive = Layer8.effect(Tablinum, Effect21.gen(function* () {
|
|
|
2379
2559
|
config.onMembersChanged?.();
|
|
2380
2560
|
}
|
|
2381
2561
|
}
|
|
2382
|
-
}).pipe(Effect21.ignore, Effect21.forkIn(scope)));
|
|
2562
|
+
}).pipe(Effect21.ignore, Effect21.provide(logLayer), Effect21.forkIn(scope)));
|
|
2383
2563
|
};
|
|
2384
2564
|
const handles = new Map;
|
|
2385
2565
|
for (const [, def] of allSchemaEntries) {
|
|
2386
2566
|
const validator = buildValidator(def.name, def);
|
|
2387
2567
|
const partialValidator = buildPartialValidator(def.name, def);
|
|
2388
|
-
const handle = createCollectionHandle(def, storage, watchCtx, validator, partialValidator, uuidv7, onWrite);
|
|
2568
|
+
const handle = createCollectionHandle(def, storage, watchCtx, validator, partialValidator, uuidv7, identity.publicKey, onWrite, config.logLevel);
|
|
2389
2569
|
handles.set(def.name, handle);
|
|
2390
2570
|
}
|
|
2391
2571
|
yield* syncHandle.startSubscription();
|
|
2572
|
+
yield* Effect21.logInfo("Tablinum ready", {
|
|
2573
|
+
dbName: config.dbName,
|
|
2574
|
+
collections: schemaEntries.map(([name]) => name),
|
|
2575
|
+
relays: config.relays
|
|
2576
|
+
});
|
|
2392
2577
|
const selfMember = yield* storage.getRecord("_members", identity.publicKey);
|
|
2393
2578
|
if (!selfMember) {
|
|
2394
2579
|
yield* putMemberRecord({
|
|
@@ -2397,18 +2582,19 @@ var TablinumLive = Layer8.effect(Tablinum, Effect21.gen(function* () {
|
|
|
2397
2582
|
addedInEpoch: getCurrentEpoch(epochStore).id
|
|
2398
2583
|
});
|
|
2399
2584
|
}
|
|
2400
|
-
const
|
|
2585
|
+
const withLog = (effect) => Effect21.provideService(effect, References3.MinimumLogLevel, config.logLevel);
|
|
2586
|
+
const ensureOpen = (effect) => withLog(Effect21.gen(function* () {
|
|
2401
2587
|
if (yield* Ref5.get(closedRef)) {
|
|
2402
2588
|
return yield* new StorageError({ message: "Database is closed" });
|
|
2403
2589
|
}
|
|
2404
2590
|
return yield* effect;
|
|
2405
|
-
});
|
|
2406
|
-
const ensureSyncOpen = (effect) => Effect21.gen(function* () {
|
|
2591
|
+
}));
|
|
2592
|
+
const ensureSyncOpen = (effect) => withLog(Effect21.gen(function* () {
|
|
2407
2593
|
if (yield* Ref5.get(closedRef)) {
|
|
2408
2594
|
return yield* new SyncError({ message: "Database is closed", phase: "init" });
|
|
2409
2595
|
}
|
|
2410
2596
|
return yield* effect;
|
|
2411
|
-
});
|
|
2597
|
+
}));
|
|
2412
2598
|
const dbHandle = {
|
|
2413
2599
|
collection: (name) => {
|
|
2414
2600
|
const handle = handles.get(name);
|
|
@@ -2424,12 +2610,12 @@ var TablinumLive = Layer8.effect(Tablinum, Effect21.gen(function* () {
|
|
|
2424
2610
|
relays: [...config.relays],
|
|
2425
2611
|
dbName: config.dbName
|
|
2426
2612
|
}),
|
|
2427
|
-
close: () => Effect21.gen(function* () {
|
|
2613
|
+
close: () => withLog(Effect21.gen(function* () {
|
|
2428
2614
|
if (yield* Ref5.get(closedRef))
|
|
2429
2615
|
return;
|
|
2430
2616
|
yield* Ref5.set(closedRef, true);
|
|
2431
2617
|
yield* Scope4.close(scope, Exit.void);
|
|
2432
|
-
}),
|
|
2618
|
+
})),
|
|
2433
2619
|
rebuild: () => ensureOpen(rebuild(storage, allSchemaEntries.map(([, def]) => def.name))),
|
|
2434
2620
|
sync: () => ensureSyncOpen(syncHandle.sync()),
|
|
2435
2621
|
getSyncStatus: () => syncStatus.get(),
|
|
@@ -2473,20 +2659,52 @@ var TablinumLive = Layer8.effect(Tablinum, Effect21.gen(function* () {
|
|
|
2473
2659
|
})),
|
|
2474
2660
|
getMembers: () => ensureOpen(Effect21.gen(function* () {
|
|
2475
2661
|
const allRecords = yield* storage.getAllRecords("_members");
|
|
2476
|
-
return allRecords.filter((record) => !record.
|
|
2662
|
+
return allRecords.filter((record) => !record._d).map(mapMemberRecord);
|
|
2663
|
+
})),
|
|
2664
|
+
getProfile: () => ensureOpen(Effect21.gen(function* () {
|
|
2665
|
+
const record = yield* storage.getRecord("_members", identity.publicKey);
|
|
2666
|
+
if (!record)
|
|
2667
|
+
return {};
|
|
2668
|
+
const profile = {};
|
|
2669
|
+
if (record.name !== undefined)
|
|
2670
|
+
profile.name = record.name;
|
|
2671
|
+
if (record.picture !== undefined)
|
|
2672
|
+
profile.picture = record.picture;
|
|
2673
|
+
if (record.about !== undefined)
|
|
2674
|
+
profile.about = record.about;
|
|
2675
|
+
if (record.nip05 !== undefined)
|
|
2676
|
+
profile.nip05 = record.nip05;
|
|
2677
|
+
return profile;
|
|
2477
2678
|
})),
|
|
2478
2679
|
setProfile: (profile) => ensureOpen(Effect21.gen(function* () {
|
|
2479
2680
|
const existing = yield* storage.getRecord("_members", identity.publicKey);
|
|
2480
2681
|
if (!existing) {
|
|
2481
2682
|
return yield* new ValidationError({ message: "Current user is not a member" });
|
|
2482
2683
|
}
|
|
2483
|
-
|
|
2684
|
+
const { _d, _u, _a, _e, ...memberFields } = existing;
|
|
2685
|
+
yield* putMemberRecord({ ...memberFields, ...profile });
|
|
2484
2686
|
}))
|
|
2485
2687
|
};
|
|
2486
2688
|
return dbHandle;
|
|
2487
|
-
})).pipe(
|
|
2689
|
+
}).pipe(Effect21.withLogSpan("tablinum.init"))).pipe(Layer9.provide(AllServicesLive));
|
|
2488
2690
|
|
|
2489
2691
|
// src/db/create-tablinum.ts
|
|
2692
|
+
function resolveLogLevel(input) {
|
|
2693
|
+
if (input === undefined || input === "none")
|
|
2694
|
+
return "None";
|
|
2695
|
+
switch (input) {
|
|
2696
|
+
case "debug":
|
|
2697
|
+
return "Debug";
|
|
2698
|
+
case "info":
|
|
2699
|
+
return "Info";
|
|
2700
|
+
case "warning":
|
|
2701
|
+
return "Warn";
|
|
2702
|
+
case "error":
|
|
2703
|
+
return "Error";
|
|
2704
|
+
default:
|
|
2705
|
+
return input;
|
|
2706
|
+
}
|
|
2707
|
+
}
|
|
2490
2708
|
function validateConfig(config) {
|
|
2491
2709
|
return Effect22.gen(function* () {
|
|
2492
2710
|
if (Object.keys(config.schema).length === 0) {
|
|
@@ -2500,19 +2718,26 @@ function createTablinum(config) {
|
|
|
2500
2718
|
return Effect22.gen(function* () {
|
|
2501
2719
|
yield* validateConfig(config);
|
|
2502
2720
|
const runtimeConfig = yield* resolveRuntimeConfig(config);
|
|
2721
|
+
const logLevel = resolveLogLevel(config.logLevel);
|
|
2503
2722
|
const configValue = {
|
|
2504
2723
|
...runtimeConfig,
|
|
2505
2724
|
schema: config.schema,
|
|
2725
|
+
logLevel,
|
|
2506
2726
|
onSyncError: config.onSyncError,
|
|
2507
2727
|
onRemoved: config.onRemoved,
|
|
2508
2728
|
onMembersChanged: config.onMembersChanged
|
|
2509
2729
|
};
|
|
2510
|
-
const configLayer =
|
|
2511
|
-
const
|
|
2512
|
-
const
|
|
2730
|
+
const configLayer = Layer10.succeed(Config, configValue);
|
|
2731
|
+
const logLayer = Layer10.succeed(References4.MinimumLogLevel, logLevel);
|
|
2732
|
+
const fullLayer = TablinumLive.pipe(Layer10.provide(configLayer), Layer10.provide(logLayer));
|
|
2733
|
+
const ctx = yield* Layer10.build(fullLayer);
|
|
2513
2734
|
return ServiceMap10.get(ctx, Tablinum);
|
|
2514
2735
|
});
|
|
2515
2736
|
}
|
|
2737
|
+
|
|
2738
|
+
// src/index.ts
|
|
2739
|
+
import { LogLevel } from "effect";
|
|
2740
|
+
|
|
2516
2741
|
// src/db/invite.ts
|
|
2517
2742
|
import { Schema as Schema7 } from "effect";
|
|
2518
2743
|
var InviteSchema = Schema7.Struct({
|
|
@@ -2558,6 +2783,7 @@ export {
|
|
|
2558
2783
|
StorageError,
|
|
2559
2784
|
RelayError,
|
|
2560
2785
|
NotFoundError,
|
|
2786
|
+
LogLevel,
|
|
2561
2787
|
EpochId,
|
|
2562
2788
|
DatabaseName,
|
|
2563
2789
|
CryptoError,
|