@syncular/client 0.15.44 → 0.15.46
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/README.md +37 -5
- package/dist/browser-storage-persistence.d.ts +20 -0
- package/dist/browser-storage-persistence.js +34 -0
- package/dist/bun-database.d.ts +1 -1
- package/dist/bun-database.js +1 -1
- package/dist/client.d.ts +3 -3
- package/dist/client.js +6 -6
- package/dist/database.d.ts +1 -1
- package/dist/devtools.d.ts +1 -1
- package/dist/http.d.ts +6 -2
- package/dist/http.js +68 -1
- package/dist/index.d.ts +4 -2
- package/dist/index.js +4 -2
- package/dist/invalidation.d.ts +1 -1
- package/dist/leader-lock.d.ts +2 -2
- package/dist/multi-tab.js +1 -1
- package/dist/naming.d.ts +1 -1
- package/dist/naming.js +1 -1
- package/dist/node-database.js +1 -1
- package/dist/query-guard.d.ts +1 -1
- package/dist/query-guard.js +1 -1
- package/dist/remote.d.ts +76 -0
- package/dist/remote.js +441 -0
- package/dist/schema.d.ts +3 -3
- package/dist/sql-tag.d.ts +1 -1
- package/dist/transport.d.ts +12 -1
- package/dist/transport.js +1 -1
- package/dist/wasm-database.d.ts +6 -4
- package/dist/wasm-database.js +13 -8
- package/dist/window.d.ts +1 -1
- package/dist/window.js +1 -1
- package/dist/worker-entry.js +1 -1
- package/dist/worker-host.d.ts +5 -5
- package/dist/worker-host.js +2 -2
- package/dist/worker-protocol.d.ts +3 -3
- package/dist/worker-protocol.js +1 -1
- package/package.json +3 -3
- package/src/browser-storage-persistence.ts +52 -0
- package/src/bun-database.ts +1 -1
- package/src/client.ts +6 -6
- package/src/database.ts +1 -1
- package/src/devtools.ts +1 -1
- package/src/http.ts +100 -8
- package/src/index.ts +4 -2
- package/src/invalidation.ts +1 -1
- package/src/leader-lock.ts +2 -2
- package/src/multi-tab.ts +1 -1
- package/src/naming.ts +1 -1
- package/src/node-database.ts +1 -1
- package/src/query-guard.ts +1 -1
- package/src/remote.ts +724 -0
- package/src/schema.ts +3 -3
- package/src/sql-tag.ts +1 -1
- package/src/transport.ts +20 -1
- package/src/wasm-database.ts +13 -8
- package/src/window.ts +1 -1
- package/src/worker-entry.ts +1 -1
- package/src/worker-host.ts +6 -6
- package/src/worker-protocol.ts +3 -3
package/README.md
CHANGED
|
@@ -3,9 +3,20 @@
|
|
|
3
3
|
The TypeScript client protocol core (SPEC.md §§3–8, client side) plus its
|
|
4
4
|
browser platform bindings.
|
|
5
5
|
|
|
6
|
+
The normal `SyncClient` also runs in a CLI or background service.
|
|
7
|
+
Use `openBunDatabase(path)` or `openNodeDatabase(path)` for a persistent local
|
|
8
|
+
replica. See the [server-side sync client guide](https://syncular.dev/guide-server-clients/).
|
|
9
|
+
|
|
10
|
+
`SyncRemoteClient` is the database-less server client. It sends ordinary
|
|
11
|
+
push-only commits through `/sync` and can call registered typed queries,
|
|
12
|
+
server-authoritative commands, and live query watches through the remote
|
|
13
|
+
operation transport. See [remote server operations](https://syncular.dev/guide-remote-operations/).
|
|
14
|
+
Its schema and sync transport are optional for query-only or command-only
|
|
15
|
+
processes.
|
|
16
|
+
|
|
6
17
|
## Client-local FTS5 projections
|
|
7
18
|
|
|
8
|
-
Generated schemas may attach `ftsIndexes` to a synced table
|
|
19
|
+
Generated schemas may attach `ftsIndexes` to a synced table. The
|
|
9
20
|
client materializes each as a contentful local FTS5 table with a private stable
|
|
10
21
|
source identity and insert/update/delete triggers. Existing visible rows are
|
|
11
22
|
bulk-indexed on first creation; schema reset recreates the projection. The FTS
|
|
@@ -16,8 +27,7 @@ strings.
|
|
|
16
27
|
|
|
17
28
|
## Browser modes — there are exactly two
|
|
18
29
|
|
|
19
|
-
**Persistent worker mode is THE mode
|
|
20
|
-
2026-07-03). The whole client core — `SyncClient`, the fetch/WebSocket
|
|
30
|
+
**Persistent worker mode is THE mode.** The whole client core — `SyncClient`, the fetch/WebSocket
|
|
21
31
|
transports, and SQLite on the `opfs-sahpool` VFS — runs inside a Web
|
|
22
32
|
Worker. The UI thread talks to it through a thin postMessage RPC:
|
|
23
33
|
|
|
@@ -34,7 +44,7 @@ import { createSyncClientHandle } from '@syncular/client';
|
|
|
34
44
|
const handle = await createSyncClientHandle({
|
|
35
45
|
worker: () => new Worker(new URL('./worker.ts', import.meta.url), { type: 'module' }),
|
|
36
46
|
schema,
|
|
37
|
-
database: { mode: 'persistent', name: 'app' }, // OPFS, survives reloads
|
|
47
|
+
database: { mode: 'persistent', name: 'app' }, // OPFS, survives reloads while the origin remains stored
|
|
38
48
|
endpoints: {
|
|
39
49
|
syncUrl: '/sync',
|
|
40
50
|
segmentsUrl: '/segments',
|
|
@@ -58,6 +68,25 @@ SPEC §8.4); the supported page-level realtime supervisor owns reconnect and
|
|
|
58
68
|
resume policy. The main thread gets `onSyncNeeded` / `onConflict` / `onSynced`
|
|
59
69
|
events for rendering.
|
|
60
70
|
|
|
71
|
+
OPFS is best effort until the browser grants origin persistence. The page owns
|
|
72
|
+
that decision because `StorageManager.persist()` is a Window API and should be
|
|
73
|
+
requested from a user action:
|
|
74
|
+
|
|
75
|
+
```ts
|
|
76
|
+
import {
|
|
77
|
+
checkBrowserStoragePersistence,
|
|
78
|
+
requestBrowserStoragePersistence,
|
|
79
|
+
} from '@syncular/client';
|
|
80
|
+
|
|
81
|
+
const current = await checkBrowserStoragePersistence();
|
|
82
|
+
const requested = await requestBrowserStoragePersistence(); // user action
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Each call returns `persistent` or a structured `best-effort` reason. Keep the
|
|
86
|
+
client usable after denial, display the state, and warn whenever best-effort
|
|
87
|
+
storage contains pending outbox commits. Origin eviction removes OPFS and its
|
|
88
|
+
outbox together; persistence does not prevent a user from clearing site data.
|
|
89
|
+
|
|
61
90
|
**Ephemeral in-memory mode is EXPLICIT.** `openWasmDatabase()` returns an
|
|
62
91
|
in-memory sqlite-wasm database for tests, demos and SSR. Nothing
|
|
63
92
|
persists, on purpose, and that is the only main-thread mode.
|
|
@@ -103,7 +132,7 @@ HTTP rounds still work with no socket, but do not imply continuous convergence:
|
|
|
103
132
|
a host trigger must actually run them. See the complete
|
|
104
133
|
[realtime lifecycle guide](https://syncular.dev/concepts-realtime/).
|
|
105
134
|
|
|
106
|
-
## Multi-tab followers
|
|
135
|
+
## Multi-tab followers
|
|
107
136
|
|
|
108
137
|
By default, every tab of the same origin shares ONE core:
|
|
109
138
|
one sync loop, one WebSocket, one OPFS database, N tabs.
|
|
@@ -393,6 +422,9 @@ successful history may be dismissed. See SPEC §7.2.1.
|
|
|
393
422
|
`FileSystemSyncAccessHandle`, unlike the Atomics-based `opfs` VFS).
|
|
394
423
|
- Browsers without OPFS (~pre-2023) are **unsupported**:
|
|
395
424
|
`openPersistentWasmDatabase` fails loud instead of degrading.
|
|
425
|
+
- OPFS is reload-persistent but begins as best-effort origin storage. The root
|
|
426
|
+
package exports `checkBrowserStoragePersistence()` and
|
|
427
|
+
`requestBrowserStoragePersistence()` for the page-level persistence policy.
|
|
396
428
|
- **Never IndexedDB.** There is no wa-sqlite/absurd-sql style fallback
|
|
397
429
|
and none is planned.
|
|
398
430
|
- `openPersistentWasmDatabase` refuses to run on the main thread — not a
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser storage durability for the origin that owns Syncular's OPFS
|
|
3
|
+
* database. Persistence is requested by the page because `persist()` is a
|
|
4
|
+
* Window-only API and browsers may evaluate the request against user
|
|
5
|
+
* engagement.
|
|
6
|
+
*/
|
|
7
|
+
export type BrowserStoragePersistence = {
|
|
8
|
+
readonly state: 'persistent';
|
|
9
|
+
} | {
|
|
10
|
+
readonly state: 'best-effort';
|
|
11
|
+
readonly reason: 'not-granted' | 'unavailable' | 'check-failed' | 'request-failed';
|
|
12
|
+
};
|
|
13
|
+
/** Check whether the current origin is protected from automatic eviction. */
|
|
14
|
+
export declare function checkBrowserStoragePersistence(): Promise<BrowserStoragePersistence>;
|
|
15
|
+
/**
|
|
16
|
+
* Request eviction-resistant storage for the current origin. Call this from a
|
|
17
|
+
* user action near the first important offline write. A best-effort result is
|
|
18
|
+
* an explicit durability state; the OPFS database remains usable.
|
|
19
|
+
*/
|
|
20
|
+
export declare function requestBrowserStoragePersistence(): Promise<BrowserStoragePersistence>;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/** Check whether the current origin is protected from automatic eviction. */
|
|
2
|
+
export async function checkBrowserStoragePersistence() {
|
|
3
|
+
const storage = typeof navigator === 'undefined' ? undefined : navigator.storage;
|
|
4
|
+
if (storage === undefined || typeof storage.persisted !== 'function') {
|
|
5
|
+
return { state: 'best-effort', reason: 'unavailable' };
|
|
6
|
+
}
|
|
7
|
+
try {
|
|
8
|
+
return (await storage.persisted())
|
|
9
|
+
? { state: 'persistent' }
|
|
10
|
+
: { state: 'best-effort', reason: 'not-granted' };
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
return { state: 'best-effort', reason: 'check-failed' };
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Request eviction-resistant storage for the current origin. Call this from a
|
|
18
|
+
* user action near the first important offline write. A best-effort result is
|
|
19
|
+
* an explicit durability state; the OPFS database remains usable.
|
|
20
|
+
*/
|
|
21
|
+
export async function requestBrowserStoragePersistence() {
|
|
22
|
+
const storage = typeof navigator === 'undefined' ? undefined : navigator.storage;
|
|
23
|
+
if (storage === undefined || typeof storage.persist !== 'function') {
|
|
24
|
+
return { state: 'best-effort', reason: 'unavailable' };
|
|
25
|
+
}
|
|
26
|
+
try {
|
|
27
|
+
return (await storage.persist())
|
|
28
|
+
? { state: 'persistent' }
|
|
29
|
+
: { state: 'best-effort', reason: 'not-granted' };
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return { state: 'best-effort', reason: 'request-failed' };
|
|
33
|
+
}
|
|
34
|
+
}
|
package/dist/bun-database.d.ts
CHANGED
package/dist/bun-database.js
CHANGED
package/dist/client.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* SyncClient
|
|
2
|
+
* SyncClient implements the client side of SPEC.md §§3–8.
|
|
3
3
|
*
|
|
4
4
|
* A plain library running on whatever thread it is constructed on
|
|
5
|
-
*
|
|
5
|
+
* Storage is behind `ClientDatabase`, network
|
|
6
6
|
* behind `SyncTransport`/`SegmentDownloader`/`RealtimeConnector`, multi-tab
|
|
7
7
|
* ownership behind `LeaderLock`. One combined push+pull request per
|
|
8
8
|
* `sync()` round (§7.2); local reads go straight to the database.
|
|
@@ -252,7 +252,7 @@ export declare class SyncClient {
|
|
|
252
252
|
*/
|
|
253
253
|
activateSecurity(options?: SecurityActivation): Promise<void>;
|
|
254
254
|
get clientId(): string;
|
|
255
|
-
/** The underlying database
|
|
255
|
+
/** The underlying database: raw SQL is the local query API. */
|
|
256
256
|
get database(): ClientDatabase;
|
|
257
257
|
/**
|
|
258
258
|
* The raw-SQL read tier. Guarded (query-guard.ts): a single read-only
|
package/dist/client.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* SyncClient
|
|
2
|
+
* SyncClient implements the client side of SPEC.md §§3–8.
|
|
3
3
|
*
|
|
4
4
|
* A plain library running on whatever thread it is constructed on
|
|
5
|
-
*
|
|
5
|
+
* Storage is behind `ClientDatabase`, network
|
|
6
6
|
* behind `SyncTransport`/`SegmentDownloader`/`RealtimeConnector`, multi-tab
|
|
7
7
|
* ownership behind `LeaderLock`. One combined push+pull request per
|
|
8
8
|
* `sync()` round (§7.2); local reads go straight to the database.
|
|
@@ -236,7 +236,7 @@ export class SyncClient {
|
|
|
236
236
|
this.#config.onSyncNeeded?.('startup');
|
|
237
237
|
this.#config.onSyncIntent?.({ kind: 'interactive' });
|
|
238
238
|
}
|
|
239
|
-
//
|
|
239
|
+
// Console introspection is a no-op outside a dev page.
|
|
240
240
|
this.#devtoolsUnregister = registerDevtools({
|
|
241
241
|
kind: 'client',
|
|
242
242
|
ref: this,
|
|
@@ -391,7 +391,7 @@ export class SyncClient {
|
|
|
391
391
|
get clientId() {
|
|
392
392
|
return this.#clientId;
|
|
393
393
|
}
|
|
394
|
-
/** The underlying database
|
|
394
|
+
/** The underlying database: raw SQL is the local query API. */
|
|
395
395
|
get database() {
|
|
396
396
|
this.#requireActive();
|
|
397
397
|
return this.#db;
|
|
@@ -460,7 +460,7 @@ export class SyncClient {
|
|
|
460
460
|
};
|
|
461
461
|
});
|
|
462
462
|
}
|
|
463
|
-
// -- live-query invalidation
|
|
463
|
+
// -- live-query invalidation ----------------------------------------------
|
|
464
464
|
/**
|
|
465
465
|
* Subscribe to fine-grained invalidation. The callback fires ONCE per
|
|
466
466
|
* apply batch (never per row, I1) with the `{tables, scopeKeys}` touched
|
|
@@ -1920,7 +1920,7 @@ export class SyncClient {
|
|
|
1920
1920
|
}
|
|
1921
1921
|
/**
|
|
1922
1922
|
* One request/response round trip (§8.7): over the socket whenever it
|
|
1923
|
-
* is connected (
|
|
1923
|
+
* is connected (the socket IS the sync-round
|
|
1924
1924
|
* transport, not a fallback pair), otherwise through the configured
|
|
1925
1925
|
* `SyncTransport` seam (loopback/conformance hosts, HTTP-only
|
|
1926
1926
|
* producers).
|
package/dist/database.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Storage abstraction
|
|
2
|
+
* Storage abstraction: the client core runs on any SQLite that
|
|
3
3
|
* implements this minimal synchronous surface. Tests use bun:sqlite
|
|
4
4
|
* (`./bun-database`); browsers use sqlite-wasm + OPFS (`./wasm-database`).
|
|
5
5
|
* Methods are synchronous because both backends execute synchronously once
|
package/dist/devtools.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The client-side introspection registry
|
|
2
|
+
* The client-side introspection registry: every live
|
|
3
3
|
* `SyncClient` / `SyncClientHandle` on a page registers itself on
|
|
4
4
|
* `globalThis.__SYNCULAR__`, so a first integration debugs from the console
|
|
5
5
|
* instead of hand-exposing the client:
|
package/dist/http.d.ts
CHANGED
|
@@ -2,16 +2,20 @@
|
|
|
2
2
|
* Browser transport bindings (§1.1, §5.4/§5.5, §8.1): fetch-based sync
|
|
3
3
|
* transport, segment download with signed-URL preference and direct-serve
|
|
4
4
|
* fallback, and a WebSocket realtime connector. Core tests never use these
|
|
5
|
-
* (the loopback doctrine);
|
|
5
|
+
* (the loopback doctrine); the browser fixture exercises them in a real browser.
|
|
6
6
|
*/
|
|
7
7
|
import type { BlobTransport } from './blob.js';
|
|
8
|
-
import type { RealtimeConnector, SegmentDownloader, SyncTransport } from './transport.js';
|
|
8
|
+
import type { RealtimeConnector, RemoteOperationTransport, RemoteOperationRealtimeConnector, SegmentDownloader, SyncTransport } from './transport.js';
|
|
9
9
|
export interface HttpTransportOptions {
|
|
10
10
|
readonly headers?: Readonly<Record<string, string>>;
|
|
11
11
|
readonly fetch?: typeof fetch;
|
|
12
12
|
}
|
|
13
13
|
/** POST `<mount>/sync` with SSP2 bodies (§1.1). */
|
|
14
14
|
export declare function httpSyncTransport(syncUrl: string, options?: HttpTransportOptions): SyncTransport;
|
|
15
|
+
/** POST one registered authoritative operation to `<mount>/operations`. */
|
|
16
|
+
export declare function httpRemoteOperationTransport(operationsUrl: string, options?: HttpTransportOptions): RemoteOperationTransport;
|
|
17
|
+
/** WebSocket connector for registered query snapshots. */
|
|
18
|
+
export declare function webSocketRemoteOperationConnector(realtimeUrl: string): RemoteOperationRealtimeConnector;
|
|
15
19
|
/**
|
|
16
20
|
* §5.5 direct endpoint with the `X-Syncular-Scopes` re-authorization
|
|
17
21
|
* header, plus the §5.4 `fetchUrl` capability (advertises accept bit 3).
|
package/dist/http.js
CHANGED
|
@@ -35,6 +35,60 @@ export function httpSyncTransport(syncUrl, options) {
|
|
|
35
35
|
return new Uint8Array(await response.arrayBuffer());
|
|
36
36
|
};
|
|
37
37
|
}
|
|
38
|
+
/** POST one registered authoritative operation to `<mount>/operations`. */
|
|
39
|
+
export function httpRemoteOperationTransport(operationsUrl, options) {
|
|
40
|
+
const doFetch = options?.fetch ?? fetch;
|
|
41
|
+
return async (request) => {
|
|
42
|
+
const response = await doFetch(operationsUrl, {
|
|
43
|
+
method: 'POST',
|
|
44
|
+
headers: {
|
|
45
|
+
'Content-Type': 'application/vnd.syncular.operations.v1+json',
|
|
46
|
+
...options?.headers,
|
|
47
|
+
},
|
|
48
|
+
body: request.slice().buffer,
|
|
49
|
+
});
|
|
50
|
+
if (!response.ok)
|
|
51
|
+
await throwHttpError(response);
|
|
52
|
+
return new Uint8Array(await response.arrayBuffer());
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
/** WebSocket connector for registered query snapshots. */
|
|
56
|
+
export function webSocketRemoteOperationConnector(realtimeUrl) {
|
|
57
|
+
return (handlers) => new Promise((resolve, reject) => {
|
|
58
|
+
const socket = new WebSocket(realtimeUrl);
|
|
59
|
+
let opened = false;
|
|
60
|
+
socket.binaryType = 'arraybuffer';
|
|
61
|
+
socket.onopen = () => {
|
|
62
|
+
opened = true;
|
|
63
|
+
resolve({
|
|
64
|
+
send: (bytes) => socket.send(bytes.slice().buffer),
|
|
65
|
+
close: () => socket.close(),
|
|
66
|
+
});
|
|
67
|
+
};
|
|
68
|
+
socket.onmessage = (event) => {
|
|
69
|
+
if (event.data instanceof ArrayBuffer) {
|
|
70
|
+
handlers.onMessage(new Uint8Array(event.data));
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
socket.onerror = () => {
|
|
74
|
+
if (!opened) {
|
|
75
|
+
reject(new ClientSyncError('sync.transport_failed', 'remote operation realtime socket failed to connect', true));
|
|
76
|
+
}
|
|
77
|
+
try {
|
|
78
|
+
socket.close();
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
handlers.onClose?.();
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
socket.onclose = () => {
|
|
85
|
+
if (!opened) {
|
|
86
|
+
reject(new ClientSyncError('sync.transport_failed', 'remote operation realtime socket closed while connecting', true));
|
|
87
|
+
}
|
|
88
|
+
handlers.onClose?.();
|
|
89
|
+
};
|
|
90
|
+
});
|
|
91
|
+
}
|
|
38
92
|
/**
|
|
39
93
|
* §5.5 direct endpoint with the `X-Syncular-Scopes` re-authorization
|
|
40
94
|
* header, plus the §5.4 `fetchUrl` capability (advertises accept bit 3).
|
|
@@ -170,8 +224,10 @@ export function httpBlobTransport(blobsBaseUrl, options) {
|
|
|
170
224
|
export function webSocketRealtimeConnector(realtimeUrl) {
|
|
171
225
|
return (handlers) => new Promise((resolve, reject) => {
|
|
172
226
|
const socket = new WebSocket(realtimeUrl);
|
|
227
|
+
let opened = false;
|
|
173
228
|
socket.binaryType = 'arraybuffer';
|
|
174
229
|
socket.onopen = () => {
|
|
230
|
+
opened = true;
|
|
175
231
|
resolve({
|
|
176
232
|
send: (text) => socket.send(text),
|
|
177
233
|
sendBytes: (bytes) => {
|
|
@@ -187,9 +243,20 @@ export function webSocketRealtimeConnector(realtimeUrl) {
|
|
|
187
243
|
handlers.onBinary(new Uint8Array(event.data));
|
|
188
244
|
};
|
|
189
245
|
socket.onerror = () => {
|
|
190
|
-
|
|
246
|
+
if (!opened) {
|
|
247
|
+
reject(new ClientSyncError('sync.transport_failed', 'realtime socket failed to connect', true));
|
|
248
|
+
}
|
|
249
|
+
try {
|
|
250
|
+
socket.close();
|
|
251
|
+
}
|
|
252
|
+
catch {
|
|
253
|
+
handlers.onClose?.();
|
|
254
|
+
}
|
|
191
255
|
};
|
|
192
256
|
socket.onclose = () => {
|
|
257
|
+
if (!opened) {
|
|
258
|
+
reject(new ClientSyncError('sync.transport_failed', 'realtime socket closed while connecting', true));
|
|
259
|
+
}
|
|
193
260
|
handlers.onClose?.();
|
|
194
261
|
};
|
|
195
262
|
});
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @syncular/client
|
|
3
|
-
*
|
|
2
|
+
* @syncular/client is the TypeScript client protocol core.
|
|
3
|
+
* SPEC.md is normative.
|
|
4
4
|
*
|
|
5
5
|
* Browser-safe root: database backends live behind subpath exports
|
|
6
6
|
* (`./bun` for bun:sqlite tests, `./wasm` for sqlite-wasm + OPFS); the
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
export * from './apply.js';
|
|
12
12
|
export * from './availability.js';
|
|
13
13
|
export * from './blob.js';
|
|
14
|
+
export * from './browser-storage-persistence.js';
|
|
14
15
|
export * from './client.js';
|
|
15
16
|
export * from './content-type.js';
|
|
16
17
|
export * from './database.js';
|
|
@@ -29,6 +30,7 @@ export * from './outbox.js';
|
|
|
29
30
|
export * from './outcomes.js';
|
|
30
31
|
export * from './query-guard.js';
|
|
31
32
|
export * from './reactive-store.js';
|
|
33
|
+
export * from './remote.js';
|
|
32
34
|
export * from './realtime-supervisor.js';
|
|
33
35
|
export * from './schema.js';
|
|
34
36
|
export * from './sql-tag.js';
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @syncular/client
|
|
3
|
-
*
|
|
2
|
+
* @syncular/client is the TypeScript client protocol core.
|
|
3
|
+
* SPEC.md is normative.
|
|
4
4
|
*
|
|
5
5
|
* Browser-safe root: database backends live behind subpath exports
|
|
6
6
|
* (`./bun` for bun:sqlite tests, `./wasm` for sqlite-wasm + OPFS); the
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
export * from './apply.js';
|
|
12
12
|
export * from './availability.js';
|
|
13
13
|
export * from './blob.js';
|
|
14
|
+
export * from './browser-storage-persistence.js';
|
|
14
15
|
export * from './client.js';
|
|
15
16
|
export * from './content-type.js';
|
|
16
17
|
export * from './database.js';
|
|
@@ -29,6 +30,7 @@ export * from './outbox.js';
|
|
|
29
30
|
export * from './outcomes.js';
|
|
30
31
|
export * from './query-guard.js';
|
|
31
32
|
export * from './reactive-store.js';
|
|
33
|
+
export * from './remote.js';
|
|
32
34
|
export * from './realtime-supervisor.js';
|
|
33
35
|
export * from './schema.js';
|
|
34
36
|
export * from './sql-tag.js';
|
package/dist/invalidation.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Revisioned client-local observation events (SPEC §7.5
|
|
2
|
+
* Revisioned client-local observation events (SPEC §7.5).
|
|
3
3
|
*
|
|
4
4
|
* The core records observer domains while it owns the SQLite transaction,
|
|
5
5
|
* increments the persisted local revision in that same transaction, and emits
|
package/dist/leader-lock.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Multi-tab ownership seam
|
|
2
|
+
* Multi-tab ownership seam: exactly one core instance owns the
|
|
3
3
|
* local database. The interface is the whole B3 deliverable — cross-tab
|
|
4
4
|
* follower fanout is post-gate. Browsers use Web Locks; tests use the
|
|
5
5
|
* no-op single-owner lock.
|
|
@@ -14,7 +14,7 @@ export interface LeaderLock {
|
|
|
14
14
|
* Resolves immediately: the lease when leadership was free, `undefined`
|
|
15
15
|
* when another owner holds it. The worker handle uses this so a second
|
|
16
16
|
* tab gets a clear not-leader state instead of blocking forever
|
|
17
|
-
* (followers are post-gate
|
|
17
|
+
* (followers are post-gate).
|
|
18
18
|
*/
|
|
19
19
|
tryAcquire?(name: string): Promise<LeaderLease | undefined>;
|
|
20
20
|
}
|
package/dist/multi-tab.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Multi-tab followers
|
|
2
|
+
* Multi-tab followers: one core per origin, N tabs.
|
|
3
3
|
*
|
|
4
4
|
* The leader tab holds the Web Locks lease and runs the worker core (the
|
|
5
5
|
* existing worker-host path, unchanged). Every OTHER tab is a FOLLOWER: it
|
package/dist/naming.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The pinned snake→camel naming map
|
|
2
|
+
* The pinned snake→camel naming map. The
|
|
3
3
|
* client-side copy of the typegen algorithm (kept in lockstep by shared
|
|
4
4
|
* test vectors; the Rust core carries the same function). Used by `mutate`
|
|
5
5
|
* to accept BOTH casings for value keys: the canonical camelCase the
|
package/dist/naming.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The pinned snake→camel naming map
|
|
2
|
+
* The pinned snake→camel naming map. The
|
|
3
3
|
* client-side copy of the typegen algorithm (kept in lockstep by shared
|
|
4
4
|
* test vectors; the Rust core carries the same function). Used by `mutate`
|
|
5
5
|
* to accept BOTH casings for value keys: the canonical camelCase the
|
package/dist/node-database.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `ClientDatabase` on better-sqlite3 — the Electron-main / plain-Node
|
|
3
|
-
* backend
|
|
3
|
+
* backend. Semantics mirror `./bun-database`
|
|
4
4
|
* exactly (synchronous exec/query/transaction with the shared savepoint
|
|
5
5
|
* helper, and the same §5.3 sqlite-image ATTACH path), so the core behaves
|
|
6
6
|
* identically whether it runs on bun:sqlite (tests), sqlite-wasm (browser)
|
package/dist/query-guard.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The raw-query guard
|
|
2
|
+
* The raw-query guard. `client.query()` and the React
|
|
3
3
|
* `useRawSql` hook are the untrusted raw-SQL tier: an app hands us a SQL
|
|
4
4
|
* string and we run it against the local database. Two rules make that safe
|
|
5
5
|
* to expose, enforced HERE in the core (previously they lived in the
|
package/dist/query-guard.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The raw-query guard
|
|
2
|
+
* The raw-query guard. `client.query()` and the React
|
|
3
3
|
* `useRawSql` hook are the untrusted raw-SQL tier: an app hands us a SQL
|
|
4
4
|
* string and we run it against the local database. Two rules make that safe
|
|
5
5
|
* to expose, enforced HERE in the core (previously they lived in the
|
package/dist/remote.d.ts
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Database-less SSP2 producer (§6.10). It prepares and sends ordinary commits
|
|
3
|
+
* through the existing push path without creating a local replica or outbox.
|
|
4
|
+
*/
|
|
5
|
+
import { type PushOperationResult, type PushResultFrame } from '@syncular/core';
|
|
6
|
+
import type { MutationInput } from './client.js';
|
|
7
|
+
import type { EncryptionConfig } from './encryption.js';
|
|
8
|
+
import { ClientSyncError } from './errors.js';
|
|
9
|
+
import { type ClientSchema } from './schema.js';
|
|
10
|
+
import type { SyncTransport } from './transport.js';
|
|
11
|
+
import type { RemoteOperationRealtimeConnector, RemoteOperationTransport } from './transport.js';
|
|
12
|
+
export interface SyncRemoteClientConfig {
|
|
13
|
+
/** Required only for ordinary row commits. */
|
|
14
|
+
readonly schema?: ClientSchema;
|
|
15
|
+
readonly clientId: string;
|
|
16
|
+
/** Required only for ordinary row commits. */
|
|
17
|
+
readonly transport?: SyncTransport;
|
|
18
|
+
readonly operations?: RemoteOperationTransport;
|
|
19
|
+
readonly operationRealtime?: RemoteOperationRealtimeConnector;
|
|
20
|
+
readonly encryption?: EncryptionConfig;
|
|
21
|
+
}
|
|
22
|
+
export interface RemoteCommitInput {
|
|
23
|
+
/** Stable caller-owned idempotency identity for this logical commit. */
|
|
24
|
+
readonly requestId: string;
|
|
25
|
+
readonly mutations: readonly MutationInput[];
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Prepared bytes are the retry unit. Persist them when a process must retry
|
|
29
|
+
* across restart, especially when encryption uses randomized nonces.
|
|
30
|
+
*/
|
|
31
|
+
export interface PreparedRemoteCommit {
|
|
32
|
+
readonly requestId: string;
|
|
33
|
+
readonly bytes: Uint8Array;
|
|
34
|
+
}
|
|
35
|
+
export interface RemoteCommitResult {
|
|
36
|
+
readonly requestId: string;
|
|
37
|
+
readonly status: PushResultFrame['status'];
|
|
38
|
+
readonly commitSeq?: number;
|
|
39
|
+
readonly results: readonly PushOperationResult[];
|
|
40
|
+
}
|
|
41
|
+
export interface RemoteQueryDescriptor<Row, Params = undefined> {
|
|
42
|
+
readonly id: string;
|
|
43
|
+
readonly mapRow: (row: Readonly<Record<string, unknown>>) => Row;
|
|
44
|
+
readonly __row?: Row;
|
|
45
|
+
readonly __params?: Params;
|
|
46
|
+
}
|
|
47
|
+
export interface RemoteQueryResult<Row> {
|
|
48
|
+
readonly rows: readonly Row[];
|
|
49
|
+
readonly maxCommitSeq: number;
|
|
50
|
+
}
|
|
51
|
+
export interface RemoteQueryWatchHandlers<Row> {
|
|
52
|
+
onSnapshot(result: RemoteQueryResult<Row>): void;
|
|
53
|
+
onError?(error: ClientSyncError): void;
|
|
54
|
+
}
|
|
55
|
+
export interface RemoteCommandDescriptor<Input = undefined> {
|
|
56
|
+
readonly id: string;
|
|
57
|
+
readonly __input?: Input;
|
|
58
|
+
}
|
|
59
|
+
export declare function remoteCommand<Input = undefined>(id: string): RemoteCommandDescriptor<Input>;
|
|
60
|
+
export interface RemoteCommandResult {
|
|
61
|
+
readonly requestId: string;
|
|
62
|
+
readonly status: 'applied' | 'cached' | 'rejected';
|
|
63
|
+
readonly commitSeq?: number;
|
|
64
|
+
readonly results: readonly unknown[];
|
|
65
|
+
}
|
|
66
|
+
export declare class SyncRemoteClient {
|
|
67
|
+
#private;
|
|
68
|
+
constructor(config: SyncRemoteClientConfig);
|
|
69
|
+
prepareCommit(input: RemoteCommitInput): Promise<PreparedRemoteCommit>;
|
|
70
|
+
sendCommit(prepared: PreparedRemoteCommit): Promise<RemoteCommitResult>;
|
|
71
|
+
commit(input: RemoteCommitInput): Promise<RemoteCommitResult>;
|
|
72
|
+
query<Row, Params = undefined>(descriptor: RemoteQueryDescriptor<Row, Params>, params?: Params): Promise<RemoteQueryResult<Row>>;
|
|
73
|
+
command<Input = undefined>(descriptor: RemoteCommandDescriptor<Input>, requestId: string, input?: Input): Promise<RemoteCommandResult>;
|
|
74
|
+
watch<Row, Params = undefined>(descriptor: RemoteQueryDescriptor<Row, Params>, params: Params, handlers: RemoteQueryWatchHandlers<Row>): Promise<() => void>;
|
|
75
|
+
close(): void;
|
|
76
|
+
}
|