tablinum 0.2.0 → 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 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";
@@ -72,11 +81,9 @@ 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.
78
-
79
- This API uses Svelte's async runes support, so enable it in your app config:
86
+ Import from `tablinum/svelte` for reactive bindings. This API uses Svelte's async runes support, so enable it in your app config:
80
87
 
81
88
  ```js
82
89
  // svelte.config.js
@@ -89,9 +96,7 @@ const config = {
89
96
  };
90
97
  ```
91
98
 
92
- ### Setup
93
-
94
- Create a database helper that defines your schema and initializes the database:
99
+ Create a database helper:
95
100
 
96
101
  ```typescript
97
102
  // src/lib/db.ts
@@ -118,9 +123,7 @@ export const db = new Tablinum({
118
123
  export const todos = db.collection("todos");
119
124
  ```
120
125
 
121
- ### Component
122
-
123
- Async collection reads are reactive when used inside `$derived(await ...)` expressions.
126
+ Use it in a component:
124
127
 
125
128
  ```svelte
126
129
  <script lang="ts">
@@ -188,13 +191,48 @@ Async collection reads are reactive when used inside `$derived(await ...)` expre
188
191
  </svelte:boundary>
189
192
  ```
190
193
 
191
- ### Key concepts
194
+ #### Key concepts
192
195
 
193
196
  - **`new Tablinum(config)`** starts initialization immediately and exposes `db.ready`
194
197
  - **Async queries are reactive** when used inside `$derived(await ...)`
195
198
  - **`db.status`** tracks initialization and terminal state; **`db.syncStatus`** tracks sync activity
196
199
  - **`createTablinum(config)`** still exists as a convenience and resolves once `db.ready` completes
197
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
+ ```
233
+
234
+ The Svelte example app (`bun run demo:svelte`) is pre-configured to use these local relays.
235
+
198
236
  ## License
199
237
 
200
238
  MIT
@@ -6,10 +6,12 @@ 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
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
16
  readonly first: () => Effect.Effect<Option.Option<InferRecord<C>>, StorageError>;
15
17
  readonly count: () => Effect.Effect<number, StorageError>;
@@ -18,4 +20,4 @@ export interface CollectionHandle<C extends CollectionDef<CollectionFields>> {
18
20
  readonly orderBy: (field: string & keyof Omit<InferRecord<C>, "id">) => QueryBuilder<InferRecord<C>>;
19
21
  }
20
22
  export type OnWriteCallback = (event: StoredEvent) => Effect.Effect<void, StorageError>;
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>;
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>;
@@ -4,7 +4,7 @@ import type { CollectionHandle } from "../crud/collection-handle.ts";
4
4
  import type { SchemaConfig } from "../schema/types.ts";
5
5
  import type { StorageError, SyncError, RelayError, CryptoError, ValidationError } from "../errors.ts";
6
6
  import type { Invite } from "./invite.ts";
7
- import type { MemberRecord } from "./members.ts";
7
+ import type { MemberRecord, AuthorProfile } from "./members.ts";
8
8
  import type { RelayStatus } from "../sync/relay.ts";
9
9
  export type SyncStatus = "idle" | "syncing";
10
10
  export interface DatabaseHandle<S extends SchemaConfig> {
@@ -25,6 +25,7 @@ export interface DatabaseHandle<S extends SchemaConfig> {
25
25
  readonly addMember: (pubkey: string) => Effect.Effect<void, ValidationError | StorageError | CryptoError>;
26
26
  readonly removeMember: (pubkey: string) => Effect.Effect<void, ValidationError | StorageError | SyncError | RelayError | CryptoError>;
27
27
  readonly getMembers: () => Effect.Effect<ReadonlyArray<MemberRecord>, StorageError>;
28
+ readonly getProfile: () => Effect.Effect<AuthorProfile, StorageError>;
28
29
  readonly setProfile: (profile: {
29
30
  name?: string;
30
31
  picture?: string;
package/dist/index.d.ts CHANGED
@@ -10,7 +10,7 @@ export { encodeInvite, decodeInvite } from "./db/invite.ts";
10
10
  export type { Invite } from "./db/invite.ts";
11
11
  export { EpochId, DatabaseName } from "./brands.ts";
12
12
  export type { EpochKey, EpochKeyInput } from "./db/epoch.ts";
13
- export type { MemberRecord } from "./db/members.ts";
13
+ export type { MemberRecord, AuthorProfile } from "./db/members.ts";
14
14
  export type { CollectionHandle } from "./crud/collection-handle.ts";
15
15
  export type { CollectionOptions } from "./schema/collection.ts";
16
16
  export type { WhereClause, QueryBuilder, OrderByBuilder } from "./crud/query-builder.ts";