tablinum 0.0.1 → 0.1.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 (99) 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-localstr.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} +2 -22
  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/sync/gift-wrap.d.ts +9 -0
  22. package/dist/sync/negentropy.d.ts +9 -0
  23. package/dist/sync/publish-queue.d.ts +10 -0
  24. package/dist/sync/relay.d.ts +17 -0
  25. package/dist/sync/sync-service.d.ts +14 -0
  26. package/dist/sync/sync-status.d.ts +7 -0
  27. package/dist/utils/uuid.d.ts +2 -0
  28. package/package.json +17 -1
  29. package/.changeset/README.md +0 -8
  30. package/.changeset/config.json +0 -11
  31. package/.context/attachments/pasted_text_2026-03-07_14-02-40.txt +0 -571
  32. package/.context/attachments/pasted_text_2026-03-07_15-48-27.txt +0 -498
  33. package/.context/notes.md +0 -0
  34. package/.context/plans/add-changesets-to-douala-v4.md +0 -48
  35. package/.context/plans/dexie-js-style-query-language-for-localstr.md +0 -115
  36. package/.context/plans/dexie-js-style-query-language-with-per-collection-.md +0 -336
  37. package/.context/plans/implementation-plan-localstr-v0-2.md +0 -263
  38. package/.context/plans/project-init-effect-v4-bun-oxlint-oxfmt-vitest.md +0 -71
  39. package/.context/plans/revise-localstr-prd-v0-2.md +0 -132
  40. package/.context/plans/svelte-5-runes-bindings-for-localstr.md +0 -233
  41. package/.context/todos.md +0 -0
  42. package/.github/workflows/release.yml +0 -36
  43. package/.oxlintrc.json +0 -8
  44. package/bun.lock +0 -705
  45. package/examples/svelte/bun.lock +0 -261
  46. package/examples/svelte/package.json +0 -21
  47. package/examples/svelte/src/app.html +0 -11
  48. package/examples/svelte/src/lib/db.ts +0 -44
  49. package/examples/svelte/src/routes/+page.svelte +0 -322
  50. package/examples/svelte/svelte.config.js +0 -16
  51. package/examples/svelte/tsconfig.json +0 -6
  52. package/examples/svelte/vite.config.ts +0 -6
  53. package/examples/vanilla/app.ts +0 -219
  54. package/examples/vanilla/index.html +0 -144
  55. package/examples/vanilla/serve.ts +0 -42
  56. package/prds/localstr-v0.2.md +0 -221
  57. package/prek.toml +0 -10
  58. package/scripts/validate.ts +0 -392
  59. package/src/crud/collection-handle.ts +0 -189
  60. package/src/crud/query-builder.ts +0 -414
  61. package/src/crud/watch.ts +0 -78
  62. package/src/db/create-localstr.ts +0 -217
  63. package/src/db/database-handle.ts +0 -16
  64. package/src/db/identity.ts +0 -49
  65. package/src/errors.ts +0 -37
  66. package/src/main.ts +0 -10
  67. package/src/schema/collection.ts +0 -53
  68. package/src/schema/field.ts +0 -25
  69. package/src/schema/validate.ts +0 -111
  70. package/src/storage/events-store.ts +0 -24
  71. package/src/storage/giftwraps-store.ts +0 -23
  72. package/src/storage/idb.ts +0 -244
  73. package/src/storage/lww.ts +0 -17
  74. package/src/storage/records-store.ts +0 -76
  75. package/src/svelte/collection.svelte.ts +0 -87
  76. package/src/svelte/database.svelte.ts +0 -83
  77. package/src/svelte/index.svelte.ts +0 -52
  78. package/src/svelte/live-query.svelte.ts +0 -29
  79. package/src/svelte/query.svelte.ts +0 -101
  80. package/src/sync/gift-wrap.ts +0 -33
  81. package/src/sync/negentropy.ts +0 -83
  82. package/src/sync/publish-queue.ts +0 -61
  83. package/src/sync/relay.ts +0 -239
  84. package/src/sync/sync-service.ts +0 -183
  85. package/src/sync/sync-status.ts +0 -17
  86. package/src/utils/uuid.ts +0 -22
  87. package/src/vendor/negentropy.js +0 -616
  88. package/tests/db/create-localstr.test.ts +0 -174
  89. package/tests/db/identity.test.ts +0 -33
  90. package/tests/main.test.ts +0 -9
  91. package/tests/schema/collection.test.ts +0 -27
  92. package/tests/schema/field.test.ts +0 -41
  93. package/tests/schema/validate.test.ts +0 -85
  94. package/tests/setup.ts +0 -1
  95. package/tests/storage/idb.test.ts +0 -144
  96. package/tests/storage/lww.test.ts +0 -33
  97. package/tests/sync/gift-wrap.test.ts +0 -56
  98. package/tsconfig.json +0 -18
  99. package/vitest.config.ts +0 -8
