tablinum 0.1.3 → 0.4.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.
Files changed (60) hide show
  1. package/README.md +134 -85
  2. package/dist/brands.d.ts +5 -0
  3. package/dist/crud/collection-handle.d.ts +8 -6
  4. package/dist/crud/query-builder.d.ts +4 -13
  5. package/dist/crud/watch.d.ts +0 -10
  6. package/dist/db/create-tablinum.d.ts +7 -0
  7. package/dist/db/database-handle.d.ts +23 -1
  8. package/dist/db/epoch.d.ts +48 -0
  9. package/dist/db/invite.d.ts +8 -0
  10. package/dist/db/key-rotation.d.ts +24 -0
  11. package/dist/db/members.d.ts +24 -0
  12. package/dist/db/runtime-config.d.ts +16 -0
  13. package/dist/index.d.ts +8 -1
  14. package/dist/index.js +1744 -842
  15. package/dist/layers/EpochStoreLive.d.ts +6 -0
  16. package/dist/layers/GiftWrapLive.d.ts +5 -0
  17. package/dist/layers/IdentityLive.d.ts +5 -0
  18. package/dist/layers/PublishQueueLive.d.ts +5 -0
  19. package/dist/layers/RelayLive.d.ts +3 -0
  20. package/dist/layers/StorageLive.d.ts +4 -0
  21. package/dist/layers/SyncStatusLive.d.ts +3 -0
  22. package/dist/layers/TablinumLive.d.ts +5 -0
  23. package/dist/layers/index.d.ts +8 -0
  24. package/dist/schema/collection.d.ts +2 -0
  25. package/dist/schema/field.d.ts +7 -2
  26. package/dist/schema/types.d.ts +0 -4
  27. package/dist/services/Config.d.ts +16 -0
  28. package/dist/services/EpochStore.d.ts +6 -0
  29. package/dist/services/GiftWrap.d.ts +6 -0
  30. package/dist/services/Identity.d.ts +6 -0
  31. package/dist/services/PublishQueue.d.ts +6 -0
  32. package/dist/services/Relay.d.ts +6 -0
  33. package/dist/services/Storage.d.ts +6 -0
  34. package/dist/services/Sync.d.ts +6 -0
  35. package/dist/services/SyncStatus.d.ts +6 -0
  36. package/dist/services/Tablinum.d.ts +7 -0
  37. package/dist/services/index.d.ts +10 -0
  38. package/dist/storage/idb.d.ts +13 -3
  39. package/dist/storage/lww.d.ts +0 -5
  40. package/dist/storage/records-store.d.ts +1 -7
  41. package/dist/svelte/collection.svelte.d.ts +10 -6
  42. package/dist/svelte/deferred.d.ts +7 -0
  43. package/dist/svelte/index.svelte.d.ts +6 -6
  44. package/dist/svelte/index.svelte.js +2116 -1084
  45. package/dist/svelte/query.svelte.d.ts +6 -6
  46. package/dist/svelte/tablinum.svelte.d.ts +36 -0
  47. package/dist/sync/gift-wrap.d.ts +6 -2
  48. package/dist/sync/negentropy.d.ts +1 -1
  49. package/dist/sync/publish-queue.d.ts +3 -2
  50. package/dist/sync/relay.d.ts +9 -2
  51. package/dist/sync/sync-service.d.ts +5 -2
  52. package/dist/sync/sync-status.d.ts +1 -0
  53. package/dist/utils/diff.d.ts +2 -0
  54. package/dist/utils/uuid.d.ts +0 -1
  55. package/package.json +10 -7
  56. package/dist/main.d.ts +0 -1
  57. package/dist/storage/events-store.d.ts +0 -6
  58. package/dist/storage/giftwraps-store.d.ts +0 -6
  59. package/dist/svelte/database.svelte.d.ts +0 -15
  60. package/dist/svelte/live-query.svelte.d.ts +0 -8
package/README.md CHANGED
@@ -1,30 +1,39 @@
1
1
  # tablinum
2
2
 
