@netkitty/analysis 1.0.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.
Files changed (79) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/LICENSE +21 -0
  3. package/README.md +150 -0
  4. package/README.zh-CN.md +146 -0
  5. package/dist/index.d.ts +30 -0
  6. package/dist/index.js +27 -0
  7. package/dist/lib/streaming/Analysis.d.ts +54 -0
  8. package/dist/lib/streaming/Analysis.js +159 -0
  9. package/dist/lib/streaming/backends/BrowserFileReadBackend.d.ts +14 -0
  10. package/dist/lib/streaming/backends/BrowserFileReadBackend.js +36 -0
  11. package/dist/lib/streaming/backends/NodeFileReadBackend.d.ts +15 -0
  12. package/dist/lib/streaming/backends/NodeFileReadBackend.js +62 -0
  13. package/dist/lib/streaming/filter/FilterExpression.d.ts +35 -0
  14. package/dist/lib/streaming/filter/FilterExpression.js +166 -0
  15. package/dist/lib/streaming/indexer/ConversationKey.d.ts +18 -0
  16. package/dist/lib/streaming/indexer/ConversationKey.js +54 -0
  17. package/dist/lib/streaming/indexer/FrameIndexer.d.ts +16 -0
  18. package/dist/lib/streaming/indexer/FrameIndexer.js +58 -0
  19. package/dist/lib/streaming/indexer/PcapIndexBuilder.d.ts +20 -0
  20. package/dist/lib/streaming/indexer/PcapIndexBuilder.js +52 -0
  21. package/dist/lib/streaming/interfaces/IAnalysisReducer.d.ts +14 -0
  22. package/dist/lib/streaming/interfaces/IAnalysisReducer.js +2 -0
  23. package/dist/lib/streaming/interfaces/IIndexStore.d.ts +16 -0
  24. package/dist/lib/streaming/interfaces/IIndexStore.js +2 -0
  25. package/dist/lib/streaming/interfaces/IReadBackend.d.ts +11 -0
  26. package/dist/lib/streaming/interfaces/IReadBackend.js +2 -0
  27. package/dist/lib/streaming/interfaces/IWorkerChannel.d.ts +12 -0
  28. package/dist/lib/streaming/interfaces/IWorkerChannel.js +2 -0
  29. package/dist/lib/streaming/reducers/ConversationsReducer.d.ts +34 -0
  30. package/dist/lib/streaming/reducers/ConversationsReducer.js +66 -0
  31. package/dist/lib/streaming/reducers/EndpointsReducer.d.ts +25 -0
  32. package/dist/lib/streaming/reducers/EndpointsReducer.js +49 -0
  33. package/dist/lib/streaming/reducers/ReducerFactories.d.ts +13 -0
  34. package/dist/lib/streaming/reducers/ReducerFactories.js +42 -0
  35. package/dist/lib/streaming/reducers/TcpStreamReducer.d.ts +35 -0
  36. package/dist/lib/streaming/reducers/TcpStreamReducer.js +153 -0
  37. package/dist/lib/streaming/stores/ColumnarIndexStore.d.ts +21 -0
  38. package/dist/lib/streaming/stores/ColumnarIndexStore.js +134 -0
  39. package/dist/lib/streaming/types/AnalysisEvents.d.ts +8 -0
  40. package/dist/lib/streaming/types/AnalysisEvents.js +2 -0
  41. package/dist/lib/streaming/types/AnalysisOptions.d.ts +5 -0
  42. package/dist/lib/streaming/types/AnalysisOptions.js +2 -0
  43. package/dist/lib/streaming/types/AnalysisPhase.d.ts +2 -0
  44. package/dist/lib/streaming/types/AnalysisPhase.js +2 -0
  45. package/dist/lib/streaming/types/AnalysisSource.d.ts +2 -0
  46. package/dist/lib/streaming/types/AnalysisSource.js +2 -0
  47. package/dist/lib/streaming/types/Frame.d.ts +7 -0
  48. package/dist/lib/streaming/types/Frame.js +2 -0
  49. package/dist/lib/streaming/types/FrameIndexRecord.d.ts +14 -0
  50. package/dist/lib/streaming/types/FrameIndexRecord.js +2 -0
  51. package/dist/lib/streaming/types/FrameRow.d.ts +12 -0
  52. package/dist/lib/streaming/types/FrameRow.js +2 -0
  53. package/dist/lib/streaming/types/UpdateContext.d.ts +7 -0
  54. package/dist/lib/streaming/types/UpdateContext.js +2 -0
  55. package/dist/lib/streaming/worker/AnalysisWorkerCore.d.ts +10 -0
  56. package/dist/lib/streaming/worker/AnalysisWorkerCore.js +192 -0
  57. package/dist/lib/streaming/worker/BuiltinScans.d.ts +23 -0
  58. package/dist/lib/streaming/worker/BuiltinScans.js +121 -0
  59. package/dist/lib/streaming/worker/IWorkerEndpoint.d.ts +9 -0
  60. package/dist/lib/streaming/worker/IWorkerEndpoint.js +2 -0
  61. package/dist/lib/streaming/worker/NodeWorkerChannel.d.ts +16 -0
  62. package/dist/lib/streaming/worker/NodeWorkerChannel.js +60 -0
  63. package/dist/lib/streaming/worker/SyntheticFrame.d.ts +8 -0
  64. package/dist/lib/streaming/worker/SyntheticFrame.js +38 -0
  65. package/dist/lib/streaming/worker/WebWorkerChannel.d.ts +23 -0
  66. package/dist/lib/streaming/worker/WebWorkerChannel.js +59 -0
  67. package/dist/lib/streaming/worker/WebWorkerEndpoint.d.ts +19 -0
  68. package/dist/lib/streaming/worker/WebWorkerEndpoint.js +44 -0
  69. package/dist/lib/streaming/worker/WorkerEndpoint.d.ts +13 -0
  70. package/dist/lib/streaming/worker/WorkerEndpoint.js +45 -0
  71. package/dist/lib/streaming/worker/WorkerMessage.d.ts +24 -0
  72. package/dist/lib/streaming/worker/WorkerMessage.js +2 -0
  73. package/dist/lib/streaming/worker/analysisWorker.d.ts +1 -0
  74. package/dist/lib/streaming/worker/analysisWorker.js +11 -0
  75. package/dist/lib/streaming/worker/analysisWorkerBrowser.d.ts +1 -0
  76. package/dist/lib/streaming/worker/analysisWorkerBrowser.js +12 -0
  77. package/dist/lib/streaming/worker/spawnNodeAnalysisChannel.d.ts +7 -0
  78. package/dist/lib/streaming/worker/spawnNodeAnalysisChannel.js +18 -0
  79. package/package.json +65 -0
