libpetri 0.4.1 → 0.5.1

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.
@@ -9,1021 +9,1075 @@ function eventFilterAll() {
9
9
  return { eventTypes: null, transitionNames: null, placeNames: null };
10
10
  }
11
11
 
12
- // src/debug/marking-cache.ts
13
- var SNAPSHOT_INTERVAL = 256;
14
- var MarkingCache = class {
15
- _snapshots = [];
16
- /**
17
- * Computes the state at the given event index, using cached snapshots
18
- * to minimize the number of events that need to be replayed.
19
- *
20
- * @param events the full event list for the session
21
- * @param targetIndex event index to compute state at (exclusive upper bound)
22
- */
23
- computeAt(events, targetIndex) {
24
- if (targetIndex <= 0) {
25
- return computeState([]);
26
- }
27
- this.ensureCachedUpTo(events, targetIndex);
28
- if (this._snapshots.length === 0) {
29
- return computeState(events.slice(0, targetIndex));
30
- }
31
- const snapshotSlot = Math.min(Math.floor(targetIndex / SNAPSHOT_INTERVAL), this._snapshots.length) - 1;
32
- if (snapshotSlot < 0) {
33
- return computeState(events.slice(0, targetIndex));
34
- }
35
- const snapshotEventIndex = (snapshotSlot + 1) * SNAPSHOT_INTERVAL;
36
- if (snapshotEventIndex === targetIndex) {
37
- return this._snapshots[snapshotSlot];
38
- }
39
- return replayDelta(this._snapshots[snapshotSlot], events.slice(snapshotEventIndex, targetIndex));
12
+ // src/debug/place-analysis.ts
13
+ var PlaceAnalysis = class _PlaceAnalysis {
14
+ _data;
15
+ constructor(data) {
16
+ this._data = data;
40
17
  }
41
- /** Invalidates the cache. */
42
- invalidate() {
43
- this._snapshots.length = 0;
18
+ get data() {
19
+ return this._data;
44
20
  }
45
- /** Extends the snapshot cache to cover at least up to the given event index. */
46
- ensureCachedUpTo(events, targetIndex) {
47
- const neededSnapshots = Math.floor(targetIndex / SNAPSHOT_INTERVAL);
48
- while (this._snapshots.length < neededSnapshots) {
49
- const nextSnapshotIndex = (this._snapshots.length + 1) * SNAPSHOT_INTERVAL;
50
- if (nextSnapshotIndex > events.length) break;
51
- if (this._snapshots.length === 0) {
52
- this._snapshots.push(computeState(events.slice(0, nextSnapshotIndex)));
53
- } else {
54
- const prevSnapshotIndex = this._snapshots.length * SNAPSHOT_INTERVAL;
55
- const delta = events.slice(prevSnapshotIndex, nextSnapshotIndex);
56
- this._snapshots.push(replayDelta(this._snapshots[this._snapshots.length - 1], delta));
21
+ isStart(placeName) {
22
+ const info = this._data.get(placeName);
23
+ return info != null && !info.hasIncoming;
24
+ }
25
+ isEnd(placeName) {
26
+ const info = this._data.get(placeName);
27
+ return info != null && !info.hasOutgoing;
28
+ }
29
+ /** Build place analysis from a PetriNet. */
30
+ static from(net) {
31
+ const data = /* @__PURE__ */ new Map();
32
+ function ensure(place) {
33
+ let info = data.get(place.name);
34
+ if (!info) {
35
+ info = { tokenType: "unknown", hasIncoming: false, hasOutgoing: false };
36
+ data.set(place.name, info);
37
+ }
38
+ return info;
39
+ }
40
+ for (const transition of net.transitions) {
41
+ for (const input of transition.inputSpecs) {
42
+ const info = ensure(input.place);
43
+ info.hasOutgoing = true;
44
+ }
45
+ if (transition.outputSpec) {
46
+ const outputPlaces = collectOutputPlaces(transition.outputSpec);
47
+ for (const place of outputPlaces) {
48
+ const info = ensure(place);
49
+ info.hasIncoming = true;
50
+ }
51
+ }
52
+ for (const inh of transition.inhibitors) {
53
+ ensure(inh.place);
54
+ }
55
+ for (const read of transition.reads) {
56
+ const info = ensure(read.place);
57
+ info.hasOutgoing = true;
58
+ }
59
+ for (const reset of transition.resets) {
60
+ ensure(reset.place);
57
61
  }
58
62
  }
63
+ return new _PlaceAnalysis(data);
59
64
  }
60
65
  };
61
- function replayDelta(base, delta) {
62
- const marking = /* @__PURE__ */ new Map();
63
- for (const [key, value] of base.marking) {
64
- marking.set(key, [...value]);
66
+ function collectOutputPlaces(out) {
67
+ const spec = out;
68
+ switch (spec.type) {
69
+ case "place":
70
+ return spec.place ? [spec.place] : [];
71
+ case "and":
72
+ case "xor":
73
+ return (spec.children ?? []).flatMap((c) => collectOutputPlaces(c));
74
+ case "timeout":
75
+ return spec.child ? collectOutputPlaces(spec.child) : [];
76
+ case "forward-input":
77
+ return spec.to ? [spec.to] : [];
78
+ default:
79
+ return [];
65
80
  }
66
- const enabled = new Set(base.enabledTransitions);
67
- const inFlight = new Set(base.inFlightTransitions);
68
- applyEvents(marking, enabled, inFlight, delta);
69
- return toImmutableState(marking, enabled, inFlight);
70
81
  }
71
82
 
72
- // src/debug/net-event-converter.ts
73
- function toEventInfo(event, compact = false) {
74
- switch (event.type) {
75
- case "execution-started":
76
- return {
77
- type: "ExecutionStarted",
78
- timestamp: new Date(event.timestamp).toISOString(),
79
- transitionName: null,
80
- placeName: null,
81
- details: { netName: event.netName, executionId: event.executionId }
82
- };
83
- case "execution-completed":
84
- return {
85
- type: "ExecutionCompleted",
86
- timestamp: new Date(event.timestamp).toISOString(),
87
- transitionName: null,
88
- placeName: null,
89
- details: {
90
- netName: event.netName,
91
- executionId: event.executionId,
92
- totalDurationMs: event.totalDurationMs
93
- }
94
- };
95
- case "transition-enabled":
96
- return {
97
- type: "TransitionEnabled",
98
- timestamp: new Date(event.timestamp).toISOString(),
99
- transitionName: event.transitionName,
100
- placeName: null,
101
- details: {}
102
- };
103
- case "transition-clock-restarted":
104
- return {
105
- type: "TransitionClockRestarted",
106
- timestamp: new Date(event.timestamp).toISOString(),
107
- transitionName: event.transitionName,
108
- placeName: null,
109
- details: {}
110
- };
111
- case "transition-started":
112
- return {
113
- type: "TransitionStarted",
114
- timestamp: new Date(event.timestamp).toISOString(),
115
- transitionName: event.transitionName,
116
- placeName: null,
117
- details: {
118
- consumedTokens: event.consumedTokens.map((t) => compact ? compactTokenInfo(t) : tokenInfo(t))
119
- }
120
- };
121
- case "transition-completed":
122
- return {
123
- type: "TransitionCompleted",
124
- timestamp: new Date(event.timestamp).toISOString(),
125
- transitionName: event.transitionName,
126
- placeName: null,
127
- details: {
128
- producedTokens: event.producedTokens.map((t) => compact ? compactTokenInfo(t) : tokenInfo(t)),
129
- durationMs: event.durationMs
130
- }
131
- };
132
- case "transition-failed":
133
- return {
134
- type: "TransitionFailed",
135
- timestamp: new Date(event.timestamp).toISOString(),
136
- transitionName: event.transitionName,
137
- placeName: null,
138
- details: {
139
- errorMessage: event.errorMessage,
140
- exceptionType: event.exceptionType
141
- }
142
- };
143
- case "transition-timed-out":
144
- return {
145
- type: "TransitionTimedOut",
146
- timestamp: new Date(event.timestamp).toISOString(),
147
- transitionName: event.transitionName,
148
- placeName: null,
149
- details: {
150
- deadlineMs: event.deadlineMs,
151
- actualDurationMs: event.actualDurationMs
152
- }
153
- };
154
- case "action-timed-out":
155
- return {
156
- type: "ActionTimedOut",
157
- timestamp: new Date(event.timestamp).toISOString(),
158
- transitionName: event.transitionName,
159
- placeName: null,
160
- details: { timeoutMs: event.timeoutMs }
161
- };
162
- case "token-added":
163
- return {
164
- type: "TokenAdded",
165
- timestamp: new Date(event.timestamp).toISOString(),
166
- transitionName: null,
167
- placeName: event.placeName,
168
- details: {
169
- token: compact ? compactTokenInfo(event.token) : tokenInfo(event.token)
170
- }
171
- };
172
- case "token-removed":
173
- return {
174
- type: "TokenRemoved",
175
- timestamp: new Date(event.timestamp).toISOString(),
176
- transitionName: null,
177
- placeName: event.placeName,
178
- details: {
179
- token: compact ? compactTokenInfo(event.token) : tokenInfo(event.token)
180
- }
181
- };
182
- case "marking-snapshot":
183
- return {
184
- type: "MarkingSnapshot",
185
- timestamp: new Date(event.timestamp).toISOString(),
186
- transitionName: null,
187
- placeName: null,
188
- details: {
189
- marking: convertMarking(event.marking, compact)
83
+ // src/debug/debug-event-store.ts
84
+ var DEFAULT_MAX_EVENTS = 1e4;
85
+ var DebugEventStore = class {
86
+ _events = [];
87
+ _subscribers = /* @__PURE__ */ new Set();
88
+ _sessionId;
89
+ _maxEvents;
90
+ _eventCount = 0;
91
+ _evictedCount = 0;
92
+ constructor(sessionId, maxEvents = DEFAULT_MAX_EVENTS) {
93
+ if (maxEvents <= 0) throw new Error(`maxEvents must be positive, got: ${maxEvents}`);
94
+ this._sessionId = sessionId;
95
+ this._maxEvents = maxEvents;
96
+ }
97
+ get sessionId() {
98
+ return this._sessionId;
99
+ }
100
+ get maxEvents() {
101
+ return this._maxEvents;
102
+ }
103
+ /** Total events appended (including evicted). */
104
+ eventCount() {
105
+ return this._eventCount;
106
+ }
107
+ /** Number of events evicted from the store. */
108
+ evictedCount() {
109
+ return this._evictedCount;
110
+ }
111
+ // ======================== EventStore Implementation ========================
112
+ append(event) {
113
+ this._events.push(event);
114
+ this._eventCount++;
115
+ while (this._events.length > this._maxEvents) {
116
+ this._events.shift();
117
+ this._evictedCount++;
118
+ }
119
+ if (this._subscribers.size > 0) {
120
+ const subscribers = [...this._subscribers];
121
+ queueMicrotask(() => {
122
+ for (const sub of subscribers) {
123
+ try {
124
+ sub(event);
125
+ } catch (e) {
126
+ console.warn("Subscriber threw exception during event broadcast", e);
127
+ }
190
128
  }
191
- };
192
- case "log-message": {
193
- const details = {
194
- loggerName: event.logger,
195
- level: event.level,
196
- message: event.message
197
- };
198
- if (event.error != null) details["throwable"] = event.error;
199
- if (event.errorMessage != null) details["throwableMessage"] = event.errorMessage;
200
- return {
201
- type: "LogMessage",
202
- timestamp: new Date(event.timestamp).toISOString(),
203
- transitionName: event.transitionName,
204
- placeName: null,
205
- details
206
- };
129
+ });
207
130
  }
208
131
  }
209
- }
210
- function tokenInfo(token) {
211
- const value = token.value;
212
- const type = value != null ? typeof value === "object" ? value.constructor.name : typeof value : "null";
213
- const fullValue = value != null ? String(value) : "null";
214
- return {
215
- id: null,
216
- type,
217
- value: fullValue,
218
- timestamp: new Date(token.createdAt).toISOString()
219
- };
220
- }
221
- function compactTokenInfo(token) {
222
- const value = token.value;
223
- const type = value != null ? typeof value === "object" ? value.constructor.name : typeof value : "null";
224
- return {
225
- id: null,
226
- type,
227
- value: null,
228
- timestamp: new Date(token.createdAt).toISOString()
229
- };
230
- }
231
- function convertMarking(marking, compact = false) {
232
- const result = {};
233
- const mapper = compact ? compactTokenInfo : tokenInfo;
234
- for (const [name, tokens] of marking) {
235
- result[name] = tokens.map(mapper);
132
+ events() {
133
+ return this._events;
134
+ }
135
+ isEnabled() {
136
+ return true;
137
+ }
138
+ size() {
139
+ return this._events.length;
236
140
  }
237
- return result;
238
- }
239
-
240
- // src/debug/debug-protocol-handler.ts
241
- var BATCH_SIZE = 500;
242
- var DebugProtocolHandler = class {
243
- _sessionRegistry;
244
- _clients = /* @__PURE__ */ new Map();
245
- constructor(sessionRegistry) {
246
- this._sessionRegistry = sessionRegistry;
141
+ isEmpty() {
142
+ return this._events.length === 0;
247
143
  }
248
- /** Registers a new client connection. */
249
- clientConnected(clientId, sink) {
250
- this._clients.set(clientId, new ClientState(sink));
144
+ // ======================== Live Tailing ========================
145
+ /** Subscribe to receive events as they occur. */
146
+ subscribe(listener) {
147
+ this._subscribers.add(listener);
148
+ return {
149
+ cancel: () => {
150
+ this._subscribers.delete(listener);
151
+ },
152
+ isActive: () => this._subscribers.has(listener)
153
+ };
251
154
  }
252
- /** Cleans up when a client disconnects. */
253
- clientDisconnected(clientId) {
254
- const state = this._clients.get(clientId);
255
- this._clients.delete(clientId);
256
- if (state) state.subscriptions.cancelAll();
155
+ /** Number of active subscribers. */
156
+ subscriberCount() {
157
+ return this._subscribers.size;
257
158
  }
258
- /** Handles a command from a connected client. */
259
- handleCommand(clientId, command) {
260
- const clientState = this._clients.get(clientId);
261
- if (!clientState) return;
262
- try {
263
- switch (command.type) {
264
- case "listSessions":
265
- this.handleListSessions(clientState, command);
266
- break;
267
- case "subscribe":
268
- this.handleSubscribe(clientState, command);
269
- break;
270
- case "unsubscribe":
271
- this.handleUnsubscribe(clientState, command);
272
- break;
273
- case "seek":
274
- this.handleSeek(clientState, command);
275
- break;
276
- case "playbackSpeed":
277
- this.handlePlaybackSpeed(clientState, command);
278
- break;
279
- case "filter":
280
- this.handleSetFilter(clientState, command);
281
- break;
282
- case "pause":
283
- this.handlePause(clientState, command);
284
- break;
285
- case "resume":
286
- this.handleResume(clientState, command);
287
- break;
288
- case "stepForward":
289
- this.handleStepForward(clientState, command);
290
- break;
291
- case "stepBackward":
292
- this.handleStepBackward(clientState, command);
293
- break;
294
- case "setBreakpoint":
295
- this.handleSetBreakpoint(clientState, command);
296
- break;
297
- case "clearBreakpoint":
298
- this.handleClearBreakpoint(clientState, command);
299
- break;
300
- case "listBreakpoints":
301
- this.handleListBreakpoints(clientState, command);
302
- break;
303
- }
304
- } catch (e) {
305
- this.sendError(clientState, "COMMAND_ERROR", e instanceof Error ? e.message : String(e), null);
306
- }
159
+ // ======================== Historical Replay ========================
160
+ /** Returns events starting from a specific index. */
161
+ eventsFrom(fromIndex) {
162
+ const adjustedSkip = Math.max(0, fromIndex - this._evictedCount);
163
+ if (adjustedSkip <= 0) return this._events;
164
+ return this._events.slice(adjustedSkip);
307
165
  }
308
- // ======================== Command Handlers ========================
309
- handleListSessions(client, cmd) {
310
- const limit = cmd.limit ?? 50;
311
- const sessions = cmd.activeOnly ? this._sessionRegistry.listActiveSessions(limit) : this._sessionRegistry.listSessions(limit);
312
- const summaries = sessions.map((s) => ({
313
- sessionId: s.sessionId,
314
- netName: s.netName,
315
- startTime: new Date(s.startTime).toISOString(),
316
- active: s.active,
317
- eventCount: s.eventStore.eventCount()
318
- }));
319
- this.send(client, { type: "sessionList", sessions: summaries });
166
+ /** Returns all events since the specified timestamp. */
167
+ eventsSince(from) {
168
+ return this._events.filter((e) => e.timestamp >= from);
320
169
  }
321
- handleSubscribe(client, cmd) {
322
- const debugSession = this._sessionRegistry.getSession(cmd.sessionId);
323
- if (!debugSession) {
324
- this.sendError(client, "SESSION_NOT_FOUND", `Session not found: ${cmd.sessionId}`, cmd.sessionId);
325
- return;
326
- }
327
- const eventStore = debugSession.eventStore;
328
- client.subscriptions.cancel(cmd.sessionId);
329
- const events = eventStore.events();
330
- const computed = computeState(events);
331
- const structure = buildNetStructure(debugSession);
332
- this.send(client, {
333
- type: "subscribed",
334
- sessionId: cmd.sessionId,
335
- netName: debugSession.netName,
336
- dotDiagram: debugSession.dotDiagram,
337
- structure,
338
- currentMarking: mapToRecord(computed.marking),
339
- enabledTransitions: computed.enabledTransitions,
340
- inFlightTransitions: computed.inFlightTransitions,
341
- eventCount: eventStore.eventCount(),
342
- mode: cmd.mode
170
+ /** Returns events within a time range. */
171
+ eventsBetween(from, to) {
172
+ return this._events.filter((e) => e.timestamp >= from && e.timestamp < to);
173
+ }
174
+ /**
175
+ * Returns an iterator over all retained events.
176
+ * Useful for archive writers that need zero-copy traversal.
177
+ */
178
+ [Symbol.iterator]() {
179
+ return this._events[Symbol.iterator]();
180
+ }
181
+ // ======================== Lifecycle ========================
182
+ /** Close the store (no-op in JS, but matches Java interface). */
183
+ close() {
184
+ this._subscribers.clear();
185
+ }
186
+ };
187
+
188
+ // src/debug/debug-session-registry.ts
189
+ function buildNetStructure(session) {
190
+ if (session.importedStructure) {
191
+ return session.importedStructure;
192
+ }
193
+ const places = session.places;
194
+ if (!places) {
195
+ return { places: [], transitions: [] };
196
+ }
197
+ const placeInfos = [];
198
+ for (const [name, info] of places.data) {
199
+ placeInfos.push({
200
+ name,
201
+ graphId: `p_${sanitize(name)}`,
202
+ tokenType: info.tokenType,
203
+ isStart: !info.hasIncoming,
204
+ isEnd: !info.hasOutgoing,
205
+ isEnvironment: false
343
206
  });
344
- const fromIndex = cmd.fromIndex ?? 0;
345
- if (cmd.mode === "live") {
346
- this.subscribeLive(client, cmd.sessionId, debugSession, fromIndex);
347
- } else {
348
- this.subscribeReplay(client, cmd.sessionId, debugSession, fromIndex);
349
- }
350
207
  }
351
- subscribeLive(client, sessionId, debugSession, fromIndex) {
352
- const eventStore = debugSession.eventStore;
353
- let eventIndex = fromIndex;
354
- const historicalEvents = eventStore.eventsFrom(fromIndex);
355
- if (historicalEvents.length > 0) {
356
- const filtered = historicalEvents.filter((e) => client.subscriptions.matchesFilter(sessionId, e)).map((e) => toEventInfo(e));
357
- this.sendInBatches(client, sessionId, fromIndex, filtered);
358
- eventIndex = fromIndex + historicalEvents.length;
359
- }
360
- const subscription = eventStore.subscribe((event) => {
361
- if (!client.subscriptions.isPaused(sessionId) && client.subscriptions.matchesFilter(sessionId, event)) {
362
- const eventInfo = toEventInfo(event);
363
- const idx = eventIndex++;
364
- const hitBreakpoint = client.subscriptions.checkBreakpoints(sessionId, event);
365
- if (hitBreakpoint) {
366
- client.subscriptions.setPaused(sessionId, true);
367
- this.send(client, {
368
- type: "breakpointHit",
369
- sessionId,
370
- breakpointId: hitBreakpoint.id,
371
- event: eventInfo,
372
- eventIndex: idx
373
- });
374
- }
375
- this.send(client, { type: "event", sessionId, index: idx, event: eventInfo });
376
- }
208
+ const transitionInfos = [];
209
+ for (const t of session.transitions) {
210
+ transitionInfos.push({
211
+ name: t.name,
212
+ graphId: `t_${sanitize(t.name)}`
377
213
  });
378
- client.subscriptions.addSubscription(sessionId, subscription, eventIndex);
379
214
  }
380
- subscribeReplay(client, sessionId, debugSession, fromIndex) {
381
- const eventStore = debugSession.eventStore;
382
- const events = eventStore.eventsFrom(fromIndex);
383
- const converted = events.map((e) => toEventInfo(e));
384
- this.sendInBatches(client, sessionId, fromIndex, converted);
385
- const eventIndex = fromIndex + events.length;
386
- client.subscriptions.addSubscription(sessionId, null, eventIndex);
387
- client.subscriptions.setPaused(sessionId, true);
215
+ return { places: placeInfos, transitions: transitionInfos };
216
+ }
217
+ var DebugSessionRegistry = class {
218
+ _sessions = /* @__PURE__ */ new Map();
219
+ _maxSessions;
220
+ _eventStoreFactory;
221
+ _completionListeners;
222
+ constructor(maxSessions = 50, eventStoreFactory, completionListeners) {
223
+ this._maxSessions = maxSessions;
224
+ this._eventStoreFactory = eventStoreFactory ?? ((id) => new DebugEventStore(id));
225
+ this._completionListeners = completionListeners ? [...completionListeners] : [];
388
226
  }
389
- handleUnsubscribe(client, cmd) {
390
- client.subscriptions.cancel(cmd.sessionId);
391
- this.send(client, { type: "unsubscribed", sessionId: cmd.sessionId });
227
+ /**
228
+ * Registers a new debug session for the given Petri net.
229
+ * Generates DOT diagram and extracts net structure.
230
+ */
231
+ register(sessionId, net) {
232
+ const dotDiagram = dotExport(net);
233
+ const places = PlaceAnalysis.from(net);
234
+ const eventStore = this._eventStoreFactory(sessionId);
235
+ const session = {
236
+ sessionId,
237
+ netName: net.name,
238
+ dotDiagram,
239
+ places,
240
+ transitions: net.transitions,
241
+ eventStore,
242
+ startTime: Date.now(),
243
+ active: true,
244
+ importedStructure: null
245
+ };
246
+ this.evictIfNecessary();
247
+ this._sessions.set(sessionId, session);
248
+ return session;
392
249
  }
393
- handleSeek(client, cmd) {
394
- const debugSession = this._sessionRegistry.getSession(cmd.sessionId);
395
- if (!debugSession) {
396
- this.sendError(client, "SESSION_NOT_FOUND", "Session not found", cmd.sessionId);
397
- return;
250
+ /**
251
+ * Marks a session as completed (no longer active) and notifies completion listeners.
252
+ */
253
+ complete(sessionId) {
254
+ const session = this._sessions.get(sessionId);
255
+ if (session) {
256
+ const completed = { ...session, active: false };
257
+ this._sessions.set(sessionId, completed);
258
+ this.notifyCompletionListeners(completed);
398
259
  }
399
- const events = debugSession.eventStore.events();
400
- const targetTs = new Date(cmd.timestamp).getTime();
401
- let targetIndex = 0;
402
- for (let i = 0; i < events.length; i++) {
403
- if (events[i].timestamp >= targetTs) {
404
- targetIndex = i;
405
- break;
406
- }
407
- targetIndex = i + 1;
260
+ }
261
+ /** Removes a session from the registry. */
262
+ remove(sessionId) {
263
+ const removed = this._sessions.get(sessionId);
264
+ if (removed) {
265
+ this._sessions.delete(sessionId);
266
+ removed.eventStore.close();
408
267
  }
409
- client.subscriptions.setEventIndex(cmd.sessionId, targetIndex);
410
- const computed = client.subscriptions.computeStateAt(cmd.sessionId, events, targetIndex);
411
- this.send(client, {
412
- type: "markingSnapshot",
413
- sessionId: cmd.sessionId,
414
- marking: mapToRecord(computed.marking),
415
- enabledTransitions: computed.enabledTransitions,
416
- inFlightTransitions: computed.inFlightTransitions
417
- });
268
+ return removed;
269
+ }
270
+ /** Returns a session by ID. */
271
+ getSession(sessionId) {
272
+ return this._sessions.get(sessionId);
273
+ }
274
+ /** Lists sessions, ordered by start time (most recent first). */
275
+ listSessions(limit) {
276
+ return [...this._sessions.values()].sort((a, b) => b.startTime - a.startTime).slice(0, limit);
418
277
  }
419
- handlePlaybackSpeed(client, cmd) {
420
- client.subscriptions.setSpeed(cmd.sessionId, cmd.speed);
421
- this.send(client, {
422
- type: "playbackStateChanged",
423
- sessionId: cmd.sessionId,
424
- paused: client.subscriptions.isPaused(cmd.sessionId),
425
- speed: cmd.speed,
426
- currentIndex: client.subscriptions.getEventIndex(cmd.sessionId)
427
- });
278
+ /** Lists only active sessions. */
279
+ listActiveSessions(limit) {
280
+ return [...this._sessions.values()].filter((s) => s.active).sort((a, b) => b.startTime - a.startTime).slice(0, limit);
428
281
  }
429
- handleSetFilter(client, cmd) {
430
- client.subscriptions.setFilter(cmd.sessionId, cmd.filter);
431
- this.send(client, { type: "filterApplied", sessionId: cmd.sessionId, filter: cmd.filter });
282
+ /** Total number of sessions. */
283
+ get size() {
284
+ return this._sessions.size;
432
285
  }
433
- handlePause(client, cmd) {
434
- client.subscriptions.setPaused(cmd.sessionId, true);
435
- this.send(client, {
436
- type: "playbackStateChanged",
437
- sessionId: cmd.sessionId,
438
- paused: true,
439
- speed: client.subscriptions.getSpeed(cmd.sessionId),
440
- currentIndex: client.subscriptions.getEventIndex(cmd.sessionId)
441
- });
286
+ /**
287
+ * Registers an imported (archived) session as an inactive, read-only session.
288
+ */
289
+ registerImported(sessionId, netName, dotDiagram, structure, eventStore, startTime) {
290
+ this.evictIfNecessary();
291
+ const session = {
292
+ sessionId,
293
+ netName,
294
+ dotDiagram,
295
+ places: null,
296
+ transitions: /* @__PURE__ */ new Set(),
297
+ eventStore,
298
+ startTime,
299
+ active: false,
300
+ importedStructure: structure
301
+ };
302
+ this._sessions.set(sessionId, session);
303
+ return session;
442
304
  }
443
- handleResume(client, cmd) {
444
- client.subscriptions.setPaused(cmd.sessionId, false);
445
- this.send(client, {
446
- type: "playbackStateChanged",
447
- sessionId: cmd.sessionId,
448
- paused: false,
449
- speed: client.subscriptions.getSpeed(cmd.sessionId),
450
- currentIndex: client.subscriptions.getEventIndex(cmd.sessionId)
305
+ /** Notifies all completion listeners. Exceptions are caught and logged. */
306
+ notifyCompletionListeners(session) {
307
+ for (const listener of this._completionListeners) {
308
+ try {
309
+ listener(session);
310
+ } catch (e) {
311
+ console.warn(`Session completion listener failed for ${session.sessionId}`, e);
312
+ }
313
+ }
314
+ }
315
+ /** Evicts oldest inactive sessions if at capacity. */
316
+ evictIfNecessary() {
317
+ if (this._sessions.size < this._maxSessions) return;
318
+ const candidates = [...this._sessions.values()].sort((a, b) => {
319
+ if (a.active !== b.active) return a.active ? 1 : -1;
320
+ return a.startTime - b.startTime;
451
321
  });
322
+ for (const candidate of candidates) {
323
+ if (this._sessions.size < this._maxSessions) break;
324
+ const evicted = this._sessions.get(candidate.sessionId);
325
+ if (evicted) {
326
+ this._sessions.delete(candidate.sessionId);
327
+ evicted.eventStore.close();
328
+ }
329
+ }
452
330
  }
453
- handleStepForward(client, cmd) {
454
- const debugSession = this._sessionRegistry.getSession(cmd.sessionId);
455
- if (!debugSession) {
456
- this.sendError(client, "SESSION_NOT_FOUND", `Session not found: ${cmd.sessionId}`, cmd.sessionId);
457
- return;
331
+ };
332
+
333
+ // src/debug/marking-cache.ts
334
+ var SNAPSHOT_INTERVAL = 256;
335
+ var MarkingCache = class {
336
+ _snapshots = [];
337
+ /**
338
+ * Computes the state at the given event index, using cached snapshots
339
+ * to minimize the number of events that need to be replayed.
340
+ *
341
+ * @param events the full event list for the session
342
+ * @param targetIndex event index to compute state at (exclusive upper bound)
343
+ */
344
+ computeAt(events, targetIndex) {
345
+ if (targetIndex <= 0) {
346
+ return computeState([]);
458
347
  }
459
- const events = debugSession.eventStore.events();
460
- const currentIndex = client.subscriptions.getEventIndex(cmd.sessionId);
461
- if (currentIndex < events.length) {
462
- const event = events[currentIndex];
463
- this.send(client, {
464
- type: "event",
465
- sessionId: cmd.sessionId,
466
- index: currentIndex,
467
- event: toEventInfo(event)
468
- });
469
- client.subscriptions.setEventIndex(cmd.sessionId, currentIndex + 1);
348
+ this.ensureCachedUpTo(events, targetIndex);
349
+ if (this._snapshots.length === 0) {
350
+ return computeState(events.slice(0, targetIndex));
470
351
  }
471
- }
472
- handleStepBackward(client, cmd) {
473
- const debugSession = this._sessionRegistry.getSession(cmd.sessionId);
474
- if (!debugSession) {
475
- this.sendError(client, "SESSION_NOT_FOUND", `Session not found: ${cmd.sessionId}`, cmd.sessionId);
476
- return;
352
+ const snapshotSlot = Math.min(Math.floor(targetIndex / SNAPSHOT_INTERVAL), this._snapshots.length) - 1;
353
+ if (snapshotSlot < 0) {
354
+ return computeState(events.slice(0, targetIndex));
477
355
  }
478
- let currentIndex = client.subscriptions.getEventIndex(cmd.sessionId);
479
- if (currentIndex > 0) {
480
- currentIndex--;
481
- client.subscriptions.setEventIndex(cmd.sessionId, currentIndex);
482
- const events = debugSession.eventStore.events();
483
- const computed = client.subscriptions.computeStateAt(cmd.sessionId, events, currentIndex);
484
- this.send(client, {
485
- type: "markingSnapshot",
486
- sessionId: cmd.sessionId,
487
- marking: mapToRecord(computed.marking),
488
- enabledTransitions: computed.enabledTransitions,
489
- inFlightTransitions: computed.inFlightTransitions
490
- });
356
+ const snapshotEventIndex = (snapshotSlot + 1) * SNAPSHOT_INTERVAL;
357
+ if (snapshotEventIndex === targetIndex) {
358
+ return this._snapshots[snapshotSlot];
491
359
  }
360
+ return replayDelta(this._snapshots[snapshotSlot], events.slice(snapshotEventIndex, targetIndex));
492
361
  }
493
- handleSetBreakpoint(client, cmd) {
494
- client.subscriptions.addBreakpoint(cmd.sessionId, cmd.breakpoint);
495
- this.send(client, { type: "breakpointSet", sessionId: cmd.sessionId, breakpoint: cmd.breakpoint });
496
- }
497
- handleClearBreakpoint(client, cmd) {
498
- client.subscriptions.removeBreakpoint(cmd.sessionId, cmd.breakpointId);
499
- this.send(client, { type: "breakpointCleared", sessionId: cmd.sessionId, breakpointId: cmd.breakpointId });
500
- }
501
- handleListBreakpoints(client, cmd) {
502
- const breakpoints = client.subscriptions.getBreakpoints(cmd.sessionId);
503
- this.send(client, { type: "breakpointList", sessionId: cmd.sessionId, breakpoints });
504
- }
505
- // ======================== Helper Methods ========================
506
- send(client, response) {
507
- client.sink(response);
508
- }
509
- sendError(client, code, message, sessionId) {
510
- this.send(client, { type: "error", code, message, sessionId });
362
+ /** Invalidates the cache. */
363
+ invalidate() {
364
+ this._snapshots.length = 0;
511
365
  }
512
- sendInBatches(client, sessionId, startIndex, events) {
513
- if (events.length === 0) {
514
- this.send(client, { type: "eventBatch", sessionId, startIndex, events: [], hasMore: false });
515
- return;
516
- }
517
- for (let i = 0; i < events.length; i += BATCH_SIZE) {
518
- const end = Math.min(i + BATCH_SIZE, events.length);
519
- const chunk = events.slice(i, end);
520
- const hasMore = end < events.length;
521
- this.send(client, { type: "eventBatch", sessionId, startIndex: startIndex + i, events: chunk, hasMore });
366
+ /** Extends the snapshot cache to cover at least up to the given event index. */
367
+ ensureCachedUpTo(events, targetIndex) {
368
+ const neededSnapshots = Math.floor(targetIndex / SNAPSHOT_INTERVAL);
369
+ while (this._snapshots.length < neededSnapshots) {
370
+ const nextSnapshotIndex = (this._snapshots.length + 1) * SNAPSHOT_INTERVAL;
371
+ if (nextSnapshotIndex > events.length) break;
372
+ if (this._snapshots.length === 0) {
373
+ this._snapshots.push(computeState(events.slice(0, nextSnapshotIndex)));
374
+ } else {
375
+ const prevSnapshotIndex = this._snapshots.length * SNAPSHOT_INTERVAL;
376
+ const delta = events.slice(prevSnapshotIndex, nextSnapshotIndex);
377
+ this._snapshots.push(replayDelta(this._snapshots[this._snapshots.length - 1], delta));
378
+ }
522
379
  }
523
380
  }
524
381
  };
525
- function computeState(events) {
382
+ function replayDelta(base, delta) {
526
383
  const marking = /* @__PURE__ */ new Map();
527
- const enabled = /* @__PURE__ */ new Set();
528
- const inFlight = /* @__PURE__ */ new Set();
529
- applyEvents(marking, enabled, inFlight, events);
384
+ for (const [key, value] of base.marking) {
385
+ marking.set(key, [...value]);
386
+ }
387
+ const enabled = new Set(base.enabledTransitions);
388
+ const inFlight = new Set(base.inFlightTransitions);
389
+ applyEvents(marking, enabled, inFlight, delta);
530
390
  return toImmutableState(marking, enabled, inFlight);
531
391
  }
532
- function applyEvents(marking, enabled, inFlight, events) {
533
- for (const event of events) {
534
- switch (event.type) {
535
- case "token-added": {
536
- let tokens = marking.get(event.placeName);
537
- if (!tokens) {
538
- tokens = [];
539
- marking.set(event.placeName, tokens);
392
+
393
+ // src/debug/net-event-converter.ts
394
+ function toEventInfo(event, compact = false) {
395
+ switch (event.type) {
396
+ case "execution-started":
397
+ return {
398
+ type: "ExecutionStarted",
399
+ timestamp: new Date(event.timestamp).toISOString(),
400
+ transitionName: null,
401
+ placeName: null,
402
+ details: { netName: event.netName, executionId: event.executionId }
403
+ };
404
+ case "execution-completed":
405
+ return {
406
+ type: "ExecutionCompleted",
407
+ timestamp: new Date(event.timestamp).toISOString(),
408
+ transitionName: null,
409
+ placeName: null,
410
+ details: {
411
+ netName: event.netName,
412
+ executionId: event.executionId,
413
+ totalDurationMs: event.totalDurationMs
414
+ }
415
+ };
416
+ case "transition-enabled":
417
+ return {
418
+ type: "TransitionEnabled",
419
+ timestamp: new Date(event.timestamp).toISOString(),
420
+ transitionName: event.transitionName,
421
+ placeName: null,
422
+ details: {}
423
+ };
424
+ case "transition-clock-restarted":
425
+ return {
426
+ type: "TransitionClockRestarted",
427
+ timestamp: new Date(event.timestamp).toISOString(),
428
+ transitionName: event.transitionName,
429
+ placeName: null,
430
+ details: {}
431
+ };
432
+ case "transition-started":
433
+ return {
434
+ type: "TransitionStarted",
435
+ timestamp: new Date(event.timestamp).toISOString(),
436
+ transitionName: event.transitionName,
437
+ placeName: null,
438
+ details: {
439
+ consumedTokens: event.consumedTokens.map((t) => compact ? compactTokenInfo(t) : tokenInfo(t))
440
+ }
441
+ };
442
+ case "transition-completed":
443
+ return {
444
+ type: "TransitionCompleted",
445
+ timestamp: new Date(event.timestamp).toISOString(),
446
+ transitionName: event.transitionName,
447
+ placeName: null,
448
+ details: {
449
+ producedTokens: event.producedTokens.map((t) => compact ? compactTokenInfo(t) : tokenInfo(t)),
450
+ durationMs: event.durationMs
451
+ }
452
+ };
453
+ case "transition-failed":
454
+ return {
455
+ type: "TransitionFailed",
456
+ timestamp: new Date(event.timestamp).toISOString(),
457
+ transitionName: event.transitionName,
458
+ placeName: null,
459
+ details: {
460
+ errorMessage: event.errorMessage,
461
+ exceptionType: event.exceptionType
540
462
  }
541
- tokens.push(tokenInfo(event.token));
542
- break;
543
- }
544
- case "token-removed": {
545
- const tokens = marking.get(event.placeName);
546
- if (tokens && tokens.length > 0) tokens.shift();
547
- break;
548
- }
549
- case "marking-snapshot": {
550
- marking.clear();
551
- const converted = convertMarking(event.marking);
552
- for (const [key, value] of Object.entries(converted)) {
553
- marking.set(key, [...value]);
463
+ };
464
+ case "transition-timed-out":
465
+ return {
466
+ type: "TransitionTimedOut",
467
+ timestamp: new Date(event.timestamp).toISOString(),
468
+ transitionName: event.transitionName,
469
+ placeName: null,
470
+ details: {
471
+ deadlineMs: event.deadlineMs,
472
+ actualDurationMs: event.actualDurationMs
554
473
  }
555
- break;
556
- }
557
- case "transition-enabled":
558
- enabled.add(event.transitionName);
559
- break;
560
- case "transition-started":
561
- enabled.delete(event.transitionName);
562
- inFlight.add(event.transitionName);
563
- break;
564
- case "transition-completed":
565
- inFlight.delete(event.transitionName);
566
- break;
567
- case "transition-failed":
568
- inFlight.delete(event.transitionName);
569
- break;
570
- case "transition-timed-out":
571
- inFlight.delete(event.transitionName);
572
- break;
573
- case "action-timed-out":
574
- inFlight.delete(event.transitionName);
575
- break;
576
- default:
577
- break;
474
+ };
475
+ case "action-timed-out":
476
+ return {
477
+ type: "ActionTimedOut",
478
+ timestamp: new Date(event.timestamp).toISOString(),
479
+ transitionName: event.transitionName,
480
+ placeName: null,
481
+ details: { timeoutMs: event.timeoutMs }
482
+ };
483
+ case "token-added":
484
+ return {
485
+ type: "TokenAdded",
486
+ timestamp: new Date(event.timestamp).toISOString(),
487
+ transitionName: null,
488
+ placeName: event.placeName,
489
+ details: {
490
+ token: compact ? compactTokenInfo(event.token) : tokenInfo(event.token)
491
+ }
492
+ };
493
+ case "token-removed":
494
+ return {
495
+ type: "TokenRemoved",
496
+ timestamp: new Date(event.timestamp).toISOString(),
497
+ transitionName: null,
498
+ placeName: event.placeName,
499
+ details: {
500
+ token: compact ? compactTokenInfo(event.token) : tokenInfo(event.token)
501
+ }
502
+ };
503
+ case "marking-snapshot":
504
+ return {
505
+ type: "MarkingSnapshot",
506
+ timestamp: new Date(event.timestamp).toISOString(),
507
+ transitionName: null,
508
+ placeName: null,
509
+ details: {
510
+ marking: convertMarking(event.marking, compact)
511
+ }
512
+ };
513
+ case "log-message": {
514
+ const details = {
515
+ loggerName: event.logger,
516
+ level: event.level,
517
+ message: event.message
518
+ };
519
+ if (event.error != null) details["throwable"] = event.error;
520
+ if (event.errorMessage != null) details["throwableMessage"] = event.errorMessage;
521
+ return {
522
+ type: "LogMessage",
523
+ timestamp: new Date(event.timestamp).toISOString(),
524
+ transitionName: event.transitionName,
525
+ placeName: null,
526
+ details
527
+ };
578
528
  }
579
529
  }
580
530
  }
581
- function toImmutableState(marking, enabled, inFlight) {
582
- const resultMarking = /* @__PURE__ */ new Map();
583
- for (const [key, value] of marking) {
584
- resultMarking.set(key, [...value]);
585
- }
531
+ function tokenInfo(token) {
532
+ const value = token.value;
533
+ const type = value != null ? typeof value === "object" ? value.constructor.name : typeof value : "null";
534
+ const fullValue = value != null ? String(value) : "null";
586
535
  return {
587
- marking: resultMarking,
588
- enabledTransitions: [...enabled],
589
- inFlightTransitions: [...inFlight]
536
+ id: null,
537
+ type,
538
+ value: fullValue,
539
+ timestamp: new Date(token.createdAt).toISOString()
590
540
  };
591
541
  }
592
- function buildNetStructure(debugSession) {
593
- const places = debugSession.places;
594
- const transitions = debugSession.transitions;
595
- const placeInfos = [...places.data.entries()].map(([name, info]) => ({
596
- name,
597
- graphId: `p_${sanitize(name)}`,
598
- tokenType: info.tokenType,
599
- isStart: !info.hasIncoming,
600
- isEnd: !info.hasOutgoing,
601
- isEnvironment: false
602
- }));
603
- const transitionInfos = [...transitions].map((t) => ({
604
- name: t.name,
605
- graphId: `t_${sanitize(t.name)}`
606
- }));
607
- return { places: placeInfos, transitions: transitionInfos };
542
+ function compactTokenInfo(token) {
543
+ const value = token.value;
544
+ const type = value != null ? typeof value === "object" ? value.constructor.name : typeof value : "null";
545
+ return {
546
+ id: null,
547
+ type,
548
+ value: null,
549
+ timestamp: new Date(token.createdAt).toISOString()
550
+ };
608
551
  }
609
- function mapToRecord(map) {
552
+ function convertMarking(marking, compact = false) {
610
553
  const result = {};
611
- for (const [key, value] of map) {
612
- result[key] = value;
554
+ const mapper = compact ? compactTokenInfo : tokenInfo;
555
+ for (const [name, tokens] of marking) {
556
+ result[name] = tokens.map(mapper);
613
557
  }
614
558
  return result;
615
559
  }
616
- var ClientState = class {
617
- sink;
618
- subscriptions = new SubscriptionState();
619
- constructor(sink) {
620
- this.sink = sink;
621
- }
622
- };
623
- var SubscriptionState = class {
624
- _sessionSubs = /* @__PURE__ */ new Map();
625
- addSubscription(sessionId, subscription, eventIndex) {
626
- this._sessionSubs.set(sessionId, {
627
- subscription,
628
- eventIndex,
629
- markingCache: new MarkingCache(),
630
- breakpoints: /* @__PURE__ */ new Map(),
631
- paused: false,
632
- speed: 1,
633
- filter: null
634
- });
635
- }
636
- cancel(sessionId) {
637
- const sub = this._sessionSubs.get(sessionId);
638
- if (sub?.subscription) sub.subscription.cancel();
639
- this._sessionSubs.delete(sessionId);
640
- }
641
- cancelAll() {
642
- for (const sub of this._sessionSubs.values()) {
643
- if (sub.subscription) sub.subscription.cancel();
644
- }
645
- this._sessionSubs.clear();
646
- }
647
- isPaused(sessionId) {
648
- return this._sessionSubs.get(sessionId)?.paused ?? false;
649
- }
650
- setPaused(sessionId, paused) {
651
- const sub = this._sessionSubs.get(sessionId);
652
- if (sub) sub.paused = paused;
653
- }
654
- getSpeed(sessionId) {
655
- return this._sessionSubs.get(sessionId)?.speed ?? 1;
656
- }
657
- setSpeed(sessionId, speed) {
658
- const sub = this._sessionSubs.get(sessionId);
659
- if (sub) sub.speed = speed;
660
- }
661
- getEventIndex(sessionId) {
662
- return this._sessionSubs.get(sessionId)?.eventIndex ?? 0;
663
- }
664
- setEventIndex(sessionId, index) {
665
- const sub = this._sessionSubs.get(sessionId);
666
- if (sub) sub.eventIndex = index;
560
+
561
+ // src/debug/debug-protocol-handler.ts
562
+ var BATCH_SIZE = 500;
563
+ var DebugProtocolHandler = class {
564
+ _sessionRegistry;
565
+ _clients = /* @__PURE__ */ new Map();
566
+ constructor(sessionRegistry) {
567
+ this._sessionRegistry = sessionRegistry;
667
568
  }
668
- computeStateAt(sessionId, events, targetIndex) {
669
- const sub = this._sessionSubs.get(sessionId);
670
- if (sub) return sub.markingCache.computeAt(events, targetIndex);
671
- return computeState(events.slice(0, targetIndex));
569
+ /** Registers a new client connection. */
570
+ clientConnected(clientId, sink) {
571
+ this._clients.set(clientId, new ClientState(sink));
672
572
  }
673
- setFilter(sessionId, filter) {
674
- const sub = this._sessionSubs.get(sessionId);
675
- if (sub) sub.filter = filter;
573
+ /** Cleans up when a client disconnects. */
574
+ clientDisconnected(clientId) {
575
+ const state = this._clients.get(clientId);
576
+ this._clients.delete(clientId);
577
+ if (state) state.subscriptions.cancelAll();
676
578
  }
677
- matchesFilter(sessionId, event) {
678
- const sub = this._sessionSubs.get(sessionId);
679
- if (!sub?.filter) return true;
680
- const filter = sub.filter;
681
- if (filter.eventTypes && filter.eventTypes.length > 0) {
682
- const eventType = eventTypeToName(event);
683
- if (!filter.eventTypes.includes(eventType)) return false;
684
- }
685
- if (filter.transitionNames && filter.transitionNames.length > 0) {
686
- const name = extractTransitionName(event);
687
- if (!name || !filter.transitionNames.includes(name)) return false;
688
- }
689
- if (filter.placeNames && filter.placeNames.length > 0) {
690
- const name = extractPlaceName(event);
691
- if (!name || !filter.placeNames.includes(name)) return false;
579
+ /** Handles a command from a connected client. */
580
+ handleCommand(clientId, command) {
581
+ const clientState = this._clients.get(clientId);
582
+ if (!clientState) return;
583
+ try {
584
+ switch (command.type) {
585
+ case "listSessions":
586
+ this.handleListSessions(clientState, command);
587
+ break;
588
+ case "subscribe":
589
+ this.handleSubscribe(clientState, command);
590
+ break;
591
+ case "unsubscribe":
592
+ this.handleUnsubscribe(clientState, command);
593
+ break;
594
+ case "seek":
595
+ this.handleSeek(clientState, command);
596
+ break;
597
+ case "playbackSpeed":
598
+ this.handlePlaybackSpeed(clientState, command);
599
+ break;
600
+ case "filter":
601
+ this.handleSetFilter(clientState, command);
602
+ break;
603
+ case "pause":
604
+ this.handlePause(clientState, command);
605
+ break;
606
+ case "resume":
607
+ this.handleResume(clientState, command);
608
+ break;
609
+ case "stepForward":
610
+ this.handleStepForward(clientState, command);
611
+ break;
612
+ case "stepBackward":
613
+ this.handleStepBackward(clientState, command);
614
+ break;
615
+ case "setBreakpoint":
616
+ this.handleSetBreakpoint(clientState, command);
617
+ break;
618
+ case "clearBreakpoint":
619
+ this.handleClearBreakpoint(clientState, command);
620
+ break;
621
+ case "listBreakpoints":
622
+ this.handleListBreakpoints(clientState, command);
623
+ break;
624
+ }
625
+ } catch (e) {
626
+ this.sendError(clientState, "COMMAND_ERROR", e instanceof Error ? e.message : String(e), null);
692
627
  }
693
- return true;
694
- }
695
- addBreakpoint(sessionId, breakpoint) {
696
- const sub = this._sessionSubs.get(sessionId);
697
- if (sub) sub.breakpoints.set(breakpoint.id, breakpoint);
698
628
  }
699
- removeBreakpoint(sessionId, breakpointId) {
700
- const sub = this._sessionSubs.get(sessionId);
701
- if (sub) sub.breakpoints.delete(breakpointId);
629
+ // ======================== Command Handlers ========================
630
+ handleListSessions(client, cmd) {
631
+ const limit = cmd.limit ?? 50;
632
+ const sessions = cmd.activeOnly ? this._sessionRegistry.listActiveSessions(limit) : this._sessionRegistry.listSessions(limit);
633
+ const summaries = sessions.map((s) => ({
634
+ sessionId: s.sessionId,
635
+ netName: s.netName,
636
+ startTime: new Date(s.startTime).toISOString(),
637
+ active: s.active,
638
+ eventCount: s.eventStore.eventCount()
639
+ }));
640
+ this.send(client, { type: "sessionList", sessions: summaries });
702
641
  }
703
- getBreakpoints(sessionId) {
704
- const sub = this._sessionSubs.get(sessionId);
705
- return sub ? [...sub.breakpoints.values()] : [];
642
+ handleSubscribe(client, cmd) {
643
+ const debugSession = this._sessionRegistry.getSession(cmd.sessionId);
644
+ if (!debugSession) {
645
+ this.sendError(client, "SESSION_NOT_FOUND", `Session not found: ${cmd.sessionId}`, cmd.sessionId);
646
+ return;
647
+ }
648
+ const eventStore = debugSession.eventStore;
649
+ client.subscriptions.cancel(cmd.sessionId);
650
+ const events = eventStore.events();
651
+ const computed = computeState(events);
652
+ const structure = buildNetStructure(debugSession);
653
+ this.send(client, {
654
+ type: "subscribed",
655
+ sessionId: cmd.sessionId,
656
+ netName: debugSession.netName,
657
+ dotDiagram: debugSession.dotDiagram,
658
+ structure,
659
+ currentMarking: mapToRecord(computed.marking),
660
+ enabledTransitions: computed.enabledTransitions,
661
+ inFlightTransitions: computed.inFlightTransitions,
662
+ eventCount: eventStore.eventCount(),
663
+ mode: cmd.mode
664
+ });
665
+ const fromIndex = cmd.fromIndex ?? 0;
666
+ if (cmd.mode === "live") {
667
+ this.subscribeLive(client, cmd.sessionId, debugSession, fromIndex);
668
+ } else {
669
+ this.subscribeReplay(client, cmd.sessionId, debugSession, fromIndex);
670
+ }
706
671
  }
707
- checkBreakpoints(sessionId, event) {
708
- const sub = this._sessionSubs.get(sessionId);
709
- if (!sub || sub.breakpoints.size === 0) return null;
710
- for (const bp of sub.breakpoints.values()) {
711
- if (!bp.enabled) continue;
712
- if (matchesBreakpoint(bp, event)) return bp;
672
+ subscribeLive(client, sessionId, debugSession, fromIndex) {
673
+ const eventStore = debugSession.eventStore;
674
+ let eventIndex = fromIndex;
675
+ const historicalEvents = eventStore.eventsFrom(fromIndex);
676
+ if (historicalEvents.length > 0) {
677
+ const filtered = historicalEvents.filter((e) => client.subscriptions.matchesFilter(sessionId, e)).map((e) => toEventInfo(e));
678
+ this.sendInBatches(client, sessionId, fromIndex, filtered);
679
+ eventIndex = fromIndex + historicalEvents.length;
713
680
  }
714
- return null;
681
+ const subscription = eventStore.subscribe((event) => {
682
+ if (!client.subscriptions.isPaused(sessionId) && client.subscriptions.matchesFilter(sessionId, event)) {
683
+ const eventInfo = toEventInfo(event);
684
+ const idx = eventIndex++;
685
+ const hitBreakpoint = client.subscriptions.checkBreakpoints(sessionId, event);
686
+ if (hitBreakpoint) {
687
+ client.subscriptions.setPaused(sessionId, true);
688
+ this.send(client, {
689
+ type: "breakpointHit",
690
+ sessionId,
691
+ breakpointId: hitBreakpoint.id,
692
+ event: eventInfo,
693
+ eventIndex: idx
694
+ });
695
+ }
696
+ this.send(client, { type: "event", sessionId, index: idx, event: eventInfo });
697
+ }
698
+ });
699
+ client.subscriptions.addSubscription(sessionId, subscription, eventIndex);
715
700
  }
716
- };
717
- function eventTypeToName(event) {
718
- const map = {
719
- "execution-started": "ExecutionStarted",
720
- "execution-completed": "ExecutionCompleted",
721
- "transition-enabled": "TransitionEnabled",
722
- "transition-clock-restarted": "TransitionClockRestarted",
723
- "transition-started": "TransitionStarted",
724
- "transition-completed": "TransitionCompleted",
725
- "transition-failed": "TransitionFailed",
726
- "transition-timed-out": "TransitionTimedOut",
727
- "action-timed-out": "ActionTimedOut",
728
- "token-added": "TokenAdded",
729
- "token-removed": "TokenRemoved",
730
- "marking-snapshot": "MarkingSnapshot",
731
- "log-message": "LogMessage"
732
- };
733
- return map[event.type] ?? event.type;
734
- }
735
- function extractTransitionName(event) {
736
- switch (event.type) {
737
- case "transition-enabled":
738
- case "transition-clock-restarted":
739
- case "transition-started":
740
- case "transition-completed":
741
- case "transition-failed":
742
- case "transition-timed-out":
743
- case "action-timed-out":
744
- case "log-message":
745
- return event.transitionName;
746
- default:
747
- return null;
701
+ subscribeReplay(client, sessionId, debugSession, fromIndex) {
702
+ const eventStore = debugSession.eventStore;
703
+ const events = eventStore.eventsFrom(fromIndex);
704
+ const converted = events.map((e) => toEventInfo(e));
705
+ this.sendInBatches(client, sessionId, fromIndex, converted);
706
+ const eventIndex = fromIndex + events.length;
707
+ client.subscriptions.addSubscription(sessionId, null, eventIndex);
708
+ client.subscriptions.setPaused(sessionId, true);
748
709
  }
749
- }
750
- function extractPlaceName(event) {
751
- switch (event.type) {
752
- case "token-added":
753
- case "token-removed":
754
- return event.placeName;
755
- default:
756
- return null;
710
+ handleUnsubscribe(client, cmd) {
711
+ client.subscriptions.cancel(cmd.sessionId);
712
+ this.send(client, { type: "unsubscribed", sessionId: cmd.sessionId });
757
713
  }
758
- }
759
- function matchesBreakpoint(bp, event) {
760
- switch (bp.type) {
761
- case "TRANSITION_ENABLED":
762
- return event.type === "transition-enabled" && (bp.target === null || bp.target === event.transitionName);
763
- case "TRANSITION_START":
764
- return event.type === "transition-started" && (bp.target === null || bp.target === event.transitionName);
765
- case "TRANSITION_COMPLETE":
766
- return event.type === "transition-completed" && (bp.target === null || bp.target === event.transitionName);
767
- case "TRANSITION_FAIL":
768
- return event.type === "transition-failed" && (bp.target === null || bp.target === event.transitionName);
769
- case "TOKEN_ADDED":
770
- return event.type === "token-added" && (bp.target === null || bp.target === event.placeName);
771
- case "TOKEN_REMOVED":
772
- return event.type === "token-removed" && (bp.target === null || bp.target === event.placeName);
773
- default:
774
- return false;
714
+ handleSeek(client, cmd) {
715
+ const debugSession = this._sessionRegistry.getSession(cmd.sessionId);
716
+ if (!debugSession) {
717
+ this.sendError(client, "SESSION_NOT_FOUND", "Session not found", cmd.sessionId);
718
+ return;
719
+ }
720
+ const events = debugSession.eventStore.events();
721
+ const targetTs = new Date(cmd.timestamp).getTime();
722
+ let targetIndex = 0;
723
+ for (let i = 0; i < events.length; i++) {
724
+ if (events[i].timestamp >= targetTs) {
725
+ targetIndex = i;
726
+ break;
727
+ }
728
+ targetIndex = i + 1;
729
+ }
730
+ client.subscriptions.setEventIndex(cmd.sessionId, targetIndex);
731
+ const computed = client.subscriptions.computeStateAt(cmd.sessionId, events, targetIndex);
732
+ this.send(client, {
733
+ type: "markingSnapshot",
734
+ sessionId: cmd.sessionId,
735
+ marking: mapToRecord(computed.marking),
736
+ enabledTransitions: computed.enabledTransitions,
737
+ inFlightTransitions: computed.inFlightTransitions
738
+ });
775
739
  }
776
- }
777
-
778
- // src/debug/debug-event-store.ts
779
- var DEFAULT_MAX_EVENTS = 1e4;
780
- var DebugEventStore = class {
781
- _events = [];
782
- _subscribers = /* @__PURE__ */ new Set();
783
- _sessionId;
784
- _maxEvents;
785
- _eventCount = 0;
786
- _evictedCount = 0;
787
- constructor(sessionId, maxEvents = DEFAULT_MAX_EVENTS) {
788
- if (maxEvents <= 0) throw new Error(`maxEvents must be positive, got: ${maxEvents}`);
789
- this._sessionId = sessionId;
790
- this._maxEvents = maxEvents;
740
+ handlePlaybackSpeed(client, cmd) {
741
+ client.subscriptions.setSpeed(cmd.sessionId, cmd.speed);
742
+ this.send(client, {
743
+ type: "playbackStateChanged",
744
+ sessionId: cmd.sessionId,
745
+ paused: client.subscriptions.isPaused(cmd.sessionId),
746
+ speed: cmd.speed,
747
+ currentIndex: client.subscriptions.getEventIndex(cmd.sessionId)
748
+ });
791
749
  }
792
- get sessionId() {
793
- return this._sessionId;
750
+ handleSetFilter(client, cmd) {
751
+ client.subscriptions.setFilter(cmd.sessionId, cmd.filter);
752
+ this.send(client, { type: "filterApplied", sessionId: cmd.sessionId, filter: cmd.filter });
794
753
  }
795
- get maxEvents() {
796
- return this._maxEvents;
754
+ handlePause(client, cmd) {
755
+ client.subscriptions.setPaused(cmd.sessionId, true);
756
+ this.send(client, {
757
+ type: "playbackStateChanged",
758
+ sessionId: cmd.sessionId,
759
+ paused: true,
760
+ speed: client.subscriptions.getSpeed(cmd.sessionId),
761
+ currentIndex: client.subscriptions.getEventIndex(cmd.sessionId)
762
+ });
797
763
  }
798
- /** Total events appended (including evicted). */
799
- eventCount() {
800
- return this._eventCount;
764
+ handleResume(client, cmd) {
765
+ client.subscriptions.setPaused(cmd.sessionId, false);
766
+ this.send(client, {
767
+ type: "playbackStateChanged",
768
+ sessionId: cmd.sessionId,
769
+ paused: false,
770
+ speed: client.subscriptions.getSpeed(cmd.sessionId),
771
+ currentIndex: client.subscriptions.getEventIndex(cmd.sessionId)
772
+ });
801
773
  }
802
- /** Number of events evicted from the store. */
803
- evictedCount() {
804
- return this._evictedCount;
774
+ handleStepForward(client, cmd) {
775
+ const debugSession = this._sessionRegistry.getSession(cmd.sessionId);
776
+ if (!debugSession) {
777
+ this.sendError(client, "SESSION_NOT_FOUND", `Session not found: ${cmd.sessionId}`, cmd.sessionId);
778
+ return;
779
+ }
780
+ const events = debugSession.eventStore.events();
781
+ const currentIndex = client.subscriptions.getEventIndex(cmd.sessionId);
782
+ if (currentIndex < events.length) {
783
+ const event = events[currentIndex];
784
+ this.send(client, {
785
+ type: "event",
786
+ sessionId: cmd.sessionId,
787
+ index: currentIndex,
788
+ event: toEventInfo(event)
789
+ });
790
+ client.subscriptions.setEventIndex(cmd.sessionId, currentIndex + 1);
791
+ }
805
792
  }
806
- // ======================== EventStore Implementation ========================
807
- append(event) {
808
- this._events.push(event);
809
- this._eventCount++;
810
- while (this._events.length > this._maxEvents) {
811
- this._events.shift();
812
- this._evictedCount++;
793
+ handleStepBackward(client, cmd) {
794
+ const debugSession = this._sessionRegistry.getSession(cmd.sessionId);
795
+ if (!debugSession) {
796
+ this.sendError(client, "SESSION_NOT_FOUND", `Session not found: ${cmd.sessionId}`, cmd.sessionId);
797
+ return;
813
798
  }
814
- if (this._subscribers.size > 0) {
815
- const subscribers = [...this._subscribers];
816
- queueMicrotask(() => {
817
- for (const sub of subscribers) {
818
- try {
819
- sub(event);
820
- } catch (e) {
821
- console.warn("Subscriber threw exception during event broadcast", e);
822
- }
823
- }
799
+ let currentIndex = client.subscriptions.getEventIndex(cmd.sessionId);
800
+ if (currentIndex > 0) {
801
+ currentIndex--;
802
+ client.subscriptions.setEventIndex(cmd.sessionId, currentIndex);
803
+ const events = debugSession.eventStore.events();
804
+ const computed = client.subscriptions.computeStateAt(cmd.sessionId, events, currentIndex);
805
+ this.send(client, {
806
+ type: "markingSnapshot",
807
+ sessionId: cmd.sessionId,
808
+ marking: mapToRecord(computed.marking),
809
+ enabledTransitions: computed.enabledTransitions,
810
+ inFlightTransitions: computed.inFlightTransitions
824
811
  });
825
812
  }
826
813
  }
827
- events() {
828
- return this._events;
814
+ handleSetBreakpoint(client, cmd) {
815
+ client.subscriptions.addBreakpoint(cmd.sessionId, cmd.breakpoint);
816
+ this.send(client, { type: "breakpointSet", sessionId: cmd.sessionId, breakpoint: cmd.breakpoint });
829
817
  }
830
- isEnabled() {
831
- return true;
818
+ handleClearBreakpoint(client, cmd) {
819
+ client.subscriptions.removeBreakpoint(cmd.sessionId, cmd.breakpointId);
820
+ this.send(client, { type: "breakpointCleared", sessionId: cmd.sessionId, breakpointId: cmd.breakpointId });
832
821
  }
833
- size() {
834
- return this._events.length;
822
+ handleListBreakpoints(client, cmd) {
823
+ const breakpoints = client.subscriptions.getBreakpoints(cmd.sessionId);
824
+ this.send(client, { type: "breakpointList", sessionId: cmd.sessionId, breakpoints });
835
825
  }
836
- isEmpty() {
837
- return this._events.length === 0;
826
+ // ======================== Helper Methods ========================
827
+ send(client, response) {
828
+ client.sink(response);
838
829
  }
839
- // ======================== Live Tailing ========================
840
- /** Subscribe to receive events as they occur. */
841
- subscribe(listener) {
842
- this._subscribers.add(listener);
843
- return {
844
- cancel: () => {
845
- this._subscribers.delete(listener);
846
- },
847
- isActive: () => this._subscribers.has(listener)
848
- };
830
+ sendError(client, code, message, sessionId) {
831
+ this.send(client, { type: "error", code, message, sessionId });
849
832
  }
850
- /** Number of active subscribers. */
851
- subscriberCount() {
852
- return this._subscribers.size;
833
+ sendInBatches(client, sessionId, startIndex, events) {
834
+ if (events.length === 0) {
835
+ this.send(client, { type: "eventBatch", sessionId, startIndex, events: [], hasMore: false });
836
+ return;
837
+ }
838
+ for (let i = 0; i < events.length; i += BATCH_SIZE) {
839
+ const end = Math.min(i + BATCH_SIZE, events.length);
840
+ const chunk = events.slice(i, end);
841
+ const hasMore = end < events.length;
842
+ this.send(client, { type: "eventBatch", sessionId, startIndex: startIndex + i, events: chunk, hasMore });
843
+ }
853
844
  }
854
- // ======================== Historical Replay ========================
855
- /** Returns events starting from a specific index. */
856
- eventsFrom(fromIndex) {
857
- const adjustedSkip = Math.max(0, fromIndex - this._evictedCount);
858
- if (adjustedSkip <= 0) return this._events;
859
- return this._events.slice(adjustedSkip);
845
+ };
846
+ function computeState(events) {
847
+ const marking = /* @__PURE__ */ new Map();
848
+ const enabled = /* @__PURE__ */ new Set();
849
+ const inFlight = /* @__PURE__ */ new Set();
850
+ applyEvents(marking, enabled, inFlight, events);
851
+ return toImmutableState(marking, enabled, inFlight);
852
+ }
853
+ function applyEvents(marking, enabled, inFlight, events) {
854
+ for (const event of events) {
855
+ switch (event.type) {
856
+ case "token-added": {
857
+ let tokens = marking.get(event.placeName);
858
+ if (!tokens) {
859
+ tokens = [];
860
+ marking.set(event.placeName, tokens);
861
+ }
862
+ tokens.push(tokenInfo(event.token));
863
+ break;
864
+ }
865
+ case "token-removed": {
866
+ const tokens = marking.get(event.placeName);
867
+ if (tokens && tokens.length > 0) tokens.shift();
868
+ break;
869
+ }
870
+ case "marking-snapshot": {
871
+ marking.clear();
872
+ const converted = convertMarking(event.marking);
873
+ for (const [key, value] of Object.entries(converted)) {
874
+ marking.set(key, [...value]);
875
+ }
876
+ break;
877
+ }
878
+ case "transition-enabled":
879
+ enabled.add(event.transitionName);
880
+ break;
881
+ case "transition-started":
882
+ enabled.delete(event.transitionName);
883
+ inFlight.add(event.transitionName);
884
+ break;
885
+ case "transition-completed":
886
+ inFlight.delete(event.transitionName);
887
+ break;
888
+ case "transition-failed":
889
+ inFlight.delete(event.transitionName);
890
+ break;
891
+ case "transition-timed-out":
892
+ inFlight.delete(event.transitionName);
893
+ break;
894
+ case "action-timed-out":
895
+ inFlight.delete(event.transitionName);
896
+ break;
897
+ default:
898
+ break;
899
+ }
860
900
  }
861
- /** Returns all events since the specified timestamp. */
862
- eventsSince(from) {
863
- return this._events.filter((e) => e.timestamp >= from);
901
+ }
902
+ function toImmutableState(marking, enabled, inFlight) {
903
+ const resultMarking = /* @__PURE__ */ new Map();
904
+ for (const [key, value] of marking) {
905
+ resultMarking.set(key, [...value]);
864
906
  }
865
- /** Returns events within a time range. */
866
- eventsBetween(from, to) {
867
- return this._events.filter((e) => e.timestamp >= from && e.timestamp < to);
907
+ return {
908
+ marking: resultMarking,
909
+ enabledTransitions: [...enabled],
910
+ inFlightTransitions: [...inFlight]
911
+ };
912
+ }
913
+ function mapToRecord(map) {
914
+ const result = {};
915
+ for (const [key, value] of map) {
916
+ result[key] = value;
868
917
  }
869
- // ======================== Lifecycle ========================
870
- /** Close the store (no-op in JS, but matches Java interface). */
871
- close() {
872
- this._subscribers.clear();
918
+ return result;
919
+ }
920
+ var ClientState = class {
921
+ sink;
922
+ subscriptions = new SubscriptionState();
923
+ constructor(sink) {
924
+ this.sink = sink;
873
925
  }
874
926
  };
875
-
876
- // src/debug/place-analysis.ts
877
- var PlaceAnalysis = class _PlaceAnalysis {
878
- _data;
879
- constructor(data) {
880
- this._data = data;
881
- }
882
- get data() {
883
- return this._data;
884
- }
885
- isStart(placeName) {
886
- const info = this._data.get(placeName);
887
- return info != null && !info.hasIncoming;
927
+ var SubscriptionState = class {
928
+ _sessionSubs = /* @__PURE__ */ new Map();
929
+ addSubscription(sessionId, subscription, eventIndex) {
930
+ this._sessionSubs.set(sessionId, {
931
+ subscription,
932
+ eventIndex,
933
+ markingCache: new MarkingCache(),
934
+ breakpoints: /* @__PURE__ */ new Map(),
935
+ paused: false,
936
+ speed: 1,
937
+ filter: null
938
+ });
888
939
  }
889
- isEnd(placeName) {
890
- const info = this._data.get(placeName);
891
- return info != null && !info.hasOutgoing;
940
+ cancel(sessionId) {
941
+ const sub = this._sessionSubs.get(sessionId);
942
+ if (sub?.subscription) sub.subscription.cancel();
943
+ this._sessionSubs.delete(sessionId);
892
944
  }
893
- /** Build place analysis from a PetriNet. */
894
- static from(net) {
895
- const data = /* @__PURE__ */ new Map();
896
- function ensure(place) {
897
- let info = data.get(place.name);
898
- if (!info) {
899
- info = { tokenType: "unknown", hasIncoming: false, hasOutgoing: false };
900
- data.set(place.name, info);
901
- }
902
- return info;
903
- }
904
- for (const transition of net.transitions) {
905
- for (const input of transition.inputSpecs) {
906
- const info = ensure(input.place);
907
- info.hasOutgoing = true;
908
- }
909
- if (transition.outputSpec) {
910
- const outputPlaces = collectOutputPlaces(transition.outputSpec);
911
- for (const place of outputPlaces) {
912
- const info = ensure(place);
913
- info.hasIncoming = true;
914
- }
915
- }
916
- for (const inh of transition.inhibitors) {
917
- ensure(inh.place);
918
- }
919
- for (const read of transition.reads) {
920
- const info = ensure(read.place);
921
- info.hasOutgoing = true;
922
- }
923
- for (const reset of transition.resets) {
924
- ensure(reset.place);
925
- }
945
+ cancelAll() {
946
+ for (const sub of this._sessionSubs.values()) {
947
+ if (sub.subscription) sub.subscription.cancel();
926
948
  }
927
- return new _PlaceAnalysis(data);
949
+ this._sessionSubs.clear();
950
+ }
951
+ isPaused(sessionId) {
952
+ return this._sessionSubs.get(sessionId)?.paused ?? false;
928
953
  }
929
- };
930
- function collectOutputPlaces(out) {
931
- const spec = out;
932
- switch (spec.type) {
933
- case "place":
934
- return spec.place ? [spec.place] : [];
935
- case "and":
936
- case "xor":
937
- return (spec.children ?? []).flatMap((c) => collectOutputPlaces(c));
938
- case "timeout":
939
- return spec.child ? collectOutputPlaces(spec.child) : [];
940
- case "forward-input":
941
- return spec.to ? [spec.to] : [];
942
- default:
943
- return [];
954
+ setPaused(sessionId, paused) {
955
+ const sub = this._sessionSubs.get(sessionId);
956
+ if (sub) sub.paused = paused;
944
957
  }
945
- }
946
-
947
- // src/debug/debug-session-registry.ts
948
- var DebugSessionRegistry = class {
949
- _sessions = /* @__PURE__ */ new Map();
950
- _maxSessions;
951
- _eventStoreFactory;
952
- constructor(maxSessions = 50, eventStoreFactory) {
953
- this._maxSessions = maxSessions;
954
- this._eventStoreFactory = eventStoreFactory ?? ((id) => new DebugEventStore(id));
958
+ getSpeed(sessionId) {
959
+ return this._sessionSubs.get(sessionId)?.speed ?? 1;
955
960
  }
956
- /**
957
- * Registers a new debug session for the given Petri net.
958
- * Generates DOT diagram and extracts net structure.
959
- */
960
- register(sessionId, net) {
961
- const dotDiagram = dotExport(net);
962
- const places = PlaceAnalysis.from(net);
963
- const eventStore = this._eventStoreFactory(sessionId);
964
- const session = {
965
- sessionId,
966
- netName: net.name,
967
- dotDiagram,
968
- places,
969
- transitions: net.transitions,
970
- eventStore,
971
- startTime: Date.now(),
972
- active: true
973
- };
974
- this.evictIfNecessary();
975
- this._sessions.set(sessionId, session);
976
- return session;
961
+ setSpeed(sessionId, speed) {
962
+ const sub = this._sessionSubs.get(sessionId);
963
+ if (sub) sub.speed = speed;
977
964
  }
978
- /** Marks a session as completed (no longer active). */
979
- complete(sessionId) {
980
- const session = this._sessions.get(sessionId);
981
- if (session) {
982
- this._sessions.set(sessionId, { ...session, active: false });
983
- }
965
+ getEventIndex(sessionId) {
966
+ return this._sessionSubs.get(sessionId)?.eventIndex ?? 0;
984
967
  }
985
- /** Removes a session from the registry. */
986
- remove(sessionId) {
987
- const removed = this._sessions.get(sessionId);
988
- if (removed) {
989
- this._sessions.delete(sessionId);
990
- removed.eventStore.close();
991
- }
992
- return removed;
968
+ setEventIndex(sessionId, index) {
969
+ const sub = this._sessionSubs.get(sessionId);
970
+ if (sub) sub.eventIndex = index;
993
971
  }
994
- /** Returns a session by ID. */
995
- getSession(sessionId) {
996
- return this._sessions.get(sessionId);
972
+ computeStateAt(sessionId, events, targetIndex) {
973
+ const sub = this._sessionSubs.get(sessionId);
974
+ if (sub) return sub.markingCache.computeAt(events, targetIndex);
975
+ return computeState(events.slice(0, targetIndex));
997
976
  }
998
- /** Lists sessions, ordered by start time (most recent first). */
999
- listSessions(limit) {
1000
- return [...this._sessions.values()].sort((a, b) => b.startTime - a.startTime).slice(0, limit);
977
+ setFilter(sessionId, filter) {
978
+ const sub = this._sessionSubs.get(sessionId);
979
+ if (sub) sub.filter = filter;
1001
980
  }
1002
- /** Lists only active sessions. */
1003
- listActiveSessions(limit) {
1004
- return [...this._sessions.values()].filter((s) => s.active).sort((a, b) => b.startTime - a.startTime).slice(0, limit);
981
+ matchesFilter(sessionId, event) {
982
+ const sub = this._sessionSubs.get(sessionId);
983
+ if (!sub?.filter) return true;
984
+ const filter = sub.filter;
985
+ if (filter.eventTypes && filter.eventTypes.length > 0) {
986
+ const eventType = eventTypeToName(event);
987
+ if (!filter.eventTypes.includes(eventType)) return false;
988
+ }
989
+ if (filter.transitionNames && filter.transitionNames.length > 0) {
990
+ const name = extractTransitionName(event);
991
+ if (!name || !filter.transitionNames.includes(name)) return false;
992
+ }
993
+ if (filter.placeNames && filter.placeNames.length > 0) {
994
+ const name = extractPlaceName(event);
995
+ if (!name || !filter.placeNames.includes(name)) return false;
996
+ }
997
+ return true;
1005
998
  }
1006
- /** Total number of sessions. */
1007
- get size() {
1008
- return this._sessions.size;
999
+ addBreakpoint(sessionId, breakpoint) {
1000
+ const sub = this._sessionSubs.get(sessionId);
1001
+ if (sub) sub.breakpoints.set(breakpoint.id, breakpoint);
1009
1002
  }
1010
- /** Evicts oldest inactive sessions if at capacity. */
1011
- evictIfNecessary() {
1012
- if (this._sessions.size < this._maxSessions) return;
1013
- const candidates = [...this._sessions.values()].sort((a, b) => {
1014
- if (a.active !== b.active) return a.active ? 1 : -1;
1015
- return a.startTime - b.startTime;
1016
- });
1017
- for (const candidate of candidates) {
1018
- if (this._sessions.size < this._maxSessions) break;
1019
- const evicted = this._sessions.get(candidate.sessionId);
1020
- if (evicted) {
1021
- this._sessions.delete(candidate.sessionId);
1022
- evicted.eventStore.close();
1023
- }
1003
+ removeBreakpoint(sessionId, breakpointId) {
1004
+ const sub = this._sessionSubs.get(sessionId);
1005
+ if (sub) sub.breakpoints.delete(breakpointId);
1006
+ }
1007
+ getBreakpoints(sessionId) {
1008
+ const sub = this._sessionSubs.get(sessionId);
1009
+ return sub ? [...sub.breakpoints.values()] : [];
1010
+ }
1011
+ checkBreakpoints(sessionId, event) {
1012
+ const sub = this._sessionSubs.get(sessionId);
1013
+ if (!sub || sub.breakpoints.size === 0) return null;
1014
+ for (const bp of sub.breakpoints.values()) {
1015
+ if (!bp.enabled) continue;
1016
+ if (matchesBreakpoint(bp, event)) return bp;
1024
1017
  }
1018
+ return null;
1025
1019
  }
1026
1020
  };
1021
+ function eventTypeToName(event) {
1022
+ const map = {
1023
+ "execution-started": "ExecutionStarted",
1024
+ "execution-completed": "ExecutionCompleted",
1025
+ "transition-enabled": "TransitionEnabled",
1026
+ "transition-clock-restarted": "TransitionClockRestarted",
1027
+ "transition-started": "TransitionStarted",
1028
+ "transition-completed": "TransitionCompleted",
1029
+ "transition-failed": "TransitionFailed",
1030
+ "transition-timed-out": "TransitionTimedOut",
1031
+ "action-timed-out": "ActionTimedOut",
1032
+ "token-added": "TokenAdded",
1033
+ "token-removed": "TokenRemoved",
1034
+ "marking-snapshot": "MarkingSnapshot",
1035
+ "log-message": "LogMessage"
1036
+ };
1037
+ return map[event.type] ?? event.type;
1038
+ }
1039
+ function extractTransitionName(event) {
1040
+ switch (event.type) {
1041
+ case "transition-enabled":
1042
+ case "transition-clock-restarted":
1043
+ case "transition-started":
1044
+ case "transition-completed":
1045
+ case "transition-failed":
1046
+ case "transition-timed-out":
1047
+ case "action-timed-out":
1048
+ case "log-message":
1049
+ return event.transitionName;
1050
+ default:
1051
+ return null;
1052
+ }
1053
+ }
1054
+ function extractPlaceName(event) {
1055
+ switch (event.type) {
1056
+ case "token-added":
1057
+ case "token-removed":
1058
+ return event.placeName;
1059
+ default:
1060
+ return null;
1061
+ }
1062
+ }
1063
+ function matchesBreakpoint(bp, event) {
1064
+ switch (bp.type) {
1065
+ case "TRANSITION_ENABLED":
1066
+ return event.type === "transition-enabled" && (bp.target === null || bp.target === event.transitionName);
1067
+ case "TRANSITION_START":
1068
+ return event.type === "transition-started" && (bp.target === null || bp.target === event.transitionName);
1069
+ case "TRANSITION_COMPLETE":
1070
+ return event.type === "transition-completed" && (bp.target === null || bp.target === event.transitionName);
1071
+ case "TRANSITION_FAIL":
1072
+ return event.type === "transition-failed" && (bp.target === null || bp.target === event.transitionName);
1073
+ case "TOKEN_ADDED":
1074
+ return event.type === "token-added" && (bp.target === null || bp.target === event.placeName);
1075
+ case "TOKEN_REMOVED":
1076
+ return event.type === "token-removed" && (bp.target === null || bp.target === event.placeName);
1077
+ default:
1078
+ return false;
1079
+ }
1080
+ }
1027
1081
 
1028
1082
  // src/debug/debug-aware-event-store.ts
1029
1083
  var DebugAwareEventStore = class {
@@ -1058,6 +1112,249 @@ var DebugAwareEventStore = class {
1058
1112
  }
1059
1113
  };
1060
1114
 
1115
+ // src/debug/archive/session-archive.ts
1116
+ var CURRENT_VERSION = 1;
1117
+
1118
+ // src/debug/archive/file-session-archive-storage.ts
1119
+ import { mkdir, readdir, readFile, stat, writeFile } from "fs/promises";
1120
+ import { join, normalize, resolve } from "path";
1121
+ import { Writable } from "stream";
1122
+ var EXTENSION = ".archive.gz";
1123
+ var FileSessionArchiveStorage = class {
1124
+ _directory;
1125
+ constructor(directory) {
1126
+ this._directory = resolve(directory);
1127
+ }
1128
+ async storeStreaming(sessionId, writer) {
1129
+ const target = this.archivePath(sessionId);
1130
+ const dir = join(target, "..");
1131
+ await mkdir(dir, { recursive: true });
1132
+ const chunks = [];
1133
+ const writable = new Writable({
1134
+ write(chunk, _encoding, callback) {
1135
+ chunks.push(chunk);
1136
+ callback();
1137
+ }
1138
+ });
1139
+ await writer(writable);
1140
+ await writeFile(target, Buffer.concat(chunks));
1141
+ }
1142
+ async list(limit, prefix) {
1143
+ try {
1144
+ await stat(this._directory);
1145
+ } catch {
1146
+ return [];
1147
+ }
1148
+ const results = [];
1149
+ let prefixDirs;
1150
+ try {
1151
+ prefixDirs = await readdir(this._directory);
1152
+ } catch {
1153
+ return [];
1154
+ }
1155
+ for (const prefixDir of prefixDirs) {
1156
+ const prefixPath = join(this._directory, prefixDir);
1157
+ try {
1158
+ const s = await stat(prefixPath);
1159
+ if (!s.isDirectory()) continue;
1160
+ } catch {
1161
+ continue;
1162
+ }
1163
+ if (prefix && prefixDir.length === 1 && prefixDir[0] !== prefix[0]) continue;
1164
+ let files;
1165
+ try {
1166
+ files = await readdir(prefixPath);
1167
+ } catch {
1168
+ continue;
1169
+ }
1170
+ for (const file of files) {
1171
+ if (!file.endsWith(EXTENSION)) continue;
1172
+ const sessionId = file.slice(0, -EXTENSION.length);
1173
+ if (prefix && !sessionId.startsWith(prefix)) continue;
1174
+ const filePath = join(prefixPath, file);
1175
+ try {
1176
+ const fileStat = await stat(filePath);
1177
+ const relativePath = join(prefixDir, file);
1178
+ results.push({
1179
+ sessionId,
1180
+ key: relativePath,
1181
+ sizeBytes: fileStat.size,
1182
+ lastModified: fileStat.mtimeMs
1183
+ });
1184
+ } catch {
1185
+ }
1186
+ }
1187
+ }
1188
+ results.sort((a, b) => b.lastModified - a.lastModified);
1189
+ return results.slice(0, limit);
1190
+ }
1191
+ async retrieve(sessionId) {
1192
+ const path = this.archivePath(sessionId);
1193
+ try {
1194
+ return await readFile(path);
1195
+ } catch {
1196
+ throw new Error(`Archive not found for session: ${sessionId}`);
1197
+ }
1198
+ }
1199
+ isAvailable() {
1200
+ return true;
1201
+ }
1202
+ archivePath(sessionId) {
1203
+ if (!sessionId || !sessionId.trim()) {
1204
+ throw new Error(`Invalid session ID: ${sessionId}`);
1205
+ }
1206
+ const prefix = sessionId[0];
1207
+ const resolved = normalize(join(this._directory, prefix, sessionId + EXTENSION));
1208
+ if (!resolved.startsWith(this._directory)) {
1209
+ throw new Error(`Session ID escapes archive directory: ${sessionId}`);
1210
+ }
1211
+ return resolved;
1212
+ }
1213
+ };
1214
+
1215
+ // src/debug/archive/session-archive-writer.ts
1216
+ import { gzipSync } from "zlib";
1217
+ var SessionArchiveWriter = class {
1218
+ /**
1219
+ * Writes a complete session archive and returns the compressed bytes.
1220
+ */
1221
+ write(session) {
1222
+ const structure = buildNetStructure(session);
1223
+ const metadata = {
1224
+ version: CURRENT_VERSION,
1225
+ sessionId: session.sessionId,
1226
+ netName: session.netName,
1227
+ dotDiagram: session.dotDiagram,
1228
+ startTime: new Date(session.startTime).toISOString(),
1229
+ eventCount: session.eventStore.eventCount(),
1230
+ structure
1231
+ };
1232
+ const parts = [];
1233
+ const metaBytes = Buffer.from(JSON.stringify(metadata), "utf-8");
1234
+ const metaLen = Buffer.alloc(4);
1235
+ metaLen.writeUInt32BE(metaBytes.length);
1236
+ parts.push(metaLen, metaBytes);
1237
+ for (const event of session.eventStore) {
1238
+ const eventInfo = toEventInfo(event);
1239
+ const eventBytes = Buffer.from(JSON.stringify(eventInfo), "utf-8");
1240
+ const eventLen = Buffer.alloc(4);
1241
+ eventLen.writeUInt32BE(eventBytes.length);
1242
+ parts.push(eventLen, eventBytes);
1243
+ }
1244
+ const raw = Buffer.concat(parts);
1245
+ return gzipSync(raw);
1246
+ }
1247
+ };
1248
+
1249
+ // src/debug/archive/session-archive-reader.ts
1250
+ import { gunzipSync } from "zlib";
1251
+ var MAX_EVENT_SIZE = 10 * 1024 * 1024;
1252
+ var SessionArchiveReader = class {
1253
+ /** Reads only the metadata header from an archive. */
1254
+ readMetadata(compressed) {
1255
+ const data = gunzipSync(compressed);
1256
+ const metaLen = data.readUInt32BE(0);
1257
+ const metaJson = data.subarray(4, 4 + metaLen).toString("utf-8");
1258
+ const metadata = JSON.parse(metaJson);
1259
+ if (metadata.version !== CURRENT_VERSION) {
1260
+ throw new Error(`Unsupported archive version: ${metadata.version} (expected ${CURRENT_VERSION})`);
1261
+ }
1262
+ return metadata;
1263
+ }
1264
+ /** Reads the full archive: metadata + all events into a DebugEventStore. */
1265
+ readFull(compressed) {
1266
+ const data = gunzipSync(compressed);
1267
+ let offset = 0;
1268
+ const metaLen = data.readUInt32BE(offset);
1269
+ offset += 4;
1270
+ const metaJson = data.subarray(offset, offset + metaLen).toString("utf-8");
1271
+ offset += metaLen;
1272
+ const metadata = JSON.parse(metaJson);
1273
+ if (metadata.version !== CURRENT_VERSION) {
1274
+ throw new Error(`Unsupported archive version: ${metadata.version} (expected ${CURRENT_VERSION})`);
1275
+ }
1276
+ const eventStore = new DebugEventStore(metadata.sessionId, Number.MAX_SAFE_INTEGER);
1277
+ while (offset < data.length) {
1278
+ if (offset + 4 > data.length) break;
1279
+ const eventLen = data.readUInt32BE(offset);
1280
+ offset += 4;
1281
+ if (eventLen <= 0 || eventLen > MAX_EVENT_SIZE) {
1282
+ throw new Error(`Invalid event size: ${eventLen}`);
1283
+ }
1284
+ if (offset + eventLen > data.length) break;
1285
+ const eventJson = data.subarray(offset, offset + eventLen).toString("utf-8");
1286
+ offset += eventLen;
1287
+ const eventInfo = JSON.parse(eventJson);
1288
+ const netEvent = eventInfoToNetEvent(eventInfo);
1289
+ eventStore.append(netEvent);
1290
+ }
1291
+ return { metadata, eventStore };
1292
+ }
1293
+ };
1294
+ function eventInfoToNetEvent(info) {
1295
+ const timestamp = new Date(info.timestamp).getTime();
1296
+ const d = info.details;
1297
+ switch (info.type) {
1298
+ case "ExecutionStarted":
1299
+ return { type: "execution-started", timestamp, netName: d["netName"], executionId: d["executionId"] };
1300
+ case "ExecutionCompleted":
1301
+ return { type: "execution-completed", timestamp, netName: d["netName"], executionId: d["executionId"], totalDurationMs: d["totalDurationMs"] };
1302
+ case "TransitionEnabled":
1303
+ return { type: "transition-enabled", timestamp, transitionName: info.transitionName };
1304
+ case "TransitionClockRestarted":
1305
+ return { type: "transition-clock-restarted", timestamp, transitionName: info.transitionName };
1306
+ case "TransitionStarted": {
1307
+ const tokens = d["consumedTokens"].map((t) => infoToToken(t));
1308
+ return { type: "transition-started", timestamp, transitionName: info.transitionName, consumedTokens: tokens };
1309
+ }
1310
+ case "TransitionCompleted": {
1311
+ const tokens = d["producedTokens"].map((t) => infoToToken(t));
1312
+ return { type: "transition-completed", timestamp, transitionName: info.transitionName, producedTokens: tokens, durationMs: d["durationMs"] };
1313
+ }
1314
+ case "TransitionFailed":
1315
+ return { type: "transition-failed", timestamp, transitionName: info.transitionName, errorMessage: d["errorMessage"], exceptionType: d["exceptionType"] };
1316
+ case "TransitionTimedOut":
1317
+ return { type: "transition-timed-out", timestamp, transitionName: info.transitionName, deadlineMs: d["deadlineMs"], actualDurationMs: d["actualDurationMs"] };
1318
+ case "ActionTimedOut":
1319
+ return { type: "action-timed-out", timestamp, transitionName: info.transitionName, timeoutMs: d["timeoutMs"] };
1320
+ case "TokenAdded": {
1321
+ const t = d["token"];
1322
+ return { type: "token-added", timestamp, placeName: info.placeName, token: infoToToken(t) };
1323
+ }
1324
+ case "TokenRemoved": {
1325
+ const t = d["token"];
1326
+ return { type: "token-removed", timestamp, placeName: info.placeName, token: infoToToken(t) };
1327
+ }
1328
+ case "MarkingSnapshot": {
1329
+ const markingData = d["marking"];
1330
+ const marking = /* @__PURE__ */ new Map();
1331
+ for (const [place, tokens] of Object.entries(markingData)) {
1332
+ marking.set(place, tokens.map((t) => infoToToken(t)));
1333
+ }
1334
+ return { type: "marking-snapshot", timestamp, marking };
1335
+ }
1336
+ case "LogMessage":
1337
+ return {
1338
+ type: "log-message",
1339
+ timestamp,
1340
+ transitionName: info.transitionName,
1341
+ logger: d["loggerName"],
1342
+ level: d["level"],
1343
+ message: d["message"],
1344
+ error: d["throwable"] ?? null,
1345
+ errorMessage: d["throwableMessage"] ?? null
1346
+ };
1347
+ default:
1348
+ return { type: "transition-enabled", timestamp, transitionName: info.transitionName ?? "unknown" };
1349
+ }
1350
+ }
1351
+ function infoToToken(t) {
1352
+ return {
1353
+ value: t.value,
1354
+ createdAt: t.timestamp ? new Date(t.timestamp).getTime() : Date.now()
1355
+ };
1356
+ }
1357
+
1061
1358
  // src/debug/index.ts
1062
1359
  async function debugUiAssetPath() {
1063
1360
  const dynamicImport = Function("m", "return import(m)");
@@ -1067,14 +1364,19 @@ async function debugUiAssetPath() {
1067
1364
  return nodePath.join(thisDir, "..", "debug-ui");
1068
1365
  }
1069
1366
  export {
1367
+ CURRENT_VERSION,
1070
1368
  DEFAULT_MAX_EVENTS,
1071
1369
  DebugAwareEventStore,
1072
1370
  DebugEventStore,
1073
1371
  DebugProtocolHandler,
1074
1372
  DebugSessionRegistry,
1373
+ FileSessionArchiveStorage,
1075
1374
  MarkingCache,
1076
1375
  PlaceAnalysis,
1077
1376
  SNAPSHOT_INTERVAL,
1377
+ SessionArchiveReader,
1378
+ SessionArchiveWriter,
1379
+ buildNetStructure,
1078
1380
  compactTokenInfo,
1079
1381
  convertMarking,
1080
1382
  debugUiAssetPath,