libpetri 1.5.1 → 1.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/debug/index.d.ts +179 -14
- package/dist/debug/index.js +186 -40
- package/dist/debug/index.js.map +1 -1
- package/dist/doclet/index.d.ts +1 -1
- package/dist/{event-store-Y8q_wapJ.d.ts → event-store-DePCZb33.d.ts} +1 -1
- package/dist/export/index.d.ts +1 -1
- package/dist/index.d.ts +4 -4
- package/dist/index.js +28 -6
- package/dist/index.js.map +1 -1
- package/dist/{petri-net-C3Jy5HCt.d.ts → petri-net-DrTpTRNy.d.ts} +22 -4
- package/dist/verification/index.d.ts +1 -1
- package/package.json +1 -1
package/dist/debug/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Writable } from 'node:stream';
|
|
2
|
-
import { a as PetriNet, b as Transition, T as Token } from '../petri-net-
|
|
3
|
-
import { E as EventStore, N as NetEvent } from '../event-store-
|
|
2
|
+
import { a as PetriNet, b as Transition, T as Token } from '../petri-net-DrTpTRNy.js';
|
|
3
|
+
import { E as EventStore, N as NetEvent } from '../event-store-DePCZb33.js';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Commands sent from debug UI client to server via WebSocket.
|
|
@@ -34,6 +34,8 @@ type DebugCommand = {
|
|
|
34
34
|
readonly type: 'listSessions';
|
|
35
35
|
readonly limit?: number;
|
|
36
36
|
readonly activeOnly?: boolean;
|
|
37
|
+
/** Optional tag filter (AND semantics). Empty or missing matches all. (libpetri 1.6.0+) */
|
|
38
|
+
readonly tagFilter?: Readonly<Record<string, string>>;
|
|
37
39
|
} | {
|
|
38
40
|
readonly type: 'seek';
|
|
39
41
|
readonly sessionId: string;
|
|
@@ -94,6 +96,12 @@ interface SessionSummary {
|
|
|
94
96
|
readonly startTime: string;
|
|
95
97
|
readonly active: boolean;
|
|
96
98
|
readonly eventCount: number;
|
|
99
|
+
/** User-defined session tags. Empty object if none. (libpetri 1.6.0+) */
|
|
100
|
+
readonly tags?: Readonly<Record<string, string>>;
|
|
101
|
+
/** ISO-8601 end time, present only for completed sessions. (libpetri 1.6.0+) */
|
|
102
|
+
readonly endTime?: string;
|
|
103
|
+
/** Session duration in milliseconds, present only for completed sessions. (libpetri 1.6.0+) */
|
|
104
|
+
readonly durationMs?: number;
|
|
97
105
|
}
|
|
98
106
|
interface TokenInfo {
|
|
99
107
|
readonly id: string | null;
|
|
@@ -320,6 +328,15 @@ interface DebugSession {
|
|
|
320
328
|
readonly startTime: number;
|
|
321
329
|
readonly active: boolean;
|
|
322
330
|
readonly importedStructure: NetStructure | null;
|
|
331
|
+
/** Stamped on first `complete()`. Undefined while the session is active. (libpetri 1.6.0+) */
|
|
332
|
+
readonly endTime?: number;
|
|
333
|
+
/**
|
|
334
|
+
* Per-session tag storage, mutated in place by the registry. The reference is
|
|
335
|
+
* readonly but the underlying object is not — prefer
|
|
336
|
+
* {@link DebugSessionRegistry.tag}/{@link DebugSessionRegistry.tagsFor}
|
|
337
|
+
* over direct access.
|
|
338
|
+
*/
|
|
339
|
+
readonly tags: Record<string, string>;
|
|
323
340
|
}
|
|
324
341
|
/** Builds the net structure from a session's stored place and transition info. */
|
|
325
342
|
declare function buildNetStructure(session: DebugSession): NetStructure;
|
|
@@ -333,26 +350,54 @@ declare class DebugSessionRegistry {
|
|
|
333
350
|
/**
|
|
334
351
|
* Registers a new debug session for the given Petri net.
|
|
335
352
|
* Generates DOT diagram and extracts net structure.
|
|
353
|
+
*
|
|
354
|
+
* @param sessionId unique session id
|
|
355
|
+
* @param net the Petri net being executed
|
|
356
|
+
* @param tags optional user-defined tags (libpetri 1.6.0+) — e.g. `{channel: 'voice'}`
|
|
336
357
|
*/
|
|
337
|
-
register(sessionId: string, net: PetriNet): DebugSession;
|
|
358
|
+
register(sessionId: string, net: PetriNet, tags?: Readonly<Record<string, string>>): DebugSession;
|
|
338
359
|
/**
|
|
339
360
|
* Marks a session as completed (no longer active) and notifies completion listeners.
|
|
361
|
+
*
|
|
362
|
+
* <p>Stamps `endTime = Date.now()` on the first completion. Idempotent: subsequent
|
|
363
|
+
* calls preserve the existing endTime. (libpetri 1.6.0+)
|
|
340
364
|
*/
|
|
341
365
|
complete(sessionId: string): void;
|
|
342
|
-
/** Removes a session from the registry. */
|
|
366
|
+
/** Removes a session from the registry. Tags die with the session. */
|
|
343
367
|
remove(sessionId: string): DebugSession | undefined;
|
|
344
368
|
/** Returns a session by ID. */
|
|
345
369
|
getSession(sessionId: string): DebugSession | undefined;
|
|
370
|
+
/**
|
|
371
|
+
* Sets or overwrites a single tag on a session. (libpetri 1.6.0+)
|
|
372
|
+
*
|
|
373
|
+
* Tags accumulate until the session is removed. Setting a key that already
|
|
374
|
+
* exists replaces its value.
|
|
375
|
+
*
|
|
376
|
+
* If `sessionId` is not a currently-registered session the call is a no-op.
|
|
377
|
+
* A tag write that races with {@link remove} is harmless — the write lands on
|
|
378
|
+
* the now-orphaned session object and is garbage collected along with it.
|
|
379
|
+
*/
|
|
380
|
+
tag(sessionId: string, key: string, value: string): void;
|
|
381
|
+
/**
|
|
382
|
+
* Returns a snapshot of the tags attached to a session. Returns an empty
|
|
383
|
+
* object if the session has no tags or does not exist. (libpetri 1.6.0+)
|
|
384
|
+
*/
|
|
385
|
+
tagsFor(sessionId: string): Readonly<Record<string, string>>;
|
|
346
386
|
/** Lists sessions, ordered by start time (most recent first). */
|
|
347
|
-
listSessions(limit: number): readonly DebugSession[];
|
|
387
|
+
listSessions(limit: number, tagFilter?: Readonly<Record<string, string>>): readonly DebugSession[];
|
|
348
388
|
/** Lists only active sessions. */
|
|
349
|
-
listActiveSessions(limit: number): readonly DebugSession[];
|
|
389
|
+
listActiveSessions(limit: number, tagFilter?: Readonly<Record<string, string>>): readonly DebugSession[];
|
|
350
390
|
/** Total number of sessions. */
|
|
351
391
|
get size(): number;
|
|
352
392
|
/**
|
|
353
393
|
* Registers an imported (archived) session as an inactive, read-only session.
|
|
394
|
+
*
|
|
395
|
+
* @param endTime when the original session ended (libpetri 1.6.0+)
|
|
396
|
+
* @param tags user-defined tags attached to the imported session (libpetri 1.6.0+)
|
|
354
397
|
*/
|
|
355
|
-
registerImported(sessionId: string, netName: string, dotDiagram: string, structure: NetStructure, eventStore: DebugEventStore, startTime: number): DebugSession;
|
|
398
|
+
registerImported(sessionId: string, netName: string, dotDiagram: string, structure: NetStructure, eventStore: DebugEventStore, startTime: number, endTime?: number, tags?: Readonly<Record<string, string>>): DebugSession;
|
|
399
|
+
/** AND-match: all filter entries must exactly match the session's tags. */
|
|
400
|
+
private matchesTagFilter;
|
|
356
401
|
/** Notifies all completion listeners. Exceptions are caught and logged. */
|
|
357
402
|
private notifyCompletionListeners;
|
|
358
403
|
/** Evicts oldest inactive sessions if at capacity. */
|
|
@@ -388,6 +433,7 @@ declare class DebugProtocolHandler {
|
|
|
388
433
|
/** Handles a command from a connected client. */
|
|
389
434
|
handleCommand(clientId: string, command: DebugCommand): void;
|
|
390
435
|
private handleListSessions;
|
|
436
|
+
private toProtocolSummary;
|
|
391
437
|
private handleSubscribe;
|
|
392
438
|
private subscribeLive;
|
|
393
439
|
private subscribeReplay;
|
|
@@ -468,19 +514,101 @@ declare class DebugAwareEventStore implements EventStore {
|
|
|
468
514
|
|
|
469
515
|
/**
|
|
470
516
|
* Metadata header for a session archive file.
|
|
517
|
+
*
|
|
518
|
+
* Discriminated union across format versions so callers can pattern-match on
|
|
519
|
+
* `archive.version` to access v2-only fields with type narrowing:
|
|
520
|
+
*
|
|
521
|
+
* ```ts
|
|
522
|
+
* const archive = reader.readMetadata(bytes);
|
|
523
|
+
* if (archive.version === 2) {
|
|
524
|
+
* console.log(archive.tags, archive.endTime, archive.metadata.hasErrors);
|
|
525
|
+
* }
|
|
526
|
+
* ```
|
|
527
|
+
*
|
|
528
|
+
* ## Version contract
|
|
529
|
+
*
|
|
530
|
+
* - **v1** (libpetri 1.5.x–1.6.x): original format. Header carries `sessionId`,
|
|
531
|
+
* `netName`, `dotDiagram`, `startTime`, `eventCount`, and net `structure`.
|
|
532
|
+
* - **v2** (libpetri 1.7.0+): adds `endTime`, user-defined `tags`, and pre-computed
|
|
533
|
+
* {@link SessionMetadata} (event-type histogram, first/last event timestamps,
|
|
534
|
+
* hasErrors). Events inside v2 archives are serialized the same way as in v1 —
|
|
535
|
+
* only the header is enriched.
|
|
536
|
+
*
|
|
537
|
+
* The {@link SessionArchiveReader} peeks the `version` field via a lenient JSON
|
|
538
|
+
* parse and dispatches to the correct concrete type. Both v1 and v2 archives
|
|
539
|
+
* remain readable and may coexist in the same storage bucket.
|
|
471
540
|
*/
|
|
472
541
|
|
|
473
|
-
|
|
474
|
-
|
|
542
|
+
/** Common fields shared by v1 and v2 archive headers. */
|
|
543
|
+
interface SessionArchiveBase {
|
|
475
544
|
readonly sessionId: string;
|
|
476
545
|
readonly netName: string;
|
|
477
546
|
readonly dotDiagram: string;
|
|
547
|
+
/** ISO-8601 instant the session started. */
|
|
478
548
|
readonly startTime: string;
|
|
479
549
|
readonly eventCount: number;
|
|
480
550
|
readonly structure: NetStructure;
|
|
481
551
|
}
|
|
482
|
-
/**
|
|
483
|
-
|
|
552
|
+
/** Legacy v1 archive header (libpetri 1.5.x–1.6.x). */
|
|
553
|
+
interface SessionArchiveV1 extends SessionArchiveBase {
|
|
554
|
+
readonly version: 1;
|
|
555
|
+
}
|
|
556
|
+
/**
|
|
557
|
+
* v2 archive header (libpetri 1.7.0+). Adds end time, tags, and pre-computed
|
|
558
|
+
* metadata so listing tools and samplers can filter/aggregate without scanning
|
|
559
|
+
* the event body.
|
|
560
|
+
*/
|
|
561
|
+
interface SessionArchiveV2 extends SessionArchiveBase {
|
|
562
|
+
readonly version: 2;
|
|
563
|
+
/** ISO-8601 instant the session ended. Undefined for sessions archived while still active. */
|
|
564
|
+
readonly endTime?: string;
|
|
565
|
+
/** User-defined session tags (e.g., `{channel: "voice"}`). Always present; may be empty. */
|
|
566
|
+
readonly tags: Readonly<Record<string, string>>;
|
|
567
|
+
/** Pre-computed aggregate stats. Always present; `emptyMetadata()` for no-event sessions. */
|
|
568
|
+
readonly metadata: SessionMetadata;
|
|
569
|
+
}
|
|
570
|
+
/**
|
|
571
|
+
* Discriminated union of all supported archive header versions.
|
|
572
|
+
*
|
|
573
|
+
* Type-narrowing example:
|
|
574
|
+
* ```ts
|
|
575
|
+
* if (archive.version === 2) {
|
|
576
|
+
* // TS knows archive is SessionArchiveV2 here — tags / endTime / metadata typed.
|
|
577
|
+
* }
|
|
578
|
+
* ```
|
|
579
|
+
*/
|
|
580
|
+
type SessionArchive = SessionArchiveV1 | SessionArchiveV2;
|
|
581
|
+
/** Version written by default by {@link SessionArchiveWriter.write} (latest supported). */
|
|
582
|
+
declare const CURRENT_VERSION = 2;
|
|
583
|
+
/**
|
|
584
|
+
* Pre-computed aggregate statistics attached to a v2 session archive header.
|
|
585
|
+
*
|
|
586
|
+
* Computed once during archive write by a single-pass scan of the event store.
|
|
587
|
+
* Readers can answer `hasErrors`, histogram, and first/last timestamp queries
|
|
588
|
+
* without iterating the event stream — enabling cheap triage, sampling, and
|
|
589
|
+
* listing of many archives.
|
|
590
|
+
*
|
|
591
|
+
* For v1 archives (no pre-computed metadata), callers can recompute on-demand
|
|
592
|
+
* via {@link computeMetadata}.
|
|
593
|
+
*/
|
|
594
|
+
interface SessionMetadata {
|
|
595
|
+
/**
|
|
596
|
+
* Count of events per `NetEvent` subtype name (PascalCase, matching the
|
|
597
|
+
* wire format used by `NetEventInfo.type` — e.g. `TransitionStarted -> 412`).
|
|
598
|
+
* Keys are stored in alphabetical order for deterministic JSON output.
|
|
599
|
+
*/
|
|
600
|
+
readonly eventTypeHistogram: Readonly<Record<string, number>>;
|
|
601
|
+
/** ISO-8601 timestamp of the oldest event, or undefined if the session had no events. */
|
|
602
|
+
readonly firstEventTime?: string;
|
|
603
|
+
/** ISO-8601 timestamp of the newest event, or undefined if the session had no events. */
|
|
604
|
+
readonly lastEventTime?: string;
|
|
605
|
+
/**
|
|
606
|
+
* True if the session contains at least one error-signal event
|
|
607
|
+
* (`TransitionFailed`, `TransitionTimedOut`, `ActionTimedOut`, or
|
|
608
|
+
* a `LogMessage` at level `ERROR`).
|
|
609
|
+
*/
|
|
610
|
+
readonly hasErrors: boolean;
|
|
611
|
+
}
|
|
484
612
|
|
|
485
613
|
/**
|
|
486
614
|
* File-system backed storage for session archives.
|
|
@@ -501,21 +629,58 @@ declare class FileSessionArchiveStorage implements SessionArchiveStorage {
|
|
|
501
629
|
* Writes a debug session to a length-prefixed binary archive format.
|
|
502
630
|
*
|
|
503
631
|
* Format (inside gzip):
|
|
504
|
-
* [4 bytes: metadata JSON length][N bytes: metadata JSON]
|
|
505
|
-
* [4 bytes: event JSON length][N bytes: event JSON]
|
|
632
|
+
* `[4 bytes: metadata JSON length][N bytes: metadata JSON]`
|
|
633
|
+
* `[4 bytes: event JSON length][N bytes: event JSON]`
|
|
506
634
|
* ...
|
|
507
635
|
* (EOF terminates the stream)
|
|
636
|
+
*
|
|
637
|
+
* ## Format selection
|
|
638
|
+
*
|
|
639
|
+
* `write()` defaults to {@link CURRENT_VERSION} (v2 as of libpetri 1.7.0).
|
|
640
|
+
* Callers that need to emit legacy archives — compatibility tests or readers
|
|
641
|
+
* pinned to libpetri ≤ 1.6.1 — can call `writeV1()`. v2 archives cost one extra
|
|
642
|
+
* pass over the event store to pre-compute {@link SessionMetadata}; the savings
|
|
643
|
+
* at read time (no event scan needed for hasErrors / histogram queries) pay it
|
|
644
|
+
* back the first time a caller lists or samples a bucket of sessions.
|
|
508
645
|
*/
|
|
509
646
|
|
|
510
647
|
declare class SessionArchiveWriter {
|
|
511
648
|
/**
|
|
512
|
-
* Writes a complete session archive
|
|
649
|
+
* Writes a complete session archive in the current format (v2 as of 1.7.0)
|
|
650
|
+
* and returns the compressed bytes.
|
|
513
651
|
*/
|
|
514
652
|
write(session: DebugSession): Buffer;
|
|
653
|
+
/**
|
|
654
|
+
* Writes a session in the legacy v1 format. Use only for compatibility
|
|
655
|
+
* testing or when producing archives for consumers pinned to libpetri ≤ 1.6.1.
|
|
656
|
+
*/
|
|
657
|
+
writeV1(session: DebugSession): Buffer;
|
|
658
|
+
/**
|
|
659
|
+
* Writes a session in the v2 format — richer header with `endTime`, `tags`,
|
|
660
|
+
* and pre-computed {@link SessionMetadata}.
|
|
661
|
+
*
|
|
662
|
+
* Two passes over the event store: one to compute metadata, one to serialize
|
|
663
|
+
* events. `DebugEventStore` stores events in a plain readonly array and its
|
|
664
|
+
* `[Symbol.iterator]()` returns a fresh array iterator each call, so both
|
|
665
|
+
* passes walk the same sequence from the start.
|
|
666
|
+
*/
|
|
667
|
+
writeV2(session: DebugSession): Buffer;
|
|
668
|
+
/**
|
|
669
|
+
* Shared framing logic: length-prefixed header JSON, then length-prefixed
|
|
670
|
+
* event JSON, then gzip. Both v1 and v2 archives use the identical event
|
|
671
|
+
* wire format, so the body loop is version-agnostic.
|
|
672
|
+
*/
|
|
673
|
+
private writeFramed;
|
|
515
674
|
}
|
|
516
675
|
|
|
517
676
|
/**
|
|
518
677
|
* Reads session archives from length-prefixed binary format.
|
|
678
|
+
*
|
|
679
|
+
* Handles both v1 (libpetri 1.5.x–1.6.x) and v2 (libpetri 1.7.0+) archives via
|
|
680
|
+
* a lenient "version probe": parse the header JSON once, switch on the
|
|
681
|
+
* `version` field, narrow to the correct concrete type, and normalize missing
|
|
682
|
+
* optional fields to their defaults. Events inside the body use the same wire
|
|
683
|
+
* format across versions, so the event read path is shared.
|
|
519
684
|
*/
|
|
520
685
|
|
|
521
686
|
interface ImportedSession {
|
package/dist/debug/index.js
CHANGED
|
@@ -118,7 +118,11 @@ var DebugEventStore = class {
|
|
|
118
118
|
};
|
|
119
119
|
|
|
120
120
|
// src/debug/archive/session-archive.ts
|
|
121
|
-
var CURRENT_VERSION =
|
|
121
|
+
var CURRENT_VERSION = 2;
|
|
122
|
+
var MIN_SUPPORTED_VERSION = 1;
|
|
123
|
+
function emptyMetadata() {
|
|
124
|
+
return { eventTypeHistogram: {}, hasErrors: false };
|
|
125
|
+
}
|
|
122
126
|
|
|
123
127
|
// src/debug/archive/session-archive-reader.ts
|
|
124
128
|
var MAX_EVENT_SIZE = 10 * 1024 * 1024;
|
|
@@ -128,11 +132,7 @@ var SessionArchiveReader = class {
|
|
|
128
132
|
const data = gunzipSync(compressed);
|
|
129
133
|
const metaLen = data.readUInt32BE(0);
|
|
130
134
|
const metaJson = data.subarray(4, 4 + metaLen).toString("utf-8");
|
|
131
|
-
|
|
132
|
-
if (metadata.version !== CURRENT_VERSION) {
|
|
133
|
-
throw new Error(`Unsupported archive version: ${metadata.version} (expected ${CURRENT_VERSION})`);
|
|
134
|
-
}
|
|
135
|
-
return metadata;
|
|
135
|
+
return parseHeader(metaJson);
|
|
136
136
|
}
|
|
137
137
|
/** Reads the full archive: metadata + all events into a DebugEventStore. */
|
|
138
138
|
readFull(compressed) {
|
|
@@ -142,10 +142,7 @@ var SessionArchiveReader = class {
|
|
|
142
142
|
offset += 4;
|
|
143
143
|
const metaJson = data.subarray(offset, offset + metaLen).toString("utf-8");
|
|
144
144
|
offset += metaLen;
|
|
145
|
-
const metadata =
|
|
146
|
-
if (metadata.version !== CURRENT_VERSION) {
|
|
147
|
-
throw new Error(`Unsupported archive version: ${metadata.version} (expected ${CURRENT_VERSION})`);
|
|
148
|
-
}
|
|
145
|
+
const metadata = parseHeader(metaJson);
|
|
149
146
|
const eventStore = new DebugEventStore(metadata.sessionId, Number.MAX_SAFE_INTEGER);
|
|
150
147
|
while (offset < data.length) {
|
|
151
148
|
if (offset + 4 > data.length) break;
|
|
@@ -164,6 +161,25 @@ var SessionArchiveReader = class {
|
|
|
164
161
|
return { metadata, eventStore };
|
|
165
162
|
}
|
|
166
163
|
};
|
|
164
|
+
function parseHeader(metaJson) {
|
|
165
|
+
const raw = JSON.parse(metaJson);
|
|
166
|
+
switch (raw.version) {
|
|
167
|
+
case 1:
|
|
168
|
+
return raw;
|
|
169
|
+
case 2: {
|
|
170
|
+
const v2 = raw;
|
|
171
|
+
return {
|
|
172
|
+
...v2,
|
|
173
|
+
tags: v2.tags ?? {},
|
|
174
|
+
metadata: v2.metadata ?? emptyMetadata()
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
default:
|
|
178
|
+
throw new Error(
|
|
179
|
+
`Unsupported archive version: ${raw.version} (reader supports ${MIN_SUPPORTED_VERSION}..${CURRENT_VERSION})`
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
167
183
|
function eventInfoToNetEvent(info) {
|
|
168
184
|
const timestamp = new Date(info.timestamp).getTime();
|
|
169
185
|
const d = info.details;
|
|
@@ -341,8 +357,12 @@ var DebugSessionRegistry = class {
|
|
|
341
357
|
/**
|
|
342
358
|
* Registers a new debug session for the given Petri net.
|
|
343
359
|
* Generates DOT diagram and extracts net structure.
|
|
360
|
+
*
|
|
361
|
+
* @param sessionId unique session id
|
|
362
|
+
* @param net the Petri net being executed
|
|
363
|
+
* @param tags optional user-defined tags (libpetri 1.6.0+) — e.g. `{channel: 'voice'}`
|
|
344
364
|
*/
|
|
345
|
-
register(sessionId, net) {
|
|
365
|
+
register(sessionId, net, tags) {
|
|
346
366
|
const dotDiagram = dotExport(net);
|
|
347
367
|
const places = PlaceAnalysis.from(net);
|
|
348
368
|
const eventStore = this._eventStoreFactory(sessionId);
|
|
@@ -355,7 +375,8 @@ var DebugSessionRegistry = class {
|
|
|
355
375
|
eventStore,
|
|
356
376
|
startTime: Date.now(),
|
|
357
377
|
active: true,
|
|
358
|
-
importedStructure: null
|
|
378
|
+
importedStructure: null,
|
|
379
|
+
tags: tags ? { ...tags } : {}
|
|
359
380
|
};
|
|
360
381
|
this.evictIfNecessary();
|
|
361
382
|
this._sessions.set(sessionId, session);
|
|
@@ -363,16 +384,20 @@ var DebugSessionRegistry = class {
|
|
|
363
384
|
}
|
|
364
385
|
/**
|
|
365
386
|
* Marks a session as completed (no longer active) and notifies completion listeners.
|
|
387
|
+
*
|
|
388
|
+
* <p>Stamps `endTime = Date.now()` on the first completion. Idempotent: subsequent
|
|
389
|
+
* calls preserve the existing endTime. (libpetri 1.6.0+)
|
|
366
390
|
*/
|
|
367
391
|
complete(sessionId) {
|
|
368
392
|
const session = this._sessions.get(sessionId);
|
|
369
393
|
if (session) {
|
|
370
|
-
const
|
|
394
|
+
const endTime = session.endTime ?? Date.now();
|
|
395
|
+
const completed = { ...session, active: false, endTime };
|
|
371
396
|
this._sessions.set(sessionId, completed);
|
|
372
397
|
this.notifyCompletionListeners(completed);
|
|
373
398
|
}
|
|
374
399
|
}
|
|
375
|
-
/** Removes a session from the registry. */
|
|
400
|
+
/** Removes a session from the registry. Tags die with the session. */
|
|
376
401
|
remove(sessionId) {
|
|
377
402
|
const removed = this._sessions.get(sessionId);
|
|
378
403
|
if (removed) {
|
|
@@ -385,13 +410,36 @@ var DebugSessionRegistry = class {
|
|
|
385
410
|
getSession(sessionId) {
|
|
386
411
|
return this._sessions.get(sessionId);
|
|
387
412
|
}
|
|
413
|
+
/**
|
|
414
|
+
* Sets or overwrites a single tag on a session. (libpetri 1.6.0+)
|
|
415
|
+
*
|
|
416
|
+
* Tags accumulate until the session is removed. Setting a key that already
|
|
417
|
+
* exists replaces its value.
|
|
418
|
+
*
|
|
419
|
+
* If `sessionId` is not a currently-registered session the call is a no-op.
|
|
420
|
+
* A tag write that races with {@link remove} is harmless — the write lands on
|
|
421
|
+
* the now-orphaned session object and is garbage collected along with it.
|
|
422
|
+
*/
|
|
423
|
+
tag(sessionId, key, value) {
|
|
424
|
+
const session = this._sessions.get(sessionId);
|
|
425
|
+
if (!session) return;
|
|
426
|
+
session.tags[key] = value;
|
|
427
|
+
}
|
|
428
|
+
/**
|
|
429
|
+
* Returns a snapshot of the tags attached to a session. Returns an empty
|
|
430
|
+
* object if the session has no tags or does not exist. (libpetri 1.6.0+)
|
|
431
|
+
*/
|
|
432
|
+
tagsFor(sessionId) {
|
|
433
|
+
const session = this._sessions.get(sessionId);
|
|
434
|
+
return session ? { ...session.tags } : {};
|
|
435
|
+
}
|
|
388
436
|
/** Lists sessions, ordered by start time (most recent first). */
|
|
389
|
-
listSessions(limit) {
|
|
390
|
-
return [...this._sessions.values()].sort((a, b) => b.startTime - a.startTime).slice(0, limit);
|
|
437
|
+
listSessions(limit, tagFilter) {
|
|
438
|
+
return [...this._sessions.values()].filter((s) => this.matchesTagFilter(s, tagFilter)).sort((a, b) => b.startTime - a.startTime).slice(0, limit);
|
|
391
439
|
}
|
|
392
440
|
/** Lists only active sessions. */
|
|
393
|
-
listActiveSessions(limit) {
|
|
394
|
-
return [...this._sessions.values()].filter((s) => s.active).sort((a, b) => b.startTime - a.startTime).slice(0, limit);
|
|
441
|
+
listActiveSessions(limit, tagFilter) {
|
|
442
|
+
return [...this._sessions.values()].filter((s) => s.active).filter((s) => this.matchesTagFilter(s, tagFilter)).sort((a, b) => b.startTime - a.startTime).slice(0, limit);
|
|
395
443
|
}
|
|
396
444
|
/** Total number of sessions. */
|
|
397
445
|
get size() {
|
|
@@ -399,8 +447,11 @@ var DebugSessionRegistry = class {
|
|
|
399
447
|
}
|
|
400
448
|
/**
|
|
401
449
|
* Registers an imported (archived) session as an inactive, read-only session.
|
|
450
|
+
*
|
|
451
|
+
* @param endTime when the original session ended (libpetri 1.6.0+)
|
|
452
|
+
* @param tags user-defined tags attached to the imported session (libpetri 1.6.0+)
|
|
402
453
|
*/
|
|
403
|
-
registerImported(sessionId, netName, dotDiagram, structure, eventStore, startTime) {
|
|
454
|
+
registerImported(sessionId, netName, dotDiagram, structure, eventStore, startTime, endTime, tags) {
|
|
404
455
|
this.evictIfNecessary();
|
|
405
456
|
const session = {
|
|
406
457
|
sessionId,
|
|
@@ -411,11 +462,24 @@ var DebugSessionRegistry = class {
|
|
|
411
462
|
eventStore,
|
|
412
463
|
startTime,
|
|
413
464
|
active: false,
|
|
414
|
-
importedStructure: structure
|
|
465
|
+
importedStructure: structure,
|
|
466
|
+
endTime,
|
|
467
|
+
tags: tags ? { ...tags } : {}
|
|
415
468
|
};
|
|
416
469
|
this._sessions.set(sessionId, session);
|
|
417
470
|
return session;
|
|
418
471
|
}
|
|
472
|
+
/** AND-match: all filter entries must exactly match the session's tags. */
|
|
473
|
+
matchesTagFilter(session, filter) {
|
|
474
|
+
if (!filter) return true;
|
|
475
|
+
const keys = Object.keys(filter);
|
|
476
|
+
if (keys.length === 0) return true;
|
|
477
|
+
const tags = session.tags;
|
|
478
|
+
for (const key of keys) {
|
|
479
|
+
if (tags[key] !== filter[key]) return false;
|
|
480
|
+
}
|
|
481
|
+
return true;
|
|
482
|
+
}
|
|
419
483
|
/** Notifies all completion listeners. Exceptions are caught and logged. */
|
|
420
484
|
notifyCompletionListeners(session) {
|
|
421
485
|
for (const listener of this._completionListeners) {
|
|
@@ -754,15 +818,22 @@ var DebugProtocolHandler = class {
|
|
|
754
818
|
// ======================== Command Handlers ========================
|
|
755
819
|
handleListSessions(client, cmd) {
|
|
756
820
|
const limit = cmd.limit ?? 50;
|
|
757
|
-
const sessions = cmd.activeOnly ? this._sessionRegistry.listActiveSessions(limit) : this._sessionRegistry.listSessions(limit);
|
|
758
|
-
const summaries = sessions.map((s) => (
|
|
821
|
+
const sessions = cmd.activeOnly ? this._sessionRegistry.listActiveSessions(limit, cmd.tagFilter) : this._sessionRegistry.listSessions(limit, cmd.tagFilter);
|
|
822
|
+
const summaries = sessions.map((s) => this.toProtocolSummary(s));
|
|
823
|
+
this.send(client, { type: "sessionList", sessions: summaries });
|
|
824
|
+
}
|
|
825
|
+
toProtocolSummary(s) {
|
|
826
|
+
const tags = this._sessionRegistry.tagsFor(s.sessionId);
|
|
827
|
+
return {
|
|
759
828
|
sessionId: s.sessionId,
|
|
760
829
|
netName: s.netName,
|
|
761
830
|
startTime: new Date(s.startTime).toISOString(),
|
|
762
831
|
active: s.active,
|
|
763
|
-
eventCount: s.eventStore.eventCount()
|
|
764
|
-
|
|
765
|
-
|
|
832
|
+
eventCount: s.eventStore.eventCount(),
|
|
833
|
+
tags: Object.keys(tags).length > 0 ? tags : void 0,
|
|
834
|
+
endTime: s.endTime !== void 0 ? new Date(s.endTime).toISOString() : void 0,
|
|
835
|
+
durationMs: s.endTime !== void 0 ? s.endTime - s.startTime : void 0
|
|
836
|
+
};
|
|
766
837
|
}
|
|
767
838
|
handleSubscribe(client, cmd) {
|
|
768
839
|
const debugSession = this._sessionRegistry.getSession(cmd.sessionId);
|
|
@@ -1024,13 +1095,7 @@ var DebugProtocolHandler = class {
|
|
|
1024
1095
|
}
|
|
1025
1096
|
broadcastSessionList() {
|
|
1026
1097
|
const sessions = this._sessionRegistry.listSessions(50);
|
|
1027
|
-
const summaries = sessions.map((s) => (
|
|
1028
|
-
sessionId: s.sessionId,
|
|
1029
|
-
netName: s.netName,
|
|
1030
|
-
startTime: new Date(s.startTime).toISOString(),
|
|
1031
|
-
active: s.active,
|
|
1032
|
-
eventCount: s.eventStore.eventCount()
|
|
1033
|
-
}));
|
|
1098
|
+
const summaries = sessions.map((s) => this.toProtocolSummary(s));
|
|
1034
1099
|
const response = { type: "sessionList", sessions: summaries };
|
|
1035
1100
|
for (const clientState of this._clients.values()) {
|
|
1036
1101
|
try {
|
|
@@ -1442,23 +1507,105 @@ var FileSessionArchiveStorage = class {
|
|
|
1442
1507
|
|
|
1443
1508
|
// src/debug/archive/session-archive-writer.ts
|
|
1444
1509
|
import { gzipSync } from "zlib";
|
|
1510
|
+
|
|
1511
|
+
// src/debug/archive/session-metadata.ts
|
|
1512
|
+
function computeMetadata(events) {
|
|
1513
|
+
const raw = {};
|
|
1514
|
+
let first;
|
|
1515
|
+
let last;
|
|
1516
|
+
let hasErrors = false;
|
|
1517
|
+
for (const event of events) {
|
|
1518
|
+
const typeName = toEventInfo(event).type;
|
|
1519
|
+
raw[typeName] = (raw[typeName] ?? 0) + 1;
|
|
1520
|
+
if (first === void 0) first = event.timestamp;
|
|
1521
|
+
last = event.timestamp;
|
|
1522
|
+
if (isErrorEvent(event)) hasErrors = true;
|
|
1523
|
+
}
|
|
1524
|
+
const histogram = {};
|
|
1525
|
+
for (const key of Object.keys(raw).sort()) {
|
|
1526
|
+
histogram[key] = raw[key];
|
|
1527
|
+
}
|
|
1528
|
+
return {
|
|
1529
|
+
eventTypeHistogram: histogram,
|
|
1530
|
+
firstEventTime: first !== void 0 ? new Date(first).toISOString() : void 0,
|
|
1531
|
+
lastEventTime: last !== void 0 ? new Date(last).toISOString() : void 0,
|
|
1532
|
+
hasErrors
|
|
1533
|
+
};
|
|
1534
|
+
}
|
|
1535
|
+
function isErrorEvent(event) {
|
|
1536
|
+
switch (event.type) {
|
|
1537
|
+
case "transition-failed":
|
|
1538
|
+
case "transition-timed-out":
|
|
1539
|
+
case "action-timed-out":
|
|
1540
|
+
return true;
|
|
1541
|
+
case "log-message":
|
|
1542
|
+
return event.level.toUpperCase() === "ERROR";
|
|
1543
|
+
default:
|
|
1544
|
+
return false;
|
|
1545
|
+
}
|
|
1546
|
+
}
|
|
1547
|
+
|
|
1548
|
+
// src/debug/archive/session-archive-writer.ts
|
|
1445
1549
|
var SessionArchiveWriter = class {
|
|
1446
1550
|
/**
|
|
1447
|
-
* Writes a complete session archive
|
|
1551
|
+
* Writes a complete session archive in the current format (v2 as of 1.7.0)
|
|
1552
|
+
* and returns the compressed bytes.
|
|
1448
1553
|
*/
|
|
1449
1554
|
write(session) {
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1555
|
+
return this.writeV2(session);
|
|
1556
|
+
}
|
|
1557
|
+
/**
|
|
1558
|
+
* Writes a session in the legacy v1 format. Use only for compatibility
|
|
1559
|
+
* testing or when producing archives for consumers pinned to libpetri ≤ 1.6.1.
|
|
1560
|
+
*/
|
|
1561
|
+
writeV1(session) {
|
|
1562
|
+
const header = {
|
|
1563
|
+
version: 1,
|
|
1453
1564
|
sessionId: session.sessionId,
|
|
1454
1565
|
netName: session.netName,
|
|
1455
1566
|
dotDiagram: session.dotDiagram,
|
|
1456
1567
|
startTime: new Date(session.startTime).toISOString(),
|
|
1457
1568
|
eventCount: session.eventStore.eventCount(),
|
|
1458
|
-
structure
|
|
1569
|
+
structure: buildNetStructure(session)
|
|
1459
1570
|
};
|
|
1571
|
+
return this.writeFramed(header, session);
|
|
1572
|
+
}
|
|
1573
|
+
/**
|
|
1574
|
+
* Writes a session in the v2 format — richer header with `endTime`, `tags`,
|
|
1575
|
+
* and pre-computed {@link SessionMetadata}.
|
|
1576
|
+
*
|
|
1577
|
+
* Two passes over the event store: one to compute metadata, one to serialize
|
|
1578
|
+
* events. `DebugEventStore` stores events in a plain readonly array and its
|
|
1579
|
+
* `[Symbol.iterator]()` returns a fresh array iterator each call, so both
|
|
1580
|
+
* passes walk the same sequence from the start.
|
|
1581
|
+
*/
|
|
1582
|
+
writeV2(session) {
|
|
1583
|
+
const metadata = computeMetadata(session.eventStore);
|
|
1584
|
+
const header = {
|
|
1585
|
+
version: 2,
|
|
1586
|
+
sessionId: session.sessionId,
|
|
1587
|
+
netName: session.netName,
|
|
1588
|
+
dotDiagram: session.dotDiagram,
|
|
1589
|
+
startTime: new Date(session.startTime).toISOString(),
|
|
1590
|
+
endTime: session.endTime !== void 0 ? new Date(session.endTime).toISOString() : void 0,
|
|
1591
|
+
eventCount: session.eventStore.eventCount(),
|
|
1592
|
+
// Snapshot of tags at archive-write time — record the state that was
|
|
1593
|
+
// current when the session was archived, not whatever happens on the
|
|
1594
|
+
// live session afterwards.
|
|
1595
|
+
tags: { ...session.tags },
|
|
1596
|
+
metadata,
|
|
1597
|
+
structure: buildNetStructure(session)
|
|
1598
|
+
};
|
|
1599
|
+
return this.writeFramed(header, session);
|
|
1600
|
+
}
|
|
1601
|
+
/**
|
|
1602
|
+
* Shared framing logic: length-prefixed header JSON, then length-prefixed
|
|
1603
|
+
* event JSON, then gzip. Both v1 and v2 archives use the identical event
|
|
1604
|
+
* wire format, so the body loop is version-agnostic.
|
|
1605
|
+
*/
|
|
1606
|
+
writeFramed(header, session) {
|
|
1460
1607
|
const parts = [];
|
|
1461
|
-
const metaBytes = Buffer.from(JSON.stringify(
|
|
1608
|
+
const metaBytes = Buffer.from(JSON.stringify(header), "utf-8");
|
|
1462
1609
|
const metaLen = Buffer.alloc(4);
|
|
1463
1610
|
metaLen.writeUInt32BE(metaBytes.length);
|
|
1464
1611
|
parts.push(metaLen, metaBytes);
|
|
@@ -1469,8 +1616,7 @@ var SessionArchiveWriter = class {
|
|
|
1469
1616
|
eventLen.writeUInt32BE(eventBytes.length);
|
|
1470
1617
|
parts.push(eventLen, eventBytes);
|
|
1471
1618
|
}
|
|
1472
|
-
|
|
1473
|
-
return gzipSync(raw);
|
|
1619
|
+
return gzipSync(Buffer.concat(parts));
|
|
1474
1620
|
}
|
|
1475
1621
|
};
|
|
1476
1622
|
|