@@ -0,0 +1,192 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.installAnalysisHandlers = installAnalysisHandlers;
4
+ const codec_1 = require("@netkitty/codec");
5
+ const pcap_core_1 = require("@netkitty/pcap-core");
6
+ const ColumnarIndexStore_1 = require("../stores/ColumnarIndexStore");
7
+ const FrameIndexer_1 = require("../indexer/FrameIndexer");
8
+ const PcapIndexBuilder_1 = require("../indexer/PcapIndexBuilder");
9
+ const FilterExpression_1 = require("../filter/FilterExpression");
10
+ const BuiltinScans_1 = require("./BuiltinScans");
11
+ const SyntheticFrame_1 = require("./SyntheticFrame");
12
+ /**
13
+ * Environment-agnostic analysis-worker logic: owns the read backend, columnar index, indexer
14
+ * dictionaries and codec, and wires all request handlers plus watch tailing onto the given endpoint.
15
+ * `makeBackend` adapts the source (node: a path string → NodeFileReadBackend; browser: a Blob →
16
+ * BrowserFileReadBackend). node and browser worker entrypoints each call this with their endpoint and
17
+ * backend factory, so the handler logic lives in one place.
18
+ */
19
+ function installAnalysisHandlers(endpoint, makeBackend) {
20
+ const store = new ColumnarIndexStore_1.ColumnarIndexStore();
21
+ const indexer = new FrameIndexer_1.FrameIndexer(store);
22
+ const codec = new codec_1.Codec();
23
+ let backend = null;
24
+ function frameRow(record) {
25
+ return {
26
+ index: record.index,
27
+ timestamp: record.timestamp,
28
+ length: record.capturedLength,
29
+ topProtocol: indexer.protocolName(record.protocolId),
30
+ conversationKey: indexer.conversationKey(record.conversationHash),
31
+ info: ''
32
+ };
33
+ }
34
+ async function materializeFrame(index, needs) {
35
+ const record = store.get(index);
36
+ if (!record || !backend)
37
+ return null;
38
+ const bytes = await backend.read(record.fileOffset, record.capturedLength);
39
+ const layers = await codec.decode(Buffer.from(bytes));
40
+ //JSON round-trip strips the FlexibleObject proxy / closures so the layer tree is structured-cloneable.
41
+ const plain = JSON.parse(JSON.stringify(layers));
42
+ //Projection: a reducer that declares `needs` only receives those layers, cutting cross-thread bytes.
43
+ const projected = needs ? plain.filter((layer) => needs.includes(layer.id)) : plain;
44
+ return { ...frameRow(record), capturedLength: record.capturedLength, layers: projected };
45
+ }
46
+ endpoint.handle('open', async (payload) => {
47
+ const { source } = payload;
48
+ backend = makeBackend(source);
49
+ const builder = new PcapIndexBuilder_1.PcapIndexBuilder(indexer, codec);
50
+ const frameCount = await builder.build(backend, {
51
+ onProgress: (bytesRead, totalBytes) => {
52
+ endpoint.notify('progress', { frames: store.count(), bytesRead: bytesRead, totalBytes: totalBytes });
53
+ }
54
+ });
55
+ endpoint.notify('complete', { frameCount: frameCount });
56
+ return { frameCount: frameCount };
57
+ });
58
+ endpoint.handle('frameCount', () => store.count());
59
+ //Built-in stats computed inside the worker by scanning the index columns (no decode, no frame
60
+ //transfer, main thread stays free). Results equal ConversationsReducer/EndpointsReducer.
61
+ endpoint.handle('conversations', () => (0, BuiltinScans_1.computeConversations)(store, (hash) => indexer.conversationKey(hash)));
62
+ endpoint.handle('endpoints', () => (0, BuiltinScans_1.computeEndpoints)(store, (hash) => indexer.conversationKey(hash)));
63
+ //Display filter with a column pre-filter: decide each frame from the index columns (conversation
64
+ //key + top protocol) first, and only re-decode the frames a predicate can't settle from columns
65
+ //(direction-sensitive fields, deeper protocols). Common filters (ip.addr / tcp.port / tcp|udp|arp)
66
+ //are answered entirely from the index — no decode.
67
+ endpoint.handle('filter', async (payload) => {
68
+ const { displayFilter } = payload;
69
+ const expression = (0, FilterExpression_1.parseFilter)(displayFilter);
70
+ const matches = [];
71
+ if (!backend)
72
+ return matches;
73
+ const first = store.firstIndex();
74
+ const end = first + store.count();
75
+ for (let index = first; index < end; index++) {
76
+ const record = store.get(index);
77
+ if (!record)
78
+ continue;
79
+ const conversationKey = indexer.conversationKey(record.conversationHash);
80
+ const topProtocol = indexer.protocolName(record.protocolId);
81
+ const decided = (0, FilterExpression_1.matchesIndexed)(expression, conversationKey, topProtocol, record.directionForward);
82
+ if (decided === true) {
83
+ matches.push(index);
84
+ continue;
85
+ }
86
+ if (decided === false)
87
+ continue;
88
+ const bytes = await backend.read(record.fileOffset, record.capturedLength);
89
+ const layers = await codec.decode(Buffer.from(bytes));
90
+ if ((0, FilterExpression_1.matchesFilter)(layers, expression))
91
+ matches.push(index);
92
+ }
93
+ return matches;
94
+ });
95
+ endpoint.handle('getFrames', (payload) => {
96
+ const { from, to } = payload;
97
+ return store.range(from, to).map(frameRow);
98
+ });
99
+ endpoint.handle('getFrame', async (payload) => {
100
+ const { index } = payload;
101
+ return materializeFrame(index);
102
+ });
103
+ //Batch of materialized frames (with layers, optionally projected to `needs`), for reducer replay.
104
+ //v1 re-decodes each frame; a v2 optimization runs built-in reducers inside the worker over columns.
105
+ endpoint.handle('getFrameBatch', async (payload) => {
106
+ const { from, to, needs, indexOnly } = payload;
107
+ const out = [];
108
+ for (let index = from; index < to; index++) {
109
+ if (indexOnly) {
110
+ //indexOnly reducer: synthesize the five-tuple frame from index columns, skip decode.
111
+ const record = store.get(index);
112
+ if (!record)
113
+ continue;
114
+ const key = indexer.conversationKey(record.conversationHash);
115
+ const layers = key !== null ? (0, SyntheticFrame_1.synthesizeLayers)(key, record.directionForward) : [];
116
+ out.push({ ...frameRow(record), capturedLength: record.capturedLength, layers: layers });
117
+ }
118
+ else {
119
+ const frame = await materializeFrame(index, needs);
120
+ if (frame !== null)
121
+ out.push(frame);
122
+ }
123
+ }
124
+ return out;
125
+ });
126
+ //--- watch (tail) ----------------------------------------------------------------------------
127
+ //Incremental tail: one long-lived parser, pread newly-appended bytes at a tracked position, index
128
+ //each new frame and push it (with layers) as a 'frame' notify for live reducer feeding. maxFrames
129
+ //caps the index via FIFO eviction so watch memory stays bounded.
130
+ let watchParser = null;
131
+ let watchPosition = 0;
132
+ let watchLastData = Buffer.alloc(0);
133
+ let watchPending = [];
134
+ let watchMaxFrames = Number.POSITIVE_INFINITY;
135
+ let pumping = false;
136
+ let pumpAgain = false;
137
+ let watchTimer = null;
138
+ async function drainWatch() {
139
+ for (const frame of watchPending) {
140
+ const layers = await codec.decode(frame.data);
141
+ const timestamp = frame.info.seconds + frame.info.microseconds / 1_000_000;
142
+ const index = indexer.add(layers, frame.info.packetOffset, frame.info.packetLength, frame.info.packetLength, timestamp);
143
+ const record = store.get(index);
144
+ if (record) {
145
+ endpoint.notify('frame', { ...frameRow(record), capturedLength: record.capturedLength, layers: JSON.parse(JSON.stringify(layers)) });
146
+ }
147
+ if (store.count() > watchMaxFrames)
148
+ store.evictOldest(store.count() - watchMaxFrames);
149
+ }
150
+ watchPending = [];
151
+ }
152
+ async function pumpWatch() {
153
+ if (pumping) {
154
+ pumpAgain = true;
155
+ return;
156
+ }
157
+ pumping = true;
158
+ do {
159
+ pumpAgain = false;
160
+ if (!backend || !watchParser)
161
+ break;
162
+ const size = await backend.size();
163
+ while (watchPosition < size) {
164
+ const length = Math.min(65536, size - watchPosition);
165
+ const bytes = await backend.read(watchPosition, length);
166
+ if (bytes.length === 0)
167
+ break;
168
+ watchPosition += bytes.length;
169
+ watchParser.write(Buffer.from(bytes));
170
+ await drainWatch();
171
+ }
172
+ } while (pumpAgain);
173
+ pumping = false;
174
+ }
175
+ endpoint.handle('watch', async (payload) => {
176
+ const { source, maxFrames } = payload;
177
+ backend = makeBackend(source);
178
+ watchMaxFrames = maxFrames !== undefined ? maxFrames : Number.POSITIVE_INFINITY;
179
+ watchPosition = 0;
180
+ watchPending = [];
181
+ watchParser = new pcap_core_1.PcapParserCore({
182
+ onPacketData: (data) => { watchLastData = data; },
183
+ onPacket: (info) => { watchPending.push({ info: info, data: watchLastData }); }
184
+ });
185
+ await pumpWatch();
186
+ //Active polling rather than fs.watch events: a self-driven timer tails reliably even under load.
187
+ if (watchTimer !== null)
188
+ clearInterval(watchTimer);
189
+ watchTimer = setInterval(() => { void pumpWatch(); }, 100);
190
+ return { frameCount: store.count() };
191
+ });
192
+ }
@@ -0,0 +1,23 @@
1
+ import { IIndexStore } from '../interfaces/IIndexStore';
2
+ import { ConversationSummary } from '../reducers/ConversationsReducer';
3
+ import { EndpointSummary } from '../reducers/EndpointsReducer';
4
+ /**
5
+ * Built-in Conversations/Endpoints computed by scanning the index columns directly — no re-decode, no
6
+ * cross-thread frame transfer. Everything these reducers need (the five-tuple via keyOf, captured
7
+ * length, timestamp, and the direction bit) is already in the index. Runs inside the worker; only the
8
+ * small result table crosses back to the main thread, so the main thread stays free. Results are
9
+ * identical to ConversationsReducer/EndpointsReducer, just derived from columns instead of layers.
10
+ *
11
+ * Hot loops key their caches on the numeric conversationHash (fast Map) and resolve/parse the string
12
+ * key only once per conversation, so cost is ~one Map lookup + a few field bumps per frame.
13
+ */
14
+ export declare function computeConversations(store: IIndexStore, keyOf: (conversationHash: number) => string | null): ConversationSummary[];
15
+ /**
16
+ * Built-in Endpoints table, computed by scanning the index columns directly (no re-decode). Groups
17
+ * every frame's two endpoints, accumulating per-address total/tx/rx packet and byte counts from the
18
+ * direction bit. Result matches EndpointsReducer.
19
+ * @param store index columns to scan.
20
+ * @param keyOf resolves a conversation hash to its `protocol|endpointA|endpointB` key, or null when unknown.
21
+ * @returns one summary per distinct endpoint address seen.
22
+ */
23
+ export declare function computeEndpoints(store: IIndexStore, keyOf: (conversationHash: number) => string | null): EndpointSummary[];
@@ -0,0 +1,121 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.computeConversations = computeConversations;
4
+ exports.computeEndpoints = computeEndpoints;
5
+ /**
6
+ * Built-in Conversations/Endpoints computed by scanning the index columns directly — no re-decode, no
7
+ * cross-thread frame transfer. Everything these reducers need (the five-tuple via keyOf, captured
8
+ * length, timestamp, and the direction bit) is already in the index. Runs inside the worker; only the
9
+ * small result table crosses back to the main thread, so the main thread stays free. Results are
10
+ * identical to ConversationsReducer/EndpointsReducer, just derived from columns instead of layers.
11
+ *
12
+ * Hot loops key their caches on the numeric conversationHash (fast Map) and resolve/parse the string
13
+ * key only once per conversation, so cost is ~one Map lookup + a few field bumps per frame.
14
+ */
15
+ function computeConversations(store, keyOf) {
16
+ const byHash = new Map();
17
+ const first = store.firstIndex();
18
+ const end = first + store.count();
19
+ for (let i = first; i < end; i++) {
20
+ const record = store.get(i);
21
+ if (!record)
22
+ continue;
23
+ let conversation = byHash.get(record.conversationHash);
24
+ if (conversation === undefined) {
25
+ const key = keyOf(record.conversationHash);
26
+ if (key === null) {
27
+ byHash.set(record.conversationHash, null);
28
+ continue;
29
+ }
30
+ const bar1 = key.indexOf('|');
31
+ const bar2 = key.indexOf('|', bar1 + 1);
32
+ conversation = {
33
+ protocol: key.slice(0, bar1),
34
+ endpointA: key.slice(bar1 + 1, bar2),
35
+ endpointB: key.slice(bar2 + 1),
36
+ packets: 0,
37
+ bytes: 0,
38
+ packetsAToB: 0,
39
+ packetsBToA: 0,
40
+ firstTimestamp: record.timestamp,
41
+ lastTimestamp: record.timestamp,
42
+ firstIndex: record.index,
43
+ lastIndex: record.index
44
+ };
45
+ byHash.set(record.conversationHash, conversation);
46
+ }
47
+ if (conversation === null)
48
+ continue;
49
+ conversation.packets++;
50
+ conversation.bytes += record.capturedLength;
51
+ if (record.directionForward)
52
+ conversation.packetsAToB++;
53
+ else
54
+ conversation.packetsBToA++;
55
+ if (record.timestamp < conversation.firstTimestamp)
56
+ conversation.firstTimestamp = record.timestamp;
57
+ if (record.timestamp > conversation.lastTimestamp)
58
+ conversation.lastTimestamp = record.timestamp;
59
+ if (record.index < conversation.firstIndex)
60
+ conversation.firstIndex = record.index;
61
+ if (record.index > conversation.lastIndex)
62
+ conversation.lastIndex = record.index;
63
+ }
64
+ const out = [];
65
+ for (const conversation of byHash.values())
66
+ if (conversation !== null)
67
+ out.push(conversation);
68
+ return out;
69
+ }
70
+ /**
71
+ * Built-in Endpoints table, computed by scanning the index columns directly (no re-decode). Groups
72
+ * every frame's two endpoints, accumulating per-address total/tx/rx packet and byte counts from the
73
+ * direction bit. Result matches EndpointsReducer.
74
+ * @param store index columns to scan.
75
+ * @param keyOf resolves a conversation hash to its `protocol|endpointA|endpointB` key, or null when unknown.
76
+ * @returns one summary per distinct endpoint address seen.
77
+ */
78
+ function computeEndpoints(store, keyOf) {
79
+ const endpoints = new Map();
80
+ const byHash = new Map();
81
+ const first = store.firstIndex();
82
+ const end = first + store.count();
83
+ for (let i = first; i < end; i++) {
84
+ const record = store.get(i);
85
+ if (!record)
86
+ continue;
87
+ let pair = byHash.get(record.conversationHash);
88
+ if (pair === undefined) {
89
+ const key = keyOf(record.conversationHash);
90
+ if (key === null) {
91
+ byHash.set(record.conversationHash, null);
92
+ continue;
93
+ }
94
+ const bar1 = key.indexOf('|');
95
+ const bar2 = key.indexOf('|', bar1 + 1);
96
+ pair = { a: getOrCreate(endpoints, key.slice(bar1 + 1, bar2)), b: getOrCreate(endpoints, key.slice(bar2 + 1)) };
97
+ byHash.set(record.conversationHash, pair);
98
+ }
99
+ if (pair === null)
100
+ continue;
101
+ const source = record.directionForward ? pair.a : pair.b;
102
+ const destination = record.directionForward ? pair.b : pair.a;
103
+ source.packets++;
104
+ source.bytes += record.capturedLength;
105
+ source.txPackets++;
106
+ source.txBytes += record.capturedLength;
107
+ destination.packets++;
108
+ destination.bytes += record.capturedLength;
109
+ destination.rxPackets++;
110
+ destination.rxBytes += record.capturedLength;
111
+ }
112
+ return [...endpoints.values()];
113
+ }
114
+ function getOrCreate(endpoints, address) {
115
+ let endpoint = endpoints.get(address);
116
+ if (!endpoint) {
117
+ endpoint = { address: address, packets: 0, bytes: 0, txPackets: 0, txBytes: 0, rxPackets: 0, rxBytes: 0 };
118
+ endpoints.set(address, endpoint);
119
+ }
120
+ return endpoint;
121
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * The worker-side transport an analysis worker needs: register request handlers and push notifies.
3
+ * Implemented by WorkerEndpoint (node worker_threads) and WebWorkerEndpoint (browser Web Worker), so
4
+ * the worker handler logic (AnalysisWorkerCore) is written once against this interface.
5
+ */
6
+ export interface IWorkerEndpoint {
7
+ handle(method: string, handler: (payload: unknown) => unknown | Promise<unknown>): void;
8
+ notify(method: string, payload?: unknown, transfer?: ArrayBuffer[]): void;
9
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,16 @@
1
+ import { Worker } from 'node:worker_threads';
2
+ import { IWorkerChannel } from '../interfaces/IWorkerChannel';
3
+ /**
4
+ * Main-thread side of the worker channel over a worker_threads Worker: correlated request/response
5
+ * (a monotonic id maps each reply to its promise), fire-and-forget notify, and notify handlers for
6
+ * worker→main pushes. terminate() kills the worker and rejects anything still in flight — this is how
7
+ * Analysis.close() releases the worker's whole heap in one shot.
8
+ */
9
+ export declare class NodeWorkerChannel implements IWorkerChannel {
10
+ #private;
11
+ constructor(worker: Worker);
12
+ request<T>(method: string, payload?: unknown, transfer?: ArrayBuffer[]): Promise<T>;
13
+ notify(method: string, payload?: unknown, transfer?: ArrayBuffer[]): void;
14
+ on(method: string, handler: (payload: unknown) => void): void;
15
+ terminate(): void;
16
+ }
@@ -0,0 +1,60 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.NodeWorkerChannel = void 0;
4
+ /**
5
+ * Main-thread side of the worker channel over a worker_threads Worker: correlated request/response
6
+ * (a monotonic id maps each reply to its promise), fire-and-forget notify, and notify handlers for
7
+ * worker→main pushes. terminate() kills the worker and rejects anything still in flight — this is how
8
+ * Analysis.close() releases the worker's whole heap in one shot.
9
+ */
10
+ class NodeWorkerChannel {
11
+ #worker;
12
+ #nextId = 1;
13
+ #pending = new Map();
14
+ #handlers = new Map();
15
+ constructor(worker) {
16
+ this.#worker = worker;
17
+ this.#worker.on('message', (message) => this.#onMessage(message));
18
+ this.#worker.on('error', (error) => this.#failAll(error));
19
+ }
20
+ request(method, payload, transfer) {
21
+ const id = this.#nextId++;
22
+ return new Promise((resolve, reject) => {
23
+ this.#pending.set(id, { resolve: resolve, reject: reject });
24
+ this.#worker.postMessage({ kind: 'request', id: id, method: method, payload: payload }, transfer ? transfer : []);
25
+ });
26
+ }
27
+ notify(method, payload, transfer) {
28
+ this.#worker.postMessage({ kind: 'notify', method: method, payload: payload }, transfer ? transfer : []);
29
+ }
30
+ on(method, handler) {
31
+ this.#handlers.set(method, handler);
32
+ }
33
+ terminate() {
34
+ void this.#worker.terminate();
35
+ this.#failAll(new Error('worker channel terminated'));
36
+ }
37
+ #onMessage(message) {
38
+ if (message.kind === 'response') {
39
+ const pending = this.#pending.get(message.id);
40
+ if (!pending)
41
+ return;
42
+ this.#pending.delete(message.id);
43
+ if (message.ok)
44
+ pending.resolve(message.payload);
45
+ else
46
+ pending.reject(new Error(message.error !== undefined ? message.error : 'worker request failed'));
47
+ }
48
+ else if (message.kind === 'notify') {
49
+ const handler = this.#handlers.get(message.method);
50
+ if (handler)
51
+ handler(message.payload);
52
+ }
53
+ }
54
+ #failAll(error) {
55
+ for (const pending of this.#pending.values())
56
+ pending.reject(error);
57
+ this.#pending.clear();
58
+ }
59
+ }
60
+ exports.NodeWorkerChannel = NodeWorkerChannel;
@@ -0,0 +1,8 @@
1
+ import { CodecDecodeResult } from '@netkitty/codec';
2
+ /**
3
+ * Rebuild the five-tuple layers of a frame from its index columns (conversation key + direction bit),
4
+ * without decoding the packet. Feeds indexOnly reducers on replay so they skip re-decode entirely.
5
+ * Only the fields flowOf reads are reconstructed (sip/dip/srcport/dstport or smac/dmac); deep fields
6
+ * are absent by design — that is why only reducers that declare indexOnly may be fed these frames.
7
+ */
8
+ export declare function synthesizeLayers(conversationKey: string, directionForward: number): CodecDecodeResult[];
@@ -0,0 +1,38 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.synthesizeLayers = synthesizeLayers;
4
+ /**
5
+ * Rebuild the five-tuple layers of a frame from its index columns (conversation key + direction bit),
6
+ * without decoding the packet. Feeds indexOnly reducers on replay so they skip re-decode entirely.
7
+ * Only the fields flowOf reads are reconstructed (sip/dip/srcport/dstport or smac/dmac); deep fields
8
+ * are absent by design — that is why only reducers that declare indexOnly may be fed these frames.
9
+ */
10
+ function synthesizeLayers(conversationKey, directionForward) {
11
+ const bar1 = conversationKey.indexOf('|');
12
+ const bar2 = conversationKey.indexOf('|', bar1 + 1);
13
+ const proto = conversationKey.slice(0, bar1);
14
+ const endpointA = conversationKey.slice(bar1 + 1, bar2);
15
+ const endpointB = conversationKey.slice(bar2 + 1);
16
+ const source = directionForward ? endpointA : endpointB;
17
+ const destination = directionForward ? endpointB : endpointA;
18
+ if (proto === 'tcp' || proto === 'udp') {
19
+ const s = splitEndpoint(source);
20
+ const d = splitEndpoint(destination);
21
+ const ipId = s.ip.includes(':') ? 'ipv6' : 'ipv4';
22
+ return [layer('eth', {}), layer(ipId, { sip: s.ip, dip: d.ip }), layer(proto, { srcport: s.port, dstport: d.port })];
23
+ }
24
+ if (proto === 'ip') {
25
+ const ipId = source.includes(':') ? 'ipv6' : 'ipv4';
26
+ return [layer('eth', {}), layer(ipId, { sip: source, dip: destination })];
27
+ }
28
+ if (proto === 'eth')
29
+ return [layer('eth', { smac: source, dmac: destination })];
30
+ return [layer('eth', {})];
31
+ }
32
+ function layer(id, data) {
33
+ return { id: id, name: id, nickname: id, protocol: true, errors: [], data: data };
34
+ }
35
+ function splitEndpoint(endpoint) {
36
+ const colon = endpoint.lastIndexOf(':');
37
+ return colon < 0 ? { ip: endpoint, port: '' } : { ip: endpoint.slice(0, colon), port: endpoint.slice(colon + 1) };
38
+ }
@@ -0,0 +1,23 @@
1
+ import { IWorkerChannel } from '../interfaces/IWorkerChannel';
2
+ /** Structural minimum of a DOM Worker — avoids pulling the dom lib (which clashes with node types). */
3
+ export interface WebWorkerLike {
4
+ postMessage(message: unknown, transfer?: unknown[]): void;
5
+ terminate(): void;
6
+ onmessage: ((event: {
7
+ data: unknown;
8
+ }) => void) | null;
9
+ onerror: ((event: unknown) => void) | null;
10
+ }
11
+ /**
12
+ * Browser side of the worker channel over a Web Worker: correlated request/response, fire-and-forget
13
+ * notify, notify handlers for worker→main pushes, terminate rejecting anything in flight. Mirrors
14
+ * NodeWorkerChannel exactly; only the transport (postMessage/onmessage vs worker_threads) differs.
15
+ */
16
+ export declare class WebWorkerChannel implements IWorkerChannel {
17
+ #private;
18
+ constructor(worker: WebWorkerLike);
19
+ request<T>(method: string, payload?: unknown, transfer?: ArrayBuffer[]): Promise<T>;
20
+ notify(method: string, payload?: unknown, transfer?: ArrayBuffer[]): void;
21
+ on(method: string, handler: (payload: unknown) => void): void;
22
+ terminate(): void;
23
+ }
@@ -0,0 +1,59 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.WebWorkerChannel = void 0;
4
+ /**
5
+ * Browser side of the worker channel over a Web Worker: correlated request/response, fire-and-forget
6
+ * notify, notify handlers for worker→main pushes, terminate rejecting anything in flight. Mirrors
7
+ * NodeWorkerChannel exactly; only the transport (postMessage/onmessage vs worker_threads) differs.
8
+ */
9
+ class WebWorkerChannel {
10
+ #worker;
11
+ #nextId = 1;
12
+ #pending = new Map();
13
+ #handlers = new Map();
14
+ constructor(worker) {
15
+ this.#worker = worker;
16
+ this.#worker.onmessage = (event) => this.#onMessage(event.data);
17
+ this.#worker.onerror = () => this.#failAll(new Error('web worker error'));
18
+ }
19
+ request(method, payload, transfer) {
20
+ const id = this.#nextId++;
21
+ return new Promise((resolve, reject) => {
22
+ this.#pending.set(id, { resolve: resolve, reject: reject });
23
+ this.#worker.postMessage({ kind: 'request', id: id, method: method, payload: payload }, transfer ? transfer : []);
24
+ });
25
+ }
26
+ notify(method, payload, transfer) {
27
+ this.#worker.postMessage({ kind: 'notify', method: method, payload: payload }, transfer ? transfer : []);
28
+ }
29
+ on(method, handler) {
30
+ this.#handlers.set(method, handler);
31
+ }
32
+ terminate() {
33
+ this.#worker.terminate();
34
+ this.#failAll(new Error('worker channel terminated'));
35
+ }
36
+ #onMessage(message) {
37
+ if (message.kind === 'response') {
38
+ const pending = this.#pending.get(message.id);
39
+ if (!pending)
40
+ return;
41
+ this.#pending.delete(message.id);
42
+ if (message.ok)
43
+ pending.resolve(message.payload);
44
+ else
45
+ pending.reject(new Error(message.error !== undefined ? message.error : 'worker request failed'));
46
+ }
47
+ else if (message.kind === 'notify') {
48
+ const handler = this.#handlers.get(message.method);
49
+ if (handler)
50
+ handler(message.payload);
51
+ }
52
+ }
53
+ #failAll(error) {
54
+ for (const pending of this.#pending.values())
55
+ pending.reject(error);
56
+ this.#pending.clear();
57
+ }
58
+ }
59
+ exports.WebWorkerChannel = WebWorkerChannel;
@@ -0,0 +1,19 @@
1
+ import { IWorkerEndpoint } from './IWorkerEndpoint';
2
+ /** Structural minimum of a Web Worker global scope (self) — avoids pulling the webworker/dom lib. */
3
+ export interface WorkerScopeLike {
4
+ onmessage: ((event: {
5
+ data: unknown;
6
+ }) => void) | null;
7
+ postMessage(message: unknown, transfer?: unknown[]): void;
8
+ }
9
+ /**
10
+ * Browser worker side of the channel: registers request handlers (their awaited return becomes the
11
+ * response) and pushes notify messages back to the main thread over the worker global scope. Same
12
+ * request/response wiring as WorkerEndpoint (node), only the transport differs.
13
+ */
14
+ export declare class WebWorkerEndpoint implements IWorkerEndpoint {
15
+ #private;
16
+ constructor(scope: WorkerScopeLike);
17
+ handle(method: string, handler: (payload: unknown) => unknown | Promise<unknown>): void;
18
+ notify(method: string, payload?: unknown, transfer?: ArrayBuffer[]): void;
19
+ }
@@ -0,0 +1,44 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.WebWorkerEndpoint = void 0;
4
+ /**
5
+ * Browser worker side of the channel: registers request handlers (their awaited return becomes the
6
+ * response) and pushes notify messages back to the main thread over the worker global scope. Same
7
+ * request/response wiring as WorkerEndpoint (node), only the transport differs.
8
+ */
9
+ class WebWorkerEndpoint {
10
+ #scope;
11
+ #handlers = new Map();
12
+ constructor(scope) {
13
+ this.#scope = scope;
14
+ this.#scope.onmessage = (event) => { void this.#onMessage(event.data); };
15
+ }
16
+ handle(method, handler) {
17
+ this.#handlers.set(method, handler);
18
+ }
19
+ notify(method, payload, transfer) {
20
+ this.#scope.postMessage({ kind: 'notify', method: method, payload: payload }, transfer ? transfer : []);
21
+ }
22
+ async #onMessage(message) {
23
+ if (message.kind === 'request') {
24
+ const handler = this.#handlers.get(message.method);
25
+ if (!handler) {
26
+ this.#scope.postMessage({ kind: 'response', id: message.id, ok: false, error: `no handler for method: ${message.method}` });
27
+ return;
28
+ }
29
+ try {
30
+ const result = await handler(message.payload);
31
+ this.#scope.postMessage({ kind: 'response', id: message.id, ok: true, payload: result });
32
+ }
33
+ catch (error) {
34
+ this.#scope.postMessage({ kind: 'response', id: message.id, ok: false, error: error instanceof Error ? error.message : String(error) });
35
+ }
36
+ }
37
+ else if (message.kind === 'notify') {
38
+ const handler = this.#handlers.get(message.method);
39
+ if (handler)
40
+ void handler(message.payload);
41
+ }
42
+ }
43
+ }
44
+ exports.WebWorkerEndpoint = WebWorkerEndpoint;
@@ -0,0 +1,13 @@
1
+ import { MessagePort } from 'node:worker_threads';
2
+ /**
3
+ * Worker side of the channel: registers request handlers (their return value, awaited, becomes the
4
+ * response) and can push notify messages back to the main thread. Shared by the real analysis worker
5
+ * and test workers, so the request/response wiring lives in one place. A thrown/rejected handler is
6
+ * reported as an error response rather than crashing the worker.
7
+ */
8
+ export declare class WorkerEndpoint {
9
+ #private;
10
+ constructor(port: MessagePort);
11
+ handle(method: string, handler: (payload: unknown) => unknown | Promise<unknown>): void;
12
+ notify(method: string, payload?: unknown, transfer?: ArrayBuffer[]): void;
13
+ }