@lyku/para-sync 0.0.1-pre.1 → 0.0.1-pre.2
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/package.json +1 -1
- package/src/client.d.ts +7 -34
- package/src/client.js +52 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lyku/para-sync",
|
|
3
|
-
"version": "0.0.1-pre.
|
|
3
|
+
"version": "0.0.1-pre.2",
|
|
4
4
|
"description": "synced<T> distributed object sync for the para:* suite — server-authoritative records with live, version-reconciled client replicas. Pre-release: API may change before 0.1.0. Currently provides the pluggable SyncTransport interface and the InProcessTransport implementation (monolith/edge/no-bus deployments); NatsTransport and the synced<T> primitive land on top.",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
package/src/client.d.ts
CHANGED
|
@@ -1,36 +1,3 @@
|
|
|
1
|
-
/** @typedef {import('./transport.js').SyncEnvelope} SyncEnvelope */
|
|
2
|
-
/** @typedef {import('./transport.js').SyncTransport} SyncTransport */
|
|
3
|
-
/**
|
|
4
|
-
* A schema's parse result — matches para-schema's Result<T, string>.
|
|
5
|
-
* @typedef {{ tag: 'Ok', value: any } | { tag: 'Err', error: string }} Result
|
|
6
|
-
*/
|
|
7
|
-
/**
|
|
8
|
-
* Anything with a `parse` returning {@link Result}. In production this is a
|
|
9
|
-
* para-schema `SchemaValue`; in tests it can be a hand-rolled gate. The replica
|
|
10
|
-
* depends only on this shape, never on para-schema directly — which is also why
|
|
11
|
-
* the client gates branch on `.tag` instead of using the throw-on-Err `::`
|
|
12
|
-
* convention (a malformed delta must trigger recovery, not crash the apply).
|
|
13
|
-
* @typedef {{ parse(v: unknown): Result }} SyncSchema
|
|
14
|
-
*/
|
|
15
|
-
/**
|
|
16
|
-
* A minimal reactive value cell: get / peek / set. A para-signals `signal()`
|
|
17
|
-
* satisfies it exactly and is the default. Injectable for testing and for the
|
|
18
|
-
* fork-backed cell on the para-svelte side.
|
|
19
|
-
* @typedef {{ get(): any, peek(): any, set(v: any): void }} Cell
|
|
20
|
-
*/
|
|
21
|
-
/**
|
|
22
|
-
* @typedef {'ok' | 'stale' | 'skew' | 'refetching'} ReplicaStatus
|
|
23
|
-
* - ok last apply succeeded; replica is current
|
|
24
|
-
* - stale uninitialized, or a refetch failed / none available
|
|
25
|
-
* - skew an inbound value failed `parse` (malformed or schema-skew)
|
|
26
|
-
* - refetching a recovery refetch is in flight
|
|
27
|
-
*/
|
|
28
|
-
/**
|
|
29
|
-
* @typedef {object} ReplicaMeta
|
|
30
|
-
* @property {string | null} schemaVersion schema version of the applied value
|
|
31
|
-
* @property {number} sequence sequence of the applied value (-1 if uninitialized)
|
|
32
|
-
* @property {ReplicaStatus} status
|
|
33
|
-
*/
|
|
34
1
|
/**
|
|
35
2
|
* Create a client replica for one synced key.
|
|
36
3
|
*
|
|
@@ -42,14 +9,19 @@
|
|
|
42
9
|
* @param {() => Promise<SyncEnvelope>} [opts.refetch] Err/skew/gap fallback: fetch the
|
|
43
10
|
* current authoritative snapshot. Omit → no recovery (status goes 'stale').
|
|
44
11
|
* @param {Cell} [opts.cell] reactive cell (default: a para signal)
|
|
12
|
+
* @param {string} [opts.schemaVersion] the client's expected schema
|
|
13
|
+
* version ("major.minor"). When set, an inbound envelope whose MAJOR
|
|
14
|
+
* differs is treated as a breaking skew: not applied, recovery refetched.
|
|
15
|
+
* A minor difference is compatible and falls through to the parse gate.
|
|
45
16
|
*/
|
|
46
|
-
export function createClientReplica({ key, schema, transport, seed, refetch, cell }: {
|
|
17
|
+
export function createClientReplica({ key, schema, transport, seed, refetch, cell, schemaVersion, }: {
|
|
47
18
|
key: string;
|
|
48
19
|
schema: SyncSchema;
|
|
49
20
|
transport: SyncTransport;
|
|
50
21
|
seed?: SyncEnvelope;
|
|
51
22
|
refetch?: () => Promise<SyncEnvelope>;
|
|
52
23
|
cell?: Cell;
|
|
24
|
+
schemaVersion?: string;
|
|
53
25
|
}): {
|
|
54
26
|
/** current value (tracked read) */
|
|
55
27
|
get: () => any;
|
|
@@ -66,6 +38,7 @@ export function createClientReplica({ key, schema, transport, seed, refetch, cel
|
|
|
66
38
|
gaps: number;
|
|
67
39
|
parseErrors: number;
|
|
68
40
|
refetches: number;
|
|
41
|
+
schemaSkews: number;
|
|
69
42
|
};
|
|
70
43
|
/** resolves when no recovery refetch is in flight (test/await aid) */
|
|
71
44
|
whenIdle: () => Promise<void>;
|
package/src/client.js
CHANGED
|
@@ -50,6 +50,21 @@ import { signal } from "@lyku/para-signals";
|
|
|
50
50
|
* @property {ReplicaStatus} status
|
|
51
51
|
*/
|
|
52
52
|
|
|
53
|
+
/**
|
|
54
|
+
* Compare two "major.minor" version strings by MAJOR only. Returns true iff both
|
|
55
|
+
* are well-formed and their majors differ (a breaking change). Missing/malformed
|
|
56
|
+
* versions return false — let the parse gate be the backstop rather than block on
|
|
57
|
+
* a version-format quirk.
|
|
58
|
+
* @param {string | undefined} a
|
|
59
|
+
* @param {string | undefined} b
|
|
60
|
+
*/
|
|
61
|
+
function majorMismatch(a, b) {
|
|
62
|
+
const ma = /^(\d+)\./.exec(a == null ? "" : String(a));
|
|
63
|
+
const mb = /^(\d+)\./.exec(b == null ? "" : String(b));
|
|
64
|
+
if (ma === null || mb === null) return false;
|
|
65
|
+
return ma[1] !== mb[1];
|
|
66
|
+
}
|
|
67
|
+
|
|
53
68
|
/**
|
|
54
69
|
* Create a client replica for one synced key.
|
|
55
70
|
*
|
|
@@ -61,8 +76,20 @@ import { signal } from "@lyku/para-signals";
|
|
|
61
76
|
* @param {() => Promise<SyncEnvelope>} [opts.refetch] Err/skew/gap fallback: fetch the
|
|
62
77
|
* current authoritative snapshot. Omit → no recovery (status goes 'stale').
|
|
63
78
|
* @param {Cell} [opts.cell] reactive cell (default: a para signal)
|
|
79
|
+
* @param {string} [opts.schemaVersion] the client's expected schema
|
|
80
|
+
* version ("major.minor"). When set, an inbound envelope whose MAJOR
|
|
81
|
+
* differs is treated as a breaking skew: not applied, recovery refetched.
|
|
82
|
+
* A minor difference is compatible and falls through to the parse gate.
|
|
64
83
|
*/
|
|
65
|
-
export function createClientReplica({
|
|
84
|
+
export function createClientReplica({
|
|
85
|
+
key,
|
|
86
|
+
schema,
|
|
87
|
+
transport,
|
|
88
|
+
seed,
|
|
89
|
+
refetch,
|
|
90
|
+
cell,
|
|
91
|
+
schemaVersion,
|
|
92
|
+
}) {
|
|
66
93
|
const value = cell ?? signal(undefined);
|
|
67
94
|
/** @type {Cell} */
|
|
68
95
|
const meta = signal(
|
|
@@ -73,7 +100,14 @@ export function createClientReplica({ key, schema, transport, seed, refetch, cel
|
|
|
73
100
|
let disposed = false;
|
|
74
101
|
let pending = Promise.resolve();
|
|
75
102
|
|
|
76
|
-
const stats = {
|
|
103
|
+
const stats = {
|
|
104
|
+
applied: 0,
|
|
105
|
+
ignoredStale: 0,
|
|
106
|
+
gaps: 0,
|
|
107
|
+
parseErrors: 0,
|
|
108
|
+
refetches: 0,
|
|
109
|
+
schemaSkews: 0,
|
|
110
|
+
};
|
|
77
111
|
|
|
78
112
|
/** @param {Partial<ReplicaMeta>} patch */
|
|
79
113
|
const setMeta = (patch) => meta.set({ ...meta.peek(), ...patch });
|
|
@@ -110,6 +144,22 @@ export function createClientReplica({ key, schema, transport, seed, refetch, cel
|
|
|
110
144
|
function ingest(envelope, source) {
|
|
111
145
|
if (disposed) return;
|
|
112
146
|
|
|
147
|
+
// ── schema-version gate ──
|
|
148
|
+
// A breaking (major) schema-version difference means the value was produced
|
|
149
|
+
// against an incompatible shape. Don't apply it — the parse gate would
|
|
150
|
+
// likely reject it too, but the version is the explicit, earlier signal and
|
|
151
|
+
// tells us this is "different shape" (refetch), not "behind but compatible".
|
|
152
|
+
// A minor difference (same major) is compatible and falls through to parse.
|
|
153
|
+
if (
|
|
154
|
+
schemaVersion !== undefined &&
|
|
155
|
+
majorMismatch(schemaVersion, envelope.schema_version)
|
|
156
|
+
) {
|
|
157
|
+
stats.schemaSkews++;
|
|
158
|
+
setMeta({ status: "skew" });
|
|
159
|
+
if (source !== "refetch") startRefetch();
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
|
|
113
163
|
// ── parse gate (every inbound value crosses a trust boundary) ──
|
|
114
164
|
const res = schema.parse(envelope.value);
|
|
115
165
|
if (res.tag !== "Ok") {
|