@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.
Files changed (30) hide show
  1. package/README.md +23 -0
  2. package/dist/browser/applicationPortClient.d.ts +6 -0
  3. package/dist/browser/applicationPortClient.d.ts.map +1 -0
  4. package/dist/browser/applicationPortHost.d.ts +10 -0
  5. package/dist/browser/applicationPortHost.d.ts.map +1 -0
  6. package/dist/browser/applicationPortModel.d.ts +154 -0
  7. package/dist/browser/applicationPortModel.d.ts.map +1 -0
  8. package/dist/browser/applicationPortWire.d.ts +327 -0
  9. package/dist/browser/applicationPortWire.d.ts.map +1 -0
  10. package/dist/browser/index.d.ts +6 -0
  11. package/dist/browser/index.d.ts.map +1 -0
  12. package/dist/browser/index.mjs +1180 -0
  13. package/dist/browser/index.mjs.map +7 -0
  14. package/dist/browser/webSocketApplicationPortClient.d.ts +46 -0
  15. package/dist/browser/webSocketApplicationPortClient.d.ts.map +1 -0
  16. package/dist/node/applicationPortClient.d.ts +6 -0
  17. package/dist/node/applicationPortClient.d.ts.map +1 -0
  18. package/dist/node/applicationPortHost.d.ts +10 -0
  19. package/dist/node/applicationPortHost.d.ts.map +1 -0
  20. package/dist/node/applicationPortModel.d.ts +154 -0
  21. package/dist/node/applicationPortModel.d.ts.map +1 -0
  22. package/dist/node/applicationPortWire.d.ts +327 -0
  23. package/dist/node/applicationPortWire.d.ts.map +1 -0
  24. package/dist/node/index.d.ts +6 -0
  25. package/dist/node/index.d.ts.map +1 -0
  26. package/dist/node/index.mjs +1180 -0
  27. package/dist/node/index.mjs.map +7 -0
  28. package/dist/node/webSocketApplicationPortClient.d.ts +46 -0
  29. package/dist/node/webSocketApplicationPortClient.d.ts.map +1 -0
  30. 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