@nativewindow/tsdb 1.0.1 → 1.0.3

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 (3) hide show
  1. package/dist/client.js +169 -109
  2. package/dist/index.js +122 -57
  3. package/package.json +5 -5
package/dist/client.js CHANGED
@@ -1,113 +1,173 @@
1
+ //#region client.ts
2
+ /** @internal Type guard for IPC envelope. */
1
3
  function isEnvelope(data) {
2
- return typeof data === "object" && data !== null && "$ch" in data && typeof data.$ch === "string";
4
+ return typeof data === "object" && data !== null && "$ch" in data && typeof data.$ch === "string";
3
5
  }
6
+ /**
7
+ * Create TanStack DB collection options that sync data from a
8
+ * native-window host process via IPC.
9
+ *
10
+ * The returned config is **read-only** — no `onInsert`/`onUpdate`/
11
+ * `onDelete` handlers. Data flows host to webview only.
12
+ *
13
+ * The sync function prefers `window.__native_message_listeners__`
14
+ * (the external listener registry exposed by `createChannelClient`)
15
+ * when available. This avoids the frozen-property crash caused by
16
+ * assigning to the read-only `window.__native_message__`. When the
17
+ * IPC client is not present, it falls back to direct interposition
18
+ * of `window.__native_message__` with a try/catch guard.
19
+ *
20
+ * @param config - Collection and channel configuration.
21
+ * @returns An object compatible with `createCollection()` from `@tanstack/db`.
22
+ *
23
+ * @example
24
+ * ```ts
25
+ * import { createCollection } from "@tanstack/db";
26
+ * import { useLiveQuery } from "@tanstack/react-db";
27
+ * import { nativeWindowCollectionOptions } from "@nativewindow/tsdb/client";
28
+ *
29
+ * type Todo = { id: string; text: string; done: boolean };
30
+ *
31
+ * const todoCollection = createCollection(
32
+ * nativeWindowCollectionOptions<Todo, string>({
33
+ * id: "todos",
34
+ * channel: "tsdb:todos",
35
+ * getKey: (todo) => todo.id,
36
+ * }),
37
+ * );
38
+ *
39
+ * function TodoList() {
40
+ * const { data: todos } = useLiveQuery((q) =>
41
+ * q.from({ todos: todoCollection }),
42
+ * );
43
+ * return (
44
+ * <ul>
45
+ * {todos.map((todo) => (
46
+ * <li key={todo.id}>{todo.text}</li>
47
+ * ))}
48
+ * </ul>
49
+ * );
50
+ * }
51
+ * ```
52
+ */
4
53
  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
- };
54
+ const { id, channel, getKey } = config;
55
+ return {
56
+ id,
57
+ getKey,
58
+ sync: { sync: (params) => {
59
+ const { begin, write, commit, markReady } = params;
60
+ function processPayload(payload) {
61
+ switch (payload.t) {
62
+ case "i":
63
+ begin();
64
+ write({
65
+ type: "insert",
66
+ value: payload.v
67
+ });
68
+ commit();
69
+ break;
70
+ case "u":
71
+ begin();
72
+ write({
73
+ type: "update",
74
+ value: payload.v
75
+ });
76
+ commit();
77
+ break;
78
+ case "d":
79
+ begin();
80
+ write({
81
+ key: payload.k,
82
+ type: "delete"
83
+ });
84
+ commit();
85
+ break;
86
+ case "s":
87
+ begin();
88
+ for (const item of payload.items) write({
89
+ type: "insert",
90
+ value: item.v
91
+ });
92
+ commit();
93
+ markReady();
94
+ break;
95
+ case "b":
96
+ begin();
97
+ for (const op of payload.ops) switch (op.t) {
98
+ case "i":
99
+ write({
100
+ type: "insert",
101
+ value: op.v
102
+ });
103
+ break;
104
+ case "u":
105
+ write({
106
+ type: "update",
107
+ value: op.v
108
+ });
109
+ break;
110
+ case "d":
111
+ write({
112
+ key: op.k,
113
+ type: "delete"
114
+ });
115
+ break;
116
+ }
117
+ commit();
118
+ break;
119
+ }
120
+ }
121
+ /** Handler that filters for our channel and dispatches. */
122
+ function handler(msg) {
123
+ let parsed;
124
+ try {
125
+ parsed = JSON.parse(msg);
126
+ } catch {
127
+ return;
128
+ }
129
+ if (!isEnvelope(parsed) || parsed.$ch !== channel) return;
130
+ const payload = parsed.p;
131
+ if (!payload || typeof payload.t !== "string") return;
132
+ processPayload(payload);
133
+ }
134
+ const registry = window.__native_message_listeners__;
135
+ if (registry && typeof registry.add === "function") {
136
+ registry.add(handler);
137
+ return () => {
138
+ registry.remove(handler);
139
+ };
140
+ }
141
+ const prev = window.__native_message__;
142
+ function fallbackHandler(msg) {
143
+ let parsed;
144
+ try {
145
+ parsed = JSON.parse(msg);
146
+ } catch {
147
+ prev?.(msg);
148
+ return;
149
+ }
150
+ if (!isEnvelope(parsed) || parsed.$ch !== channel) {
151
+ prev?.(msg);
152
+ return;
153
+ }
154
+ const payload = parsed.p;
155
+ if (!payload || typeof payload.t !== "string") {
156
+ prev?.(msg);
157
+ return;
158
+ }
159
+ processPayload(payload);
160
+ }
161
+ try {
162
+ window.__native_message__ = fallbackHandler;
163
+ } catch {}
164
+ return () => {
165
+ try {
166
+ if (window.__native_message__ === fallbackHandler) window.__native_message__ = prev;
167
+ } catch {}
168
+ };
169
+ } }
170
+ };
110
171
  }
