tablinum 0.5.0 → 0.6.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.
@@ -0,0 +1,3 @@
1
+ import type { NostrEvent } from "nostr-tools/pure";
2
+ export declare function packEvent(event: NostrEvent): Uint8Array;
3
+ export declare function unpackEvent(id: string, compact: Uint8Array): NostrEvent;
@@ -3,11 +3,10 @@ import { unwrapEvent } from "nostr-tools/nip59";
3
3
  import type { NostrEvent, UnsignedEvent } from "nostr-tools/pure";
4
4
  import { CryptoError } from "../errors.ts";
5
5
  import type { EpochStore } from "../db/epoch.ts";
6
- type Rumor = ReturnType<typeof unwrapEvent>;
6
+ export type Rumor = ReturnType<typeof unwrapEvent>;
7
7
  export interface GiftWrapHandle {
8
8
  readonly wrap: (rumor: Partial<UnsignedEvent>) => Effect.Effect<NostrEvent, CryptoError>;
9
9
  readonly unwrap: (giftWrap: NostrEvent) => Effect.Effect<Rumor, CryptoError>;
10
10
  }
11
11
  export declare function createGiftWrapHandle(senderPrivateKey: Uint8Array, recipientPublicKey: string, decryptionPrivateKey: Uint8Array): GiftWrapHandle;
12
12
  export declare function createEpochGiftWrapHandle(senderPrivateKey: Uint8Array, epochStore: EpochStore): GiftWrapHandle;
13
- export {};
@@ -14,5 +14,7 @@ export interface SyncHandle {
14
14
  readonly publishLocal: (giftWrap: StoredGiftWrap) => Effect.Effect<void>;
15
15
  readonly startSubscription: () => Effect.Effect<void>;
16
16
  readonly addEpochSubscription: (publicKey: string) => Effect.Effect<void>;
17
+ readonly startHealing: () => void;
18
+ readonly stopHealing: () => void;
17
19
  }
18
20
  export declare function createSyncHandle(storage: IDBStorageHandle, giftWrapHandle: GiftWrapHandle, relay: RelayHandle, publishQueue: PublishQueueHandle, syncStatus: SyncStatusHandle, watchCtx: WatchContext, relayUrls: readonly string[], knownCollections: ReadonlyMap<string, number>, epochStore: EpochStore, personalPrivateKey: Uint8Array, personalPublicKey: string, scope: Scope.Scope, logLevel: LogLevel.LogLevel, onSyncError?: ((error: unknown) => void) | undefined, onNewAuthor?: ((pubkey: string) => void) | undefined, onRemoved?: ((notice: RemovalNotice) => void) | undefined, onMembersChanged?: (() => void) | undefined): SyncHandle;
package/package.json CHANGED
@@ -1,9 +1,10 @@
1
1
  {
2
2
  "name": "tablinum",
3
- "version": "0.5.0",
3
+ "version": "0.6.1",
4
4
  "repository": {
5
5
  "type": "git",
6
- "url": "git+https://github.com/kevmodrome/tablinum.git"
6
+ "url": "git+https://github.com/kevmodrome/tablinum.git",
7
+ "directory": "packages/tablinum"
7
8
  },
8
9
  "files": [
9
10
  "dist"
@@ -23,41 +24,21 @@
23
24
  }
24
25
  },
25
26
  "scripts": {
26
- "dev": "bun run src/main.ts",
27
- "lint": "oxlint",
28
- "format": "oxfmt --write .",
29
- "format:check": "oxfmt --check .",
30
- "test": "bun run vitest run",
31
- "test:watch": "bun run vitest",
32
- "validate": "bun run scripts/validate.ts",
33
- "demo": "bun run examples/vanilla/serve.ts",
34
- "demo:svelte": "cd examples/svelte && bun run dev",
35
- "build": "bun build src/index.ts --outdir dist --format esm --packages external && bun build src/svelte/index.svelte.ts --outdir dist/svelte --format esm --packages external && tsc -p tsconfig.build.json --noCheck && tsc -p tsconfig.build.svelte.json --noCheck",
36
- "changeset": "changeset",
37
- "version": "changeset version",
38
- "release": "changeset publish",
39
- "relays": "docker compose up -d",
40
- "relays:down": "docker compose down",
41
- "relays:clean": "docker compose down -v"
27
+ "dev": "npx tsx src/main.ts",
28
+ "build": "esbuild src/index.ts --outdir=dist --format=esm --bundle --packages=external && esbuild src/svelte/index.svelte.ts --outdir=dist/svelte --format=esm --bundle --packages=external && tsc -p tsconfig.build.json --noCheck && tsc -p tsconfig.build.svelte.json --noCheck"
42
29
  },
