libpetri 1.5.1 → 1.8.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.
@@ -118,7 +118,11 @@ var DebugEventStore = class {
118
118
  };
119
119
 
120
120
  // src/debug/archive/session-archive.ts
121
- var CURRENT_VERSION = 1;
121
+ var CURRENT_VERSION = 3;
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
- const metadata = JSON.parse(metaJson);
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 = JSON.parse(metaJson);
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,33 @@ 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
+ case 3: {
178
+ const v3 = raw;
179
+ return {
180
+ ...v3,
181
+ tags: v3.tags ?? {},
182
+ metadata: v3.metadata ?? emptyMetadata()
183
+ };
184
+ }
185
+ default:
186
+ throw new Error(
187
+ `Unsupported archive version: ${raw.version} (reader supports ${MIN_SUPPORTED_VERSION}..${CURRENT_VERSION})`
188
+ );
189
+ }
190
+ }
167
191
  function eventInfoToNetEvent(info) {
168
192
  const timestamp = new Date(info.timestamp).getTime();
169
193
  const d = info.details;
@@ -222,10 +246,8 @@ function eventInfoToNetEvent(info) {
222
246
  }
223
247
  }
224
248
  function infoToToken(t) {
225
- return {
226
- value: t.value,
227
- createdAt: t.timestamp ? new Date(t.timestamp).getTime() : Date.now()
228
- };
249
+ const createdAt = t.timestamp ? new Date(t.timestamp).getTime() : Date.now();
250
+ return t.structured === void 0 ? { value: t.value, createdAt } : { value: t.value, createdAt, structured: t.structured };
229
251
  }
230
252
 
231
253
  // src/debug/place-analysis.ts
