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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -21,3 +21,52 @@ compatible Node implementations. It verifies the expected dApp, plan, protocol
21
21
  version, and grant lifetime before publishing an `ApplicationPortClient`.
22
22
  Transport loss rejects pending unary and streaming work; reconnecting creates a
23
23
  new attachment and resumes only through the existing cursor protocol.
24
+
25
+ A host's `closeAdmission()` synchronously detaches inbound delivery and aborts
26
+ accepted requests. `drain()` closes admission if necessary and waits for unary
27
+ work, pending stream opening/reads, and source cancellation. `close()` combines
28
+ both; every caller joins the same retained promise. Cleanup failures reject
29
+ that promise after the remaining resources receive their cleanup attempts.
30
+ Late-acquired and invalid stream sources remain owned. A dApp session work
31
+ binding can delegate `close` to `host.closeAdmission()` and `drain` to
32
+ `host.drain()` so its Actor/Provider dependencies remain alive during drainage.
33
+ These are cooperative lifecycle guarantees, not external-effect rollback.
34
+
35
+ `createDappEngineApplicationPortHost()` adds exact engine/system-reference
36
+ admission to the same protocol host. It validates attachment identity before
37
+ endpoint acquisition and captures explicitly classified read/write operations.
38
+ Each unary start and stream open/resume rechecks readiness and incarnation;
39
+ writes require their own writable capability, while public reads need no write
40
+ capability. A declared read cannot bypass a matching write capability.
41
+
42
+ The helper binds work directly to the selected engine session, so replacement
43
+ and stop drain it before Actor/Provider teardown. Its required async `disconnect`
44
+ callback must settle pending clients and reject future transport work. Drain
45
+ awaits both transport release and accepted host work. Endpoint activation occurs
46
+ only after ownership is registered, and startup failure awaits cleanup. The
47
+ returned host exposes lifecycle only; it exposes no providers or locators.
48
+ This API does not replace domain authorization/consent or qualify browser
49
+ crash recovery. Existing browser-host durable-write admission remains separate.
50
+
51
+ ### Selected operation recovery admission
52
+
53
+ An engine-aware host can additionally receive `recovery` with an immutable
54
+ operation profile, an opaque bound-authority scope ID and a synchronous
55
+ `readSnapshot(requirement, binding)` callback. The adapter captures the profile
56
+ and callback at attachment and freezes the exact engine/plan/network binding
57
+ passed to observation. The selected profile must cover every bound operation
58
+ exactly and agree with its read/write classification.
59
+
60
+ Each write first passes current engine identity and operation authorization,
61
+ then the core recovery validator, then another authorization check in case the
62
+ observer stopped or revoked the system reentrantly. Public reads never invoke
63
+ the private snapshot callback. A missing guarantee, stale binding, unavailable,
64
+ corrupt, pending or uncertain state fails closed with a stable recovery code;
65
+ private callback exceptions are not exposed through domain error mapping.
66
+
67
+ The snapshot observes a readiness result already maintained by the selected
68
+ recovery owner. It does not run I/O, sign, broadcast or acquire a second journal.
69
+ Reported guarantees require separate adapter qualification. Kit-journal mode
70
+ retains the existing receipt executor and crash-durable requirement. Omitting
71
+ this optional profile preserves existing host composition and does not enable
72
+ browser writes.
@@ -5,6 +5,8 @@ export interface CreateApplicationPortHostOptions extends ApplicationPortPeerOpt
5
5
  }
6
6
  export interface ApplicationPortHost {
7
7
  close(): Promise<void>;
8
+ closeAdmission(): void;
9
+ drain(): Promise<void>;
8
10
  }
9
11
  export declare function createApplicationPortHost(options: CreateApplicationPortHostOptions): ApplicationPortHost;
10
12
  //# sourceMappingURL=applicationPortHost.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"applicationPortHost.d.ts","sourceRoot":"","sources":["../../src/applicationPortHost.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EACV,wBAAwB,EACxB,wBAAwB,EACxB,0BAA0B,EAI3B,MAAM,2BAA2B,CAAA;AAWlC,MAAM,WAAW,gCAAiC,SAAQ,0BAA0B;IAClF,QAAQ,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,wBAAwB,CAAA;IAC/D,QAAQ,CAAC,UAAU,EAAE,SAAS,wBAAwB,EAAE,CAAA;CACzD;AAED,MAAM,WAAW,mBAAmB;IAClC,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAA;CACvB;AAmbD,wBAAgB,yBAAyB,CAAC,OAAO,EAAE,gCAAgC,GAAG,mBAAmB,CAExG"}
