tablinum 0.0.1 → 0.1.1

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.
Files changed (105) hide show
  1. package/README.md +77 -1
  2. package/dist/crud/collection-handle.d.ts +21 -0
  3. package/dist/crud/query-builder.d.ts +42 -0
  4. package/dist/crud/watch.d.ts +25 -0
  5. package/dist/db/create-tablinum.d.ts +12 -0
  6. package/dist/db/database-handle.d.ts +13 -0
  7. package/dist/db/identity.d.ts +8 -0
  8. package/dist/errors.d.ts +58 -0
  9. package/{src/index.ts → dist/index.d.ts} +4 -24
  10. package/dist/index.js +1833 -0
  11. package/dist/main.d.ts +1 -0
  12. package/dist/schema/collection.d.ts +12 -0
  13. package/dist/schema/field.d.ts +17 -0
  14. package/{src/schema/types.ts → dist/schema/types.d.ts} +5 -10
  15. package/dist/schema/validate.d.ts +13 -0
  16. package/dist/storage/events-store.d.ts +6 -0
  17. package/dist/storage/giftwraps-store.d.ts +6 -0
  18. package/dist/storage/idb.d.ts +35 -0
  19. package/dist/storage/lww.d.ts +10 -0
  20. package/dist/storage/records-store.d.ts +12 -0
  21. package/dist/svelte/collection.svelte.d.ts +20 -0
  22. package/dist/svelte/database.svelte.d.ts +15 -0
  23. package/dist/svelte/index.svelte.d.ts +16 -0
  24. package/dist/svelte/index.svelte.js +2050 -0
  25. package/dist/svelte/live-query.svelte.d.ts +8 -0
  26. package/dist/svelte/query.svelte.d.ts +39 -0
  27. package/dist/sync/gift-wrap.d.ts +9 -0
  28. package/dist/sync/negentropy.d.ts +9 -0
  29. package/dist/sync/publish-queue.d.ts +10 -0
  30. package/dist/sync/relay.d.ts +17 -0
  31. package/dist/sync/sync-service.d.ts +14 -0
  32. package/dist/sync/sync-status.d.ts +7 -0
  33. package/dist/utils/uuid.d.ts +2 -0
  34. package/package.json +22 -1
  35. package/.changeset/README.md +0 -8
  36. package/.changeset/config.json +0 -11
  37. package/.context/attachments/pasted_text_2026-03-07_14-02-40.txt +0 -571
  38. package/.context/attachments/pasted_text_2026-03-07_15-48-27.txt +0 -498
  39. package/.context/notes.md +0 -0
  40. package/.context/plans/add-changesets-to-douala-v4.md +0 -48
  41. package/.context/plans/dexie-js-style-query-language-for-localstr.md +0 -115
  42. package/.context/plans/dexie-js-style-query-language-with-per-collection-.md +0 -336
  43. package/.context/plans/implementation-plan-localstr-v0-2.md +0 -263
  44. package/.context/plans/project-init-effect-v4-bun-oxlint-oxfmt-vitest.md +0 -71
  45. package/.context/plans/revise-localstr-prd-v0-2.md +0 -132
  46. package/.context/plans/svelte-5-runes-bindings-for-localstr.md +0 -233
  47. package/.context/todos.md +0 -0
  48. package/.github/workflows/release.yml +0 -36
  49. package/.oxlintrc.json +0 -8
  50. package/bun.lock +0 -705
  51. package/examples/svelte/bun.lock +0 -261
  52. package/examples/svelte/package.json +0 -21
  53. package/examples/svelte/src/app.html +0 -11
  54. package/examples/svelte/src/lib/db.ts +0 -44
  55. package/examples/svelte/src/routes/+page.svelte +0 -322
  56. package/examples/svelte/svelte.config.js +0 -16
  57. package/examples/svelte/tsconfig.json +0 -6
  58. package/examples/svelte/vite.config.ts +0 -6
  59. package/examples/vanilla/app.ts +0 -219
  60. package/examples/vanilla/index.html +0 -144
  61. package/examples/vanilla/serve.ts +0 -42
  62. package/prds/localstr-v0.2.md +0 -221
  63. package/prek.toml +0 -10
  64. package/scripts/validate.ts +0 -392
  65. package/src/crud/collection-handle.ts +0 -189
  66. package/src/crud/query-builder.ts +0 -414
  67. package/src/crud/watch.ts +0 -78
  68. package/src/db/create-localstr.ts +0 -217
  69. package/src/db/database-handle.ts +0 -16
  70. package/src/db/identity.ts +0 -49
  71. package/src/errors.ts +0 -37
  72. package/src/main.ts +0 -10
  73. package/src/schema/collection.ts +0 -53
  74. package/src/schema/field.ts +0 -25
  75. package/src/schema/validate.ts +0 -111
  76. package/src/storage/events-store.ts +0 -24
  77. package/src/storage/giftwraps-store.ts +0 -23
  78. package/src/storage/idb.ts +0 -244
  79. package/src/storage/lww.ts +0 -17
  80. package/src/storage/records-store.ts +0 -76
  81. package/src/svelte/collection.svelte.ts +0 -87
  82. package/src/svelte/database.svelte.ts +0 -83
  83. package/src/svelte/index.svelte.ts +0 -52
  84. package/src/svelte/live-query.svelte.ts +0 -29
  85. package/src/svelte/query.svelte.ts +0 -101
  86. package/src/sync/gift-wrap.ts +0 -33
  87. package/src/sync/negentropy.ts +0 -83
  88. package/src/sync/publish-queue.ts +0 -61
  89. package/src/sync/relay.ts +0 -239
  90. package/src/sync/sync-service.ts +0 -183
  91. package/src/sync/sync-status.ts +0 -17
  92. package/src/utils/uuid.ts +0 -22
  93. package/src/vendor/negentropy.js +0 -616
  94. package/tests/db/create-localstr.test.ts +0 -174
  95. package/tests/db/identity.test.ts +0 -33
  96. package/tests/main.test.ts +0 -9
  97. package/tests/schema/collection.test.ts +0 -27
  98. package/tests/schema/field.test.ts +0 -41
  99. package/tests/schema/validate.test.ts +0 -85
  100. package/tests/setup.ts +0 -1
  101. package/tests/storage/idb.test.ts +0 -144
  102. package/tests/storage/lww.test.ts +0 -33
  103. package/tests/sync/gift-wrap.test.ts +0 -56
  104. package/tsconfig.json +0 -18
  105. package/vitest.config.ts +0 -8
