peachsim 0.1.2207 → 0.1.2209

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.
Files changed (147) hide show
  1. package/cli/bridge-diagnostics.ts +4 -4
  2. package/cli/bridge-flow-runner.ts +33 -33
  3. package/cli/browser-evals.ts +2 -2
  4. package/cli/commands/control.ts +1 -1
  5. package/cli/commands/cpu-profile.ts +5 -4
  6. package/cli/commands/debug.ts +3 -3
  7. package/cli/commands/diagnose.ts +2 -2
  8. package/cli/commands/flow.ts +7 -7
  9. package/cli/commands/inspect/core.ts +22 -22
  10. package/cli/commands/inspect/get-node.ts +2 -2
  11. package/cli/commands/inspect/shared.ts +2 -2
  12. package/cli/commands/inspect/wait-event.ts +1 -1
  13. package/cli/commands/inspect.ts +33 -33
  14. package/cli/commands/record-three.ts +17 -17
  15. package/cli/commands/record.ts +44 -40
  16. package/cli/commands/shell-boolean-mode.ts +1 -1
  17. package/cli/commands/slides.ts +5 -3
  18. package/cli/commands/three-mode.ts +3 -3
  19. package/cli/commands/upload.ts +8 -8
  20. package/cli/drivers/types.ts +1 -1
  21. package/cli/runtime-diagnostics.ts +2 -2
  22. package/cli/scene-video.ts +5 -3
  23. package/cli/send-to-box.ts +1 -1
  24. package/cli/ws-bridge.ts +3 -3
  25. package/dist-lib/agent-daemon-client.cjs +1 -1
  26. package/dist-lib/agent-events.cjs +1 -1
  27. package/dist-lib/attached-projects.cjs +1 -1
  28. package/dist-lib/auth/shared-session.cjs +1 -1
  29. package/dist-lib/backend-origin.cjs +1 -1
  30. package/dist-lib/beta.cjs +1 -1
  31. package/dist-lib/beta.mjs +1 -1
  32. package/dist-lib/bridge-constants.cjs +1 -1
  33. package/dist-lib/bridge-contract-input.cjs +1 -1
  34. package/dist-lib/bridge-contract-input.mjs +1 -1
  35. package/dist-lib/bridge-contract.cjs +1 -1
  36. package/dist-lib/bridge-contract.mjs +1 -1
  37. package/dist-lib/bundle-channel.cjs +448 -22
  38. package/dist-lib/capture-contract.cjs +1 -1
  39. package/dist-lib/capture-contract.mjs +1 -1
  40. package/dist-lib/cloud-contract.cjs +1 -1
  41. package/dist-lib/cloud-contract.mjs +1 -1
  42. package/dist-lib/cloud-test-args.cjs +1 -1
  43. package/dist-lib/cloud-test-args.mjs +1 -1
  44. package/dist-lib/cloud.cjs +1 -1
  45. package/dist-lib/cloud.mjs +1 -1
  46. package/dist-lib/config.cjs +1 -1
  47. package/dist-lib/config.mjs +1 -1
  48. package/dist-lib/detox/index.cjs +4 -4
  49. package/dist-lib/dev-bundle-resolution.cjs +1 -1
  50. package/dist-lib/expo-swift-bundle.cjs +436 -16
  51. package/dist-lib/expo-swift-metadata.cjs +1 -1
  52. package/dist-lib/expo-swift-metadata.mjs +1 -1
  53. package/dist-lib/home-paths.cjs +1 -1
  54. package/dist-lib/host/bridge-host.cjs +449 -23
  55. package/dist-lib/host/fetch-proxy-handler.cjs +4 -4
  56. package/dist-lib/host/fetch-proxy-overrides.cjs +1 -1
  57. package/dist-lib/host/fetch-proxy-overrides.mjs +1 -1
  58. package/dist-lib/host/replacement-module-handler.cjs +1 -1
  59. package/dist-lib/host/websocket-proxy.cjs +1 -1
  60. package/dist-lib/index.cjs +10 -10
  61. package/dist-lib/jump-to-source-babel.cjs +1 -1
  62. package/dist-lib/jump-to-source-native.cjs +1 -1
  63. package/dist-lib/kotlin-host.cjs +7 -1
  64. package/dist-lib/menu.cjs +1 -1
  65. package/dist-lib/menu.mjs +1 -1
  66. package/dist-lib/metro-fingerprint-registry.cjs +1 -1
  67. package/dist-lib/metro-fingerprint-registry.mjs +1 -1
  68. package/dist-lib/metro-production-bundle.cjs +1 -1
  69. package/dist-lib/metro-production-bundle.mjs +1 -1
  70. package/dist-lib/metro.cjs +4 -4
  71. package/dist-lib/native-app-identity-project.cjs +2 -2
  72. package/dist-lib/native-app-identity-project.mjs +2 -2
  73. package/dist-lib/native-app-identity.cjs +1 -1
  74. package/dist-lib/native-app-identity.mjs +1 -1
  75. package/dist-lib/native-firebase-metadata.cjs +1 -1
  76. package/dist-lib/native-firebase-metadata.mjs +1 -1
  77. package/dist-lib/pdfium-raster.cjs +1 -1
  78. package/dist-lib/pdfium-raster.mjs +1 -1
  79. package/dist-lib/plist.cjs +1 -1
  80. package/dist-lib/plist.mjs +1 -1
  81. package/dist-lib/profiles.cjs +1 -1
  82. package/dist-lib/public-brand.cjs +1 -1
  83. package/dist-lib/public-origins-lib-esm.json +24 -24
  84. package/dist-lib/public-origins-lib.json +59 -53
  85. package/dist-lib/react-native-host-modules.cjs +1 -1
  86. package/dist-lib/react-native-host-modules.mjs +1 -1
  87. package/dist-lib/reap-browser.cjs +1 -1
  88. package/dist-lib/scripts/dev-server-scanner.cjs +1 -1
  89. package/dist-lib/sdk.cjs +23 -23
  90. package/dist-lib/sdk.mjs +23 -23
  91. package/dist-lib/skills.cjs +5176 -75
  92. package/dist-lib/swift-compile.cjs +98 -6
  93. package/dist-lib/swift-compile.mjs +98 -6
  94. package/dist-lib/swift-package.cjs +1 -1
  95. package/dist-lib/swift-package.mjs +1 -1
  96. package/dist-lib/swift-submit.cjs +1 -1
  97. package/dist-lib/swift-submit.mjs +1 -1
  98. package/dist-lib/swift-toolchain.cjs +1 -1
  99. package/dist-lib/swift.cjs +1 -1
  100. package/dist-lib/vite-swift.cjs +351 -13
  101. package/dist-lib/vite.cjs +4 -4
  102. package/dist-lib/worklet-executable.cjs +1 -1
  103. package/dist-lib/worklet-executable.mjs +1 -1
  104. package/dist-types/packages/peach-engine/src/engine/animated.d.ts +2 -1
  105. package/dist-types/packages/peach-engine/src/engine/animation-graph.d.ts +1 -0
  106. package/dist-types/packages/peach-engine/src/engine/image-loader.d.ts +1 -1
  107. package/dist-types/packages/peach-engine/src/engine/liquid-glass.d.ts +6 -0
  108. package/dist-types/packages/peach-engine/src/engine/node.d.ts +1 -0
  109. package/dist-types/packages/peach-engine/src/engine/types.d.ts +1 -0
  110. package/dist-types/packages/peach-engine/src/render-worker/canvaskit-webgpu.d.ts +8 -0
  111. package/dist-types/packages/peach-engine/src/render-worker/compositor-gpu.d.ts +3 -0
  112. package/dist-types/packages/peach-engine/src/render-worker/compositor-protocol.d.ts +14 -0
  113. package/dist-types/packages/peach-engine/src/render-worker/compositor-runtime/render-tree.d.ts +18 -0
  114. package/dist-types/packages/peach-engine/src/settings/store.d.ts +1 -0
  115. package/kotlin/sdk.tar.gz +0 -0
  116. package/package.json +1 -1
  117. package/runtime-companion.json +1 -1
  118. package/sdk/ios/System/Library/Frameworks/Foundation.framework/Headers/NSCompoundPredicate.h +1 -1
  119. package/sdk/ios/System/Library/Frameworks/Foundation.framework/Headers/NSExpression.h +1 -1
  120. package/sdk/ios/System/Library/Frameworks/Foundation.framework/Headers/NSISO8601DateFormatter.h +1 -1
  121. package/sdk/ios/System/Library/Frameworks/Foundation.framework/Headers/NSMeasurementFormatter.h +1 -1
  122. package/sdk/ios/System/Library/Frameworks/Intents.framework/Headers/INObjectCollection.h +27 -0
  123. package/sdk/ios/System/Library/Frameworks/Intents.framework/Headers/INObjectSection.h +24 -0
  124. package/sdk/ios/System/Library/Frameworks/Intents.framework/Headers/Intents.h +9 -0
  125. package/sdk/ios/System/Library/Frameworks/Intents.framework/Modules/module.modulemap +5 -0
  126. package/sdk/ios/System/Library/Frameworks/UIKit.framework/Headers/UIFeedbackGenerator.h +1 -1
  127. package/sdk/ios/System/Library/Frameworks/UIKit.framework/Headers/UIImpactFeedbackGenerator.h +1 -1
  128. package/sdk/ios/System/Library/Frameworks/UIKit.framework/Headers/UINibLoading.h +1 -1
  129. package/sdk/ios/System/Library/Frameworks/UIKit.framework/Headers/UINotificationFeedbackGenerator.h +1 -1
  130. package/sdk/ios/System/Library/Frameworks/UIKit.framework/Headers/UISelectionFeedbackGenerator.h +1 -1
  131. package/sdk/ios/usr/include/ObjectiveC.apinotes +7 -0
  132. package/src/android-theme.ts +136 -2
  133. package/src/expo-swift-host.ts +90 -1
  134. package/src/generated/android-framework-text.json +4566 -0
  135. package/src/generated/android-framework-themes.json +11 -0
  136. package/src/kotlin-app.ts +24 -4
  137. package/src/kotlin-host.ts +9 -0
  138. package/src/nitro-adapter.lock.json +246 -246
  139. package/src/nitro-superset.lock.json +4 -4
  140. package/src/swift-compile.ts +120 -2
  141. package/src/swift-distribution.lock.json +8 -8
  142. package/src/swift-objc-standin.ts +9 -2
  143. package/src/swift-shared-sdk.ts +2 -1
  144. package/src/swift-standin-builtin-behavior.ts +248 -0
  145. package/src/swift-standin-emit.ts +2 -0
  146. package/src/swift-standins.ts +17 -5
  147. package/src/turbo-runtime.lock.json +17 -17