43
30
  "dependencies": {
44
31
  "@noble/curves": "^2.0.1",
45
32
  "@noble/hashes": "^2.0.1",
46
- "effect": "4.0.0-beta.28",
33
+ "effect": "4.0.0-beta.31",
47
34
  "idb": "^8.0.3",
48
35
  "nostr-tools": "^2.23.3"
49
36
  },
50
37
  "devDependencies": {
51
- "@changesets/cli": "^2.30.0",
52
38
  "@effect/vitest": "4.0.0-beta.31",
53
- "@j178/prek": "0.3.5",
54
- "@types/bun": "^1.3.10",
39
+ "esbuild": "^0.24.0",
55
40
  "fake-indexeddb": "^6.2.5",
56
- "oxfmt": "0.38.0",
57
- "oxlint": "^1.53.0",
58
- "svelte": "^5.53.10",
59
- "typescript": "^5.7.0",
60
- "vitest": "^3.0.0"
41
+ "svelte": "^5.53.10"
61
42
  },
62
43
  "peerDependencies": {
63
44
  "svelte": "^5.0.0"
package/README.md DELETED
@@ -1,298 +0,0 @@
1
- # tablinum
2
-
3
- A local-first database for the browser with encrypted sync and built-in collaboration.
4
-
5
- ## Features
6
-
7
- ### Local first
8
-
9
- Your app works offline. All data lives on your device in the browser.
10
-
11
- ### Backed up and encrypted
12
-
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.
14
-
15
- ### Identity and collaboration built in
16
-
17
- Tablinum has a built-in system for sharing a database with other people, and for removing access when needed.
18
-
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
37
-
38
- ### Install
39
-
40
- ```bash
41
- npm install tablinum
42
- ```
43
-
44
- ### Quick start (Effect)
45
-
46
- ```typescript
47
- import { Effect } from "effect";
48
- import { createTablinum, collection, field } from "tablinum";
49
-
50
- const schema = {
51
- todos: collection("todos", {
52
- title: field.string(),
53
- done: field.boolean(),
54
- }),
55
- };
56
-
57
- const program = Effect.gen(function* () {
58
- const db = yield* createTablinum({
59
- schema,
60
- relays: ["wss://relay.example.com"],
61
- });
62
-
63
- const todos = db.collection("todos");
64
-
65
- // Create
66
- const id = yield* todos.add({ title: "Buy milk", done: false });
67
-
68
- // Read
69
- const todo = yield* todos.get(id);
70
-
71
- // Query
72
- const pending = yield* todos.where("done").equals(false).get();
73
-
74
- // Update
75
- yield* todos.update(id, { done: true });
76
-
77
- // Sync across devices
78
- yield* db.sync();
79
- });
80
-
81
- Effect.runPromise(Effect.scoped(program));
82
- ```
83
-
84
- ### Quick start (Svelte 5)
85
-
86
- Import from `tablinum/svelte` for reactive bindings. This API uses Svelte's async runes support, so enable it in your app config:
87
-
88
- ```js
89
- // svelte.config.js
90
- const config = {
91
- compilerOptions: {
92
- experimental: {
93
- async: true,
94
- },
95
- },
96
- };
97
- ```
98
-
99
- Create a database helper:
100
-
101
- ```typescript
102
- // src/lib/db.ts
103
- import { Tablinum, collection, field } from "tablinum/svelte";
104
-
105
- const schema = {
106
- todos: collection(
107
- "todos",
108
- {
109
- title: field.string(),
110
- done: field.boolean(),
111
- },
112
- { indices: ["done"] },
113
- ),
114
- };
115
-
116
- export type AppSchema = typeof schema;
117
-
118
- export const db = new Tablinum({
119
- schema,
120
- relays: ["wss://relay.example.com"],
121
- });
122
-
123
- export const todos = db.collection("todos");
124
- ```
125
-
126
- Use it in a component:
127
-
128
- ```svelte
129
- <script lang="ts">
130
- import { db, todos } from "$lib/db";
131
-
132
- let title = $state("");
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
- );
139
-
140
- async function addTodo(e: SubmitEvent) {
141
- e.preventDefault();
142
- if (!title.trim()) return;
143
- await todos.add({ title: title.trim(), done: false });
144
- title = "";
145
- }
146
-
147
- async function toggle(id: string, currentDone: boolean) {
148
- await todos.update(id, { done: !currentDone });
149
- }
150
-
151
- async function remove(id: string) {
152
- await todos.delete(id);
153
- }
154
- </script>
155
-
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>
192
- ```
193
-
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
- ## Logging
202
-
203
- Tablinum uses [Effect's built-in logging](https://effect.website/docs/observability/logging/) under the hood. By default, logging is completely silent. Set `logLevel` in your config to enable it:
204
-
205
- ```typescript
206
- const db = yield* createTablinum({
207
- schema,
208
- relays: ["wss://relay.example.com"],
209
- logLevel: "debug", // "debug" | "info" | "warning" | "error" | "none"
210
- });
211
- ```
212
-
213
- Wire it to an environment variable for easy toggling:
214
-
215
- ```typescript
216
- // Effect API
217
- createTablinum({
218
- schema,
219
- relays: ["wss://relay.example.com"],
220
- logLevel: import.meta.env.VITE_LOG_LEVEL ?? "none",
221
- });
222
-
223
- // Svelte API
224
- new Tablinum({
225
- schema,
226
- relays: ["wss://relay.example.com"],
227
- logLevel: import.meta.env.VITE_LOG_LEVEL ?? "none",
228
- });
229
- ```
230
-
231
- ### Log levels
232
-
233
- | Level | What you see |
234
- |-------|-------------|
235
- | `"none"` | Nothing (default) |
236
- | `"error"` | Unrecoverable failures |
237
- | `"warning"` | Recoverable issues (e.g. rejected writes from removed members) |
238
- | `"info"` | Lifecycle milestones — storage opened, identity loaded, sync started/complete |
239
- | `"debug"` | Everything above plus CRUD operations (with record data), relay reconciliation details, gift wrap processing |
240
-
241
- ### Log spans
242
-
243
- Key operations include timing spans that appear automatically in log output:
244
-
245
- ```
246
- [11:50:31] INFO (#1) tablinum.init=13ms: Tablinum ready { ... }
247
- [11:50:41] INFO (#1) tablinum.sync=520ms: Sync complete { changed: ["todos"] }
248
- ```
249
-
250
- Spans: `tablinum.init`, `tablinum.sync`, `tablinum.syncRelay`, `tablinum.negentropy`.
251
-
252
- ### Using Effect's LogLevel type
253
-
254
- Power users can also pass Effect's `LogLevel` type directly:
255
-
256
- ```typescript
257
- import { LogLevel } from "tablinum";
258
- createTablinum({ ..., logLevel: LogLevel.Debug });
259
- ```
260
-
261
- ## How it works
262
-
263
- Tablinum is built on [Effect](https://effect.website) and stores all data locally in [IndexedDB](https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API).
264
-
265
- 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.
266
-
267
- ### Picking a relay
268
-
269
- Tablinum syncs through Nostr relays that support [NIP-77 (Negentropy)](https://github.com/nostr-protocol/nips/blob/master/77.md). You have two options:
270
-
271
- - **Use a public relay** — find NIP-77 compatible relays at [nostrwat.ch](https://nostrwat.ch)
272
- - **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
273
-
274
- ## Development
275
-
276
- ### Local relays
277
-
278
- To run the example app or integration tests against local relays, spin up three [strfry](https://github.com/hoytech/strfry) instances with Docker:
279
-
280
- ```bash
281
- bun run relays # starts 3 relays on ports 7984, 7985, 7986
282
- bun run relays:down # stop relays (data is preserved)
283
- bun run relays:clean # stop relays and delete all data
284
- ```
285
-
286
- The first run builds strfry from source, which takes a few minutes. Subsequent starts are instant.
287
-
288
- Then point your app at the local relays:
289
-
290
- ```typescript
291
- relays: ["ws://localhost:7984", "ws://localhost:7985", "ws://localhost:7986"]
292
- ```
293
-
294
- The Svelte example app (`bun run demo:svelte`) is pre-configured to use these local relays.
295
-
296
- ## License
297
-
298
- MIT