1
+ {"version":3,"file":"applicationPortHost.d.ts","sourceRoot":"","sources":["../../src/applicationPortHost.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EACV,wBAAwB,EACxB,wBAAwB,EACxB,0BAA0B,EAI3B,MAAM,2BAA2B,CAAA;AAWlC,MAAM,WAAW,gCAAiC,SAAQ,0BAA0B;IAClF,QAAQ,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,wBAAwB,CAAA;IAC/D,QAAQ,CAAC,UAAU,EAAE,SAAS,wBAAwB,EAAE,CAAA;CACzD;AAED,MAAM,WAAW,mBAAmB;IAClC,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAA;IACtB,cAAc,IAAI,IAAI,CAAA;IACtB,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAA;CACvB;AA+eD,wBAAgB,yBAAyB,CAAC,OAAO,EAAE,gCAAgC,GAAG,mBAAmB,CAExG"}
@@ -0,0 +1,20 @@
1
+ import type { DappEngineSession, DappEngineSystemReference } from '@xyo-network/dapp-kit';
2
+ import type { ApplicationPortHost, CreateApplicationPortHostOptions } from './applicationPortHost.ts';
3
+ import type { ApplicationPortOperation } from './applicationPortModel.ts';
4
+ import type { DappEngineOperationRecoveryOptions } from './engineOperationRecovery.ts';
5
+ export type { DappEngineOperationRecoveryOptions } from './engineOperationRecovery.ts';
6
+ export { DappEnginePortAdmissionError } from './enginePortAdmissionError.ts';
7
+ export interface DappEngineApplicationPortOperation {
8
+ readonly access: 'read' | 'write';
9
+ readonly operation: ApplicationPortOperation;
10
+ }
11
+ export interface CreateDappEngineApplicationPortHostOptions extends Omit<CreateApplicationPortHostOptions, 'operations'> {
12
+ readonly disconnect: () => Promise<void>;
13
+ readonly engine: DappEngineSession;
14
+ readonly operations: readonly DappEngineApplicationPortOperation[];
15
+ readonly recovery?: DappEngineOperationRecoveryOptions;
16
+ readonly reference: DappEngineSystemReference;
17
+ }
18
+ /** Engine admission around the existing application-port protocol and work owner. */
19
+ export declare function createDappEngineApplicationPortHost(options: CreateDappEngineApplicationPortHostOptions): Promise<ApplicationPortHost>;
20
+ //# sourceMappingURL=engineApplicationPortHost.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"engineApplicationPortHost.d.ts","sourceRoot":"","sources":["../../src/engineApplicationPortHost.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,iBAAiB,EAAE,yBAAyB,EAC7C,MAAM,uBAAuB,CAAA;AAK9B,OAAO,KAAK,EAAE,mBAAmB,EAAE,gCAAgC,EAAE,MAAM,0BAA0B,CAAA;AAErG,OAAO,KAAK,EAAkC,wBAAwB,EAAE,MAAM,2BAA2B,CAAA;AAEzG,OAAO,KAAK,EAAE,kCAAkC,EAAE,MAAM,8BAA8B,CAAA;AAItF,YAAY,EAAE,kCAAkC,EAAE,MAAM,8BAA8B,CAAA;AACtF,OAAO,EAAE,4BAA4B,EAAE,MAAM,+BAA+B,CAAA;AAE5E,MAAM,WAAW,kCAAkC;IACjD,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAA;IACjC,QAAQ,CAAC,SAAS,EAAE,wBAAwB,CAAA;CAC7C;AAED,MAAM,WAAW,0CAA2C,SAAQ,IAAI,CAAC,gCAAgC,EAAE,YAAY,CAAC;IACtH,QAAQ,CAAC,UAAU,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;IACxC,QAAQ,CAAC,MAAM,EAAE,iBAAiB,CAAA;IAClC,QAAQ,CAAC,UAAU,EAAE,SAAS,kCAAkC,EAAE,CAAA;IAClE,QAAQ,CAAC,QAAQ,CAAC,EAAE,kCAAkC,CAAA;IACtD,QAAQ,CAAC,SAAS,EAAE,yBAAyB,CAAA;CAC9C;AA4GD,qFAAqF;AACrF,wBAAsB,mCAAmC,CAAC,OAAO,EAAE,0CAA0C,GAAG,OAAO,CAAC,mBAAmB,CAAC,CAuD3I"}
@@ -0,0 +1,10 @@
1
+ import type { DappEngineSystemReference, DappEngineSystemStatus, DappOperationRecoveryBinding, DappOperationRecoveryProfile, DappOperationRecoveryRequirement, DappOperationRecoverySnapshot } from '@xyo-network/dapp-kit';
2
+ import type { DappEngineApplicationPortOperation } from './engineApplicationPortHost.ts';
3
+ export interface DappEngineOperationRecoveryOptions {
4
+ readonly authorityScopeId: string;
5
+ readonly profile: DappOperationRecoveryProfile;
6
+ readonly readSnapshot: (requirement: Readonly<DappOperationRecoveryRequirement>, binding: Readonly<DappOperationRecoveryBinding>) => DappOperationRecoverySnapshot;
7
+ }
8
+ /** Capture a boot-selected profile; observation does not acquire or execute recovery work. */
9
+ export declare function bindEngineOperationRecovery(options: DappEngineOperationRecoveryOptions | undefined, operations: readonly DappEngineApplicationPortOperation[], reference: DappEngineSystemReference, target: DappEngineSystemStatus): ((portId: string, operationId: string) => void) | undefined;
10
+ //# sourceMappingURL=engineOperationRecovery.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"engineOperationRecovery.d.ts","sourceRoot":"","sources":["../../src/engineOperationRecovery.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,yBAAyB,EAAE,sBAAsB,EAAE,4BAA4B,EAAE,4BAA4B,EAAE,gCAAgC,EAAE,6BAA6B,EAC/K,MAAM,uBAAuB,CAAA;AAK9B,OAAO,KAAK,EAAE,kCAAkC,EAAE,MAAM,gCAAgC,CAAA;AAGxF,MAAM,WAAW,kCAAkC;IACjD,QAAQ,CAAC,gBAAgB,EAAE,MAAM,CAAA;IACjC,QAAQ,CAAC,OAAO,EAAE,4BAA4B,CAAA;IAC9C,QAAQ,CAAC,YAAY,EAAE,CAAC,WAAW,EAAE,QAAQ,CAAC,gCAAgC,CAAC,EAAE,OAAO,EAAE,QAAQ,CAAC,4BAA4B,CAAC,KAAK,6BAA6B,CAAA;CACnK;AAID,8FAA8F;AAC9F,wBAAgB,2BAA2B,CACzC,OAAO,EAAE,kCAAkC,GAAG,SAAS,EACvD,UAAU,EAAE,SAAS,kCAAkC,EAAE,EACzD,SAAS,EAAE,yBAAyB,EACpC,MAAM,EAAE,sBAAsB,GAC7B,CAAC,CAAC,MAAM,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,KAAK,IAAI,CAAC,GAAG,SAAS,CAgD7D"}
@@ -0,0 +1,5 @@
1
+ export declare class DappEnginePortAdmissionError extends Error {
2
+ readonly code: string;
3
+ constructor(code: string);
4
+ }
5
+ //# sourceMappingURL=enginePortAdmissionError.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"enginePortAdmissionError.d.ts","sourceRoot":"","sources":["../../src/enginePortAdmissionError.ts"],"names":[],"mappings":"AAAA,qBAAa,4BAA6B,SAAQ,KAAK;IACrD,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;gBAET,IAAI,EAAE,MAAM;CAKzB"}
@@ -2,5 +2,6 @@ export * from './applicationPortClient.ts';
2
2
  export * from './applicationPortHost.ts';
3
3
  export * from './applicationPortModel.ts';
4
4
  export * from './applicationPortWire.ts';
5
+ export * from './engineApplicationPortHost.ts';
5
6
  export * from './webSocketApplicationPortClient.ts';
6
7
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,4BAA4B,CAAA;AAC1C,cAAc,0BAA0B,CAAA;AACxC,cAAc,2BAA2B,CAAA;AACzC,cAAc,0BAA0B,CAAA;AACxC,cAAc,qCAAqC,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,4BAA4B,CAAA;AAC1C,cAAc,0BAA0B,CAAA;AACxC,cAAc,2BAA2B,CAAA;AACzC,cAAc,0BAA0B,CAAA;AACxC,cAAc,gCAAgC,CAAA;AAC9C,cAAc,qCAAqC,CAAA"}
@@ -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,233 @@ 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
+ return;
1114
+ }
1115
+ admitRecovery(portId, operationId);
1116
+ authorize();
1117
+ };
1118
+ if (operation.streaming) {
1119
+ const open = operation.open.bind(operation);
1120
+ const resume = operation.resume?.bind(operation);
1121
+ return Object.freeze({
1122
+ portId,
1123
+ operationId,
1124
+ streaming: true,
1125
+ open: (context) => {
1126
+ admit();
1127
+ return open(context);
1128
+ },
1129
+ ...resume === void 0 ? {} : {
1130
+ resume: (context) => {
1131
+ admit();
1132
+ return resume(context);
1133
+ }
1134
+ }
1135
+ });
1136
+ }
1137
+ const handle = operation.handle.bind(operation);
1138
+ return Object.freeze({
1139
+ portId,
1140
+ operationId,
1141
+ streaming: false,
1142
+ handle: (context) => {
1143
+ admit();
1144
+ return handle(context);
1145
+ }
1146
+ });
1147
+ });
1148
+ }
1149
+ async function createDappEngineApplicationPortHost(options) {
1150
+ const reference = DappEngineSystemReferenceZod.parse(options.reference);
1151
+ const grant = PortAttachmentGrantZod4.parse(options.grant);
1152
+ const readStatus = options.engine.status.bind(options.engine);
1153
+ const readTarget = () => target(readStatus, reference, grant);
1154
+ const selected = readTarget();
1155
+ const admitRecovery = bindEngineOperationRecovery(options.recovery, options.operations, reference, selected);
1156
+ const boundOperations = operations(options.operations, grant, readTarget, admitRecovery);
1157
+ const deferred = deferredEndpoint(options.endpoint);
1158
+ const mapDomainError = options.mapError.bind(options);
1159
+ const disconnect = options.disconnect.bind(options);
1160
+ const binding = options.engine.bindSystemWork(reference, () => {
1161
+ const host = createApplicationPortHost({
1162
+ authenticatedPrincipalId: options.authenticatedPrincipalId,
1163
+ endpoint: deferred.endpoint,
1164
+ grant,
1165
+ now: options.now,
1166
+ onProtocolError: options.onProtocolError,
1167
+ operations: boundOperations,
1168
+ mapError: (error) => error instanceof DappEnginePortAdmissionError ? { code: error.code, retriable: false } : mapDomainError(error)
1169
+ });
1170
+ let disconnected;
1171
+ return {
1172
+ close: () => {
1173
+ host.closeAdmission();
1174
+ disconnected = (async () => {
1175
+ await disconnect();
1176
+ })();
1177
+ void disconnected.catch(() => {
1178
+ });
1179
+ },
1180
+ drain: async () => {
1181
+ const outcomes = await Promise.allSettled([host.drain(), disconnected]);
1182
+ const errors = outcomes.filter((outcome) => outcome.status === "rejected").map((outcome) => outcome.reason);
1183
+ if (errors.length > 0) throw new AggregateError(errors, "Engine application-port drainage failed");
1184
+ }
1185
+ };
1186
+ });
1187
+ try {
1188
+ deferred.activate();
1189
+ readTarget();
1190
+ } catch (cause) {
1191
+ try {
1192
+ await binding.close();
1193
+ } catch (cleanup) {
1194
+ throw new AggregateError([cause, cleanup], "Engine application-port setup and cleanup failed", { cause: cleanup });
1195
+ }
1196
+ throw cause;
1197
+ }
1198
+ return Object.freeze({
1199
+ close: () => binding.close(),
1200
+ closeAdmission: () => {
1201
+ void binding.close();
1202
+ },
1203
+ drain: () => binding.close()
1204
+ });
1205
+ }
1206
+
926
1207
  // src/webSocketApplicationPortClient.ts
927
1208
  import { PortFrameZod as PortFrameZod3 } from "@xyo-network/dapp-kit";
928
1209
  var WebSocketApplicationPortClientErrorCode = {
@@ -1165,6 +1446,7 @@ export {
1165
1446
  ApplicationPortWireError,
1166
1447
  ApplicationPortWireErrorCode,
1167
1448
  ApplicationPortWireMessageZod,
1449
+ DappEnginePortAdmissionError,
1168
1450
  WebSocketApplicationPortClientError,
1169
1451
  WebSocketApplicationPortClientErrorCode,
1170
1452
  applicationPortErrorFrame,
@@ -1174,6 +1456,7 @@ export {
1174
1456
  connectWebSocketApplicationPort,
1175
1457
  createApplicationPortClient,
1176
1458
  createApplicationPortHost,
1459
+ createDappEngineApplicationPortHost,
1177
1460
  isApplicationPortFrame,
1178
1461
  parseApplicationPortFrame,
1179
1462
  parseApplicationPortWireText,