@nativewindow/tsdb 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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Francesco Saverio Cannizzaro
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,111 @@
1
+ # @nativewindow/tsdb
2
+
3
+ [![npm](https://img.shields.io/npm/v/@nativewindow/tsdb)](https://www.npmjs.com/package/@nativewindow/tsdb)
4
+
5
+ > [!WARNING]
6
+ > This project is in **alpha**. APIs may change without notice.
7
+
8
+ [TanStack DB](https://tanstack.com/db) collection adapter for [native-window](https://github.com/nativewindow/webview) IPC. Sync data between the host process and webview collections over the native IPC bridge.
9
+
10
+ ## Install
11
+
12
+ ```bash
13
+ bun add @nativewindow/tsdb
14
+ # or
15
+ deno add npm:@nativewindow/tsdb
16
+ ```
17
+
18
+ ## Overview
19
+
20
+ Two entry points for the two sides of the bridge:
21
+
22
+ | Entry | Side | Export |
23
+ | --------------------------- | -------------------- | ------------------------------- |
24
+ | `@nativewindow/tsdb` | Host (Bun/Deno/Node) | `createDataSource` |
25
+ | `@nativewindow/tsdb/client` | Webview | `nativeWindowCollectionOptions` |
26
+
27
+ ## Host side (Bun/Deno/Node)
28
+
29
+ Create a data source and sync data to the webview:
30
+
31
+ ```ts
32
+ import { createDataSource } from "@nativewindow/tsdb";
33
+
34
+ const ds = createDataSource(win, {
35
+ channel: "todos",
36
+ getKey: (item) => item.id,
37
+ });
38
+
39
+ // Single operations
40
+ ds.insert({ id: 1, text: "Buy milk", done: false });
41
+ ds.update({ id: 1, text: "Buy milk", done: true });
42
+ ds.delete({ id: 1 });
43
+
44
+ // Batch multiple operations in a single message
45
+ ds.batch((b) => {
46
+ b.insert({ id: 1, text: "Buy milk", done: false });
47
+ b.insert({ id: 2, text: "Walk dog", done: false });
48
+ b.update({ id: 1, text: "Buy milk", done: true });
49
+ });
50
+
51
+ // Send a full snapshot (replaces all data)
52
+ ds.sync([
53
+ { id: 1, text: "Buy milk", done: true },
54
+ { id: 2, text: "Walk dog", done: false },
55
+ ]);
56
+ ```
57
+
58
+ ## Webview side (TanStack DB collection)
59
+
60
+ Use `nativeWindowCollectionOptions` with TanStack DB's `createCollection`:
61
+
62
+ ```ts
63
+ import { createCollection } from "@tanstack/db";
64
+ import { nativeWindowCollectionOptions } from "@nativewindow/tsdb/client";
65
+
66
+ const todos = createCollection(
67
+ nativeWindowCollectionOptions({
68
+ id: "todos",
69
+ channel: "todos",
70
+ getKey: (item) => item.id,
71
+ }),
72
+ );
73
+ ```
74
+
75
+ Works with `useLiveQuery` from `@tanstack/react-db`:
76
+
77
+ ```tsx
78
+ import { useLiveQuery } from "@tanstack/react-db";
79
+
80
+ function TodoList() {
81
+ const todos = useLiveQuery((q) => q.from({ todos }).many());
82
+
83
+ return (
84
+ <ul>
85
+ {todos.map((t) => (
86
+ <li key={t.id}>{t.text}</li>
87
+ ))}
88
+ </ul>
89
+ );
90
+ }
91
+ ```
92
+
93
+ ## Wire Protocol
94
+
95
+ Operations use a single-letter discriminated union over the `{ $ch, p }` IPC envelope:
96
+
97
+ | Type | `t` | Payload |
98
+ | -------- | ----- | --------------------------- |
99
+ | Insert | `"i"` | `{ t: "i", k: TKey, d: T }` |
100
+ | Update | `"u"` | `{ t: "u", k: TKey, d: T }` |
101
+ | Delete | `"d"` | `{ t: "d", k: TKey }` |
102
+ | Snapshot | `"s"` | `{ t: "s", d: T[] }` |
103
+ | Batch | `"b"` | `{ t: "b", o: SyncOp[] }` |
104
+
105
+ ## Documentation
106
+
107
+ Full documentation at [nativewindow.fcannizzaro.com](https://nativewindow.fcannizzaro.com)
108
+
109
+ ## License
110
+
111
+ MIT
@@ -0,0 +1,111 @@
1
+ /**
2
+ * Options for {@link nativeWindowCollectionOptions}.
3
+ *
4
+ * @example
5
+ * ```ts
6
+ * import { createCollection } from "@tanstack/db";
7
+ * import { nativeWindowCollectionOptions } from "@nativewindow/tsdb/client";
8
+ *
9
+ * const todos = createCollection(
10
+ * nativeWindowCollectionOptions<Todo, string>({
11
+ * id: "todos",
12
+ * channel: "tsdb:todos",
13
+ * getKey: (todo) => todo.id,
14
+ * }),
15
+ * );
16
+ * ```
17
+ */
18
+ export interface NativeWindowCollectionConfig<T extends object, TKey extends string | number> {
19
+ /** Collection ID for TanStack DB. */
20
+ id: string;
21
+ /**
22
+ * IPC channel name. Must match the `channel` passed to
23
+ * `createDataSource` on the host side.
24
+ */
25
+ channel: string;
26
+ /** Extract the primary key from an item. */
27
+ getKey: (item: T) => TKey;
28
+ }
29
+ /**
30
+ * Sync parameters provided by TanStack DB's `createCollection`.
31
+ * Declared locally to avoid a runtime dependency on `@tanstack/db`.
32
+ * @internal
33
+ */
34
+ interface SyncParams<T extends object, TKey extends string | number> {
35
+ collection: unknown;
36
+ begin: (options?: {
37
+ immediate?: boolean;
38
+ }) => void;
39
+ write: (message: {
40
+ value: T;
41
+ type: "insert" | "update" | "delete";
42
+ previousValue?: T;
43
+ metadata?: Record<string, unknown>;
44
+ } | {
45
+ key: TKey;
46
+ type: "delete";
47
+ }) => void;
48
+ commit: () => void;
49
+ markReady: () => void;
50
+ truncate: () => void;
51
+ }
52
+ /**
53
+ * Return type of {@link nativeWindowCollectionOptions}.
54
+ * Structurally compatible with `CollectionConfig` from `@tanstack/db`.
55
+ */
56
+ export interface NativeWindowCollectionResult<T extends object, TKey extends string | number> {
57
+ id: string;
58
+ getKey: (item: T) => TKey;
59
+ sync: {
60
+ sync: (params: SyncParams<T, TKey>) => () => void;
61
+ };
62
+ }
63
+ /**
64
+ * Create TanStack DB collection options that sync data from a
65
+ * native-window host process via IPC.
66
+ *
67
+ * The returned config is **read-only** — no `onInsert`/`onUpdate`/
68
+ * `onDelete` handlers. Data flows host to webview only.
69
+ *
70
+ * The sync function prefers `window.__native_message_listeners__`
71
+ * (the external listener registry exposed by `createChannelClient`)
72
+ * when available. This avoids the frozen-property crash caused by
73
+ * assigning to the read-only `window.__native_message__`. When the
74
+ * IPC client is not present, it falls back to direct interposition
75
+ * of `window.__native_message__` with a try/catch guard.
76
+ *
77
+ * @param config - Collection and channel configuration.
78
+ * @returns An object compatible with `createCollection()` from `@tanstack/db`.
79
+ *
80
+ * @example
81
+ * ```ts
82
+ * import { createCollection } from "@tanstack/db";
83
+ * import { useLiveQuery } from "@tanstack/react-db";
84
+ * import { nativeWindowCollectionOptions } from "@nativewindow/tsdb/client";
85
+ *
86
+ * type Todo = { id: string; text: string; done: boolean };
87
+ *
88
+ * const todoCollection = createCollection(
89
+ * nativeWindowCollectionOptions<Todo, string>({
90
+ * id: "todos",
91
+ * channel: "tsdb:todos",
92
+ * getKey: (todo) => todo.id,
93
+ * }),
94
+ * );
95
+ *
96
+ * function TodoList() {
97
+ * const { data: todos } = useLiveQuery((q) =>
98
+ * q.from({ todos: todoCollection }),
99
+ * );
100
+ * return (
101
+ * <ul>
102
+ * {todos.map((todo) => (
103
+ * <li key={todo.id}>{todo.text}</li>
104
+ * ))}
105
+ * </ul>
106
+ * );
107
+ * }
108
+ * ```
109
+ */
110
+ export declare function nativeWindowCollectionOptions<T extends object, TKey extends string | number = string>(config: NativeWindowCollectionConfig<T, TKey>): NativeWindowCollectionResult<T, TKey>;
111
+ export {};
package/dist/client.js ADDED
@@ -0,0 +1,113 @@
1
+ function isEnvelope(data) {
2
+ return typeof data === "object" && data !== null && "$ch" in data && typeof data.$ch === "string";
3
+ }
4
+ function nativeWindowCollectionOptions(config) {
5
+ const { id, channel, getKey } = config;
6
+ return {
7
+ id,
8
+ getKey,
9
+ sync: {
10
+ sync: (params) => {
11
+ const { begin, write, commit, markReady } = params;
12
+ function processPayload(payload) {
13
+ switch (payload.t) {
14
+ case "i":
15
+ begin();
16
+ write({ type: "insert", value: payload.v });
17
+ commit();
18
+ break;
19
+ case "u":
20
+ begin();
21
+ write({ type: "update", value: payload.v });
22
+ commit();
23
+ break;
24
+ case "d":
25
+ begin();
26
+ write({ key: payload.k, type: "delete" });
27
+ commit();
28
+ break;
29
+ case "s":
30
+ begin();
31
+ for (const item of payload.items) {
32
+ write({ type: "insert", value: item.v });
33
+ }
34
+ commit();
35
+ markReady();
36
+ break;
37
+ case "b":
38
+ begin();
39
+ for (const op of payload.ops) {
40
+ switch (op.t) {
41
+ case "i":
42
+ write({ type: "insert", value: op.v });
43
+ break;
44
+ case "u":
45
+ write({ type: "update", value: op.v });
46
+ break;
47
+ case "d":
48
+ write({ key: op.k, type: "delete" });
49
+ break;
50
+ }
51
+ }
52
+ commit();
53
+ break;
54
+ }
55
+ }
56
+ function handler(msg) {
57
+ let parsed;
58
+ try {
59
+ parsed = JSON.parse(msg);
60
+ } catch {
61
+ return;
62
+ }
63
+ if (!isEnvelope(parsed) || parsed.$ch !== channel) return;
64
+ const payload = parsed.p;
65
+ if (!payload || typeof payload.t !== "string") return;
66
+ processPayload(payload);
67
+ }
68
+ const registry = window.__native_message_listeners__;
69
+ if (registry && typeof registry.add === "function") {
70
+ registry.add(handler);
71
+ return () => {
72
+ registry.remove(handler);
73
+ };
74
+ }
75
+ const prev = window.__native_message__;
76
+ function fallbackHandler(msg) {
77
+ let parsed;
78
+ try {
79
+ parsed = JSON.parse(msg);
80
+ } catch {
81
+ prev?.(msg);
82
+ return;
83
+ }
84
+ if (!isEnvelope(parsed) || parsed.$ch !== channel) {
85
+ prev?.(msg);
86
+ return;
87
+ }
88
+ const payload = parsed.p;
89
+ if (!payload || typeof payload.t !== "string") {
90
+ prev?.(msg);
91
+ return;
92
+ }
93
+ processPayload(payload);
94
+ }
95
+ try {
96
+ window.__native_message__ = fallbackHandler;
97
+ } catch {
98
+ }
99
+ return () => {
100
+ try {
101
+ if (window.__native_message__ === fallbackHandler) {
102
+ window.__native_message__ = prev;
103
+ }
104
+ } catch {
105
+ }
106
+ };
107
+ }
108
+ }
109
+ };
110
+ }
111
+ export {
112
+ nativeWindowCollectionOptions
113
+ };
@@ -0,0 +1,150 @@
1
+ /**
2
+ * Minimal interface for sending messages to the webview.
3
+ * Satisfied by {@link NativeWindow} without importing it directly.
4
+ *
5
+ * @example
6
+ * ```ts
7
+ * const win = new NativeWindow();
8
+ * // win satisfies MessageSender
9
+ * ```
10
+ */
11
+ export interface MessageSender {
12
+ postMessage(message: string): void;
13
+ }
14
+ /**
15
+ * Options for {@link createDataSource}.
16
+ *
17
+ * @example
18
+ * ```ts
19
+ * const ds = createDataSource<Todo, string>(win, {
20
+ * channel: "tsdb:todos",
21
+ * getKey: (todo) => todo.id,
22
+ * });
23
+ * ```
24
+ */
25
+ export interface DataSourceOptions<T extends object, TKey extends string | number> {
26
+ /**
27
+ * IPC channel name for this data source.
28
+ * Must match the `channel` passed to `nativeWindowCollectionOptions`
29
+ * on the webview side.
30
+ */
31
+ channel: string;
32
+ /** Extract the primary key from an item. */
33
+ getKey: (item: T) => TKey;
34
+ }
35
+ /**
36
+ * Builder for batched operations passed to {@link DataSource.batch}.
37
+ *
38
+ * @example
39
+ * ```ts
40
+ * ds.batch((b) => {
41
+ * b.insert({ id: "1", text: "A", done: false });
42
+ * b.insert({ id: "2", text: "B", done: false });
43
+ * b.delete("3");
44
+ * });
45
+ * ```
46
+ */
47
+ export interface BatchBuilder<T extends object, TKey extends string | number> {
48
+ /** Queue an insert operation. */
49
+ insert(item: T): void;
50
+ /** Queue a full-item update operation. */
51
+ update(key: TKey, item: T): void;
52
+ /** Queue a delete operation. */
53
+ delete(key: TKey): void;
54
+ }
55
+ /**
56
+ * A host-side data source that pushes mutations to the webview
57
+ * via native-window IPC. Data flows host to webview only.
58
+ *
59
+ * Each mutation updates an internal item map *and* sends an IPC
60
+ * message so the webview collection stays in sync.
61
+ *
62
+ * @example
63
+ * ```ts
64
+ * const ds = createDataSource<Todo, string>(win, {
65
+ * channel: "tsdb:todos",
66
+ * getKey: (todo) => todo.id,
67
+ * });
68
+ *
69
+ * ds.insert({ id: "1", text: "Buy milk", done: false });
70
+ * ds.update("1", { id: "1", text: "Buy oat milk", done: false });
71
+ * ds.delete("1");
72
+ * ```
73
+ */
74
+ export interface DataSource<T extends object, TKey extends string | number> {
75
+ /** Send an insert to the webview and track the item internally. */
76
+ insert(item: T): void;
77
+ /** Send a full-item update to the webview and update internal state. */
78
+ update(key: TKey, item: T): void;
79
+ /** Send a delete to the webview and remove the item internally. */
80
+ delete(key: TKey): void;
81
+ /**
82
+ * Execute multiple operations as a single IPC message.
83
+ * The webview applies them atomically within one begin/commit cycle.
84
+ *
85
+ * @example
86
+ * ```ts
87
+ * ds.batch((b) => {
88
+ * b.insert({ id: "1", text: "A", done: false });
89
+ * b.insert({ id: "2", text: "B", done: false });
90
+ * b.delete("3");
91
+ * });
92
+ * ```
93
+ */
94
+ batch(fn: (builder: BatchBuilder<T, TKey>) => void): void;
95
+ /**
96
+ * Send the current internal state as a full snapshot to the webview.
97
+ * If `items` is provided, replaces the internal state first.
98
+ *
99
+ * Call this after page load to initialize or re-initialize the
100
+ * webview collection (e.g. from an `onPageLoad` handler).
101
+ *
102
+ * @example
103
+ * ```ts
104
+ * // Send current state
105
+ * ds.sync();
106
+ *
107
+ * // Replace internal state and send
108
+ * ds.sync([
109
+ * { id: "1", text: "A", done: false },
110
+ * { id: "2", text: "B", done: true },
111
+ * ]);
112
+ * ```
113
+ */
114
+ sync(items?: T[]): void;
115
+ }
116
+ /**
117
+ * Create a host-side data source that pushes mutations to the
118
+ * webview via native-window IPC.
119
+ *
120
+ * The data source maintains an internal `Map` of all items so it
121
+ * can send full snapshots on demand (e.g. after a page reload).
122
+ * Individual mutations (`insert`, `update`, `delete`) update the
123
+ * map *and* send an incremental IPC message.
124
+ *
125
+ * @param sender - Any object with a `postMessage` method (e.g. a `NativeWindow`).
126
+ * @param options - Data source configuration.
127
+ * @returns A {@link DataSource} with imperative push methods.
128
+ *
129
+ * @example
130
+ * ```ts
131
+ * import { NativeWindow } from "@nativewindow/webview";
132
+ * import { createDataSource } from "@nativewindow/tsdb";
133
+ *
134
+ * const win = new NativeWindow({ title: "My App" });
135
+ * win.loadUrl("http://localhost:5173");
136
+ *
137
+ * const todos = createDataSource<Todo, string>(win, {
138
+ * channel: "tsdb:todos",
139
+ * getKey: (todo) => todo.id,
140
+ * });
141
+ *
142
+ * todos.insert({ id: "1", text: "Hello", done: false });
143
+ *
144
+ * // Re-send full state after page load
145
+ * win.onPageLoad((event) => {
146
+ * if (event === "finished") todos.sync();
147
+ * });
148
+ * ```
149
+ */
150
+ export declare function createDataSource<T extends object, TKey extends string | number = string>(sender: MessageSender, options: DataSourceOptions<T, TKey>): DataSource<T, TKey>;
package/dist/index.js ADDED
@@ -0,0 +1,59 @@
1
+ function createDataSource(sender, options) {
2
+ const { channel, getKey } = options;
3
+ const items = /* @__PURE__ */ new Map();
4
+ function send(payload) {
5
+ sender.postMessage(JSON.stringify({ $ch: channel, p: payload }));
6
+ }
7
+ function sendSnapshot() {
8
+ const entries = [];
9
+ items.forEach((v, k) => entries.push({ k, v }));
10
+ send({ t: "s", items: entries });
11
+ }
12
+ return {
13
+ insert(item) {
14
+ const key = getKey(item);
15
+ items.set(key, item);
16
+ send({ t: "i", k: key, v: item });
17
+ },
18
+ update(key, item) {
19
+ items.set(key, item);
20
+ send({ t: "u", k: key, v: item });
21
+ },
22
+ delete(key) {
23
+ items.delete(key);
24
+ send({ t: "d", k: key });
25
+ },
26
+ batch(fn) {
27
+ const ops = [];
28
+ const builder = {
29
+ insert(item) {
30
+ const key = getKey(item);
31
+ items.set(key, item);
32
+ ops.push({ t: "i", k: key, v: item });
33
+ },
34
+ update(key, item) {
35
+ items.set(key, item);
36
+ ops.push({ t: "u", k: key, v: item });
37
+ },
38
+ delete(key) {
39
+ items.delete(key);
40
+ ops.push({ t: "d", k: key });
41
+ }
42
+ };
43
+ fn(builder);
44
+ if (ops.length > 0) send({ t: "b", ops });
45
+ },
46
+ sync(newItems) {
47
+ if (newItems !== void 0) {
48
+ items.clear();
49
+ for (const item of newItems) {
50
+ items.set(getKey(item), item);
51
+ }
52
+ }
53
+ sendSnapshot();
54
+ }
55
+ };
56
+ }
57
+ export {
58
+ createDataSource
59
+ };
package/package.json ADDED
@@ -0,0 +1,58 @@
1
+ {
2
+ "name": "@nativewindow/tsdb",
3
+ "version": "0.1.1",
4
+ "description": "TanStack DB collection adapter for native-window IPC (alpha)",
5
+ "homepage": "https://nativewindow.fcannizzaro.com",
6
+ "bugs": {
7
+ "url": "https://github.com/nativewindow/webview/issues"
8
+ },
9
+ "license": "MIT",
10
+ "author": {
11
+ "name": "Francesco Saverio Cannizzaro (fcannizzaro)",
12
+ "url": "https://fcannizzaro.com"
13
+ },
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "https://github.com/nativewindow/webview/tree/main/packages/tsdb"
17
+ },
18
+ "funding": [
19
+ {
20
+ "type": "patreon",
21
+ "url": "https://www.patreon.com/fcannizzaro"
22
+ }
23
+ ],
24
+ "files": [
25
+ "dist",
26
+ "README.md",
27
+ "LICENSE"
28
+ ],
29
+ "type": "module",
30
+ "main": "./dist/index.js",
31
+ "types": "./dist/index.d.ts",
32
+ "exports": {
33
+ ".": {
34
+ "types": "./dist/index.d.ts",
35
+ "import": "./dist/index.js"
36
+ },
37
+ "./client": {
38
+ "types": "./dist/client.d.ts",
39
+ "import": "./dist/client.js"
40
+ }
41
+ },
42
+ "scripts": {
43
+ "test": "vitest run",
44
+ "build": "vite build",
45
+ "typecheck": "tsc --noEmit"
46
+ },
47
+ "devDependencies": {
48
+ "@types/bun": "^1.3.9",
49
+ "vite": "^7.3.1",
50
+ "vite-plugin-dts": "^4.5.4",
51
+ "vitest": "^4.0.18",
52
+ "zod": "^4.3.6"
53
+ },
54
+ "peerDependencies": {
55
+ "@nativewindow/webview": "workspace:*",
56
+ "typescript": "^5"
57
+ }
58
+ }