@xyo-network/dapp-kit-port 2.1.4 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -467,11 +467,11 @@ import {
467
467
  validatePortFrame as validatePortFrame2
468
468
  } from "@xyo-network/dapp-kit";
469
469
  function registeredOperations(options) {
470
- const operations = /* @__PURE__ */ new Map();
470
+ const operations2 = /* @__PURE__ */ new Map();
471
471
  for (const operation of options.operations) {
472
- const key = applicationPortOperationKey(operation.portId, operation.operationId);
472
+ const key2 = applicationPortOperationKey(operation.portId, operation.operationId);
473
473
  const grant = applicationPortOperationGrant(options.grant, operation.portId, operation.operationId);
474
- if (operations.has(key)) {
474
+ if (operations2.has(key2)) {
475
475
  throw new ApplicationPortProxyError(
476
476
  ApplicationPortProxyErrorCode.duplicateRequest,
477
477
  `Application-port operation is registered more than once: ${operation.portId}.${operation.operationId}`
@@ -483,15 +483,18 @@ function registeredOperations(options) {
483
483
  `Registered operation conflicts with attachment grant: ${operation.portId}.${operation.operationId}`
484
484
  );
485
485
  }
486
- operations.set(key, operation);
486
+ operations2.set(key2, operation);
487
487
  }
488
- return operations;
488
+ return operations2;
489
489
  }
490
490
  var ApplicationPortHostInstance = class {
491
+ cleanupErrors = [];
492
+ closePromise;
491
493
  isClosed = false;
492
494
  listener;
493
495
  operations;
494
496
  options;
497
+ ownedRequests = /* @__PURE__ */ new Set();
495
498
  requests = /* @__PURE__ */ new Map();
496
499
  constructor(options) {
497
500
  this.options = { ...options, grant: PortAttachmentGrantZod2.parse(options.grant) };
@@ -500,19 +503,42 @@ var ApplicationPortHostInstance = class {
500
503
  options.endpoint.addMessageListener(this.listener);
501
504
  options.endpoint.start?.();
502
505
  }
503
- async close() {
504
- if (this.isClosed) return;
506
+ close() {
507
+ return this.drain();
508
+ }
509
+ closeAdmission() {
510
+ if (this.closePromise !== void 0) return;
511
+ const completion = Promise.withResolvers();
512
+ this.closePromise = completion.promise;
513
+ void this.closePromise.catch(() => {
514
+ });
505
515
  this.isClosed = true;
506
- this.options.endpoint.removeMessageListener(this.listener);
507
- const completions = [];
508
- for (const state of this.requests.values()) {
516
+ try {
517
+ this.options.endpoint.removeMessageListener(this.listener);
518
+ } catch (cause) {
519
+ this.cleanupErrors.push(cause);
520
+ }
521
+ for (const state of this.ownedRequests) {
509
522
  state.isCancelled = true;
510
523
  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
524
  }
514
- await Promise.allSettled(completions);
515
- this.requests.clear();
525
+ void Promise.resolve().then(async () => {
526
+ const outcomes = await Promise.allSettled([...this.ownedRequests].map(async (state) => {
527
+ if (state.kind === "unary") await state.completion;
528
+ else await this.cleanupStream(state, ApplicationPortProxyErrorCode.detached);
529
+ }));
530
+ for (const outcome of outcomes) {
531
+ if (outcome.status === "rejected") this.cleanupErrors.push(outcome.reason);
532
+ }
533
+ this.requests.clear();
534
+ if (this.cleanupErrors.length > 0) {
535
+ throw new AggregateError([...this.cleanupErrors], "Application-port host cleanup failed");
536
+ }
537
+ }).then(completion.resolve, completion.reject);
538
+ }
539
+ drain() {
540
+ this.closeAdmission();
541
+ return this.closePromise;
516
542
  }
517
543
  cancelAcknowledgement(frame) {
518
544
  return parseApplicationPortFrame({
@@ -534,21 +560,35 @@ var ApplicationPortHostInstance = class {
534
560
  await state.completion;
535
561
  return;
536
562
  }
537
- await state.openPromise;
538
- if (state.pumpPromise !== void 0) await state.pumpPromise;
539
- await state.source?.cancel?.(frame.reasonCode);
563
+ await this.cleanupStream(state, frame.reasonCode);
540
564
  if (!this.isClosed && !state.isTerminal) {
541
565
  state.isTerminal = true;
542
- this.post(this.cancelAcknowledgement(frame));
566
+ if (state.cleanupFailed) this.postError(frame, ApplicationPortProxyErrorCode.hostFailure);
567
+ else this.post(this.cancelAcknowledgement(frame));
543
568
  }
544
569
  this.requests.delete(frame.requestId);
545
570
  }
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
- }
571
+ cleanupStream(state, reasonCode) {
572
+ if (state.cleanupPromise !== void 0) return state.cleanupPromise;
573
+ const completion = Promise.withResolvers();
574
+ state.cleanupPromise = completion.promise;
575
+ void Promise.resolve().then(async () => {
576
+ for (const work of [() => state.openPromise, () => state.pumpPromise, () => state.source?.cancel?.(reasonCode)]) {
577
+ try {
578
+ await work();
579
+ } catch (cause) {
580
+ state.cleanupFailed = true;
581
+ this.cleanupErrors.push(cause);
582
+ }
583
+ }
584
+ }).catch((cause) => {
585
+ state.cleanupFailed = true;
586
+ this.cleanupErrors.push(cause);
587
+ }).finally(() => {
588
+ this.ownedRequests.delete(state);
589
+ completion.resolve();
590
+ });
591
+ return state.cleanupPromise;
552
592
  }
553
593
  dispatchStart(frame) {
554
594
  const operation = this.operations.get(applicationPortOperationKey(frame.portId, frame.operationId));
@@ -594,7 +634,10 @@ var ApplicationPortHostInstance = class {
594
634
  ...frame.frameKind === "stream-resume" ? { resumeCursor: frame.resumeCursor } : {}
595
635
  };
596
636
  this.requests.set(frame.requestId, state);
597
- state.openPromise = this.runStreamOpen(state, frame, operation);
637
+ this.ownedRequests.add(state);
638
+ const completion = Promise.withResolvers();
639
+ state.openPromise = completion.promise;
640
+ void this.runStreamOpen(state, frame, operation).then(completion.resolve, completion.reject);
598
641
  }
599
642
  post(frame) {
600
643
  if (!this.isClosed) this.options.endpoint.send(frame);
@@ -604,8 +647,10 @@ var ApplicationPortHostInstance = class {
604
647
  }
605
648
  async pump(state) {
606
649
  if (state.pumpPromise !== void 0 || state.source === void 0 || state.isTerminal || state.isCancelled) return;
607
- const pumping = this.pumpOnce(state);
650
+ const completion = Promise.withResolvers();
651
+ const pumping = completion.promise;
608
652
  state.pumpPromise = pumping;
653
+ void this.pumpOnce(state).then(completion.resolve, completion.reject);
609
654
  await pumping;
610
655
  if (state.pumpPromise === pumping) state.pumpPromise = void 0;
611
656
  }
@@ -677,8 +722,8 @@ var ApplicationPortHostInstance = class {
677
722
  async runStreamOpen(state, frame, operation) {
678
723
  try {
679
724
  const source = await this.startStreamSource(state, frame, operation);
680
- this.verifyStreamSource(source, frame, state.grant);
681
725
  state.source = source;
726
+ this.verifyStreamSource(source, frame, state.grant);
682
727
  state.lastSequence = source.nextSequence - 1;
683
728
  state.resumeCursor = source.resumeCursor;
684
729
  if (state.isCancelled || this.isClosed) return;
@@ -697,6 +742,7 @@ var ApplicationPortHostInstance = class {
697
742
  state.isTerminal = true;
698
743
  this.post(applicationPortErrorFrame(this.options.grant, frame, this.mapError(frame, error)));
699
744
  this.requests.delete(frame.requestId);
745
+ void this.cleanupStream(state, ApplicationPortProxyErrorCode.hostFailure);
700
746
  }
701
747
  }
702
748
  }
@@ -728,6 +774,7 @@ var ApplicationPortHostInstance = class {
728
774
  this.post(this.cancelAcknowledgement(frame));
729
775
  }
730
776
  this.requests.delete(frame.requestId);
777
+ this.ownedRequests.delete(state);
731
778
  }
732
779
  }
733
780
  async sendNext(state) {
@@ -745,6 +792,7 @@ var ApplicationPortHostInstance = class {
745
792
  ...read.resumeCursor === void 0 ? {} : { resumeCursor: read.resumeCursor }
746
793
  }));
747
794
  this.requests.delete(state.frame.requestId);
795
+ this.ownedRequests.delete(state);
748
796
  return false;
749
797
  }
750
798
  if (source.resumable && read.value.resumeCursor === void 0) {
@@ -797,7 +845,10 @@ var ApplicationPortHostInstance = class {
797
845
  kind: "unary"
798
846
  };
799
847
  this.requests.set(frame.requestId, state);
800
- state.completion = this.runUnary(state, frame, operation);
848
+ this.ownedRequests.add(state);
849
+ const completion = Promise.withResolvers();
850
+ state.completion = completion.promise;
851
+ void this.runUnary(state, frame, operation).then(completion.resolve, completion.reject);
801
852
  }
802
853
  terminateStreamWithError(state, cause, code) {
803
854
  const source = state.source;
@@ -813,6 +864,9 @@ var ApplicationPortHostInstance = class {
813
864
  error
814
865
  }));
815
866
  this.requests.delete(state.frame.requestId);
867
+ state.isCancelled = true;
868
+ state.controller.abort(ApplicationPortProxyErrorCode.hostFailure);
869
+ void this.cleanupStream(state, ApplicationPortProxyErrorCode.hostFailure);
816
870
  }
817
871
  verifyStreamSource(source, frame, grant) {
818
872
  const resumeMismatch = frame.frameKind === "stream-resume" && (source.streamId !== frame.streamId || source.nextSequence !== frame.lastAcceptedSequence + 1);
@@ -923,6 +977,232 @@ function serializeApplicationPortWireMessage(message, maximumFrameBytes) {
923
977
  return source;
924
978
  }
925
979
 
980
+ // src/engineApplicationPortHost.ts
981
+ import {
982
+ DappEngineSystemReferenceZod,
983
+ PortAttachmentGrantZod as PortAttachmentGrantZod4,
984
+ validateDappEngineSystemReference
985
+ } from "@xyo-network/dapp-kit";
986
+
987
+ // src/engineOperationRecovery.ts
988
+ import {
989
+ DappOperationRecoveryBindingZod,
990
+ DappOperationRecoveryProfileZod,
991
+ validateDappOperationRecoveryAdmission
992
+ } from "@xyo-network/dapp-kit";
993
+
994
+ // src/enginePortAdmissionError.ts
995
+ var DappEnginePortAdmissionError = class extends Error {
996
+ code;
997
+ constructor(code) {
998
+ super(code);
999
+ this.name = "DappEnginePortAdmissionError";
1000
+ this.code = code;
1001
+ }
1002
+ };
1003
+
1004
+ // src/engineOperationRecovery.ts
1005
+ var key = (portId, operationId) => JSON.stringify([portId, operationId]);
1006
+ function bindEngineOperationRecovery(options, operations2, reference, target2) {
1007
+ if (options === void 0) return void 0;
1008
+ const parsed = DappOperationRecoveryProfileZod.safeParse(options.profile);
1009
+ if (!parsed.success || typeof options.readSnapshot !== "function") throw new DappEnginePortAdmissionError("recovery.profile-invalid");
1010
+ const profile = parsed.data;
1011
+ if (profile.operations.length !== operations2.length) throw new DappEnginePortAdmissionError("recovery.profile-mismatch");
1012
+ const requirements = /* @__PURE__ */ new Map();
1013
+ for (const requirement of profile.operations) {
1014
+ if (requirement.recovery.kind !== "read-only") Object.freeze(requirement.recovery.requiredGuarantees);
1015
+ Object.freeze(requirement.recovery);
1016
+ requirements.set(key(requirement.portId, requirement.operationId), Object.freeze(requirement));
1017
+ }
1018
+ for (const binding2 of operations2) {
1019
+ const requirement = requirements.get(key(binding2.operation.portId, binding2.operation.operationId));
1020
+ if (requirement === void 0 || requirement.recovery.kind === "read-only" !== (binding2.access === "read")) {
1021
+ throw new DappEnginePortAdmissionError("recovery.profile-mismatch");
1022
+ }
1023
+ }
1024
+ const parsedBinding = DappOperationRecoveryBindingZod.safeParse({
1025
+ reference,
1026
+ dappId: target2.status.dappId,
1027
+ hostSessionId: target2.status.hostSessionId,
1028
+ planId: target2.status.planId,
1029
+ network: target2.declaration.network,
1030
+ authorityScopeId: options.authorityScopeId
1031
+ });
1032
+ if (!parsedBinding.success) throw new DappEnginePortAdmissionError("recovery.binding-invalid");
1033
+ Object.freeze(parsedBinding.data.reference);
1034
+ Object.freeze(parsedBinding.data.network);
1035
+ const binding = Object.freeze(parsedBinding.data);
1036
+ const readSnapshot = options.readSnapshot.bind(options);
1037
+ return (portId, operationId) => {
1038
+ const requirement = requirements.get(key(portId, operationId));
1039
+ if (requirement.recovery.kind === "read-only") return;
1040
+ let snapshot;
1041
+ try {
1042
+ snapshot = readSnapshot(requirement, binding);
1043
+ } catch {
1044
+ throw new DappEnginePortAdmissionError("recovery.unavailable");
1045
+ }
1046
+ let admission;
1047
+ try {
1048
+ admission = validateDappOperationRecoveryAdmission(requirement, binding, snapshot);
1049
+ } catch {
1050
+ throw new DappEnginePortAdmissionError("recovery.snapshot-invalid");
1051
+ }
1052
+ if (!admission.success) throw new DappEnginePortAdmissionError(admission.issue.code);
1053
+ };
1054
+ }
1055
+
1056
+ // src/engineApplicationPortHost.ts
1057
+ function deferredEndpoint(source) {
1058
+ let listener;
1059
+ let attached = false;
1060
+ let closed = false;
1061
+ const endpoint = {
1062
+ addMessageListener: (value) => {
1063
+ listener = value;
1064
+ },
1065
+ removeMessageListener: (value) => {
1066
+ closed = true;
1067
+ if (attached) source.removeMessageListener(value);
1068
+ listener = void 0;
1069
+ },
1070
+ send: (message) => source.send(message)
1071
+ };
1072
+ return {
1073
+ endpoint,
1074
+ activate: () => {
1075
+ if (closed || listener === void 0) throw new DappEnginePortAdmissionError("engine.system-unavailable");
1076
+ attached = true;
1077
+ source.addMessageListener(listener);
1078
+ if (!closed) source.start?.();
1079
+ }
1080
+ };
1081
+ }
1082
+ function target(readStatus, reference, grant) {
1083
+ const result = validateDappEngineSystemReference(readStatus(), reference);
1084
+ if (!result.success) throw new DappEnginePortAdmissionError(result.issue.code);
1085
+ const status = result.value.status;
1086
+ if (grant.dappId !== status.dappId || grant.hostSessionId !== status.hostSessionId || grant.planId !== status.planId || grant.systemInstanceId !== status.systemInstanceId) {
1087
+ throw new DappEnginePortAdmissionError("engine.attachment-mismatch");
1088
+ }
1089
+ return result.value;
1090
+ }
1091
+ function operations(bindings, grant, readTarget, admitRecovery) {
1092
+ const keys = /* @__PURE__ */ new Set();
1093
+ return bindings.map(({ access, operation }) => {
1094
+ const {
1095
+ portId,
1096
+ operationId,
1097
+ streaming
1098
+ } = operation;
1099
+ const key2 = applicationPortOperationKey(portId, operationId);
1100
+ const allowed = grant.operations.find((value) => value.portId === portId && value.operationId === operationId);
1101
+ if (access !== "read" && access !== "write" || keys.has(key2) || allowed?.streaming !== streaming) {
1102
+ throw new DappEnginePortAdmissionError("engine.operation-binding-invalid");
1103
+ }
1104
+ keys.add(key2);
1105
+ const authorize = () => {
1106
+ const capability = readTarget().status.writeCapabilities.find((value) => value.portId === portId && value.operationId === operationId);
1107
+ if (access === "read" && capability !== void 0) throw new DappEnginePortAdmissionError("engine.operation-access-invalid");
1108
+ if (access === "write" && capability?.writable !== true) throw new DappEnginePortAdmissionError("engine.operation-not-writable");
1109
+ };
1110
+ const admit = () => {
1111
+ authorize();
1112
+ if (access === "write" && admitRecovery !== void 0) {
1113
+ admitRecovery(portId, operationId);
1114
+ authorize();
1115
+ }
1116
+ };
1117
+ if (operation.streaming) {
1118
+ const open = operation.open.bind(operation);
1119
+ const resume = operation.resume?.bind(operation);
1120
+ return Object.freeze({
1121
+ portId,
1122
+ operationId,
1123
+ streaming: true,
1124
+ open: (context) => {
1125
+ admit();
1126
+ return open(context);
1127
+ },
1128
+ ...resume === void 0 ? {} : {
1129
+ resume: (context) => {
1130
+ admit();
1131
+ return resume(context);
1132
+ }
1133
+ }
1134
+ });
1135
+ }
1136
+ const handle = operation.handle.bind(operation);
1137
+ return Object.freeze({
1138
+ portId,
1139
+ operationId,
1140
+ streaming: false,
1141
+ handle: (context) => {
1142
+ admit();
1143
+ return handle(context);
1144
+ }
1145
+ });
1146
+ });
1147
+ }
1148
+ async function createDappEngineApplicationPortHost(options) {
1149
+ const reference = DappEngineSystemReferenceZod.parse(options.reference);
1150
+ const grant = PortAttachmentGrantZod4.parse(options.grant);
1151
+ const readStatus = options.engine.status.bind(options.engine);
1152
+ const readTarget = () => target(readStatus, reference, grant);
1153
+ const selected = readTarget();
1154
+ const admitRecovery = bindEngineOperationRecovery(options.recovery, options.operations, reference, selected);
1155
+ const boundOperations = operations(options.operations, grant, readTarget, admitRecovery);
1156
+ const deferred = deferredEndpoint(options.endpoint);
1157
+ const mapDomainError = options.mapError.bind(options);
1158
+ const disconnect = options.disconnect.bind(options);
1159
+ const binding = options.engine.bindSystemWork(reference, () => {
1160
+ const host = createApplicationPortHost({
1161
+ authenticatedPrincipalId: options.authenticatedPrincipalId,
1162
+ endpoint: deferred.endpoint,
1163
+ grant,
1164
+ now: options.now,
1165
+ onProtocolError: options.onProtocolError,
1166
+ operations: boundOperations,
1167
+ mapError: (error) => error instanceof DappEnginePortAdmissionError ? { code: error.code, retriable: false } : mapDomainError(error)
1168
+ });
1169
+ let disconnected;
1170
+ return {
1171
+ close: () => {
1172
+ host.closeAdmission();
1173
+ disconnected = (async () => {
1174
+ await disconnect();
1175
+ })();
1176
+ void disconnected.catch(() => {
1177
+ });
1178
+ },
1179
+ drain: async () => {
1180
+ const outcomes = await Promise.allSettled([host.drain(), disconnected]);
1181
+ const errors = outcomes.filter((outcome) => outcome.status === "rejected").map((outcome) => outcome.reason);
1182
+ if (errors.length > 0) throw new AggregateError(errors, "Engine application-port drainage failed");
1183
+ }
1184
+ };
1185
+ });
1186
+ try {
1187
+ deferred.activate();
1188
+ readTarget();
1189
+ } catch (cause) {
1190
+ try {
1191
+ await binding.close();
1192
+ } catch (cleanup) {
1193
+ throw new AggregateError([cause, cleanup], "Engine application-port setup and cleanup failed", { cause: cleanup });
1194
+ }
1195
+ throw cause;
1196
+ }
1197
+ return Object.freeze({
1198
+ close: () => binding.close(),
1199
+ closeAdmission: () => {
1200
+ void binding.close();
1201
+ },
1202
+ drain: () => binding.close()
1203
+ });
1204
+ }
1205
+
926
1206
  // src/webSocketApplicationPortClient.ts
927
1207
  import { PortFrameZod as PortFrameZod3 } from "@xyo-network/dapp-kit";
928
1208
  var WebSocketApplicationPortClientErrorCode = {
@@ -1165,6 +1445,7 @@ export {
1165
1445
  ApplicationPortWireError,
1166
1446
  ApplicationPortWireErrorCode,
1167
1447
  ApplicationPortWireMessageZod,
1448
+ DappEnginePortAdmissionError,
1168
1449
  WebSocketApplicationPortClientError,
1169
1450
  WebSocketApplicationPortClientErrorCode,
1170
1451
  applicationPortErrorFrame,
@@ -1174,6 +1455,7 @@ export {
1174
1455
  connectWebSocketApplicationPort,
1175
1456
  createApplicationPortClient,
1176
1457
  createApplicationPortHost,
1458
+ createDappEngineApplicationPortHost,
1177
1459
  isApplicationPortFrame,
1178
1460
  parseApplicationPortFrame,
1179
1461
  parseApplicationPortWireText,