@syncular/client 0.15.44 → 0.15.45
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 +26 -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 +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.js +3 -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/schema.d.ts +3 -3
- package/dist/sql-tag.d.ts +1 -1
- package/dist/transport.d.ts +1 -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 +1 -1
- package/src/index.ts +3 -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/schema.ts +3 -3
- package/src/sql-tag.ts +1 -1
- package/src/transport.ts +1 -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
|
@@ -5,7 +5,7 @@ browser platform bindings.
|
|
|
5
5
|
|
|
6
6
|
## Client-local FTS5 projections
|
|
7
7
|
|
|
8
|
-
Generated schemas may attach `ftsIndexes` to a synced table
|
|
8
|
+
Generated schemas may attach `ftsIndexes` to a synced table. The
|
|
9
9
|
client materializes each as a contentful local FTS5 table with a private stable
|
|
10
10
|
source identity and insert/update/delete triggers. Existing visible rows are
|
|
11
11
|
bulk-indexed on first creation; schema reset recreates the projection. The FTS
|
|
@@ -16,8 +16,7 @@ strings.
|
|
|
16
16
|
|
|
17
17
|
## Browser modes — there are exactly two
|
|
18
18
|
|
|
19
|
-
**Persistent worker mode is THE mode
|
|
20
|
-
2026-07-03). The whole client core — `SyncClient`, the fetch/WebSocket
|
|
19
|
+
**Persistent worker mode is THE mode.** The whole client core — `SyncClient`, the fetch/WebSocket
|
|
21
20
|
transports, and SQLite on the `opfs-sahpool` VFS — runs inside a Web
|
|
22
21
|
Worker. The UI thread talks to it through a thin postMessage RPC:
|
|
23
22
|
|
|
@@ -34,7 +33,7 @@ import { createSyncClientHandle } from '@syncular/client';
|
|
|
34
33
|
const handle = await createSyncClientHandle({
|
|
35
34
|
worker: () => new Worker(new URL('./worker.ts', import.meta.url), { type: 'module' }),
|
|
36
35
|
schema,
|
|
37
|
-
database: { mode: 'persistent', name: 'app' }, // OPFS, survives reloads
|
|
36
|
+
database: { mode: 'persistent', name: 'app' }, // OPFS, survives reloads while the origin remains stored
|
|
38
37
|
endpoints: {
|
|
39
38
|
syncUrl: '/sync',
|
|
40
39
|
segmentsUrl: '/segments',
|
|
@@ -58,6 +57,25 @@ SPEC §8.4); the supported page-level realtime supervisor owns reconnect and
|
|
|
58
57
|
resume policy. The main thread gets `onSyncNeeded` / `onConflict` / `onSynced`
|
|
59
58
|
events for rendering.
|
|
60
59
|
|
|
60
|
+
OPFS is best effort until the browser grants origin persistence. The page owns
|
|
61
|
+
that decision because `StorageManager.persist()` is a Window API and should be
|
|
62
|
+
requested from a user action:
|
|
63
|
+
|
|
64
|
+
```ts
|
|
65
|
+
import {
|
|
66
|
+
checkBrowserStoragePersistence,
|
|
67
|
+
requestBrowserStoragePersistence,
|
|
68
|
+
} from '@syncular/client';
|
|
69
|
+
|
|
70
|
+
const current = await checkBrowserStoragePersistence();
|
|
71
|
+
const requested = await requestBrowserStoragePersistence(); // user action
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Each call returns `persistent` or a structured `best-effort` reason. Keep the
|
|
75
|
+
client usable after denial, display the state, and warn whenever best-effort
|
|
76
|
+
storage contains pending outbox commits. Origin eviction removes OPFS and its
|
|
77
|
+
outbox together; persistence does not prevent a user from clearing site data.
|
|
78
|
+
|
|
61
79
|
**Ephemeral in-memory mode is EXPLICIT.** `openWasmDatabase()` returns an
|
|
62
80
|
in-memory sqlite-wasm database for tests, demos and SSR. Nothing
|
|
63
81
|
persists, on purpose, and that is the only main-thread mode.
|
|
@@ -103,7 +121,7 @@ HTTP rounds still work with no socket, but do not imply continuous convergence:
|
|
|
103
121
|
a host trigger must actually run them. See the complete
|
|
104
122
|
[realtime lifecycle guide](https://syncular.dev/concepts-realtime/).
|
|
105
123
|
|
|
106
|
-
## Multi-tab followers
|
|
124
|
+
## Multi-tab followers
|
|
107
125
|
|
|
108
126
|
By default, every tab of the same origin shares ONE core:
|
|
109
127
|
one sync loop, one WebSocket, one OPFS database, N tabs.
|
|
@@ -393,6 +411,9 @@ successful history may be dismissed. See SPEC §7.2.1.
|
|
|
393
411
|
`FileSystemSyncAccessHandle`, unlike the Atomics-based `opfs` VFS).
|
|
394
412
|
- Browsers without OPFS (~pre-2023) are **unsupported**:
|
|
395
413
|
`openPersistentWasmDatabase` fails loud instead of degrading.
|
|
414
|
+
- OPFS is reload-persistent but begins as best-effort origin storage. The root
|
|
415
|
+
package exports `checkBrowserStoragePersistence()` and
|
|
416
|
+
`requestBrowserStoragePersistence()` for the page-level persistence policy.
|
|
396
417
|
- **Never IndexedDB.** There is no wa-sqlite/absurd-sql style fallback
|
|
397
418
|
and none is planned.
|
|
398
419
|
- `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,7 +2,7 @@
|
|
|
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
8
|
import type { RealtimeConnector, SegmentDownloader, SyncTransport } from './transport.js';
|
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';
|
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';
|
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/schema.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Client schema IR (SPEC.md §2.4, §3.1) — the same shape the server
|
|
3
|
-
* compiles
|
|
3
|
+
* compiles and is emitted by codegen. Drives local table
|
|
4
4
|
* DDL, the generated row codec, mutation helpers, and the §3.3 purge
|
|
5
5
|
* mapping (scope variable → local column).
|
|
6
6
|
*/
|
|
@@ -17,7 +17,7 @@ export interface ClientIndexSpec {
|
|
|
17
17
|
readonly columns: readonly string[];
|
|
18
18
|
readonly unique: boolean;
|
|
19
19
|
}
|
|
20
|
-
/** One client-local contentful FTS5 projection
|
|
20
|
+
/** One client-local contentful FTS5 projection. */
|
|
21
21
|
export interface ClientFtsIndexSpec {
|
|
22
22
|
readonly name: string;
|
|
23
23
|
readonly columns: readonly string[];
|
|
@@ -58,7 +58,7 @@ export interface CompiledClientTable {
|
|
|
58
58
|
/**
|
|
59
59
|
* Scope variable → the pattern's literal prefix (§3.1). A stored-scope
|
|
60
60
|
* value `v` for this variable has scope key `prefix:v` — the invalidation
|
|
61
|
-
* vocabulary
|
|
61
|
+
* vocabulary and the delta-routing key.
|
|
62
62
|
*/
|
|
63
63
|
readonly scopePrefixByVariable: ReadonlyMap<string, string>;
|
|
64
64
|
/** Local secondary indexes to create on the mirror table (declaration
|
package/dist/sql-tag.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The `sql` tagged template — the raw tier's composition helper
|
|
3
|
-
*
|
|
3
|
+
* Structural injection safety: an interpolated
|
|
4
4
|
* value can only ever become a `?` bind parameter; SQL text can only enter
|
|
5
5
|
* through the literal template, `sql.ident()` (allowlist-gated) or a loud
|
|
6
6
|
* `sql.raw()`. This helper is deliberately dumb plumbing and stays that
|
package/dist/transport.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Transport seams
|
|
2
|
+
* Transport seams: request/response bytes, segment download,
|
|
3
3
|
* and the realtime attach surface matching §8's client side. Tests use
|
|
4
4
|
* loopback implementations that call the server library directly — the
|
|
5
5
|
* loopback doctrine; HTTP/WebSocket bindings live in `./http`.
|
package/dist/transport.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Transport seams
|
|
2
|
+
* Transport seams: request/response bytes, segment download,
|
|
3
3
|
* and the realtime attach surface matching §8's client side. Tests use
|
|
4
4
|
* loopback implementations that call the server library directly — the
|
|
5
5
|
* loopback doctrine; HTTP/WebSocket bindings live in `./http`.
|
package/dist/wasm-database.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { type ClientDatabase } from './database.js';
|
|
|
3
3
|
* EXPLICIT ephemeral mode: an in-memory sqlite-wasm database. For tests,
|
|
4
4
|
* demos and SSR only — nothing persists. The persistent mode is
|
|
5
5
|
* `openPersistentWasmDatabase` inside a worker; there is no fallback from
|
|
6
|
-
* one to the other
|
|
6
|
+
* one to the other.
|
|
7
7
|
*/
|
|
8
8
|
export declare function openWasmDatabase(): Promise<ClientDatabase>;
|
|
9
9
|
export interface PersistentWasmDatabaseOptions {
|
|
@@ -18,13 +18,15 @@ export interface PersistentWasmDatabaseOptions {
|
|
|
18
18
|
readonly initialCapacity?: number;
|
|
19
19
|
}
|
|
20
20
|
/**
|
|
21
|
-
* THE persistent browser mode: a named database on OPFS via the
|
|
21
|
+
* THE reload-persistent browser mode: a named database on OPFS via the
|
|
22
22
|
* `opfs-sahpool` VFS. Worker-context only — not because SAHPool requires
|
|
23
23
|
* it (it uses `FileSystemSyncAccessHandle`, no `Atomics.wait`, and could
|
|
24
24
|
* technically run on the main thread), but because the persistent mode IS
|
|
25
|
-
* whole-core-in-a-worker
|
|
25
|
+
* whole-core-in-a-worker and
|
|
26
26
|
* this factory enforces that decision. No COOP/COEP headers required.
|
|
27
27
|
*
|
|
28
|
-
* Support floor: no OPFS → a loud `ClientSyncError`, never a fallback.
|
|
28
|
+
* Support floor: no OPFS → a loud `ClientSyncError`, never a fallback. This
|
|
29
|
+
* factory cannot request eviction-resistant origin storage because that API
|
|
30
|
+
* belongs to the page's Window context.
|
|
29
31
|
*/
|
|
30
32
|
export declare function openPersistentWasmDatabase(name: string, options?: PersistentWasmDatabaseOptions): Promise<ClientDatabase>;
|
package/dist/wasm-database.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `ClientDatabase` on @sqlite.org/sqlite-wasm
|
|
3
|
-
*
|
|
2
|
+
* `ClientDatabase` on @sqlite.org/sqlite-wasm. Two modes, no ladder between
|
|
3
|
+
* them:
|
|
4
4
|
*
|
|
5
|
-
* - `openPersistentWasmDatabase(name)` — THE persistent browser mode:
|
|
5
|
+
* - `openPersistentWasmDatabase(name)` — THE reload-persistent browser mode:
|
|
6
6
|
* OPFS via the `opfs-sahpool` VFS, restricted to Web Worker contexts
|
|
7
7
|
* because the whole client core runs in a worker by design. SAHPool
|
|
8
8
|
* needs **no COOP/COEP headers and no SharedArrayBuffer** (it is built
|
|
@@ -10,7 +10,10 @@
|
|
|
10
10
|
* proxy — the COOP/COEP requirement documented by sqlite-wasm applies
|
|
11
11
|
* only to `oo1.OpfsDb`, which this binding no longer uses). Browsers
|
|
12
12
|
* without OPFS are unsupported (support floor ~2023+): the factory
|
|
13
|
-
* fails loud. Never IndexedDB, never a silent in-memory fallback.
|
|
13
|
+
* fails loud. Never IndexedDB, never a silent in-memory fallback. The
|
|
14
|
+
* browser's separate origin-eviction policy is exposed from the root package
|
|
15
|
+
* by `checkBrowserStoragePersistence` and
|
|
16
|
+
* `requestBrowserStoragePersistence`.
|
|
14
17
|
* - `openWasmDatabase()` — EXPLICIT ephemeral: an in-memory database for
|
|
15
18
|
* tests, demos and SSR. Nothing survives a reload, on purpose.
|
|
16
19
|
*
|
|
@@ -106,7 +109,7 @@ function initSqlite3() {
|
|
|
106
109
|
* EXPLICIT ephemeral mode: an in-memory sqlite-wasm database. For tests,
|
|
107
110
|
* demos and SSR only — nothing persists. The persistent mode is
|
|
108
111
|
* `openPersistentWasmDatabase` inside a worker; there is no fallback from
|
|
109
|
-
* one to the other
|
|
112
|
+
* one to the other.
|
|
110
113
|
*/
|
|
111
114
|
export async function openWasmDatabase() {
|
|
112
115
|
const sqlite3 = await initSqlite3();
|
|
@@ -133,14 +136,16 @@ function opfsSahPoolError(error, directory) {
|
|
|
133
136
|
`or rename the directory. Underlying error: ${detail}`, true);
|
|
134
137
|
}
|
|
135
138
|
/**
|
|
136
|
-
* THE persistent browser mode: a named database on OPFS via the
|
|
139
|
+
* THE reload-persistent browser mode: a named database on OPFS via the
|
|
137
140
|
* `opfs-sahpool` VFS. Worker-context only — not because SAHPool requires
|
|
138
141
|
* it (it uses `FileSystemSyncAccessHandle`, no `Atomics.wait`, and could
|
|
139
142
|
* technically run on the main thread), but because the persistent mode IS
|
|
140
|
-
* whole-core-in-a-worker
|
|
143
|
+
* whole-core-in-a-worker and
|
|
141
144
|
* this factory enforces that decision. No COOP/COEP headers required.
|
|
142
145
|
*
|
|
143
|
-
* Support floor: no OPFS → a loud `ClientSyncError`, never a fallback.
|
|
146
|
+
* Support floor: no OPFS → a loud `ClientSyncError`, never a fallback. This
|
|
147
|
+
* factory cannot request eviction-resistant origin storage because that API
|
|
148
|
+
* belongs to the page's Window context.
|
|
144
149
|
*/
|
|
145
150
|
export async function openPersistentWasmDatabase(name, options) {
|
|
146
151
|
if (!/^[A-Za-z0-9._-]+$/.test(name)) {
|
package/dist/window.d.ts
CHANGED
package/dist/window.js
CHANGED
package/dist/worker-entry.js
CHANGED
package/dist/worker-host.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Main-thread side of the worker mode
|
|
3
|
-
* multi-tab topology
|
|
2
|
+
* Main-thread side of the worker mode and the
|
|
3
|
+
* multi-tab topology.
|
|
4
4
|
*
|
|
5
5
|
* `createSyncClientHandle` acquires the Web Locks leader lock and, when it
|
|
6
6
|
* wins, spawns the worker running the WHOLE core — so exactly one core runs
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* holding the lock). The returned {@link SyncClientHandle} is a thin, fully
|
|
9
9
|
* async proxy over the `worker-protocol` RPC.
|
|
10
10
|
*
|
|
11
|
-
* Multi-tab is the DEFAULT
|
|
11
|
+
* Multi-tab is the DEFAULT. The follower path is
|
|
12
12
|
* conformance-covered): a tab that LOSES the election becomes a FOLLOWER
|
|
13
13
|
* (`role === 'follower'`) that proxies every call to the leader tab over a
|
|
14
14
|
* BroadcastChannel (see `multi-tab.ts`). When the leader tab closes, its
|
|
@@ -86,7 +86,7 @@ export interface SyncClientHandleConfig {
|
|
|
86
86
|
/** Shared by default; isolated derives the database/lock/channel tuple. */
|
|
87
87
|
readonly replica?: BrowserReplicaMode;
|
|
88
88
|
/**
|
|
89
|
-
* Multi-tab followers
|
|
89
|
+
* Multi-tab followers. On by default: a tab that loses the
|
|
90
90
|
* leader election becomes a FOLLOWER that proxies to the leader over a
|
|
91
91
|
* BroadcastChannel, and contests + promotes when the leader closes. Set
|
|
92
92
|
* false for the single-tab contract — the loser is a dead
|
|
@@ -172,7 +172,7 @@ export declare class SyncClientHandle {
|
|
|
172
172
|
/** @internal — dispatch a worker/relayed event to handle-local listeners. */
|
|
173
173
|
__dispatchEvent(event: SyncWorkerEvent): void;
|
|
174
174
|
/**
|
|
175
|
-
*
|
|
175
|
+
* Subscribe to fine-grained invalidation. The identical
|
|
176
176
|
* surface as `SyncClient.onInvalidate`, so React bindings target one
|
|
177
177
|
* interface across direct, worker-leader, and follower modes. Returns an
|
|
178
178
|
* unsubscribe function.
|
package/dist/worker-host.js
CHANGED
|
@@ -98,7 +98,7 @@ export class SyncClientHandle {
|
|
|
98
98
|
this.#diagnostics = internals.diagnostics;
|
|
99
99
|
this.#roleListeners = internals.roleListeners ?? new Set();
|
|
100
100
|
this.#leadershipListeners = internals.leadershipListeners ?? new Set();
|
|
101
|
-
//
|
|
101
|
+
// Console introspection is a no-op outside a dev page.
|
|
102
102
|
this.#devtoolsUnregister = registerDevtools({
|
|
103
103
|
kind: 'handle',
|
|
104
104
|
ref: this,
|
|
@@ -181,7 +181,7 @@ export class SyncClientHandle {
|
|
|
181
181
|
}
|
|
182
182
|
}
|
|
183
183
|
/**
|
|
184
|
-
*
|
|
184
|
+
* Subscribe to fine-grained invalidation. The identical
|
|
185
185
|
* surface as `SyncClient.onInvalidate`, so React bindings target one
|
|
186
186
|
* interface across direct, worker-leader, and follower modes. Returns an
|
|
187
187
|
* unsubscribe function.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The worker RPC protocol
|
|
2
|
+
* The worker RPC protocol: the whole
|
|
3
3
|
* client core runs in a Web Worker; the UI thread talks to it through
|
|
4
4
|
* this thin, multiplexed postMessage protocol. Exactly SIX message types
|
|
5
5
|
* (`init`, `call`, `ready`, `result`, `error`, `event`) — every logical
|
|
@@ -30,14 +30,14 @@ import type { CommitOutcome, CommitOutcomeQuery, ResolveCommitOutcomeInput } fro
|
|
|
30
30
|
import type { ClientSchema } from './schema.js';
|
|
31
31
|
import type { SubscriptionRecord } from './state.js';
|
|
32
32
|
import type { WindowBase } from './window.js';
|
|
33
|
-
/** The handle exists but this tab lost the leader election
|
|
33
|
+
/** The handle exists but this tab lost the leader election. */
|
|
34
34
|
export declare const NOT_LEADER_CODE = "client.not_leader";
|
|
35
35
|
/** The worker (or its RPC channel) failed outside protocol semantics. */
|
|
36
36
|
export declare const WORKER_FAILED_CODE = "client.worker_failed";
|
|
37
37
|
/** A page/worker bundle identity changed and a full host reload is required. */
|
|
38
38
|
export declare const WORKER_RESTART_REQUIRED_CODE = "client.worker_restart_required";
|
|
39
39
|
export type WorkerDatabaseInit = {
|
|
40
|
-
/**
|
|
40
|
+
/** Reload-persistent OPFS mode; origin eviction policy stays page-owned. */
|
|
41
41
|
readonly mode: 'persistent';
|
|
42
42
|
readonly name: string;
|
|
43
43
|
/** Optional pool directory override (default `.syncular/<name>`). */
|
package/dist/worker-protocol.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// ---------------------------------------------------------------------------
|
|
2
2
|
// Client-local error codes (never wire codes; §10 stays server-owned)
|
|
3
3
|
// ---------------------------------------------------------------------------
|
|
4
|
-
/** The handle exists but this tab lost the leader election
|
|
4
|
+
/** The handle exists but this tab lost the leader election. */
|
|
5
5
|
export const NOT_LEADER_CODE = 'client.not_leader';
|
|
6
6
|
/** The worker (or its RPC channel) failed outside protocol semantics. */
|
|
7
7
|
export const WORKER_FAILED_CODE = 'client.worker_failed';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@syncular/client",
|
|
3
|
-
"version": "0.15.
|
|
3
|
+
"version": "0.15.45",
|
|
4
4
|
"description": "Syncular TypeScript client core — offline-first sync over SQLite (WASM/OPFS, Bun, Node)",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Benjamin Kniffler",
|
|
@@ -89,7 +89,7 @@
|
|
|
89
89
|
},
|
|
90
90
|
"dependencies": {
|
|
91
91
|
"@sqlite.org/sqlite-wasm": "^3.53.0-build1",
|
|
92
|
-
"@syncular/core": "0.15.
|
|
92
|
+
"@syncular/core": "0.15.45"
|
|
93
93
|
},
|
|
94
94
|
"peerDependencies": {
|
|
95
95
|
"better-sqlite3": ">=11"
|
|
@@ -100,7 +100,7 @@
|
|
|
100
100
|
}
|
|
101
101
|
},
|
|
102
102
|
"devDependencies": {
|
|
103
|
-
"@syncular/server": "0.15.
|
|
103
|
+
"@syncular/server": "0.15.45",
|
|
104
104
|
"@types/better-sqlite3": "^7.6.13",
|
|
105
105
|
"better-sqlite3": "^12.11.1"
|
|
106
106
|
}
|
|
@@ -0,0 +1,52 @@
|
|
|
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:
|
|
12
|
+
| 'not-granted'
|
|
13
|
+
| 'unavailable'
|
|
14
|
+
| 'check-failed'
|
|
15
|
+
| 'request-failed';
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/** Check whether the current origin is protected from automatic eviction. */
|
|
19
|
+
export async function checkBrowserStoragePersistence(): Promise<BrowserStoragePersistence> {
|
|
20
|
+
const storage =
|
|
21
|
+
typeof navigator === 'undefined' ? undefined : navigator.storage;
|
|
22
|
+
if (storage === undefined || typeof storage.persisted !== 'function') {
|
|
23
|
+
return { state: 'best-effort', reason: 'unavailable' };
|
|
24
|
+
}
|
|
25
|
+
try {
|
|
26
|
+
return (await storage.persisted())
|
|
27
|
+
? { state: 'persistent' }
|
|
28
|
+
: { state: 'best-effort', reason: 'not-granted' };
|
|
29
|
+
} catch {
|
|
30
|
+
return { state: 'best-effort', reason: 'check-failed' };
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Request eviction-resistant storage for the current origin. Call this from a
|
|
36
|
+
* user action near the first important offline write. A best-effort result is
|
|
37
|
+
* an explicit durability state; the OPFS database remains usable.
|
|
38
|
+
*/
|
|
39
|
+
export async function requestBrowserStoragePersistence(): Promise<BrowserStoragePersistence> {
|
|
40
|
+
const storage =
|
|
41
|
+
typeof navigator === 'undefined' ? undefined : navigator.storage;
|
|
42
|
+
if (storage === undefined || typeof storage.persist !== 'function') {
|
|
43
|
+
return { state: 'best-effort', reason: 'unavailable' };
|
|
44
|
+
}
|
|
45
|
+
try {
|
|
46
|
+
return (await storage.persist())
|
|
47
|
+
? { state: 'persistent' }
|
|
48
|
+
: { state: 'best-effort', reason: 'not-granted' };
|
|
49
|
+
} catch {
|
|
50
|
+
return { state: 'best-effort', reason: 'request-failed' };
|
|
51
|
+
}
|
|
52
|
+
}
|
package/src/bun-database.ts
CHANGED
package/src/client.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.
|
|
@@ -708,7 +708,7 @@ export class SyncClient {
|
|
|
708
708
|
this.#config.onSyncNeeded?.('startup');
|
|
709
709
|
this.#config.onSyncIntent?.({ kind: 'interactive' });
|
|
710
710
|
}
|
|
711
|
-
//
|
|
711
|
+
// Console introspection is a no-op outside a dev page.
|
|
712
712
|
this.#devtoolsUnregister = registerDevtools({
|
|
713
713
|
kind: 'client',
|
|
714
714
|
ref: this,
|
|
@@ -881,7 +881,7 @@ export class SyncClient {
|
|
|
881
881
|
return this.#clientId;
|
|
882
882
|
}
|
|
883
883
|
|
|
884
|
-
/** The underlying database
|
|
884
|
+
/** The underlying database: raw SQL is the local query API. */
|
|
885
885
|
get database(): ClientDatabase {
|
|
886
886
|
this.#requireActive();
|
|
887
887
|
return this.#db;
|
|
@@ -960,7 +960,7 @@ export class SyncClient {
|
|
|
960
960
|
});
|
|
961
961
|
}
|
|
962
962
|
|
|
963
|
-
// -- live-query invalidation
|
|
963
|
+
// -- live-query invalidation ----------------------------------------------
|
|
964
964
|
|
|
965
965
|
/**
|
|
966
966
|
* Subscribe to fine-grained invalidation. The callback fires ONCE per
|
|
@@ -2725,7 +2725,7 @@ export class SyncClient {
|
|
|
2725
2725
|
|
|
2726
2726
|
/**
|
|
2727
2727
|
* One request/response round trip (§8.7): over the socket whenever it
|
|
2728
|
-
* is connected (
|
|
2728
|
+
* is connected (the socket IS the sync-round
|
|
2729
2729
|
* transport, not a fallback pair), otherwise through the configured
|
|
2730
2730
|
* `SyncTransport` seam (loopback/conformance hosts, HTTP-only
|
|
2731
2731
|
* producers).
|
package/src/database.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/src/devtools.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/src/http.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
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';
|
|
8
8
|
import { SSP2_CONTENT_TYPE } from './content-type';
|
package/src/index.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';
|
|
12
12
|
export * from './availability';
|
|
13
13
|
export * from './blob';
|
|
14
|
+
export * from './browser-storage-persistence';
|
|
14
15
|
export * from './client';
|
|
15
16
|
export * from './content-type';
|
|
16
17
|
export * from './database';
|
package/src/invalidation.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/src/leader-lock.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.
|
|
@@ -15,7 +15,7 @@ export interface LeaderLock {
|
|
|
15
15
|
* Resolves immediately: the lease when leadership was free, `undefined`
|
|
16
16
|
* when another owner holds it. The worker handle uses this so a second
|
|
17
17
|
* tab gets a clear not-leader state instead of blocking forever
|
|
18
|
-
* (followers are post-gate
|
|
18
|
+
* (followers are post-gate).
|
|
19
19
|
*/
|
|
20
20
|
tryAcquire?(name: string): Promise<LeaderLease | undefined>;
|
|
21
21
|
}
|
package/src/multi-tab.ts
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/src/naming.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/src/node-database.ts
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/src/query-guard.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/src/schema.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Client schema IR (SPEC.md §2.4, §3.1) — the same shape the server
|
|
3
|
-
* compiles
|
|
3
|
+
* compiles and is emitted by codegen. Drives local table
|
|
4
4
|
* DDL, the generated row codec, mutation helpers, and the §3.3 purge
|
|
5
5
|
* mapping (scope variable → local column).
|
|
6
6
|
*/
|
|
@@ -19,7 +19,7 @@ export interface ClientIndexSpec {
|
|
|
19
19
|
readonly unique: boolean;
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
/** One client-local contentful FTS5 projection
|
|
22
|
+
/** One client-local contentful FTS5 projection. */
|
|
23
23
|
export interface ClientFtsIndexSpec {
|
|
24
24
|
readonly name: string;
|
|
25
25
|
readonly columns: readonly string[];
|
|
@@ -63,7 +63,7 @@ export interface CompiledClientTable {
|
|
|
63
63
|
/**
|
|
64
64
|
* Scope variable → the pattern's literal prefix (§3.1). A stored-scope
|
|
65
65
|
* value `v` for this variable has scope key `prefix:v` — the invalidation
|
|
66
|
-
* vocabulary
|
|
66
|
+
* vocabulary and the delta-routing key.
|
|
67
67
|
*/
|
|
68
68
|
readonly scopePrefixByVariable: ReadonlyMap<string, string>;
|
|
69
69
|
/** Local secondary indexes to create on the mirror table (declaration
|
package/src/sql-tag.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The `sql` tagged template — the raw tier's composition helper
|
|
3
|
-
*
|
|
3
|
+
* Structural injection safety: an interpolated
|
|
4
4
|
* value can only ever become a `?` bind parameter; SQL text can only enter
|
|
5
5
|
* through the literal template, `sql.ident()` (allowlist-gated) or a loud
|
|
6
6
|
* `sql.raw()`. This helper is deliberately dumb plumbing and stays that
|
package/src/transport.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Transport seams
|
|
2
|
+
* Transport seams: request/response bytes, segment download,
|
|
3
3
|
* and the realtime attach surface matching §8's client side. Tests use
|
|
4
4
|
* loopback implementations that call the server library directly — the
|
|
5
5
|
* loopback doctrine; HTTP/WebSocket bindings live in `./http`.
|
package/src/wasm-database.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `ClientDatabase` on @sqlite.org/sqlite-wasm
|
|
3
|
-
*
|
|
2
|
+
* `ClientDatabase` on @sqlite.org/sqlite-wasm. Two modes, no ladder between
|
|
3
|
+
* them:
|
|
4
4
|
*
|
|
5
|
-
* - `openPersistentWasmDatabase(name)` — THE persistent browser mode:
|
|
5
|
+
* - `openPersistentWasmDatabase(name)` — THE reload-persistent browser mode:
|
|
6
6
|
* OPFS via the `opfs-sahpool` VFS, restricted to Web Worker contexts
|
|
7
7
|
* because the whole client core runs in a worker by design. SAHPool
|
|
8
8
|
* needs **no COOP/COEP headers and no SharedArrayBuffer** (it is built
|
|
@@ -10,7 +10,10 @@
|
|
|
10
10
|
* proxy — the COOP/COEP requirement documented by sqlite-wasm applies
|
|
11
11
|
* only to `oo1.OpfsDb`, which this binding no longer uses). Browsers
|
|
12
12
|
* without OPFS are unsupported (support floor ~2023+): the factory
|
|
13
|
-
* fails loud. Never IndexedDB, never a silent in-memory fallback.
|
|
13
|
+
* fails loud. Never IndexedDB, never a silent in-memory fallback. The
|
|
14
|
+
* browser's separate origin-eviction policy is exposed from the root package
|
|
15
|
+
* by `checkBrowserStoragePersistence` and
|
|
16
|
+
* `requestBrowserStoragePersistence`.
|
|
14
17
|
* - `openWasmDatabase()` — EXPLICIT ephemeral: an in-memory database for
|
|
15
18
|
* tests, demos and SSR. Nothing survives a reload, on purpose.
|
|
16
19
|
*
|
|
@@ -184,7 +187,7 @@ function initSqlite3(): Promise<Sqlite3Static> {
|
|
|
184
187
|
* EXPLICIT ephemeral mode: an in-memory sqlite-wasm database. For tests,
|
|
185
188
|
* demos and SSR only — nothing persists. The persistent mode is
|
|
186
189
|
* `openPersistentWasmDatabase` inside a worker; there is no fallback from
|
|
187
|
-
* one to the other
|
|
190
|
+
* one to the other.
|
|
188
191
|
*/
|
|
189
192
|
export async function openWasmDatabase(): Promise<ClientDatabase> {
|
|
190
193
|
const sqlite3 = await initSqlite3();
|
|
@@ -238,14 +241,16 @@ function opfsSahPoolError(error: unknown, directory: string): ClientSyncError {
|
|
|
238
241
|
}
|
|
239
242
|
|
|
240
243
|
/**
|
|
241
|
-
* THE persistent browser mode: a named database on OPFS via the
|
|
244
|
+
* THE reload-persistent browser mode: a named database on OPFS via the
|
|
242
245
|
* `opfs-sahpool` VFS. Worker-context only — not because SAHPool requires
|
|
243
246
|
* it (it uses `FileSystemSyncAccessHandle`, no `Atomics.wait`, and could
|
|
244
247
|
* technically run on the main thread), but because the persistent mode IS
|
|
245
|
-
* whole-core-in-a-worker
|
|
248
|
+
* whole-core-in-a-worker and
|
|
246
249
|
* this factory enforces that decision. No COOP/COEP headers required.
|
|
247
250
|
*
|
|
248
|
-
* Support floor: no OPFS → a loud `ClientSyncError`, never a fallback.
|
|
251
|
+
* Support floor: no OPFS → a loud `ClientSyncError`, never a fallback. This
|
|
252
|
+
* factory cannot request eviction-resistant origin storage because that API
|
|
253
|
+
* belongs to the page's Window context.
|
|
249
254
|
*/
|
|
250
255
|
export async function openPersistentWasmDatabase(
|
|
251
256
|
name: string,
|
package/src/window.ts
CHANGED
package/src/worker-entry.ts
CHANGED
package/src/worker-host.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Main-thread side of the worker mode
|
|
3
|
-
* multi-tab topology
|
|
2
|
+
* Main-thread side of the worker mode and the
|
|
3
|
+
* multi-tab topology.
|
|
4
4
|
*
|
|
5
5
|
* `createSyncClientHandle` acquires the Web Locks leader lock and, when it
|
|
6
6
|
* wins, spawns the worker running the WHOLE core — so exactly one core runs
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* holding the lock). The returned {@link SyncClientHandle} is a thin, fully
|
|
9
9
|
* async proxy over the `worker-protocol` RPC.
|
|
10
10
|
*
|
|
11
|
-
* Multi-tab is the DEFAULT
|
|
11
|
+
* Multi-tab is the DEFAULT. The follower path is
|
|
12
12
|
* conformance-covered): a tab that LOSES the election becomes a FOLLOWER
|
|
13
13
|
* (`role === 'follower'`) that proxies every call to the leader tab over a
|
|
14
14
|
* BroadcastChannel (see `multi-tab.ts`). When the leader tab closes, its
|
|
@@ -181,7 +181,7 @@ export interface SyncClientHandleConfig {
|
|
|
181
181
|
/** Shared by default; isolated derives the database/lock/channel tuple. */
|
|
182
182
|
readonly replica?: BrowserReplicaMode;
|
|
183
183
|
/**
|
|
184
|
-
* Multi-tab followers
|
|
184
|
+
* Multi-tab followers. On by default: a tab that loses the
|
|
185
185
|
* leader election becomes a FOLLOWER that proxies to the leader over a
|
|
186
186
|
* BroadcastChannel, and contests + promotes when the leader closes. Set
|
|
187
187
|
* false for the single-tab contract — the loser is a dead
|
|
@@ -318,7 +318,7 @@ export class SyncClientHandle {
|
|
|
318
318
|
this.#diagnostics = internals.diagnostics;
|
|
319
319
|
this.#roleListeners = internals.roleListeners ?? new Set();
|
|
320
320
|
this.#leadershipListeners = internals.leadershipListeners ?? new Set();
|
|
321
|
-
//
|
|
321
|
+
// Console introspection is a no-op outside a dev page.
|
|
322
322
|
this.#devtoolsUnregister = registerDevtools({
|
|
323
323
|
kind: 'handle',
|
|
324
324
|
ref: this,
|
|
@@ -401,7 +401,7 @@ export class SyncClientHandle {
|
|
|
401
401
|
}
|
|
402
402
|
|
|
403
403
|
/**
|
|
404
|
-
*
|
|
404
|
+
* Subscribe to fine-grained invalidation. The identical
|
|
405
405
|
* surface as `SyncClient.onInvalidate`, so React bindings target one
|
|
406
406
|
* interface across direct, worker-leader, and follower modes. Returns an
|
|
407
407
|
* unsubscribe function.
|
package/src/worker-protocol.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The worker RPC protocol
|
|
2
|
+
* The worker RPC protocol: the whole
|
|
3
3
|
* client core runs in a Web Worker; the UI thread talks to it through
|
|
4
4
|
* this thin, multiplexed postMessage protocol. Exactly SIX message types
|
|
5
5
|
* (`init`, `call`, `ready`, `result`, `error`, `event`) — every logical
|
|
@@ -63,7 +63,7 @@ import type { WindowBase } from './window';
|
|
|
63
63
|
// Client-local error codes (never wire codes; §10 stays server-owned)
|
|
64
64
|
// ---------------------------------------------------------------------------
|
|
65
65
|
|
|
66
|
-
/** The handle exists but this tab lost the leader election
|
|
66
|
+
/** The handle exists but this tab lost the leader election. */
|
|
67
67
|
export const NOT_LEADER_CODE = 'client.not_leader';
|
|
68
68
|
/** The worker (or its RPC channel) failed outside protocol semantics. */
|
|
69
69
|
export const WORKER_FAILED_CODE = 'client.worker_failed';
|
|
@@ -76,7 +76,7 @@ export const WORKER_RESTART_REQUIRED_CODE = 'client.worker_restart_required';
|
|
|
76
76
|
|
|
77
77
|
export type WorkerDatabaseInit =
|
|
78
78
|
| {
|
|
79
|
-
/**
|
|
79
|
+
/** Reload-persistent OPFS mode; origin eviction policy stays page-owned. */
|
|
80
80
|
readonly mode: 'persistent';
|
|
81
81
|
readonly name: string;
|
|
82
82
|
/** Optional pool directory override (default `.syncular/<name>`). */
|