@@ -711,8 +711,8 @@ async function getKeyboardState(bridge: WsBridge) {
711
711
  .send({
712
712
  type: 'evaluate',
713
713
  code: `(async () => {
714
- const kb = window.__sootsimKeyboard
715
- const test = window.__sootsimTest
714
+ const kb = globalThis.__sootsimKeyboard
715
+ const test = globalThis.__sootsimTest
716
716
  if (!kb) return { error: 'keyboard bridge not available' }
717
717
  const layout = typeof kb.getLayout === 'function' ? kb.getLayout() : null
718
718
  const secureTextEntry =
@@ -818,11 +818,11 @@ async function runShellVisualCommand(
818
818
  // documentElement '#333333' sniff is never set in embedded / shell-chrome
819
819
  // contexts, so 'toggle' read 'light' forever and stuck on the boot scheme.
820
820
  window.postMessage({ type: 'contrast-action', action: 'set-appearance', value: requested }, '*')
821
- // let the engine's message handler apply + publish window.__sootsimColorScheme
821
+ // let the engine's message handler apply + publish globalThis.__sootsimColorScheme
822
822
  await new Promise((r) => setTimeout(r, 60))
823
- const applied = window.__sootsimColorScheme?.resolved
823
+ const applied = globalThis.__sootsimColorScheme?.resolved
824
824
  ?? (window.matchMedia?.('(prefers-color-scheme: dark)')?.matches ? 'dark' : 'light')
825
- const setting = window.__sootsimColorScheme?.setting ?? requested
825
+ const setting = globalThis.__sootsimColorScheme?.setting ?? requested
826
826
  return { ok: true, requested, setting, applied }
827
827
  })()`,
828
828
  })
@@ -832,7 +832,7 @@ async function runShellVisualCommand(
832
832
  return bridge.send({
833
833
  type: 'evaluate',
834
834
  code: `(async () => {
835
- const toggleLock = window.SootSim?.bridges?.mainShell?.toggleLock
835
+ const toggleLock = globalThis.SootSim?.bridges?.mainShell?.toggleLock
836
836
  if (typeof toggleLock !== 'function') {
837
837
  throw new Error('peach mainShell.toggleLock bridge unavailable')
838
838
  }
@@ -1294,7 +1294,7 @@ export async function runInspect(args: string[], opts: InspectOptions) {
1294
1294
  // merged console count (see MERGED_CONSOLE_COUNT_EVAL) so the notice agrees with `get errors`.
1295
1295
  code: `(async () => ({
1296
1296
  console: ${MERGED_CONSOLE_COUNT_EVAL},
1297
- requests: (await window.__sootsimTest?.getRequestCounts?.()) || null,
1297
+ requests: (await globalThis.__sootsimTest?.getRequestCounts?.()) || null,
1298
1298
  }))()`,
1299
1299
  })
1300
1300
  return (raw as SummaryCounts) || { console: null, requests: null }
@@ -1439,7 +1439,7 @@ export async function runInspect(args: string[], opts: InspectOptions) {
1439
1439
  payload = await evalInBridge<FooterPayload>(
1440
1440
  b,
1441
1441
  `(() => {
1442
- const tl = window.SootSim && window.SootSim.bridges && window.SootSim.bridges.timeline
1442
+ const tl = globalThis.SootSim && globalThis.SootSim.bridges && globalThis.SootSim.bridges.timeline
1443
1443
  if (!tl || typeof tl.summary !== 'function') return null
1444
1444
  const cursorKey = ${JSON.stringify(cliKey)}
1445
1445
  const summary = tl.summary({ sinceCursor: cursorKey })
@@ -2273,7 +2273,7 @@ export async function runInspect(args: string[], opts: InspectOptions) {
2273
2273
  const tiPayload = await bridge.send({
2274
2274
  type: 'evaluate',
2275
2275
  code: `(async () => {
2276
- const t = window.__sootsimTest
2276
+ const t = globalThis.__sootsimTest
2277
2277
  if (!t) return null
2278
2278
  const n = await (t.findByTestId(${tiArg}) || t.findById(${tiArg}))
2279
2279
  if (!n || !n.absolutePosition || !n.layout) return null
@@ -2857,7 +2857,7 @@ export async function runInspect(args: string[], opts: InspectOptions) {
2857
2857
  result = await bridge.send({
2858
2858
  type: 'evaluate',
2859
2859
  code: `(() => {
2860
- const h = window.__sootsimRenderHost
2860
+ const h = globalThis.__sootsimRenderHost
2861
2861
  if (!h || typeof h.getOwnershipSnapshot !== 'function') {
2862
2862
  return { error: 'getOwnershipSnapshot not available' }
2863
2863
  }
@@ -2869,8 +2869,8 @@ export async function runInspect(args: string[], opts: InspectOptions) {
2869
2869
  result = await bridge.send({
2870
2870
  type: 'evaluate',
2871
2871
  code: `(async () => {
2872
- const kb = window.__sootsimKeyboard
2873
- const test = window.__sootsimTest
2872
+ const kb = globalThis.__sootsimKeyboard
2873
+ const test = globalThis.__sootsimTest
2874
2874
  if (!kb) return { error: 'keyboard bridge not available' }
2875
2875
  const layout = typeof kb.getLayout === 'function' ? kb.getLayout() : null
2876
2876
  const secureTextEntry =
@@ -3206,7 +3206,7 @@ export async function runInspect(args: string[], opts: InspectOptions) {
3206
3206
  let issuedReload = false
3207
3207
  let inPlaceReload = false
3208
3208
  // a reload rewrites the running app, so it takes the cli lease like any other write; a foreign lease refusal must not be swallowed by the reconnect handling below.
3209
- await evalInBridge(bridge, 'window.__sootsimConsole?.clear()', {
3209
+ await evalInBridge(bridge, 'globalThis.__sootsimConsole?.clear()', {
3210
3210
  acquireLock: true,
3211
3211
  })
3212
3212
  try {
@@ -3231,7 +3231,7 @@ export async function runInspect(args: string[], opts: InspectOptions) {
3231
3231
  if (m && m[1] !== loaded) engineStale = true
3232
3232
  }
3233
3233
  } catch {}
3234
- const reloadExternalApp = window.SootSim?.bridges?.hotRemount?.reloadExternalApp
3234
+ const reloadExternalApp = globalThis.SootSim?.bridges?.hotRemount?.reloadExternalApp
3235
3235
  // the restart can first finish work the running app owns, such
3236
3236
  // as suspending a recording, while the old app still answers
3237
3237
  // ready. resolve with it so the ready poll reads the new app.
@@ -3338,7 +3338,7 @@ export async function runInspect(args: string[], opts: InspectOptions) {
3338
3338
  }
3339
3339
 
3340
3340
  case 'storage-clear': {
3341
- await evalInBridge(bridge, 'window.__sootsimConsole?.clear()', {
3341
+ await evalInBridge(bridge, 'globalThis.__sootsimConsole?.clear()', {
3342
3342
  acquireLock: true,
3343
3343
  })
3344
3344
  const cleared = await evalInBridge(bridge, resetGuestAppStateEval(true), {
@@ -3504,18 +3504,18 @@ export async function runInspect(args: string[], opts: InspectOptions) {
3504
3504
  globals['test (→ __sootsimTest)'] = testMethods
3505
3505
 
3506
3506
  // debug
3507
- if (window.__sootsimDebug) {
3508
- globals['debug (→ __sootsimDebug)'] = Object.keys(window.__sootsimDebug)
3507
+ if (globalThis.__sootsimDebug) {
3508
+ globals['debug (→ __sootsimDebug)'] = Object.keys(globalThis.__sootsimDebug)
3509
3509
  }
3510
3510
 
3511
3511
  // interact
3512
- if (window.__sootsimInteract) {
3513
- globals['interact (→ __sootsimInteract)'] = Object.keys(window.__sootsimInteract)
3512
+ if (globalThis.__sootsimInteract) {
3513
+ globals['interact (→ __sootsimInteract)'] = Object.keys(globalThis.__sootsimInteract)
3514
3514
  }
3515
3515
 
3516
3516
  // keyboard
3517
- if (window.__sootsimKeyboard) {
3518
- globals['keyboard (→ __sootsimKeyboard)'] = Object.keys(window.__sootsimKeyboard)
3517
+ if (globalThis.__sootsimKeyboard) {
3518
+ globals['keyboard (→ __sootsimKeyboard)'] = Object.keys(globalThis.__sootsimKeyboard)
3519
3519
  }
3520
3520
 
3521
3521
  // other globals
@@ -3588,7 +3588,7 @@ export async function runInspect(args: string[], opts: InspectOptions) {
3588
3588
  const result = await bridge.send({
3589
3589
  type: 'evaluate',
3590
3590
  code: `(async () => {
3591
- const perf = window.SootSim?.bridges?.scrollPerf
3591
+ const perf = globalThis.SootSim?.bridges?.scrollPerf
3592
3592
  if (!perf) return { error: 'scroll performance profile unavailable' }
3593
3593
  await perf.start(${Math.floor(parsedLimit)}, ${JSON.stringify(pressTestID)})
3594
3594
  return { started: true }
@@ -3607,7 +3607,7 @@ export async function runInspect(args: string[], opts: InspectOptions) {
3607
3607
  const result = await bridge.send({
3608
3608
  type: 'evaluate',
3609
3609
  code: `(async () => {
3610
- const perf = window.SootSim?.bridges?.scrollPerf
3610
+ const perf = globalThis.SootSim?.bridges?.scrollPerf
3611
3611
  if (!perf) return { error: 'scroll performance profile unavailable' }
3612
3612
  return await perf.stop()
3613
3613
  })()`,
@@ -3689,10 +3689,10 @@ export async function runInspect(args: string[], opts: InspectOptions) {
3689
3689
  const result = await bridge.send({
3690
3690
  type: 'evaluate',
3691
3691
  code: `(async () => {
3692
- if (!window.__sootsimShellPerf) {
3692
+ if (!globalThis.__sootsimShellPerf) {
3693
3693
  return { error: 'shell frame profile unavailable (__sootsimShellPerf missing on the page)' }
3694
3694
  }
3695
- await window.__sootsimShellPerf.start()
3695
+ await globalThis.__sootsimShellPerf.start()
3696
3696
  return { started: true }
3697
3697
  })()`,
3698
3698
  })
@@ -3710,10 +3710,10 @@ export async function runInspect(args: string[], opts: InspectOptions) {
3710
3710
  const result = await bridge.send({
3711
3711
  type: 'evaluate',
3712
3712
  code: `(async () => {
3713
- if (!window.__sootsimShellPerf) {
3713
+ if (!globalThis.__sootsimShellPerf) {
3714
3714
  return { error: 'shell frame profile unavailable (__sootsimShellPerf missing on the page)' }
3715
3715
  }
3716
- return await window.__sootsimShellPerf.stop()
3716
+ return await globalThis.__sootsimShellPerf.stop()
3717
3717
  })()`,
3718
3718
  })
3719
3719
  if (result?.error) {
@@ -3761,17 +3761,17 @@ export async function runInspect(args: string[], opts: InspectOptions) {
3761
3761
  const result = await bridge.send({
3762
3762
  type: 'evaluate',
3763
3763
  code: `(async () => {
3764
- if (!window.__sootsimShellPerf) {
3764
+ if (!globalThis.__sootsimShellPerf) {
3765
3765
  return { error: 'shell frame profile unavailable (__sootsimShellPerf missing on the page)' }
3766
3766
  }
3767
- await window.__sootsimShellPerf.start()
3767
+ await globalThis.__sootsimShellPerf.start()
3768
3768
  // give a frame for profiling to engage before the event
3769
3769
  await new Promise(r => requestAnimationFrame(() => r(undefined)))
3770
3770
  window.dispatchEvent(new Event('${eventName}'))
3771
3771
  // shell animations are ~300-500ms; fixed timing avoids complex
3772
3772
  // animation-end detection
3773
3773
  await new Promise(r => setTimeout(r, 600))
3774
- return await window.__sootsimShellPerf.stop()
3774
+ return await globalThis.__sootsimShellPerf.stop()
3775
3775
  })()`,
3776
3776
  })
3777
3777
  if (result?.error) {
@@ -4006,7 +4006,7 @@ export async function runInspect(args: string[], opts: InspectOptions) {
4006
4006
  if (sub === 'clear') {
4007
4007
  await bridge.send({
4008
4008
  type: 'evaluate',
4009
- code: 'window.__sootsimObservability?.network.clear(); "cleared"',
4009
+ code: 'globalThis.__sootsimObservability?.network.clear(); "cleared"',
4010
4010
  })
4011
4011
  console.log(' network buffer cleared')
4012
4012
  break
@@ -4021,7 +4021,7 @@ export async function runInspect(args: string[], opts: InspectOptions) {
4021
4021
  const entry = (await bridge.send({
4022
4022
  type: 'evaluate',
4023
4023
  code: `(() => {
4024
- const obs = window.__sootsimObservability;
4024
+ const obs = globalThis.__sootsimObservability;
4025
4025
  if (!obs) return null;
4026
4026
  return obs.network.getSnapshot().find(e => e.id === ${JSON.stringify(id)}) || null;
4027
4027
  })()`,
@@ -4051,7 +4051,7 @@ export async function runInspect(args: string[], opts: InspectOptions) {
4051
4051
  const entries = (await bridge.send({
4052
4052
  type: 'evaluate',
4053
4053
  code: `(() => {
4054
- const obs = window.__sootsimObservability;
4054
+ const obs = globalThis.__sootsimObservability;
4055
4055
  if (!obs) return { ok: false };
4056
4056
  return { ok: true, entries: obs.network.getSnapshot() };
4057
4057
  })()`,
@@ -138,7 +138,7 @@ examples:
138
138
  // remember the shell state we mutate so a plain run is side-effect free
139
139
  const before = await evalInPage<{ threeMode?: boolean; editor?: boolean }>(
140
140
  bridge,
141
- `(() => { const s = window.SootSim?.bridges?.settings?.get?.() || {}
141
+ `(() => { const s = globalThis.SootSim?.bridges?.settings?.get?.() || {}
142
142
  return { threeMode: s.threeMode === true, editor: s.threeRecordingEditor === true } })()`,
143
143
  )
144
144
 
@@ -157,13 +157,13 @@ examples:
157
157
  // by the flow's open), and this phase records the flat canvas
158
158
  await evalInPage(
159
159
  bridge,
160
- `(() => { const s = window.SootSim?.bridges?.settings
160
+ `(() => { const s = globalThis.SootSim?.bridges?.settings
161
161
  s?.set?.('threeRecordingEditor', false)
162
162
  s?.set?.('threeMode', false) })()`,
163
163
  )
164
164
  const started = await evalInPage<boolean>(
165
165
  bridge,
166
- `(async () => !!(await window.SootSim?.bridges?.startRecording?.('video', undefined, { skipEntitlement: true })))()`,
166
+ `(async () => !!(await globalThis.SootSim?.bridges?.startRecording?.('video', undefined, { skipEntitlement: true })))()`,
167
167
  )
168
168
  if (!started) {
169
169
  throw new Error(
@@ -176,7 +176,7 @@ examples:
176
176
  })
177
177
  await sleep(400)
178
178
  const durationMs = Math.min(Date.now() - t0, MAX_FILM_MS)
179
- await evalInPage(bridge, `window.SootSim?.bridges?.stopRecording?.()`)
179
+ await evalInPage(bridge, `globalThis.SootSim?.bridges?.stopRecording?.()`)
180
180
  if (flowCode !== 0) {
181
181
  console.error(
182
182
  ` record --three: flow failed (exit ${flowCode}) — not filming a broken take`,
@@ -189,7 +189,7 @@ examples:
189
189
  await sleep(400)
190
190
  const st = await evalInPage<{ state?: string; hasBlob?: boolean }>(
191
191
  bridge,
192
- `(() => { const s = window.SootSim?.bridges?.getRecordingState?.()
192
+ `(() => { const s = globalThis.SootSim?.bridges?.getRecordingState?.()
193
193
  return { state: s?.state, hasBlob: !!s?.lastVideoBlob } })()`,
194
194
  )
195
195
  if (st?.state === 'idle' && st.hasBlob) {
@@ -206,20 +206,20 @@ examples:
206
206
  // -- phase 2: bring up the 3d stage + recording editor -----------------
207
207
  await evalInPage(
208
208
  bridge,
209
- `window.SootSim?.bridges?.settings?.set?.('threeMode', true)`,
209
+ `globalThis.SootSim?.bridges?.settings?.set?.('threeMode', true)`,
210
210
  )
211
211
  await waitForThreeModeRuntimeReadyOnBridge(bridge)
212
212
  await sleep(1400)
213
213
  await evalInPage(
214
214
  bridge,
215
- `window.SootSim?.bridges?.settings?.set?.('threeRecordingEditor', true)`,
215
+ `globalThis.SootSim?.bridges?.settings?.set?.('threeRecordingEditor', true)`,
216
216
  )
217
217
  let replaySeeded = false
218
218
  for (let i = 0; i < 60; i++) {
219
219
  await sleep(250)
220
220
  const seeded = await evalInPage<boolean>(
221
221
  bridge,
222
- `(() => { const rs = window.SootSim?.replayStore
222
+ `(() => { const rs = globalThis.SootSim?.replayStore
223
223
  return rs?.value?.preview?.surface === 'video' && !!rs?.value?.preview?.flowVideoUrl })()`,
224
224
  )
225
225
  if (seeded) {
@@ -243,7 +243,7 @@ examples:
243
243
  const rec = await evalInPage<{ ok: boolean; error?: string }>(
244
244
  bridge,
245
245
  `(() => {
246
- const c = window.SootSim?.bridges?.threeMode?.getCanvas?.()
246
+ const c = globalThis.SootSim?.bridges?.threeMode?.getCanvas?.()
247
247
  if (!c || !c.captureStream) return { ok: false, error: 'no stage canvas' }
248
248
  let mime = 'video/webm;codecs=vp9'
249
249
  if (!('MediaRecorder' in window) || !MediaRecorder.isTypeSupported(mime)) mime = 'video/webm'
@@ -257,7 +257,7 @@ examples:
257
257
  fr.onload = () => { st.b64 = String(fr.result).split(',')[1] || '' }
258
258
  fr.readAsDataURL(blob)
259
259
  }
260
- window.__sootsimFilm = st
260
+ globalThis.__sootsimFilm = st
261
261
  mr.start(200)
262
262
  return { ok: true }
263
263
  })()`,
@@ -270,7 +270,7 @@ examples:
270
270
  await evalInPage(
271
271
  bridge,
272
272
  `((dur) => {
273
- const rs = window.SootSim?.replayStore
273
+ const rs = globalThis.SootSim?.replayStore
274
274
  if (!rs) return false
275
275
  const probed = rs.value?.playback?.durationMs
276
276
  const total = probed && probed > 500 ? probed : dur
@@ -291,14 +291,14 @@ examples:
291
291
 
292
292
  await evalInPage(
293
293
  bridge,
294
- `(() => { const s = window.__sootsimFilm; if (s?.mr?.state === 'recording') s.mr.stop(); return true })()`,
294
+ `(() => { const s = globalThis.__sootsimFilm; if (s?.mr?.state === 'recording') s.mr.stop(); return true })()`,
295
295
  )
296
296
  let b64Size = 0
297
297
  for (let i = 0; i < 60; i++) {
298
298
  await sleep(500)
299
299
  const probe = await evalInPage<{ ready: boolean; size: number }>(
300
300
  bridge,
301
- `(() => { const s = window.__sootsimFilm; return { ready: !!(s && s.b64), size: s && s.b64 ? s.b64.length : 0 } })()`,
301
+ `(() => { const s = globalThis.__sootsimFilm; return { ready: !!(s && s.b64), size: s && s.b64 ? s.b64.length : 0 } })()`,
302
302
  )
303
303
  if (probe?.ready && probe.size > 0) {
304
304
  b64Size = probe.size
@@ -317,13 +317,13 @@ examples:
317
317
  for (let offset = 0; offset < b64Size; offset += CHUNK) {
318
318
  const part = await evalInPage<{ data: string }>(
319
319
  bridge,
320
- `(() => ({ data: window.__sootsimFilm.b64.substr(${offset}, ${CHUNK}) }))()`,
320
+ `(() => ({ data: globalThis.__sootsimFilm.b64.substr(${offset}, ${CHUNK}) }))()`,
321
321
  )
322
322
  b64 += part.data
323
323
  }
324
324
  await evalInPage(
325
325
  bridge,
326
- `(() => { try { delete window.__sootsimFilm } catch {} return true })()`,
326
+ `(() => { try { delete globalThis.__sootsimFilm } catch {} return true })()`,
327
327
  )
328
328
 
329
329
  const webmBytes = Buffer.from(b64, 'base64')
@@ -372,13 +372,13 @@ examples:
372
372
  if (!before?.editor) {
373
373
  await evalInPage(
374
374
  bridge,
375
- `window.SootSim?.bridges?.settings?.set?.('threeRecordingEditor', false)`,
375
+ `globalThis.SootSim?.bridges?.settings?.set?.('threeRecordingEditor', false)`,
376
376
  )
377
377
  }
378
378
  if (!before?.threeMode) {
379
379
  await evalInPage(
380
380
  bridge,
381
- `window.SootSim?.bridges?.settings?.set?.('threeMode', false)`,
381
+ `globalThis.SootSim?.bridges?.settings?.set?.('threeMode', false)`,
382
382
  )
383
383
  }
384
384
  }
@@ -709,7 +709,7 @@ async function assertRecorderAvailable(bridge: WsBridge) {
709
709
  )
710
710
  if (!ok) {
711
711
  console.error(
712
- ' window.__sootsimRecorder missing — is peach engine running in this sim?',
712
+ ' globalThis.__sootsimRecorder missing — is peach engine running in this sim?',
713
713
  )
714
714
  peachExit(1)
715
715
  }
@@ -840,7 +840,7 @@ async function renderStoreScene(
840
840
  const requestId: string | null = await bridge.send(
841
841
  {
842
842
  type: 'evaluate',
843
- code: 'window.__sootsimRecorder.waitSceneVideoRequest()',
843
+ code: 'globalThis.__sootsimRecorder.waitSceneVideoRequest()',
844
844
  },
845
845
  { timeoutMs: UPLOAD_TIMEOUT_MS },
846
846
  )
@@ -852,7 +852,7 @@ async function renderStoreScene(
852
852
  for (;;) {
853
853
  const chunk: { data: string; offset: number; done: boolean } = await bridge.send({
854
854
  type: 'evaluate',
855
- code: `window.__sootsimRecorder.getSceneVideoChunk(${JSON.stringify(requestId)}, ${offset})`,
855
+ code: `globalThis.__sootsimRecorder.getSceneVideoChunk(${JSON.stringify(requestId)}, ${offset})`,
856
856
  })
857
857
  chunks.push(Buffer.from(chunk.data, 'base64'))
858
858
  offset = chunk.offset
@@ -877,19 +877,19 @@ async function renderStoreScene(
877
877
  const data = bytes.subarray(start, start + 1024 * 1024).toString('base64')
878
878
  await bridge.send({
879
879
  type: 'evaluate',
880
- code: `window.__sootsimRecorder.appendSceneVideoChunk(${JSON.stringify(requestId)}, ${JSON.stringify(data)})`,
880
+ code: `globalThis.__sootsimRecorder.appendSceneVideoChunk(${JSON.stringify(requestId)}, ${JSON.stringify(data)})`,
881
881
  })
882
882
  }
883
883
  await bridge.send({
884
884
  type: 'evaluate',
885
- code: `window.__sootsimRecorder.finishSceneVideo(${JSON.stringify(requestId)}, ${JSON.stringify({ durationMs: result.durationMs, format: video.format })})`,
885
+ code: `globalThis.__sootsimRecorder.finishSceneVideo(${JSON.stringify(requestId)}, ${JSON.stringify({ durationMs: result.durationMs, format: video.format })})`,
886
886
  })
887
887
  } catch (error) {
888
888
  const message = error instanceof Error ? error.message : String(error)
889
889
  await bridge
890
890
  .send({
891
891
  type: 'evaluate',
892
- code: `window.__sootsimRecorder.finishSceneVideo(${JSON.stringify(requestId)}, ${JSON.stringify({ error: message })})`,
892
+ code: `globalThis.__sootsimRecorder.finishSceneVideo(${JSON.stringify(requestId)}, ${JSON.stringify({ error: message })})`,
893
893
  })
894
894
  .catch(() => {})
895
895
  throw error
@@ -1151,41 +1151,45 @@ async function recordStart(args: string[], opts: RecordOptions) {
1151
1151
  peachExit(1)
1152
1152
  }
1153
1153
  } else {
1154
+ // a format the caller named; otherwise the recorder records its own default and says which
1154
1155
  const startOpts: {
1155
- format: string
1156
+ format?: string
1156
1157
  fps: number
1157
1158
  layers?: string
1158
1159
  scene: boolean
1159
1160
  sceneWithVideo: boolean
1160
1161
  lockstep?: boolean
1161
1162
  } = {
1162
- format,
1163
1163
  fps,
1164
1164
  scene: session.sceneOnly === true,
1165
1165
  sceneWithVideo: !lockstep && !session.sceneOnly,
1166
1166
  }
1167
+ if (formatArg) startOpts.format = format
1167
1168
  if (lockstep) startOpts.lockstep = true
1168
1169
  if (layers) startOpts.layers = layers
1169
1170
  writeRecordingState(session)
1170
- const result: { ok: boolean; error?: string; scene?: boolean } = await bridge.send({
1171
- type: 'evaluate',
1172
- code: `(() => {
1173
- if (!window.__sootsimRecorder) return { ok: false, error: 'recorder unavailable in this sim' };
1174
- if (document.hidden) return { ok: false, error: 'the peach tab is hidden; bring it to the foreground before recording' };
1175
- return window.__sootsimRecorder.start(${JSON.stringify(startOpts)});
1171
+ const result: { ok: boolean; error?: string; scene?: boolean; format?: string } =
1172
+ await bridge.send({
1173
+ type: 'evaluate',
1174
+ code: `(() => {
1175
+ if (!globalThis.__sootsimRecorder) return { ok: false, error: 'recorder unavailable in this sim' };
1176
+ if (globalThis.document?.hidden) return { ok: false, error: 'the peach tab is hidden; bring it to the foreground before recording' };
1177
+ return globalThis.__sootsimRecorder.start(${JSON.stringify(startOpts)});
1176
1178
  })()`,
1177
- })
1179
+ })
1178
1180
  if (!result.ok) {
1179
1181
  clearRecordingState()
1180
1182
  console.error(` start failed: ${result.error ?? 'unknown error'}`)
1181
1183
  peachExit(1)
1182
1184
  }
1183
1185
  session.scene = session.sceneOnly || result.scene === true
1186
+ if (result.format === 'mp4' || result.format === 'webm')
1187
+ session.format = result.format
1184
1188
  writeRecordingState(session)
1185
1189
  }
1186
1190
  if (mode === 'video') {
1187
1191
  console.log(
1188
- ` recording ${format} @ ${fps}fps${layers ? ` (${layers})` : ''} — run \`peach record stop --output <path>\` when done`,
1192
+ ` recording ${session.format} @ ${fps}fps${layers ? ` (${layers})` : ''} — run \`peach record stop --output <path>\` when done`,
1189
1193
  )
1190
1194
  } else {
1191
1195
  console.log(
@@ -1211,7 +1215,7 @@ async function recordPreludeStart(args: string[], opts: RecordOptions) {
1211
1215
  const ok: boolean = await bridge.send({
1212
1216
  type: 'evaluate',
1213
1217
  code: `(() => {
1214
- const start = window.SootSim?.bridges?.startRecordingPrelude
1218
+ const start = globalThis.SootSim?.bridges?.startRecordingPrelude
1215
1219
  if (typeof start !== 'function') return false
1216
1220
  return Promise.resolve(start()).then((value) => value === true)
1217
1221
  })()`,
@@ -1221,7 +1225,7 @@ async function recordPreludeStart(args: string[], opts: RecordOptions) {
1221
1225
  .send({
1222
1226
  type: 'evaluate',
1223
1227
  code: `(() => {
1224
- const getError = window.SootSim?.bridges?.getRecordingStartError
1228
+ const getError = globalThis.SootSim?.bridges?.getRecordingStartError
1225
1229
  return typeof getError === 'function' ? getError() : null
1226
1230
  })()`,
1227
1231
  })
@@ -1274,9 +1278,9 @@ async function probeEngineRecordingState(
1274
1278
  } | null = await bridge.send({
1275
1279
  type: 'evaluate',
1276
1280
  code: `(() => {
1277
- const store = window.SootSim?.bridges?.getRecordingState?.();
1278
- const headless = window.__sootsimRecorder?.state?.();
1279
- const eventRecorder = window.SootSim?.bridges?.eventRecorder ?? window.__sootsimEventRecorder;
1281
+ const store = globalThis.SootSim?.bridges?.getRecordingState?.();
1282
+ const headless = globalThis.__sootsimRecorder?.state?.();
1283
+ const eventRecorder = globalThis.SootSim?.bridges?.eventRecorder ?? globalThis.__sootsimEventRecorder;
1280
1284
  const eventRecording =
1281
1285
  typeof eventRecorder?.isRecording === 'function'
1282
1286
  ? eventRecorder.isRecording() === true
@@ -1305,14 +1309,14 @@ async function cancelEngineRecording(bridge: WsBridge): Promise<{
1305
1309
  .send({
1306
1310
  type: 'evaluate',
1307
1311
  code: `(() => {
1308
- const store = window.SootSim?.bridges?.getRecordingState?.();
1309
- const eventRecorder = window.SootSim?.bridges?.eventRecorder ?? window.__sootsimEventRecorder;
1312
+ const store = globalThis.SootSim?.bridges?.getRecordingState?.();
1313
+ const eventRecorder = globalThis.SootSim?.bridges?.eventRecorder ?? globalThis.__sootsimEventRecorder;
1310
1314
  const hadStoreRecording = store && store.state !== 'idle';
1311
1315
  const hadEventRecording =
1312
1316
  typeof eventRecorder?.isRecording === 'function'
1313
1317
  ? eventRecorder.isRecording() === true
1314
1318
  : false;
1315
- const cancel = window.SootSim?.bridges?.cancelRecording;
1319
+ const cancel = globalThis.SootSim?.bridges?.cancelRecording;
1316
1320
  if (typeof cancel === 'function') cancel();
1317
1321
  return Boolean(hadStoreRecording || hadEventRecording);
1318
1322
  })()`,
@@ -1322,7 +1326,7 @@ async function cancelEngineRecording(bridge: WsBridge): Promise<{
1322
1326
  .send({
1323
1327
  type: 'evaluate',
1324
1328
  code: `(async () => {
1325
- const r = window.__sootsimRecorder;
1329
+ const r = globalThis.__sootsimRecorder;
1326
1330
  if (r && typeof r.forceRelease === 'function') return await r.forceRelease();
1327
1331
  return false;
1328
1332
  })()`,
@@ -1462,13 +1466,13 @@ async function recordCancel(args: string[], opts: RecordOptions) {
1462
1466
  if (state.mode === 'live' || state.mode === 'combined') {
1463
1467
  await bridge.send({
1464
1468
  type: 'evaluate',
1465
- code: `void window.SootSim?.bridges?.cancelRecording?.()`,
1469
+ code: `void globalThis.SootSim?.bridges?.cancelRecording?.()`,
1466
1470
  })
1467
1471
  } else {
1468
1472
  await bridge.send({
1469
1473
  type: 'evaluate',
1470
1474
  code: `(async () => {
1471
- const recorder = window.__sootsimRecorder;
1475
+ const recorder = globalThis.__sootsimRecorder;
1472
1476
  await recorder.stop();
1473
1477
  return recorder.forceRelease();
1474
1478
  })()`,
@@ -1574,7 +1578,7 @@ async function recordStop(args: string[], opts: RecordOptions) {
1574
1578
  frameCount?: number
1575
1579
  } = await bridge.send({
1576
1580
  type: 'evaluate',
1577
- code: `window.__sootsimRecorder.stop({ sceneOnly: ${sceneOnly} })`,
1581
+ code: `globalThis.__sootsimRecorder.stop({ sceneOnly: ${sceneOnly} })`,
1578
1582
  })
1579
1583
  if (!stopResult.ok) {
1580
1584
  console.error(` stop failed: ${stopResult.error ?? 'unknown error'}`)
@@ -1619,7 +1623,7 @@ async function recordStop(args: string[], opts: RecordOptions) {
1619
1623
  async function assertStoreBridgesAvailable(bridge: WsBridge) {
1620
1624
  const ok = await probeSim(bridge, {
1621
1625
  type: 'evaluate',
1622
- code: 'typeof window.SootSim?.bridges?.startRecording === "function" && typeof window.SootSim?.bridges?.stopRecording === "function"',
1626
+ code: 'typeof globalThis.SootSim?.bridges?.startRecording === "function" && typeof globalThis.SootSim?.bridges?.stopRecording === "function"',
1623
1627
  })
1624
1628
  if (!ok) {
1625
1629
  console.error(
@@ -1646,7 +1650,7 @@ async function assertGuestBundleLoaded(bridge: WsBridge, mode: RecordMode) {
1646
1650
  const hasBundle = await probeSim(bridge, {
1647
1651
  type: 'evaluate',
1648
1652
  code: `(() => {
1649
- const fn = window.__sootsimCaptureBundle
1653
+ const fn = globalThis.__sootsimCaptureBundle
1650
1654
  if (typeof fn !== 'function') return false
1651
1655
  const snap = fn()
1652
1656
  return !!(snap && snap.bundleUrl)
@@ -1701,15 +1705,15 @@ async function injectPreviewSessionAuth(
1701
1705
  const ok: boolean = await bridge.send({
1702
1706
  type: 'evaluate',
1703
1707
  code: `(() => {
1704
- const set = window.SootSim && window.SootSim.bridges && window.SootSim.bridges.setSession
1708
+ const set = globalThis.SootSim && globalThis.SootSim.bridges && globalThis.SootSim.bridges.setSession
1705
1709
  if (typeof set !== 'function') return false
1706
1710
  set({ token: ${JSON.stringify(token)}, user: null })
1707
- window.__sootsimPreviewUploadIdentity = ${JSON.stringify(uploadIdentity)}
1708
- window.__sootsimPreviewUseInjectedBearer = true
1711
+ globalThis.__sootsimPreviewUploadIdentity = ${JSON.stringify(uploadIdentity)}
1712
+ globalThis.__sootsimPreviewUseInjectedBearer = true
1709
1713
  ${
1710
1714
  uploadOrigin
1711
- ? `window.__sootsimUploadOrigin = ${JSON.stringify(uploadOrigin)}`
1712
- : 'delete window.__sootsimUploadOrigin'
1715
+ ? `globalThis.__sootsimUploadOrigin = ${JSON.stringify(uploadOrigin)}`
1716
+ : 'delete globalThis.__sootsimUploadOrigin'
1713
1717
  }
1714
1718
  return true
1715
1719
  })()`,
@@ -1739,7 +1743,7 @@ async function evalStoreStart(
1739
1743
  const layersArg = layers ? JSON.stringify(layers) : 'undefined'
1740
1744
  const ok: boolean = await bridge.send({
1741
1745
  type: 'evaluate',
1742
- code: `window.SootSim.bridges.startRecording(${JSON.stringify(mode)}, ${layersArg}, { skipEntitlement: true, capture: ${JSON.stringify(capture)}, externalSceneVideo: ${mode === 'combined'} })`,
1746
+ code: `globalThis.SootSim.bridges.startRecording(${JSON.stringify(mode)}, ${layersArg}, { skipEntitlement: true, capture: ${JSON.stringify(capture)}, externalSceneVideo: ${mode === 'combined'} })`,
1743
1747
  })
1744
1748
  if (ok === true) return { ok: true, error: null }
1745
1749
  // the engine knows exactly why it refused — read the structured reason
@@ -1748,7 +1752,7 @@ async function evalStoreStart(
1748
1752
  const error: string | null = await bridge
1749
1753
  .send({
1750
1754
  type: 'evaluate',
1751
- code: `(window.SootSim?.bridges?.getRecordingStartError?.() ?? null)`,
1755
+ code: `(globalThis.SootSim?.bridges?.getRecordingStartError?.() ?? null)`,
1752
1756
  })
1753
1757
  .catch(() => null)
1754
1758
  return { ok: false, error: error ?? null }
@@ -1762,7 +1766,7 @@ async function injectGitHubWorkflowIdentity(bridge: WsBridge): Promise<void> {
1762
1766
  : null
1763
1767
  await bridge.send({
1764
1768
  type: 'evaluate',
1765
- code: `window.__peachGithubJobIdentity = ${JSON.stringify(identity)}`,
1769
+ code: `globalThis.__peachGithubJobIdentity = ${JSON.stringify(identity)}`,
1766
1770
  })
1767
1771
  }
1768
1772
 
@@ -1774,7 +1778,7 @@ async function evalStoreStop(bridge: WsBridge): Promise<boolean | 'pending'> {
1774
1778
  await injectGitHubWorkflowIdentity(bridge)
1775
1779
  const stopped: boolean | 'pending' | null = await bridge.send({
1776
1780
  type: 'evaluate',
1777
- code: `window.SootSim.bridges.stopRecording()`,
1781
+ code: `globalThis.SootSim.bridges.stopRecording()`,
1778
1782
  })
1779
1783
  return stopped === 'pending' ? stopped : stopped !== false
1780
1784
  }
@@ -1805,7 +1809,7 @@ async function pollStoreUpload(bridge: WsBridge): Promise<StoreUploadResult> {
1805
1809
  } | null = await bridge.send({
1806
1810
  type: 'evaluate',
1807
1811
  // remaining time is computed in the page, so the two clocks never mix.
1808
- code: `(() => { const s = window.SootSim?.bridges?.getRecordingState?.(); return s ? { state: s.state, lastUpload: s.lastUpload, uploadError: s.uploadError, uploadRemainingMs: s.uploadDeadlineAtMs == null ? null : s.uploadDeadlineAtMs - Date.now() } : null })()`,
1812
+ code: `(() => { const s = globalThis.SootSim?.bridges?.getRecordingState?.(); return s ? { state: s.state, lastUpload: s.lastUpload, uploadError: s.uploadError, uploadRemainingMs: s.uploadDeadlineAtMs == null ? null : s.uploadDeadlineAtMs - Date.now() } : null })()`,
1809
1813
  })
1810
1814
  if (snap && snap.state === 'processing' && snap.uploadRemainingMs != null) {
1811
1815
  deadline = Math.max(
@@ -63,7 +63,7 @@ export async function runShellBooleanMode(
63
63
  }))
64
64
  const deadline = Date.now() + 1200
65
65
  while (Date.now() < deadline) {
66
- if (Boolean(window.SootSim?.bridges?.settings?.get?.()?.[modeKey]) === target) {
66
+ if (Boolean(globalThis.SootSim?.bridges?.settings?.get?.()?.[modeKey]) === target) {
67
67
  return true
68
68
  }
69
69
  await new Promise((resolve) => setTimeout(resolve, 40))