@inventure71/paddockjs 1.0.0 → 2.0.1

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 (203) hide show
  1. package/CHANGELOG.md +58 -0
  2. package/INSTALL_AND_UPDATE.md +4 -3
  3. package/README.md +219 -12
  4. package/docs/architecture.md +126 -45
  5. package/docs/concepts.md +9 -5
  6. package/docs/custom_model_controller.md +329 -0
  7. package/docs/data_contract.md +317 -22
  8. package/docs/data_model.md +1 -1
  9. package/docs/index.md +10 -0
  10. package/docs/learnings.md +22 -0
  11. package/docs/rules.md +71 -14
  12. package/docs/sense_contract.md +59 -0
  13. package/docs/system_specs.md +75 -13
  14. package/docs/training.md +280 -32
  15. package/examples/python/README.md +62 -0
  16. package/examples/python/base_policy_server.py +191 -0
  17. package/examples/python/requirements.txt +2 -0
  18. package/package.json +12 -4
  19. package/src/api/PaddockSimulatorController.js +6 -33
  20. package/src/app/BrowserExpertAdapter.js +76 -9
  21. package/src/app/BrowserExpertExternalRenderer.js +303 -0
  22. package/src/app/F1SimulatorApp.js +291 -1630
  23. package/src/app/banners/bannerFormatters.js +55 -0
  24. package/src/app/banners/raceDataBannerController.js +141 -0
  25. package/src/app/banners/stewardMessageController.js +99 -0
  26. package/src/app/camera/cameraConstants.js +21 -0
  27. package/src/app/camera/cameraController.js +265 -0
  28. package/src/app/camera/cameraGeometry.js +176 -0
  29. package/src/app/domBindings.js +4 -0
  30. package/src/app/readouts/carOverviewRenderer.js +109 -0
  31. package/src/app/readouts/controlStateRenderer.js +37 -0
  32. package/src/app/readouts/raceStatusRenderer.js +133 -0
  33. package/src/app/readouts/readoutFormatters.js +58 -0
  34. package/src/app/readouts/telemetryRenderer.js +113 -0
  35. package/src/app/readouts/timingTowerRenderer.js +177 -0
  36. package/src/app/rendering/appAssets.js +38 -0
  37. package/src/app/rendering/carRenderer.js +286 -0
  38. package/src/app/rendering/displayUtils.js +22 -0
  39. package/src/app/rendering/drsTrailRenderer.js +90 -0
  40. package/src/app/rendering/expertSensorRenderer.js +126 -0
  41. package/src/app/rendering/pitLaneStatusRenderer.js +50 -0
  42. package/src/app/rendering/replayGhostRenderer.js +124 -0
  43. package/src/app/rendering/trackRenderer.js +25 -0
  44. package/src/app/runtime/frameLoop.js +66 -0
  45. package/src/app/runtime/runtimeTiming.js +34 -0
  46. package/src/app/runtime/runtimeVisibility.js +45 -0
  47. package/src/config/defaultOptions.js +20 -0
  48. package/src/config/restartOptions.js +34 -0
  49. package/src/environment/actions.js +13 -5
  50. package/src/environment/controllerLoop.js +261 -0
  51. package/src/environment/episode.js +98 -2
  52. package/src/environment/evaluation.js +54 -32
  53. package/src/environment/index.d.ts +376 -10
  54. package/src/environment/index.js +1 -0
  55. package/src/environment/metrics.js +159 -0
  56. package/src/environment/observationOptions.js +22 -1
  57. package/src/environment/observationVector.js +200 -0
  58. package/src/environment/observations.js +199 -141
  59. package/src/environment/options.js +37 -5
  60. package/src/environment/recorder.js +13 -6
  61. package/src/environment/rewards.js +13 -2
  62. package/src/environment/runtime.js +272 -23
  63. package/src/environment/scenarios.js +57 -9
  64. package/src/environment/sensors/bodySenses.js +23 -0
  65. package/src/environment/sensors/boundarySenses.js +17 -0
  66. package/src/environment/sensors/carRays.js +75 -0
  67. package/src/environment/sensors/contactSenses.js +32 -0
  68. package/src/environment/sensors/index.js +157 -0
  69. package/src/environment/sensors/indexedRayBands.js +103 -0
  70. package/src/environment/sensors/nearbyCars.js +52 -0
  71. package/src/environment/sensors/opponentRadar.js +75 -0
  72. package/src/environment/sensors/rayConfig.js +110 -0
  73. package/src/environment/sensors/rayDefaults.js +6 -0
  74. package/src/environment/sensors/rayGeometry.js +60 -0
  75. package/src/environment/sensors/rayGuards.js +19 -0
  76. package/src/environment/sensors/sensorTargets.js +66 -0
  77. package/src/environment/sensors/surfaceRays.js +360 -0
  78. package/src/environment/sensors/trackRays.js +424 -0
  79. package/src/environment/sensors.js +1 -367
  80. package/src/environment/specs.js +159 -26
  81. package/src/environment/workerProtocol.js +13 -1
  82. package/src/index.d.ts +171 -6
  83. package/src/index.js +6 -32
  84. package/src/rendering/proceduralTrackAsset.js +20 -823
  85. package/src/rendering/renderSnapshot.js +38 -12
  86. package/src/rendering/track/finishLineRenderer.js +34 -0
  87. package/src/rendering/track/gridRenderer.js +47 -0
  88. package/src/rendering/track/offsetStrokeSafety.js +136 -0
  89. package/src/rendering/track/pitLaneRenderer.js +248 -0
  90. package/src/rendering/track/trackMaterialRenderer.js +326 -0
  91. package/src/rendering/track/trackRenderConstants.js +38 -0
  92. package/src/rendering/track/trackRenderGeometry.js +94 -0
  93. package/src/simulation/collisionGeometry.js +1 -1
  94. package/src/simulation/driver/driverControlConstants.js +28 -0
  95. package/src/simulation/driver/driverController.js +47 -0
  96. package/src/simulation/driver/driverInput.js +28 -0
  97. package/src/simulation/driver/driverMath.js +6 -0
  98. package/src/simulation/driver/driverPersonality.js +25 -0
  99. package/src/simulation/driver/edgeRecovery.js +48 -0
  100. package/src/simulation/driver/racingControls.js +399 -0
  101. package/src/simulation/driver/racingLinePlan.js +166 -0
  102. package/src/simulation/driver/recoveryDynamics.js +38 -0
  103. package/src/simulation/driver/rejoinControls.js +213 -0
  104. package/src/simulation/driver/safetyCarControls.js +35 -0
  105. package/src/simulation/driver/trafficScan.js +51 -0
  106. package/src/simulation/driverController.js +4 -619
  107. package/src/simulation/participants/participantInteractions.js +120 -0
  108. package/src/simulation/pit/pitFlow.js +239 -0
  109. package/src/simulation/pit/pitIntent.js +92 -0
  110. package/src/simulation/pit/pitKinematics.js +24 -0
  111. package/src/simulation/pit/pitOccupancy.js +47 -0
  112. package/src/simulation/pit/pitPenaltyService.js +71 -0
  113. package/src/simulation/pit/pitQueue.js +60 -0
  114. package/src/simulation/pit/pitRouteMovement.js +167 -0
  115. package/src/simulation/pit/pitRouting.js +319 -0
  116. package/src/simulation/pit/pitService.js +43 -0
  117. package/src/simulation/pit/pitServiceConstants.js +21 -0
  118. package/src/simulation/pit/pitServiceProfile.js +45 -0
  119. package/src/simulation/pit/pitSnapshots.js +61 -0
  120. package/src/simulation/pit/pitState.js +113 -0
  121. package/src/simulation/pit/pitTireService.js +94 -0
  122. package/src/simulation/race/classification.js +104 -0
  123. package/src/simulation/race/gridPenalties.js +37 -0
  124. package/src/simulation/race/raceControlState.js +38 -0
  125. package/src/simulation/race/raceDistance.js +32 -0
  126. package/src/simulation/race/raceFinish.js +65 -0
  127. package/src/simulation/race/raceLifecycle.js +67 -0
  128. package/src/simulation/race/raceOrder.js +113 -0
  129. package/src/simulation/race/raceProgress.js +129 -0
  130. package/src/simulation/race/raceSetup.js +93 -0
  131. package/src/simulation/race/raceStep.js +80 -0
  132. package/src/simulation/race/redFlag.js +15 -0
  133. package/src/simulation/race/retirements.js +44 -0
  134. package/src/simulation/race/safetyCar.js +92 -0
  135. package/src/simulation/race/stalledDnf.js +77 -0
  136. package/src/simulation/raceSimulation.js +231 -2817
  137. package/src/simulation/replay/replayGhosts.js +128 -0
  138. package/src/simulation/rules/moduleConfig.js +72 -0
  139. package/src/simulation/rules/penaltyConfig.js +99 -0
  140. package/src/simulation/rules/penaltyEvents.js +18 -0
  141. package/src/simulation/rules/penaltyStats.js +26 -0
  142. package/src/simulation/rules/ruleConfigMerge.js +41 -0
  143. package/src/simulation/rules/ruleDefaults.js +85 -0
  144. package/src/simulation/rules/rulesReview.js +144 -0
  145. package/src/simulation/rules/rulesetPresets.js +47 -0
  146. package/src/simulation/rules/trackLimitsSteward.js +1 -1
  147. package/src/simulation/rulesConfig.js +5 -323
  148. package/src/simulation/snapshots/raceSnapshots.js +193 -0
  149. package/src/simulation/timing/drsTiming.js +87 -0
  150. package/src/simulation/timing/gapEstimation.js +38 -0
  151. package/src/simulation/timing/lapTelemetry.js +182 -0
  152. package/src/simulation/timing/raceTiming.js +15 -0
  153. package/src/simulation/timing/sectorPerformance.js +60 -0
  154. package/src/simulation/timing/timingConstants.js +5 -0
  155. package/src/simulation/timing/timingHistory.js +46 -0
  156. package/src/simulation/timing/timingLines.js +79 -0
  157. package/src/simulation/track/centerlineSpline.js +59 -0
  158. package/src/simulation/track/drsZones.js +67 -0
  159. package/src/simulation/track/pitLaneAccess.js +103 -0
  160. package/src/simulation/track/pitLaneLayout.js +395 -0
  161. package/src/simulation/track/pitLaneState.js +160 -0
  162. package/src/simulation/track/proceduralCenterline.js +237 -0
  163. package/src/simulation/track/sampleGeometry.js +49 -0
  164. package/src/simulation/track/spatialQueries.js +154 -0
  165. package/src/simulation/track/startStraight.js +141 -0
  166. package/src/simulation/track/trackConstants.js +109 -0
  167. package/src/simulation/track/trackGenerationOptions.js +283 -0
  168. package/src/simulation/track/trackMath.js +241 -0
  169. package/src/simulation/track/trackModel.js +240 -0
  170. package/src/simulation/track/trackQueryGrid.js +94 -0
  171. package/src/simulation/track/trackQueryIndex.js +659 -0
  172. package/src/simulation/track/trackQueryPitIndex.js +140 -0
  173. package/src/simulation/track/trackQueryScratch.js +46 -0
  174. package/src/simulation/track/trackQueryStats.js +28 -0
  175. package/src/simulation/track/trackSectors.js +19 -0
  176. package/src/simulation/track/trackStatePolicy.js +24 -0
  177. package/src/simulation/track/trackValidation.js +98 -0
  178. package/src/simulation/trackModel.js +1 -1706
  179. package/src/simulation/vehicle/contactResolution.js +181 -0
  180. package/src/simulation/vehicle/mainTrackWheelSurface.js +82 -0
  181. package/src/simulation/vehicle/pitWheelSurface.js +69 -0
  182. package/src/simulation/vehicle/runoffResponse.js +87 -0
  183. package/src/simulation/vehicle/surfacePriority.js +28 -0
  184. package/src/simulation/vehicle/vehicleGeometry.js +247 -0
  185. package/src/simulation/vehicle/vehicleKinematics.js +37 -0
  186. package/src/simulation/vehicle/vehiclePhysics.js +364 -0
  187. package/src/simulation/vehicle/vehicleSnapshots.js +327 -0
  188. package/src/simulation/vehicle/vehicleState.js +220 -0
  189. package/src/simulation/vehicle/wheelSurface.js +152 -0
  190. package/src/simulation/vehicleGeometry.js +1 -247
  191. package/src/simulation/vehiclePhysics.js +1 -136
  192. package/src/simulation/warmup/runtimeWarmup.js +224 -0
  193. package/src/simulation/wheelSurface.js +1 -338
  194. package/src/styles.css +42 -0
  195. package/src/ui/cameraControlsTemplate.js +20 -0
  196. package/src/ui/carOverviewTemplate.js +33 -0
  197. package/src/ui/componentTemplates.js +14 -360
  198. package/src/ui/raceCanvasTemplate.js +69 -0
  199. package/src/ui/raceControlsTemplate.js +36 -0
  200. package/src/ui/shellTemplate.js +5 -4
  201. package/src/ui/telemetryDrawerTemplate.js +55 -0
  202. package/src/ui/telemetryTemplates.js +136 -0
  203. package/src/ui/timingTowerTemplate.js +34 -0
