@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.
- package/README.md +50 -0
- package/dist/TerminalSurface.d.ts +5 -0
- package/dist/TerminalSurface.d.ts.map +1 -1
- package/dist/TerminalSurface.js +19 -4
- package/dist/TerminalSurface.js.map +1 -1
- package/dist/index.d.ts +4 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/performance.d.ts +31 -0
- package/dist/performance.d.ts.map +1 -1
- package/dist/performance.js.map +1 -1
- package/dist/routed-activation.d.ts +110 -0
- package/dist/routed-activation.d.ts.map +1 -0
- package/dist/routed-activation.js +287 -0
- package/dist/routed-activation.js.map +1 -0
- package/dist/routed-control.d.ts +69 -0
- package/dist/routed-control.d.ts.map +1 -0
- package/dist/routed-control.js +383 -0
- package/dist/routed-control.js.map +1 -0
- package/dist/routed-frames.d.ts +69 -0
- package/dist/routed-frames.d.ts.map +1 -0
- package/dist/routed-frames.js +660 -0
- package/dist/routed-frames.js.map +1 -0
- package/dist/runtime.d.ts +73 -4
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +1034 -53
- package/dist/runtime.js.map +1 -1
- package/dist/terminal-render.worker.js +1089 -45
- package/dist/terminal-render.worker.js.map +3 -3
- package/dist/worker-messages.d.ts +18 -1
- package/dist/worker-messages.d.ts.map +1 -1
- package/dist/workspace/Workspace.d.ts +12 -1
- package/dist/workspace/Workspace.d.ts.map +1 -1
- package/dist/workspace/Workspace.js +77 -14
- package/dist/workspace/Workspace.js.map +1 -1
- package/dist/workspace/index.d.ts +1 -1
- package/dist/workspace/index.d.ts.map +1 -1
- package/dist/workspace/index.js.map +1 -1
- 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
|
|
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
|
-
|
|
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
|
-
|
|
3336
|
-
|
|
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
|
-
|
|
4365
|
+
scrollbarChanged = true;
|
|
3350
4366
|
}
|
|
3351
4367
|
}
|
|
3352
|
-
const full = (rowSection.flags & 1) !== 0;
|
|
3353
4368
|
const useNativeCatalog = !previous.catalogFallback;
|
|
3354
|
-
const rows =
|
|
3355
|
-
const nativeRows =
|
|
3356
|
-
const nativeStyleRows =
|
|
3357
|
-
const rowRevisions =
|
|
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 =
|
|
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);
|