@econ-v1/rpc 6.7.0 → 6.10.0
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/dist/client-device-pending-contract.d.ts +14 -0
- package/dist/client-device-pending-contract.d.ts.map +1 -0
- package/dist/client.d.ts +15 -13
- package/dist/client.d.ts.map +1 -1
- package/dist/device-boot-contract.d.ts +7 -0
- package/dist/device-boot-contract.d.ts.map +1 -0
- package/dist/device-cleanup-contract.d.ts +17 -0
- package/dist/device-cleanup-contract.d.ts.map +1 -0
- package/dist/dialog-contract.d.ts +26 -0
- package/dist/dialog-contract.d.ts.map +1 -0
- package/dist/errors.d.ts +33 -6
- package/dist/errors.d.ts.map +1 -1
- package/dist/index.d.ts +144 -14
- package/dist/index.d.ts.map +1 -1
- package/dist/message-port-client.d.ts +134 -16
- package/dist/message-port-client.d.ts.map +1 -1
- package/dist/node-app-memory-contract.d.ts +10 -0
- package/dist/node-app-memory-contract.d.ts.map +1 -0
- package/dist/onboarding/discovery-contract.d.ts +61 -34
- package/dist/onboarding/discovery-contract.d.ts.map +1 -1
- package/dist/onboarding/onboarding-contract.d.ts +119 -27
- package/dist/onboarding/onboarding-contract.d.ts.map +1 -1
- package/dist/onboarding/owner-claim-contract.d.ts +50 -0
- package/dist/onboarding/owner-claim-contract.d.ts.map +1 -0
- package/dist/pairing-status-contract.d.ts +40 -0
- package/dist/pairing-status-contract.d.ts.map +1 -0
- package/dist/protocol.d.ts +50 -12
- package/dist/protocol.d.ts.map +1 -1
- package/dist/stage-context.d.ts +31 -16
- package/dist/stage-context.d.ts.map +1 -1
- package/dist/sync-cursors-contract.d.ts +6 -0
- package/dist/sync-cursors-contract.d.ts.map +1 -0
- package/dist/system-notice-contract.d.ts +70 -0
- package/dist/system-notice-contract.d.ts.map +1 -0
- package/dist/transport-contract.d.ts +11 -0
- package/dist/transport-contract.d.ts.map +1 -0
- package/dist/transport.d.ts +2 -2
- package/dist/transport.d.ts.map +1 -1
- package/dist/trust-contract.d.ts +23 -0
- package/dist/trust-contract.d.ts.map +1 -0
- package/package.json +16 -12
- package/src/client-device-pending-contract.js +6 -0
- package/src/client.js +26 -0
- package/src/device-boot-contract.js +4 -0
- package/src/device-cleanup-contract.js +9 -0
- package/src/dialog-contract.js +6 -0
- package/src/errors.js +111 -0
- package/src/index.js +194 -0
- package/src/message-port-client.js +628 -0
- package/src/node-app-memory-contract.js +10 -0
- package/src/onboarding/discovery-contract.js +83 -0
- package/src/onboarding/onboarding-contract.js +106 -0
- package/src/onboarding/owner-claim-contract.js +59 -0
- package/src/pairing-status-contract.js +36 -0
- package/src/protocol.js +758 -0
- package/src/stage-context.js +27 -0
- package/src/sync-cursors-contract.js +3 -0
- package/src/system-notice-contract.js +64 -0
- package/src/transport-contract.js +11 -0
- package/src/transport.js +6 -0
- package/src/trust-contract.js +36 -0
- package/dist/client.js +0 -2
- package/dist/client.js.map +0 -1
- package/dist/errors.js +0 -57
- package/dist/errors.js.map +0 -1
- package/dist/index.js +0 -7
- package/dist/index.js.map +0 -1
- package/dist/message-port-client.js +0 -365
- package/dist/message-port-client.js.map +0 -1
- package/dist/onboarding/discovery-contract.js +0 -42
- package/dist/onboarding/discovery-contract.js.map +0 -1
- package/dist/onboarding/onboarding-contract.js +0 -37
- package/dist/onboarding/onboarding-contract.js.map +0 -1
- package/dist/protocol.js +0 -295
- package/dist/protocol.js.map +0 -1
- package/dist/stage-context.js +0 -2
- package/dist/stage-context.js.map +0 -1
- package/dist/transport.js +0 -2
- package/dist/transport.js.map +0 -1
package/src/protocol.js
ADDED
|
@@ -0,0 +1,758 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
/** @import { AppEventPayload, AppEventType, ClientError, CoreKernelEventType, DomainEvent, JsonObject, JsonValue, KernelEventPayload, QueryDiff, QueryName } from '@econ-v1/domain' */
|
|
4
|
+
/** @typedef {{ readonly occurredAt: string; readonly payload: { readonly state?: string }; readonly type: "device-cleanup.changed" }} DeviceCleanupDomainEvent */
|
|
5
|
+
/** @typedef {DomainEvent | DeviceCleanupDomainEvent} ProtocolDomainEvent */
|
|
6
|
+
/** @typedef {CoreKernelEventType | "device-cleanup.changed"} ProtocolCoreKernelEventType */
|
|
7
|
+
import { appEventInstance, appEventScope, isCoreKernelEventType, isKernelEventType, isQueryName, } from "@econ-v1/domain";
|
|
8
|
+
import { deserializeClientError, isJsonObject, isSerializableClientError, serializeClientError, } from "./errors.js";
|
|
9
|
+
/**
|
|
10
|
+
* @param {RpcRequest} request
|
|
11
|
+
* @returns {string}
|
|
12
|
+
*/
|
|
13
|
+
export function serializeRpcRequest(request) {
|
|
14
|
+
switch (request.type) {
|
|
15
|
+
case "command":
|
|
16
|
+
return stringify({ id: request.id, name: request.name, payload: request.payload, type: request.type });
|
|
17
|
+
case "query":
|
|
18
|
+
return stringify({ id: request.id, name: request.name, params: request.params, type: request.type });
|
|
19
|
+
case "live-query.subscribe":
|
|
20
|
+
return stringify({ id: request.id, name: request.name, params: request.params, subscriptionId: request.subscriptionId, type: request.type });
|
|
21
|
+
case "live-query.unsubscribe":
|
|
22
|
+
case "event.unsubscribe":
|
|
23
|
+
return stringify({ id: request.id, subscriptionId: request.subscriptionId, type: request.type });
|
|
24
|
+
case "event.subscribe":
|
|
25
|
+
return stringify({ eventType: request.eventType, id: request.id, subscriptionId: request.subscriptionId, type: request.type });
|
|
26
|
+
case "event.publish":
|
|
27
|
+
return stringify({ event: projectDomainEvent(request.event), id: request.id, type: request.type });
|
|
28
|
+
case "stage.prepare":
|
|
29
|
+
return stringify({ appName: request.appName, id: request.id, type: request.type });
|
|
30
|
+
case "stage.open-session":
|
|
31
|
+
return stringify({ appName: request.appName, id: request.id, manifestSha256: request.manifestSha256, type: request.type });
|
|
32
|
+
case "stage.query":
|
|
33
|
+
return stringify({ id: request.id, name: request.name, params: request.params, sessionId: request.sessionId, type: request.type });
|
|
34
|
+
case "stage.live-query.subscribe":
|
|
35
|
+
return stringify({ id: request.id, name: request.name, params: request.params, sessionId: request.sessionId, subscriptionId: request.subscriptionId, type: request.type });
|
|
36
|
+
case "stage.live-query.unsubscribe":
|
|
37
|
+
case "stage.event.unsubscribe":
|
|
38
|
+
return stringify({ id: request.id, sessionId: request.sessionId, subscriptionId: request.subscriptionId, type: request.type });
|
|
39
|
+
case "stage.event.subscribe":
|
|
40
|
+
return stringify({ eventType: request.eventType, id: request.id, sessionId: request.sessionId, subscriptionId: request.subscriptionId, type: request.type });
|
|
41
|
+
case "stage.event.publish":
|
|
42
|
+
return stringify({ event: projectDomainEvent(request.event), id: request.id, sessionId: request.sessionId, type: request.type });
|
|
43
|
+
case "stage.capability.invoke":
|
|
44
|
+
return stringify({ capability: request.capability, id: request.id, payload: request.payload, sessionId: request.sessionId, type: request.type });
|
|
45
|
+
case "stage.close-session":
|
|
46
|
+
return stringify({ id: request.id, sessionId: request.sessionId, type: request.type });
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* @param {string} serialized
|
|
51
|
+
* @returns {RpcRequest}
|
|
52
|
+
*/
|
|
53
|
+
export function deserializeRpcRequest(serialized) {
|
|
54
|
+
const value = parseRecord(serialized);
|
|
55
|
+
const type = stringField(value, "type");
|
|
56
|
+
const id = stringField(value, "id");
|
|
57
|
+
switch (type) {
|
|
58
|
+
case "command": return { id, name: versionedNameField(value, "name"), payload: jsonField(value, "payload"), type };
|
|
59
|
+
case "query": return { id, name: queryNameField(value), params: jsonField(value, "params"), type };
|
|
60
|
+
case "live-query.subscribe": return { id, name: queryNameField(value), params: jsonField(value, "params"), subscriptionId: stringField(value, "subscriptionId"), type };
|
|
61
|
+
case "live-query.unsubscribe": return { id, subscriptionId: stringField(value, "subscriptionId"), type };
|
|
62
|
+
case "event.subscribe": return { eventType: eventTypeField(value), id, subscriptionId: stringField(value, "subscriptionId"), type };
|
|
63
|
+
case "event.unsubscribe": return { id, subscriptionId: stringField(value, "subscriptionId"), type };
|
|
64
|
+
case "event.publish": return { event: domainEventField(value), id, type };
|
|
65
|
+
case "stage.prepare": return { appName: stringField(value, "appName"), id, type };
|
|
66
|
+
case "stage.open-session": return { appName: stringField(value, "appName"), id, manifestSha256: stringField(value, "manifestSha256"), type };
|
|
67
|
+
case "stage.query": return { id, name: queryNameField(value), params: jsonField(value, "params"), sessionId: stringField(value, "sessionId"), type };
|
|
68
|
+
case "stage.live-query.subscribe": return { id, name: queryNameField(value), params: jsonField(value, "params"), sessionId: stringField(value, "sessionId"), subscriptionId: stringField(value, "subscriptionId"), type };
|
|
69
|
+
case "stage.live-query.unsubscribe": return { id, sessionId: stringField(value, "sessionId"), subscriptionId: stringField(value, "subscriptionId"), type };
|
|
70
|
+
case "stage.event.subscribe": return { eventType: eventTypeField(value), id, sessionId: stringField(value, "sessionId"), subscriptionId: stringField(value, "subscriptionId"), type };
|
|
71
|
+
case "stage.event.unsubscribe": return { id, sessionId: stringField(value, "sessionId"), subscriptionId: stringField(value, "subscriptionId"), type };
|
|
72
|
+
case "stage.event.publish": return { event: domainEventField(value), id, sessionId: stringField(value, "sessionId"), type };
|
|
73
|
+
case "stage.capability.invoke": return { capability: stringField(value, "capability"), id, payload: jsonField(value, "payload"), sessionId: stringField(value, "sessionId"), type };
|
|
74
|
+
case "stage.close-session": return { id, sessionId: stringField(value, "sessionId"), type };
|
|
75
|
+
default: throw new TypeError("Unknown RPC request type");
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* @param {JsonObject} value
|
|
80
|
+
* @param {string} field
|
|
81
|
+
* @returns {string}
|
|
82
|
+
*/
|
|
83
|
+
function versionedNameField(value, field) {
|
|
84
|
+
const name = stringField(value, field);
|
|
85
|
+
if (!/^[-a-z0-9.]+\.v[1-9][0-9]*$/u.test(name))
|
|
86
|
+
throw new TypeError(`Expected ${field} to be versioned`);
|
|
87
|
+
return name;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* @param {RpcResponse} response
|
|
91
|
+
* @returns {string}
|
|
92
|
+
*/
|
|
93
|
+
export function serializeRpcResponse(response) {
|
|
94
|
+
return response.ok
|
|
95
|
+
? stringify({ id: response.id, ok: true, value: response.value })
|
|
96
|
+
: stringify({ error: serializeClientError(response.error), id: response.id, ok: false });
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* @param {string} serialized
|
|
100
|
+
* @returns {RpcResponse}
|
|
101
|
+
*/
|
|
102
|
+
export function deserializeRpcResponse(serialized) {
|
|
103
|
+
const value = parseRecord(serialized);
|
|
104
|
+
const id = stringField(value, "id");
|
|
105
|
+
if (value.ok === true)
|
|
106
|
+
return { id, ok: true, value: jsonField(value, "value") };
|
|
107
|
+
if (value.ok === false && isSerializableClientError(value.error))
|
|
108
|
+
return { error: deserializeClientError(value.error), id, ok: false };
|
|
109
|
+
throw new TypeError("Invalid RPC response");
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* @param {RpcEvent} event
|
|
113
|
+
* @returns {string}
|
|
114
|
+
*/
|
|
115
|
+
export function serializeRpcEvent(event) {
|
|
116
|
+
return event.type === "live-query.changed"
|
|
117
|
+
? stringify({ diff: projectQueryDiff(event.diff), subscriptionId: event.subscriptionId, type: event.type })
|
|
118
|
+
: stringify({ event: projectDomainEvent(event.event), subscriptionId: event.subscriptionId, type: event.type });
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* @param {string} serialized
|
|
122
|
+
* @returns {RpcEvent}
|
|
123
|
+
*/
|
|
124
|
+
export function deserializeRpcEvent(serialized) {
|
|
125
|
+
const value = parseRecord(serialized);
|
|
126
|
+
const subscriptionId = stringField(value, "subscriptionId");
|
|
127
|
+
if (value.type === "live-query.changed")
|
|
128
|
+
return { diff: queryDiffField(value), subscriptionId, type: value.type };
|
|
129
|
+
if (value.type === "kernel.event")
|
|
130
|
+
return { event: domainEventField(value), subscriptionId, type: value.type };
|
|
131
|
+
throw new TypeError("Unknown RPC event type");
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* @param {unknown} value
|
|
135
|
+
* @returns {string}
|
|
136
|
+
*/
|
|
137
|
+
function stringify(value) {
|
|
138
|
+
return JSON.stringify(value);
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* @param {string} serialized
|
|
142
|
+
* @returns {JsonObject}
|
|
143
|
+
*/
|
|
144
|
+
function parseRecord(serialized) {
|
|
145
|
+
const value = JSON.parse(serialized);
|
|
146
|
+
if (!isJsonObject(value))
|
|
147
|
+
throw new TypeError("Expected an RPC object");
|
|
148
|
+
return value;
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* @param {JsonObject} value
|
|
152
|
+
* @param {string} field
|
|
153
|
+
* @returns {string}
|
|
154
|
+
*/
|
|
155
|
+
function stringField(value, field) {
|
|
156
|
+
const candidate = value[field];
|
|
157
|
+
if (typeof candidate !== "string")
|
|
158
|
+
throw new TypeError(`Expected ${field} to be a string`);
|
|
159
|
+
return candidate;
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* @param {JsonObject} value
|
|
163
|
+
* @param {string} field
|
|
164
|
+
* @returns {JsonValue}
|
|
165
|
+
*/
|
|
166
|
+
function jsonField(value, field) {
|
|
167
|
+
const candidate = value[field];
|
|
168
|
+
if (candidate === undefined)
|
|
169
|
+
throw new TypeError(`Expected ${field}`);
|
|
170
|
+
return candidate;
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* @param {JsonObject} value
|
|
174
|
+
* @returns {QueryName}
|
|
175
|
+
*/
|
|
176
|
+
function queryNameField(value) {
|
|
177
|
+
const name = stringField(value, "name");
|
|
178
|
+
if (!isQueryName(name))
|
|
179
|
+
throw new TypeError("Expected a versioned query name");
|
|
180
|
+
return name;
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* @param {JsonObject} value
|
|
184
|
+
* @returns {ProtocolDomainEvent["type"]}
|
|
185
|
+
*/
|
|
186
|
+
function eventTypeField(value) {
|
|
187
|
+
const type = stringField(value, "eventType");
|
|
188
|
+
if (!isKernelEventType(type))
|
|
189
|
+
throw new TypeError("Expected a known domain event type");
|
|
190
|
+
return /** @type {ProtocolDomainEvent["type"]} */ (type);
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* @param {JsonObject} value
|
|
194
|
+
* @returns {ProtocolDomainEvent}
|
|
195
|
+
*/
|
|
196
|
+
function domainEventField(value) {
|
|
197
|
+
const event = objectField(value, "event", "Expected a domain event");
|
|
198
|
+
const occurredAt = timestampField(event, "occurredAt");
|
|
199
|
+
const type = eventTypeField({ eventType: stringField(event, "type") });
|
|
200
|
+
// App events (`app.<resource>`) carry an app-defined payload, so they are decoded
|
|
201
|
+
// structurally rather than by the exhaustive core-event switch below. The switch
|
|
202
|
+
// stays exhaustive over `CoreKernelEventType`, which is what makes a new core event
|
|
203
|
+
// type a compile error here instead of a silent decode failure.
|
|
204
|
+
//
|
|
205
|
+
// The CLOSED vocabulary is tested FIRST. `AppEventType` is spelled `app.${string}`,
|
|
206
|
+
// which at the type level also matches core events that happen to start with `app.`
|
|
207
|
+
// (`app.data.changed`, `app.data.committed`) — testing the app branch first would
|
|
208
|
+
// narrow those two out of the switch below and break its exhaustiveness. The runtime
|
|
209
|
+
// guards already agree on this precedence: `isAppEventType` rejects a dotted resource,
|
|
210
|
+
// so `app.data.changed` was never an app event. This just makes the compiler agree.
|
|
211
|
+
if (type !== "device-cleanup.changed" && !isCoreKernelEventType(type)) {
|
|
212
|
+
const appType = /** @type {AppEventType} */ (type);
|
|
213
|
+
return { occurredAt, payload: appEventPayload(event, appType), type: appType };
|
|
214
|
+
}
|
|
215
|
+
const coreType = /** @type {ProtocolCoreKernelEventType} */ (type);
|
|
216
|
+
switch (coreType) {
|
|
217
|
+
case "transport.state-changed":
|
|
218
|
+
return { occurredAt, payload: transportStateChangedPayload(event), type: coreType };
|
|
219
|
+
case "stage.catalog-invalidated":
|
|
220
|
+
return { occurredAt, payload: stageCatalogInvalidatedPayload(event), type: coreType };
|
|
221
|
+
case "stage.registry-changed":
|
|
222
|
+
return { occurredAt, payload: stageRegistryChangedPayload(event), type: coreType };
|
|
223
|
+
case "query.invalidated":
|
|
224
|
+
return { occurredAt, payload: queryInvalidatedPayload(event), type: coreType };
|
|
225
|
+
case "outbox.state-changed":
|
|
226
|
+
return { occurredAt, payload: outboxStateChangedPayload(event), type: coreType };
|
|
227
|
+
case "domain.entity-changed":
|
|
228
|
+
return { occurredAt, payload: domainEntityChangedPayload(event), type: coreType };
|
|
229
|
+
case "sync.delta-applied":
|
|
230
|
+
return { occurredAt, payload: syncDeltaAppliedPayload(event), type: coreType };
|
|
231
|
+
case "client-storage.state-changed":
|
|
232
|
+
return { occurredAt, payload: clientStorageStateChangedPayload(event), type: coreType };
|
|
233
|
+
case "offline.stage-readiness-changed":
|
|
234
|
+
return { occurredAt, payload: offlineStageReadinessChangedPayload(event), type: coreType };
|
|
235
|
+
case "offline.identity-invalidated":
|
|
236
|
+
return { occurredAt, payload: offlineIdentityInvalidatedPayload(event), type: coreType };
|
|
237
|
+
case "app.data.changed":
|
|
238
|
+
return { occurredAt, payload: appDataEventPayload(event), type: coreType };
|
|
239
|
+
case "app.data.committed":
|
|
240
|
+
return { occurredAt, payload: appDataEventPayload(event), type: coreType };
|
|
241
|
+
case "onboarding.flow-changed":
|
|
242
|
+
return { occurredAt, payload: onboardingFlowChangedPayload(event), type: coreType };
|
|
243
|
+
case "device-cleanup.changed":
|
|
244
|
+
return { occurredAt, payload: deviceCleanupChangedPayload(event), type: coreType };
|
|
245
|
+
case "room.turn-stream":
|
|
246
|
+
return { occurredAt, payload: roomTurnStreamPayload(event), type: coreType };
|
|
247
|
+
case "room.job-stream":
|
|
248
|
+
return { occurredAt, payload: roomJobStreamPayload(event), type: coreType };
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* Decode an `app.<resource>` payload. The `resource` on the wire must match the
|
|
253
|
+
* resource encoded in the event type — a frame claiming `app.contest` while carrying
|
|
254
|
+
* `resource: "wallet"` is rejected rather than delivered, so a stage's declared
|
|
255
|
+
* `app.<resource>` scope cannot be widened by a malformed or hostile frame.
|
|
256
|
+
* @param {JsonObject} event
|
|
257
|
+
* @param {AppEventType} type
|
|
258
|
+
* @returns {AppEventPayload}
|
|
259
|
+
*/
|
|
260
|
+
function appEventPayload(event, type) {
|
|
261
|
+
const payload = objectField(event, "payload");
|
|
262
|
+
const resource = stringField(payload, "resource");
|
|
263
|
+
// The scope encoded in the type must match the payload's resource, so a frame
|
|
264
|
+
// claiming `app.contest` while carrying `resource: "wallet"` is rejected
|
|
265
|
+
// rather than delivered. The INSTANCE is compared separately: an
|
|
266
|
+
// instance-addressed type may legitimately carry the matching id.
|
|
267
|
+
if (appEventScope(type) !== `app.${resource}`) {
|
|
268
|
+
throw new TypeError("App event resource does not match its event type");
|
|
269
|
+
}
|
|
270
|
+
const instance = payload["instance"];
|
|
271
|
+
if (instance !== undefined && typeof instance !== "string") {
|
|
272
|
+
throw new TypeError("Expected instance to be a string");
|
|
273
|
+
}
|
|
274
|
+
const declaredInstance = appEventInstance(type);
|
|
275
|
+
if (declaredInstance !== undefined && instance !== undefined && instance !== declaredInstance) {
|
|
276
|
+
throw new TypeError("App event instance does not match its event type");
|
|
277
|
+
}
|
|
278
|
+
return {
|
|
279
|
+
data: objectField(payload, "data"),
|
|
280
|
+
...(instance === undefined ? {} : { instance }),
|
|
281
|
+
resource,
|
|
282
|
+
type: stringField(payload, "type"),
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
/**
|
|
286
|
+
* @param {JsonObject} event
|
|
287
|
+
* @returns {KernelEventPayload<"room.job-stream">}
|
|
288
|
+
*/
|
|
289
|
+
function roomJobStreamPayload(event) {
|
|
290
|
+
const payload = objectField(event, "payload");
|
|
291
|
+
const kind = stringField(payload, "kind");
|
|
292
|
+
if (kind !== "job" && kind !== "job_step")
|
|
293
|
+
throw new TypeError("Expected a known room job-stream kind");
|
|
294
|
+
const job = objectField(payload, "job");
|
|
295
|
+
/** @type {"open" | "done" | "aborted" | "capped"} */
|
|
296
|
+
const status = /** @type {"open" | "done" | "aborted" | "capped"} */ (stringField(job, "status"));
|
|
297
|
+
if (status !== "open" && status !== "done" && status !== "aborted" && status !== "capped")
|
|
298
|
+
throw new TypeError("Expected a known job status");
|
|
299
|
+
/** @type {"user" | "scheduled"} */
|
|
300
|
+
const origin = /** @type {"user" | "scheduled"} */ (stringField(job, "origin"));
|
|
301
|
+
if (origin !== "user" && origin !== "scheduled")
|
|
302
|
+
throw new TypeError("Expected a known job origin");
|
|
303
|
+
/** @param {JsonObject} source @param {string} field */
|
|
304
|
+
const optionalStr = (source, field) => {
|
|
305
|
+
const value = source[field];
|
|
306
|
+
if (value !== undefined && typeof value !== "string")
|
|
307
|
+
throw new TypeError(`Expected ${field} to be a string`);
|
|
308
|
+
return value;
|
|
309
|
+
};
|
|
310
|
+
/** @param {JsonObject} source @param {string} field */
|
|
311
|
+
const optionalNum = (source, field) => {
|
|
312
|
+
const value = source[field];
|
|
313
|
+
if (value !== undefined && typeof value !== "number")
|
|
314
|
+
throw new TypeError(`Expected ${field} to be a number`);
|
|
315
|
+
return value;
|
|
316
|
+
};
|
|
317
|
+
/** @param {JsonObject} source @param {string} field */
|
|
318
|
+
const numField = (source, field) => {
|
|
319
|
+
const value = source[field];
|
|
320
|
+
if (typeof value !== "number")
|
|
321
|
+
throw new TypeError(`Expected ${field} to be a number`);
|
|
322
|
+
return value;
|
|
323
|
+
};
|
|
324
|
+
const closedAt = optionalStr(job, "closedAt");
|
|
325
|
+
const openedBySeq = optionalNum(job, "openedBySeq");
|
|
326
|
+
const mappedJob = {
|
|
327
|
+
...(closedAt === undefined ? {} : { closedAt }), jobId: stringField(job, "jobId"),
|
|
328
|
+
openedAt: stringField(job, "openedAt"), ...(openedBySeq === undefined ? {} : { openedBySeq }),
|
|
329
|
+
origin, pendingArrivals: numField(job, "pendingArrivals"), queueCount: numField(job, "queueCount"),
|
|
330
|
+
roomId: stringField(job, "roomId"), status, stepCount: numField(job, "stepCount"),
|
|
331
|
+
turnsUsed: numField(job, "turnsUsed"),
|
|
332
|
+
};
|
|
333
|
+
/** @type {KernelEventPayload<"room.job-stream">["step"]} */
|
|
334
|
+
let mappedStep;
|
|
335
|
+
if (payload.step !== undefined) {
|
|
336
|
+
const step = objectField(payload, "step");
|
|
337
|
+
const actor = objectField(step, "actor");
|
|
338
|
+
const actorKind = stringField(actor, "kind");
|
|
339
|
+
if (actorKind !== "orchestrator" && actorKind !== "specialist")
|
|
340
|
+
throw new TypeError("Expected a known step actor kind");
|
|
341
|
+
const name = optionalStr(actor, "name");
|
|
342
|
+
const slug = optionalStr(actor, "slug");
|
|
343
|
+
const outcome = optionalStr(step, "outcome");
|
|
344
|
+
const runId = optionalStr(step, "runId");
|
|
345
|
+
const seq = optionalNum(step, "seq");
|
|
346
|
+
mappedStep = {
|
|
347
|
+
actor: { kind: actorKind, ...(name === undefined ? {} : { name }), ...(slug === undefined ? {} : { slug }) },
|
|
348
|
+
detail: stringField(step, "detail"), kind: stringField(step, "kind"),
|
|
349
|
+
...(outcome === undefined ? {} : { outcome }), ...(runId === undefined ? {} : { runId }),
|
|
350
|
+
...(seq === undefined ? {} : { seq }), stepId: stringField(step, "stepId"), ts: stringField(step, "ts"),
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
return { job: mappedJob, kind, roomId: stringField(payload, "roomId"),
|
|
354
|
+
...(mappedStep === undefined ? {} : { step: mappedStep }) };
|
|
355
|
+
}
|
|
356
|
+
/**
|
|
357
|
+
* @param {JsonObject} event
|
|
358
|
+
* @returns {KernelEventPayload<"room.turn-stream">}
|
|
359
|
+
*/
|
|
360
|
+
function roomTurnStreamPayload(event) {
|
|
361
|
+
const payload = objectField(event, "payload");
|
|
362
|
+
const kind = stringField(payload, "kind");
|
|
363
|
+
if (kind !== "room.turn_started" && kind !== "turn.completed" && kind !== "turn.delta" &&
|
|
364
|
+
kind !== "turn.failed" && kind !== "turn.tool_delta" && kind !== "work.delta")
|
|
365
|
+
throw new TypeError("Expected a known room turn-stream kind");
|
|
366
|
+
/** @param {string} field */
|
|
367
|
+
const optional = (field) => {
|
|
368
|
+
const value = payload[field];
|
|
369
|
+
if (value !== undefined && typeof value !== "string")
|
|
370
|
+
throw new TypeError(`Expected ${field} to be a string`);
|
|
371
|
+
return value;
|
|
372
|
+
};
|
|
373
|
+
const anchorSession = stringField(payload, "anchorSession");
|
|
374
|
+
const agentName = optional("agentName");
|
|
375
|
+
const argsDelta = optional("argsDelta");
|
|
376
|
+
const runId = optional("runId");
|
|
377
|
+
const callId = optional("callId");
|
|
378
|
+
const callIndex = payload.callIndex;
|
|
379
|
+
if (callIndex !== undefined && typeof callIndex !== "number")
|
|
380
|
+
throw new TypeError("Expected callIndex to be a number");
|
|
381
|
+
const reason = optional("reason");
|
|
382
|
+
const responderName = optional("responderName");
|
|
383
|
+
const responderSlug = optional("responderSlug");
|
|
384
|
+
const text = optional("text");
|
|
385
|
+
const toolName = optional("toolName");
|
|
386
|
+
return {
|
|
387
|
+
...(agentName === undefined ? {} : { agentName }), anchorSession,
|
|
388
|
+
...(argsDelta === undefined ? {} : { argsDelta }), ...(callId === undefined ? {} : { callId }),
|
|
389
|
+
...(callIndex === undefined ? {} : { callIndex }), kind, ...(reason === undefined ? {} : { reason }),
|
|
390
|
+
...(responderName === undefined ? {} : { responderName }),
|
|
391
|
+
...(responderSlug === undefined ? {} : { responderSlug }), roomId: stringField(payload, "roomId"),
|
|
392
|
+
...(runId === undefined ? {} : { runId }), ...(text === undefined ? {} : { text }),
|
|
393
|
+
...(toolName === undefined ? {} : { toolName }),
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
/**
|
|
397
|
+
* @param {JsonObject} event
|
|
398
|
+
* @returns {KernelEventPayload<"client-storage.state-changed">}
|
|
399
|
+
*/
|
|
400
|
+
function clientStorageStateChangedPayload(event) {
|
|
401
|
+
const payload = objectField(event, "payload");
|
|
402
|
+
const status = stringField(payload, "status");
|
|
403
|
+
if (status !== "ready" && status !== "recovery-required" && status !== "starting") {
|
|
404
|
+
throw new TypeError("Expected a known client-storage status");
|
|
405
|
+
}
|
|
406
|
+
// `code`/`message` accompany "recovery-required" only; a wire frame that omits them for
|
|
407
|
+
// the other two states is well-formed, so they are decoded as optional rather than
|
|
408
|
+
// required-and-empty (which would erase the distinction the shell renders on).
|
|
409
|
+
const code = payload["code"];
|
|
410
|
+
const message = payload["message"];
|
|
411
|
+
if (code !== undefined && typeof code !== "string")
|
|
412
|
+
throw new TypeError("Expected code to be a string");
|
|
413
|
+
if (message !== undefined && typeof message !== "string") {
|
|
414
|
+
throw new TypeError("Expected message to be a string");
|
|
415
|
+
}
|
|
416
|
+
return {
|
|
417
|
+
...(code === undefined ? {} : { code }),
|
|
418
|
+
...(message === undefined ? {} : { message }),
|
|
419
|
+
status,
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
/**
|
|
423
|
+
* @param {JsonObject} event
|
|
424
|
+
* @returns {KernelEventPayload<"offline.stage-readiness-changed">}
|
|
425
|
+
*/
|
|
426
|
+
function offlineStageReadinessChangedPayload(event) {
|
|
427
|
+
const payload = objectField(event, "payload");
|
|
428
|
+
const readiness = objectField(payload, "readiness");
|
|
429
|
+
const state = stringField(readiness, "state");
|
|
430
|
+
if (state !== "caching-assets" && state !== "needs-first-sync" &&
|
|
431
|
+
state !== "offline-ready" && state !== "update-available" &&
|
|
432
|
+
state !== "failed" && state !== "removing") {
|
|
433
|
+
throw new TypeError("Expected a known offline stage state");
|
|
434
|
+
}
|
|
435
|
+
const failureCode = optionalStringField(readiness, "failureCode");
|
|
436
|
+
if (failureCode !== undefined && failureCode !== "asset-fetch-failed" &&
|
|
437
|
+
failureCode !== "asset-integrity-failed" && failureCode !== "projection-seed-failed" &&
|
|
438
|
+
failureCode !== "schema-incompatible" && failureCode !== "quota-exceeded" &&
|
|
439
|
+
failureCode !== "identity-mismatch" && failureCode !== "active-resource-corrupt") {
|
|
440
|
+
throw new TypeError("Expected a known offline readiness failure code");
|
|
441
|
+
}
|
|
442
|
+
const activeManifestSha256 = optionalStringField(readiness, "activeManifestSha256");
|
|
443
|
+
const candidateManifestSha256 = optionalStringField(readiness, "candidateManifestSha256");
|
|
444
|
+
return {
|
|
445
|
+
readiness: {
|
|
446
|
+
...(activeManifestSha256 === undefined ? {} : { activeManifestSha256 }),
|
|
447
|
+
appName: stringField(readiness, "appName"),
|
|
448
|
+
...(candidateManifestSha256 === undefined ? {} : { candidateManifestSha256 }),
|
|
449
|
+
...(failureCode === undefined ? {} : { failureCode }),
|
|
450
|
+
state,
|
|
451
|
+
updatedAt: timestampField(readiness, "updatedAt"),
|
|
452
|
+
},
|
|
453
|
+
};
|
|
454
|
+
}
|
|
455
|
+
/**
|
|
456
|
+
* @param {JsonObject} event
|
|
457
|
+
* @returns {KernelEventPayload<"offline.identity-invalidated">}
|
|
458
|
+
*/
|
|
459
|
+
function offlineIdentityInvalidatedPayload(event) {
|
|
460
|
+
const payload = objectField(event, "payload");
|
|
461
|
+
const scope = objectField(payload, "scope");
|
|
462
|
+
return {
|
|
463
|
+
scope: {
|
|
464
|
+
deviceDid: stringField(scope, "deviceDid"),
|
|
465
|
+
homeNodeId: stringField(scope, "homeNodeId"),
|
|
466
|
+
},
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
/**
|
|
470
|
+
* @param {JsonObject} event
|
|
471
|
+
* @returns {KernelEventPayload<"app.data.changed">}
|
|
472
|
+
*/
|
|
473
|
+
function appDataEventPayload(event) {
|
|
474
|
+
const payload = objectField(event, "payload");
|
|
475
|
+
return {
|
|
476
|
+
appName: stringField(payload, "appName"),
|
|
477
|
+
cursor: stringField(payload, "cursor"),
|
|
478
|
+
revision: stringField(payload, "revision"),
|
|
479
|
+
stream: stringField(payload, "stream"),
|
|
480
|
+
};
|
|
481
|
+
}
|
|
482
|
+
/**
|
|
483
|
+
* Decodes only `stepId`, because that is the entire payload the domain declares. Any other
|
|
484
|
+
* field a frame carries is dropped here rather than forwarded: this event crosses the wire to
|
|
485
|
+
* every subscriber, and the flow's secrets (the Wi-Fi password, the match-code digits, the
|
|
486
|
+
* invitation code) must not be able to ride along even if a future producer misbehaves.
|
|
487
|
+
* @param {JsonObject} event
|
|
488
|
+
* @returns {KernelEventPayload<"onboarding.flow-changed">}
|
|
489
|
+
*/
|
|
490
|
+
function onboardingFlowChangedPayload(event) {
|
|
491
|
+
const payload = objectField(event, "payload");
|
|
492
|
+
return { stepId: stringField(payload, "stepId") };
|
|
493
|
+
}
|
|
494
|
+
/**
|
|
495
|
+
* @param {JsonObject} event
|
|
496
|
+
* @returns {{ readonly state?: string }}
|
|
497
|
+
*/
|
|
498
|
+
function deviceCleanupChangedPayload(event) {
|
|
499
|
+
const payload = objectField(event, "payload");
|
|
500
|
+
const state = optionalStringField(payload, "state");
|
|
501
|
+
return { ...(state === undefined ? {} : { state }) };
|
|
502
|
+
}
|
|
503
|
+
/**
|
|
504
|
+
* @param {JsonObject} event
|
|
505
|
+
* @returns {KernelEventPayload<"stage.catalog-invalidated">}
|
|
506
|
+
*/
|
|
507
|
+
function stageCatalogInvalidatedPayload(event) {
|
|
508
|
+
const payload = objectField(event, "payload");
|
|
509
|
+
return { reason: stringField(payload, "reason") };
|
|
510
|
+
}
|
|
511
|
+
/**
|
|
512
|
+
* @param {JsonObject} value
|
|
513
|
+
* @returns {QueryDiff<JsonValue>}
|
|
514
|
+
*/
|
|
515
|
+
function queryDiffField(value) {
|
|
516
|
+
const record = objectField(value, "diff", "Expected a query diff");
|
|
517
|
+
return {
|
|
518
|
+
added: arrayField(record, "added"),
|
|
519
|
+
deleted: arrayField(record, "deleted"),
|
|
520
|
+
revision: numberField(record, "revision"),
|
|
521
|
+
updated: arrayField(record, "updated"),
|
|
522
|
+
};
|
|
523
|
+
}
|
|
524
|
+
/**
|
|
525
|
+
* @param {ProtocolDomainEvent} event
|
|
526
|
+
* @returns {object}
|
|
527
|
+
*/
|
|
528
|
+
function projectDomainEvent(event) {
|
|
529
|
+
return { occurredAt: event.occurredAt, payload: event.payload, type: event.type };
|
|
530
|
+
}
|
|
531
|
+
/**
|
|
532
|
+
* @param {QueryDiff<JsonValue>} diff
|
|
533
|
+
* @returns {object}
|
|
534
|
+
*/
|
|
535
|
+
function projectQueryDiff(diff) {
|
|
536
|
+
return { added: diff.added, deleted: diff.deleted, revision: diff.revision, updated: diff.updated };
|
|
537
|
+
}
|
|
538
|
+
/**
|
|
539
|
+
* @param {JsonObject} value
|
|
540
|
+
* @param {string} field
|
|
541
|
+
* @returns {readonly JsonValue[]}
|
|
542
|
+
*/
|
|
543
|
+
function arrayField(value, field) {
|
|
544
|
+
const candidate = value[field];
|
|
545
|
+
if (!Array.isArray(candidate))
|
|
546
|
+
throw new TypeError(`Expected ${field} to be an array`);
|
|
547
|
+
return candidate;
|
|
548
|
+
}
|
|
549
|
+
/**
|
|
550
|
+
* @param {JsonObject} value
|
|
551
|
+
* @param {string} field
|
|
552
|
+
* @returns {number}
|
|
553
|
+
*/
|
|
554
|
+
function numberField(value, field) {
|
|
555
|
+
const candidate = value[field];
|
|
556
|
+
if (typeof candidate !== "number" || !Number.isFinite(candidate)) {
|
|
557
|
+
throw new TypeError(`Expected ${field} to be a finite number`);
|
|
558
|
+
}
|
|
559
|
+
return candidate;
|
|
560
|
+
}
|
|
561
|
+
/** @param {JsonObject} value @param {string} field @returns {number} */
|
|
562
|
+
function integerField(value, field) {
|
|
563
|
+
const candidate = numberField(value, field);
|
|
564
|
+
if (!Number.isInteger(candidate))
|
|
565
|
+
throw new TypeError(`Expected ${field} to be an integer`);
|
|
566
|
+
return candidate;
|
|
567
|
+
}
|
|
568
|
+
/** @param {JsonObject} value @param {string} field @returns {string | undefined} */
|
|
569
|
+
function optionalStringField(value, field) {
|
|
570
|
+
const candidate = value[field];
|
|
571
|
+
if (candidate === undefined)
|
|
572
|
+
return undefined;
|
|
573
|
+
if (typeof candidate !== "string")
|
|
574
|
+
throw new TypeError(`Expected ${field} to be a string`);
|
|
575
|
+
return candidate;
|
|
576
|
+
}
|
|
577
|
+
/**
|
|
578
|
+
* @param {JsonObject} value
|
|
579
|
+
* @param {string} field
|
|
580
|
+
* @param {string} [message=`Expected ${field} to be an object`]
|
|
581
|
+
* @returns {JsonObject}
|
|
582
|
+
*/
|
|
583
|
+
function objectField(value, field, message = `Expected ${field} to be an object`) {
|
|
584
|
+
const candidate = value[field];
|
|
585
|
+
if (!isJsonObject(candidate))
|
|
586
|
+
throw new TypeError(message);
|
|
587
|
+
return candidate;
|
|
588
|
+
}
|
|
589
|
+
/**
|
|
590
|
+
* @param {JsonObject} value
|
|
591
|
+
* @param {string} field
|
|
592
|
+
* @returns {readonly string[]}
|
|
593
|
+
*/
|
|
594
|
+
function stringArrayField(value, field) {
|
|
595
|
+
const candidate = value[field];
|
|
596
|
+
if (!Array.isArray(candidate) || !candidate.every((item) => typeof item === "string")) {
|
|
597
|
+
throw new TypeError(`Expected ${field} to be an array of strings`);
|
|
598
|
+
}
|
|
599
|
+
return candidate;
|
|
600
|
+
}
|
|
601
|
+
/**
|
|
602
|
+
* @param {JsonObject} value
|
|
603
|
+
* @param {string} field
|
|
604
|
+
* @returns {string}
|
|
605
|
+
*/
|
|
606
|
+
function timestampField(value, field) {
|
|
607
|
+
const candidate = stringField(value, field);
|
|
608
|
+
const timestampPattern = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/u;
|
|
609
|
+
if (!timestampPattern.test(candidate) || !Number.isFinite(Date.parse(candidate))) {
|
|
610
|
+
throw new TypeError(`Expected ${field} to be an RFC 3339 timestamp`);
|
|
611
|
+
}
|
|
612
|
+
return candidate;
|
|
613
|
+
}
|
|
614
|
+
/**
|
|
615
|
+
* @param {JsonObject} event
|
|
616
|
+
* @returns {KernelEventPayload<"transport.state-changed">}
|
|
617
|
+
*/
|
|
618
|
+
function transportStateChangedPayload(event) {
|
|
619
|
+
const payload = objectField(event, "payload");
|
|
620
|
+
const state = objectField(payload, "state");
|
|
621
|
+
const changedAt = timestampField(state, "changedAt");
|
|
622
|
+
const path = stringField(state, "path");
|
|
623
|
+
const status = stringField(state, "status");
|
|
624
|
+
if (status === "connected" && isConnectedTransportPath(path)) {
|
|
625
|
+
return { state: { changedAt, path, status } };
|
|
626
|
+
}
|
|
627
|
+
if (path === "offline") {
|
|
628
|
+
if (status === "connecting")
|
|
629
|
+
return { state: { attempt: integerField(state, "attempt"), changedAt, path, status } };
|
|
630
|
+
if (status === "reconnecting") {
|
|
631
|
+
return { state: { attempt: integerField(state, "attempt"), changedAt,
|
|
632
|
+
nextRetryAt: timestampField(state, "nextRetryAt"), path, status } };
|
|
633
|
+
}
|
|
634
|
+
if (status === "offline") {
|
|
635
|
+
const reason = optionalStringField(state, "reason");
|
|
636
|
+
if (reason !== undefined && !isTransportOfflineReason(reason))
|
|
637
|
+
throw new TypeError("Expected a valid transport offline reason");
|
|
638
|
+
return { state: { changedAt, path, status, ...(reason === undefined ? {} : { reason }) } };
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
throw new TypeError("Expected a valid transport state");
|
|
642
|
+
}
|
|
643
|
+
/**
|
|
644
|
+
* @param {JsonObject} event
|
|
645
|
+
* @returns {KernelEventPayload<"stage.registry-changed">}
|
|
646
|
+
*/
|
|
647
|
+
function stageRegistryChangedPayload(event) {
|
|
648
|
+
const payload = objectField(event, "payload");
|
|
649
|
+
return { appNames: stringArrayField(payload, "appNames") };
|
|
650
|
+
}
|
|
651
|
+
/**
|
|
652
|
+
* @param {JsonObject} event
|
|
653
|
+
* @returns {KernelEventPayload<"query.invalidated">}
|
|
654
|
+
*/
|
|
655
|
+
function queryInvalidatedPayload(event) {
|
|
656
|
+
const payload = objectField(event, "payload");
|
|
657
|
+
return { name: queryNameField(payload) };
|
|
658
|
+
}
|
|
659
|
+
/**
|
|
660
|
+
* @param {JsonObject} event
|
|
661
|
+
* @returns {KernelEventPayload<"outbox.state-changed">}
|
|
662
|
+
*/
|
|
663
|
+
function outboxStateChangedPayload(event) {
|
|
664
|
+
const payload = objectField(event, "payload");
|
|
665
|
+
const intent = objectField(payload, "intent");
|
|
666
|
+
return {
|
|
667
|
+
intent: {
|
|
668
|
+
domain: stringField(intent, "domain"),
|
|
669
|
+
id: stringField(intent, "id"),
|
|
670
|
+
idempotencyKey: stringField(intent, "idempotencyKey"),
|
|
671
|
+
payload: jsonField(intent, "payload"),
|
|
672
|
+
state: outboxIntentStateField(intent),
|
|
673
|
+
},
|
|
674
|
+
};
|
|
675
|
+
}
|
|
676
|
+
/**
|
|
677
|
+
* @param {JsonObject} event
|
|
678
|
+
* @returns {KernelEventPayload<"domain.entity-changed">}
|
|
679
|
+
*/
|
|
680
|
+
function domainEntityChangedPayload(event) {
|
|
681
|
+
const payload = objectField(event, "payload");
|
|
682
|
+
return {
|
|
683
|
+
action: entityActionField(payload),
|
|
684
|
+
entityId: stringField(payload, "entityId"),
|
|
685
|
+
entityType: stringField(payload, "entityType"),
|
|
686
|
+
};
|
|
687
|
+
}
|
|
688
|
+
/**
|
|
689
|
+
* @param {JsonObject} event
|
|
690
|
+
* @returns {KernelEventPayload<"sync.delta-applied">}
|
|
691
|
+
*/
|
|
692
|
+
function syncDeltaAppliedPayload(event) {
|
|
693
|
+
const payload = objectField(event, "payload");
|
|
694
|
+
return {
|
|
695
|
+
cursor: stringField(payload, "cursor"),
|
|
696
|
+
domain: stringField(payload, "domain"),
|
|
697
|
+
};
|
|
698
|
+
}
|
|
699
|
+
/** @param {string} value */
|
|
700
|
+
function isTransportOfflineReason(value) {
|
|
701
|
+
return value === "no-network" || value === "node-unreachable" || value === "session-required";
|
|
702
|
+
}
|
|
703
|
+
/**
|
|
704
|
+
* @param {string} value
|
|
705
|
+
* @returns {value is "iroh-relay" | "lan-direct" | "lan-webtransport" | "wan-direct" | "webrtc-direct"}
|
|
706
|
+
*/
|
|
707
|
+
function isConnectedTransportPath(value) {
|
|
708
|
+
return (value === "iroh-relay" || value === "lan-direct" || value === "lan-webtransport" ||
|
|
709
|
+
value === "wan-direct" || value === "webrtc-direct");
|
|
710
|
+
}
|
|
711
|
+
/**
|
|
712
|
+
* @param {JsonObject} value
|
|
713
|
+
* @returns {"acknowledged" | "failed" | "pending" | "replaying"}
|
|
714
|
+
*/
|
|
715
|
+
function outboxIntentStateField(value) {
|
|
716
|
+
const state = stringField(value, "state");
|
|
717
|
+
if (state === "acknowledged" || state === "failed" || state === "pending" || state === "replaying") {
|
|
718
|
+
return state;
|
|
719
|
+
}
|
|
720
|
+
throw new TypeError("Expected a known outbox intent state");
|
|
721
|
+
}
|
|
722
|
+
/**
|
|
723
|
+
* @param {JsonObject} value
|
|
724
|
+
* @returns {"created" | "deleted" | "updated"}
|
|
725
|
+
*/
|
|
726
|
+
function entityActionField(value) {
|
|
727
|
+
const action = stringField(value, "action");
|
|
728
|
+
if (action === "created" || action === "deleted" || action === "updated")
|
|
729
|
+
return action;
|
|
730
|
+
throw new TypeError("Expected a known entity action");
|
|
731
|
+
}
|
|
732
|
+
/**
|
|
733
|
+
* @typedef {| { readonly id: string; readonly name: string; readonly payload: JsonValue; readonly type: "command" }
|
|
734
|
+
* | { readonly id: string; readonly name: QueryName; readonly params: JsonValue; readonly type: "query" }
|
|
735
|
+
* | { readonly id: string; readonly name: QueryName; readonly params: JsonValue; readonly subscriptionId: string; readonly type: "live-query.subscribe" }
|
|
736
|
+
* | { readonly id: string; readonly subscriptionId: string; readonly type: "live-query.unsubscribe" }
|
|
737
|
+
* | { readonly eventType: ProtocolDomainEvent["type"]; readonly id: string; readonly subscriptionId: string; readonly type: "event.subscribe" }
|
|
738
|
+
* | { readonly id: string; readonly subscriptionId: string; readonly type: "event.unsubscribe" }
|
|
739
|
+
* | { readonly event: ProtocolDomainEvent; readonly id: string; readonly type: "event.publish" }
|
|
740
|
+
* | { readonly appName: string; readonly id: string; readonly type: "stage.prepare" }
|
|
741
|
+
* | { readonly appName: string; readonly id: string; readonly manifestSha256: string; readonly type: "stage.open-session" }
|
|
742
|
+
* | { readonly id: string; readonly name: QueryName; readonly params: JsonValue; readonly sessionId: string; readonly type: "stage.query" }
|
|
743
|
+
* | { readonly id: string; readonly name: QueryName; readonly params: JsonValue; readonly sessionId: string; readonly subscriptionId: string; readonly type: "stage.live-query.subscribe" }
|
|
744
|
+
* | { readonly id: string; readonly sessionId: string; readonly subscriptionId: string; readonly type: "stage.live-query.unsubscribe" }
|
|
745
|
+
* | { readonly eventType: ProtocolDomainEvent["type"]; readonly id: string; readonly sessionId: string; readonly subscriptionId: string; readonly type: "stage.event.subscribe" }
|
|
746
|
+
* | { readonly id: string; readonly sessionId: string; readonly subscriptionId: string; readonly type: "stage.event.unsubscribe" }
|
|
747
|
+
* | { readonly event: ProtocolDomainEvent; readonly id: string; readonly sessionId: string; readonly type: "stage.event.publish" }
|
|
748
|
+
* | { readonly capability: string; readonly id: string; readonly payload: JsonValue; readonly sessionId: string; readonly type: "stage.capability.invoke" }
|
|
749
|
+
* | { readonly id: string; readonly sessionId: string; readonly type: "stage.close-session" }} RpcRequest
|
|
750
|
+
*/
|
|
751
|
+
/**
|
|
752
|
+
* @typedef {| { readonly id: string; readonly ok: true; readonly value: JsonValue }
|
|
753
|
+
* | { readonly error: ClientError; readonly id: string; readonly ok: false }} RpcResponse
|
|
754
|
+
*/
|
|
755
|
+
/**
|
|
756
|
+
* @typedef {| { readonly diff: QueryDiff<JsonValue>; readonly subscriptionId: string; readonly type: "live-query.changed" }
|
|
757
|
+
* | { readonly event: ProtocolDomainEvent; readonly subscriptionId: string; readonly type: "kernel.event" }} RpcEvent
|
|
758
|
+
*/
|