This project is in alpha — APIs may change without notice.
@nativewindow/webview

TanStack DB Adapter

Sync host-side data to TanStack DB collections in the webview

Overview

The @nativewindow/tsdb package bridges host-side data to TanStack DB collections in the webview. It provides:

  • Imperative push API on the host — insert, update, delete, batch, and sync
  • Collection options creator on the webview — plug directly into createCollection() from TanStack DB
  • Read-only data flow — data flows host to webview only; the webview collection is a reactive read-only view
  • Automatic snapshot/resync — call sync() after page load to re-initialize the webview collection

The package is pure TypeScript with zero runtime dependencies. It uses the same { $ch, p } envelope format as the Typed IPC layer and works alongside typed channels on the same window.

Two entry points are provided:

Entry PointSideDescription
@nativewindow/tsdbHost (Bun/Deno/Node.js)createDataSource() — push mutations to the webview
@nativewindow/tsdb/clientWebview (browser)nativeWindowCollectionOptions() — TanStack DB collection config

Installation

bun add @nativewindow/tsdb
# or
deno add npm:@nativewindow/tsdb

In your webview app, install TanStack DB separately:

bun add @tanstack/db @tanstack/react-db
# or
deno add npm:@tanstack/db npm:@tanstack/react-db

Note: @tanstack/db is not a peer dependency of the TSDB package — it is only needed in the webview app's own dependencies. The adapter uses a structural interface compatible with TanStack DB's createCollection() without importing it.

Host Side

createDataSource

Create a data source bound to a window (or any object with a postMessage method):

import { NativeWindow } from "@nativewindow/webview";
import { createDataSource } from "@nativewindow/tsdb";

type Todo = { id: string; text: string; done: boolean };

const win = new NativeWindow({ title: "My App" });
win.loadUrl("http://localhost:5173");

const todos = createDataSource<Todo, string>(win, {
  channel: "tsdb:todos",
  getKey: (todo) => todo.id,
});

// Push data to the webview
todos.insert({ id: "1", text: "Buy milk", done: false });
todos.update("1", { id: "1", text: "Buy oat milk", done: false });
todos.delete("1");

// Re-send full state after page load
win.onPageLoad((event) => {
  if (event === "finished") todos.sync();
});

The sender parameter accepts any object implementing the MessageSender interface ({ postMessage(message: string): void }). NativeWindow satisfies this interface.

OptionTypeDescription
channelstringIPC channel name (e.g. "tsdb:todos"). Must match the webview side.
getKey(item: T) => TKeyExtract the primary key from an item.

DataSource API

MethodDescription
insert(item)Send an insert to the webview and track the item internally.
update(key, item)Send a full-item update to the webview and update internal state.
delete(key)Send a delete to the webview and remove the item internally.
batch(fn)Execute multiple operations as a single IPC message (see below).
sync(items?)Send the current internal state as a full snapshot. If items is provided, replaces the internal state first.

Batch Operations

Use batch() to send multiple operations in a single IPC message. The webview applies them atomically within one begin/commit cycle:

todos.batch((b) => {
  b.insert({ id: "1", text: "A", done: false });
  b.insert({ id: "2", text: "B", done: false });
  b.delete("3");
});

If the callback produces no operations, no message is sent.

Snapshots with sync()

sync() sends the data source's current internal state as a full snapshot to the webview. The webview collection replaces its contents and calls markReady(), signaling to TanStack DB that the initial data is available.

// Send current state
todos.sync();

// Replace internal state and send
todos.sync([
  { id: "1", text: "A", done: false },
  { id: "2", text: "B", done: true },
]);

Typical usage: call sync() from an onPageLoad("finished") handler to re-initialize the webview collection after navigation or reload. The data source maintains an internal Map of all items, so you do not need to track items externally.

Webview Side

nativeWindowCollectionOptions

Create collection options that plug into TanStack DB's createCollection():

