minecraft-renderer 0.1.62 → 0.1.64

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 (36) hide show
  1. package/README.md +1 -1
  2. package/dist/mesherWasm.js +22 -22
  3. package/dist/minecraft-renderer.js +54 -54
  4. package/dist/minecraft-renderer.js.meta.json +1 -1
  5. package/dist/threeWorker.js +407 -407
  6. package/package.json +1 -1
  7. package/src/graphicsBackend/config.ts +3 -3
  8. package/src/graphicsBackend/rendererDefaultOptions.ts +41 -24
  9. package/src/graphicsBackend/rendererOptionsSync.ts +23 -23
  10. package/src/graphicsBackend/types.ts +3 -3
  11. package/src/index.ts +8 -8
  12. package/src/lib/bindAbortableListener.test.ts +65 -0
  13. package/src/lib/bindAbortableListener.ts +41 -0
  14. package/src/lib/workerProxy.ts +238 -118
  15. package/src/lib/workerSyncOps.test.ts +154 -0
  16. package/src/lib/worldrendererCommon.removeColumn.test.ts +182 -0
  17. package/src/lib/worldrendererCommon.ts +86 -54
  18. package/src/three/documentRenderer.ts +1 -1
  19. package/src/three/entities.ts +21 -11
  20. package/src/three/graphicsBackendBase.ts +18 -8
  21. package/src/three/menuBackground/activeView.ts +1 -1
  22. package/src/three/menuBackground/config.ts +9 -9
  23. package/src/three/menuBackground/index.ts +10 -10
  24. package/src/three/menuBackground/renderer.ts +12 -12
  25. package/src/three/menuBackground/types.ts +9 -9
  26. package/src/three/menuBackground/{futuristic.ts → v2.ts} +110 -59
  27. package/src/three/menuBackground/{futuristicMeta.ts → v2Meta.ts} +6 -6
  28. package/src/three/modules/rain.ts +1 -1
  29. package/src/three/worldRendererThree.ts +2 -1
  30. package/src/wasm-mesher/tests/mesherWasmRequestTracker.test.ts +29 -0
  31. package/src/wasm-mesher/worker/mesherWasm.ts +7 -0
  32. package/src/wasm-mesher/worker/mesherWasmRequestTracker.ts +10 -0
  33. package/src/worldView/worldView.spiral.test.ts +38 -0
  34. package/src/worldView/worldView.ts +41 -8
  35. package/src/worldView/worldViewWorkerBridge.test.ts +59 -0
  36. package/src/lib/workerProxy.restore.test.ts +0 -29