@@ -0,0 +1,303 @@
1
+ import { renderTrackSurface } from './rendering/trackRenderer.js';
2
+
3
+ export function createBrowserExpertExternalRendererBridge(app) {
4
+ let unsubscribeExternalRenderer = null;
5
+ let attached = false;
6
+ let lastMeta = null;
7
+ let lastFrameAt = null;
8
+ let lastError = null;
9
+ let driverIdMap = new Map();
10
+ let trackFingerprint = null;
11
+ let appliedTrackSurface = false;
12
+
13
+ function localDriverRecords() {
14
+ const source = Array.isArray(app.drivers) && app.drivers.length > 0
15
+ ? app.drivers
16
+ : app.options?.drivers;
17
+ return Array.isArray(source) ? source : [];
18
+ }
19
+
20
+ function localDriverIds() {
21
+ return localDriverRecords()
22
+ .map((driver) => driver?.id)
23
+ .filter((id) => typeof id === 'string' && id.length > 0);
24
+ }
25
+
26
+ function localDriverById() {
27
+ return new Map(localDriverRecords().map((driver) => [driver.id, driver]));
28
+ }
29
+
30
+ function rewriteFrame(frame) {
31
+ if (!frame || typeof frame !== 'object') return frame;
32
+ const snapshot = frame.snapshot;
33
+ if (!snapshot || typeof snapshot !== 'object' || !Array.isArray(snapshot.cars)) return frame;
34
+
35
+ const knownLocalIds = localDriverIds();
36
+ if (knownLocalIds.length === 0) return frame;
37
+ const knownLocalIdSet = new Set(knownLocalIds);
38
+
39
+ const activeRawIds = new Set();
40
+ const usedLocalIds = new Set();
41
+
42
+ snapshot.cars.forEach((car, index) => {
43
+ const rawId = typeof car?.id === 'string' && car.id.length > 0
44
+ ? car.id
45
+ : `external-${String(index)}`;
46
+ activeRawIds.add(rawId);
47
+ if (knownLocalIdSet.has(rawId) && !usedLocalIds.has(rawId)) {
48
+ driverIdMap.set(rawId, rawId);
49
+ usedLocalIds.add(rawId);
50
+ return;
51
+ }
52
+ const mapped = driverIdMap.get(rawId);
53
+ if (typeof mapped === 'string' && mapped.length > 0 && !usedLocalIds.has(mapped)) {
54
+ usedLocalIds.add(mapped);
55
+ return;
56
+ }
57
+ const nextLocalId = knownLocalIds.find((id) => !usedLocalIds.has(id));
58
+ if (nextLocalId) {
59
+ driverIdMap.set(rawId, nextLocalId);
60
+ usedLocalIds.add(nextLocalId);
61
+ return;
62
+ }
63
+ driverIdMap.set(rawId, rawId);
64
+ usedLocalIds.add(rawId);
65
+ });
66
+
67
+ for (const rawId of Array.from(driverIdMap.keys())) {
68
+ if (!activeRawIds.has(rawId)) driverIdMap.delete(rawId);
69
+ }
70
+
71
+ const mapId = (value) => {
72
+ if (typeof value !== 'string' || value.length === 0) return value;
73
+ return driverIdMap.get(value) ?? value;
74
+ };
75
+
76
+ const drivers = localDriverById();
77
+ const mappedCars = snapshot.cars.map((car, index) => {
78
+ const rawId = typeof car?.id === 'string' && car.id.length > 0
79
+ ? car.id
80
+ : `external-${String(index)}`;
81
+ const mappedId = mapId(rawId);
82
+ const driver = drivers.get(mappedId);
83
+ return {
84
+ ...car,
85
+ id: mappedId,
86
+ code: car?.code ?? driver?.code ?? mappedId,
87
+ timingCode: car?.timingCode ?? driver?.timingCode ?? driver?.code ?? mappedId,
88
+ name: car?.name ?? driver?.name ?? mappedId,
89
+ color: car?.color ?? driver?.color ?? '#9ca3af',
90
+ icon: car?.icon ?? driver?.icon ?? (driver?.code ?? mappedId),
91
+ team: car?.team ?? driver?.team ?? null,
92
+ };
93
+ });
94
+
95
+ const mappedEvents = Array.isArray(snapshot.events)
96
+ ? snapshot.events.map((event) => ({
97
+ ...event,
98
+ carId: mapId(event?.carId),
99
+ otherCarId: mapId(event?.otherCarId),
100
+ winnerId: mapId(event?.winnerId),
101
+ }))
102
+ : snapshot.events;
103
+
104
+ const mappedPenalties = Array.isArray(snapshot.penalties)
105
+ ? snapshot.penalties.map((penalty) => ({
106
+ ...penalty,
107
+ driverId: mapId(penalty?.driverId),
108
+ }))
109
+ : snapshot.penalties;
110
+
111
+ const raceControl = snapshot.raceControl && typeof snapshot.raceControl === 'object'
112
+ ? {
113
+ ...snapshot.raceControl,
114
+ winner: snapshot.raceControl.winner && typeof snapshot.raceControl.winner === 'object'
115
+ ? {
116
+ ...snapshot.raceControl.winner,
117
+ id: mapId(snapshot.raceControl.winner.id),
118
+ }
119
+ : snapshot.raceControl.winner,
120
+ classification: Array.isArray(snapshot.raceControl.classification)
121
+ ? snapshot.raceControl.classification.map((entry) => ({
122
+ ...entry,
123
+ id: mapId(entry?.id),
124
+ driverId: mapId(entry?.driverId),
125
+ }))
126
+ : snapshot.raceControl.classification,
127
+ }
128
+ : snapshot.raceControl;
129
+
130
+ let mappedObservation = frame.observation;
131
+ if (frame.observation && typeof frame.observation === 'object' && !Array.isArray(frame.observation)) {
132
+ mappedObservation = {};
133
+ Object.entries(frame.observation).forEach(([driverId, value]) => {
134
+ mappedObservation[mapId(driverId)] = value;
135
+ });
136
+ }
137
+
138
+ return {
139
+ ...frame,
140
+ snapshot: {
141
+ ...snapshot,
142
+ cars: mappedCars,
143
+ events: mappedEvents,
144
+ penalties: mappedPenalties,
145
+ raceControl,
146
+ },
147
+ observation: mappedObservation,
148
+ meta: {
149
+ ...(frame.meta && typeof frame.meta === 'object' ? frame.meta : {}),
150
+ externalDriverMap: Object.fromEntries(driverIdMap.entries()),
151
+ },
152
+ };
153
+ }
154
+
155
+ function fingerprintTrack(snapshot) {
156
+ const track = snapshot?.track;
157
+ if (!track || typeof track !== 'object') return null;
158
+ const drs = Array.isArray(track.drsZones)
159
+ ? track.drsZones.map((zone) => [zone?.id ?? null, zone?.start ?? null, zone?.end ?? null])
160
+ : [];
161
+ const firstSample = Array.isArray(track.samples) && track.samples.length > 0 ? track.samples[0] : null;
162
+ const lastSample = Array.isArray(track.samples) && track.samples.length > 0
163
+ ? track.samples[track.samples.length - 1]
164
+ : null;
165
+ return JSON.stringify({
166
+ name: track.name ?? null,
167
+ length: track.length ?? null,
168
+ width: track.width ?? null,
169
+ sampleCount: track.sampleCount ?? null,
170
+ finish: track.finish
171
+ ? {
172
+ x: track.finish.x ?? null,
173
+ y: track.finish.y ?? null,
174
+ heading: track.finish.heading ?? null,
175
+ }
176
+ : null,
177
+ firstSample: firstSample
178
+ ? {
179
+ x: firstSample.x ?? null,
180
+ y: firstSample.y ?? null,
181
+ heading: firstSample.heading ?? null,
182
+ }
183
+ : null,
184
+ lastSample: lastSample
185
+ ? {
186
+ x: lastSample.x ?? null,
187
+ y: lastSample.y ?? null,
188
+ heading: lastSample.heading ?? null,
189
+ }
190
+ : null,
191
+ drs,
192
+ });
193
+ }
194
+
195
+ function syncTrackSurface(snapshot) {
196
+ if (!snapshot || typeof snapshot !== 'object') return;
197
+ const nextFingerprint = fingerprintTrack(snapshot);
198
+ if (!nextFingerprint) return;
199
+ if (nextFingerprint === trackFingerprint) return;
200
+ if (
201
+ !app.trackAsset ||
202
+ !app.drsLayer ||
203
+ !app.sensorLayer ||
204
+ !app.pitLaneStatusLayer
205
+ ) {
206
+ trackFingerprint = nextFingerprint;
207
+ return;
208
+ }
209
+ app.pitLaneStatusRenderer?.reset?.();
210
+ app.cameraController?.invalidateTrackCaches?.();
211
+ renderTrackSurface({
212
+ drsLayer: app.drsLayer,
213
+ sensorLayer: app.sensorLayer,
214
+ pitLaneStatusLayer: app.pitLaneStatusLayer,
215
+ trackAsset: app.trackAsset,
216
+ snapshot,
217
+ });
218
+ trackFingerprint = nextFingerprint;
219
+ appliedTrackSurface = true;
220
+ }
221
+
222
+ function assertDetached(method) {
223
+ if (!attached) return;
224
+ throw new Error(`Browser expert ${method}() is disabled while external renderer mode is attached.`);
225
+ }
226
+
227
+ function detach() {
228
+ if (typeof unsubscribeExternalRenderer === 'function') {
229
+ try {
230
+ unsubscribeExternalRenderer();
231
+ } catch (error) {
232
+ lastError = error instanceof Error ? error.message : String(error);
233
+ }
234
+ }
235
+ unsubscribeExternalRenderer = null;
236
+ attached = false;
237
+ driverIdMap = new Map();
238
+ if (appliedTrackSurface && typeof app.renderTrack === 'function') {
239
+ try {
240
+ app.renderTrack();
241
+ } catch (error) {
242
+ lastError = error instanceof Error ? error.message : String(error);
243
+ }
244
+ }
245
+ trackFingerprint = null;
246
+ appliedTrackSurface = false;
247
+ }
248
+
249
+ function attach(source) {
250
+ if (!source || typeof source.subscribe !== 'function') {
251
+ throw new Error('attachExternalRenderer(source) requires a source with subscribe(onFrame).');
252
+ }
253
+ detach();
254
+ lastError = null;
255
+ const unsubscribe = source.subscribe((frame) => {
256
+ if (!frame || typeof frame !== 'object') {
257
+ lastError = 'Invalid external frame payload.';
258
+ return;
259
+ }
260
+ const rewrittenFrame = rewriteFrame(frame);
261
+ const snapshot = rewrittenFrame.snapshot;
262
+ if (!snapshot || typeof snapshot !== 'object') {
263
+ lastError = 'External frame is missing snapshot.';
264
+ return;
265
+ }
266
+ try {
267
+ syncTrackSurface(snapshot);
268
+ app.renderExpertFrame(snapshot, {
269
+ forceDomUpdate: true,
270
+ observation: rewrittenFrame.observation && typeof rewrittenFrame.observation === 'object'
271
+ ? rewrittenFrame.observation
272
+ : {},
273
+ });
274
+ lastMeta = rewrittenFrame.meta ?? null;
275
+ lastFrameAt = Date.now();
276
+ lastError = null;
277
+ } catch (error) {
278
+ lastError = error instanceof Error ? error.message : String(error);
279
+ }
280
+ });
281
+ if (typeof unsubscribe !== 'function') {
282
+ throw new Error('attachExternalRenderer(source) subscribe(onFrame) must return an unsubscribe function.');
283
+ }
284
+ unsubscribeExternalRenderer = unsubscribe;
285
+ attached = true;
286
+ }
287
+
288
+ function getState() {
289
+ return {
290
+ attached,
291
+ lastMeta,
292
+ lastFrameAt,
293
+ lastError,
294
+ };
295
+ }
296
+
297
+ return {
298
+ assertDetached,
299
+ attach,
300
+ detach,
301
+ getState,
302
+ };
303
+ }