@xyo-network/dapp-kit-port 0.1.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/README.md +23 -0
- package/dist/browser/applicationPortClient.d.ts +6 -0
- package/dist/browser/applicationPortClient.d.ts.map +1 -0
- package/dist/browser/applicationPortHost.d.ts +10 -0
- package/dist/browser/applicationPortHost.d.ts.map +1 -0
- package/dist/browser/applicationPortModel.d.ts +154 -0
- package/dist/browser/applicationPortModel.d.ts.map +1 -0
- package/dist/browser/applicationPortWire.d.ts +327 -0
- package/dist/browser/applicationPortWire.d.ts.map +1 -0
- package/dist/browser/index.d.ts +6 -0
- package/dist/browser/index.d.ts.map +1 -0
- package/dist/browser/index.mjs +1180 -0
- package/dist/browser/index.mjs.map +7 -0
- package/dist/browser/webSocketApplicationPortClient.d.ts +46 -0
- package/dist/browser/webSocketApplicationPortClient.d.ts.map +1 -0
- package/dist/node/applicationPortClient.d.ts +6 -0
- package/dist/node/applicationPortClient.d.ts.map +1 -0
- package/dist/node/applicationPortHost.d.ts +10 -0
- package/dist/node/applicationPortHost.d.ts.map +1 -0
- package/dist/node/applicationPortModel.d.ts +154 -0
- package/dist/node/applicationPortModel.d.ts.map +1 -0
- package/dist/node/applicationPortWire.d.ts +327 -0
- package/dist/node/applicationPortWire.d.ts.map +1 -0
- package/dist/node/index.d.ts +6 -0
- package/dist/node/index.d.ts.map +1 -0
- package/dist/node/index.mjs +1180 -0
- package/dist/node/index.mjs.map +7 -0
- package/dist/node/webSocketApplicationPortClient.d.ts +46 -0
- package/dist/node/webSocketApplicationPortClient.d.ts.map +1 -0
- package/package.json +74 -0
|
@@ -0,0 +1,1180 @@
|
|
|
1
|
+
// src/applicationPortClient.ts
|
|
2
|
+
import {
|
|
3
|
+
canonicalizeIJson,
|
|
4
|
+
PortAttachmentGrantZod,
|
|
5
|
+
validatePortFrame
|
|
6
|
+
} from "@xyo-network/dapp-kit";
|
|
7
|
+
|
|
8
|
+
// src/applicationPortModel.ts
|
|
9
|
+
import { PortFrameZod } from "@xyo-network/dapp-kit";
|
|
10
|
+
var ApplicationPortProxyErrorCode = {
|
|
11
|
+
cancelled: "port.cancelled",
|
|
12
|
+
detached: "port.detached",
|
|
13
|
+
directionInvalid: "port.direction-invalid",
|
|
14
|
+
duplicateRequest: "port.duplicate-request",
|
|
15
|
+
hostFailure: "port.host-failure",
|
|
16
|
+
operationKindMismatch: "port.operation-kind-mismatch",
|
|
17
|
+
operationUnavailable: "port.operation-unavailable",
|
|
18
|
+
requestUnknown: "port.request-unknown",
|
|
19
|
+
streamStateInvalid: "port.stream-state-invalid"
|
|
20
|
+
};
|
|
21
|
+
var ApplicationPortProxyError = class extends Error {
|
|
22
|
+
code;
|
|
23
|
+
issues;
|
|
24
|
+
constructor(code, message, issues) {
|
|
25
|
+
super(message);
|
|
26
|
+
this.name = "ApplicationPortProxyError";
|
|
27
|
+
this.code = code;
|
|
28
|
+
this.issues = issues;
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
var ApplicationPortRemoteError = class extends Error {
|
|
32
|
+
code;
|
|
33
|
+
details;
|
|
34
|
+
retriable;
|
|
35
|
+
constructor(error) {
|
|
36
|
+
super(`Remote application-port operation failed: ${error.code}`);
|
|
37
|
+
this.name = "ApplicationPortRemoteError";
|
|
38
|
+
this.code = error.code;
|
|
39
|
+
this.details = error.details;
|
|
40
|
+
this.retriable = error.retriable;
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
var ApplicationPortCancelledError = class extends Error {
|
|
44
|
+
code = ApplicationPortProxyErrorCode.cancelled;
|
|
45
|
+
reasonCode;
|
|
46
|
+
constructor(reasonCode) {
|
|
47
|
+
super(`Application-port request was cancelled: ${reasonCode}`);
|
|
48
|
+
this.name = "ApplicationPortCancelledError";
|
|
49
|
+
this.reasonCode = reasonCode;
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
function applicationPortOperationKey(portId, operationId) {
|
|
53
|
+
return `${portId}\0${operationId}`;
|
|
54
|
+
}
|
|
55
|
+
function applicationPortOperationGrant(grant, portId, operationId) {
|
|
56
|
+
return grant.operations.find((operation) => operation.portId === portId && operation.operationId === operationId);
|
|
57
|
+
}
|
|
58
|
+
function applicationPortFrameBase(grant, frame) {
|
|
59
|
+
return {
|
|
60
|
+
schema: "network.xyo.dapp.port.frame",
|
|
61
|
+
schemaVersion: 1,
|
|
62
|
+
dappId: grant.dappId,
|
|
63
|
+
protocolVersion: grant.protocolVersion,
|
|
64
|
+
planId: grant.planId,
|
|
65
|
+
systemInstanceId: grant.systemInstanceId,
|
|
66
|
+
hostSessionId: grant.hostSessionId,
|
|
67
|
+
attachmentId: grant.attachmentId,
|
|
68
|
+
requestId: frame.requestId,
|
|
69
|
+
deadlineUnixMs: frame.deadlineUnixMs,
|
|
70
|
+
portId: frame.portId,
|
|
71
|
+
operationId: frame.operationId
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
function parseApplicationPortFrame(value) {
|
|
75
|
+
return PortFrameZod.parse(value);
|
|
76
|
+
}
|
|
77
|
+
function applicationPortErrorFrame(grant, frame, error) {
|
|
78
|
+
return parseApplicationPortFrame({
|
|
79
|
+
...applicationPortFrameBase(grant, frame),
|
|
80
|
+
frameKind: "error",
|
|
81
|
+
error
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// src/applicationPortClient.ts
|
|
86
|
+
var ApplicationPortStreamInstance = class {
|
|
87
|
+
requestId;
|
|
88
|
+
streamId;
|
|
89
|
+
client;
|
|
90
|
+
state;
|
|
91
|
+
constructor(client, state, streamId) {
|
|
92
|
+
this.client = client;
|
|
93
|
+
this.requestId = state.frame.requestId;
|
|
94
|
+
this.state = state;
|
|
95
|
+
this.streamId = streamId;
|
|
96
|
+
}
|
|
97
|
+
[Symbol.asyncIterator]() {
|
|
98
|
+
return this;
|
|
99
|
+
}
|
|
100
|
+
cancel(reasonCode = "client-cancelled") {
|
|
101
|
+
this.client.cancel(this.state, reasonCode);
|
|
102
|
+
}
|
|
103
|
+
credit(value) {
|
|
104
|
+
this.client.credit(this.state, value);
|
|
105
|
+
}
|
|
106
|
+
async next() {
|
|
107
|
+
const value = this.state.queue.shift();
|
|
108
|
+
if (value !== void 0) return { done: false, value };
|
|
109
|
+
if (this.state.terminalError !== void 0) throw this.state.terminalError;
|
|
110
|
+
if (this.state.isTerminal) return { done: true, value: void 0 };
|
|
111
|
+
const waiter = { promise: Promise.withResolvers() };
|
|
112
|
+
this.state.waiters.push(waiter);
|
|
113
|
+
return await waiter.promise.promise;
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
var ApplicationPortClientInstance = class {
|
|
117
|
+
isClosed = false;
|
|
118
|
+
listener;
|
|
119
|
+
options;
|
|
120
|
+
requests = /* @__PURE__ */ new Map();
|
|
121
|
+
constructor(options) {
|
|
122
|
+
this.options = { ...options, grant: PortAttachmentGrantZod.parse(options.grant) };
|
|
123
|
+
this.listener = (message) => this.receive(message);
|
|
124
|
+
options.endpoint.addMessageListener(this.listener);
|
|
125
|
+
options.endpoint.start?.();
|
|
126
|
+
}
|
|
127
|
+
cancel(state, reasonCode) {
|
|
128
|
+
if (state.isTerminal || state.reasonCode !== void 0 || this.isClosed) return;
|
|
129
|
+
state.reasonCode = reasonCode;
|
|
130
|
+
this.post(parseApplicationPortFrame({
|
|
131
|
+
...applicationPortFrameBase(this.options.grant, state.frame),
|
|
132
|
+
frameKind: "cancel",
|
|
133
|
+
reasonCode
|
|
134
|
+
}));
|
|
135
|
+
}
|
|
136
|
+
close() {
|
|
137
|
+
if (this.isClosed) return;
|
|
138
|
+
this.isClosed = true;
|
|
139
|
+
this.options.endpoint.removeMessageListener(this.listener);
|
|
140
|
+
const error = new ApplicationPortProxyError(
|
|
141
|
+
ApplicationPortProxyErrorCode.detached,
|
|
142
|
+
"Application-port client detached before the request completed"
|
|
143
|
+
);
|
|
144
|
+
for (const state of this.requests.values()) this.fail(state, error);
|
|
145
|
+
this.requests.clear();
|
|
146
|
+
}
|
|
147
|
+
credit(state, value) {
|
|
148
|
+
if (!state.isOpen || state.isTerminal || state.streamId === void 0) {
|
|
149
|
+
throw new ApplicationPortProxyError(
|
|
150
|
+
ApplicationPortProxyErrorCode.streamStateInvalid,
|
|
151
|
+
"Credit requires an open stream"
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
const grant = state.grant;
|
|
155
|
+
this.verifyCredit(value, state.availableCredit, grant);
|
|
156
|
+
state.availableCredit += value;
|
|
157
|
+
this.post(parseApplicationPortFrame({
|
|
158
|
+
...applicationPortFrameBase(this.options.grant, state.frame),
|
|
159
|
+
frameKind: "stream-credit",
|
|
160
|
+
streamId: state.streamId,
|
|
161
|
+
credit: value
|
|
162
|
+
}));
|
|
163
|
+
}
|
|
164
|
+
request(options) {
|
|
165
|
+
if (options.signal?.aborted === true) return Promise.reject(new ApplicationPortCancelledError("client-abort"));
|
|
166
|
+
const grant = this.requireGrant(options.portId, options.operationId, false);
|
|
167
|
+
const frame = this.requestFrame(options);
|
|
168
|
+
const promise = Promise.withResolvers();
|
|
169
|
+
const state = {
|
|
170
|
+
frame,
|
|
171
|
+
grant,
|
|
172
|
+
isTerminal: false,
|
|
173
|
+
kind: "unary",
|
|
174
|
+
promise
|
|
175
|
+
};
|
|
176
|
+
this.requests.set(frame.requestId, state);
|
|
177
|
+
this.setAbortBinding(state, options.signal);
|
|
178
|
+
this.post(frame);
|
|
179
|
+
return promise.promise;
|
|
180
|
+
}
|
|
181
|
+
resumeStream(options) {
|
|
182
|
+
if (options.signal?.aborted === true) return Promise.reject(new ApplicationPortCancelledError("client-abort"));
|
|
183
|
+
const grant = this.requireGrant(options.portId, options.operationId, true);
|
|
184
|
+
if (!grant.resumable) {
|
|
185
|
+
throw new ApplicationPortProxyError(
|
|
186
|
+
ApplicationPortProxyErrorCode.operationKindMismatch,
|
|
187
|
+
"Application-port stream does not permit resume"
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
this.verifyCredit(options.credit, 0, grant);
|
|
191
|
+
const frame = parseApplicationPortFrame({
|
|
192
|
+
...this.newFrameBase(options),
|
|
193
|
+
frameKind: "stream-resume",
|
|
194
|
+
streamId: options.streamId,
|
|
195
|
+
lastAcceptedSequence: options.lastAcceptedSequence,
|
|
196
|
+
resumeCursor: options.resumeCursor,
|
|
197
|
+
credit: options.credit
|
|
198
|
+
});
|
|
199
|
+
const state = this.streamState(frame, grant, options.credit);
|
|
200
|
+
state.availableCredit = options.credit;
|
|
201
|
+
state.lastAcceptedSequence = options.lastAcceptedSequence;
|
|
202
|
+
state.resumeCursor = options.resumeCursor;
|
|
203
|
+
state.streamId = options.streamId;
|
|
204
|
+
this.requests.set(frame.requestId, state);
|
|
205
|
+
this.setAbortBinding(state, options.signal);
|
|
206
|
+
this.post(frame);
|
|
207
|
+
return state.opened.promise;
|
|
208
|
+
}
|
|
209
|
+
stream(options) {
|
|
210
|
+
if (options.signal?.aborted === true) return Promise.reject(new ApplicationPortCancelledError("client-abort"));
|
|
211
|
+
const grant = this.requireGrant(options.portId, options.operationId, true);
|
|
212
|
+
this.verifyCredit(options.initialCredit, 0, grant);
|
|
213
|
+
const frame = this.requestFrame(options);
|
|
214
|
+
const state = this.streamState(frame, grant, options.initialCredit);
|
|
215
|
+
this.requests.set(frame.requestId, state);
|
|
216
|
+
this.setAbortBinding(state, options.signal);
|
|
217
|
+
this.post(frame);
|
|
218
|
+
return state.opened.promise;
|
|
219
|
+
}
|
|
220
|
+
completeStream(state) {
|
|
221
|
+
state.isTerminal = true;
|
|
222
|
+
state.removeAbortListener?.();
|
|
223
|
+
for (const waiter of state.waiters.splice(0)) {
|
|
224
|
+
if (state.terminalError === void 0) waiter.promise.resolve({ done: true, value: void 0 });
|
|
225
|
+
else waiter.promise.reject(state.terminalError);
|
|
226
|
+
}
|
|
227
|
+
this.requests.delete(state.frame.requestId);
|
|
228
|
+
}
|
|
229
|
+
fail(state, error) {
|
|
230
|
+
state.isTerminal = true;
|
|
231
|
+
state.removeAbortListener?.();
|
|
232
|
+
if (state.kind === "unary") state.promise.reject(error);
|
|
233
|
+
else {
|
|
234
|
+
state.terminalError = error;
|
|
235
|
+
state.opened.reject(error);
|
|
236
|
+
this.completeStream(state);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
handleStreamFrame(state, frame) {
|
|
240
|
+
if (frame.frameKind === "error") {
|
|
241
|
+
this.fail(state, new ApplicationPortRemoteError(frame.error));
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
if (frame.frameKind === "cancel-ack") {
|
|
245
|
+
this.fail(state, new ApplicationPortCancelledError(state.reasonCode ?? "remote-cancelled"));
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
if (frame.frameKind === "stream-open") {
|
|
249
|
+
this.openStream(state, frame);
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
if (!state.isOpen || !("streamId" in frame) || frame.streamId !== state.streamId) {
|
|
253
|
+
this.fail(state, new ApplicationPortProxyError(
|
|
254
|
+
ApplicationPortProxyErrorCode.streamStateInvalid,
|
|
255
|
+
"Stream frame arrived before a matching open"
|
|
256
|
+
));
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
if (frame.frameKind === "stream-item") this.receiveStreamItem(state, frame);
|
|
260
|
+
else if (frame.frameKind === "stream-error" || frame.frameKind === "stream-close") {
|
|
261
|
+
this.terminateStream(state, frame);
|
|
262
|
+
} else {
|
|
263
|
+
this.report(ApplicationPortProxyErrorCode.directionInvalid, "Client received a client-to-host stream frame", frame);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
newFrameBase(options) {
|
|
267
|
+
if (this.isClosed) {
|
|
268
|
+
throw new ApplicationPortProxyError(ApplicationPortProxyErrorCode.detached, "Application-port client is closed");
|
|
269
|
+
}
|
|
270
|
+
return applicationPortFrameBase(this.options.grant, {
|
|
271
|
+
...options,
|
|
272
|
+
requestId: this.options.nextRequestId()
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
openStream(state, frame) {
|
|
276
|
+
const grant = state.grant;
|
|
277
|
+
const resumeMismatch = state.lastAcceptedSequence > 0 && frame.nextSequence !== state.lastAcceptedSequence + 1;
|
|
278
|
+
if (state.isOpen || frame.resumable !== grant.resumable || state.streamId !== void 0 && state.streamId !== frame.streamId || resumeMismatch) {
|
|
279
|
+
this.fail(state, new ApplicationPortProxyError(
|
|
280
|
+
ApplicationPortProxyErrorCode.streamStateInvalid,
|
|
281
|
+
"Stream open conflicts with the pending request"
|
|
282
|
+
));
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
state.isOpen = true;
|
|
286
|
+
state.streamId = frame.streamId;
|
|
287
|
+
state.lastAcceptedSequence = frame.nextSequence - 1;
|
|
288
|
+
state.resumeCursor = frame.resumeCursor;
|
|
289
|
+
state.opened.resolve(new ApplicationPortStreamInstance(this, state, frame.streamId));
|
|
290
|
+
if (state.frame.frameKind === "request") this.credit(state, state.initialCredit);
|
|
291
|
+
}
|
|
292
|
+
post(frame) {
|
|
293
|
+
if (this.isClosed) {
|
|
294
|
+
throw new ApplicationPortProxyError(ApplicationPortProxyErrorCode.detached, "Application-port client is closed");
|
|
295
|
+
}
|
|
296
|
+
this.options.endpoint.send(frame);
|
|
297
|
+
}
|
|
298
|
+
receive(value) {
|
|
299
|
+
if (this.isClosed) return;
|
|
300
|
+
const result = validatePortFrame(value, {
|
|
301
|
+
authenticatedPrincipalId: this.options.authenticatedPrincipalId,
|
|
302
|
+
grant: this.options.grant,
|
|
303
|
+
now: this.options.now()
|
|
304
|
+
});
|
|
305
|
+
if (!result.success) {
|
|
306
|
+
this.options.onProtocolError({
|
|
307
|
+
cause: new ApplicationPortProxyError(
|
|
308
|
+
ApplicationPortProxyErrorCode.directionInvalid,
|
|
309
|
+
"Application-port frame failed attachment validation",
|
|
310
|
+
result.issues
|
|
311
|
+
),
|
|
312
|
+
value
|
|
313
|
+
});
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
this.receiveValid(result.value);
|
|
317
|
+
}
|
|
318
|
+
receiveStreamItem(state, frame) {
|
|
319
|
+
if (state.availableCredit <= 0) {
|
|
320
|
+
this.fail(state, new ApplicationPortProxyError(
|
|
321
|
+
ApplicationPortProxyErrorCode.streamStateInvalid,
|
|
322
|
+
"Stream item exceeded receiver credit"
|
|
323
|
+
));
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
state.availableCredit -= 1;
|
|
327
|
+
const canonical = canonicalizeIJson({
|
|
328
|
+
body: frame.body,
|
|
329
|
+
consistency: frame.consistency,
|
|
330
|
+
resumeCursor: frame.resumeCursor ?? null
|
|
331
|
+
});
|
|
332
|
+
const prior = state.seen.get(frame.sequence);
|
|
333
|
+
if (prior !== void 0) {
|
|
334
|
+
if (prior !== canonical) {
|
|
335
|
+
this.fail(state, new ApplicationPortProxyError(
|
|
336
|
+
ApplicationPortProxyErrorCode.streamStateInvalid,
|
|
337
|
+
"Redelivered stream sequence changed content"
|
|
338
|
+
));
|
|
339
|
+
}
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
if (frame.sequence !== state.lastAcceptedSequence + 1) {
|
|
343
|
+
this.fail(state, new ApplicationPortProxyError(
|
|
344
|
+
ApplicationPortProxyErrorCode.streamStateInvalid,
|
|
345
|
+
"Stream item sequence is not contiguous"
|
|
346
|
+
));
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
state.seen.set(frame.sequence, canonical);
|
|
350
|
+
state.lastAcceptedSequence = frame.sequence;
|
|
351
|
+
state.resumeCursor = frame.resumeCursor;
|
|
352
|
+
const value = this.streamValue(frame);
|
|
353
|
+
const waiter = state.waiters.shift();
|
|
354
|
+
if (waiter === void 0) state.queue.push(value);
|
|
355
|
+
else waiter.promise.resolve({ done: false, value });
|
|
356
|
+
}
|
|
357
|
+
receiveUnary(state, frame) {
|
|
358
|
+
if (frame.frameKind === "response") {
|
|
359
|
+
state.isTerminal = true;
|
|
360
|
+
state.removeAbortListener?.();
|
|
361
|
+
state.promise.resolve({ body: frame.body, consistency: frame.consistency });
|
|
362
|
+
this.requests.delete(frame.requestId);
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
if (frame.frameKind === "error") this.fail(state, new ApplicationPortRemoteError(frame.error));
|
|
366
|
+
else if (frame.frameKind === "cancel-ack") {
|
|
367
|
+
this.fail(state, new ApplicationPortCancelledError(state.reasonCode ?? "remote-cancelled"));
|
|
368
|
+
} else this.report(ApplicationPortProxyErrorCode.directionInvalid, "Client received a client-to-host frame", frame);
|
|
369
|
+
}
|
|
370
|
+
receiveValid(frame) {
|
|
371
|
+
const state = this.requests.get(frame.requestId);
|
|
372
|
+
if (state === void 0) {
|
|
373
|
+
this.report(ApplicationPortProxyErrorCode.requestUnknown, "Response targets an unknown request", frame);
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
if (frame.portId !== state.frame.portId || frame.operationId !== state.frame.operationId) {
|
|
377
|
+
this.fail(state, new ApplicationPortProxyError(
|
|
378
|
+
ApplicationPortProxyErrorCode.streamStateInvalid,
|
|
379
|
+
"Response operation does not match its request"
|
|
380
|
+
));
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
if (state.kind === "stream") {
|
|
384
|
+
this.handleStreamFrame(state, frame);
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
this.receiveUnary(state, frame);
|
|
388
|
+
}
|
|
389
|
+
report(code, message, value) {
|
|
390
|
+
this.options.onProtocolError({ cause: new ApplicationPortProxyError(code, message), value });
|
|
391
|
+
}
|
|
392
|
+
requestFrame(options) {
|
|
393
|
+
return parseApplicationPortFrame({
|
|
394
|
+
...this.newFrameBase(options),
|
|
395
|
+
frameKind: "request",
|
|
396
|
+
body: options.body
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
requireGrant(portId, operationId, streaming) {
|
|
400
|
+
const grant = applicationPortOperationGrant(this.options.grant, portId, operationId);
|
|
401
|
+
if (grant?.streaming !== streaming) {
|
|
402
|
+
throw new ApplicationPortProxyError(
|
|
403
|
+
ApplicationPortProxyErrorCode.operationKindMismatch,
|
|
404
|
+
`Attachment does not grant ${streaming ? "stream" : "unary"} operation ${portId}.${operationId}`
|
|
405
|
+
);
|
|
406
|
+
}
|
|
407
|
+
return grant;
|
|
408
|
+
}
|
|
409
|
+
setAbortBinding(state, signal) {
|
|
410
|
+
if (signal === void 0) return;
|
|
411
|
+
const listener = () => this.cancel(state, "client-abort");
|
|
412
|
+
signal.addEventListener("abort", listener, { once: true });
|
|
413
|
+
Object.assign(state, { removeAbortListener: () => signal.removeEventListener("abort", listener) });
|
|
414
|
+
}
|
|
415
|
+
streamState(frame, grant, initialCredit) {
|
|
416
|
+
return {
|
|
417
|
+
availableCredit: 0,
|
|
418
|
+
frame,
|
|
419
|
+
grant,
|
|
420
|
+
initialCredit,
|
|
421
|
+
isOpen: false,
|
|
422
|
+
isTerminal: false,
|
|
423
|
+
kind: "stream",
|
|
424
|
+
lastAcceptedSequence: 0,
|
|
425
|
+
opened: Promise.withResolvers(),
|
|
426
|
+
queue: [],
|
|
427
|
+
seen: /* @__PURE__ */ new Map(),
|
|
428
|
+
waiters: []
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
streamValue(frame) {
|
|
432
|
+
return {
|
|
433
|
+
body: frame.body,
|
|
434
|
+
consistency: frame.consistency,
|
|
435
|
+
...frame.resumeCursor === void 0 ? {} : { resumeCursor: frame.resumeCursor },
|
|
436
|
+
sequence: frame.sequence
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
terminateStream(state, frame) {
|
|
440
|
+
if (frame.lastSequence !== Math.max(0, state.lastAcceptedSequence)) {
|
|
441
|
+
this.fail(state, new ApplicationPortProxyError(
|
|
442
|
+
ApplicationPortProxyErrorCode.streamStateInvalid,
|
|
443
|
+
"Stream terminal sequence does not match accepted items"
|
|
444
|
+
));
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
state.resumeCursor = frame.resumeCursor;
|
|
448
|
+
if (frame.frameKind === "stream-error") state.terminalError = new ApplicationPortRemoteError(frame.error);
|
|
449
|
+
this.completeStream(state);
|
|
450
|
+
}
|
|
451
|
+
verifyCredit(value, availableCredit, grant) {
|
|
452
|
+
if (!Number.isSafeInteger(value) || value <= 0 || availableCredit + value > grant.maximumCredit) {
|
|
453
|
+
throw new ApplicationPortProxyError(
|
|
454
|
+
ApplicationPortProxyErrorCode.streamStateInvalid,
|
|
455
|
+
"Outstanding credit exceeds the stream grant"
|
|
456
|
+
);
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
};
|
|
460
|
+
function createApplicationPortClient(options) {
|
|
461
|
+
return new ApplicationPortClientInstance(options);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// src/applicationPortHost.ts
|
|
465
|
+
import {
|
|
466
|
+
PortAttachmentGrantZod as PortAttachmentGrantZod2,
|
|
467
|
+
validatePortFrame as validatePortFrame2
|
|
468
|
+
} from "@xyo-network/dapp-kit";
|
|
469
|
+
function registeredOperations(options) {
|
|
470
|
+
const operations = /* @__PURE__ */ new Map();
|
|
471
|
+
for (const operation of options.operations) {
|
|
472
|
+
const key = applicationPortOperationKey(operation.portId, operation.operationId);
|
|
473
|
+
const grant = applicationPortOperationGrant(options.grant, operation.portId, operation.operationId);
|
|
474
|
+
if (operations.has(key)) {
|
|
475
|
+
throw new ApplicationPortProxyError(
|
|
476
|
+
ApplicationPortProxyErrorCode.duplicateRequest,
|
|
477
|
+
`Application-port operation is registered more than once: ${operation.portId}.${operation.operationId}`
|
|
478
|
+
);
|
|
479
|
+
}
|
|
480
|
+
if (grant?.streaming !== operation.streaming) {
|
|
481
|
+
throw new ApplicationPortProxyError(
|
|
482
|
+
ApplicationPortProxyErrorCode.operationKindMismatch,
|
|
483
|
+
`Registered operation conflicts with attachment grant: ${operation.portId}.${operation.operationId}`
|
|
484
|
+
);
|
|
485
|
+
}
|
|
486
|
+
operations.set(key, operation);
|
|
487
|
+
}
|
|
488
|
+
return operations;
|
|
489
|
+
}
|
|
490
|
+
var ApplicationPortHostInstance = class {
|
|
491
|
+
isClosed = false;
|
|
492
|
+
listener;
|
|
493
|
+
operations;
|
|
494
|
+
options;
|
|
495
|
+
requests = /* @__PURE__ */ new Map();
|
|
496
|
+
constructor(options) {
|
|
497
|
+
this.options = { ...options, grant: PortAttachmentGrantZod2.parse(options.grant) };
|
|
498
|
+
this.operations = registeredOperations(this.options);
|
|
499
|
+
this.listener = (message) => void this.receive(message);
|
|
500
|
+
options.endpoint.addMessageListener(this.listener);
|
|
501
|
+
options.endpoint.start?.();
|
|
502
|
+
}
|
|
503
|
+
async close() {
|
|
504
|
+
if (this.isClosed) return;
|
|
505
|
+
this.isClosed = true;
|
|
506
|
+
this.options.endpoint.removeMessageListener(this.listener);
|
|
507
|
+
const completions = [];
|
|
508
|
+
for (const state of this.requests.values()) {
|
|
509
|
+
state.isCancelled = true;
|
|
510
|
+
state.controller.abort(ApplicationPortProxyErrorCode.detached);
|
|
511
|
+
if (state.kind === "unary" && state.completion !== void 0) completions.push(state.completion);
|
|
512
|
+
if (state.kind === "stream") this.collectStreamClosePromises(state, completions);
|
|
513
|
+
}
|
|
514
|
+
await Promise.allSettled(completions);
|
|
515
|
+
this.requests.clear();
|
|
516
|
+
}
|
|
517
|
+
cancelAcknowledgement(frame) {
|
|
518
|
+
return parseApplicationPortFrame({
|
|
519
|
+
...applicationPortFrameBase(this.options.grant, frame),
|
|
520
|
+
frameKind: "cancel-ack",
|
|
521
|
+
willEmitNoNewWork: true
|
|
522
|
+
});
|
|
523
|
+
}
|
|
524
|
+
async cancelRequest(frame) {
|
|
525
|
+
const state = this.requests.get(frame.requestId);
|
|
526
|
+
if (state === void 0) {
|
|
527
|
+
this.report(ApplicationPortProxyErrorCode.requestUnknown, "Cancellation targets an unknown request", frame);
|
|
528
|
+
return;
|
|
529
|
+
}
|
|
530
|
+
if (state.isTerminal || state.isCancelled) return;
|
|
531
|
+
state.isCancelled = true;
|
|
532
|
+
state.controller.abort(frame.reasonCode);
|
|
533
|
+
if (state.kind === "unary") {
|
|
534
|
+
await state.completion;
|
|
535
|
+
return;
|
|
536
|
+
}
|
|
537
|
+
await state.openPromise;
|
|
538
|
+
if (state.pumpPromise !== void 0) await state.pumpPromise;
|
|
539
|
+
await state.source?.cancel?.(frame.reasonCode);
|
|
540
|
+
if (!this.isClosed && !state.isTerminal) {
|
|
541
|
+
state.isTerminal = true;
|
|
542
|
+
this.post(this.cancelAcknowledgement(frame));
|
|
543
|
+
}
|
|
544
|
+
this.requests.delete(frame.requestId);
|
|
545
|
+
}
|
|
546
|
+
collectStreamClosePromises(state, completions) {
|
|
547
|
+
if (state.openPromise !== void 0) completions.push(state.openPromise);
|
|
548
|
+
if (state.pumpPromise !== void 0) completions.push(state.pumpPromise);
|
|
549
|
+
if (state.source?.cancel !== void 0) {
|
|
550
|
+
completions.push(state.source.cancel(ApplicationPortProxyErrorCode.detached));
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
dispatchStart(frame) {
|
|
554
|
+
const operation = this.operations.get(applicationPortOperationKey(frame.portId, frame.operationId));
|
|
555
|
+
const grant = applicationPortOperationGrant(this.options.grant, frame.portId, frame.operationId);
|
|
556
|
+
if (operation === void 0 || grant === void 0) {
|
|
557
|
+
this.postError(frame, ApplicationPortProxyErrorCode.operationUnavailable);
|
|
558
|
+
return;
|
|
559
|
+
}
|
|
560
|
+
if (operation.streaming !== grant.streaming) {
|
|
561
|
+
this.postError(frame, ApplicationPortProxyErrorCode.operationKindMismatch);
|
|
562
|
+
return;
|
|
563
|
+
}
|
|
564
|
+
if (operation.streaming && grant.streaming) {
|
|
565
|
+
this.openStream(frame, operation, grant);
|
|
566
|
+
return;
|
|
567
|
+
}
|
|
568
|
+
if (frame.frameKind === "request" && !operation.streaming) {
|
|
569
|
+
this.startUnary(frame, operation);
|
|
570
|
+
return;
|
|
571
|
+
}
|
|
572
|
+
this.postError(frame, ApplicationPortProxyErrorCode.operationKindMismatch);
|
|
573
|
+
}
|
|
574
|
+
mapError(frame, cause) {
|
|
575
|
+
const fallback = { code: ApplicationPortProxyErrorCode.hostFailure, retriable: false };
|
|
576
|
+
try {
|
|
577
|
+
const mapped = this.options.mapError(cause);
|
|
578
|
+
applicationPortErrorFrame(this.options.grant, frame, mapped);
|
|
579
|
+
return mapped;
|
|
580
|
+
} catch {
|
|
581
|
+
return fallback;
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
openStream(frame, operation, grant) {
|
|
585
|
+
const state = {
|
|
586
|
+
availableCredit: frame.frameKind === "stream-resume" ? frame.credit : 0,
|
|
587
|
+
controller: new AbortController(),
|
|
588
|
+
frame,
|
|
589
|
+
grant,
|
|
590
|
+
isCancelled: false,
|
|
591
|
+
isTerminal: false,
|
|
592
|
+
kind: "stream",
|
|
593
|
+
lastSequence: frame.frameKind === "stream-resume" ? frame.lastAcceptedSequence : -1,
|
|
594
|
+
...frame.frameKind === "stream-resume" ? { resumeCursor: frame.resumeCursor } : {}
|
|
595
|
+
};
|
|
596
|
+
this.requests.set(frame.requestId, state);
|
|
597
|
+
state.openPromise = this.runStreamOpen(state, frame, operation);
|
|
598
|
+
}
|
|
599
|
+
post(frame) {
|
|
600
|
+
if (!this.isClosed) this.options.endpoint.send(frame);
|
|
601
|
+
}
|
|
602
|
+
postError(frame, code) {
|
|
603
|
+
this.post(applicationPortErrorFrame(this.options.grant, frame, { code, retriable: false }));
|
|
604
|
+
}
|
|
605
|
+
async pump(state) {
|
|
606
|
+
if (state.pumpPromise !== void 0 || state.source === void 0 || state.isTerminal || state.isCancelled) return;
|
|
607
|
+
const pumping = this.pumpOnce(state);
|
|
608
|
+
state.pumpPromise = pumping;
|
|
609
|
+
await pumping;
|
|
610
|
+
if (state.pumpPromise === pumping) state.pumpPromise = void 0;
|
|
611
|
+
}
|
|
612
|
+
async pumpOnce(state) {
|
|
613
|
+
try {
|
|
614
|
+
while (state.availableCredit > 0 && !state.isTerminal && !state.isCancelled && !this.isClosed) {
|
|
615
|
+
if (!await this.sendNext(state)) return;
|
|
616
|
+
}
|
|
617
|
+
} catch (error) {
|
|
618
|
+
if (!state.isCancelled && !this.isClosed) this.terminateStreamWithError(state, error);
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
async receive(value) {
|
|
622
|
+
if (this.isClosed) return;
|
|
623
|
+
const result = validatePortFrame2(value, {
|
|
624
|
+
authenticatedPrincipalId: this.options.authenticatedPrincipalId,
|
|
625
|
+
grant: this.options.grant,
|
|
626
|
+
now: this.options.now()
|
|
627
|
+
});
|
|
628
|
+
if (!result.success) {
|
|
629
|
+
this.reportInvalid(result.issues, value);
|
|
630
|
+
return;
|
|
631
|
+
}
|
|
632
|
+
const frame = result.value;
|
|
633
|
+
if (frame.frameKind === "cancel") {
|
|
634
|
+
await this.cancelRequest(frame);
|
|
635
|
+
return;
|
|
636
|
+
}
|
|
637
|
+
if (frame.frameKind === "stream-credit") {
|
|
638
|
+
await this.receiveCredit(frame);
|
|
639
|
+
return;
|
|
640
|
+
}
|
|
641
|
+
if (frame.frameKind !== "request" && frame.frameKind !== "stream-resume") {
|
|
642
|
+
this.report(ApplicationPortProxyErrorCode.directionInvalid, "Host received a server-to-client frame", frame);
|
|
643
|
+
return;
|
|
644
|
+
}
|
|
645
|
+
if (this.requests.has(frame.requestId)) {
|
|
646
|
+
this.report(ApplicationPortProxyErrorCode.duplicateRequest, "Request ID is already active", frame);
|
|
647
|
+
return;
|
|
648
|
+
}
|
|
649
|
+
this.dispatchStart(frame);
|
|
650
|
+
}
|
|
651
|
+
async receiveCredit(frame) {
|
|
652
|
+
const state = this.requests.get(frame.requestId);
|
|
653
|
+
if (state?.kind !== "stream" || state.source?.streamId !== frame.streamId || state.isTerminal) {
|
|
654
|
+
this.report(ApplicationPortProxyErrorCode.streamStateInvalid, "Stream credit targets an inactive stream", frame);
|
|
655
|
+
return;
|
|
656
|
+
}
|
|
657
|
+
if (state.availableCredit + frame.credit > state.grant.maximumCredit) {
|
|
658
|
+
this.terminateStreamWithError(state, void 0, "port.credit-exceeded");
|
|
659
|
+
return;
|
|
660
|
+
}
|
|
661
|
+
state.availableCredit += frame.credit;
|
|
662
|
+
await this.pump(state);
|
|
663
|
+
}
|
|
664
|
+
report(code, message, value) {
|
|
665
|
+
this.options.onProtocolError({ cause: new ApplicationPortProxyError(code, message), value });
|
|
666
|
+
}
|
|
667
|
+
reportInvalid(issues, value) {
|
|
668
|
+
this.options.onProtocolError({
|
|
669
|
+
cause: new ApplicationPortProxyError(
|
|
670
|
+
ApplicationPortProxyErrorCode.directionInvalid,
|
|
671
|
+
"Application-port frame failed attachment validation",
|
|
672
|
+
issues
|
|
673
|
+
),
|
|
674
|
+
value
|
|
675
|
+
});
|
|
676
|
+
}
|
|
677
|
+
async runStreamOpen(state, frame, operation) {
|
|
678
|
+
try {
|
|
679
|
+
const source = await this.startStreamSource(state, frame, operation);
|
|
680
|
+
this.verifyStreamSource(source, frame, state.grant);
|
|
681
|
+
state.source = source;
|
|
682
|
+
state.lastSequence = source.nextSequence - 1;
|
|
683
|
+
state.resumeCursor = source.resumeCursor;
|
|
684
|
+
if (state.isCancelled || this.isClosed) return;
|
|
685
|
+
this.post(parseApplicationPortFrame({
|
|
686
|
+
...applicationPortFrameBase(this.options.grant, frame),
|
|
687
|
+
frameKind: "stream-open",
|
|
688
|
+
streamId: source.streamId,
|
|
689
|
+
delivery: "at-least-once",
|
|
690
|
+
resumable: source.resumable,
|
|
691
|
+
nextSequence: source.nextSequence,
|
|
692
|
+
...source.resumeCursor === void 0 ? {} : { resumeCursor: source.resumeCursor }
|
|
693
|
+
}));
|
|
694
|
+
await this.pump(state);
|
|
695
|
+
} catch (error) {
|
|
696
|
+
if (!state.isCancelled && !this.isClosed) {
|
|
697
|
+
state.isTerminal = true;
|
|
698
|
+
this.post(applicationPortErrorFrame(this.options.grant, frame, this.mapError(frame, error)));
|
|
699
|
+
this.requests.delete(frame.requestId);
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
async runUnary(state, frame, operation) {
|
|
704
|
+
try {
|
|
705
|
+
const result = await operation.handle({
|
|
706
|
+
body: frame.body,
|
|
707
|
+
grant: this.options.grant,
|
|
708
|
+
requestId: frame.requestId,
|
|
709
|
+
signal: state.controller.signal
|
|
710
|
+
});
|
|
711
|
+
if (!state.isCancelled && !this.isClosed) {
|
|
712
|
+
state.isTerminal = true;
|
|
713
|
+
this.post(parseApplicationPortFrame({
|
|
714
|
+
...applicationPortFrameBase(this.options.grant, frame),
|
|
715
|
+
frameKind: "response",
|
|
716
|
+
consistency: result.consistency,
|
|
717
|
+
body: result.body
|
|
718
|
+
}));
|
|
719
|
+
}
|
|
720
|
+
} catch (error) {
|
|
721
|
+
if (!state.isCancelled && !this.isClosed) {
|
|
722
|
+
state.isTerminal = true;
|
|
723
|
+
this.post(applicationPortErrorFrame(this.options.grant, frame, this.mapError(frame, error)));
|
|
724
|
+
}
|
|
725
|
+
} finally {
|
|
726
|
+
if (state.isCancelled && !state.isTerminal && !this.isClosed) {
|
|
727
|
+
state.isTerminal = true;
|
|
728
|
+
this.post(this.cancelAcknowledgement(frame));
|
|
729
|
+
}
|
|
730
|
+
this.requests.delete(frame.requestId);
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
async sendNext(state) {
|
|
734
|
+
const source = state.source;
|
|
735
|
+
if (source === void 0) return false;
|
|
736
|
+
const read = await source.next(state.controller.signal);
|
|
737
|
+
if (state.isCancelled || this.isClosed) return false;
|
|
738
|
+
if (read.done) {
|
|
739
|
+
state.isTerminal = true;
|
|
740
|
+
this.post(parseApplicationPortFrame({
|
|
741
|
+
...applicationPortFrameBase(this.options.grant, state.frame),
|
|
742
|
+
frameKind: "stream-close",
|
|
743
|
+
streamId: source.streamId,
|
|
744
|
+
lastSequence: Math.max(0, state.lastSequence),
|
|
745
|
+
...read.resumeCursor === void 0 ? {} : { resumeCursor: read.resumeCursor }
|
|
746
|
+
}));
|
|
747
|
+
this.requests.delete(state.frame.requestId);
|
|
748
|
+
return false;
|
|
749
|
+
}
|
|
750
|
+
if (source.resumable && read.value.resumeCursor === void 0) {
|
|
751
|
+
throw new ApplicationPortProxyError(
|
|
752
|
+
ApplicationPortProxyErrorCode.streamStateInvalid,
|
|
753
|
+
"Resumable stream item did not provide a durable cursor"
|
|
754
|
+
);
|
|
755
|
+
}
|
|
756
|
+
state.availableCredit -= 1;
|
|
757
|
+
state.lastSequence += 1;
|
|
758
|
+
state.resumeCursor = read.value.resumeCursor;
|
|
759
|
+
this.post(parseApplicationPortFrame({
|
|
760
|
+
...applicationPortFrameBase(this.options.grant, state.frame),
|
|
761
|
+
frameKind: "stream-item",
|
|
762
|
+
streamId: source.streamId,
|
|
763
|
+
sequence: state.lastSequence,
|
|
764
|
+
...read.value.resumeCursor === void 0 ? {} : { resumeCursor: read.value.resumeCursor },
|
|
765
|
+
consistency: read.value.consistency,
|
|
766
|
+
body: read.value.body
|
|
767
|
+
}));
|
|
768
|
+
return true;
|
|
769
|
+
}
|
|
770
|
+
async startStreamSource(state, frame, operation) {
|
|
771
|
+
const context = {
|
|
772
|
+
body: frame.frameKind === "request" ? frame.body : null,
|
|
773
|
+
grant: this.options.grant,
|
|
774
|
+
requestId: frame.requestId,
|
|
775
|
+
signal: state.controller.signal
|
|
776
|
+
};
|
|
777
|
+
if (frame.frameKind === "request") return await operation.open(context);
|
|
778
|
+
if (operation.resume === void 0) {
|
|
779
|
+
throw new ApplicationPortProxyError(
|
|
780
|
+
ApplicationPortProxyErrorCode.operationUnavailable,
|
|
781
|
+
"Stream operation does not implement resume"
|
|
782
|
+
);
|
|
783
|
+
}
|
|
784
|
+
return await operation.resume({
|
|
785
|
+
...context,
|
|
786
|
+
lastAcceptedSequence: frame.lastAcceptedSequence,
|
|
787
|
+
resumeCursor: frame.resumeCursor,
|
|
788
|
+
streamId: frame.streamId
|
|
789
|
+
});
|
|
790
|
+
}
|
|
791
|
+
startUnary(frame, operation) {
|
|
792
|
+
const state = {
|
|
793
|
+
controller: new AbortController(),
|
|
794
|
+
frame,
|
|
795
|
+
isCancelled: false,
|
|
796
|
+
isTerminal: false,
|
|
797
|
+
kind: "unary"
|
|
798
|
+
};
|
|
799
|
+
this.requests.set(frame.requestId, state);
|
|
800
|
+
state.completion = this.runUnary(state, frame, operation);
|
|
801
|
+
}
|
|
802
|
+
terminateStreamWithError(state, cause, code) {
|
|
803
|
+
const source = state.source;
|
|
804
|
+
if (source === void 0 || state.isTerminal || this.isClosed) return;
|
|
805
|
+
state.isTerminal = true;
|
|
806
|
+
const error = code === void 0 ? this.mapError(state.frame, cause) : { code, retriable: false };
|
|
807
|
+
this.post(parseApplicationPortFrame({
|
|
808
|
+
...applicationPortFrameBase(this.options.grant, state.frame),
|
|
809
|
+
frameKind: "stream-error",
|
|
810
|
+
streamId: source.streamId,
|
|
811
|
+
lastSequence: Math.max(0, state.lastSequence),
|
|
812
|
+
...state.resumeCursor === void 0 ? {} : { resumeCursor: state.resumeCursor },
|
|
813
|
+
error
|
|
814
|
+
}));
|
|
815
|
+
this.requests.delete(state.frame.requestId);
|
|
816
|
+
}
|
|
817
|
+
verifyStreamSource(source, frame, grant) {
|
|
818
|
+
const resumeMismatch = frame.frameKind === "stream-resume" && (source.streamId !== frame.streamId || source.nextSequence !== frame.lastAcceptedSequence + 1);
|
|
819
|
+
if (source.resumable !== grant.resumable || source.resumable && source.resumeCursor === void 0 || resumeMismatch) {
|
|
820
|
+
throw new ApplicationPortProxyError(
|
|
821
|
+
ApplicationPortProxyErrorCode.streamStateInvalid,
|
|
822
|
+
"Stream source conflicts with its grant or resume request"
|
|
823
|
+
);
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
};
|
|
827
|
+
function createApplicationPortHost(options) {
|
|
828
|
+
return new ApplicationPortHostInstance(options);
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
// src/applicationPortWire.ts
|
|
832
|
+
import {
|
|
833
|
+
canonicalizeIJson as canonicalizeIJson2,
|
|
834
|
+
IdentifierZod,
|
|
835
|
+
parseStrictJsonText,
|
|
836
|
+
PortAttachmentGrantZod as PortAttachmentGrantZod3,
|
|
837
|
+
PortFrameZod as PortFrameZod2
|
|
838
|
+
} from "@xyo-network/dapp-kit";
|
|
839
|
+
import { z } from "zod";
|
|
840
|
+
var ApplicationPortTransportSchema = "network.xyo.dapp.port.transport";
|
|
841
|
+
var ApplicationPortTransportHelloZod = z.strictObject({
|
|
842
|
+
schema: z.literal(ApplicationPortTransportSchema),
|
|
843
|
+
schemaVersion: z.literal(1),
|
|
844
|
+
messageKind: z.literal("hello"),
|
|
845
|
+
maximumFrameBytes: z.number().int().positive().max(16 * 1024 * 1024),
|
|
846
|
+
grant: PortAttachmentGrantZod3
|
|
847
|
+
});
|
|
848
|
+
var ApplicationPortTransportErrorZod = z.strictObject({
|
|
849
|
+
schema: z.literal(ApplicationPortTransportSchema),
|
|
850
|
+
schemaVersion: z.literal(1),
|
|
851
|
+
messageKind: z.literal("error"),
|
|
852
|
+
code: IdentifierZod,
|
|
853
|
+
retriable: z.boolean()
|
|
854
|
+
});
|
|
855
|
+
var ApplicationPortWireMessageZod = z.union([
|
|
856
|
+
PortFrameZod2,
|
|
857
|
+
ApplicationPortTransportHelloZod,
|
|
858
|
+
ApplicationPortTransportErrorZod
|
|
859
|
+
]);
|
|
860
|
+
var ApplicationPortWireErrorCode = {
|
|
861
|
+
frameTooLarge: "port.wire-frame-too-large",
|
|
862
|
+
invalidMessage: "port.wire-invalid-message"
|
|
863
|
+
};
|
|
864
|
+
var ApplicationPortWireError = class extends Error {
|
|
865
|
+
code;
|
|
866
|
+
constructor(code, message, options) {
|
|
867
|
+
super(message, options);
|
|
868
|
+
this.name = "ApplicationPortWireError";
|
|
869
|
+
this.code = code;
|
|
870
|
+
}
|
|
871
|
+
};
|
|
872
|
+
function byteLength(value) {
|
|
873
|
+
return new TextEncoder().encode(value).byteLength;
|
|
874
|
+
}
|
|
875
|
+
function verifyMaximumFrameBytes(value) {
|
|
876
|
+
if (!Number.isSafeInteger(value) || value <= 0 || value > 16 * 1024 * 1024) {
|
|
877
|
+
throw new RangeError("maximum application-port frame bytes must be between 1 and 16777216");
|
|
878
|
+
}
|
|
879
|
+
return value;
|
|
880
|
+
}
|
|
881
|
+
function isApplicationPortFrame(message) {
|
|
882
|
+
return message.schema === "network.xyo.dapp.port.frame";
|
|
883
|
+
}
|
|
884
|
+
function parseApplicationPortWireText(source, maximumFrameBytes) {
|
|
885
|
+
const maximum = verifyMaximumFrameBytes(maximumFrameBytes);
|
|
886
|
+
if (byteLength(source) > maximum) {
|
|
887
|
+
throw new ApplicationPortWireError(
|
|
888
|
+
ApplicationPortWireErrorCode.frameTooLarge,
|
|
889
|
+
`application-port frame exceeds ${maximum} bytes`
|
|
890
|
+
);
|
|
891
|
+
}
|
|
892
|
+
try {
|
|
893
|
+
return ApplicationPortWireMessageZod.parse(parseStrictJsonText(source, { maximumDepth: 64 }));
|
|
894
|
+
} catch (cause) {
|
|
895
|
+
if (cause instanceof ApplicationPortWireError) throw cause;
|
|
896
|
+
throw new ApplicationPortWireError(
|
|
897
|
+
ApplicationPortWireErrorCode.invalidMessage,
|
|
898
|
+
"application-port wire message is invalid",
|
|
899
|
+
{ cause }
|
|
900
|
+
);
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
function serializeApplicationPortWireMessage(message, maximumFrameBytes) {
|
|
904
|
+
const maximum = verifyMaximumFrameBytes(maximumFrameBytes);
|
|
905
|
+
let source;
|
|
906
|
+
try {
|
|
907
|
+
source = canonicalizeIJson2(ApplicationPortWireMessageZod.parse(message));
|
|
908
|
+
} catch (cause) {
|
|
909
|
+
throw new ApplicationPortWireError(
|
|
910
|
+
ApplicationPortWireErrorCode.invalidMessage,
|
|
911
|
+
"application-port wire message is invalid",
|
|
912
|
+
{ cause }
|
|
913
|
+
);
|
|
914
|
+
}
|
|
915
|
+
if (byteLength(source) > maximum) {
|
|
916
|
+
throw new ApplicationPortWireError(
|
|
917
|
+
ApplicationPortWireErrorCode.frameTooLarge,
|
|
918
|
+
`application-port frame exceeds ${maximum} bytes`
|
|
919
|
+
);
|
|
920
|
+
}
|
|
921
|
+
return source;
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
// src/webSocketApplicationPortClient.ts
|
|
925
|
+
import { PortFrameZod as PortFrameZod3 } from "@xyo-network/dapp-kit";
|
|
926
|
+
var WebSocketApplicationPortClientErrorCode = {
|
|
927
|
+
aborted: "port.websocket-aborted",
|
|
928
|
+
closed: "port.websocket-closed",
|
|
929
|
+
handshakeInvalid: "port.websocket-handshake-invalid",
|
|
930
|
+
serverRejected: "port.websocket-server-rejected",
|
|
931
|
+
socketError: "port.websocket-socket-error",
|
|
932
|
+
timeout: "port.websocket-timeout",
|
|
933
|
+
urlInvalid: "port.websocket-url-invalid"
|
|
934
|
+
};
|
|
935
|
+
var WebSocketApplicationPortClientError = class extends Error {
|
|
936
|
+
code;
|
|
937
|
+
constructor(code, message, options) {
|
|
938
|
+
super(message, options);
|
|
939
|
+
this.name = "WebSocketApplicationPortClientError";
|
|
940
|
+
this.code = code;
|
|
941
|
+
}
|
|
942
|
+
};
|
|
943
|
+
function report(options, error) {
|
|
944
|
+
try {
|
|
945
|
+
options.onTransportError?.(error);
|
|
946
|
+
} catch {
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
function webSocketUrl(value) {
|
|
950
|
+
let url;
|
|
951
|
+
try {
|
|
952
|
+
url = new URL(value.toString());
|
|
953
|
+
} catch (cause) {
|
|
954
|
+
throw new WebSocketApplicationPortClientError(
|
|
955
|
+
WebSocketApplicationPortClientErrorCode.urlInvalid,
|
|
956
|
+
"application-port WebSocket URL is invalid",
|
|
957
|
+
{ cause }
|
|
958
|
+
);
|
|
959
|
+
}
|
|
960
|
+
if (url.protocol !== "ws:" && url.protocol !== "wss:" || url.username !== "" || url.password !== "" || url.hash !== "" || url.search !== "") {
|
|
961
|
+
throw new WebSocketApplicationPortClientError(
|
|
962
|
+
WebSocketApplicationPortClientErrorCode.urlInvalid,
|
|
963
|
+
"application-port URL must be credential-free WebSocket without query or fragment data"
|
|
964
|
+
);
|
|
965
|
+
}
|
|
966
|
+
return url.href;
|
|
967
|
+
}
|
|
968
|
+
function positiveInteger(value, name, maximum) {
|
|
969
|
+
if (!Number.isSafeInteger(value) || value <= 0 || value > maximum) {
|
|
970
|
+
throw new RangeError(`${name} must be between 1 and ${maximum}`);
|
|
971
|
+
}
|
|
972
|
+
return value;
|
|
973
|
+
}
|
|
974
|
+
function closeWebSocket(socket, code, reason) {
|
|
975
|
+
if (socket.readyState === WebSocket.OPEN) socket.close(code, reason);
|
|
976
|
+
else if (socket.readyState === WebSocket.CONNECTING) socket.close();
|
|
977
|
+
}
|
|
978
|
+
function assertExpectedGrant(grant, expected, now) {
|
|
979
|
+
if (grant.dappId !== expected.dappId || grant.planId !== expected.planId || grant.protocolVersion !== expected.protocolVersion || grant.expiresAt <= now) {
|
|
980
|
+
throw new WebSocketApplicationPortClientError(
|
|
981
|
+
WebSocketApplicationPortClientErrorCode.handshakeInvalid,
|
|
982
|
+
"application-port attachment does not match the expected dApp, plan, protocol, or lifetime"
|
|
983
|
+
);
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
var BrowserWebSocketApplicationPortEndpoint = class {
|
|
987
|
+
listeners = /* @__PURE__ */ new Set();
|
|
988
|
+
maximumFrameBytes;
|
|
989
|
+
onFailure;
|
|
990
|
+
socket;
|
|
991
|
+
constructor(socket, maximumFrameBytes, onFailure) {
|
|
992
|
+
this.maximumFrameBytes = maximumFrameBytes;
|
|
993
|
+
this.onFailure = onFailure;
|
|
994
|
+
this.socket = socket;
|
|
995
|
+
this.socket.addEventListener("message", this.receive);
|
|
996
|
+
}
|
|
997
|
+
addMessageListener(listener) {
|
|
998
|
+
this.listeners.add(listener);
|
|
999
|
+
}
|
|
1000
|
+
dispose() {
|
|
1001
|
+
this.listeners.clear();
|
|
1002
|
+
this.socket.removeEventListener("message", this.receive);
|
|
1003
|
+
}
|
|
1004
|
+
removeMessageListener(listener) {
|
|
1005
|
+
this.listeners.delete(listener);
|
|
1006
|
+
}
|
|
1007
|
+
send(message) {
|
|
1008
|
+
if (this.socket.readyState !== WebSocket.OPEN) {
|
|
1009
|
+
throw new WebSocketApplicationPortClientError(
|
|
1010
|
+
WebSocketApplicationPortClientErrorCode.closed,
|
|
1011
|
+
"application-port WebSocket is not open"
|
|
1012
|
+
);
|
|
1013
|
+
}
|
|
1014
|
+
const frame = PortFrameZod3.parse(message);
|
|
1015
|
+
this.socket.send(serializeApplicationPortWireMessage(frame, this.maximumFrameBytes));
|
|
1016
|
+
}
|
|
1017
|
+
receive = (event) => {
|
|
1018
|
+
try {
|
|
1019
|
+
if (typeof event.data !== "string") {
|
|
1020
|
+
throw new WebSocketApplicationPortClientError(
|
|
1021
|
+
WebSocketApplicationPortClientErrorCode.handshakeInvalid,
|
|
1022
|
+
"binary application-port frames are forbidden"
|
|
1023
|
+
);
|
|
1024
|
+
}
|
|
1025
|
+
const message = parseApplicationPortWireText(event.data, this.maximumFrameBytes);
|
|
1026
|
+
if (!isApplicationPortFrame(message)) {
|
|
1027
|
+
throw new WebSocketApplicationPortClientError(
|
|
1028
|
+
WebSocketApplicationPortClientErrorCode.handshakeInvalid,
|
|
1029
|
+
"server sent a transport control message after attachment"
|
|
1030
|
+
);
|
|
1031
|
+
}
|
|
1032
|
+
for (const listener of this.listeners) listener(message);
|
|
1033
|
+
} catch (error) {
|
|
1034
|
+
this.onFailure(error);
|
|
1035
|
+
}
|
|
1036
|
+
};
|
|
1037
|
+
};
|
|
1038
|
+
async function connectWebSocketApplicationPort(options) {
|
|
1039
|
+
if (options.signal?.aborted === true) {
|
|
1040
|
+
throw new WebSocketApplicationPortClientError(
|
|
1041
|
+
WebSocketApplicationPortClientErrorCode.aborted,
|
|
1042
|
+
"application-port connection was aborted before launch"
|
|
1043
|
+
);
|
|
1044
|
+
}
|
|
1045
|
+
const maximumFrameBytes = positiveInteger(
|
|
1046
|
+
options.maximumFrameBytes ?? 256 * 1024,
|
|
1047
|
+
"maximum frame bytes",
|
|
1048
|
+
16 * 1024 * 1024
|
|
1049
|
+
);
|
|
1050
|
+
const handshakeTimeoutMs = positiveInteger(options.handshakeTimeoutMs ?? 1e4, "handshake timeout", 6e4);
|
|
1051
|
+
const createWebSocket = options.webSocketFactory ?? ((url) => new WebSocket(url));
|
|
1052
|
+
const socket = createWebSocket(webSocketUrl(options.url));
|
|
1053
|
+
const closed = Promise.withResolvers();
|
|
1054
|
+
socket.addEventListener("close", (event) => closed.resolve({
|
|
1055
|
+
code: event.code,
|
|
1056
|
+
reason: event.reason,
|
|
1057
|
+
wasClean: event.wasClean
|
|
1058
|
+
}), { once: true });
|
|
1059
|
+
let timer;
|
|
1060
|
+
let abortListener;
|
|
1061
|
+
try {
|
|
1062
|
+
const hello = await new Promise((resolve, reject) => {
|
|
1063
|
+
const cleanup = () => {
|
|
1064
|
+
clearTimeout(timer);
|
|
1065
|
+
socket.removeEventListener("message", onMessage);
|
|
1066
|
+
socket.removeEventListener("error", onError);
|
|
1067
|
+
socket.removeEventListener("close", onClose);
|
|
1068
|
+
if (abortListener !== void 0) options.signal?.removeEventListener("abort", abortListener);
|
|
1069
|
+
};
|
|
1070
|
+
const fail = (error) => {
|
|
1071
|
+
cleanup();
|
|
1072
|
+
closeWebSocket(socket, 4e3, "attachment failed");
|
|
1073
|
+
reject(error instanceof Error ? error : new Error("application-port attachment failed", { cause: error }));
|
|
1074
|
+
};
|
|
1075
|
+
const onClose = () => fail(new WebSocketApplicationPortClientError(
|
|
1076
|
+
WebSocketApplicationPortClientErrorCode.closed,
|
|
1077
|
+
"application-port WebSocket closed before attachment"
|
|
1078
|
+
));
|
|
1079
|
+
const onError = () => fail(new WebSocketApplicationPortClientError(
|
|
1080
|
+
WebSocketApplicationPortClientErrorCode.socketError,
|
|
1081
|
+
"application-port WebSocket failed before attachment"
|
|
1082
|
+
));
|
|
1083
|
+
const onMessage = (event) => {
|
|
1084
|
+
try {
|
|
1085
|
+
if (typeof event.data !== "string") throw new TypeError("binary handshake");
|
|
1086
|
+
const message = parseApplicationPortWireText(event.data, maximumFrameBytes);
|
|
1087
|
+
const rejected = ApplicationPortTransportErrorZod.safeParse(message);
|
|
1088
|
+
if (rejected.success) {
|
|
1089
|
+
fail(new WebSocketApplicationPortClientError(
|
|
1090
|
+
WebSocketApplicationPortClientErrorCode.serverRejected,
|
|
1091
|
+
`application-port server rejected attachment: ${rejected.data.code}`
|
|
1092
|
+
));
|
|
1093
|
+
return;
|
|
1094
|
+
}
|
|
1095
|
+
const parsed = ApplicationPortTransportHelloZod.parse(message);
|
|
1096
|
+
assertExpectedGrant(parsed.grant, options.expected, options.now());
|
|
1097
|
+
cleanup();
|
|
1098
|
+
resolve(parsed);
|
|
1099
|
+
} catch (cause) {
|
|
1100
|
+
fail(cause instanceof WebSocketApplicationPortClientError ? cause : new WebSocketApplicationPortClientError(
|
|
1101
|
+
WebSocketApplicationPortClientErrorCode.handshakeInvalid,
|
|
1102
|
+
"application-port attachment handshake is invalid",
|
|
1103
|
+
{ cause }
|
|
1104
|
+
));
|
|
1105
|
+
}
|
|
1106
|
+
};
|
|
1107
|
+
abortListener = () => fail(new WebSocketApplicationPortClientError(
|
|
1108
|
+
WebSocketApplicationPortClientErrorCode.aborted,
|
|
1109
|
+
"application-port connection was aborted"
|
|
1110
|
+
));
|
|
1111
|
+
options.signal?.addEventListener("abort", abortListener, { once: true });
|
|
1112
|
+
socket.addEventListener("message", onMessage);
|
|
1113
|
+
socket.addEventListener("error", onError, { once: true });
|
|
1114
|
+
socket.addEventListener("close", onClose, { once: true });
|
|
1115
|
+
timer = setTimeout(() => fail(new WebSocketApplicationPortClientError(
|
|
1116
|
+
WebSocketApplicationPortClientErrorCode.timeout,
|
|
1117
|
+
"application-port attachment handshake timed out"
|
|
1118
|
+
)), handshakeTimeoutMs);
|
|
1119
|
+
});
|
|
1120
|
+
const negotiatedMaximum = Math.min(maximumFrameBytes, hello.maximumFrameBytes);
|
|
1121
|
+
const endpoint = new BrowserWebSocketApplicationPortEndpoint(socket, negotiatedMaximum, (error) => {
|
|
1122
|
+
report(options, error);
|
|
1123
|
+
closeWebSocket(socket, 4001, "invalid application-port frame");
|
|
1124
|
+
});
|
|
1125
|
+
const client = createApplicationPortClient({
|
|
1126
|
+
authenticatedPrincipalId: hello.grant.principalId,
|
|
1127
|
+
endpoint,
|
|
1128
|
+
grant: hello.grant,
|
|
1129
|
+
nextRequestId: options.nextRequestId,
|
|
1130
|
+
now: options.now,
|
|
1131
|
+
onProtocolError: options.onProtocolError
|
|
1132
|
+
});
|
|
1133
|
+
socket.addEventListener("close", () => {
|
|
1134
|
+
endpoint.dispose();
|
|
1135
|
+
client.close();
|
|
1136
|
+
}, { once: true });
|
|
1137
|
+
let isClosed = false;
|
|
1138
|
+
return {
|
|
1139
|
+
client,
|
|
1140
|
+
grant: hello.grant,
|
|
1141
|
+
whenClosed: closed.promise,
|
|
1142
|
+
close: () => {
|
|
1143
|
+
if (isClosed) return;
|
|
1144
|
+
isClosed = true;
|
|
1145
|
+
client.close();
|
|
1146
|
+
endpoint.dispose();
|
|
1147
|
+
closeWebSocket(socket, 1e3, "client detached");
|
|
1148
|
+
}
|
|
1149
|
+
};
|
|
1150
|
+
} catch (error) {
|
|
1151
|
+
report(options, error);
|
|
1152
|
+
throw error;
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1155
|
+
export {
|
|
1156
|
+
ApplicationPortCancelledError,
|
|
1157
|
+
ApplicationPortProxyError,
|
|
1158
|
+
ApplicationPortProxyErrorCode,
|
|
1159
|
+
ApplicationPortRemoteError,
|
|
1160
|
+
ApplicationPortTransportErrorZod,
|
|
1161
|
+
ApplicationPortTransportHelloZod,
|
|
1162
|
+
ApplicationPortTransportSchema,
|
|
1163
|
+
ApplicationPortWireError,
|
|
1164
|
+
ApplicationPortWireErrorCode,
|
|
1165
|
+
ApplicationPortWireMessageZod,
|
|
1166
|
+
WebSocketApplicationPortClientError,
|
|
1167
|
+
WebSocketApplicationPortClientErrorCode,
|
|
1168
|
+
applicationPortErrorFrame,
|
|
1169
|
+
applicationPortFrameBase,
|
|
1170
|
+
applicationPortOperationGrant,
|
|
1171
|
+
applicationPortOperationKey,
|
|
1172
|
+
connectWebSocketApplicationPort,
|
|
1173
|
+
createApplicationPortClient,
|
|
1174
|
+
createApplicationPortHost,
|
|
1175
|
+
isApplicationPortFrame,
|
|
1176
|
+
parseApplicationPortFrame,
|
|
1177
|
+
parseApplicationPortWireText,
|
|
1178
|
+
serializeApplicationPortWireMessage
|
|
1179
|
+
};
|
|
1180
|
+
//# sourceMappingURL=index.mjs.map
|