@lyku/para-sync 0.0.1-pre.0 → 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 +6 -2
- package/src/client.d.ts +97 -0
- package/src/client.js +52 -2
- package/src/index.d.ts +2 -0
- package/src/transport.d.ts +226 -0
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"
|
|
@@ -8,8 +8,12 @@
|
|
|
8
8
|
"type": "module",
|
|
9
9
|
"main": "./src/index.js",
|
|
10
10
|
"module": "./src/index.js",
|
|
11
|
+
"types": "./src/index.d.ts",
|
|
11
12
|
"exports": {
|
|
12
|
-
".":
|
|
13
|
+
".": {
|
|
14
|
+
"types": "./src/index.d.ts",
|
|
15
|
+
"import": "./src/index.js"
|
|
16
|
+
}
|
|
13
17
|
},
|
|
14
18
|
"dependencies": {
|
|
15
19
|
"@lyku/para-signals": "^0.0.1-pre.0"
|
package/src/client.d.ts
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Create a client replica for one synced key.
|
|
3
|
+
*
|
|
4
|
+
* @param {object} opts
|
|
5
|
+
* @param {string} opts.key synced key, e.g. "user:123"
|
|
6
|
+
* @param {SyncSchema} opts.schema the `parse` gate
|
|
7
|
+
* @param {SyncTransport} opts.transport change-envelope source
|
|
8
|
+
* @param {SyncEnvelope} [opts.seed] SSR-embedded initial envelope
|
|
9
|
+
* @param {() => Promise<SyncEnvelope>} [opts.refetch] Err/skew/gap fallback: fetch the
|
|
10
|
+
* current authoritative snapshot. Omit → no recovery (status goes 'stale').
|
|
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.
|
|
16
|
+
*/
|
|
17
|
+
export function createClientReplica({ key, schema, transport, seed, refetch, cell, schemaVersion, }: {
|
|
18
|
+
key: string;
|
|
19
|
+
schema: SyncSchema;
|
|
20
|
+
transport: SyncTransport;
|
|
21
|
+
seed?: SyncEnvelope;
|
|
22
|
+
refetch?: () => Promise<SyncEnvelope>;
|
|
23
|
+
cell?: Cell;
|
|
24
|
+
schemaVersion?: string;
|
|
25
|
+
}): {
|
|
26
|
+
/** current value (tracked read) */
|
|
27
|
+
get: () => any;
|
|
28
|
+
/** current value (untracked) */
|
|
29
|
+
peek: () => any;
|
|
30
|
+
/** reconcile metadata (tracked): { schemaVersion, sequence, status } */
|
|
31
|
+
meta: () => any;
|
|
32
|
+
/** reconcile metadata (untracked) */
|
|
33
|
+
peekMeta: () => any;
|
|
34
|
+
/** observability counters — read directly */
|
|
35
|
+
stats: {
|
|
36
|
+
applied: number;
|
|
37
|
+
ignoredStale: number;
|
|
38
|
+
gaps: number;
|
|
39
|
+
parseErrors: number;
|
|
40
|
+
refetches: number;
|
|
41
|
+
schemaSkews: number;
|
|
42
|
+
};
|
|
43
|
+
/** resolves when no recovery refetch is in flight (test/await aid) */
|
|
44
|
+
whenIdle: () => Promise<void>;
|
|
45
|
+
/** stop listening; idempotent */
|
|
46
|
+
dispose: () => void;
|
|
47
|
+
};
|
|
48
|
+
export type SyncEnvelope = import("./transport.js").SyncEnvelope;
|
|
49
|
+
export type SyncTransport = import("./transport.js").SyncTransport;
|
|
50
|
+
/**
|
|
51
|
+
* A schema's parse result — matches para-schema's Result<T, string>.
|
|
52
|
+
*/
|
|
53
|
+
export type Result = {
|
|
54
|
+
tag: "Ok";
|
|
55
|
+
value: any;
|
|
56
|
+
} | {
|
|
57
|
+
tag: "Err";
|
|
58
|
+
error: string;
|
|
59
|
+
};
|
|
60
|
+
/**
|
|
61
|
+
* Anything with a `parse` returning {@link Result}. In production this is a
|
|
62
|
+
* para-schema `SchemaValue`; in tests it can be a hand-rolled gate. The replica
|
|
63
|
+
* depends only on this shape, never on para-schema directly — which is also why
|
|
64
|
+
* the client gates branch on `.tag` instead of using the throw-on-Err `::`
|
|
65
|
+
* convention (a malformed delta must trigger recovery, not crash the apply).
|
|
66
|
+
*/
|
|
67
|
+
export type SyncSchema = {
|
|
68
|
+
parse(v: unknown): Result;
|
|
69
|
+
};
|
|
70
|
+
/**
|
|
71
|
+
* A minimal reactive value cell: get / peek / set. A para-signals `signal()`
|
|
72
|
+
* satisfies it exactly and is the default. Injectable for testing and for the
|
|
73
|
+
* fork-backed cell on the para-svelte side.
|
|
74
|
+
*/
|
|
75
|
+
export type Cell = {
|
|
76
|
+
get(): any;
|
|
77
|
+
peek(): any;
|
|
78
|
+
set(v: any): void;
|
|
79
|
+
};
|
|
80
|
+
/**
|
|
81
|
+
* - ok last apply succeeded; replica is current
|
|
82
|
+
* - stale uninitialized, or a refetch failed / none available
|
|
83
|
+
* - skew an inbound value failed `parse` (malformed or schema-skew)
|
|
84
|
+
* - refetching a recovery refetch is in flight
|
|
85
|
+
*/
|
|
86
|
+
export type ReplicaStatus = "ok" | "stale" | "skew" | "refetching";
|
|
87
|
+
export type ReplicaMeta = {
|
|
88
|
+
/**
|
|
89
|
+
* schema version of the applied value
|
|
90
|
+
*/
|
|
91
|
+
schemaVersion: string | null;
|
|
92
|
+
/**
|
|
93
|
+
* sequence of the applied value (-1 if uninitialized)
|
|
94
|
+
*/
|
|
95
|
+
sequence: number;
|
|
96
|
+
status: ReplicaStatus;
|
|
97
|
+
};
|
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") {
|
package/src/index.d.ts
ADDED
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The change envelope. Delta/full-object model: the new value travels with
|
|
3
|
+
* the reconcile key, so steady-state replication is deliver → parse → apply,
|
|
4
|
+
* with no refetch round-trip (refetch is the Err/gap/skew fallback only).
|
|
5
|
+
*
|
|
6
|
+
* @typedef {object} SyncEnvelope
|
|
7
|
+
* @property {unknown} value The full changed object. NOT validated by
|
|
8
|
+
* the transport — `parse` gating is the
|
|
9
|
+
* consumer's job at the apply boundary.
|
|
10
|
+
* @property {string} schema_version Reconcile key, part 1: the model's schema
|
|
11
|
+
* version (e.g. "3.1"). Distinguishes a
|
|
12
|
+
* compatible-but-behind replica from a
|
|
13
|
+
* different/breaking shape.
|
|
14
|
+
* @property {number} sequence Reconcile key, part 2: the object's
|
|
15
|
+
* monotonic Postgres-authoritative sequence.
|
|
16
|
+
* Sole job is ordering + gap detection.
|
|
17
|
+
*/
|
|
18
|
+
/**
|
|
19
|
+
* A listener for a key's change envelopes.
|
|
20
|
+
* @callback SyncHandler
|
|
21
|
+
* @param {SyncEnvelope} envelope
|
|
22
|
+
* @returns {void}
|
|
23
|
+
*/
|
|
24
|
+
/**
|
|
25
|
+
* Returned by subscribe(); calling it removes the subscription. Idempotent.
|
|
26
|
+
* @callback Unsub
|
|
27
|
+
* @returns {void}
|
|
28
|
+
*/
|
|
29
|
+
/**
|
|
30
|
+
* The pluggable transport contract. `synced<T>` depends ONLY on this shape, not
|
|
31
|
+
* on any concrete transport — that decoupling is what lets the same primitive
|
|
32
|
+
* run on a single box (InProcessTransport) or across services (NatsTransport).
|
|
33
|
+
*
|
|
34
|
+
* Contract notes that every implementation must honor:
|
|
35
|
+
* - publish(key, envelope) delivers `envelope` to every current subscriber of
|
|
36
|
+
* `key`. Publishing to a key with no subscribers is a no-op (never throws).
|
|
37
|
+
* - The transport is a dumb pipe: it does NOT retain the latest value, does
|
|
38
|
+
* NOT validate the envelope, and does NOT dedupe by sequence. A subscriber
|
|
39
|
+
* receives only publishes that happen AFTER it subscribes — initial state
|
|
40
|
+
* arrives via the SSR seed, not the transport.
|
|
41
|
+
* - subscribe(key, handler) returns an idempotent Unsub.
|
|
42
|
+
*
|
|
43
|
+
* @typedef {object} SyncTransport
|
|
44
|
+
* @property {(key: string, envelope: SyncEnvelope) => void} publish
|
|
45
|
+
* @property {(key: string, handler: SyncHandler) => Unsub} subscribe
|
|
46
|
+
*/
|
|
47
|
+
/**
|
|
48
|
+
* In-process implementation of {@link SyncTransport}. A keyed pub/sub emitter:
|
|
49
|
+
* `Map<key, Set<handler>>`. No bus, no network, no serialization — the write
|
|
50
|
+
* and the listen happen in the same process, so delivery is a synchronous call.
|
|
51
|
+
*
|
|
52
|
+
* Why a plain emitter and not a para-signal per key: the transport contract is
|
|
53
|
+
* notification semantics ("deliver future publishes to this handler"), not
|
|
54
|
+
* value-cell semantics ("hand me the current value on subscribe, then deltas").
|
|
55
|
+
* Initial state is the SSR seed's job; the transport only carries changes. A
|
|
56
|
+
* keyed emitter models that exactly, without the current-value-on-subscribe and
|
|
57
|
+
* Object.is-dedupe behavior a signal would impose.
|
|
58
|
+
*
|
|
59
|
+
* @implements {SyncTransport}
|
|
60
|
+
*/
|
|
61
|
+
export class InProcessTransport implements SyncTransport {
|
|
62
|
+
/**
|
|
63
|
+
* key → set of handlers. A key is present iff it has ≥1 live subscriber;
|
|
64
|
+
* the entry is deleted when its last subscriber unsubscribes (no key leak).
|
|
65
|
+
* @type {Map<string, Set<SyncHandler>>}
|
|
66
|
+
*/
|
|
67
|
+
_subs: Map<string, Set<SyncHandler>>;
|
|
68
|
+
/**
|
|
69
|
+
* Deliver `envelope` to every current subscriber of `key`, in subscription
|
|
70
|
+
* order. No-op if `key` has no subscribers.
|
|
71
|
+
* @param {string} key
|
|
72
|
+
* @param {SyncEnvelope} envelope
|
|
73
|
+
*/
|
|
74
|
+
publish(key: string, envelope: SyncEnvelope): void;
|
|
75
|
+
/**
|
|
76
|
+
* Subscribe `handler` to `key`'s change envelopes. Returns an idempotent
|
|
77
|
+
* Unsub; calling it more than once is safe and will not remove a handler that
|
|
78
|
+
* was re-subscribed in between.
|
|
79
|
+
* @param {string} key
|
|
80
|
+
* @param {SyncHandler} handler
|
|
81
|
+
* @returns {Unsub}
|
|
82
|
+
*/
|
|
83
|
+
subscribe(key: string, handler: SyncHandler): Unsub;
|
|
84
|
+
/**
|
|
85
|
+
* Diagnostic: number of keys with at least one live subscriber. Useful for
|
|
86
|
+
* leak checks (a healthy server returns to a steady key count as sessions
|
|
87
|
+
* come and go).
|
|
88
|
+
* @returns {number}
|
|
89
|
+
*/
|
|
90
|
+
keyCount(): number;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* NATS implementation of {@link SyncTransport}, for multi-service deployments
|
|
94
|
+
* where a write in the writer's service must reach listeners in other services.
|
|
95
|
+
* Matches Lyku's existing full-object-over-NATS convention.
|
|
96
|
+
*
|
|
97
|
+
* Honors the same contract as InProcessTransport (deliver to current
|
|
98
|
+
* subscribers, no retention, dumb pipe, idempotent unsub). On top it adds:
|
|
99
|
+
* - subject mapping (key → NATS subject),
|
|
100
|
+
* - the wire codec (encode on publish, decode on receipt),
|
|
101
|
+
* - LOCAL FANOUT: N local subscribers to one key share ONE bus subscription,
|
|
102
|
+
* and that bus subscription is torn down when the last local subscriber
|
|
103
|
+
* leaves (the cross-service analog of subscriber-set GC).
|
|
104
|
+
*
|
|
105
|
+
* @implements {SyncTransport}
|
|
106
|
+
*/
|
|
107
|
+
export class NatsTransport implements SyncTransport {
|
|
108
|
+
/**
|
|
109
|
+
* @param {object} opts
|
|
110
|
+
* @param {SyncNatsConnection} opts.connection
|
|
111
|
+
* @param {SyncCodec} [opts.codec] default: identity (tests only)
|
|
112
|
+
* @param {(key: string) => string} [opts.subjectOf] default: `synced.${key}`
|
|
113
|
+
*/
|
|
114
|
+
constructor({ connection, codec, subjectOf }: {
|
|
115
|
+
connection: SyncNatsConnection;
|
|
116
|
+
codec?: SyncCodec;
|
|
117
|
+
subjectOf?: (key: string) => string;
|
|
118
|
+
});
|
|
119
|
+
_nc: SyncNatsConnection;
|
|
120
|
+
_codec: SyncCodec;
|
|
121
|
+
_subjectOf: (key: string) => string;
|
|
122
|
+
/**
|
|
123
|
+
* key → { natsUnsub, handlers } — present iff the key has ≥1 local
|
|
124
|
+
* subscriber (and therefore one live bus subscription).
|
|
125
|
+
* @type {Map<string, { natsUnsub: () => void, handlers: Set<SyncHandler> }>}
|
|
126
|
+
*/
|
|
127
|
+
_keys: Map<string, {
|
|
128
|
+
natsUnsub: () => void;
|
|
129
|
+
handlers: Set<SyncHandler>;
|
|
130
|
+
}>;
|
|
131
|
+
/**
|
|
132
|
+
* @param {string} key
|
|
133
|
+
* @param {SyncEnvelope} envelope
|
|
134
|
+
*/
|
|
135
|
+
publish(key: string, envelope: SyncEnvelope): void;
|
|
136
|
+
/**
|
|
137
|
+
* @param {string} key
|
|
138
|
+
* @param {SyncHandler} handler
|
|
139
|
+
* @returns {Unsub}
|
|
140
|
+
*/
|
|
141
|
+
subscribe(key: string, handler: SyncHandler): Unsub;
|
|
142
|
+
/**
|
|
143
|
+
* Diagnostic: number of keys with a live bus subscription.
|
|
144
|
+
* @returns {number}
|
|
145
|
+
*/
|
|
146
|
+
keyCount(): number;
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* The change envelope. Delta/full-object model: the new value travels with
|
|
150
|
+
* the reconcile key, so steady-state replication is deliver → parse → apply,
|
|
151
|
+
* with no refetch round-trip (refetch is the Err/gap/skew fallback only).
|
|
152
|
+
*/
|
|
153
|
+
export type SyncEnvelope = {
|
|
154
|
+
/**
|
|
155
|
+
* The full changed object. NOT validated by
|
|
156
|
+
* the transport — `parse` gating is the
|
|
157
|
+
* consumer's job at the apply boundary.
|
|
158
|
+
*/
|
|
159
|
+
value: unknown;
|
|
160
|
+
/**
|
|
161
|
+
* Reconcile key, part 1: the model's schema
|
|
162
|
+
* version (e.g. "3.1"). Distinguishes a
|
|
163
|
+
* compatible-but-behind replica from a
|
|
164
|
+
* different/breaking shape.
|
|
165
|
+
*/
|
|
166
|
+
schema_version: string;
|
|
167
|
+
/**
|
|
168
|
+
* Reconcile key, part 2: the object's
|
|
169
|
+
* monotonic Postgres-authoritative sequence.
|
|
170
|
+
* Sole job is ordering + gap detection.
|
|
171
|
+
*/
|
|
172
|
+
sequence: number;
|
|
173
|
+
};
|
|
174
|
+
/**
|
|
175
|
+
* A listener for a key's change envelopes.
|
|
176
|
+
*/
|
|
177
|
+
export type SyncHandler = (envelope: SyncEnvelope) => void;
|
|
178
|
+
/**
|
|
179
|
+
* Returned by subscribe(); calling it removes the subscription. Idempotent.
|
|
180
|
+
*/
|
|
181
|
+
export type Unsub = () => void;
|
|
182
|
+
/**
|
|
183
|
+
* The pluggable transport contract. `synced<T>` depends ONLY on this shape, not
|
|
184
|
+
* on any concrete transport — that decoupling is what lets the same primitive
|
|
185
|
+
* run on a single box (InProcessTransport) or across services (NatsTransport).
|
|
186
|
+
*
|
|
187
|
+
* Contract notes that every implementation must honor:
|
|
188
|
+
* - publish(key, envelope) delivers `envelope` to every current subscriber of
|
|
189
|
+
* `key`. Publishing to a key with no subscribers is a no-op (never throws).
|
|
190
|
+
* - The transport is a dumb pipe: it does NOT retain the latest value, does
|
|
191
|
+
* NOT validate the envelope, and does NOT dedupe by sequence. A subscriber
|
|
192
|
+
* receives only publishes that happen AFTER it subscribes — initial state
|
|
193
|
+
* arrives via the SSR seed, not the transport.
|
|
194
|
+
* - subscribe(key, handler) returns an idempotent Unsub.
|
|
195
|
+
*/
|
|
196
|
+
export type SyncTransport = {
|
|
197
|
+
publish: (key: string, envelope: SyncEnvelope) => void;
|
|
198
|
+
subscribe: (key: string, handler: SyncHandler) => Unsub;
|
|
199
|
+
};
|
|
200
|
+
/**
|
|
201
|
+
* A NATS connection, callback-adapted. NatsTransport expects delivery as a
|
|
202
|
+
* callback + an unsubscribe, NOT nats.js's raw async-iterable subscription —
|
|
203
|
+
* the iterable→callback adaptation is a 3-line caller concern that Lyku already
|
|
204
|
+
* does (`const sub = nc.subscribe(subj); (async () => { for await (const m of
|
|
205
|
+
* sub) onMessage(m.data); })(); return () => sub.unsubscribe();`). Keeping that
|
|
206
|
+
* out of the transport makes delivery synchronous and deterministic to test.
|
|
207
|
+
*/
|
|
208
|
+
export type SyncNatsConnection = {
|
|
209
|
+
publish: (subject: string, payload: Uint8Array) => void;
|
|
210
|
+
/**
|
|
211
|
+
* subscribe to `subject`; `onMessage` is called per message with the raw
|
|
212
|
+
* payload; returns an unsubscribe function.
|
|
213
|
+
*/
|
|
214
|
+
subscribe: (subject: string, onMessage: (payload: Uint8Array) => void) => (() => void);
|
|
215
|
+
};
|
|
216
|
+
/**
|
|
217
|
+
* Wire codec: envelope ⇄ bytes. NATS payloads are `Uint8Array`. In production
|
|
218
|
+
* inject a BON or msgpackr codec (envelopes carry bigint IDs, which JSON cannot
|
|
219
|
+
* represent — BON is the SSR/wire serializer for exactly this reason). Defaults
|
|
220
|
+
* to identity (object passthrough), which works for in-memory fakes/tests but
|
|
221
|
+
* NOT a real NATS connection.
|
|
222
|
+
*/
|
|
223
|
+
export type SyncCodec = {
|
|
224
|
+
encode: (envelope: SyncEnvelope) => any;
|
|
225
|
+
decode: (payload: any) => SyncEnvelope;
|
|
226
|
+
};
|