@chestnutlabs/gcode-parser 0.1.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.
@@ -0,0 +1,117 @@
1
+ /**
2
+ * Worker protocol v1 (DD-003 §4.3).
3
+ *
4
+ * A parse ends in exactly one terminal message:
5
+ * - `done` — complete AND limit-bounded parses (limit-bounded: `ir.header.complete === false`
6
+ * + `stats.stopReason`); the client RESOLVES `parse()` with the ParseResult.
7
+ * - `error` — failures producing no usable IR; the client REJECTS.
8
+ * - `cancelled` — client rejects with E_CANCELLED; `partial` attached when requested.
9
+ */
10
+ import type { DialectMetadata, ToolpathIR } from '@chestnutlabs/toolpath-core';
11
+ import type { ParseOptions, ParseStats } from './parse.js';
12
+ import type { BlobLike, ReadableStreamLike } from './streaming.js';
13
+ export declare const PROTOCOL_VERSION = 1;
14
+ export interface WireParseOptions extends ParseOptions {
15
+ /** Attach the bounded partial IR to a `cancelled` response (DD-003 §5.2). */
16
+ partialOnCancel?: boolean;
17
+ /** Cooperative yield interval override (tests only; default 50 ms). */
18
+ yieldIntervalMs?: number;
19
+ /**
20
+ * Progressive preview (DD-004 §5.4, issue #60). Default 'auto': partial
21
+ * snapshots stream once bytesProcessed ≥ minInputBytes (default 25 MiB,
22
+ * provisional until #61 ratifies), at most one per intervalMs (default 1000).
23
+ * `false` disables; `{ minInputBytes: 0 }` forces immediate previews.
24
+ */
25
+ partialPreview?: false | {
26
+ minInputBytes?: number;
27
+ intervalMs?: number;
28
+ };
29
+ /**
30
+ * Dialect annotation (DD-005 §4.5): SERIALIZABLE selection only — adapter
31
+ * implementations live inside the worker entry. 'auto' (default) runs
32
+ * detection over the entry's registered set; an array restricts by id;
33
+ * false disables.
34
+ */
35
+ dialects?: 'auto' | string[] | false;
36
+ /** Per-adapter structured-cloneable configuration, keyed by adapter id. */
37
+ dialectConfig?: Record<string, unknown>;
38
+ /**
39
+ * Container extraction (DD-005 §4.4): 'auto' (default) sniffs sliceable
40
+ * inputs against the worker entry's registered container adapters; an array
41
+ * restricts by id; false disables.
42
+ */
43
+ containers?: 'auto' | string[] | false;
44
+ /** Plate selection for multi-plate containers (default 0 + a
45
+ * `container-multiple-plates` warning carrying the discovered plate list). */
46
+ plate?: number;
47
+ }
48
+ /** §5.4 provisional progressive-preview threshold (ratified by #61). */
49
+ export declare const PARTIAL_PREVIEW_MIN_BYTES: number;
50
+ /** Blob is structured-cloneable; ReadableStream is transferred (§4.2). */
51
+ export type ParseInputWire = string | Uint8Array | BlobLike | ReadableStreamLike<Uint8Array>;
52
+ export interface ParseRequest {
53
+ v: number;
54
+ type: 'parse';
55
+ id: number;
56
+ input: ParseInputWire;
57
+ opts?: WireParseOptions;
58
+ }
59
+ export interface CancelRequest {
60
+ v: number;
61
+ type: 'cancel';
62
+ id: number;
63
+ }
64
+ export type WorkerRequest = ParseRequest | CancelRequest;
65
+ export interface ProgressMessage {
66
+ v: number;
67
+ type: 'progress';
68
+ id: number;
69
+ bytesProcessed: number;
70
+ totalBytes: number;
71
+ phase: 'parsing' | 'finalizing';
72
+ }
73
+ export interface DoneMessage {
74
+ v: number;
75
+ type: 'done';
76
+ id: number;
77
+ ir: ToolpathIR;
78
+ stats: ParseStats;
79
+ /** Dialect/machine metadata beside the IR (DD-005 §4.2); thumbnails transferred. */
80
+ metadata?: DialectMetadata;
81
+ }
82
+ export interface ErrorMessage {
83
+ v: number;
84
+ type: 'error';
85
+ id: number;
86
+ error: {
87
+ code: string;
88
+ message: string;
89
+ srcByte?: number;
90
+ };
91
+ }
92
+ export interface CancelledMessage {
93
+ v: number;
94
+ type: 'cancelled';
95
+ id: number;
96
+ partial?: {
97
+ ir: ToolpathIR;
98
+ stats: ParseStats;
99
+ };
100
+ }
101
+ /**
102
+ * Progressive-preview snapshot (#60): a path-aligned DELTA of segments since the
103
+ * previous partial, transferred zero-copy. `header.complete` is always false;
104
+ * the terminal `done` IR REPLACES the accumulated preview wholesale.
105
+ */
106
+ export interface PartialMessage {
107
+ v: number;
108
+ type: 'partial';
109
+ id: number;
110
+ slice: ToolpathIR;
111
+ /** Total segments emitted across all partials so far (= end of this delta). */
112
+ cumulativeSegments: number;
113
+ }
114
+ export type WorkerResponse = ProgressMessage | PartialMessage | DoneMessage | ErrorMessage | CancelledMessage;
115
+ /** Collect every ArrayBuffer backing an IR's typed arrays for zero-copy transfer. */
116
+ export declare function irTransferList(ir: ToolpathIR): ArrayBuffer[];
117
+ //# sourceMappingURL=protocol.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"protocol.d.ts","sourceRoot":"","sources":["../src/protocol.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,29 @@
1
+ export const PROTOCOL_VERSION = 1;
2
+ /** §5.4 provisional progressive-preview threshold (ratified by #61). */
3
+ export const PARTIAL_PREVIEW_MIN_BYTES = 25 * 1024 * 1024;
4
+ /** Collect every ArrayBuffer backing an IR's typed arrays for zero-copy transfer. */
5
+ export function irTransferList(ir) {
6
+ const s = ir.segments;
7
+ const buffers = new Set();
8
+ for (const view of [
9
+ s.x0,
10
+ s.y0,
11
+ s.z0,
12
+ s.x1,
13
+ s.y1,
14
+ s.z1,
15
+ s.e,
16
+ s.feedrate,
17
+ s.kind,
18
+ s.tool,
19
+ s.layer,
20
+ s.feature,
21
+ s.object,
22
+ s.srcByte,
23
+ ir.sourceIndex.byteOffsets,
24
+ ir.sourceIndex.segmentIndices
25
+ ]) {
26
+ buffers.add(view.buffer);
27
+ }
28
+ return [...buffers];
29
+ }
@@ -0,0 +1,73 @@
1
+ /**
2
+ * GcodeParseSession — the worker client (DD-003 §4.1/§4.4/§5, issue #45).
3
+ *
4
+ * Terminal contract (§4.3): `done` RESOLVES `parse()` (including limit-bounded
5
+ * results — check `result.ir.header.complete` / `result.stats.stopReason`);
6
+ * `error` REJECTS with ParseSessionError; `cancelled` REJECTS with
7
+ * CancelledError (optional bounded partial attached when `partialOnCancel`).
8
+ *
9
+ * Cancellation (§5.2): cooperative via the worker's yield loop. If the worker
10
+ * fails to acknowledge within TERMINATE_FALLBACK_MS the session terminates it —
11
+ * strictly a last-resort backstop, asserted against in tests.
12
+ */
13
+ import type { ParseResult } from './parse.js';
14
+ import type { ToolpathIR } from '@chestnutlabs/toolpath-core';
15
+ import { type ParseInputWire, type WireParseOptions } from './protocol.js';
16
+ export interface WorkerLike {
17
+ /** Transfer list may carry ArrayBuffers and (in browsers) transferable streams. */
18
+ postMessage(msg: unknown, transfer?: unknown[]): void;
19
+ terminate(): void;
20
+ onmessage: ((ev: {
21
+ data: unknown;
22
+ }) => void) | null;
23
+ onerror: ((err: unknown) => void) | null;
24
+ }
25
+ export interface SessionOptions {
26
+ /** Escape hatch (DD-003 §4.4 option c): supply a Worker instance or factory. */
27
+ worker?: WorkerLike | (() => WorkerLike);
28
+ /** Cancel-acknowledgement backstop before terminate() (default 2000 ms). */
29
+ terminateFallbackMs?: number;
30
+ }
31
+ export interface ParseProgress {
32
+ bytesProcessed: number;
33
+ totalBytes: number;
34
+ phase: 'parsing' | 'finalizing';
35
+ }
36
+ export declare class ParseSessionError extends Error {
37
+ readonly code: string;
38
+ readonly srcByte?: number | undefined;
39
+ constructor(code: string, message: string, srcByte?: number | undefined);
40
+ }
41
+ export declare class CancelledError extends ParseSessionError {
42
+ readonly partial?: ParseResult | undefined;
43
+ /** True only when the terminate() backstop fired (cooperative path failed). */
44
+ readonly terminated: boolean;
45
+ constructor(message: string, partial?: ParseResult | undefined,
46
+ /** True only when the terminate() backstop fired (cooperative path failed). */
47
+ terminated?: boolean);
48
+ }
49
+ export declare class GcodeParseSession {
50
+ private readonly factory;
51
+ private readonly terminateFallbackMs;
52
+ private worker;
53
+ private active;
54
+ private nextId;
55
+ private progressListeners;
56
+ private partialListeners;
57
+ constructor(opts?: SessionOptions);
58
+ onProgress(cb: (p: ParseProgress) => void): () => void;
59
+ /**
60
+ * Progressive-preview deltas (#60): path-aligned IR slices streamed while a
61
+ * large parse runs (`partialPreview` wire option; on by default ≥ 25 MiB).
62
+ * The resolved `done` IR REPLACES everything the partials showed.
63
+ */
64
+ onPartial(cb: (slice: ToolpathIR, cumulativeSegments: number) => void): () => void;
65
+ parse(input: ParseInputWire, opts?: WireParseOptions): Promise<ParseResult>;
66
+ cancel(): void;
67
+ dispose(): void;
68
+ private ensureWorker;
69
+ private teardownWorker;
70
+ private settle;
71
+ private onMessage;
72
+ }
73
+ //# sourceMappingURL=session.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session.d.ts","sourceRoot":"","sources":["../src/session.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAC9C,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,6BAA6B,CAAC;AAE9D,OAAO,EAAoB,KAAK,cAAc,EAAE,KAAK,gBAAgB,EAAuB,MAAM,eAAe,CAAC;AAElH,MAAM,WAAW,UAAU;IACzB,mFAAmF;IACnF,WAAW,CAAC,GAAG,EAAE,OAAO,EAAE,QAAQ,CAAC,EAAE,OAAO,EAAE,GAAG,IAAI,CAAC;IACtD,SAAS,IAAI,IAAI,CAAC;IAClB,SAAS,EAAE,CAAC,CAAC,EAAE,EAAE;QAAE,IAAI,EAAE,OAAO,CAAA;KAAE,KAAK,IAAI,CAAC,GAAG,IAAI,CAAC;IACpD,OAAO,EAAE,CAAC,CAAC,GAAG,EAAE,OAAO,KAAK,IAAI,CAAC,GAAG,IAAI,CAAC;CAC1C;AAED,MAAM,WAAW,cAAc;IAC7B,gFAAgF;IAChF,MAAM,CAAC,EAAE,UAAU,GAAG,CAAC,MAAM,UAAU,CAAC,CAAC;IACzC,4EAA4E;IAC5E,mBAAmB,CAAC,EAAE,MAAM,CAAC;CAC9B;AAED,MAAM,WAAW,aAAa;IAC5B,cAAc,EAAE,MAAM,CAAC;IACvB,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,SAAS,GAAG,YAAY,CAAC;CACjC;AAED,qBAAa,iBAAkB,SAAQ,KAAK;aAExB,IAAI,EAAE,MAAM;aAEZ,OAAO,CAAC,EAAE,MAAM;gBAFhB,IAAI,EAAE,MAAM,EAC5B,OAAO,EAAE,MAAM,EACC,OAAO,CAAC,EAAE,MAAM,YAAA;CAKnC;AAED,qBAAa,cAAe,SAAQ,iBAAiB;aAGjC,OAAO,CAAC,EAAE,WAAW;IACrC,+EAA+E;aAC/D,UAAU;gBAH1B,OAAO,EAAE,MAAM,EACC,OAAO,CAAC,EAAE,WAAW,YAAA;IACrC,+EAA+E;IAC/D,UAAU,UAAQ;CAKrC;AAeD,qBAAa,iBAAiB;IAC5B,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAmB;IAC3C,OAAO,CAAC,QAAQ,CAAC,mBAAmB,CAAS;IAC7C,OAAO,CAAC,MAAM,CAA2B;IACzC,OAAO,CAAC,MAAM,CAA4B;IAC1C,OAAO,CAAC,MAAM,CAAK;IACnB,OAAO,CAAC,iBAAiB,CAAyC;IAClE,OAAO,CAAC,gBAAgB,CAAsE;gBAElF,IAAI,GAAE,cAAmB;IAOrC,UAAU,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,aAAa,KAAK,IAAI,GAAG,MAAM,IAAI;IAKtD;;;;OAIG;IACH,SAAS,CAAC,EAAE,EAAE,CAAC,KAAK,EAAE,UAAU,EAAE,kBAAkB,EAAE,MAAM,KAAK,IAAI,GAAG,MAAM,IAAI;IAKlF,KAAK,CAAC,KAAK,EAAE,cAAc,EAAE,IAAI,GAAE,gBAAqB,GAAG,OAAO,CAAC,WAAW,CAAC;IAiB/E,MAAM,IAAI,IAAI;IAed,OAAO,IAAI,IAAI;IAUf,OAAO,CAAC,YAAY;IAuBpB,OAAO,CAAC,cAAc;IAStB,OAAO,CAAC,MAAM;IAOd,OAAO,CAAC,SAAS;CAmClB"}
@@ -0,0 +1,156 @@
1
+ import { isReadableStreamLike } from './streaming.js';
2
+ import { PROTOCOL_VERSION } from './protocol.js';
3
+ export class ParseSessionError extends Error {
4
+ code;
5
+ srcByte;
6
+ constructor(code, message, srcByte) {
7
+ super(message);
8
+ this.code = code;
9
+ this.srcByte = srcByte;
10
+ this.name = 'ParseSessionError';
11
+ }
12
+ }
13
+ export class CancelledError extends ParseSessionError {
14
+ partial;
15
+ terminated;
16
+ constructor(message, partial,
17
+ /** True only when the terminate() backstop fired (cooperative path failed). */
18
+ terminated = false) {
19
+ super('E_CANCELLED', message);
20
+ this.partial = partial;
21
+ this.terminated = terminated;
22
+ this.name = 'CancelledError';
23
+ }
24
+ }
25
+ /** Default worker path (DD-003 §4.4 option a): bundler-native module worker. */
26
+ function defaultWorkerFactory() {
27
+ return new Worker(new URL('./worker.js', import.meta.url), { type: 'module' });
28
+ }
29
+ export class GcodeParseSession {
30
+ factory;
31
+ terminateFallbackMs;
32
+ worker = null;
33
+ active = null;
34
+ nextId = 1;
35
+ progressListeners = new Set();
36
+ partialListeners = new Set();
37
+ constructor(opts = {}) {
38
+ const supplied = opts.worker;
39
+ this.factory =
40
+ supplied === undefined ? defaultWorkerFactory : typeof supplied === 'function' ? supplied : () => supplied;
41
+ this.terminateFallbackMs = opts.terminateFallbackMs ?? 2000;
42
+ }
43
+ onProgress(cb) {
44
+ this.progressListeners.add(cb);
45
+ return () => this.progressListeners.delete(cb);
46
+ }
47
+ /**
48
+ * Progressive-preview deltas (#60): path-aligned IR slices streamed while a
49
+ * large parse runs (`partialPreview` wire option; on by default ≥ 25 MiB).
50
+ * The resolved `done` IR REPLACES everything the partials showed.
51
+ */
52
+ onPartial(cb) {
53
+ this.partialListeners.add(cb);
54
+ return () => this.partialListeners.delete(cb);
55
+ }
56
+ parse(input, opts = {}) {
57
+ if (this.active !== null) {
58
+ return Promise.reject(new ParseSessionError('E_BUSY', 'a parse is already in progress on this session'));
59
+ }
60
+ const worker = this.ensureWorker();
61
+ const id = this.nextId++;
62
+ return new Promise((resolve, reject) => {
63
+ this.active = { id, resolve, reject, partialOnCancel: opts.partialOnCancel === true };
64
+ // §4.2 wire strategy: Uint8Array transfers its buffer (zero-copy); a
65
+ // ReadableStream is transferred; a Blob is a cheap cloneable handle.
66
+ let transfer;
67
+ if (input instanceof Uint8Array)
68
+ transfer = [input.buffer];
69
+ else if (isReadableStreamLike(input) && typeof input !== 'string')
70
+ transfer = [input];
71
+ worker.postMessage({ v: PROTOCOL_VERSION, type: 'parse', id, input, opts }, transfer);
72
+ });
73
+ }
74
+ cancel() {
75
+ const active = this.active;
76
+ if (active === null || this.worker === null)
77
+ return;
78
+ this.worker.postMessage({ v: PROTOCOL_VERSION, type: 'cancel', id: active.id });
79
+ active.cancelTimer = setTimeout(() => {
80
+ // Last-resort backstop (§5.2): the cooperative path failed to acknowledge.
81
+ this.teardownWorker();
82
+ this.settle(active, undefined, new CancelledError('cancelled (worker terminated after timeout)', undefined, true));
83
+ }, this.terminateFallbackMs);
84
+ }
85
+ dispose() {
86
+ const active = this.active;
87
+ if (active !== null) {
88
+ this.settle(active, undefined, new ParseSessionError('E_DISPOSED', 'session disposed'));
89
+ }
90
+ this.teardownWorker();
91
+ this.progressListeners.clear();
92
+ this.partialListeners.clear();
93
+ }
94
+ ensureWorker() {
95
+ if (this.worker === null) {
96
+ const w = this.factory();
97
+ w.onmessage = (ev) => this.onMessage(ev.data);
98
+ w.onerror = (err) => {
99
+ const active = this.active;
100
+ this.teardownWorker();
101
+ if (active !== null) {
102
+ this.settle(active, undefined, new ParseSessionError('E_WORKER_CRASHED', `worker crashed: ${err instanceof Error ? err.message : String(err)}`));
103
+ }
104
+ };
105
+ this.worker = w;
106
+ }
107
+ return this.worker;
108
+ }
109
+ teardownWorker() {
110
+ if (this.worker !== null) {
111
+ this.worker.onmessage = null;
112
+ this.worker.onerror = null;
113
+ this.worker.terminate();
114
+ this.worker = null;
115
+ }
116
+ }
117
+ settle(active, result, error) {
118
+ if (active.cancelTimer !== undefined)
119
+ clearTimeout(active.cancelTimer);
120
+ if (this.active === active)
121
+ this.active = null;
122
+ if (error !== undefined)
123
+ active.reject(error);
124
+ else if (result !== undefined)
125
+ active.resolve(result);
126
+ }
127
+ onMessage(msg) {
128
+ const active = this.active;
129
+ if (active === null || msg.id !== active.id) {
130
+ if (msg.type === 'progress')
131
+ return; // stale progress after settle — ignore
132
+ return;
133
+ }
134
+ switch (msg.type) {
135
+ case 'progress':
136
+ for (const cb of this.progressListeners) {
137
+ cb({ bytesProcessed: msg.bytesProcessed, totalBytes: msg.totalBytes, phase: msg.phase });
138
+ }
139
+ return;
140
+ case 'partial':
141
+ for (const cb of this.partialListeners) {
142
+ cb(msg.slice, msg.cumulativeSegments);
143
+ }
144
+ return;
145
+ case 'done':
146
+ this.settle(active, { ir: msg.ir, stats: msg.stats, metadata: msg.metadata });
147
+ return;
148
+ case 'cancelled':
149
+ this.settle(active, undefined, new CancelledError('parse cancelled', msg.partial ? { ir: msg.partial.ir, stats: msg.partial.stats } : undefined));
150
+ return;
151
+ case 'error':
152
+ this.settle(active, undefined, new ParseSessionError(msg.error.code, msg.error.message, msg.error.srcByte));
153
+ return;
154
+ }
155
+ }
156
+ }
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Streaming inputs (DD-003 §4.2, phase 3, issue #46).
3
+ *
4
+ * Worker-side line-drain streaming: bytes → TextDecoder{stream:true} → a rolling
5
+ * buffer bounded by `maxLineLength` → complete lines fed to the shared engine.
6
+ * Yields happen naturally at every chunk `read()` plus the same §5.2 time-slice
7
+ * yield between lines of a large chunk, so cancellation stays cooperative.
8
+ *
9
+ * Limits on unknown-size streams are enforced INCREMENTALLY: `maxInputBytes` is
10
+ * checked as bytes arrive (a `Blob`'s known size is still checked upfront), and an
11
+ * overlong line cannot grow the rolling buffer beyond `maxLineLength` — the
12
+ * remainder of the line is discarded with a `line-too-long` warning, exactly like
13
+ * the in-memory path, so hostile input cannot balloon memory.
14
+ *
15
+ * Byte offsets use the same single-byte (ASCII/UTF-8) accounting as the in-memory
16
+ * drivers so stream and sync parses of the same file produce identical IR.
17
+ */
18
+ import { type AsyncParseHooks, type AsyncParseResult, type ParseOptions } from './parse.js';
19
+ /** Anything that can hand us chunks of bytes. */
20
+ export type StreamInput = BlobLike | ReadableStreamLike<Uint8Array>;
21
+ export interface BlobLike {
22
+ readonly size: number;
23
+ stream(): ReadableStreamLike<Uint8Array>;
24
+ }
25
+ export interface ReadableStreamLike<T> {
26
+ getReader(): {
27
+ read(): Promise<{
28
+ done: boolean;
29
+ value?: T;
30
+ }>;
31
+ releaseLock?(): void;
32
+ cancel?(): Promise<void>;
33
+ };
34
+ }
35
+ export declare function isBlobLike(v: unknown): v is BlobLike;
36
+ export declare function isReadableStreamLike(v: unknown): v is ReadableStreamLike<Uint8Array>;
37
+ /**
38
+ * Parse a byte stream into a ToolpathIR with the same semantics (and identical
39
+ * output) as the in-memory drivers. Cooperative and cancellable throughout.
40
+ */
41
+ export declare function parseGcodeStreamToIR(input: StreamInput, opts?: ParseOptions, hooks?: AsyncParseHooks): Promise<AsyncParseResult>;
42
+ //# sourceMappingURL=streaming.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"streaming.d.ts","sourceRoot":"","sources":["../src/streaming.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AACH,OAAO,EAGL,KAAK,eAAe,EACpB,KAAK,gBAAgB,EACrB,KAAK,YAAY,EAClB,MAAM,YAAY,CAAC;AAEpB,iDAAiD;AACjD,MAAM,MAAM,WAAW,GAAG,QAAQ,GAAG,kBAAkB,CAAC,UAAU,CAAC,CAAC;AAEpE,MAAM,WAAW,QAAQ;IACvB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,MAAM,IAAI,kBAAkB,CAAC,UAAU,CAAC,CAAC;CAC1C;AAED,MAAM,WAAW,kBAAkB,CAAC,CAAC;IACnC,SAAS,IAAI;QAAE,IAAI,IAAI,OAAO,CAAC;YAAE,IAAI,EAAE,OAAO,CAAC;YAAC,KAAK,CAAC,EAAE,CAAC,CAAA;SAAE,CAAC,CAAC;QAAC,WAAW,CAAC,IAAI,IAAI,CAAC;QAAC,MAAM,CAAC,IAAI,OAAO,CAAC,IAAI,CAAC,CAAA;KAAE,CAAC;CAChH;AAED,wBAAgB,UAAU,CAAC,CAAC,EAAE,OAAO,GAAG,CAAC,IAAI,QAAQ,CAOpD;AAED,wBAAgB,oBAAoB,CAAC,CAAC,EAAE,OAAO,GAAG,CAAC,IAAI,kBAAkB,CAAC,UAAU,CAAC,CAEpF;AAcD;;;GAGG;AACH,wBAAsB,oBAAoB,CACxC,KAAK,EAAE,WAAW,EAClB,IAAI,GAAE,YAAiB,EACvB,KAAK,GAAE,eAAoB,GAC1B,OAAO,CAAC,gBAAgB,CAAC,CA4H3B"}
@@ -0,0 +1,162 @@
1
+ /**
2
+ * Streaming inputs (DD-003 §4.2, phase 3, issue #46).
3
+ *
4
+ * Worker-side line-drain streaming: bytes → TextDecoder{stream:true} → a rolling
5
+ * buffer bounded by `maxLineLength` → complete lines fed to the shared engine.
6
+ * Yields happen naturally at every chunk `read()` plus the same §5.2 time-slice
7
+ * yield between lines of a large chunk, so cancellation stays cooperative.
8
+ *
9
+ * Limits on unknown-size streams are enforced INCREMENTALLY: `maxInputBytes` is
10
+ * checked as bytes arrive (a `Blob`'s known size is still checked upfront), and an
11
+ * overlong line cannot grow the rolling buffer beyond `maxLineLength` — the
12
+ * remainder of the line is discarded with a `line-too-long` warning, exactly like
13
+ * the in-memory path, so hostile input cannot balloon memory.
14
+ *
15
+ * Byte offsets use the same single-byte (ASCII/UTF-8) accounting as the in-memory
16
+ * drivers so stream and sync parses of the same file produce identical IR.
17
+ */
18
+ import { createEngine, makePartialEmitter } from './parse.js';
19
+ export function isBlobLike(v) {
20
+ return (typeof v === 'object' &&
21
+ v !== null &&
22
+ typeof v.stream === 'function' &&
23
+ typeof v.size === 'number');
24
+ }
25
+ export function isReadableStreamLike(v) {
26
+ return typeof v === 'object' && v !== null && typeof v.getReader === 'function';
27
+ }
28
+ function yieldMacrotask() {
29
+ return new Promise((resolve) => {
30
+ const ch = new MessageChannel();
31
+ ch.port1.onmessage = () => {
32
+ ch.port1.close();
33
+ ch.port2.close();
34
+ resolve();
35
+ };
36
+ ch.port2.postMessage(null);
37
+ });
38
+ }
39
+ /**
40
+ * Parse a byte stream into a ToolpathIR with the same semantics (and identical
41
+ * output) as the in-memory drivers. Cooperative and cancellable throughout.
42
+ */
43
+ export async function parseGcodeStreamToIR(input, opts = {}, hooks = {}) {
44
+ const engine = createEngine('', opts);
45
+ const limits = engine.limits;
46
+ const yieldEvery = hooks.yieldIntervalMs ?? 50;
47
+ const knownSize = isBlobLike(input) ? input.size : undefined;
48
+ if (knownSize !== undefined && knownSize > limits.maxInputBytes) {
49
+ engine.setSourceBytes(knownSize);
50
+ engine.markInputTooBig();
51
+ return { ...engine.finish(), cancelled: false };
52
+ }
53
+ const stream = isBlobLike(input) ? input.stream() : input;
54
+ const reader = stream.getReader();
55
+ const decoder = new TextDecoder();
56
+ let buffer = '';
57
+ let lineStartOffset = 0;
58
+ let bytesRead = 0;
59
+ /** True while discarding the remainder of a line that exceeded maxLineLength. */
60
+ let skippingOverlongLine = false;
61
+ let overlongWarned = false;
62
+ let cancelled = false;
63
+ let sliceStart = Date.now();
64
+ const partial = makePartialEmitter(engine, hooks);
65
+ const drainCompleteLines = async () => {
66
+ for (;;) {
67
+ const nl = buffer.indexOf('\n');
68
+ if (nl === -1)
69
+ return;
70
+ const line = buffer.slice(0, nl);
71
+ buffer = buffer.slice(nl + 1);
72
+ if (skippingOverlongLine) {
73
+ // The discarded prefix of this line already tripped the limit; count the
74
+ // full line (prefix included) into offsets and resume normal processing.
75
+ skippingOverlongLine = false;
76
+ overlongWarned = false;
77
+ lineStartOffset += line.length + 1;
78
+ continue;
79
+ }
80
+ engine.processLine(line, lineStartOffset);
81
+ lineStartOffset += line.length + 1;
82
+ if (engine.stopped())
83
+ return;
84
+ if (Date.now() - sliceStart >= yieldEvery) {
85
+ hooks.onProgress?.(bytesRead, knownSize ?? 0, 0);
86
+ partial(bytesRead);
87
+ await yieldMacrotask();
88
+ if (hooks.shouldCancel?.()) {
89
+ cancelled = true;
90
+ return;
91
+ }
92
+ sliceStart = Date.now();
93
+ }
94
+ }
95
+ };
96
+ const boundBuffer = () => {
97
+ if (buffer.length > limits.maxLineLength && buffer.indexOf('\n') === -1) {
98
+ if (!overlongWarned) {
99
+ // Route the warning through the engine by feeding an overlong marker line:
100
+ // processLine's own maxLineLength branch records `line-too-long` (bounded).
101
+ engine.processLine('x'.repeat(limits.maxLineLength + 1), lineStartOffset);
102
+ overlongWarned = true;
103
+ }
104
+ // Discard the buffered prefix but keep offset accounting for the final line cut.
105
+ lineStartOffset += buffer.length;
106
+ buffer = '';
107
+ skippingOverlongLine = true;
108
+ }
109
+ };
110
+ try {
111
+ for (;;) {
112
+ if (engine.stopped() || cancelled)
113
+ break;
114
+ const { done, value } = await reader.read();
115
+ if (hooks.shouldCancel?.()) {
116
+ cancelled = true;
117
+ break;
118
+ }
119
+ if (done)
120
+ break;
121
+ if (value === undefined || value.byteLength === 0)
122
+ continue;
123
+ bytesRead += value.byteLength;
124
+ engine.setSourceBytes(bytesRead);
125
+ if (bytesRead > limits.maxInputBytes) {
126
+ engine.markInputTooBig();
127
+ break;
128
+ }
129
+ const decoded = decoder.decode(value, { stream: true });
130
+ // §4.5 stream detection windows: bounded head + rolling tail capture.
131
+ const windows = hooks.captureWindows;
132
+ if (windows !== undefined) {
133
+ if (windows.head.length < 64 * 1024) {
134
+ windows.head = (windows.head + decoded).slice(0, 64 * 1024);
135
+ }
136
+ windows.tail = (windows.tail + decoded).slice(-16 * 1024);
137
+ }
138
+ buffer += decoded;
139
+ boundBuffer();
140
+ await drainCompleteLines();
141
+ }
142
+ // Flush the decoder tail and any final unterminated line.
143
+ if (!engine.stopped() && !cancelled) {
144
+ buffer += decoder.decode();
145
+ boundBuffer();
146
+ await drainCompleteLines();
147
+ if (!engine.stopped() && !cancelled && !skippingOverlongLine && buffer.length > 0) {
148
+ engine.processLine(buffer, lineStartOffset);
149
+ buffer = '';
150
+ }
151
+ }
152
+ }
153
+ finally {
154
+ reader.releaseLock?.();
155
+ }
156
+ hooks.onProgress?.(bytesRead, knownSize ?? bytesRead, 0);
157
+ engine.setSourceBytes(bytesRead);
158
+ if (cancelled) {
159
+ return { ...engine.finish(lineStartOffset), cancelled: true };
160
+ }
161
+ return { ...engine.finish(), cancelled: false };
162
+ }
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Transport-agnostic worker handler (DD-003 §4.3/§5.2, issue #45).
3
+ *
4
+ * Contains the entire worker-side protocol logic without referencing any Worker
5
+ * global, so it is directly testable in Node: the host supplies `post` and feeds
6
+ * inbound messages to the returned handler. The Web Worker entry (`worker.ts`)
7
+ * is a two-line shim over this.
8
+ */
9
+ import type { DialectMetadata, ToolpathIR, Warning } from '@chestnutlabs/toolpath-core';
10
+ import { type CommandEvent } from './parse.js';
11
+ import { type ReadableStreamLike } from './streaming.js';
12
+ import { type WorkerRequest, type WorkerResponse } from './protocol.js';
13
+ export type PostFn = (msg: WorkerResponse, transfer?: ArrayBuffer[]) => void;
14
+ /**
15
+ * Structural mirror of @chestnutlabs/gcode-dialects' runner factory (DD-005
16
+ * §4.5): the parser never imports the dialects package — worker ENTRIES
17
+ * compose the two (`worker.ts` batteries-included, `worker-slim.ts` none,
18
+ * custom workers their own set).
19
+ */
20
+ export interface DialectRunLike {
21
+ onComment: (text: string, srcByte: number) => void;
22
+ onCommand: (event: CommandEvent) => void;
23
+ finalize(ir: ToolpathIR): {
24
+ metadata?: DialectMetadata;
25
+ warnings: Warning[];
26
+ };
27
+ }
28
+ export interface DialectRunnerFactoryLike {
29
+ adapterIds: string[];
30
+ createRun(input: {
31
+ selection: 'auto' | string[];
32
+ config?: Record<string, unknown>;
33
+ headText: string;
34
+ tailText: string;
35
+ containerMeta?: Record<string, string>;
36
+ }): DialectRunLike | null;
37
+ }
38
+ /** Structural mirror of gcode-containers' adapter (DD-005 §4.4) — same composition rule as dialects. */
39
+ export interface OpenedContainerLike {
40
+ id: string;
41
+ plates: {
42
+ index: number;
43
+ name: string;
44
+ estimatedBytes: number;
45
+ }[];
46
+ metadata: {
47
+ machine?: import('@chestnutlabs/toolpath-core').MachineGeometry;
48
+ filaments?: import('@chestnutlabs/toolpath-core').FilamentInfo[];
49
+ raw: Record<string, string>;
50
+ warnings: {
51
+ code: string;
52
+ message: string;
53
+ }[];
54
+ };
55
+ openPlate(index: number): ReadableStreamLike<Uint8Array>;
56
+ }
57
+ export interface ContainerAdapterLike {
58
+ id: string;
59
+ sniff(prefix: Uint8Array): boolean;
60
+ open(bytes: Uint8Array): Promise<OpenedContainerLike>;
61
+ }
62
+ export interface WorkerHandlerOptions {
63
+ /** Dialect runner composed by the worker entry (absent in worker-slim). */
64
+ dialects?: DialectRunnerFactoryLike;
65
+ /** Container adapters composed by the worker entry (absent in worker-slim). */
66
+ containers?: ContainerAdapterLike[];
67
+ }
68
+ export declare function createWorkerHandler(post: PostFn, handlerOpts?: WorkerHandlerOptions): (msg: WorkerRequest) => void;
69
+ //# sourceMappingURL=worker-core.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"worker-core.d.ts","sourceRoot":"","sources":["../src/worker-core.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,KAAK,EAAE,eAAe,EAAE,UAAU,EAAE,OAAO,EAAE,MAAM,6BAA6B,CAAC;AACxF,OAAO,EAIL,KAAK,YAAY,EAElB,MAAM,YAAY,CAAC;AACpB,OAAO,EAA0D,KAAK,kBAAkB,EAAE,MAAM,gBAAgB,CAAC;AACjH,OAAO,EAKL,KAAK,aAAa,EAClB,KAAK,cAAc,EACpB,MAAM,eAAe,CAAC;AAEvB,MAAM,MAAM,MAAM,GAAG,CAAC,GAAG,EAAE,cAAc,EAAE,QAAQ,CAAC,EAAE,WAAW,EAAE,KAAK,IAAI,CAAC;AAE7E;;;;;GAKG;AACH,MAAM,WAAW,cAAc;IAC7B,SAAS,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IACnD,SAAS,EAAE,CAAC,KAAK,EAAE,YAAY,KAAK,IAAI,CAAC;IACzC,QAAQ,CAAC,EAAE,EAAE,UAAU,GAAG;QAAE,QAAQ,CAAC,EAAE,eAAe,CAAC;QAAC,QAAQ,EAAE,OAAO,EAAE,CAAA;KAAE,CAAC;CAC/E;AAED,MAAM,WAAW,wBAAwB;IACvC,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,SAAS,CAAC,KAAK,EAAE;QACf,SAAS,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;QAC7B,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;QACjC,QAAQ,EAAE,MAAM,CAAC;QACjB,QAAQ,EAAE,MAAM,CAAC;QACjB,aAAa,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;KACxC,GAAG,cAAc,GAAG,IAAI,CAAC;CAC3B;AAED,wGAAwG;AACxG,MAAM,WAAW,mBAAmB;IAClC,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,cAAc,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IAClE,QAAQ,EAAE;QACR,OAAO,CAAC,EAAE,OAAO,6BAA6B,EAAE,eAAe,CAAC;QAChE,SAAS,CAAC,EAAE,OAAO,6BAA6B,EAAE,YAAY,EAAE,CAAC;QACjE,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;QAC5B,QAAQ,EAAE;YAAE,IAAI,EAAE,MAAM,CAAC;YAAC,OAAO,EAAE,MAAM,CAAA;SAAE,EAAE,CAAC;KAC/C,CAAC;IACF,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,kBAAkB,CAAC,UAAU,CAAC,CAAC;CAC1D;AAED,MAAM,WAAW,oBAAoB;IACnC,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,CAAC,MAAM,EAAE,UAAU,GAAG,OAAO,CAAC;IACnC,IAAI,CAAC,KAAK,EAAE,UAAU,GAAG,OAAO,CAAC,mBAAmB,CAAC,CAAC;CACvD;AAED,MAAM,WAAW,oBAAoB;IACnC,2EAA2E;IAC3E,QAAQ,CAAC,EAAE,wBAAwB,CAAC;IACpC,+EAA+E;IAC/E,UAAU,CAAC,EAAE,oBAAoB,EAAE,CAAC;CACrC;AA+CD,wBAAgB,mBAAmB,CACjC,IAAI,EAAE,MAAM,EACZ,WAAW,GAAE,oBAAyB,GACrC,CAAC,GAAG,EAAE,aAAa,KAAK,IAAI,CAgS9B"}