@@ -1,33 +0,0 @@
1
- import { Effect } from "effect";
2
- import { wrapEvent, unwrapEvent, type Rumor } from "nostr-tools/nip59";
3
- import type { NostrEvent, UnsignedEvent } from "nostr-tools/pure";
4
- import { CryptoError } from "../errors.ts";
5
-
6
- export interface GiftWrapHandle {
7
- readonly wrap: (rumor: Partial<UnsignedEvent>) => Effect.Effect<NostrEvent, CryptoError>;
8
- readonly unwrap: (giftWrap: NostrEvent) => Effect.Effect<Rumor, CryptoError>;
9
- }
10
-
11
- export function createGiftWrapHandle(privateKey: Uint8Array, publicKey: string): GiftWrapHandle {
12
- return {
13
- wrap: (rumor) =>
14
- Effect.try({
15
- try: () => wrapEvent(rumor, privateKey, publicKey),
16
- catch: (e) =>
17
- new CryptoError({
18
- message: `Gift wrap failed: ${e instanceof Error ? e.message : String(e)}`,
19
- cause: e,
20
- }),
21
- }),
22
-
23
- unwrap: (giftWrap) =>
24
- Effect.try({
25
- try: () => unwrapEvent(giftWrap, privateKey),
26
- catch: (e) =>
27
- new CryptoError({
28
- message: `Gift unwrap failed: ${e instanceof Error ? e.message : String(e)}`,
29
- cause: e,
30
- }),
31
- }),
32
- };
33
- }
@@ -1,83 +0,0 @@
1
- import { Effect } from "effect";
2
- // @ts-expect-error -- vendored JS without types
3
- import { Negentropy, NegentropyStorageVector } from "../vendor/negentropy.js";
4
- import type { IDBStorageHandle } from "../storage/idb.ts";
5
- import type { RelayHandle } from "./relay.ts";
6
- import type { Filter } from "nostr-tools/filter";
7
- import { SyncError, RelayError, StorageError } from "../errors.ts";
8
-
9
- export interface ReconcileResult {
10
- readonly haveIds: string[];
11
- readonly needIds: string[];
12
- }
13
-
14
- function hexToBytes(hex: string): Uint8Array {
15
- const bytes = new Uint8Array(hex.length / 2);
16
- for (let i = 0; i < hex.length; i += 2) {
17
- bytes[i / 2] = parseInt(hex.substring(i, i + 2), 16);
18
- }
19
- return bytes;
20
- }
21
-
22
- export function reconcileWithRelay(
23
- storage: IDBStorageHandle,
24
- relay: RelayHandle,
25
- relayUrl: string,
26
- publicKey: string,
27
- ): Effect.Effect<ReconcileResult, SyncError | RelayError | StorageError> {
28
- return Effect.gen(function* () {
29
- const allGiftWraps = yield* storage.getAllGiftWraps();
30
-
31
- const storageVector = new NegentropyStorageVector();
32
- for (const gw of allGiftWraps) {
33
- storageVector.insert(gw.createdAt, hexToBytes(gw.id));
34
- }
35
- storageVector.seal();
36
-
37
- const neg = new Negentropy(storageVector, 0);
38
-
39
- const filter: Filter = {
40
- kinds: [1059],
41
- "#p": [publicKey],
42
- };
43
-
44
- const allHaveIds: string[] = [];
45
- const allNeedIds: string[] = [];
46
- const subId = `neg-${Date.now()}`;
47
-
48
- const initialMsg: string = yield* Effect.tryPromise({
49
- try: () => neg.initiate(),
50
- catch: (e) =>
51
- new SyncError({
52
- message: `Negentropy initiate failed: ${e instanceof Error ? e.message : String(e)}`,
53
- phase: "negotiate",
54
- cause: e,
55
- }),
56
- });
57
-
58
- let currentMsg: string | null = initialMsg;
59
-
60
- while (currentMsg !== null) {
61
- const response = yield* relay.sendNegMsg(relayUrl, subId, filter, currentMsg);
62
-
63
- if (response.msgHex === null) break;
64
-
65
- const reconcileResult: [string | null, string[], string[]] = yield* Effect.tryPromise({
66
- try: () => neg.reconcile(response.msgHex),
67
- catch: (e) =>
68
- new SyncError({
69
- message: `Negentropy reconcile failed: ${e instanceof Error ? e.message : String(e)}`,
70
- phase: "negotiate",
71
- cause: e,
72
- }),
73
- });
74
-
75
- const [nextMsg, haveIds, needIds] = reconcileResult;
76
- for (const id of haveIds) allHaveIds.push(id);
77
- for (const id of needIds) allNeedIds.push(id);
78
- currentMsg = nextMsg;
79
- }
80
-
81
- return { haveIds: allHaveIds, needIds: allNeedIds };
82
- });
83
- }
@@ -1,61 +0,0 @@
1
- import { Effect, Ref } from "effect";
2
- import type { IDBStorageHandle } from "../storage/idb.ts";
3
- import type { RelayHandle } from "./relay.ts";
4
- import type { NostrEvent } from "nostr-tools/pure";
5
- import type { RelayError, StorageError } from "../errors.ts";
6
-
7
- export interface PublishQueueHandle {
8
- readonly enqueue: (eventId: string) => Effect.Effect<void>;
9
- readonly flush: (relayUrls: readonly string[]) => Effect.Effect<void, RelayError | StorageError>;
10
- readonly size: () => Effect.Effect<number>;
11
- }
12
-
13
- export function createPublishQueue(
14
- storage: IDBStorageHandle,
15
- relay: RelayHandle,
16
- ): Effect.Effect<PublishQueueHandle> {
17
- return Effect.gen(function* () {
18
- const pendingRef = yield* Ref.make<Set<string>>(new Set());
19
-
20
- return {
21
- enqueue: (eventId) =>
22
- Ref.update(pendingRef, (set) => {
23
- const next = new Set(set);
24
- next.add(eventId);
25
- return next;
26
- }),
27
-
28
- flush: (relayUrls) =>
29
- Effect.gen(function* () {
30
- const pending = yield* Ref.get(pendingRef);
31
- if (pending.size === 0) return;
32
-
33
- const succeeded = new Set<string>();
34
-
35
- for (const eventId of pending) {
36
- const gw = yield* storage.getGiftWrap(eventId);
37
- if (!gw) {
38
- succeeded.add(eventId);
39
- continue;
40
- }
41
- const result = yield* Effect.result(
42
- relay.publish(gw.event as unknown as NostrEvent, relayUrls),
43
- );
44
- if (result._tag === "Success") {
45
- succeeded.add(eventId);
46
- }
47
- }
48
-
49
- yield* Ref.update(pendingRef, (set) => {
50
- const next = new Set(set);
51
- for (const id of succeeded) {
52
- next.delete(id);
53
- }
54
- return next;
55
- });
56
- }),
57
-
58
- size: () => Ref.get(pendingRef).pipe(Effect.map((s) => s.size)),
59
- };
60
- });
61
- }
package/src/sync/relay.ts DELETED
@@ -1,239 +0,0 @@
1
- import { Effect } from "effect";
2
- import { Relay } from "nostr-tools/relay";
3
- import type { NostrEvent } from "nostr-tools/pure";
4
- import type { Filter } from "nostr-tools/filter";
5
- import { RelayError } from "../errors.ts";
6
-
7
- export interface RelayHandle {
8
- readonly publish: (event: NostrEvent, urls: readonly string[]) => Effect.Effect<void, RelayError>;
9
- readonly fetchEvents: (
10
- ids: readonly string[],
11
- url: string,
12
- ) => Effect.Effect<NostrEvent[], RelayError>;
13
- readonly fetchByFilter: (filter: Filter, url: string) => Effect.Effect<NostrEvent[], RelayError>;
14
- readonly subscribe: (
15
- filter: Filter,
16
- url: string,
17
- onEvent: (event: NostrEvent) => void,
18
- ) => Effect.Effect<void, RelayError>;
19
- readonly sendNegMsg: (
20
- url: string,
21
- subId: string,
22
- filter: Filter,
23
- msgHex: string,
24
- ) => Effect.Effect<{ msgHex: string | null; haveIds: string[]; needIds: string[] }, RelayError>;
25
- readonly closeAll: () => Effect.Effect<void>;
26
- }
27
-
28
- export function createRelayHandle(): RelayHandle {
29
- const connections = new Map<string, Relay>();
30
-
31
- const getRelay = (url: string): Effect.Effect<Relay, RelayError> =>
32
- Effect.tryPromise({
33
- try: async () => {
34
- const existing = connections.get(url);
35
- if (existing && (existing as any).connected !== false) {
36
- return existing;
37
- }
38
- // Clean up stale entry
39
- connections.delete(url);
40
- const relay = await Relay.connect(url);
41
- connections.set(url, relay);
42
- return relay;
43
- },
44
- catch: (e) =>
45
- new RelayError({
46
- message: `Connect to ${url} failed: ${e instanceof Error ? e.message : String(e)}`,
47
- url,
48
- cause: e,
49
- }),
50
- });
51
-
52
- return {
53
- publish: (event, urls) =>
54
- Effect.gen(function* () {
55
- const errors: Array<{ url: string; error: unknown }> = [];
56
- for (const url of urls) {
57
- const result = yield* Effect.result(
58
- Effect.gen(function* () {
59
- const relay = yield* getRelay(url);
60
- yield* Effect.tryPromise({
61
- try: () => relay.publish(event),
62
- catch: (e) => {
63
- // Connection may be stale, remove so next attempt reconnects
64
- connections.delete(url);
65
- return new RelayError({
66
- message: `Publish to ${url} failed: ${e instanceof Error ? e.message : String(e)}`,
67
- url,
68
- cause: e,
69
- });
70
- },
71
- });
72
- }),
73
- );
74
- if (result._tag === "Failure") {
75
- errors.push({ url, error: result });
76
- }
77
- }
78
- if (errors.length === urls.length && urls.length > 0) {
79
- return yield* new RelayError({
80
- message: `Publish failed on all ${urls.length} relays`,
81
- });
82
- }
83
- }),
84
-
85
- fetchEvents: (ids, url) =>
86
- Effect.gen(function* () {
87
- if (ids.length === 0) return [] as NostrEvent[];
88
- const relay = yield* getRelay(url);
89
- return yield* Effect.tryPromise({
90
- try: () =>
91
- new Promise<NostrEvent[]>((resolve) => {
92
- const events: NostrEvent[] = [];
93
- const timer = setTimeout(() => {
94
- sub.close();
95
- resolve(events);
96
- }, 10000);
97
- const sub = relay.subscribe([{ ids: ids as string[] }], {
98
- onevent(evt: NostrEvent) {
99
- events.push(evt);
100
- },
101
- oneose() {
102
- clearTimeout(timer);
103
- sub.close();
104
- resolve(events);
105
- },
106
- });
107
- }),
108
- catch: (e) => {
109
- connections.delete(url);
110
- return new RelayError({
111
- message: `Fetch from ${url} failed: ${e instanceof Error ? e.message : String(e)}`,
112
- url,
113
- cause: e,
114
- });
115
- },
116
- });
117
- }),
118
-
119
- fetchByFilter: (filter, url) =>
120
- Effect.gen(function* () {
121
- const relay = yield* getRelay(url);
122
- return yield* Effect.tryPromise({
123
- try: () =>
124
- new Promise<NostrEvent[]>((resolve) => {
125
- const events: NostrEvent[] = [];
126
- const timer = setTimeout(() => {
127
- sub.close();
128
- resolve(events);
129
- }, 10000);
130
- const sub = relay.subscribe([filter], {
131
- onevent(evt: NostrEvent) {
132
- events.push(evt);
133
- },
134
- oneose() {
135
- clearTimeout(timer);
136
- sub.close();
137
- resolve(events);
138
- },
139
- });
140
- }),
141
- catch: (e) => {
142
- connections.delete(url);
143
- return new RelayError({
144
- message: `Fetch from ${url} failed: ${e instanceof Error ? e.message : String(e)}`,
145
- url,
146
- cause: e,
147
- });
148
- },
149
- });
150
- }),
151
-
152
- subscribe: (filter, url, onEvent) =>
153
- Effect.gen(function* () {
154
- const relay = yield* getRelay(url);
155
- relay.subscribe([filter], {
156
- onevent(evt: NostrEvent) {
157
- onEvent(evt);
158
- },
159
- oneose() {
160
- // Initial fetch complete, keep subscription open for real-time
161
- },
162
- });
163
- }),
164
-
165
- sendNegMsg: (url, subId, filter, msgHex) =>
166
- Effect.gen(function* () {
167
- const relay = yield* getRelay(url);
168
- return yield* Effect.tryPromise({
169
- try: () =>
170
- new Promise<{
171
- msgHex: string | null;
172
- haveIds: string[];
173
- needIds: string[];
174
- }>((resolve, reject) => {
175
- const timer = setTimeout(() => {
176
- reject(new Error("NIP-77 negotiation timeout"));
177
- }, 30000);
178
-
179
- const sub = relay.subscribe([filter], {
180
- onevent() {},
181
- oneose() {},
182
- });
183
-
184
- // Use raw WebSocket for NIP-77
185
- const ws = (relay as any)._ws || (relay as any).ws;
186
- if (!ws) {
187
- clearTimeout(timer);
188
- sub.close();
189
- reject(new Error("Cannot access relay WebSocket"));
190
- return;
191
- }
192
-
193
- const handler = (msg: MessageEvent) => {
194
- try {
195
- const data = JSON.parse(typeof msg.data === "string" ? msg.data : "");
196
- if (!Array.isArray(data)) return;
197
- if (data[0] === "NEG-MSG" && data[1] === subId) {
198
- clearTimeout(timer);
199
- sub.close();
200
- resolve({
201
- msgHex: data[2] as string,
202
- haveIds: [],
203
- needIds: [],
204
- });
205
- ws.removeEventListener("message", handler);
206
- } else if (data[0] === "NEG-ERR" && data[1] === subId) {
207
- clearTimeout(timer);
208
- sub.close();
209
- reject(new Error(`NEG-ERR: ${data[2]}`));
210
- ws.removeEventListener("message", handler);
211
- }
212
- } catch {
213
- // ignore parse errors
214
- }
215
- };
216
-
217
- ws.addEventListener("message", handler);
218
- ws.send(JSON.stringify(["NEG-OPEN", subId, filter, msgHex]));
219
- }),
220
- catch: (e) => {
221
- connections.delete(url);
222
- return new RelayError({
223
- message: `NIP-77 negotiation with ${url} failed: ${e instanceof Error ? e.message : String(e)}`,
224
- url,
225
- cause: e,
226
- });
227
- },
228
- });
229
- }),
230
-
231
- closeAll: () =>
232
- Effect.sync(() => {
233
- for (const [url, relay] of connections) {
234
- relay.close();
235
- connections.delete(url);
236
- }
237
- }),
238
- };
239
- }
@@ -1,183 +0,0 @@
1
- import { Effect, Ref } from "effect";
2
- import type { NostrEvent } from "nostr-tools/pure";
3
- import type { IDBStorageHandle, StoredEvent, StoredGiftWrap } from "../storage/idb.ts";
4
- import { applyEvent } from "../storage/records-store.ts";
5
- import type { GiftWrapHandle } from "./gift-wrap.ts";
6
- import type { RelayHandle } from "./relay.ts";
7
- import type { PublishQueueHandle } from "./publish-queue.ts";
8
- import type { SyncStatusHandle } from "./sync-status.ts";
9
- import { reconcileWithRelay } from "./negentropy.ts";
10
- import type { WatchContext } from "../crud/watch.ts";
11
- import { notifyChange, notifyReplayComplete } from "../crud/watch.ts";
12
- import { CryptoError, RelayError, StorageError, SyncError } from "../errors.ts";
13
-
14
- export interface SyncHandle {
15
- readonly sync: () => Effect.Effect<void, SyncError | RelayError | CryptoError | StorageError>;
16
- readonly publishLocal: (giftWrap: StoredGiftWrap) => Effect.Effect<void>;
17
- readonly startSubscription: () => Effect.Effect<void>;
18
- }
19
-
20
- export function createSyncHandle(
21
- storage: IDBStorageHandle,
22
- giftWrapHandle: GiftWrapHandle,
23
- relay: RelayHandle,
24
- publishQueue: PublishQueueHandle,
25
- syncStatus: SyncStatusHandle,
26
- watchCtx: WatchContext,
27
- relayUrls: readonly string[],
28
- publicKey: string,
29
- onSyncError?: ((error: unknown) => void) | undefined,
30
- ): SyncHandle {
31
- // Process a single remote gift wrap: store, unwrap, apply event
32
- const processGiftWrap = (
33
- remoteGw: NostrEvent,
34
- ): Effect.Effect<string | null, StorageError | CryptoError> =>
35
- Effect.gen(function* () {
36
- // Skip if we already have this gift wrap
37
- const existing = yield* storage.getGiftWrap(remoteGw.id);
38
- if (existing) return null;
39
-
40
- // Store gift wrap
41
- yield* storage.putGiftWrap({
42
- id: remoteGw.id,
43
- event: remoteGw as unknown as Record<string, unknown>,
44
- createdAt: remoteGw.created_at,
45
- });
46
-
47
- // Unwrap to get rumor
48
- const unwrapResult = yield* Effect.result(giftWrapHandle.unwrap(remoteGw));
49
- if (unwrapResult._tag === "Failure") return null;
50
-
51
- const rumor = unwrapResult.success;
52
-
53
- // Parse the rumor content and d-tag
54
- const dTag = rumor.tags.find((t: string[]) => t[0] === "d")?.[1];
55
- if (!dTag) return null;
56
-
57
- const colonIdx = dTag.indexOf(":");
58
- if (colonIdx === -1) return null;
59
-
60
- const collectionName = dTag.substring(0, colonIdx);
61
- const recordId = dTag.substring(colonIdx + 1);
62
-
63
- let data: Record<string, unknown> | null = null;
64
- let kind: "create" | "update" | "delete" = "update";
65
-
66
- try {
67
- const parsed = JSON.parse(rumor.content);
68
- if (parsed === null || parsed._deleted) {
69
- kind = "delete";
70
- } else {
71
- data = parsed;
72
- }
73
- } catch {
74
- return null;
75
- }
76
-
77
- const event: StoredEvent = {
78
- id: rumor.id,
79
- collection: collectionName,
80
- recordId,
81
- kind,
82
- data,
83
- createdAt: rumor.created_at * 1000,
84
- };
85
-
86
- yield* storage.putEvent(event);
87
- yield* applyEvent(storage, event);
88
- return collectionName;
89
- });
90
-
91
- return {
92
- sync: () =>
93
- Effect.gen(function* () {
94
- yield* syncStatus.set("syncing");
95
- yield* Ref.set(watchCtx.replayingRef, true);
96
-
97
- const changedCollections = new Set<string>();
98
-
99
- try {
100
- for (const url of relayUrls) {
101
- const reconcileResult = yield* Effect.result(
102
- reconcileWithRelay(storage, relay, url, publicKey),
103
- );
104
-
105
- if (reconcileResult._tag === "Failure") continue;
106
-
107
- const { haveIds, needIds } = reconcileResult.success;
108
-
109
- // Download missing gift wraps
110
- if (needIds.length > 0) {
111
- const fetchResult = yield* Effect.result(relay.fetchEvents(needIds, url));
112
-
113
- if (fetchResult._tag === "Success") {
114
- for (const remoteGw of fetchResult.success) {
115
- const result = yield* Effect.result(processGiftWrap(remoteGw));
116
- if (result._tag === "Success" && result.success) {
117
- changedCollections.add(result.success);
118
- }
119
- }
120
- }
121
- }
122
-
123
- // Upload gift wraps the relay is missing
124
- if (haveIds.length > 0) {
125
- for (const id of haveIds) {
126
- const gw = yield* storage.getGiftWrap(id);
127
- if (gw) {
128
- yield* Effect.result(relay.publish(gw.event as unknown as NostrEvent, [url]));
129
- }
130
- }
131
- }
132
- }
133
-
134
- // Flush pending publications
135
- yield* Effect.result(publishQueue.flush(relayUrls));
136
- } finally {
137
- yield* notifyReplayComplete(watchCtx, [...changedCollections]);
138
- yield* syncStatus.set("idle");
139
- }
140
- }),
141
-
142
- publishLocal: (giftWrap) =>
143
- Effect.gen(function* () {
144
- const result = yield* Effect.result(
145
- relay.publish(giftWrap.event as unknown as NostrEvent, relayUrls),
146
- );
147
- if (result._tag === "Failure") {
148
- yield* publishQueue.enqueue(giftWrap.id);
149
- console.error("[localstr:publishLocal] relay error:", result.failure);
150
- if (onSyncError) onSyncError(result.failure);
151
- }
152
- }),
153
-
154
- startSubscription: () =>
155
- Effect.gen(function* () {
156
- for (const url of relayUrls) {
157
- const subResult = yield* Effect.result(
158
- relay.subscribe({ kinds: [1059], "#p": [publicKey] }, url, (evt: NostrEvent) => {
159
- // Process incoming gift wrap in a fire-and-forget fiber
160
- Effect.runFork(
161
- Effect.gen(function* () {
162
- const result = yield* Effect.result(processGiftWrap(evt));
163
- if (result._tag === "Success" && result.success) {
164
- yield* notifyChange(watchCtx, {
165
- collection: result.success,
166
- recordId: "",
167
- kind: "create",
168
- });
169
- }
170
- }),
171
- );
172
- }),
173
- );
174
- if (subResult._tag === "Failure") {
175
- console.error("[localstr:subscribe] failed for", url, subResult.failure);
176
- if (onSyncError) onSyncError(subResult.failure);
177
- } else {
178
- console.log("[localstr:subscribe] listening on", url);
179
- }
180
- }
181
- }),
182
- };
183
- }
@@ -1,17 +0,0 @@
1
- import { Effect, SubscriptionRef } from "effect";
2
- import type { SyncStatus } from "../db/database-handle.ts";
3
-
4
- export interface SyncStatusHandle {
5
- readonly get: () => Effect.Effect<SyncStatus>;
6
- readonly set: (status: SyncStatus) => Effect.Effect<void>;
7
- }
8
-
9
- export function createSyncStatusHandle(): Effect.Effect<SyncStatusHandle> {
10
- return Effect.gen(function* () {
11
- const ref = yield* SubscriptionRef.make<SyncStatus>("idle");
12
- return {
13
- get: () => SubscriptionRef.get(ref),
14
- set: (status: SyncStatus) => SubscriptionRef.set(ref, status),
15
- };
16
- });
17
- }
package/src/utils/uuid.ts DELETED
@@ -1,22 +0,0 @@
1
- /** Generate a UUIDv7 (time-sortable) using crypto.getRandomValues. */
2
- export function uuidv7(): string {
3
- const now = Date.now();
4
- const bytes = new Uint8Array(16);
5
- crypto.getRandomValues(bytes);
6
-
7
- // Timestamp: 48 bits in bytes 0-5
8
- bytes[0] = (now / 2 ** 40) & 0xff;
9
- bytes[1] = (now / 2 ** 32) & 0xff;
10
- bytes[2] = (now / 2 ** 24) & 0xff;
11
- bytes[3] = (now / 2 ** 16) & 0xff;
12
- bytes[4] = (now / 2 ** 8) & 0xff;
13
- bytes[5] = now & 0xff;
14
-
15
- // Version 7: set bits 0111 in byte 6 high nibble
16
- bytes[6] = (bytes[6]! & 0x0f) | 0x70;
17
- // Variant 10: set bits 10 in byte 8 high 2 bits
18
- bytes[8] = (bytes[8]! & 0x3f) | 0x80;
19
-
20
- const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
21
- return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
22
- }