import { createCollection } from "@tanstack/db";
import { nativeWindowCollectionOptions } from "@nativewindow/tsdb/client";

type Todo = { id: string; text: string; done: boolean };

const todoCollection = createCollection(
  nativeWindowCollectionOptions<Todo, string>({
    id: "todos",
    channel: "tsdb:todos",
    getKey: (todo) => todo.id,
  }),
);
OptionTypeDescription
idstringCollection ID for TanStack DB.
channelstringIPC channel name. Must match the channel passed to createDataSource on the host side.
getKey(item: T) => TKeyExtract the primary key from an item.

The returned config is read-only — no onInsert/onUpdate/onDelete mutation handlers. Data flows host to webview only. TanStack DB manages the subscribe/unsubscribe lifecycle automatically via the cleanup function returned by the sync callback.

Using with TanStack DB and React

Combine with useLiveQuery from @tanstack/react-db for reactive rendering:

import { useLiveQuery } from "@tanstack/react-db";
import { createCollection } from "@tanstack/db";
import { nativeWindowCollectionOptions } from "@nativewindow/tsdb/client";

type Todo = { id: string; text: string; done: boolean };

const todoCollection = createCollection(
  nativeWindowCollectionOptions<Todo, string>({
    id: "todos",
    channel: "tsdb:todos",
    getKey: (todo) => todo.id,
  }),
);

function TodoList() {
  const { data: todos } = useLiveQuery((q) => q.from({ todo: todoCollection }));

  return (
    <ul>
      {todos.map((todo) => (
        <li key={todo.id} style={{ textDecoration: todo.done ? "line-through" : "none" }}>
          {todo.text}
        </li>
      ))}
    </ul>
  );
}

The component re-renders automatically whenever the host pushes new data via the data source.

Wire Protocol

Messages use the same { $ch, p } JSON envelope format as the Typed IPC layer. The $ch field is set to the channel string from the data source options (e.g. "tsdb:todos"). The p payload is a discriminated union on a t field:

tDescriptionPayload Shape
"i"Insert{ t: "i", k: TKey, v: T }
"u"Update{ t: "u", k: TKey, v: T }
"d"Delete{ t: "d", k: TKey }
"s"Snapshot (full state){ t: "s", items: Array<{ k: TKey, v: T }> }
"b"Batch (atomic){ t: "b", ops: Array<SyncOp> }

Single-letter keys minimize payload size over the IPC bridge.

Note: The TSDB adapter uses postMessage directly — it does not register as a typed channel. This means TSDB traffic and typed channel traffic coexist on the same window without interfering with each other.

Integration with IPC

The webview-side sync function uses two strategies to receive messages:

  1. Preferred: listener registry — when the IPC client (createChannelClient or the auto-injected script) is present, window.__native_message_listeners__ is available. The sync function calls registry.add(handler) to register, and registry.remove(handler) on cleanup. This is the recommended path when using the Typed IPC layer.

  2. Fallback: direct interposition — when the IPC client is not present (standalone usage), the sync function interposes on window.__native_message__ directly, chaining to the previous handler. The assignment is wrapped in try/catch for safety in case the property was frozen.

For the best experience, use the TSDB adapter alongside the Typed IPC layer. See the Security guide for details on how the listener registry is hardened.

Type Exports

Host (@nativewindow/tsdb)

ExportDescription
MessageSenderSender interface: { postMessage(message: string): void }
DataSourceOptions<T, TKey>Options for createDataSource (channel, getKey)
DataSource<T, TKey>Return type of createDataSource (insert, update, delete, batch, sync)
BatchBuilder<T, TKey>Builder passed to the batch() callback (insert, update, delete)

Webview (@nativewindow/tsdb/client)

ExportDescription
NativeWindowCollectionConfig<T, TKey>Options for nativeWindowCollectionOptions (id, channel, getKey)
NativeWindowCollectionResult<T, TKey>Return type of nativeWindowCollectionOptions — pass directly to createCollection()

On this page