package/README.md CHANGED
@@ -1 +1,77 @@
1
- # localstr
1
+ # tablinum
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).
6
+
7
+ ## Features
8
+
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
15
+
16
+ ## How it works
17
+
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.
19
+
20
+ ## Getting started
21
+
22
+ ### Pick a relay
23
+
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:
25
+
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
28
+
29
+ ### Install
30
+
31
+ ```bash
32
+ npm install tablinum
33
+ ```
34
+
35
+ ### Quick start
36
+
37
+ ```typescript
38
+ import { Effect } from "effect";
39
+ import { createLocalstr, collection, field } from "tablinum";
40
+
41
+ const schema = {
42
+ todos: collection("todos", {
43
+ title: field.string(),
44
+ done: field.boolean(),
45
+ }),
46
+ };
47
+
48
+ const program = Effect.gen(function* () {
49
+ const db = yield* createLocalstr({
50
+ schema,
51
+ relays: ["wss://relay.example.com"],
52
+ });
53
+
54
+ const todos = db.collection("todos");
55
+
56
+ // Create
57
+ const id = yield* todos.add({ title: "Buy milk", done: false });
58
+
59
+ // Read
60
+ const todo = yield* todos.get(id);
61
+
62
+ // Query
63
+ const pending = yield* todos.where("done").equals(false).toArray();
64
+
65
+ // Update
66
+ yield* todos.update(id, { done: true });
67
+
68
+ // Sync across devices
69
+ yield* db.sync();
70
+ });
71
+
72
+ Effect.runPromise(Effect.scoped(program));
73
+ ```
74
+
75
+ ## License
76
+
77
+ MIT
@@ -0,0 +1,21 @@
1
+ import { Effect, Stream } from "effect";
2
+ import type { CollectionDef, CollectionFields } from "../schema/collection.ts";
3
+ import type { InferRecord } from "../schema/types.ts";
4
+ import type { RecordValidator, PartialValidator } from "../schema/validate.ts";
5
+ import type { IDBStorageHandle, StoredEvent } from "../storage/idb.ts";
6
+ import { NotFoundError, StorageError, ValidationError } from "../errors.ts";
7
+ import type { WatchContext } from "./watch.ts";
8
+ import type { WhereClause, OrderByBuilder } from "./query-builder.ts";
9
+ export interface CollectionHandle<C extends CollectionDef<CollectionFields>> {
10
+ readonly add: (data: Omit<InferRecord<C>, "id">) => Effect.Effect<string, ValidationError | StorageError>;
11
+ readonly update: (id: string, data: Partial<Omit<InferRecord<C>, "id">>) => Effect.Effect<void, ValidationError | StorageError | NotFoundError>;
12
+ readonly delete: (id: string) => Effect.Effect<void, StorageError | NotFoundError>;
13
+ readonly get: (id: string) => Effect.Effect<InferRecord<C>, StorageError | NotFoundError>;
14
+ readonly first: () => Effect.Effect<InferRecord<C> | null, StorageError>;
15
+ readonly count: () => Effect.Effect<number, StorageError>;
16
+ readonly watch: () => Stream.Stream<ReadonlyArray<InferRecord<C>>, StorageError>;
17
+ 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>>;
19
+ }
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>;
@@ -0,0 +1,42 @@
1
+ import { Effect, Stream } from "effect";
2
+ import type { IDBStorageHandle } from "../storage/idb.ts";
3
+ import type { StorageError, ValidationError } from "../errors.ts";
4
+ import type { CollectionDef, CollectionFields } from "../schema/collection.ts";
5
+ import type { WatchContext } from "./watch.ts";
6
+ export interface WhereClause<T> {
7
+ readonly equals: (value: string | number | boolean) => QueryBuilder<T>;
8
+ readonly above: (value: number) => QueryBuilder<T>;
9
+ readonly aboveOrEqual: (value: number) => QueryBuilder<T>;
10
+ readonly below: (value: number) => QueryBuilder<T>;
11
+ readonly belowOrEqual: (value: number) => QueryBuilder<T>;
12
+ readonly between: (lower: number, upper: number, options?: {
13
+ includeLower?: boolean;
14
+ includeUpper?: boolean;
15
+ }) => QueryBuilder<T>;
16
+ readonly startsWith: (prefix: string) => QueryBuilder<T>;
17
+ readonly anyOf: (values: ReadonlyArray<string | number | boolean>) => QueryBuilder<T>;
18
+ readonly noneOf: (values: ReadonlyArray<string | number | boolean>) => QueryBuilder<T>;
19
+ }
20
+ export interface QueryBuilder<T> {
21
+ readonly and: (fn: (item: T) => boolean) => QueryBuilder<T>;
22
+ readonly sortBy: (field: string) => QueryBuilder<T>;
23
+ readonly reverse: () => QueryBuilder<T>;
24
+ readonly offset: (n: number) => QueryBuilder<T>;
25
+ readonly limit: (n: number) => QueryBuilder<T>;
26
+ readonly get: () => Effect.Effect<ReadonlyArray<T>, StorageError | ValidationError>;
27
+ readonly first: () => Effect.Effect<T | null, StorageError | ValidationError>;
28
+ readonly count: () => Effect.Effect<number, StorageError | ValidationError>;
29
+ readonly watch: () => Stream.Stream<ReadonlyArray<T>, StorageError | ValidationError>;
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>;
41
+ 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>;
@@ -0,0 +1,25 @@
1
+ import { Effect, PubSub, Ref, Stream } from "effect";
2
+ import type { IDBStorageHandle } from "../storage/idb.ts";
3
+ import type { StorageError } from "../errors.ts";
4
+ export interface ChangeEvent {
5
+ readonly collection: string;
6
+ readonly recordId: string;
7
+ readonly kind: "create" | "update" | "delete";
8
+ }
9
+ export interface WatchContext {
10
+ readonly pubsub: PubSub.PubSub<ChangeEvent>;
11
+ readonly replayingRef: Ref.Ref<boolean>;
12
+ }
13
+ /**
14
+ * Create a reactive Stream that emits the current result set whenever it changes.
15
+ */
16
+ 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
+ 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
+ export declare function notifyReplayComplete(ctx: WatchContext, collections: ReadonlyArray<string>): Effect.Effect<void>;
@@ -0,0 +1,12 @@
1
+ import { Effect, Scope } from "effect";
2
+ import type { SchemaConfig } from "../schema/types.ts";
3
+ import type { DatabaseHandle } from "./database-handle.ts";
4
+ import { CryptoError, StorageError, ValidationError } from "../errors.ts";
5
+ export interface LocalstrConfig<S extends SchemaConfig> {
6
+ readonly schema: S;
7
+ readonly relays: readonly string[];
8
+ readonly privateKey?: Uint8Array | undefined;
9
+ readonly dbName?: string | undefined;
10
+ readonly onSyncError?: ((error: Error) => void) | undefined;
11
+ }
12
+ export declare function createLocalstr<S extends SchemaConfig>(config: LocalstrConfig<S>): Effect.Effect<DatabaseHandle<S>, ValidationError | StorageError | CryptoError, Scope.Scope>;
@@ -0,0 +1,13 @@
1
+ import type { Effect } from "effect";
2
+ import type { CollectionHandle } from "../crud/collection-handle.ts";
3
+ import type { SchemaConfig } from "../schema/types.ts";
4
+ import type { StorageError, SyncError, RelayError, CryptoError } from "../errors.ts";
5
+ export type SyncStatus = "idle" | "syncing";
6
+ export interface DatabaseHandle<S extends SchemaConfig> {
7
+ readonly collection: <K extends string & keyof S>(name: K) => CollectionHandle<S[K]>;
8
+ readonly exportKey: () => string;
9
+ readonly close: () => Effect.Effect<void, StorageError>;
10
+ readonly rebuild: () => Effect.Effect<void, StorageError>;
11
+ readonly sync: () => Effect.Effect<void, SyncError | RelayError | CryptoError | StorageError>;
12
+ readonly getSyncStatus: () => Effect.Effect<SyncStatus>;
13
+ }
@@ -0,0 +1,8 @@
1
+ import { Effect } from "effect";
2
+ import { CryptoError } from "../errors.ts";
3
+ export interface Identity {
4
+ readonly privateKey: Uint8Array;
5
+ readonly publicKey: string;
6
+ readonly exportKey: () => string;
7
+ }
8
+ export declare function createIdentity(suppliedKey?: Uint8Array): Effect.Effect<Identity, CryptoError>;
@@ -0,0 +1,58 @@
1
+ declare const ValidationError_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => import("effect/Cause").YieldableError & {
2
+ readonly _tag: "ValidationError";
3
+ } & Readonly<A>;
4
+ export declare class ValidationError extends ValidationError_base<{
5
+ readonly message: string;
6
+ readonly field?: string | undefined;
7
+ }> {
8
+ }
9
+ declare const StorageError_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => import("effect/Cause").YieldableError & {
10
+ readonly _tag: "StorageError";
11
+ } & Readonly<A>;
12
+ export declare class StorageError extends StorageError_base<{
13
+ readonly message: string;
14
+ readonly cause?: unknown;
15
+ }> {
16
+ }
17
+ declare const CryptoError_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => import("effect/Cause").YieldableError & {
18
+ readonly _tag: "CryptoError";
19
+ } & Readonly<A>;
20
+ export declare class CryptoError extends CryptoError_base<{
21
+ readonly message: string;
22
+ readonly cause?: unknown;
23
+ }> {
24
+ }
25
+ declare const RelayError_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => import("effect/Cause").YieldableError & {
26
+ readonly _tag: "RelayError";
27
+ } & Readonly<A>;
28
+ export declare class RelayError extends RelayError_base<{
29
+ readonly message: string;
30
+ readonly url?: string | undefined;
31
+ readonly cause?: unknown;
32
+ }> {
33
+ }
34
+ declare const SyncError_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => import("effect/Cause").YieldableError & {
35
+ readonly _tag: "SyncError";
36
+ } & Readonly<A>;
37
+ export declare class SyncError extends SyncError_base<{
38
+ readonly message: string;
39
+ readonly phase?: string | undefined;
40
+ readonly cause?: unknown;
41
+ }> {
42
+ }
43
+ declare const NotFoundError_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => import("effect/Cause").YieldableError & {
44
+ readonly _tag: "NotFoundError";
45
+ } & Readonly<A>;
46
+ export declare class NotFoundError extends NotFoundError_base<{
47
+ readonly collection: string;
48
+ readonly id: string;
49
+ }> {
50
+ }
51
+ declare const ClosedError_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => import("effect/Cause").YieldableError & {
52
+ readonly _tag: "ClosedError";
53
+ } & Readonly<A>;
54
+ export declare class ClosedError extends ClosedError_base<{
55
+ readonly message: string;
56
+ }> {
57
+ }
58
+ export {};
@@ -1,32 +1,12 @@
1
- // Schema
2
1
  export { field } from "./schema/field.ts";
3
2
  export { collection } from "./schema/collection.ts";
4
3
  export type { CollectionDef, CollectionFields } from "./schema/collection.ts";
5
4
  export type { FieldDef, FieldKind } from "./schema/field.ts";
6
5
  export type { InferRecord, SchemaConfig } from "./schema/types.ts";
7
-
8
- // Database
9
6
  export { createLocalstr } from "./db/create-localstr.ts";
10
7
  export type { LocalstrConfig } from "./db/create-localstr.ts";
11
8
  export type { DatabaseHandle, SyncStatus } from "./db/database-handle.ts";
12
-
13
- // CRUD
14
9
  export type { CollectionHandle } from "./crud/collection-handle.ts";
15
10
  export type { CollectionOptions } from "./schema/collection.ts";
16
- export type {
17
- WhereClause,
18
- QueryBuilder,
19
- OrderByBuilder,
20
- QueryExecutor,
21
- } from "./crud/query-builder.ts";
22
-
23
- // Errors
24
- export {
25
- ValidationError,
26
- StorageError,
27
- CryptoError,
28
- RelayError,
29
- SyncError,
30
- NotFoundError,
31
- ClosedError,
32
- } from "./errors.ts";
11
+ export type { WhereClause, QueryBuilder, OrderByBuilder, QueryExecutor, } from "./crud/query-builder.ts";
12
+ export { ValidationError, StorageError, CryptoError, RelayError, SyncError, NotFoundError, ClosedError, } from "./errors.ts";