tablinum 0.0.1 → 0.1.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.
Files changed (105) 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-tablinum.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} +4 -24
  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/svelte/collection.svelte.d.ts +20 -0
  22. package/dist/svelte/database.svelte.d.ts +15 -0
  23. package/dist/svelte/index.svelte.d.ts +16 -0
  24. package/dist/svelte/index.svelte.js +2050 -0
  25. package/dist/svelte/live-query.svelte.d.ts +8 -0
  26. package/dist/svelte/query.svelte.d.ts +39 -0
  27. package/dist/sync/gift-wrap.d.ts +9 -0
  28. package/dist/sync/negentropy.d.ts +9 -0
  29. package/dist/sync/publish-queue.d.ts +10 -0
  30. package/dist/sync/relay.d.ts +17 -0
  31. package/dist/sync/sync-service.d.ts +14 -0
  32. package/dist/sync/sync-status.d.ts +7 -0
  33. package/dist/utils/uuid.d.ts +2 -0
  34. package/package.json +22 -1
  35. package/.changeset/README.md +0 -8
  36. package/.changeset/config.json +0 -11
  37. package/.context/attachments/pasted_text_2026-03-07_14-02-40.txt +0 -571
  38. package/.context/attachments/pasted_text_2026-03-07_15-48-27.txt +0 -498
  39. package/.context/notes.md +0 -0
  40. package/.context/plans/add-changesets-to-douala-v4.md +0 -48
  41. package/.context/plans/dexie-js-style-query-language-for-localstr.md +0 -115
  42. package/.context/plans/dexie-js-style-query-language-with-per-collection-.md +0 -336
  43. package/.context/plans/implementation-plan-localstr-v0-2.md +0 -263
  44. package/.context/plans/project-init-effect-v4-bun-oxlint-oxfmt-vitest.md +0 -71
  45. package/.context/plans/revise-localstr-prd-v0-2.md +0 -132
  46. package/.context/plans/svelte-5-runes-bindings-for-localstr.md +0 -233
  47. package/.context/todos.md +0 -0
  48. package/.github/workflows/release.yml +0 -36
  49. package/.oxlintrc.json +0 -8
  50. package/bun.lock +0 -705
  51. package/examples/svelte/bun.lock +0 -261
  52. package/examples/svelte/package.json +0 -21
  53. package/examples/svelte/src/app.html +0 -11
  54. package/examples/svelte/src/lib/db.ts +0 -44
  55. package/examples/svelte/src/routes/+page.svelte +0 -322
  56. package/examples/svelte/svelte.config.js +0 -16
  57. package/examples/svelte/tsconfig.json +0 -6
  58. package/examples/svelte/vite.config.ts +0 -6
  59. package/examples/vanilla/app.ts +0 -219
  60. package/examples/vanilla/index.html +0 -144
  61. package/examples/vanilla/serve.ts +0 -42
  62. package/prds/localstr-v0.2.md +0 -221
  63. package/prek.toml +0 -10
  64. package/scripts/validate.ts +0 -392
  65. package/src/crud/collection-handle.ts +0 -189
  66. package/src/crud/query-builder.ts +0 -414
  67. package/src/crud/watch.ts +0 -78
  68. package/src/db/create-localstr.ts +0 -217
  69. package/src/db/database-handle.ts +0 -16
  70. package/src/db/identity.ts +0 -49
  71. package/src/errors.ts +0 -37
  72. package/src/main.ts +0 -10
  73. package/src/schema/collection.ts +0 -53
  74. package/src/schema/field.ts +0 -25
  75. package/src/schema/validate.ts +0 -111
  76. package/src/storage/events-store.ts +0 -24
  77. package/src/storage/giftwraps-store.ts +0 -23
  78. package/src/storage/idb.ts +0 -244
  79. package/src/storage/lww.ts +0 -17
  80. package/src/storage/records-store.ts +0 -76
  81. package/src/svelte/collection.svelte.ts +0 -87
  82. package/src/svelte/database.svelte.ts +0 -83
  83. package/src/svelte/index.svelte.ts +0 -52
  84. package/src/svelte/live-query.svelte.ts +0 -29
  85. package/src/svelte/query.svelte.ts +0 -101
  86. package/src/sync/gift-wrap.ts +0 -33
  87. package/src/sync/negentropy.ts +0 -83
  88. package/src/sync/publish-queue.ts +0 -61
  89. package/src/sync/relay.ts +0 -239
  90. package/src/sync/sync-service.ts +0 -183
  91. package/src/sync/sync-status.ts +0 -17
  92. package/src/utils/uuid.ts +0 -22
  93. package/src/vendor/negentropy.js +0 -616
  94. package/tests/db/create-localstr.test.ts +0 -174
  95. package/tests/db/identity.test.ts +0 -33
  96. package/tests/main.test.ts +0 -9
  97. package/tests/schema/collection.test.ts +0 -27
  98. package/tests/schema/field.test.ts +0 -41
  99. package/tests/schema/validate.test.ts +0 -85
  100. package/tests/setup.ts +0 -1
  101. package/tests/storage/idb.test.ts +0 -144
  102. package/tests/storage/lww.test.ts +0 -33
  103. package/tests/sync/gift-wrap.test.ts +0 -56
  104. package/tsconfig.json +0 -18
  105. 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 { createTablinum, 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* createTablinum({
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 TablinumConfig<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 createTablinum<S extends SchemaConfig>(config: TablinumConfig<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
- export { createLocalstr } from "./db/create-localstr.ts";
10
- export type { LocalstrConfig } from "./db/create-localstr.ts";
6
+ export { createTablinum } from "./db/create-tablinum.ts";
7
+ export type { TablinumConfig } from "./db/create-tablinum.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";