3
- A local-first storage library for the browser. Define typed collections, read and write data locally via IndexedDB, and sync across devices through Nostr relays.
4
-
5
- Built on [Effect](https://effect.website) and [IndexedDB](https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API).
3
+ A local-first database for the browser with encrypted sync and built-in collaboration.
6
4
 
7
5
  ## Features
8
6
 
9
- - **Typed schema** — define collections with `collection()` and `field.*()` builders, get full TypeScript inference
10
- - **Local-first** — all reads hit IndexedDB, no network required
11
- - **Cross-device sync** — replicate data via Nostr relays using NIP-59 gift wrapping for privacy
12
- - **Efficient reconciliation** — NIP-77 negentropy for minimal sync overhead
13
- - **Dexie-style queries** — chainable `.where()`, `.above()`, `.below()`, `.orderBy()` API
14
- - **Svelte 5 bindings** — optional reactive runes integration
7
+ ### Local first
15
8
 
16
- ## How it works
9
+ Your app works offline. All data lives on your device in the browser.
17
10
 
18
- Tablinum stores all data locally in IndexedDB so your app works offline and reads are instant. When you call `sync()`, it replicates data to [Nostr](https://nostr.com) relays so your other devices can pick it up. All data sent to relays is encrypted using [NIP-59 gift wrapping](https://github.com/nostr-protocol/nips/blob/master/59.md) — relays never see your application data. Sync uses [NIP-77 negentropy](https://github.com/nostr-protocol/nips/blob/master/77.md) to efficiently reconcile what each side has, minimizing bandwidth.
11
+ ### Backed up and encrypted
19
12
 
20
- ## Getting started
13
+ Data syncs to relays so it's safe across devices. Everything stored on relays is end-to-end encrypted - relay operators cannot read your data.
21
14
 
22
- ### Pick a relay
15
+ ### Identity and collaboration built in
23
16
 
24
- Tablinum syncs through Nostr relays, which must support [NIP-77 (Negentropy)](https://github.com/nostr-protocol/nips/blob/master/77.md). You have two options:
17
+ Tablinum has a built-in system for sharing a database with other people, and for removing access when needed.
25
18
 
26
- - **Use a public relay**find NIP-77 compatible relays at [nostrwat.ch](https://nostrwat.ch)
27
- - **Self-host a relay** — [strfry](https://github.com/hoytech/strfry) is a good choice if you want full control over where your users' data is stored
19
+ Every database has a shared secret key. When you invite someone, they get a copy of the key so they can read and write data. If someone leaves or is removed, a new key is created and shared with everyone _except_ the removed person like changing the locks when a roommate moves out. Old data is still available to the removed person but they will not get anything new.
20
+
21
+ Invites are just links. Share one, and the other person has everything they need to join.
22
+
23
+ ### Typed collections and queries
24
+
25
+ Define your data shape once with `collection()` and `field.*()` builders, and get full TypeScript inference everywhere. Query with a chainable API:
26
+
27
+ ```typescript
28
+ const pending = await todos.where("done").equals(false).get();
29
+ const recent = await todos.orderBy("createdAt").get();
30
+ ```
31
+
32
+ ### Svelte 5 bindings
33
+
34
+ Optional reactive integration using Svelte 5 async runes. No Effect knowledge needed — the API is plain async/await.
35
+
36
+ ## Getting started
28
37
 
29
38
  ### Install
30
39
 
@@ -32,7 +41,7 @@ Tablinum syncs through Nostr relays, which must support [NIP-77 (Negentropy)](ht
32
41
  npm install tablinum
33
42
  ```
34
43
 
35
- ### Quick start
44
+ ### Quick start (Effect)
36
45
 
37
46
  ```typescript
38
47
  import { Effect } from "effect";
@@ -60,7 +69,7 @@ const program = Effect.gen(function* () {
60
69
  const todo = yield* todos.get(id);
61
70
 
62
71
  // Query
63
- const pending = yield* todos.where("done").equals(false).toArray();
72
+ const pending = yield* todos.where("done").equals(false).get();
64
73
 
65
74
  // Update
66
75
  yield* todos.update(id, { done: true });
@@ -72,17 +81,26 @@ const program = Effect.gen(function* () {
72
81
  Effect.runPromise(Effect.scoped(program));
73
82
  ```
74
83
 
75
- ## Svelte 5
84
+ ### Quick start (Svelte 5)
76
85
 
77
- Import from `tablinum/svelte` for reactive bindings that use Svelte 5 runes. No Effect knowledge needed the API is plain async/await.
86
+ Import from `tablinum/svelte` for reactive bindings. This API uses Svelte's async runes support, so enable it in your app config:
78
87
 
79
- ### Setup
88
+ ```js
89
+ // svelte.config.js
90
+ const config = {
91
+ compilerOptions: {
92
+ experimental: {
93
+ async: true,
94
+ },
95
+ },
96
+ };
97
+ ```
80
98
 
81
- Create a database helper that defines your schema and initializes the database:
99
+ Create a database helper:
82
100
 
83
101
  ```typescript
84
102
  // src/lib/db.ts
85
- import { createTablinum, collection, field } from "tablinum/svelte";
103
+ import { Tablinum, collection, field } from "tablinum/svelte";
86
104
 
87
105
  const schema = {
88
106
  todos: collection(
@@ -97,92 +115,123 @@ const schema = {
97
115
 
98
116
  export type AppSchema = typeof schema;
99
117
 
100
- export async function initDb() {
101
- return createTablinum({
102
- schema,
103
- relays: ["wss://relay.example.com"],
104
- });
105
- }
106
- ```
118
+ export const db = new Tablinum({
119
+ schema,
120
+ relays: ["wss://relay.example.com"],
121
+ });
107
122
 
108
- ### Component
123
+ export const todos = db.collection("todos");
124
+ ```
109
125
 
110
- Collections and live queries are reactive — their `.items` property updates automatically when data changes.
126
+ Use it in a component:
111
127
 
112
128
  ```svelte
113
129
  <script lang="ts">
114
- import { onDestroy } from "svelte";
115
- import { initDb, type AppSchema } from "$lib/db";
116
- import type { Database, Collection, LiveQuery } from "tablinum/svelte";
117
-
118
- let db: Database<AppSchema> | null = $state(null);
119
- let todos: Collection<AppSchema["todos"]> | null = $state(null);
120
- let pending: LiveQuery<{ id: string; title: string; done: boolean }> | null = $state(null);
130
+ import { db, todos } from "$lib/db";
121
131
 
122
132
  let title = $state("");
123
-
124
- async function init() {
125
- db = await initDb();
126
- todos = db.collection("todos");
127
- pending = todos.where("done").equals(false).live();
128
- }
129
-
130
- init();
133
+ let booted = $derived(await db.ready.then(() => true, () => false));
134
+ let pending = $derived(
135
+ booted && db.status === "ready"
136
+ ? await todos.where("done").equals(false).get()
137
+ : [],
138
+ );
131
139
 
132
140
  async function addTodo(e: SubmitEvent) {
133
141
  e.preventDefault();
134
- if (!todos || !title.trim()) return;
142
+ if (!title.trim()) return;
135
143
  await todos.add({ title: title.trim(), done: false });
136
144
  title = "";
137
145
  }
138
146
 
139
147
  async function toggle(id: string, currentDone: boolean) {
140
- await todos?.update(id, { done: !currentDone });
148
+ await todos.update(id, { done: !currentDone });
141
149
  }
142
150
 
143
151
  async function remove(id: string) {
144
- await todos?.delete(id);
152
+ await todos.delete(id);
145
153
  }
146
-
147
- onDestroy(() => {
148
- pending?.destroy();
149
- db?.close();
150
- });
151
154
  </script>
152
155
 
153
- <!-- All todos (reactive via collection.items) -->
154
- <p>{todos?.items.length ?? 0} total</p>
155
-
156
- <!-- Filtered todos (reactive via live query) -->
157
- <form onsubmit={addTodo}>
158
- <input bind:value={title} placeholder="Add a todo..." />
159
- <button type="submit">Add</button>
160
- </form>
161
-
162
- <ul>
163
- {#each pending?.items ?? [] as todo (todo.id)}
164
- <li>
165
- <input type="checkbox" checked={todo.done} onchange={() => toggle(todo.id, todo.done)} />
166
- <span>{todo.title}</span>
167
- <button onclick={() => remove(todo.id)}>Delete</button>
168
- </li>
169
- {/each}
170
- </ul>
171
-
172
- <!-- Sync status is reactive -->
173
- {#if db?.status === "syncing"}
174
- <p>Syncing...</p>
175
- {/if}
176
-
177
- <button onclick={() => db?.sync()}>Sync</button>
156
+ <svelte:boundary>
157
+ {#snippet pending()}
158
+ <p>Initializing database...</p>
159
+ {/snippet}
160
+
161
+ {#if db.status === "error"}
162
+ <p>{db.error?.message}</p>
163
+ {:else}
164
+ <p>{pending.length} pending</p>
165
+
166
+ <form onsubmit={addTodo}>
167
+ <input bind:value={title} placeholder="Add a todo..." />
168
+ <button type="submit">Add</button>
169
+ </form>
170
+
171
+ <ul>
172
+ {#each pending as todo (todo.id)}
173
+ <li>
174
+ <input
175
+ type="checkbox"
176
+ checked={todo.done}
177
+ onchange={() => toggle(todo.id, todo.done)}
178
+ />
179
+ <span>{todo.title}</span>
180
+ <button onclick={() => remove(todo.id)}>Delete</button>
181
+ </li>
182
+ {/each}
183
+ </ul>
184
+
185
+ {#if db.syncStatus === "syncing"}
186
+ <p>Syncing...</p>
187
+ {/if}
188
+
189
+ <button onclick={() => db.sync()}>Sync</button>
190
+ {/if}
191
+ </svelte:boundary>
178
192
  ```
179
193
 
180
- ### Key concepts
194
+ #### Key concepts
195
+
196
+ - **`new Tablinum(config)`** starts initialization immediately and exposes `db.ready`
197
+ - **Async queries are reactive** when used inside `$derived(await ...)`
198
+ - **`db.status`** tracks initialization and terminal state; **`db.syncStatus`** tracks sync activity
199
+ - **`createTablinum(config)`** still exists as a convenience and resolves once `db.ready` completes
200
+
201
+ ## How it works
202
+
203
+ 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).
204
+
205
+ Sync happens through [Nostr](https://nostr.com) relays. All data sent to relays is encrypted using [NIP-59 gift wrapping](https://github.com/nostr-protocol/nips/blob/master/59.md). Relays never see your application data. Sync uses [NIP-77 negentropy](https://github.com/nostr-protocol/nips/blob/master/77.md) to efficiently reconcile what each side has, minimizing bandwidth usage.
206
+
207
+ ### Picking a relay
208
+
209
+ Tablinum syncs through Nostr relays that support [NIP-77 (Negentropy)](https://github.com/nostr-protocol/nips/blob/master/77.md). You have two options:
210
+
211
+ - **Use a public relay** — find NIP-77 compatible relays at [nostrwat.ch](https://nostrwat.ch)
212
+ - **Self-host a relay** — [strfry](https://github.com/hoytech/strfry) is a good choice if you want full control over where your users' data is stored
213
+
214
+ ## Development
215
+
216
+ ### Local relays
217
+
218
+ To run the example app or integration tests against local relays, spin up three [strfry](https://github.com/hoytech/strfry) instances with Docker:
219
+
220
+ ```bash
221
+ bun run relays # starts 3 relays on ports 7984, 7985, 7986
222
+ bun run relays:down # stop relays (data is preserved)
223
+ bun run relays:clean # stop relays and delete all data
224
+ ```
225
+
226
+ The first run builds strfry from source, which takes a few minutes. Subsequent starts are instant.
227
+
228
+ Then point your app at the local relays:
229
+
230
+ ```typescript
231
+ relays: ["ws://localhost:7984", "ws://localhost:7985", "ws://localhost:7986"]
232
+ ```
181
233
 
182
- - **`createTablinum`** returns a `Promise<Database>` (no Effect boilerplate)
183
- - **`collection.items`** is a reactive `$state` array of all records in the collection
184
- - **`.where("field").equals(value).live()`** returns a `LiveQuery` whose `.items` update automatically
185
- - **Cleanup**: call `.destroy()` on live queries and `.close()` on the database in `onDestroy`
234
+ The Svelte example app (`bun run demo:svelte`) is pre-configured to use these local relays.
186
235
 
187
236
  ## License
188
237
 
@@ -0,0 +1,5 @@
1
+ import { Brand } from "effect";
2
+ export type EpochId = Brand.Branded<string, "EpochId">;
3
+ export declare const EpochId: Brand.Constructor<EpochId>;
4
+ export type DatabaseName = Brand.Branded<string, "DatabaseName">;
5
+ export declare const DatabaseName: Brand.Constructor<DatabaseName>;
@@ -1,21 +1,23 @@
1
- import { Effect, Stream } from "effect";
1
+ import { Effect, Option, Stream } from "effect";
2
2
  import type { CollectionDef, CollectionFields } from "../schema/collection.ts";
3
3
  import type { InferRecord } from "../schema/types.ts";
4
4
  import type { RecordValidator, PartialValidator } from "../schema/validate.ts";
5
5
  import type { IDBStorageHandle, StoredEvent } from "../storage/idb.ts";
6
6
  import { NotFoundError, StorageError, ValidationError } from "../errors.ts";
7
7
  import type { WatchContext } from "./watch.ts";
8
- import type { WhereClause, OrderByBuilder } from "./query-builder.ts";
8
+ import type { WhereClause, QueryBuilder } from "./query-builder.ts";
9
+ export declare function pruneEvents(storage: IDBStorageHandle, collection: string, recordId: string, retention: number): Effect.Effect<void, StorageError>;
9
10
  export interface CollectionHandle<C extends CollectionDef<CollectionFields>> {
10
11
  readonly add: (data: Omit<InferRecord<C>, "id">) => Effect.Effect<string, ValidationError | StorageError>;
11
12
  readonly update: (id: string, data: Partial<Omit<InferRecord<C>, "id">>) => Effect.Effect<void, ValidationError | StorageError | NotFoundError>;
12
13
  readonly delete: (id: string) => Effect.Effect<void, StorageError | NotFoundError>;
14
+ readonly undo: (id: string) => Effect.Effect<void, StorageError | NotFoundError>;
13
15
  readonly get: (id: string) => Effect.Effect<InferRecord<C>, StorageError | NotFoundError>;
14
- readonly first: () => Effect.Effect<InferRecord<C> | null, StorageError>;
16
+ readonly first: () => Effect.Effect<Option.Option<InferRecord<C>>, StorageError>;
15
17
  readonly count: () => Effect.Effect<number, StorageError>;
16
18
  readonly watch: () => Stream.Stream<ReadonlyArray<InferRecord<C>>, StorageError>;
17
19
  readonly where: (field: string & keyof Omit<InferRecord<C>, "id">) => WhereClause<InferRecord<C>>;
18
- readonly orderBy: (field: string & keyof Omit<InferRecord<C>, "id">) => OrderByBuilder<InferRecord<C>>;
20
+ readonly orderBy: (field: string & keyof Omit<InferRecord<C>, "id">) => QueryBuilder<InferRecord<C>>;
19
21
  }
20
- export type OnWriteCallback = (event: StoredEvent) => Effect.Effect<void>;
21
- export declare function createCollectionHandle<C extends CollectionDef<CollectionFields>>(def: C, storage: IDBStorageHandle, watchCtx: WatchContext, validator: RecordValidator<C["fields"]>, partialValidator: PartialValidator<C["fields"]>, makeEventId: () => string, onWrite?: OnWriteCallback): CollectionHandle<C>;
22
+ 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>;
@@ -1,4 +1,4 @@
1
- import { Effect, Stream } from "effect";
1
+ import { Effect, Option, Stream } from "effect";
2
2
  import type { IDBStorageHandle } from "../storage/idb.ts";
3
3
  import type { StorageError, ValidationError } from "../errors.ts";
4
4
  import type { CollectionDef, CollectionFields } from "../schema/collection.ts";
@@ -24,19 +24,10 @@ export interface QueryBuilder<T> {
24
24
  readonly offset: (n: number) => QueryBuilder<T>;
25
25
  readonly limit: (n: number) => QueryBuilder<T>;
26
26
  readonly get: () => Effect.Effect<ReadonlyArray<T>, StorageError | ValidationError>;
27
- readonly first: () => Effect.Effect<T | null, StorageError | ValidationError>;
27
+ readonly first: () => Effect.Effect<Option.Option<T>, StorageError | ValidationError>;
28
28
  readonly count: () => Effect.Effect<number, StorageError | ValidationError>;
29
29
  readonly watch: () => Stream.Stream<ReadonlyArray<T>, StorageError | ValidationError>;
30
30
  }
31
- export interface OrderByBuilder<T> {
32
- readonly reverse: () => OrderByBuilder<T>;
33
- readonly offset: (n: number) => OrderByBuilder<T>;
34
- readonly limit: (n: number) => OrderByBuilder<T>;
35
- readonly get: () => Effect.Effect<ReadonlyArray<T>, StorageError | ValidationError>;
36
- readonly first: () => Effect.Effect<T | null, StorageError | ValidationError>;
37
- readonly count: () => Effect.Effect<number, StorageError | ValidationError>;
38
- readonly watch: () => Stream.Stream<ReadonlyArray<T>, StorageError | ValidationError>;
39
- }
40
- export type QueryExecutor<T> = QueryBuilder<T>;
31
+ export type OrderByBuilder<T> = QueryBuilder<T>;
41
32
  export declare function createWhereClause<T>(storage: IDBStorageHandle, watchCtx: WatchContext, collectionName: string, def: CollectionDef<CollectionFields>, fieldName: string, mapRecord: (record: Record<string, unknown>) => T): WhereClause<T>;
42
- export declare function createOrderByBuilder<T>(storage: IDBStorageHandle, watchCtx: WatchContext, collectionName: string, def: CollectionDef<CollectionFields>, fieldName: string, mapRecord: (record: Record<string, unknown>) => T): OrderByBuilder<T>;
33
+ export declare function createOrderByBuilder<T>(storage: IDBStorageHandle, watchCtx: WatchContext, collectionName: string, def: CollectionDef<CollectionFields>, fieldName: string, mapRecord: (record: Record<string, unknown>) => T): QueryBuilder<T>;
@@ -10,16 +10,6 @@ export interface WatchContext {
10
10
  readonly pubsub: PubSub.PubSub<ChangeEvent>;
11
11
  readonly replayingRef: Ref.Ref<boolean>;
12
12
  }
13
- /**
14
- * Create a reactive Stream that emits the current result set whenever it changes.
15
- */
16
13
  export declare function watchCollection<T>(ctx: WatchContext, storage: IDBStorageHandle, collectionName: string, filter?: (record: Record<string, unknown>) => boolean, mapRecord?: (record: Record<string, unknown>) => T): Stream.Stream<ReadonlyArray<T>, StorageError>;
17
- /**
18
- * Notify subscribers of a change.
19
- */
20
14
  export declare function notifyChange(ctx: WatchContext, event: ChangeEvent): Effect.Effect<void>;
21
- /**
22
- * Emit a replay-complete notification for all collections that changed.
23
- * Call after sync replay to trigger batched watch updates.
24
- */
25
15
  export declare function notifyReplayComplete(ctx: WatchContext, collections: ReadonlyArray<string>): Effect.Effect<void>;
@@ -1,12 +1,19 @@
1
1
  import { Effect, Scope } from "effect";
2
2
  import type { SchemaConfig } from "../schema/types.ts";
3
3
  import type { DatabaseHandle } from "./database-handle.ts";
4
+ import type { EpochKeyInput } from "./epoch.ts";
4
5
  import { CryptoError, StorageError, ValidationError } from "../errors.ts";
5
6
  export interface TablinumConfig<S extends SchemaConfig> {
6
7
  readonly schema: S;
7
8
  readonly relays: readonly string[];
8
9
  readonly privateKey?: Uint8Array | undefined;
10
+ readonly epochKeys?: ReadonlyArray<EpochKeyInput> | undefined;
9
11
  readonly dbName?: string | undefined;
10
12
  readonly onSyncError?: ((error: Error) => void) | undefined;
13
+ readonly onRemoved?: ((info: {
14
+ epochId: string;
15
+ removedBy: string;
16
+ }) => void) | undefined;
17
+ readonly onMembersChanged?: (() => void) | undefined;
11
18
  }
12
19
  export declare function createTablinum<S extends SchemaConfig>(config: TablinumConfig<S>): Effect.Effect<DatabaseHandle<S>, ValidationError | StorageError | CryptoError, Scope.Scope>;
@@ -1,13 +1,35 @@
1
1
  import type { Effect } from "effect";
2
+ import type { CollectionDef, CollectionFields } from "../schema/collection.ts";
2
3
  import type { CollectionHandle } from "../crud/collection-handle.ts";
3
4
  import type { SchemaConfig } from "../schema/types.ts";
4
- import type { StorageError, SyncError, RelayError, CryptoError } from "../errors.ts";
5
+ import type { StorageError, SyncError, RelayError, CryptoError, ValidationError } from "../errors.ts";
6
+ import type { Invite } from "./invite.ts";
7
+ import type { MemberRecord, AuthorProfile } from "./members.ts";
8
+ import type { RelayStatus } from "../sync/relay.ts";
5
9
  export type SyncStatus = "idle" | "syncing";
6
10
  export interface DatabaseHandle<S extends SchemaConfig> {
7
11
  readonly collection: <K extends string & keyof S>(name: K) => CollectionHandle<S[K]>;
12
+ readonly publicKey: string;
13
+ readonly members: CollectionHandle<CollectionDef<CollectionFields>>;
8
14
  readonly exportKey: () => string;
15
+ readonly exportInvite: () => Invite;
9
16
  readonly close: () => Effect.Effect<void, StorageError>;
10
17
  readonly rebuild: () => Effect.Effect<void, StorageError>;
11
18
  readonly sync: () => Effect.Effect<void, SyncError | RelayError | CryptoError | StorageError>;
12
19
  readonly getSyncStatus: () => Effect.Effect<SyncStatus>;
20
+ readonly subscribeSyncStatus: (callback: (status: SyncStatus) => void) => () => void;
21
+ readonly pendingCount: () => Effect.Effect<number>;
22
+ readonly subscribePendingCount: (callback: (count: number) => void) => () => void;
23
+ readonly getRelayStatus: () => RelayStatus;
24
+ readonly subscribeRelayStatus: (callback: (status: RelayStatus) => void) => () => void;
25
+ readonly addMember: (pubkey: string) => Effect.Effect<void, ValidationError | StorageError | CryptoError>;
26
+ readonly removeMember: (pubkey: string) => Effect.Effect<void, ValidationError | StorageError | SyncError | RelayError | CryptoError>;
27
+ readonly getMembers: () => Effect.Effect<ReadonlyArray<MemberRecord>, StorageError>;
28
+ readonly getProfile: () => Effect.Effect<AuthorProfile, StorageError>;
29
+ readonly setProfile: (profile: {
30
+ name?: string;
31
+ picture?: string;
32
+ about?: string;
33
+ nip05?: string;
34
+ }) => Effect.Effect<void, ValidationError | StorageError | CryptoError>;
13
35
  }
@@ -0,0 +1,48 @@
1
+ import { Option, Schema } from "effect";
2
+ import { bytesToHex, hexToBytes } from "@noble/hashes/utils.js";
3
+ import { EpochId, DatabaseName } from "../brands.ts";
4
+ export { EpochId, DatabaseName };
5
+ export { bytesToHex, hexToBytes };
6
+ export interface EpochKeyInput {
7
+ readonly epochId: EpochId;
8
+ readonly key: string;
9
+ }
10
+ export interface EpochKey {
11
+ readonly id: EpochId;
12
+ readonly privateKey: string;
13
+ readonly publicKey: string;
14
+ readonly createdBy: string;
15
+ readonly parentEpoch?: EpochId;
16
+ }
17
+ export interface EpochStore {
18
+ readonly epochs: Map<EpochId, EpochKey>;
19
+ readonly keysByPublicKey: Map<string, Uint8Array>;
20
+ currentEpochId: EpochId;
21
+ }
22
+ export declare const EpochKeyInputSchema: Schema.Struct<{
23
+ readonly epochId: Schema.String;
24
+ readonly key: Schema.String;
25
+ }>;
26
+ interface EpochStoreSnapshot {
27
+ readonly epochs: ReadonlyArray<{
28
+ readonly id: string;
29
+ readonly privateKey: string;
30
+ readonly createdBy: string;
31
+ readonly parentEpoch?: string;
32
+ }>;
33
+ readonly currentEpochId: string;
34
+ }
35
+ export declare function createEpochKey(id: EpochId, privateKeyHex: string, createdBy: string, parentEpoch?: EpochId): EpochKey;
36
+ export declare function createEpochStore(initialEpoch: EpochKey): EpochStore;
37
+ export declare function addEpoch(store: EpochStore, epoch: EpochKey): void;
38
+ export declare function hydrateEpochStore(snapshot: EpochStoreSnapshot): EpochStore;
39
+ export declare function createEpochStoreFromInputs(epochKeys: ReadonlyArray<EpochKeyInput>, options?: {
40
+ readonly createdBy?: string | undefined;
41
+ }): EpochStore;
42
+ export declare function getCurrentEpoch(store: EpochStore): EpochKey;
43
+ export declare function getCurrentPublicKey(store: EpochStore): string;
44
+ export declare function getAllPublicKeys(store: EpochStore): string[];
45
+ export declare function getDecryptionKey(store: EpochStore, publicKey: string): Uint8Array | undefined;
46
+ export declare function exportEpochKeys(store: EpochStore): ReadonlyArray<EpochKeyInput>;
47
+ export declare function stringifyEpochStore(store: EpochStore): string;
48
+ export declare function deserializeEpochStore(raw: string): Option.Option<EpochStore>;
@@ -0,0 +1,8 @@
1
+ import { DatabaseName, type EpochKeyInput } from "./epoch.ts";
2
+ export interface Invite {
3
+ readonly epochKeys: Array<EpochKeyInput>;
4
+ readonly relays: string[];
5
+ readonly dbName: DatabaseName;
6
+ }
7
+ export declare function encodeInvite(invite: Invite): string;
8
+ export declare function decodeInvite(encoded: string): Invite;
@@ -0,0 +1,24 @@
1
+ import { Option } from "effect";
2
+ import type { NostrEvent } from "nostr-tools/pure";
3
+ import type { EpochKey, EpochStore } from "./epoch.ts";
4
+ import { EpochId } from "./epoch.ts";
5
+ export interface RotationResult {
6
+ readonly epoch: EpochKey;
7
+ readonly wrappedEvents: NostrEvent[];
8
+ readonly removalNotices: NostrEvent[];
9
+ }
10
+ export interface RotationData {
11
+ readonly _rotation: true;
12
+ readonly epochId: EpochId;
13
+ readonly epochKey: string;
14
+ readonly parentEpoch: EpochId;
15
+ readonly removedMembers: readonly string[];
16
+ }
17
+ export interface RemovalNotice {
18
+ readonly _removed: true;
19
+ readonly epochId: EpochId;
20
+ readonly removedBy: string;
21
+ }
22
+ export declare function createRotation(epochStore: EpochStore, senderPrivateKey: Uint8Array, senderPublicKey: string, remainingMemberPubkeys: string[], removedMemberPubkeys: string[]): RotationResult;
23
+ export declare function parseRotationEvent(content: string, dTag: string): Option.Option<RotationData>;
24
+ export declare function parseRemovalNotice(content: string, dTag: string): Option.Option<RemovalNotice>;
@@ -0,0 +1,24 @@
1
+ import { Effect, Option } from "effect";
2
+ import type { CollectionDef } from "../schema/collection.ts";
3
+ import type { RelayHandle } from "../sync/relay.ts";
4
+ import { RelayError } from "../errors.ts";
5
+ import type { EpochId } from "../brands.ts";
6
+ export interface MemberRecord {
7
+ readonly id: string;
8
+ readonly name?: string;
9
+ readonly picture?: string;
10
+ readonly about?: string;
11
+ readonly nip05?: string;
12
+ readonly addedAt: number;
13
+ readonly addedInEpoch: EpochId;
14
+ readonly removedAt?: number;
15
+ readonly removedInEpoch?: EpochId;
16
+ }
17
+ export interface AuthorProfile {
18
+ readonly name?: string;
19
+ readonly picture?: string;
20
+ readonly about?: string;
21
+ readonly nip05?: string;
22
+ }
23
+ export declare const membersCollectionDef: CollectionDef;
24
+ export declare function fetchAuthorProfile(relay: RelayHandle, relayUrls: readonly string[], pubkey: string): Effect.Effect<Option.Option<AuthorProfile>, RelayError>;
@@ -0,0 +1,16 @@
1
+ import { Effect } from "effect";
2
+ import { ValidationError } from "../errors.ts";
3
+ import { type EpochKeyInput, DatabaseName } from "./epoch.ts";
4
+ export interface RuntimeConfigSource {
5
+ readonly relays: readonly string[];
6
+ readonly privateKey?: Uint8Array | undefined;
7
+ readonly epochKeys?: ReadonlyArray<EpochKeyInput> | undefined;
8
+ readonly dbName?: string | undefined;
9
+ }
10
+ export interface ResolvedRuntimeConfig {
11
+ readonly relays: readonly string[];
12
+ readonly privateKey?: Uint8Array | undefined;
13
+ readonly epochKeys?: ReadonlyArray<EpochKeyInput> | undefined;
14
+ readonly dbName: DatabaseName;
15
+ }
16
+ export declare function resolveRuntimeConfig(source: RuntimeConfigSource): Effect.Effect<ResolvedRuntimeConfig, ValidationError>;
package/dist/index.d.ts CHANGED
@@ -6,7 +6,14 @@ export type { InferRecord, SchemaConfig } from "./schema/types.ts";
6
6
  export { createTablinum } from "./db/create-tablinum.ts";
7
7
  export type { TablinumConfig } from "./db/create-tablinum.ts";
8
8
  export type { DatabaseHandle, SyncStatus } from "./db/database-handle.ts";
9
+ export { encodeInvite, decodeInvite } from "./db/invite.ts";
10
+ export type { Invite } from "./db/invite.ts";
11
+ export { EpochId, DatabaseName } from "./brands.ts";
12
+ export type { EpochKey, EpochKeyInput } from "./db/epoch.ts";
13
+ export type { MemberRecord, AuthorProfile } from "./db/members.ts";
9
14
  export type { CollectionHandle } from "./crud/collection-handle.ts";
10
15
  export type { CollectionOptions } from "./schema/collection.ts";
11
- export type { WhereClause, QueryBuilder, OrderByBuilder, QueryExecutor, } from "./crud/query-builder.ts";
16
+ export type { WhereClause, QueryBuilder, OrderByBuilder } from "./crud/query-builder.ts";
17
+ export { TablinumLive } from "./layers/TablinumLive.ts";
18
+ export type { TablinumConfigShape } from "./services/Config.ts";
12
19
  export { ValidationError, StorageError, CryptoError, RelayError, SyncError, NotFoundError, ClosedError, } from "./errors.ts";