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,244 +0,0 @@
1
- import { Effect, Scope } from "effect";
2
- import { openDB, type IDBPDatabase } from "idb";
3
- import { StorageError } from "../errors.ts";
4
- import type { SchemaConfig } from "../schema/types.ts";
5
-
6
- export interface StoredEvent {
7
- readonly id: string;
8
- readonly collection: string;
9
- readonly recordId: string;
10
- readonly kind: "create" | "update" | "delete";
11
- readonly data: Record<string, unknown> | null;
12
- readonly createdAt: number;
13
- }
14
-
15
- export interface StoredGiftWrap {
16
- readonly id: string;
17
- readonly event: Record<string, unknown>;
18
- readonly createdAt: number;
19
- }
20
-
21
- export interface IDBStorageHandle {
22
- // Records — per-collection stores with flat shape
23
- readonly putRecord: (
24
- collection: string,
25
- record: Record<string, unknown>,
26
- ) => Effect.Effect<void, StorageError>;
27
- readonly getRecord: (
28
- collection: string,
29
- id: string,
30
- ) => Effect.Effect<Record<string, unknown> | undefined, StorageError>;
31
- readonly getAllRecords: (
32
- collection: string,
33
- ) => Effect.Effect<ReadonlyArray<Record<string, unknown>>, StorageError>;
34
- readonly countRecords: (collection: string) => Effect.Effect<number, StorageError>;
35
- readonly clearRecords: (collection: string) => Effect.Effect<void, StorageError>;
36
-
37
- // Index-based queries
38
- readonly getByIndex: (
39
- collection: string,
40
- indexName: string,
41
- value: IDBValidKey,
42
- ) => Effect.Effect<ReadonlyArray<Record<string, unknown>>, StorageError>;
43
- readonly getByIndexRange: (
44
- collection: string,
45
- indexName: string,
46
- range: IDBKeyRange,
47
- ) => Effect.Effect<ReadonlyArray<Record<string, unknown>>, StorageError>;
48
- readonly getAllSorted: (
49
- collection: string,
50
- indexName: string,
51
- direction?: "next" | "prev",
52
- ) => Effect.Effect<ReadonlyArray<Record<string, unknown>>, StorageError>;
53
-
54
- // Events
55
- readonly putEvent: (event: StoredEvent) => Effect.Effect<void, StorageError>;
56
- readonly getEvent: (id: string) => Effect.Effect<StoredEvent | undefined, StorageError>;
57
- readonly getAllEvents: () => Effect.Effect<ReadonlyArray<StoredEvent>, StorageError>;
58
- readonly getEventsByRecord: (
59
- collection: string,
60
- recordId: string,
61
- ) => Effect.Effect<ReadonlyArray<StoredEvent>, StorageError>;
62
-
63
- // Gift wraps
64
- readonly putGiftWrap: (gw: StoredGiftWrap) => Effect.Effect<void, StorageError>;
65
- readonly getGiftWrap: (id: string) => Effect.Effect<StoredGiftWrap | undefined, StorageError>;
66
- readonly getAllGiftWraps: () => Effect.Effect<ReadonlyArray<StoredGiftWrap>, StorageError>;
67
-
68
- readonly close: () => Effect.Effect<void>;
69
- }
70
-
71
- const DB_NAME = "localstr";
72
-
73
- function storeName(collection: string): string {
74
- return `col_${collection}`;
75
- }
76
-
77
- function schemaVersion(schema: SchemaConfig): number {
78
- const sig = Object.entries(schema)
79
- .sort(([a], [b]) => a.localeCompare(b))
80
- .map(([name, def]) => {
81
- const indices = [...(def.indices ?? [])].sort().join(",");
82
- return `${name}:${indices}`;
83
- })
84
- .join("|");
85
- let hash = 1;
86
- for (let i = 0; i < sig.length; i++) {
87
- hash = (hash * 31 + sig.charCodeAt(i)) | 0;
88
- }
89
- return Math.abs(hash) + 1;
90
- }
91
-
92
- function wrap<T>(label: string, fn: () => Promise<T>): Effect.Effect<T, StorageError> {
93
- return Effect.tryPromise({
94
- try: fn,
95
- catch: (e) =>
96
- new StorageError({
97
- message: `IndexedDB ${label} failed: ${e instanceof Error ? e.message : String(e)}`,
98
- cause: e,
99
- }),
100
- });
101
- }
102
-
103
- export function openIDBStorage(
104
- dbName: string | undefined,
105
- schema: SchemaConfig,
106
- ): Effect.Effect<IDBStorageHandle, StorageError, Scope.Scope> {
107
- return Effect.gen(function* () {
108
- const name = dbName ?? DB_NAME;
109
- const version = schemaVersion(schema);
110
-
111
- const db: IDBPDatabase = yield* Effect.tryPromise({
112
- try: () =>
113
- openDB(name, version, {
114
- upgrade(database) {
115
- // Create events store if missing
116
- if (!database.objectStoreNames.contains("events")) {
117
- const events = database.createObjectStore("events", {
118
- keyPath: "id",
119
- });
120
- events.createIndex("by-record", ["collection", "recordId"]);
121
- }
122
-
123
- // Create giftwraps store if missing
124
- if (!database.objectStoreNames.contains("giftwraps")) {
125
- database.createObjectStore("giftwraps", {
126
- keyPath: "id",
127
- });
128
- }
129
-
130
- // Remove old shared records store if it exists
131
- if (database.objectStoreNames.contains("records")) {
132
- database.deleteObjectStore("records");
133
- }
134
-
135
- // Determine which collection stores should exist
136
- const expectedStores = new Set<string>();
137
- for (const [, def] of Object.entries(schema)) {
138
- const sn = storeName(def.name);
139
- expectedStores.add(sn);
140
-
141
- if (!database.objectStoreNames.contains(sn)) {
142
- // Create new collection store
143
- const store = database.createObjectStore(sn, { keyPath: "id" });
144
- for (const idx of def.indices ?? []) {
145
- store.createIndex(idx, idx);
146
- }
147
- } else {
148
- // Update indices on existing store
149
- const tx = (database as any).transaction;
150
- // During upgrade, we can access stores via transaction
151
- // The idb library handles this through the upgrade callback
152
- const store = (tx as IDBTransaction).objectStore(sn);
153
- const existingIndices = new Set(Array.from(store.indexNames));
154
- const wantedIndices = new Set(def.indices ?? []);
155
-
156
- // Remove stale indices
157
- for (const idx of existingIndices) {
158
- if (!wantedIndices.has(idx)) {
159
- store.deleteIndex(idx);
160
- }
161
- }
162
- // Add new indices
163
- for (const idx of wantedIndices) {
164
- if (!existingIndices.has(idx as string)) {
165
- store.createIndex(idx as string, idx as string);
166
- }
167
- }
168
- }
169
- }
170
-
171
- // Remove collection stores no longer in schema
172
- const allStores = Array.from(database.objectStoreNames);
173
- for (const existing of allStores) {
174
- if (existing.startsWith("col_") && !expectedStores.has(existing)) {
175
- database.deleteObjectStore(existing);
176
- }
177
- }
178
- },
179
- }),
180
- catch: (e) =>
181
- new StorageError({
182
- message: "Failed to open IndexedDB",
183
- cause: e,
184
- }),
185
- });
186
-
187
- yield* Effect.addFinalizer(() => Effect.sync(() => db.close()));
188
-
189
- const handle: IDBStorageHandle = {
190
- putRecord: (collection, record) =>
191
- wrap("putRecord", () => db.put(storeName(collection), record).then(() => undefined)),
192
-
193
- getRecord: (collection, id) => wrap("getRecord", () => db.get(storeName(collection), id)),
194
-
195
- getAllRecords: (collection) => wrap("getAllRecords", () => db.getAll(storeName(collection))),
196
-
197
- countRecords: (collection) => wrap("countRecords", () => db.count(storeName(collection))),
198
-
199
- clearRecords: (collection) => wrap("clearRecords", () => db.clear(storeName(collection))),
200
-
201
- getByIndex: (collection, indexName, value) =>
202
- wrap("getByIndex", () => db.getAllFromIndex(storeName(collection), indexName, value)),
203
-
204
- getByIndexRange: (collection, indexName, range) =>
205
- wrap("getByIndexRange", () => db.getAllFromIndex(storeName(collection), indexName, range)),
206
-
207
- getAllSorted: (collection, indexName, direction) =>
208
- wrap("getAllSorted", async () => {
209
- const sn = storeName(collection);
210
- const tx = db.transaction(sn, "readonly");
211
- const store = tx.objectStore(sn);
212
- const index = store.index(indexName);
213
- const results: Record<string, unknown>[] = [];
214
- let cursor = await index.openCursor(null, direction ?? "next");
215
- while (cursor) {
216
- results.push(cursor.value as Record<string, unknown>);
217
- cursor = await cursor.continue();
218
- }
219
- return results;
220
- }),
221
-
222
- putEvent: (event) => wrap("putEvent", () => db.put("events", event).then(() => undefined)),
223
-
224
- getEvent: (id) => wrap("getEvent", () => db.get("events", id)),
225
-
226
- getAllEvents: () => wrap("getAllEvents", () => db.getAll("events")),
227
-
228
- getEventsByRecord: (collection, recordId) =>
229
- wrap("getEventsByRecord", () =>
230
- db.getAllFromIndex("events", "by-record", [collection, recordId]),
231
- ),
232
-
233
- putGiftWrap: (gw) => wrap("putGiftWrap", () => db.put("giftwraps", gw).then(() => undefined)),
234
-
235
- getGiftWrap: (id) => wrap("getGiftWrap", () => db.get("giftwraps", id)),
236
-
237
- getAllGiftWraps: () => wrap("getAllGiftWraps", () => db.getAll("giftwraps")),
238
-
239
- close: () => Effect.sync(() => db.close()),
240
- };
241
-
242
- return handle;
243
- });
244
- }
@@ -1,17 +0,0 @@
1
- export interface EventMeta {
2
- readonly id: string;
3
- readonly createdAt: number;
4
- }
5
-
6
- /**
7
- * Last-write-wins resolution.
8
- * Higher `createdAt` wins. Ties broken by lowest event ID (lexicographic).
9
- * Returns the winner.
10
- */
11
- export function resolveWinner<T extends EventMeta>(existing: T | null, incoming: T): T {
12
- if (existing === null) return incoming;
13
- if (incoming.createdAt > existing.createdAt) return incoming;
14
- if (incoming.createdAt < existing.createdAt) return existing;
15
- // Tie: lowest event ID wins
16
- return incoming.id < existing.id ? incoming : existing;
17
- }
@@ -1,76 +0,0 @@
1
- import { Effect } from "effect";
2
- import type { IDBStorageHandle, StoredEvent } from "./idb.ts";
3
- import { resolveWinner } from "./lww.ts";
4
- import type { StorageError } from "../errors.ts";
5
-
6
- /**
7
- * Build a flat record from an event for storage in per-collection IDB stores.
8
- */
9
- function buildRecord(event: StoredEvent): Record<string, unknown> {
10
- return {
11
- id: event.recordId,
12
- _deleted: event.kind === "delete",
13
- _updatedAt: event.createdAt,
14
- ...(event.data ?? {}),
15
- };
16
- }
17
-
18
- /**
19
- * Apply an event to the records store using LWW resolution.
20
- * Returns true if the incoming event won and the record was updated.
21
- */
22
- export function applyEvent(
23
- storage: IDBStorageHandle,
24
- event: StoredEvent,
25
- ): Effect.Effect<boolean, StorageError> {
26
- return Effect.gen(function* () {
27
- const existingEvents = yield* storage.getEventsByRecord(event.collection, event.recordId);
28
-
29
- // Find the current winning event for this record
30
- let currentWinner: StoredEvent | null = null;
31
- for (const e of existingEvents) {
32
- if (e.id === event.id) continue; // Skip the event we're applying
33
- currentWinner = resolveWinner(currentWinner, e);
34
- }
35
-
36
- const winner = resolveWinner(currentWinner, event);
37
- const incomingWon = winner.id === event.id;
38
-
39
- if (incomingWon) {
40
- yield* storage.putRecord(event.collection, buildRecord(event));
41
- }
42
-
43
- return incomingWon;
44
- });
45
- }
46
-
47
- /**
48
- * Rebuild the records store by clearing it and replaying all events.
49
- */
50
- export function rebuild(
51
- storage: IDBStorageHandle,
52
- collections: ReadonlyArray<string>,
53
- ): Effect.Effect<void, StorageError> {
54
- return Effect.gen(function* () {
55
- for (const col of collections) {
56
- yield* storage.clearRecords(col);
57
- }
58
- const allEvents = yield* storage.getAllEvents();
59
-
60
- // Sort by createdAt for deterministic replay
61
- const sorted = [...allEvents].sort((a, b) => a.createdAt - b.createdAt);
62
-
63
- // Group events by record and resolve each
64
- const winners = new Map<string, StoredEvent>();
65
- for (const event of sorted) {
66
- const key = `${event.collection}:${event.recordId}`;
67
- const current = winners.get(key) ?? null;
68
- winners.set(key, resolveWinner(current, event));
69
- }
70
-
71
- // Write winning records
72
- for (const event of winners.values()) {
73
- yield* storage.putRecord(event.collection, buildRecord(event));
74
- }
75
- });
76
- }
@@ -1,87 +0,0 @@
1
- import { Effect, Fiber, Stream } from "effect";
2
- import type { CollectionDef, CollectionFields } from "../schema/collection.ts";
3
- import type { InferRecord } from "../schema/types.ts";
4
- import type { CollectionHandle } from "../crud/collection-handle.ts";
5
- import { LiveQuery } from "./live-query.svelte.ts";
6
- import {
7
- wrapWhereClause,
8
- wrapOrderByBuilder,
9
- type SvelteWhereClause,
10
- type SvelteOrderByBuilder,
11
- } from "./query.svelte.ts";
12
-
13
- export class Collection<C extends CollectionDef<CollectionFields>> {
14
- items = $state<ReadonlyArray<InferRecord<C>>>([]);
15
- error = $state<Error | null>(null);
16
-
17
- #handle: CollectionHandle<C>;
18
- #watchFiber: Fiber.Fiber<void, unknown> | null = null;
19
- #liveQueries: Set<LiveQuery<unknown>> = new Set();
20
-
21
- constructor(handle: CollectionHandle<C>) {
22
- this.#handle = handle;
23
-
24
- // Auto-watch: subscribe to all records, surface errors to this.error
25
- const watchEffect = Stream.runForEach(handle.watch(), (records) =>
26
- Effect.sync(() => {
27
- this.items = records;
28
- }),
29
- ).pipe(
30
- Effect.catch((e) =>
31
- Effect.sync(() => {
32
- this.error = e instanceof Error ? e : new Error(String(e));
33
- }),
34
- ),
35
- );
36
- this.#watchFiber = Effect.runFork(watchEffect);
37
- }
38
-
39
- #run = async <R>(effect: Effect.Effect<R, unknown>): Promise<R> => {
40
- try {
41
- this.error = null;
42
- return await Effect.runPromise(effect);
43
- } catch (e) {
44
- this.error = e instanceof Error ? e : new Error(String(e));
45
- throw this.error;
46
- }
47
- };
48
-
49
- #onLive = (lq: LiveQuery<unknown>): void => {
50
- this.#liveQueries.add(lq);
51
- };
52
-
53
- add = (data: Omit<InferRecord<C>, "id">): Promise<string> => this.#run(this.#handle.add(data));
54
-
55
- update = (id: string, data: Partial<Omit<InferRecord<C>, "id">>): Promise<void> =>
56
- this.#run(this.#handle.update(id, data));
57
-
58
- delete = (id: string): Promise<void> => this.#run(this.#handle.delete(id));
59
-
60
- get = (id: string): Promise<InferRecord<C>> => this.#run(this.#handle.get(id));
61
-
62
- first = (): Promise<InferRecord<C> | null> => this.#run(this.#handle.first());
63
-
64
- count = (): Promise<number> => this.#run(this.#handle.count());
65
-
66
- where = (field: string & keyof Omit<InferRecord<C>, "id">): SvelteWhereClause<InferRecord<C>> => {
67
- return wrapWhereClause(this.#handle.where(field), this.#onLive);
68
- };
69
-
70
- orderBy = (
71
- field: string & keyof Omit<InferRecord<C>, "id">,
72
- ): SvelteOrderByBuilder<InferRecord<C>> => {
73
- return wrapOrderByBuilder(this.#handle.orderBy(field), this.#onLive);
74
- };
75
-
76
- /** @internal Called by Database.close() */
77
- _destroy(): void {
78
- if (this.#watchFiber) {
79
- Effect.runFork(Fiber.interrupt(this.#watchFiber));
80
- this.#watchFiber = null;
81
- }
82
- for (const lq of this.#liveQueries) {
83
- lq.destroy();
84
- }
85
- this.#liveQueries.clear();
86
- }
87
- }
@@ -1,83 +0,0 @@
1
- import { Effect, Scope, Exit } from "effect";
2
- import type { SchemaConfig } from "../schema/types.ts";
3
- import type { CollectionDef, CollectionFields } from "../schema/collection.ts";
4
- import type { DatabaseHandle, SyncStatus } from "../db/database-handle.ts";
5
- import { Collection } from "./collection.svelte.ts";
6
-
7
- export class Database<S extends SchemaConfig> {
8
- status = $state<SyncStatus>("idle");
9
- error = $state<Error | null>(null);
10
-
11
- #handle: DatabaseHandle<S>;
12
- #scope: Scope.Closeable;
13
- #collections = new Map<string, Collection<CollectionDef<CollectionFields>>>();
14
- #statusInterval: ReturnType<typeof setInterval> | null = null;
15
- #closed = false;
16
-
17
- constructor(handle: DatabaseHandle<S>, scope: Scope.Closeable) {
18
- this.#handle = handle;
19
- this.#scope = scope;
20
-
21
- // Poll sync status
22
- this.#statusInterval = setInterval(() => {
23
- if (this.#closed) return;
24
- Effect.runPromise(this.#handle.getSyncStatus())
25
- .then((s) => {
26
- this.status = s;
27
- })
28
- .catch(() => {
29
- /* database may be closed, ignore */
30
- });
31
- }, 1000);
32
- }
33
-
34
- collection<K extends string & keyof S>(name: K): Collection<S[K]> {
35
- let col = this.#collections.get(name);
36
- if (!col) {
37
- const handle = this.#handle.collection(name);
38
- col = new Collection(handle) as Collection<CollectionDef<CollectionFields>>;
39
- this.#collections.set(name, col);
40
- }
41
- return col as unknown as Collection<S[K]>;
42
- }
43
-
44
- exportKey(): string {
45
- return this.#handle.exportKey();
46
- }
47
-
48
- close = async (): Promise<void> => {
49
- if (this.#closed) return;
50
- this.#closed = true;
51
-
52
- if (this.#statusInterval) {
53
- clearInterval(this.#statusInterval);
54
- this.#statusInterval = null;
55
- }
56
- for (const col of this.#collections.values()) {
57
- col._destroy();
58
- }
59
- this.#collections.clear();
60
- await Effect.runPromise(this.#handle.close());
61
- await Effect.runPromise(Scope.close(this.#scope, Exit.void));
62
- };
63
-
64
- sync = async (): Promise<void> => {
65
- try {
66
- this.error = null;
67
- await Effect.runPromise(this.#handle.sync());
68
- } catch (e) {
69
- this.error = e instanceof Error ? e : new Error(String(e));
70
- throw this.error;
71
- }
72
- };
73
-
74
- rebuild = async (): Promise<void> => {
75
- try {
76
- this.error = null;
77
- await Effect.runPromise(this.#handle.rebuild());
78
- } catch (e) {
79
- this.error = e instanceof Error ? e : new Error(String(e));
80
- throw this.error;
81
- }
82
- };
83
- }
@@ -1,52 +0,0 @@
1
- import { Effect, Exit, Scope } from "effect";
2
- import type { SchemaConfig } from "../schema/types.ts";
3
- import {
4
- createLocalstr as coreCreateLocalstr,
5
- type LocalstrConfig,
6
- } from "../db/create-localstr.ts";
7
- import { Database } from "./database.svelte.ts";
8
-
9
- // Re-export schema utilities (unchanged)
10
- export { field } from "../schema/field.ts";
11
- export { collection } from "../schema/collection.ts";
12
- export type { CollectionDef, CollectionFields } from "../schema/collection.ts";
13
- export type { FieldDef, FieldKind } from "../schema/field.ts";
14
- export type { InferRecord, SchemaConfig } from "../schema/types.ts";
15
- export type { LocalstrConfig } from "../db/create-localstr.ts";
16
- export type { SyncStatus } from "../db/database-handle.ts";
17
-
18
- // Re-export errors
19
- export {
20
- ValidationError,
21
- StorageError,
22
- CryptoError,
23
- RelayError,
24
- SyncError,
25
- NotFoundError,
26
- ClosedError,
27
- } from "../errors.ts";
28
-
29
- // Re-export Svelte classes
30
- export { Database } from "./database.svelte.ts";
31
- export { Collection } from "./collection.svelte.ts";
32
- export { LiveQuery } from "./live-query.svelte.ts";
33
- export type {
34
- SvelteQueryBuilder,
35
- SvelteWhereClause,
36
- SvelteOrderByBuilder,
37
- } from "./query.svelte.ts";
38
-
39
- export async function createLocalstr<S extends SchemaConfig>(
40
- config: LocalstrConfig<S>,
41
- ): Promise<Database<S>> {
42
- const scope = Effect.runSync(Scope.make());
43
- try {
44
- const handle = await Effect.runPromise(
45
- coreCreateLocalstr(config).pipe(Effect.provideService(Scope.Scope, scope)),
46
- );
47
- return new Database(handle, scope) as Database<S>;
48
- } catch (e) {
49
- await Effect.runPromise(Scope.close(scope, Exit.fail(e)));
50
- throw e;
51
- }
52
- }
@@ -1,29 +0,0 @@
1
- import { Effect, Fiber, Stream } from "effect";
2
-
3
- export class LiveQuery<T> {
4
- items = $state<ReadonlyArray<T>>([]);
5
- error = $state<Error | null>(null);
6
- #fiber: Fiber.Fiber<void, unknown> | null = null;
7
-
8
- constructor(stream: Stream.Stream<ReadonlyArray<T>, unknown>) {
9
- const effect = Stream.runForEach(stream, (records) =>
10
- Effect.sync(() => {
11
- this.items = records;
12
- }),
13
- ).pipe(
14
- Effect.catch((e) =>
15
- Effect.sync(() => {
16
- this.error = e instanceof Error ? e : new Error(String(e));
17
- }),
18
- ),
19
- );
20
- this.#fiber = Effect.runFork(effect);
21
- }
22
-
23
- destroy(): void {
24
- if (this.#fiber) {
25
- Effect.runFork(Fiber.interrupt(this.#fiber));
26
- this.#fiber = null;
27
- }
28
- }
29
- }
@@ -1,101 +0,0 @@
1
- import { Effect } from "effect";
2
- import type { WhereClause, QueryBuilder, OrderByBuilder } from "../crud/query-builder.ts";
3
- import { LiveQuery } from "./live-query.svelte.ts";
4
-
5
- export type OnLiveCallback = (lq: LiveQuery<unknown>) => void;
6
-
7
- export interface SvelteQueryBuilder<T> {
8
- readonly and: (fn: (item: T) => boolean) => SvelteQueryBuilder<T>;
9
- readonly sortBy: (field: string) => SvelteQueryBuilder<T>;
10
- readonly reverse: () => SvelteQueryBuilder<T>;
11
- readonly offset: (n: number) => SvelteQueryBuilder<T>;
12
- readonly limit: (n: number) => SvelteQueryBuilder<T>;
13
- readonly get: () => Promise<ReadonlyArray<T>>;
14
- readonly first: () => Promise<T | null>;
15
- readonly count: () => Promise<number>;
16
- readonly live: () => LiveQuery<T>;
17
- }
18
-
19
- export interface SvelteWhereClause<T> {
20
- readonly equals: (value: string | number | boolean) => SvelteQueryBuilder<T>;
21
- readonly above: (value: number) => SvelteQueryBuilder<T>;
22
- readonly aboveOrEqual: (value: number) => SvelteQueryBuilder<T>;
23
- readonly below: (value: number) => SvelteQueryBuilder<T>;
24
- readonly belowOrEqual: (value: number) => SvelteQueryBuilder<T>;
25
- readonly between: (
26
- lower: number,
27
- upper: number,
28
- options?: { includeLower?: boolean; includeUpper?: boolean },
29
- ) => SvelteQueryBuilder<T>;
30
- readonly startsWith: (prefix: string) => SvelteQueryBuilder<T>;
31
- readonly anyOf: (values: ReadonlyArray<string | number | boolean>) => SvelteQueryBuilder<T>;
32
- readonly noneOf: (values: ReadonlyArray<string | number | boolean>) => SvelteQueryBuilder<T>;
33
- }
34
-
35
- export interface SvelteOrderByBuilder<T> {
36
- readonly reverse: () => SvelteOrderByBuilder<T>;
37
- readonly offset: (n: number) => SvelteOrderByBuilder<T>;
38
- readonly limit: (n: number) => SvelteOrderByBuilder<T>;
39
- readonly get: () => Promise<ReadonlyArray<T>>;
40
- readonly first: () => Promise<T | null>;
41
- readonly count: () => Promise<number>;
42
- readonly live: () => LiveQuery<T>;
43
- }
44
-
45
- function wrapQueryBuilder<T>(
46
- builder: QueryBuilder<T>,
47
- onLive?: OnLiveCallback,
48
- ): SvelteQueryBuilder<T> {
49
- return {
50
- and: (fn) => wrapQueryBuilder(builder.and(fn), onLive),
51
- sortBy: (field) => wrapQueryBuilder(builder.sortBy(field), onLive),
52
- reverse: () => wrapQueryBuilder(builder.reverse(), onLive),
53
- offset: (n) => wrapQueryBuilder(builder.offset(n), onLive),
54
- limit: (n) => wrapQueryBuilder(builder.limit(n), onLive),
55
- get: () => Effect.runPromise(builder.get()),
56
- first: () => Effect.runPromise(builder.first()),
57
- count: () => Effect.runPromise(builder.count()),
58
- live: () => {
59
- const lq = new LiveQuery(builder.watch());
60
- onLive?.(lq as LiveQuery<unknown>);
61
- return lq;
62
- },
63
- };
64
- }
65
-
66
- export function wrapWhereClause<T>(
67
- clause: WhereClause<T>,
68
- onLive?: OnLiveCallback,
69
- ): SvelteWhereClause<T> {
70
- return {
71
- equals: (value) => wrapQueryBuilder(clause.equals(value), onLive),
72
- above: (value) => wrapQueryBuilder(clause.above(value), onLive),
73
- aboveOrEqual: (value) => wrapQueryBuilder(clause.aboveOrEqual(value), onLive),
74
- below: (value) => wrapQueryBuilder(clause.below(value), onLive),
75
- belowOrEqual: (value) => wrapQueryBuilder(clause.belowOrEqual(value), onLive),
76
- between: (lower, upper, options) =>
77
- wrapQueryBuilder(clause.between(lower, upper, options), onLive),
78
- startsWith: (prefix) => wrapQueryBuilder(clause.startsWith(prefix), onLive),
79
- anyOf: (values) => wrapQueryBuilder(clause.anyOf(values), onLive),
80
- noneOf: (values) => wrapQueryBuilder(clause.noneOf(values), onLive),
81
- };
82
- }
83
-
84
- export function wrapOrderByBuilder<T>(
85
- builder: OrderByBuilder<T>,
86
- onLive?: OnLiveCallback,
87
- ): SvelteOrderByBuilder<T> {
88
- return {
89
- reverse: () => wrapOrderByBuilder(builder.reverse(), onLive),
90
- offset: (n) => wrapOrderByBuilder(builder.offset(n), onLive),
91
- limit: (n) => wrapOrderByBuilder(builder.limit(n), onLive),
92
- get: () => Effect.runPromise(builder.get()),
93
- first: () => Effect.runPromise(builder.first()),
94
- count: () => Effect.runPromise(builder.count()),
95
- live: () => {
96
- const lq = new LiveQuery(builder.watch());
97
- onLive?.(lq as LiveQuery<unknown>);
98
- return lq;
99
- },
100
- };
101
- }