cantraceviewer 0.1.0-rc.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/LICENSE.md +19 -0
- package/README.md +131 -0
- package/dist/client.d.ts +21 -0
- package/dist/client.js +37 -0
- package/dist/direct.d.ts +39 -0
- package/dist/direct.js +143 -0
- package/dist/handles.d.ts +24 -0
- package/dist/handles.js +54 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +1 -0
- package/dist/node-transport.d.ts +12 -0
- package/dist/node-transport.js +38 -0
- package/dist/node-worker.d.ts +1 -0
- package/dist/node-worker.js +27 -0
- package/dist/node.d.ts +12 -0
- package/dist/node.js +25 -0
- package/dist/protocol.d.ts +73 -0
- package/dist/protocol.js +1 -0
- package/dist/rpc-client.d.ts +53 -0
- package/dist/rpc-client.js +177 -0
- package/dist/types.d.ts +99 -0
- package/dist/types.js +1 -0
- package/dist/wasm-bindgen/cantraceviewer.d.ts +95 -0
- package/dist/wasm-bindgen/cantraceviewer.js +588 -0
- package/dist/wasm-bindgen/cantraceviewer_bg.wasm +0 -0
- package/dist/wasm-bindgen/cantraceviewer_bg.wasm.d.ts +25 -0
- package/dist/worker-runtime.d.ts +23 -0
- package/dist/worker-runtime.js +152 -0
- package/dist/worker.d.ts +1 -0
- package/dist/worker.js +11 -0
- package/package.json +58 -0
package/LICENSE.md
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
Copyright (c) 2026 Tom Ford
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
4
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
5
|
+
in the Software without restriction, including without limitation the rights
|
|
6
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
7
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
8
|
+
furnished to do so, subject to the following conditions:
|
|
9
|
+
|
|
10
|
+
The above copyright notice and this permission notice shall be included in all
|
|
11
|
+
copies or substantial portions of the Software.
|
|
12
|
+
|
|
13
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
14
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
15
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
16
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
17
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
18
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
19
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# cantraceviewer
|
|
2
|
+
|
|
3
|
+
`cantraceviewer` parses DBC files and CAN traces, then decodes selected signal series. It supports ASC, PCAN TRC 1.x/2.x, BLF, and MF4 traces.
|
|
4
|
+
|
|
5
|
+
The package has three ESM entries:
|
|
6
|
+
|
|
7
|
+
- `cantraceviewer` — asynchronous browser client using one dedicated Web Worker
|
|
8
|
+
- `cantraceviewer/direct` — synchronous in-process WebAssembly client
|
|
9
|
+
- `cantraceviewer/node` — asynchronous Node.js client using one dedicated worker thread
|
|
10
|
+
|
|
11
|
+
The browser and Node clients are transports over the same direct implementation. The package has no server component and does not persist input data.
|
|
12
|
+
|
|
13
|
+
## Browser
|
|
14
|
+
|
|
15
|
+
Create the client in browser code, not during server-side rendering. `openTrace` transfers and detaches the exact input `ArrayBuffer`.
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
import { createCanTraceClient } from 'cantraceviewer';
|
|
19
|
+
|
|
20
|
+
const client = await createCanTraceClient();
|
|
21
|
+
const { handle: dbc, catalog } = await client.openDbc(await dbcFile.text());
|
|
22
|
+
const trace = await client.openTrace('asc', await traceFile.arrayBuffer());
|
|
23
|
+
|
|
24
|
+
const message = catalog.messages[0];
|
|
25
|
+
const signal = message.signals[0];
|
|
26
|
+
const series = await client.getSignalValues(
|
|
27
|
+
dbc,
|
|
28
|
+
trace.handle,
|
|
29
|
+
{
|
|
30
|
+
canId: message.canId,
|
|
31
|
+
isExtended: message.isExtended,
|
|
32
|
+
sizeBytes: message.sizeBytes
|
|
33
|
+
},
|
|
34
|
+
signal.name
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
await client.closeTrace(trace.handle);
|
|
38
|
+
await client.closeDbc(dbc);
|
|
39
|
+
await client.close();
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
`openTrace` also returns metadata, parse warnings, embedded DBC files, and the native MF4 signal catalog where applicable.
|
|
43
|
+
|
|
44
|
+
## Direct
|
|
45
|
+
|
|
46
|
+
The direct client is fully synchronous. The caller must read, fetch, or asynchronously compile the WASM before creating it. The exported `wasmUrl` identifies the package-local binary.
|
|
47
|
+
|
|
48
|
+
```ts
|
|
49
|
+
import { readFile } from 'node:fs/promises';
|
|
50
|
+
import { createDirectClient, wasmUrl } from 'cantraceviewer/direct';
|
|
51
|
+
|
|
52
|
+
const wasm = await readFile(wasmUrl);
|
|
53
|
+
const client = createDirectClient(wasm);
|
|
54
|
+
const { handle: dbc, catalog } = client.openDbc(dbcText);
|
|
55
|
+
const trace = client.openTrace('blf', traceBytes);
|
|
56
|
+
const message = catalog.messages[0];
|
|
57
|
+
const series = client.getSignalValues(
|
|
58
|
+
dbc,
|
|
59
|
+
trace.handle,
|
|
60
|
+
{
|
|
61
|
+
canId: message.canId,
|
|
62
|
+
isExtended: message.isExtended,
|
|
63
|
+
sizeBytes: message.sizeBytes
|
|
64
|
+
},
|
|
65
|
+
message.signals[0].name
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
client.closeTrace(trace.handle);
|
|
69
|
+
client.closeDbc(dbc);
|
|
70
|
+
client.close();
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
`createDirectClient` also accepts a precompiled `WebAssembly.Module`. WebAssembly initializes once per JavaScript realm; later direct clients reuse that runtime while retaining separate handle ownership. Closing handles releases their Rust allocations, but WebAssembly memory pages are not guaranteed to return to the operating system until the realm or process ends.
|
|
74
|
+
|
|
75
|
+
Direct parsing and decoding block the calling thread. Do not use this entry on a browser UI thread or Electron main thread. It is intended for an existing Worker, worker thread, isolated process, benchmark, or controlled test.
|
|
76
|
+
|
|
77
|
+
## Node.js
|
|
78
|
+
|
|
79
|
+
The Node entry has the same asynchronous API as the browser entry. It owns the direct client and all handles inside one `worker_threads` worker.
|
|
80
|
+
|
|
81
|
+
```ts
|
|
82
|
+
import { readFile } from 'node:fs/promises';
|
|
83
|
+
import { createCanTraceClient } from 'cantraceviewer/node';
|
|
84
|
+
|
|
85
|
+
const client = await createCanTraceClient();
|
|
86
|
+
const { handle: dbc, catalog } = await client.openDbc(await readFile('network.dbc', 'utf8'));
|
|
87
|
+
const file = await readFile('drive.blf');
|
|
88
|
+
const buffer = Uint8Array.from(file).buffer;
|
|
89
|
+
const trace = await client.openTrace('blf', buffer);
|
|
90
|
+
|
|
91
|
+
// Decode as in the browser example, using dbc and trace.handle.
|
|
92
|
+
|
|
93
|
+
await client.closeTrace(trace.handle);
|
|
94
|
+
await client.closeDbc(dbc);
|
|
95
|
+
await client.close();
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
The copy in this example creates an exact `ArrayBuffer`. If a Node buffer already spans an ordinary `ArrayBuffer` exactly, that underlying buffer can be passed directly.
|
|
99
|
+
|
|
100
|
+
## Electron
|
|
101
|
+
|
|
102
|
+
Two arrangements are supported:
|
|
103
|
+
|
|
104
|
+
1. Use `cantraceviewer` in the renderer. Parsing and decoding run in its browser Web Worker. Serve the renderer over HTTP(S) or a standard, secure custom protocol so its ESM Worker and WASM assets can load; a bare `file://` renderer is not supported.
|
|
105
|
+
2. Use `cantraceviewer/node` in the main process and expose application-specific operations to the renderer through a context-isolated preload and Electron IPC. Parsing and decoding run in the package's worker thread.
|
|
106
|
+
|
|
107
|
+
Do not import `cantraceviewer/direct` in the renderer or main process because it blocks that thread.
|
|
108
|
+
|
|
109
|
+
## Handles and lifecycle
|
|
110
|
+
|
|
111
|
+
DBC and trace handles are opaque and belong to the client that created them. A handle cannot be used with another client. Closing a handle is idempotent. Closing a client invalidates all of its remaining handles.
|
|
112
|
+
|
|
113
|
+
Each asynchronous client processes requests in call order. A worker startup failure, crash, message failure, or unexpected Node worker exit is fatal for that client. Pending and future operations reject, all handles become invalid, and the package does not restart the worker automatically. Create a new client to recover.
|
|
114
|
+
|
|
115
|
+
Errors are standard `Error` objects. Their names and messages are diagnostics, not stable values for application branching.
|
|
116
|
+
|
|
117
|
+
## Transfer semantics
|
|
118
|
+
|
|
119
|
+
The browser and Node clients require the exact ordinary `ArrayBuffer` containing a trace. The buffer is transferred to the worker and detached immediately, including when parsing later fails. Typed-array views are rejected rather than copied implicitly. Node buffers marked as untransferable are rejected before posting and remain attached.
|
|
120
|
+
|
|
121
|
+
Decoded `timesMs` and `values` are two `Float64Array` views over one exactly sized `ArrayBuffer` transferred from the worker. Transferring that shared buffer elsewhere detaches both views.
|
|
122
|
+
|
|
123
|
+
The direct client accepts a `Uint8Array` and does not detach it.
|
|
124
|
+
|
|
125
|
+
## Supported environments
|
|
126
|
+
|
|
127
|
+
- Node.js 22.12 or newer for `cantraceviewer/node`
|
|
128
|
+
- Browsers and Electron renderers with ESM, module Workers, WebAssembly, and transferable `ArrayBuffer` support for `cantraceviewer`
|
|
129
|
+
- ESM-aware bundlers that preserve the standard `new Worker(new URL(..., import.meta.url))` asset pattern
|
|
130
|
+
|
|
131
|
+
The package does not support CommonJS, server persistence, browser environments without module Workers, synchronous browser UI-thread use, or synchronous Electron main-thread use.
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { type CanTraceClient } from './rpc-client.js';
|
|
2
|
+
export type { CanTraceClient } from './rpc-client.js';
|
|
3
|
+
/** Worker surface the browser transport depends on; production uses a real module Worker. @internal */
|
|
4
|
+
export type ClientWorkerEvent = {
|
|
5
|
+
data?: unknown;
|
|
6
|
+
message?: string;
|
|
7
|
+
};
|
|
8
|
+
/** @internal */
|
|
9
|
+
export type ClientWorker = {
|
|
10
|
+
postMessage(message: unknown, transfer: Transferable[]): void;
|
|
11
|
+
addEventListener(type: 'message' | 'error' | 'messageerror', listener: (event: ClientWorkerEvent) => void): void;
|
|
12
|
+
terminate(): void;
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* Create a client backed by a new dedicated module Worker. Call in the browser only; nothing is
|
|
16
|
+
* instantiated at module import time, so importing this package is SSR/prerender safe. This module
|
|
17
|
+
* never touches Node built-ins, so it stays bundleable for the browser.
|
|
18
|
+
*/
|
|
19
|
+
export declare function createCanTraceClient(): Promise<CanTraceClient>;
|
|
20
|
+
/** Internal seam for deterministic tests; not exported from the package root. @internal */
|
|
21
|
+
export declare function createCanTraceClientForWorker(createWorker: () => ClientWorker): Promise<CanTraceClient>;
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { createRpcClient } from "./rpc-client.js";
|
|
2
|
+
/**
|
|
3
|
+
* Create a client backed by a new dedicated module Worker. Call in the browser only; nothing is
|
|
4
|
+
* instantiated at module import time, so importing this package is SSR/prerender safe. This module
|
|
5
|
+
* never touches Node built-ins, so it stays bundleable for the browser.
|
|
6
|
+
*/
|
|
7
|
+
export async function createCanTraceClient() {
|
|
8
|
+
if (typeof Worker === 'undefined') {
|
|
9
|
+
throw new Error('createCanTraceClient requires Web Worker support; call it in the browser');
|
|
10
|
+
}
|
|
11
|
+
// Keep this constructor call in literal `new Worker(new URL(...), ...)` form so bundlers
|
|
12
|
+
// detect it and emit the worker entry chunk.
|
|
13
|
+
return createCanTraceClientForWorker(() => new Worker(new URL('./worker.js', import.meta.url), { type: 'module' }));
|
|
14
|
+
}
|
|
15
|
+
/** Internal seam for deterministic tests; not exported from the package root. @internal */
|
|
16
|
+
export async function createCanTraceClientForWorker(createWorker) {
|
|
17
|
+
return createRpcClient((handlers) => browserTransport(createWorker(), handlers));
|
|
18
|
+
}
|
|
19
|
+
/** Web Worker events mapped onto the shared client core. */
|
|
20
|
+
function browserTransport(worker, handlers) {
|
|
21
|
+
worker.addEventListener('message', (event) => handlers.message(event.data));
|
|
22
|
+
worker.addEventListener('error', (event) => {
|
|
23
|
+
handlers.fail(new Error(`worker crashed${event.message ? `: ${event.message}` : ''}`));
|
|
24
|
+
});
|
|
25
|
+
worker.addEventListener('messageerror', () => {
|
|
26
|
+
handlers.fail(new Error('worker message failed to deserialize'));
|
|
27
|
+
});
|
|
28
|
+
return {
|
|
29
|
+
postMessage(message, transfer) {
|
|
30
|
+
worker.postMessage(message, transfer);
|
|
31
|
+
},
|
|
32
|
+
async terminate() {
|
|
33
|
+
// Web Worker termination is immediate and synchronous; nothing to await.
|
|
34
|
+
worker.terminate();
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
}
|
package/dist/direct.d.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/// <reference lib="dom" />
|
|
2
|
+
import type { DbcHandle, DbcMessageIdentity, DecodedSignalSeries, OpenDbcResult, OpenTraceResult, TraceHandle, TraceType } from './types.js';
|
|
3
|
+
export type * from './types.js';
|
|
4
|
+
/** WASM bytes or an already compiled module. Compilation of bytes is synchronous. */
|
|
5
|
+
export type DirectWasmInput = BufferSource | WebAssembly.Module;
|
|
6
|
+
/** Package-local WASM asset URL. Read, fetch, or compile it before synchronous initialization. */
|
|
7
|
+
export declare const wasmUrl: URL;
|
|
8
|
+
/**
|
|
9
|
+
* Fully synchronous in-process client. Every operation compiles, parses, or decodes on the calling
|
|
10
|
+
* thread and returns its result directly; nothing here creates a Worker or a promise.
|
|
11
|
+
*
|
|
12
|
+
* Use it only where blocking is acceptable: inside a Worker or worker thread, in a dedicated
|
|
13
|
+
* process, in benchmarks, or in tests. Do not call it on a browser UI thread or an Electron main
|
|
14
|
+
* thread; use the asynchronous browser or Node clients there.
|
|
15
|
+
*/
|
|
16
|
+
export type DirectClient = {
|
|
17
|
+
openDbc(text: string): OpenDbcResult;
|
|
18
|
+
/** Idempotent for handles this client issued; repeat calls do nothing. */
|
|
19
|
+
closeDbc(handle: DbcHandle): void;
|
|
20
|
+
openTrace(traceType: TraceType, bytes: Uint8Array): OpenTraceResult;
|
|
21
|
+
/** Idempotent for handles this client issued; repeat calls do nothing. */
|
|
22
|
+
closeTrace(handle: TraceHandle): void;
|
|
23
|
+
/**
|
|
24
|
+
* Decode one DBC signal over the trace's raw frames. Both returned arrays are views over one
|
|
25
|
+
* exactly-sized ArrayBuffer, so transports can transfer the result without copying.
|
|
26
|
+
*/
|
|
27
|
+
getSignalValues(dbcHandle: DbcHandle, traceHandle: TraceHandle, messageIdentity: DbcMessageIdentity, signalName: string): DecodedSignalSeries;
|
|
28
|
+
/** Read one signal the trace already carries decoded, identified by its MF4 catalog id. */
|
|
29
|
+
getMf4SignalValues(traceHandle: TraceHandle, signalId: number): DecodedSignalSeries;
|
|
30
|
+
/** Idempotent. Frees every handle this client still owns. */
|
|
31
|
+
close(): void;
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* Create a synchronous client over one WASM instance. WASM initialization happens once per
|
|
35
|
+
* JavaScript realm: later clients reuse that instance and ignore their input, but each client owns
|
|
36
|
+
* its own handles. Fetching or reading the bytes is the caller's job and can be asynchronous; this
|
|
37
|
+
* call is not.
|
|
38
|
+
*/
|
|
39
|
+
export declare function createDirectClient(wasm: DirectWasmInput): DirectClient;
|
package/dist/direct.js
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/// <reference lib="dom" />
|
|
2
|
+
import { createHandleRegistry } from "./handles.js";
|
|
3
|
+
import { initSync, Dbc as WasmDbc, Trace as WasmTrace } from './wasm-bindgen/cantraceviewer.js';
|
|
4
|
+
/** Package-local WASM asset URL. Read, fetch, or compile it before synchronous initialization. */
|
|
5
|
+
export const wasmUrl = new URL('./wasm-bindgen/cantraceviewer_bg.wasm', import.meta.url);
|
|
6
|
+
/**
|
|
7
|
+
* Create a synchronous client over one WASM instance. WASM initialization happens once per
|
|
8
|
+
* JavaScript realm: later clients reuse that instance and ignore their input, but each client owns
|
|
9
|
+
* its own handles. Fetching or reading the bytes is the caller's job and can be asynchronous; this
|
|
10
|
+
* call is not.
|
|
11
|
+
*/
|
|
12
|
+
export function createDirectClient(wasm) {
|
|
13
|
+
// The generated `initSync` returns the existing instance when it is already initialized.
|
|
14
|
+
withWasmErrors(() => initSync({ module: wasm }));
|
|
15
|
+
const handles = createHandleRegistry();
|
|
16
|
+
let clientClosed = false;
|
|
17
|
+
function assertClientOpen() {
|
|
18
|
+
if (clientClosed)
|
|
19
|
+
throw new Error('client is closed');
|
|
20
|
+
}
|
|
21
|
+
return {
|
|
22
|
+
openDbc(text) {
|
|
23
|
+
assertClientOpen();
|
|
24
|
+
const dbc = withWasmErrors(() => WasmDbc.parse(text));
|
|
25
|
+
try {
|
|
26
|
+
const catalog = JSON.parse(withWasmErrors(() => dbc.catalogJson()));
|
|
27
|
+
return { handle: handles.issue('dbc', dbc), catalog };
|
|
28
|
+
}
|
|
29
|
+
catch (error) {
|
|
30
|
+
dbc.free();
|
|
31
|
+
throw error;
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
closeDbc(handle) {
|
|
35
|
+
freeHandle(handles.release('dbc', handle));
|
|
36
|
+
},
|
|
37
|
+
openTrace(traceType, bytes) {
|
|
38
|
+
assertClientOpen();
|
|
39
|
+
const trace = withWasmErrors(() => parseTrace(traceType, bytes));
|
|
40
|
+
try {
|
|
41
|
+
const metadata = {
|
|
42
|
+
measurementStartMs: trace.measurementStartMs ?? null,
|
|
43
|
+
validMessageCount: trace.validMessageCount,
|
|
44
|
+
skippedLineCount: trace.skippedLineCount,
|
|
45
|
+
durationNs: trace.durationNs ?? null
|
|
46
|
+
};
|
|
47
|
+
const isMf4 = traceType === 'mf4';
|
|
48
|
+
const hasRawFrames = trace.hasRawFrames;
|
|
49
|
+
const mf4Catalog = isMf4
|
|
50
|
+
? JSON.parse(withWasmErrors(() => trace.mf4CatalogJson()))
|
|
51
|
+
: null;
|
|
52
|
+
const embeddedDbcs = isMf4
|
|
53
|
+
? JSON.parse(withWasmErrors(() => trace.mf4EmbeddedDbcsJson()))
|
|
54
|
+
: [];
|
|
55
|
+
const warnings = isMf4
|
|
56
|
+
? JSON.parse(withWasmErrors(() => trace.mf4WarningsJson()))
|
|
57
|
+
: [];
|
|
58
|
+
return {
|
|
59
|
+
handle: handles.issue('trace', trace),
|
|
60
|
+
metadata,
|
|
61
|
+
hasRawFrames,
|
|
62
|
+
mf4Catalog,
|
|
63
|
+
embeddedDbcs,
|
|
64
|
+
warnings
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
trace.free();
|
|
69
|
+
throw error;
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
closeTrace(handle) {
|
|
73
|
+
freeHandle(handles.release('trace', handle));
|
|
74
|
+
},
|
|
75
|
+
getSignalValues(dbcHandle, traceHandle, messageIdentity, signalName) {
|
|
76
|
+
assertClientOpen();
|
|
77
|
+
const dbc = handles.payload('dbc', dbcHandle);
|
|
78
|
+
const trace = handles.payload('trace', traceHandle);
|
|
79
|
+
return unpackSeries(withWasmErrors(() => dbc.decodeSignal(trace, messageIdentity.canId, messageIdentity.isExtended, messageIdentity.sizeBytes, signalName)));
|
|
80
|
+
},
|
|
81
|
+
getMf4SignalValues(traceHandle, signalId) {
|
|
82
|
+
assertClientOpen();
|
|
83
|
+
const trace = handles.payload('trace', traceHandle);
|
|
84
|
+
return unpackSeries(withWasmErrors(() => trace.decodeMf4Signal(signalId)));
|
|
85
|
+
},
|
|
86
|
+
close() {
|
|
87
|
+
if (clientClosed)
|
|
88
|
+
return;
|
|
89
|
+
clientClosed = true;
|
|
90
|
+
let firstError;
|
|
91
|
+
for (const payload of handles.releaseAll()) {
|
|
92
|
+
try {
|
|
93
|
+
freeHandle(payload);
|
|
94
|
+
}
|
|
95
|
+
catch (error) {
|
|
96
|
+
firstError ??= error;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
if (firstError)
|
|
100
|
+
throw firstError;
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
function freeHandle(payload) {
|
|
105
|
+
if (payload)
|
|
106
|
+
withWasmErrors(() => payload.free());
|
|
107
|
+
}
|
|
108
|
+
function parseTrace(traceType, bytes) {
|
|
109
|
+
switch (traceType) {
|
|
110
|
+
case 'asc':
|
|
111
|
+
return WasmTrace.parseAsc(bytes);
|
|
112
|
+
case 'trc':
|
|
113
|
+
return WasmTrace.parseTrc(bytes);
|
|
114
|
+
case 'blf':
|
|
115
|
+
return WasmTrace.parseBlf(bytes);
|
|
116
|
+
case 'mf4':
|
|
117
|
+
return WasmTrace.parseMf4(bytes);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
function unpackSeries(packed) {
|
|
121
|
+
const count = packed.length / 2;
|
|
122
|
+
return {
|
|
123
|
+
timesMs: packed.subarray(0, count),
|
|
124
|
+
values: packed.subarray(count)
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
function withWasmErrors(operation) {
|
|
128
|
+
try {
|
|
129
|
+
return operation();
|
|
130
|
+
}
|
|
131
|
+
catch (error) {
|
|
132
|
+
throw normalizeWasmError(error);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
function normalizeWasmError(error) {
|
|
136
|
+
if (error instanceof WebAssembly.RuntimeError) {
|
|
137
|
+
return new Error(`WebAssembly execution failed: ${error.message}`);
|
|
138
|
+
}
|
|
139
|
+
if (error instanceof WebAssembly.CompileError || error instanceof WebAssembly.LinkError) {
|
|
140
|
+
return new Error(`WebAssembly failed to load: ${error.message}`);
|
|
141
|
+
}
|
|
142
|
+
return error;
|
|
143
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { DbcHandle, TraceHandle } from './types.js';
|
|
2
|
+
export type HandleKind = 'dbc' | 'trace';
|
|
3
|
+
type HandleFor = {
|
|
4
|
+
dbc: DbcHandle;
|
|
5
|
+
trace: TraceHandle;
|
|
6
|
+
};
|
|
7
|
+
/**
|
|
8
|
+
* Owns the handles one client issued. `Payloads` maps each handle kind to what the client needs to
|
|
9
|
+
* act on it: the direct client stores wasm-bindgen objects, the transport clients store wire IDs.
|
|
10
|
+
*/
|
|
11
|
+
export type HandleRegistry<Payloads extends Record<HandleKind, unknown>> = {
|
|
12
|
+
issue<K extends HandleKind>(kind: K, payload: Payloads[K]): HandleFor[K];
|
|
13
|
+
/** Payload of an owned, open handle. Throws for foreign, wrong-kind, or closed handles. */
|
|
14
|
+
payload<K extends HandleKind>(kind: K, handle: HandleFor[K]): Payloads[K];
|
|
15
|
+
/**
|
|
16
|
+
* Mark an owned handle closed and return its payload for cleanup, or null when it was already
|
|
17
|
+
* closed. Throws for foreign or wrong-kind handles.
|
|
18
|
+
*/
|
|
19
|
+
release<K extends HandleKind>(kind: K, handle: HandleFor[K]): Payloads[K] | null;
|
|
20
|
+
/** Mark every live handle closed and return their payloads in issue order. */
|
|
21
|
+
releaseAll(): Payloads[HandleKind][];
|
|
22
|
+
};
|
|
23
|
+
export declare function createHandleRegistry<Payloads extends Record<HandleKind, unknown>>(): HandleRegistry<Payloads>;
|
|
24
|
+
export {};
|
package/dist/handles.js
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Client-local handle identity, shared by the synchronous direct client and the asynchronous
|
|
3
|
+
* transport clients. @internal
|
|
4
|
+
*
|
|
5
|
+
* Each handle carries one private symbol token. The token is a primitive, so arbitrary reactive
|
|
6
|
+
* proxies return it unchanged and object spreads copy it. The mutable state stays in this
|
|
7
|
+
* client-local registry rather than on the handle, so proxies cannot duplicate or wrap it.
|
|
8
|
+
*/
|
|
9
|
+
const HandleToken = Symbol('CAN Trace Viewer handle token');
|
|
10
|
+
export function createHandleRegistry() {
|
|
11
|
+
const entries = new Map();
|
|
12
|
+
function entryFor(kind, handle) {
|
|
13
|
+
const token = handle[HandleToken];
|
|
14
|
+
const entry = token ? entries.get(token) : undefined;
|
|
15
|
+
if (!entry || entry.kind !== kind) {
|
|
16
|
+
throw new Error(`${kind} handle does not belong to this client`);
|
|
17
|
+
}
|
|
18
|
+
return entry;
|
|
19
|
+
}
|
|
20
|
+
return {
|
|
21
|
+
issue(kind, payload) {
|
|
22
|
+
const token = Symbol(`CAN Trace Viewer ${kind} handle`);
|
|
23
|
+
entries.set(token, { closed: false, kind, payload: payload });
|
|
24
|
+
// Object spread copies this enumerable symbol while proxies preserve its primitive value.
|
|
25
|
+
return { [HandleToken]: token };
|
|
26
|
+
},
|
|
27
|
+
payload(kind, handle) {
|
|
28
|
+
const entry = entryFor(kind, handle);
|
|
29
|
+
if (entry.closed)
|
|
30
|
+
throw new Error(`${kind} handle is closed`);
|
|
31
|
+
return entry.payload;
|
|
32
|
+
},
|
|
33
|
+
release(kind, handle) {
|
|
34
|
+
const entry = entryFor(kind, handle);
|
|
35
|
+
if (entry.closed)
|
|
36
|
+
return null;
|
|
37
|
+
entry.closed = true;
|
|
38
|
+
const payload = entry.payload;
|
|
39
|
+
entry.payload = null;
|
|
40
|
+
return payload;
|
|
41
|
+
},
|
|
42
|
+
releaseAll() {
|
|
43
|
+
const payloads = [];
|
|
44
|
+
for (const entry of entries.values()) {
|
|
45
|
+
if (entry.closed)
|
|
46
|
+
continue;
|
|
47
|
+
entry.closed = true;
|
|
48
|
+
payloads.push(entry.payload);
|
|
49
|
+
entry.payload = null;
|
|
50
|
+
}
|
|
51
|
+
return payloads;
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { createCanTraceClient } from "./client.js";
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { type CanTraceClient } from './rpc-client.js';
|
|
2
|
+
/** worker_threads surface used by the private Node transport and its tests. @internal */
|
|
3
|
+
export type NodeClientWorker = {
|
|
4
|
+
postMessage(message: unknown, transfer?: readonly ArrayBuffer[]): void;
|
|
5
|
+
on(event: 'message', listener: (data: unknown) => void): unknown;
|
|
6
|
+
on(event: 'messageerror', listener: (error: Error) => void): unknown;
|
|
7
|
+
on(event: 'error', listener: (error: Error) => void): unknown;
|
|
8
|
+
on(event: 'exit', listener: (code: number) => void): unknown;
|
|
9
|
+
terminate(): Promise<unknown>;
|
|
10
|
+
};
|
|
11
|
+
/** Private seam for deterministic transport tests. @internal */
|
|
12
|
+
export declare function createNodeClientForWorker(createWorker: () => NodeClientWorker): Promise<CanTraceClient>;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { isMarkedAsUntransferable } from 'node:worker_threads';
|
|
2
|
+
import { createRpcClient } from "./rpc-client.js";
|
|
3
|
+
/** Private seam for deterministic transport tests. @internal */
|
|
4
|
+
export async function createNodeClientForWorker(createWorker) {
|
|
5
|
+
return createRpcClient((handlers) => nodeTransport(createWorker(), handlers));
|
|
6
|
+
}
|
|
7
|
+
/** worker_threads EventEmitter events mapped onto the shared client core. */
|
|
8
|
+
function nodeTransport(worker, handlers) {
|
|
9
|
+
let stopping = false;
|
|
10
|
+
worker.on('message', (data) => handlers.message(data));
|
|
11
|
+
worker.on('error', (error) => {
|
|
12
|
+
handlers.fail(new Error(`worker thread crashed: ${describe(error)}`, { cause: error }));
|
|
13
|
+
});
|
|
14
|
+
worker.on('messageerror', (error) => {
|
|
15
|
+
handlers.fail(new Error(`worker thread message failed to deserialize: ${describe(error)}`));
|
|
16
|
+
});
|
|
17
|
+
worker.on('exit', (code) => {
|
|
18
|
+
// A requested terminate always ends in an exit event; only an unrequested one is a failure.
|
|
19
|
+
if (stopping)
|
|
20
|
+
return;
|
|
21
|
+
handlers.fail(new Error(`worker thread exited unexpectedly with code ${code}`));
|
|
22
|
+
});
|
|
23
|
+
return {
|
|
24
|
+
postMessage(message, transfer) {
|
|
25
|
+
if (transfer.some(isMarkedAsUntransferable)) {
|
|
26
|
+
throw new Error('openTrace cannot transfer an ArrayBuffer marked as untransferable');
|
|
27
|
+
}
|
|
28
|
+
worker.postMessage(message, transfer);
|
|
29
|
+
},
|
|
30
|
+
async terminate() {
|
|
31
|
+
stopping = true;
|
|
32
|
+
await worker.terminate();
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
function describe(error) {
|
|
37
|
+
return error instanceof Error ? error.message : String(error);
|
|
38
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { parentPort } from 'node:worker_threads';
|
|
3
|
+
// Relative imports carry `.ts` extensions throughout this package because Node loads this worker
|
|
4
|
+
// entry from source through type stripping, which resolves specifiers literally. `tsc` rewrites
|
|
5
|
+
// them to `.js` when it emits the packaged build.
|
|
6
|
+
import { createDirectClient } from "./direct.js";
|
|
7
|
+
import { startWorkerRuntime } from "./worker-runtime.js";
|
|
8
|
+
// Dedicated worker-thread entry: one per Node client, created by createCanTraceClient(). It reads
|
|
9
|
+
// the packaged WASM binary from disk, compiles it, and owns the direct client for its lifetime.
|
|
10
|
+
const port = parentPort;
|
|
11
|
+
if (!port) {
|
|
12
|
+
throw new Error('the cantraceviewer worker entry must run inside a worker thread');
|
|
13
|
+
}
|
|
14
|
+
const endpoint = {
|
|
15
|
+
postMessage(message, transfer = []) {
|
|
16
|
+
// Transfer lists are ArrayBuffers only, which worker_threads transfers like postMessage.
|
|
17
|
+
port.postMessage(message, transfer);
|
|
18
|
+
},
|
|
19
|
+
addEventListener(_type, listener) {
|
|
20
|
+
port.on('message', (data) => listener({ data }));
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
startWorkerRuntime(endpoint, async () => {
|
|
24
|
+
const bytes = await readFile(new URL('./wasm-bindgen/cantraceviewer_bg.wasm', import.meta.url));
|
|
25
|
+
// Compiling here keeps the only asynchronous step outside the synchronous direct client.
|
|
26
|
+
return createDirectClient(await WebAssembly.compile(bytes));
|
|
27
|
+
});
|
package/dist/node.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { CanTraceClient } from './rpc-client.js';
|
|
2
|
+
export type { CanTraceClient } from './rpc-client.js';
|
|
3
|
+
export type * from './types.js';
|
|
4
|
+
/**
|
|
5
|
+
* Create a client backed by one new dedicated worker thread. Same interface, lifecycle, and
|
|
6
|
+
* ordering guarantees as the browser client; the worker reads the packaged WASM binary from disk
|
|
7
|
+
* instead of fetching it.
|
|
8
|
+
*
|
|
9
|
+
* Nothing is instantiated at module import time. Import this entry only from Node; the package root
|
|
10
|
+
* stays free of Node built-ins for browser bundles.
|
|
11
|
+
*/
|
|
12
|
+
export declare function createCanTraceClient(): Promise<CanTraceClient>;
|
package/dist/node.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { Worker } from 'node:worker_threads';
|
|
2
|
+
import { createNodeClientForWorker } from "./node-transport.js";
|
|
3
|
+
/**
|
|
4
|
+
* Create a client backed by one new dedicated worker thread. Same interface, lifecycle, and
|
|
5
|
+
* ordering guarantees as the browser client; the worker reads the packaged WASM binary from disk
|
|
6
|
+
* instead of fetching it.
|
|
7
|
+
*
|
|
8
|
+
* Nothing is instantiated at module import time. Import this entry only from Node; the package root
|
|
9
|
+
* stays free of Node built-ins for browser bundles.
|
|
10
|
+
*/
|
|
11
|
+
export async function createCanTraceClient() {
|
|
12
|
+
return createNodeClientForWorker(() => new Worker(workerEntry(), { execArgv: workerExecArgv() }));
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* The packaged build ships compiled JavaScript siblings. A source checkout runs this file as
|
|
16
|
+
* TypeScript, where Node's type stripping loads the TypeScript worker entry directly.
|
|
17
|
+
*/
|
|
18
|
+
function workerEntry() {
|
|
19
|
+
const entry = import.meta.url.endsWith('.ts') ? './node-worker.ts' : './node-worker.js';
|
|
20
|
+
return new URL(entry, import.meta.url);
|
|
21
|
+
}
|
|
22
|
+
/** Flags such as `--input-type` are valid only for eval/stdin and break a file-backed Worker. */
|
|
23
|
+
function workerExecArgv() {
|
|
24
|
+
return process.execArgv.filter((argument) => argument !== '--input-type' && !argument.startsWith('--input-type='));
|
|
25
|
+
}
|