node-opcua-address-space 2.181.0 → 2.182.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/api/helpers/ensure_secure_access.ts +53 -2
- package/api/index.ts +30 -1
- package/api/loader/generateAddressSpaceRaw.ts +44 -139
- package/api/loader/load_nodeset2.ts +5 -0
- package/api/loader/narrow_instance_datatypes.ts +264 -0
- package/api/loader/nodeset_builtin_formats.ts +143 -0
- package/api/loader/nodeset_format.ts +153 -0
- package/api/loader/nodeset_image.ts +37 -4
- package/api/loader/nodeset_image_codec.ts +153 -34
- package/api/loader/nodeset_record.ts +26 -2
- package/api/loader/nodeset_record_applier.ts +54 -3
- package/api/loader/nodeset_records_to_xml.ts +620 -0
- package/api/loader/nodeset_source.ts +185 -26
- package/api/loader/nodeset_to_image.ts +16 -5
- package/api/loader/nodeset_xml_header.ts +119 -0
- package/api/loader/nodeset_xml_primitives.ts +209 -0
- package/api/loader/nodeset_xml_producer.ts +61 -2
- package/api/loader/parsers/variant_parser.ts +9 -0
- package/bin/nodeset_equivalence.js +750 -0
- package/bin/nodeset_tool_common.js +72 -0
- package/bin/opcua-nodeset-image.js +93 -103
- package/bin/opcua-nodeset-live.js +139 -0
- package/dist/api/helpers/ensure_secure_access.d.ts +20 -0
- package/dist/api/helpers/ensure_secure_access.js +39 -0
- package/dist/api/helpers/ensure_secure_access.js.map +1 -1
- package/dist/api/index.d.ts +7 -2
- package/dist/api/index.js +21 -1
- package/dist/api/index.js.map +1 -1
- package/dist/api/loader/generateAddressSpaceRaw.d.ts +1 -7
- package/dist/api/loader/generateAddressSpaceRaw.js +32 -125
- package/dist/api/loader/generateAddressSpaceRaw.js.map +1 -1
- package/dist/api/loader/load_nodeset2.js +4 -0
- package/dist/api/loader/load_nodeset2.js.map +1 -1
- package/dist/api/loader/narrow_instance_datatypes.d.ts +30 -0
- package/dist/api/loader/narrow_instance_datatypes.js +217 -0
- package/dist/api/loader/narrow_instance_datatypes.js.map +1 -0
- package/dist/api/loader/nodeset_builtin_formats.d.ts +27 -0
- package/dist/api/loader/nodeset_builtin_formats.js +133 -0
- package/dist/api/loader/nodeset_builtin_formats.js.map +1 -0
- package/dist/api/loader/nodeset_format.d.ts +123 -0
- package/dist/api/loader/nodeset_format.js +63 -0
- package/dist/api/loader/nodeset_format.js.map +1 -0
- package/dist/api/loader/nodeset_image.d.ts +17 -1
- package/dist/api/loader/nodeset_image.js +32 -4
- package/dist/api/loader/nodeset_image.js.map +1 -1
- package/dist/api/loader/nodeset_image_codec.d.ts +4 -0
- package/dist/api/loader/nodeset_image_codec.js +142 -53
- package/dist/api/loader/nodeset_image_codec.js.map +1 -1
- package/dist/api/loader/nodeset_record.d.ts +26 -2
- package/dist/api/loader/nodeset_record.js +11 -2
- package/dist/api/loader/nodeset_record.js.map +1 -1
- package/dist/api/loader/nodeset_record_applier.js +52 -3
- package/dist/api/loader/nodeset_record_applier.js.map +1 -1
- package/dist/api/loader/nodeset_records_to_xml.d.ts +16 -0
- package/dist/api/loader/nodeset_records_to_xml.js +536 -0
- package/dist/api/loader/nodeset_records_to_xml.js.map +1 -0
- package/dist/api/loader/nodeset_source.d.ts +68 -7
- package/dist/api/loader/nodeset_source.js +157 -20
- package/dist/api/loader/nodeset_source.js.map +1 -1
- package/dist/api/loader/nodeset_to_image.d.ts +4 -4
- package/dist/api/loader/nodeset_to_image.js +14 -4
- package/dist/api/loader/nodeset_to_image.js.map +1 -1
- package/dist/api/loader/nodeset_xml_header.d.ts +22 -0
- package/dist/api/loader/nodeset_xml_header.js +113 -0
- package/dist/api/loader/nodeset_xml_header.js.map +1 -0
- package/dist/api/loader/nodeset_xml_primitives.d.ts +37 -0
- package/dist/api/loader/nodeset_xml_primitives.js +196 -0
- package/dist/api/loader/nodeset_xml_primitives.js.map +1 -0
- package/dist/api/loader/nodeset_xml_producer.js +56 -1
- package/dist/api/loader/nodeset_xml_producer.js.map +1 -1
- package/dist/api/loader/parsers/variant_parser.js +7 -0
- package/dist/api/loader/parsers/variant_parser.js.map +1 -1
- package/dist/impl/_instantiate_helpers.js +1 -0
- package/dist/impl/_instantiate_helpers.js.map +1 -1
- package/dist/impl/alarms_and_conditions/ua_alarm_condition_impl.js +4 -0
- package/dist/impl/alarms_and_conditions/ua_alarm_condition_impl.js.map +1 -1
- package/dist/impl/alarms_and_conditions/ua_certificate_expiration_alarm_impl.js +14 -0
- package/dist/impl/alarms_and_conditions/ua_certificate_expiration_alarm_impl.js.map +1 -1
- package/dist/impl/base_node_impl.d.ts +9 -0
- package/dist/impl/base_node_impl.js +11 -0
- package/dist/impl/base_node_impl.js.map +1 -1
- package/dist/impl/base_node_private.js +67 -97
- package/dist/impl/base_node_private.js.map +1 -1
- package/dist/impl/nodeset_tools/construct_namespace_dependency.js +5 -1
- package/dist/impl/nodeset_tools/construct_namespace_dependency.js.map +1 -1
- package/dist/impl/nodeset_tools/nodeset_to_records.js +61 -15
- package/dist/impl/nodeset_tools/nodeset_to_records.js.map +1 -1
- package/dist/impl/nodeset_tools/nodeset_to_xml.d.ts +1 -8
- package/dist/impl/nodeset_tools/nodeset_to_xml.js +174 -190
- package/dist/impl/nodeset_tools/nodeset_to_xml.js.map +1 -1
- package/dist/impl/ua_data_type_impl.d.ts +21 -0
- package/dist/impl/ua_data_type_impl.js +27 -2
- package/dist/impl/ua_data_type_impl.js.map +1 -1
- package/dist/impl/ua_object_type_impl.d.ts +9 -0
- package/dist/impl/ua_object_type_impl.js +12 -1
- package/dist/impl/ua_object_type_impl.js.map +1 -1
- package/dist/impl/ua_variable_impl.js +27 -10
- package/dist/impl/ua_variable_impl.js.map +1 -1
- package/dist/impl/ua_variable_impl_ext_obj.js +19 -2
- package/dist/impl/ua_variable_impl_ext_obj.js.map +1 -1
- package/dist/impl/ua_variable_type_impl.d.ts +8 -1
- package/dist/impl/ua_variable_type_impl.js +39 -2
- package/dist/impl/ua_variable_type_impl.js.map +1 -1
- package/dist/tsconfig_base.tsbuildinfo +1 -1
- package/distNodeJS/generate_address_space.js +4 -0
- package/distNodeJS/generate_address_space.js.map +1 -1
- package/impl/_instantiate_helpers.ts +3 -0
- package/impl/alarms_and_conditions/ua_acknowledgeable_condition_impl.ts +4 -4
- package/impl/alarms_and_conditions/ua_alarm_condition_impl.ts +19 -9
- package/impl/alarms_and_conditions/ua_base_event_impl.ts +9 -9
- package/impl/alarms_and_conditions/ua_certificate_expiration_alarm_impl.ts +17 -0
- package/impl/alarms_and_conditions/ua_condition_impl.ts +18 -18
- package/impl/alarms_and_conditions/ua_exclusive_deviation_alarm_impl.ts +2 -2
- package/impl/alarms_and_conditions/ua_exclusive_limit_alarm_impl.ts +1 -1
- package/impl/alarms_and_conditions/ua_limit_alarm_impl.ts +16 -16
- package/impl/alarms_and_conditions/ua_non_exclusive_deviation_alarm_impl.ts +2 -2
- package/impl/alarms_and_conditions/ua_non_exclusive_limit_alarm_impl.ts +4 -4
- package/impl/alarms_and_conditions/ua_off_normal_alarm_impl.ts +1 -1
- package/impl/base_node_impl.ts +15 -0
- package/impl/base_node_private.ts +97 -146
- package/impl/data_access/ua_multistate_discrete_impl.ts +1 -1
- package/impl/data_access/ua_multistate_value_discrete_impl.ts +2 -2
- package/impl/data_access/ua_two_state_discrete_impl.ts +2 -2
- package/impl/nodeset_tools/construct_namespace_dependency.ts +5 -1
- package/impl/nodeset_tools/nodeset_to_records.ts +66 -16
- package/impl/nodeset_tools/nodeset_to_xml.ts +127 -160
- package/impl/state_machine/finite_state_machine.ts +3 -3
- package/impl/state_machine/ua_shelving_state_machine_ex.ts +20 -20
- package/impl/ua_data_type_impl.ts +42 -4
- package/impl/ua_method_impl.ts +1 -1
- package/impl/ua_object_type_impl.ts +16 -3
- package/impl/ua_variable_impl.ts +27 -10
- package/impl/ua_variable_impl_ext_obj.ts +20 -2
- package/impl/ua_variable_type_impl.ts +49 -3
- package/package.json +29 -27
- package/source_nodejs/generate_address_space.ts +7 -1
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module node-opcua-address-space
|
|
3
|
+
*
|
|
4
|
+
* The two serialisations the loader has always read, expressed as {@link NodesetFormat}s.
|
|
5
|
+
*
|
|
6
|
+
* Registering them here rather than branching on a boolean inside the loader is the whole point
|
|
7
|
+
* of the registry: the built-ins get no privilege a third-party format does not also get, so the
|
|
8
|
+
* contract cannot quietly grow a dependency on something only the built-ins can reach. If these
|
|
9
|
+
* two can be written against it, so can a fourth.
|
|
10
|
+
*
|
|
11
|
+
* Importing this module registers them. The loader does that once, at its own import.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { getMinOPCUADate } from "node-opcua-date-time";
|
|
15
|
+
import type { NodeSetLoaderOptions } from "../interfaces/nodeset_loader_options.js";
|
|
16
|
+
import {
|
|
17
|
+
type NodesetDocument,
|
|
18
|
+
type NodesetFormat,
|
|
19
|
+
type NodesetHead,
|
|
20
|
+
type NodesetModel,
|
|
21
|
+
type NodesetModelInfo,
|
|
22
|
+
registerNodesetFormat
|
|
23
|
+
} from "./nodeset_format.js";
|
|
24
|
+
import { imageLinesToRecords, readNodesetImageInfo } from "./nodeset_image.js";
|
|
25
|
+
import type { NodesetRecord } from "./nodeset_record.js";
|
|
26
|
+
import { headerComplete, parseDependencies, sliceHeader } from "./nodeset_xml_header.js";
|
|
27
|
+
import { xmlNodesetRecords } from "./nodeset_xml_producer.js";
|
|
28
|
+
|
|
29
|
+
/** the name the NodeSet2 XML format is registered under */
|
|
30
|
+
export const NODESET2_XML_FORMAT = "nodeset2-xml";
|
|
31
|
+
/** the name the NDJSON image format is registered under */
|
|
32
|
+
export const NDJSON_IMAGE_FORMAT = "ndjson";
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* NodeSet2 XML: the fallback.
|
|
36
|
+
*
|
|
37
|
+
* It claims every document no other format recognised, which is exactly what the loader did
|
|
38
|
+
* before there was a registry, and it is the right default for a reason that outlives this
|
|
39
|
+
* refactor: XML has no compact magic number worth sniffing. A document may open with a byte-order
|
|
40
|
+
* mark, an XML declaration, a comment, a doctype or whitespace before `<UANodeSet` ever appears,
|
|
41
|
+
* and a sniffer strict enough to be sure would reject documents the loader reads today. So it
|
|
42
|
+
* does not guess. It takes what is left, and reports a real parse error if it was wrong, which is
|
|
43
|
+
* a better diagnostic than "unrecognised format" ever was.
|
|
44
|
+
*/
|
|
45
|
+
const nodeset2XmlFormat: NodesetFormat = {
|
|
46
|
+
name: NODESET2_XML_FORMAT,
|
|
47
|
+
extensions: [".xml"],
|
|
48
|
+
priority: -100,
|
|
49
|
+
sniff(_head: NodesetHead): boolean {
|
|
50
|
+
return true;
|
|
51
|
+
},
|
|
52
|
+
async readModels(document: NodesetDocument): Promise<NodesetModelInfo> {
|
|
53
|
+
const head = await document.readHead(headerComplete);
|
|
54
|
+
return await parseDependencies(sliceHeader(head, document.name));
|
|
55
|
+
},
|
|
56
|
+
records(document: NodesetDocument, _options?: NodeSetLoaderOptions): AsyncIterable<NodesetRecord> {
|
|
57
|
+
return xmlNodesetRecords(textChunks(document));
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
/** the chunks of a document as text: the XML producer takes text, and a source may deliver bytes */
|
|
62
|
+
async function* textChunks(document: NodesetDocument): AsyncGenerator<string> {
|
|
63
|
+
const decoder = new TextDecoder("utf-8");
|
|
64
|
+
for await (const chunk of document.chunks()) {
|
|
65
|
+
yield typeof chunk === "string" ? chunk : decoder.decode(chunk, { stream: true });
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* The NDJSON image: node-opcua's own cache of a document it has already parsed.
|
|
71
|
+
*
|
|
72
|
+
* Its first line is a header record, so it is told from every other line-delimited nodeset format
|
|
73
|
+
* by the two fields that open it: `{"kind":"header","schema":<n>`. That is a stronger test than
|
|
74
|
+
* "begins with a brace", which is all the loader used to do, and it is what lets a foreign
|
|
75
|
+
* `.jsonl` sit in the same directory without being mistaken for one of ours.
|
|
76
|
+
*/
|
|
77
|
+
const ndjsonImageFormat: NodesetFormat = {
|
|
78
|
+
name: NDJSON_IMAGE_FORMAT,
|
|
79
|
+
extensions: [".ndjson", ".ndjson.gz"],
|
|
80
|
+
priority: 100,
|
|
81
|
+
sniff(head: NodesetHead): boolean {
|
|
82
|
+
return isNdjsonHeaderLine(head.firstLine);
|
|
83
|
+
},
|
|
84
|
+
async readModels(document: NodesetDocument): Promise<NodesetModelInfo> {
|
|
85
|
+
// an image is small, a few hundred KB, and its header is line 1: reading it whole costs
|
|
86
|
+
// nothing that the load itself was not about to spend
|
|
87
|
+
const { header } = await readNodesetImageInfo(await document.rawBytes());
|
|
88
|
+
const models: NodesetModel[] = header.models.map((m) => ({
|
|
89
|
+
modelUri: m.modelUri,
|
|
90
|
+
version: m.version,
|
|
91
|
+
publicationDate: m.publicationDate ? new Date(m.publicationDate) : getMinOPCUADate(),
|
|
92
|
+
requiredModel: m.requiredModels.map((r) => ({
|
|
93
|
+
...r,
|
|
94
|
+
publicationDate: new Date(r.publicationDate ?? Number.NaN)
|
|
95
|
+
}))
|
|
96
|
+
}));
|
|
97
|
+
if (models.length === 0 && header.namespaceUris.length >= 1) {
|
|
98
|
+
models.push({
|
|
99
|
+
modelUri: header.namespaceUris[0],
|
|
100
|
+
version: "1",
|
|
101
|
+
publicationDate: getMinOPCUADate(),
|
|
102
|
+
requiredModel: []
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
return { models, namespaceUris: header.namespaceUris };
|
|
106
|
+
},
|
|
107
|
+
async *records(document: NodesetDocument, _options?: NodeSetLoaderOptions): AsyncGenerator<NodesetRecord> {
|
|
108
|
+
yield* imageLinesToRecords(await document.lines());
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* whether this line is the header record of an NDJSON image.
|
|
114
|
+
*
|
|
115
|
+
* Deliberately a string test before it is a parse: the discriminating fields are written first
|
|
116
|
+
* and the line can be hundreds of kilobytes, so there is no reason to build an object to answer
|
|
117
|
+
* a question the first thirty characters already settle. The parse is the confirmation, not the
|
|
118
|
+
* test, and it is only reached for a line that already looks like ours.
|
|
119
|
+
*/
|
|
120
|
+
export function isNdjsonHeaderLine(line: string): boolean {
|
|
121
|
+
const start = line.slice(0, 200);
|
|
122
|
+
if (!start.startsWith("{") || !start.includes('"kind"') || !start.includes('"header"')) {
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
try {
|
|
126
|
+
const parsed = JSON.parse(line) as { kind?: unknown; schema?: unknown };
|
|
127
|
+
return parsed?.kind === "header" && typeof parsed.schema === "number";
|
|
128
|
+
} catch {
|
|
129
|
+
return false;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
let registered = false;
|
|
134
|
+
|
|
135
|
+
/** register the built-in formats; called by the loader, and safe to call more than once */
|
|
136
|
+
export function registerBuiltinNodesetFormats(): void {
|
|
137
|
+
if (registered) return;
|
|
138
|
+
registered = true;
|
|
139
|
+
registerNodesetFormat(nodeset2XmlFormat);
|
|
140
|
+
registerNodesetFormat(ndjsonImageFormat);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
registerBuiltinNodesetFormats();
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module node-opcua-address-space
|
|
3
|
+
*
|
|
4
|
+
* Which serialisation a nodeset document is written in, and who can read it.
|
|
5
|
+
*
|
|
6
|
+
* The loader is built as `producer -> NodesetRecord -> consumer`: whatever a document is written
|
|
7
|
+
* in, it becomes a stream of records, and everything downstream of that -- the address space, the
|
|
8
|
+
* NodeSet2 XML writer, the image writer -- neither knows nor asks. What was missing is the other
|
|
9
|
+
* half of that seam. The *choice* of producer was a hard-coded pair, XML or image, decided on one
|
|
10
|
+
* byte, so a third serialisation had no way in short of editing the loader.
|
|
11
|
+
*
|
|
12
|
+
* A {@link NodesetFormat} is that way in. It answers three questions and nothing else: are these
|
|
13
|
+
* bytes mine, what does this document depend on, and what are its records. Formats are found by
|
|
14
|
+
* sniffing the document, never by its file name: an extension is a claim about a file, and the
|
|
15
|
+
* loader reads sources that are not files at all.
|
|
16
|
+
*
|
|
17
|
+
* Two formats are built in and registered by the loader itself, so the default behaviour is
|
|
18
|
+
* exactly what it always was: NodeSet2 XML, and the NDJSON image. Anything else is registered by
|
|
19
|
+
* the code that implements it, the way the Node.js entry point installs its zlib codecs.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import type { RequiredModel } from "node-opcua-address-space-base";
|
|
23
|
+
import type { NodeSetLoaderOptions } from "../interfaces/nodeset_loader_options.js";
|
|
24
|
+
import type { NodesetRecord } from "./nodeset_record.js";
|
|
25
|
+
import type { NodesetChunk } from "./nodeset_source.js";
|
|
26
|
+
|
|
27
|
+
/** a model a document defines, together with the models it says it needs */
|
|
28
|
+
export interface NodesetModel extends RequiredModel {
|
|
29
|
+
requiredModel: RequiredModel[];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* what a document says about itself before its body is read: enough, and only enough, to sort a
|
|
34
|
+
* set of documents into the order they have to be loaded in.
|
|
35
|
+
*/
|
|
36
|
+
export interface NodesetModelInfo {
|
|
37
|
+
namespaceUris: string[];
|
|
38
|
+
models: NodesetModel[];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* the beginning of a document, for {@link NodesetFormat.sniff}.
|
|
43
|
+
*
|
|
44
|
+
* `text` is inflated when the document arrived gzipped, so a sniffer never has to care whether it
|
|
45
|
+
* is looking at a compressed file: compression is a property of how a document was stored, never
|
|
46
|
+
* of what it says. It is decoded as UTF-8, which is lossless for the text formats and, for a
|
|
47
|
+
* binary container, faithful across the ASCII and NUL runs that container formats put their magic
|
|
48
|
+
* in (a tar header, for instance, is ASCII and NUL for its whole first block).
|
|
49
|
+
*/
|
|
50
|
+
export interface NodesetHead {
|
|
51
|
+
/** the first bytes of the document, decoded as UTF-8, inflated if the document was gzipped */
|
|
52
|
+
readonly text: string;
|
|
53
|
+
/** `text` up to the first line feed: what the line-delimited formats decide on */
|
|
54
|
+
readonly firstLine: string;
|
|
55
|
+
/** whether the document was gzip on the wire */
|
|
56
|
+
readonly gzip: boolean;
|
|
57
|
+
/** the source's name, for diagnostics only; never a basis for the decision */
|
|
58
|
+
readonly name: string;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* a document handed to a format, in whichever shape that format finds convenient. Every accessor
|
|
63
|
+
* inflates first when the document was gzipped, and the results are computed once and shared, so
|
|
64
|
+
* a format that asks for both the lines and the bytes pays for one inflate.
|
|
65
|
+
*/
|
|
66
|
+
export interface NodesetDocument {
|
|
67
|
+
readonly name: string;
|
|
68
|
+
/** the whole document, inflated */
|
|
69
|
+
bytes(): Promise<Uint8Array>;
|
|
70
|
+
/** the whole document as text, inflated and decoded */
|
|
71
|
+
text(): Promise<string>;
|
|
72
|
+
/** the document's lines, for the line-delimited formats */
|
|
73
|
+
lines(): Promise<string[]>;
|
|
74
|
+
/** the bytes exactly as they arrived, still compressed if they were */
|
|
75
|
+
rawBytes(): Promise<Uint8Array>;
|
|
76
|
+
/** the document as it arrives, for a format that parses incrementally rather than whole */
|
|
77
|
+
chunks(): AsyncIterable<NodesetChunk>;
|
|
78
|
+
/**
|
|
79
|
+
* read from the start until `complete(text)` holds, or the document ends. What was read stays
|
|
80
|
+
* in the document and is delivered again, first, by {@link NodesetDocument.chunks}, so a
|
|
81
|
+
* format that reads a header this way has not spent the source.
|
|
82
|
+
*
|
|
83
|
+
* This is how a format avoids reading a body it does not need: the XML header pre-pass stops
|
|
84
|
+
* after a few hundred bytes of a four megabyte file.
|
|
85
|
+
*/
|
|
86
|
+
readHead(complete: (text: string) => boolean): Promise<string>;
|
|
87
|
+
/** the SHA-256 of the bytes as they arrived, when the loader was asked to compute one */
|
|
88
|
+
digest(): string | undefined;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** a serialisation the loader can read */
|
|
92
|
+
export interface NodesetFormat {
|
|
93
|
+
/** how this format is named in diagnostics and in the tools; unique among registered formats */
|
|
94
|
+
readonly name: string;
|
|
95
|
+
/**
|
|
96
|
+
* the file extensions this format is usually written with, lowercase and dotted.
|
|
97
|
+
* Documentation and tooling only: the loader never decides on a name.
|
|
98
|
+
*/
|
|
99
|
+
readonly extensions?: readonly string[];
|
|
100
|
+
/**
|
|
101
|
+
* which format wins when more than one claims a document. Higher is stronger; the default is
|
|
102
|
+
* 0. NodeSet2 XML registers at -100 because it is the fallback: it claims anything no other
|
|
103
|
+
* format recognised, which is the behaviour the loader has always had.
|
|
104
|
+
*/
|
|
105
|
+
readonly priority?: number;
|
|
106
|
+
/** whether this format claims the document that begins like this */
|
|
107
|
+
sniff(head: NodesetHead): boolean;
|
|
108
|
+
/** what the document depends on, read without parsing its body */
|
|
109
|
+
readModels(document: NodesetDocument): Promise<NodesetModelInfo>;
|
|
110
|
+
/** the document's records, in document order */
|
|
111
|
+
records(document: NodesetDocument, options?: NodeSetLoaderOptions): AsyncIterable<NodesetRecord>;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const formats = new Map<string, NodesetFormat>();
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* make a format available to the loader. Returns a function that removes it again, which is what
|
|
118
|
+
* a test uses to put the registry back as it found it; production code registers once, at import.
|
|
119
|
+
*
|
|
120
|
+
* Registering a name twice replaces the earlier format, so a caller can override a built-in.
|
|
121
|
+
*/
|
|
122
|
+
export function registerNodesetFormat(format: NodesetFormat): () => void {
|
|
123
|
+
formats.set(format.name, format);
|
|
124
|
+
return () => {
|
|
125
|
+
if (formats.get(format.name) === format) {
|
|
126
|
+
formats.delete(format.name);
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** every registered format, strongest claim first */
|
|
132
|
+
export function nodesetFormats(): readonly NodesetFormat[] {
|
|
133
|
+
return [...formats.values()].sort((a, b) => (b.priority ?? 0) - (a.priority ?? 0));
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** the registered format of that name, if any */
|
|
137
|
+
export function nodesetFormatByName(name: string): NodesetFormat | undefined {
|
|
138
|
+
return formats.get(name);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* which format claims a document that begins like this, or `undefined` when none does. Formats
|
|
143
|
+
* are asked strongest first, so a specific format is always given the chance to claim a document
|
|
144
|
+
* before the XML fallback sees it.
|
|
145
|
+
*/
|
|
146
|
+
export function findNodesetFormat(head: NodesetHead): NodesetFormat | undefined {
|
|
147
|
+
for (const format of nodesetFormats()) {
|
|
148
|
+
if (format.sniff(head)) {
|
|
149
|
+
return format;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return undefined;
|
|
153
|
+
}
|
|
@@ -31,9 +31,24 @@ import type { NodesetChunk, NodesetChunkStream } from "./nodeset_source.js";
|
|
|
31
31
|
|
|
32
32
|
export { NodesetImageError, type NodesetImageHeader, type NodesetImageTrailer } from "./nodeset_image_codec.js";
|
|
33
33
|
|
|
34
|
-
|
|
34
|
+
const GZIP_MAGIC_0 = 0x1f;
|
|
35
|
+
const GZIP_MAGIC_1 = 0x8b;
|
|
36
|
+
/** `{`: the first byte of a document whose header record was not compressed */
|
|
37
|
+
const OPEN_BRACE = 0x7b;
|
|
38
|
+
|
|
39
|
+
/** whether these bytes are gzip, and so need inflating before they are text */
|
|
40
|
+
export function isGzip(firstBytes: Uint8Array): boolean {
|
|
41
|
+
return firstBytes.length >= 2 && firstBytes[0] === GZIP_MAGIC_0 && firstBytes[1] === GZIP_MAGIC_1;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* a NodeSet-NDJSON document, compressed or not, told from NodeSet2 XML by its first byte: a gzip
|
|
46
|
+
* stream starts `1f 8b`, an XML document `<`, and an uncompressed document `{`. Compression is a
|
|
47
|
+
* property of how a document was stored, never of what it says, so a reader that took only the
|
|
48
|
+
* compressed form would be refusing the same document written plainly.
|
|
49
|
+
*/
|
|
35
50
|
export function isNodesetImage(firstBytes: Uint8Array): boolean {
|
|
36
|
-
return firstBytes.length >=
|
|
51
|
+
return isGzip(firstBytes) || (firstBytes.length >= 1 && firstBytes[0] === OPEN_BRACE);
|
|
37
52
|
}
|
|
38
53
|
|
|
39
54
|
async function toReadableStream(source: Uint8Array | NodesetChunkStream): Promise<ReadableStream<Uint8Array>> {
|
|
@@ -59,11 +74,28 @@ async function toReadableStream(source: Uint8Array | NodesetChunkStream): Promis
|
|
|
59
74
|
});
|
|
60
75
|
}
|
|
61
76
|
|
|
62
|
-
|
|
77
|
+
/** how the text of an image is compressed; see {@link setImageDeflater} */
|
|
78
|
+
export type ImageDeflater = (text: string) => Promise<Uint8Array>;
|
|
79
|
+
|
|
80
|
+
const deflateWithCompressionStream: ImageDeflater = async (text) => {
|
|
63
81
|
const compressed = new Blob([text])
|
|
64
82
|
.stream()
|
|
65
83
|
.pipeThrough(new CompressionStream("gzip") as unknown as ReadableWritablePair<Uint8Array, Uint8Array>);
|
|
66
84
|
return new Uint8Array(await new Response(compressed).arrayBuffer());
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
let gzip: ImageDeflater = deflateWithCompressionStream;
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* how an image is compressed: CompressionStream everywhere by default, which offers no way to ask
|
|
91
|
+
* for a compression level and so writes at the default one; the Node.js entry point installs zlib
|
|
92
|
+
* at level 9, four percent smaller on the published nodesets for no change to the reader -- every
|
|
93
|
+
* gzip stream inflates the same whatever level wrote it. Returns the deflater installed before.
|
|
94
|
+
*/
|
|
95
|
+
export function setImageDeflater(deflater: ImageDeflater): ImageDeflater {
|
|
96
|
+
const previous = gzip;
|
|
97
|
+
gzip = deflater;
|
|
98
|
+
return previous;
|
|
67
99
|
}
|
|
68
100
|
|
|
69
101
|
/**
|
|
@@ -115,7 +147,8 @@ export function inflatedImageLines(image: Uint8Array): Promise<string[]> {
|
|
|
115
147
|
lines = (async () => {
|
|
116
148
|
let text: string;
|
|
117
149
|
try {
|
|
118
|
-
text
|
|
150
|
+
// an uncompressed document is already its own text; only gzip needs inflating
|
|
151
|
+
text = isGzip(image) ? await inflateImage(image) : new TextDecoder("utf-8").decode(image);
|
|
119
152
|
} catch (err) {
|
|
120
153
|
throw new NodesetImageError(`the image cannot be inflated: ${(err as Error).message}`);
|
|
121
154
|
}
|