@zlink-systems/framework 0.31.0 → 0.32.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/dist/.tsbuildinfo +1 -1
- package/dist/contracts/Actors/ZLinkActor.d.ts +2 -0
- package/dist/contracts/Actors/ZLinkSessionBindingIdentity.d.ts +6 -0
- package/dist/contracts/Actors/ZLinkSessionBindingIdentity.js +2 -0
- package/dist/contracts/Actors/index.d.ts +1 -0
- package/dist/contracts/Actors/index.js +1 -0
- package/dist/contracts/Common/payload-slab.d.ts +4 -0
- package/dist/contracts/Common/payload-slab.js +30 -0
- package/dist/contracts/Handlers/Contexts.d.ts +1 -0
- package/dist/internal.d.ts +9 -0
- package/dist/internal.js +23 -1
- package/dist/runtime/actors/actor-client.d.ts +1 -1
- package/dist/runtime/actors/actor-client.js +32 -23
- package/dist/runtime/actors/actor-mailbox.d.ts +1 -1
- package/dist/runtime/actors/actor-runtime-state.d.ts +1 -1
- package/dist/runtime/actors/spot-actor-dispatch.d.ts +4 -2
- package/dist/runtime/actors/spot-actor-dispatch.js +2 -1
- package/dist/runtime/application-jobs/application-ingress-record-owner.d.ts +1 -0
- package/dist/runtime/application-jobs/application-ingress-record-owner.js +3 -0
- package/dist/runtime/backend/contracts/index.d.ts +2 -0
- package/dist/runtime/backend/mesh-completion-table.d.ts +1 -0
- package/dist/runtime/backend/mesh-completion-table.js +1 -0
- package/dist/runtime/backend/node/node-mesh-backend-adapter.js +5 -1
- package/dist/runtime/backend/node/node-raw-binding-port.d.ts +7 -2
- package/dist/runtime/backend/node/node-raw-binding-port.js +17 -11
- package/dist/runtime/backend/node/node-raw-mesh-backend.d.ts +2 -2
- package/dist/runtime/backend/node/node-raw-mesh-backend.js +15 -8
- package/dist/runtime/channels/channel-dispatch-pipeline.d.ts +7 -6
- package/dist/runtime/channels/channel-dispatch-pipeline.js +1 -0
- package/dist/runtime/channels/channel-dispatchers.d.ts +7 -3
- package/dist/runtime/channels/channel-dispatchers.js +27 -19
- package/dist/runtime/channels/channel-envelope.js +543 -23
- package/dist/runtime/channels/channel-receive-loops.d.ts +9 -6
- package/dist/runtime/channels/channel-receive-loops.js +59 -18
- package/dist/runtime/channels/channel-runtime-lifecycle.d.ts +22 -3
- package/dist/runtime/channels/channel-socket-registry.d.ts +22 -4
- package/dist/runtime/diagnostics/runtime-metrics.d.ts +1 -1
- package/dist/runtime/diagnostics/runtime-metrics.js +2 -1
- package/dist/runtime/execution/serial-execution-queue.d.ts +26 -13
- package/dist/runtime/foundation/raw-service-mesh-runtime.d.ts +27 -11
- package/dist/runtime/foundation/raw-service-mesh-runtime.js +29 -25
- package/dist/runtime/foundation/service-liveness-registry.d.ts +1 -1
- package/dist/runtime/foundation/service-mailbox.d.ts +3 -1
- package/dist/runtime/foundation/service-mailbox.js +4 -1
- package/dist/runtime/foundation/service-runtime-contracts.d.ts +3 -1
- package/dist/runtime/foundation/service-stateful-registry.d.ts +2 -0
- package/dist/runtime/foundation/service-stateful-registry.js +28 -12
- package/dist/runtime/foundation/service-stateful-runtime.d.ts +51 -19
- package/dist/runtime/foundation/service-stateful-runtime.js +54 -5
- package/dist/runtime/foundation/service-stateful-wire-codec.d.ts +3 -0
- package/dist/runtime/foundation/service-stateful-wire-codec.js +15 -0
- package/dist/runtime/foundation/service-wire-constants.generated.d.ts +1 -0
- package/dist/runtime/foundation/service-wire-constants.generated.js +12 -0
- package/dist/runtime/foundation/service-wire-m6a-codec.d.ts +2 -0
- package/dist/runtime/foundation/service-wire-m6a-codec.js +17 -7
- package/dist/runtime/host/actor-placement-coordinator.d.ts +1 -1
- package/dist/runtime/host/actor-transfer-runtime.d.ts +3 -3
- package/dist/runtime/host/application-job-queue.d.ts +1 -1
- package/dist/runtime/host/index.d.ts +89 -36
- package/dist/runtime/host/index.js +101 -7
- package/dist/runtime/host/location-runtime-owner.d.ts +2 -2
- package/dist/runtime/host/service-relocation-host-runtime.d.ts +181 -44
- package/dist/runtime/host/service-relocation-host-runtime.js +47 -13
- package/dist/runtime/host/spot-address-transport.js +50 -42
- package/dist/runtime/locations/auto-connect-loop.d.ts +0 -1
- package/dist/runtime/locations/auto-connect-loop.js +25 -19
- package/dist/runtime/locations/auto-connect-planner.js +2 -5
- package/dist/runtime/locations/auto-connect-reconciler.d.ts +1 -0
- package/dist/runtime/locations/auto-connect-reconciler.js +35 -23
- package/dist/runtime/locations/in-memory-location-store.d.ts +0 -2
- package/dist/runtime/locations/in-memory-location-store.js +1 -38
- package/dist/runtime/locations/location-store-repository.d.ts +0 -1
- package/dist/runtime/locations/location-store-repository.js +13 -115
- package/dist/runtime/locations/runtime.d.ts +3 -3
- package/dist/runtime/locations/runtime.js +12 -16
- package/dist/runtime/messaging/creation-payload-codec.js +2 -2
- package/dist/runtime/protocol/service_wire_codec.generated.d.ts +4 -0
- package/dist/runtime/protocol/service_wire_codec.generated.js +17 -7
- package/dist/runtime/spots/index.d.ts +20 -5
- package/dist/runtime/spots/index.js +14 -0
- package/dist/runtime/spots/spot-actor-admission-coordinator.d.ts +3 -3
- package/dist/runtime/spots/spot-actor-packet-dispatch.d.ts +1 -0
- package/dist/runtime/spots/spot-actor-packet-dispatch.js +13 -7
- package/dist/runtime/spots/spot-actor-packet-drain.d.ts +1 -0
- package/dist/runtime/spots/spot-actor-packet-drain.js +12 -5
- package/dist/runtime/spots/spot-entry-activation.d.ts +2 -1
- package/dist/runtime/spots/spot-node-runtime-manager.d.ts +8 -6
- package/dist/runtime/spots/spot-node-runtime-manager.js +2 -0
- package/dist/runtime/spots/spot-route-packet-dispatch.js +7 -4
- package/dist/runtime/spots/spot-routed-spot-packet-dispatch.js +3 -1
- package/dist/runtime/spots/spot-serial-executor.d.ts +2 -1
- package/dist/runtime/spots/spot-subscription-dispatch.js +3 -1
- package/dist/runtime/spots/spot-timer.d.ts +1 -1
- package/dist/runtime/streams/actor-session-binding-registry.d.ts +9 -3
- package/dist/runtime/streams/actor-session-binding-registry.js +17 -0
- package/dist/runtime/streams/index.d.ts +3 -3
- package/dist/runtime/streams/index.js +22 -2
- package/dist/runtime/streams/managed-stream.js +2 -1
- package/dist/runtime/streams/session-calls.d.ts +1 -1
- package/dist/runtime/streams/session-calls.js +3 -9
- package/dist/runtime/streams/session-context.d.ts +2 -2
- package/dist/runtime/streams/session-context.js +2 -2
- package/dist/runtime/streams/stream-session-runtime.d.ts +1 -0
- package/dist/runtime/streams/stream-session-runtime.js +37 -12
- package/package.json +3 -3
- package/src/contracts/Actors/ZLinkActor.ts +6 -0
- package/src/contracts/Actors/ZLinkSessionBindingIdentity.ts +7 -0
- package/src/contracts/Actors/index.ts +1 -0
- package/src/contracts/Common/payload-slab.ts +29 -0
- package/src/contracts/Handlers/Contexts.ts +1 -0
- package/src/internal.ts +17 -0
- package/src/runtime/actors/actor-client.ts +48 -23
- package/src/runtime/actors/actor-mailbox.ts +1 -1
- package/src/runtime/actors/actor-runtime-state.ts +1 -1
- package/src/runtime/actors/spot-actor-dispatch.ts +4 -3
- package/src/runtime/application-jobs/application-ingress-record-owner.ts +4 -0
- package/src/runtime/backend/contracts/index.ts +2 -0
- package/src/runtime/backend/mesh-completion-table.ts +2 -0
- package/src/runtime/backend/node/node-mesh-backend-adapter.ts +5 -1
- package/src/runtime/backend/node/node-raw-binding-port.ts +20 -7
- package/src/runtime/backend/node/node-raw-mesh-backend.ts +18 -10
- package/src/runtime/channels/channel-dispatch-pipeline.ts +6 -5
- package/src/runtime/channels/channel-dispatchers.ts +39 -19
- package/src/runtime/channels/channel-envelope.ts +547 -20
- package/src/runtime/channels/channel-receive-loops.ts +93 -36
- package/src/runtime/channels/channel-runtime-lifecycle.ts +2 -2
- package/src/runtime/channels/channel-socket-registry.ts +3 -3
- package/src/runtime/diagnostics/runtime-metrics.ts +3 -1
- package/src/runtime/execution/serial-execution-queue.ts +13 -13
- package/src/runtime/foundation/raw-service-mesh-runtime.ts +40 -39
- package/src/runtime/foundation/service-liveness-registry.ts +1 -1
- package/src/runtime/foundation/service-mailbox.ts +6 -1
- package/src/runtime/foundation/service-runtime-contracts.ts +7 -1
- package/src/runtime/foundation/service-stateful-registry.ts +39 -14
- package/src/runtime/foundation/service-stateful-runtime.ts +87 -21
- package/src/runtime/foundation/service-stateful-wire-codec.ts +18 -0
- package/src/runtime/foundation/service-wire-constants.generated.ts +10 -0
- package/src/runtime/foundation/service-wire-m6a-codec.ts +29 -7
- package/src/runtime/host/actor-placement-coordinator.ts +1 -1
- package/src/runtime/host/actor-transfer-runtime.ts +3 -3
- package/src/runtime/host/application-job-queue.ts +1 -1
- package/src/runtime/host/index.ts +183 -44
- package/src/runtime/host/location-runtime-owner.ts +2 -2
- package/src/runtime/host/service-relocation-host-runtime.ts +101 -65
- package/src/runtime/host/spot-address-transport.ts +67 -61
- package/src/runtime/locations/auto-connect-loop.ts +24 -16
- package/src/runtime/locations/auto-connect-planner.ts +2 -5
- package/src/runtime/locations/auto-connect-reconciler.ts +37 -24
- package/src/runtime/locations/in-memory-location-store.ts +1 -74
- package/src/runtime/locations/location-store-repository.ts +32 -153
- package/src/runtime/locations/runtime.ts +16 -28
- package/src/runtime/messaging/creation-payload-codec.ts +3 -3
- package/src/runtime/protocol/service_wire_codec.generated.ts +18 -8
- package/src/runtime/protocol/service_wire_pilot_codec.generated.ts +1 -1
- package/src/runtime/spots/index.ts +28 -4
- package/src/runtime/spots/spot-actor-admission-coordinator.ts +1 -1
- package/src/runtime/spots/spot-actor-packet-dispatch.ts +16 -6
- package/src/runtime/spots/spot-actor-packet-drain.ts +21 -6
- package/src/runtime/spots/spot-entry-activation.ts +1 -1
- package/src/runtime/spots/spot-node-runtime-manager.ts +8 -6
- package/src/runtime/spots/spot-route-packet-dispatch.ts +8 -4
- package/src/runtime/spots/spot-routed-spot-packet-dispatch.ts +3 -1
- package/src/runtime/spots/spot-serial-executor.ts +1 -1
- package/src/runtime/spots/spot-subscription-dispatch.ts +3 -1
- package/src/runtime/spots/spot-timer.ts +1 -1
- package/src/runtime/streams/actor-session-binding-registry.ts +30 -3
- package/src/runtime/streams/index.ts +30 -4
- package/src/runtime/streams/managed-stream.ts +2 -1
- package/src/runtime/streams/session-calls.ts +9 -14
- package/src/runtime/streams/session-context.ts +6 -11
- package/src/runtime/streams/stream-session-runtime.ts +49 -20
|
@@ -893,7 +893,7 @@ export function decodeChannelHeader(
|
|
|
893
893
|
if (parts.length === 0) {
|
|
894
894
|
throw new ZLinkConfigurationException('Channel envelope header part is missing.');
|
|
895
895
|
}
|
|
896
|
-
return validateChannelHeader(parseChannelHeaderBytes(parts[0].data()), flowEnabled);
|
|
896
|
+
return validateChannelHeader(parseChannelHeaderBytes(parts[0].data(), flowEnabled), flowEnabled);
|
|
897
897
|
}
|
|
898
898
|
|
|
899
899
|
const CHANNEL_JSON_PARSE_OPTIONS = Object.freeze({ rejectPropertyName: isPrototypeKey });
|
|
@@ -902,31 +902,68 @@ function parseWireJson(payload: string, schema?: ZLinkJsonSchema): unknown {
|
|
|
902
902
|
return parseFrameworkJsonV1(payload, CHANNEL_JSON_PARSE_OPTIONS, schema);
|
|
903
903
|
}
|
|
904
904
|
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
905
|
+
const CHANNEL_HEADER_FIELD_NAMES = [
|
|
906
|
+
'',
|
|
907
|
+
'formatMarker',
|
|
908
|
+
'kind',
|
|
909
|
+
'channelName',
|
|
910
|
+
'messageName',
|
|
911
|
+
'contentType',
|
|
912
|
+
'correlationId',
|
|
913
|
+
'deadline',
|
|
914
|
+
'topic',
|
|
915
|
+
'errorCode',
|
|
916
|
+
'errorMessage',
|
|
917
|
+
'source',
|
|
918
|
+
'flowId',
|
|
919
|
+
'flowOrigin',
|
|
920
|
+
'metadata'
|
|
921
|
+
] as const;
|
|
922
|
+
const CHANNEL_HEADER_FIELD_COUNT = CHANNEL_HEADER_FIELD_NAMES.length - 1;
|
|
923
|
+
const CHANNEL_HEADER_INVALID_VALUE = Symbol('invalid channel header value');
|
|
924
|
+
const CANONICAL_CHANNEL_HEADER_PREFIX = '{"formatMarker":242,"kind":';
|
|
925
|
+
|
|
926
|
+
function parseChannelHeaderBytes(payload: Buffer, flowEnabled: boolean): unknown {
|
|
927
|
+
if (startsWithJson(payload, CANONICAL_CHANNEL_HEADER_PREFIX)) {
|
|
928
|
+
const reader = new CanonicalChannelHeaderReader(
|
|
929
|
+
payload,
|
|
930
|
+
CANONICAL_CHANNEL_HEADER_PREFIX.length
|
|
931
|
+
);
|
|
932
|
+
const canonical = tryParseCanonicalChannelHeader(reader, flowEnabled);
|
|
933
|
+
// Canonical ASCII headers need no UTF-8 pass; all other paths are checked
|
|
934
|
+
// before byte slices are decoded.
|
|
935
|
+
if (canonical !== undefined && !reader.sawNonAscii) return canonical;
|
|
936
|
+
if (canonical !== undefined) {
|
|
937
|
+
if (!isUtf8(payload)) {
|
|
938
|
+
throw new ZLinkFrameworkException(
|
|
939
|
+
ZLinkFrameworkErrorKind.ProtocolError,
|
|
940
|
+
'Channel envelope header must contain valid UTF-8.'
|
|
941
|
+
);
|
|
942
|
+
}
|
|
943
|
+
return canonical;
|
|
944
|
+
}
|
|
945
|
+
}
|
|
911
946
|
if (!isUtf8(payload)) {
|
|
912
947
|
throw new ZLinkFrameworkException(
|
|
913
948
|
ZLinkFrameworkErrorKind.ProtocolError,
|
|
914
949
|
'Channel envelope header must contain valid UTF-8.'
|
|
915
950
|
);
|
|
916
951
|
}
|
|
917
|
-
return
|
|
952
|
+
return new ChannelHeaderJsonReader(payload, flowEnabled).parse();
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
function startsWithJson(bytes: Buffer, value: string): boolean {
|
|
956
|
+
if (bytes.length < value.length) return false;
|
|
957
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
958
|
+
if (bytes[index] !== value.charCodeAt(index)) return false;
|
|
959
|
+
}
|
|
960
|
+
return true;
|
|
918
961
|
}
|
|
919
962
|
|
|
920
|
-
/**
|
|
921
|
-
* Accept the byte layout emitted by writeChannelHeader without materializing
|
|
922
|
-
* the enclosing JSON text. Any variation falls through to the established
|
|
923
|
-
* framework-json-v1 parser, which remains the sole authority for JSON
|
|
924
|
-
* failures and unknown properties. Header metadata accepts every valid string key.
|
|
925
|
-
*/
|
|
926
963
|
function tryParseCanonicalChannelHeader(
|
|
927
|
-
reader: CanonicalChannelHeaderReader
|
|
964
|
+
reader: CanonicalChannelHeaderReader,
|
|
965
|
+
flowEnabled: boolean
|
|
928
966
|
): Record<string, unknown> | undefined {
|
|
929
|
-
if (!reader.consume('{"formatMarker":242,"kind":')) return undefined;
|
|
930
967
|
const kind = reader.readSingleDigit();
|
|
931
968
|
if (kind === undefined || !reader.consume(',"channelName":')) return undefined;
|
|
932
969
|
const channelName = reader.readString();
|
|
@@ -963,9 +1000,12 @@ function tryParseCanonicalChannelHeader(
|
|
|
963
1000
|
if (!reader.readOptionalNullableString(CANONICAL_ERROR_MESSAGE, 'errorMessage', header))
|
|
964
1001
|
return undefined;
|
|
965
1002
|
if (!reader.readOptionalNullableString(CANONICAL_SOURCE, 'source', header)) return undefined;
|
|
1003
|
+
const metadata = reader.readMetadata();
|
|
1004
|
+
if (metadata === undefined) return undefined;
|
|
966
1005
|
// Empty metadata validates to the shared empty record.
|
|
967
|
-
if (
|
|
968
|
-
if (!reader.readOptionalString(CANONICAL_FLOW_ID, 'flowId', header))
|
|
1006
|
+
if (metadata !== NULL_JSON_STRING) header.metadata = metadata;
|
|
1007
|
+
if (!reader.readOptionalString(CANONICAL_FLOW_ID, 'flowId', header, flowEnabled))
|
|
1008
|
+
return undefined;
|
|
969
1009
|
if (!reader.readOptionalSingleDigit(CANONICAL_FLOW_ORIGIN, 'flowOrigin', header))
|
|
970
1010
|
return undefined;
|
|
971
1011
|
return reader.consume('}') && reader.atEnd() ? header : undefined;
|
|
@@ -984,7 +1024,12 @@ class CanonicalChannelHeaderReader {
|
|
|
984
1024
|
/** A string held a byte outside ASCII, so the caller must still check UTF-8. */
|
|
985
1025
|
sawNonAscii = false;
|
|
986
1026
|
|
|
987
|
-
constructor(
|
|
1027
|
+
constructor(
|
|
1028
|
+
private readonly bytes: Buffer,
|
|
1029
|
+
initialIndex = 0
|
|
1030
|
+
) {
|
|
1031
|
+
this.index = initialIndex;
|
|
1032
|
+
}
|
|
988
1033
|
|
|
989
1034
|
atEnd(): boolean {
|
|
990
1035
|
return this.index === this.bytes.length;
|
|
@@ -1053,6 +1098,37 @@ class CanonicalChannelHeaderReader {
|
|
|
1053
1098
|
return this.readString();
|
|
1054
1099
|
}
|
|
1055
1100
|
|
|
1101
|
+
readMetadata(): Record<string, unknown> | symbol | undefined {
|
|
1102
|
+
if (!this.consume(',"metadata":{')) return undefined;
|
|
1103
|
+
if (this.consume('}')) return NULL_JSON_STRING;
|
|
1104
|
+
const metadata: Record<string, unknown> = {};
|
|
1105
|
+
for (;;) {
|
|
1106
|
+
const key = this.readString();
|
|
1107
|
+
if (key === undefined) return undefined;
|
|
1108
|
+
if (Object.prototype.hasOwnProperty.call(metadata, key)) {
|
|
1109
|
+
throw new ZLinkFrameworkException(
|
|
1110
|
+
ZLinkFrameworkErrorKind.ProtocolError,
|
|
1111
|
+
'duplicate envelope metadata key'
|
|
1112
|
+
);
|
|
1113
|
+
}
|
|
1114
|
+
if (!this.consume(':')) return undefined;
|
|
1115
|
+
const value = this.readString();
|
|
1116
|
+
if (value === undefined) return undefined;
|
|
1117
|
+
if (key === '__proto__') {
|
|
1118
|
+
Object.defineProperty(metadata, key, {
|
|
1119
|
+
configurable: true,
|
|
1120
|
+
enumerable: true,
|
|
1121
|
+
value,
|
|
1122
|
+
writable: true
|
|
1123
|
+
});
|
|
1124
|
+
} else {
|
|
1125
|
+
metadata[key] = value;
|
|
1126
|
+
}
|
|
1127
|
+
if (this.consume('}')) return metadata;
|
|
1128
|
+
if (!this.consume(',')) return undefined;
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1056
1132
|
readOptionalNullableString(
|
|
1057
1133
|
token: string,
|
|
1058
1134
|
name: string,
|
|
@@ -1065,14 +1141,53 @@ class CanonicalChannelHeaderReader {
|
|
|
1065
1141
|
return true;
|
|
1066
1142
|
}
|
|
1067
1143
|
|
|
1068
|
-
readOptionalString(
|
|
1144
|
+
readOptionalString(
|
|
1145
|
+
token: string,
|
|
1146
|
+
name: string,
|
|
1147
|
+
target: Record<string, unknown>,
|
|
1148
|
+
decode = true
|
|
1149
|
+
): boolean {
|
|
1069
1150
|
if (!this.consume(token)) return true;
|
|
1151
|
+
if (!decode) return this.skipString();
|
|
1070
1152
|
const value = this.readString();
|
|
1071
1153
|
if (value === undefined) return false;
|
|
1072
1154
|
target[name] = value;
|
|
1073
1155
|
return true;
|
|
1074
1156
|
}
|
|
1075
1157
|
|
|
1158
|
+
private skipString(): boolean {
|
|
1159
|
+
if (this.bytes[this.index] !== 0x22) return false;
|
|
1160
|
+
this.index += 1;
|
|
1161
|
+
while (this.index < this.bytes.length) {
|
|
1162
|
+
const value = this.bytes[this.index++]!;
|
|
1163
|
+
if (value === 0x22) return true;
|
|
1164
|
+
if (value < 0x20) return false;
|
|
1165
|
+
if (value >= 0x80) this.sawNonAscii = true;
|
|
1166
|
+
if (value !== 0x5c) continue;
|
|
1167
|
+
if (this.index === this.bytes.length) return false;
|
|
1168
|
+
const escaped = this.bytes[this.index++]!;
|
|
1169
|
+
if (escaped === 0x75) {
|
|
1170
|
+
for (let count = 0; count < 4; count += 1) {
|
|
1171
|
+
if (this.index === this.bytes.length || !isHexDigit(this.bytes[this.index]!))
|
|
1172
|
+
return false;
|
|
1173
|
+
this.index += 1;
|
|
1174
|
+
}
|
|
1175
|
+
} else if (
|
|
1176
|
+
escaped !== 0x22 &&
|
|
1177
|
+
escaped !== 0x5c &&
|
|
1178
|
+
escaped !== 0x2f &&
|
|
1179
|
+
escaped !== 0x62 &&
|
|
1180
|
+
escaped !== 0x66 &&
|
|
1181
|
+
escaped !== 0x6e &&
|
|
1182
|
+
escaped !== 0x72 &&
|
|
1183
|
+
escaped !== 0x74
|
|
1184
|
+
) {
|
|
1185
|
+
return false;
|
|
1186
|
+
}
|
|
1187
|
+
}
|
|
1188
|
+
return false;
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1076
1191
|
readOptionalSingleDigit(token: string, name: string, target: Record<string, unknown>): boolean {
|
|
1077
1192
|
if (!this.consume(token)) return true;
|
|
1078
1193
|
const value = this.readSingleDigit();
|
|
@@ -1090,6 +1205,418 @@ function isHexDigit(value: number): boolean {
|
|
|
1090
1205
|
);
|
|
1091
1206
|
}
|
|
1092
1207
|
|
|
1208
|
+
/** Reads a noncanonical channel header directly from its UTF-8 bytes. */
|
|
1209
|
+
class ChannelHeaderJsonReader {
|
|
1210
|
+
private index = 0;
|
|
1211
|
+
private propertyName = '';
|
|
1212
|
+
private stringStart = 0;
|
|
1213
|
+
private stringEnd = 0;
|
|
1214
|
+
private stringHasEscapes = false;
|
|
1215
|
+
|
|
1216
|
+
constructor(
|
|
1217
|
+
private readonly bytes: Buffer,
|
|
1218
|
+
private readonly flowEnabled: boolean
|
|
1219
|
+
) {}
|
|
1220
|
+
|
|
1221
|
+
parse(): unknown {
|
|
1222
|
+
if (
|
|
1223
|
+
this.bytes.length >= 3 &&
|
|
1224
|
+
this.bytes[0] === 0xef &&
|
|
1225
|
+
this.bytes[1] === 0xbb &&
|
|
1226
|
+
this.bytes[2] === 0xbf
|
|
1227
|
+
) {
|
|
1228
|
+
throw new SyntaxError('framework-json-v1 does not allow a UTF-8 BOM.');
|
|
1229
|
+
}
|
|
1230
|
+
this.skipWhitespace();
|
|
1231
|
+
if (this.bytes[this.index] !== 0x7b) {
|
|
1232
|
+
this.skipValue();
|
|
1233
|
+
this.skipWhitespace();
|
|
1234
|
+
if (this.index !== this.bytes.length) this.fail('trailing data');
|
|
1235
|
+
return CHANNEL_HEADER_INVALID_VALUE;
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
const header: Record<string, unknown> = {};
|
|
1239
|
+
let seenFields = 0;
|
|
1240
|
+
this.index += 1;
|
|
1241
|
+
this.skipWhitespace();
|
|
1242
|
+
if (this.consumeByte(0x7d)) return this.finish(header);
|
|
1243
|
+
|
|
1244
|
+
for (;;) {
|
|
1245
|
+
const field = this.readPropertyName(true);
|
|
1246
|
+
const propertyName = this.propertyName;
|
|
1247
|
+
this.skipWhitespace();
|
|
1248
|
+
this.expectByte(0x3a, "':' after object property");
|
|
1249
|
+
this.skipWhitespace();
|
|
1250
|
+
|
|
1251
|
+
if (field !== 0) {
|
|
1252
|
+
const bit = 1 << field;
|
|
1253
|
+
if ((seenFields & bit) !== 0) {
|
|
1254
|
+
throw new ZLinkFrameworkException(
|
|
1255
|
+
ZLinkFrameworkErrorKind.ProtocolError,
|
|
1256
|
+
'duplicate envelope header field'
|
|
1257
|
+
);
|
|
1258
|
+
}
|
|
1259
|
+
seenFields |= bit;
|
|
1260
|
+
const name = CHANNEL_HEADER_FIELD_NAMES[field]!;
|
|
1261
|
+
if (!this.flowEnabled && (field === 12 || field === 13)) {
|
|
1262
|
+
this.skipValue();
|
|
1263
|
+
header[name] = undefined;
|
|
1264
|
+
} else if (field === 14) {
|
|
1265
|
+
header[name] = this.readMetadata();
|
|
1266
|
+
} else {
|
|
1267
|
+
header[name] = this.readValue(field === 1 || field === 2 || field === 13);
|
|
1268
|
+
}
|
|
1269
|
+
} else {
|
|
1270
|
+
const value = this.readValue();
|
|
1271
|
+
if (propertyName === '__proto__') {
|
|
1272
|
+
Object.defineProperty(header, propertyName, {
|
|
1273
|
+
configurable: true,
|
|
1274
|
+
enumerable: true,
|
|
1275
|
+
value,
|
|
1276
|
+
writable: true
|
|
1277
|
+
});
|
|
1278
|
+
} else {
|
|
1279
|
+
header[propertyName] = value;
|
|
1280
|
+
}
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
this.skipWhitespace();
|
|
1284
|
+
if (this.consumeByte(0x7d)) break;
|
|
1285
|
+
this.expectByte(0x2c, "',' or '}' after object property");
|
|
1286
|
+
this.skipWhitespace();
|
|
1287
|
+
}
|
|
1288
|
+
return this.finish(header);
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
private finish(header: Record<string, unknown>): Record<string, unknown> {
|
|
1292
|
+
this.skipWhitespace();
|
|
1293
|
+
if (this.index !== this.bytes.length) this.fail('trailing data');
|
|
1294
|
+
return header;
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
private readMetadata(): unknown {
|
|
1298
|
+
if (this.bytes[this.index] !== 0x7b) return this.readValue();
|
|
1299
|
+
this.index += 1;
|
|
1300
|
+
this.skipWhitespace();
|
|
1301
|
+
if (this.consumeByte(0x7d)) return undefined;
|
|
1302
|
+
const metadata: Record<string, unknown> = {};
|
|
1303
|
+
|
|
1304
|
+
for (;;) {
|
|
1305
|
+
const key = this.readString();
|
|
1306
|
+
if (Object.prototype.hasOwnProperty.call(metadata, key)) {
|
|
1307
|
+
throw new ZLinkFrameworkException(
|
|
1308
|
+
ZLinkFrameworkErrorKind.ProtocolError,
|
|
1309
|
+
'duplicate envelope metadata key'
|
|
1310
|
+
);
|
|
1311
|
+
}
|
|
1312
|
+
this.skipWhitespace();
|
|
1313
|
+
this.expectByte(0x3a, "':' after object property");
|
|
1314
|
+
this.skipWhitespace();
|
|
1315
|
+
const value = this.readValue();
|
|
1316
|
+
if (key === '__proto__') {
|
|
1317
|
+
Object.defineProperty(metadata, key, {
|
|
1318
|
+
configurable: true,
|
|
1319
|
+
enumerable: true,
|
|
1320
|
+
value,
|
|
1321
|
+
writable: true
|
|
1322
|
+
});
|
|
1323
|
+
} else {
|
|
1324
|
+
metadata[key] = value;
|
|
1325
|
+
}
|
|
1326
|
+
this.skipWhitespace();
|
|
1327
|
+
if (this.consumeByte(0x7d)) return metadata;
|
|
1328
|
+
this.expectByte(0x2c, "',' or '}' after object property");
|
|
1329
|
+
this.skipWhitespace();
|
|
1330
|
+
}
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1333
|
+
private readPropertyName(materializeUnknown = false): number {
|
|
1334
|
+
this.scanString();
|
|
1335
|
+
let field = 0;
|
|
1336
|
+
let decoded: string | undefined;
|
|
1337
|
+
if (this.stringHasEscapes) {
|
|
1338
|
+
decoded = this.decodeString();
|
|
1339
|
+
for (let candidate = 1; candidate <= CHANNEL_HEADER_FIELD_COUNT; candidate += 1) {
|
|
1340
|
+
if (CHANNEL_HEADER_FIELD_NAMES[candidate] === decoded) {
|
|
1341
|
+
field = candidate;
|
|
1342
|
+
break;
|
|
1343
|
+
}
|
|
1344
|
+
}
|
|
1345
|
+
} else {
|
|
1346
|
+
for (let candidate = 1; candidate <= CHANNEL_HEADER_FIELD_COUNT; candidate += 1) {
|
|
1347
|
+
if (
|
|
1348
|
+
this.matchesAscii(
|
|
1349
|
+
this.stringStart + 1,
|
|
1350
|
+
this.stringEnd - 1,
|
|
1351
|
+
CHANNEL_HEADER_FIELD_NAMES[candidate]!
|
|
1352
|
+
)
|
|
1353
|
+
) {
|
|
1354
|
+
field = candidate;
|
|
1355
|
+
break;
|
|
1356
|
+
}
|
|
1357
|
+
}
|
|
1358
|
+
}
|
|
1359
|
+
this.propertyName =
|
|
1360
|
+
field !== 0
|
|
1361
|
+
? CHANNEL_HEADER_FIELD_NAMES[field]!
|
|
1362
|
+
: (decoded ??
|
|
1363
|
+
(materializeUnknown
|
|
1364
|
+
? this.bytes.toString('utf8', this.stringStart + 1, this.stringEnd - 1)
|
|
1365
|
+
: ''));
|
|
1366
|
+
return field;
|
|
1367
|
+
}
|
|
1368
|
+
|
|
1369
|
+
private readValue(integerOnly = false): unknown {
|
|
1370
|
+
const value = this.bytes[this.index];
|
|
1371
|
+
if (value === 0x22) return this.readString();
|
|
1372
|
+
if (value === 0x7b) return this.readObject();
|
|
1373
|
+
if (value === 0x5b) return this.readArray();
|
|
1374
|
+
if (value === 0x74) {
|
|
1375
|
+
this.readLiteral('true');
|
|
1376
|
+
return true;
|
|
1377
|
+
}
|
|
1378
|
+
if (value === 0x66) {
|
|
1379
|
+
this.readLiteral('false');
|
|
1380
|
+
return false;
|
|
1381
|
+
}
|
|
1382
|
+
if (value === 0x6e) {
|
|
1383
|
+
this.readLiteral('null');
|
|
1384
|
+
return null;
|
|
1385
|
+
}
|
|
1386
|
+
return this.readNumber(integerOnly);
|
|
1387
|
+
}
|
|
1388
|
+
|
|
1389
|
+
private readObject(): Record<string, unknown> {
|
|
1390
|
+
this.index += 1;
|
|
1391
|
+
this.skipWhitespace();
|
|
1392
|
+
const object: Record<string, unknown> = {};
|
|
1393
|
+
if (this.consumeByte(0x7d)) return object;
|
|
1394
|
+
for (;;) {
|
|
1395
|
+
const key = this.readString();
|
|
1396
|
+
this.skipWhitespace();
|
|
1397
|
+
this.expectByte(0x3a, "':' after object property");
|
|
1398
|
+
this.skipWhitespace();
|
|
1399
|
+
const value = this.readValue();
|
|
1400
|
+
if (key === '__proto__') {
|
|
1401
|
+
Object.defineProperty(object, key, {
|
|
1402
|
+
configurable: true,
|
|
1403
|
+
enumerable: true,
|
|
1404
|
+
value,
|
|
1405
|
+
writable: true
|
|
1406
|
+
});
|
|
1407
|
+
} else {
|
|
1408
|
+
object[key] = value;
|
|
1409
|
+
}
|
|
1410
|
+
this.skipWhitespace();
|
|
1411
|
+
if (this.consumeByte(0x7d)) return object;
|
|
1412
|
+
this.expectByte(0x2c, "',' or '}' after object property");
|
|
1413
|
+
this.skipWhitespace();
|
|
1414
|
+
}
|
|
1415
|
+
}
|
|
1416
|
+
|
|
1417
|
+
private readArray(): unknown[] {
|
|
1418
|
+
this.index += 1;
|
|
1419
|
+
this.skipWhitespace();
|
|
1420
|
+
const array: unknown[] = [];
|
|
1421
|
+
if (this.consumeByte(0x5d)) return array;
|
|
1422
|
+
for (;;) {
|
|
1423
|
+
array.push(this.readValue());
|
|
1424
|
+
this.skipWhitespace();
|
|
1425
|
+
if (this.consumeByte(0x5d)) return array;
|
|
1426
|
+
this.expectByte(0x2c, "',' or ']' after array item");
|
|
1427
|
+
this.skipWhitespace();
|
|
1428
|
+
}
|
|
1429
|
+
}
|
|
1430
|
+
|
|
1431
|
+
private skipValue(): void {
|
|
1432
|
+
const value = this.bytes[this.index];
|
|
1433
|
+
if (value === 0x22) {
|
|
1434
|
+
this.scanString();
|
|
1435
|
+
return;
|
|
1436
|
+
}
|
|
1437
|
+
if (value === 0x7b) return this.skipObject();
|
|
1438
|
+
if (value === 0x5b) return this.skipArray();
|
|
1439
|
+
if (value === 0x74) return this.readLiteral('true');
|
|
1440
|
+
if (value === 0x66) return this.readLiteral('false');
|
|
1441
|
+
if (value === 0x6e) return this.readLiteral('null');
|
|
1442
|
+
this.readNumber();
|
|
1443
|
+
}
|
|
1444
|
+
|
|
1445
|
+
private skipObject(): void {
|
|
1446
|
+
this.index += 1;
|
|
1447
|
+
this.skipWhitespace();
|
|
1448
|
+
if (this.consumeByte(0x7d)) return;
|
|
1449
|
+
for (;;) {
|
|
1450
|
+
this.readPropertyName();
|
|
1451
|
+
this.skipWhitespace();
|
|
1452
|
+
this.expectByte(0x3a, "':' after object property");
|
|
1453
|
+
this.skipWhitespace();
|
|
1454
|
+
this.skipValue();
|
|
1455
|
+
this.skipWhitespace();
|
|
1456
|
+
if (this.consumeByte(0x7d)) return;
|
|
1457
|
+
this.expectByte(0x2c, "',' or '}' after object property");
|
|
1458
|
+
this.skipWhitespace();
|
|
1459
|
+
}
|
|
1460
|
+
}
|
|
1461
|
+
|
|
1462
|
+
private skipArray(): void {
|
|
1463
|
+
this.index += 1;
|
|
1464
|
+
this.skipWhitespace();
|
|
1465
|
+
if (this.consumeByte(0x5d)) return;
|
|
1466
|
+
for (;;) {
|
|
1467
|
+
this.skipValue();
|
|
1468
|
+
this.skipWhitespace();
|
|
1469
|
+
if (this.consumeByte(0x5d)) return;
|
|
1470
|
+
this.expectByte(0x2c, "',' or ']' after array item");
|
|
1471
|
+
this.skipWhitespace();
|
|
1472
|
+
}
|
|
1473
|
+
}
|
|
1474
|
+
|
|
1475
|
+
private readString(): string {
|
|
1476
|
+
this.scanString();
|
|
1477
|
+
return this.decodeString();
|
|
1478
|
+
}
|
|
1479
|
+
|
|
1480
|
+
private scanString(): void {
|
|
1481
|
+
if (this.bytes[this.index] !== 0x22) this.fail('string');
|
|
1482
|
+
this.stringStart = this.index++;
|
|
1483
|
+
this.stringHasEscapes = false;
|
|
1484
|
+
while (this.index < this.bytes.length) {
|
|
1485
|
+
const value = this.bytes[this.index++]!;
|
|
1486
|
+
if (value === 0x22) {
|
|
1487
|
+
this.stringEnd = this.index;
|
|
1488
|
+
return;
|
|
1489
|
+
}
|
|
1490
|
+
if (value < 0x20) this.fail('valid string character');
|
|
1491
|
+
if (value !== 0x5c) continue;
|
|
1492
|
+
this.stringHasEscapes = true;
|
|
1493
|
+
if (this.index === this.bytes.length) this.fail('valid string escape');
|
|
1494
|
+
const escaped = this.bytes[this.index++]!;
|
|
1495
|
+
if (escaped === 0x75) {
|
|
1496
|
+
for (let count = 0; count < 4; count += 1) {
|
|
1497
|
+
if (this.index === this.bytes.length || !isHexDigit(this.bytes[this.index]!))
|
|
1498
|
+
this.fail('four hexadecimal digits after \\u');
|
|
1499
|
+
this.index += 1;
|
|
1500
|
+
}
|
|
1501
|
+
} else if (
|
|
1502
|
+
escaped !== 0x22 &&
|
|
1503
|
+
escaped !== 0x5c &&
|
|
1504
|
+
escaped !== 0x2f &&
|
|
1505
|
+
escaped !== 0x62 &&
|
|
1506
|
+
escaped !== 0x66 &&
|
|
1507
|
+
escaped !== 0x6e &&
|
|
1508
|
+
escaped !== 0x72 &&
|
|
1509
|
+
escaped !== 0x74
|
|
1510
|
+
) {
|
|
1511
|
+
this.fail('valid string escape');
|
|
1512
|
+
}
|
|
1513
|
+
}
|
|
1514
|
+
this.fail('terminated string');
|
|
1515
|
+
}
|
|
1516
|
+
|
|
1517
|
+
private decodeString(): string {
|
|
1518
|
+
return this.stringHasEscapes
|
|
1519
|
+
? (JSON.parse(this.bytes.toString('utf8', this.stringStart, this.stringEnd)) as string)
|
|
1520
|
+
: this.bytes.toString('utf8', this.stringStart + 1, this.stringEnd - 1);
|
|
1521
|
+
}
|
|
1522
|
+
|
|
1523
|
+
private readNumber(integerOnly = false): number {
|
|
1524
|
+
const start = this.index;
|
|
1525
|
+
let hasFractionOrExponent = false;
|
|
1526
|
+
if (this.bytes[this.index] === 0x2d) this.index += 1;
|
|
1527
|
+
if (this.bytes[this.index] === 0x30) {
|
|
1528
|
+
this.index += 1;
|
|
1529
|
+
} else if (this.isDigitOneToNine(this.bytes[this.index])) {
|
|
1530
|
+
while (this.isDigit(this.bytes[this.index])) this.index += 1;
|
|
1531
|
+
} else {
|
|
1532
|
+
this.fail('JSON value');
|
|
1533
|
+
}
|
|
1534
|
+
if (this.bytes[this.index] === 0x2e) {
|
|
1535
|
+
hasFractionOrExponent = true;
|
|
1536
|
+
this.index += 1;
|
|
1537
|
+
if (!this.isDigit(this.bytes[this.index])) this.fail('digit after decimal point');
|
|
1538
|
+
while (this.isDigit(this.bytes[this.index])) this.index += 1;
|
|
1539
|
+
}
|
|
1540
|
+
if (this.bytes[this.index] === 0x65 || this.bytes[this.index] === 0x45) {
|
|
1541
|
+
hasFractionOrExponent = true;
|
|
1542
|
+
this.index += 1;
|
|
1543
|
+
if (this.bytes[this.index] === 0x2b || this.bytes[this.index] === 0x2d) this.index += 1;
|
|
1544
|
+
if (!this.isDigit(this.bytes[this.index])) this.fail('digit in exponent');
|
|
1545
|
+
while (this.isDigit(this.bytes[this.index])) this.index += 1;
|
|
1546
|
+
}
|
|
1547
|
+
if (integerOnly && hasFractionOrExponent) {
|
|
1548
|
+
throw new ZLinkFrameworkException(
|
|
1549
|
+
ZLinkFrameworkErrorKind.ProtocolError,
|
|
1550
|
+
'Channel envelope integer fields must use an integer literal.'
|
|
1551
|
+
);
|
|
1552
|
+
}
|
|
1553
|
+
if (integerOnly) {
|
|
1554
|
+
const negative = this.bytes[start] === 0x2d;
|
|
1555
|
+
const digitsStart = start + (negative ? 1 : 0);
|
|
1556
|
+
if (this.index - digitsStart <= 3) {
|
|
1557
|
+
let integer = 0;
|
|
1558
|
+
for (let offset = digitsStart; offset < this.index; offset += 1) {
|
|
1559
|
+
integer = integer * 10 + this.bytes[offset]! - 0x30;
|
|
1560
|
+
}
|
|
1561
|
+
return negative ? -integer : integer;
|
|
1562
|
+
}
|
|
1563
|
+
}
|
|
1564
|
+
const number = Number(this.bytes.toString('ascii', start, this.index));
|
|
1565
|
+
if (!Number.isFinite(number)) {
|
|
1566
|
+
throw new TypeError('framework-json-v1 only accepts finite JSON numbers.');
|
|
1567
|
+
}
|
|
1568
|
+
return number;
|
|
1569
|
+
}
|
|
1570
|
+
|
|
1571
|
+
private readLiteral(value: 'true' | 'false' | 'null'): void {
|
|
1572
|
+
for (let offset = 0; offset < value.length; offset += 1) {
|
|
1573
|
+
if (this.bytes[this.index + offset] !== value.charCodeAt(offset)) this.fail(value);
|
|
1574
|
+
}
|
|
1575
|
+
this.index += value.length;
|
|
1576
|
+
}
|
|
1577
|
+
|
|
1578
|
+
private matchesAscii(start: number, end: number, value: string): boolean {
|
|
1579
|
+
if (end - start !== value.length) return false;
|
|
1580
|
+
for (let offset = 0; offset < value.length; offset += 1) {
|
|
1581
|
+
if (this.bytes[start + offset] !== value.charCodeAt(offset)) return false;
|
|
1582
|
+
}
|
|
1583
|
+
return true;
|
|
1584
|
+
}
|
|
1585
|
+
|
|
1586
|
+
private isDigit(value: number | undefined): boolean {
|
|
1587
|
+
return value !== undefined && value >= 0x30 && value <= 0x39;
|
|
1588
|
+
}
|
|
1589
|
+
|
|
1590
|
+
private isDigitOneToNine(value: number | undefined): boolean {
|
|
1591
|
+
return value !== undefined && value >= 0x31 && value <= 0x39;
|
|
1592
|
+
}
|
|
1593
|
+
|
|
1594
|
+
private skipWhitespace(): void {
|
|
1595
|
+
while (
|
|
1596
|
+
this.bytes[this.index] === 0x20 ||
|
|
1597
|
+
this.bytes[this.index] === 0x09 ||
|
|
1598
|
+
this.bytes[this.index] === 0x0a ||
|
|
1599
|
+
this.bytes[this.index] === 0x0d
|
|
1600
|
+
) {
|
|
1601
|
+
this.index += 1;
|
|
1602
|
+
}
|
|
1603
|
+
}
|
|
1604
|
+
|
|
1605
|
+
private consumeByte(value: number): boolean {
|
|
1606
|
+
if (this.bytes[this.index] !== value) return false;
|
|
1607
|
+
this.index += 1;
|
|
1608
|
+
return true;
|
|
1609
|
+
}
|
|
1610
|
+
|
|
1611
|
+
private expectByte(value: number, expected: string): void {
|
|
1612
|
+
if (!this.consumeByte(value)) this.fail(expected);
|
|
1613
|
+
}
|
|
1614
|
+
|
|
1615
|
+
private fail(expected: string): never {
|
|
1616
|
+
throw new SyntaxError(`framework-json-v1 expected ${expected} at offset ${this.index}.`);
|
|
1617
|
+
}
|
|
1618
|
+
}
|
|
1619
|
+
|
|
1093
1620
|
function schemaForInboundChannelEnvelope(
|
|
1094
1621
|
header: ZLinkChannelEnvelopeHeader
|
|
1095
1622
|
): ZLinkJsonSchema | undefined {
|