@vgai/live 0.5.40 → 0.5.41

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.
@@ -226,6 +226,15 @@ export declare class GameClient {
226
226
  providers(): Promise<ProviderInfo[]>;
227
227
  commands(): Promise<DebugCommandInfo[]>;
228
228
  snapshot(sinceSeq?: number): Promise<DebugSnapshot>;
229
+ /**
230
+ * Read only the simulation clock for a predicate-free wait.
231
+ *
232
+ * `snapshot()` intentionally batches every declared provider for `waitFor`, whose predicate may
233
+ * read any of them. `waitSimTime` has no predicate: serializing a large `unity` provider every
234
+ * 150ms can itself consume multiple frames and corrupt the performance window it is advancing.
235
+ * Keep the two hardening observers fed exactly as an ordinary snapshot does.
236
+ */
237
+ private readTime;
229
238
  /** Fixture-teardown perf read: p50/p95 effective ticks-per-second over all
230
239
  * the snapshot polls this test performed. */
231
240
  tpsStats(): TpsStats;
@@ -504,6 +504,20 @@ export class GameClient {
504
504
  await this.#hiddenRecovery.observeTick(snap.time.tick);
505
505
  return snap;
506
506
  }
507
+ /**
508
+ * Read only the simulation clock for a predicate-free wait.
509
+ *
510
+ * `snapshot()` intentionally batches every declared provider for `waitFor`, whose predicate may
511
+ * read any of them. `waitSimTime` has no predicate: serializing a large `unity` provider every
512
+ * 150ms can itself consume multiple frames and corrupt the performance window it is advancing.
513
+ * Keep the two hardening observers fed exactly as an ordinary snapshot does.
514
+ */
515
+ async readTime() {
516
+ const time = await this.callBridge('state', 'time');
517
+ this.#tps.record(time.tick, Date.now());
518
+ await this.#hiddenRecovery.observeTick(time.tick);
519
+ return time;
520
+ }
507
521
  /** Fixture-teardown perf read: p50/p95 effective ticks-per-second over all
508
522
  * the snapshot polls this test performed. */
509
523
  tpsStats() {
@@ -622,12 +636,15 @@ export class GameClient {
622
636
  // goes heartbeat-silent well before this loop's own guard ever needs to.
623
637
  let heartbeat = { lastEmitWallMs: startWall, lastEmitTick: start.time.tick };
624
638
  for (;;) {
625
- const current = await this.snapshot();
626
- if (current.time.simSeconds - start.time.simSeconds >= budget.simSeconds)
639
+ const currentTime = await this.readTime();
640
+ if (currentTime.simSeconds - start.time.simSeconds >= budget.simSeconds)
627
641
  return;
628
- stalledPolls = lastTick !== null && current.time.tick === lastTick ? stalledPolls + 1 : 0;
629
- lastTick = current.time.tick;
642
+ stalledPolls = lastTick !== null && currentTime.tick === lastTick ? stalledPolls + 1 : 0;
643
+ lastTick = currentTime.tick;
630
644
  if (stalledPolls >= WAIT_FOR_STALL_POLL_LIMIT) {
645
+ // Failure diagnostics still carry one honest complete terminal snapshot. The hot path above
646
+ // stays clock-only; the expensive provider batch is paid only when it will be printed.
647
+ const current = await this.snapshot();
631
648
  throw await this.toSessionFailure(new WaitForTimeoutError({
632
649
  budget,
633
650
  startSnapshot: start,
@@ -639,8 +656,8 @@ export class GameClient {
639
656
  }
640
657
  const heartbeatResult = maybeHeartbeat({
641
658
  nowMs: Date.now(),
642
- tick: current.time.tick,
643
- simSeconds: current.time.simSeconds,
659
+ tick: currentTime.tick,
660
+ simSeconds: currentTime.simSeconds,
644
661
  testTitle: this.#testTitle,
645
662
  state: heartbeat,
646
663
  });
@@ -171,7 +171,10 @@ export class RelayTransport {
171
171
  }
172
172
  async call(method, callArgs) {
173
173
  const hidden = await this.preflightHidden();
174
- if (method === 'snapshot') {
174
+ // A predicate-free `waitSimTime` poll reads only the `time` provider. It needs the same hidden
175
+ // tab deterministic driver as a full snapshot or a hidden editor would freeze that lighter
176
+ // clock read forever.
177
+ if (method === 'snapshot' || (method === 'state' && callArgs[0] === 'time')) {
175
178
  if (hidden) {
176
179
  const now = Date.now();
177
180
  const elapsed = this.hiddenDriveLastWallMs === null ? 1000 / 60 : now - this.hiddenDriveLastWallMs;
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@vgai/live",
3
3
  "author": "Volter AI, Inc.",
4
4
  "license": "Apache-2.0",
5
- "version": "0.5.40",
5
+ "version": "0.5.41",
6
6
  "type": "module",
7
7
  "repository": {
8
8
  "type": "git",
@@ -32,8 +32,8 @@
32
32
  "prepack": "npm run build"
33
33
  },
34
34
  "dependencies": {
35
- "@vgai/editor-sdk": "0.5.40",
36
- "@vgai/sdk": "0.5.40"
35
+ "@vgai/editor-sdk": "0.5.41",
36
+ "@vgai/sdk": "0.5.41"
37
37
  },
38
38
  "peerDependencies": {
39
39
  "@playwright/test": ">=1.58.2 <2"
@@ -628,6 +628,21 @@ export class GameClient {
628
628
  return snap;
629
629
  }
630
630
 
631
+ /**
632
+ * Read only the simulation clock for a predicate-free wait.
633
+ *
634
+ * `snapshot()` intentionally batches every declared provider for `waitFor`, whose predicate may
635
+ * read any of them. `waitSimTime` has no predicate: serializing a large `unity` provider every
636
+ * 150ms can itself consume multiple frames and corrupt the performance window it is advancing.
637
+ * Keep the two hardening observers fed exactly as an ordinary snapshot does.
638
+ */
639
+ private async readTime(): Promise<DebugSnapshot['time']> {
640
+ const time = await this.callBridge<DebugSnapshot['time']>('state', 'time');
641
+ this.#tps.record(time.tick, Date.now());
642
+ await this.#hiddenRecovery.observeTick(time.tick);
643
+ return time;
644
+ }
645
+
631
646
  /** Fixture-teardown perf read: p50/p95 effective ticks-per-second over all
632
647
  * the snapshot polls this test performed. */
633
648
  tpsStats(): TpsStats {
@@ -755,11 +770,14 @@ export class GameClient {
755
770
  // goes heartbeat-silent well before this loop's own guard ever needs to.
756
771
  let heartbeat: HeartbeatState = { lastEmitWallMs: startWall, lastEmitTick: start.time.tick };
757
772
  for (;;) {
758
- const current = await this.snapshot();
759
- if (current.time.simSeconds - start.time.simSeconds >= budget.simSeconds) return;
760
- stalledPolls = lastTick !== null && current.time.tick === lastTick ? stalledPolls + 1 : 0;
761
- lastTick = current.time.tick;
773
+ const currentTime = await this.readTime();
774
+ if (currentTime.simSeconds - start.time.simSeconds >= budget.simSeconds) return;
775
+ stalledPolls = lastTick !== null && currentTime.tick === lastTick ? stalledPolls + 1 : 0;
776
+ lastTick = currentTime.tick;
762
777
  if (stalledPolls >= WAIT_FOR_STALL_POLL_LIMIT) {
778
+ // Failure diagnostics still carry one honest complete terminal snapshot. The hot path above
779
+ // stays clock-only; the expensive provider batch is paid only when it will be printed.
780
+ const current = await this.snapshot();
763
781
  throw await this.toSessionFailure(
764
782
  new WaitForTimeoutError({
765
783
  budget,
@@ -774,8 +792,8 @@ export class GameClient {
774
792
  }
775
793
  const heartbeatResult = maybeHeartbeat({
776
794
  nowMs: Date.now(),
777
- tick: current.time.tick,
778
- simSeconds: current.time.simSeconds,
795
+ tick: currentTime.tick,
796
+ simSeconds: currentTime.simSeconds,
779
797
  testTitle: this.#testTitle,
780
798
  state: heartbeat,
781
799
  });
@@ -232,7 +232,10 @@ export class RelayTransport implements BridgeTransport {
232
232
 
233
233
  async call(method: string, callArgs: unknown[]): Promise<BridgeCallOutcome> {
234
234
  const hidden = await this.preflightHidden();
235
- if (method === 'snapshot') {
235
+ // A predicate-free `waitSimTime` poll reads only the `time` provider. It needs the same hidden
236
+ // tab deterministic driver as a full snapshot or a hidden editor would freeze that lighter
237
+ // clock read forever.
238
+ if (method === 'snapshot' || (method === 'state' && callArgs[0] === 'time')) {
236
239
  if (hidden) {
237
240
  const now = Date.now();
238
241
  const elapsed =