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.
@@ -0,0 +1,152 @@
1
+ /**
2
+ * Drive one synchronous DirectClient behind a message endpoint. Shared by the browser Worker entry
3
+ * and the Node worker-thread entry. @internal
4
+ *
5
+ * The worker owns every direct handle because wasm-bindgen objects carry hidden state that cannot
6
+ * cross threads; only plain data crosses the wire. Each request runs to completion before the next
7
+ * one starts, in post order, so a later close never interrupts an active decode. A per-request
8
+ * error is replied as an error envelope and never poisons the queue. Boot failure is reported once
9
+ * and never retried here; the owning client treats it as fatal and terminates this worker.
10
+ */
11
+ export function startWorkerRuntime(endpoint, loadClient) {
12
+ const dbcs = new Map();
13
+ const traces = new Map();
14
+ let direct = null;
15
+ // Wire IDs are never recycled.
16
+ let nextWireId = 1;
17
+ // Loading WASM is the only asynchronous step: bytes arrive over fetch or from disk, and may be
18
+ // compiled, before the synchronous direct client exists.
19
+ const boot = loadClient()
20
+ .then((loaded) => {
21
+ direct = loaded;
22
+ endpoint.postMessage({ type: 'ready' });
23
+ }, (error) => {
24
+ endpoint.postMessage({ type: 'boot-error', error: toWireError(error) });
25
+ })
26
+ .catch(() => undefined);
27
+ // Requests posted before boot finishes wait behind it; afterwards each one is handled
28
+ // synchronously in the order the endpoint delivered it.
29
+ let queue = boot;
30
+ endpoint.addEventListener('message', (event) => {
31
+ const request = event.data;
32
+ queue = queue.then(() => handle(request)).catch(() => undefined);
33
+ });
34
+ function handle(request) {
35
+ let executed;
36
+ try {
37
+ if (!direct)
38
+ throw new Error('worker WASM initialization failed');
39
+ executed = execute(direct, request);
40
+ }
41
+ catch (error) {
42
+ endpoint.postMessage({ type: 'error', id: request.id, error: toWireError(error) });
43
+ return;
44
+ }
45
+ try {
46
+ endpoint.postMessage({ type: 'ok', id: request.id, result: executed.result }, executed.transfer ?? []);
47
+ }
48
+ catch (error) {
49
+ // Response shaping failed after the operation succeeded: release what it created.
50
+ try {
51
+ executed.undo?.();
52
+ }
53
+ catch {
54
+ // Cleanup failure must not replace the reported response error.
55
+ }
56
+ endpoint.postMessage({ type: 'error', id: request.id, error: toWireError(error) });
57
+ }
58
+ }
59
+ function execute(client, request) {
60
+ switch (request.op) {
61
+ case 'openDbc': {
62
+ const { handle, catalog } = client.openDbc(request.text);
63
+ const dbcId = nextWireId++;
64
+ dbcs.set(dbcId, handle);
65
+ const result = { dbcId, catalog };
66
+ return {
67
+ result,
68
+ undo: () => {
69
+ dbcs.delete(dbcId);
70
+ client.closeDbc(handle);
71
+ }
72
+ };
73
+ }
74
+ case 'closeDbc': {
75
+ const handle = dbcs.get(request.dbcId);
76
+ if (handle) {
77
+ dbcs.delete(request.dbcId);
78
+ client.closeDbc(handle);
79
+ }
80
+ return { result: null };
81
+ }
82
+ case 'openTrace': {
83
+ const opened = client.openTrace(request.traceType, new Uint8Array(request.buffer));
84
+ const traceId = nextWireId++;
85
+ traces.set(traceId, opened.handle);
86
+ const result = {
87
+ traceId,
88
+ metadata: opened.metadata,
89
+ hasRawFrames: opened.hasRawFrames,
90
+ mf4Catalog: opened.mf4Catalog,
91
+ embeddedDbcs: opened.embeddedDbcs,
92
+ warnings: opened.warnings
93
+ };
94
+ return {
95
+ result,
96
+ undo: () => {
97
+ traces.delete(traceId);
98
+ client.closeTrace(opened.handle);
99
+ }
100
+ };
101
+ }
102
+ case 'closeTrace': {
103
+ const handle = traces.get(request.traceId);
104
+ if (handle) {
105
+ traces.delete(request.traceId);
106
+ client.closeTrace(handle);
107
+ }
108
+ return { result: null };
109
+ }
110
+ case 'getSignalValues':
111
+ return packSeries(client.getSignalValues(requireHandle(dbcs, request.dbcId, 'dbc'), requireHandle(traces, request.traceId, 'trace'), request.messageIdentity, request.signalName));
112
+ case 'getMf4SignalValues':
113
+ return packSeries(client.getMf4SignalValues(requireHandle(traces, request.traceId, 'trace'), request.signalId));
114
+ case 'closeClient': {
115
+ dbcs.clear();
116
+ traces.clear();
117
+ client.close();
118
+ return { result: null };
119
+ }
120
+ }
121
+ }
122
+ }
123
+ function requireHandle(handles, id, kind) {
124
+ const handle = handles.get(id);
125
+ if (!handle)
126
+ throw new Error(`unknown ${kind} id ${id}`);
127
+ return handle;
128
+ }
129
+ function packSeries(series) {
130
+ const buffer = series.timesMs.buffer;
131
+ // Regression guard: direct decode output must be both views over one exactly-sized plain
132
+ // ArrayBuffer, not into WebAssembly.Memory (whose buffer is far larger and non-transferable).
133
+ if (!(buffer instanceof ArrayBuffer) ||
134
+ series.values.buffer !== buffer ||
135
+ buffer.byteLength !== series.timesMs.byteLength + series.values.byteLength) {
136
+ throw new Error('decoded series is not backed by one transferable ArrayBuffer');
137
+ }
138
+ const result = {
139
+ buffer,
140
+ timesByteOffset: series.timesMs.byteOffset,
141
+ timesLength: series.timesMs.length,
142
+ valuesByteOffset: series.values.byteOffset,
143
+ valuesLength: series.values.length
144
+ };
145
+ // Transfer the unique buffer once; the client reconstructs both views over it.
146
+ return { result, transfer: [buffer] };
147
+ }
148
+ function toWireError(error) {
149
+ if (error instanceof Error)
150
+ return { name: error.name, message: error.message };
151
+ return { name: 'Error', message: String(error) };
152
+ }
@@ -0,0 +1 @@
1
+ export {};
package/dist/worker.js ADDED
@@ -0,0 +1,11 @@
1
+ import { createDirectClient } from "./direct.js";
2
+ import { startWorkerRuntime } from "./worker-runtime.js";
3
+ // Dedicated module Worker entry: one per client, created lazily by createCanTraceClient(). It
4
+ // loads the package-local generated WASM binary and owns the direct client for its lifetime.
5
+ startWorkerRuntime(self, async () => {
6
+ const response = await fetch(new URL('./wasm-bindgen/cantraceviewer_bg.wasm', import.meta.url));
7
+ if (!response.ok) {
8
+ throw new Error(`failed to fetch WASM module: HTTP ${response.status}`);
9
+ }
10
+ return createDirectClient(await WebAssembly.compile(await response.arrayBuffer()));
11
+ });
package/package.json ADDED
@@ -0,0 +1,58 @@
1
+ {
2
+ "name": "cantraceviewer",
3
+ "version": "0.1.0-rc.0",
4
+ "description": "Parse CAN traces and DBC files, then decode signal series in browsers or Node.js.",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "author": "Tom Ford",
8
+ "homepage": "https://cantraceviewer.com",
9
+ "bugs": "https://github.com/tomrford/cantraceviewer/issues",
10
+ "repository": {
11
+ "type": "git",
12
+ "url": "git+https://github.com/tomrford/cantraceviewer.git",
13
+ "directory": "packages/core"
14
+ },
15
+ "keywords": [
16
+ "CAN",
17
+ "DBC",
18
+ "ASC",
19
+ "TRC",
20
+ "BLF",
21
+ "MF4",
22
+ "WebAssembly"
23
+ ],
24
+ "engines": {
25
+ "node": ">=22.12"
26
+ },
27
+ "exports": {
28
+ ".": {
29
+ "types": "./dist/index.d.ts",
30
+ "import": "./dist/index.js"
31
+ },
32
+ "./direct": {
33
+ "types": "./dist/direct.d.ts",
34
+ "import": "./dist/direct.js"
35
+ },
36
+ "./node": {
37
+ "types": "./dist/node.d.ts",
38
+ "import": "./dist/node.js"
39
+ }
40
+ },
41
+ "files": [
42
+ "dist",
43
+ "LICENSE.md",
44
+ "README.md"
45
+ ],
46
+ "publishConfig": {
47
+ "access": "public",
48
+ "provenance": true
49
+ },
50
+ "devDependencies": {
51
+ "typescript": "6.0.3"
52
+ },
53
+ "scripts": {
54
+ "build": "node ./scripts/build.mjs",
55
+ "pack:artifact": "node ./scripts/pack.mjs",
56
+ "pack:check": "pnpm pack --dry-run"
57
+ }
58
+ }