cesr-ts 0.2.3 → 0.3.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 (91) hide show
  1. package/README.md +36 -3
  2. package/esm/src/adapters/async-iterable.js +9 -9
  3. package/esm/src/annotate/annotator.js +18 -10
  4. package/esm/src/annotate/comments.js +3 -6
  5. package/esm/src/annotate/render.js +123 -24
  6. package/esm/src/bench/parser-benchmark.js +134 -0
  7. package/esm/src/core/bytes.js +6 -0
  8. package/esm/src/core/errors.js +24 -0
  9. package/esm/src/core/parser-attachment-collector.js +154 -0
  10. package/esm/src/core/parser-constants.js +74 -0
  11. package/esm/src/core/parser-deferred-frames.js +73 -0
  12. package/esm/src/core/parser-engine.js +128 -505
  13. package/esm/src/core/parser-frame-parser.js +643 -0
  14. package/esm/src/core/parser-policy.js +137 -0
  15. package/esm/src/core/parser-stream-state.js +62 -0
  16. package/esm/src/core/recovery-diagnostics.js +25 -0
  17. package/esm/src/index.js +4 -0
  18. package/esm/src/parser/attachment-fallback-policy.js +142 -0
  19. package/esm/src/parser/group-dispatch.js +547 -233
  20. package/esm/src/primitives/counter.js +4 -5
  21. package/esm/src/primitives/mapper.js +126 -45
  22. package/esm/src/primitives/matter.js +1 -1
  23. package/esm/src/router/router-stub.js +6 -6
  24. package/esm/src/serder/serder.js +44 -7
  25. package/esm/src/serder/smell.js +2 -1
  26. package/esm/src/tables/counter-version-registry.js +201 -0
  27. package/esm/src/version.js +2 -2
  28. package/package.json +3 -1
  29. package/types/_dnt.polyfills.d.ts +5 -0
  30. package/types/_dnt.polyfills.d.ts.map +1 -1
  31. package/types/src/adapters/async-iterable.d.ts +2 -2
  32. package/types/src/adapters/async-iterable.d.ts.map +1 -1
  33. package/types/src/adapters/effection.d.ts +2 -2
  34. package/types/src/adapters/effection.d.ts.map +1 -1
  35. package/types/src/annotate/annotator.d.ts.map +1 -1
  36. package/types/src/annotate/comments.d.ts.map +1 -1
  37. package/types/src/annotate/render.d.ts +8 -2
  38. package/types/src/annotate/render.d.ts.map +1 -1
  39. package/types/src/annotate/types.d.ts +2 -2
  40. package/types/src/annotate/types.d.ts.map +1 -1
  41. package/types/src/bench/parser-benchmark.d.ts +70 -0
  42. package/types/src/bench/parser-benchmark.d.ts.map +1 -0
  43. package/types/src/core/bytes.d.ts +6 -0
  44. package/types/src/core/bytes.d.ts.map +1 -1
  45. package/types/src/core/errors.d.ts +10 -0
  46. package/types/src/core/errors.d.ts.map +1 -1
  47. package/types/src/core/parser-attachment-collector.d.ts +51 -0
  48. package/types/src/core/parser-attachment-collector.d.ts.map +1 -0
  49. package/types/src/core/parser-constants.d.ts +30 -0
  50. package/types/src/core/parser-constants.d.ts.map +1 -0
  51. package/types/src/core/parser-deferred-frames.d.ts +38 -0
  52. package/types/src/core/parser-deferred-frames.d.ts.map +1 -0
  53. package/types/src/core/parser-engine.d.ts +53 -44
  54. package/types/src/core/parser-engine.d.ts.map +1 -1
  55. package/types/src/core/parser-frame-parser.d.ts +89 -0
  56. package/types/src/core/parser-frame-parser.d.ts.map +1 -0
  57. package/types/src/core/parser-policy.d.ts +27 -0
  58. package/types/src/core/parser-policy.d.ts.map +1 -0
  59. package/types/src/core/parser-stream-state.d.ts +30 -0
  60. package/types/src/core/parser-stream-state.d.ts.map +1 -0
  61. package/types/src/core/recovery-diagnostics.d.ts +59 -0
  62. package/types/src/core/recovery-diagnostics.d.ts.map +1 -0
  63. package/types/src/core/types.d.ts +61 -7
  64. package/types/src/core/types.d.ts.map +1 -1
  65. package/types/src/index.d.ts +4 -0
  66. package/types/src/index.d.ts.map +1 -1
  67. package/types/src/parser/attachment-fallback-policy.d.ts +78 -0
  68. package/types/src/parser/attachment-fallback-policy.d.ts.map +1 -0
  69. package/types/src/parser/group-dispatch.d.ts +85 -15
  70. package/types/src/parser/group-dispatch.d.ts.map +1 -1
  71. package/types/src/primitives/aggor.d.ts +2 -2
  72. package/types/src/primitives/aggor.d.ts.map +1 -1
  73. package/types/src/primitives/blinder.d.ts +2 -2
  74. package/types/src/primitives/blinder.d.ts.map +1 -1
  75. package/types/src/primitives/counter.d.ts.map +1 -1
  76. package/types/src/primitives/mapper.d.ts +44 -1
  77. package/types/src/primitives/mapper.d.ts.map +1 -1
  78. package/types/src/primitives/mediar.d.ts +2 -2
  79. package/types/src/primitives/mediar.d.ts.map +1 -1
  80. package/types/src/primitives/sealer.d.ts +2 -2
  81. package/types/src/primitives/sealer.d.ts.map +1 -1
  82. package/types/src/router/router-stub.d.ts +5 -5
  83. package/types/src/router/router-stub.d.ts.map +1 -1
  84. package/types/src/serder/serder.d.ts +2 -2
  85. package/types/src/serder/serder.d.ts.map +1 -1
  86. package/types/src/serder/serdery.d.ts +2 -2
  87. package/types/src/serder/serdery.d.ts.map +1 -1
  88. package/types/src/serder/smell.d.ts.map +1 -1
  89. package/types/src/tables/counter-version-registry.d.ts +90 -0
  90. package/types/src/tables/counter-version-registry.d.ts.map +1 -0
  91. package/types/src/version.d.ts +2 -2
