@vibecook/ghosttea-react 0.10.0 → 0.11.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.
Files changed (40) hide show
  1. package/README.md +50 -0
  2. package/dist/TerminalSurface.d.ts +5 -0
  3. package/dist/TerminalSurface.d.ts.map +1 -1
  4. package/dist/TerminalSurface.js +19 -4
  5. package/dist/TerminalSurface.js.map +1 -1
  6. package/dist/index.d.ts +4 -3
  7. package/dist/index.d.ts.map +1 -1
  8. package/dist/index.js +2 -1
  9. package/dist/index.js.map +1 -1
  10. package/dist/performance.d.ts +31 -0
  11. package/dist/performance.d.ts.map +1 -1
  12. package/dist/performance.js.map +1 -1
  13. package/dist/routed-activation.d.ts +110 -0
  14. package/dist/routed-activation.d.ts.map +1 -0
  15. package/dist/routed-activation.js +287 -0
  16. package/dist/routed-activation.js.map +1 -0
  17. package/dist/routed-control.d.ts +69 -0
  18. package/dist/routed-control.d.ts.map +1 -0
  19. package/dist/routed-control.js +383 -0
  20. package/dist/routed-control.js.map +1 -0
  21. package/dist/routed-frames.d.ts +69 -0
  22. package/dist/routed-frames.d.ts.map +1 -0
  23. package/dist/routed-frames.js +660 -0
  24. package/dist/routed-frames.js.map +1 -0
  25. package/dist/runtime.d.ts +73 -4
  26. package/dist/runtime.d.ts.map +1 -1
  27. package/dist/runtime.js +1034 -53
  28. package/dist/runtime.js.map +1 -1
  29. package/dist/terminal-render.worker.js +1089 -45
  30. package/dist/terminal-render.worker.js.map +3 -3
  31. package/dist/worker-messages.d.ts +18 -1
  32. package/dist/worker-messages.d.ts.map +1 -1
  33. package/dist/workspace/Workspace.d.ts +12 -1
  34. package/dist/workspace/Workspace.d.ts.map +1 -1
  35. package/dist/workspace/Workspace.js +77 -14
  36. package/dist/workspace/Workspace.js.map +1 -1
  37. package/dist/workspace/index.d.ts +1 -1
  38. package/dist/workspace/index.d.ts.map +1 -1
  39. package/dist/workspace/index.js.map +1 -1
  40. package/package.json +4 -4
@@ -2686,6 +2686,934 @@ function definitionCatalogFits(installed, definitions, maxDefinitions, retainedD
2686
2686
  return true;
2687
2687
  }
2688
2688
 
