tablinum 0.4.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 +60 -0
- package/dist/crud/collection-handle.d.ts +2 -1
- package/dist/db/create-tablinum.d.ts +4 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +106 -50
- package/dist/services/Config.d.ts +2 -0
- package/dist/svelte/collection.svelte.d.ts +2 -1
- package/dist/svelte/index.svelte.js +114 -59
- package/dist/sync/sync-service.d.ts +2 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -198,6 +198,66 @@ Use it in a component:
|
|
|
198
198
|
- **`db.status`** tracks initialization and terminal state; **`db.syncStatus`** tracks sync activity
|
|
199
199
|
- **`createTablinum(config)`** still exists as a convenience and resolves once `db.ready` completes
|
|
200
200
|
|
|
201
|
+
## Logging
|
|
202
|
+
|
|
203
|
+
Tablinum uses [Effect's built-in logging](https://effect.website/docs/observability/logging/) under the hood. By default, logging is completely silent. Set `logLevel` in your config to enable it:
|
|
204
|
+
|
|
205
|
+
```typescript
|
|
206
|
+
const db = yield* createTablinum({
|
|
207
|
+
schema,
|
|
208
|
+
relays: ["wss://relay.example.com"],
|
|
209
|
+
logLevel: "debug", // "debug" | "info" | "warning" | "error" | "none"
|
|
210
|
+
});
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
Wire it to an environment variable for easy toggling:
|
|
214
|
+
|
|
215
|
+
```typescript
|
|
216
|
+
// Effect API
|
|
217
|
+
createTablinum({
|
|
218
|
+
schema,
|
|
219
|
+
relays: ["wss://relay.example.com"],
|
|
220
|
+
logLevel: import.meta.env.VITE_LOG_LEVEL ?? "none",
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
// Svelte API
|
|
224
|
+
new Tablinum({
|
|
225
|
+
schema,
|
|
226
|
+
relays: ["wss://relay.example.com"],
|
|
227
|
+
logLevel: import.meta.env.VITE_LOG_LEVEL ?? "none",
|
|
228
|
+
});
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
### Log levels
|
|
232
|
+
|
|
233
|
+
| Level | What you see |
|
|
234
|
+
|-------|-------------|
|
|
235
|
+
| `"none"` | Nothing (default) |
|
|
236
|
+
| `"error"` | Unrecoverable failures |
|
|
237
|
+
| `"warning"` | Recoverable issues (e.g. rejected writes from removed members) |
|
|
238
|
+
| `"info"` | Lifecycle milestones — storage opened, identity loaded, sync started/complete |
|
|
239
|
+
| `"debug"` | Everything above plus CRUD operations (with record data), relay reconciliation details, gift wrap processing |
|
|
240
|
+
|
|
241
|
+
### Log spans
|
|
242
|
+
|
|
243
|
+
Key operations include timing spans that appear automatically in log output:
|
|
244
|
+
|
|
245
|
+
```
|
|
246
|
+
[11:50:31] INFO (#1) tablinum.init=13ms: Tablinum ready { ... }
|
|
247
|
+
[11:50:41] INFO (#1) tablinum.sync=520ms: Sync complete { changed: ["todos"] }
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
Spans: `tablinum.init`, `tablinum.sync`, `tablinum.syncRelay`, `tablinum.negentropy`.
|
|
251
|
+
|
|
252
|
+
### Using Effect's LogLevel type
|
|
253
|
+
|
|
254
|
+
Power users can also pass Effect's `LogLevel` type directly:
|
|
255
|
+
|
|
256
|
+
```typescript
|
|
257
|
+
import { LogLevel } from "tablinum";
|
|
258
|
+
createTablinum({ ..., logLevel: LogLevel.Debug });
|
|
259
|
+
```
|
|
260
|
+
|
|
201
261
|
## How it works
|
|
202
262
|
|
|
203
263
|
Tablinum is built on [Effect](https://effect.website) and stores all data locally in [IndexedDB](https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API).
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Effect, Option, Stream } from "effect";
|
|
2
|
+
import type { LogLevel } from "effect";
|
|
2
3
|
import type { CollectionDef, CollectionFields } from "../schema/collection.ts";
|
|
3
4
|
import type { InferRecord } from "../schema/types.ts";
|
|
4
5
|
import type { RecordValidator, PartialValidator } from "../schema/validate.ts";
|
|
@@ -20,4 +21,4 @@ export interface CollectionHandle<C extends CollectionDef<CollectionFields>> {
|
|
|
20
21
|
readonly orderBy: (field: string & keyof Omit<InferRecord<C>, "id">) => QueryBuilder<InferRecord<C>>;
|
|
21
22
|
}
|
|
22
23
|
export type OnWriteCallback = (event: StoredEvent) => Effect.Effect<void, StorageError>;
|
|
23
|
-
export declare function createCollectionHandle<C extends CollectionDef<CollectionFields>>(def: C, storage: IDBStorageHandle, watchCtx: WatchContext, validator: RecordValidator<C["fields"]>, partialValidator: PartialValidator<C["fields"]>, makeEventId: () => string, localAuthor?: string, onWrite?: OnWriteCallback): CollectionHandle<C>;
|
|
24
|
+
export declare function createCollectionHandle<C extends CollectionDef<CollectionFields>>(def: C, storage: IDBStorageHandle, watchCtx: WatchContext, validator: RecordValidator<C["fields"]>, partialValidator: PartialValidator<C["fields"]>, makeEventId: () => string, localAuthor?: string, onWrite?: OnWriteCallback, logLevel?: LogLevel.LogLevel): CollectionHandle<C>;
|
|
@@ -1,14 +1,18 @@
|
|
|
1
1
|
import { Effect, Scope } from "effect";
|
|
2
|
+
import type { LogLevel } from "effect";
|
|
2
3
|
import type { SchemaConfig } from "../schema/types.ts";
|
|
3
4
|
import type { DatabaseHandle } from "./database-handle.ts";
|
|
4
5
|
import type { EpochKeyInput } from "./epoch.ts";
|
|
5
6
|
import { CryptoError, StorageError, ValidationError } from "../errors.ts";
|
|
7
|
+
export type TablinumLogLevel = "debug" | "info" | "warning" | "error" | "none" | LogLevel.LogLevel;
|
|
8
|
+
export declare function resolveLogLevel(input: TablinumLogLevel | undefined): LogLevel.LogLevel;
|
|
6
9
|
export interface TablinumConfig<S extends SchemaConfig> {
|
|
7
10
|
readonly schema: S;
|
|
8
11
|
readonly relays: readonly string[];
|
|
9
12
|
readonly privateKey?: Uint8Array | undefined;
|
|
10
13
|
readonly epochKeys?: ReadonlyArray<EpochKeyInput> | undefined;
|
|
11
14
|
readonly dbName?: string | undefined;
|
|
15
|
+
readonly logLevel?: TablinumLogLevel | undefined;
|
|
12
16
|
readonly onSyncError?: ((error: Error) => void) | undefined;
|
|
13
17
|
readonly onRemoved?: ((info: {
|
|
14
18
|
epochId: string;
|
package/dist/index.d.ts
CHANGED
|
@@ -4,7 +4,8 @@ export type { CollectionDef, CollectionFields } from "./schema/collection.ts";
|
|
|
4
4
|
export type { FieldDef, FieldKind } from "./schema/field.ts";
|
|
5
5
|
export type { InferRecord, SchemaConfig } from "./schema/types.ts";
|
|
6
6
|
export { createTablinum } from "./db/create-tablinum.ts";
|
|
7
|
-
export type { TablinumConfig } from "./db/create-tablinum.ts";
|
|
7
|
+
export type { TablinumConfig, TablinumLogLevel } from "./db/create-tablinum.ts";
|
|
8
|
+
export { LogLevel } from "effect";
|
|
8
9
|
export type { DatabaseHandle, SyncStatus } from "./db/database-handle.ts";
|
|
9
10
|
export { encodeInvite, decodeInvite } from "./db/invite.ts";
|
|
10
11
|
export type { Invite } from "./db/invite.ts";
|
package/dist/index.js
CHANGED
|
@@ -54,7 +54,7 @@ function collection(name, fields, options) {
|
|
|
54
54
|
return { _tag: "CollectionDef", name, fields, indices, eventRetention };
|
|
55
55
|
}
|
|
56
56
|
// src/db/create-tablinum.ts
|
|
57
|
-
import { Effect as Effect22, Layer as
|
|
57
|
+
import { Effect as Effect22, Layer as Layer10, References as References4, ServiceMap as ServiceMap10 } from "effect";
|
|
58
58
|
|
|
59
59
|
// src/db/runtime-config.ts
|
|
60
60
|
import { Effect, Schema as Schema2 } from "effect";
|
|
@@ -220,7 +220,7 @@ class Tablinum extends ServiceMap2.Service()("tablinum/Tablinum") {
|
|
|
220
220
|
}
|
|
221
221
|
|
|
222
222
|
// src/layers/TablinumLive.ts
|
|
223
|
-
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";
|
|
224
224
|
|
|
225
225
|
// src/crud/watch.ts
|
|
226
226
|
import { Effect as Effect2, PubSub, Ref, Stream } from "effect";
|
|
@@ -434,7 +434,7 @@ function buildPartialValidator(collectionName, def) {
|
|
|
434
434
|
}
|
|
435
435
|
|
|
436
436
|
// src/crud/collection-handle.ts
|
|
437
|
-
import { Effect as Effect6, Option as Option3 } from "effect";
|
|
437
|
+
import { Effect as Effect6, Option as Option3, References } from "effect";
|
|
438
438
|
|
|
439
439
|
// src/utils/uuid.ts
|
|
440
440
|
var alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
|
|
@@ -682,8 +682,9 @@ function mapRecord(record) {
|
|
|
682
682
|
const { _d, _u, _a, _e, ...fields } = record;
|
|
683
683
|
return fields;
|
|
684
684
|
}
|
|
685
|
-
function createCollectionHandle(def, storage, watchCtx, validator, partialValidator, makeEventId, localAuthor, onWrite) {
|
|
685
|
+
function createCollectionHandle(def, storage, watchCtx, validator, partialValidator, makeEventId, localAuthor, onWrite, logLevel = "None") {
|
|
686
686
|
const collectionName = def.name;
|
|
687
|
+
const withLog = (effect) => Effect6.provideService(effect, References.MinimumLogLevel, logLevel);
|
|
687
688
|
const commitEvent = (event) => Effect6.gen(function* () {
|
|
688
689
|
yield* storage.putEvent(event);
|
|
689
690
|
yield* applyEvent(storage, event);
|
|
@@ -696,7 +697,7 @@ function createCollectionHandle(def, storage, watchCtx, validator, partialValida
|
|
|
696
697
|
});
|
|
697
698
|
});
|
|
698
699
|
const handle = {
|
|
699
|
-
add: (data) => Effect6.gen(function* () {
|
|
700
|
+
add: (data) => withLog(Effect6.gen(function* () {
|
|
700
701
|
const id = uuidv7();
|
|
701
702
|
const fullRecord = { id, ...data };
|
|
702
703
|
yield* validator(fullRecord);
|
|
@@ -710,9 +711,10 @@ function createCollectionHandle(def, storage, watchCtx, validator, partialValida
|
|
|
710
711
|
author: localAuthor
|
|
711
712
|
};
|
|
712
713
|
yield* commitEvent(event);
|
|
714
|
+
yield* Effect6.logDebug("Record added", { collection: collectionName, recordId: id, data: fullRecord });
|
|
713
715
|
return id;
|
|
714
|
-
}),
|
|
715
|
-
update: (id, data) => Effect6.gen(function* () {
|
|
716
|
+
})),
|
|
717
|
+
update: (id, data) => withLog(Effect6.gen(function* () {
|
|
716
718
|
const existing = yield* storage.getRecord(collectionName, id);
|
|
717
719
|
if (!existing || existing._d) {
|
|
718
720
|
return yield* new NotFoundError({
|
|
@@ -735,9 +737,10 @@ function createCollectionHandle(def, storage, watchCtx, validator, partialValida
|
|
|
735
737
|
author: localAuthor
|
|
736
738
|
};
|
|
737
739
|
yield* commitEvent(event);
|
|
740
|
+
yield* Effect6.logDebug("Record updated", { collection: collectionName, recordId: id, data: diff });
|
|
738
741
|
yield* pruneEvents(storage, collectionName, id, def.eventRetention);
|
|
739
|
-
}),
|
|
740
|
-
delete: (id) => Effect6.gen(function* () {
|
|
742
|
+
})),
|
|
743
|
+
delete: (id) => withLog(Effect6.gen(function* () {
|
|
741
744
|
const existing = yield* storage.getRecord(collectionName, id);
|
|
742
745
|
if (!existing || existing._d) {
|
|
743
746
|
return yield* new NotFoundError({
|
|
@@ -755,8 +758,9 @@ function createCollectionHandle(def, storage, watchCtx, validator, partialValida
|
|
|
755
758
|
author: localAuthor
|
|
756
759
|
};
|
|
757
760
|
yield* commitEvent(event);
|
|
761
|
+
yield* Effect6.logDebug("Record deleted", { collection: collectionName, recordId: id });
|
|
758
762
|
yield* pruneEvents(storage, collectionName, id, def.eventRetention);
|
|
759
|
-
}),
|
|
763
|
+
})),
|
|
760
764
|
undo: (id) => Effect6.gen(function* () {
|
|
761
765
|
const existing = yield* storage.getRecord(collectionName, id);
|
|
762
766
|
if (!existing) {
|
|
@@ -805,7 +809,7 @@ function createCollectionHandle(def, storage, watchCtx, validator, partialValida
|
|
|
805
809
|
}
|
|
806
810
|
|
|
807
811
|
// src/sync/sync-service.ts
|
|
808
|
-
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";
|
|
809
813
|
import { unwrapEvent } from "nostr-tools/nip59";
|
|
810
814
|
import { GiftWrap as GiftWrap2 } from "nostr-tools/kinds";
|
|
811
815
|
|
|
@@ -1376,8 +1380,13 @@ function reconcileWithRelay(storage, relay, relayUrl, publicKeys) {
|
|
|
1376
1380
|
allNeedIds.push(id);
|
|
1377
1381
|
currentMsg = nextMsg;
|
|
1378
1382
|
}
|
|
1383
|
+
yield* Effect7.logDebug("Negentropy reconciliation complete", {
|
|
1384
|
+
relay: relayUrl,
|
|
1385
|
+
have: allHaveIds.length,
|
|
1386
|
+
need: allNeedIds.length
|
|
1387
|
+
});
|
|
1379
1388
|
return { haveIds: allHaveIds, needIds: allNeedIds };
|
|
1380
|
-
});
|
|
1389
|
+
}).pipe(Effect7.withLogSpan("tablinum.negentropy"));
|
|
1381
1390
|
}
|
|
1382
1391
|
|
|
1383
1392
|
// src/db/key-rotation.ts
|
|
@@ -1463,7 +1472,8 @@ function parseRemovalNotice(content, dTag) {
|
|
|
1463
1472
|
}
|
|
1464
1473
|
|
|
1465
1474
|
// src/sync/sync-service.ts
|
|
1466
|
-
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);
|
|
1467
1477
|
const getSubscriptionPubKeys = () => {
|
|
1468
1478
|
return getAllPublicKeys(epochStore);
|
|
1469
1479
|
};
|
|
@@ -1473,7 +1483,7 @@ function createSyncHandle(storage, giftWrapHandle, relay, publishQueue, syncStat
|
|
|
1473
1483
|
kind: "create"
|
|
1474
1484
|
});
|
|
1475
1485
|
const forkHandled = (effect) => {
|
|
1476
|
-
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)));
|
|
1477
1487
|
};
|
|
1478
1488
|
let autoFlushActive = false;
|
|
1479
1489
|
const autoFlushEffect = Effect8.gen(function* () {
|
|
@@ -1530,6 +1540,7 @@ function createSyncHandle(storage, giftWrapHandle, relay, publishQueue, syncStat
|
|
|
1530
1540
|
if (rumor.pubkey) {
|
|
1531
1541
|
const reject = yield* shouldRejectWrite(rumor.pubkey);
|
|
1532
1542
|
if (reject) {
|
|
1543
|
+
yield* Effect8.logWarning("Rejected write from removed member", { author: rumor.pubkey.slice(0, 12) });
|
|
1533
1544
|
yield* storage.putGiftWrap({ id: remoteGw.id, createdAt: remoteGw.created_at });
|
|
1534
1545
|
return null;
|
|
1535
1546
|
}
|
|
@@ -1572,6 +1583,7 @@ function createSyncHandle(storage, giftWrapHandle, relay, publishQueue, syncStat
|
|
|
1572
1583
|
if (didApply && (kind === "u" || kind === "d")) {
|
|
1573
1584
|
yield* pruneEvents(storage, collectionName, recordId, retention);
|
|
1574
1585
|
}
|
|
1586
|
+
yield* Effect8.logDebug("Processed gift wrap", { collection: collectionName, recordId, kind, author: author?.slice(0, 12) });
|
|
1575
1587
|
if (author && onNewAuthor) {
|
|
1576
1588
|
onNewAuthor(author);
|
|
1577
1589
|
}
|
|
@@ -1640,12 +1652,14 @@ function createSyncHandle(storage, giftWrapHandle, relay, publishQueue, syncStat
|
|
|
1640
1652
|
}).pipe(Effect8.tapError((err) => Effect8.sync(() => onSyncError?.(err))), Effect8.ignore);
|
|
1641
1653
|
}), { discard: true });
|
|
1642
1654
|
const syncRelay = (url, pubKeys, changedCollections) => Effect8.gen(function* () {
|
|
1655
|
+
yield* Effect8.logDebug("Syncing relay", { relay: url });
|
|
1643
1656
|
const reconcileResult = yield* Effect8.result(reconcileWithRelay(storage, relay, url, Array.from(pubKeys)));
|
|
1644
1657
|
if (reconcileResult._tag === "Failure") {
|
|
1645
1658
|
onSyncError?.(reconcileResult.failure);
|
|
1646
1659
|
return;
|
|
1647
1660
|
}
|
|
1648
1661
|
const { haveIds, needIds } = reconcileResult.success;
|
|
1662
|
+
yield* Effect8.logDebug("Relay reconciliation result", { relay: url, need: needIds.length, have: haveIds.length });
|
|
1649
1663
|
if (needIds.length > 0) {
|
|
1650
1664
|
const fetched = yield* relay.fetchEvents(needIds, url).pipe(Effect8.tapError((err) => Effect8.sync(() => onSyncError?.(err))), Effect8.orElseSucceed(() => []));
|
|
1651
1665
|
const sorted = [...fetched].sort((a, b) => a.created_at - b.created_at);
|
|
@@ -1663,9 +1677,10 @@ function createSyncHandle(storage, giftWrapHandle, relay, publishQueue, syncStat
|
|
|
1663
1677
|
yield* relay.publish(gw.event, [url]).pipe(Effect8.andThen(storage.stripGiftWrapBlob(id)), Effect8.tapError((err) => Effect8.sync(() => onSyncError?.(err))), Effect8.ignore);
|
|
1664
1678
|
}), { discard: true });
|
|
1665
1679
|
}
|
|
1666
|
-
});
|
|
1680
|
+
}).pipe(Effect8.withLogSpan("tablinum.syncRelay"));
|
|
1667
1681
|
const handle = {
|
|
1668
1682
|
sync: () => Effect8.gen(function* () {
|
|
1683
|
+
yield* Effect8.logInfo("Sync started");
|
|
1669
1684
|
yield* syncStatus.set("syncing");
|
|
1670
1685
|
yield* Ref3.set(watchCtx.replayingRef, true);
|
|
1671
1686
|
const changedCollections = new Set;
|
|
@@ -1679,7 +1694,8 @@ function createSyncHandle(storage, giftWrapHandle, relay, publishQueue, syncStat
|
|
|
1679
1694
|
yield* notifyReplayComplete(watchCtx, [...changedCollections]);
|
|
1680
1695
|
yield* syncStatus.set("idle");
|
|
1681
1696
|
})));
|
|
1682
|
-
|
|
1697
|
+
yield* Effect8.logInfo("Sync complete", { changed: [...changedCollections] });
|
|
1698
|
+
}).pipe(Effect8.withLogSpan("tablinum.sync")),
|
|
1683
1699
|
publishLocal: (giftWrap) => Effect8.gen(function* () {
|
|
1684
1700
|
if (!giftWrap.event)
|
|
1685
1701
|
return;
|
|
@@ -1805,7 +1821,7 @@ class SyncStatus extends ServiceMap9.Service()("tablinum/SyncStatus") {
|
|
|
1805
1821
|
}
|
|
1806
1822
|
|
|
1807
1823
|
// src/layers/IdentityLive.ts
|
|
1808
|
-
import { Effect as Effect11, Layer } from "effect";
|
|
1824
|
+
import { Effect as Effect11, Layer as Layer2 } from "effect";
|
|
1809
1825
|
import { hexToBytes as hexToBytes3 } from "@noble/hashes/utils.js";
|
|
1810
1826
|
|
|
1811
1827
|
// src/db/identity.ts
|
|
@@ -1843,21 +1859,25 @@ function createIdentity(suppliedKey) {
|
|
|
1843
1859
|
}
|
|
1844
1860
|
|
|
1845
1861
|
// src/layers/IdentityLive.ts
|
|
1846
|
-
var IdentityLive =
|
|
1862
|
+
var IdentityLive = Layer2.effect(Identity, Effect11.gen(function* () {
|
|
1847
1863
|
const config = yield* Config;
|
|
1848
1864
|
const storage = yield* Storage;
|
|
1849
1865
|
const idbKey = yield* storage.getMeta("identity_key");
|
|
1850
1866
|
const resolvedKey = config.privateKey ?? (typeof idbKey === "string" && idbKey.length === 64 ? hexToBytes3(idbKey) : undefined);
|
|
1851
1867
|
const identity = yield* createIdentity(resolvedKey);
|
|
1852
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
|
+
});
|
|
1853
1873
|
return identity;
|
|
1854
1874
|
}));
|
|
1855
1875
|
|
|
1856
1876
|
// src/layers/EpochStoreLive.ts
|
|
1857
|
-
import { Effect as Effect12, Layer as
|
|
1877
|
+
import { Effect as Effect12, Layer as Layer3, Option as Option7 } from "effect";
|
|
1858
1878
|
import { generateSecretKey as generateSecretKey2 } from "nostr-tools/pure";
|
|
1859
1879
|
import { bytesToHex as bytesToHex3 } from "@noble/hashes/utils.js";
|
|
1860
|
-
var EpochStoreLive =
|
|
1880
|
+
var EpochStoreLive = Layer3.effect(EpochStore, Effect12.gen(function* () {
|
|
1861
1881
|
const config = yield* Config;
|
|
1862
1882
|
const identity = yield* Identity;
|
|
1863
1883
|
const storage = yield* Storage;
|
|
@@ -1865,21 +1885,24 @@ var EpochStoreLive = Layer2.effect(EpochStore, Effect12.gen(function* () {
|
|
|
1865
1885
|
if (typeof idbRaw === "string") {
|
|
1866
1886
|
const idbStore = deserializeEpochStore(idbRaw);
|
|
1867
1887
|
if (Option7.isSome(idbStore)) {
|
|
1888
|
+
yield* Effect12.logInfo("Epoch store loaded", { source: "storage", epochs: idbStore.value.epochs.size });
|
|
1868
1889
|
return idbStore.value;
|
|
1869
1890
|
}
|
|
1870
1891
|
}
|
|
1871
1892
|
if (config.epochKeys && config.epochKeys.length > 0) {
|
|
1872
1893
|
const store2 = createEpochStoreFromInputs(config.epochKeys);
|
|
1873
1894
|
yield* storage.putMeta("epochs", stringifyEpochStore(store2));
|
|
1895
|
+
yield* Effect12.logInfo("Epoch store loaded", { source: "config", epochs: store2.epochs.size });
|
|
1874
1896
|
return store2;
|
|
1875
1897
|
}
|
|
1876
1898
|
const store = createEpochStoreFromInputs([{ epochId: EpochId("epoch-0"), key: bytesToHex3(generateSecretKey2()) }], { createdBy: identity.publicKey });
|
|
1877
1899
|
yield* storage.putMeta("epochs", stringifyEpochStore(store));
|
|
1900
|
+
yield* Effect12.logInfo("Epoch store loaded", { source: "generated", epochs: store.epochs.size });
|
|
1878
1901
|
return store;
|
|
1879
1902
|
}));
|
|
1880
1903
|
|
|
1881
1904
|
// src/layers/StorageLive.ts
|
|
1882
|
-
import { Effect as Effect14, Layer as
|
|
1905
|
+
import { Effect as Effect14, Layer as Layer4 } from "effect";
|
|
1883
1906
|
|
|
1884
1907
|
// src/storage/idb.ts
|
|
1885
1908
|
import { Effect as Effect13 } from "effect";
|
|
@@ -2037,16 +2060,18 @@ function openIDBStorage(dbName, schema) {
|
|
|
2037
2060
|
}
|
|
2038
2061
|
|
|
2039
2062
|
// src/layers/StorageLive.ts
|
|
2040
|
-
var StorageLive =
|
|
2063
|
+
var StorageLive = Layer4.effect(Storage, Effect14.gen(function* () {
|
|
2041
2064
|
const config = yield* Config;
|
|
2042
|
-
|
|
2065
|
+
const handle = yield* openIDBStorage(config.dbName, {
|
|
2043
2066
|
...config.schema,
|
|
2044
2067
|
_members: membersCollectionDef
|
|
2045
2068
|
});
|
|
2069
|
+
yield* Effect14.logInfo("Storage opened", { dbName: config.dbName });
|
|
2070
|
+
return handle;
|
|
2046
2071
|
}));
|
|
2047
2072
|
|
|
2048
2073
|
// src/layers/RelayLive.ts
|
|
2049
|
-
import { Layer as
|
|
2074
|
+
import { Layer as Layer5 } from "effect";
|
|
2050
2075
|
|
|
2051
2076
|
// src/sync/relay.ts
|
|
2052
2077
|
import { Effect as Effect15, Option as Option8, Schema as Schema6, ScopedCache, Scope as Scope3 } from "effect";
|
|
@@ -2259,10 +2284,10 @@ function createRelayHandle() {
|
|
|
2259
2284
|
}
|
|
2260
2285
|
|
|
2261
2286
|
// src/layers/RelayLive.ts
|
|
2262
|
-
var RelayLive =
|
|
2287
|
+
var RelayLive = Layer5.effect(Relay, createRelayHandle());
|
|
2263
2288
|
|
|
2264
2289
|
// src/layers/GiftWrapLive.ts
|
|
2265
|
-
import { Effect as Effect17, Layer as
|
|
2290
|
+
import { Effect as Effect17, Layer as Layer6 } from "effect";
|
|
2266
2291
|
|
|
2267
2292
|
// src/sync/gift-wrap.ts
|
|
2268
2293
|
import { Effect as Effect16 } from "effect";
|
|
@@ -2299,14 +2324,14 @@ function createEpochGiftWrapHandle(senderPrivateKey, epochStore) {
|
|
|
2299
2324
|
}
|
|
2300
2325
|
|
|
2301
2326
|
// src/layers/GiftWrapLive.ts
|
|
2302
|
-
var GiftWrapLive =
|
|
2327
|
+
var GiftWrapLive = Layer6.effect(GiftWrap3, Effect17.gen(function* () {
|
|
2303
2328
|
const identity = yield* Identity;
|
|
2304
2329
|
const epochStore = yield* EpochStore;
|
|
2305
2330
|
return createEpochGiftWrapHandle(identity.privateKey, epochStore);
|
|
2306
2331
|
}));
|
|
2307
2332
|
|
|
2308
2333
|
// src/layers/PublishQueueLive.ts
|
|
2309
|
-
import { Effect as Effect19, Layer as
|
|
2334
|
+
import { Effect as Effect19, Layer as Layer7 } from "effect";
|
|
2310
2335
|
|
|
2311
2336
|
// src/sync/publish-queue.ts
|
|
2312
2337
|
import { Effect as Effect18, Ref as Ref4 } from "effect";
|
|
@@ -2380,14 +2405,14 @@ function createPublishQueue(storage, relay) {
|
|
|
2380
2405
|
}
|
|
2381
2406
|
|
|
2382
2407
|
// src/layers/PublishQueueLive.ts
|
|
2383
|
-
var PublishQueueLive =
|
|
2408
|
+
var PublishQueueLive = Layer7.effect(PublishQueue, Effect19.gen(function* () {
|
|
2384
2409
|
const storage = yield* Storage;
|
|
2385
2410
|
const relay = yield* Relay;
|
|
2386
2411
|
return yield* createPublishQueue(storage, relay);
|
|
2387
2412
|
}));
|
|
2388
2413
|
|
|
2389
2414
|
// src/layers/SyncStatusLive.ts
|
|
2390
|
-
import { Layer as
|
|
2415
|
+
import { Layer as Layer8 } from "effect";
|
|
2391
2416
|
|
|
2392
2417
|
// src/sync/sync-status.ts
|
|
2393
2418
|
import { Effect as Effect20, SubscriptionRef } from "effect";
|
|
@@ -2411,7 +2436,7 @@ function createSyncStatusHandle() {
|
|
|
2411
2436
|
}
|
|
2412
2437
|
|
|
2413
2438
|
// src/layers/SyncStatusLive.ts
|
|
2414
|
-
var SyncStatusLive =
|
|
2439
|
+
var SyncStatusLive = Layer8.effect(SyncStatus, createSyncStatusHandle());
|
|
2415
2440
|
|
|
2416
2441
|
// src/layers/TablinumLive.ts
|
|
2417
2442
|
function reportSyncError(onSyncError, error) {
|
|
@@ -2432,12 +2457,12 @@ function mapMemberRecord(record) {
|
|
|
2432
2457
|
...record.removedInEpoch !== undefined ? { removedInEpoch: record.removedInEpoch } : {}
|
|
2433
2458
|
};
|
|
2434
2459
|
}
|
|
2435
|
-
var IdentityWithDeps = IdentityLive.pipe(
|
|
2436
|
-
var EpochStoreWithDeps = EpochStoreLive.pipe(
|
|
2437
|
-
var GiftWrapWithDeps = GiftWrapLive.pipe(
|
|
2438
|
-
var PublishQueueWithDeps = PublishQueueLive.pipe(
|
|
2439
|
-
var AllServicesLive =
|
|
2440
|
-
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* () {
|
|
2441
2466
|
const config = yield* Config;
|
|
2442
2467
|
const identity = yield* Identity;
|
|
2443
2468
|
const epochStore = yield* EpochStore;
|
|
@@ -2447,6 +2472,7 @@ var TablinumLive = Layer8.effect(Tablinum, Effect21.gen(function* () {
|
|
|
2447
2472
|
const publishQueue = yield* PublishQueue;
|
|
2448
2473
|
const syncStatus = yield* SyncStatus;
|
|
2449
2474
|
const scope = yield* Effect21.scope;
|
|
2475
|
+
const logLayer = Layer9.succeed(References3.MinimumLogLevel, config.logLevel);
|
|
2450
2476
|
const pubsub = yield* PubSub2.unbounded();
|
|
2451
2477
|
const replayingRef = yield* Ref5.make(false);
|
|
2452
2478
|
const closedRef = yield* Ref5.make(false);
|
|
@@ -2455,7 +2481,7 @@ var TablinumLive = Layer8.effect(Tablinum, Effect21.gen(function* () {
|
|
|
2455
2481
|
const allSchemaEntries = [...schemaEntries, ["_members", membersCollectionDef]];
|
|
2456
2482
|
const knownCollections = new Map(allSchemaEntries.map(([, def]) => [def.name, def.eventRetention]));
|
|
2457
2483
|
let notifyAuthor;
|
|
2458
|
-
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);
|
|
2459
2485
|
const onWrite = (event) => Effect21.gen(function* () {
|
|
2460
2486
|
const content = event.kind === "d" ? JSON.stringify(null) : JSON.stringify(event.data);
|
|
2461
2487
|
const dTag = `${event.collection}:${event.recordId}`;
|
|
@@ -2533,16 +2559,21 @@ var TablinumLive = Layer8.effect(Tablinum, Effect21.gen(function* () {
|
|
|
2533
2559
|
config.onMembersChanged?.();
|
|
2534
2560
|
}
|
|
2535
2561
|
}
|
|
2536
|
-
}).pipe(Effect21.ignore, Effect21.forkIn(scope)));
|
|
2562
|
+
}).pipe(Effect21.ignore, Effect21.provide(logLayer), Effect21.forkIn(scope)));
|
|
2537
2563
|
};
|
|
2538
2564
|
const handles = new Map;
|
|
2539
2565
|
for (const [, def] of allSchemaEntries) {
|
|
2540
2566
|
const validator = buildValidator(def.name, def);
|
|
2541
2567
|
const partialValidator = buildPartialValidator(def.name, def);
|
|
2542
|
-
const handle = createCollectionHandle(def, storage, watchCtx, validator, partialValidator, uuidv7, identity.publicKey, onWrite);
|
|
2568
|
+
const handle = createCollectionHandle(def, storage, watchCtx, validator, partialValidator, uuidv7, identity.publicKey, onWrite, config.logLevel);
|
|
2543
2569
|
handles.set(def.name, handle);
|
|
2544
2570
|
}
|
|
2545
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
|
+
});
|
|
2546
2577
|
const selfMember = yield* storage.getRecord("_members", identity.publicKey);
|
|
2547
2578
|
if (!selfMember) {
|
|
2548
2579
|
yield* putMemberRecord({
|
|
@@ -2551,18 +2582,19 @@ var TablinumLive = Layer8.effect(Tablinum, Effect21.gen(function* () {
|
|
|
2551
2582
|
addedInEpoch: getCurrentEpoch(epochStore).id
|
|
2552
2583
|
});
|
|
2553
2584
|
}
|
|
2554
|
-
const
|
|
2585
|
+
const withLog = (effect) => Effect21.provideService(effect, References3.MinimumLogLevel, config.logLevel);
|
|
2586
|
+
const ensureOpen = (effect) => withLog(Effect21.gen(function* () {
|
|
2555
2587
|
if (yield* Ref5.get(closedRef)) {
|
|
2556
2588
|
return yield* new StorageError({ message: "Database is closed" });
|
|
2557
2589
|
}
|
|
2558
2590
|
return yield* effect;
|
|
2559
|
-
});
|
|
2560
|
-
const ensureSyncOpen = (effect) => Effect21.gen(function* () {
|
|
2591
|
+
}));
|
|
2592
|
+
const ensureSyncOpen = (effect) => withLog(Effect21.gen(function* () {
|
|
2561
2593
|
if (yield* Ref5.get(closedRef)) {
|
|
2562
2594
|
return yield* new SyncError({ message: "Database is closed", phase: "init" });
|
|
2563
2595
|
}
|
|
2564
2596
|
return yield* effect;
|
|
2565
|
-
});
|
|
2597
|
+
}));
|
|
2566
2598
|
const dbHandle = {
|
|
2567
2599
|
collection: (name) => {
|
|
2568
2600
|
const handle = handles.get(name);
|
|
@@ -2578,12 +2610,12 @@ var TablinumLive = Layer8.effect(Tablinum, Effect21.gen(function* () {
|
|
|
2578
2610
|
relays: [...config.relays],
|
|
2579
2611
|
dbName: config.dbName
|
|
2580
2612
|
}),
|
|
2581
|
-
close: () => Effect21.gen(function* () {
|
|
2613
|
+
close: () => withLog(Effect21.gen(function* () {
|
|
2582
2614
|
if (yield* Ref5.get(closedRef))
|
|
2583
2615
|
return;
|
|
2584
2616
|
yield* Ref5.set(closedRef, true);
|
|
2585
2617
|
yield* Scope4.close(scope, Exit.void);
|
|
2586
|
-
}),
|
|
2618
|
+
})),
|
|
2587
2619
|
rebuild: () => ensureOpen(rebuild(storage, allSchemaEntries.map(([, def]) => def.name))),
|
|
2588
2620
|
sync: () => ensureSyncOpen(syncHandle.sync()),
|
|
2589
2621
|
getSyncStatus: () => syncStatus.get(),
|
|
@@ -2654,9 +2686,25 @@ var TablinumLive = Layer8.effect(Tablinum, Effect21.gen(function* () {
|
|
|
2654
2686
|
}))
|
|
2655
2687
|
};
|
|
2656
2688
|
return dbHandle;
|
|
2657
|
-
})).pipe(
|
|
2689
|
+
}).pipe(Effect21.withLogSpan("tablinum.init"))).pipe(Layer9.provide(AllServicesLive));
|
|
2658
2690
|
|
|
2659
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
|
+
}
|
|
2660
2708
|
function validateConfig(config) {
|
|
2661
2709
|
return Effect22.gen(function* () {
|
|
2662
2710
|
if (Object.keys(config.schema).length === 0) {
|
|
@@ -2670,19 +2718,26 @@ function createTablinum(config) {
|
|
|
2670
2718
|
return Effect22.gen(function* () {
|
|
2671
2719
|
yield* validateConfig(config);
|
|
2672
2720
|
const runtimeConfig = yield* resolveRuntimeConfig(config);
|
|
2721
|
+
const logLevel = resolveLogLevel(config.logLevel);
|
|
2673
2722
|
const configValue = {
|
|
2674
2723
|
...runtimeConfig,
|
|
2675
2724
|
schema: config.schema,
|
|
2725
|
+
logLevel,
|
|
2676
2726
|
onSyncError: config.onSyncError,
|
|
2677
2727
|
onRemoved: config.onRemoved,
|
|
2678
2728
|
onMembersChanged: config.onMembersChanged
|
|
2679
2729
|
};
|
|
2680
|
-
const configLayer =
|
|
2681
|
-
const
|
|
2682
|
-
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);
|
|
2683
2734
|
return ServiceMap10.get(ctx, Tablinum);
|
|
2684
2735
|
});
|
|
2685
2736
|
}
|
|
2737
|
+
|
|
2738
|
+
// src/index.ts
|
|
2739
|
+
import { LogLevel } from "effect";
|
|
2740
|
+
|
|
2686
2741
|
// src/db/invite.ts
|
|
2687
2742
|
import { Schema as Schema7 } from "effect";
|
|
2688
2743
|
var InviteSchema = Schema7.Struct({
|
|
@@ -2728,6 +2783,7 @@ export {
|
|
|
2728
2783
|
StorageError,
|
|
2729
2784
|
RelayError,
|
|
2730
2785
|
NotFoundError,
|
|
2786
|
+
LogLevel,
|
|
2731
2787
|
EpochId,
|
|
2732
2788
|
DatabaseName,
|
|
2733
2789
|
CryptoError,
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { ServiceMap } from "effect";
|
|
2
|
+
import type { LogLevel } from "effect";
|
|
2
3
|
import type { ResolvedRuntimeConfig } from "../db/runtime-config.ts";
|
|
3
4
|
import type { SchemaConfig } from "../schema/types.ts";
|
|
4
5
|
export interface TablinumConfigShape extends ResolvedRuntimeConfig {
|
|
5
6
|
readonly schema: SchemaConfig;
|
|
7
|
+
readonly logLevel: LogLevel.LogLevel;
|
|
6
8
|
readonly onSyncError?: ((error: Error) => void) | undefined;
|
|
7
9
|
readonly onRemoved?: ((info: {
|
|
8
10
|
epochId: string;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { LogLevel } from "effect";
|
|
1
2
|
import type { CollectionDef, CollectionFields } from "../schema/collection.ts";
|
|
2
3
|
import type { InferRecord } from "../schema/types.ts";
|
|
3
4
|
import type { CollectionHandle } from "../crud/collection-handle.ts";
|
|
@@ -6,7 +7,7 @@ export declare class Collection<C extends CollectionDef<CollectionFields>> {
|
|
|
6
7
|
#private;
|
|
7
8
|
error: Error | null;
|
|
8
9
|
/** @internal */
|
|
9
|
-
_bind(handle: CollectionHandle<C
|
|
10
|
+
_bind(handle: CollectionHandle<C>, logLevel?: LogLevel.LogLevel): void;
|
|
10
11
|
/** @internal */
|
|
11
12
|
_fail(err: Error): void;
|
|
12
13
|
add: (data: Omit<InferRecord<C>, "id">) => Promise<string>;
|
|
@@ -217,10 +217,10 @@ class NotFoundError extends Data.TaggedError("NotFoundError") {
|
|
|
217
217
|
class ClosedError extends Data.TaggedError("ClosedError") {
|
|
218
218
|
}
|
|
219
219
|
// src/svelte/tablinum.svelte.ts
|
|
220
|
-
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";
|
|
221
221
|
|
|
222
222
|
// src/db/create-tablinum.ts
|
|
223
|
-
import { Effect as Effect22, Layer as
|
|
223
|
+
import { Effect as Effect22, Layer as Layer10, References as References4, ServiceMap as ServiceMap10 } from "effect";
|
|
224
224
|
|
|
225
225
|
// src/db/runtime-config.ts
|
|
226
226
|
import { Effect, Schema as Schema3 } from "effect";
|
|
@@ -255,7 +255,7 @@ class Tablinum extends ServiceMap2.Service()("tablinum/Tablinum") {
|
|
|
255
255
|
}
|
|
256
256
|
|
|
257
257
|
// src/layers/TablinumLive.ts
|
|
258
|
-
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";
|
|
259
259
|
|
|
260
260
|
// src/crud/watch.ts
|
|
261
261
|
import { Effect as Effect2, PubSub, Ref, Stream } from "effect";
|
|
@@ -469,7 +469,7 @@ function buildPartialValidator(collectionName, def) {
|
|
|
469
469
|
}
|
|
470
470
|
|
|
471
471
|
// src/crud/collection-handle.ts
|
|
472
|
-
import { Effect as Effect6, Option as Option3 } from "effect";
|
|
472
|
+
import { Effect as Effect6, Option as Option3, References } from "effect";
|
|
473
473
|
|
|
474
474
|
// src/utils/uuid.ts
|
|
475
475
|
var alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
|
|
@@ -717,8 +717,9 @@ function mapRecord(record) {
|
|
|
717
717
|
const { _d, _u, _a, _e, ...fields } = record;
|
|
718
718
|
return fields;
|
|
719
719
|
}
|
|
720
|
-
function createCollectionHandle(def, storage, watchCtx, validator, partialValidator, makeEventId, localAuthor, onWrite) {
|
|
720
|
+
function createCollectionHandle(def, storage, watchCtx, validator, partialValidator, makeEventId, localAuthor, onWrite, logLevel = "None") {
|
|
721
721
|
const collectionName = def.name;
|
|
722
|
+
const withLog = (effect) => Effect6.provideService(effect, References.MinimumLogLevel, logLevel);
|
|
722
723
|
const commitEvent = (event) => Effect6.gen(function* () {
|
|
723
724
|
yield* storage.putEvent(event);
|
|
724
725
|
yield* applyEvent(storage, event);
|
|
@@ -731,7 +732,7 @@ function createCollectionHandle(def, storage, watchCtx, validator, partialValida
|
|
|
731
732
|
});
|
|
732
733
|
});
|
|
733
734
|
const handle = {
|
|
734
|
-
add: (data) => Effect6.gen(function* () {
|
|
735
|
+
add: (data) => withLog(Effect6.gen(function* () {
|
|
735
736
|
const id = uuidv7();
|
|
736
737
|
const fullRecord = { id, ...data };
|
|
737
738
|
yield* validator(fullRecord);
|
|
@@ -745,9 +746,10 @@ function createCollectionHandle(def, storage, watchCtx, validator, partialValida
|
|
|
745
746
|
author: localAuthor
|
|
746
747
|
};
|
|
747
748
|
yield* commitEvent(event);
|
|
749
|
+
yield* Effect6.logDebug("Record added", { collection: collectionName, recordId: id, data: fullRecord });
|
|
748
750
|
return id;
|
|
749
|
-
}),
|
|
750
|
-
update: (id, data) => Effect6.gen(function* () {
|
|
751
|
+
})),
|
|
752
|
+
update: (id, data) => withLog(Effect6.gen(function* () {
|
|
751
753
|
const existing = yield* storage.getRecord(collectionName, id);
|
|
752
754
|
if (!existing || existing._d) {
|
|
753
755
|
return yield* new NotFoundError({
|
|
@@ -770,9 +772,10 @@ function createCollectionHandle(def, storage, watchCtx, validator, partialValida
|
|
|
770
772
|
author: localAuthor
|
|
771
773
|
};
|
|
772
774
|
yield* commitEvent(event);
|
|
775
|
+
yield* Effect6.logDebug("Record updated", { collection: collectionName, recordId: id, data: diff });
|
|
773
776
|
yield* pruneEvents(storage, collectionName, id, def.eventRetention);
|
|
774
|
-
}),
|
|
775
|
-
delete: (id) => Effect6.gen(function* () {
|
|
777
|
+
})),
|
|
778
|
+
delete: (id) => withLog(Effect6.gen(function* () {
|
|
776
779
|
const existing = yield* storage.getRecord(collectionName, id);
|
|
777
780
|
if (!existing || existing._d) {
|
|
778
781
|
return yield* new NotFoundError({
|
|
@@ -790,8 +793,9 @@ function createCollectionHandle(def, storage, watchCtx, validator, partialValida
|
|
|
790
793
|
author: localAuthor
|
|
791
794
|
};
|
|
792
795
|
yield* commitEvent(event);
|
|
796
|
+
yield* Effect6.logDebug("Record deleted", { collection: collectionName, recordId: id });
|
|
793
797
|
yield* pruneEvents(storage, collectionName, id, def.eventRetention);
|
|
794
|
-
}),
|
|
798
|
+
})),
|
|
795
799
|
undo: (id) => Effect6.gen(function* () {
|
|
796
800
|
const existing = yield* storage.getRecord(collectionName, id);
|
|
797
801
|
if (!existing) {
|
|
@@ -840,7 +844,7 @@ function createCollectionHandle(def, storage, watchCtx, validator, partialValida
|
|
|
840
844
|
}
|
|
841
845
|
|
|
842
846
|
// src/sync/sync-service.ts
|
|
843
|
-
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";
|
|
844
848
|
import { unwrapEvent } from "nostr-tools/nip59";
|
|
845
849
|
import { GiftWrap as GiftWrap2 } from "nostr-tools/kinds";
|
|
846
850
|
|
|
@@ -1411,8 +1415,13 @@ function reconcileWithRelay(storage, relay, relayUrl, publicKeys) {
|
|
|
1411
1415
|
allNeedIds.push(id);
|
|
1412
1416
|
currentMsg = nextMsg;
|
|
1413
1417
|
}
|
|
1418
|
+
yield* Effect7.logDebug("Negentropy reconciliation complete", {
|
|
1419
|
+
relay: relayUrl,
|
|
1420
|
+
have: allHaveIds.length,
|
|
1421
|
+
need: allNeedIds.length
|
|
1422
|
+
});
|
|
1414
1423
|
return { haveIds: allHaveIds, needIds: allNeedIds };
|
|
1415
|
-
});
|
|
1424
|
+
}).pipe(Effect7.withLogSpan("tablinum.negentropy"));
|
|
1416
1425
|
}
|
|
1417
1426
|
|
|
1418
1427
|
// src/db/key-rotation.ts
|
|
@@ -1498,7 +1507,8 @@ function parseRemovalNotice(content, dTag) {
|
|
|
1498
1507
|
}
|
|
1499
1508
|
|
|
1500
1509
|
// src/sync/sync-service.ts
|
|
1501
|
-
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);
|
|
1502
1512
|
const getSubscriptionPubKeys = () => {
|
|
1503
1513
|
return getAllPublicKeys(epochStore);
|
|
1504
1514
|
};
|
|
@@ -1508,7 +1518,7 @@ function createSyncHandle(storage, giftWrapHandle, relay, publishQueue, syncStat
|
|
|
1508
1518
|
kind: "create"
|
|
1509
1519
|
});
|
|
1510
1520
|
const forkHandled = (effect) => {
|
|
1511
|
-
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)));
|
|
1512
1522
|
};
|
|
1513
1523
|
let autoFlushActive = false;
|
|
1514
1524
|
const autoFlushEffect = Effect8.gen(function* () {
|
|
@@ -1565,6 +1575,7 @@ function createSyncHandle(storage, giftWrapHandle, relay, publishQueue, syncStat
|
|
|
1565
1575
|
if (rumor.pubkey) {
|
|
1566
1576
|
const reject = yield* shouldRejectWrite(rumor.pubkey);
|
|
1567
1577
|
if (reject) {
|
|
1578
|
+
yield* Effect8.logWarning("Rejected write from removed member", { author: rumor.pubkey.slice(0, 12) });
|
|
1568
1579
|
yield* storage.putGiftWrap({ id: remoteGw.id, createdAt: remoteGw.created_at });
|
|
1569
1580
|
return null;
|
|
1570
1581
|
}
|
|
@@ -1607,6 +1618,7 @@ function createSyncHandle(storage, giftWrapHandle, relay, publishQueue, syncStat
|
|
|
1607
1618
|
if (didApply && (kind === "u" || kind === "d")) {
|
|
1608
1619
|
yield* pruneEvents(storage, collectionName, recordId, retention);
|
|
1609
1620
|
}
|
|
1621
|
+
yield* Effect8.logDebug("Processed gift wrap", { collection: collectionName, recordId, kind, author: author?.slice(0, 12) });
|
|
1610
1622
|
if (author && onNewAuthor) {
|
|
1611
1623
|
onNewAuthor(author);
|
|
1612
1624
|
}
|
|
@@ -1675,12 +1687,14 @@ function createSyncHandle(storage, giftWrapHandle, relay, publishQueue, syncStat
|
|
|
1675
1687
|
}).pipe(Effect8.tapError((err) => Effect8.sync(() => onSyncError?.(err))), Effect8.ignore);
|
|
1676
1688
|
}), { discard: true });
|
|
1677
1689
|
const syncRelay = (url, pubKeys, changedCollections) => Effect8.gen(function* () {
|
|
1690
|
+
yield* Effect8.logDebug("Syncing relay", { relay: url });
|
|
1678
1691
|
const reconcileResult = yield* Effect8.result(reconcileWithRelay(storage, relay, url, Array.from(pubKeys)));
|
|
1679
1692
|
if (reconcileResult._tag === "Failure") {
|
|
1680
1693
|
onSyncError?.(reconcileResult.failure);
|
|
1681
1694
|
return;
|
|
1682
1695
|
}
|
|
1683
1696
|
const { haveIds, needIds } = reconcileResult.success;
|
|
1697
|
+
yield* Effect8.logDebug("Relay reconciliation result", { relay: url, need: needIds.length, have: haveIds.length });
|
|
1684
1698
|
if (needIds.length > 0) {
|
|
1685
1699
|
const fetched = yield* relay.fetchEvents(needIds, url).pipe(Effect8.tapError((err) => Effect8.sync(() => onSyncError?.(err))), Effect8.orElseSucceed(() => []));
|
|
1686
1700
|
const sorted = [...fetched].sort((a, b) => a.created_at - b.created_at);
|
|
@@ -1698,9 +1712,10 @@ function createSyncHandle(storage, giftWrapHandle, relay, publishQueue, syncStat
|
|
|
1698
1712
|
yield* relay.publish(gw.event, [url]).pipe(Effect8.andThen(storage.stripGiftWrapBlob(id)), Effect8.tapError((err) => Effect8.sync(() => onSyncError?.(err))), Effect8.ignore);
|
|
1699
1713
|
}), { discard: true });
|
|
1700
1714
|
}
|
|
1701
|
-
});
|
|
1715
|
+
}).pipe(Effect8.withLogSpan("tablinum.syncRelay"));
|
|
1702
1716
|
const handle = {
|
|
1703
1717
|
sync: () => Effect8.gen(function* () {
|
|
1718
|
+
yield* Effect8.logInfo("Sync started");
|
|
1704
1719
|
yield* syncStatus.set("syncing");
|
|
1705
1720
|
yield* Ref3.set(watchCtx.replayingRef, true);
|
|
1706
1721
|
const changedCollections = new Set;
|
|
@@ -1714,7 +1729,8 @@ function createSyncHandle(storage, giftWrapHandle, relay, publishQueue, syncStat
|
|
|
1714
1729
|
yield* notifyReplayComplete(watchCtx, [...changedCollections]);
|
|
1715
1730
|
yield* syncStatus.set("idle");
|
|
1716
1731
|
})));
|
|
1717
|
-
|
|
1732
|
+
yield* Effect8.logInfo("Sync complete", { changed: [...changedCollections] });
|
|
1733
|
+
}).pipe(Effect8.withLogSpan("tablinum.sync")),
|
|
1718
1734
|
publishLocal: (giftWrap) => Effect8.gen(function* () {
|
|
1719
1735
|
if (!giftWrap.event)
|
|
1720
1736
|
return;
|
|
@@ -1840,7 +1856,7 @@ class SyncStatus extends ServiceMap9.Service()("tablinum/SyncStatus") {
|
|
|
1840
1856
|
}
|
|
1841
1857
|
|
|
1842
1858
|
// src/layers/IdentityLive.ts
|
|
1843
|
-
import { Effect as Effect11, Layer } from "effect";
|
|
1859
|
+
import { Effect as Effect11, Layer as Layer2 } from "effect";
|
|
1844
1860
|
import { hexToBytes as hexToBytes3 } from "@noble/hashes/utils.js";
|
|
1845
1861
|
|
|
1846
1862
|
// src/db/identity.ts
|
|
@@ -1878,21 +1894,25 @@ function createIdentity(suppliedKey) {
|
|
|
1878
1894
|
}
|
|
1879
1895
|
|
|
1880
1896
|
// src/layers/IdentityLive.ts
|
|
1881
|
-
var IdentityLive =
|
|
1897
|
+
var IdentityLive = Layer2.effect(Identity, Effect11.gen(function* () {
|
|
1882
1898
|
const config = yield* Config;
|
|
1883
1899
|
const storage = yield* Storage;
|
|
1884
1900
|
const idbKey = yield* storage.getMeta("identity_key");
|
|
1885
1901
|
const resolvedKey = config.privateKey ?? (typeof idbKey === "string" && idbKey.length === 64 ? hexToBytes3(idbKey) : undefined);
|
|
1886
1902
|
const identity = yield* createIdentity(resolvedKey);
|
|
1887
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
|
+
});
|
|
1888
1908
|
return identity;
|
|
1889
1909
|
}));
|
|
1890
1910
|
|
|
1891
1911
|
// src/layers/EpochStoreLive.ts
|
|
1892
|
-
import { Effect as Effect12, Layer as
|
|
1912
|
+
import { Effect as Effect12, Layer as Layer3, Option as Option7 } from "effect";
|
|
1893
1913
|
import { generateSecretKey as generateSecretKey2 } from "nostr-tools/pure";
|
|
1894
1914
|
import { bytesToHex as bytesToHex3 } from "@noble/hashes/utils.js";
|
|
1895
|
-
var EpochStoreLive =
|
|
1915
|
+
var EpochStoreLive = Layer3.effect(EpochStore, Effect12.gen(function* () {
|
|
1896
1916
|
const config = yield* Config;
|
|
1897
1917
|
const identity = yield* Identity;
|
|
1898
1918
|
const storage = yield* Storage;
|
|
@@ -1900,21 +1920,24 @@ var EpochStoreLive = Layer2.effect(EpochStore, Effect12.gen(function* () {
|
|
|
1900
1920
|
if (typeof idbRaw === "string") {
|
|
1901
1921
|
const idbStore = deserializeEpochStore(idbRaw);
|
|
1902
1922
|
if (Option7.isSome(idbStore)) {
|
|
1923
|
+
yield* Effect12.logInfo("Epoch store loaded", { source: "storage", epochs: idbStore.value.epochs.size });
|
|
1903
1924
|
return idbStore.value;
|
|
1904
1925
|
}
|
|
1905
1926
|
}
|
|
1906
1927
|
if (config.epochKeys && config.epochKeys.length > 0) {
|
|
1907
1928
|
const store2 = createEpochStoreFromInputs(config.epochKeys);
|
|
1908
1929
|
yield* storage.putMeta("epochs", stringifyEpochStore(store2));
|
|
1930
|
+
yield* Effect12.logInfo("Epoch store loaded", { source: "config", epochs: store2.epochs.size });
|
|
1909
1931
|
return store2;
|
|
1910
1932
|
}
|
|
1911
1933
|
const store = createEpochStoreFromInputs([{ epochId: EpochId("epoch-0"), key: bytesToHex3(generateSecretKey2()) }], { createdBy: identity.publicKey });
|
|
1912
1934
|
yield* storage.putMeta("epochs", stringifyEpochStore(store));
|
|
1935
|
+
yield* Effect12.logInfo("Epoch store loaded", { source: "generated", epochs: store.epochs.size });
|
|
1913
1936
|
return store;
|
|
1914
1937
|
}));
|
|
1915
1938
|
|
|
1916
1939
|
// src/layers/StorageLive.ts
|
|
1917
|
-
import { Effect as Effect14, Layer as
|
|
1940
|
+
import { Effect as Effect14, Layer as Layer4 } from "effect";
|
|
1918
1941
|
|
|
1919
1942
|
// src/storage/idb.ts
|
|
1920
1943
|
import { Effect as Effect13 } from "effect";
|
|
@@ -2072,16 +2095,18 @@ function openIDBStorage(dbName, schema) {
|
|
|
2072
2095
|
}
|
|
2073
2096
|
|
|
2074
2097
|
// src/layers/StorageLive.ts
|
|
2075
|
-
var StorageLive =
|
|
2098
|
+
var StorageLive = Layer4.effect(Storage, Effect14.gen(function* () {
|
|
2076
2099
|
const config = yield* Config;
|
|
2077
|
-
|
|
2100
|
+
const handle = yield* openIDBStorage(config.dbName, {
|
|
2078
2101
|
...config.schema,
|
|
2079
2102
|
_members: membersCollectionDef
|
|
2080
2103
|
});
|
|
2104
|
+
yield* Effect14.logInfo("Storage opened", { dbName: config.dbName });
|
|
2105
|
+
return handle;
|
|
2081
2106
|
}));
|
|
2082
2107
|
|
|
2083
2108
|
// src/layers/RelayLive.ts
|
|
2084
|
-
import { Layer as
|
|
2109
|
+
import { Layer as Layer5 } from "effect";
|
|
2085
2110
|
|
|
2086
2111
|
// src/sync/relay.ts
|
|
2087
2112
|
import { Effect as Effect15, Option as Option8, Schema as Schema7, ScopedCache, Scope as Scope3 } from "effect";
|
|
@@ -2294,10 +2319,10 @@ function createRelayHandle() {
|
|
|
2294
2319
|
}
|
|
2295
2320
|
|
|
2296
2321
|
// src/layers/RelayLive.ts
|
|
2297
|
-
var RelayLive =
|
|
2322
|
+
var RelayLive = Layer5.effect(Relay, createRelayHandle());
|
|
2298
2323
|
|
|
2299
2324
|
// src/layers/GiftWrapLive.ts
|
|
2300
|
-
import { Effect as Effect17, Layer as
|
|
2325
|
+
import { Effect as Effect17, Layer as Layer6 } from "effect";
|
|
2301
2326
|
|
|
2302
2327
|
// src/sync/gift-wrap.ts
|
|
2303
2328
|
import { Effect as Effect16 } from "effect";
|
|
@@ -2334,14 +2359,14 @@ function createEpochGiftWrapHandle(senderPrivateKey, epochStore) {
|
|
|
2334
2359
|
}
|
|
2335
2360
|
|
|
2336
2361
|
// src/layers/GiftWrapLive.ts
|
|
2337
|
-
var GiftWrapLive =
|
|
2362
|
+
var GiftWrapLive = Layer6.effect(GiftWrap3, Effect17.gen(function* () {
|
|
2338
2363
|
const identity = yield* Identity;
|
|
2339
2364
|
const epochStore = yield* EpochStore;
|
|
2340
2365
|
return createEpochGiftWrapHandle(identity.privateKey, epochStore);
|
|
2341
2366
|
}));
|
|
2342
2367
|
|
|
2343
2368
|
// src/layers/PublishQueueLive.ts
|
|
2344
|
-
import { Effect as Effect19, Layer as
|
|
2369
|
+
import { Effect as Effect19, Layer as Layer7 } from "effect";
|
|
2345
2370
|
|
|
2346
2371
|
// src/sync/publish-queue.ts
|
|
2347
2372
|
import { Effect as Effect18, Ref as Ref4 } from "effect";
|
|
@@ -2415,14 +2440,14 @@ function createPublishQueue(storage, relay) {
|
|
|
2415
2440
|
}
|
|
2416
2441
|
|
|
2417
2442
|
// src/layers/PublishQueueLive.ts
|
|
2418
|
-
var PublishQueueLive =
|
|
2443
|
+
var PublishQueueLive = Layer7.effect(PublishQueue, Effect19.gen(function* () {
|
|
2419
2444
|
const storage = yield* Storage;
|
|
2420
2445
|
const relay = yield* Relay;
|
|
2421
2446
|
return yield* createPublishQueue(storage, relay);
|
|
2422
2447
|
}));
|
|
2423
2448
|
|
|
2424
2449
|
// src/layers/SyncStatusLive.ts
|
|
2425
|
-
import { Layer as
|
|
2450
|
+
import { Layer as Layer8 } from "effect";
|
|
2426
2451
|
|
|
2427
2452
|
// src/sync/sync-status.ts
|
|
2428
2453
|
import { Effect as Effect20, SubscriptionRef } from "effect";
|
|
@@ -2446,7 +2471,7 @@ function createSyncStatusHandle() {
|
|
|
2446
2471
|
}
|
|
2447
2472
|
|
|
2448
2473
|
// src/layers/SyncStatusLive.ts
|
|
2449
|
-
var SyncStatusLive =
|
|
2474
|
+
var SyncStatusLive = Layer8.effect(SyncStatus, createSyncStatusHandle());
|
|
2450
2475
|
|
|
2451
2476
|
// src/layers/TablinumLive.ts
|
|
2452
2477
|
function reportSyncError(onSyncError, error) {
|
|
@@ -2467,12 +2492,12 @@ function mapMemberRecord(record) {
|
|
|
2467
2492
|
...record.removedInEpoch !== undefined ? { removedInEpoch: record.removedInEpoch } : {}
|
|
2468
2493
|
};
|
|
2469
2494
|
}
|
|
2470
|
-
var IdentityWithDeps = IdentityLive.pipe(
|
|
2471
|
-
var EpochStoreWithDeps = EpochStoreLive.pipe(
|
|
2472
|
-
var GiftWrapWithDeps = GiftWrapLive.pipe(
|
|
2473
|
-
var PublishQueueWithDeps = PublishQueueLive.pipe(
|
|
2474
|
-
var AllServicesLive =
|
|
2475
|
-
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* () {
|
|
2476
2501
|
const config = yield* Config;
|
|
2477
2502
|
const identity = yield* Identity;
|
|
2478
2503
|
const epochStore = yield* EpochStore;
|
|
@@ -2482,6 +2507,7 @@ var TablinumLive = Layer8.effect(Tablinum, Effect21.gen(function* () {
|
|
|
2482
2507
|
const publishQueue = yield* PublishQueue;
|
|
2483
2508
|
const syncStatus = yield* SyncStatus;
|
|
2484
2509
|
const scope = yield* Effect21.scope;
|
|
2510
|
+
const logLayer = Layer9.succeed(References3.MinimumLogLevel, config.logLevel);
|
|
2485
2511
|
const pubsub = yield* PubSub2.unbounded();
|
|
2486
2512
|
const replayingRef = yield* Ref5.make(false);
|
|
2487
2513
|
const closedRef = yield* Ref5.make(false);
|
|
@@ -2490,7 +2516,7 @@ var TablinumLive = Layer8.effect(Tablinum, Effect21.gen(function* () {
|
|
|
2490
2516
|
const allSchemaEntries = [...schemaEntries, ["_members", membersCollectionDef]];
|
|
2491
2517
|
const knownCollections = new Map(allSchemaEntries.map(([, def]) => [def.name, def.eventRetention]));
|
|
2492
2518
|
let notifyAuthor;
|
|
2493
|
-
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);
|
|
2494
2520
|
const onWrite = (event) => Effect21.gen(function* () {
|
|
2495
2521
|
const content = event.kind === "d" ? JSON.stringify(null) : JSON.stringify(event.data);
|
|
2496
2522
|
const dTag = `${event.collection}:${event.recordId}`;
|
|
@@ -2568,16 +2594,21 @@ var TablinumLive = Layer8.effect(Tablinum, Effect21.gen(function* () {
|
|
|
2568
2594
|
config.onMembersChanged?.();
|
|
2569
2595
|
}
|
|
2570
2596
|
}
|
|
2571
|
-
}).pipe(Effect21.ignore, Effect21.forkIn(scope)));
|
|
2597
|
+
}).pipe(Effect21.ignore, Effect21.provide(logLayer), Effect21.forkIn(scope)));
|
|
2572
2598
|
};
|
|
2573
2599
|
const handles = new Map;
|
|
2574
2600
|
for (const [, def] of allSchemaEntries) {
|
|
2575
2601
|
const validator = buildValidator(def.name, def);
|
|
2576
2602
|
const partialValidator = buildPartialValidator(def.name, def);
|
|
2577
|
-
const handle = createCollectionHandle(def, storage, watchCtx, validator, partialValidator, uuidv7, identity.publicKey, onWrite);
|
|
2603
|
+
const handle = createCollectionHandle(def, storage, watchCtx, validator, partialValidator, uuidv7, identity.publicKey, onWrite, config.logLevel);
|
|
2578
2604
|
handles.set(def.name, handle);
|
|
2579
2605
|
}
|
|
2580
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
|
+
});
|
|
2581
2612
|
const selfMember = yield* storage.getRecord("_members", identity.publicKey);
|
|
2582
2613
|
if (!selfMember) {
|
|
2583
2614
|
yield* putMemberRecord({
|
|
@@ -2586,18 +2617,19 @@ var TablinumLive = Layer8.effect(Tablinum, Effect21.gen(function* () {
|
|
|
2586
2617
|
addedInEpoch: getCurrentEpoch(epochStore).id
|
|
2587
2618
|
});
|
|
2588
2619
|
}
|
|
2589
|
-
const
|
|
2620
|
+
const withLog = (effect) => Effect21.provideService(effect, References3.MinimumLogLevel, config.logLevel);
|
|
2621
|
+
const ensureOpen = (effect) => withLog(Effect21.gen(function* () {
|
|
2590
2622
|
if (yield* Ref5.get(closedRef)) {
|
|
2591
2623
|
return yield* new StorageError({ message: "Database is closed" });
|
|
2592
2624
|
}
|
|
2593
2625
|
return yield* effect;
|
|
2594
|
-
});
|
|
2595
|
-
const ensureSyncOpen = (effect) => Effect21.gen(function* () {
|
|
2626
|
+
}));
|
|
2627
|
+
const ensureSyncOpen = (effect) => withLog(Effect21.gen(function* () {
|
|
2596
2628
|
if (yield* Ref5.get(closedRef)) {
|
|
2597
2629
|
return yield* new SyncError({ message: "Database is closed", phase: "init" });
|
|
2598
2630
|
}
|
|
2599
2631
|
return yield* effect;
|
|
2600
|
-
});
|
|
2632
|
+
}));
|
|
2601
2633
|
const dbHandle = {
|
|
2602
2634
|
collection: (name) => {
|
|
2603
2635
|
const handle = handles.get(name);
|
|
@@ -2613,12 +2645,12 @@ var TablinumLive = Layer8.effect(Tablinum, Effect21.gen(function* () {
|
|
|
2613
2645
|
relays: [...config.relays],
|
|
2614
2646
|
dbName: config.dbName
|
|
2615
2647
|
}),
|
|
2616
|
-
close: () => Effect21.gen(function* () {
|
|
2648
|
+
close: () => withLog(Effect21.gen(function* () {
|
|
2617
2649
|
if (yield* Ref5.get(closedRef))
|
|
2618
2650
|
return;
|
|
2619
2651
|
yield* Ref5.set(closedRef, true);
|
|
2620
2652
|
yield* Scope4.close(scope, Exit.void);
|
|
2621
|
-
}),
|
|
2653
|
+
})),
|
|
2622
2654
|
rebuild: () => ensureOpen(rebuild(storage, allSchemaEntries.map(([, def]) => def.name))),
|
|
2623
2655
|
sync: () => ensureSyncOpen(syncHandle.sync()),
|
|
2624
2656
|
getSyncStatus: () => syncStatus.get(),
|
|
@@ -2689,9 +2721,25 @@ var TablinumLive = Layer8.effect(Tablinum, Effect21.gen(function* () {
|
|
|
2689
2721
|
}))
|
|
2690
2722
|
};
|
|
2691
2723
|
return dbHandle;
|
|
2692
|
-
})).pipe(
|
|
2724
|
+
}).pipe(Effect21.withLogSpan("tablinum.init"))).pipe(Layer9.provide(AllServicesLive));
|
|
2693
2725
|
|
|
2694
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
|
+
}
|
|
2695
2743
|
function validateConfig(config) {
|
|
2696
2744
|
return Effect22.gen(function* () {
|
|
2697
2745
|
if (Object.keys(config.schema).length === 0) {
|
|
@@ -2705,22 +2753,25 @@ function createTablinum(config) {
|
|
|
2705
2753
|
return Effect22.gen(function* () {
|
|
2706
2754
|
yield* validateConfig(config);
|
|
2707
2755
|
const runtimeConfig = yield* resolveRuntimeConfig(config);
|
|
2756
|
+
const logLevel = resolveLogLevel(config.logLevel);
|
|
2708
2757
|
const configValue = {
|
|
2709
2758
|
...runtimeConfig,
|
|
2710
2759
|
schema: config.schema,
|
|
2760
|
+
logLevel,
|
|
2711
2761
|
onSyncError: config.onSyncError,
|
|
2712
2762
|
onRemoved: config.onRemoved,
|
|
2713
2763
|
onMembersChanged: config.onMembersChanged
|
|
2714
2764
|
};
|
|
2715
|
-
const configLayer =
|
|
2716
|
-
const
|
|
2717
|
-
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);
|
|
2718
2769
|
return ServiceMap10.get(ctx, Tablinum);
|
|
2719
2770
|
});
|
|
2720
2771
|
}
|
|
2721
2772
|
|
|
2722
2773
|
// src/svelte/collection.svelte.ts
|
|
2723
|
-
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";
|
|
2724
2775
|
|
|
2725
2776
|
// src/svelte/deferred.ts
|
|
2726
2777
|
function createDeferred() {
|
|
@@ -2813,10 +2864,12 @@ class Collection {
|
|
|
2813
2864
|
#version = $state(0);
|
|
2814
2865
|
#watchAbort = null;
|
|
2815
2866
|
#watchFiber = null;
|
|
2816
|
-
|
|
2867
|
+
#logLevel = "None";
|
|
2868
|
+
_bind(handle, logLevel = "None") {
|
|
2817
2869
|
if (this.#handle)
|
|
2818
2870
|
return;
|
|
2819
2871
|
this.#handle = handle;
|
|
2872
|
+
this.#logLevel = logLevel;
|
|
2820
2873
|
this.error = null;
|
|
2821
2874
|
this.#settleReady();
|
|
2822
2875
|
this.#startWatch();
|
|
@@ -2845,7 +2898,7 @@ class Collection {
|
|
|
2845
2898
|
if (!abort.signal.aborted) {
|
|
2846
2899
|
this.error = e instanceof Error ? e : new Error(String(e));
|
|
2847
2900
|
}
|
|
2848
|
-
}))));
|
|
2901
|
+
})), Effect24.provideService(References5.MinimumLogLevel, this.#logLevel)));
|
|
2849
2902
|
}
|
|
2850
2903
|
#touchVersion = () => {
|
|
2851
2904
|
this.#version;
|
|
@@ -2857,7 +2910,7 @@ class Collection {
|
|
|
2857
2910
|
};
|
|
2858
2911
|
#run = async (getEffect) => {
|
|
2859
2912
|
await this.#ready.promise;
|
|
2860
|
-
return Effect24.runPromise(getEffect());
|
|
2913
|
+
return Effect24.runPromise(getEffect().pipe(Effect24.provideService(References5.MinimumLogLevel, this.#logLevel)));
|
|
2861
2914
|
};
|
|
2862
2915
|
add = (data) => {
|
|
2863
2916
|
return this.#run(() => this.#handleOrThrow().add(data));
|
|
@@ -2924,8 +2977,10 @@ class Tablinum2 {
|
|
|
2924
2977
|
#unsubscribeRelayStatus = null;
|
|
2925
2978
|
#closed = false;
|
|
2926
2979
|
#readyState = createDeferred();
|
|
2980
|
+
#logLevel;
|
|
2927
2981
|
constructor(config) {
|
|
2928
2982
|
this.ready = this.#readyState.promise;
|
|
2983
|
+
this.#logLevel = resolveLogLevel(config.logLevel);
|
|
2929
2984
|
this.#init(config);
|
|
2930
2985
|
}
|
|
2931
2986
|
#settleReady(err) {
|
|
@@ -2936,16 +2991,16 @@ class Tablinum2 {
|
|
|
2936
2991
|
}
|
|
2937
2992
|
}
|
|
2938
2993
|
#bindCollections(handle) {
|
|
2939
|
-
this.#members._bind(handle.members);
|
|
2994
|
+
this.#members._bind(handle.members, this.#logLevel);
|
|
2940
2995
|
for (const [name, collection2] of this.#collections) {
|
|
2941
|
-
collection2._bind(handle.collection(name));
|
|
2996
|
+
collection2._bind(handle.collection(name), this.#logLevel);
|
|
2942
2997
|
}
|
|
2943
2998
|
}
|
|
2944
2999
|
#runHandleEffect = async (run) => {
|
|
2945
3000
|
const handle = this.#requireReady();
|
|
2946
3001
|
try {
|
|
2947
3002
|
this.error = null;
|
|
2948
|
-
return await Effect25.runPromise(run(handle));
|
|
3003
|
+
return await Effect25.runPromise(run(handle).pipe(Effect25.provideService(References6.MinimumLogLevel, this.#logLevel)));
|
|
2949
3004
|
} catch (e) {
|
|
2950
3005
|
this.error = e instanceof Error ? e : new Error(String(e));
|
|
2951
3006
|
throw this.error;
|
|
@@ -3003,7 +3058,7 @@ class Tablinum2 {
|
|
|
3003
3058
|
col = new Collection;
|
|
3004
3059
|
this.#collections.set(name, col);
|
|
3005
3060
|
if (this.#handle) {
|
|
3006
|
-
col._bind(this.#handle.collection(name));
|
|
3061
|
+
col._bind(this.#handle.collection(name), this.#logLevel);
|
|
3007
3062
|
}
|
|
3008
3063
|
}
|
|
3009
3064
|
return col;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Effect, Scope } from "effect";
|
|
2
|
+
import type { LogLevel } from "effect";
|
|
2
3
|
import type { IDBStorageHandle, StoredGiftWrap } from "../storage/idb.ts";
|
|
3
4
|
import type { GiftWrapHandle } from "./gift-wrap.ts";
|
|
4
5
|
import type { RelayHandle } from "./relay.ts";
|
|
@@ -14,4 +15,4 @@ export interface SyncHandle {
|
|
|
14
15
|
readonly startSubscription: () => Effect.Effect<void>;
|
|
15
16
|
readonly addEpochSubscription: (publicKey: string) => Effect.Effect<void>;
|
|
16
17
|
}
|
|
17
|
-
export declare function createSyncHandle(storage: IDBStorageHandle, giftWrapHandle: GiftWrapHandle, relay: RelayHandle, publishQueue: PublishQueueHandle, syncStatus: SyncStatusHandle, watchCtx: WatchContext, relayUrls: readonly string[], knownCollections: ReadonlyMap<string, number>, epochStore: EpochStore, personalPrivateKey: Uint8Array, personalPublicKey: string, scope: Scope.Scope, onSyncError?: ((error: unknown) => void) | undefined, onNewAuthor?: ((pubkey: string) => void) | undefined, onRemoved?: ((notice: RemovalNotice) => void) | undefined, onMembersChanged?: (() => void) | undefined): SyncHandle;
|
|
18
|
+
export declare function createSyncHandle(storage: IDBStorageHandle, giftWrapHandle: GiftWrapHandle, relay: RelayHandle, publishQueue: PublishQueueHandle, syncStatus: SyncStatusHandle, watchCtx: WatchContext, relayUrls: readonly string[], knownCollections: ReadonlyMap<string, number>, epochStore: EpochStore, personalPrivateKey: Uint8Array, personalPublicKey: string, scope: Scope.Scope, logLevel: LogLevel.LogLevel, onSyncError?: ((error: unknown) => void) | undefined, onNewAuthor?: ((pubkey: string) => void) | undefined, onRemoved?: ((notice: RemovalNotice) => void) | undefined, onMembersChanged?: (() => void) | undefined): SyncHandle;
|