@jarenjs/contract 0.43.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 +508 -0
- package/dist/types/adapters/fetch.d.ts +27 -0
- package/dist/types/adapters/node.d.ts +47 -0
- package/dist/types/app/binding.d.ts +122 -0
- package/dist/types/app/effect.d.ts +77 -0
- package/dist/types/app/index.d.ts +31 -0
- package/dist/types/app/subscription.d.ts +82 -0
- package/dist/types/bundle.d.ts +43 -0
- package/dist/types/cli.d.ts +15 -0
- package/dist/types/client/http.d.ts +242 -0
- package/dist/types/client/outcome.d.ts +289 -0
- package/dist/types/compat.d.ts +36 -0
- package/dist/types/compile.d.ts +196 -0
- package/dist/types/describe.d.ts +115 -0
- package/dist/types/diff.d.ts +91 -0
- package/dist/types/errors.d.ts +205 -0
- package/dist/types/http/dispatch.d.ts +148 -0
- package/dist/types/http/serve.d.ts +154 -0
- package/dist/types/http/wire.d.ts +334 -0
- package/dist/types/index.d.ts +39 -0
- package/dist/types/ledger.d.ts +207 -0
- package/dist/types/local/index.d.ts +127 -0
- package/dist/types/messages.d.ts +63 -0
- package/dist/types/path.d.ts +119 -0
- package/dist/types/pipeline.d.ts +157 -0
- package/dist/types/port/client.d.ts +142 -0
- package/dist/types/port/frame.d.ts +195 -0
- package/dist/types/port/serve.d.ts +102 -0
- package/dist/types/project/index.d.ts +34 -0
- package/dist/types/project/markdown.d.ts +28 -0
- package/dist/types/project/openapi.d.ts +102 -0
- package/dist/types/project/tools.d.ts +57 -0
- package/dist/types/project/typescript.d.ts +59 -0
- package/dist/types/public.d.ts +73 -0
- package/dist/types/revision.d.ts +36 -0
- package/dist/types/stream/client.d.ts +104 -0
- package/dist/types/stream/server.d.ts +106 -0
- package/dist/types/stream/sse.d.ts +62 -0
- package/docs/APP-INTEGRATION.md +301 -0
- package/docs/CONTRACT-FORMAT.md +1923 -0
- package/package.json +110 -0
- package/schemas/jaren-contract-port.draft-07.schema.json +241 -0
- package/schemas/jaren-contract-port.schema.json +241 -0
- package/schemas/jaren-contract.draft-07.schema.json +287 -0
- package/schemas/jaren-contract.schema.json +287 -0
- package/src/adapters/fetch.js +109 -0
- package/src/adapters/node.js +238 -0
- package/src/app/binding.js +426 -0
- package/src/app/effect.js +190 -0
- package/src/app/index.js +26 -0
- package/src/app/subscription.js +130 -0
- package/src/bundle.js +168 -0
- package/src/cli.js +264 -0
- package/src/client/http.js +1150 -0
- package/src/client/outcome.js +364 -0
- package/src/compat.js +62 -0
- package/src/compile.js +1162 -0
- package/src/describe.js +109 -0
- package/src/diff.js +610 -0
- package/src/errors.js +236 -0
- package/src/http/dispatch.js +1054 -0
- package/src/http/serve.js +301 -0
- package/src/http/wire.js +469 -0
- package/src/index.js +33 -0
- package/src/ledger.js +225 -0
- package/src/local/index.js +363 -0
- package/src/messages.js +68 -0
- package/src/path.js +471 -0
- package/src/pipeline.js +241 -0
- package/src/port/client.js +518 -0
- package/src/port/frame.js +196 -0
- package/src/port/serve.js +442 -0
- package/src/project/index.js +29 -0
- package/src/project/markdown.js +244 -0
- package/src/project/openapi.js +564 -0
- package/src/project/openapi.jslt.json +149 -0
- package/src/project/tools.js +139 -0
- package/src/project/typescript.js +152 -0
- package/src/project/typescript.jtlt.json +72 -0
- package/src/public.js +206 -0
- package/src/revision.js +90 -0
- package/src/stream/client.js +212 -0
- package/src/stream/server.js +306 -0
- package/src/stream/sse.js +67 -0
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
//@ts-check
|
|
2
|
+
/**
|
|
3
|
+
* @file The server half of the stream binding, carrier-neutral
|
|
4
|
+
* (docs/CONTRACT-FORMAT.md §17–§18): take the subscription a handler
|
|
5
|
+
* settled (the duck-typed LIVE shape — `result`/`snapshot()`,
|
|
6
|
+
* `subscribe(cb) → stop`, `close()`, optional `replay(seq)`), decide
|
|
7
|
+
* resumption, read and validate the snapshot, forward each emission,
|
|
8
|
+
* and guarantee `stop()` then `close()` run **exactly once** however
|
|
9
|
+
* the stream ends — peer disconnect, unsubscribe, server shutdown, an
|
|
10
|
+
* error emission, an invalid snapshot. The HTTP dispatcher renders the
|
|
11
|
+
* intents this module raises as SSE events; the port server renders
|
|
12
|
+
* them as push frames — the sequencing is decided here once so the two
|
|
13
|
+
* carriers can never disagree.
|
|
14
|
+
*
|
|
15
|
+
* Total for everything a handler's subscription can do: a throwing
|
|
16
|
+
* `snapshot()`/`result` accessor, a `subscribe` that throws, a hostile
|
|
17
|
+
* emission, a throwing `stop`/`close` — every one settles into the
|
|
18
|
+
* `fault` intent (the cause for the binding's `onError`, never the
|
|
19
|
+
* wire) or is swallowed at close, and the stream still terminates.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { toPromise, isThenable } from '@jarenjs/core/function';
|
|
23
|
+
|
|
24
|
+
import { verdict } from '../http/wire.js';
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* @typedef {import('../pipeline.js').PipelineRoute} PipelineRoute
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* The duck-typed subscription of docs/CONTRACT-FORMAT.md §17.1.
|
|
32
|
+
* @typedef {{ result?: unknown, snapshot?: () => unknown,
|
|
33
|
+
* subscribe: (cb: (emission: any) => void) => (() => void),
|
|
34
|
+
* close: () => void, replay?: (seq: number) => unknown, mode?: unknown }} SubscriptionLike
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* What the carrier renders. Every hook is called at most once per
|
|
39
|
+
* event, in wire order; after `error` or `end` no further hook fires.
|
|
40
|
+
* `error` carries the intent (`'invalid-snapshot'` — send `JC2091`;
|
|
41
|
+
* `'source'` — the subscription emitted `{ error }`, send the host
|
|
42
|
+
* fault code) and the cause for the observer. `done` fires exactly once
|
|
43
|
+
* after the stream terminated for any reason — the carrier releases
|
|
44
|
+
* its resources (timers, registries) there.
|
|
45
|
+
* @typedef {Object} StreamHooks
|
|
46
|
+
* @property {(seq: number, value: unknown, resumed: boolean) => void} snapshot
|
|
47
|
+
* @property {(seq: number, emission: { patch: unknown[], seq: number }) => void} patch
|
|
48
|
+
* @property {(intent: 'invalid-snapshot' | 'source', cause: unknown, lastSeq: number) => void} error
|
|
49
|
+
* @property {(reason: string, lastSeq: number) => void} end
|
|
50
|
+
* @property {() => void} done
|
|
51
|
+
*/
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* @typedef {Object} StreamOptions
|
|
55
|
+
* @property {number | null} lastSeq - the peer's `Last-Event-ID` / `lastSeq`, or `null`
|
|
56
|
+
* @property {boolean} validate - whether snapshots run the output validator
|
|
57
|
+
*/
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Whether a settled handler value is a usable subscription.
|
|
61
|
+
* Reads guardedly; a hostile value classifies as "not a subscription".
|
|
62
|
+
* @param {unknown} value
|
|
63
|
+
* @returns {value is SubscriptionLike}
|
|
64
|
+
*/
|
|
65
|
+
export function isSubscriptionLike(value) {
|
|
66
|
+
try {
|
|
67
|
+
if (value === null || typeof value !== 'object') return false;
|
|
68
|
+
const s = /** @type {any} */ (value);
|
|
69
|
+
return typeof s.subscribe === 'function' && typeof s.close === 'function'
|
|
70
|
+
&& (typeof s.snapshot === 'function' || s.result !== undefined);
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* A guarded read of the current snapshot document.
|
|
79
|
+
* @param {SubscriptionLike} sub
|
|
80
|
+
* @returns {{ ok: true, value: unknown } | { ok: false, cause: unknown }}
|
|
81
|
+
*/
|
|
82
|
+
function readSnapshot(sub) {
|
|
83
|
+
try {
|
|
84
|
+
return { ok: true, value: typeof sub.snapshot === 'function' ? sub.snapshot() : sub.result };
|
|
85
|
+
}
|
|
86
|
+
catch (err) {
|
|
87
|
+
return { ok: false, cause: err };
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Run one subscription over a carrier. Emissions are forwarded
|
|
93
|
+
* verbatim (a patch is never mutated); an emission whose serialized
|
|
94
|
+
* patch exceeds `policy.stream.maxPatchBytes` is replaced by a fresh
|
|
95
|
+
* snapshot at that emission's seq (§18.1); an `{ error }` emission —
|
|
96
|
+
* and a hostile one — raises the `error` intent and ends the stream.
|
|
97
|
+
*
|
|
98
|
+
* Returns the stopper: `stop(reason)` with a string emits the `end`
|
|
99
|
+
* event with that reason first (a server shutdown); `stop(null)` is
|
|
100
|
+
* silent (the peer is gone, or asked). Idempotent; the subscription's
|
|
101
|
+
* own `stop()` and `close()` run exactly once either way.
|
|
102
|
+
*
|
|
103
|
+
* @param {PipelineRoute} route
|
|
104
|
+
* @param {SubscriptionLike} sub
|
|
105
|
+
* @param {StreamHooks} hooks
|
|
106
|
+
* @param {StreamOptions} options
|
|
107
|
+
* @returns {{ stop: (reason: string | null) => void }}
|
|
108
|
+
*/
|
|
109
|
+
export function runSubscription(route, sub, hooks, options) {
|
|
110
|
+
const policy = /** @type {NonNullable<import('../compile.js').CompiledPolicy['stream']>} */ (
|
|
111
|
+
route.op.policy.stream ?? { resume: 'snapshot', heartbeatMs: 15000, maxPatchBytes: null });
|
|
112
|
+
const maxPatchBytes = policy.maxPatchBytes;
|
|
113
|
+
let finished = false;
|
|
114
|
+
let ready = false;
|
|
115
|
+
/** @type {any[]} */
|
|
116
|
+
const buffered = [];
|
|
117
|
+
/** @type {(() => void) | null} */
|
|
118
|
+
let stopSub = null;
|
|
119
|
+
let lastSeq = 0;
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* The one stopper every path returns: on a finished stream it is a
|
|
123
|
+
* no-op, so an early-failed stream hands back the same shape.
|
|
124
|
+
* @type {{ stop: (reason: string | null) => void }}
|
|
125
|
+
*/
|
|
126
|
+
const stopper = {
|
|
127
|
+
stop: (reason) => {
|
|
128
|
+
if (finished) return;
|
|
129
|
+
if (reason !== null) {
|
|
130
|
+
try {
|
|
131
|
+
hooks.end(reason, lastSeq);
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
// the end event is best-effort on a dying carrier
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
release();
|
|
138
|
+
},
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
/** Release the subscription exactly once; a throwing stop/close is swallowed. */
|
|
142
|
+
function release() {
|
|
143
|
+
if (finished) return;
|
|
144
|
+
finished = true;
|
|
145
|
+
if (stopSub !== null) {
|
|
146
|
+
try {
|
|
147
|
+
stopSub();
|
|
148
|
+
}
|
|
149
|
+
catch {
|
|
150
|
+
// a throwing stop never blocks the close
|
|
151
|
+
}
|
|
152
|
+
stopSub = null;
|
|
153
|
+
}
|
|
154
|
+
try {
|
|
155
|
+
sub.close();
|
|
156
|
+
}
|
|
157
|
+
catch {
|
|
158
|
+
// a throwing close never blocks termination
|
|
159
|
+
}
|
|
160
|
+
try {
|
|
161
|
+
hooks.done();
|
|
162
|
+
}
|
|
163
|
+
catch {
|
|
164
|
+
// the carrier's cleanup must not break termination
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* End with an error intent: the carrier renders the event, then the
|
|
170
|
+
* subscription is released.
|
|
171
|
+
* @param {'invalid-snapshot' | 'source'} intent
|
|
172
|
+
* @param {unknown} cause
|
|
173
|
+
*/
|
|
174
|
+
function fail(intent, cause) {
|
|
175
|
+
if (finished) return;
|
|
176
|
+
try {
|
|
177
|
+
hooks.error(intent, cause, lastSeq);
|
|
178
|
+
}
|
|
179
|
+
catch {
|
|
180
|
+
// a carrier that cannot render still releases
|
|
181
|
+
}
|
|
182
|
+
release();
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Read, validate and emit a fresh snapshot at `seq`.
|
|
187
|
+
* @param {number} seq
|
|
188
|
+
* @param {boolean} resumed
|
|
189
|
+
* @returns {boolean} false when the stream ended instead
|
|
190
|
+
*/
|
|
191
|
+
function emitSnapshot(seq, resumed) {
|
|
192
|
+
const snap = readSnapshot(sub);
|
|
193
|
+
if (!snap.ok) {
|
|
194
|
+
fail('source', snap.cause);
|
|
195
|
+
return false;
|
|
196
|
+
}
|
|
197
|
+
if (options.validate) {
|
|
198
|
+
const v = verdict(route.validateOutput, snap.value);
|
|
199
|
+
if (!v.valid) {
|
|
200
|
+
fail('invalid-snapshot', v.thrown !== undefined ? v.thrown : v.errors);
|
|
201
|
+
return false;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
lastSeq = seq;
|
|
205
|
+
hooks.snapshot(seq, snap.value, resumed);
|
|
206
|
+
return true;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* One emission from the subscription, delivered or buffered.
|
|
211
|
+
* @param {any} emission
|
|
212
|
+
*/
|
|
213
|
+
function deliver(emission) {
|
|
214
|
+
if (finished) return;
|
|
215
|
+
if (!ready) {
|
|
216
|
+
buffered.push(emission);
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
let patch;
|
|
220
|
+
let seq;
|
|
221
|
+
let sourceError;
|
|
222
|
+
try {
|
|
223
|
+
sourceError = emission === null || typeof emission !== 'object' ? new TypeError('the subscription emitted a non-object') : undefined;
|
|
224
|
+
if (sourceError === undefined && emission.error !== undefined) sourceError = emission.error;
|
|
225
|
+
if (sourceError === undefined) {
|
|
226
|
+
patch = emission.patch;
|
|
227
|
+
seq = emission.seq;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
catch (err) {
|
|
231
|
+
sourceError = err;
|
|
232
|
+
}
|
|
233
|
+
if (sourceError !== undefined) {
|
|
234
|
+
fail('source', sourceError);
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
if (!Array.isArray(patch) || !Number.isFinite(seq)) {
|
|
238
|
+
fail('source', new TypeError('the subscription emitted a value that is not { patch, seq }'));
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
if (seq <= lastSeq && lastSeq !== 0) return; // an already-delivered record (a replay overlap)
|
|
242
|
+
if (maxPatchBytes !== null && JSON.stringify(patch).length > maxPatchBytes) {
|
|
243
|
+
// the consumer swaps its document instead of patching it (§18.1)
|
|
244
|
+
emitSnapshot(seq, false);
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
lastSeq = seq;
|
|
248
|
+
hooks.patch(seq, { patch, seq });
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// subscribe FIRST so nothing between the subscription and the initial
|
|
252
|
+
// events is lost; emissions buffer until the initial events are out.
|
|
253
|
+
// In the synchronous snapshot path nothing can land in between — the
|
|
254
|
+
// buffer exists for the (possibly asynchronous) replay path.
|
|
255
|
+
try {
|
|
256
|
+
stopSub = sub.subscribe(deliver);
|
|
257
|
+
}
|
|
258
|
+
catch (err) {
|
|
259
|
+
fail('source', err);
|
|
260
|
+
return stopper;
|
|
261
|
+
}
|
|
262
|
+
if (typeof stopSub !== 'function') stopSub = null;
|
|
263
|
+
|
|
264
|
+
/** Flush what buffered while the initial events were decided. */
|
|
265
|
+
function flush() {
|
|
266
|
+
ready = true;
|
|
267
|
+
while (buffered.length > 0 && !finished) deliver(buffered.shift());
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const wantsReplay = options.lastSeq !== null && policy.resume === 'replay' && typeof sub.replay === 'function';
|
|
271
|
+
if (wantsReplay) {
|
|
272
|
+
let replayed;
|
|
273
|
+
try {
|
|
274
|
+
replayed = /** @type {NonNullable<SubscriptionLike['replay']>} */ (sub.replay)(/** @type {number} */ (options.lastSeq));
|
|
275
|
+
}
|
|
276
|
+
catch (err) {
|
|
277
|
+
fail('source', err);
|
|
278
|
+
return stopper;
|
|
279
|
+
}
|
|
280
|
+
const settle = (/** @type {unknown} */ entries) => {
|
|
281
|
+
if (finished) return;
|
|
282
|
+
if (Array.isArray(entries)) {
|
|
283
|
+
lastSeq = /** @type {number} */ (options.lastSeq);
|
|
284
|
+
ready = true;
|
|
285
|
+
for (let i = 0; i < entries.length && !finished; i++) deliver(entries[i]);
|
|
286
|
+
flush();
|
|
287
|
+
}
|
|
288
|
+
else {
|
|
289
|
+
// the handler cannot replay: a fresh snapshot, resume refused (JC2095)
|
|
290
|
+
if (emitSnapshot(0, false)) flush();
|
|
291
|
+
}
|
|
292
|
+
};
|
|
293
|
+
if (isThenable(replayed)) {
|
|
294
|
+
toPromise(replayed).then(settle, (/** @type {unknown} */ err) => fail('source', err));
|
|
295
|
+
}
|
|
296
|
+
else settle(replayed);
|
|
297
|
+
}
|
|
298
|
+
else {
|
|
299
|
+
// a fresh stream, or a resume the policy answers with a snapshot
|
|
300
|
+
if (emitSnapshot(0, false)) flush();
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
return stopper;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
export { STREAM_ERRORS, STREAM_EVENTS, STREAM_MEDIA, HEARTBEAT_LINE, encodeStreamEvent } from './sse.js';
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
//@ts-check
|
|
2
|
+
/**
|
|
3
|
+
* @file The stream wire's shared shapes (docs/CONTRACT-FORMAT.md §18):
|
|
4
|
+
* the stream code table as data (`STREAM_ERRORS`, the same
|
|
5
|
+
* table-as-data shape as `HTTP_ERRORS` and `PORT_LOCAL_ERRORS`), the
|
|
6
|
+
* event names both carriers speak, and the SSE text framing on top of
|
|
7
|
+
* `@jarenjs/core/text/sse` — one JSON value per event, a heartbeat
|
|
8
|
+
* comment line, and the `JC1009` host refusal for text the frame
|
|
9
|
+
* cannot carry. The port carrier uses the same tables and data shapes
|
|
10
|
+
* with no SSE text at all (§18.2).
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { encodeSseEvent } from '@jarenjs/core/text/sse';
|
|
14
|
+
|
|
15
|
+
import { ContractHostError } from '../errors.js';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* The stream request-time codes — code → `{ kind, msgid, retryable }`.
|
|
19
|
+
* The normative table is docs/CONTRACT-FORMAT.md §18.3; a test holds
|
|
20
|
+
* them equal. `JC2095` (a refused resume) is deliberately absent: it is
|
|
21
|
+
* informational, carried as `resumed: false` in a snapshot's event
|
|
22
|
+
* data, never an outcome.
|
|
23
|
+
*/
|
|
24
|
+
export const STREAM_ERRORS = Object.freeze({
|
|
25
|
+
JC2090: Object.freeze({ kind: 'contract', msgid: 'contract/not-a-stream', retryable: false }),
|
|
26
|
+
JC2091: Object.freeze({ kind: 'contract', msgid: 'contract/invalid-snapshot', retryable: false }),
|
|
27
|
+
JC2092: Object.freeze({ kind: 'contract', msgid: 'contract/seq-regression', retryable: false }),
|
|
28
|
+
JC2093: Object.freeze({ kind: 'contract', msgid: 'contract/stream-error', retryable: false }),
|
|
29
|
+
JC2094: Object.freeze({ kind: 'network', msgid: 'contract/heartbeat-missed', retryable: true }),
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
/** The event names of the stream wire, on both carriers. */
|
|
33
|
+
export const STREAM_EVENTS = Object.freeze(['snapshot', 'patch', 'error', 'end']);
|
|
34
|
+
|
|
35
|
+
/** The media type of the SSE carrier. */
|
|
36
|
+
export const STREAM_MEDIA = 'text/event-stream';
|
|
37
|
+
|
|
38
|
+
/** The heartbeat comment line the SSE carrier writes every `heartbeatMs`. */
|
|
39
|
+
export const HEARTBEAT_LINE = ':\n\n';
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* One stream event as SSE text: the event name, the seq as the SSE id,
|
|
43
|
+
* the JSON data. JSON output never carries a raw CR, so the encoder's
|
|
44
|
+
* refusal is reachable only through a host handing this function
|
|
45
|
+
* non-JSON text — `JC1009`, thrown.
|
|
46
|
+
* @param {string} event - `snapshot | patch | error | end`
|
|
47
|
+
* @param {number | null} seq - the event's seq; `null` writes no id line
|
|
48
|
+
* @param {unknown} data - the event's JSON data
|
|
49
|
+
* @returns {string}
|
|
50
|
+
* @throws {ContractHostError} `JC1009` when the text cannot ride an SSE frame
|
|
51
|
+
*/
|
|
52
|
+
export function encodeStreamEvent(event, seq, data) {
|
|
53
|
+
let text;
|
|
54
|
+
try {
|
|
55
|
+
text = JSON.stringify(data);
|
|
56
|
+
}
|
|
57
|
+
catch (err) {
|
|
58
|
+
throw new ContractHostError('JC1009', `the ${event} event's data cannot be serialized as JSON (${err instanceof Error ? err.message : 'not JSON'})`);
|
|
59
|
+
}
|
|
60
|
+
if (text === undefined) text = 'null';
|
|
61
|
+
try {
|
|
62
|
+
return encodeSseEvent({ event, id: seq === null ? null : String(seq), data: text });
|
|
63
|
+
}
|
|
64
|
+
catch (err) {
|
|
65
|
+
throw new ContractHostError('JC1009', err instanceof Error ? err.message : 'the event cannot ride an SSE frame');
|
|
66
|
+
}
|
|
67
|
+
}
|