2689
+ // ../ghosttea-protocol/dist/routed.js
2690
+ var ROUTED_PROTOCOL_VERSION = { major: 1, minor: 0 };
2691
+ var DEFAULT_ROUTED_RECEIVER_CAPACITIES = {
2692
+ connectionCreditBytes: 8388608,
2693
+ perActivationCreditBytes: 2097152,
2694
+ stagingBytesPerSession: 16777216,
2695
+ stagingBytesTotal: 67108864,
2696
+ maxConcurrentActivations: 128,
2697
+ maxConcurrentSeeds: 4
2698
+ };
2699
+ var ROUTED_DOOR_LIMITS = {
2700
+ helloDeadlineMs: 5e3,
2701
+ preAuthMaxBytes: 65536,
2702
+ preAuthConnectionCap: 64,
2703
+ maxConnectionSets: 256,
2704
+ heartbeatIntervalMs: 5e3,
2705
+ heartbeatTtlMs: 15e3
2706
+ };
2707
+ var ROUTED_CLOSE_CODES = {
2708
+ GOING_AWAY: 1001,
2709
+ POLICY_PRE_AUTH: 1008,
2710
+ SERVER_ERROR: 1011,
2711
+ STALE_ROUTE: 4e3,
2712
+ FENCED: 4001,
2713
+ SUPERSEDED: 4002,
2714
+ PROTOCOL: 4003,
2715
+ LEG_TIMEOUT: 4004
2716
+ };
2717
+ function compareRoutedSceneContent(left, right) {
2718
+ if (left.sceneEpoch.cellBootId !== right.sceneEpoch.cellBootId || left.sceneEpoch.modelGeneration !== right.sceneEpoch.modelGeneration) {
2719
+ return null;
2720
+ }
2721
+ if (left.sceneRevision === right.sceneRevision)
2722
+ return 0;
2723
+ return left.sceneRevision < right.sceneRevision ? -1 : 1;
2724
+ }
2725
+ var ROUTED_MESSAGE_TYPES = [
2726
+ "ConnectionHello",
2727
+ "ConnectionAccepted",
2728
+ "ConnectionRefused",
2729
+ "LegHeartbeat",
2730
+ "LegHeartbeatAck",
2731
+ "AttachControlLeg",
2732
+ "ControlLegAttached",
2733
+ "AttachRefused",
2734
+ "DeclareDemand",
2735
+ "DemandAccepted",
2736
+ "CellActivationStatus",
2737
+ "ClaimGeometry",
2738
+ "ReleaseGeometry",
2739
+ "TransferGeometry",
2740
+ "GeometryCommitted",
2741
+ "GeometryRefused",
2742
+ "AttachFramesLeg",
2743
+ "FramesLegAttached",
2744
+ "TransportCredit",
2745
+ "SceneApplied",
2746
+ "CalibrationPing"
2747
+ ];
2748
+ var ROUTED_LEG_OUTBOUND = {
2749
+ control: [
2750
+ "ConnectionAccepted",
2751
+ "ConnectionRefused",
2752
+ "LegHeartbeatAck",
2753
+ "ControlLegAttached",
2754
+ "AttachRefused",
2755
+ "DemandAccepted",
2756
+ "CellActivationStatus",
2757
+ "GeometryCommitted",
2758
+ "GeometryRefused"
2759
+ ],
2760
+ frames: ["ConnectionAccepted", "ConnectionRefused", "LegHeartbeatAck", "FramesLegAttached", "AttachRefused"]
2761
+ };
2762
+ var routedMessageTypeSet = new Set(ROUTED_MESSAGE_TYPES);
2763
+ function isRecord(value) {
2764
+ return typeof value === "object" && value !== null && !Array.isArray(value);
2765
+ }
2766
+ function isCounter(value) {
2767
+ return Number.isSafeInteger(value) && Number(value) >= 0;
2768
+ }
2769
+ function isPositiveInteger(value) {
2770
+ return Number.isSafeInteger(value) && Number(value) > 0;
2771
+ }
2772
+ function isNonEmptyString(value) {
2773
+ return typeof value === "string" && value.length > 0;
2774
+ }
2775
+ function isStringArray(value) {
2776
+ return Array.isArray(value) && value.every((item) => typeof item === "string");
2777
+ }
2778
+ function isSortedUniqueStringArray(value, allowed) {
2779
+ return isStringArray(value) && value.every((item, index) => (allowed === void 0 || allowed.has(item)) && (index === 0 || String(value[index - 1]) < item));
2780
+ }
2781
+ function hasOptionalCounter(record, key) {
2782
+ return record[key] === void 0 || isCounter(record[key]);
2783
+ }
2784
+ function hasOptionalString(record, key) {
2785
+ return record[key] === void 0 || typeof record[key] === "string";
2786
+ }
2787
+ function isStamp(value) {
2788
+ if (!isRecord(value) || !isRecord(value.sceneEpoch))
2789
+ return false;
2790
+ return isNonEmptyString(value.sceneEpoch.cellBootId) && isCounter(value.sceneEpoch.modelGeneration) && isCounter(value.sceneRevision);
2791
+ }
2792
+ var transportChannels = /* @__PURE__ */ new Set(["control", "frames"]);
2793
+ var sessionRights = /* @__PURE__ */ new Set(["geometry", "geometryAdmin", "input", "read"]);
2794
+ function isGrantProtectedHeader(value, type) {
2795
+ return isRecord(value) && value.v === 1 && value.typ === type && value.iss === "fieldd" && value.alg === "HS256" && isRecord(value.kid) && isNonEmptyString(value.kid.cellBootId) && isCounter(value.kid.keyGeneration);
2796
+ }
2797
+ function isRoutedCellTransportGrant(value) {
2798
+ if (!isRecord(value) || !isGrantProtectedHeader(value.protected, "CellTransportGrant"))
2799
+ return false;
2800
+ const claims = value.claims;
2801
+ return isRecord(claims) && isNonEmptyString(claims.audienceCellBootId) && isNonEmptyString(claims.clientId) && isNonEmptyString(claims.connectionSetId) && isSortedUniqueStringArray(claims.allowedChannels, transportChannels) && claims.allowedChannels.length > 0 && isCounter(claims.transportGrantGeneration) && isCounter(claims.issuedAt) && isCounter(claims.expiresAt) && isNonEmptyString(claims.nonce) && isNonEmptyString(value.mac);
2802
+ }
2803
+ function isRoutedSessionAttachGrant(value) {
2804
+ if (!isRecord(value) || !isGrantProtectedHeader(value.protected, "SessionAttachGrant"))
2805
+ return false;
2806
+ const claims = value.claims;
2807
+ return isRecord(claims) && isNonEmptyString(claims.audienceCellBootId) && isNonEmptyString(claims.clientId) && isNonEmptyString(claims.sessionId) && hasOptionalCounter(claims, "leaseEpoch") && isCounter(claims.routeRevision) && isCounter(claims.grantGeneration) && isSortedUniqueStringArray(claims.rights, sessionRights) && isCounter(claims.issuedAt) && isCounter(claims.expiresAt) && isNonEmptyString(value.mac);
2808
+ }
2809
+ function isRoutedReceiverCapacities(value) {
2810
+ return isRecord(value) && isCounter(value.connectionCreditBytes) && isCounter(value.perActivationCreditBytes) && isCounter(value.stagingBytesPerSession) && isCounter(value.stagingBytesTotal) && isCounter(value.maxConcurrentActivations) && isCounter(value.maxConcurrentSeeds);
2811
+ }
2812
+ function isRoutedProtocolLimits(value) {
2813
+ return isRecord(value) && isCounter(value.maxControlMessageBytes) && isCounter(value.maxPresentationChunkBytes) && isCounter(value.maxBatchLatencyMs) && isCounter(value.maxCreditReturnDelayMs) && isCounter(value.maxSceneAppliedDelayMs) && isCounter(value.sceneAppliedRefreshMs) && isCounter(value.presentationStatusRefreshMs) && isCounter(value.activationAttachDeadlineMs) && isCounter(value.maxActivationCatchupMs) && isCounter(value.maxCatchupBytes) && isCounter(value.creditAccountDrainTtlMs) && isCounter(value.urgentReserveBytes) && isCounter(value.maxBulkBytesAdmittedAhead) && isCounter(value.maxUrgentPresentationUnitBytes);
2814
+ }
2815
+ function isSourceDemand(value) {
2816
+ return isRecord(value) && (value.mode === "none" || value.mode === "snapshot" || value.mode === "live") && (value.cadenceClass === void 0 || value.cadenceClass === "low" || value.cadenceClass === "normal" || value.cadenceClass === "high") && (value.urgency === void 0 || value.urgency === "background" || value.urgency === "normal" || value.urgency === "urgent");
2817
+ }
2818
+ function isTrfIdentity(value) {
2819
+ return isRecord(value) && typeof value.sessionHandle === "string" && /^(0|[1-9][0-9]*)$/.test(value.sessionHandle) && typeof value.viewHandle === "string" && /^(0|[1-9][0-9]*)$/.test(value.viewHandle);
2820
+ }
2821
+ function isFramesAttachOutcome(value) {
2822
+ if (!isRecord(value) || value.kind !== "resume-accepted" && value.kind !== "seed-required")
2823
+ return false;
2824
+ if (!hasOptionalString(value, "reason"))
2825
+ return false;
2826
+ if (value.kind === "resume-accepted")
2827
+ return isStamp(value.from) && isStamp(value.newestAvailable);
2828
+ return (value.from === void 0 || isStamp(value.from)) && (value.newestAvailable === void 0 || isStamp(value.newestAvailable));
2829
+ }
2830
+ function isGeometryClaimant(value) {
2831
+ return isRecord(value) && isNonEmptyString(value.clientId) && isNonEmptyString(value.viewId);
2832
+ }
2833
+ function isGeometryHolder(value) {
2834
+ return isGeometryClaimant(value) && isCounter(value.holderGeneration);
2835
+ }
2836
+ function hasActivation(record) {
2837
+ return isNonEmptyString(record.activationId);
2838
+ }
2839
+ function validateRoutedBody(type, body) {
2840
+ switch (type) {
2841
+ case "ConnectionHello":
2842
+ return isCounter(body.protocolMajor) && isCounter(body.protocolMinor) && (body.channel === "control" || body.channel === "frames") && isRoutedCellTransportGrant(body.transportGrant) && (body.receiverCapacities === void 0 || isRoutedReceiverCapacities(body.receiverCapacities)) && isStringArray(body.capabilities);
2843
+ case "ConnectionAccepted":
2844
+ return isRecord(body.selectedProtocolVersion) && isCounter(body.selectedProtocolVersion.major) && isCounter(body.selectedProtocolVersion.minor) && isNonEmptyString(body.connectionSetId) && (body.channel === "control" || body.channel === "frames") && isCounter(body.legGeneration) && isCounter(body.heartbeatTtlMs) && hasOptionalCounter(body, "creditEpoch") && (body.initialWindows === void 0 || isRoutedReceiverCapacities(body.initialWindows)) && isRoutedProtocolLimits(body.protocolLimits) && isStringArray(body.capabilities);
2845
+ case "ConnectionRefused":
2846
+ return typeof body.code === "string" && typeof body.retryable === "boolean";
2847
+ case "LegHeartbeat":
2848
+ return isNonEmptyString(body.connectionSetId) && (body.channel === "control" || body.channel === "frames") && isCounter(body.legGeneration) && isCounter(body.sequence);
2849
+ case "LegHeartbeatAck":
2850
+ return isCounter(body.sequence);
2851
+ case "AttachControlLeg":
2852
+ return hasActivation(body) && isRoutedSessionAttachGrant(body.attachGrant) && (body.replacesActivationId === void 0 || isNonEmptyString(body.replacesActivationId)) && isSourceDemand(body.initialDemand);
2853
+ case "ControlLegAttached":
2854
+ return isNonEmptyString(body.sessionId) && hasActivation(body) && isCounter(body.grantGenerationAccepted) && isSortedUniqueStringArray(body.rights, sessionRights);
2855
+ case "AttachRefused":
2856
+ return (body.activationId === void 0 || isNonEmptyString(body.activationId)) && typeof body.code === "string" && typeof body.retryable === "boolean";
2857
+ case "DeclareDemand":
2858
+ return isNonEmptyString(body.sessionId) && hasActivation(body) && isCounter(body.leaseEpoch) && isCounter(body.demandSequence) && isSourceDemand(body.demand);
2859
+ case "DemandAccepted":
2860
+ return isCounter(body.demandSequence);
2861
+ case "CellActivationStatus":
2862
+ if (!isNonEmptyString(body.sessionId) || !hasActivation(body) || !isCounter(body.cellStatusSequence) || !isCounter(body.leaseTtlMs) || body.acceptedContent !== void 0 && !isStamp(body.acceptedContent) || !isRecord(body.presentation) || !["presenting", "stopped", "revoked"].includes(String(body.presentation.state)) || !hasOptionalString(body.presentation, "reason") || !isRecord(body.input) || !["allowed", "suspended", "revoked"].includes(String(body.input.state)) || !hasOptionalString(body.input, "reason")) {
2863
+ return false;
2864
+ }
2865
+ return body.input.state !== "allowed" || body.presentation.state === "presenting";
2866
+ case "ClaimGeometry":
2867
+ return isNonEmptyString(body.sessionId) && hasActivation(body) && isCounter(body.leaseEpoch) && isGeometryClaimant(body.claimant) && isPositiveInteger(body.cols) && isPositiveInteger(body.rows) && isCounter(body.expectRevision);
2868
+ case "ReleaseGeometry":
2869
+ return isNonEmptyString(body.sessionId) && hasActivation(body) && isCounter(body.leaseEpoch) && isGeometryHolder(body.holder);
2870
+ case "TransferGeometry":
2871
+ return isNonEmptyString(body.sessionId) && hasActivation(body) && isCounter(body.leaseEpoch) && isGeometryHolder(body.from) && isGeometryClaimant(body.to) && isCounter(body.expectRevision) && isPositiveInteger(body.cols) && isPositiveInteger(body.rows);
2872
+ case "GeometryCommitted":
2873
+ return isGeometryHolder(body.holder) && isCounter(body.geometryRevision) && isPositiveInteger(body.cols) && isPositiveInteger(body.rows);
2874
+ case "GeometryRefused":
2875
+ return typeof body.code === "string" && (body.currentHolder === void 0 || isGeometryHolder(body.currentHolder)) && hasOptionalCounter(body, "geometryRevision");
2876
+ case "AttachFramesLeg":
2877
+ return hasActivation(body) && isRoutedSessionAttachGrant(body.attachGrant) && (body.resume === void 0 || isRecord(body.resume) && isNonEmptyString(body.resume.resumeToken) && isStamp(body.resume.from));
2878
+ case "FramesLegAttached":
2879
+ return isNonEmptyString(body.sessionId) && hasActivation(body) && isNonEmptyString(body.resumeToken) && isTrfIdentity(body.trfIdentity) && isFramesAttachOutcome(body.outcome);
2880
+ case "TransportCredit":
2881
+ return isCounter(body.creditEpoch) && isCounter(body.creditSequence) && isCounter(body.connectionBytesReturned) && Array.isArray(body.accounts) && body.accounts.every((account) => isRecord(account) && isNonEmptyString(account.activationId) && isCounter(account.bytesReturned));
2882
+ case "SceneApplied":
2883
+ return isNonEmptyString(body.sessionId) && hasActivation(body) && isCounter(body.leaseEpoch) && isStamp(body.appliedContent);
2884
+ case "CalibrationPing":
2885
+ return isCounter(body.sequence) && typeof body.t0 === "number" && Number.isFinite(body.t0);
2886
+ }
2887
+ }
2888
+ function decodeRoutedMessage(raw, allowed = ROUTED_MESSAGE_TYPES) {
2889
+ let value = raw;
2890
+ if (typeof raw === "string") {
2891
+ try {
2892
+ value = JSON.parse(raw);
2893
+ } catch {
2894
+ return { ok: false, error: "not-json" };
2895
+ }
2896
+ }
2897
+ if (!isRecord(value))
2898
+ return { ok: false, error: "not-an-object" };
2899
+ if (typeof value.type !== "string")
2900
+ return { ok: false, error: "missing-type" };
2901
+ if (!routedMessageTypeSet.has(value.type))
2902
+ return { ok: false, error: "unknown-type" };
2903
+ const type = value.type;
2904
+ if (!allowed.includes(type))
2905
+ return { ok: false, error: "not-allowed-here" };
2906
+ const body = { ...value };
2907
+ delete body.type;
2908
+ if (!validateRoutedBody(type, body))
2909
+ return { ok: false, error: "invalid" };
2910
+ return { ok: true, message: value };
2911
+ }
2912
+ function encodeRoutedMessage(type, body) {
2913
+ if (!isRecord(body) || Object.hasOwn(body, "type") || !validateRoutedBody(type, body)) {
2914
+ throw new TypeError(`invalid routed ${type} message`);
2915
+ }
2916
+ return JSON.stringify({ type, ...body });
2917
+ }
2918
+ var ROUTED_PRESENTATION_ENVELOPE_MAGIC = [84, 80];
2919
+ var ROUTED_PRESENTATION_ENVELOPE_VERSION = 1;
2920
+ var ROUTED_PRESENTATION_ENVELOPE_PREFIX_BYTES = 8;
2921
+ var ROUTED_PRESENTATION_ENVELOPE_MAX_HEADER_BYTES = 4096;
2922
+ var routedUtf8Encoder = new TextEncoder();
2923
+ var routedUtf8Decoder = new TextDecoder("utf-8", { fatal: true });
2924
+ function isFiniteNumber(value) {
2925
+ return typeof value === "number" && Number.isFinite(value);
2926
+ }
2927
+ function isTransferLayout(value) {
2928
+ return isRecord(value) && isPositiveInteger(value.cols) && isPositiveInteger(value.rows) && isCounter(value.scrollbackRows);
2929
+ }
2930
+ function isTransferChecksum(value) {
2931
+ return isRecord(value) && value.alg === "crc32c" && isCounter(value.value);
2932
+ }
2933
+ function isCalibrationEcho(value) {
2934
+ return isRecord(value) && isCounter(value.sequence) && isFiniteNumber(value.t0) && isFiniteNumber(value.t1) && isFiniteNumber(value.t2);
2935
+ }
2936
+ function isProfilingEnvelope(value) {
2937
+ return isRecord(value) && isFiniteNumber(value.damageFirstTs) && isFiniteNumber(value.damageLastTs) && isFiniteNumber(value.encodeTs) && (value.probeId === void 0 || typeof value.probeId === "string");
2938
+ }
2939
+ function validateEnvelopeHeader(value) {
2940
+ if (!isRecord(value))
2941
+ return false;
2942
+ if (!isCounter(value.creditEpoch) || !isCounter(value.activationSequence) || !isNonEmptyString(value.sessionId) || !isNonEmptyString(value.activationId) || !isCounter(value.leaseEpoch) || !["trf1-frame", "transfer-begin", "transfer-chunk", "transfer-end", "calibration"].includes(String(value.kind))) {
2943
+ return false;
2944
+ }
2945
+ const base = value.baseContent;
2946
+ const result = value.resultContent;
2947
+ if (base !== void 0 && base !== null && !isStamp(base))
2948
+ return false;
2949
+ if (result !== void 0 && !isStamp(result))
2950
+ return false;
2951
+ if (value.profiling !== void 0 && !isProfilingEnvelope(value.profiling))
2952
+ return false;
2953
+ if (isStamp(base) && isStamp(result) && compareRoutedSceneContent(base, result) === null)
2954
+ return false;
2955
+ switch (value.kind) {
2956
+ case "trf1-frame":
2957
+ return Object.hasOwn(value, "baseContent") && isStamp(result);
2958
+ case "transfer-begin": {
2959
+ if (!isRecord(value.transfer) || !isNonEmptyString(value.transfer.transferId))
2960
+ return false;
2961
+ if (value.transfer.kind !== "seed" && value.transfer.kind !== "catchup")
2962
+ return false;
2963
+ if (!isCounter(value.transfer.totalBytes) || !isCounter(value.transfer.chunkCount))
2964
+ return false;
2965
+ if (!isTransferLayout(value.transfer.targetLayout) || !isTransferChecksum(value.transfer.checksum) || !isStamp(result)) {
2966
+ return false;
2967
+ }
2968
+ if (value.transfer.kind === "seed")
2969
+ return base === null;
2970
+ return isStamp(base);
2971
+ }
2972
+ case "transfer-chunk":
2973
+ return isRecord(value.transfer) && isNonEmptyString(value.transfer.transferId) && isCounter(value.transfer.chunkIndex) && isCounter(value.transfer.byteOffset);
2974
+ case "transfer-end":
2975
+ return isRecord(value.transfer) && isNonEmptyString(value.transfer.transferId);
2976
+ case "calibration":
2977
+ return isCalibrationEcho(value.calibration);
2978
+ default:
2979
+ return false;
2980
+ }
2981
+ }
2982
+ function decodeRoutedPresentationEnvelope(bytes) {
2983
+ if (bytes.byteLength < ROUTED_PRESENTATION_ENVELOPE_PREFIX_BYTES)
2984
+ return { ok: false, error: "short" };
2985
+ if (bytes[0] !== ROUTED_PRESENTATION_ENVELOPE_MAGIC[0] || bytes[1] !== ROUTED_PRESENTATION_ENVELOPE_MAGIC[1]) {
2986
+ return { ok: false, error: "bad-magic" };
2987
+ }
2988
+ if (bytes[2] !== ROUTED_PRESENTATION_ENVELOPE_VERSION)
2989
+ return { ok: false, error: "bad-version" };
2990
+ if (bytes[3] !== 0)
2991
+ return { ok: false, error: "bad-reserved" };
2992
+ const headerLength = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength).getUint32(4, false);
2993
+ if (headerLength > ROUTED_PRESENTATION_ENVELOPE_MAX_HEADER_BYTES) {
2994
+ return { ok: false, error: "header-too-large" };
2995
+ }
2996
+ const headerEnd = ROUTED_PRESENTATION_ENVELOPE_PREFIX_BYTES + headerLength;
2997
+ if (headerEnd > bytes.byteLength)
2998
+ return { ok: false, error: "header-truncated" };
2999
+ let text;
3000
+ try {
3001
+ text = routedUtf8Decoder.decode(bytes.subarray(ROUTED_PRESENTATION_ENVELOPE_PREFIX_BYTES, headerEnd));
3002
+ } catch {
3003
+ return { ok: false, error: "header-not-utf8" };
3004
+ }
3005
+ let header;
3006
+ try {
3007
+ header = JSON.parse(text);
3008
+ } catch {
3009
+ return { ok: false, error: "header-not-json" };
3010
+ }
3011
+ if (!validateEnvelopeHeader(header))
3012
+ return { ok: false, error: "header-invalid" };
3013
+ return {
3014
+ ok: true,
3015
+ envelope: { header, payload: bytes.subarray(headerEnd) },
3016
+ chargedBytes: bytes.byteLength
3017
+ };
3018
+ }
3019
+ function routedCrc32c(bytes, seed = 0) {
3020
+ let crc = (seed ^ 4294967295) >>> 0;
3021
+ for (const byte of bytes) {
3022
+ crc ^= byte;
3023
+ for (let bit = 0; bit < 8; bit += 1) {
3024
+ crc = (crc >>> 1 ^ 2197175160 & -(crc & 1)) >>> 0;
3025
+ }
3026
+ }
3027
+ return (crc ^ 4294967295) >>> 0;
3028
+ }
3029
+
3030
+ // src/routed-frames.ts
3031
+ var socketOpen = 1;
3032
+ var socketClosing = 2;
3033
+ function exactStamp(left, right) {
3034
+ return left !== void 0 && compareRoutedSceneContent(left, right) === 0;
3035
+ }
3036
+ function copyArrayBuffer(bytes) {
3037
+ const copy = bytes.slice();
3038
+ return copy.buffer;
3039
+ }
3040
+ function capacitiesFitWithin(accepted, advertised) {
3041
+ return accepted.connectionCreditBytes <= advertised.connectionCreditBytes && accepted.perActivationCreditBytes <= advertised.perActivationCreditBytes && accepted.stagingBytesPerSession <= advertised.stagingBytesPerSession && accepted.stagingBytesTotal <= advertised.stagingBytesTotal && accepted.maxConcurrentActivations <= advertised.maxConcurrentActivations && accepted.maxConcurrentSeeds <= advertised.maxConcurrentSeeds;
3042
+ }
3043
+ var RoutedFramesTransport = class {
3044
+ #socketFactory;
3045
+ #applyFrame;
3046
+ #emit;
3047
+ #creditReturned;
3048
+ #connections = /* @__PURE__ */ new Map();
3049
+ #activations = /* @__PURE__ */ new Map();
3050
+ #stagingBytes = 0;
3051
+ #disposed = false;
3052
+ constructor(options) {
3053
+ this.#socketFactory = options.socketFactory ?? ((url) => new WebSocket(url));
3054
+ this.#applyFrame = options.applyFrame;
3055
+ this.#emit = options.emit;
3056
+ this.#creditReturned = options.creditReturned;
3057
+ }
3058
+ attach(request) {
3059
+ if (this.#disposed) return;
3060
+ const claims = request.attachGrant.claims;
3061
+ if (claims.sessionId.length === 0 || request.activationId.length === 0 || !/^(0|[1-9][0-9]*)$/.test(request.sessionHandle) || claims.audienceCellBootId !== request.cellBootId || request.transportGrant.claims.audienceCellBootId !== request.cellBootId) {
3062
+ throw new Error("Routed frames attach grant does not match the requested cell");
3063
+ }
3064
+ const previous = this.#activations.get(request.activationId);
3065
+ if (previous?.appliedContent !== void 0 && request.resume !== void 0 && !exactStamp(previous.appliedContent, request.resume.from)) {
3066
+ throw new Error("Routed frames resume does not name the worker's applied scene");
3067
+ }
3068
+ const appliedContent = request.resume?.from ?? previous?.appliedContent;
3069
+ const lastActivationSequence = previous?.lastActivationSequence ?? -1;
3070
+ const workerStatusSequence = previous?.workerStatusSequence ?? 0;
3071
+ if (previous) this.detach(request.activationId);
3072
+ const activation = {
3073
+ request,
3074
+ sessionId: claims.sessionId,
3075
+ leaseEpoch: claims.leaseEpoch ?? 0,
3076
+ ...appliedContent === void 0 ? {} : { appliedContent },
3077
+ lastActivationSequence,
3078
+ workerStatusSequence
3079
+ };
3080
+ this.#activations.set(request.activationId, activation);
3081
+ this.#emit({ type: "frames-state", activationId: request.activationId, state: "attaching" });
3082
+ const connection = this.#connection(request);
3083
+ connection.activationIds.add(request.activationId);
3084
+ if (connection.accepted) this.#sendAttach(connection, activation);
3085
+ }
3086
+ detach(activationId) {
3087
+ const activation = this.#activations.get(activationId);
3088
+ if (!activation) return;
3089
+ this.#releaseTransfer(activation);
3090
+ if (activation.sceneRefreshTimer !== void 0) clearTimeout(activation.sceneRefreshTimer);
3091
+ if (activation.presentationRefreshTimer !== void 0) clearTimeout(activation.presentationRefreshTimer);
3092
+ this.#activations.delete(activationId);
3093
+ const connection = this.#connections.get(activation.request.cellBootId);
3094
+ if (connection) {
3095
+ connection.activationIds.delete(activationId);
3096
+ this.#scheduleAccountDrain(connection, activationId);
3097
+ }
3098
+ }
3099
+ dispose() {
3100
+ if (this.#disposed) return;
3101
+ this.#disposed = true;
3102
+ for (const activationId of [...this.#activations.keys()]) this.detach(activationId);
3103
+ for (const connection of this.#connections.values()) {
3104
+ if (connection.heartbeatTimer !== void 0) clearTimeout(connection.heartbeatTimer);
3105
+ if (connection.heartbeatDeadlineTimer !== void 0) clearTimeout(connection.heartbeatDeadlineTimer);
3106
+ if (connection.creditTimer !== void 0) clearTimeout(connection.creditTimer);
3107
+ for (const timer of connection.accountDrainTimers.values()) clearTimeout(timer);
3108
+ if (connection.socket.readyState < socketClosing) connection.socket.close(1e3, "runtime-destroyed");
3109
+ }
3110
+ this.#connections.clear();
3111
+ }
3112
+ #connection(request) {
3113
+ const existing = this.#connections.get(request.cellBootId);
3114
+ if (existing && existing.framesUrl === request.framesUrl && existing.transportGrant.claims.connectionSetId === request.transportGrant.claims.connectionSetId && existing.socket.readyState < socketClosing) {
3115
+ return existing;
3116
+ }
3117
+ if (existing) this.#closeConnection(existing, 1e3, "connection-replaced", true);
3118
+ const socket = this.#socketFactory(request.framesUrl);
3119
+ socket.binaryType = "arraybuffer";
3120
+ const connection = {
3121
+ cellBootId: request.cellBootId,
3122
+ framesUrl: request.framesUrl,
3123
+ transportGrant: request.transportGrant,
3124
+ receiverCapacities: request.receiverCapacities ?? DEFAULT_ROUTED_RECEIVER_CAPACITIES,
3125
+ capabilities: request.capabilities ?? ["resume"],
3126
+ socket,
3127
+ activationIds: /* @__PURE__ */ new Set(),
3128
+ heartbeatSequence: 0,
3129
+ heartbeatAckSequence: 0,
3130
+ creditSequence: 0,
3131
+ connectionBytesReturned: 0,
3132
+ connectionBytesReported: 0,
3133
+ accountBytesReturned: /* @__PURE__ */ new Map(),
3134
+ accountDrainTimers: /* @__PURE__ */ new Map()
3135
+ };
3136
+ this.#connections.set(request.cellBootId, connection);
3137
+ socket.addEventListener("open", () => this.#hello(connection));
3138
+ socket.addEventListener("message", (event) => void this.#message(connection, event));
3139
+ socket.addEventListener("close", (event) => this.#closed(connection, event.code, event.reason));
3140
+ socket.addEventListener("error", () => {
3141
+ });
3142
+ return connection;
3143
+ }
3144
+ #hello(connection) {
3145
+ this.#send(
3146
+ connection,
3147
+ encodeRoutedMessage("ConnectionHello", {
3148
+ protocolMajor: ROUTED_PROTOCOL_VERSION.major,
3149
+ protocolMinor: ROUTED_PROTOCOL_VERSION.minor,
3150
+ channel: "frames",
3151
+ transportGrant: connection.transportGrant,
3152
+ receiverCapacities: connection.receiverCapacities,
3153
+ capabilities: connection.capabilities
3154
+ })
3155
+ );
3156
+ }
3157
+ async #message(connection, event) {
3158
+ if (this.#connections.get(connection.cellBootId) !== connection) return;
3159
+ if (typeof event.data === "string") {
3160
+ this.#text(connection, event.data);
3161
+ return;
3162
+ }
3163
+ let bytes;
3164
+ if (event.data instanceof ArrayBuffer) bytes = new Uint8Array(event.data);
3165
+ else if (ArrayBuffer.isView(event.data)) {
3166
+ bytes = new Uint8Array(event.data.buffer, event.data.byteOffset, event.data.byteLength);
3167
+ } else if (event.data instanceof Blob) {
3168
+ bytes = new Uint8Array(await event.data.arrayBuffer());
3169
+ } else {
3170
+ this.#protocolFailure(connection, void 0, 0, "unsupported binary message");
3171
+ return;
3172
+ }
3173
+ this.#binary(connection, bytes);
3174
+ }
3175
+ #text(connection, text) {
3176
+ const maxBytes = connection.accepted?.protocolLimits.maxControlMessageBytes ?? 262144;
3177
+ if (new TextEncoder().encode(text).byteLength > maxBytes) {
3178
+ this.#protocolFailure(connection, void 0, 0, "frames control message too large");
3179
+ return;
3180
+ }
3181
+ const decoded = decodeRoutedMessage(text, ROUTED_LEG_OUTBOUND.frames);
3182
+ if (!decoded.ok) {
3183
+ this.#protocolFailure(connection, void 0, 0, `invalid frames message: ${decoded.error}`);
3184
+ return;
3185
+ }
3186
+ const message = decoded.message;
3187
+ if (message.type === "ConnectionAccepted") {
3188
+ if (connection.accepted || message.channel !== "frames" || message.connectionSetId !== connection.transportGrant.claims.connectionSetId || message.selectedProtocolVersion.major !== ROUTED_PROTOCOL_VERSION.major || message.selectedProtocolVersion.minor > ROUTED_PROTOCOL_VERSION.minor || message.capabilities.some((capability) => !connection.capabilities.includes(capability)) || message.creditEpoch === void 0 || message.initialWindows === void 0 || !capacitiesFitWithin(message.initialWindows, connection.receiverCapacities)) {
3189
+ this.#protocolFailure(connection, void 0, 0, "invalid frames acceptance");
3190
+ return;
3191
+ }
3192
+ connection.accepted = message;
3193
+ this.#armHeartbeatDeadline(connection);
3194
+ this.#scheduleHeartbeat(connection);
3195
+ for (const activationId of connection.activationIds) {
3196
+ const activation = this.#activations.get(activationId);
3197
+ if (activation) this.#sendAttach(connection, activation);
3198
+ }
3199
+ return;
3200
+ }
3201
+ if (message.type === "ConnectionRefused") {
3202
+ connection.refusal = message;
3203
+ this.#closeConnection(connection, 1e3, message.code, true);
3204
+ return;
3205
+ }
3206
+ if (!connection.accepted) {
3207
+ this.#protocolFailure(connection, void 0, 0, "message before ConnectionAccepted");
3208
+ return;
3209
+ }
3210
+ if (message.type === "LegHeartbeatAck") {
3211
+ if (message.sequence > connection.heartbeatSequence) {
3212
+ this.#protocolFailure(connection, void 0, 0, "heartbeat ack is ahead");
3213
+ return;
3214
+ }
3215
+ if (message.sequence > connection.heartbeatAckSequence) {
3216
+ connection.heartbeatAckSequence = message.sequence;
3217
+ this.#armHeartbeatDeadline(connection);
3218
+ }
3219
+ return;
3220
+ }
3221
+ if (message.type === "AttachRefused") {
3222
+ if (message.activationId !== void 0 && !connection.activationIds.has(message.activationId)) {
3223
+ if (!this.#activations.has(message.activationId)) return;
3224
+ this.#protocolFailure(connection, void 0, 0, "attach refusal binding mismatch");
3225
+ return;
3226
+ }
3227
+ this.#emit({
3228
+ type: "attach-refused",
3229
+ ...message.activationId === void 0 ? {} : { activationId: message.activationId },
3230
+ code: message.code,
3231
+ retryable: message.retryable
3232
+ });
3233
+ return;
3234
+ }
3235
+ if (message.type === "FramesLegAttached") {
3236
+ const activation = this.#activations.get(message.activationId);
3237
+ if (!connection.activationIds.has(message.activationId) && !activation) return;
3238
+ const duplicateIdentity = [...connection.activationIds].some((activationId) => {
3239
+ if (activationId === message.activationId) return false;
3240
+ const identity = this.#activations.get(activationId)?.identity;
3241
+ return identity?.sessionHandle === message.trfIdentity.sessionHandle && identity.viewHandle === message.trfIdentity.viewHandle;
3242
+ });
3243
+ if (!activation || !connection.activationIds.has(message.activationId) || message.sessionId !== activation.sessionId || message.trfIdentity.sessionHandle !== activation.request.sessionHandle || duplicateIdentity || message.outcome.kind === "resume-accepted" && (!connection.accepted.capabilities.includes("resume") || activation.request.resume === void 0 || message.outcome.from === void 0 || !exactStamp(activation.request.resume.from, message.outcome.from))) {
3244
+ this.#protocolFailure(connection, message.activationId, 0, "frames attach identity mismatch");
3245
+ return;
3246
+ }
3247
+ activation.identity = message.trfIdentity;
3248
+ activation.resumeToken = message.resumeToken;
3249
+ this.#emit({ type: "frames-attached", attached: message });
3250
+ this.#emit({
3251
+ type: "frames-state",
3252
+ activationId: message.activationId,
3253
+ state: message.outcome.kind === "resume-accepted" ? "resuming" : "seeding",
3254
+ resumeToken: message.resumeToken
3255
+ });
3256
+ this.#reportPresentation(
3257
+ connection,
3258
+ activation,
3259
+ message.outcome.kind === "resume-accepted" ? "recovering" : "seeding"
3260
+ );
3261
+ return;
3262
+ }
3263
+ this.#protocolFailure(connection, void 0, 0, `unexpected frames message ${message.type}`);
3264
+ }
3265
+ #sendAttach(connection, activation) {
3266
+ const resume = connection.accepted?.capabilities.includes("resume") ? activation.request.resume : void 0;
3267
+ const body = {
3268
+ activationId: activation.request.activationId,
3269
+ attachGrant: activation.request.attachGrant,
3270
+ ...resume === void 0 ? {} : { resume }
3271
+ };
3272
+ this.#send(connection, encodeRoutedMessage("AttachFramesLeg", body));
3273
+ }
3274
+ #binary(connection, bytes) {
3275
+ if (!connection.accepted) {
3276
+ this.#protocolFailure(connection, void 0, bytes.byteLength, "binary message before acceptance");
3277
+ return;
3278
+ }
3279
+ const decoded = decodeRoutedPresentationEnvelope(bytes);
3280
+ if (!decoded.ok) {
3281
+ this.#protocolFailure(connection, void 0, bytes.byteLength, `bad envelope: ${decoded.error}`);
3282
+ return;
3283
+ }
3284
+ const { header, payload } = decoded.envelope;
3285
+ if (header.creditEpoch !== connection.accepted.creditEpoch) {
3286
+ this.#protocolFailure(connection, header.activationId, decoded.chargedBytes, "credit epoch mismatch");
3287
+ return;
3288
+ }
3289
+ if (header.profiling !== void 0 && !connection.accepted.capabilities.includes("profiling-envelope")) {
3290
+ this.#protocolFailure(connection, header.activationId, decoded.chargedBytes, "unnegotiated profiling envelope");
3291
+ return;
3292
+ }
3293
+ if (header.kind === "calibration") {
3294
+ if (payload.byteLength !== 0) {
3295
+ this.#protocolFailure(connection, void 0, decoded.chargedBytes, "calibration payload is not empty");
3296
+ return;
3297
+ }
3298
+ const sequence = header.calibration.sequence;
3299
+ if (sequence > connection.heartbeatSequence) {
3300
+ this.#protocolFailure(connection, void 0, decoded.chargedBytes, "calibration echo is ahead");
3301
+ return;
3302
+ }
3303
+ if (sequence > connection.heartbeatAckSequence) {
3304
+ connection.heartbeatAckSequence = sequence;
3305
+ this.#armHeartbeatDeadline(connection);
3306
+ }
3307
+ this.#returnCredit(connection, void 0, decoded.chargedBytes);
3308
+ return;
3309
+ }
3310
+ const activation = this.#activations.get(header.activationId);
3311
+ if (!activation || !connection.activationIds.has(header.activationId) || !activation.identity) {
3312
+ this.#returnCredit(connection, header.activationId, decoded.chargedBytes);
3313
+ if (activation)
3314
+ this.#protocolFailure(connection, header.activationId, 0, "presentation activation binding mismatch");
3315
+ return;
3316
+ }
3317
+ if (header.sessionId !== activation.sessionId || header.leaseEpoch !== activation.leaseEpoch || header.activationSequence <= activation.lastActivationSequence) {
3318
+ this.#protocolFailure(
3319
+ connection,
3320
+ header.activationId,
3321
+ decoded.chargedBytes,
3322
+ "presentation identity or sequence mismatch"
3323
+ );
3324
+ return;
3325
+ }
3326
+ activation.lastActivationSequence = header.activationSequence;
3327
+ if ((header.kind === "transfer-begin" || header.kind === "transfer-end") && payload.byteLength !== 0) {
3328
+ this.#protocolFailure(connection, header.activationId, decoded.chargedBytes, "transfer metadata carries payload");
3329
+ return;
3330
+ }
3331
+ switch (header.kind) {
3332
+ case "trf1-frame":
3333
+ this.#incremental(connection, activation, header, payload, decoded.chargedBytes);
3334
+ break;
3335
+ case "transfer-begin":
3336
+ this.#beginTransfer(connection, activation, header, decoded.chargedBytes);
3337
+ break;
3338
+ case "transfer-chunk":
3339
+ this.#transferChunk(connection, activation, header, payload, decoded.chargedBytes);
3340
+ break;
3341
+ case "transfer-end":
3342
+ this.#endTransfer(connection, activation, header, decoded.chargedBytes);
3343
+ break;
3344
+ }
3345
+ }
3346
+ #incremental(connection, activation, header, payload, chargedBytes) {
3347
+ if (payload.byteLength > connection.accepted.protocolLimits.maxPresentationChunkBytes || !header.resultContent || header.baseContent === void 0 || header.baseContent !== null && !exactStamp(activation.appliedContent, header.baseContent)) {
3348
+ this.#protocolFailure(connection, header.activationId, chargedBytes, "incremental base mismatch");
3349
+ return;
3350
+ }
3351
+ try {
3352
+ this.#emit({ type: "frames-state", activationId: header.activationId, state: "applying" });
3353
+ this.#applyFrame(copyArrayBuffer(payload), activation.identity);
3354
+ activation.appliedContent = header.resultContent;
3355
+ this.#returnCredit(connection, header.activationId, chargedBytes);
3356
+ this.#sceneApplied(connection, activation);
3357
+ } catch (error) {
3358
+ this.#protocolFailure(connection, header.activationId, chargedBytes, String(error));
3359
+ }
3360
+ }
3361
+ #beginTransfer(connection, activation, header, chargedBytes) {
3362
+ const transfer = header.transfer;
3363
+ const limits = connection.accepted.protocolLimits;
3364
+ const windows = connection.accepted.initialWindows;
3365
+ if (activation.transfer || !transfer?.kind || transfer.totalBytes === void 0 || transfer.chunkCount === void 0 || !transfer.targetLayout || !transfer.checksum || !header.resultContent || transfer.totalBytes > windows.stagingBytesPerSession || this.#stagingBytes + transfer.totalBytes > windows.stagingBytesTotal || transfer.kind === "catchup" && transfer.totalBytes > limits.maxCatchupBytes || transfer.kind === "catchup" && (header.baseContent == null || !exactStamp(activation.appliedContent, header.baseContent))) {
3366
+ this.#protocolFailure(connection, header.activationId, chargedBytes, "transfer budget or header invalid");
3367
+ return;
3368
+ }
3369
+ const activeSeeds = [...this.#activations.values()].filter(
3370
+ (candidate) => candidate.transfer?.kind === "seed"
3371
+ ).length;
3372
+ if (transfer.kind === "seed" && activeSeeds >= windows.maxConcurrentSeeds) {
3373
+ this.#protocolFailure(connection, header.activationId, chargedBytes, "seed concurrency exceeded");
3374
+ return;
3375
+ }
3376
+ activation.transfer = {
3377
+ transferId: transfer.transferId,
3378
+ kind: transfer.kind,
3379
+ bytes: new Uint8Array(transfer.totalBytes),
3380
+ chunkCount: transfer.chunkCount,
3381
+ chunks: /* @__PURE__ */ new Set(),
3382
+ ranges: [],
3383
+ checksum: transfer.checksum.value,
3384
+ targetLayout: transfer.targetLayout,
3385
+ baseContent: header.baseContent ?? null,
3386
+ resultContent: header.resultContent
3387
+ };
3388
+ this.#stagingBytes += transfer.totalBytes;
3389
+ this.#returnCredit(connection, header.activationId, chargedBytes);
3390
+ this.#emit({ type: "frames-state", activationId: header.activationId, state: "seeding" });
3391
+ }
3392
+ #transferChunk(connection, activation, header, payload, chargedBytes) {
3393
+ const staging = activation.transfer;
3394
+ const transfer = header.transfer;
3395
+ const chunkIndex = transfer?.chunkIndex;
3396
+ const byteOffset = transfer?.byteOffset;
3397
+ if (!staging || transfer?.transferId !== staging.transferId || chunkIndex === void 0 || byteOffset === void 0 || chunkIndex >= staging.chunkCount || staging.chunks.has(chunkIndex) || payload.byteLength > connection.accepted.protocolLimits.maxPresentationChunkBytes || byteOffset + payload.byteLength > staging.bytes.byteLength) {
3398
+ this.#protocolFailure(connection, header.activationId, chargedBytes, "transfer chunk invalid");
3399
+ return;
3400
+ }
3401
+ const end = byteOffset + payload.byteLength;
3402
+ if (staging.ranges.some((range) => byteOffset < range.end && end > range.start)) {
3403
+ this.#protocolFailure(connection, header.activationId, chargedBytes, "transfer chunk overlap");
3404
+ return;
3405
+ }
3406
+ staging.bytes.set(payload, byteOffset);
3407
+ staging.chunks.add(chunkIndex);
3408
+ staging.ranges.push({ start: byteOffset, end });
3409
+ this.#returnCredit(connection, header.activationId, chargedBytes);
3410
+ }
3411
+ #endTransfer(connection, activation, header, chargedBytes) {
3412
+ const staging = activation.transfer;
3413
+ if (!staging || header.transfer?.transferId !== staging.transferId) {
3414
+ this.#protocolFailure(connection, header.activationId, chargedBytes, "transfer end without begin");
3415
+ return;
3416
+ }
3417
+ this.#returnCredit(connection, header.activationId, chargedBytes);
3418
+ const ranges = [...staging.ranges].sort((left, right) => left.start - right.start);
3419
+ let cursor = 0;
3420
+ for (const range of ranges) {
3421
+ if (range.start !== cursor) {
3422
+ this.#protocolFailure(connection, header.activationId, 0, "transfer has a range gap");
3423
+ return;
3424
+ }
3425
+ cursor = range.end;
3426
+ }
3427
+ if (staging.chunks.size !== staging.chunkCount || cursor !== staging.bytes.byteLength || routedCrc32c(staging.bytes) !== staging.checksum || staging.kind === "catchup" && (staging.baseContent === null || !exactStamp(activation.appliedContent, staging.baseContent))) {
3428
+ this.#protocolFailure(connection, header.activationId, 0, "transfer validation failed");
3429
+ return;
3430
+ }
3431
+ try {
3432
+ this.#emit({ type: "frames-state", activationId: header.activationId, state: "applying" });
3433
+ this.#applyFrame(staging.bytes.buffer, activation.identity, staging.targetLayout);
3434
+ activation.appliedContent = staging.resultContent;
3435
+ this.#releaseTransfer(activation);
3436
+ this.#sceneApplied(connection, activation);
3437
+ } catch (error) {
3438
+ this.#protocolFailure(connection, header.activationId, 0, String(error));
3439
+ }
3440
+ }
3441
+ #releaseTransfer(activation) {
3442
+ if (!activation.transfer) return;
3443
+ this.#stagingBytes = Math.max(0, this.#stagingBytes - activation.transfer.bytes.byteLength);
3444
+ delete activation.transfer;
3445
+ }
3446
+ #sceneApplied(connection, activation) {
3447
+ const appliedContent = activation.appliedContent;
3448
+ if (!appliedContent) return;
3449
+ this.#send(
3450
+ connection,
3451
+ encodeRoutedMessage("SceneApplied", {
3452
+ sessionId: activation.sessionId,
3453
+ activationId: activation.request.activationId,
3454
+ leaseEpoch: activation.leaseEpoch,
3455
+ appliedContent
3456
+ })
3457
+ );
3458
+ this.#emit({
3459
+ type: "frames-state",
3460
+ activationId: activation.request.activationId,
3461
+ state: "active",
3462
+ ...activation.resumeToken === void 0 ? {} : { resumeToken: activation.resumeToken },
3463
+ appliedContent
3464
+ });
3465
+ this.#reportPresentation(connection, activation, "active");
3466
+ if (activation.sceneRefreshTimer !== void 0) clearTimeout(activation.sceneRefreshTimer);
3467
+ activation.sceneRefreshTimer = setTimeout(
3468
+ () => this.#sceneApplied(connection, activation),
3469
+ connection.accepted.protocolLimits.sceneAppliedRefreshMs
3470
+ );
3471
+ }
3472
+ #reportPresentation(connection, activation, state) {
3473
+ activation.workerStatusSequence += 1;
3474
+ const status = {
3475
+ activationId: activation.request.activationId,
3476
+ workerStatusSequence: activation.workerStatusSequence,
3477
+ state,
3478
+ ...activation.appliedContent === void 0 ? {} : { sceneContent: activation.appliedContent },
3479
+ leaseTtlMs: Math.max(
3480
+ connection.accepted?.protocolLimits.presentationStatusRefreshMs ?? 2e3,
3481
+ connection.accepted?.heartbeatTtlMs ?? ROUTED_DOOR_LIMITS.heartbeatTtlMs
3482
+ )
3483
+ };
3484
+ this.#emit({ type: "presentation-status", status });
3485
+ if (activation.presentationRefreshTimer !== void 0) clearTimeout(activation.presentationRefreshTimer);
3486
+ const refreshMs = connection.accepted?.protocolLimits.presentationStatusRefreshMs ?? 2e3;
3487
+ activation.presentationRefreshTimer = setTimeout(
3488
+ () => this.#reportPresentation(connection, activation, state),
3489
+ refreshMs
3490
+ );
3491
+ }
3492
+ #returnCredit(connection, activationId, bytes) {
3493
+ if (bytes <= 0) return;
3494
+ connection.connectionBytesReturned += bytes;
3495
+ if (activationId) {
3496
+ connection.accountBytesReturned.set(
3497
+ activationId,
3498
+ (connection.accountBytesReturned.get(activationId) ?? 0) + bytes
3499
+ );
3500
+ if (!connection.activationIds.has(activationId)) this.#scheduleAccountDrain(connection, activationId);
3501
+ }
3502
+ const limits = connection.accepted?.protocolLimits;
3503
+ if (connection.creditTimer !== void 0) return;
3504
+ connection.creditTimer = setTimeout(() => this.#flushCredit(connection), limits?.maxCreditReturnDelayMs ?? 16);
3505
+ }
3506
+ #flushCredit(connection) {
3507
+ if (connection.creditTimer !== void 0) clearTimeout(connection.creditTimer);
3508
+ delete connection.creditTimer;
3509
+ if (!connection.accepted || connection.connectionBytesReturned === 0) return;
3510
+ connection.creditSequence += 1;
3511
+ const newlyReturned = connection.connectionBytesReturned - connection.connectionBytesReported;
3512
+ connection.connectionBytesReported = connection.connectionBytesReturned;
3513
+ this.#send(
3514
+ connection,
3515
+ encodeRoutedMessage("TransportCredit", {
3516
+ creditEpoch: connection.accepted.creditEpoch,
3517
+ creditSequence: connection.creditSequence,
3518
+ connectionBytesReturned: connection.connectionBytesReturned,
3519
+ accounts: [...connection.accountBytesReturned].map(([activationId, bytesReturned]) => ({
3520
+ activationId,
3521
+ bytesReturned
3522
+ }))
3523
+ })
3524
+ );
3525
+ this.#creditReturned?.(newlyReturned);
3526
+ }
3527
+ #scheduleAccountDrain(connection, activationId) {
3528
+ const previous = connection.accountDrainTimers.get(activationId);
3529
+ if (previous !== void 0) clearTimeout(previous);
3530
+ const ttl = connection.accepted?.protocolLimits.creditAccountDrainTtlMs ?? 5e3;
3531
+ connection.accountDrainTimers.set(
3532
+ activationId,
3533
+ setTimeout(() => {
3534
+ connection.accountDrainTimers.delete(activationId);
3535
+ if (!connection.activationIds.has(activationId)) connection.accountBytesReturned.delete(activationId);
3536
+ }, ttl)
3537
+ );
3538
+ }
3539
+ #scheduleHeartbeat(connection) {
3540
+ if (connection.heartbeatTimer !== void 0) clearTimeout(connection.heartbeatTimer);
3541
+ connection.heartbeatTimer = setTimeout(
3542
+ () => {
3543
+ if (!connection.accepted || connection.socket.readyState !== socketOpen) return;
3544
+ connection.heartbeatSequence += 1;
3545
+ this.#send(
3546
+ connection,
3547
+ encodeRoutedMessage("CalibrationPing", {
3548
+ sequence: connection.heartbeatSequence,
3549
+ t0: Date.now()
3550
+ })
3551
+ );
3552
+ this.#scheduleHeartbeat(connection);
3553
+ },
3554
+ Math.max(
3555
+ 100,
3556
+ Math.min(
3557
+ ROUTED_DOOR_LIMITS.heartbeatIntervalMs,
3558
+ Math.floor((connection.accepted?.heartbeatTtlMs ?? ROUTED_DOOR_LIMITS.heartbeatTtlMs) / 3)
3559
+ )
3560
+ )
3561
+ );
3562
+ }
3563
+ #armHeartbeatDeadline(connection) {
3564
+ if (connection.heartbeatDeadlineTimer !== void 0) clearTimeout(connection.heartbeatDeadlineTimer);
3565
+ const ttl = connection.accepted?.heartbeatTtlMs ?? ROUTED_DOOR_LIMITS.heartbeatTtlMs;
3566
+ connection.heartbeatDeadlineTimer = setTimeout(() => {
3567
+ if (this.#connections.get(connection.cellBootId) !== connection) return;
3568
+ this.#closeConnection(connection, ROUTED_CLOSE_CODES.LEG_TIMEOUT, "heartbeat timeout", true);
3569
+ }, ttl);
3570
+ }
3571
+ #protocolFailure(connection, activationId, chargedBytes, reason) {
3572
+ this.#returnCredit(connection, activationId, chargedBytes);
3573
+ this.#flushCredit(connection);
3574
+ if (activationId) this.#emit({ type: "frames-state", activationId, state: "failed", reason: "PROTOCOL" });
3575
+ this.#closeConnection(connection, ROUTED_CLOSE_CODES.PROTOCOL, reason, true);
3576
+ }
3577
+ #send(connection, text) {
3578
+ if (connection.socket.readyState === socketOpen) connection.socket.send(text);
3579
+ }
3580
+ #closeConnection(connection, code, reason, closeSocket) {
3581
+ if (connection.heartbeatTimer !== void 0) clearTimeout(connection.heartbeatTimer);
3582
+ if (connection.heartbeatDeadlineTimer !== void 0) clearTimeout(connection.heartbeatDeadlineTimer);
3583
+ if (connection.creditTimer !== void 0) clearTimeout(connection.creditTimer);
3584
+ for (const timer of connection.accountDrainTimers.values()) clearTimeout(timer);
3585
+ if (closeSocket && connection.socket.readyState < socketClosing)
3586
+ connection.socket.close(code, reason.slice(0, 123));
3587
+ this.#closed(connection, code, reason);
3588
+ }
3589
+ #closed(connection, code, reason) {
3590
+ if (this.#connections.get(connection.cellBootId) !== connection) return;
3591
+ this.#connections.delete(connection.cellBootId);
3592
+ if (connection.heartbeatTimer !== void 0) clearTimeout(connection.heartbeatTimer);
3593
+ if (connection.heartbeatDeadlineTimer !== void 0) clearTimeout(connection.heartbeatDeadlineTimer);
3594
+ if (connection.creditTimer !== void 0) clearTimeout(connection.creditTimer);
3595
+ for (const timer of connection.accountDrainTimers.values()) clearTimeout(timer);
3596
+ connection.accountDrainTimers.clear();
3597
+ const activationIds = [...connection.activationIds];
3598
+ for (const activationId of activationIds) {
3599
+ const activation = this.#activations.get(activationId);
3600
+ if (!activation) continue;
3601
+ this.#releaseTransfer(activation);
3602
+ if (activation.sceneRefreshTimer !== void 0) clearTimeout(activation.sceneRefreshTimer);
3603
+ if (activation.presentationRefreshTimer !== void 0) clearTimeout(activation.presentationRefreshTimer);
3604
+ }
3605
+ this.#emit({
3606
+ type: "transport-closed",
3607
+ cellBootId: connection.cellBootId,
3608
+ code,
3609
+ reason,
3610
+ activationIds,
3611
+ preAuth: connection.refusal === void 0 && connection.accepted === void 0 && code === 1008,
3612
+ ...connection.refusal === void 0 ? {} : { refusal: connection.refusal }
3613
+ });
3614
+ }
3615
+ };
3616
+
2689
3617
  // src/terminal-render.worker.ts
