@zlink-systems/framework 0.31.0 → 0.32.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (181) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/contracts/Actors/ZLinkActor.d.ts +2 -0
  3. package/dist/contracts/Actors/ZLinkSessionBindingIdentity.d.ts +6 -0
  4. package/dist/contracts/Actors/ZLinkSessionBindingIdentity.js +2 -0
  5. package/dist/contracts/Actors/index.d.ts +1 -0
  6. package/dist/contracts/Actors/index.js +1 -0
  7. package/dist/contracts/Common/payload-slab.d.ts +4 -0
  8. package/dist/contracts/Common/payload-slab.js +30 -0
  9. package/dist/contracts/Handlers/Contexts.d.ts +1 -0
  10. package/dist/internal.d.ts +9 -0
  11. package/dist/internal.js +23 -1
  12. package/dist/runtime/actors/actor-client.d.ts +1 -1
  13. package/dist/runtime/actors/actor-client.js +32 -23
  14. package/dist/runtime/actors/actor-context.js +1 -1
  15. package/dist/runtime/actors/actor-creation.js +1 -1
  16. package/dist/runtime/actors/actor-mailbox.d.ts +1 -1
  17. package/dist/runtime/actors/actor-runtime-state.d.ts +3 -2
  18. package/dist/runtime/actors/actor-runtime-state.js +24 -8
  19. package/dist/runtime/actors/index.d.ts +3 -3
  20. package/dist/runtime/actors/index.js +19 -32
  21. package/dist/runtime/actors/spot-actor-dispatch.d.ts +4 -2
  22. package/dist/runtime/actors/spot-actor-dispatch.js +2 -1
  23. package/dist/runtime/application-jobs/application-ingress-record-owner.d.ts +1 -0
  24. package/dist/runtime/application-jobs/application-ingress-record-owner.js +3 -0
  25. package/dist/runtime/backend/contracts/index.d.ts +2 -0
  26. package/dist/runtime/backend/mesh-completion-table.d.ts +1 -0
  27. package/dist/runtime/backend/mesh-completion-table.js +1 -0
  28. package/dist/runtime/backend/node/node-mesh-backend-adapter.js +5 -1
  29. package/dist/runtime/backend/node/node-raw-binding-port.d.ts +7 -2
  30. package/dist/runtime/backend/node/node-raw-binding-port.js +17 -11
  31. package/dist/runtime/backend/node/node-raw-mesh-backend.d.ts +2 -2
  32. package/dist/runtime/backend/node/node-raw-mesh-backend.js +15 -8
  33. package/dist/runtime/channels/channel-dispatch-pipeline.d.ts +7 -6
  34. package/dist/runtime/channels/channel-dispatch-pipeline.js +1 -0
  35. package/dist/runtime/channels/channel-dispatchers.d.ts +7 -3
  36. package/dist/runtime/channels/channel-dispatchers.js +27 -19
  37. package/dist/runtime/channels/channel-envelope.js +543 -23
  38. package/dist/runtime/channels/channel-receive-loops.d.ts +9 -6
  39. package/dist/runtime/channels/channel-receive-loops.js +59 -18
  40. package/dist/runtime/channels/channel-runtime-lifecycle.d.ts +22 -3
  41. package/dist/runtime/channels/channel-socket-registry.d.ts +22 -4
  42. package/dist/runtime/diagnostics/runtime-metrics.d.ts +1 -1
  43. package/dist/runtime/diagnostics/runtime-metrics.js +2 -1
  44. package/dist/runtime/execution/serial-execution-queue.d.ts +26 -13
  45. package/dist/runtime/foundation/raw-service-mesh-runtime.d.ts +27 -11
  46. package/dist/runtime/foundation/raw-service-mesh-runtime.js +29 -25
  47. package/dist/runtime/foundation/service-liveness-registry.d.ts +1 -1
  48. package/dist/runtime/foundation/service-mailbox.d.ts +3 -1
  49. package/dist/runtime/foundation/service-mailbox.js +4 -1
  50. package/dist/runtime/foundation/service-runtime-contracts.d.ts +3 -1
  51. package/dist/runtime/foundation/service-stateful-registry.d.ts +2 -0
  52. package/dist/runtime/foundation/service-stateful-registry.js +28 -12
  53. package/dist/runtime/foundation/service-stateful-runtime.d.ts +51 -19
  54. package/dist/runtime/foundation/service-stateful-runtime.js +54 -5
  55. package/dist/runtime/foundation/service-stateful-wire-codec.d.ts +3 -0
  56. package/dist/runtime/foundation/service-stateful-wire-codec.js +15 -0
  57. package/dist/runtime/foundation/service-wire-constants.generated.d.ts +1 -0
  58. package/dist/runtime/foundation/service-wire-constants.generated.js +12 -0
  59. package/dist/runtime/foundation/service-wire-m6a-codec.d.ts +2 -0
  60. package/dist/runtime/foundation/service-wire-m6a-codec.js +17 -7
  61. package/dist/runtime/host/actor-placement-coordinator.d.ts +1 -1
  62. package/dist/runtime/host/actor-transfer-runtime.d.ts +3 -3
  63. package/dist/runtime/host/application-job-queue.d.ts +1 -1
  64. package/dist/runtime/host/index.d.ts +89 -36
  65. package/dist/runtime/host/index.js +101 -7
  66. package/dist/runtime/host/location-runtime-owner.d.ts +2 -2
  67. package/dist/runtime/host/service-relocation-host-runtime.d.ts +181 -44
  68. package/dist/runtime/host/service-relocation-host-runtime.js +47 -13
  69. package/dist/runtime/host/spot-address-transport.js +50 -42
  70. package/dist/runtime/locations/actor-location-claims.js +9 -10
  71. package/dist/runtime/locations/auto-connect-loop.d.ts +0 -1
  72. package/dist/runtime/locations/auto-connect-loop.js +25 -19
  73. package/dist/runtime/locations/auto-connect-planner.js +2 -5
  74. package/dist/runtime/locations/auto-connect-reconciler.d.ts +1 -0
  75. package/dist/runtime/locations/auto-connect-reconciler.js +35 -23
  76. package/dist/runtime/locations/in-memory-location-store.d.ts +0 -2
  77. package/dist/runtime/locations/in-memory-location-store.js +1 -38
  78. package/dist/runtime/locations/location-store-repository.d.ts +0 -1
  79. package/dist/runtime/locations/location-store-repository.js +13 -115
  80. package/dist/runtime/locations/runtime.d.ts +3 -3
  81. package/dist/runtime/locations/runtime.js +12 -16
  82. package/dist/runtime/messaging/creation-payload-codec.js +2 -2
  83. package/dist/runtime/protocol/service_wire_codec.generated.d.ts +4 -0
  84. package/dist/runtime/protocol/service_wire_codec.generated.js +17 -7
  85. package/dist/runtime/spots/index.d.ts +20 -5
  86. package/dist/runtime/spots/index.js +14 -0
  87. package/dist/runtime/spots/spot-actor-admission-coordinator.d.ts +3 -3
  88. package/dist/runtime/spots/spot-actor-packet-dispatch.d.ts +1 -0
  89. package/dist/runtime/spots/spot-actor-packet-dispatch.js +13 -7
  90. package/dist/runtime/spots/spot-actor-packet-drain.d.ts +1 -0
  91. package/dist/runtime/spots/spot-actor-packet-drain.js +12 -5
  92. package/dist/runtime/spots/spot-entry-activation.d.ts +2 -1
  93. package/dist/runtime/spots/spot-node-runtime-manager.d.ts +8 -6
  94. package/dist/runtime/spots/spot-node-runtime-manager.js +2 -0
  95. package/dist/runtime/spots/spot-route-packet-dispatch.js +7 -4
  96. package/dist/runtime/spots/spot-routed-spot-packet-dispatch.js +3 -1
  97. package/dist/runtime/spots/spot-serial-executor.d.ts +2 -1
  98. package/dist/runtime/spots/spot-subscription-dispatch.js +3 -1
  99. package/dist/runtime/spots/spot-timer.d.ts +1 -1
  100. package/dist/runtime/streams/actor-session-binding-registry.d.ts +9 -3
  101. package/dist/runtime/streams/actor-session-binding-registry.js +17 -0
  102. package/dist/runtime/streams/index.d.ts +3 -3
  103. package/dist/runtime/streams/index.js +22 -2
  104. package/dist/runtime/streams/managed-stream.js +2 -1
  105. package/dist/runtime/streams/session-calls.d.ts +1 -1
  106. package/dist/runtime/streams/session-calls.js +3 -9
  107. package/dist/runtime/streams/session-context.d.ts +2 -2
  108. package/dist/runtime/streams/session-context.js +2 -2
  109. package/dist/runtime/streams/stream-session-runtime.d.ts +1 -0
  110. package/dist/runtime/streams/stream-session-runtime.js +37 -12
  111. package/package.json +3 -3
  112. package/src/contracts/Actors/ZLinkActor.ts +6 -0
  113. package/src/contracts/Actors/ZLinkSessionBindingIdentity.ts +7 -0
  114. package/src/contracts/Actors/index.ts +1 -0
  115. package/src/contracts/Common/payload-slab.ts +29 -0
  116. package/src/contracts/Handlers/Contexts.ts +1 -0
  117. package/src/internal.ts +17 -0
  118. package/src/runtime/actors/actor-client.ts +48 -23
  119. package/src/runtime/actors/actor-context.ts +1 -1
  120. package/src/runtime/actors/actor-creation.ts +1 -1
  121. package/src/runtime/actors/actor-mailbox.ts +1 -1
  122. package/src/runtime/actors/actor-runtime-state.ts +28 -10
  123. package/src/runtime/actors/index.ts +30 -35
  124. package/src/runtime/actors/spot-actor-dispatch.ts +4 -3
  125. package/src/runtime/application-jobs/application-ingress-record-owner.ts +4 -0
  126. package/src/runtime/backend/contracts/index.ts +2 -0
  127. package/src/runtime/backend/mesh-completion-table.ts +2 -0
  128. package/src/runtime/backend/node/node-mesh-backend-adapter.ts +5 -1
  129. package/src/runtime/backend/node/node-raw-binding-port.ts +20 -7
  130. package/src/runtime/backend/node/node-raw-mesh-backend.ts +18 -10
  131. package/src/runtime/channels/channel-dispatch-pipeline.ts +6 -5
  132. package/src/runtime/channels/channel-dispatchers.ts +39 -19
  133. package/src/runtime/channels/channel-envelope.ts +547 -20
  134. package/src/runtime/channels/channel-receive-loops.ts +93 -36
  135. package/src/runtime/channels/channel-runtime-lifecycle.ts +2 -2
  136. package/src/runtime/channels/channel-socket-registry.ts +3 -3
  137. package/src/runtime/diagnostics/runtime-metrics.ts +3 -1
  138. package/src/runtime/execution/serial-execution-queue.ts +13 -13
  139. package/src/runtime/foundation/raw-service-mesh-runtime.ts +40 -39
  140. package/src/runtime/foundation/service-liveness-registry.ts +1 -1
  141. package/src/runtime/foundation/service-mailbox.ts +6 -1
  142. package/src/runtime/foundation/service-runtime-contracts.ts +7 -1
  143. package/src/runtime/foundation/service-stateful-registry.ts +39 -14
  144. package/src/runtime/foundation/service-stateful-runtime.ts +87 -21
  145. package/src/runtime/foundation/service-stateful-wire-codec.ts +18 -0
  146. package/src/runtime/foundation/service-wire-constants.generated.ts +10 -0
  147. package/src/runtime/foundation/service-wire-m6a-codec.ts +29 -7
  148. package/src/runtime/host/actor-placement-coordinator.ts +1 -1
  149. package/src/runtime/host/actor-transfer-runtime.ts +3 -3
  150. package/src/runtime/host/application-job-queue.ts +1 -1
  151. package/src/runtime/host/index.ts +183 -44
  152. package/src/runtime/host/location-runtime-owner.ts +2 -2
  153. package/src/runtime/host/service-relocation-host-runtime.ts +101 -65
  154. package/src/runtime/host/spot-address-transport.ts +67 -61
  155. package/src/runtime/locations/actor-location-claims.ts +14 -14
  156. package/src/runtime/locations/auto-connect-loop.ts +24 -16
  157. package/src/runtime/locations/auto-connect-planner.ts +2 -5
  158. package/src/runtime/locations/auto-connect-reconciler.ts +37 -24
  159. package/src/runtime/locations/in-memory-location-store.ts +1 -74
  160. package/src/runtime/locations/location-store-repository.ts +32 -153
  161. package/src/runtime/locations/runtime.ts +16 -28
  162. package/src/runtime/messaging/creation-payload-codec.ts +3 -3
  163. package/src/runtime/protocol/service_wire_codec.generated.ts +18 -8
  164. package/src/runtime/protocol/service_wire_pilot_codec.generated.ts +1 -1
  165. package/src/runtime/spots/index.ts +28 -4
  166. package/src/runtime/spots/spot-actor-admission-coordinator.ts +1 -1
  167. package/src/runtime/spots/spot-actor-packet-dispatch.ts +16 -6
  168. package/src/runtime/spots/spot-actor-packet-drain.ts +21 -6
  169. package/src/runtime/spots/spot-entry-activation.ts +1 -1
  170. package/src/runtime/spots/spot-node-runtime-manager.ts +8 -6
  171. package/src/runtime/spots/spot-route-packet-dispatch.ts +8 -4
  172. package/src/runtime/spots/spot-routed-spot-packet-dispatch.ts +3 -1
  173. package/src/runtime/spots/spot-serial-executor.ts +1 -1
  174. package/src/runtime/spots/spot-subscription-dispatch.ts +3 -1
  175. package/src/runtime/spots/spot-timer.ts +1 -1
  176. package/src/runtime/streams/actor-session-binding-registry.ts +30 -3
  177. package/src/runtime/streams/index.ts +30 -4
  178. package/src/runtime/streams/managed-stream.ts +2 -1
  179. package/src/runtime/streams/session-calls.ts +9 -14
  180. package/src/runtime/streams/session-context.ts +6 -11
  181. 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
- function parseChannelHeaderBytes(payload: Buffer): unknown {
906
- const reader = new CanonicalChannelHeaderReader(payload);
907
- const canonical = tryParseCanonicalChannelHeader(reader);
908
- // An all-ASCII canonical header is valid UTF-8 by construction; any other
909
- // header is checked before it is decoded.
910
- if (canonical !== undefined && !reader.sawNonAscii) return canonical;
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 canonical ?? parseFrameworkJsonV1(payload.toString());
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 (!reader.consume(',"metadata":{}')) return undefined;
968
- if (!reader.readOptionalString(CANONICAL_FLOW_ID, 'flowId', header)) return undefined;
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(private readonly bytes: Buffer) {}
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(token: string, name: string, target: Record<string, unknown>): boolean {
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 {