111
- export {
112
- nativeWindowCollectionOptions
113
- };
172
+ //#endregion
173
+ export { nativeWindowCollectionOptions };
package/dist/index.js CHANGED
@@ -1,59 +1,124 @@
1
+ //#region index.ts
2
+ /**
3
+ * Create a host-side data source that pushes mutations to the
4
+ * webview via native-window IPC.
5
+ *
6
+ * The data source maintains an internal `Map` of all items so it
7
+ * can send full snapshots on demand (e.g. after a page reload).
8
+ * Individual mutations (`insert`, `update`, `delete`) update the
9
+ * map *and* send an incremental IPC message.
10
+ *
11
+ * @param sender - Any object with a `postMessage` method (e.g. a `NativeWindow`).
12
+ * @param options - Data source configuration.
13
+ * @returns A {@link DataSource} with imperative push methods.
14
+ *
15
+ * @example
16
+ * ```ts
17
+ * import { NativeWindow } from "@nativewindow/webview";
18
+ * import { createDataSource } from "@nativewindow/tsdb";
19
+ *
20
+ * const win = new NativeWindow({ title: "My App" });
21
+ * win.loadUrl("http://localhost:5173");
22
+ *
23
+ * const todos = createDataSource<Todo, string>(win, {
24
+ * channel: "tsdb:todos",
25
+ * getKey: (todo) => todo.id,
26
+ * });
27
+ *
28
+ * todos.insert({ id: "1", text: "Hello", done: false });
29
+ *
30
+ * // Re-send full state after page load
31
+ * win.onPageLoad((event) => {
32
+ * if (event === "finished") todos.sync();
33
+ * });
34
+ * ```
35
+ */
1
36
  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
- };
37
+ const { channel, getKey } = options;
38
+ const items = /* @__PURE__ */ new Map();
39
+ function send(payload) {
40
+ sender.postMessage(JSON.stringify({
41
+ $ch: channel,
42
+ p: payload
43
+ }));
44
+ }
45
+ function sendSnapshot() {
46
+ const entries = [];
47
+ items.forEach((v, k) => entries.push({
48
+ k,
49
+ v
50
+ }));
51
+ send({
52
+ t: "s",
53
+ items: entries
54
+ });
55
+ }
56
+ return {
57
+ insert(item) {
58
+ const key = getKey(item);
59
+ items.set(key, item);
60
+ send({
61
+ t: "i",
62
+ k: key,
63
+ v: item
64
+ });
65
+ },
66
+ update(key, item) {
67
+ items.set(key, item);
68
+ send({
69
+ t: "u",
70
+ k: key,
71
+ v: item
72
+ });
73
+ },
74
+ delete(key) {
75
+ items.delete(key);
76
+ send({
77
+ t: "d",
78
+ k: key
79
+ });
80
+ },
81
+ batch(fn) {
82
+ const ops = [];
83
+ fn({
84
+ insert(item) {
85
+ const key = getKey(item);
86
+ items.set(key, item);
87
+ ops.push({
88
+ t: "i",
89
+ k: key,
90
+ v: item
91
+ });
92
+ },
93
+ update(key, item) {
94
+ items.set(key, item);
95
+ ops.push({
96
+ t: "u",
97
+ k: key,
98
+ v: item
99
+ });
100
+ },
101
+ delete(key) {
102
+ items.delete(key);
103
+ ops.push({
104
+ t: "d",
105
+ k: key
106
+ });
107
+ }
108
+ });
109
+ if (ops.length > 0) send({
110
+ t: "b",
111
+ ops
112
+ });
113
+ },
114
+ sync(newItems) {
115
+ if (newItems !== void 0) {
116
+ items.clear();
117
+ for (const item of newItems) items.set(getKey(item), item);
118
+ }
119
+ sendSnapshot();
120
+ }
121
+ };
56
122
  }
57
- export {
58
- createDataSource
59
- };
123
+ //#endregion
124
+ export { createDataSource };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nativewindow/tsdb",
3
- "version": "1.0.1",
3
+ "version": "1.0.3",
4
4
  "description": "TanStack DB collection adapter for native-window IPC (beta)",
5
5
  "homepage": "https://nativewindow.fcannizzaro.com",
6
6
  "bugs": {
@@ -46,13 +46,13 @@
46
46
  },
47
47
  "devDependencies": {
48
48
  "@types/bun": "^1.3.9",
49
- "vite": "^7.3.1",
49
+ "vite": "^8.0.3",
50
50
  "vite-plugin-dts": "^4.5.4",
51
- "vitest": "^4.0.18",
51
+ "vitest": "^4.1.2",
52
52
  "zod": "^4.3.6"
53
53
  },
54
54
  "peerDependencies": {
55
- "@nativewindow/webview": "workspace:*",
56
- "typescript": "^5"
55
+ "@nativewindow/webview": "^1.0.0",
56
+ "typescript": "^6.0.2"
57
57
  }
58
58
  }