@cosyte/astm 0.0.11 → 0.0.14
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/CHANGELOG.md +196 -0
- package/README.md +19 -2
- package/dist/index.cjs +47 -4
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +206 -45
- package/dist/index.d.ts +206 -45
- package/dist/index.mjs +47 -4
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -1103,7 +1103,7 @@ declare function detectFraming(bytes: Uint8Array, options?: DetectFramingOptions
|
|
|
1103
1103
|
* Any warning code from any of the three registries: the record layer
|
|
1104
1104
|
* (`ASTM_RECORD_*` / `ASTM_NONSTANDARD_DELIMITERS` / `ASTM_UNKNOWN_ESCAPE_SEQUENCE`),
|
|
1105
1105
|
* the frame codec (`ASTM_FRAME_*`), or the LTP protocol layer (`ASTM_LTP_*`). A
|
|
1106
|
-
* {@link
|
|
1106
|
+
* {@link AstmQuirkTolerance} may *name* any of them, but the safety gate refuses all
|
|
1107
1107
|
* but the small, benign, record-layer subset, so the union exists mainly to let
|
|
1108
1108
|
* the gate reject a frame/LTP code with a precise, typed message.
|
|
1109
1109
|
*/
|
|
@@ -1136,7 +1136,7 @@ interface AstmProfileProvenance {
|
|
|
1136
1136
|
readonly note?: string;
|
|
1137
1137
|
}
|
|
1138
1138
|
/**
|
|
1139
|
-
* Optional structural narrowing for
|
|
1139
|
+
* Optional structural narrowing for an {@link AstmQuirkTolerance}. When present, the
|
|
1140
1140
|
* tolerance applies only to warnings whose PHI-free {@link AstmPosition} matches
|
|
1141
1141
|
* every provided field, so a profile can expect a deviation on one record type
|
|
1142
1142
|
* (e.g. an unknown escape only inside `R` result values) without blanket-tolerating
|
|
@@ -1911,7 +1911,7 @@ interface AstmMessage {
|
|
|
1911
1911
|
* The vendor profile that was active for this parse, when one applied: its
|
|
1912
1912
|
* `name` and resolved `lineage` (attribution only; the profile object itself is
|
|
1913
1913
|
* not embedded). Absent when no profile applied. A profile that tolerated a
|
|
1914
|
-
* deviation shows up as a `PROFILE_QUIRK_APPLIED` entry in {@link warnings}, not
|
|
1914
|
+
* deviation shows up as a `PROFILE_QUIRK_APPLIED` entry in {@link AstmMessage.warnings}, not
|
|
1915
1915
|
* here.
|
|
1916
1916
|
*/
|
|
1917
1917
|
readonly profile?: {
|
|
@@ -2071,15 +2071,43 @@ declare function attachComments(records: readonly AstmRecord[], warnings: AstmRe
|
|
|
2071
2071
|
* The serializer refuses: an embedded `CR`/`LF` is a typed {@link AstmSerializeError},
|
|
2072
2072
|
* never emitted raw.
|
|
2073
2073
|
*
|
|
2074
|
+
* **A value keeps the bytes the *frame* layer reserves, on purpose.** `STX`, `ETB`
|
|
2075
|
+
* and `ETX` are deliberately **not** refused here. They break a record only once it
|
|
2076
|
+
* is framed, and this layer returns a `string`, which is not yet on any wire: a
|
|
2077
|
+
* consumer on a raw transport, which is a real ASTM deployment this library models
|
|
2078
|
+
* (`detectFraming`), gets such a value back byte for byte through parse and emit,
|
|
2079
|
+
* measured on every modeled value. Refusing it here would take a
|
|
2080
|
+
* byte the caller genuinely supplied away from consumers who never frame anything.
|
|
2081
|
+
* `composeAstmFrames` is the total gate on the framed route, including through
|
|
2082
|
+
* `serializeFramedAstm`, and refuses it there (`ASTM_FRAME_RESERVED_BYTE`).
|
|
2083
|
+
* `CR`/`LF` are different: they end a *record*, so they corrupt this layer's own
|
|
2084
|
+
* output, which is why they are refused at this one.
|
|
2085
|
+
*
|
|
2086
|
+
* **One position is not a modeled value and does not keep the byte:** the surplus
|
|
2087
|
+
* of a header's delimiter declaration, where any control character is dropped
|
|
2088
|
+
* silently (see `declarationResidual` below, where that disposition is argued and
|
|
2089
|
+
* where it long predates the frame-layer refusal). So "the record layer carries
|
|
2090
|
+
* these bytes" is a statement about values, not about every byte of every line.
|
|
2091
|
+
*
|
|
2074
2092
|
* **Check the delimiter set before writing.** Three conditions are required for the
|
|
2075
2093
|
* emitted bytes to read back as the records that produced them (each separator exactly
|
|
2076
2094
|
* one character, no separator a `CR`/`LF`, no two separators the same character) and a
|
|
2077
2095
|
* set failing any of them is a typed {@link AstmSerializeError}
|
|
2078
2096
|
* (`ASTM_EMIT_INVALID_DELIMITERS`) rather than output this library's own parser would
|
|
2079
|
-
* reject or silently re-read with a different field tree. These are
|
|
2080
|
-
*
|
|
2081
|
-
*
|
|
2082
|
-
*
|
|
2097
|
+
* reject or silently re-read with a different field tree. These are conditions on the
|
|
2098
|
+
* **set alone**, so they are not a readback guarantee on their own: a set can pass all
|
|
2099
|
+
* three and still collide with a particular record's type letter, which is why that is
|
|
2100
|
+
* checked per record, on the bytes actually written
|
|
2101
|
+
* (`ASTM_EMIT_TYPE_LETTER_COLLISION`). That one is a **transcoding** condition, not
|
|
2102
|
+
* merely a caller's choice of set, so it is reachable on the default canonical path
|
|
2103
|
+
* too, when a record was read under a set other than the one being emitted.
|
|
2104
|
+
*
|
|
2105
|
+
* **What is still not guaranteed.** Together the two checks guarantee that every
|
|
2106
|
+
* record re-reads as its own *type*. They do not guarantee that every *field* of it
|
|
2107
|
+
* lands where it did: an escape sequence whose body is itself a delimiter is read as
|
|
2108
|
+
* one opaque atom, so that delimiter never becomes a boundary and the fields after it
|
|
2109
|
+
* shift. That is reported on the parse side (`ASTM_UNKNOWN_ESCAPE_SEQUENCE`) and is
|
|
2110
|
+
* not what these refusals cover.
|
|
2083
2111
|
*/
|
|
2084
2112
|
|
|
2085
2113
|
/**
|
|
@@ -2104,7 +2132,8 @@ declare class AstmSerializeError extends Error {
|
|
|
2104
2132
|
/**
|
|
2105
2133
|
* Stable discriminant. `ASTM_EMIT_UNENCODABLE_VALUE` for a `CR`/`LF` in a value;
|
|
2106
2134
|
* `ASTM_EMIT_INVALID_DELIMITERS` for a delimiter set that cannot be emitted
|
|
2107
|
-
* reversibly
|
|
2135
|
+
* reversibly; `ASTM_EMIT_TYPE_LETTER_COLLISION` for a record whose own type
|
|
2136
|
+
* letter the set being emitted with would escape away.
|
|
2108
2137
|
*/
|
|
2109
2138
|
readonly code: AstmSerializeErrorCode;
|
|
2110
2139
|
/** 0-based ordinal of the record within the message, when known. */
|
|
@@ -2119,8 +2148,14 @@ declare class AstmSerializeError extends Error {
|
|
|
2119
2148
|
* (`CR`/`LF`), which the escape codec has no mnemonic for.
|
|
2120
2149
|
* - `ASTM_EMIT_INVALID_DELIMITERS`: the delimiter set to emit against failed one
|
|
2121
2150
|
* of the three conditions readback requires (see {@link serializeAstmRecords}).
|
|
2122
|
-
* Those conditions are
|
|
2151
|
+
* Those conditions are checked against the *set alone*, so this code means a
|
|
2123
2152
|
* set was rejected, not that every unreversible set is.
|
|
2153
|
+
* - `ASTM_EMIT_TYPE_LETTER_COLLISION`: this record's type letter would not be the
|
|
2154
|
+
* first character of its own emitted line, so the record would read back as a
|
|
2155
|
+
* **different** record. Raised per record rather than per set, because whether
|
|
2156
|
+
* a set collides depends on which record is being written: a record read under
|
|
2157
|
+
* one delimiter set and written under another can hit it with **no** set passed
|
|
2158
|
+
* at all, so it is not avoided by emitting on the default canonical path.
|
|
2124
2159
|
*
|
|
2125
2160
|
* @example
|
|
2126
2161
|
* ```ts
|
|
@@ -2128,7 +2163,7 @@ declare class AstmSerializeError extends Error {
|
|
|
2128
2163
|
* const code: AstmSerializeErrorCode = "ASTM_EMIT_INVALID_DELIMITERS";
|
|
2129
2164
|
* ```
|
|
2130
2165
|
*/
|
|
2131
|
-
type AstmSerializeErrorCode = "ASTM_EMIT_UNENCODABLE_VALUE" | "ASTM_EMIT_INVALID_DELIMITERS";
|
|
2166
|
+
type AstmSerializeErrorCode = "ASTM_EMIT_UNENCODABLE_VALUE" | "ASTM_EMIT_INVALID_DELIMITERS" | "ASTM_EMIT_TYPE_LETTER_COLLISION";
|
|
2132
2167
|
/**
|
|
2133
2168
|
* Escape-encode one component leaf for spec-clean emit: the inverse of
|
|
2134
2169
|
* `decodeEscapes`. The **escape character itself is encoded first** (`&` → `&E&`)
|
|
@@ -2146,8 +2181,10 @@ type AstmSerializeErrorCode = "ASTM_EMIT_UNENCODABLE_VALUE" | "ASTM_EMIT_INVALID
|
|
|
2146
2181
|
* @returns The escaped component text.
|
|
2147
2182
|
* @throws {@link AstmSerializeError} for a `CR`/`LF` in the leaf
|
|
2148
2183
|
* (`ASTM_EMIT_UNENCODABLE_VALUE`), or for a delimiter set failing one of the
|
|
2149
|
-
* three conditions readback requires (`ASTM_EMIT_INVALID_DELIMITERS`)
|
|
2150
|
-
*
|
|
2184
|
+
* three conditions readback requires (`ASTM_EMIT_INVALID_DELIMITERS`). This
|
|
2185
|
+
* helper takes a leaf and no record, so it never raises
|
|
2186
|
+
* `ASTM_EMIT_TYPE_LETTER_COLLISION`: a caller assembling a record line out of it
|
|
2187
|
+
* is outside that check and gets no guarantee that a type letter survives.
|
|
2151
2188
|
* @example
|
|
2152
2189
|
* ```ts
|
|
2153
2190
|
* import { encodeComponent, CANONICAL_DELIMITERS } from "@cosyte/astm";
|
|
@@ -2171,9 +2208,13 @@ declare function encodeComponent(leaf: string, d: Delimiters, recordIndex?: numb
|
|
|
2171
2208
|
* @param d - The delimiters to emit against; defaults to `H|\^&`.
|
|
2172
2209
|
* @returns The record's wire text, terminator excluded.
|
|
2173
2210
|
* @throws {@link AstmSerializeError} when a component contains an unencodable `CR`/`LF`
|
|
2174
|
-
* (`ASTM_EMIT_UNENCODABLE_VALUE`),
|
|
2175
|
-
* readback requires (`ASTM_EMIT_INVALID_DELIMITERS`)
|
|
2176
|
-
*
|
|
2211
|
+
* (`ASTM_EMIT_UNENCODABLE_VALUE`), when `d` fails one of the three conditions
|
|
2212
|
+
* readback requires (`ASTM_EMIT_INVALID_DELIMITERS`), or when this record's type
|
|
2213
|
+
* letter would not be the first character of its own emitted line
|
|
2214
|
+
* (`ASTM_EMIT_TYPE_LETTER_COLLISION`), which would make it read back as a
|
|
2215
|
+
* different record. That last one is a transcoding condition and needs no `d`
|
|
2216
|
+
* argument to fire: a record read under a different delimiter set can carry a
|
|
2217
|
+
* type letter the canonical set escapes away.
|
|
2177
2218
|
* @example
|
|
2178
2219
|
* ```ts
|
|
2179
2220
|
* import { serializeAstmRecord, parseAstmRecords } from "@cosyte/astm";
|
|
@@ -2202,18 +2243,29 @@ declare function serializeAstmRecord(record: AstmRecord, d?: Delimiters): string
|
|
|
2202
2243
|
* fails is an {@link AstmSerializeError} with code `ASTM_EMIT_INVALID_DELIMITERS`.
|
|
2203
2244
|
* This is stricter than the parser, which reads some sets it cannot reverse, so
|
|
2204
2245
|
* `serializeAstmRecords(msg, msg.delimiters)` can refuse a message that parsed:
|
|
2205
|
-
* in exactly the cases where it used to emit a stream that read back wrong.
|
|
2206
|
-
*
|
|
2207
|
-
*
|
|
2208
|
-
*
|
|
2246
|
+
* in exactly the cases where it used to emit a stream that read back wrong.
|
|
2247
|
+
*
|
|
2248
|
+
* **And each record is checked against the set it is being written with.** Those
|
|
2249
|
+
* three conditions read the set alone, so they cannot see that `field` = `R`
|
|
2250
|
+
* escapes an `R` record's own type letter away. Every record's emitted line is
|
|
2251
|
+
* therefore checked to start with the letter the record models, and one that does
|
|
2252
|
+
* not is `ASTM_EMIT_TYPE_LETTER_COLLISION` rather than a stream that reads back as
|
|
2253
|
+
* different records. What neither check promises is that every *field* lands where
|
|
2254
|
+
* it did: an escape sequence whose body is a delimiter is an opaque atom, so that
|
|
2255
|
+
* delimiter never becomes a boundary, and the parse side reports that
|
|
2256
|
+
* (`ASTM_UNKNOWN_ESCAPE_SEQUENCE`) rather than emit refusing it.
|
|
2209
2257
|
*
|
|
2210
2258
|
* @param input - A parsed {@link AstmMessage} or a list of {@link AstmRecord}s.
|
|
2211
2259
|
* @param d - The delimiters to emit against; defaults to the canonical `H|\^&` set.
|
|
2212
2260
|
* @returns The serialized record stream (`CR` after every record).
|
|
2213
2261
|
* @throws {@link AstmSerializeError} when a component contains an unencodable `CR`/`LF`
|
|
2214
|
-
* (`ASTM_EMIT_UNENCODABLE_VALUE`),
|
|
2215
|
-
* readback requires (`ASTM_EMIT_INVALID_DELIMITERS`)
|
|
2216
|
-
*
|
|
2262
|
+
* (`ASTM_EMIT_UNENCODABLE_VALUE`), when `d` fails one of the three conditions
|
|
2263
|
+
* readback requires (`ASTM_EMIT_INVALID_DELIMITERS`), or when a record's type
|
|
2264
|
+
* letter would not be the first character of its own emitted line
|
|
2265
|
+
* (`ASTM_EMIT_TYPE_LETTER_COLLISION`). The last of the three fires on the
|
|
2266
|
+
* **default** canonical path as well: it compares each record against the set
|
|
2267
|
+
* being emitted with, so a record read under a different set can carry a type
|
|
2268
|
+
* letter the canonical set escapes away. Omitting `d` is not a way around it.
|
|
2217
2269
|
* @example
|
|
2218
2270
|
* ```ts
|
|
2219
2271
|
* import { parseAstmRecords, serializeAstmRecords } from "@cosyte/astm";
|
|
@@ -2232,8 +2284,10 @@ declare function serializeAstmRecords(input: AstmMessage | readonly AstmRecord[]
|
|
|
2232
2284
|
* @returns The escaped field text.
|
|
2233
2285
|
* @throws {@link AstmSerializeError} when a component contains an unencodable `CR`/`LF`
|
|
2234
2286
|
* (`ASTM_EMIT_UNENCODABLE_VALUE`), or when `d` fails one of the three conditions
|
|
2235
|
-
* readback requires (`ASTM_EMIT_INVALID_DELIMITERS`)
|
|
2236
|
-
*
|
|
2287
|
+
* readback requires (`ASTM_EMIT_INVALID_DELIMITERS`). Like
|
|
2288
|
+
* {@link encodeComponent} this takes no record, so it never raises
|
|
2289
|
+
* `ASTM_EMIT_TYPE_LETTER_COLLISION`: encoding a record's type-letter field
|
|
2290
|
+
* through it will escape that letter away without objecting.
|
|
2237
2291
|
* @example
|
|
2238
2292
|
* ```ts
|
|
2239
2293
|
* import { serializeField, tokenizeRecord, CANONICAL_DELIMITERS } from "@cosyte/astm";
|
|
@@ -4353,7 +4407,8 @@ declare function decodeAstmFrames(bytes: Uint8Array, options?: FrameOptions): De
|
|
|
4353
4407
|
* case of);
|
|
4354
4408
|
* - the **frame number** `0`–`7`, starting at `1` and rolling over `7 → 0 → 1`,
|
|
4355
4409
|
* continuous across every frame in the stream (as the decoder's sequence check
|
|
4356
|
-
* expects);
|
|
4410
|
+
* expects); a caller may start the sequence elsewhere to compose one transfer
|
|
4411
|
+
* across several calls, but only at a number a frame can actually carry;
|
|
4357
4412
|
* - the **240-byte text split**: a record longer than 240 bytes is split across
|
|
4358
4413
|
* frames closed by `ETB` (intermediate) with a final `ETX`; the seven framing
|
|
4359
4414
|
* control bytes are never counted toward the 240.
|
|
@@ -4375,6 +4430,12 @@ declare function decodeAstmFrames(bytes: Uint8Array, options?: FrameOptions): De
|
|
|
4375
4430
|
* empty frame.
|
|
4376
4431
|
* - `ASTM_FRAME_UNENCODABLE_CHARACTER`: a record given as a `string` holds a
|
|
4377
4432
|
* character above `U+00FF`, which has no single byte to stand for.
|
|
4433
|
+
* - `ASTM_FRAME_RESERVED_BYTE`: a record holds a byte this layer reads as frame
|
|
4434
|
+
* structure (`STX`, `ETB` or `ETX`). Framing has no escape mechanism, so such
|
|
4435
|
+
* a byte cannot be carried inside a frame at all.
|
|
4436
|
+
* - `ASTM_FRAME_INVALID_START_FRAME_NUMBER`: `options.startFrameNumber` is not a
|
|
4437
|
+
* whole number from `0` to `7`. A frame's number is one ASCII digit, so a
|
|
4438
|
+
* value outside that range has no digit to be written as.
|
|
4378
4439
|
*
|
|
4379
4440
|
* @example
|
|
4380
4441
|
* ```ts
|
|
@@ -4382,7 +4443,7 @@ declare function decodeAstmFrames(bytes: Uint8Array, options?: FrameOptions): De
|
|
|
4382
4443
|
* const code: AstmFrameEncodeErrorCode = "ASTM_FRAME_UNENCODABLE_CHARACTER";
|
|
4383
4444
|
* ```
|
|
4384
4445
|
*/
|
|
4385
|
-
type AstmFrameEncodeErrorCode = "ASTM_FRAME_EMPTY_RECORD" | "ASTM_FRAME_UNENCODABLE_CHARACTER";
|
|
4446
|
+
type AstmFrameEncodeErrorCode = "ASTM_FRAME_EMPTY_RECORD" | "ASTM_FRAME_UNENCODABLE_CHARACTER" | "ASTM_FRAME_RESERVED_BYTE" | "ASTM_FRAME_INVALID_START_FRAME_NUMBER";
|
|
4386
4447
|
/**
|
|
4387
4448
|
* Thrown by {@link composeAstmFrames} when the input cannot be framed into a
|
|
4388
4449
|
* spec-clean stream. Carries a stable code + positional context, never the
|
|
@@ -4404,6 +4465,30 @@ type AstmFrameEncodeErrorCode = "ASTM_FRAME_EMPTY_RECORD" | "ASTM_FRAME_UNENCODA
|
|
|
4404
4465
|
* The refusal removes no capability, it routes you to the parameter that already
|
|
4405
4466
|
* carried it.
|
|
4406
4467
|
*
|
|
4468
|
+
* **The reserved-byte case, and why it has no escape hatch.** `STX`, `ETB` and
|
|
4469
|
+
* `ETX` are what {@link decodeAstmFrames} reads as frame structure: `STX` opens
|
|
4470
|
+
* a frame, and the first `ETB`/`ETX` after it *is* the end of that frame's text.
|
|
4471
|
+
* A record carrying one of those bytes therefore cannot be framed and read back,
|
|
4472
|
+
* whichever form it arrives in, and this layer has no escape sequence to hide one
|
|
4473
|
+
* behind. Passing bytes instead of a string does **not** route around it, because
|
|
4474
|
+
* the byte is the problem rather than the encoding. Take the byte out of the value
|
|
4475
|
+
* before framing: which byte belongs in a clinical value is the sender's call, not
|
|
4476
|
+
* this library's, so it refuses rather than substituting or deleting one.
|
|
4477
|
+
*
|
|
4478
|
+
* **The start-frame-number case, and why an option gets an error of its own.** A
|
|
4479
|
+
* frame's number is a single ASCII digit, `FN_ZERO + n` for `n` in `0`-`7`, and
|
|
4480
|
+
* nothing used to check that `options.startFrameNumber` was one. What went out
|
|
4481
|
+
* instead was whatever byte that sum truncated to: measured on this package's own
|
|
4482
|
+
* round trip, `-1` wrote `/` into the frame-number position and `NaN`, `Infinity`
|
|
4483
|
+
* and `-Infinity` each wrote a `NUL` byte into every frame of the stream, after
|
|
4484
|
+
* which the decoder read no frame number at all and emitted **none** of the
|
|
4485
|
+
* records. Out-of-domain values that happened to land back on a digit were worse
|
|
4486
|
+
* in a quieter way: `1.5` and `257` both produced the byte-for-byte stream a
|
|
4487
|
+
* `startFrameNumber` of `1` produces, so the option silently accepted a value it
|
|
4488
|
+
* documented as invalid. This one refusal is about the caller's own option rather
|
|
4489
|
+
* than about record content, so its message names the value received. Nothing
|
|
4490
|
+
* from the stream is quoted.
|
|
4491
|
+
*
|
|
4407
4492
|
* @example
|
|
4408
4493
|
* ```ts
|
|
4409
4494
|
* import { composeAstmFrames, AstmFrameEncodeError } from "@cosyte/astm";
|
|
@@ -4422,7 +4507,9 @@ declare class AstmFrameEncodeError extends Error {
|
|
|
4422
4507
|
/**
|
|
4423
4508
|
* Position of the offending character within that record, when applicable:
|
|
4424
4509
|
* its index in the string, never the character itself and never its code
|
|
4425
|
-
* point. Enough to find it in the caller's own data.
|
|
4510
|
+
* point. Enough to find it in the caller's own data. For a record supplied as
|
|
4511
|
+
* a `Uint8Array` this is the offending byte's index, which is the same
|
|
4512
|
+
* position: a record string is a byte string, so the two indices coincide.
|
|
4426
4513
|
*/
|
|
4427
4514
|
readonly characterIndex?: number;
|
|
4428
4515
|
/** @internal */
|
|
@@ -4431,8 +4518,42 @@ declare class AstmFrameEncodeError extends Error {
|
|
|
4431
4518
|
/** Options for {@link composeAstmFrames}. */
|
|
4432
4519
|
interface ComposeFramesOptions {
|
|
4433
4520
|
/**
|
|
4434
|
-
* The frame number to start the sequence at
|
|
4435
|
-
*
|
|
4521
|
+
* The frame number to start the sequence at: a whole number from `0` to `7`.
|
|
4522
|
+
* Defaults to `1`, the number ASTM gives the first frame of a transfer and the
|
|
4523
|
+
* one {@link decodeAstmFrames} expects to read first.
|
|
4524
|
+
*
|
|
4525
|
+
* **Any other value writes a continuation rather than the start of a transfer,
|
|
4526
|
+
* and that is what the option is for**: composing one transfer across more than
|
|
4527
|
+
* one call. Concatenating `composeAstmFrames(head)` with
|
|
4528
|
+
* `composeAstmFrames(tail, { startFrameNumber: n })`, where `n` is the number
|
|
4529
|
+
* after the last frame `head` used, is byte-identical to composing the whole
|
|
4530
|
+
* list in a single call and decodes with an empty warnings array, across the
|
|
4531
|
+
* `7 → 0` rollover included.
|
|
4532
|
+
*
|
|
4533
|
+
* **Decoded on its own, a stream that starts anywhere but `1` opens on a
|
|
4534
|
+
* sequence gap.** The decoder never bridges a gap silently, so it warns
|
|
4535
|
+
* (`ASTM_FRAME_SEQUENCE_GAP`) and does not emit that first record.
|
|
4536
|
+
* **What {@link parseFramedAstm} does after that varies with the message
|
|
4537
|
+
* shape and no rule is offered for it here.** It may throw, under more than
|
|
4538
|
+
* one code, and it may return a message that is simply one record short. **The
|
|
4539
|
+
* one thing that does hold is that the record layer never reports the loss**:
|
|
4540
|
+
* `parseFramedAstm` hands the record parser only the frames the codec
|
|
4541
|
+
* vouched for, so `message.warnings` carries what the surviving records
|
|
4542
|
+
* warrant and nothing about the record that did not survive. **Read
|
|
4543
|
+
* `frameWarnings`.** That is the cost of the option rather than a defect in
|
|
4544
|
+
* the caller's records, and it is why `1` is the default.
|
|
4545
|
+
*
|
|
4546
|
+
* **Finding the number to continue from.** Nothing here returns it, and the
|
|
4547
|
+
* frame count is not it once a record splits: decode the part you just
|
|
4548
|
+
* composed and read the last frame's number, then add one modulo 8, as in
|
|
4549
|
+
* `((decodeAstmFrames(part).frames.at(-1)?.frameNumber ?? 0) + 1) % 8`. A frame
|
|
4550
|
+
* carries no number only when the stream ends immediately after its `STX`,
|
|
4551
|
+
* which is not something this encoder writes, so that fallback never fires on
|
|
4552
|
+
* a part composed here. Getting the number wrong costs the record at the join,
|
|
4553
|
+
* warned rather than silently.
|
|
4554
|
+
*
|
|
4555
|
+
* A value outside `0`-`7`, or one that is not a whole number, is refused with
|
|
4556
|
+
* `ASTM_FRAME_INVALID_START_FRAME_NUMBER`.
|
|
4436
4557
|
*/
|
|
4437
4558
|
readonly startFrameNumber?: number;
|
|
4438
4559
|
}
|
|
@@ -4456,20 +4577,45 @@ interface ComposeFramesOptions {
|
|
|
4456
4577
|
* resulting `Uint8Array`. Those two forms are the whole of what `records` accepts.
|
|
4457
4578
|
* A caller reaching this from JavaScript with some **other** typed array is outside
|
|
4458
4579
|
* the signature and gets no such refusal: each element is still written as its low
|
|
4459
|
-
* byte, the same substitution described above.
|
|
4460
|
-
*
|
|
4461
|
-
*
|
|
4462
|
-
*
|
|
4463
|
-
*
|
|
4464
|
-
*
|
|
4580
|
+
* byte, the same substitution described above. The reserved-byte check below does
|
|
4581
|
+
* not reach that route either, because it compares elements against the three byte
|
|
4582
|
+
* values and an element like `0x0103` is not one of them, though its low byte
|
|
4583
|
+
* becomes an `ETX` on the wire. Pass a `Uint8Array` or a string.
|
|
4584
|
+
*
|
|
4585
|
+
* **A record carrying a frame-structure byte is refused too, in either form.**
|
|
4586
|
+
* `STX`, `ETB` and `ETX` are what the decoder reads as the shape of a frame, and
|
|
4587
|
+
* framing has no escape sequence to hide one behind, so a record holding one is
|
|
4588
|
+
* `ASTM_FRAME_RESERVED_BYTE` rather than a frame truncated at that byte. It used
|
|
4589
|
+
* to be written as given, and the result was not reliably loud: with the two
|
|
4590
|
+
* following bytes matching, the truncated frame verifies and a whole record is
|
|
4591
|
+
* absorbed into the previous one with an empty warnings array at both layers.
|
|
4592
|
+
* Supplying the record as a `Uint8Array` does not route around this check.
|
|
4593
|
+
*
|
|
4594
|
+
* **`options.startFrameNumber` is checked before *this function* reads a record**,
|
|
4595
|
+
* so on this entry point the refusal never depends on the caller's data.
|
|
4596
|
+
* ({@link serializeFramedAstm} serializes every record *before* it gets here, so
|
|
4597
|
+
* a record that cannot be serialized is refused first on that route.) It has to
|
|
4598
|
+
* be a whole number from `0` to `7`, because a frame's number is one ASCII digit;
|
|
4599
|
+
* anything else is `ASTM_FRAME_INVALID_START_FRAME_NUMBER` rather than whatever
|
|
4600
|
+
* byte the arithmetic truncated to. A value other than the default `1` writes a
|
|
4601
|
+
* **continuation** of a sequence already in progress, which is what the option is
|
|
4602
|
+
* for, and a continuation decoded on its own opens on a sequence gap: see
|
|
4603
|
+
* {@link ComposeFramesOptions}.
|
|
4604
|
+
*
|
|
4605
|
+
* What is still **not** guaranteed: that an accepted record round-trips through
|
|
4606
|
+
* the *record* layer. A delimiter colliding with a record's type letter, for one,
|
|
4607
|
+
* frames and de-frames byte-exactly and still re-reads as a different record.
|
|
4465
4608
|
*
|
|
4466
4609
|
* @param records - The reassembled record byte-strings (`Uint8Array` or latin1
|
|
4467
4610
|
* `string`), one entry per complete record.
|
|
4468
4611
|
* @param options - Encode options.
|
|
4469
4612
|
* @returns The framed byte stream.
|
|
4470
|
-
* @throws {@link AstmFrameEncodeError} when
|
|
4471
|
-
*
|
|
4472
|
-
*
|
|
4613
|
+
* @throws {@link AstmFrameEncodeError} when `options.startFrameNumber` is not a
|
|
4614
|
+
* whole number from `0` to `7` (`ASTM_FRAME_INVALID_START_FRAME_NUMBER`), the
|
|
4615
|
+
* list is empty, a record has no bytes (`ASTM_FRAME_EMPTY_RECORD`), a record
|
|
4616
|
+
* string holds a character above `U+00FF`
|
|
4617
|
+
* (`ASTM_FRAME_UNENCODABLE_CHARACTER`), or a record holds an `STX`, `ETB` or
|
|
4618
|
+
* `ETX` byte (`ASTM_FRAME_RESERVED_BYTE`).
|
|
4473
4619
|
* @example
|
|
4474
4620
|
* ```ts
|
|
4475
4621
|
* import { composeAstmFrames, decodeAstmFrames } from "@cosyte/astm";
|
|
@@ -4544,20 +4690,35 @@ declare function parseFramedAstm(bytes: Uint8Array, options?: FrameOptions & Ast
|
|
|
4544
4690
|
* delimiters, embedded delimiters re-escaped) and then framed **independently**
|
|
4545
4691
|
* (one record per `ETX`-closed frame run), so the framing exactly mirrors what
|
|
4546
4692
|
* {@link decodeAstmFrames} reassembles: `parseFramedAstm(serializeFramedAstm(msg))`
|
|
4547
|
-
* yields an equal message
|
|
4693
|
+
* yields an equal message **with the default `startFrameNumber`**. That clause is
|
|
4694
|
+
* load-bearing rather than pedantic: a non-default start writes a continuation of
|
|
4695
|
+
* a sequence already in progress, and a continuation read on its own opens on a
|
|
4696
|
+
* sequence gap, so the decoder does not emit its first record and this round trip
|
|
4697
|
+
* does not hold for it. See {@link ComposeFramesOptions}.
|
|
4548
4698
|
*
|
|
4549
4699
|
* A value carrying a character above `U+00FF` is **refused** here rather than
|
|
4550
4700
|
* framed: the record layer is happy to hold one, but a frame carries bytes and
|
|
4551
|
-
* nothing in the message says which character encoding to turn it into.
|
|
4552
|
-
*
|
|
4701
|
+
* nothing in the message says which character encoding to turn it into.
|
|
4702
|
+
*
|
|
4703
|
+
* A value carrying a raw `STX`, `ETB` or `ETX` is refused too, and being Latin-1
|
|
4704
|
+
* is no exemption: those three are what {@link decodeAstmFrames} reads as the
|
|
4705
|
+
* shape of a frame, and framing has no escape sequence to hide one behind. The
|
|
4706
|
+
* record layer is happy to hold those as well, so this is a message that
|
|
4707
|
+
* serializes to records perfectly well and cannot be framed.
|
|
4553
4708
|
*
|
|
4554
4709
|
* @param input - A parsed {@link AstmMessage} or a list of {@link AstmRecord}s.
|
|
4555
4710
|
* @param options - Frame-encode options.
|
|
4556
4711
|
* @returns The framed byte stream.
|
|
4557
|
-
* @throws {@link AstmSerializeError} when a value contains an unencodable `CR`/`LF
|
|
4558
|
-
*
|
|
4559
|
-
*
|
|
4560
|
-
* (`
|
|
4712
|
+
* @throws {@link AstmSerializeError} when a value contains an unencodable `CR`/`LF`
|
|
4713
|
+
* (`ASTM_EMIT_UNENCODABLE_VALUE`), or when a record's own type letter would not
|
|
4714
|
+
* survive being written in the canonical set this function serializes with
|
|
4715
|
+
* (`ASTM_EMIT_TYPE_LETTER_COLLISION`). The second reaches messages parsed under a
|
|
4716
|
+
* **different** delimiter set, since this function passes no set of its own.
|
|
4717
|
+
* @throws {@link AstmFrameEncodeError} when `options.startFrameNumber` is not a
|
|
4718
|
+
* whole number from `0` to `7` (`ASTM_FRAME_INVALID_START_FRAME_NUMBER`), there
|
|
4719
|
+
* are no records to frame (`ASTM_FRAME_EMPTY_RECORD`), a value holds a
|
|
4720
|
+
* character above `U+00FF` (`ASTM_FRAME_UNENCODABLE_CHARACTER`), or a record
|
|
4721
|
+
* holds an `STX`, `ETB` or `ETX` byte (`ASTM_FRAME_RESERVED_BYTE`).
|
|
4561
4722
|
* @example
|
|
4562
4723
|
* ```ts
|
|
4563
4724
|
* import { parseAstmRecords, serializeFramedAstm, parseFramedAstm } from "@cosyte/astm";
|
package/dist/index.mjs
CHANGED
|
@@ -1385,7 +1385,8 @@ var AstmSerializeError = class extends Error {
|
|
|
1385
1385
|
/**
|
|
1386
1386
|
* Stable discriminant. `ASTM_EMIT_UNENCODABLE_VALUE` for a `CR`/`LF` in a value;
|
|
1387
1387
|
* `ASTM_EMIT_INVALID_DELIMITERS` for a delimiter set that cannot be emitted
|
|
1388
|
-
* reversibly
|
|
1388
|
+
* reversibly; `ASTM_EMIT_TYPE_LETTER_COLLISION` for a record whose own type
|
|
1389
|
+
* letter the set being emitted with would escape away.
|
|
1389
1390
|
*/
|
|
1390
1391
|
code;
|
|
1391
1392
|
/** 0-based ordinal of the record within the message, when known. */
|
|
@@ -1449,10 +1450,24 @@ function serializeAstmRecord(record, d = CANONICAL_DELIMITERS) {
|
|
|
1449
1450
|
return serializeRecordChecked(record, d);
|
|
1450
1451
|
}
|
|
1451
1452
|
function serializeRecordChecked(record, d) {
|
|
1453
|
+
return assertTypeLetterSurvives(serializeRecordText(record, d), record);
|
|
1454
|
+
}
|
|
1455
|
+
function serializeRecordText(record, d) {
|
|
1452
1456
|
if (record.type === "M" || record.type === "S") return serializeVerbatimRecord(record, d);
|
|
1453
1457
|
if (record.type === "H") return serializeHeader(record, d);
|
|
1454
1458
|
return record.fields.map((f) => encodeField(f.repeats, d, record.recordIndex)).join(d.field);
|
|
1455
1459
|
}
|
|
1460
|
+
function recordTypeLetter(record) {
|
|
1461
|
+
return record.type === "unsupported" ? record.rawType : record.type;
|
|
1462
|
+
}
|
|
1463
|
+
function assertTypeLetterSurvives(text, record) {
|
|
1464
|
+
if (text.charAt(0) === recordTypeLetter(record)) return text;
|
|
1465
|
+
throw new AstmSerializeError(
|
|
1466
|
+
"A record's type letter is escaped away by the delimiters being emitted with, so the record would read back as a different record. The record's index is on the error; the letter itself is not quoted, because on an unsupported record it is a byte off the wire.",
|
|
1467
|
+
record.recordIndex,
|
|
1468
|
+
"ASTM_EMIT_TYPE_LETTER_COLLISION"
|
|
1469
|
+
);
|
|
1470
|
+
}
|
|
1456
1471
|
function serializeVerbatimRecord(record, d) {
|
|
1457
1472
|
if (recoversVerbatim(record, d)) return record.rawLine;
|
|
1458
1473
|
return record.fields.map((f) => encodeField(f.repeats, d, record.recordIndex)).join(d.field);
|
|
@@ -2227,7 +2242,9 @@ var AstmFrameEncodeError = class extends Error {
|
|
|
2227
2242
|
/**
|
|
2228
2243
|
* Position of the offending character within that record, when applicable:
|
|
2229
2244
|
* its index in the string, never the character itself and never its code
|
|
2230
|
-
* point. Enough to find it in the caller's own data.
|
|
2245
|
+
* point. Enough to find it in the caller's own data. For a record supplied as
|
|
2246
|
+
* a `Uint8Array` this is the offending byte's index, which is the same
|
|
2247
|
+
* position: a record string is a byte string, so the two indices coincide.
|
|
2231
2248
|
*/
|
|
2232
2249
|
characterIndex;
|
|
2233
2250
|
/** @internal */
|
|
@@ -2257,6 +2274,29 @@ function toBytes(input, recordIndex) {
|
|
|
2257
2274
|
}
|
|
2258
2275
|
return out;
|
|
2259
2276
|
}
|
|
2277
|
+
var RESERVED_STRUCTURE_BYTES = /* @__PURE__ */ new Set([STX, ETB, ETX]);
|
|
2278
|
+
function assertFramable(bytes, recordIndex) {
|
|
2279
|
+
for (let i = 0; i < bytes.length; i += 1) {
|
|
2280
|
+
const byte = bytes[i];
|
|
2281
|
+
if (byte !== void 0 && RESERVED_STRUCTURE_BYTES.has(byte)) {
|
|
2282
|
+
throw new AstmFrameEncodeError(
|
|
2283
|
+
"A record contains a byte the framing layer reads as structure (STX, ETB or ETX). A frame has no escape sequence for one, so it cannot be carried inside a frame: remove it from the record before framing.",
|
|
2284
|
+
recordIndex,
|
|
2285
|
+
"ASTM_FRAME_RESERVED_BYTE",
|
|
2286
|
+
i
|
|
2287
|
+
);
|
|
2288
|
+
}
|
|
2289
|
+
}
|
|
2290
|
+
}
|
|
2291
|
+
function assertStartFrameNumber(value) {
|
|
2292
|
+
if (!Number.isInteger(value) || value < 0 || value > FRAME_NUMBER_MODULUS - 1) {
|
|
2293
|
+
throw new AstmFrameEncodeError(
|
|
2294
|
+
`startFrameNumber must be a whole number from 0 to 7: a frame's number is one ASCII digit and the sequence is modulo 8. Received: ${String(value)}.`,
|
|
2295
|
+
void 0,
|
|
2296
|
+
"ASTM_FRAME_INVALID_START_FRAME_NUMBER"
|
|
2297
|
+
);
|
|
2298
|
+
}
|
|
2299
|
+
}
|
|
2260
2300
|
function encodeFrame(text, frameNumber, isFinal) {
|
|
2261
2301
|
const term = isFinal ? ETX : ETB;
|
|
2262
2302
|
const bytes = [STX, FN_ZERO + frameNumber, ...text, term];
|
|
@@ -2265,11 +2305,13 @@ function encodeFrame(text, frameNumber, isFinal) {
|
|
|
2265
2305
|
return [...bytes, hex.charCodeAt(0), hex.charCodeAt(1), CR, LF];
|
|
2266
2306
|
}
|
|
2267
2307
|
function composeAstmFrames(records, options = {}) {
|
|
2308
|
+
const start = options.startFrameNumber ?? FIRST_FRAME_NUMBER;
|
|
2309
|
+
assertStartFrameNumber(start);
|
|
2268
2310
|
if (records.length === 0) {
|
|
2269
2311
|
throw new AstmFrameEncodeError("Cannot frame an empty record list: nothing to transmit.");
|
|
2270
2312
|
}
|
|
2271
2313
|
const out = [];
|
|
2272
|
-
let frameNumber =
|
|
2314
|
+
let frameNumber = start;
|
|
2273
2315
|
records.forEach((record, recordIndex) => {
|
|
2274
2316
|
const bytes = toBytes(record, recordIndex);
|
|
2275
2317
|
if (bytes.length === 0) {
|
|
@@ -2278,6 +2320,7 @@ function composeAstmFrames(records, options = {}) {
|
|
|
2278
2320
|
recordIndex
|
|
2279
2321
|
);
|
|
2280
2322
|
}
|
|
2323
|
+
assertFramable(bytes, recordIndex);
|
|
2281
2324
|
for (let offset = 0; offset < bytes.length; offset += MAX_FRAME_TEXT) {
|
|
2282
2325
|
const chunk = bytes.subarray(offset, offset + MAX_FRAME_TEXT);
|
|
2283
2326
|
const isFinal = offset + MAX_FRAME_TEXT >= bytes.length;
|
|
@@ -2313,7 +2356,7 @@ function concatBytes3(chunks) {
|
|
|
2313
2356
|
}
|
|
2314
2357
|
|
|
2315
2358
|
// src/index.ts
|
|
2316
|
-
var VERSION = "0.0.
|
|
2359
|
+
var VERSION = "0.0.14";
|
|
2317
2360
|
|
|
2318
2361
|
export { ALL_ASTM_WARNING_CODES, AMBIGUOUS_CODES, ACK as ASTM_ACK, ENQ as ASTM_ENQ, EOT as ASTM_EOT, NAK as ASTM_NAK, AstmAmbiguousStreamError, AstmFrameEncodeError, AstmFrameStrictError, AstmParseError, AstmProfileDefinitionError, AstmSerializeError, AstmStrictError, CANONICAL_DELIMITERS, FATAL_CODES, FRAME_WARNING_CODES, LIVD_WARNING_CODES, LTP_WARNING_CODES, SAFETY_CRITICAL_CODES, TOLERABLE_CODES, VERSION, WARNING_CODES, ambiguousMessageKind, ambiguousValueSplit, applyAstmProfile, applyAstmProfileToWarnings, applyLivd, astmDateToLocalISO, astmProfiles, attachComments, buildAstmMessage, classifyMessage, comments, commentsFor, composeAstmFrames, computeChecksum, decodeAstmFrames, decodeEscapes, deepFreeze, defineAstmProfile, defineLivdCatalog, delimitersRedeclared, detectFraming, encodeComponent, fieldScalar, fieldsUnseparated, frameBadChecksum, frameOversize, frameSequenceGap, frameUnterminated, getAstmProfile, getDefaultAstmProfile, interpretAbnormalFlag, interpretResultStatus, isNonStandard, isSafetyCriticalCode, listAstmProfiles, livdAmbiguousMapping, livdUnmappedCode, lookupLivdForRecord, ltpAmbiguousTransport, ltpFrameRejected, ltpInitialState, ltpReduce, ltpUnexpectedEvent, messages, nonStandardDelimiters, orders, orphanComment, parseAstmDate, parseAstmRecords, parseChecksumHex, parseFramedAstm, parseReferenceRange, partialTimestamp, patient, primaryCode, profileQuirkApplied, query, readDelimiters, recognizeUniversalTestId, resolveProfileTransport, results, serializeAstmRecord, serializeAstmRecords, serializeField, serializeFramedAstm, setDefaultAstmProfile, splitEscapeAware, toChecksumHex, tokenizeHeader, tokenizeRecord, undefinedAbnormalFlag, undefinedResultStatus, uninterpretedQueryStatus, unitsAbsent, unknownEscapeSequence, unknownRecordType, unpairedEscapeCharacter, unparseableReferenceRange, unreadableRedeclaration };
|
|
2319
2362
|
//# sourceMappingURL=index.mjs.map
|