@@ -86,104 +86,232 @@ export const useWorkerProxy = <T extends { __workerProxy: Record<string, (...arg
86
86
 
87
87
  const DEBUG_SYNC = false
88
88
 
89
- const sendWorkerSync = (syncId: string, obj: any, worker: Worker, debugKey: string) => {
90
- try {
91
- worker.postMessage({
92
- type: 'sync',
93
- syncId,
94
- value: cloneValtioObject(obj)
95
- })
96
- currentWorkerSyncStats.toWorker++
97
- globalThis.debugSyncMessagesOutgoing ??= 0
98
- globalThis.debugSyncMessagesOutgoing++
99
- } catch (err) {
100
- console.error('Failed to send worker sync', err)
101
- findProblemTransfer(obj)
102
- }
103
- }
89
+ // rendererState: worker→main only; playerState: main→worker only. Applying ops on the
90
+ // receiver re-fires local subscribers; no echo loop while directions stay split.
91
+
92
+ type SyncDirection = 'toWorker' | 'fromWorker'
93
+
94
+ export type WireSyncOp =
95
+ | { kind: 'set', path: (string | number | symbol)[], value: unknown }
96
+ | { kind: 'delete', path: (string | number | symbol)[] }
97
+
98
+ type ValtioOp = readonly unknown[]
104
99
 
105
- // Add stats tracking variables
106
100
  const currentWorkerSyncStats = { toWorker: 0, fromWorker: 0 }
107
101
 
108
- if (typeof window !== 'undefined') {
109
- setInterval(() => {
102
+ let debugSyncStatsInterval: ReturnType<typeof setInterval> | null = null
103
+
104
+ const ensureDebugSyncStatsInterval = () => {
105
+ if (debugSyncStatsInterval != null) return
106
+ if (typeof window === 'undefined') return
107
+ debugSyncStatsInterval = setInterval(() => {
110
108
  globalThis.debugWorkerSyncStats = { ...currentWorkerSyncStats }
111
109
  currentWorkerSyncStats.toWorker = 0
112
110
  currentWorkerSyncStats.fromWorker = 0
113
111
  }, 1000)
114
112
  }
115
113
 
114
+ const bumpSyncStat = (direction: SyncDirection) => {
115
+ ensureDebugSyncStatsInterval()
116
+ if (direction === 'toWorker') {
117
+ currentWorkerSyncStats.toWorker++
118
+ } else {
119
+ currentWorkerSyncStats.fromWorker++
120
+ }
121
+ }
122
+
123
+ /** @internal vitest only */
124
+ export const resetWorkerSyncStatsForTest = () => {
125
+ currentWorkerSyncStats.toWorker = 0
126
+ currentWorkerSyncStats.fromWorker = 0
127
+ if (debugSyncStatsInterval != null) {
128
+ clearInterval(debugSyncStatsInterval)
129
+ debugSyncStatsInterval = null
130
+ }
131
+ }
132
+
133
+ /** @internal vitest only */
134
+ export const getWorkerSyncStatsForTest = () => ({ ...currentWorkerSyncStats })
135
+
116
136
  const getSyncId = () => {
117
137
  return Math.random().toString(36).slice(2, 15) + Math.random().toString(36).slice(2, 15)
118
138
  }
119
139
 
120
- const applySyncPatch = (target: any, patch: any, worker: Worker) => {
121
- Object.assign(target, restoreTransferred(patch, [], worker, false))
140
+ export const setByPath = (target: any, path: (string | number | symbol)[], value: unknown) => {
141
+ if (path.length === 0) return
142
+ let cur = target
143
+ for (let i = 0; i < path.length - 1; i++) {
144
+ const key = path[i]!
145
+ if (cur[key] == null || typeof cur[key] !== 'object') {
146
+ cur[key] = {}
147
+ }
148
+ cur = cur[key]
149
+ }
150
+ cur[path[path.length - 1]!] = value
122
151
  }
123
152
 
124
- const setupObjectSync = (obj: any, originalObj: any, worker: Worker, isValtio: boolean, debugKey: string) => {
125
- if (!obj['__syncToWorker'] && !obj['__syncFromWorker'] && !isValtio) return
153
+ export const deleteByPath = (target: any, path: (string | number | symbol)[]) => {
154
+ if (path.length === 0) return
155
+ let cur = target
156
+ for (let i = 0; i < path.length - 1; i++) {
157
+ cur = cur[path[i]!]
158
+ if (cur == null) return
159
+ }
160
+ delete cur[path[path.length - 1]!]
161
+ }
126
162
 
127
- const syncId = getSyncId()
128
- obj['__syncId'] = syncId
163
+ export const prepareOpValueForTransfer = (value: any, worker: Worker): any => {
164
+ if (value == null || typeof value !== 'object') {
165
+ return value
166
+ }
129
167
 
130
- if (obj['__syncToWorker'] || isValtio) {
131
- const syncToWorker = () => {
132
- sendWorkerSync(syncId, originalObj, worker, `toWorker:${debugKey}`)
133
- }
134
- if (isValtio) {
135
- subscribe(originalObj, syncToWorker)
136
- }
168
+ if (value instanceof Vec3) {
169
+ return { x: value.x, y: value.y, z: value.z, __restorer: 'Vec3' }
170
+ }
137
171
 
138
- const interval = obj['__syncToWorkerInterval'] ?? 0
139
- if (interval > 0) {
140
- setInterval(syncToWorker, interval)
141
- }
172
+ if (typeof value['prepareForTransfer'] === 'function') {
173
+ return value['prepareForTransfer'](worker)
142
174
  }
143
175
 
144
- if (originalObj['__syncFromWorker']) {
145
- worker.addEventListener('message', (event: any) => {
146
- if (event.data.type === 'sync' && event.data.syncId === syncId) {
147
- currentWorkerSyncStats.fromWorker++
148
- applySyncPatch(originalObj, event.data.value, worker)
149
- }
150
- })
176
+ if (ArrayBuffer.isView(value)) {
177
+ return value
178
+ }
179
+
180
+ if (Array.isArray(value)) {
181
+ return value.map(item => prepareOpValueForTransfer(item, worker))
182
+ }
183
+
184
+ if (getVersion(value) !== undefined) {
185
+ return cloneValtioObject(value)
151
186
  }
187
+
188
+ const result = {} as any
189
+ for (const key in value) {
190
+ if (Object.prototype.hasOwnProperty.call(value, key)) {
191
+ result[key] = prepareOpValueForTransfer(value[key], worker)
192
+ }
193
+ }
194
+ return result
152
195
  }
153
196
 
154
- const serializeMapForTransfer = (map: Map<unknown, unknown>) => ({
155
- __restorer: 'Map',
156
- __mapEntries: Array.from(map.entries()),
157
- })
197
+ const wireOpsFromValtioOps = (ops: ValtioOp[], worker: Worker): WireSyncOp[] => {
198
+ const wire: WireSyncOp[] = []
199
+ for (const op of ops) {
200
+ const kind = op[0]
201
+ if (kind === 'delete') {
202
+ wire.push({ kind: 'delete', path: op[1] as (string | number | symbol)[] })
203
+ } else if (kind === 'set') {
204
+ wire.push({
205
+ kind: 'set',
206
+ path: op[1] as (string | number | symbol)[],
207
+ value: prepareOpValueForTransfer(op[2], worker)
208
+ })
209
+ }
210
+ }
211
+ return wire
212
+ }
158
213
 
159
- const serializeSetForTransfer = (set: Set<unknown>) => ({
160
- __restorer: 'Set',
161
- __setValues: [...set],
162
- })
214
+ export const sendWorkerSyncOps = (
215
+ syncId: string,
216
+ ops: ValtioOp[],
217
+ worker: Worker,
218
+ direction: SyncDirection,
219
+ debugKey: string
220
+ ) => {
221
+ if (ops.length === 0) return
222
+ const wire = wireOpsFromValtioOps(ops, worker)
223
+ if (wire.length === 0) return
224
+ try {
225
+ worker.postMessage({ type: 'sync', syncId, ops: wire })
226
+ if (direction === 'toWorker') {
227
+ bumpSyncStat('toWorker')
228
+ }
229
+ if (DEBUG_SYNC) console.log(`sync ${debugKey}`, wire.length, 'ops')
230
+ } catch (err) {
231
+ console.error('Failed to send worker sync ops', err, debugKey)
232
+ for (const op of wire) {
233
+ if (op.kind === 'set') findProblemTransfer(op.value)
234
+ }
235
+ }
236
+ }
163
237
 
164
- const isSetLike = (value: unknown): value is Set<unknown> => {
165
- return value instanceof Set || Object.prototype.toString.call(value) === '[object Set]'
238
+ export const applySyncOps = (
239
+ target: any,
240
+ wireOps: WireSyncOp[],
241
+ worker: Worker,
242
+ countReceive: 'fromWorker' | false = false
243
+ ) => {
244
+ for (const op of wireOps) {
245
+ if (op.kind === 'delete') {
246
+ deleteByPath(target, op.path)
247
+ } else {
248
+ setByPath(target, op.path, restoreTransferred(op.value, [], worker, false, false))
249
+ }
250
+ }
251
+ if (countReceive === 'fromWorker') {
252
+ bumpSyncStat('fromWorker')
253
+ }
166
254
  }
167
255
 
168
- const isMapLike = (value: unknown): value is Map<unknown, unknown> => {
169
- return value instanceof Map || Object.prototype.toString.call(value) === '[object Map]'
256
+ /** Full snapshot for plain (non-Valtio) objects on interval sync only, e.g. nonReactiveState. */
257
+ const sendWorkerSyncSnapshot = (syncId: string, obj: any, worker: Worker, direction: SyncDirection, debugKey: string) => {
258
+ try {
259
+ const value = cloneValtioObject(obj)
260
+ worker.postMessage({ type: 'sync', syncId, value })
261
+ if (direction === 'toWorker') {
262
+ bumpSyncStat('toWorker')
263
+ }
264
+ if (DEBUG_SYNC) console.log(`sync snapshot ${debugKey}`)
265
+ } catch (err) {
266
+ console.error('Failed to send worker sync snapshot', err, debugKey)
267
+ findProblemTransfer(obj)
268
+ }
170
269
  }
171
270
 
172
- const iterableFromPlainObject = (obj: Record<string, unknown>) => {
173
- return Object.keys(obj)
174
- .filter(k => !k.startsWith('__'))
175
- .sort((a, b) => Number(a) - Number(b))
176
- .map(k => obj[k])
271
+ const applySyncSnapshot = (target: any, patch: any, worker: Worker, countReceive: 'fromWorker' | false = false) => {
272
+ Object.assign(target, restoreTransferred(patch, [], worker, false, false))
273
+ if (countReceive === 'fromWorker') {
274
+ bumpSyncStat('fromWorker')
275
+ }
177
276
  }
178
277
 
179
- const cloneValtioObject = (obj: any) => {
180
- if (isMapLike(obj)) {
181
- return serializeMapForTransfer(obj)
278
+ const setupObjectSync = (obj: any, originalObj: any, worker: Worker, isValtio: boolean, debugKey: string) => {
279
+ const syncFromWorker = obj['__syncFromWorker'] || originalObj['__syncFromWorker']
280
+ const syncToWorker = obj['__syncToWorker'] || originalObj['__syncToWorker']
281
+ if (!syncToWorker && !syncFromWorker && !isValtio) return
282
+
283
+ const syncId = getSyncId()
284
+ obj['__syncId'] = syncId
285
+
286
+ if (syncToWorker || isValtio) {
287
+ if (isValtio && syncToWorker !== false) {
288
+ subscribe(originalObj, (ops) => {
289
+ sendWorkerSyncOps(syncId, ops as ValtioOp[], worker, 'toWorker', `toWorker:${debugKey}`)
290
+ })
291
+ }
292
+
293
+ const interval = obj['__syncToWorkerInterval'] ?? originalObj['__syncToWorkerInterval'] ?? 0
294
+ if (interval > 0 && !isValtio) {
295
+ setInterval(() => {
296
+ sendWorkerSyncSnapshot(syncId, originalObj, worker, 'toWorker', `toWorker:interval:${debugKey}`)
297
+ }, interval)
298
+ }
182
299
  }
183
- if (isSetLike(obj)) {
184
- return serializeSetForTransfer(obj)
300
+
301
+ if (originalObj['__syncFromWorker']) {
302
+ worker.addEventListener('message', (event: any) => {
303
+ if (event.data.type === 'sync' && event.data.syncId === syncId) {
304
+ if (event.data.ops) {
305
+ applySyncOps(originalObj, event.data.ops, worker, 'fromWorker')
306
+ } else if (event.data.value) {
307
+ applySyncSnapshot(originalObj, event.data.value, worker, 'fromWorker')
308
+ }
309
+ }
310
+ })
185
311
  }
312
+ }
186
313
 
314
+ const cloneValtioObject = (obj: any) => {
187
315
  if (getVersion(obj) === undefined) {
188
316
  return obj
189
317
  }
@@ -215,21 +343,11 @@ export const deepPrepareForTransfer = (obj: any, worker: Worker, autoRemoveMetho
215
343
  continue
216
344
  }
217
345
 
218
- // print a warning for Date, RegExp, WeakMap, WeakSet
219
- if (obj[key] instanceof Date || obj[key] instanceof RegExp || obj[key] instanceof WeakMap || obj[key] instanceof WeakSet) {
346
+ // print a warning for Date, RegExp, Map, Set, WeakMap, WeakSet
347
+ if (obj[key] instanceof Date || obj[key] instanceof RegExp || obj[key] instanceof Map || obj[key] instanceof Set || obj[key] instanceof WeakMap || obj[key] instanceof WeakSet) {
220
348
  console.warn(`Warning: ${key} is a ${typeof obj[key]}, which is not supported for transfer.`)
221
349
  }
222
350
 
223
- // default restorers main -> worker
224
- if (isMapLike(obj[key])) {
225
- newObj[key] = serializeMapForTransfer(obj[key])
226
- continue
227
- }
228
- // Set (only primitive values)
229
- if (isSetLike(obj[key])) {
230
- newObj[key] = serializeSetForTransfer(obj[key])
231
- continue
232
- }
233
351
  if (obj[key] instanceof Vec3) {
234
352
  newObj[key] = { x: obj[key].x, y: obj[key].y, z: obj[key].z }
235
353
  newObj[key]['__restorer'] = 'Vec3'
@@ -247,6 +365,19 @@ export const deepPrepareForTransfer = (obj: any, worker: Worker, autoRemoveMetho
247
365
  const isValtio = getVersion(obj[key]) !== undefined
248
366
  newObj[key] = isValtio ? cloneValtioObject(obj[key]) : obj[key]
249
367
 
368
+ if (obj[key]['__syncFromWorker']) {
369
+ newObj[key]['__syncFromWorker'] = true
370
+ }
371
+ if (obj[key]['__syncToWorker']) {
372
+ newObj[key]['__syncToWorker'] = true
373
+ }
374
+ if (obj[key]['__syncFromWorkerInterval']) {
375
+ newObj[key]['__syncFromWorkerInterval'] = obj[key]['__syncFromWorkerInterval']
376
+ }
377
+ if (obj[key]['__syncToWorkerInterval']) {
378
+ newObj[key]['__syncToWorkerInterval'] = obj[key]['__syncToWorkerInterval']
379
+ }
380
+
250
381
  // Try to enable sync main -> worker
251
382
  const tryEnableDefaultSync = obj[key]['__syncToWorker'] !== false && !_isInsideValtio && isValtio && !obj[key]['__syncFromWorker']
252
383
  newObj[key]['__syncToWorker'] ??= tryEnableDefaultSync
@@ -258,6 +389,10 @@ export const deepPrepareForTransfer = (obj: any, worker: Worker, autoRemoveMetho
258
389
  setupObjectSync(newObj[key], originalObj[key], worker, true, key)
259
390
  continue
260
391
  }
392
+ if (newObj[key]['__syncFromWorker'] || newObj[key]['__syncToWorker']) {
393
+ setupObjectSync(newObj[key], originalObj[key], worker, isValtio, key)
394
+ continue
395
+ }
261
396
  setupObjectSync(newObj[key], originalObj[key], worker, false, key)
262
397
 
263
398
 
@@ -285,67 +420,52 @@ export const findProblemTransfer = (obj: any, path: string[] = []) => {
285
420
  }
286
421
  }
287
422
 
423
+ // Tracks which syncIds already have listeners/timers wired, per worker, so a
424
+ // given synced object is never armed twice (prevents runaway interval/listener
425
+ // accumulation if a payload carrying __sync* flags is ever restored again).
426
+ const armedSyncIds = new WeakMap<Worker, Set<string>>()
427
+
288
428
  const receiveSyncedObject = (obj: any, worker: Worker, debugKey: string) => {
289
429
  if (!obj['__syncId']) return
290
430
  const syncId = obj['__syncId']
291
431
 
432
+ let armed = armedSyncIds.get(worker)
433
+ if (!armed) {
434
+ armed = new Set()
435
+ armedSyncIds.set(worker, armed)
436
+ }
437
+ if (armed.has(syncId)) return
438
+ armed.add(syncId)
439
+
292
440
  if (obj['__syncToWorker']) {
293
441
  worker.addEventListener('message', (event: any) => {
294
442
  if (event.data.type === 'sync' && event.data.syncId === syncId) {
295
- applySyncPatch(obj, event.data.value, worker)
443
+ if (event.data.ops) {
444
+ applySyncOps(obj, event.data.ops, worker)
445
+ } else if (event.data.value) {
446
+ applySyncSnapshot(obj, event.data.value, worker)
447
+ }
296
448
  }
297
449
  })
298
450
  }
299
451
 
300
452
  if (obj['__syncFromWorker']) {
301
- const syncFromWorker = () => {
302
- sendWorkerSync(syncId, obj, worker, `fromWorker:${debugKey}`)
303
- }
304
-
305
453
  if (obj['__valtio']) {
306
- subscribe(obj, syncFromWorker)
454
+ subscribe(obj, (ops) => {
455
+ sendWorkerSyncOps(syncId, ops as ValtioOp[], worker, 'fromWorker', `fromWorker:${debugKey}`)
456
+ })
307
457
  }
308
458
 
309
459
  const interval = obj['__syncFromWorkerInterval'] ?? 0
310
- if (interval > 0) {
311
- setInterval(syncFromWorker, interval)
460
+ if (interval > 0 && !obj['__valtio']) {
461
+ setInterval(() => {
462
+ sendWorkerSyncSnapshot(syncId, obj, worker, 'fromWorker', `fromWorker:interval:${debugKey}`)
463
+ }, interval)
312
464
  }
313
465
  }
314
466
  }
315
467
 
316
468
  const defaultRestorers = [
317
- {
318
- restorerName: 'Map',
319
- restoreTransferred(obj, _worker: Worker) {
320
- if (Array.isArray(obj)) {
321
- return new Map(obj)
322
- }
323
- const raw = obj.__mapEntries ?? obj.entries
324
- if (Array.isArray(raw)) {
325
- return new Map(raw)
326
- }
327
- if (raw != null && typeof raw === 'object' && typeof raw !== 'function') {
328
- return new Map(Object.entries(raw as Record<string, unknown>))
329
- }
330
- return new Map()
331
- }
332
- },
333
- {
334
- restorerName: 'Set',
335
- restoreTransferred(obj, _worker: Worker) {
336
- if (Array.isArray(obj)) {
337
- return new Set(obj)
338
- }
339
- const raw = obj.__setValues ?? obj.values
340
- if (Array.isArray(raw)) {
341
- return new Set(raw)
342
- }
343
- if (raw != null && typeof raw === 'object' && typeof raw !== 'function') {
344
- return new Set(iterableFromPlainObject(raw as Record<string, unknown>))
345
- }
346
- return new Set()
347
- }
348
- },
349
469
  {
350
470
  restorerName: 'Vec3',
351
471
  restoreTransferred(obj, worker: Worker) {
@@ -358,7 +478,7 @@ export const addDefaultRestorer = (restorer: { restorerName: string, restoreTran
358
478
  defaultRestorers.unshift(restorer)
359
479
  }
360
480
 
361
- export const restoreTransferred = (obj: any, restorersArg: any[], worker: Worker, errorHandler: ((error: Error) => void) | boolean = true) => {
481
+ export const restoreTransferred = (obj: any, restorersArg: any[], worker: Worker, errorHandler: ((error: Error) => void) | boolean = true, armSync = true) => {
362
482
  const restorers = [...defaultRestorers, ...restorersArg]
363
483
 
364
484
  const restoreValue = (value: any, debugKey: string): any => {
@@ -400,7 +520,7 @@ export const restoreTransferred = (obj: any, restorersArg: any[], worker: Worker
400
520
  value = proxy(value)
401
521
  }
402
522
 
403
- receiveSyncedObject(value, worker, debugKey)
523
+ if (armSync) receiveSyncedObject(value, worker, debugKey)
404
524
  return value
405
525
  }
406
526
 
@@ -0,0 +1,154 @@
1
+ //@ts-nocheck
2
+ import { describe, expect, it, vi, beforeEach, afterEach } from 'vitest'
3
+ import { proxy, subscribe } from 'valtio'
4
+ import { Vec3 } from 'vec3'
5
+ import {
6
+ applySyncOps,
7
+ getWorkerSyncStatsForTest,
8
+ resetWorkerSyncStatsForTest,
9
+ sendWorkerSyncOps,
10
+ setByPath,
11
+ type WireSyncOp,
12
+ } from './workerProxy'
13
+ import { defaultPerformanceInstabilityFactors } from '../performanceMonitor'
14
+
15
+ const fakeWorker = () => {
16
+ const listeners: Array<(event: { data: any }) => void> = []
17
+ return {
18
+ postMessage: vi.fn((data: any) => {
19
+ for (const listener of listeners) {
20
+ listener({ data })
21
+ }
22
+ }),
23
+ addEventListener: vi.fn((_type: string, listener: (event: { data: any }) => void) => {
24
+ listeners.push(listener)
25
+ }),
26
+ removeEventListener: vi.fn(),
27
+ } as unknown as Worker
28
+ }
29
+
30
+ const makeRendererState = () => proxy({
31
+ world: {
32
+ chunksLoaded: {} as Record<string, true>,
33
+ heightmaps: {} as Record<string, Int16Array>,
34
+ allChunksLoaded: false,
35
+ mesherWork: false,
36
+ instabilityFactors: defaultPerformanceInstabilityFactors(),
37
+ intersectMedia: null as null | object,
38
+ },
39
+ renderer: '...',
40
+ preventEscapeMenu: false,
41
+ })
42
+
43
+ describe('workerSyncOps', () => {
44
+ it('set op round-trips mesherWork', () => {
45
+ const source = makeRendererState()
46
+ const target = makeRendererState()
47
+ const ops: WireSyncOp[] = [{ kind: 'set', path: ['world', 'mesherWork'], value: true }]
48
+ applySyncOps(target, ops, fakeWorker())
49
+ expect(target.world.mesherWork).toBe(true)
50
+ expect(source.world.mesherWork).toBe(false)
51
+ })
52
+
53
+ it('top-level set round-trips renderer', () => {
54
+ const target = makeRendererState()
55
+ applySyncOps(target, [{ kind: 'set', path: ['renderer'], value: 'WebGL2 r123' }], fakeWorker())
56
+ expect(target.renderer).toBe('WebGL2 r123')
57
+ })
58
+
59
+ it('nested set creates chunksLoaded key on receiver', () => {
60
+ const target = makeRendererState()
61
+ applySyncOps(target, [{ kind: 'set', path: ['world', 'chunksLoaded', '1,2'], value: true }], fakeWorker())
62
+ expect(target.world.chunksLoaded['1,2']).toBe(true)
63
+ })
64
+
65
+ it('delete op removes heightmap key on receiver', () => {
66
+ const target = makeRendererState()
67
+ target.world.heightmaps['1,2'] = new Int16Array(256)
68
+ applySyncOps(target, [{ kind: 'delete', path: ['world', 'heightmaps', '1,2'] }], fakeWorker())
69
+ expect(target.world.heightmaps['1,2']).toBeUndefined()
70
+ })
71
+
72
+ it('Int16Array value survives copy without neutering sender buffer', () => {
73
+ const source = new Int16Array([1, 2, 3])
74
+ const sender = makeRendererState()
75
+ sender.world.heightmaps['0,0'] = source
76
+ const receiver = makeRendererState()
77
+ const buf = sender.world.heightmaps['0,0']!
78
+ applySyncOps(receiver, [{
79
+ kind: 'set',
80
+ path: ['world', 'heightmaps', '0,0'],
81
+ value: new Int16Array(buf),
82
+ }], fakeWorker())
83
+ expect([...receiver.world.heightmaps['0,0']!]).toEqual([1, 2, 3])
84
+ expect(sender.world.heightmaps['0,0']![0]).toBe(1)
85
+ })
86
+
87
+ it('Vec3 value survives via restorer', () => {
88
+ const target = makeRendererState() as any
89
+ const vec = new Vec3(1, 2, 3)
90
+ applySyncOps(target, [{
91
+ kind: 'set',
92
+ path: ['world', 'intersectMedia'],
93
+ value: { pos: { x: 1, y: 2, z: 3, __restorer: 'Vec3' } },
94
+ }], fakeWorker())
95
+ expect(target.world.intersectMedia.pos).toBeInstanceOf(Vec3)
96
+ expect(target.world.intersectMedia.pos.x).toBe(1)
97
+ })
98
+
99
+ it('batched ops in one tick produce one message with multiple ops', async () => {
100
+ const worker = fakeWorker()
101
+ const syncId = 'test-sync'
102
+ const source = makeRendererState()
103
+ let messageCount = 0
104
+ ;(worker.postMessage as ReturnType<typeof vi.fn>).mockImplementation((data: any) => {
105
+ messageCount++
106
+ expect(data.ops.length).toBeGreaterThanOrEqual(2)
107
+ })
108
+ await new Promise<void>((resolve) => {
109
+ subscribe(source, (ops) => {
110
+ sendWorkerSyncOps(syncId, ops, worker, 'toWorker', 'test')
111
+ resolve()
112
+ })
113
+ source.world.mesherWork = true
114
+ source.renderer = 'batch'
115
+ })
116
+ expect(messageCount).toBe(1)
117
+ })
118
+
119
+ describe('debugWorkerSyncStats', () => {
120
+ beforeEach(() => {
121
+ resetWorkerSyncStatsForTest()
122
+ })
123
+
124
+ afterEach(() => {
125
+ resetWorkerSyncStatsForTest()
126
+ vi.useRealTimers()
127
+ })
128
+
129
+ it('one postMessage increments toWorker by 1 regardless of op count', () => {
130
+ const worker = fakeWorker()
131
+ sendWorkerSyncOps('id', [
132
+ ['set', ['world', 'mesherWork'], true, false],
133
+ ['set', ['renderer'], 'x', '...'],
134
+ ], worker, 'toWorker', 'test')
135
+ expect(worker.postMessage).toHaveBeenCalledTimes(1)
136
+ expect(getWorkerSyncStatsForTest().toWorker).toBe(1)
137
+ })
138
+
139
+ it('applySyncOps with fromWorker counts one receive per message', () => {
140
+ const target = makeRendererState()
141
+ applySyncOps(target, [
142
+ { kind: 'set', path: ['world', 'mesherWork'], value: true },
143
+ { kind: 'set', path: ['renderer'], value: 'y' },
144
+ ], fakeWorker(), 'fromWorker')
145
+ expect(getWorkerSyncStatsForTest().fromWorker).toBe(1)
146
+ })
147
+ })
148
+
149
+ it('setByPath handles length-1 path', () => {
150
+ const target = { renderer: 'old' }
151
+ setByPath(target, ['renderer'], 'new')
152
+ expect(target.renderer).toBe('new')
153
+ })
154
+ })