@@ -341,8 +363,12 @@ var DebugSessionRegistry = class {
341
363
  /**
342
364
  * Registers a new debug session for the given Petri net.
343
365
  * Generates DOT diagram and extracts net structure.
366
+ *
367
+ * @param sessionId unique session id
368
+ * @param net the Petri net being executed
369
+ * @param tags optional user-defined tags (libpetri 1.6.0+) — e.g. `{channel: 'voice'}`
344
370
  */
345
- register(sessionId, net) {
371
+ register(sessionId, net, tags) {
346
372
  const dotDiagram = dotExport(net);
347
373
  const places = PlaceAnalysis.from(net);
348
374
  const eventStore = this._eventStoreFactory(sessionId);
@@ -355,7 +381,8 @@ var DebugSessionRegistry = class {
355
381
  eventStore,
356
382
  startTime: Date.now(),
357
383
  active: true,
358
- importedStructure: null
384
+ importedStructure: null,
385
+ tags: tags ? { ...tags } : {}
359
386
  };
360
387
  this.evictIfNecessary();
361
388
  this._sessions.set(sessionId, session);
@@ -363,16 +390,20 @@ var DebugSessionRegistry = class {
363
390
  }
364
391
  /**
365
392
  * Marks a session as completed (no longer active) and notifies completion listeners.
393
+ *
394
+ * <p>Stamps `endTime = Date.now()` on the first completion. Idempotent: subsequent
395
+ * calls preserve the existing endTime. (libpetri 1.6.0+)
366
396
  */
367
397
  complete(sessionId) {
368
398
  const session = this._sessions.get(sessionId);
369
399
  if (session) {
370
- const completed = { ...session, active: false };
400
+ const endTime = session.endTime ?? Date.now();
401
+ const completed = { ...session, active: false, endTime };
371
402
  this._sessions.set(sessionId, completed);
372
403
  this.notifyCompletionListeners(completed);
373
404
  }
374
405
  }
375
- /** Removes a session from the registry. */
406
+ /** Removes a session from the registry. Tags die with the session. */
376
407
  remove(sessionId) {
377
408
  const removed = this._sessions.get(sessionId);
378
409
  if (removed) {
@@ -385,13 +416,36 @@ var DebugSessionRegistry = class {
385
416
  getSession(sessionId) {
386
417
  return this._sessions.get(sessionId);
387
418
  }
419
+ /**
420
+ * Sets or overwrites a single tag on a session. (libpetri 1.6.0+)
421
+ *
422
+ * Tags accumulate until the session is removed. Setting a key that already
423
+ * exists replaces its value.
424
+ *
425
+ * If `sessionId` is not a currently-registered session the call is a no-op.
426
+ * A tag write that races with {@link remove} is harmless — the write lands on
427
+ * the now-orphaned session object and is garbage collected along with it.
428
+ */
429
+ tag(sessionId, key, value) {
430
+ const session = this._sessions.get(sessionId);
431
+ if (!session) return;
432
+ session.tags[key] = value;
433
+ }
434
+ /**
435
+ * Returns a snapshot of the tags attached to a session. Returns an empty
436
+ * object if the session has no tags or does not exist. (libpetri 1.6.0+)
437
+ */
438
+ tagsFor(sessionId) {
439
+ const session = this._sessions.get(sessionId);
440
+ return session ? { ...session.tags } : {};
441
+ }
388
442
  /** 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);
443
+ listSessions(limit, tagFilter) {
444
+ return [...this._sessions.values()].filter((s) => this.matchesTagFilter(s, tagFilter)).sort((a, b) => b.startTime - a.startTime).slice(0, limit);
391
445
  }
392
446
  /** 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);
447
+ listActiveSessions(limit, tagFilter) {
448
+ 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
449
  }
396
450
  /** Total number of sessions. */
397
451
  get size() {
@@ -399,8 +453,11 @@ var DebugSessionRegistry = class {
399
453
  }
400
454
  /**
401
455
  * Registers an imported (archived) session as an inactive, read-only session.
456
+ *
457
+ * @param endTime when the original session ended (libpetri 1.6.0+)
458
+ * @param tags user-defined tags attached to the imported session (libpetri 1.6.0+)
402
459
  */
403
- registerImported(sessionId, netName, dotDiagram, structure, eventStore, startTime) {
460
+ registerImported(sessionId, netName, dotDiagram, structure, eventStore, startTime, endTime, tags) {
404
461
  this.evictIfNecessary();
405
462
  const session = {
406
463
  sessionId,
@@ -411,11 +468,24 @@ var DebugSessionRegistry = class {
411
468
  eventStore,
412
469
  startTime,
413
470
  active: false,
414
- importedStructure: structure
471
+ importedStructure: structure,
472
+ endTime,
473
+ tags: tags ? { ...tags } : {}
415
474
  };
416
475
  this._sessions.set(sessionId, session);
417
476
  return session;
418
477
  }
478
+ /** AND-match: all filter entries must exactly match the session's tags. */
479
+ matchesTagFilter(session, filter) {
480
+ if (!filter) return true;
481
+ const keys = Object.keys(filter);
482
+ if (keys.length === 0) return true;
483
+ const tags = session.tags;
484
+ for (const key of keys) {
485
+ if (tags[key] !== filter[key]) return false;
486
+ }
487
+ return true;
488
+ }
419
489
  /** Notifies all completion listeners. Exceptions are caught and logged. */
420
490
  notifyCompletionListeners(session) {
421
491
  for (const listener of this._completionListeners) {
@@ -646,12 +716,30 @@ function tokenInfo(token) {
646
716
  const value = token.value;
647
717
  const type = value != null ? typeof value === "object" ? value.constructor.name : typeof value : "null";
648
718
  const fullValue = value != null ? String(value) : "null";
649
- return {
719
+ const info = {
650
720
  id: null,
651
721
  type,
652
722
  value: fullValue,
653
723
  timestamp: new Date(token.createdAt).toISOString()
654
724
  };
725
+ const structured = structuredValue(value);
726
+ return structured === void 0 ? info : { ...info, structured };
727
+ }
728
+ function structuredValue(value) {
729
+ if (value == null) return void 0;
730
+ const t = typeof value;
731
+ if (t === "string" || t === "number" || t === "boolean") return value;
732
+ if (t === "bigint") return String(value);
733
+ if (t === "symbol" || t === "function") return void 0;
734
+ try {
735
+ const cloned = JSON.parse(JSON.stringify(value));
736
+ if (cloned && typeof cloned === "object" && !Array.isArray(cloned) && Object.keys(cloned).length === 0) {
737
+ return void 0;
738
+ }
739
+ return cloned;
740
+ } catch {
741
+ return void 0;
742
+ }
655
743
  }
656
744
  function compactTokenInfo(token) {
657
745
  const value = token.value;
@@ -754,15 +842,22 @@ var DebugProtocolHandler = class {
754
842
  // ======================== Command Handlers ========================
755
843
  handleListSessions(client, cmd) {
756
844
  const limit = cmd.limit ?? 50;
757
- const sessions = cmd.activeOnly ? this._sessionRegistry.listActiveSessions(limit) : this._sessionRegistry.listSessions(limit);
758
- const summaries = sessions.map((s) => ({
845
+ const sessions = cmd.activeOnly ? this._sessionRegistry.listActiveSessions(limit, cmd.tagFilter) : this._sessionRegistry.listSessions(limit, cmd.tagFilter);
846
+ const summaries = sessions.map((s) => this.toProtocolSummary(s));
847
+ this.send(client, { type: "sessionList", sessions: summaries });
848
+ }
849
+ toProtocolSummary(s) {
850
+ const tags = this._sessionRegistry.tagsFor(s.sessionId);
851
+ return {
759
852
  sessionId: s.sessionId,
760
853
  netName: s.netName,
761
854
  startTime: new Date(s.startTime).toISOString(),
762
855
  active: s.active,
763
- eventCount: s.eventStore.eventCount()
764
- }));
765
- this.send(client, { type: "sessionList", sessions: summaries });
856
+ eventCount: s.eventStore.eventCount(),
857
+ tags: Object.keys(tags).length > 0 ? tags : void 0,
858
+ endTime: s.endTime !== void 0 ? new Date(s.endTime).toISOString() : void 0,
859
+ durationMs: s.endTime !== void 0 ? s.endTime - s.startTime : void 0
860
+ };
766
861
  }
767
862
  handleSubscribe(client, cmd) {
768
863
  const debugSession = this._sessionRegistry.getSession(cmd.sessionId);
@@ -1024,13 +1119,7 @@ var DebugProtocolHandler = class {
1024
1119
  }
1025
1120
  broadcastSessionList() {
1026
1121
  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
- }));
1122
+ const summaries = sessions.map((s) => this.toProtocolSummary(s));
1034
1123
  const response = { type: "sessionList", sessions: summaries };
1035
1124
  for (const clientState of this._clients.values()) {
1036
1125
  try {
@@ -1442,23 +1531,126 @@ var FileSessionArchiveStorage = class {
1442
1531
 
1443
1532
  // src/debug/archive/session-archive-writer.ts
1444
1533
  import { gzipSync } from "zlib";
1534
+
1535
+ // src/debug/archive/session-metadata.ts
1536
+ function computeMetadata(events) {
1537
+ const raw = {};
1538
+ let first;
1539
+ let last;
1540
+ let hasErrors = false;
1541
+ for (const event of events) {
1542
+ const typeName = toEventInfo(event).type;
1543
+ raw[typeName] = (raw[typeName] ?? 0) + 1;
1544
+ if (first === void 0) first = event.timestamp;
1545
+ last = event.timestamp;
1546
+ if (isErrorEvent(event)) hasErrors = true;
1547
+ }
1548
+ const histogram = {};
1549
+ for (const key of Object.keys(raw).sort()) {
1550
+ histogram[key] = raw[key];
1551
+ }
1552
+ return {
1553
+ eventTypeHistogram: histogram,
1554
+ firstEventTime: first !== void 0 ? new Date(first).toISOString() : void 0,
1555
+ lastEventTime: last !== void 0 ? new Date(last).toISOString() : void 0,
1556
+ hasErrors
1557
+ };
1558
+ }
1559
+ function isErrorEvent(event) {
1560
+ switch (event.type) {
1561
+ case "transition-failed":
1562
+ case "transition-timed-out":
1563
+ case "action-timed-out":
1564
+ return true;
1565
+ case "log-message":
1566
+ return event.level.toUpperCase() === "ERROR";
1567
+ default:
1568
+ return false;
1569
+ }
1570
+ }
1571
+
1572
+ // src/debug/archive/session-archive-writer.ts
1445
1573
  var SessionArchiveWriter = class {
1446
1574
  /**
1447
- * Writes a complete session archive and returns the compressed bytes.
1575
+ * Writes a complete session archive in the current format (v3 as of 1.8.0)
1576
+ * and returns the compressed bytes.
1448
1577
  */
1449
1578
  write(session) {
1450
- const structure = buildNetStructure(session);
1451
- const metadata = {
1452
- version: CURRENT_VERSION,
1579
+ return this.writeV3(session);
1580
+ }
1581
+ /**
1582
+ * Writes a session in the legacy v1 format. Use only for compatibility
1583
+ * testing or when producing archives for consumers pinned to libpetri ≤ 1.6.1.
1584
+ */
1585
+ writeV1(session) {
1586
+ const header = {
1587
+ version: 1,
1453
1588
  sessionId: session.sessionId,
1454
1589
  netName: session.netName,
1455
1590
  dotDiagram: session.dotDiagram,
1456
1591
  startTime: new Date(session.startTime).toISOString(),
1457
1592
  eventCount: session.eventStore.eventCount(),
1458
- structure
1593
+ structure: buildNetStructure(session)
1459
1594
  };
1595
+ return this.writeFramed(header, session);
1596
+ }
1597
+ /**
1598
+ * Writes a session in the v2 format — richer header with `endTime`, `tags`,
1599
+ * and pre-computed {@link SessionMetadata}.
1600
+ *
1601
+ * Two passes over the event store: one to compute metadata, one to serialize
1602
+ * events. `DebugEventStore` stores events in a plain readonly array and its
1603
+ * `[Symbol.iterator]()` returns a fresh array iterator each call, so both
1604
+ * passes walk the same sequence from the start.
1605
+ */
1606
+ writeV2(session) {
1607
+ const metadata = computeMetadata(session.eventStore);
1608
+ const header = {
1609
+ version: 2,
1610
+ sessionId: session.sessionId,
1611
+ netName: session.netName,
1612
+ dotDiagram: session.dotDiagram,
1613
+ startTime: new Date(session.startTime).toISOString(),
1614
+ endTime: session.endTime !== void 0 ? new Date(session.endTime).toISOString() : void 0,
1615
+ eventCount: session.eventStore.eventCount(),
1616
+ // Snapshot of tags at archive-write time — record the state that was
1617
+ // current when the session was archived, not whatever happens on the
1618
+ // live session afterwards.
1619
+ tags: { ...session.tags },
1620
+ metadata,
1621
+ structure: buildNetStructure(session)
1622
+ };
1623
+ return this.writeFramed(header, session);
1624
+ }
1625
+ /**
1626
+ * Writes a session in the v3 format — same header shape as v2, with version=3
1627
+ * signalling that token payloads carry a `structured` field alongside the
1628
+ * legacy `value` string (see {@link tokenInfo}).
1629
+ */
1630
+ writeV3(session) {
1631
+ const metadata = computeMetadata(session.eventStore);
1632
+ const header = {
1633
+ version: 3,
1634
+ sessionId: session.sessionId,
1635
+ netName: session.netName,
1636
+ dotDiagram: session.dotDiagram,
1637
+ startTime: new Date(session.startTime).toISOString(),
1638
+ endTime: session.endTime !== void 0 ? new Date(session.endTime).toISOString() : void 0,
1639
+ eventCount: session.eventStore.eventCount(),
1640
+ tags: { ...session.tags },
1641
+ metadata,
1642
+ structure: buildNetStructure(session)
1643
+ };
1644
+ return this.writeFramed(header, session);
1645
+ }
1646
+ /**
1647
+ * Shared framing logic: length-prefixed header JSON, then length-prefixed
1648
+ * event JSON, then gzip. Both v1 and v2 archives use the identical event
1649
+ * wire format, so the body loop is version-agnostic.
1650
+ */
1651
+ writeFramed(header, session) {
1460
1652
  const parts = [];
1461
- const metaBytes = Buffer.from(JSON.stringify(metadata), "utf-8");
1653
+ const metaBytes = Buffer.from(JSON.stringify(header), "utf-8");
1462
1654
  const metaLen = Buffer.alloc(4);
1463
1655
  metaLen.writeUInt32BE(metaBytes.length);
1464
1656
  parts.push(metaLen, metaBytes);
@@ -1469,8 +1661,7 @@ var SessionArchiveWriter = class {
1469
1661
  eventLen.writeUInt32BE(eventBytes.length);
1470
1662
  parts.push(eventLen, eventBytes);
1471
1663
  }
1472
- const raw = Buffer.concat(parts);
1473
- return gzipSync(raw);
1664
+ return gzipSync(Buffer.concat(parts));
1474
1665
  }
1475
1666
  };
1476
1667