2690
3618
  var hiddenCursor = { x: 0, y: 0, visible: false, style: 1, blinking: false };
2691
3619
  var CURSOR_BLINK_INTERVAL_MS = 600;
@@ -2731,6 +3659,37 @@ var pendingFrameCreditBytes = 0;
2731
3659
  var frameCreditTimer;
2732
3660
  var shaderAnimationScheduled = false;
2733
3661
  var performanceMeasurement;
3662
+ var lifetimeStartedAt = performance.now();
3663
+ var lifetimeFrames = {
3664
+ received: 0,
3665
+ bytes: 0,
3666
+ full: 0,
3667
+ incremental: 0,
3668
+ stale: 0,
3669
+ decodes: 0,
3670
+ applies: 0
3671
+ };
3672
+ var lifetimeSessions = /* @__PURE__ */ new Map();
3673
+ var lifetimeRenderer = { queueSubmits: 0, presents: 0 };
3674
+ var lifetimeFlow = { creditBytesReturned: 0, creditBatchesReturned: 0 };
3675
+ function sessionCounters(sessionHandle) {
3676
+ let counters = lifetimeSessions.get(sessionHandle);
3677
+ if (!counters) {
3678
+ counters = { received: 0, bytes: 0, full: 0, incremental: 0, stale: 0, decodes: 0, applies: 0 };
3679
+ lifetimeSessions.set(sessionHandle, counters);
3680
+ }
3681
+ return counters;
3682
+ }
3683
+ function counterSnapshot() {
3684
+ return {
3685
+ backend: renderer?.kind ?? "starting",
3686
+ durationMs: performance.now() - lifetimeStartedAt,
3687
+ frames: { ...lifetimeFrames },
3688
+ renderer: { ...lifetimeRenderer },
3689
+ flow: { ...lifetimeFlow },
3690
+ sessions: Object.fromEntries([...lifetimeSessions].map(([id, counters]) => [id, { ...counters }]))
3691
+ };
3692
+ }
2734
3693
  function appendPerformanceSample(samples, value) {
2735
3694
  if (samples.length < MAX_PERFORMANCE_SAMPLES) samples.push(value);
2736
3695
  }
