@felipe-software/react-native-gpui 0.0.0-stage → 1.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.
- package/.reanimated-prebuilt/react-native-reanimated.mjs +29373 -0
- package/LICENSE +21 -0
- package/README.md +657 -2
- package/cli/bin.ts +379 -0
- package/cli/bundle.ts +32 -0
- package/cli/commands/do.ts +213 -0
- package/cli/commands/flow.ts +283 -0
- package/cli/commands/get.ts +584 -0
- package/cli/commands/shot.ts +312 -0
- package/cli/commands/trace.ts +293 -0
- package/cli/host.ts +714 -0
- package/cli/hotReload.ts +210 -0
- package/cli/run.ts +66 -0
- package/cli/selectors.ts +205 -0
- package/dist/Animated.d.ts +120 -0
- package/dist/ChatList.d.ts +28 -0
- package/dist/Dimensions.d.ts +31 -0
- package/dist/StyleSheet.d.ts +18 -0
- package/dist/Window.d.ts +33 -0
- package/dist/apis.d.ts +165 -0
- package/dist/app-state.d.ts +27 -0
- package/dist/chat-list-types.d.ts +136 -0
- package/dist/colors.d.ts +32 -0
- package/dist/commands.d.ts +230 -0
- package/dist/components.d.ts +419 -0
- package/dist/hotModules.d.ts +1 -0
- package/dist/index.d.ts +31 -0
- package/dist/index.development.js +14876 -0
- package/dist/index.js +12 -0
- package/dist/keyboard.d.ts +140 -0
- package/dist/native-menu.d.ts +7 -0
- package/dist/platform-driver.d.ts +32 -0
- package/dist/platform-services.d.ts +35 -0
- package/dist/portal.d.ts +43 -0
- package/dist/raf.d.ts +4 -0
- package/dist/reanimated/colors-processor-shim.d.ts +10 -0
- package/dist/reanimated/reanimated-host.d.ts +10 -0
- package/dist/reanimated/seam.d.ts +34 -0
- package/dist/reanimated/ui-entry.d.ts +2 -0
- package/dist/reanimated/worklet-channel.d.ts +10 -0
- package/dist/reanimated/worklet-runtime.d.ts +466 -0
- package/dist/reanimated/worklets.d.ts +199 -0
- package/dist/reconciler.d.ts +102 -0
- package/dist/refresh.d.ts +1 -0
- package/dist/render.d.ts +37 -0
- package/dist/runtime.d.ts +235 -0
- package/dist/surfaces.d.ts +51 -0
- package/dist/types.d.ts +276 -0
- package/dist/ui-runtime.js +42276 -0
- package/dist/window-context.d.ts +7 -0
- package/dist/window-host.d.ts +41 -0
- package/dist/wire-delta.d.ts +20 -0
- package/package.json +176 -5
- package/scripts/bundle-app.mjs +88 -0
- package/scripts/conformance-utils.mjs +293 -0
- package/scripts/hot-update-alias-plugin.mjs +88 -0
- package/scripts/native-package-exports-plugin.mjs +235 -0
- package/scripts/pixel.mjs +152 -0
- package/scripts/png.mjs +198 -0
- package/scripts/react-refresh-swc.mjs +35 -0
- package/scripts/reanimated-bun-plugin.mjs +188 -0
- package/scripts/service-binary.d.mts +27 -0
- package/scripts/service-binary.mjs +100 -0
- package/src/reanimated/colors-processor-shim.ts +119 -0
- package/src/reanimated/reanimated-host.ts +69 -0
- package/src/reanimated/rngpui-reanimated-prebuilt.d.ts +7 -0
- package/src/reanimated/seam.ts +566 -0
- package/src/reanimated/ui-entry.ts +80 -0
- package/src/reanimated/worklet-channel.ts +64 -0
- package/src/reanimated/worklet-runtime.ts +2569 -0
- package/src/reanimated/worklets.ts +1334 -0
- package/src/refresh.ts +33 -0
|
@@ -0,0 +1,1334 @@
|
|
|
1
|
+
// react-native-worklets — the cross-runtime surface for the gpui JavaScriptCore target.
|
|
2
|
+
//
|
|
3
|
+
// port of soot's react-native-worklets stub
|
|
4
|
+
// (packages/compat/src/stubs/react-native-worklets-pkg/index.ts) onto rngpui's
|
|
5
|
+
// existing export surface. the bundler redirect maps every
|
|
6
|
+
// `react-native-worklets` import onto this module, so every export the previous
|
|
7
|
+
// single-runtime stub had MUST continue to exist (the export surface is a contract).
|
|
8
|
+
//
|
|
9
|
+
// ARCHITECTURE (see plans/off-thread-reanimated.md): rngpui runs TWO JavaScriptCore
|
|
10
|
+
// runtimes — the React runtime (role 'react') and the UI runtime (role 'ui') — that
|
|
11
|
+
// cross only as JSON strings through rust host fns, with shared-value primitives in
|
|
12
|
+
// a shared ArrayBuffer (`globalThis.__rngpui_svSlots`). on the React runtime, with
|
|
13
|
+
// the host bridge present (`typeof __rngpui_uiPost === 'function'`), workletized
|
|
14
|
+
// runOnUI / scheduleOnUI / runOnUIAsync DISPATCH to the UI runtime via the worklet
|
|
15
|
+
// runtime. SharedValues mirror through the shared slots so UI-side mappers and
|
|
16
|
+
// animation clocks mutate the same values. runOnJS-tagged callbacks round-trip back
|
|
17
|
+
// to the React runtime.
|
|
18
|
+
//
|
|
19
|
+
// WITHOUT the bridge (bun unit tests, early bundle eval, headless harnesses): every
|
|
20
|
+
// function degrades to LOCAL execution — the previous identity-passthrough behavior
|
|
21
|
+
// — because `runtime()` returns null and `shouldUseRemoteUIRuntime()` is false. the
|
|
22
|
+
// existing unit tests stay green unchanged.
|
|
23
|
+
|
|
24
|
+
// Force the reanimated native seam to evaluate (and install `__reanimatedModuleProxy`,
|
|
25
|
+
// `global._updateProps`, `__RUNTIME_KIND`, the version shims, the frame-callback
|
|
26
|
+
// registry). react-native-worklets is ALWAYS imported by reanimated before its
|
|
27
|
+
// NativeReanimated constructor runs, and reanimated's own seam import can be
|
|
28
|
+
// tree-shaken when the turbomodule path is dead, so this is the reliable install
|
|
29
|
+
// point. the seam's globals must exist before the first `withSpring(...)` evaluates,
|
|
30
|
+
// AND before we install the worklet runtime below (the runtime reads __RUNTIME_KIND-
|
|
31
|
+
// adjacent globals). keep this import FIRST.
|
|
32
|
+
import './seam'
|
|
33
|
+
|
|
34
|
+
import {
|
|
35
|
+
installWorkletRuntime,
|
|
36
|
+
markObjectSV,
|
|
37
|
+
type WorkletRuntime as WorkletRuntimeInstance,
|
|
38
|
+
type WorkletRuntimeRole,
|
|
39
|
+
} from './worklet-runtime'
|
|
40
|
+
import { installChannel, makeChannel } from './worklet-channel'
|
|
41
|
+
|
|
42
|
+
// =============================================================================
|
|
43
|
+
// types
|
|
44
|
+
// =============================================================================
|
|
45
|
+
|
|
46
|
+
export interface WorkletFunction {
|
|
47
|
+
__workletHash?: number
|
|
48
|
+
__initData?: { code?: string; location?: string; sourceMap?: string }
|
|
49
|
+
__closure?: Record<string, unknown>
|
|
50
|
+
(...args: unknown[]): unknown
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface SerializableRef {
|
|
54
|
+
__init?: () => unknown
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface Synchronizable<T> {
|
|
58
|
+
getBlocking(): T
|
|
59
|
+
setBlocking(value: T): void
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface IWorkletsModule {
|
|
63
|
+
makeShareableClone: (value: unknown) => unknown
|
|
64
|
+
scheduleOnUI: (fn: () => void) => void
|
|
65
|
+
executeOnUIRuntimeSync: <T extends (...args: unknown[]) => unknown>(fn: T) => T
|
|
66
|
+
createWorkletRuntime: (name: string, initializer?: () => void) => unknown
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export type MakeShareableClone = (value: unknown) => unknown
|
|
70
|
+
export type WorkletRuntime = unknown
|
|
71
|
+
|
|
72
|
+
// =============================================================================
|
|
73
|
+
// runtime kind — the React runtime claims UI (2) so reanimated v4's runtimeKind
|
|
74
|
+
// branches take the UI path (animations actually run, setNativeProps executes,
|
|
75
|
+
// cancelAnimation writes the SV directly). The seam installs __RUNTIME_KIND=2; the
|
|
76
|
+
// guards below read it live.
|
|
77
|
+
// =============================================================================
|
|
78
|
+
|
|
79
|
+
export enum RuntimeKind {
|
|
80
|
+
ReactNative = 1,
|
|
81
|
+
UI = 2,
|
|
82
|
+
Worker = 3,
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const RNGPUI_UI_RUNTIME_HOLDER = { __rngpui_ui_runtime: true }
|
|
86
|
+
const RNGPUI_UI_SCHEDULER_HOLDER = { __rngpui_ui_scheduler: true }
|
|
87
|
+
|
|
88
|
+
export function getUIRuntimeHolder(): object {
|
|
89
|
+
return RNGPUI_UI_RUNTIME_HOLDER
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function getUISchedulerHolder(): object {
|
|
93
|
+
return RNGPUI_UI_SCHEDULER_HOLDER
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export const UIRuntimeId = 1
|
|
97
|
+
|
|
98
|
+
// =============================================================================
|
|
99
|
+
// runtime install — wire the per-runtime WorkletRuntime + channel at module load,
|
|
100
|
+
// role auto-detected from which rust host post-fn is present in THIS runtime:
|
|
101
|
+
// - __rngpui_uiPost present ⇒ we are the React runtime ⇒ role 'react' (the
|
|
102
|
+
// bundler redirects this stub onto the app bundle = React runtime, but the UI
|
|
103
|
+
// bundle's ui-entry.ts also imports the worklets redirect, so this code runs on
|
|
104
|
+
// both — detect, don't assume).
|
|
105
|
+
// - __rngpui_jsPost present (and not __rngpui_uiPost) ⇒ role 'ui'.
|
|
106
|
+
// Without either host fn (bun unit tests, early bundle eval): no runtime installs
|
|
107
|
+
// and every path stays local exactly as the previous single-runtime stub did.
|
|
108
|
+
//
|
|
109
|
+
// `runtime()` reads the installed singleton off globalThis (so the runtime module
|
|
110
|
+
// and this stub agree without a cross-import cycle for resolution).
|
|
111
|
+
// =============================================================================
|
|
112
|
+
|
|
113
|
+
function detectRole(): WorkletRuntimeRole | null {
|
|
114
|
+
const g = globalThis as { __rngpui_uiPost?: unknown; __rngpui_jsPost?: unknown }
|
|
115
|
+
if (typeof g.__rngpui_uiPost === 'function') return 'react'
|
|
116
|
+
if (typeof g.__rngpui_jsPost === 'function') return 'ui'
|
|
117
|
+
return null
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// shouldUseRemoteUIRuntime: in rngpui this is exactly "host bridge present AND role
|
|
121
|
+
// react" — the React runtime dispatches worklets to the UI runtime; the UI runtime
|
|
122
|
+
// runs them locally (it has __rngpui_jsPost but not __rngpui_uiPost). collapses to
|
|
123
|
+
// false without the bridge, so every dispatch stays local in bun tests.
|
|
124
|
+
function shouldUseRemoteUIRuntime(): boolean {
|
|
125
|
+
return detectRole() === 'react'
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
;(() => {
|
|
129
|
+
const role = detectRole()
|
|
130
|
+
if (!role) return
|
|
131
|
+
const channel = makeChannel(role)
|
|
132
|
+
const rt = installWorkletRuntime({ role, channel })
|
|
133
|
+
if (rt) installChannel(role, rt)
|
|
134
|
+
})()
|
|
135
|
+
|
|
136
|
+
// =============================================================================
|
|
137
|
+
// createShareable — wire an upstream-decorated SharedValue across the React↔UI
|
|
138
|
+
// shared slots. `mutables.ts` calls `createShareable(UIRuntimeId, initial,
|
|
139
|
+
// { hostDecorator, guestDecorator })`; upstream's `mutableHostDecorator` builds the
|
|
140
|
+
// full SharedValue surface (value getter/setter, modify, addListener, _animation,
|
|
141
|
+
// _isReanimatedSharedValue) around `{value: initial}`.
|
|
142
|
+
//
|
|
143
|
+
// routing (faithful to soot):
|
|
144
|
+
// 1. allocate a shared slot with the initial primitive (object slot for arrays /
|
|
145
|
+
// plain objects)
|
|
146
|
+
// 2. let upstream's `mutableHostDecorator` build its full surface
|
|
147
|
+
// 3. redirect writes through the slot via the public addListener bridge so the
|
|
148
|
+
// peer sees them (skip-notify on the bounce-back to avoid loops)
|
|
149
|
+
// 4. attach `_id = slotId` so serializeValue routes this as {kind:'sv'} and the
|
|
150
|
+
// peer's deserializeValue hands the worklet body a proxy bound to the same slot
|
|
151
|
+
//
|
|
152
|
+
// Without the runtime (bun tests) the decorated SV still works locally; it just
|
|
153
|
+
// won't propagate cross-runtime.
|
|
154
|
+
// =============================================================================
|
|
155
|
+
|
|
156
|
+
interface ShareableGuestBase<TValue = unknown> {
|
|
157
|
+
isHost: false
|
|
158
|
+
__shareableRef: true
|
|
159
|
+
getSync(): TValue
|
|
160
|
+
setAsync(value: TValue | ((prev: TValue) => TValue)): void
|
|
161
|
+
setSync(value: TValue | ((prev: TValue) => TValue)): void
|
|
162
|
+
getAsync(): Promise<TValue>
|
|
163
|
+
modify?(modifier: (value: TValue) => TValue, forceUpdate?: boolean): void
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
interface ShareableConfig<H = unknown, G = unknown> {
|
|
167
|
+
hostDecorator?: (host: { value: unknown }) => H
|
|
168
|
+
guestDecorator?: (guest: ShareableGuestBase) => G
|
|
169
|
+
initSynchronously?: boolean
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const _RNGPUI_SAB_SUPPRESS_NOTIFY = Symbol('rngpui_sab_suppress_notify')
|
|
173
|
+
|
|
174
|
+
// keeps the host-decorated SharedValue (its peer-write→`_value` bridge) alive
|
|
175
|
+
// exactly as long as the consumer holds the guest surface, so the slot is reclaimed
|
|
176
|
+
// when the *consumer's* object dies, not earlier.
|
|
177
|
+
const SAB_HOST_TARGET = Symbol('rngpui_sab_host_target')
|
|
178
|
+
|
|
179
|
+
// reclaim slot ids when a SharedValue's JS wrapper is garbage-collected. real
|
|
180
|
+
// reanimated releases the native Shareable when the JS Mutable is collected; this is
|
|
181
|
+
// the faithful analog. it is the SOLE caller of rt.freeSlot (exactly once per
|
|
182
|
+
// registered wrapper), so there is no free→reuse→stale-finalizer ABA: the slot is
|
|
183
|
+
// not on the free-list until this fires, and it fires once.
|
|
184
|
+
const sabSlotFinalizer = new FinalizationRegistry<{
|
|
185
|
+
rt: RuntimeShape
|
|
186
|
+
slotId: number
|
|
187
|
+
}>(({ rt, slotId }) => {
|
|
188
|
+
try {
|
|
189
|
+
rt.freeSlot(slotId)
|
|
190
|
+
} catch {}
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
// throttle the "createShareable failed; falling back to local-only" warnings. when
|
|
194
|
+
// the worklet runtime exhausts its slot pool during a re-render storm,
|
|
195
|
+
// createShareable's catch fires for every one of hundreds of thousands of failed
|
|
196
|
+
// allocs; an unthrottled console.warn per call floods the console and contributes to
|
|
197
|
+
// OOM-ing the runtime. log at most once per window with a suppressed count.
|
|
198
|
+
const shareableFallbackThrottle = { lastAt: 0, suppressed: 0 }
|
|
199
|
+
function warnShareableFallback(label: string, err: unknown): void {
|
|
200
|
+
shareableFallbackThrottle.suppressed++
|
|
201
|
+
const now = Date.now()
|
|
202
|
+
if (now - shareableFallbackThrottle.lastAt <= 2000) return
|
|
203
|
+
console.warn(
|
|
204
|
+
`[rngpui-sab] ${label}; falling back to local-only (+${shareableFallbackThrottle.suppressed} suppressed):`,
|
|
205
|
+
err,
|
|
206
|
+
)
|
|
207
|
+
shareableFallbackThrottle.lastAt = now
|
|
208
|
+
shareableFallbackThrottle.suppressed = 0
|
|
209
|
+
}
|
|
210
|
+
const RNGPUI_REANIMATED_ANIMATION_DESCRIPTOR = Symbol.for('rngpui.reanimatedAnimationDescriptor')
|
|
211
|
+
const RNGPUI_GESTURE_EVENT_TIMESTAMP = Symbol.for('rngpui.gestureEventTimestamp')
|
|
212
|
+
const RNGPUI_ANIMATION_REQUEST = '__rngpuiReanimatedAnimation'
|
|
213
|
+
|
|
214
|
+
const HOST_SET_SHARED_VALUE_WORKLET = markSyntheticWorklet(function (
|
|
215
|
+
sharedValue: unknown,
|
|
216
|
+
payload: unknown,
|
|
217
|
+
) {
|
|
218
|
+
'worklet'
|
|
219
|
+
function revive(value: unknown): unknown {
|
|
220
|
+
if (
|
|
221
|
+
value &&
|
|
222
|
+
typeof value === 'object' &&
|
|
223
|
+
(value as Record<string, unknown>).__rngpuiReanimatedAnimation === true
|
|
224
|
+
) {
|
|
225
|
+
const request = value as { type?: unknown; args?: unknown[] }
|
|
226
|
+
const factories = (
|
|
227
|
+
globalThis as unknown as {
|
|
228
|
+
__rngpuiReanimatedAnimationFactories?: Record<string, (...args: unknown[]) => unknown>
|
|
229
|
+
}
|
|
230
|
+
).__rngpuiReanimatedAnimationFactories
|
|
231
|
+
const factory = typeof request.type === 'string' ? factories?.[request.type] : null
|
|
232
|
+
if (typeof factory !== 'function') {
|
|
233
|
+
throw new Error(`[rngpui worklets] missing animation factory ${request.type}`)
|
|
234
|
+
}
|
|
235
|
+
return factory(...(Array.isArray(request.args) ? request.args.map(revive) : []))
|
|
236
|
+
}
|
|
237
|
+
if (Array.isArray(value)) return value.map(revive)
|
|
238
|
+
if (value && typeof value === 'object' && Object.getPrototypeOf(value) === Object.prototype) {
|
|
239
|
+
const out: Record<string, unknown> = {}
|
|
240
|
+
for (const key of Object.keys(value as Record<string, unknown>)) {
|
|
241
|
+
out[key] = revive((value as Record<string, unknown>)[key])
|
|
242
|
+
}
|
|
243
|
+
return out
|
|
244
|
+
}
|
|
245
|
+
return value
|
|
246
|
+
}
|
|
247
|
+
;(sharedValue as { value: unknown }).value = revive(payload)
|
|
248
|
+
},
|
|
249
|
+
0x50071,
|
|
250
|
+
// explicit source keeps the cross-runtime function independent of transpiler changes to
|
|
251
|
+
// Function#toString. this is the exact JS equivalent evaluated on the UI runtime. keep in sync.
|
|
252
|
+
`function (sharedValue, payload) {
|
|
253
|
+
function revive(value) {
|
|
254
|
+
if (value && typeof value === 'object' && value.__rngpuiReanimatedAnimation === true) {
|
|
255
|
+
var factories = globalThis.__rngpuiReanimatedAnimationFactories
|
|
256
|
+
var factory = typeof value.type === 'string' && factories ? factories[value.type] : null
|
|
257
|
+
if (typeof factory !== 'function') {
|
|
258
|
+
throw new Error('[rngpui worklets] missing animation factory ' + value.type)
|
|
259
|
+
}
|
|
260
|
+
var args = Array.isArray(value.args) ? value.args.map(revive) : []
|
|
261
|
+
return factory.apply(null, args)
|
|
262
|
+
}
|
|
263
|
+
if (Array.isArray(value)) return value.map(revive)
|
|
264
|
+
if (value && typeof value === 'object' && Object.getPrototypeOf(value) === Object.prototype) {
|
|
265
|
+
var out = {}
|
|
266
|
+
for (var key in value) {
|
|
267
|
+
if (Object.prototype.hasOwnProperty.call(value, key)) out[key] = revive(value[key])
|
|
268
|
+
}
|
|
269
|
+
return out
|
|
270
|
+
}
|
|
271
|
+
return value
|
|
272
|
+
}
|
|
273
|
+
sharedValue.value = revive(payload)
|
|
274
|
+
}`)
|
|
275
|
+
|
|
276
|
+
function getSabRuntime(): RuntimeShape | null {
|
|
277
|
+
return runtime()
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function isSabBackable(value: unknown): value is number | boolean {
|
|
281
|
+
return typeof value === 'number' || typeof value === 'boolean'
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function valueToFloat(v: unknown): number {
|
|
285
|
+
if (typeof v === 'number') return v
|
|
286
|
+
if (typeof v === 'boolean') return v ? 1 : 0
|
|
287
|
+
return Number.NaN
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function markSyntheticWorklet<T extends (...args: unknown[]) => unknown>(
|
|
291
|
+
fn: T,
|
|
292
|
+
hash: number,
|
|
293
|
+
code?: string,
|
|
294
|
+
): T & WorkletFunction {
|
|
295
|
+
const worklet = fn as T & WorkletFunction
|
|
296
|
+
Object.defineProperty(worklet, '__workletHash', {
|
|
297
|
+
value: hash,
|
|
298
|
+
enumerable: false,
|
|
299
|
+
configurable: true,
|
|
300
|
+
writable: false,
|
|
301
|
+
})
|
|
302
|
+
Object.defineProperty(worklet, '__initData', {
|
|
303
|
+
// Synthetic worklets that cross runtimes pass explicit source above. toString is
|
|
304
|
+
// sufficient for local compatibility stubs that never leave this source runtime.
|
|
305
|
+
value: { code: code ?? fn.toString() },
|
|
306
|
+
enumerable: false,
|
|
307
|
+
configurable: true,
|
|
308
|
+
writable: false,
|
|
309
|
+
})
|
|
310
|
+
Object.defineProperty(worklet, '__closure', {
|
|
311
|
+
value: {},
|
|
312
|
+
enumerable: false,
|
|
313
|
+
configurable: true,
|
|
314
|
+
writable: true,
|
|
315
|
+
})
|
|
316
|
+
return worklet
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
export function createShareable<TValue, THostDecorated = unknown, TGuestDecorated = unknown>(
|
|
320
|
+
_hostRuntimeId: number,
|
|
321
|
+
initial: TValue,
|
|
322
|
+
config?: ShareableConfig<THostDecorated, TGuestDecorated>,
|
|
323
|
+
): unknown {
|
|
324
|
+
// legacy single-arg form (`createShareable(value)`) — passthrough.
|
|
325
|
+
if (config === undefined && typeof _hostRuntimeId !== 'number') {
|
|
326
|
+
return _hostRuntimeId
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const base: { value: TValue } = { value: initial }
|
|
330
|
+
// upstream wraps `hostDecorator` with the babel-worklets transform, which reads
|
|
331
|
+
// captured variables via `this.__closure`. invoking it locally requires `this` to
|
|
332
|
+
// be the function itself — same convention the worklet runtime uses inside
|
|
333
|
+
// handleRunWorklet (`fn.apply(fn, args)`).
|
|
334
|
+
const decorated = config?.hostDecorator
|
|
335
|
+
? config.hostDecorator.call(config.hostDecorator, base)
|
|
336
|
+
: base
|
|
337
|
+
|
|
338
|
+
// object SVs (arrays, plain objects) route through the parallel object-slot path:
|
|
339
|
+
// each runtime keeps a Map<id, value> and updates ship as svUpdateBatch
|
|
340
|
+
// objectUpdates.
|
|
341
|
+
if (!isSabBackable(initial)) {
|
|
342
|
+
try {
|
|
343
|
+
const rt = getSabRuntime()
|
|
344
|
+
if (!rt) return decorated
|
|
345
|
+
const host = wireObjectSharedValue(rt, decorated, initial)
|
|
346
|
+
if (shouldUseRemoteUIRuntime() && config?.guestDecorator && hasSharedValueId(host)) {
|
|
347
|
+
const slotId = host._id
|
|
348
|
+
const guest = createGuestSharedValue(rt, slotId, 'object', false, config.guestDecorator)
|
|
349
|
+
Object.defineProperty(guest as object, SAB_HOST_TARGET, {
|
|
350
|
+
value: host,
|
|
351
|
+
enumerable: false,
|
|
352
|
+
configurable: true,
|
|
353
|
+
writable: false,
|
|
354
|
+
})
|
|
355
|
+
sabSlotFinalizer.register(guest as object, { rt, slotId })
|
|
356
|
+
return guest
|
|
357
|
+
}
|
|
358
|
+
if (hasSharedValueId(host)) {
|
|
359
|
+
sabSlotFinalizer.register(host as object, { rt, slotId: host._id })
|
|
360
|
+
}
|
|
361
|
+
return host
|
|
362
|
+
} catch (err) {
|
|
363
|
+
warnShareableFallback('object createShareable failed', err)
|
|
364
|
+
return decorated
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
try {
|
|
369
|
+
const rt = getSabRuntime()
|
|
370
|
+
if (!rt) {
|
|
371
|
+
// worklet runtime not yet wired (test contexts without the host bridge). the
|
|
372
|
+
// decorated SV still works locally, it just won't propagate cross-runtime.
|
|
373
|
+
return decorated
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
const isBool = typeof initial === 'boolean'
|
|
377
|
+
// tell the runtime this slot holds a boolean so worklet-side proxy reads restore
|
|
378
|
+
// the boolean type (Float64 0/1 → false/true), matching upstream.
|
|
379
|
+
const slotId = rt.allocSharedValueSlot(valueToFloat(initial), isBool)
|
|
380
|
+
|
|
381
|
+
const target = decorated as unknown as {
|
|
382
|
+
addListener?: (id: number, listener: (v: unknown) => void) => void
|
|
383
|
+
_value?: unknown
|
|
384
|
+
[k: symbol]: boolean | undefined
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// upstream's `_value` is defined non-configurable, so we can't override it.
|
|
388
|
+
// instead use the public `addListener(id, fn)` surface to inject a sync listener
|
|
389
|
+
// that bounces every settled write into the slot. this catches both
|
|
390
|
+
// animation-frame writes (valueSetter → _value = animation.current) and direct
|
|
391
|
+
// `.value =` assignments (which route through valueSetter → _value =).
|
|
392
|
+
//
|
|
393
|
+
// the suppression flag prevents the peer-notify roundtrip from looping: when a
|
|
394
|
+
// peer write arrives, we replay it through `_value =` to fire upstream's local
|
|
395
|
+
// listeners (mappers, observers); that fires our sync listener too, but
|
|
396
|
+
// suppression is set so it skips the write back.
|
|
397
|
+
//
|
|
398
|
+
// we use `syncToPeer` (not `writeSlot`) so the sync listener does NOT fire the
|
|
399
|
+
// runtime's local subscribers — the upstream `_value` listeners already fired the
|
|
400
|
+
// fan-out for this local write. firing the runtime subscribers here would recurse
|
|
401
|
+
// back into upstream's `_value` setter and double the upstream listener work for
|
|
402
|
+
// every local write.
|
|
403
|
+
const SAB_SYNC_LISTENER_ID = -(slotId * 2 + 1)
|
|
404
|
+
if (typeof target.addListener === 'function') {
|
|
405
|
+
target.addListener(SAB_SYNC_LISTENER_ID, (newValue: unknown) => {
|
|
406
|
+
if (target[_RNGPUI_SAB_SUPPRESS_NOTIFY]) return
|
|
407
|
+
rt.syncToPeer(slotId, valueToFloat(newValue))
|
|
408
|
+
})
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// peer writes route through the upstream `_value` setter to fire the local
|
|
412
|
+
// listener fan-out (mappers included) without going through the animation router
|
|
413
|
+
// on `.value`. the listener holds `target` WEAKLY and re-derives the per-instance
|
|
414
|
+
// setter each call: a strong capture here would pin every SharedValue in the
|
|
415
|
+
// long-lived runtime's `listeners` Map forever (the root slot leak). once the
|
|
416
|
+
// wrapper is GC'd the FinalizationRegistry frees the slot, and the resulting
|
|
417
|
+
// releaseSlotState() drops this now-dead listener.
|
|
418
|
+
const weakTarget = new WeakRef(target as object)
|
|
419
|
+
rt.subscribeSlot(slotId, (next) => {
|
|
420
|
+
const t = weakTarget.deref() as typeof target | undefined
|
|
421
|
+
if (!t) return
|
|
422
|
+
const setter = Object.getOwnPropertyDescriptor(t, '_value')?.set
|
|
423
|
+
if (!setter) return
|
|
424
|
+
t[_RNGPUI_SAB_SUPPRESS_NOTIFY] = true
|
|
425
|
+
try {
|
|
426
|
+
setter.call(t, isBool ? next !== 0 : next)
|
|
427
|
+
} finally {
|
|
428
|
+
t[_RNGPUI_SAB_SUPPRESS_NOTIFY] = false
|
|
429
|
+
}
|
|
430
|
+
})
|
|
431
|
+
|
|
432
|
+
// tag with `_id` so the runtime's serializeValue routes this as `{kind:'sv'}` and
|
|
433
|
+
// the peer's deserializeValue hands the worklet body a proxy that reads/writes the
|
|
434
|
+
// same slot.
|
|
435
|
+
Object.defineProperty(target, '_id', {
|
|
436
|
+
value: slotId,
|
|
437
|
+
enumerable: false,
|
|
438
|
+
configurable: true,
|
|
439
|
+
writable: false,
|
|
440
|
+
})
|
|
441
|
+
|
|
442
|
+
if (shouldUseRemoteUIRuntime() && config?.guestDecorator) {
|
|
443
|
+
const guest = createGuestSharedValue(rt, slotId, 'primitive', isBool, config.guestDecorator)
|
|
444
|
+
Object.defineProperty(guest as object, SAB_HOST_TARGET, {
|
|
445
|
+
value: target,
|
|
446
|
+
enumerable: false,
|
|
447
|
+
configurable: true,
|
|
448
|
+
writable: false,
|
|
449
|
+
})
|
|
450
|
+
sabSlotFinalizer.register(guest as object, { rt, slotId })
|
|
451
|
+
return guest
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
sabSlotFinalizer.register(target as object, { rt, slotId })
|
|
455
|
+
return target as THostDecorated
|
|
456
|
+
} catch (err) {
|
|
457
|
+
warnShareableFallback('createShareable failed', err)
|
|
458
|
+
return decorated
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
function hasSharedValueId(value: unknown): value is { _id: number } {
|
|
463
|
+
return (
|
|
464
|
+
!!value && typeof value === 'object' && typeof (value as { _id?: unknown })._id === 'number'
|
|
465
|
+
)
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
type SharedValueSlotKind = 'primitive' | 'object'
|
|
469
|
+
|
|
470
|
+
function createGuestSharedValue<TGuestDecorated>(
|
|
471
|
+
rt: RuntimeShape,
|
|
472
|
+
slotId: number,
|
|
473
|
+
kind: SharedValueSlotKind,
|
|
474
|
+
isBool: boolean,
|
|
475
|
+
guestDecorator: (guest: ShareableGuestBase) => TGuestDecorated,
|
|
476
|
+
): TGuestDecorated {
|
|
477
|
+
let guestSurface: unknown
|
|
478
|
+
const read = () => {
|
|
479
|
+
if (kind === 'object') return rt.readObjectSlot(slotId)
|
|
480
|
+
const value = rt.readSlot(slotId)
|
|
481
|
+
return isBool ? value !== 0 : value
|
|
482
|
+
}
|
|
483
|
+
// the SAB-plumbing methods must be NON-ENUMERABLE. reanimated's
|
|
484
|
+
// `mutableGuestDecorator` `value` getter calls `mutable.getSync()` and the setter
|
|
485
|
+
// calls `mutable.setAsync()`, so these have to survive for the lifetime of the
|
|
486
|
+
// SharedValue. but consumers iterate the SharedValue with `Object.keys()` — e.g.
|
|
487
|
+
// @gorhom/bottom-sheet's `resetContext` worklet does
|
|
488
|
+
// `Object.keys(context).map(k => context[k] = undefined)`. an enumerable
|
|
489
|
+
// getSync/setAsync would be wiped and `value` access would throw `getSync is not a
|
|
490
|
+
// function`.
|
|
491
|
+
//
|
|
492
|
+
// a plain-value write into the guest SharedValue. the shared slot (and the
|
|
493
|
+
// object-slot Map) is shared memory between the React and UI runtimes, so writing
|
|
494
|
+
// it locally is both (a) immediately visible to a same-frame `getSync()` on this
|
|
495
|
+
// runtime and (b) propagated to the peer via the `svUpdateBatch` flush — which
|
|
496
|
+
// fires the UI-side host SharedValue's listeners (mappers, useAnimatedStyle). this
|
|
497
|
+
// preserves the upstream contract: a worklet that writes `sv.value = x` then
|
|
498
|
+
// synchronously reads it back sees its own write.
|
|
499
|
+
const writeLocalSlot = (value: unknown) => {
|
|
500
|
+
if (kind === 'object') {
|
|
501
|
+
rt.writeObjectSlot(slotId, value)
|
|
502
|
+
} else {
|
|
503
|
+
rt.writeSlot(slotId, valueToFloat(value))
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
// route every write through the host runtime ONLY when the payload is (or contains)
|
|
507
|
+
// an animation descriptor — `withTiming`/`withSpring`/… must run their per-frame
|
|
508
|
+
// driver on the reanimated UI runtime. plain values stay synchronous via the shared
|
|
509
|
+
// slot.
|
|
510
|
+
//
|
|
511
|
+
// upstream invariant: ALL writes go through ONE `valueSetter` on ONE UI runtime, and
|
|
512
|
+
// a plain `.value =` write nulls `mutable._animation`, which cancels any running
|
|
513
|
+
// animation (`cancelAnimation` is literally `sv.value = sv.value`). our split runs
|
|
514
|
+
// the animation driver on the UI host but keeps plain writes React-local for
|
|
515
|
+
// same-frame sync read-back — so a plain write would never reach the UI valueSetter
|
|
516
|
+
// and the previous gesture's `onEnd` `withSpring` is never cancelled. mirror the
|
|
517
|
+
// FIRST plain write after an animation to the host valueSetter so its `_animation` is
|
|
518
|
+
// nulled exactly like upstream; keep mirroring until the host setter has actually
|
|
519
|
+
// applied the cancel/reset.
|
|
520
|
+
let hostAnimationPending = false
|
|
521
|
+
let hostPlainSetScheduled = false
|
|
522
|
+
let hostPlainSetPayload: unknown
|
|
523
|
+
let hostPlainSetGeneration = 0
|
|
524
|
+
let hostPlainSetSettledGeneration = 0
|
|
525
|
+
let hostPlainSetInFlight = 0
|
|
526
|
+
const flushHostPlainSet = () => {
|
|
527
|
+
if (!hostPlainSetScheduled) return
|
|
528
|
+
const payload = hostPlainSetPayload
|
|
529
|
+
hostPlainSetScheduled = false
|
|
530
|
+
hostPlainSetPayload = undefined
|
|
531
|
+
const generation = ++hostPlainSetGeneration
|
|
532
|
+
hostPlainSetInFlight++
|
|
533
|
+
void dispatchHostValueSet(rt, guestSurface, payload, true)
|
|
534
|
+
.catch(() => {})
|
|
535
|
+
.finally(() => {
|
|
536
|
+
hostPlainSetInFlight = Math.max(0, hostPlainSetInFlight - 1)
|
|
537
|
+
hostPlainSetSettledGeneration = Math.max(hostPlainSetSettledGeneration, generation)
|
|
538
|
+
if (
|
|
539
|
+
hostPlainSetSettledGeneration >= hostPlainSetGeneration &&
|
|
540
|
+
hostPlainSetInFlight === 0 &&
|
|
541
|
+
!hostPlainSetScheduled
|
|
542
|
+
) {
|
|
543
|
+
hostAnimationPending = false
|
|
544
|
+
}
|
|
545
|
+
})
|
|
546
|
+
}
|
|
547
|
+
const queueHostPlainSet = (value: unknown) => {
|
|
548
|
+
hostPlainSetPayload = encodeHostValue(value)
|
|
549
|
+
if (hostPlainSetScheduled) return
|
|
550
|
+
hostPlainSetScheduled = true
|
|
551
|
+
queueMicrotask(flushHostPlainSet)
|
|
552
|
+
}
|
|
553
|
+
const resolveSetValue = (value: unknown): unknown => {
|
|
554
|
+
if (
|
|
555
|
+
typeof value === 'function' &&
|
|
556
|
+
(value as unknown as Record<string, unknown>).__isAnimationDefinition !== true
|
|
557
|
+
) {
|
|
558
|
+
return (value as (current: unknown) => unknown)(read())
|
|
559
|
+
}
|
|
560
|
+
return value
|
|
561
|
+
}
|
|
562
|
+
const setValue = (value: unknown) => {
|
|
563
|
+
const resolved = resolveSetValue(value)
|
|
564
|
+
if (containsAnimationRequest(resolved)) {
|
|
565
|
+
flushHostPlainSet()
|
|
566
|
+
hostAnimationPending = true
|
|
567
|
+
void dispatchHostValueSet(rt, guestSurface, encodeHostValue(resolved), false).catch(() => {})
|
|
568
|
+
} else {
|
|
569
|
+
writeLocalSlot(resolved)
|
|
570
|
+
if (hostAnimationPending) {
|
|
571
|
+
queueHostPlainSet(resolved)
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
const guest = {} as ShareableGuestBase
|
|
576
|
+
Object.defineProperties(guest, {
|
|
577
|
+
isHost: { value: false, enumerable: false, configurable: true, writable: true },
|
|
578
|
+
__shareableRef: { value: true, enumerable: false, configurable: true, writable: true },
|
|
579
|
+
getSync: { value: read, enumerable: false, configurable: true, writable: true },
|
|
580
|
+
getAsync: {
|
|
581
|
+
value: () => Promise.resolve(read()),
|
|
582
|
+
enumerable: false,
|
|
583
|
+
configurable: true,
|
|
584
|
+
writable: true,
|
|
585
|
+
},
|
|
586
|
+
setAsync: { value: setValue, enumerable: false, configurable: true, writable: true },
|
|
587
|
+
setSync: { value: setValue, enumerable: false, configurable: true, writable: true },
|
|
588
|
+
})
|
|
589
|
+
guestSurface = guestDecorator.call(guestDecorator, guest)
|
|
590
|
+
Object.defineProperty(guestSurface as object, '_id', {
|
|
591
|
+
value: slotId,
|
|
592
|
+
enumerable: false,
|
|
593
|
+
configurable: true,
|
|
594
|
+
writable: false,
|
|
595
|
+
})
|
|
596
|
+
if (kind === 'object') {
|
|
597
|
+
markObjectSV(guestSurface as object)
|
|
598
|
+
}
|
|
599
|
+
return guestSurface as TGuestDecorated
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
function dispatchHostValueSet(
|
|
603
|
+
rt: RuntimeShape,
|
|
604
|
+
guestSurface: unknown,
|
|
605
|
+
payload: unknown,
|
|
606
|
+
awaitReply: boolean,
|
|
607
|
+
): Promise<unknown> {
|
|
608
|
+
return rt
|
|
609
|
+
.dispatchWorklet(HOST_SET_SHARED_VALUE_WORKLET, [guestSurface, payload], { awaitReply })
|
|
610
|
+
.catch((err: unknown) => {
|
|
611
|
+
console.error('[rngpui worklets] shared value host set failed:', err)
|
|
612
|
+
throw err
|
|
613
|
+
})
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
function encodeHostValue(value: unknown): unknown {
|
|
617
|
+
const descriptor = readAnimationDescriptor(value)
|
|
618
|
+
if (descriptor) {
|
|
619
|
+
return {
|
|
620
|
+
[RNGPUI_ANIMATION_REQUEST]: true,
|
|
621
|
+
type: descriptor.type,
|
|
622
|
+
args: descriptor.args.map(encodeHostValue),
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
if (Array.isArray(value)) return value.map(encodeHostValue)
|
|
626
|
+
if (value && typeof value === 'object' && Object.getPrototypeOf(value) === Object.prototype) {
|
|
627
|
+
const out: Record<string, unknown> = {}
|
|
628
|
+
for (const key of Object.keys(value as Record<string, unknown>)) {
|
|
629
|
+
out[key] = encodeHostValue((value as Record<string, unknown>)[key])
|
|
630
|
+
}
|
|
631
|
+
return out
|
|
632
|
+
}
|
|
633
|
+
return value
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
// true when `value` is an animation descriptor (`withTiming`/`withSpring`/…) or has
|
|
637
|
+
// one nested inside an array / plain object — those must run their per-frame driver
|
|
638
|
+
// on the reanimated UI runtime, so the write routes through
|
|
639
|
+
// `HOST_SET_SHARED_VALUE_WORKLET`. plain values write the shared slot directly.
|
|
640
|
+
function containsAnimationRequest(value: unknown): boolean {
|
|
641
|
+
if (!value || typeof value !== 'object') return false
|
|
642
|
+
if (readAnimationDescriptor(value)) return true
|
|
643
|
+
if (Array.isArray(value)) return value.some(containsAnimationRequest)
|
|
644
|
+
if (Object.getPrototypeOf(value) === Object.prototype) {
|
|
645
|
+
for (const key of Object.keys(value as Record<string, unknown>)) {
|
|
646
|
+
if (containsAnimationRequest((value as Record<string, unknown>)[key])) return true
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
return false
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
function readAnimationDescriptor(value: unknown): { type: string; args: unknown[] } | null {
|
|
653
|
+
if (!value || typeof value !== 'object') return null
|
|
654
|
+
const descriptor = (value as { [RNGPUI_REANIMATED_ANIMATION_DESCRIPTOR]?: unknown })[
|
|
655
|
+
RNGPUI_REANIMATED_ANIMATION_DESCRIPTOR
|
|
656
|
+
]
|
|
657
|
+
if (!descriptor || typeof descriptor !== 'object') return null
|
|
658
|
+
const type = (descriptor as { type?: unknown }).type
|
|
659
|
+
const args = (descriptor as { args?: unknown }).args
|
|
660
|
+
if (typeof type !== 'string' || !Array.isArray(args)) return null
|
|
661
|
+
return { type, args }
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
// =============================================================================
|
|
665
|
+
// runtime accessor — duck-typed against worklet-runtime.ts. resolved via globalThis
|
|
666
|
+
// so the stub and the runtime agree without an import-time resolution cycle.
|
|
667
|
+
// =============================================================================
|
|
668
|
+
|
|
669
|
+
interface RuntimeShape {
|
|
670
|
+
dispatchWorklet(
|
|
671
|
+
fn: WorkletFunction,
|
|
672
|
+
args: unknown[],
|
|
673
|
+
opts?: { awaitReply?: boolean; eventTimestamp?: number },
|
|
674
|
+
): Promise<unknown>
|
|
675
|
+
registerJSCallback<T extends (...args: unknown[]) => unknown>(fn: T): T
|
|
676
|
+
allocSharedValueSlot(initial: number, isBool?: boolean): number
|
|
677
|
+
readSlot(id: number): number
|
|
678
|
+
writeSlot(id: number, value: number, skipNotify?: boolean): boolean
|
|
679
|
+
syncToPeer(id: number, value: number): boolean
|
|
680
|
+
subscribeSlot(id: number, fn: (value: number) => void): () => void
|
|
681
|
+
allocObjectSharedValueSlot(initial: unknown): number
|
|
682
|
+
readObjectSlot(id: number): unknown
|
|
683
|
+
writeObjectSlot(id: number, value: unknown, skipNotify?: boolean): boolean
|
|
684
|
+
syncObjectToPeer(id: number, value: unknown): boolean
|
|
685
|
+
subscribeObjectSlot(id: number, fn: (value: unknown) => void): () => void
|
|
686
|
+
freeSlot(id: number): void
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
/**
|
|
690
|
+
* Wire an upstream-decorated object-shaped SharedValue (initial is array or plain
|
|
691
|
+
* object) for cross-runtime propagation. Same routing pattern as the primitive path:
|
|
692
|
+
* sync listener bounces local writes to the peer, peer writes route through `_value`
|
|
693
|
+
* setter so upstream listeners (mappers, useDerivedValue) fan out.
|
|
694
|
+
*/
|
|
695
|
+
function wireObjectSharedValue(rt: RuntimeShape, decorated: unknown, initial: unknown): unknown {
|
|
696
|
+
const slotId = rt.allocObjectSharedValueSlot(initial)
|
|
697
|
+
|
|
698
|
+
const target = decorated as unknown as {
|
|
699
|
+
addListener?: (id: number, listener: (v: unknown) => void) => void
|
|
700
|
+
modify?: (modifier: (value: unknown) => unknown, forceUpdate?: boolean) => void
|
|
701
|
+
value?: unknown
|
|
702
|
+
_value?: unknown
|
|
703
|
+
[k: symbol]: boolean | undefined
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
const SAB_SYNC_LISTENER_ID = -(slotId * 2 + 1)
|
|
707
|
+
if (typeof target.addListener === 'function') {
|
|
708
|
+
target.addListener(SAB_SYNC_LISTENER_ID, (newValue: unknown) => {
|
|
709
|
+
if (target[_RNGPUI_SAB_SUPPRESS_NOTIFY]) return
|
|
710
|
+
rt.syncObjectToPeer(slotId, newValue)
|
|
711
|
+
})
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
const _valueDesc = Object.getOwnPropertyDescriptor(target, '_value')
|
|
715
|
+
|
|
716
|
+
const readCurrentValue = () => {
|
|
717
|
+
try {
|
|
718
|
+
if (_valueDesc?.get) return _valueDesc.get.call(target)
|
|
719
|
+
} catch {}
|
|
720
|
+
try {
|
|
721
|
+
return target.value
|
|
722
|
+
} catch {}
|
|
723
|
+
return initial
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
// upstream ViewDescriptorsSet.add mutates the descriptor array in place and calls
|
|
727
|
+
// modify(..., false). valueSetter then intentionally skips listener fan-out because
|
|
728
|
+
// the returned array reference is unchanged. on native that mutation happens on the
|
|
729
|
+
// UI runtime where mappers read the same shareable. in rngpui it happens on react,
|
|
730
|
+
// so explicitly mirror object-SV modify results into the UI object slot.
|
|
731
|
+
const originalModify = target.modify
|
|
732
|
+
if (typeof originalModify === 'function') {
|
|
733
|
+
target.modify = (modifier, forceUpdate = true) => {
|
|
734
|
+
originalModify.call(target, modifier, forceUpdate)
|
|
735
|
+
if (!target[_RNGPUI_SAB_SUPPRESS_NOTIFY]) {
|
|
736
|
+
rt.syncObjectToPeer(slotId, readCurrentValue())
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
// weak target + re-derived setter, same rationale as the primitive path: a strong
|
|
742
|
+
// capture would pin every object SharedValue in the runtime's objectListeners Map
|
|
743
|
+
// forever. releaseSlotState() (via the FinalizationRegistry) drops this listener
|
|
744
|
+
// once the wrapper is collected.
|
|
745
|
+
const weakObjTarget = new WeakRef(target as object)
|
|
746
|
+
rt.subscribeObjectSlot(slotId, (next) => {
|
|
747
|
+
const t = weakObjTarget.deref() as typeof target | undefined
|
|
748
|
+
if (!t) return
|
|
749
|
+
const setter = Object.getOwnPropertyDescriptor(t, '_value')?.set
|
|
750
|
+
if (!setter) return
|
|
751
|
+
t[_RNGPUI_SAB_SUPPRESS_NOTIFY] = true
|
|
752
|
+
try {
|
|
753
|
+
setter.call(t, next)
|
|
754
|
+
} finally {
|
|
755
|
+
t[_RNGPUI_SAB_SUPPRESS_NOTIFY] = false
|
|
756
|
+
}
|
|
757
|
+
})
|
|
758
|
+
|
|
759
|
+
Object.defineProperty(target, '_id', {
|
|
760
|
+
value: slotId,
|
|
761
|
+
enumerable: false,
|
|
762
|
+
configurable: true,
|
|
763
|
+
writable: false,
|
|
764
|
+
})
|
|
765
|
+
// brand so the runtime's serializeValue routes this as kind:'svObject' instead of
|
|
766
|
+
// the slot-backed kind:'sv'.
|
|
767
|
+
markObjectSV(target as object)
|
|
768
|
+
|
|
769
|
+
return target
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
function runtime(): RuntimeShape | null {
|
|
773
|
+
const r = (globalThis as { __rngpui_worklet_runtime?: RuntimeShape }).__rngpui_worklet_runtime
|
|
774
|
+
return r ?? null
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
function isWorkletFn(fn: unknown): fn is WorkletFunction {
|
|
778
|
+
if (typeof fn !== 'function') return false
|
|
779
|
+
const f = fn as WorkletFunction
|
|
780
|
+
return typeof f.__workletHash === 'number' && typeof f.__initData?.code === 'string'
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
// =============================================================================
|
|
784
|
+
// core dispatchers
|
|
785
|
+
// =============================================================================
|
|
786
|
+
|
|
787
|
+
/**
|
|
788
|
+
* In real reanimated, runOnJS schedules `fn` on the JS runtime. On rngpui the React
|
|
789
|
+
* runtime IS the JS runtime, so the wrapper executes locally — the worklet runtime
|
|
790
|
+
* registers the wrapper so that when it's captured in a closure that later ships to
|
|
791
|
+
* the UI runtime, the UI-side proxy routes call-args back here. Without the runtime,
|
|
792
|
+
* identity.
|
|
793
|
+
*/
|
|
794
|
+
export function runOnJS<T extends (...args: unknown[]) => unknown>(fn: T): T {
|
|
795
|
+
const rt = runtime()
|
|
796
|
+
if (!rt) return fn
|
|
797
|
+
return rt.registerJSCallback(fn)
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
/**
|
|
801
|
+
* `runOnUI(fn)` returns a wrapper that schedules workletized functions on the UI
|
|
802
|
+
* runtime; the UI runtime itself runs them on its local microtask path.
|
|
803
|
+
*/
|
|
804
|
+
export function runOnUI<T extends (...args: unknown[]) => unknown>(fn: T): T {
|
|
805
|
+
const wrapped = ((...args: unknown[]) => {
|
|
806
|
+
scheduleOnUI(fn, ...args)
|
|
807
|
+
}) as unknown as T
|
|
808
|
+
return wrapped
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
/**
|
|
812
|
+
* `runOnRuntime(runtime, fn)` is the explicit cross-runtime escape hatch. rngpui has
|
|
813
|
+
* one peer runtime (the UI runtime); this routes through the worklet runtime when
|
|
814
|
+
* available so callers that genuinely need off-thread execution still get it.
|
|
815
|
+
* Degrades to local execution when no peer is wired.
|
|
816
|
+
*/
|
|
817
|
+
export function runOnRuntime<T extends (...args: unknown[]) => unknown>(
|
|
818
|
+
_runtime: unknown,
|
|
819
|
+
fn: T,
|
|
820
|
+
): T {
|
|
821
|
+
const wrapped = ((...args: unknown[]) => {
|
|
822
|
+
const rt = runtime()
|
|
823
|
+
if (!rt || !isWorkletFn(fn)) return (fn as (...a: unknown[]) => unknown).apply(fn, args)
|
|
824
|
+
return rt.dispatchWorklet(fn as unknown as WorkletFunction, args, { awaitReply: false })
|
|
825
|
+
}) as unknown as T
|
|
826
|
+
return wrapped
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
/** Real signature: scheduleOnRN(fn, ...args) — calls fn(...args) async. */
|
|
830
|
+
export function scheduleOnRN<T extends (...args: unknown[]) => unknown>(
|
|
831
|
+
fn: T,
|
|
832
|
+
...args: unknown[]
|
|
833
|
+
): void {
|
|
834
|
+
if (typeof fn !== 'function') return
|
|
835
|
+
const scheduledArgs = args.map(cloneScheduledValue)
|
|
836
|
+
// we ARE the RN runtime (react). just queue locally.
|
|
837
|
+
queueMicrotask(() => {
|
|
838
|
+
try {
|
|
839
|
+
fn.apply(null, scheduledArgs)
|
|
840
|
+
} catch (err) {
|
|
841
|
+
console.error('[rngpui worklets] scheduleOnRN threw:', err)
|
|
842
|
+
}
|
|
843
|
+
})
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
function cloneScheduledValue(value: unknown): unknown {
|
|
847
|
+
if (Array.isArray(value)) return value.map(cloneScheduledValue)
|
|
848
|
+
if (!value || typeof value !== 'object') return value
|
|
849
|
+
const proto = Object.getPrototypeOf(value)
|
|
850
|
+
if (proto !== Object.prototype && proto !== null) return value
|
|
851
|
+
return Object.fromEntries(
|
|
852
|
+
Object.entries(value as Record<string, unknown>).map(([key, item]) => [
|
|
853
|
+
key,
|
|
854
|
+
cloneScheduledValue(item),
|
|
855
|
+
]),
|
|
856
|
+
)
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
/**
|
|
860
|
+
* `scheduleOnUI(fn, ...args)` — real iOS schedules `fn` on the UI runtime. rngpui's
|
|
861
|
+
* UI runtime is the second JavaScriptCore runtime, so workletized callbacks dispatch there
|
|
862
|
+
* from the React runtime. plain callbacks (not serializable worklets) and the no-
|
|
863
|
+
* bridge path keep the local microtask behavior.
|
|
864
|
+
*/
|
|
865
|
+
export function scheduleOnUI<T extends (...args: unknown[]) => unknown>(
|
|
866
|
+
fn: T,
|
|
867
|
+
...args: unknown[]
|
|
868
|
+
): void {
|
|
869
|
+
const rt = runtime()
|
|
870
|
+
const isWorklet = isWorkletFn(fn)
|
|
871
|
+
const dispatchToShell = shouldUseRemoteUIRuntime() && !!rt && isWorklet
|
|
872
|
+
if (dispatchToShell) {
|
|
873
|
+
rt.dispatchWorklet(fn as unknown as WorkletFunction, args, { awaitReply: false }).catch(
|
|
874
|
+
(err: unknown) => {
|
|
875
|
+
console.error('[rngpui worklets] scheduleOnUI dispatch failed:', err)
|
|
876
|
+
},
|
|
877
|
+
)
|
|
878
|
+
return
|
|
879
|
+
}
|
|
880
|
+
queueMicrotask(() => {
|
|
881
|
+
try {
|
|
882
|
+
// call with fn as `this` so the babel-emitted `this.__closure.X` references
|
|
883
|
+
// resolve, same convention as the worklet runtime.
|
|
884
|
+
;(fn as { _recur?: unknown })._recur = fn
|
|
885
|
+
fn.apply(fn, args)
|
|
886
|
+
} catch (err) {
|
|
887
|
+
console.error('[rngpui worklets] scheduleOnUI local threw:', err)
|
|
888
|
+
}
|
|
889
|
+
})
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
/**
|
|
893
|
+
* Invoke a GestureDetector lifecycle callback respecting RN's worklet / non-worklet
|
|
894
|
+
* runtime split. a callback that IS a worklet runs on the UI runtime — the same
|
|
895
|
+
* runtime as the `useAnimatedStyle` mapper and the `withTiming`/`withSpring` driver —
|
|
896
|
+
* so a shared value's `valueSetter` animation lifecycle stays coherent.
|
|
897
|
+
*
|
|
898
|
+
* the `gesture` carries RNGH's `runOnJS` flag. a callback is dispatched to the UI
|
|
899
|
+
* runtime ONLY when it is a worklet AND the gesture did not opt into `runOnJS(true)`.
|
|
900
|
+
* a `runOnJS(true)` gesture (e.g. tamagui's press gesture, whose onBegin/onEnd are
|
|
901
|
+
* babel-workletized yet mutate React state) must run on the React runtime. a
|
|
902
|
+
* non-worklet callback — and every callback when the bridge is absent — runs
|
|
903
|
+
* synchronously on the React runtime as before.
|
|
904
|
+
*/
|
|
905
|
+
export function invokeGestureCallback(
|
|
906
|
+
// biome-ignore lint/suspicious/noExplicitAny: gesture callbacks carry event-specific payloads
|
|
907
|
+
cb: ((...a: any[]) => unknown) | null | undefined,
|
|
908
|
+
gesture: { _runOnJS?: boolean } | null | undefined,
|
|
909
|
+
...args: unknown[]
|
|
910
|
+
): void {
|
|
911
|
+
if (typeof cb !== 'function') return
|
|
912
|
+
const rt = runtime()
|
|
913
|
+
const runOnJSFlag = gesture?._runOnJS === true
|
|
914
|
+
const eventTimestamp =
|
|
915
|
+
args[0] &&
|
|
916
|
+
typeof args[0] === 'object' &&
|
|
917
|
+
typeof (args[0] as Record<symbol, unknown>)[RNGPUI_GESTURE_EVENT_TIMESTAMP] === 'number'
|
|
918
|
+
? ((args[0] as Record<symbol, unknown>)[RNGPUI_GESTURE_EVENT_TIMESTAMP] as number)
|
|
919
|
+
: undefined
|
|
920
|
+
if (rt && shouldUseRemoteUIRuntime() && isWorkletFn(cb) && !runOnJSFlag) {
|
|
921
|
+
const dispatchOpts: { awaitReply: false; eventTimestamp?: number } = { awaitReply: false }
|
|
922
|
+
if (typeof eventTimestamp === 'number') {
|
|
923
|
+
dispatchOpts.eventTimestamp = eventTimestamp
|
|
924
|
+
}
|
|
925
|
+
rt.dispatchWorklet(cb as unknown as WorkletFunction, args, dispatchOpts).catch(
|
|
926
|
+
(err: unknown) => {
|
|
927
|
+
console.error('[rngpui rngh] gesture callback dispatch failed:', err)
|
|
928
|
+
},
|
|
929
|
+
)
|
|
930
|
+
return
|
|
931
|
+
}
|
|
932
|
+
;(cb as { _recur?: unknown })._recur = cb
|
|
933
|
+
cb.apply(cb, args)
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
/**
|
|
937
|
+
* Sync UI execution is not feasible across runtimes (a blocking wait on the React
|
|
938
|
+
* runtime would freeze the bundle). Best-effort: identity. Upstream only uses
|
|
939
|
+
* executeOnUIRuntimeSync for init-time reads — apps that genuinely need synchronous
|
|
940
|
+
* UI-runtime semantics will see local/stale state, which is acceptable for those
|
|
941
|
+
* bootstrap reads.
|
|
942
|
+
*/
|
|
943
|
+
export function executeOnUIRuntimeSync<T extends (...args: unknown[]) => unknown>(fn: T): T {
|
|
944
|
+
return fn
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
/** Sync runOnUI — runs LOCALLY on the React runtime. cross-runtime sync is impossible;
|
|
948
|
+
* upstream only uses this for init-time reads. set `this = fn` so the babel-emitted
|
|
949
|
+
* `this.__closure.X` and `this._recur` references inside worklet bodies resolve. */
|
|
950
|
+
export function runOnUISync<T extends (...args: unknown[]) => unknown>(
|
|
951
|
+
fn: T,
|
|
952
|
+
...args: unknown[]
|
|
953
|
+
): unknown {
|
|
954
|
+
;(fn as { _recur?: unknown })._recur = fn
|
|
955
|
+
return fn.apply(fn, args)
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
// runOnUIAsync awaits the dispatch result.
|
|
959
|
+
export function runOnUIAsync<T extends (...args: unknown[]) => unknown>(
|
|
960
|
+
fn: T,
|
|
961
|
+
...args: unknown[]
|
|
962
|
+
): Promise<unknown> {
|
|
963
|
+
const rt = runtime()
|
|
964
|
+
if (!rt || !isWorkletFn(fn) || !shouldUseRemoteUIRuntime()) {
|
|
965
|
+
return Promise.resolve(fn.apply(fn, args))
|
|
966
|
+
}
|
|
967
|
+
return rt.dispatchWorklet(fn as unknown as WorkletFunction, args, { awaitReply: true })
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
// =============================================================================
|
|
971
|
+
// shareables — rngpui shares state via the slot-backed SharedValue layer when
|
|
972
|
+
// applicable; the rest of these are identity matching the previous stub contract.
|
|
973
|
+
// =============================================================================
|
|
974
|
+
|
|
975
|
+
// stable across the React realm; the runtime serializer reads the same Symbol.for
|
|
976
|
+
// key to emit a `shareableRef` instead of a deep snapshot, so a re-shipped shareable
|
|
977
|
+
// resolves to the SAME persistent UI-side object (upstream `serializableMappingCache`
|
|
978
|
+
// contract). without the stamp the serializer deep-clones reanimated's `remoteState`
|
|
979
|
+
// on every stopMapper/startMapper, breaking 2nd-animation spring continuity.
|
|
980
|
+
const SHAREABLE_ID_KEY = Symbol.for('rngpui.shareableId')
|
|
981
|
+
let nextShareableId = 1
|
|
982
|
+
|
|
983
|
+
export function makeShareable<T>(value: T): T {
|
|
984
|
+
// identity on the home (react) runtime — matches upstream, where the shareable on
|
|
985
|
+
// its home runtime is just the object. only stamp plain mutable records (reanimated
|
|
986
|
+
// `remoteState`, scroll-handler state); leave primitives / SharedValues / functions
|
|
987
|
+
// / frozen objects untouched.
|
|
988
|
+
if (
|
|
989
|
+
value !== null &&
|
|
990
|
+
typeof value === 'object' &&
|
|
991
|
+
Object.getPrototypeOf(value) === Object.prototype &&
|
|
992
|
+
!Object.isFrozen(value)
|
|
993
|
+
) {
|
|
994
|
+
const rec = value as Record<symbol, unknown>
|
|
995
|
+
if (typeof rec[SHAREABLE_ID_KEY] !== 'number') {
|
|
996
|
+
try {
|
|
997
|
+
Object.defineProperty(value, SHAREABLE_ID_KEY, {
|
|
998
|
+
value: nextShareableId++,
|
|
999
|
+
enumerable: false,
|
|
1000
|
+
writable: false,
|
|
1001
|
+
configurable: false,
|
|
1002
|
+
})
|
|
1003
|
+
} catch {
|
|
1004
|
+
// sealed between check and define — fall back to identity (the deep-snapshot
|
|
1005
|
+
// path; no continuity, but no crash).
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
return value
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
export function createSerializable(config: { __init?: () => unknown }): unknown {
|
|
1013
|
+
return config
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
export function createSynchronizable<T>(initial: T): Synchronizable<T> {
|
|
1017
|
+
let value = initial
|
|
1018
|
+
return {
|
|
1019
|
+
getBlocking() {
|
|
1020
|
+
return value
|
|
1021
|
+
},
|
|
1022
|
+
setBlocking(v: T) {
|
|
1023
|
+
value = v
|
|
1024
|
+
},
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
const SERIALIZABLE_MAPPING_CACHE = Symbol.for('rngpui.serializableMappingCache')
|
|
1029
|
+
export const serializableMappingCache = ((
|
|
1030
|
+
globalThis as Record<symbol, WeakMap<object, unknown> | undefined>
|
|
1031
|
+
)[SERIALIZABLE_MAPPING_CACHE] ??= new WeakMap<object, unknown>())
|
|
1032
|
+
|
|
1033
|
+
// =============================================================================
|
|
1034
|
+
// worklet introspection — polyfill __closure/__workletHash on demand so upstream's
|
|
1035
|
+
// areWorkletsEqual (useHandler/useEvent) never throws on a not-yet-transformed fn.
|
|
1036
|
+
// =============================================================================
|
|
1037
|
+
|
|
1038
|
+
export function isWorkletFunction(fn: unknown): fn is WorkletFunction {
|
|
1039
|
+
if (typeof fn !== 'function') return false
|
|
1040
|
+
const f = fn as WorkletFunction
|
|
1041
|
+
if (f.__closure === undefined) {
|
|
1042
|
+
try {
|
|
1043
|
+
Object.defineProperty(f, '__closure', { value: {}, writable: true, configurable: true })
|
|
1044
|
+
} catch {}
|
|
1045
|
+
}
|
|
1046
|
+
if (f.__workletHash === undefined) {
|
|
1047
|
+
try {
|
|
1048
|
+
Object.defineProperty(f, '__workletHash', { value: 0, writable: true, configurable: true })
|
|
1049
|
+
} catch {}
|
|
1050
|
+
}
|
|
1051
|
+
return true
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
export function createWorkletRuntime(name: string, _initializer?: () => void): unknown {
|
|
1055
|
+
return { name }
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
export function callMicrotasks(): void {
|
|
1059
|
+
// host js engine drains microtasks after each loop tick.
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
export const WorkletsModule: IWorkletsModule = {
|
|
1063
|
+
makeShareableClone: (value: unknown) => value,
|
|
1064
|
+
scheduleOnUI: (fn: () => void) => scheduleOnUI(fn),
|
|
1065
|
+
executeOnUIRuntimeSync: (fn) => fn,
|
|
1066
|
+
createWorkletRuntime: (name: string) => ({ name }),
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
export function useWorklet<T>(fn: T, _deps?: unknown[]): T {
|
|
1070
|
+
return fn
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
export function createWorklet<T extends (...args: unknown[]) => unknown>(fn: T): T {
|
|
1074
|
+
return fn
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
export class WorkletRuntimeClass {
|
|
1078
|
+
name: string
|
|
1079
|
+
constructor(name = 'default') {
|
|
1080
|
+
this.name = name
|
|
1081
|
+
}
|
|
1082
|
+
run<T>(fn: () => T): T {
|
|
1083
|
+
return fn()
|
|
1084
|
+
}
|
|
1085
|
+
destroy(): void {}
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
export const Worklets = {
|
|
1089
|
+
defaultContext: { name: 'default' },
|
|
1090
|
+
createContext(name = 'default') {
|
|
1091
|
+
return { name }
|
|
1092
|
+
},
|
|
1093
|
+
createRuntime(name = 'default', _initializer?: () => void) {
|
|
1094
|
+
return new WorkletRuntimeClass(name)
|
|
1095
|
+
},
|
|
1096
|
+
createSharedValue<T>(initial: T) {
|
|
1097
|
+
return { value: initial }
|
|
1098
|
+
},
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
export function currentContext() {
|
|
1102
|
+
return Worklets.defaultContext
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
export const shareableMappingCache = serializableMappingCache
|
|
1106
|
+
export function makeShareableCloneRecursive(value: unknown) {
|
|
1107
|
+
return value
|
|
1108
|
+
}
|
|
1109
|
+
export function makeShareableCloneOnUIRecursive(value: unknown) {
|
|
1110
|
+
return value
|
|
1111
|
+
}
|
|
1112
|
+
export function isShareableRef(_value: unknown): boolean {
|
|
1113
|
+
return false
|
|
1114
|
+
}
|
|
1115
|
+
export const isSerializableRef = isShareableRef
|
|
1116
|
+
|
|
1117
|
+
// =============================================================================
|
|
1118
|
+
// runtime-kind type guards — the React runtime claims UI (2) via the seam's
|
|
1119
|
+
// __RUNTIME_KIND=2, so isUIRuntime/isWorkletRuntime are true and isRNRuntime is
|
|
1120
|
+
// false. Upstream gates the runOnUI hop on these; reporting UI keeps reanimated's
|
|
1121
|
+
// hook/component layer on the eager path while runOnUI still dispatches to the real
|
|
1122
|
+
// UI runtime when the bridge is present. Read the live global so the guards stay
|
|
1123
|
+
// honest if the kind ever changes.
|
|
1124
|
+
// =============================================================================
|
|
1125
|
+
|
|
1126
|
+
export function getRuntimeKind(): RuntimeKind {
|
|
1127
|
+
const kind = (globalThis as { __RUNTIME_KIND?: number }).__RUNTIME_KIND
|
|
1128
|
+
return kind === RuntimeKind.ReactNative || kind === RuntimeKind.Worker ? kind : RuntimeKind.UI
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
export function scheduleOnRuntime(_runtime: unknown, fn: WorkletFunction, ...args: unknown[]) {
|
|
1132
|
+
scheduleOnUI(fn, ...args)
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
export function isRNRuntime(): boolean {
|
|
1136
|
+
return getRuntimeKind() === RuntimeKind.ReactNative
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
export function isUIRuntime(): boolean {
|
|
1140
|
+
return getRuntimeKind() === RuntimeKind.UI
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
export function isWorkerRuntime(): boolean {
|
|
1144
|
+
return getRuntimeKind() === RuntimeKind.Worker
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
export function isWorkletRuntime(): boolean {
|
|
1148
|
+
return getRuntimeKind() !== RuntimeKind.ReactNative
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
export function runOnRuntimeAsync<T extends (...args: unknown[]) => unknown>(
|
|
1152
|
+
runtime: unknown,
|
|
1153
|
+
fn: T,
|
|
1154
|
+
): T {
|
|
1155
|
+
return runOnRuntime(runtime, fn)
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
export function runOnRuntimeSyncWithId<T extends (...args: unknown[]) => unknown>(
|
|
1159
|
+
_runtimeId: number,
|
|
1160
|
+
fn: T,
|
|
1161
|
+
...args: unknown[]
|
|
1162
|
+
): unknown {
|
|
1163
|
+
return runOnUISync(fn, ...args)
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
export function scheduleOnRuntimeWithId(
|
|
1167
|
+
_runtimeId: number,
|
|
1168
|
+
fn: WorkletFunction,
|
|
1169
|
+
...args: unknown[]
|
|
1170
|
+
): void {
|
|
1171
|
+
scheduleOnUI(fn, ...args)
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
export function isShareable(value: unknown): boolean {
|
|
1175
|
+
if (value == null || typeof value !== 'object') return false
|
|
1176
|
+
const v = value as { __isShareable?: boolean; _isReanimatedSharedValue?: boolean }
|
|
1177
|
+
return Boolean(v.__isShareable || v._isReanimatedSharedValue)
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
export function isSynchronizable(value: unknown): boolean {
|
|
1181
|
+
if (value == null || typeof value !== 'object') return false
|
|
1182
|
+
const v = value as { __isSynchronizable?: boolean; addListener?: unknown; removeListener?: unknown }
|
|
1183
|
+
if (v.__isSynchronizable) return true
|
|
1184
|
+
return typeof v.addListener === 'function' && typeof v.removeListener === 'function'
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
const _customSerializables: Array<{ klass: unknown; serializer: unknown }> = []
|
|
1188
|
+
export function registerCustomSerializable(klass: unknown, serializer: unknown): void {
|
|
1189
|
+
_customSerializables.push({ klass, serializer })
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
export function getStaticFeatureFlag(_name: string): boolean {
|
|
1193
|
+
return false
|
|
1194
|
+
}
|
|
1195
|
+
export function getDynamicFeatureFlag(_name: string): boolean {
|
|
1196
|
+
return false
|
|
1197
|
+
}
|
|
1198
|
+
export function setDynamicFeatureFlag(_name: string, _value: boolean): void {}
|
|
1199
|
+
|
|
1200
|
+
// Reanimated installs this debug hook during module initialization. Its native
|
|
1201
|
+
// implementation is a no-op that reports false, and RNGPUI has no slow-animation mode.
|
|
1202
|
+
export function toggleSlowAnimationsOnUIRuntime(): boolean {
|
|
1203
|
+
return false
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1206
|
+
export default {
|
|
1207
|
+
runOnJS,
|
|
1208
|
+
runOnUI,
|
|
1209
|
+
runOnRuntime,
|
|
1210
|
+
scheduleOnRN,
|
|
1211
|
+
scheduleOnUI,
|
|
1212
|
+
makeShareable,
|
|
1213
|
+
createSerializable,
|
|
1214
|
+
createSynchronizable,
|
|
1215
|
+
serializableMappingCache,
|
|
1216
|
+
isWorkletFunction,
|
|
1217
|
+
executeOnUIRuntimeSync,
|
|
1218
|
+
createWorkletRuntime,
|
|
1219
|
+
callMicrotasks,
|
|
1220
|
+
WorkletsModule,
|
|
1221
|
+
RuntimeKind,
|
|
1222
|
+
useWorklet,
|
|
1223
|
+
createWorklet,
|
|
1224
|
+
WorkletRuntime: WorkletRuntimeClass,
|
|
1225
|
+
Worklets,
|
|
1226
|
+
currentContext,
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
// install `globalThis.__workletsModuleProxy` so reanimated's NativeWorklets
|
|
1230
|
+
// constructor doesn't throw "Native part of Worklets doesn't seem to be initialized"
|
|
1231
|
+
// before the bundle finishes evaluating. Identity-shaped serializables + local/
|
|
1232
|
+
// dispatched scheduling — there is one worklet execution path (this module) for
|
|
1233
|
+
// mappers, useAnimatedStyle, _updateProps.
|
|
1234
|
+
;(() => {
|
|
1235
|
+
const g = globalThis as Record<string, unknown> & { __workletsModuleProxy?: unknown }
|
|
1236
|
+
if (g.__workletsModuleProxy !== undefined) return
|
|
1237
|
+
const wrapAsSerializable = (value: unknown) => ({ __init: () => value })
|
|
1238
|
+
const proxy = {
|
|
1239
|
+
makeShareableClone: (value: unknown) => value,
|
|
1240
|
+
makeShareableCloneOnUI: (value: unknown) => value,
|
|
1241
|
+
createSerializable: (value: unknown) => wrapAsSerializable(value),
|
|
1242
|
+
createSerializableImport: () => wrapAsSerializable(undefined),
|
|
1243
|
+
createSerializableString: (s: string) => wrapAsSerializable(s),
|
|
1244
|
+
createSerializableNumber: (n: number) => wrapAsSerializable(n),
|
|
1245
|
+
createSerializableBoolean: (b: boolean) => wrapAsSerializable(b),
|
|
1246
|
+
createSerializableBigInt: (b: bigint) => wrapAsSerializable(b),
|
|
1247
|
+
createSerializableUndefined: () => wrapAsSerializable(undefined),
|
|
1248
|
+
createSerializableNull: () => wrapAsSerializable(null),
|
|
1249
|
+
createSerializableTurboModuleLike: (props: object) => wrapAsSerializable(props),
|
|
1250
|
+
createSerializableObject: (obj: object) => wrapAsSerializable(obj),
|
|
1251
|
+
createSerializableHostObject: (obj: object) => wrapAsSerializable(obj),
|
|
1252
|
+
createSerializableArray: (arr: unknown[]) => wrapAsSerializable(arr),
|
|
1253
|
+
createSerializableMap: (keys: unknown[], values: unknown[]) =>
|
|
1254
|
+
wrapAsSerializable(new Map(keys.map((k, i) => [k, values[i]]))),
|
|
1255
|
+
createSerializableSet: (values: unknown[]) => wrapAsSerializable(new Set(values)),
|
|
1256
|
+
createSerializableInitializer: (obj: object) => wrapAsSerializable(obj),
|
|
1257
|
+
createSerializableFunction: (fn: (...a: unknown[]) => unknown) => wrapAsSerializable(fn),
|
|
1258
|
+
createSerializableWorklet: (worklet: object) => wrapAsSerializable(worklet),
|
|
1259
|
+
createCustomSerializable: (data: unknown) => data,
|
|
1260
|
+
registerCustomSerializable: () => {},
|
|
1261
|
+
createShareable: (_hostRuntimeId: number, initial: unknown) => wrapAsSerializable(initial),
|
|
1262
|
+
scheduleOnUI: (serializable: { __init?: () => unknown }) => {
|
|
1263
|
+
const fn = serializable?.__init?.()
|
|
1264
|
+
if (typeof fn === 'function') scheduleOnUI(fn as (...args: unknown[]) => unknown)
|
|
1265
|
+
},
|
|
1266
|
+
runOnUISync: (worklet: { __init?: () => unknown }) => worklet?.__init?.(),
|
|
1267
|
+
createWorkletRuntime: (name: string) => ({ name }),
|
|
1268
|
+
scheduleOnRuntime: () => {},
|
|
1269
|
+
scheduleOnRuntimeWithId: () => {},
|
|
1270
|
+
runOnRuntimeSync: () => undefined,
|
|
1271
|
+
runOnRuntimeSyncWithId: () => undefined,
|
|
1272
|
+
createSynchronizable: (value: unknown) => ({ value, __init: () => value }),
|
|
1273
|
+
synchronizableGetDirty: (ref: { value?: unknown }) => ref?.value,
|
|
1274
|
+
synchronizableGetBlocking: (ref: { value?: unknown }) => ref?.value,
|
|
1275
|
+
synchronizableSetBlocking: (ref: { value?: unknown }, next: unknown) => {
|
|
1276
|
+
if (ref) ref.value = next
|
|
1277
|
+
},
|
|
1278
|
+
synchronizableLock: () => {},
|
|
1279
|
+
synchronizableUnlock: () => {},
|
|
1280
|
+
reportFatalErrorOnJS: (msg: string, stack: string) => {
|
|
1281
|
+
console.error('[reanimated worklet]', msg, stack)
|
|
1282
|
+
},
|
|
1283
|
+
getStaticFeatureFlag: () => false,
|
|
1284
|
+
setDynamicFeatureFlag: () => {},
|
|
1285
|
+
getUIRuntimeHolder: () => RNGPUI_UI_RUNTIME_HOLDER,
|
|
1286
|
+
getUISchedulerHolder: () => RNGPUI_UI_SCHEDULER_HOLDER,
|
|
1287
|
+
}
|
|
1288
|
+
Object.defineProperty(g, '__workletsModuleProxy', {
|
|
1289
|
+
value: proxy,
|
|
1290
|
+
writable: true,
|
|
1291
|
+
enumerable: false,
|
|
1292
|
+
configurable: true,
|
|
1293
|
+
})
|
|
1294
|
+
// reanimated v4 branches heavily on __RUNTIME_KIND. claim UI (2) — see the seam.
|
|
1295
|
+
;(g as { __RUNTIME_KIND?: number }).__RUNTIME_KIND = RuntimeKind.UI
|
|
1296
|
+
})()
|
|
1297
|
+
|
|
1298
|
+
// lazy object-shareable wirer for the worklet-runtime serializer.
|
|
1299
|
+
//
|
|
1300
|
+
// createShareable's object branch wires a SharedValue EAGERLY (allocates an object
|
|
1301
|
+
// slot, attaches `_id` + the OBJECT_SV_BRAND, installs the peer-sync listeners). when
|
|
1302
|
+
// that eager wiring can't run — the slot pool is exhausted under churn or the runtime
|
|
1303
|
+
// isn't wired yet — it falls back to the local-only `decorated` mutable, which has NO
|
|
1304
|
+
// `_id`. if such a mutable is later captured in a worklet closure and shipped to the
|
|
1305
|
+
// UI runtime, the serializer would deep-clone it into a plain object whose peer-side
|
|
1306
|
+
// `.get()` is missing → tamagui's animatedStyle worklet throws `configRef.get is not
|
|
1307
|
+
// a function`.
|
|
1308
|
+
//
|
|
1309
|
+
// upstream reanimated never has this gap: a SharedValue is ALWAYS shippable because
|
|
1310
|
+
// its shareable clone is made LAZILY at ship time (makeShareableCloneRecursive).
|
|
1311
|
+
// restore that invariant by exposing the SAME wireObjectSharedValue used by
|
|
1312
|
+
// createShareable so the serializer can wire an unwired mutable on first ship.
|
|
1313
|
+
;(() => {
|
|
1314
|
+
const g = globalThis as Record<string, unknown> & {
|
|
1315
|
+
__rngpui_wire_object_shareable?: (value: { value: unknown }) => number | null
|
|
1316
|
+
}
|
|
1317
|
+
if (typeof g.__rngpui_wire_object_shareable === 'function') return
|
|
1318
|
+
g.__rngpui_wire_object_shareable = (value: { value: unknown }): number | null => {
|
|
1319
|
+
if (hasSharedValueId(value)) return value._id
|
|
1320
|
+
const rt = getSabRuntime()
|
|
1321
|
+
if (!rt) return null
|
|
1322
|
+
try {
|
|
1323
|
+
const host = wireObjectSharedValue(rt, value, value.value)
|
|
1324
|
+
return hasSharedValueId(host) ? host._id : null
|
|
1325
|
+
} catch (err) {
|
|
1326
|
+
warnShareableFallback('lazy object shareable wiring failed', err)
|
|
1327
|
+
return null
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
})()
|
|
1331
|
+
|
|
1332
|
+
// reference the imported runtime instance type so the import is not elided; it
|
|
1333
|
+
// documents the shape `runtime()` duck-types against.
|
|
1334
|
+
export type { WorkletRuntimeInstance }
|