minecraft-renderer 0.1.62 → 0.1.63
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/dist/minecraft-renderer.js +56 -56
- package/dist/minecraft-renderer.js.meta.json +1 -1
- package/dist/threeWorker.js +398 -398
- package/package.json +1 -1
- package/src/graphicsBackend/config.ts +3 -3
- package/src/graphicsBackend/types.ts +3 -3
- package/src/lib/bindAbortableListener.test.ts +65 -0
- package/src/lib/bindAbortableListener.ts +41 -0
- package/src/lib/workerProxy.ts +238 -118
- package/src/lib/workerSyncOps.test.ts +154 -0
- package/src/lib/worldrendererCommon.ts +70 -48
- package/src/three/documentRenderer.ts +1 -1
- package/src/three/entities.ts +6 -1
- package/src/three/graphicsBackendBase.ts +18 -8
- package/src/three/modules/rain.ts +1 -1
- package/src/three/worldRendererThree.ts +2 -1
- package/src/worldView/worldView.ts +39 -8
- package/src/worldView/worldViewWorkerBridge.test.ts +59 -0
- package/src/lib/workerProxy.restore.test.ts +0 -29
package/package.json
CHANGED
|
@@ -117,8 +117,8 @@ export const getDefaultRendererState = (): {
|
|
|
117
117
|
return {
|
|
118
118
|
reactive: proxy({
|
|
119
119
|
world: {
|
|
120
|
-
chunksLoaded:
|
|
121
|
-
heightmaps:
|
|
120
|
+
chunksLoaded: {},
|
|
121
|
+
heightmaps: {},
|
|
122
122
|
allChunksLoaded: false,
|
|
123
123
|
mesherWork: false,
|
|
124
124
|
instabilityFactors: defaultPerformanceInstabilityFactors(),
|
|
@@ -132,7 +132,7 @@ export const getDefaultRendererState = (): {
|
|
|
132
132
|
worstRenderTime: 0,
|
|
133
133
|
avgRenderTime: 0,
|
|
134
134
|
world: {
|
|
135
|
-
|
|
135
|
+
chunksLoadedCount: 0,
|
|
136
136
|
chunksTotalNumber: 0,
|
|
137
137
|
chunksFullInfo: '-'
|
|
138
138
|
},
|
|
@@ -58,7 +58,7 @@ export interface NonReactiveState {
|
|
|
58
58
|
worstRenderTime: number
|
|
59
59
|
avgRenderTime: number
|
|
60
60
|
world: {
|
|
61
|
-
|
|
61
|
+
chunksLoadedCount: number
|
|
62
62
|
chunksTotalNumber: number
|
|
63
63
|
chunksFullInfo: string
|
|
64
64
|
allChunksLoaded?: boolean
|
|
@@ -75,8 +75,8 @@ export interface NonReactiveState {
|
|
|
75
75
|
/** Renderer reactive state */
|
|
76
76
|
export interface RendererReactiveState {
|
|
77
77
|
world: {
|
|
78
|
-
chunksLoaded:
|
|
79
|
-
heightmaps:
|
|
78
|
+
chunksLoaded: Record<string, true>
|
|
79
|
+
heightmaps: Record<string, Int16Array>
|
|
80
80
|
allChunksLoaded: boolean
|
|
81
81
|
mesherWork: boolean
|
|
82
82
|
/** Low-FPS / render instability factors (see `performanceMonitor`). */
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
//@ts-nocheck
|
|
2
|
+
import { EventEmitter } from 'events'
|
|
3
|
+
import { describe, expect, it } from 'vitest'
|
|
4
|
+
import { bindAbortableEmitterListener, bindAbortableListener } from './bindAbortableListener'
|
|
5
|
+
import { WorldViewWorker } from '../worldView'
|
|
6
|
+
|
|
7
|
+
describe('bindAbortableListener', () => {
|
|
8
|
+
it('removes handler on abort', () => {
|
|
9
|
+
const emitter = new WorldViewWorker()
|
|
10
|
+
const controller = new AbortController()
|
|
11
|
+
let calls = 0
|
|
12
|
+
bindAbortableListener(emitter, 'renderDistance', () => {
|
|
13
|
+
calls++
|
|
14
|
+
}, controller.signal)
|
|
15
|
+
|
|
16
|
+
emitter.emit('renderDistance', 8)
|
|
17
|
+
expect(calls).toBe(1)
|
|
18
|
+
|
|
19
|
+
controller.abort()
|
|
20
|
+
emitter.emit('renderDistance', 12)
|
|
21
|
+
expect(calls).toBe(1)
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
it('abort removes only the bound handler on a shared emitter', () => {
|
|
25
|
+
const emitter = new WorldViewWorker()
|
|
26
|
+
const controllerA = new AbortController()
|
|
27
|
+
const controllerB = new AbortController()
|
|
28
|
+
let callsA = 0
|
|
29
|
+
let callsB = 0
|
|
30
|
+
|
|
31
|
+
bindAbortableListener(emitter, 'renderDistance', () => {
|
|
32
|
+
callsA++
|
|
33
|
+
}, controllerA.signal)
|
|
34
|
+
bindAbortableListener(emitter, 'renderDistance', () => {
|
|
35
|
+
callsB++
|
|
36
|
+
}, controllerB.signal)
|
|
37
|
+
|
|
38
|
+
emitter.emit('renderDistance', 4)
|
|
39
|
+
expect(callsA).toBe(1)
|
|
40
|
+
expect(callsB).toBe(1)
|
|
41
|
+
|
|
42
|
+
controllerA.abort()
|
|
43
|
+
emitter.emit('renderDistance', 6)
|
|
44
|
+
expect(callsA).toBe(1)
|
|
45
|
+
expect(callsB).toBe(2)
|
|
46
|
+
})
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
describe('bindAbortableEmitterListener', () => {
|
|
50
|
+
it('removes handler on abort', () => {
|
|
51
|
+
const emitter = new EventEmitter()
|
|
52
|
+
const controller = new AbortController()
|
|
53
|
+
let calls = 0
|
|
54
|
+
bindAbortableEmitterListener(emitter, 'test', () => {
|
|
55
|
+
calls++
|
|
56
|
+
}, controller.signal)
|
|
57
|
+
|
|
58
|
+
emitter.emit('test')
|
|
59
|
+
expect(calls).toBe(1)
|
|
60
|
+
|
|
61
|
+
controller.abort()
|
|
62
|
+
emitter.emit('test')
|
|
63
|
+
expect(calls).toBe(1)
|
|
64
|
+
})
|
|
65
|
+
})
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
//@ts-nocheck
|
|
2
|
+
import type { EventEmitter } from 'events'
|
|
3
|
+
import type { WorldViewEvents } from '../worldView/types'
|
|
4
|
+
import type { WorldViewWorker } from '../worldView'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Register an EventEmitter listener removed when `signal` aborts.
|
|
8
|
+
* Safe for shared emitters (e.g. worldView) — only removes this handler.
|
|
9
|
+
*/
|
|
10
|
+
export function bindAbortableListener<E extends keyof WorldViewEvents>(
|
|
11
|
+
emitter: Pick<WorldViewWorker, 'on' | 'off'>,
|
|
12
|
+
event: E,
|
|
13
|
+
handler: (...args: WorldViewEvents[E]) => void,
|
|
14
|
+
signal: AbortSignal
|
|
15
|
+
): void {
|
|
16
|
+
emitter.on(event, handler as (...args: any[]) => void)
|
|
17
|
+
if (signal.aborted) {
|
|
18
|
+
emitter.off(event, handler as (...args: any[]) => void)
|
|
19
|
+
return
|
|
20
|
+
}
|
|
21
|
+
signal.addEventListener('abort', () => {
|
|
22
|
+
emitter.off(event, handler as (...args: any[]) => void)
|
|
23
|
+
}, { once: true })
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Same pattern for plain EventEmitters (e.g. resourcesManager). */
|
|
27
|
+
export function bindAbortableEmitterListener(
|
|
28
|
+
emitter: Pick<EventEmitter, 'on' | 'off'>,
|
|
29
|
+
event: string,
|
|
30
|
+
handler: (...args: any[]) => void,
|
|
31
|
+
signal: AbortSignal
|
|
32
|
+
): void {
|
|
33
|
+
emitter.on(event, handler)
|
|
34
|
+
if (signal.aborted) {
|
|
35
|
+
emitter.off(event, handler)
|
|
36
|
+
return
|
|
37
|
+
}
|
|
38
|
+
signal.addEventListener('abort', () => {
|
|
39
|
+
emitter.off(event, handler)
|
|
40
|
+
}, { once: true })
|
|
41
|
+
}
|
package/src/lib/workerProxy.ts
CHANGED
|
@@ -86,104 +86,232 @@ export const useWorkerProxy = <T extends { __workerProxy: Record<string, (...arg
|
|
|
86
86
|
|
|
87
87
|
const DEBUG_SYNC = false
|
|
88
88
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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
|
-
|
|
109
|
-
|
|
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
|
|
121
|
-
|
|
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
|
|
125
|
-
if (
|
|
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
|
-
|
|
128
|
-
|
|
163
|
+
export const prepareOpValueForTransfer = (value: any, worker: Worker): any => {
|
|
164
|
+
if (value == null || typeof value !== 'object') {
|
|
165
|
+
return value
|
|
166
|
+
}
|
|
129
167
|
|
|
130
|
-
if (
|
|
131
|
-
|
|
132
|
-
|
|
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
|
-
|
|
139
|
-
|
|
140
|
-
setInterval(syncToWorker, interval)
|
|
141
|
-
}
|
|
172
|
+
if (typeof value['prepareForTransfer'] === 'function') {
|
|
173
|
+
return value['prepareForTransfer'](worker)
|
|
142
174
|
}
|
|
143
175
|
|
|
144
|
-
if (
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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
|
|
155
|
-
|
|
156
|
-
|
|
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
|
|
160
|
-
|
|
161
|
-
|
|
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
|
|
165
|
-
|
|
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
|
-
|
|
169
|
-
|
|
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
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
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
|
|
180
|
-
|
|
181
|
-
|
|
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
|
-
|
|
184
|
-
|
|
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
|
-
|
|
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,
|
|
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(
|
|
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
|
|