@@ -2740,6 +3699,8 @@ function flushFrameCredit() {
2740
3699
  if (pendingFrameCreditBytes === 0) return;
2741
3700
  const bytes = pendingFrameCreditBytes;
2742
3701
  pendingFrameCreditBytes = 0;
3702
+ lifetimeFlow.creditBytesReturned += bytes;
3703
+ lifetimeFlow.creditBatchesReturned += 1;
2743
3704
  postToRenderer({ type: "frame-credit", bytes });
2744
3705
  }
2745
3706
  function returnFrameCredit(bytes) {
@@ -2813,6 +3774,10 @@ function recordRenderMetrics(metrics) {
2813
3774
  target.atlasUploadBytes += metrics.atlasUploadBytes;
2814
3775
  target.atlasUploadCalls += metrics.atlasUploadCalls;
2815
3776
  }
3777
+ function recordLifetimeRenderMetrics(metrics) {
3778
+ lifetimeRenderer.queueSubmits += metrics.queueSubmits;
3779
+ lifetimeRenderer.presents += metrics.fullRenders + metrics.partialRenders;
3780
+ }
2816
3781
  async function finishPerformanceMeasurement(requestId, quietMs, timeoutMs) {
2817
3782
  const active = performanceMeasurement;
2818
3783
  if (!active) throw new Error("No terminal render performance measurement is active");
@@ -2859,25 +3824,28 @@ async function finishPerformanceMeasurement(requestId, quietMs, timeoutMs) {
2859
3824
  function postToRenderer(message) {
2860
3825
  self.postMessage(message);
2861
3826
  }
3827
+ function emptySessionSnapshot() {
3828
+ return {
3829
+ rows: [],
3830
+ nativeRows: [],
3831
+ nativeStyleRows: [],
3832
+ glyphDefinitions: /* @__PURE__ */ new Map(),
3833
+ glyphPixelBytes: 0,
3834
+ styleDefinitions: /* @__PURE__ */ new Map(),
3835
+ rowRevisions: [],
3836
+ cursor: hiddenCursor,
3837
+ layoutEpoch: 0n,
3838
+ sessionEpoch: 0n,
3839
+ sequence: 0n,
3840
+ awaitingResync: false,
3841
+ catalogFallback: false,
3842
+ scrollbar: null
3843
+ };
3844
+ }
2862
3845
  function snapshot(sessionHandle) {
2863
3846
  let value = snapshots.get(sessionHandle);
2864
3847
  if (!value) {
2865
- value = {
2866
- rows: [],
2867
- nativeRows: [],
2868
- nativeStyleRows: [],
2869
- glyphDefinitions: /* @__PURE__ */ new Map(),
2870
- glyphPixelBytes: 0,
2871
- styleDefinitions: /* @__PURE__ */ new Map(),
2872
- rowRevisions: [],
2873
- cursor: hiddenCursor,
2874
- layoutEpoch: 0n,
2875
- sessionEpoch: 0n,
2876
- sequence: 0n,
2877
- awaitingResync: false,
2878
- catalogFallback: false,
2879
- scrollbar: null
2880
- };
3848
+ value = emptySessionSnapshot();
2881
3849
  snapshots.set(sessionHandle, value);
2882
3850
  }
2883
3851
  return value;
@@ -3117,6 +4085,7 @@ async function flush() {
3117
4085
  const active = performanceMeasurement;
3118
4086
  const beforeRender = active ? performance.now() : 0;
3119
4087
  const metrics = backend.renderBatch ? backend.renderBatch(entries) : entries.map(({ id, view }) => backend.render(id, view));
4088
+ for (const metric of metrics) recordLifetimeRenderMetrics(metric ?? emptyRenderMetrics());
3120
4089
  const renderedAt = active ? performance.now() : 0;
3121
4090
  for (const { id } of entries) {
3122
4091
  const damage = surfaces.get(id)?.damage;
@@ -3200,9 +4169,11 @@ async function mount(surfaceId, sessionHandle, canvas) {
3200
4169
  invalidateFull(surfaceId);
3201
4170
  scheduleShaderAnimation();
3202
4171
  }
3203
- function applyFrame(packet) {
4172
+ function applyFrame(packet, expectedIdentity, expectedLayout) {
3204
4173
  const active = performanceMeasurement;
3205
4174
  const applyStarted = active ? performance.now() : 0;
4175
+ lifetimeFrames.received += 1;
4176
+ lifetimeFrames.bytes += packet.byteLength;
3206
4177
  if (active) {
3207
4178
  active.frames.received += 1;
3208
4179
  active.frames.bytes += packet.byteLength;
@@ -3210,15 +4181,39 @@ function applyFrame(packet) {
3210
4181
  }
3211
4182
  const frame = decodeFrame(packet);
3212
4183
  const id = frame.sessionHandle.toString();
4184
+ if (expectedIdentity && (id !== expectedIdentity.sessionHandle || frame.viewHandle.toString() !== expectedIdentity.viewHandle)) {
4185
+ throw new Error("TRF1 identity does not match the routed activation binding");
4186
+ }
4187
+ if (expectedLayout && (frame.cols !== expectedLayout.cols || frame.rows !== expectedLayout.rows)) {
4188
+ throw new Error("transfer layout does not match TRF1");
4189
+ }
4190
+ if (expectedLayout && (frame.flags & FrameFlag.FullSnapshot) === 0) {
4191
+ throw new Error("routed transfer is not a full TRF1 snapshot");
4192
+ }
4193
+ const counters = sessionCounters(id);
4194
+ lifetimeFrames.decodes += 1;
4195
+ counters.received += 1;
4196
+ counters.bytes += packet.byteLength;
4197
+ counters.decodes += 1;
4198
+ if ((frame.flags & FrameFlag.FullSnapshot) !== 0) {
4199
+ lifetimeFrames.full += 1;
4200
+ counters.full += 1;
4201
+ } else {
4202
+ lifetimeFrames.incremental += 1;
4203
+ counters.incremental += 1;
4204
+ }
3213
4205
  if (active) {
3214
4206
  active.lastFrameAt.set(id, applyStarted);
3215
4207
  if ((frame.flags & FrameFlag.FullSnapshot) !== 0) active.frames.full += 1;
3216
4208
  else active.frames.incremental += 1;
3217
4209
  }
3218
- const previous = snapshot(id);
4210
+ const replacingScene = expectedLayout !== void 0;
4211
+ const installedScene = replacingScene ? snapshots.get(id) : void 0;
4212
+ const previous = replacingScene ? emptySessionSnapshot() : snapshot(id);
3219
4213
  const fullFrame = (frame.flags & FrameFlag.FullSnapshot) !== 0;
3220
4214
  const catalogReset = (frame.flags & FrameFlag.CatalogReset) !== 0;
3221
4215
  if (catalogReset && !fullFrame) {
4216
+ if (expectedIdentity) throw new Error("catalog reset without a full routed frame");
3222
4217
  if (active) {
3223
4218
  active.frames.resyncRequested += 1;
3224
4219
  appendPerformanceSample(active.samples.frameApplyMs, performance.now() - applyStarted);
@@ -3234,21 +4229,25 @@ function applyFrame(packet) {
3234
4229
  full: fullFrame
3235
4230
  });
3236
4231
  if (classification === "stale") {
4232
+ lifetimeFrames.stale += 1;
4233
+ counters.stale += 1;
3237
4234
  if (active) {
3238
4235
  active.frames.stale += 1;
3239
4236
  appendPerformanceSample(active.samples.frameApplyMs, performance.now() - applyStarted);
3240
4237
  }
3241
- return;
4238
+ if (expectedIdentity) throw new Error("stale routed TRF1 frame");
4239
+ return void 0;
3242
4240
  }
3243
4241
  const changedSession = previous.sessionEpoch !== 0n && frame.sessionEpoch !== previous.sessionEpoch;
3244
4242
  if (classification === "resync") {
4243
+ if (expectedIdentity) throw new Error("routed TRF1 continuity requires a transfer");
3245
4244
  if (active) {
3246
4245
  active.frames.resyncRequested += 1;
3247
4246
  appendPerformanceSample(active.samples.frameApplyMs, performance.now() - applyStarted);
3248
4247
  }
3249
4248
  previous.awaitingResync = true;
3250
4249
  postToRenderer({ type: "frame-resync-needed", sessionHandle: id });
3251
- return;
4250
+ return void 0;
3252
4251
  }
3253
4252
  const completingResync = previous.awaitingResync;
3254
4253
  const rowSection = frame.sections.find((candidate) => candidate.kind === SectionKind.RowReplacements);
@@ -3258,13 +4257,39 @@ function applyFrame(packet) {
3258
4257
  const clipboardSection = frame.sections.find((candidate) => candidate.kind === SectionKind.ClipboardWrite);
3259
4258
  const scrollbarSection = frame.sections.find((candidate) => candidate.kind === SectionKind.ScrollbarState);
3260
4259
  if (!rowSection || !cursorSection) {
4260
+ if (expectedIdentity) throw new Error("routed TRF1 frame is missing required sections");
3261
4261
  if (active) appendPerformanceSample(active.samples.frameApplyMs, performance.now() - applyStarted);
3262
- return;
4262
+ return void 0;
4263
+ }
4264
+ const fullRows = (rowSection.flags & 1) !== 0;
4265
+ if (replacingScene && !fullRows) {
4266
+ throw new Error("routed transfer row section is not a full snapshot");
3263
4267
  }
3264
4268
  const glyphDefinitions = glyphSection ? decodeGlyphDefinitions(glyphSection) : NO_GLYPH_DEFINITIONS;
3265
4269
  const styleDefinitions = styleSection ? decodeStyleDefinitions(styleSection) : NO_STYLE_DEFINITIONS;
4270
+ const replacements = decodeRowReplacements(rowSection);
4271
+ for (const replacement of replacements) {
4272
+ if (replacement.row >= frame.rows) throw new RangeError("Row replacement exceeds viewport");
4273
+ }
4274
+ const nextCursor = decodeCursorState(cursorSection);
4275
+ const clipboardText = clipboardSection ? decodeClipboardWrite(clipboardSection) : void 0;
4276
+ let scrollbar;
4277
+ if (scrollbarSection) {
4278
+ const decoded = decodeScrollbarState(scrollbarSection);
4279
+ scrollbar = {
4280
+ total: Number(decoded.total),
4281
+ offset: Number(decoded.offset),
4282
+ length: Number(decoded.length)
4283
+ };
4284
+ if (!Number.isSafeInteger(scrollbar.total) || !Number.isSafeInteger(scrollbar.offset) || !Number.isSafeInteger(scrollbar.length)) {
4285
+ throw new RangeError("Scrollbar state exceeds JavaScript's safe integer range");
4286
+ }
4287
+ }
4288
+ if (expectedLayout && (scrollbar === void 0 || scrollbar.length !== expectedLayout.rows || scrollbar.total - scrollbar.length !== expectedLayout.scrollbackRows)) {
4289
+ throw new Error("transfer scrollback layout does not match TRF1");
4290
+ }
3266
4291
  if (active) active.frames.glyphDefinitions += glyphDefinitions.length;
3267
- const resetsCatalog = changedSession || completingResync || catalogReset;
4292
+ const resetsCatalog = replacingScene || changedSession || completingResync || catalogReset;
3268
4293
  const wasCatalogFallback = previous.catalogFallback;
3269
4294
  if (resetsCatalog) {
3270
4295
  previous.rows = [];
@@ -3315,13 +4340,14 @@ function applyFrame(packet) {
3315
4340
  );
3316
4341
  const admission = catalogAdmission(false, false, catalogFits);
3317
4342
  if (admission === "request-full") {
4343
+ if (expectedIdentity) throw new Error("routed catalog pressure requires a fresh transfer");
3318
4344
  if (active) {
3319
4345
  active.frames.resyncRequested += 1;
3320
4346
  appendPerformanceSample(active.samples.frameApplyMs, performance.now() - applyStarted);
3321
4347
  }
3322
4348
  previous.awaitingResync = true;
3323
4349
  postToRenderer({ type: "frame-resync-needed", sessionHandle: id });
3324
- return;
4350
+ return void 0;
3325
4351
  }
3326
4352
  if (admission === "install") {
3327
4353
  installGlyphDefinitions(previous, glyphDefinitions);
@@ -3332,34 +4358,21 @@ function applyFrame(packet) {
3332
4358
  nativeTextAnnounced = true;
3333
4359
  postToRenderer({ type: "renderer-status", backend: renderer?.kind ?? "starting", textEngine: "native" });
3334
4360
  }
3335
- if (clipboardSection) {
3336
- postToRenderer({ type: "clipboard-write", text: decodeClipboardWrite(clipboardSection) });
3337
- }
3338
- if (scrollbarSection) {
3339
- const decoded = decodeScrollbarState(scrollbarSection);
3340
- const scrollbar = {
3341
- total: Number(decoded.total),
3342
- offset: Number(decoded.offset),
3343
- length: Number(decoded.length)
3344
- };
3345
- if (!Number.isSafeInteger(scrollbar.total) || !Number.isSafeInteger(scrollbar.offset) || !Number.isSafeInteger(scrollbar.length))
3346
- throw new RangeError("Scrollbar state exceeds JavaScript's safe integer range");
4361
+ let scrollbarChanged = false;
4362
+ if (scrollbar) {
3347
4363
  if (!previous.scrollbar || previous.scrollbar.total !== scrollbar.total || previous.scrollbar.offset !== scrollbar.offset || previous.scrollbar.length !== scrollbar.length) {
3348
4364
  previous.scrollbar = scrollbar;
3349
- postToRenderer({ type: "scrollbar-state", sessionHandle: id, scrollbar });
4365
+ scrollbarChanged = true;
3350
4366
  }
3351
4367
  }
3352
- const full = (rowSection.flags & 1) !== 0;
3353
4368
  const useNativeCatalog = !previous.catalogFallback;
3354
- const rows = full ? Array(frame.rows).fill("") : previous.rows.slice();
3355
- const nativeRows = full ? Array.from({ length: frame.rows }, () => []) : previous.nativeRows.slice();
3356
- const nativeStyleRows = full ? Array.from({ length: frame.rows }, () => []) : previous.nativeStyleRows.slice();
3357
- const rowRevisions = full ? Array(frame.rows).fill(0n) : previous.rowRevisions.slice();
3358
- const replacements = decodeRowReplacements(rowSection);
4369
+ const rows = fullRows ? Array(frame.rows).fill("") : previous.rows.slice();
4370
+ const nativeRows = fullRows ? Array.from({ length: frame.rows }, () => []) : previous.nativeRows.slice();
4371
+ const nativeStyleRows = fullRows ? Array.from({ length: frame.rows }, () => []) : previous.nativeStyleRows.slice();
4372
+ const rowRevisions = fullRows ? Array(frame.rows).fill(0n) : previous.rowRevisions.slice();
3359
4373
  const damagedRows = [];
3360
4374
  if (active) active.frames.rowsDecoded += replacements.length;
3361
4375
  for (const replacement of replacements) {
3362
- if (replacement.row >= frame.rows) throw new RangeError("Row replacement exceeds viewport");
3363
4376
  if (replacement.revision < (rowRevisions[replacement.row] ?? 0n)) continue;
3364
4377
  rows[replacement.row] = replacement.text;
3365
4378
  nativeRows[replacement.row] = useNativeCatalog ? replacement.glyphs : [];
@@ -3373,15 +4386,20 @@ function applyFrame(packet) {
3373
4386
  previous.rowRevisions = rowRevisions;
3374
4387
  const geometryChanged = damagedRows.length > 0;
3375
4388
  const previousCursor = previous.cursor;
3376
- const nextCursor = decodeCursorState(cursorSection);
3377
4389
  const cursorChanged = nextCursor.x !== previousCursor.x || nextCursor.y !== previousCursor.y || nextCursor.visible !== previousCursor.visible || nextCursor.style !== previousCursor.style || nextCursor.blinking !== previousCursor.blinking;
3378
4390
  previous.cursor = nextCursor;
3379
4391
  previous.layoutEpoch = frame.layoutEpoch;
3380
4392
  previous.sessionEpoch = frame.sessionEpoch;
3381
4393
  previous.sequence = frame.frameSequence;
3382
4394
  previous.awaitingResync = false;
4395
+ if (replacingScene) {
4396
+ snapshots.set(id, previous);
4397
+ if (installedScene) clearSessionCatalog(installedScene);
4398
+ }
4399
+ if (clipboardText !== void 0) postToRenderer({ type: "clipboard-write", text: clipboardText });
4400
+ if (scrollbarChanged && scrollbar) postToRenderer({ type: "scrollbar-state", sessionHandle: id, scrollbar });
3383
4401
  if (completingResync) postToRenderer({ type: "frame-resync-complete", sessionHandle: id });
3384
- const requiresFullRedraw = full || changedSession || completingResync;
4402
+ const requiresFullRedraw = fullRows || changedSession || completingResync;
3385
4403
  if (!requiresFullRedraw && cursorChanged) damagedRows.push(previousCursor.y, nextCursor.y);
3386
4404
  const hasRowDamage = damagedRows.length > 0;
3387
4405
  for (const surfaceId of surfaceIdsForSession(id)) {
@@ -3399,8 +4417,28 @@ function applyFrame(packet) {
3399
4417
  // recovered screen apart from a partial update of the stale one.
3400
4418
  fullSnapshot: fullFrame
3401
4419
  });
4420
+ lifetimeFrames.applies += 1;
4421
+ counters.applies += 1;
3402
4422
  if (active) appendPerformanceSample(active.samples.frameApplyMs, performance.now() - applyStarted);
4423
+ return {
4424
+ sessionHandle: id,
4425
+ viewHandle: frame.viewHandle.toString(),
4426
+ cols: frame.cols,
4427
+ rows: frame.rows
4428
+ };
3403
4429
  }
4430
+ var routedFrames = new RoutedFramesTransport({
4431
+ applyFrame: (packet, identity, expectedLayout) => {
4432
+ const applied = applyFrame(packet, identity, expectedLayout);
4433
+ if (!applied) throw new Error("routed TRF1 frame was not applied");
4434
+ return applied;
4435
+ },
4436
+ emit: (event) => postToRenderer({ type: "routed-frames-event", event }),
4437
+ creditReturned: (bytes) => {
4438
+ lifetimeFlow.creditBytesReturned += bytes;
4439
+ lifetimeFlow.creditBatchesReturned += 1;
4440
+ }
4441
+ });
3404
4442
  self.onmessage = (event) => {
3405
4443
  const message = event.data;
3406
4444
  try {
@@ -3557,6 +4595,12 @@ self.onmessage = (event) => {
3557
4595
  void finishPerformanceMeasurement(message.requestId, message.quietMs, message.timeoutMs).catch((error) => {
3558
4596
  console.error("[terminal-renderer] failed to finish performance measurement", error);
3559
4597
  });
4598
+ } else if (message.type === "performance-counters") {
4599
+ postToRenderer({ type: "performance-counters", requestId: message.requestId, snapshot: counterSnapshot() });
4600
+ } else if (message.type === "routed-frames-attach") {
4601
+ routedFrames.attach(message.request);
4602
+ } else if (message.type === "routed-frames-detach") {
4603
+ routedFrames.detach(message.activationId);
3560
4604
  }
3561
4605
  } catch (error) {
3562
4606
  console.error("[terminal-renderer] rejected worker message", error);