@syncular/react 0.15.48 → 0.16.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/README.md +23 -2
- package/dist/client.d.ts +4 -72
- package/dist/client.js +1 -65
- package/dist/index.d.ts +2 -3
- package/dist/index.js +0 -4
- package/dist/provider.d.ts +3 -3
- package/dist/provider.js +5 -7
- package/dist/runtime-version.d.ts +1 -1
- package/dist/runtime-version.js +1 -1
- package/dist/use-client.d.ts +3 -3
- package/dist/use-client.js +1 -1
- package/dist/use-diagnostics.js +3 -1
- package/dist/use-mutation.d.ts +3 -1
- package/dist/use-mutation.js +1 -1
- package/dist/use-presence.js +21 -10
- package/package.json +5 -4
- package/src/client.ts +4 -180
- package/src/index.ts +2 -6
- package/src/provider.ts +6 -15
- package/src/runtime-version.ts +1 -1
- package/src/use-client.ts +3 -3
- package/src/use-diagnostics.ts +17 -13
- package/src/use-mutation.ts +5 -3
- package/src/use-presence.ts +29 -9
package/README.md
CHANGED
|
@@ -6,7 +6,10 @@ Tauri/React Native bridges.
|
|
|
6
6
|
|
|
7
7
|
The store is client-scoped, not hook-scoped. Equal queries share one local SQL
|
|
8
8
|
read per revision, rows and window completeness come from one SQLite snapshot,
|
|
9
|
-
and stale promises cannot overwrite a newer revision.
|
|
9
|
+
and stale promises cannot overwrite a newer revision. Status, conflict, and
|
|
10
|
+
outcome refreshes discard older pending results after a newer read or event.
|
|
11
|
+
Presence reads follow the same ordering and clear peers when the client or
|
|
12
|
+
scope changes. React is only a
|
|
10
13
|
`useSyncExternalStore` adapter over that renderer-independent state.
|
|
11
14
|
|
|
12
15
|
## Recommended query and mutation path
|
|
@@ -176,7 +179,7 @@ typed `upsert`, `patch`, and `remove` helpers:
|
|
|
176
179
|
|
|
177
180
|
```tsx
|
|
178
181
|
const mutation = useMutation(tasksTable, {
|
|
179
|
-
|
|
182
|
+
onEnqueued(commitId) {},
|
|
180
183
|
onError(error) {},
|
|
181
184
|
});
|
|
182
185
|
|
|
@@ -322,3 +325,21 @@ The snapshot deliberately excludes scopes, rows, clinical counts, SQL, paths,
|
|
|
322
325
|
identities, credentials, mutation bodies, stack traces, and arbitrary prose.
|
|
323
326
|
Do not enrich the copied bundle with database files, query results, or console
|
|
324
327
|
dumps. See SPEC §7.6 for the complete contract.
|
|
328
|
+
|
|
329
|
+
Query and window observers retain inactive entries only until the next
|
|
330
|
+
microtask. The final unsubscribe removes the entry from change dispatch
|
|
331
|
+
immediately; cleanup releases its rows and invalidates pending reads. A
|
|
332
|
+
same-microtask remount preserves the shared snapshot. A later subscription
|
|
333
|
+
starts a fresh read, including subscriptions held by an older React render.
|
|
334
|
+
Releasing the final window owner removes the empty claim group after the
|
|
335
|
+
core has applied the release. Disposing the store rejects unapplied retention
|
|
336
|
+
handles with `client.reactive_store_disposed`.
|
|
337
|
+
|
|
338
|
+
`SyncProvider` keeps the supplied client identity. `useSyncClient()` returns
|
|
339
|
+
that client, whose read methods can return either values or promises. Use
|
|
340
|
+
`await client.statusSnapshot()` in application code that supports multiple
|
|
341
|
+
hosts. The `schemaFloor`, `leaseState`, `upgrading`, and `syncNeeded` fields
|
|
342
|
+
come from that snapshot. Direct `conflicts`, `rejections`, and
|
|
343
|
+
`securityLifecycle` reads are now method calls. `normalizeClient` has been
|
|
344
|
+
removed; custom adapters must implement the canonical snapshot methods.
|
|
345
|
+
See the [client migration](https://syncular.dev/platform-web/#snapshot-api-migration).
|
package/dist/client.d.ts
CHANGED
|
@@ -1,24 +1,6 @@
|
|
|
1
|
-
/**
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
* `SyncClientHandle` (a promise proxy over the OPFS worker) satisfy it —
|
|
5
|
-
* their public surfaces diverge (getters vs methods, sync vs promise), so
|
|
6
|
-
* this module normalizes both into a single async-friendly facade the hooks
|
|
7
|
-
* consume. That is the "one interface across direct and worker-handle modes"
|
|
8
|
-
* the invalidation seam enables.
|
|
9
|
-
*
|
|
10
|
-
* The normalizer resolves each accessor at call time (function → call it,
|
|
11
|
-
* value → read it) and wraps every result in `Promise.resolve`, so a hook
|
|
12
|
-
* never has to care which core it holds.
|
|
13
|
-
*/
|
|
14
|
-
import type { ClientChangeListener, ClientDiagnosticsListener, ClientDiagnosticsRequest, ClientDiagnosticsSnapshot, CommitOutcome, CommitOutcomeQuery, ConflictRecord, InvalidationListener, LeadershipState, LeaseState, LocalDataPurgeInput, LocalDataPurgeResult, LocalDataRebootstrapInput, LocalDataRebootstrapResult, MutationInput, PresencePeer, QueryReadSpec, QuerySnapshot, RejectionRecord, ResolveCommitOutcomeInput, SchemaFloor, SecurityLifecycle, SqlRow, SqlValue, SyncStatusSnapshot, WindowBase, WindowState } from '@syncular/client';
|
|
15
|
-
/**
|
|
16
|
-
* The structural union of `SyncClient` and `SyncClientHandle`. Members that
|
|
17
|
-
* diverge are typed as "value or method, sync or promise"; {@link normalizeClient}
|
|
18
|
-
* collapses the divergence. Only what the hooks use is listed — the bindings
|
|
19
|
-
* never reach past this surface.
|
|
20
|
-
*/
|
|
21
|
-
export interface SyncClientLike {
|
|
1
|
+
/** The provider uses the supplied client identity and canonical snapshot methods. */
|
|
2
|
+
import type { ClientSnapshotReader, ClientChangeListener, ClientDiagnosticsListener, InvalidationListener, LeadershipState, LocalDataPurgeInput, LocalDataPurgeResult, LocalDataRebootstrapInput, LocalDataRebootstrapResult, MutationInput, PresencePeer, QueryReadSpec, QuerySnapshot, SecurityLifecycle, SqlRow, SqlValue, WindowBase, WindowState } from '@syncular/client';
|
|
3
|
+
export interface SyncClientLike extends ClientSnapshotReader {
|
|
22
4
|
readonly currentSchemaVersion?: number;
|
|
23
5
|
onChange(listener: ClientChangeListener): () => void;
|
|
24
6
|
onDiagnostics(listener: ClientDiagnosticsListener): () => void;
|
|
@@ -26,7 +8,7 @@ export interface SyncClientLike {
|
|
|
26
8
|
onPresence(listener: (scopeKey: string) => void): () => void;
|
|
27
9
|
onLeadershipChange?(listener: (state: LeadershipState) => void): () => void;
|
|
28
10
|
leadershipSnapshot?(): LeadershipState | undefined;
|
|
29
|
-
securityLifecycle
|
|
11
|
+
securityLifecycle(): SecurityLifecycle | Promise<SecurityLifecycle>;
|
|
30
12
|
beginSecurityPreflight(): void | Promise<void>;
|
|
31
13
|
/** Key-bearing activation remains available on each concrete host type. */
|
|
32
14
|
activateSecurity(): void | Promise<void>;
|
|
@@ -38,17 +20,6 @@ export interface SyncClientLike {
|
|
|
38
20
|
purgeLocalData(input: LocalDataPurgeInput): LocalDataPurgeResult | Promise<LocalDataPurgeResult>;
|
|
39
21
|
rebootstrapLocalData(input: LocalDataRebootstrapInput): LocalDataRebootstrapResult | Promise<LocalDataRebootstrapResult>;
|
|
40
22
|
querySnapshot<Row = SqlRow>(spec: QueryReadSpec): QuerySnapshot<Row> | Promise<QuerySnapshot<Row>>;
|
|
41
|
-
statusSnapshot(): SyncStatusSnapshot | Promise<SyncStatusSnapshot>;
|
|
42
|
-
diagnosticsSnapshot(request?: ClientDiagnosticsRequest): ClientDiagnosticsSnapshot | Promise<ClientDiagnosticsSnapshot>;
|
|
43
|
-
conflicts: readonly ConflictRecord[] | (() => readonly ConflictRecord[] | Promise<readonly ConflictRecord[]>);
|
|
44
|
-
rejections: readonly RejectionRecord[] | (() => readonly RejectionRecord[] | Promise<readonly RejectionRecord[]>);
|
|
45
|
-
commitOutcome(clientCommitId: string): CommitOutcome | undefined | Promise<CommitOutcome | undefined>;
|
|
46
|
-
commitOutcomes(query?: CommitOutcomeQuery): readonly CommitOutcome[] | Promise<readonly CommitOutcome[]>;
|
|
47
|
-
resolveCommitOutcome(input: ResolveCommitOutcomeInput): CommitOutcome | Promise<CommitOutcome>;
|
|
48
|
-
schemaFloor: SchemaFloor | undefined | (() => SchemaFloor | undefined | Promise<SchemaFloor | undefined>);
|
|
49
|
-
leaseState: LeaseState | undefined | (() => LeaseState | undefined | Promise<LeaseState | undefined>);
|
|
50
|
-
upgrading: boolean | (() => boolean | Promise<boolean>);
|
|
51
|
-
syncNeeded: boolean | (() => boolean | Promise<boolean>);
|
|
52
23
|
pendingCommits: () => unknown[] | Promise<unknown[]>;
|
|
53
24
|
presence(scopeKey: string): readonly PresencePeer[] | Promise<readonly PresencePeer[]>;
|
|
54
25
|
setPresence(scopeKey: string, doc: Record<string, unknown> | null): void | Promise<void>;
|
|
@@ -57,42 +28,3 @@ export interface SyncClientLike {
|
|
|
57
28
|
/** §4.8 completeness oracle (I3): the windowed-in units for a base. */
|
|
58
29
|
windowState(base: WindowBase): WindowState | Promise<WindowState>;
|
|
59
30
|
}
|
|
60
|
-
/** The uniform async facade the hooks actually call. */
|
|
61
|
-
export interface NormalizedClient {
|
|
62
|
-
readonly currentSchemaVersion?: number;
|
|
63
|
-
onChange(listener: ClientChangeListener): () => void;
|
|
64
|
-
onDiagnostics(listener: ClientDiagnosticsListener): () => void;
|
|
65
|
-
onInvalidate(listener: InvalidationListener): () => void;
|
|
66
|
-
onPresence(listener: (scopeKey: string) => void): () => void;
|
|
67
|
-
onLeadershipChange(listener: (state: LeadershipState) => void): () => void;
|
|
68
|
-
leadershipSnapshot(): LeadershipState | undefined;
|
|
69
|
-
securityLifecycle(): Promise<SecurityLifecycle>;
|
|
70
|
-
beginSecurityPreflight(): Promise<void>;
|
|
71
|
-
/** Key-bearing activation remains available on each concrete host type. */
|
|
72
|
-
activateSecurity(): Promise<void>;
|
|
73
|
-
query(sql: string, params?: readonly SqlValue[]): Promise<SqlRow[]>;
|
|
74
|
-
mutate(mutations: readonly MutationInput[]): Promise<string>;
|
|
75
|
-
patch(table: string, rowId: string, partial: Readonly<Record<string, unknown>>, options?: {
|
|
76
|
-
readonly baseVersion?: number;
|
|
77
|
-
}): Promise<string>;
|
|
78
|
-
purgeLocalData(input: LocalDataPurgeInput): Promise<LocalDataPurgeResult>;
|
|
79
|
-
rebootstrapLocalData(input: LocalDataRebootstrapInput): Promise<LocalDataRebootstrapResult>;
|
|
80
|
-
querySnapshot<Row = SqlRow>(spec: QueryReadSpec): Promise<QuerySnapshot<Row>>;
|
|
81
|
-
statusSnapshot(): Promise<SyncStatusSnapshot>;
|
|
82
|
-
diagnosticsSnapshot(request?: ClientDiagnosticsRequest): Promise<ClientDiagnosticsSnapshot>;
|
|
83
|
-
conflicts(): Promise<readonly ConflictRecord[]>;
|
|
84
|
-
rejections(): Promise<readonly RejectionRecord[]>;
|
|
85
|
-
commitOutcome(clientCommitId: string): Promise<CommitOutcome | undefined>;
|
|
86
|
-
commitOutcomes(query?: CommitOutcomeQuery): Promise<readonly CommitOutcome[]>;
|
|
87
|
-
resolveCommitOutcome(input: ResolveCommitOutcomeInput): Promise<CommitOutcome>;
|
|
88
|
-
schemaFloor(): Promise<SchemaFloor | undefined>;
|
|
89
|
-
leaseState(): Promise<LeaseState | undefined>;
|
|
90
|
-
upgrading(): Promise<boolean>;
|
|
91
|
-
syncNeeded(): Promise<boolean>;
|
|
92
|
-
pendingCommits(): Promise<unknown[]>;
|
|
93
|
-
presence(scopeKey: string): Promise<readonly PresencePeer[]>;
|
|
94
|
-
setPresence(scopeKey: string, doc: Record<string, unknown> | null): Promise<void>;
|
|
95
|
-
setWindow(base: WindowBase, units: readonly string[]): Promise<void>;
|
|
96
|
-
windowState(base: WindowBase): Promise<WindowState>;
|
|
97
|
-
}
|
|
98
|
-
export declare function normalizeClient(client: SyncClientLike): NormalizedClient;
|
package/dist/client.js
CHANGED
|
@@ -1,65 +1 @@
|
|
|
1
|
-
|
|
2
|
-
* The ONE client interface the React bindings target. Both the direct
|
|
3
|
-
* `SyncClient` (constructed on the current thread) and the worker-mode
|
|
4
|
-
* `SyncClientHandle` (a promise proxy over the OPFS worker) satisfy it —
|
|
5
|
-
* their public surfaces diverge (getters vs methods, sync vs promise), so
|
|
6
|
-
* this module normalizes both into a single async-friendly facade the hooks
|
|
7
|
-
* consume. That is the "one interface across direct and worker-handle modes"
|
|
8
|
-
* the invalidation seam enables.
|
|
9
|
-
*
|
|
10
|
-
* The normalizer resolves each accessor at call time (function → call it,
|
|
11
|
-
* value → read it) and wraps every result in `Promise.resolve`, so a hook
|
|
12
|
-
* never has to care which core it holds.
|
|
13
|
-
*/
|
|
14
|
-
import { linkRealtimeSupervisorObservation } from '@syncular/client/realtime-supervisor-observation';
|
|
15
|
-
/**
|
|
16
|
-
* Read a member by key that is EITHER a value getter (SyncClient) or a
|
|
17
|
-
* method returning a value/promise (SyncClientHandle). Read from the client
|
|
18
|
-
* so a method keeps its `this` binding; if the read is a function, call it.
|
|
19
|
-
*/
|
|
20
|
-
function resolveMember(client, key) {
|
|
21
|
-
const member = client[key];
|
|
22
|
-
const value = typeof member === 'function'
|
|
23
|
-
? member.call(client)
|
|
24
|
-
: member;
|
|
25
|
-
return Promise.resolve(value);
|
|
26
|
-
}
|
|
27
|
-
export function normalizeClient(client) {
|
|
28
|
-
const normalized = {
|
|
29
|
-
...(client.currentSchemaVersion !== undefined
|
|
30
|
-
? { currentSchemaVersion: client.currentSchemaVersion }
|
|
31
|
-
: {}),
|
|
32
|
-
onChange: (listener) => client.onChange(listener),
|
|
33
|
-
onDiagnostics: (listener) => client.onDiagnostics(listener),
|
|
34
|
-
onInvalidate: (listener) => client.onInvalidate(listener),
|
|
35
|
-
onPresence: (listener) => client.onPresence(listener),
|
|
36
|
-
onLeadershipChange: (listener) => client.onLeadershipChange?.(listener) ?? (() => { }),
|
|
37
|
-
leadershipSnapshot: () => client.leadershipSnapshot?.(),
|
|
38
|
-
securityLifecycle: () => resolveMember(client, 'securityLifecycle'),
|
|
39
|
-
beginSecurityPreflight: () => Promise.resolve(client.beginSecurityPreflight()),
|
|
40
|
-
activateSecurity: () => Promise.resolve(client.activateSecurity()),
|
|
41
|
-
query: (sql, params) => Promise.resolve(client.query(sql, params)),
|
|
42
|
-
mutate: (mutations) => Promise.resolve(client.mutate(mutations)),
|
|
43
|
-
patch: (table, rowId, partial, options) => Promise.resolve(client.patch(table, rowId, partial, options)),
|
|
44
|
-
purgeLocalData: (input) => Promise.resolve(client.purgeLocalData(input)),
|
|
45
|
-
rebootstrapLocalData: (input) => Promise.resolve(client.rebootstrapLocalData(input)),
|
|
46
|
-
querySnapshot: (spec) => Promise.resolve(client.querySnapshot(spec)),
|
|
47
|
-
statusSnapshot: () => Promise.resolve(client.statusSnapshot()),
|
|
48
|
-
diagnosticsSnapshot: (request) => Promise.resolve(client.diagnosticsSnapshot(request)),
|
|
49
|
-
conflicts: () => resolveMember(client, 'conflicts'),
|
|
50
|
-
rejections: () => resolveMember(client, 'rejections'),
|
|
51
|
-
commitOutcome: (clientCommitId) => Promise.resolve(client.commitOutcome(clientCommitId)),
|
|
52
|
-
commitOutcomes: (query) => Promise.resolve(client.commitOutcomes(query)),
|
|
53
|
-
resolveCommitOutcome: (input) => Promise.resolve(client.resolveCommitOutcome(input)),
|
|
54
|
-
schemaFloor: () => resolveMember(client, 'schemaFloor'),
|
|
55
|
-
leaseState: () => resolveMember(client, 'leaseState'),
|
|
56
|
-
upgrading: () => resolveMember(client, 'upgrading'),
|
|
57
|
-
syncNeeded: () => resolveMember(client, 'syncNeeded'),
|
|
58
|
-
pendingCommits: () => resolveMember(client, 'pendingCommits'),
|
|
59
|
-
presence: (scopeKey) => Promise.resolve(client.presence(scopeKey)),
|
|
60
|
-
setPresence: (scopeKey, doc) => Promise.resolve(client.setPresence(scopeKey, doc)),
|
|
61
|
-
setWindow: (base, units) => Promise.resolve(client.setWindow(base, units)),
|
|
62
|
-
windowState: (base) => Promise.resolve(client.windowState(base)),
|
|
63
|
-
};
|
|
64
|
-
return linkRealtimeSupervisorObservation(normalized, client);
|
|
65
|
-
}
|
|
1
|
+
export {};
|
package/dist/index.d.ts
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @syncular/react — React bindings with fine-grained live queries
|
|
3
3
|
* Works against BOTH `SyncClient`
|
|
4
|
-
* (direct) and `SyncClientHandle` (worker) through
|
|
4
|
+
* (direct) and `SyncClientHandle` (worker) through the canonical client
|
|
5
5
|
* interface. React 18+ (react is a peer dependency); no other runtime deps.
|
|
6
6
|
*
|
|
7
7
|
* See README.md for the invalidation granularity truth and the `tables`
|
|
8
8
|
* option.
|
|
9
9
|
*/
|
|
10
|
-
export type {
|
|
11
|
-
export { normalizeClient } from './client.js';
|
|
10
|
+
export type { SyncClientLike } from './client.js';
|
|
12
11
|
export { inferTables } from './infer-tables.js';
|
|
13
12
|
export { type SyncBoundaryActions, type SyncBoundaryState, SyncContext, SyncProvider, type SyncProviderProps, SyncStoreContext, } from './provider.js';
|
|
14
13
|
export { createSyncClientResource, isSyncClientResource, type SyncClientResource, type SyncClientResourceSnapshot, } from './resource.js';
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,3 @@
|
|
|
1
|
-
// `normalizeClient` is the runtime facade the hooks consume; exported so
|
|
2
|
-
// alternate hosts (e.g. `@syncular/tauri`) can assert shape-parity against
|
|
3
|
-
// the exact normalizer the bindings use.
|
|
4
|
-
export { normalizeClient } from './client.js';
|
|
5
1
|
export { inferTables } from './infer-tables.js';
|
|
6
2
|
export { SyncContext, SyncProvider, SyncStoreContext, } from './provider.js';
|
|
7
3
|
export { createSyncClientResource, isSyncClientResource, } from './resource.js';
|
package/dist/provider.d.ts
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `SyncProvider` — supplies a `SyncClient` or `SyncClientHandle` to the
|
|
3
3
|
* hook tree through React context. One provider per client; the hooks read
|
|
4
|
-
* the
|
|
4
|
+
* the supplied client. Written with `createElement` (no JSX) so the whole
|
|
5
5
|
* package typechecks under the repo's `.ts`-only root tsconfig with no jsx
|
|
6
6
|
* setting — the bindings are plain function components either way.
|
|
7
7
|
*/
|
|
8
8
|
import { ReactiveClientStore, type SyncAvailability } from '@syncular/client';
|
|
9
9
|
import { type ReactNode } from 'react';
|
|
10
|
-
import { type
|
|
10
|
+
import { type SyncClientLike } from './client.js';
|
|
11
11
|
import { type SyncClientResource } from './resource.js';
|
|
12
|
-
export declare const SyncContext: import("react").Context<
|
|
12
|
+
export declare const SyncContext: import("react").Context<SyncClientLike | undefined>;
|
|
13
13
|
export declare const SyncStoreContext: import("react").Context<ReactiveClientStore | undefined>;
|
|
14
14
|
export interface SyncProviderProps {
|
|
15
15
|
/** A `SyncClient` (direct) or `SyncClientHandle` (worker) — both satisfy it. */
|
package/dist/provider.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `SyncProvider` — supplies a `SyncClient` or `SyncClientHandle` to the
|
|
3
3
|
* hook tree through React context. One provider per client; the hooks read
|
|
4
|
-
* the
|
|
4
|
+
* the supplied client. Written with `createElement` (no JSX) so the whole
|
|
5
5
|
* package typechecks under the repo's `.ts`-only root tsconfig with no jsx
|
|
6
6
|
* setting — the bindings are plain function components either way.
|
|
7
7
|
*/
|
|
8
8
|
import { ClientSyncError, ReactiveClientStore, } from '@syncular/client';
|
|
9
9
|
import { createContext, createElement, useEffect, useMemo, useSyncExternalStore, } from 'react';
|
|
10
|
-
import {
|
|
10
|
+
import {} from './client.js';
|
|
11
11
|
import { isSyncClientResource } from './resource.js';
|
|
12
12
|
export const SyncContext = createContext(undefined);
|
|
13
13
|
export const SyncStoreContext = createContext(undefined);
|
|
@@ -16,10 +16,8 @@ function recordFor(client) {
|
|
|
16
16
|
const key = client;
|
|
17
17
|
let record = stores.get(key);
|
|
18
18
|
if (record === undefined) {
|
|
19
|
-
const normalized = normalizeClient(client);
|
|
20
19
|
record = {
|
|
21
|
-
|
|
22
|
-
store: new ReactiveClientStore(normalized),
|
|
20
|
+
store: new ReactiveClientStore(client),
|
|
23
21
|
refs: 0,
|
|
24
22
|
};
|
|
25
23
|
stores.set(key, record);
|
|
@@ -28,7 +26,7 @@ function recordFor(client) {
|
|
|
28
26
|
}
|
|
29
27
|
function ReadySyncProvider(props) {
|
|
30
28
|
const record = useMemo(() => recordFor(props.client), [props.client]);
|
|
31
|
-
const {
|
|
29
|
+
const { store } = record;
|
|
32
30
|
const status = useSyncExternalStore(store.status.subscribe, store.status.getSnapshot, store.status.getSnapshot);
|
|
33
31
|
useEffect(() => {
|
|
34
32
|
record.refs += 1;
|
|
@@ -50,7 +48,7 @@ function ReadySyncProvider(props) {
|
|
|
50
48
|
return props.renderBoundary(availability, props.retry === undefined ? {} : { retry: props.retry });
|
|
51
49
|
}
|
|
52
50
|
}
|
|
53
|
-
return createElement(SyncContext.Provider, { value:
|
|
51
|
+
return createElement(SyncContext.Provider, { value: props.client }, createElement(SyncStoreContext.Provider, { value: store }, props.children));
|
|
54
52
|
}
|
|
55
53
|
const noSubscribe = () => () => { };
|
|
56
54
|
export function SyncProvider(props) {
|
|
@@ -3,4 +3,4 @@
|
|
|
3
3
|
* Vite HMR uses it to replace a worker owner after a Syncular package upgrade
|
|
4
4
|
* even when the application's generated schema version did not change.
|
|
5
5
|
*/
|
|
6
|
-
export declare const SYNCULAR_REACT_RUNTIME_VERSION = "0.
|
|
6
|
+
export declare const SYNCULAR_REACT_RUNTIME_VERSION = "0.16.1";
|
package/dist/runtime-version.js
CHANGED
|
@@ -3,4 +3,4 @@
|
|
|
3
3
|
* Vite HMR uses it to replace a worker owner after a Syncular package upgrade
|
|
4
4
|
* even when the application's generated schema version did not change.
|
|
5
5
|
*/
|
|
6
|
-
export const SYNCULAR_REACT_RUNTIME_VERSION = '0.
|
|
6
|
+
export const SYNCULAR_REACT_RUNTIME_VERSION = '0.16.1';
|
package/dist/use-client.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ReactiveClientStore } from '@syncular/client';
|
|
2
|
-
import type {
|
|
3
|
-
/** Read the
|
|
4
|
-
export declare function useSyncClient():
|
|
2
|
+
import type { SyncClientLike } from './client.js';
|
|
3
|
+
/** Read the supplied client from context; throws outside a `SyncProvider`. */
|
|
4
|
+
export declare function useSyncClient(): SyncClientLike;
|
|
5
5
|
export declare function useReactiveStore(): ReactiveClientStore;
|
package/dist/use-client.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useContext } from 'react';
|
|
2
2
|
import { SyncContext, SyncStoreContext } from './provider.js';
|
|
3
|
-
/** Read the
|
|
3
|
+
/** Read the supplied client from context; throws outside a `SyncProvider`. */
|
|
4
4
|
export function useSyncClient() {
|
|
5
5
|
const client = useContext(SyncContext);
|
|
6
6
|
if (client === undefined) {
|
package/dist/use-diagnostics.js
CHANGED
|
@@ -19,7 +19,9 @@ export function useDiagnostics(options = {}) {
|
|
|
19
19
|
const refresh = useCallback(() => {
|
|
20
20
|
const current = ++generation.current;
|
|
21
21
|
setIsLoading(true);
|
|
22
|
-
void
|
|
22
|
+
void Promise.resolve()
|
|
23
|
+
.then(() => client.diagnosticsSnapshot(request))
|
|
24
|
+
.then((next) => {
|
|
23
25
|
if (generation.current !== current)
|
|
24
26
|
return;
|
|
25
27
|
setSnapshot(next);
|
package/dist/use-mutation.d.ts
CHANGED
|
@@ -9,12 +9,14 @@ export interface SyncTableDescriptor<Row, Insert, Update, Id> {
|
|
|
9
9
|
readonly __id?: Id;
|
|
10
10
|
}
|
|
11
11
|
export interface UseMutationOptions {
|
|
12
|
-
|
|
12
|
+
/** Runs after durable local enqueue. The server outcome can still reject this commit. */
|
|
13
|
+
readonly onEnqueued?: (clientCommitId: string) => void;
|
|
13
14
|
readonly onError?: (error: Error) => void;
|
|
14
15
|
}
|
|
15
16
|
export interface UseMutationResult {
|
|
16
17
|
mutate: (mutations: readonly MutationInput[]) => Promise<string>;
|
|
17
18
|
readonly pendingCount: number;
|
|
19
|
+
/** True while at least one local enqueue is unresolved. */
|
|
18
20
|
readonly isPending: boolean;
|
|
19
21
|
readonly error: Error | undefined;
|
|
20
22
|
readonly resetError: () => void;
|
package/dist/use-mutation.js
CHANGED
package/dist/use-presence.js
CHANGED
|
@@ -8,30 +8,41 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import { useEffect, useState } from 'react';
|
|
10
10
|
import { useSyncClient } from './use-client.js';
|
|
11
|
+
const EMPTY_PEERS = [];
|
|
11
12
|
export function usePresence(scopeKey) {
|
|
12
13
|
const client = useSyncClient();
|
|
13
|
-
const [
|
|
14
|
+
const [snapshot, setSnapshot] = useState(() => ({ client, scopeKey, peers: EMPTY_PEERS }));
|
|
15
|
+
if (snapshot.client !== client || snapshot.scopeKey !== scopeKey) {
|
|
16
|
+
setSnapshot({ client, scopeKey, peers: EMPTY_PEERS });
|
|
17
|
+
}
|
|
14
18
|
useEffect(() => {
|
|
15
|
-
let
|
|
19
|
+
let generation = 0;
|
|
16
20
|
const read = () => {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
+
const request = ++generation;
|
|
22
|
+
void Promise.resolve()
|
|
23
|
+
.then(() => client.presence(scopeKey))
|
|
24
|
+
.then((peers) => {
|
|
25
|
+
if (request === generation) {
|
|
26
|
+
setSnapshot((previous) => previous.client === client && previous.scopeKey === scopeKey
|
|
27
|
+
? { client, scopeKey, peers }
|
|
28
|
+
: previous);
|
|
29
|
+
}
|
|
21
30
|
})
|
|
22
31
|
.catch(() => {
|
|
23
|
-
|
|
32
|
+
// Keep the current scope's last snapshot until the next event.
|
|
24
33
|
});
|
|
25
34
|
};
|
|
26
|
-
read();
|
|
27
35
|
const unsubscribe = client.onPresence((changedKey) => {
|
|
28
36
|
if (changedKey === scopeKey)
|
|
29
37
|
read();
|
|
30
38
|
});
|
|
39
|
+
read();
|
|
31
40
|
return () => {
|
|
32
|
-
|
|
41
|
+
generation += 1;
|
|
33
42
|
unsubscribe();
|
|
34
43
|
};
|
|
35
44
|
}, [client, scopeKey]);
|
|
36
|
-
return
|
|
45
|
+
return snapshot.client === client && snapshot.scopeKey === scopeKey
|
|
46
|
+
? snapshot.peers
|
|
47
|
+
: EMPTY_PEERS;
|
|
37
48
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@syncular/react",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.1",
|
|
4
4
|
"description": "React hooks for Syncular offline-first sync",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Benjamin Kniffler",
|
|
@@ -55,17 +55,18 @@
|
|
|
55
55
|
"test": "bun test --preload ./test/setup.ts"
|
|
56
56
|
},
|
|
57
57
|
"dependencies": {
|
|
58
|
-
"@syncular/client": "0.
|
|
58
|
+
"@syncular/client": "0.16.1"
|
|
59
59
|
},
|
|
60
60
|
"peerDependencies": {
|
|
61
61
|
"react": ">=18.0.0"
|
|
62
62
|
},
|
|
63
63
|
"devDependencies": {
|
|
64
64
|
"@happy-dom/global-registrator": "^20.0.0",
|
|
65
|
-
"@syncular/core": "0.
|
|
66
|
-
"@syncular/server": "0.
|
|
65
|
+
"@syncular/core": "0.16.1",
|
|
66
|
+
"@syncular/server": "0.16.1",
|
|
67
67
|
"@testing-library/react": "^16.1.0",
|
|
68
68
|
"@types/react": "^18.3.0",
|
|
69
|
+
"@types/react-dom": "^18.3.0",
|
|
69
70
|
"react": "^18.3.1",
|
|
70
71
|
"react-dom": "^18.3.1",
|
|
71
72
|
"react-router-dom": "^7.18.1",
|
package/src/client.ts
CHANGED
|
@@ -1,28 +1,10 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The ONE client interface the React bindings target. Both the direct
|
|
3
|
-
* `SyncClient` (constructed on the current thread) and the worker-mode
|
|
4
|
-
* `SyncClientHandle` (a promise proxy over the OPFS worker) satisfy it —
|
|
5
|
-
* their public surfaces diverge (getters vs methods, sync vs promise), so
|
|
6
|
-
* this module normalizes both into a single async-friendly facade the hooks
|
|
7
|
-
* consume. That is the "one interface across direct and worker-handle modes"
|
|
8
|
-
* the invalidation seam enables.
|
|
9
|
-
*
|
|
10
|
-
* The normalizer resolves each accessor at call time (function → call it,
|
|
11
|
-
* value → read it) and wraps every result in `Promise.resolve`, so a hook
|
|
12
|
-
* never has to care which core it holds.
|
|
13
|
-
*/
|
|
14
|
-
|
|
1
|
+
/** The provider uses the supplied client identity and canonical snapshot methods. */
|
|
15
2
|
import type {
|
|
3
|
+
ClientSnapshotReader,
|
|
16
4
|
ClientChangeListener,
|
|
17
5
|
ClientDiagnosticsListener,
|
|
18
|
-
ClientDiagnosticsRequest,
|
|
19
|
-
ClientDiagnosticsSnapshot,
|
|
20
|
-
CommitOutcome,
|
|
21
|
-
CommitOutcomeQuery,
|
|
22
|
-
ConflictRecord,
|
|
23
6
|
InvalidationListener,
|
|
24
7
|
LeadershipState,
|
|
25
|
-
LeaseState,
|
|
26
8
|
LocalDataPurgeInput,
|
|
27
9
|
LocalDataPurgeResult,
|
|
28
10
|
LocalDataRebootstrapInput,
|
|
@@ -31,25 +13,14 @@ import type {
|
|
|
31
13
|
PresencePeer,
|
|
32
14
|
QueryReadSpec,
|
|
33
15
|
QuerySnapshot,
|
|
34
|
-
RejectionRecord,
|
|
35
|
-
ResolveCommitOutcomeInput,
|
|
36
|
-
SchemaFloor,
|
|
37
16
|
SecurityLifecycle,
|
|
38
17
|
SqlRow,
|
|
39
18
|
SqlValue,
|
|
40
|
-
SyncStatusSnapshot,
|
|
41
19
|
WindowBase,
|
|
42
20
|
WindowState,
|
|
43
21
|
} from '@syncular/client';
|
|
44
|
-
import { linkRealtimeSupervisorObservation } from '@syncular/client/realtime-supervisor-observation';
|
|
45
22
|
|
|
46
|
-
|
|
47
|
-
* The structural union of `SyncClient` and `SyncClientHandle`. Members that
|
|
48
|
-
* diverge are typed as "value or method, sync or promise"; {@link normalizeClient}
|
|
49
|
-
* collapses the divergence. Only what the hooks use is listed — the bindings
|
|
50
|
-
* never reach past this surface.
|
|
51
|
-
*/
|
|
52
|
-
export interface SyncClientLike {
|
|
23
|
+
export interface SyncClientLike extends ClientSnapshotReader {
|
|
53
24
|
readonly currentSchemaVersion?: number;
|
|
54
25
|
onChange(listener: ClientChangeListener): () => void;
|
|
55
26
|
onDiagnostics(listener: ClientDiagnosticsListener): () => void;
|
|
@@ -57,9 +28,7 @@ export interface SyncClientLike {
|
|
|
57
28
|
onPresence(listener: (scopeKey: string) => void): () => void;
|
|
58
29
|
onLeadershipChange?(listener: (state: LeadershipState) => void): () => void;
|
|
59
30
|
leadershipSnapshot?(): LeadershipState | undefined;
|
|
60
|
-
securityLifecycle:
|
|
61
|
-
| SecurityLifecycle
|
|
62
|
-
| (() => SecurityLifecycle | Promise<SecurityLifecycle>);
|
|
31
|
+
securityLifecycle(): SecurityLifecycle | Promise<SecurityLifecycle>;
|
|
63
32
|
beginSecurityPreflight(): void | Promise<void>;
|
|
64
33
|
/** Key-bearing activation remains available on each concrete host type. */
|
|
65
34
|
activateSecurity(): void | Promise<void>;
|
|
@@ -83,35 +52,6 @@ export interface SyncClientLike {
|
|
|
83
52
|
querySnapshot<Row = SqlRow>(
|
|
84
53
|
spec: QueryReadSpec,
|
|
85
54
|
): QuerySnapshot<Row> | Promise<QuerySnapshot<Row>>;
|
|
86
|
-
statusSnapshot(): SyncStatusSnapshot | Promise<SyncStatusSnapshot>;
|
|
87
|
-
diagnosticsSnapshot(
|
|
88
|
-
request?: ClientDiagnosticsRequest,
|
|
89
|
-
): ClientDiagnosticsSnapshot | Promise<ClientDiagnosticsSnapshot>;
|
|
90
|
-
conflicts:
|
|
91
|
-
| readonly ConflictRecord[]
|
|
92
|
-
| (() => readonly ConflictRecord[] | Promise<readonly ConflictRecord[]>);
|
|
93
|
-
rejections:
|
|
94
|
-
| readonly RejectionRecord[]
|
|
95
|
-
| (() => readonly RejectionRecord[] | Promise<readonly RejectionRecord[]>);
|
|
96
|
-
commitOutcome(
|
|
97
|
-
clientCommitId: string,
|
|
98
|
-
): CommitOutcome | undefined | Promise<CommitOutcome | undefined>;
|
|
99
|
-
commitOutcomes(
|
|
100
|
-
query?: CommitOutcomeQuery,
|
|
101
|
-
): readonly CommitOutcome[] | Promise<readonly CommitOutcome[]>;
|
|
102
|
-
resolveCommitOutcome(
|
|
103
|
-
input: ResolveCommitOutcomeInput,
|
|
104
|
-
): CommitOutcome | Promise<CommitOutcome>;
|
|
105
|
-
schemaFloor:
|
|
106
|
-
| SchemaFloor
|
|
107
|
-
| undefined
|
|
108
|
-
| (() => SchemaFloor | undefined | Promise<SchemaFloor | undefined>);
|
|
109
|
-
leaseState:
|
|
110
|
-
| LeaseState
|
|
111
|
-
| undefined
|
|
112
|
-
| (() => LeaseState | undefined | Promise<LeaseState | undefined>);
|
|
113
|
-
upgrading: boolean | (() => boolean | Promise<boolean>);
|
|
114
|
-
syncNeeded: boolean | (() => boolean | Promise<boolean>);
|
|
115
55
|
pendingCommits: () => unknown[] | Promise<unknown[]>;
|
|
116
56
|
presence(
|
|
117
57
|
scopeKey: string,
|
|
@@ -125,119 +65,3 @@ export interface SyncClientLike {
|
|
|
125
65
|
/** §4.8 completeness oracle (I3): the windowed-in units for a base. */
|
|
126
66
|
windowState(base: WindowBase): WindowState | Promise<WindowState>;
|
|
127
67
|
}
|
|
128
|
-
|
|
129
|
-
/**
|
|
130
|
-
* Read a member by key that is EITHER a value getter (SyncClient) or a
|
|
131
|
-
* method returning a value/promise (SyncClientHandle). Read from the client
|
|
132
|
-
* so a method keeps its `this` binding; if the read is a function, call it.
|
|
133
|
-
*/
|
|
134
|
-
function resolveMember<T>(
|
|
135
|
-
client: SyncClientLike,
|
|
136
|
-
key: keyof SyncClientLike,
|
|
137
|
-
): Promise<T> {
|
|
138
|
-
const member = client[key] as unknown;
|
|
139
|
-
const value =
|
|
140
|
-
typeof member === 'function'
|
|
141
|
-
? (member as (this: SyncClientLike) => T | Promise<T>).call(client)
|
|
142
|
-
: (member as T);
|
|
143
|
-
return Promise.resolve(value as T);
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
/** The uniform async facade the hooks actually call. */
|
|
147
|
-
export interface NormalizedClient {
|
|
148
|
-
readonly currentSchemaVersion?: number;
|
|
149
|
-
onChange(listener: ClientChangeListener): () => void;
|
|
150
|
-
onDiagnostics(listener: ClientDiagnosticsListener): () => void;
|
|
151
|
-
onInvalidate(listener: InvalidationListener): () => void;
|
|
152
|
-
onPresence(listener: (scopeKey: string) => void): () => void;
|
|
153
|
-
onLeadershipChange(listener: (state: LeadershipState) => void): () => void;
|
|
154
|
-
leadershipSnapshot(): LeadershipState | undefined;
|
|
155
|
-
securityLifecycle(): Promise<SecurityLifecycle>;
|
|
156
|
-
beginSecurityPreflight(): Promise<void>;
|
|
157
|
-
/** Key-bearing activation remains available on each concrete host type. */
|
|
158
|
-
activateSecurity(): Promise<void>;
|
|
159
|
-
query(sql: string, params?: readonly SqlValue[]): Promise<SqlRow[]>;
|
|
160
|
-
mutate(mutations: readonly MutationInput[]): Promise<string>;
|
|
161
|
-
patch(
|
|
162
|
-
table: string,
|
|
163
|
-
rowId: string,
|
|
164
|
-
partial: Readonly<Record<string, unknown>>,
|
|
165
|
-
options?: { readonly baseVersion?: number },
|
|
166
|
-
): Promise<string>;
|
|
167
|
-
purgeLocalData(input: LocalDataPurgeInput): Promise<LocalDataPurgeResult>;
|
|
168
|
-
rebootstrapLocalData(
|
|
169
|
-
input: LocalDataRebootstrapInput,
|
|
170
|
-
): Promise<LocalDataRebootstrapResult>;
|
|
171
|
-
querySnapshot<Row = SqlRow>(spec: QueryReadSpec): Promise<QuerySnapshot<Row>>;
|
|
172
|
-
statusSnapshot(): Promise<SyncStatusSnapshot>;
|
|
173
|
-
diagnosticsSnapshot(
|
|
174
|
-
request?: ClientDiagnosticsRequest,
|
|
175
|
-
): Promise<ClientDiagnosticsSnapshot>;
|
|
176
|
-
conflicts(): Promise<readonly ConflictRecord[]>;
|
|
177
|
-
rejections(): Promise<readonly RejectionRecord[]>;
|
|
178
|
-
commitOutcome(clientCommitId: string): Promise<CommitOutcome | undefined>;
|
|
179
|
-
commitOutcomes(query?: CommitOutcomeQuery): Promise<readonly CommitOutcome[]>;
|
|
180
|
-
resolveCommitOutcome(
|
|
181
|
-
input: ResolveCommitOutcomeInput,
|
|
182
|
-
): Promise<CommitOutcome>;
|
|
183
|
-
schemaFloor(): Promise<SchemaFloor | undefined>;
|
|
184
|
-
leaseState(): Promise<LeaseState | undefined>;
|
|
185
|
-
upgrading(): Promise<boolean>;
|
|
186
|
-
syncNeeded(): Promise<boolean>;
|
|
187
|
-
pendingCommits(): Promise<unknown[]>;
|
|
188
|
-
presence(scopeKey: string): Promise<readonly PresencePeer[]>;
|
|
189
|
-
setPresence(
|
|
190
|
-
scopeKey: string,
|
|
191
|
-
doc: Record<string, unknown> | null,
|
|
192
|
-
): Promise<void>;
|
|
193
|
-
setWindow(base: WindowBase, units: readonly string[]): Promise<void>;
|
|
194
|
-
windowState(base: WindowBase): Promise<WindowState>;
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
export function normalizeClient(client: SyncClientLike): NormalizedClient {
|
|
198
|
-
const normalized: NormalizedClient = {
|
|
199
|
-
...(client.currentSchemaVersion !== undefined
|
|
200
|
-
? { currentSchemaVersion: client.currentSchemaVersion }
|
|
201
|
-
: {}),
|
|
202
|
-
onChange: (listener) => client.onChange(listener),
|
|
203
|
-
onDiagnostics: (listener) => client.onDiagnostics(listener),
|
|
204
|
-
onInvalidate: (listener) => client.onInvalidate(listener),
|
|
205
|
-
onPresence: (listener) => client.onPresence(listener),
|
|
206
|
-
onLeadershipChange: (listener) =>
|
|
207
|
-
client.onLeadershipChange?.(listener) ?? (() => {}),
|
|
208
|
-
leadershipSnapshot: () => client.leadershipSnapshot?.(),
|
|
209
|
-
securityLifecycle: () => resolveMember(client, 'securityLifecycle'),
|
|
210
|
-
beginSecurityPreflight: () =>
|
|
211
|
-
Promise.resolve(client.beginSecurityPreflight()),
|
|
212
|
-
activateSecurity: () => Promise.resolve(client.activateSecurity()),
|
|
213
|
-
query: (sql, params) => Promise.resolve(client.query(sql, params)),
|
|
214
|
-
mutate: (mutations) => Promise.resolve(client.mutate(mutations)),
|
|
215
|
-
patch: (table, rowId, partial, options) =>
|
|
216
|
-
Promise.resolve(client.patch(table, rowId, partial, options)),
|
|
217
|
-
purgeLocalData: (input) => Promise.resolve(client.purgeLocalData(input)),
|
|
218
|
-
rebootstrapLocalData: (input) =>
|
|
219
|
-
Promise.resolve(client.rebootstrapLocalData(input)),
|
|
220
|
-
querySnapshot: (spec) => Promise.resolve(client.querySnapshot(spec)),
|
|
221
|
-
statusSnapshot: () => Promise.resolve(client.statusSnapshot()),
|
|
222
|
-
diagnosticsSnapshot: (request) =>
|
|
223
|
-
Promise.resolve(client.diagnosticsSnapshot(request)),
|
|
224
|
-
conflicts: () => resolveMember(client, 'conflicts'),
|
|
225
|
-
rejections: () => resolveMember(client, 'rejections'),
|
|
226
|
-
commitOutcome: (clientCommitId) =>
|
|
227
|
-
Promise.resolve(client.commitOutcome(clientCommitId)),
|
|
228
|
-
commitOutcomes: (query) => Promise.resolve(client.commitOutcomes(query)),
|
|
229
|
-
resolveCommitOutcome: (input) =>
|
|
230
|
-
Promise.resolve(client.resolveCommitOutcome(input)),
|
|
231
|
-
schemaFloor: () => resolveMember(client, 'schemaFloor'),
|
|
232
|
-
leaseState: () => resolveMember(client, 'leaseState'),
|
|
233
|
-
upgrading: () => resolveMember(client, 'upgrading'),
|
|
234
|
-
syncNeeded: () => resolveMember(client, 'syncNeeded'),
|
|
235
|
-
pendingCommits: () => resolveMember(client, 'pendingCommits'),
|
|
236
|
-
presence: (scopeKey) => Promise.resolve(client.presence(scopeKey)),
|
|
237
|
-
setPresence: (scopeKey, doc) =>
|
|
238
|
-
Promise.resolve(client.setPresence(scopeKey, doc)),
|
|
239
|
-
setWindow: (base, units) => Promise.resolve(client.setWindow(base, units)),
|
|
240
|
-
windowState: (base) => Promise.resolve(client.windowState(base)),
|
|
241
|
-
};
|
|
242
|
-
return linkRealtimeSupervisorObservation(normalized, client);
|
|
243
|
-
}
|
package/src/index.ts
CHANGED
|
@@ -1,17 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @syncular/react — React bindings with fine-grained live queries
|
|
3
3
|
* Works against BOTH `SyncClient`
|
|
4
|
-
* (direct) and `SyncClientHandle` (worker) through
|
|
4
|
+
* (direct) and `SyncClientHandle` (worker) through the canonical client
|
|
5
5
|
* interface. React 18+ (react is a peer dependency); no other runtime deps.
|
|
6
6
|
*
|
|
7
7
|
* See README.md for the invalidation granularity truth and the `tables`
|
|
8
8
|
* option.
|
|
9
9
|
*/
|
|
10
|
-
export type {
|
|
11
|
-
// `normalizeClient` is the runtime facade the hooks consume; exported so
|
|
12
|
-
// alternate hosts (e.g. `@syncular/tauri`) can assert shape-parity against
|
|
13
|
-
// the exact normalizer the bindings use.
|
|
14
|
-
export { normalizeClient } from './client';
|
|
10
|
+
export type { SyncClientLike } from './client';
|
|
15
11
|
export { inferTables } from './infer-tables';
|
|
16
12
|
export {
|
|
17
13
|
type SyncBoundaryActions,
|
package/src/provider.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `SyncProvider` — supplies a `SyncClient` or `SyncClientHandle` to the
|
|
3
3
|
* hook tree through React context. One provider per client; the hooks read
|
|
4
|
-
* the
|
|
4
|
+
* the supplied client. Written with `createElement` (no JSX) so the whole
|
|
5
5
|
* package typechecks under the repo's `.ts`-only root tsconfig with no jsx
|
|
6
6
|
* setting — the bindings are plain function components either way.
|
|
7
7
|
*/
|
|
@@ -18,16 +18,10 @@ import {
|
|
|
18
18
|
useMemo,
|
|
19
19
|
useSyncExternalStore,
|
|
20
20
|
} from 'react';
|
|
21
|
-
import {
|
|
22
|
-
type NormalizedClient,
|
|
23
|
-
normalizeClient,
|
|
24
|
-
type SyncClientLike,
|
|
25
|
-
} from './client';
|
|
21
|
+
import { type SyncClientLike } from './client';
|
|
26
22
|
import { isSyncClientResource, type SyncClientResource } from './resource';
|
|
27
23
|
|
|
28
|
-
export const SyncContext = createContext<
|
|
29
|
-
undefined,
|
|
30
|
-
);
|
|
24
|
+
export const SyncContext = createContext<SyncClientLike | undefined>(undefined);
|
|
31
25
|
export const SyncStoreContext = createContext<ReactiveClientStore | undefined>(
|
|
32
26
|
undefined,
|
|
33
27
|
);
|
|
@@ -61,7 +55,6 @@ export interface SyncBoundaryActions {
|
|
|
61
55
|
}
|
|
62
56
|
|
|
63
57
|
interface ClientRecord {
|
|
64
|
-
readonly normalized: NormalizedClient;
|
|
65
58
|
readonly store: ReactiveClientStore;
|
|
66
59
|
refs: number;
|
|
67
60
|
}
|
|
@@ -72,10 +65,8 @@ function recordFor(client: SyncClientLike): ClientRecord {
|
|
|
72
65
|
const key = client as object;
|
|
73
66
|
let record = stores.get(key);
|
|
74
67
|
if (record === undefined) {
|
|
75
|
-
const normalized = normalizeClient(client);
|
|
76
68
|
record = {
|
|
77
|
-
|
|
78
|
-
store: new ReactiveClientStore(normalized),
|
|
69
|
+
store: new ReactiveClientStore(client),
|
|
79
70
|
refs: 0,
|
|
80
71
|
};
|
|
81
72
|
stores.set(key, record);
|
|
@@ -92,7 +83,7 @@ interface ReadySyncProviderProps {
|
|
|
92
83
|
|
|
93
84
|
function ReadySyncProvider(props: ReadySyncProviderProps): ReactNode {
|
|
94
85
|
const record = useMemo(() => recordFor(props.client), [props.client]);
|
|
95
|
-
const {
|
|
86
|
+
const { store } = record;
|
|
96
87
|
const status = useSyncExternalStore(
|
|
97
88
|
store.status.subscribe,
|
|
98
89
|
store.status.getSnapshot,
|
|
@@ -125,7 +116,7 @@ function ReadySyncProvider(props: ReadySyncProviderProps): ReactNode {
|
|
|
125
116
|
}
|
|
126
117
|
return createElement(
|
|
127
118
|
SyncContext.Provider,
|
|
128
|
-
{ value:
|
|
119
|
+
{ value: props.client },
|
|
129
120
|
createElement(SyncStoreContext.Provider, { value: store }, props.children),
|
|
130
121
|
);
|
|
131
122
|
}
|
package/src/runtime-version.ts
CHANGED
|
@@ -3,4 +3,4 @@
|
|
|
3
3
|
* Vite HMR uses it to replace a worker owner after a Syncular package upgrade
|
|
4
4
|
* even when the application's generated schema version did not change.
|
|
5
5
|
*/
|
|
6
|
-
export const SYNCULAR_REACT_RUNTIME_VERSION = '0.
|
|
6
|
+
export const SYNCULAR_REACT_RUNTIME_VERSION = '0.16.1';
|
package/src/use-client.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import type { ReactiveClientStore } from '@syncular/client';
|
|
2
2
|
import { useContext } from 'react';
|
|
3
|
-
import type {
|
|
3
|
+
import type { SyncClientLike } from './client';
|
|
4
4
|
import { SyncContext, SyncStoreContext } from './provider';
|
|
5
5
|
|
|
6
|
-
/** Read the
|
|
7
|
-
export function useSyncClient():
|
|
6
|
+
/** Read the supplied client from context; throws outside a `SyncProvider`. */
|
|
7
|
+
export function useSyncClient(): SyncClientLike {
|
|
8
8
|
const client = useContext(SyncContext);
|
|
9
9
|
if (client === undefined) {
|
|
10
10
|
throw new Error(
|
package/src/use-diagnostics.ts
CHANGED
|
@@ -47,19 +47,23 @@ export function useDiagnostics(
|
|
|
47
47
|
const refresh = useCallback(() => {
|
|
48
48
|
const current = ++generation.current;
|
|
49
49
|
setIsLoading(true);
|
|
50
|
-
void
|
|
51
|
-
(
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
50
|
+
void Promise.resolve()
|
|
51
|
+
.then(() => client.diagnosticsSnapshot(request))
|
|
52
|
+
.then(
|
|
53
|
+
(next) => {
|
|
54
|
+
if (generation.current !== current) return;
|
|
55
|
+
setSnapshot(next);
|
|
56
|
+
setError(undefined);
|
|
57
|
+
setIsLoading(false);
|
|
58
|
+
},
|
|
59
|
+
(reason: unknown) => {
|
|
60
|
+
if (generation.current !== current) return;
|
|
61
|
+
setError(
|
|
62
|
+
reason instanceof Error ? reason : new Error(String(reason)),
|
|
63
|
+
);
|
|
64
|
+
setIsLoading(false);
|
|
65
|
+
},
|
|
66
|
+
);
|
|
63
67
|
}, [client, request]);
|
|
64
68
|
|
|
65
69
|
useEffect(() => {
|
package/src/use-mutation.ts
CHANGED
|
@@ -13,13 +13,15 @@ export interface SyncTableDescriptor<Row, Insert, Update, Id> {
|
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
export interface UseMutationOptions {
|
|
16
|
-
|
|
16
|
+
/** Runs after durable local enqueue. The server outcome can still reject this commit. */
|
|
17
|
+
readonly onEnqueued?: (clientCommitId: string) => void;
|
|
17
18
|
readonly onError?: (error: Error) => void;
|
|
18
19
|
}
|
|
19
20
|
|
|
20
21
|
export interface UseMutationResult {
|
|
21
22
|
mutate: (mutations: readonly MutationInput[]) => Promise<string>;
|
|
22
23
|
readonly pendingCount: number;
|
|
24
|
+
/** True while at least one local enqueue is unresolved. */
|
|
23
25
|
readonly isPending: boolean;
|
|
24
26
|
readonly error: Error | undefined;
|
|
25
27
|
readonly resetError: () => void;
|
|
@@ -66,12 +68,12 @@ export function useMutation<Row, Insert, Update, Id>(
|
|
|
66
68
|
const resetError = useCallback(() => setError(undefined), []);
|
|
67
69
|
|
|
68
70
|
const run = useCallback(
|
|
69
|
-
async (operation: () => Promise<string>): Promise<string> => {
|
|
71
|
+
async (operation: () => string | Promise<string>): Promise<string> => {
|
|
70
72
|
setPendingCount((count) => count + 1);
|
|
71
73
|
setError(undefined);
|
|
72
74
|
try {
|
|
73
75
|
const id = await operation();
|
|
74
|
-
optionsRef.current?.
|
|
76
|
+
optionsRef.current?.onEnqueued?.(id);
|
|
75
77
|
return id;
|
|
76
78
|
} catch (caught) {
|
|
77
79
|
const wrapped =
|
package/src/use-presence.ts
CHANGED
|
@@ -9,32 +9,52 @@
|
|
|
9
9
|
|
|
10
10
|
import type { PresencePeer } from '@syncular/client';
|
|
11
11
|
import { useEffect, useState } from 'react';
|
|
12
|
+
import type { SyncClientLike } from './client';
|
|
12
13
|
import { useSyncClient } from './use-client';
|
|
13
14
|
|
|
15
|
+
const EMPTY_PEERS: readonly PresencePeer[] = [];
|
|
16
|
+
|
|
14
17
|
export function usePresence(scopeKey: string): readonly PresencePeer[] {
|
|
15
18
|
const client = useSyncClient();
|
|
16
|
-
const [
|
|
19
|
+
const [snapshot, setSnapshot] = useState<{
|
|
20
|
+
client: SyncClientLike;
|
|
21
|
+
scopeKey: string;
|
|
22
|
+
peers: readonly PresencePeer[];
|
|
23
|
+
}>(() => ({ client, scopeKey, peers: EMPTY_PEERS }));
|
|
24
|
+
if (snapshot.client !== client || snapshot.scopeKey !== scopeKey) {
|
|
25
|
+
setSnapshot({ client, scopeKey, peers: EMPTY_PEERS });
|
|
26
|
+
}
|
|
17
27
|
|
|
18
28
|
useEffect(() => {
|
|
19
|
-
let
|
|
29
|
+
let generation = 0;
|
|
20
30
|
const read = () => {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
31
|
+
const request = ++generation;
|
|
32
|
+
void Promise.resolve()
|
|
33
|
+
.then(() => client.presence(scopeKey))
|
|
34
|
+
.then((peers) => {
|
|
35
|
+
if (request === generation) {
|
|
36
|
+
setSnapshot((previous) =>
|
|
37
|
+
previous.client === client && previous.scopeKey === scopeKey
|
|
38
|
+
? { client, scopeKey, peers }
|
|
39
|
+
: previous,
|
|
40
|
+
);
|
|
41
|
+
}
|
|
24
42
|
})
|
|
25
43
|
.catch(() => {
|
|
26
|
-
|
|
44
|
+
// Keep the current scope's last snapshot until the next event.
|
|
27
45
|
});
|
|
28
46
|
};
|
|
29
|
-
read();
|
|
30
47
|
const unsubscribe = client.onPresence((changedKey) => {
|
|
31
48
|
if (changedKey === scopeKey) read();
|
|
32
49
|
});
|
|
50
|
+
read();
|
|
33
51
|
return () => {
|
|
34
|
-
|
|
52
|
+
generation += 1;
|
|
35
53
|
unsubscribe();
|
|
36
54
|
};
|
|
37
55
|
}, [client, scopeKey]);
|
|
38
56
|
|
|
39
|
-
return
|
|
57
|
+
return snapshot.client === client && snapshot.scopeKey === scopeKey
|
|
58
|
+
? snapshot.peers
|
|
59
|
+
: EMPTY_PEERS;
|
|
40
60
|
}
|