@volter/editor-game 0.5.65
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/LICENSE +661 -0
- package/NOTICE +23 -0
- package/contributions/asset-budget-asset.menu.ts +26 -0
- package/contributions/asset-budget.action.ts +18 -0
- package/contributions/asset-budget.document.tsx +21 -0
- package/contributions/asset-budget.menu.ts +22 -0
- package/contributions/audio-unlock.service.ts +17 -0
- package/contributions/audio.utility.tsx +15 -0
- package/contributions/autoplay.service.ts +48 -0
- package/contributions/bridge.command.ts +172 -0
- package/contributions/build-profiles.document.tsx +21 -0
- package/contributions/build-progress.status.tsx +45 -0
- package/contributions/build.action.ts +26 -0
- package/contributions/build.command.ts +27 -0
- package/contributions/build.header.tsx +37 -0
- package/contributions/build.menu.ts +31 -0
- package/contributions/build.service.ts +31 -0
- package/contributions/connection.status.tsx +43 -0
- package/contributions/coverage.service.ts +118 -0
- package/contributions/edit-mode-audio.service.ts +23 -0
- package/contributions/edit-mode-networking.service.ts +30 -0
- package/contributions/game-document.service.ts +25 -0
- package/contributions/game-eval.command.ts +210 -0
- package/contributions/game.layout.ts +19 -0
- package/contributions/gameplay.command.ts +255 -0
- package/contributions/generation.service.ts +96 -0
- package/contributions/generations.status.tsx +53 -0
- package/contributions/ingest.service.ts +80 -0
- package/contributions/instances.command.ts +75 -0
- package/contributions/navmesh.menu.ts +39 -0
- package/contributions/navmesh.service.ts +19 -0
- package/contributions/network.utility.tsx +17 -0
- package/contributions/play.command.ts +355 -0
- package/contributions/profiler.action.ts +16 -0
- package/contributions/profiler.menu.ts +22 -0
- package/contributions/profiler.utility.tsx +17 -0
- package/contributions/react/component-board.service.ts +21 -0
- package/contributions/react/design-time-mount.service.ts +60 -0
- package/contributions/react/pasteboard.action.ts +41 -0
- package/contributions/react/react-inspector.service.ts +85 -0
- package/contributions/react/story-documents.service.ts +44 -0
- package/contributions/scene-document.service.ts +32 -0
- package/contributions/state-watch.action.ts +17 -0
- package/contributions/state-watch.menu.ts +23 -0
- package/contributions/state-watch.utility.tsx +20 -0
- package/contributions/team-playtest.service.ts +124 -0
- package/contributions/three/camera-runtime.inspector.tsx +34 -0
- package/contributions/three/component-board.service.ts +24 -0
- package/contributions/three/component-verbs.command.ts +110 -0
- package/contributions/three/component-verbs.service.ts +92 -0
- package/contributions/three/constraints.inspector.tsx +34 -0
- package/contributions/three/model-asset-sections.service.ts +26 -0
- package/contributions/three/reflection-probe-capture.inspector.tsx +32 -0
- package/contributions/three/story-documents.service.ts +31 -0
- package/contributions/three/three-authoring.service.ts +66 -0
- package/contributions/transport.header.tsx +19 -0
- package/contributions/xstate-behavior.action.ts +42 -0
- package/contributions/xstate-behavior.document.tsx +73 -0
- package/contributions/xstate-behavior.inspector.tsx +28 -0
- package/contributions/xstate-behavior.menu.ts +23 -0
- package/package.json +144 -0
- package/src/asset-budget/AssetBudgetPanel.tsx +1172 -0
- package/src/asset-budget/asset-budget-model.ts +799 -0
- package/src/asset-budget/basis-encoder.ts +165 -0
- package/src/asset-budget/gltf-io.ts +154 -0
- package/src/asset-budget/gltf-optimize.ts +384 -0
- package/src/asset-budget/image-dims.ts +78 -0
- package/src/asset-budget/optimize-apply.ts +221 -0
- package/src/audio/AudioDebuggerPanel.tsx +457 -0
- package/src/audio/audio-debugger-model.ts +87 -0
- package/src/bridge/call.ts +60 -0
- package/src/bridge/dispatch.ts +459 -0
- package/src/bridge/live-frames.ts +25 -0
- package/src/bridge/screenshot.ts +316 -0
- package/src/build/BuildProfilesPanel.tsx +476 -0
- package/src/build/build-session.ts +247 -0
- package/src/build/format-bytes.ts +14 -0
- package/src/command-results.ts +36 -0
- package/src/coverage/live-authoring-surface.ts +30 -0
- package/src/coverage/live-project-verbs.ts +162 -0
- package/src/coverage/native-system-coverage.ts +143 -0
- package/src/coverage/root-coverage.ts +79 -0
- package/src/coverage/session-coverage.ts +193 -0
- package/src/design-system-stories/ApplicationChrome.stories.tsx +100 -0
- package/src/design-system-stories/InspectorNarrowBodies.stories.tsx +406 -0
- package/src/edit-mode/edit-mode-audio.ts +56 -0
- package/src/edit-mode/edit-mode-networking.ts +112 -0
- package/src/game-document/DevicePresetPicker.tsx +84 -0
- package/src/game-document/GameCaptureFrameButton.tsx +45 -0
- package/src/game-document/GameDocument.tsx +215 -0
- package/src/game-document/GamePanel.tsx +662 -0
- package/src/game-document/InstanceInspectorPicker.tsx +183 -0
- package/src/game-document/crowd-debug.ts +183 -0
- package/src/game-document/device-preview.ts +336 -0
- package/src/game-document/game-view-store.ts +107 -0
- package/src/game-document/physics-debug.ts +187 -0
- package/src/generation/GenerationActivity.tsx +421 -0
- package/src/generation/GenerationGallery.css +231 -0
- package/src/generation/generation-documents.tsx +338 -0
- package/src/generation/generation-jobs.ts +128 -0
- package/src/generation/generation-presentation.ts +257 -0
- package/src/host/adapter-reach.ts +445 -0
- package/src/host/adapter-runtime-bindings.ts +303 -0
- package/src/host/after-paint.ts +112 -0
- package/src/host/api/configurations.ts +66 -0
- package/src/host/authoring/babylon-authoring-adapter.ts +703 -0
- package/src/host/authoring/canvas-runtime-recognition.ts +50 -0
- package/src/host/authoring/contract-hierarchy-authoring.ts +144 -0
- package/src/host/authoring/contract-scenes-stories.ts +204 -0
- package/src/host/authoring/creation-site-related.ts +55 -0
- package/src/host/authoring/ephemeral-persistence.ts +29 -0
- package/src/host/authoring/gesture-persist.ts +84 -0
- package/src/host/authoring/ingest-data-writer.ts +232 -0
- package/src/host/authoring/ingest-source-persistence.ts +826 -0
- package/src/host/authoring/mount-isolated-pixi-screen.ts +250 -0
- package/src/host/authoring/mounted-authoring.ts +41 -0
- package/src/host/authoring/owned-pixi-ticker-listeners.ts +96 -0
- package/src/host/authoring/phaser-live-authoring-adapter.ts +265 -0
- package/src/host/authoring/pixi-authoring-adapter.ts +1554 -0
- package/src/host/authoring/pixi-creation-site-write-target.ts +60 -0
- package/src/host/authoring/pixi-isolation-assets.ts +25 -0
- package/src/host/authoring/pixi-live-write-target.ts +979 -0
- package/src/host/authoring/pixi-source-identity.ts +141 -0
- package/src/host/authoring/pixi-still-presentation.ts +71 -0
- package/src/host/authoring/pixi-structure-history.ts +237 -0
- package/src/host/authoring/pixi-transform-channels.ts +205 -0
- package/src/host/authoring/selection-remount-handoff.ts +23 -0
- package/src/host/authoring/source-persistence-backend.ts +373 -0
- package/src/host/authoring/source-refresh-revisions.ts +81 -0
- package/src/host/authoring/struct-write-pipe.ts +143 -0
- package/src/host/auto-frame-window.ts +89 -0
- package/src/host/binding-resolver.ts +393 -0
- package/src/host/browser-transpile.ts +631 -0
- package/src/host/canvas-entry-runtime.ts +95 -0
- package/src/host/components/CameraAuthoringOverlay.tsx +216 -0
- package/src/host/components/HeaderTelemetry.tsx +341 -0
- package/src/host/components/PixiIsolationSceneContent.tsx +280 -0
- package/src/host/components/ResolutionPicker.tsx +89 -0
- package/src/host/components/ThreeIsolationSceneContent.tsx +180 -0
- package/src/host/components/frame-debugger-model.ts +579 -0
- package/src/host/components/header-telemetry-model.ts +74 -0
- package/src/host/components/scene-document.tsx +447 -0
- package/src/host/components/utility-view-state.ts +87 -0
- package/src/host/components/world-root-stage-binding.tsx +133 -0
- package/src/host/components/world-root-stage.ts +1046 -0
- package/src/host/coverage/authoring-read-probe.ts +583 -0
- package/src/host/coverage/capability-coverage.ts +1587 -0
- package/src/host/coverage/coverage-accounting.ts +261 -0
- package/src/host/coverage/game-contract-seam-evidence.ts +82 -0
- package/src/host/coverage/project-verb-coverage.ts +148 -0
- package/src/host/coverage/system-adapter-coverage.ts +373 -0
- package/src/host/design-system-stories/StoryLayout.tsx +104 -0
- package/src/host/design-system-stories/fixtures/authoring.ts +247 -0
- package/src/host/design-system-stories/fixtures/editor-runtime.tsx +115 -0
- package/src/host/document-preview-three.ts +110 -0
- package/src/host/entry-adjudication.ts +89 -0
- package/src/host/game-location-guard.ts +150 -0
- package/src/host/game-module-access.ts +196 -0
- package/src/host/game-realm-page.ts +358 -0
- package/src/host/game-realm-reclaim.ts +55 -0
- package/src/host/game-realm-storage.ts +104 -0
- package/src/host/gameplay-export.ts +288 -0
- package/src/host/gameplay-recording.ts +717 -0
- package/src/host/gated-globals.ts +1511 -0
- package/src/host/history/json-history-resource.ts +254 -0
- package/src/host/ingest/registry.ts +261 -0
- package/src/host/instance-extract-actions.ts +120 -0
- package/src/host/instance-fork-actions.ts +120 -0
- package/src/host/play-control-hook.ts +26 -0
- package/src/host/playwright-shim.ts +479 -0
- package/src/host/projection/dom.ts +295 -0
- package/src/host/projection/pixi.ts +288 -0
- package/src/host/r3f-entry-runtime.ts +77 -0
- package/src/host/react-mount-runtime.ts +162 -0
- package/src/host/realm-services.ts +148 -0
- package/src/host/recording-preview.ts +106 -0
- package/src/host/roots/module-root.ts +203 -0
- package/src/host/roots/react-root.ts +181 -0
- package/src/host/same-realm-loop-gate.ts +544 -0
- package/src/host/scene-view-drawability.ts +69 -0
- package/src/host/sdk/tools.ts +31 -0
- package/src/host/served-bundle-runtime-modules.ts +331 -0
- package/src/host/server-log-bridge.ts +60 -0
- package/src/host/staged-projects.ts +24 -0
- package/src/host/stories/mounted-story-viewport-source.ts +64 -0
- package/src/host/stories/story-arg-descriptors.ts +50 -0
- package/src/host/stories/story-media-presence.ts +152 -0
- package/src/host/surface-content.ts +87 -0
- package/src/host/take-named-export.ts +23 -0
- package/src/host/three-ingest-runtime.ts +76 -0
- package/src/host/types-fastnoise-lite.d.ts +7 -0
- package/src/host/types-mikktspace.d.ts +20 -0
- package/src/host/types-troika-three-text.d.ts +7 -0
- package/src/host/use-active-performance-source.ts +53 -0
- package/src/host/viewport-pose-memory.ts +48 -0
- package/src/host/viewport-root-presentation.ts +40 -0
- package/src/ingest/active-ingest.ts +251 -0
- package/src/ingest/active-scene-navigation.ts +43 -0
- package/src/ingest/authoring/ingest-dom-surface-authoring.ts +191 -0
- package/src/ingest/authoring/ingest-root-adapter.ts +897 -0
- package/src/ingest/capture-wait-report.ts +122 -0
- package/src/ingest/deferred-ingest-play.ts +216 -0
- package/src/ingest/deferred-ingest-session.ts +23 -0
- package/src/ingest/discovery-public-ingest.ts +242 -0
- package/src/ingest/dom-stub-mark.ts +7 -0
- package/src/ingest/entry-load.ts +56 -0
- package/src/ingest/game-contract-realm.ts +34 -0
- package/src/ingest/game-pointer-lock.ts +89 -0
- package/src/ingest/held-scene-repaint.ts +73 -0
- package/src/ingest/host-surface-box.ts +174 -0
- package/src/ingest/ingest-boot-viewport.ts +54 -0
- package/src/ingest/ingest-canvas-scene-document.tsx +177 -0
- package/src/ingest/ingest-canvas-scene.ts +50 -0
- package/src/ingest/ingest-evidence-hook.ts +131 -0
- package/src/ingest/ingest-frame-snapshot.ts +183 -0
- package/src/ingest/ingest-play-commands.ts +134 -0
- package/src/ingest/ingest-play-control.ts +299 -0
- package/src/ingest/ingest-render-debug.ts +320 -0
- package/src/ingest/ingest-siblings.ts +487 -0
- package/src/ingest/ingest-status.ts +62 -0
- package/src/ingest/live-ingest-facet.ts +46 -0
- package/src/ingest/module-mode.ts +229 -0
- package/src/ingest/mount-canvas-ingest-root.ts +899 -0
- package/src/ingest/mount-coverage.ts +296 -0
- package/src/ingest/mount-dom-ingest-root.ts +282 -0
- package/src/ingest/mount-ingest-root.ts +744 -0
- package/src/ingest/mount-three-ingest-root.ts +366 -0
- package/src/ingest/resolve-canvas.ts +47 -0
- package/src/ingest/resolve-three.ts +123 -0
- package/src/ingest/served-bundle.ts +109 -0
- package/src/ingest/served-html-boot.ts +292 -0
- package/src/ingest/surface-canvas.ts +84 -0
- package/src/ingest/surface-dom.ts +84 -0
- package/src/ingest/surface-three.ts +128 -0
- package/src/ingest/types.ts +76 -0
- package/src/ingest/unmount-ingest-root.ts +224 -0
- package/src/navmesh/navmesh-actions.ts +27 -0
- package/src/navmesh/navmesh-handler.ts +237 -0
- package/src/navmesh/navmesh-workflow-store.ts +78 -0
- package/src/network/NetworkInspectorPanel.tsx +644 -0
- package/src/network/network-inspector-model.ts +225 -0
- package/src/play/play-boot-stall.ts +118 -0
- package/src/play/play-log-events.ts +26 -0
- package/src/play/play-mode.ts +2553 -0
- package/src/play/play-recording.ts +335 -0
- package/src/play/react-play-live-authoring.ts +165 -0
- package/src/play-bar/PlayBar.tsx +488 -0
- package/src/play-bar/PlayerCountPicker.tsx +100 -0
- package/src/profiler/FrameDebuggerPanel.tsx +458 -0
- package/src/profiler/PerformancePanel.tsx +1068 -0
- package/src/profiler/ProfilerPanel.tsx +53 -0
- package/src/profiler/frame-debugger-store.ts +97 -0
- package/src/profiler/main-thread-busy.ts +90 -0
- package/src/react/design-time-react-mount.ts +782 -0
- package/src/react/dom-authoring-adapter.ts +764 -0
- package/src/react/pasteboard-materialize.ts +154 -0
- package/src/react/react-inspector-section.tsx +2496 -0
- package/src/react/react-world-authoring-adapter.ts +3795 -0
- package/src/react/story-documents/story-args-section.ts +19 -0
- package/src/react/story-documents/story-documents.tsx +1380 -0
- package/src/react/story-paint-bounds.ts +94 -0
- package/src/react/ui-board-document.tsx +101 -0
- package/src/react/ui-board-title.ts +9 -0
- package/src/react/ui-component-board.ts +72 -0
- package/src/services/audio-pose-guard.ts +81 -0
- package/src/services/game-audio-unlock.ts +48 -0
- package/src/state-watch/StateWatchPanel.tsx +535 -0
- package/src/three/authoring/camera-runtime-inspector-section.tsx +135 -0
- package/src/three/authoring/constraint-inspector-section.tsx +189 -0
- package/src/three/authoring/design-time-renderer.ts +189 -0
- package/src/three/authoring/model-asset-inspector-section.css +41 -0
- package/src/three/authoring/model-asset-inspector-section.tsx +869 -0
- package/src/three/authoring/oid-source-persistence.ts +557 -0
- package/src/three/authoring/r3f-design-session.ts +1174 -0
- package/src/three/authoring/r3f-source-authoring-adapter.ts +5949 -0
- package/src/three/authoring/reflection-probe-inspector-section.tsx +76 -0
- package/src/three/authoring/spatial-audio-handles.ts +215 -0
- package/src/three/authoring/spatial-collider-handles.ts +229 -0
- package/src/three/authoring/spatial-joint-handles.ts +114 -0
- package/src/three/authoring/spatial-light-handles.ts +178 -0
- package/src/three/authoring/spatial-lod-handles.ts +93 -0
- package/src/three/authoring/spatial-particle-handles.ts +368 -0
- package/src/three/authoring/three-authoring-adapter.ts +1953 -0
- package/src/three/authoring/three-scene-identity.ts +19 -0
- package/src/three/authoring/three-spatial-handles.ts +161 -0
- package/src/three/authoring/typed-three-inspector.ts +528 -0
- package/src/three/component-verbs/extract-menu.ts +71 -0
- package/src/three/component-verbs/fork-menu.ts +78 -0
- package/src/three/component-verbs/internals-menu.ts +91 -0
- package/src/three/story-documents/three-story-documents.tsx +607 -0
- package/src/three/three-board/ThreeBoardDocument.tsx +888 -0
- package/src/three/three-board/board-framing.ts +412 -0
- package/src/three/three-board/board-layout.ts +401 -0
- package/src/three/three-board/board-scene.ts +901 -0
- package/src/three/three-board/three-component-board.ts +62 -0
- package/src/xstate/XStateBehaviorSection.tsx +130 -0
- package/src/xstate/XStateMachineInspector.tsx +566 -0
- package/src/xstate/character-animation-machine.fixture.ts +74 -0
- package/src/xstate/live-behaviors.ts +106 -0
- package/src/xstate/use-live-actor-state.ts +44 -0
- package/src/xstate/xstate-graph.ts +235 -0
- package/src/xstate/xstate-layout.ts +76 -0
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THE GAME'S PAGE, IN A BOX — the host-side half of "an ingested game runs
|
|
3
|
+
* in-realm, but it does not own the tab".
|
|
4
|
+
*
|
|
5
|
+
* `gated-globals.ts` already pipes a SCOPED `window`/`document` into game
|
|
6
|
+
* modules so their input listeners can be gated. This module is the same idea
|
|
7
|
+
* carried to everything else a page-owning game reaches for, because a game
|
|
8
|
+
* written to own the whole browser window says so in four ways that all break
|
|
9
|
+
* the editor when it runs inside a pane:
|
|
10
|
+
*
|
|
11
|
+
* 1. `document.body.appendChild(canvas)` — the game's canvas, title screen,
|
|
12
|
+
* pause overlay and game-over screen all land at PAGE level, painting over
|
|
13
|
+
* editor chrome (such a title wrapper is typically `position:fixed` with a
|
|
14
|
+
* z-index in the hundreds). Substituting the mount's adopted
|
|
15
|
+
* surface for `document.body` puts every one of them inside the pane
|
|
16
|
+
* instead — including the ones the host never sees, because it is the
|
|
17
|
+
* game's OWN append that is redirected, not a post-hoc reparent of the
|
|
18
|
+
* things we thought to look for. It is also what makes teardown total:
|
|
19
|
+
* removing the surface removes the game's whole DOM, overlays included.
|
|
20
|
+
* 2. `position:fixed` inside that surface would STILL escape to the viewport,
|
|
21
|
+
* so the surface declares {@link GAME_SURFACE_CONTAINMENT_CSS} — `contain:
|
|
22
|
+
* layout` makes it the containing block for fixed and absolute
|
|
23
|
+
* descendants, so a game's full-screen overlay is full-PANE instead. This
|
|
24
|
+
* is a host policy, not per-game CSS: every ingested game with page-level
|
|
25
|
+
* overlays gets it.
|
|
26
|
+
* 3. `window.innerWidth`/`innerHeight` + a `resize` listener is how a game
|
|
27
|
+
* computes its own letterbox, in its `onResize` handler and again at
|
|
28
|
+
* init. Reporting the real window to
|
|
29
|
+
* a game living in a pane is a lie that shows up as a stretched or
|
|
30
|
+
* cropped picture, so the realm reports the SURFACE's size and the host
|
|
31
|
+
* dispatches `resize` to the realm's own listeners when the pane changes.
|
|
32
|
+
* A game that letterboxes itself then letterboxes to the pane, with no
|
|
33
|
+
* host aspect policy to get wrong.
|
|
34
|
+
* 4. `window.location.href = …` navigates the EDITOR away (a game menu with a
|
|
35
|
+
* "quit" link does exactly this). See
|
|
36
|
+
* `game-location-guard.ts`.
|
|
37
|
+
*
|
|
38
|
+
* ONE page per game realm, registered by the mount that owns the surface and
|
|
39
|
+
* dropped when it disposes. Nothing here is per-game: the mount registers a
|
|
40
|
+
* surface, the realm reports it, and a game that never touches `document.body`
|
|
41
|
+
* or `window.innerWidth` cannot tell the difference.
|
|
42
|
+
*/
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* What the host stamps on an adopted surface so a game's `position:fixed`
|
|
46
|
+
* overlay is contained to the pane.
|
|
47
|
+
*
|
|
48
|
+
* `contain: layout` makes the element a containing block for absolutely AND
|
|
49
|
+
* fixed positioned descendants — the one CSS primitive that does this without
|
|
50
|
+
* the side effects of the alternatives (`transform`/`filter` also create the
|
|
51
|
+
* containing block but force a compositing layer and, on the ADOPTED element
|
|
52
|
+
* itself, are exactly the stamp {@link claimHostSurfaceBox} exists to clear).
|
|
53
|
+
*/
|
|
54
|
+
export const GAME_SURFACE_CONTAINMENT_CSS = 'contain: layout;';
|
|
55
|
+
|
|
56
|
+
/** `EventTarget`'s own listener identity is (type, callback, capture) — read
|
|
57
|
+
* the capture flag out of either options form so the page's registration
|
|
58
|
+
* bookkeeping dedupes exactly the way the bus does. */
|
|
59
|
+
function captureOf(options?: boolean | AddEventListenerOptions | EventListenerOptions): boolean {
|
|
60
|
+
return typeof options === 'boolean' ? options : (options?.capture ?? false);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Mark carrying the text this observer last wrote, so its own write is not
|
|
64
|
+
* re-scoped and a game's next rewrite is. */
|
|
65
|
+
export const SCOPED_STYLE_MARK = 'vgaiScopedCss';
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* The `@scope` rewriter, loaded on FIRST USE — the one page-side door to the
|
|
69
|
+
* CSS transform. The transform itself is a pure postcss function that happens
|
|
70
|
+
* to live under `server/` because the dev server's `/__editor/scoped-game-css`
|
|
71
|
+
* route is its other caller; the lazy import (and the reason it is lazy) is
|
|
72
|
+
* {@link observeInjectedStyles}'s note below, and every page-side caller shares
|
|
73
|
+
* this one memo so a realm that scopes both boot-time inline CSS and runtime
|
|
74
|
+
* injected `<style>`s parses the loader once.
|
|
75
|
+
*
|
|
76
|
+
* Named here rather than re-derived per caller because the `@editor/` alias is
|
|
77
|
+
* the only door a PACKAGE has into the host: the ingest lane's
|
|
78
|
+
* `served-html-boot.ts` scopes a served document's inline CSS through exactly
|
|
79
|
+
* this function, and a package reaching `packages/editor/server/` directly is
|
|
80
|
+
* not a seam that exists.
|
|
81
|
+
*/
|
|
82
|
+
let gameCssScoper: Promise<(css: string) => string> | null = null;
|
|
83
|
+
export function loadGameCssScoper(): Promise<(css: string) => string> {
|
|
84
|
+
gameCssScoper ??= Promise.all([
|
|
85
|
+
import('@volter/editor-core/game-css-scope-transform'),
|
|
86
|
+
import('@volter/editor-sdk/session/game-css-scope'),
|
|
87
|
+
]).then(
|
|
88
|
+
([{ scopeGameCss }, { GAME_CSS_SCOPE_SELECTOR }]) =>
|
|
89
|
+
(css: string) =>
|
|
90
|
+
scopeGameCss({ css, scopeSelector: GAME_CSS_SCOPE_SELECTOR, styleSheetPath: '/' }),
|
|
91
|
+
);
|
|
92
|
+
return gameCssScoper;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* CSS applies document-wide wherever its `<style>` sits, so a style element a
|
|
97
|
+
* game creates at RUNTIME — a CDN utility framework's live sheet, a game's own
|
|
98
|
+
* theme injection — would restyle the editor chrome from inside the realm's
|
|
99
|
+
* head div. This observer runs every added or rewritten `<style>` under the
|
|
100
|
+
* surface through the SAME `@scope` transform declared sheets and boot-time
|
|
101
|
+
* inline CSS get. The heavy transform (postcss) loads on first use; until it
|
|
102
|
+
* does, the raw text is cleared rather than left leaking — a game's sheet
|
|
103
|
+
* arriving a beat late is honest, the editor repainted by it is not.
|
|
104
|
+
*
|
|
105
|
+
* ## "First use" means the first STYLE, not the first surface
|
|
106
|
+
*
|
|
107
|
+
* The import below is deliberately started by {@link scopeStyle}, not by this
|
|
108
|
+
* function. Attaching a surface is not evidence that anything will ever inject
|
|
109
|
+
* a `<style>`, and the overwhelming majority of games never do — a first-party
|
|
110
|
+
* R3F/Pixi world paints into a canvas and touches no page CSS at all.
|
|
111
|
+
*
|
|
112
|
+
* Loading it on ATTACH made every Play in every project pay for a CSS parser
|
|
113
|
+
* it would not use, and the cost was not just bytes: `postcss` destructures
|
|
114
|
+
* `fs`/`path`/`url`/`source-map-js` at module scope, and Vite honours postcss's
|
|
115
|
+
* own `browser` field by substituting a proxy that console.warns per property
|
|
116
|
+
* read. So a bare scaffolded project pressing Play emitted THIRTEEN
|
|
117
|
+
* "Module … has been externalized for browser compatibility" warnings —
|
|
118
|
+
* `fs.existsSync/readFileSync/realpathSync`,
|
|
119
|
+
* `path.isAbsolute/resolve/dirname/join/relative/sep`,
|
|
120
|
+
* `url.fileURLToPath/pathToFileURL`,
|
|
121
|
+
* `source-map-js.SourceMapConsumer/SourceMapGenerator` — straight into the
|
|
122
|
+
* game realm's console, which made the console-silence contract unreachable in
|
|
123
|
+
* a fresh scaffold and every `vgai` verb exit non-zero (measured three times:
|
|
124
|
+
* the donut, cold-barrel and lit-lanes probes). The chain was
|
|
125
|
+
* `play-mode.enterPlayModeInner` -> `gated-globals.setGameSurface` ->
|
|
126
|
+
* `GameRealmPage.surface` -> `observeInjectedStyles` -> `import('../server/
|
|
127
|
+
* scoped-game-css')` -> `postcss`. A game that genuinely injects a runtime
|
|
128
|
+
* `<style>` still loads it, because there the parser is the feature working.
|
|
129
|
+
*/
|
|
130
|
+
function observeInjectedStyles(surface: HTMLElement): MutationObserver {
|
|
131
|
+
let scoper: ((css: string) => string) | null = null;
|
|
132
|
+
const loadScoper = (): Promise<void> =>
|
|
133
|
+
loadGameCssScoper().then((loaded) => {
|
|
134
|
+
scoper = loaded;
|
|
135
|
+
});
|
|
136
|
+
const scopeStyle = (style: HTMLStyleElement): void => {
|
|
137
|
+
const text = style.textContent ?? '';
|
|
138
|
+
if (text.length === 0 || style.dataset[SCOPED_STYLE_MARK] === text) return;
|
|
139
|
+
if (!scoper) {
|
|
140
|
+
const raw = text;
|
|
141
|
+
style.textContent = '';
|
|
142
|
+
void loadScoper().then(() => {
|
|
143
|
+
if (style.isConnected && (style.textContent ?? '') === '') {
|
|
144
|
+
style.textContent = raw;
|
|
145
|
+
scopeStyle(style);
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
let scoped: string;
|
|
151
|
+
try {
|
|
152
|
+
scoped = scoper(text);
|
|
153
|
+
} catch {
|
|
154
|
+
style.textContent = ''; // unscopeable CSS stays uninstalled — never raw
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
style.dataset[SCOPED_STYLE_MARK] = scoped;
|
|
158
|
+
style.textContent = scoped;
|
|
159
|
+
};
|
|
160
|
+
const styleOf = (node: Node): HTMLStyleElement | null => {
|
|
161
|
+
if (node instanceof HTMLStyleElement) return node;
|
|
162
|
+
const parent = node.parentElement;
|
|
163
|
+
return parent instanceof HTMLStyleElement ? parent : null;
|
|
164
|
+
};
|
|
165
|
+
const observer = new MutationObserver((records) => {
|
|
166
|
+
const seen = new Set<HTMLStyleElement>();
|
|
167
|
+
for (const record of records) {
|
|
168
|
+
if (record.type === 'childList') {
|
|
169
|
+
for (const node of record.addedNodes) {
|
|
170
|
+
const direct = styleOf(node);
|
|
171
|
+
if (direct) seen.add(direct);
|
|
172
|
+
else if (node instanceof Element) {
|
|
173
|
+
for (const style of node.querySelectorAll('style')) seen.add(style);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
} else if (record.type === 'characterData') {
|
|
177
|
+
const style = styleOf(record.target);
|
|
178
|
+
if (style) seen.add(style);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
for (const style of seen) if (style.isConnected) scopeStyle(style);
|
|
182
|
+
});
|
|
183
|
+
observer.observe(surface, { childList: true, subtree: true, characterData: true });
|
|
184
|
+
for (const style of surface.querySelectorAll('style')) scopeStyle(style);
|
|
185
|
+
return observer;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* The page one game realm sees. Holds the surface element, the realm-scoped
|
|
190
|
+
* `resize` bus, and a live count of the game's own resize listeners — that
|
|
191
|
+
* count is the predicate the mount reads to decide whether the GAME resizes
|
|
192
|
+
* itself or the HOST resizes it (see `ingest-root-adapter.ts`'s `resize`).
|
|
193
|
+
*/
|
|
194
|
+
export class GameRealmPage {
|
|
195
|
+
/** The element this realm sees as `document.body`. */
|
|
196
|
+
private surfaceElement: HTMLElement | null = null;
|
|
197
|
+
/** Scopes every `<style>` the game injects at runtime — see {@link attachSurface}. */
|
|
198
|
+
private injectedStyles: MutationObserver | null = null;
|
|
199
|
+
|
|
200
|
+
get surface(): HTMLElement | null {
|
|
201
|
+
return this.surfaceElement;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
set surface(el: HTMLElement | null) {
|
|
205
|
+
this.injectedStyles?.disconnect();
|
|
206
|
+
this.injectedStyles = null;
|
|
207
|
+
this.surfaceElement = el;
|
|
208
|
+
if (el) this.injectedStyles = observeInjectedStyles(el);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/** Page metadata is realm state, not permission to rename the editor tab. */
|
|
212
|
+
private pageTitle = '';
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* The classic script the host is evaluating for this page.
|
|
216
|
+
*
|
|
217
|
+
* A served bundle still runs through the realm-shadowing prelude rather than
|
|
218
|
+
* directly in the editor tab. That means the browser cannot populate
|
|
219
|
+
* `document.currentScript` for it, even though webpack's automatic public
|
|
220
|
+
* path runtime requires that ordinary classic-script signal. The served
|
|
221
|
+
* document loader carries the declared entry here for exactly the duration
|
|
222
|
+
* of evaluation, and the document proxy reports it below.
|
|
223
|
+
*/
|
|
224
|
+
private executingClassicScript: HTMLScriptElement | null = null;
|
|
225
|
+
|
|
226
|
+
/** A connected, surface-owned destination for page-level style/script/link
|
|
227
|
+
* inserts. It intentionally is not the editor's real `document.head`. */
|
|
228
|
+
private headElement: HTMLElement | null = null;
|
|
229
|
+
|
|
230
|
+
/** Where the realm's own `window.addEventListener('resize', …)` lands.
|
|
231
|
+
* Replaced wholesale by {@link reset} — `EventTarget` has no "remove them
|
|
232
|
+
* all", and a dead game's handlers must not survive into the next mount. */
|
|
233
|
+
private resizeBus = new EventTarget();
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* The game's live `resize` registrations, keyed exactly the way `EventTarget`
|
|
237
|
+
* itself keys them: (listener, capture). A plain counter was WRONG in both
|
|
238
|
+
* directions — a duplicate `addEventListener` with the same pair bumped it
|
|
239
|
+
* while the bus deduped, and a `removeEventListener` that matched nothing
|
|
240
|
+
* decremented it anyway — and the count is not bookkeeping: it IS the
|
|
241
|
+
* predicate {@link gameResizesItself} the mount reads to decide whether the
|
|
242
|
+
* host may size the game.
|
|
243
|
+
*/
|
|
244
|
+
private readonly resizeListeners = new Map<EventListenerOrEventListenerObject, Set<boolean>>();
|
|
245
|
+
|
|
246
|
+
addResizeListener(
|
|
247
|
+
listener: EventListenerOrEventListenerObject,
|
|
248
|
+
options?: boolean | AddEventListenerOptions,
|
|
249
|
+
): void {
|
|
250
|
+
let captures = this.resizeListeners.get(listener);
|
|
251
|
+
if (!captures) {
|
|
252
|
+
captures = new Set();
|
|
253
|
+
this.resizeListeners.set(listener, captures);
|
|
254
|
+
}
|
|
255
|
+
captures.add(captureOf(options));
|
|
256
|
+
this.resizeBus.addEventListener('resize', listener, options);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
removeResizeListener(
|
|
260
|
+
listener: EventListenerOrEventListenerObject,
|
|
261
|
+
options?: boolean | EventListenerOptions,
|
|
262
|
+
): void {
|
|
263
|
+
const captures = this.resizeListeners.get(listener);
|
|
264
|
+
if (captures) {
|
|
265
|
+
captures.delete(captureOf(options));
|
|
266
|
+
if (captures.size === 0) this.resizeListeners.delete(listener);
|
|
267
|
+
}
|
|
268
|
+
this.resizeBus.removeEventListener('resize', listener, options);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* True when the game registered at least one `resize` listener of its own —
|
|
273
|
+
* i.e. it has a size policy and the host must not overwrite it.
|
|
274
|
+
*/
|
|
275
|
+
get gameResizesItself(): boolean {
|
|
276
|
+
return this.resizeListeners.size > 0;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Return the page to "no game is running".
|
|
281
|
+
*
|
|
282
|
+
* A page OUTLIVES the mount that registered its surface — it is keyed by
|
|
283
|
+
* realm id in `gated-globals.ts`'s `pages` map, and the realm's proxies close
|
|
284
|
+
* over the object, so it cannot be dropped and replaced. Clearing only the
|
|
285
|
+
* surface therefore leaked a dead game's size POLICY into the next mount in
|
|
286
|
+
* the same tab: the second ingest inherited `gameResizesItself === true` from
|
|
287
|
+
* a game that no longer exists, and the host stopped sizing it. The listeners
|
|
288
|
+
* on the bus leaked with it.
|
|
289
|
+
*/
|
|
290
|
+
reset(): void {
|
|
291
|
+
this.headElement?.remove();
|
|
292
|
+
this.headElement = null;
|
|
293
|
+
this.pageTitle = '';
|
|
294
|
+
this.executingClassicScript = null;
|
|
295
|
+
this.surface = null;
|
|
296
|
+
this.resizeListeners.clear();
|
|
297
|
+
this.resizeBus = new EventTarget();
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/** Tell the game its window changed size. Called by the mount's pane
|
|
301
|
+
* ResizeObserver; a no-op for a game that registered nothing. */
|
|
302
|
+
dispatchResize(): void {
|
|
303
|
+
this.resizeBus.dispatchEvent(new Event('resize'));
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/** The realm's `window.innerWidth`/`innerHeight`, or `null` before a surface
|
|
307
|
+
* is registered (then the real window's values stand). */
|
|
308
|
+
viewport(): { width: number; height: number } | null {
|
|
309
|
+
const el = this.surface;
|
|
310
|
+
if (!el) return null;
|
|
311
|
+
return { width: el.clientWidth, height: el.clientHeight };
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
get title(): string {
|
|
315
|
+
return this.pageTitle;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
set title(value: string) {
|
|
319
|
+
this.pageTitle = String(value);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/** What this realm sees as `document.currentScript`. */
|
|
323
|
+
get currentScript(): HTMLScriptElement | null {
|
|
324
|
+
return this.executingClassicScript;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Evaluate one host-loaded classic entry with the same `currentScript.src`
|
|
329
|
+
* its real `<script src>` would have supplied. Restoration is stack-safe so
|
|
330
|
+
* a nested loader cannot leak its carrier into its caller.
|
|
331
|
+
*/
|
|
332
|
+
withCurrentScript<T>(src: string, run: () => T): T {
|
|
333
|
+
const previous = this.executingClassicScript;
|
|
334
|
+
const script = document.createElement('script');
|
|
335
|
+
script.src = src;
|
|
336
|
+
this.executingClassicScript = script;
|
|
337
|
+
try {
|
|
338
|
+
return run();
|
|
339
|
+
} finally {
|
|
340
|
+
this.executingClassicScript = previous;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/** The game's page head, contained by its mount. Created lazily so a game
|
|
345
|
+
* that never uses page-level inserts adds no DOM of its own. */
|
|
346
|
+
head(): HTMLElement | null {
|
|
347
|
+
const surface = this.surface;
|
|
348
|
+
if (!surface) return null;
|
|
349
|
+
if (!this.headElement || this.headElement.parentElement !== surface) {
|
|
350
|
+
const head = document.createElement('div');
|
|
351
|
+
head.dataset['vgaiGameHead'] = '';
|
|
352
|
+
head.hidden = true;
|
|
353
|
+
surface.prepend(head);
|
|
354
|
+
this.headElement = head;
|
|
355
|
+
}
|
|
356
|
+
return this.headElement;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The ONE way a mounted game's browser-resource realm is ended.
|
|
3
|
+
*
|
|
4
|
+
* Every surface that runs game code in this tab — first-party play, an extra
|
|
5
|
+
* play instance, and each of the three ingest teardowns — owns a
|
|
6
|
+
* `gated-globals.ts` realm: the window/document proxies its listeners
|
|
7
|
+
* registered through, its timer/frame gate, and its page. Ending a mount and
|
|
8
|
+
* NOT ending its realm is invisible (nothing throws, nothing renders wrong) and
|
|
9
|
+
* permanent: the listeners stay attached to the real `window`, and the next
|
|
10
|
+
* mount under the same id inherits them.
|
|
11
|
+
*
|
|
12
|
+
* Extracted so play mode and ingest cannot drift — ingest used to reset the
|
|
13
|
+
* input gate to always-true and stop there, which left a dead game's listeners
|
|
14
|
+
* attached AND ungated.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { editorConsole } from '@volter/editor-core/editor-console';
|
|
18
|
+
import { disposeGameRealm } from './gated-globals';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* End one mount's realm and report anything the game left behind.
|
|
22
|
+
*
|
|
23
|
+
* `id` is the mount id; `''` (the default) is the DEFAULT realm — what ingest
|
|
24
|
+
* and every module served without a mount id resolve through. `name` is what
|
|
25
|
+
* the leak report calls the stopped game.
|
|
26
|
+
*
|
|
27
|
+
* The report is the point: a game that released everything it registered is
|
|
28
|
+
* silent, and a game that did not is named, with the count, on the editor
|
|
29
|
+
* console (and therefore in `vgai console`). The editor reclaims either way.
|
|
30
|
+
*/
|
|
31
|
+
export function reclaimGameRealm(id: string, name: string): void {
|
|
32
|
+
const released = disposeGameRealm(id);
|
|
33
|
+
const timerCount =
|
|
34
|
+
released.timers.timeouts +
|
|
35
|
+
released.timers.intervals +
|
|
36
|
+
released.timers.animationFrames +
|
|
37
|
+
released.timers.parkedCallbacks;
|
|
38
|
+
if (released.listeners === 0 && timerCount === 0) return;
|
|
39
|
+
const parts: string[] = [];
|
|
40
|
+
if (released.listeners > 0) {
|
|
41
|
+
parts.push(
|
|
42
|
+
`${released.listeners} window/document listener${released.listeners === 1 ? '' : 's'}`,
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
if (timerCount > 0) {
|
|
46
|
+
parts.push(`${timerCount} timer/frame callback${timerCount === 1 ? '' : 's'}`);
|
|
47
|
+
}
|
|
48
|
+
editorConsole.logStructured(
|
|
49
|
+
'warn',
|
|
50
|
+
`${name} stopped without releasing ${parts.join(' and ')}; the editor reclaimed them.`,
|
|
51
|
+
'environment',
|
|
52
|
+
undefined,
|
|
53
|
+
{ instanceId: id, instanceName: name },
|
|
54
|
+
);
|
|
55
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A game-realm view of ordinary same-origin browser storage.
|
|
3
|
+
*
|
|
4
|
+
* Storage DATA stays shared, exactly as it is for two tabs on one origin. The
|
|
5
|
+
* facade exists for a different reason: project code must not receive the
|
|
6
|
+
* editor's actual Storage instance and whose calls otherwise cannot be
|
|
7
|
+
* attributed to the mount that made them. A transient mount id is not a
|
|
8
|
+
* durable profile id, so this deliberately does not prefix, copy, or
|
|
9
|
+
* virtualize keys.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
export interface GameRealmStorageStats {
|
|
13
|
+
readonly reads: number;
|
|
14
|
+
readonly writes: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface GameRealmStorageView {
|
|
18
|
+
readonly storage: Storage;
|
|
19
|
+
stats(): GameRealmStorageStats;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function createGameRealmStorage(source: Storage): GameRealmStorageView {
|
|
23
|
+
let reads = 0;
|
|
24
|
+
let writes = 0;
|
|
25
|
+
const target = Object.create(Object.getPrototypeOf(source)) as object;
|
|
26
|
+
|
|
27
|
+
const getItem = (key: string): string | null => {
|
|
28
|
+
reads += 1;
|
|
29
|
+
return source.getItem(String(key));
|
|
30
|
+
};
|
|
31
|
+
const setItem = (key: string, value: string): void => {
|
|
32
|
+
writes += 1;
|
|
33
|
+
source.setItem(String(key), String(value));
|
|
34
|
+
};
|
|
35
|
+
const removeItem = (key: string): void => {
|
|
36
|
+
writes += 1;
|
|
37
|
+
source.removeItem(String(key));
|
|
38
|
+
};
|
|
39
|
+
const clear = (): void => {
|
|
40
|
+
writes += 1;
|
|
41
|
+
source.clear();
|
|
42
|
+
};
|
|
43
|
+
const key = (index: number): string | null => {
|
|
44
|
+
reads += 1;
|
|
45
|
+
return source.key(Number(index));
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const facade = new Proxy(target, {
|
|
49
|
+
get(_target, property) {
|
|
50
|
+
if (property === 'length') {
|
|
51
|
+
reads += 1;
|
|
52
|
+
return source.length;
|
|
53
|
+
}
|
|
54
|
+
if (property === 'getItem') return getItem;
|
|
55
|
+
if (property === 'setItem') return setItem;
|
|
56
|
+
if (property === 'removeItem') return removeItem;
|
|
57
|
+
if (property === 'clear') return clear;
|
|
58
|
+
if (property === 'key') return key;
|
|
59
|
+
if (property === Symbol.toStringTag) return 'Storage';
|
|
60
|
+
if (typeof property === 'string') return getItem(property) ?? undefined;
|
|
61
|
+
return Reflect.get(target, property, target);
|
|
62
|
+
},
|
|
63
|
+
set(_target, property, value) {
|
|
64
|
+
if (typeof property !== 'string') return Reflect.set(target, property, value, target);
|
|
65
|
+
setItem(property, String(value));
|
|
66
|
+
return true;
|
|
67
|
+
},
|
|
68
|
+
deleteProperty(_target, property) {
|
|
69
|
+
if (typeof property !== 'string') return Reflect.deleteProperty(target, property);
|
|
70
|
+
removeItem(property);
|
|
71
|
+
return true;
|
|
72
|
+
},
|
|
73
|
+
defineProperty(_target, property, descriptor) {
|
|
74
|
+
if (typeof property !== 'string' || !('value' in descriptor)) return false;
|
|
75
|
+
setItem(property, String(descriptor.value));
|
|
76
|
+
return true;
|
|
77
|
+
},
|
|
78
|
+
has(_target, property) {
|
|
79
|
+
if (typeof property !== 'string') return Reflect.has(target, property);
|
|
80
|
+
if (property in target) return true;
|
|
81
|
+
return getItem(property) !== null;
|
|
82
|
+
},
|
|
83
|
+
ownKeys() {
|
|
84
|
+
const keys: string[] = [];
|
|
85
|
+
for (let index = 0; index < source.length; index += 1) {
|
|
86
|
+
const name = source.key(index);
|
|
87
|
+
if (name !== null) keys.push(name);
|
|
88
|
+
}
|
|
89
|
+
return keys;
|
|
90
|
+
},
|
|
91
|
+
getOwnPropertyDescriptor(_target, property) {
|
|
92
|
+
if (typeof property !== 'string') return undefined;
|
|
93
|
+
const value = source.getItem(property);
|
|
94
|
+
return value === null
|
|
95
|
+
? undefined
|
|
96
|
+
: { configurable: true, enumerable: true, value, writable: true };
|
|
97
|
+
},
|
|
98
|
+
}) as Storage;
|
|
99
|
+
|
|
100
|
+
return {
|
|
101
|
+
storage: facade,
|
|
102
|
+
stats: () => ({ reads, writes }),
|
|
103
|
+
};
|
|
104
|
+
}
|