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.
- package/README.md +134 -85
- package/dist/brands.d.ts +5 -0
- package/dist/crud/collection-handle.d.ts +8 -6
- package/dist/crud/query-builder.d.ts +4 -13
- package/dist/crud/watch.d.ts +0 -10
- package/dist/db/create-tablinum.d.ts +7 -0
- package/dist/db/database-handle.d.ts +23 -1
- package/dist/db/epoch.d.ts +48 -0
- package/dist/db/invite.d.ts +8 -0
- package/dist/db/key-rotation.d.ts +24 -0
- package/dist/db/members.d.ts +24 -0
- package/dist/db/runtime-config.d.ts +16 -0
- package/dist/index.d.ts +8 -1
- package/dist/index.js +1744 -842
- package/dist/layers/EpochStoreLive.d.ts +6 -0
- package/dist/layers/GiftWrapLive.d.ts +5 -0
- package/dist/layers/IdentityLive.d.ts +5 -0
- package/dist/layers/PublishQueueLive.d.ts +5 -0
- package/dist/layers/RelayLive.d.ts +3 -0
- package/dist/layers/StorageLive.d.ts +4 -0
- package/dist/layers/SyncStatusLive.d.ts +3 -0
- package/dist/layers/TablinumLive.d.ts +5 -0
- package/dist/layers/index.d.ts +8 -0
- package/dist/schema/collection.d.ts +2 -0
- package/dist/schema/field.d.ts +7 -2
- package/dist/schema/types.d.ts +0 -4
- package/dist/services/Config.d.ts +16 -0
- package/dist/services/EpochStore.d.ts +6 -0
- package/dist/services/GiftWrap.d.ts +6 -0
- package/dist/services/Identity.d.ts +6 -0
- package/dist/services/PublishQueue.d.ts +6 -0
- package/dist/services/Relay.d.ts +6 -0
- package/dist/services/Storage.d.ts +6 -0
- package/dist/services/Sync.d.ts +6 -0
- package/dist/services/SyncStatus.d.ts +6 -0
- package/dist/services/Tablinum.d.ts +7 -0
- package/dist/services/index.d.ts +10 -0
- package/dist/storage/idb.d.ts +13 -3
- package/dist/storage/lww.d.ts +0 -5
- package/dist/storage/records-store.d.ts +1 -7
- package/dist/svelte/collection.svelte.d.ts +10 -6
- package/dist/svelte/deferred.d.ts +7 -0
- package/dist/svelte/index.svelte.d.ts +6 -6
- package/dist/svelte/index.svelte.js +2116 -1084
- package/dist/svelte/query.svelte.d.ts +6 -6
- package/dist/svelte/tablinum.svelte.d.ts +36 -0
- package/dist/sync/gift-wrap.d.ts +6 -2
- package/dist/sync/negentropy.d.ts +1 -1
- package/dist/sync/publish-queue.d.ts +3 -2
- package/dist/sync/relay.d.ts +9 -2
- package/dist/sync/sync-service.d.ts +5 -2
- package/dist/sync/sync-status.d.ts +1 -0
- package/dist/utils/diff.d.ts +2 -0
- package/dist/utils/uuid.d.ts +0 -1
- package/package.json +10 -7
- package/dist/main.d.ts +0 -1
- package/dist/storage/events-store.d.ts +0 -6
- package/dist/storage/giftwraps-store.d.ts +0 -6
- package/dist/svelte/database.svelte.d.ts +0 -15
- 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
|
|
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
|
-
|
|
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
|
-
|
|
9
|
+
Your app works offline. All data lives on your device in the browser.
|
|
17
10
|
|
|
18
|
-
|
|
11
|
+
### Backed up and encrypted
|
|
19
12
|
|
|
20
|
-
|
|
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
|
-
###
|
|
15
|
+
### Identity and collaboration built in
|
|
23
16
|
|
|
24
|
-
Tablinum
|
|
17
|
+
Tablinum has a built-in system for sharing a database with other people, and for removing access when needed.
|
|
25
18
|
|
|
26
|
-
|
|
27
|
-
|
|
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).
|
|
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
|
-
|
|
84
|
+
### Quick start (Svelte 5)
|
|
76
85
|
|
|
77
|
-
Import from `tablinum/svelte` for reactive bindings
|
|
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
|
-
|
|
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
|
|
99
|
+
Create a database helper:
|
|
82
100
|
|
|
83
101
|
```typescript
|
|
84
102
|
// src/lib/db.ts
|
|
85
|
-
import {
|
|
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
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
});
|
|
105
|
-
}
|
|
106
|
-
```
|
|
118
|
+
export const db = new Tablinum({
|
|
119
|
+
schema,
|
|
120
|
+
relays: ["wss://relay.example.com"],
|
|
121
|
+
});
|
|
107
122
|
|
|
108
|
-
|
|
123
|
+
export const todos = db.collection("todos");
|
|
124
|
+
```
|
|
109
125
|
|
|
110
|
-
|
|
126
|
+
Use it in a component:
|
|
111
127
|
|
|
112
128
|
```svelte
|
|
113
129
|
<script lang="ts">
|
|
114
|
-
import {
|
|
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
|
-
|
|
125
|
-
db
|
|
126
|
-
|
|
127
|
-
|
|
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 (!
|
|
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
|
|
148
|
+
await todos.update(id, { done: !currentDone });
|
|
141
149
|
}
|
|
142
150
|
|
|
143
151
|
async function remove(id: string) {
|
|
144
|
-
await todos
|
|
152
|
+
await todos.delete(id);
|
|
145
153
|
}
|
|
146
|
-
|
|
147
|
-
onDestroy(() => {
|
|
148
|
-
pending?.destroy();
|
|
149
|
-
db?.close();
|
|
150
|
-
});
|
|
151
154
|
</script>
|
|
152
155
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
<
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
{
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
<button onclick={() =>
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
package/dist/brands.d.ts
ADDED
|
@@ -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,
|
|
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
|
|
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">) =>
|
|
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
|
|
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
|
|
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):
|
|
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>;
|
package/dist/crud/watch.d.ts
CHANGED
|
@@ -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
|
|
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";
|