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.
- package/dist/debug/index.d.ts +109 -4
- package/dist/debug/index.js +1212 -910
- package/dist/debug/index.js.map +1 -1
- package/package.json +1 -1
package/dist/debug/index.js
CHANGED
|
@@ -9,1021 +9,1075 @@ function eventFilterAll() {
|
|
|
9
9
|
return { eventTypes: null, transitionNames: null, placeNames: null };
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
// src/debug/
|
|
13
|
-
var
|
|
14
|
-
|
|
15
|
-
|
|
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
|
-
|
|
42
|
-
|
|
43
|
-
this._snapshots.length = 0;
|
|
18
|
+
get data() {
|
|
19
|
+
return this._data;
|
|
44
20
|
}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
|
|
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/
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
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
|
-
|
|
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
|
-
|
|
249
|
-
|
|
250
|
-
|
|
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
|
-
/**
|
|
253
|
-
|
|
254
|
-
|
|
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
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
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
|
-
|
|
309
|
-
|
|
310
|
-
|
|
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
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
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
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
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
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
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
|
-
|
|
390
|
-
|
|
391
|
-
|
|
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
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
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
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
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
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
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
|
-
|
|
420
|
-
|
|
421
|
-
this.
|
|
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
|
-
|
|
430
|
-
|
|
431
|
-
this.
|
|
282
|
+
/** Total number of sessions. */
|
|
283
|
+
get size() {
|
|
284
|
+
return this._sessions.size;
|
|
432
285
|
}
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
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
|
-
|
|
444
|
-
|
|
445
|
-
this.
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
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
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
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
|
-
|
|
460
|
-
|
|
461
|
-
|
|
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
|
-
|
|
473
|
-
|
|
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
|
-
|
|
479
|
-
if (
|
|
480
|
-
|
|
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
|
-
|
|
494
|
-
|
|
495
|
-
this.
|
|
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
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
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
|
|
382
|
+
function replayDelta(base, delta) {
|
|
526
383
|
const marking = /* @__PURE__ */ new Map();
|
|
527
|
-
const
|
|
528
|
-
|
|
529
|
-
|
|
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
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
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
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
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
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
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
|
|
582
|
-
const
|
|
583
|
-
|
|
584
|
-
|
|
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
|
-
|
|
588
|
-
|
|
589
|
-
|
|
536
|
+
id: null,
|
|
537
|
+
type,
|
|
538
|
+
value: fullValue,
|
|
539
|
+
timestamp: new Date(token.createdAt).toISOString()
|
|
590
540
|
};
|
|
591
541
|
}
|
|
592
|
-
function
|
|
593
|
-
const
|
|
594
|
-
const
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
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
|
|
552
|
+
function convertMarking(marking, compact = false) {
|
|
610
553
|
const result = {};
|
|
611
|
-
|
|
612
|
-
|
|
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
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
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
|
-
|
|
669
|
-
|
|
670
|
-
|
|
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
|
-
|
|
674
|
-
|
|
675
|
-
|
|
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
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
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
|
-
|
|
700
|
-
|
|
701
|
-
|
|
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
|
-
|
|
704
|
-
const
|
|
705
|
-
|
|
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
|
-
|
|
708
|
-
const
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
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
|
-
|
|
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
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
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
|
-
|
|
751
|
-
|
|
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
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
return
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
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
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
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
|
-
|
|
793
|
-
|
|
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
|
-
|
|
796
|
-
|
|
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
|
-
|
|
799
|
-
|
|
800
|
-
|
|
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
|
-
|
|
803
|
-
|
|
804
|
-
|
|
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
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
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
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
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
|
-
|
|
828
|
-
|
|
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
|
-
|
|
831
|
-
|
|
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
|
-
|
|
834
|
-
|
|
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
|
-
|
|
837
|
-
|
|
826
|
+
// ======================== Helper Methods ========================
|
|
827
|
+
send(client, response) {
|
|
828
|
+
client.sink(response);
|
|
838
829
|
}
|
|
839
|
-
|
|
840
|
-
|
|
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
|
-
|
|
851
|
-
|
|
852
|
-
|
|
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
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
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
|
-
|
|
862
|
-
|
|
863
|
-
|
|
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
|
-
|
|
866
|
-
|
|
867
|
-
|
|
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
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
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
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
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
|
-
|
|
890
|
-
const
|
|
891
|
-
|
|
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
|
-
|
|
894
|
-
|
|
895
|
-
|
|
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
|
-
|
|
949
|
+
this._sessionSubs.clear();
|
|
950
|
+
}
|
|
951
|
+
isPaused(sessionId) {
|
|
952
|
+
return this._sessionSubs.get(sessionId)?.paused ?? false;
|
|
928
953
|
}
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
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
|
-
|
|
958
|
-
|
|
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
|
-
|
|
979
|
-
|
|
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
|
-
|
|
986
|
-
|
|
987
|
-
|
|
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
|
-
|
|
995
|
-
|
|
996
|
-
return
|
|
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
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
977
|
+
setFilter(sessionId, filter) {
|
|
978
|
+
const sub = this._sessionSubs.get(sessionId);
|
|
979
|
+
if (sub) sub.filter = filter;
|
|
1001
980
|
}
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
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
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
999
|
+
addBreakpoint(sessionId, breakpoint) {
|
|
1000
|
+
const sub = this._sessionSubs.get(sessionId);
|
|
1001
|
+
if (sub) sub.breakpoints.set(breakpoint.id, breakpoint);
|
|
1009
1002
|
}
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
if (
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
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,
|