@@ -1,189 +1,198 @@
1
- import { concatBytes } from "./bytes.js";
2
- import { sniff } from "../parser/cold-start.js";
3
- import { reapSerder } from "../serder/serdery.js";
4
- import { parseAttachmentGroup } from "../parser/attachment-parser.js";
5
- import { ColdStartError, DeserializeError, ParserError, ShortageError, UnknownCodeError, } from "./errors.js";
6
- import { parseCounter } from "../primitives/counter.js";
7
- import { parseMatter } from "../primitives/matter.js";
8
- import { parseVerser } from "../primitives/verser.js";
9
- import { parseIlker } from "../primitives/ilker.js";
10
- import { isLabelerCode, parseLabeler } from "../primitives/labeler.js";
11
- import { parseMapperBody } from "../primitives/mapper.js";
12
- import { CtrDexV1, CtrDexV2 } from "../tables/counter-codex.js";
13
- import { b64ToInt, intToB64 } from "./bytes.js";
14
- import { Kinds, Protocols } from "../tables/versions.js";
15
- const DEFAULT_VERSION = { major: 2, minor: 0 };
16
- const BODY_WITH_ATTACH_CODES = new Set([
17
- CtrDexV1.BodyWithAttachmentGroup,
18
- CtrDexV1.BigBodyWithAttachmentGroup,
19
- CtrDexV2.BodyWithAttachmentGroup,
20
- CtrDexV2.BigBodyWithAttachmentGroup,
21
- ]);
22
- const NON_NATIVE_BODY_CODES = new Set([
23
- CtrDexV1.NonNativeBodyGroup,
24
- CtrDexV1.BigNonNativeBodyGroup,
25
- CtrDexV2.NonNativeBodyGroup,
26
- CtrDexV2.BigNonNativeBodyGroup,
27
- ]);
28
- const FIX_BODY_CODES = new Set([
29
- CtrDexV2.FixBodyGroup,
30
- CtrDexV2.BigFixBodyGroup,
31
- ]);
32
- const MAP_BODY_CODES = new Set([
33
- CtrDexV2.MapBodyGroup,
34
- CtrDexV2.BigMapBodyGroup,
35
- ]);
36
- const GENUS_VERSION_CODE = CtrDexV2.KERIACDCGenusVersion;
37
- function tokenSize(token, cold) {
38
- return cold === "bny" ? token.fullSizeB2 : token.fullSize;
39
- }
40
- function quadletUnit(cold) {
41
- return cold === "bny" ? 3 : 4;
42
- }
43
- function isAttachmentDomain(cold) {
44
- return cold === "txt" || cold === "bny";
45
- }
46
- export class CesrParserCore {
1
+ import { createAttachmentVersionFallbackPolicy, } from "../parser/group-dispatch.js";
2
+ import { ParserError, ShortageError } from "./errors.js";
3
+ import { DEFAULT_VERSION } from "./parser-constants.js";
4
+ import { ParserStreamState } from "./parser-stream-state.js";
5
+ import { DeferredFrameLifecycle } from "./parser-deferred-frames.js";
6
+ import { FrameParser } from "./parser-frame-parser.js";
7
+ import { AttachmentCollector } from "./parser-attachment-collector.js";
8
+ import { createFrameBoundaryPolicy, } from "./parser-policy.js";
9
+ import { composeRecoveryDiagnosticObserver, } from "./recovery-diagnostics.js";
10
+ /**
11
+ * Streaming CESR parser for message-domain and CESR-native body-group streams.
12
+ * Handles chunk boundaries, pending frames, and attachment continuation.
13
+ */
14
+ export class CesrParser {
15
+ /**
16
+ * Compose parser collaborators with injected policy strategies.
17
+ *
18
+ * Policy precedence:
19
+ * 1) explicit policy objects from `ParserOptions`
20
+ * 2) derived defaults from legacy `framed` and strict/compat options
21
+ */
47
22
  constructor(options = {}) {
48
- Object.defineProperty(this, "state", {
23
+ /**
24
+ * Parser state contract (normative):
25
+ * `docs/design-docs/CESR_PARSER_STATE_MACHINE_CONTRACT.md`
26
+ */
27
+ Object.defineProperty(this, "frameBoundaryPolicy", {
28
+ enumerable: true,
29
+ configurable: true,
30
+ writable: true,
31
+ value: void 0
32
+ });
33
+ Object.defineProperty(this, "stream", {
49
34
  enumerable: true,
50
35
  configurable: true,
51
36
  writable: true,
52
- value: { buffer: new Uint8Array(0), offset: 0 }
37
+ value: void 0
53
38
  });
54
- Object.defineProperty(this, "framed", {
39
+ Object.defineProperty(this, "deferred", {
55
40
  enumerable: true,
56
41
  configurable: true,
57
42
  writable: true,
58
43
  value: void 0
59
44
  });
60
- Object.defineProperty(this, "attachmentDispatchMode", {
45
+ Object.defineProperty(this, "frameParser", {
61
46
  enumerable: true,
62
47
  configurable: true,
63
48
  writable: true,
64
49
  value: void 0
65
50
  });
66
- Object.defineProperty(this, "onAttachmentVersionFallback", {
51
+ Object.defineProperty(this, "attachmentCollector", {
67
52
  enumerable: true,
68
53
  configurable: true,
69
54
  writable: true,
70
55
  value: void 0
71
56
  });
72
- Object.defineProperty(this, "pendingFrame", {
57
+ Object.defineProperty(this, "recoveryDiagnosticObserver", {
73
58
  enumerable: true,
74
59
  configurable: true,
75
60
  writable: true,
76
- value: null
61
+ value: void 0
62
+ });
63
+ this.frameBoundaryPolicy = options.frameBoundaryPolicy ??
64
+ createFrameBoundaryPolicy(options.framed ?? false);
65
+ this.recoveryDiagnosticObserver = composeRecoveryDiagnosticObserver({
66
+ onRecoveryDiagnostic: options.onRecoveryDiagnostic,
67
+ onAttachmentVersionFallback: options.attachmentVersionFallbackPolicy
68
+ ? undefined
69
+ : options.onAttachmentVersionFallback,
70
+ });
71
+ const attachmentVersionFallbackPolicy = options.attachmentVersionFallbackPolicy ??
72
+ createAttachmentVersionFallbackPolicy({
73
+ mode: options.attachmentDispatchMode,
74
+ });
75
+ this.stream = new ParserStreamState(DEFAULT_VERSION);
76
+ this.deferred = new DeferredFrameLifecycle();
77
+ this.frameParser = new FrameParser({
78
+ frameBoundaryPolicy: this.frameBoundaryPolicy,
79
+ attachmentVersionFallbackPolicy,
80
+ onEnclosedFrames: (frames) => this.deferred.enqueueQueued(frames),
81
+ recoveryDiagnosticObserver: this.recoveryDiagnosticObserver,
82
+ });
83
+ this.attachmentCollector = new AttachmentCollector({
84
+ frameBoundaryPolicy: this.frameBoundaryPolicy,
85
+ attachmentVersionFallbackPolicy,
86
+ recoveryDiagnosticObserver: this.recoveryDiagnosticObserver,
87
+ isFrameBoundaryAhead: (input, version, cold) => this.frameParser.isFrameBoundaryAhead(input, version, cold),
77
88
  });
78
- this.framed = options.framed ?? false;
79
- this.attachmentDispatchMode = options.attachmentDispatchMode ?? "compat";
80
- this.onAttachmentVersionFallback = options.onAttachmentVersionFallback;
81
89
  }
82
90
  /** Append bytes and emit any complete parse events. */
83
91
  feed(chunk) {
84
- this.state.buffer = concatBytes(this.state.buffer, chunk);
92
+ this.stream.append(chunk);
85
93
  return this.drain();
86
94
  }
87
95
  /** Flush pending state at end-of-stream, emitting frame/error if needed. */
88
96
  flush() {
89
- const out = [];
90
- if (this.pendingFrame) {
91
- out.push({ type: "frame", frame: this.pendingFrame.frame });
92
- this.pendingFrame = null;
93
- }
94
- if (this.state.buffer.length === 0)
97
+ const out = this.deferred.flushDeferred();
98
+ if (this.stream.buffer.length === 0) {
95
99
  return out;
100
+ }
101
+ const remainder = this.stream.buffer.length;
96
102
  out.push({
97
103
  type: "error",
98
- error: new ShortageError(this.state.buffer.length + 1, this.state.buffer.length, this.state.offset),
104
+ error: new ShortageError(remainder + 1, remainder, this.stream.offset),
99
105
  });
106
+ this.stream.consume(remainder);
100
107
  return out;
101
108
  }
102
109
  reset() {
103
- this.state = { buffer: new Uint8Array(0), offset: 0 };
104
- this.pendingFrame = null;
110
+ this.stream.reset(DEFAULT_VERSION);
111
+ this.deferred.reset();
105
112
  }
106
113
  /**
107
- * Core streaming loop.
108
- * Keeps state machine behavior explicit: consume separators, parse a base
109
- * frame, then greedily collect trailing attachment groups.
114
+ * Core streaming loop. Emit pending frames, then queued frames, then new base frame with trailing attachments.
115
+ * Contract invariants:
116
+ * - `pendingFrame` is always resumed before any queued enclosed frame emits.
117
+ * - `queuedFrames` are emitted before starting a new parse from `buffer`.
110
118
  */
111
119
  drain() {
112
120
  const out = [];
113
- while (this.state.buffer.length > 0) {
121
+ while (this.stream.buffer.length > 0) {
114
122
  try {
115
- this.consumeLeadingAno();
116
- if (this.state.buffer.length === 0) {
123
+ this.stream.consumeLeadingAno();
124
+ if (this.stream.buffer.length === 0) {
117
125
  break;
118
126
  }
119
- if (this.pendingFrame) {
120
- if (!this.resumePendingFrame(out)) {
127
+ const pending = this.deferred.pending;
128
+ if (pending) {
129
+ const resume = this.attachmentCollector.resumePendingFrame(this.stream.buffer, pending.frame, pending.version, this.stream.streamVersion);
130
+ if (resume.consumed > 0) {
131
+ this.stream.consume(resume.consumed);
132
+ }
133
+ if (resume.emitPending) {
134
+ out.push({ type: "frame", frame: pending.frame });
135
+ this.deferred.clearPending();
136
+ }
137
+ if (!resume.shouldContinue) {
121
138
  break;
122
139
  }
123
140
  continue;
124
141
  }
125
- const base = this.parseFrame(this.state.buffer);
126
- this.consume(base.consumed);
127
- const attachments = [...base.frame.attachments];
128
- const version = base.version;
129
- let pausedForAttachmentShortage = false;
130
- try {
131
- while (this.state.buffer.length > 0) {
132
- const nextCold = sniff(this.state.buffer);
133
- if (nextCold === "msg")
142
+ if (this.deferred.hasQueued()) {
143
+ const frame = this.deferred.shiftQueued();
144
+ if (frame) {
145
+ out.push({ type: "frame", frame });
146
+ if (this.frameBoundaryPolicy.shouldStopAfterQueuedFrameEmission()) {
134
147
  break;
135
- if (nextCold === "ano") {
136
- this.consumeLeadingAno();
137
- continue;
138
148
  }
139
- if (!isAttachmentDomain(nextCold)) {
140
- throw new ColdStartError(`Unsupported attachment cold code ${nextCold}`);
141
- }
142
- const { group, consumed } = parseAttachmentGroup(this.state.buffer, version, nextCold, {
143
- mode: this.attachmentDispatchMode,
144
- onVersionFallback: this.onAttachmentVersionFallback,
145
- });
146
- attachments.push(group);
147
- this.consume(consumed);
148
- if (this.framed)
149
- break;
150
149
  }
150
+ continue;
151
151
  }
152
- catch (error) {
153
- if (error instanceof ShortageError) {
154
- this.pendingFrame = {
155
- frame: { serder: base.frame.serder, attachments },
156
- version,
157
- };
158
- pausedForAttachmentShortage = true;
159
- }
160
- else {
161
- throw error;
162
- }
152
+ const base = this.frameParser.parseFrame(this.stream.buffer, this.stream.streamVersion);
153
+ this.stream.consume(base.consumed);
154
+ this.stream.streamVersion = base.streamVersion;
155
+ const collected = this.attachmentCollector.collectTrailingAttachments(this.stream.buffer, base.version, base.streamVersion, base.frame.attachments);
156
+ if (collected.consumed > 0) {
157
+ this.stream.consume(collected.consumed);
163
158
  }
164
- if (pausedForAttachmentShortage) {
159
+ if (collected.pausedForShortage) {
160
+ this.deferred.setPending({ body: base.frame.body, attachments: collected.attachments }, base.version);
165
161
  break;
166
162
  }
167
163
  const completed = {
168
- serder: base.frame.serder,
169
- attachments,
164
+ body: base.frame.body,
165
+ attachments: collected.attachments,
170
166
  };
171
- if (!this.framed && attachments.length === 0 &&
172
- this.state.buffer.length === 0) {
173
- this.pendingFrame = { frame: completed, version };
167
+ // when in greedy attachment collection mode and a complete body has
168
+ // been received then set the current base frame as the pending frame to
169
+ // wait for attachments to arrive.
170
+ if (this.frameBoundaryPolicy.shouldDeferBodyOnlyFrame(completed.attachments.length, this.stream.buffer.length)) {
171
+ this.deferred.setPending(completed, base.version);
174
172
  break;
175
173
  }
174
+ // Once all attachments have arrived then emit the current completed frame
176
175
  out.push({ type: "frame", frame: completed });
177
- if (this.framed)
176
+ if (this.frameBoundaryPolicy.shouldStopAfterCompletedFrameEmission()) {
178
177
  break;
178
+ }
179
179
  }
180
180
  catch (error) {
181
+ // Shortage Errors are expected when waiting for bytes to arrive for
182
+ // either message bodies (base) frames or attachment frames.
181
183
  if (error instanceof ShortageError) {
182
184
  break;
183
185
  }
186
+ // For any other error, push a ParserError, emit an error frame, and reset
184
187
  const normalized = error instanceof ParserError
185
188
  ? error
186
- : new ParserError(String(error), this.state.offset);
189
+ : new ParserError(String(error), this.stream.offset);
190
+ this.recoveryDiagnosticObserver?.({
191
+ type: "parser-error-reset",
192
+ offset: this.stream.offset,
193
+ errorName: normalized.name,
194
+ reason: normalized.message,
195
+ });
187
196
  out.push({ type: "error", error: normalized });
188
197
  this.reset();
189
198
  break;
@@ -191,398 +200,12 @@ export class CesrParserCore {
191
200
  }
192
201
  return out;
193
202
  }
194
- resumePendingFrame(out) {
195
- if (!this.pendingFrame)
196
- return false;
197
- if (this.state.buffer.length === 0)
198
- return false;
199
- const nextCold = sniff(this.state.buffer);
200
- if (nextCold === "ano") {
201
- this.consumeLeadingAno();
202
- return this.state.buffer.length > 0;
203
- }
204
- if (nextCold === "msg") {
205
- out.push({ type: "frame", frame: this.pendingFrame.frame });
206
- this.pendingFrame = null;
207
- return true;
208
- }
209
- if (!isAttachmentDomain(nextCold)) {
210
- throw new ColdStartError(`Unsupported pending-frame continuation cold code ${nextCold}`);
211
- }
212
- if (this.pendingFrame.frame.serder.kind === Kinds.cesr) {
213
- const peek = parseCounter(this.state.buffer, this.pendingFrame.version, nextCold);
214
- if (BODY_WITH_ATTACH_CODES.has(peek.code) ||
215
- NON_NATIVE_BODY_CODES.has(peek.code) ||
216
- FIX_BODY_CODES.has(peek.code) ||
217
- MAP_BODY_CODES.has(peek.code) ||
218
- peek.code === GENUS_VERSION_CODE) {
219
- out.push({ type: "frame", frame: this.pendingFrame.frame });
220
- this.pendingFrame = null;
221
- return true;
222
- }
223
- }
224
- const { group, consumed } = parseAttachmentGroup(this.state.buffer, this.pendingFrame.version, nextCold, {
225
- mode: this.attachmentDispatchMode,
226
- onVersionFallback: this.onAttachmentVersionFallback,
227
- });
228
- this.pendingFrame.frame.attachments.push(group);
229
- this.consume(consumed);
230
- if (this.framed) {
231
- out.push({ type: "frame", frame: this.pendingFrame.frame });
232
- this.pendingFrame = null;
233
- return false;
234
- }
235
- if (this.state.buffer.length === 0) {
236
- return false;
237
- }
238
- const afterCold = sniff(this.state.buffer);
239
- if (afterCold === "ano") {
240
- this.consumeLeadingAno();
241
- return this.state.buffer.length > 0;
242
- }
243
- if (afterCold === "msg") {
244
- out.push({ type: "frame", frame: this.pendingFrame.frame });
245
- this.pendingFrame = null;
246
- return true;
247
- }
248
- return true;
249
- }
250
- consume(length) {
251
- this.state.buffer = this.state.buffer.slice(length);
252
- this.state.offset += length;
253
- }
254
- consumeLeadingAno() {
255
- while (this.state.buffer.length > 0 && sniff(this.state.buffer) === "ano") {
256
- this.consume(1);
257
- }
258
- }
259
- /**
260
- * Parse a single top-level frame start from current buffer head.
261
- * Supports message-domain serders and CESR-native body groups.
262
- */
263
- parseFrame(input, inheritedVersion = DEFAULT_VERSION) {
264
- let offset = 0;
265
- let activeVersion = inheritedVersion;
266
- let cold = sniff(input.slice(offset));
267
- while (cold === "ano") {
268
- offset += 1;
269
- if (input.length <= offset) {
270
- throw new ShortageError(offset + 1, input.length);
271
- }
272
- cold = sniff(input.slice(offset));
273
- }
274
- if (cold === "txt" || cold === "bny") {
275
- const peek = parseCounter(input.slice(offset), activeVersion, cold);
276
- if (peek.code === GENUS_VERSION_CODE) {
277
- offset += tokenSize(peek, cold);
278
- activeVersion = this.decodeVersionCounter(peek);
279
- if (input.length <= offset) {
280
- throw new ShortageError(offset + 1, input.length);
281
- }
282
- cold = sniff(input.slice(offset));
283
- }
284
- }
285
- if (cold === "msg") {
286
- const { serder, consumed } = reapSerder(input.slice(offset));
287
- return {
288
- frame: { serder, attachments: [] },
289
- consumed: offset + consumed,
290
- version: serder.gvrsn ?? serder.pvrsn,
291
- };
292
- }
293
- if (!isAttachmentDomain(cold)) {
294
- throw new ColdStartError(`Expected message or CESR body group at frame start but got ${cold}`);
295
- }
296
- const counter = parseCounter(input.slice(offset), activeVersion, cold);
297
- const headerSize = tokenSize(counter, cold);
298
- const unit = quadletUnit(cold);
299
- if (BODY_WITH_ATTACH_CODES.has(counter.code)) {
300
- return this.parseBodyWithAttachmentGroup(input, offset, headerSize, counter.count, unit, activeVersion);
301
- }
302
- if (NON_NATIVE_BODY_CODES.has(counter.code)) {
303
- return this.parseNonNativeBodyGroup(input, offset, headerSize, counter.count, unit, cold, activeVersion);
304
- }
305
- if (FIX_BODY_CODES.has(counter.code) || MAP_BODY_CODES.has(counter.code)) {
306
- return this.parseNativeBodyGroup(input, offset, headerSize, counter.count, unit, cold, activeVersion, counter.code);
307
- }
308
- throw new ColdStartError(`Unsupported body-group counter at frame start: ${counter.code}`);
309
- }
310
- /** Parse wrapped body+attachments payloads as a complete nested frame. */
311
- parseBodyWithAttachmentGroup(input, offset, headerSize, count, unit, version) {
312
- const payloadSize = count * unit;
313
- const total = headerSize + payloadSize;
314
- if (input.length < offset + total) {
315
- throw new ShortageError(offset + total, input.length);
316
- }
317
- const payload = input.slice(offset + headerSize, offset + total);
318
- const nested = this.parseCompleteFrame(payload, version, false);
319
- if (nested.consumed !== payload.length) {
320
- throw new ColdStartError("BodyWithAttachmentGroup payload did not parse to a complete frame");
321
- }
322
- return {
323
- frame: nested.frame,
324
- consumed: offset + total,
325
- version: nested.frame.serder.gvrsn ?? nested.frame.serder.pvrsn,
326
- };
327
- }
328
- /** Parse a non-native body group; keep opaque fallback when serder reap fails. */
329
- parseNonNativeBodyGroup(input, offset, headerSize, count, unit, cold, version) {
330
- const matter = parseMatter(input.slice(offset + headerSize), cold);
331
- const bodySize = tokenSize(matter, cold);
332
- const payloadSize = count * unit;
333
- if (payloadSize !== bodySize) {
334
- throw new ColdStartError(`NonNativeBodyGroup payload size mismatch: expected=${payloadSize} actual=${bodySize}`);
335
- }
336
- try {
337
- const { serder } = reapSerder(matter.raw);
338
- return {
339
- frame: { serder, attachments: [] },
340
- consumed: offset + headerSize + bodySize,
341
- version: serder.gvrsn ?? serder.pvrsn,
342
- };
343
- }
344
- catch (_error) {
345
- // Intentional recovery: NonNativeBodyGroup payload is opaque CESR body data.
346
- // If it cannot be interpreted as a KERI/ACDC Serder, preserve bytes and
347
- // continue with conservative metadata instead of failing the frame parse.
348
- return {
349
- frame: {
350
- serder: {
351
- raw: matter.raw,
352
- ked: null,
353
- proto: Protocols.keri,
354
- kind: Kinds.cesr,
355
- size: matter.raw.length,
356
- pvrsn: version,
357
- gvrsn: version,
358
- ilk: null,
359
- said: null,
360
- },
361
- attachments: [],
362
- },
363
- consumed: offset + headerSize + bodySize,
364
- version,
365
- };
366
- }
367
- }
368
- /** Parse fixed/map native body groups and extract semantic native fields. */
369
- parseNativeBodyGroup(input, offset, headerSize, count, unit, cold, version, bodyCode) {
370
- const payloadSize = count * unit;
371
- const total = headerSize + payloadSize;
372
- if (input.length < offset + total) {
373
- throw new ShortageError(offset + total, input.length);
374
- }
375
- const raw = input.slice(offset, offset + total);
376
- const metadata = this.extractNativeMetadata(raw, cold, version);
377
- const fields = this.extractNativeFields(raw, cold, version);
378
- return {
379
- frame: {
380
- serder: {
381
- raw,
382
- ked: null,
383
- proto: metadata.proto,
384
- kind: Kinds.cesr,
385
- size: raw.length,
386
- pvrsn: metadata.pvrsn,
387
- gvrsn: metadata.gvrsn,
388
- ilk: metadata.ilk,
389
- said: metadata.said,
390
- native: {
391
- bodyCode,
392
- fields,
393
- },
394
- },
395
- attachments: [],
396
- },
397
- consumed: offset + total,
398
- version,
399
- };
400
- }
401
- /** Decode genus-version counter suffix to active CESR version. */
402
- decodeVersionCounter(counter) {
403
- const triplet = counter.qb64.length >= 3
404
- ? counter.qb64.slice(-3)
405
- : intToB64(counter.count, 3);
406
- const majorRaw = b64ToInt(triplet[0] ?? "A");
407
- const minorRaw = b64ToInt(triplet[1] ?? "A");
408
- const major = majorRaw === 1 ? 1 : 2;
409
- return {
410
- major,
411
- minor: minorRaw,
412
- };
413
- }
414
- /**
415
- * Best-effort native metadata extraction.
416
- * This is intentionally advisory and may recover instead of failing hard.
417
- */
418
- extractNativeMetadata(raw, cold, fallbackVersion) {
419
- let offset = 0;
420
- let proto = Protocols.keri;
421
- let pvrsn = fallbackVersion;
422
- let gvrsn = fallbackVersion;
423
- let ilk = null;
424
- let said = null;
425
- try {
426
- const bodyCounter = parseCounter(raw, fallbackVersion, cold);
427
- offset += cold === "bny" ? bodyCounter.fullSizeB2 : bodyCounter.fullSize;
428
- // In map-body mode, labels may appear between semantic fields.
429
- // Metadata extraction skips those labels before reading core fields.
430
- if (MAP_BODY_CODES.has(bodyCounter.code)) {
431
- offset = this.skipNativeLabelers(raw, offset, cold);
432
- }
433
- const verser = parseVerser(raw.slice(offset), cold);
434
- offset += tokenSize(verser, cold);
435
- proto = verser.proto;
436
- pvrsn = verser.pvrsn;
437
- gvrsn = verser.gvrsn;
438
- if (MAP_BODY_CODES.has(bodyCounter.code)) {
439
- offset = this.skipNativeLabelers(raw, offset, cold);
440
- }
441
- const ilker = parseIlker(raw.slice(offset), cold);
442
- offset += tokenSize(ilker, cold);
443
- ilk = ilker.ilk;
444
- if (MAP_BODY_CODES.has(bodyCounter.code)) {
445
- offset = this.skipNativeLabelers(raw, offset, cold);
446
- }
447
- const saider = parseMatter(raw.slice(offset), cold);
448
- if (saider.code.startsWith("E")) {
449
- said = saider.qb64;
450
- }
451
- }
452
- catch (_error) {
453
- // Intentional recovery: native metadata extraction is advisory only.
454
- // Strict token validation happens in extractNativeFields/mapper parsing.
455
- }
456
- return { proto, pvrsn, gvrsn, ilk, said };
457
- }
458
- /** Skip consecutive native label tokens (V/W) from an offset. */
459
- skipNativeLabelers(raw, offset, cold) {
460
- let out = offset;
461
- while (out < raw.length) {
462
- const item = parseMatter(raw.slice(out), cold);
463
- if (!isLabelerCode(item.code)) {
464
- break;
465
- }
466
- out += tokenSize(item, cold);
467
- }
468
- return out;
469
- }
470
- /** Strict native field extraction for annotation and higher-level processing. */
471
- extractNativeFields(raw, cold, fallbackVersion) {
472
- const bodyCounter = parseCounter(raw, fallbackVersion, cold);
473
- if (MAP_BODY_CODES.has(bodyCounter.code)) {
474
- const mapper = parseMapperBody(raw, fallbackVersion, cold);
475
- return mapper.fields.map((field) => ({
476
- label: field.label,
477
- code: field.code,
478
- qb64: field.qb64,
479
- }));
480
- }
481
- const fields = [];
482
- const total = tokenSize(bodyCounter, cold);
483
- const payloadBytes = bodyCounter.count * quadletUnit(cold);
484
- const start = total;
485
- const end = start + payloadBytes;
486
- let offset = start;
487
- let pendingLabel = null;
488
- while (offset < end) {
489
- const at = raw.slice(offset, end);
490
- const ctr = this.tryParseCounter(at, fallbackVersion, cold);
491
- if (ctr) {
492
- const size = tokenSize(ctr, cold);
493
- offset += size;
494
- fields.push({
495
- label: pendingLabel,
496
- code: ctr.code,
497
- qb64: ctr.qb64,
498
- });
499
- pendingLabel = null;
500
- continue;
501
- }
502
- const token = parseMatter(at, cold);
503
- const size = tokenSize(token, cold);
504
- offset += size;
505
- if (isLabelerCode(token.code)) {
506
- pendingLabel = parseLabeler(at, cold).label;
507
- continue;
508
- }
509
- fields.push({
510
- label: pendingLabel,
511
- code: token.code,
512
- qb64: token.qb64,
513
- });
514
- pendingLabel = null;
515
- }
516
- if (offset !== end) {
517
- throw new ShortageError(end, offset);
518
- }
519
- if (pendingLabel !== null) {
520
- throw new ColdStartError("Dangling native map label without value");
521
- }
522
- return fields;
523
- }
524
- /** Counter probe with ordered version fallback; throws on shortage. */
525
- tryParseCounter(input, version, cold) {
526
- const attempts = [
527
- version,
528
- { major: 2, minor: 0 },
529
- { major: 1, minor: 0 },
530
- ];
531
- for (const attempt of attempts) {
532
- try {
533
- return parseCounter(input, attempt, cold);
534
- }
535
- catch (error) {
536
- if (error instanceof ShortageError) {
537
- throw error;
538
- }
539
- if (error instanceof UnknownCodeError ||
540
- error instanceof DeserializeError) {
541
- continue;
542
- }
543
- throw error;
544
- }
545
- }
546
- return null;
547
- }
548
- /** Parse one complete frame plus optional attachments from a standalone slice. */
549
- parseCompleteFrame(input, inheritedVersion = DEFAULT_VERSION, stopAtNextMessage = true) {
550
- const base = this.parseFrame(input, inheritedVersion);
551
- const version = base.version;
552
- const attachments = [];
553
- let offset = base.consumed;
554
- while (offset < input.length) {
555
- const nextCold = sniff(input.slice(offset));
556
- if (nextCold === "ano") {
557
- offset += 1;
558
- continue;
559
- }
560
- if (nextCold === "msg") {
561
- if (stopAtNextMessage)
562
- break;
563
- throw new ColdStartError("Enclosed frame payload encountered unexpected nested message start");
564
- }
565
- if (!isAttachmentDomain(nextCold)) {
566
- throw new ColdStartError(`Unsupported attachment cold code ${nextCold}`);
567
- }
568
- const { group, consumed } = parseAttachmentGroup(input.slice(offset), version, nextCold, {
569
- mode: this.attachmentDispatchMode,
570
- onVersionFallback: this.onAttachmentVersionFallback,
571
- });
572
- attachments.push(group);
573
- offset += consumed;
574
- if (this.framed)
575
- break;
576
- }
577
- return {
578
- frame: { serder: base.frame.serder, attachments },
579
- consumed: offset,
580
- };
581
- }
582
203
  }
204
+ /** CESR parser factory function. */
583
205
  export function createParser(options = {}) {
584
- return new CesrParserCore(options);
206
+ return new CesrParser(options);
585
207
  }
208
+ /** Parse a buffer of bytes into a list of frames. */
586
209
  export function parseBytes(bytes, options = {}) {
587
210
  const parser = createParser(options);
588
211
  return [...parser.feed(bytes), ...parser.flush()];