@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 +21 -0
- package/README.md +111 -0
- package/dist/client.d.ts +111 -0
- package/dist/client.js +113 -0
- package/dist/index.d.ts +150 -0
- package/dist/index.js +59 -0
- package/package.json +58 -0
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
|
+
[](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
|
package/dist/client.d.ts
ADDED
|
@@ -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
|
+
};
|
package/dist/index.d.ts
ADDED
|
@@ -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
|
+
}
|