@unrulysystems/native-motion 0.1.0-alpha.0
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/CHANGELOG.md +16 -0
- package/LICENSE +21 -0
- package/README.md +55 -0
- package/android/build.gradle +24 -0
- package/android/src/main/AndroidManifest.xml +1 -0
- package/android/src/main/java/com/unrulysystems/nativemotion/GestureExclusionModule.kt +55 -0
- package/android/src/main/java/com/unrulysystems/nativemotion/NativeMotionPackage.kt +27 -0
- package/package.json +57 -0
- package/react-native.config.js +14 -0
- package/src/native/driver/instantWindow.ts +31 -0
- package/src/native/driver/strictModeReplay.ts +20 -0
- package/src/native/driver/uiLayoutEngine.ts +845 -0
- package/src/native/driver/uiLayoutGraph.ts +338 -0
- package/src/native/driver/uiValueChannel.ts +4640 -0
- package/src/native/driver/workletDriver.ts +3860 -0
- package/src/native/motion/AnimatePresence.tsx +1605 -0
- package/src/native/motion/LayoutGroup.tsx +163 -0
- package/src/native/motion/MotionConfig.tsx +400 -0
- package/src/native/motion/MotionRoot.tsx +250 -0
- package/src/native/motion/MotionView.tsx +6761 -0
- package/src/native/motion/addScaleCorrector.ts +97 -0
- package/src/native/motion/colorEndpointUiFeed.ts +32 -0
- package/src/native/motion/colorProjection.ts +39 -0
- package/src/native/motion/composeTransform.ts +271 -0
- package/src/native/motion/constraintMeasure.ts +157 -0
- package/src/native/motion/deferredPendingSnapshots.ts +25 -0
- package/src/native/motion/discreteProjection.ts +36 -0
- package/src/native/motion/dragAncestorPanContext.ts +28 -0
- package/src/native/motion/dragControls.ts +152 -0
- package/src/native/motion/dragGestureWiring.ts +1168 -0
- package/src/native/motion/dragHandoffBinding.ts +179 -0
- package/src/native/motion/dragHubStream.ts +756 -0
- package/src/native/motion/dragPropagationLock.ts +57 -0
- package/src/native/motion/driverValueChannel.ts +3288 -0
- package/src/native/motion/externalDragDriver.ts +214 -0
- package/src/native/motion/frameData.ts +31 -0
- package/src/native/motion/gestureBinding.ts +107 -0
- package/src/native/motion/gestureStateGate.ts +563 -0
- package/src/native/motion/gestureStateResolver.ts +286 -0
- package/src/native/motion/identityValueChannelControllerAdapter.ts +2043 -0
- package/src/native/motion/identityValueChannelControllerReconciliation.ts +176 -0
- package/src/native/motion/identityValueChannelLaneMarker.ts +14 -0
- package/src/native/motion/imperativeAnimate.ts +816 -0
- package/src/native/motion/keyframeTiming.ts +7 -0
- package/src/native/motion/layoutIdBinding.ts +261 -0
- package/src/native/motion/layoutIdFlightConfig.ts +51 -0
- package/src/native/motion/layoutProjection.ts +89 -0
- package/src/native/motion/layoutScroll.ts +204 -0
- package/src/native/motion/layoutTransition.ts +1302 -0
- package/src/native/motion/lengthLayoutContext.tsx +100 -0
- package/src/native/motion/lengthLayoutHost.ts +44 -0
- package/src/native/motion/mappedKeys.ts +184 -0
- package/src/native/motion/motionViewController.ts +3180 -0
- package/src/native/motion/nativeHostMarker.ts +22 -0
- package/src/native/motion/panSessionWiring.ts +127 -0
- package/src/native/motion/pathTransition.ts +101 -0
- package/src/native/motion/popLayout.ts +176 -0
- package/src/native/motion/presenceBinding.ts +382 -0
- package/src/native/motion/scaleCorrectorRegistry.ts +123 -0
- package/src/native/motion/scrollValues.ts +260 -0
- package/src/native/motion/serializablePayload.ts +151 -0
- package/src/native/motion/severity.ts +16 -0
- package/src/native/motion/shippedSurface.ts +843 -0
- package/src/native/motion/staticLengthGate.ts +86 -0
- package/src/native/motion/styleBaseGate.ts +72 -0
- package/src/native/motion/styleValueBinding.ts +657 -0
- package/src/native/motion/systemGestureExclusion.ts +61 -0
- package/src/native/motion/tapGestureWiring.ts +215 -0
- package/src/native/motion/transformOrder.ts +38 -0
- package/src/native/motion/transformStringBinding.ts +200 -0
- package/src/native/motion/transformTemplateGate.ts +45 -0
- package/src/native/motion/transitionGate.ts +435 -0
- package/src/native/motion/useAnimate.ts +52 -0
- package/src/native/motion/useCycle.ts +44 -0
- package/src/native/motion/useInstantTransition.ts +68 -0
- package/src/native/motion/useReducedMotion.ts +58 -0
- package/src/native/motion/useScroll.ts +162 -0
- package/src/native/motion/useViewportScroll.ts +29 -0
- package/src/native/motion/valueChannel.ts +7121 -0
- package/src/native/motion/valueHooks.ts +901 -0
- package/src/native/motion/variantChildRegistry.ts +67 -0
- package/src/native/motion/variantContext.tsx +172 -0
- package/src/native/motion/variantProps.ts +598 -0
- package/src/native.ts +159 -0
- package/src/verification/harnessMetrics.ts +78 -0
- package/src/verification/probe/LayoutIdentityWorkletProbe.tsx +251 -0
- package/src/verification/probe/WorkletParityProbe.tsx +162 -0
- package/src/verification/probe/layoutIdentityProbeEngine.ts +429 -0
- package/src/verification/screens/ArcPathChecksScreen.tsx +273 -0
- package/src/verification/screens/BooleanAnimateChecksScreen.tsx +358 -0
- package/src/verification/screens/ChoreographyGalleryScreen.tsx +1010 -0
- package/src/verification/screens/ColorBindingChecksScreen.tsx +568 -0
- package/src/verification/screens/CompletionChecksScreen.tsx +294 -0
- package/src/verification/screens/ConformanceScreen.tsx +573 -0
- package/src/verification/screens/ContentionProbeScreen.tsx +224 -0
- package/src/verification/screens/DriverSmokeScreen.tsx +99 -0
- package/src/verification/screens/DurationOnlyTweenChecksScreen.tsx +294 -0
- package/src/verification/screens/DynamicDragConfigChecksScreen.tsx +237 -0
- package/src/verification/screens/FrameDataChecksScreen.tsx +252 -0
- package/src/verification/screens/GestureChecksScreen.tsx +685 -0
- package/src/verification/screens/InstantTransitionChecksScreen.tsx +839 -0
- package/src/verification/screens/LayoutAnimationStartChecksScreen.tsx +648 -0
- package/src/verification/screens/LayoutChecksScreen.tsx +822 -0
- package/src/verification/screens/LayoutCommitSpikeScreen.tsx +142 -0
- package/src/verification/screens/MotionViewChecksScreen.tsx +930 -0
- package/src/verification/screens/PresenceChecksScreen.tsx +614 -0
- package/src/verification/screens/ReducedMotionChecksScreen.tsx +614 -0
- package/src/verification/screens/RestThresholdChecksScreen.tsx +634 -0
- package/src/verification/screens/ScaleCorrectorChecksScreen.tsx +425 -0
- package/src/verification/screens/SharedLayoutContinuityChecksScreen.tsx +338 -0
- package/src/verification/screens/SharedLayoutCrossfadeChecksScreen.tsx +425 -0
- package/src/verification/screens/TransitionDefaultSelectionChecksScreen.tsx +514 -0
- package/src/verification/screens/ViewportScrollAliasChecksScreen.tsx +303 -0
- package/src/verification/screens/conformanceBanner.ts +21 -0
- package/src/verification/screens/proofConsoleTap.ts +18 -0
- package/src/verification.ts +47 -0
- package/src/web.ts +98 -0
|
@@ -0,0 +1,2043 @@
|
|
|
1
|
+
import {
|
|
2
|
+
delayGenerator,
|
|
3
|
+
keyframesGenerator,
|
|
4
|
+
ManualClock,
|
|
5
|
+
resolveSpringGenerator,
|
|
6
|
+
timingGenerator,
|
|
7
|
+
toDelayMs,
|
|
8
|
+
toKeyframesConfig,
|
|
9
|
+
toSpringConfig,
|
|
10
|
+
type Animation,
|
|
11
|
+
type DriverCommand,
|
|
12
|
+
type ElementHandle,
|
|
13
|
+
type Generator,
|
|
14
|
+
type GeneratorFactory,
|
|
15
|
+
type MotionValue,
|
|
16
|
+
type Transition,
|
|
17
|
+
} from '@unrulysystems/native-motion-core'
|
|
18
|
+
import {
|
|
19
|
+
createExternalAnimationLedger,
|
|
20
|
+
createMotionValueDriverPort,
|
|
21
|
+
initialExternalAnimationLedgerState,
|
|
22
|
+
instantFinalKeyframe,
|
|
23
|
+
isDirectPublicNumericMotionValue,
|
|
24
|
+
isDurationZeroInstantTransition,
|
|
25
|
+
isTimingLaneTransition,
|
|
26
|
+
toTimingLaneConfig,
|
|
27
|
+
type TransportedSampleProgressEntry,
|
|
28
|
+
type TransportedSampleProgressState,
|
|
29
|
+
type TransportedSampleProgressWriter,
|
|
30
|
+
type TransportedSampleIrreversibleSubphase,
|
|
31
|
+
} from '@unrulysystems/native-motion-core/internal-driver'
|
|
32
|
+
|
|
33
|
+
import {
|
|
34
|
+
identityValueChannelDriverIdForBinding,
|
|
35
|
+
type WorkletDriverBinding,
|
|
36
|
+
} from '../driver/workletDriver'
|
|
37
|
+
import {
|
|
38
|
+
createDriverValueChannelIdentityRegistry,
|
|
39
|
+
type DriverValueChannelIdentityRegistry,
|
|
40
|
+
type DriverValueChannelIdentityRegistryRecord,
|
|
41
|
+
type DriverValueChannelSinkRegistration,
|
|
42
|
+
type DriverValueChannelState,
|
|
43
|
+
} from './driverValueChannel'
|
|
44
|
+
import {
|
|
45
|
+
advanceGlobalTargetBatch,
|
|
46
|
+
channelMailboxTransferStatusHandler,
|
|
47
|
+
channelMailboxTransferStatusValidator,
|
|
48
|
+
completeIdentityGlobalValueChannelExecution,
|
|
49
|
+
constructGlobalTargetBatchFromPostListenerMotionValueSlot,
|
|
50
|
+
createIdentityGlobalValueChannelHostOperationController,
|
|
51
|
+
drainChannelMailboxTurnEnvelope,
|
|
52
|
+
IdentityGlobalValueChannelInvariantError,
|
|
53
|
+
produceIdentityGlobalValueChannelSealedEmptyBatch,
|
|
54
|
+
sealChannelMailboxTurnEnvelope,
|
|
55
|
+
type ChannelMailboxDrainRuntime,
|
|
56
|
+
type ChannelMailboxDrainState,
|
|
57
|
+
type ChannelMailboxEntry,
|
|
58
|
+
type ChannelMailboxTurnEnvelope,
|
|
59
|
+
type GlobalTargetBatchRegistryState,
|
|
60
|
+
type IdentityGlobalValueChannelExternalAnimationRuntime,
|
|
61
|
+
type IdentityGlobalValueChannelHostOperationController,
|
|
62
|
+
type IdentityGlobalValueChannelHostOperationObservation,
|
|
63
|
+
type IdentityGlobalValueChannelIdentityState,
|
|
64
|
+
type IdentityGlobalValueChannelPublicControlState,
|
|
65
|
+
type IdentityGlobalValueChannelRuntimeState,
|
|
66
|
+
type TargetBatchDependencyEventKind,
|
|
67
|
+
type ValueChannel,
|
|
68
|
+
} from './valueChannel'
|
|
69
|
+
import type { IdentityValueChannelControllerElementReconciliationPlan } from './identityValueChannelControllerReconciliation'
|
|
70
|
+
import { IDENTITY_DECLARATIVE_LANE_COMMAND } from './identityValueChannelLaneMarker'
|
|
71
|
+
import {
|
|
72
|
+
identityChannelEligibleBoundStyleSnapshot,
|
|
73
|
+
type MotionViewAuthoredStyleMountPreparation,
|
|
74
|
+
} from './styleValueBinding'
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Mount-known public-value bindings for one MotionView controller. The adapter attaches these
|
|
78
|
+
* exact objects when the controller registers its element, before that controller can issue a
|
|
79
|
+
* target command.
|
|
80
|
+
*/
|
|
81
|
+
export interface IdentityValueChannelControllerElementBinding {
|
|
82
|
+
readonly ownerDriverId: number
|
|
83
|
+
readonly ownerGeneration: number
|
|
84
|
+
readonly stylePreparation: MotionViewAuthoredStyleMountPreparation
|
|
85
|
+
readonly delegate: WorkletDriverBinding
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
declare const identityValueChannelElementAdapterBrand: unique symbol
|
|
89
|
+
|
|
90
|
+
/** Opaque wrapper created only by the shared runtime; callers cannot forge a plain binding. */
|
|
91
|
+
export interface IdentityValueChannelControllerElementAdapter extends IdentityValueChannelControllerElementBinding {
|
|
92
|
+
readonly [identityValueChannelElementAdapterBrand]: true
|
|
93
|
+
readonly binding: WorkletDriverBinding
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export interface IdentityValueChannelMountedElementObservation {
|
|
97
|
+
readonly adapter: IdentityValueChannelControllerElementAdapter
|
|
98
|
+
readonly handle: ElementHandle
|
|
99
|
+
readonly sinkRegistrations: readonly DriverValueChannelSinkRegistration[]
|
|
100
|
+
readonly delegateCommittedValues: Readonly<Record<string, number>>
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export interface IdentityValueChannelIdentityObservation {
|
|
104
|
+
readonly record: DriverValueChannelIdentityRegistryRecord<number>
|
|
105
|
+
readonly host: IdentityGlobalValueChannelHostOperationObservation
|
|
106
|
+
readonly publicControlSlot: IdentityGlobalValueChannelPublicControlState | null
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export type IdentityValueChannelControllerOrderedEdge =
|
|
110
|
+
| {
|
|
111
|
+
readonly kind: 'LISTENER_STACK_COMPLETED'
|
|
112
|
+
readonly sequence: number
|
|
113
|
+
readonly batchId: number
|
|
114
|
+
}
|
|
115
|
+
| {
|
|
116
|
+
readonly kind: 'TARGET_BATCH_DEPENDENCY_APPENDED'
|
|
117
|
+
readonly sequence: number
|
|
118
|
+
readonly batchId: number
|
|
119
|
+
readonly dependencyId: number
|
|
120
|
+
}
|
|
121
|
+
| {
|
|
122
|
+
readonly kind: 'ASYNCHRONOUS_COMPLETION_DRAINED'
|
|
123
|
+
readonly sequence: number
|
|
124
|
+
readonly batchId: number
|
|
125
|
+
readonly dependencyId: number
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export interface IdentityValueChannelCarrierDelivery {
|
|
129
|
+
readonly batchId: number
|
|
130
|
+
readonly carrierId: number
|
|
131
|
+
readonly authoredIndex: number
|
|
132
|
+
readonly identityId: number
|
|
133
|
+
readonly key: string
|
|
134
|
+
readonly taskSequence: number
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export interface IdentityValueChannelElementDelivery {
|
|
138
|
+
readonly batchId: number
|
|
139
|
+
readonly ownerDriverId: number
|
|
140
|
+
readonly ownerGeneration: number
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Private production observability. Every record is the adapter's live canonical record or an
|
|
145
|
+
* immutable envelope/delivery emitted by the real command path; this surface never accepts a
|
|
146
|
+
* caller-authored capture, slot, dependency event, or scenario result.
|
|
147
|
+
*/
|
|
148
|
+
export interface IdentityValueChannelControllerRuntimeObservation {
|
|
149
|
+
readonly identities: readonly IdentityValueChannelIdentityObservation[]
|
|
150
|
+
readonly mountedElements: readonly IdentityValueChannelMountedElementObservation[]
|
|
151
|
+
readonly runtimeState: IdentityGlobalValueChannelRuntimeState
|
|
152
|
+
readonly targetBatchState: GlobalTargetBatchRegistryState
|
|
153
|
+
readonly orderedEdges: readonly IdentityValueChannelControllerOrderedEdge[]
|
|
154
|
+
readonly closedMailboxTurns: readonly ChannelMailboxTurnEnvelope[]
|
|
155
|
+
readonly carrierDeliveries: readonly IdentityValueChannelCarrierDelivery[]
|
|
156
|
+
readonly elementDeliveries: readonly IdentityValueChannelElementDelivery[]
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Shared JS-global target/value-channel authority used by every concrete MotionView controller
|
|
161
|
+
* binding in one channel. Controller commands and public MotionValue callbacks are the only
|
|
162
|
+
* mutation entrances; test code can drive frames and replay a real emitted envelope, but cannot
|
|
163
|
+
* manufacture the controller's internal batch/capture protocol.
|
|
164
|
+
*/
|
|
165
|
+
export interface IdentityValueChannelControllerRuntime {
|
|
166
|
+
bindingForElement(
|
|
167
|
+
options: IdentityValueChannelControllerElementBinding,
|
|
168
|
+
): IdentityValueChannelControllerElementAdapter
|
|
169
|
+
advanceFrame(elapsedMs: number): void
|
|
170
|
+
drainNextMailboxTurn(): void
|
|
171
|
+
replayMailboxTurn(envelope: ChannelMailboxTurnEnvelope): void
|
|
172
|
+
runReadyAggregateReactions(): void
|
|
173
|
+
observe(): IdentityValueChannelControllerRuntimeObservation
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export interface CreateIdentityValueChannelControllerRuntimeOptions {
|
|
177
|
+
readonly channel: ValueChannel
|
|
178
|
+
readonly identityRegistry: DriverValueChannelIdentityRegistry<number>
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export interface AttachIdentityValueChannelControllerElementOptions {
|
|
182
|
+
readonly channel: ValueChannel
|
|
183
|
+
readonly stylePreparation: MotionViewAuthoredStyleMountPreparation
|
|
184
|
+
readonly resolvedStyleValues: Readonly<Record<string, number | string>>
|
|
185
|
+
readonly delegate: WorkletDriverBinding
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export interface ReconcileIdentityValueChannelControllerElementOptions {
|
|
189
|
+
readonly channel: ValueChannel
|
|
190
|
+
readonly handle: ElementHandle
|
|
191
|
+
readonly delegate: WorkletDriverBinding
|
|
192
|
+
readonly controllerBinding: WorkletDriverBinding
|
|
193
|
+
readonly adapter: IdentityValueChannelControllerElementAdapter | null
|
|
194
|
+
readonly plan: IdentityValueChannelControllerElementReconciliationPlan
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export type IdentityValueChannelControllerAdapterOperation =
|
|
198
|
+
| 'CREATE_IDENTITY_VALUE_CHANNEL_CONTROLLER_RUNTIME'
|
|
199
|
+
| 'RECONCILE_IDENTITY_VALUE_CHANNEL_CONTROLLER_ELEMENT'
|
|
200
|
+
|
|
201
|
+
export const IDENTITY_VALUE_CHANNEL_CONTROLLER_ADAPTER_UNIMPLEMENTED =
|
|
202
|
+
'NATIVE_MOTION_IDENTITY_VALUE_CHANNEL_CONTROLLER_ADAPTER_UNIMPLEMENTED' as const
|
|
203
|
+
|
|
204
|
+
export const IDENTITY_VALUE_CHANNEL_CONTROLLER_ADAPTER_INVARIANT =
|
|
205
|
+
'NATIVE_MOTION_IDENTITY_VALUE_CHANNEL_CONTROLLER_ADAPTER_INVARIANT' as const
|
|
206
|
+
|
|
207
|
+
export type IdentityValueChannelControllerAdapterInvariant = 'UNREGISTERED_WORKLET_DRIVER_BINDING'
|
|
208
|
+
|
|
209
|
+
export class IdentityValueChannelControllerAdapterInvariantError extends Error {
|
|
210
|
+
readonly code = IDENTITY_VALUE_CHANNEL_CONTROLLER_ADAPTER_INVARIANT
|
|
211
|
+
readonly invariant: IdentityValueChannelControllerAdapterInvariant
|
|
212
|
+
|
|
213
|
+
constructor(invariant: IdentityValueChannelControllerAdapterInvariant) {
|
|
214
|
+
super(
|
|
215
|
+
`native-motion: identity value-channel controller adapter invariant '${invariant}' failed`,
|
|
216
|
+
)
|
|
217
|
+
this.name = 'IdentityValueChannelControllerAdapterInvariantError'
|
|
218
|
+
this.invariant = invariant
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/** Typed fail-closed boundary for behavior deliberately absent from the RED scaffold. */
|
|
223
|
+
export class IdentityValueChannelControllerAdapterUnimplementedError extends Error {
|
|
224
|
+
readonly code = IDENTITY_VALUE_CHANNEL_CONTROLLER_ADAPTER_UNIMPLEMENTED
|
|
225
|
+
readonly operation: IdentityValueChannelControllerAdapterOperation
|
|
226
|
+
|
|
227
|
+
constructor(operation: IdentityValueChannelControllerAdapterOperation) {
|
|
228
|
+
super(
|
|
229
|
+
`native-motion: identity value-channel controller adapter operation '${operation}' is ` +
|
|
230
|
+
'unimplemented (REQ-VALUE-015, REQ-DRIVER-034, REQ-API-061)',
|
|
231
|
+
)
|
|
232
|
+
this.name = 'IdentityValueChannelControllerAdapterUnimplementedError'
|
|
233
|
+
this.operation = operation
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// ——— Runtime implementation (REQ-VALUE-015, REQ-DRIVER-034, REQ-API-061) ———
|
|
238
|
+
// The shared JS-global authority: every concrete MotionView controller binding on one channel
|
|
239
|
+
// resolves exact direct-public numeric identities into ONE canonical registry record, host
|
|
240
|
+
// operation controller, and target-batch protocol per value. Controller commands and public
|
|
241
|
+
// MotionValue callbacks are the only mutation entrances; frames, drains, replays, and reactions
|
|
242
|
+
// are driven through the runtime surface below.
|
|
243
|
+
|
|
244
|
+
interface IdentityValueChannelExecutionRecord {
|
|
245
|
+
readonly executionId: number
|
|
246
|
+
readonly controlId: number
|
|
247
|
+
readonly valueCompletionSequence: number
|
|
248
|
+
readonly lifecycleCallbackId: number
|
|
249
|
+
readonly directOperationEpoch: number
|
|
250
|
+
readonly executionLease: number
|
|
251
|
+
readonly owner: IdentityValueChannelElementAuthority
|
|
252
|
+
generator: Generator
|
|
253
|
+
handle: Animation
|
|
254
|
+
elapsedMs: number
|
|
255
|
+
done: boolean
|
|
256
|
+
stopped: boolean
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
interface IdentityValueChannelIdentityAuthority {
|
|
260
|
+
readonly identityId: number
|
|
261
|
+
readonly channelId: number
|
|
262
|
+
readonly record: DriverValueChannelIdentityRegistryRecord<number>
|
|
263
|
+
readonly controller: IdentityGlobalValueChannelHostOperationController
|
|
264
|
+
readonly slotByExecutionId: Map<number, IdentityGlobalValueChannelPublicControlState>
|
|
265
|
+
readonly executions: IdentityValueChannelExecutionRecord[]
|
|
266
|
+
readonly unsubscribeChangeLane: () => void
|
|
267
|
+
attachedSinkCount: number
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
interface IdentityValueChannelElementAuthority {
|
|
271
|
+
readonly adapter: IdentityValueChannelControllerElementAdapter
|
|
272
|
+
readonly delegate: WorkletDriverBinding
|
|
273
|
+
readonly ownerDriverId: number
|
|
274
|
+
readonly ownerGeneration: number
|
|
275
|
+
readonly eligibleKeys: readonly string[]
|
|
276
|
+
readonly resolvedStyleValues: Readonly<Record<string, number>>
|
|
277
|
+
readonly identityBindings: Map<string, IdentityValueChannelIdentityAuthority>
|
|
278
|
+
/**
|
|
279
|
+
* Identity keys a forwarded start/retarget made command-owned on the delegate. The driver's
|
|
280
|
+
* dual-authority law refuses a second write lane for them for the rest of the flight's life,
|
|
281
|
+
* so every adapter-originated delegate write skips them once the delegate proves it enforces
|
|
282
|
+
* that law (rigs without the guard keep receiving the writes their traces pin).
|
|
283
|
+
*/
|
|
284
|
+
readonly commandOwnedKeys: Set<string>
|
|
285
|
+
/** Set on the delegate's first observed dual-authority write refusal; owned writes then skip. */
|
|
286
|
+
delegateRefusesOwnedWrites: boolean
|
|
287
|
+
handle: ElementHandle | null
|
|
288
|
+
frameValuesListener: ((values: Readonly<Record<string, number>>) => void) | null
|
|
289
|
+
/**
|
|
290
|
+
* False until the mount commit's synchronous block yields. Mount-phase entrance commands keep
|
|
291
|
+
* the timed trajectory class; only a settled element's host-resolved retargets commit their
|
|
292
|
+
* carrier synchronously (the carrier keyed to the host's own returned controls).
|
|
293
|
+
*/
|
|
294
|
+
mountSettled: boolean
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* The reconcile entry's backchannel into a runtime's element/identity authorities. Only
|
|
299
|
+
* `createIdentityValueChannelControllerRuntime` registers here, so a caller cannot manufacture
|
|
300
|
+
* element authority through the public runtime surface.
|
|
301
|
+
*/
|
|
302
|
+
interface IdentityValueChannelControllerRuntimeInternals {
|
|
303
|
+
findElementByAdapter(
|
|
304
|
+
adapter: IdentityValueChannelControllerElementAdapter,
|
|
305
|
+
): IdentityValueChannelElementAuthority | null
|
|
306
|
+
ensureElement(options: {
|
|
307
|
+
readonly adapter: IdentityValueChannelControllerElementAdapter
|
|
308
|
+
readonly delegate: WorkletDriverBinding
|
|
309
|
+
readonly ownerDriverId: number
|
|
310
|
+
readonly ownerGeneration: number
|
|
311
|
+
readonly handle: ElementHandle
|
|
312
|
+
}): IdentityValueChannelElementAuthority
|
|
313
|
+
detachIdentityValue(
|
|
314
|
+
element: IdentityValueChannelElementAuthority,
|
|
315
|
+
key: string,
|
|
316
|
+
value: MotionValue<number>,
|
|
317
|
+
spliceRegistry: boolean,
|
|
318
|
+
): DriverValueChannelSinkRegistration | null
|
|
319
|
+
attachIdentityValue(
|
|
320
|
+
element: IdentityValueChannelElementAuthority,
|
|
321
|
+
key: string,
|
|
322
|
+
value: MotionValue<number>,
|
|
323
|
+
currentValue: number,
|
|
324
|
+
previous: {
|
|
325
|
+
readonly previousValue: MotionValue<number>
|
|
326
|
+
readonly previousSink: DriverValueChannelSinkRegistration
|
|
327
|
+
} | null,
|
|
328
|
+
): void
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
const controllerRuntimeInternals = new WeakMap<
|
|
332
|
+
IdentityValueChannelControllerRuntime,
|
|
333
|
+
IdentityValueChannelControllerRuntimeInternals
|
|
334
|
+
>()
|
|
335
|
+
|
|
336
|
+
/** Every element authority by its branded adapter, for the mount-commit notification seam. */
|
|
337
|
+
const elementByAdapter = new WeakMap<
|
|
338
|
+
IdentityValueChannelControllerElementAdapter,
|
|
339
|
+
IdentityValueChannelElementAuthority
|
|
340
|
+
>()
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* MotionView's first insertion effect of every commit reports it here: render-phase entrance
|
|
344
|
+
* commands (the mount block) keep their timed trajectory class, while everything after the
|
|
345
|
+
* boundary follows the settled carrier law. Idempotent per element.
|
|
346
|
+
*/
|
|
347
|
+
export function notifyIdentityValueChannelControllerMountCommit(
|
|
348
|
+
adapter: IdentityValueChannelControllerElementAdapter,
|
|
349
|
+
): void {
|
|
350
|
+
const element = elementByAdapter.get(adapter)
|
|
351
|
+
if (element !== undefined) element.mountSettled = true
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/** Whether a forwarded command owns the element's property for the rest of its flight's life. */
|
|
355
|
+
function delegateOwnsKey(element: IdentityValueChannelElementAuthority, key: string): boolean {
|
|
356
|
+
return element.commandOwnedKeys.has(key)
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
const DUAL_AUTHORITY_WRITE_REFUSAL = 'cannot write bound value'
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* The one adapter-originated delegate write lane. Command-owned keys ride it unless the
|
|
363
|
+
* delegate proves it enforces the dual-authority refusal — then they stay with the flight.
|
|
364
|
+
*/
|
|
365
|
+
function writeDelegateValues(
|
|
366
|
+
element: IdentityValueChannelElementAuthority,
|
|
367
|
+
values: Readonly<Record<string, number>>,
|
|
368
|
+
): void {
|
|
369
|
+
if (element.handle === null || element.delegate.isDisposed()) return
|
|
370
|
+
const writable: Record<string, number> = {}
|
|
371
|
+
const owned: Record<string, number> = {}
|
|
372
|
+
for (const [key, value] of Object.entries(values)) {
|
|
373
|
+
if (delegateOwnsKey(element, key)) owned[key] = value
|
|
374
|
+
else writable[key] = value
|
|
375
|
+
}
|
|
376
|
+
if (Object.keys(writable).length > 0) {
|
|
377
|
+
element.delegate.driver.write(element.handle, writable, undefined, 'bound-value-retarget')
|
|
378
|
+
}
|
|
379
|
+
if (element.delegateRefusesOwnedWrites || Object.keys(owned).length === 0) return
|
|
380
|
+
try {
|
|
381
|
+
element.delegate.driver.write(element.handle, owned, undefined, 'bound-value-retarget')
|
|
382
|
+
} catch (error) {
|
|
383
|
+
if (error instanceof Error && error.message.startsWith(DUAL_AUTHORITY_WRITE_REFUSAL)) {
|
|
384
|
+
element.delegateRefusesOwnedWrites = true
|
|
385
|
+
return
|
|
386
|
+
}
|
|
387
|
+
throw error
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* The empty mount snapshot for a reconcile-time attach whose mount had no eligible identity. Only
|
|
393
|
+
* the reconcile entrance constructs it; eligibility itself always comes from the plan's edges.
|
|
394
|
+
*/
|
|
395
|
+
const EMPTY_MOUNT_STYLE_PREPARATION = {
|
|
396
|
+
boundStyleValues: {},
|
|
397
|
+
resolvedBoundStyleValues: {},
|
|
398
|
+
staticStyleValues: {},
|
|
399
|
+
} as unknown as MotionViewAuthoredStyleMountPreparation
|
|
400
|
+
|
|
401
|
+
/** The value's @internal lifecycle emitter (motion-value.ts), the same seam the graph uses. */
|
|
402
|
+
function emitIdentityValueEvent(
|
|
403
|
+
value: MotionValue<number>,
|
|
404
|
+
event: 'animationStart' | 'animationComplete',
|
|
405
|
+
): void {
|
|
406
|
+
const emitter = value as Partial<{
|
|
407
|
+
emitEvent: (emitted: 'animationStart' | 'animationComplete') => void
|
|
408
|
+
}>
|
|
409
|
+
emitter.emitEvent?.(event)
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
function isInstantIdentityTransition(transition: Transition): boolean {
|
|
413
|
+
return transition.type === false || isDurationZeroInstantTransition(transition)
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/** One trajectory factory for a scalar/keyframe target, mirroring the drivers' lane selection. */
|
|
417
|
+
function identityTrajectoryFactory(
|
|
418
|
+
target: number | readonly (number | null)[],
|
|
419
|
+
transition: Transition,
|
|
420
|
+
): GeneratorFactory {
|
|
421
|
+
const delayMs = toDelayMs(transition)
|
|
422
|
+
const base: GeneratorFactory =
|
|
423
|
+
typeof target !== 'number'
|
|
424
|
+
? keyframesGenerator(target, toKeyframesConfig(transition))
|
|
425
|
+
: transition.type === 'tween' || isTimingLaneTransition(transition)
|
|
426
|
+
? timingGenerator(target, toTimingLaneConfig(transition))
|
|
427
|
+
: resolveSpringGenerator(target, toSpringConfig(transition))
|
|
428
|
+
return (seed) => (delayMs > 0 ? delayGenerator(base(seed), delayMs) : base(seed))
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
class IdentityValueChannelDrainProgressWriter implements TransportedSampleProgressWriter {
|
|
432
|
+
#state: TransportedSampleProgressState = {
|
|
433
|
+
activeEntry: null,
|
|
434
|
+
lastIrreversibleSubphase: null,
|
|
435
|
+
deliveredWriteWatermark: 0,
|
|
436
|
+
deliveredEventWatermark: 0,
|
|
437
|
+
retentionFinalization: 'pending',
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
beginEntry(entry: TransportedSampleProgressEntry): void {
|
|
441
|
+
this.#state = { ...this.#state, activeEntry: entry }
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
markIrreversibleSubphase(subphase: TransportedSampleIrreversibleSubphase): void {
|
|
445
|
+
this.#state = { ...this.#state, lastIrreversibleSubphase: subphase }
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
finalizeDeliveredWriteWatermarkAndRetention(
|
|
449
|
+
_channelId: number,
|
|
450
|
+
deliveredWriteWatermark: number,
|
|
451
|
+
): void {
|
|
452
|
+
this.#state = {
|
|
453
|
+
...this.#state,
|
|
454
|
+
deliveredWriteWatermark,
|
|
455
|
+
retentionFinalization: 'finalized',
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
finalizeDeliveredEventWatermarkAndRetention(
|
|
460
|
+
_channelId: number,
|
|
461
|
+
deliveredEventWatermark: number,
|
|
462
|
+
): void {
|
|
463
|
+
this.#state = {
|
|
464
|
+
...this.#state,
|
|
465
|
+
deliveredEventWatermark,
|
|
466
|
+
retentionFinalization: 'finalized',
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
clearEntry(): void {
|
|
471
|
+
this.#state = { ...this.#state, activeEntry: null, lastIrreversibleSubphase: null }
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
observe(): TransportedSampleProgressState {
|
|
475
|
+
return this.#state
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
function freshIdentityValueChannelDrainState(): ChannelMailboxDrainState {
|
|
480
|
+
return {
|
|
481
|
+
acceptedDrainIds: [],
|
|
482
|
+
completedPositions: [],
|
|
483
|
+
appliedEntryIds: [],
|
|
484
|
+
deliveredWriteWatermarks: [],
|
|
485
|
+
successfulTailCount: 0,
|
|
486
|
+
abortCount: 0,
|
|
487
|
+
abortPrefixReadyResolutionCount: 0,
|
|
488
|
+
retentionFinalizationCount: 0,
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
function resolveIdentityTarget(
|
|
493
|
+
command: DriverCommand & { readonly targets: object },
|
|
494
|
+
key: string,
|
|
495
|
+
): {
|
|
496
|
+
readonly target: number | readonly (number | null)[]
|
|
497
|
+
readonly transition: Transition
|
|
498
|
+
} {
|
|
499
|
+
if (command.kind === 'start') {
|
|
500
|
+
const targets = command.targets as Readonly<
|
|
501
|
+
Record<
|
|
502
|
+
string,
|
|
503
|
+
{ readonly to: number | readonly (number | null)[]; readonly transition?: unknown }
|
|
504
|
+
>
|
|
505
|
+
>
|
|
506
|
+
const propTarget = targets[key]!
|
|
507
|
+
return {
|
|
508
|
+
target: propTarget.to,
|
|
509
|
+
transition: (propTarget.transition ?? command.transition) as Transition,
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
const retarget = command as Extract<DriverCommand, { readonly kind: 'retarget' }>
|
|
513
|
+
return {
|
|
514
|
+
target: (retarget.targets as Readonly<Record<string, number>>)[key]!,
|
|
515
|
+
transition: (retarget.targetTransitions?.[key] ?? retarget.transition) as Transition,
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
function partitionIdentityCommandKeys(
|
|
520
|
+
identityBindings: ReadonlyMap<string, unknown>,
|
|
521
|
+
targets: Readonly<Record<string, unknown>>,
|
|
522
|
+
): { readonly identityKeys: string[]; readonly restKeys: string[] } {
|
|
523
|
+
const identityKeys: string[] = []
|
|
524
|
+
const restKeys: string[] = []
|
|
525
|
+
for (const key of Object.keys(targets)) {
|
|
526
|
+
if (identityBindings.has(key)) identityKeys.push(key)
|
|
527
|
+
else restKeys.push(key)
|
|
528
|
+
}
|
|
529
|
+
return { identityKeys, restKeys }
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
const unexpectedIdentityMailboxEntry = (): void => undefined
|
|
533
|
+
|
|
534
|
+
/** Host frames never stage a transfer status; reaching one is a caller defect, fail closed. */
|
|
535
|
+
const unexpectedIdentityTransferStatus = (): never => {
|
|
536
|
+
throw new IdentityGlobalValueChannelInvariantError('identity-unknown')
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
/** The one JS-global identity runtime for an exact public ValueChannel graph. */
|
|
540
|
+
export function createIdentityValueChannelControllerRuntime(
|
|
541
|
+
options: CreateIdentityValueChannelControllerRuntimeOptions,
|
|
542
|
+
): IdentityValueChannelControllerRuntime {
|
|
543
|
+
const { channel, identityRegistry } = options
|
|
544
|
+
|
|
545
|
+
// Monotonic id allocators; every sequence the tests pin is > 0 and order-consistent.
|
|
546
|
+
let nextIdentityId = 1
|
|
547
|
+
let nextChannelId = 1
|
|
548
|
+
let nextJsRecordId = 1
|
|
549
|
+
let nextUiRecordId = 1
|
|
550
|
+
let nextSinkId = 1
|
|
551
|
+
let nextExecutionId = 1
|
|
552
|
+
let nextControlId = 1
|
|
553
|
+
let nextValueCompletionSequence = 1
|
|
554
|
+
let nextCaptureId = 1
|
|
555
|
+
let nextDependencyId = 1
|
|
556
|
+
let nextCarrierId = 1
|
|
557
|
+
let nextCallbackId = 1
|
|
558
|
+
let nextBatchCreationSequence = 1
|
|
559
|
+
let nextExecutionCreationSequence = 1
|
|
560
|
+
let nextCommandSequence = 1
|
|
561
|
+
let nextDirectOperationEpoch = 1
|
|
562
|
+
let nextExecutionLease = 1
|
|
563
|
+
let nextWriteSequence = 1
|
|
564
|
+
let nextDependencyEventSequence = 1
|
|
565
|
+
let nextEdgeSequence = 1
|
|
566
|
+
let frameTimeMs = 0
|
|
567
|
+
let changeLaneSuppressionDepth = 0
|
|
568
|
+
|
|
569
|
+
const identities: IdentityValueChannelIdentityAuthority[] = []
|
|
570
|
+
const elements: IdentityValueChannelElementAuthority[] = []
|
|
571
|
+
const mountedElements: IdentityValueChannelElementAuthority[] = []
|
|
572
|
+
const lifecycleCallbacks = new Map<number, () => void>()
|
|
573
|
+
const transitionCallbacks = new Map<number, (value: number) => void>()
|
|
574
|
+
const carrierValues = new Map<number, number>()
|
|
575
|
+
const orderedEdges: IdentityValueChannelControllerOrderedEdge[] = []
|
|
576
|
+
const closedMailboxTurns: ChannelMailboxTurnEnvelope[] = []
|
|
577
|
+
const carrierDeliveries: IdentityValueChannelCarrierDelivery[] = []
|
|
578
|
+
const recordedCarrierDeliveryIds = new Set<number>()
|
|
579
|
+
const elementDeliveries: IdentityValueChannelElementDelivery[] = []
|
|
580
|
+
let mailboxDrainCursor = 0
|
|
581
|
+
let targetBatchState: GlobalTargetBatchRegistryState = {
|
|
582
|
+
batches: [],
|
|
583
|
+
nextBatchId: 1,
|
|
584
|
+
nextAggregateReactionSequence: 1,
|
|
585
|
+
readyBatchIdsByMailboxDrain: [],
|
|
586
|
+
}
|
|
587
|
+
let drainState: ChannelMailboxDrainState = freshIdentityValueChannelDrainState()
|
|
588
|
+
let replayDrainState: ChannelMailboxDrainState = freshIdentityValueChannelDrainState()
|
|
589
|
+
|
|
590
|
+
const runtime: IdentityGlobalValueChannelExternalAnimationRuntime = {
|
|
591
|
+
channel,
|
|
592
|
+
state: {
|
|
593
|
+
identities: [],
|
|
594
|
+
frameBuffers: {
|
|
595
|
+
executionCapacity: 4,
|
|
596
|
+
taskCapacity: 8,
|
|
597
|
+
completionCapacity: 4,
|
|
598
|
+
envelopeEntryCapacity: 16,
|
|
599
|
+
liveGrowthCount: 0,
|
|
600
|
+
},
|
|
601
|
+
pendingJsFrame: null,
|
|
602
|
+
nextMailboxDrainId: 1,
|
|
603
|
+
disposedOwnerDriverIds: [],
|
|
604
|
+
},
|
|
605
|
+
callbacks: {
|
|
606
|
+
publishChange: () => undefined,
|
|
607
|
+
markDerivedDirty: () => undefined,
|
|
608
|
+
invokeTransitionCallback(callbackId, _identityId, value) {
|
|
609
|
+
transitionCallbacks.get(callbackId)?.(value)
|
|
610
|
+
},
|
|
611
|
+
invokeLifecycleCallback(callbackId, _identityId) {
|
|
612
|
+
lifecycleCallbacks.get(callbackId)?.()
|
|
613
|
+
},
|
|
614
|
+
commitHostUpdate(record) {
|
|
615
|
+
const element = mountedElements.find(
|
|
616
|
+
(candidate) =>
|
|
617
|
+
candidate.ownerDriverId === record.driverId &&
|
|
618
|
+
(candidate.handle as number | null) === record.handleId,
|
|
619
|
+
)
|
|
620
|
+
if (element === null || element === undefined || element.handle === null) return
|
|
621
|
+
if (element.delegate.isDisposed()) return
|
|
622
|
+
const values: Record<string, number> = {}
|
|
623
|
+
for (const value of record.values) values[value.key] = value.value
|
|
624
|
+
writeDelegateValues(element, values)
|
|
625
|
+
element.frameValuesListener?.(element.delegate.driver.committed(element.handle))
|
|
626
|
+
},
|
|
627
|
+
requestJsFrame: () => undefined,
|
|
628
|
+
cancelJsFrame: () => undefined,
|
|
629
|
+
},
|
|
630
|
+
externalAnimationLedger: createExternalAnimationLedger({
|
|
631
|
+
graph: channel.graph,
|
|
632
|
+
initialState: initialExternalAnimationLedgerState(),
|
|
633
|
+
}),
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
const runtimeIdentities = runtime.state.identities as IdentityGlobalValueChannelIdentityState[]
|
|
637
|
+
|
|
638
|
+
function currentIdentity(
|
|
639
|
+
authority: IdentityValueChannelIdentityAuthority,
|
|
640
|
+
): IdentityGlobalValueChannelIdentityState {
|
|
641
|
+
const identity = runtimeIdentities.find(
|
|
642
|
+
(candidate) => candidate.identityId === authority.identityId,
|
|
643
|
+
)
|
|
644
|
+
if (identity === undefined) {
|
|
645
|
+
throw new IdentityGlobalValueChannelInvariantError('identity-unknown')
|
|
646
|
+
}
|
|
647
|
+
return identity
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
function updateIdentity(
|
|
651
|
+
authority: IdentityValueChannelIdentityAuthority,
|
|
652
|
+
update: (
|
|
653
|
+
identity: IdentityGlobalValueChannelIdentityState,
|
|
654
|
+
) => IdentityGlobalValueChannelIdentityState,
|
|
655
|
+
): IdentityGlobalValueChannelIdentityState {
|
|
656
|
+
const index = runtimeIdentities.findIndex(
|
|
657
|
+
(candidate) => candidate.identityId === authority.identityId,
|
|
658
|
+
)
|
|
659
|
+
if (index < 0) throw new IdentityGlobalValueChannelInvariantError('identity-unknown')
|
|
660
|
+
const updated = update(runtimeIdentities[index]!)
|
|
661
|
+
runtimeIdentities[index] = updated
|
|
662
|
+
return updated
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
function readPublicControlSlot(
|
|
666
|
+
authority: IdentityValueChannelIdentityAuthority,
|
|
667
|
+
): IdentityGlobalValueChannelPublicControlState | null {
|
|
668
|
+
const executionId = currentIdentity(authority).publicControlExecutionId
|
|
669
|
+
if (executionId === null) return null
|
|
670
|
+
return authority.slotByExecutionId.get(executionId) ?? null
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
/** The stop continuation every physical stopper routes: runtime state + the adapter's record. */
|
|
674
|
+
function onExecutionStopped(
|
|
675
|
+
authority: IdentityValueChannelIdentityAuthority,
|
|
676
|
+
executionId: number,
|
|
677
|
+
): void {
|
|
678
|
+
const execution = authority.executions.find(
|
|
679
|
+
(candidate) => candidate.executionId === executionId,
|
|
680
|
+
)
|
|
681
|
+
if (execution !== undefined) execution.stopped = true
|
|
682
|
+
updateIdentity(authority, (current) => ({
|
|
683
|
+
...current,
|
|
684
|
+
publicControlExecutionId:
|
|
685
|
+
current.publicControlExecutionId === executionId ? null : current.publicControlExecutionId,
|
|
686
|
+
physicalExecutions: current.physicalExecutions.map((candidate) =>
|
|
687
|
+
candidate.executionId === executionId
|
|
688
|
+
? { ...candidate, phase: 'STOPPED' as const }
|
|
689
|
+
: candidate,
|
|
690
|
+
),
|
|
691
|
+
}))
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
function initialRecordState(
|
|
695
|
+
channelId: number,
|
|
696
|
+
currentValue: number,
|
|
697
|
+
ownerGeneration: number,
|
|
698
|
+
): DriverValueChannelState {
|
|
699
|
+
return {
|
|
700
|
+
identity: {
|
|
701
|
+
channelId,
|
|
702
|
+
jsRecordId: nextJsRecordId++,
|
|
703
|
+
uiRecordId: nextUiRecordId++,
|
|
704
|
+
creationKind: 'direct-public-finite-number',
|
|
705
|
+
publicReferenceRetained: true,
|
|
706
|
+
sinkCount: 1,
|
|
707
|
+
changeObserverCount: 0,
|
|
708
|
+
publicControlSlotId: null,
|
|
709
|
+
physicalExecutionIds: [],
|
|
710
|
+
dueWorkSequences: [],
|
|
711
|
+
valueCompletionSequences: [],
|
|
712
|
+
targetBatchDependencyIds: [],
|
|
713
|
+
dependencyEventOutboxSequences: [],
|
|
714
|
+
dependencyEventAckWatermark: 0,
|
|
715
|
+
sharedBatchReferenceCount: 0,
|
|
716
|
+
pendingFrameActivity: false,
|
|
717
|
+
currentBindingGeneration: ownerGeneration,
|
|
718
|
+
retiredBindingGeneration: 0,
|
|
719
|
+
directOperationEpoch: 1,
|
|
720
|
+
executionLeaseEpoch: 1,
|
|
721
|
+
history: {
|
|
722
|
+
currentValue,
|
|
723
|
+
previousValue: currentValue,
|
|
724
|
+
currentTimestampMs: 0,
|
|
725
|
+
previousTimestampMs: 0,
|
|
726
|
+
},
|
|
727
|
+
sequences: {
|
|
728
|
+
authoritativeSequence: 1,
|
|
729
|
+
commandSequence: 1,
|
|
730
|
+
writeSequence: 1,
|
|
731
|
+
eventSequence: 1,
|
|
732
|
+
frameSequence: 1,
|
|
733
|
+
dueWorkSequence: 1,
|
|
734
|
+
valueCompletionSequence: 1,
|
|
735
|
+
batchDependencyEventSequence: 0,
|
|
736
|
+
dependencyOutboxAckSequence: 0,
|
|
737
|
+
},
|
|
738
|
+
},
|
|
739
|
+
handoff: {
|
|
740
|
+
ownership: 'UI_RUNNING',
|
|
741
|
+
desiredSide: null,
|
|
742
|
+
direction: null,
|
|
743
|
+
transferLease: 1,
|
|
744
|
+
phaseToken: 1,
|
|
745
|
+
sourceRecordId: null,
|
|
746
|
+
destinationRecordId: null,
|
|
747
|
+
absoluteTransferDeadlineMs: null,
|
|
748
|
+
absoluteNoConsumerDeadlineMs: null,
|
|
749
|
+
releaseFenceSequence: null,
|
|
750
|
+
sentThroughAuthoritativeSequence: 1,
|
|
751
|
+
drainedThroughAuthoritativeSequence: 1,
|
|
752
|
+
preparedMutableTuple: null,
|
|
753
|
+
destinationRuntime: null,
|
|
754
|
+
sourceMutationQueue: [],
|
|
755
|
+
commitQueue: [],
|
|
756
|
+
resources: {
|
|
757
|
+
preparedStateRetained: false,
|
|
758
|
+
preparedCloneRetained: false,
|
|
759
|
+
transferActivityRetained: false,
|
|
760
|
+
transferTimerArmed: false,
|
|
761
|
+
},
|
|
762
|
+
},
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
/** The change lane: a public direct write schedules its task and commits every attached sink. */
|
|
767
|
+
function subscribeChangeLane(authority: IdentityValueChannelIdentityAuthority): () => void {
|
|
768
|
+
return authority.record.value.on('change', (latest: number) => {
|
|
769
|
+
if (changeLaneSuppressionDepth > 0) return
|
|
770
|
+
if (typeof latest !== 'number') return
|
|
771
|
+
authority.controller.schedule({
|
|
772
|
+
kind: 'SCHEDULE_IDENTITY_GLOBAL_VALUE_CHANNEL_DIRECT_WRITE',
|
|
773
|
+
operation: 'SET',
|
|
774
|
+
value: latest,
|
|
775
|
+
})
|
|
776
|
+
for (const sink of currentIdentity(authority).sinks) {
|
|
777
|
+
if (!sink.attached) continue
|
|
778
|
+
const element = mountedElements.find(
|
|
779
|
+
(candidate) =>
|
|
780
|
+
candidate.ownerDriverId === sink.driverId &&
|
|
781
|
+
(candidate.handle as number | null) === sink.handleId,
|
|
782
|
+
)
|
|
783
|
+
if (element === undefined || element.handle === null || element.delegate.isDisposed()) {
|
|
784
|
+
continue
|
|
785
|
+
}
|
|
786
|
+
writeDelegateValues(element, { [sink.key]: latest })
|
|
787
|
+
element.frameValuesListener?.(element.delegate.driver.committed(element.handle))
|
|
788
|
+
}
|
|
789
|
+
})
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
/** Attach one element's key to an exact identity, creating the canonical record on first use. */
|
|
793
|
+
function attachIdentityValueToElement(
|
|
794
|
+
element: IdentityValueChannelElementAuthority,
|
|
795
|
+
key: string,
|
|
796
|
+
value: MotionValue<number>,
|
|
797
|
+
currentValue: number,
|
|
798
|
+
previous: {
|
|
799
|
+
readonly previousValue: MotionValue<number>
|
|
800
|
+
readonly previousSink: DriverValueChannelSinkRegistration
|
|
801
|
+
} | null,
|
|
802
|
+
): IdentityValueChannelIdentityAuthority | null {
|
|
803
|
+
if (element.handle === null) return null
|
|
804
|
+
const handle = element.handle as number
|
|
805
|
+
const existing = identities.find((candidate) => candidate.record.value === value)
|
|
806
|
+
if (existing !== undefined) {
|
|
807
|
+
const sink: DriverValueChannelSinkRegistration = {
|
|
808
|
+
driverId: element.ownerDriverId,
|
|
809
|
+
handle,
|
|
810
|
+
key,
|
|
811
|
+
bindingGeneration: element.ownerGeneration,
|
|
812
|
+
}
|
|
813
|
+
if (previous !== null) {
|
|
814
|
+
identityRegistry.rebind({
|
|
815
|
+
previousValue: previous.previousValue,
|
|
816
|
+
previousSink: previous.previousSink,
|
|
817
|
+
replacement: {
|
|
818
|
+
value: existing.record.value,
|
|
819
|
+
port: existing.record.port,
|
|
820
|
+
currentValue,
|
|
821
|
+
requestedState: existing.record.state,
|
|
822
|
+
sink,
|
|
823
|
+
},
|
|
824
|
+
})
|
|
825
|
+
} else {
|
|
826
|
+
identityRegistry.resolveBinding({
|
|
827
|
+
value: existing.record.value,
|
|
828
|
+
port: existing.record.port,
|
|
829
|
+
currentValue,
|
|
830
|
+
requestedState: existing.record.state,
|
|
831
|
+
sink,
|
|
832
|
+
})
|
|
833
|
+
}
|
|
834
|
+
const sinkId = nextSinkId++
|
|
835
|
+
updateIdentity(existing, (current) => ({
|
|
836
|
+
...current,
|
|
837
|
+
sinks: [
|
|
838
|
+
...current.sinks,
|
|
839
|
+
{
|
|
840
|
+
sinkId,
|
|
841
|
+
driverId: element.ownerDriverId,
|
|
842
|
+
handleId: handle,
|
|
843
|
+
bindingGeneration: element.ownerGeneration,
|
|
844
|
+
key,
|
|
845
|
+
passive: false,
|
|
846
|
+
attached: true,
|
|
847
|
+
},
|
|
848
|
+
],
|
|
849
|
+
}))
|
|
850
|
+
existing.attachedSinkCount += 1
|
|
851
|
+
element.identityBindings.set(key, existing)
|
|
852
|
+
return existing
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
const identityId = nextIdentityId++
|
|
856
|
+
const channelId = nextChannelId++
|
|
857
|
+
const port = createMotionValueDriverPort({
|
|
858
|
+
value,
|
|
859
|
+
clock: new ManualClock(),
|
|
860
|
+
channelId,
|
|
861
|
+
rawSnapshot: {
|
|
862
|
+
kind: 'collector-silent-motion-value-raw-snapshot',
|
|
863
|
+
currentValue,
|
|
864
|
+
history: { kind: 'single-sample', currentValue, currentTimestampMs: 0 },
|
|
865
|
+
directOperationEpoch: 1,
|
|
866
|
+
authoritativeSequence: 1,
|
|
867
|
+
},
|
|
868
|
+
})
|
|
869
|
+
const sink: DriverValueChannelSinkRegistration = {
|
|
870
|
+
driverId: element.ownerDriverId,
|
|
871
|
+
handle,
|
|
872
|
+
key,
|
|
873
|
+
bindingGeneration: element.ownerGeneration,
|
|
874
|
+
}
|
|
875
|
+
const requestedState = initialRecordState(channelId, currentValue, element.ownerGeneration)
|
|
876
|
+
const resolution =
|
|
877
|
+
previous !== null
|
|
878
|
+
? identityRegistry.rebind({
|
|
879
|
+
previousValue: previous.previousValue,
|
|
880
|
+
previousSink: previous.previousSink,
|
|
881
|
+
replacement: { value, port, currentValue, requestedState, sink },
|
|
882
|
+
}).replacement
|
|
883
|
+
: identityRegistry.resolveBinding({ value, port, currentValue, requestedState, sink })
|
|
884
|
+
if (resolution.kind !== 'eligible') return null
|
|
885
|
+
const record = resolution.record
|
|
886
|
+
const identity: IdentityGlobalValueChannelIdentityState = {
|
|
887
|
+
identityId,
|
|
888
|
+
channelId,
|
|
889
|
+
value: record.value,
|
|
890
|
+
ownership: {
|
|
891
|
+
kind: 'UI_RUNNING',
|
|
892
|
+
driverId: element.ownerDriverId,
|
|
893
|
+
ownerGeneration: element.ownerGeneration,
|
|
894
|
+
},
|
|
895
|
+
publicControlExecutionId: null,
|
|
896
|
+
sinks: [
|
|
897
|
+
{
|
|
898
|
+
sinkId: nextSinkId++,
|
|
899
|
+
driverId: element.ownerDriverId,
|
|
900
|
+
handleId: handle,
|
|
901
|
+
bindingGeneration: element.ownerGeneration,
|
|
902
|
+
key,
|
|
903
|
+
passive: false,
|
|
904
|
+
attached: true,
|
|
905
|
+
},
|
|
906
|
+
],
|
|
907
|
+
observers: [],
|
|
908
|
+
physicalExecutions: [],
|
|
909
|
+
pendingTasks: [],
|
|
910
|
+
pendingValueCompletions: [],
|
|
911
|
+
pendingFrameActivity: false,
|
|
912
|
+
targetBatchDependencyEvents: [],
|
|
913
|
+
dependencyEventAckWatermark: 0,
|
|
914
|
+
carrierReferenceCount: 0,
|
|
915
|
+
cache: {
|
|
916
|
+
currentValue,
|
|
917
|
+
previousValue: currentValue,
|
|
918
|
+
currentTimestampMs: 0,
|
|
919
|
+
previousTimestampMs: 0,
|
|
920
|
+
velocity: 0,
|
|
921
|
+
},
|
|
922
|
+
}
|
|
923
|
+
runtimeIdentities.push(identity)
|
|
924
|
+
const authority: IdentityValueChannelIdentityAuthority = {
|
|
925
|
+
identityId,
|
|
926
|
+
channelId,
|
|
927
|
+
record,
|
|
928
|
+
controller: createIdentityGlobalValueChannelHostOperationController({ record, runtime }),
|
|
929
|
+
slotByExecutionId: new Map(),
|
|
930
|
+
executions: [],
|
|
931
|
+
unsubscribeChangeLane: () => undefined,
|
|
932
|
+
attachedSinkCount: 1,
|
|
933
|
+
}
|
|
934
|
+
identities.push(authority)
|
|
935
|
+
;(authority as { unsubscribeChangeLane: () => void }).unsubscribeChangeLane =
|
|
936
|
+
subscribeChangeLane(authority)
|
|
937
|
+
element.identityBindings.set(key, authority)
|
|
938
|
+
return authority
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
/** Detach one element's sink from an exact identity; returns the sink for a rebind chain. */
|
|
942
|
+
function detachIdentityValueFromElement(
|
|
943
|
+
element: IdentityValueChannelElementAuthority,
|
|
944
|
+
key: string,
|
|
945
|
+
value: MotionValue<number>,
|
|
946
|
+
spliceRegistry: boolean,
|
|
947
|
+
): DriverValueChannelSinkRegistration | null {
|
|
948
|
+
if (element.handle === null) return null
|
|
949
|
+
const authority = identities.find((candidate) => candidate.record.value === value)
|
|
950
|
+
if (authority === undefined) {
|
|
951
|
+
element.identityBindings.delete(key)
|
|
952
|
+
return null
|
|
953
|
+
}
|
|
954
|
+
const handle = element.handle as number
|
|
955
|
+
const sink =
|
|
956
|
+
authority.record.sinks.find(
|
|
957
|
+
(candidate) =>
|
|
958
|
+
candidate.driverId === element.ownerDriverId &&
|
|
959
|
+
candidate.handle === handle &&
|
|
960
|
+
candidate.key === key,
|
|
961
|
+
) ?? null
|
|
962
|
+
if (sink !== null && spliceRegistry) {
|
|
963
|
+
identityRegistry.detachBinding({ value: authority.record.value, sink })
|
|
964
|
+
}
|
|
965
|
+
if (sink !== null) {
|
|
966
|
+
updateIdentity(authority, (current) => ({
|
|
967
|
+
...current,
|
|
968
|
+
sinks: current.sinks.map((candidate) =>
|
|
969
|
+
candidate.driverId === element.ownerDriverId &&
|
|
970
|
+
candidate.handleId === handle &&
|
|
971
|
+
candidate.key === key
|
|
972
|
+
? { ...candidate, attached: false }
|
|
973
|
+
: candidate,
|
|
974
|
+
),
|
|
975
|
+
}))
|
|
976
|
+
authority.attachedSinkCount -= 1
|
|
977
|
+
if (authority.attachedSinkCount === 0) authority.unsubscribeChangeLane()
|
|
978
|
+
}
|
|
979
|
+
element.identityBindings.delete(key)
|
|
980
|
+
return sink
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
/** Attach one element's eligible key to its value's canonical identity, creating it on first use. */
|
|
984
|
+
function bindIdentityKey(
|
|
985
|
+
element: IdentityValueChannelElementAuthority,
|
|
986
|
+
key: string,
|
|
987
|
+
): IdentityValueChannelIdentityAuthority | null {
|
|
988
|
+
const bound = element.adapter.stylePreparation.boundStyleValues[key]
|
|
989
|
+
const currentValue = element.resolvedStyleValues[key]
|
|
990
|
+
if (bound === undefined || currentValue === undefined || element.handle === null) return null
|
|
991
|
+
// The mount snapshot already filtered to this class; the predicate re-narrows the type (and
|
|
992
|
+
// keeps the one-read law — the scalar is the captured snapshot, never a fresh get()).
|
|
993
|
+
if (!isDirectPublicNumericMotionValue(bound, currentValue)) return null
|
|
994
|
+
return attachIdentityValueToElement(element, key, bound, currentValue, null)
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
/**
|
|
998
|
+
* One ordinary identity start: install the runtime control BEFORE the physical start so the
|
|
999
|
+
* synchronous animationStart stack observes it, then capture the graph handle/generator.
|
|
1000
|
+
*/
|
|
1001
|
+
function startIdentityExecution(
|
|
1002
|
+
element: IdentityValueChannelElementAuthority,
|
|
1003
|
+
authority: IdentityValueChannelIdentityAuthority,
|
|
1004
|
+
target: number | readonly (number | null)[],
|
|
1005
|
+
transition: Transition,
|
|
1006
|
+
): void {
|
|
1007
|
+
const identity = currentIdentity(authority)
|
|
1008
|
+
// Stale-clear: executions stopped BEFORE this start retire with it, so a successor coexists
|
|
1009
|
+
// only with the live orphan lineage (the current start's own interruption is not stale).
|
|
1010
|
+
const preStoppedExecutionIds = new Set(
|
|
1011
|
+
identity.physicalExecutions
|
|
1012
|
+
.filter((candidate) => candidate.phase === 'STOPPED')
|
|
1013
|
+
.map((candidate) => candidate.executionId),
|
|
1014
|
+
)
|
|
1015
|
+
const executionId = nextExecutionId++
|
|
1016
|
+
const controlId = nextControlId++
|
|
1017
|
+
const valueCompletionSequence = nextValueCompletionSequence++
|
|
1018
|
+
const directOperationEpoch = nextDirectOperationEpoch++
|
|
1019
|
+
const executionLease = nextExecutionLease++
|
|
1020
|
+
const initiatingSink = identity.sinks.find(
|
|
1021
|
+
(candidate) =>
|
|
1022
|
+
candidate.attached &&
|
|
1023
|
+
candidate.driverId === element.ownerDriverId &&
|
|
1024
|
+
candidate.handleId === (element.handle as number),
|
|
1025
|
+
)
|
|
1026
|
+
authority.controller.dispatch({
|
|
1027
|
+
kind: 'HOST_START_IDENTITY_GLOBAL_VALUE_CHANNEL_EXECUTION',
|
|
1028
|
+
identityId: authority.identityId,
|
|
1029
|
+
initiatingSinkId: initiatingSink?.sinkId ?? 0,
|
|
1030
|
+
commandSequence: nextCommandSequence++,
|
|
1031
|
+
directOperationEpoch,
|
|
1032
|
+
executionLease,
|
|
1033
|
+
requestedExecution: {
|
|
1034
|
+
executionId,
|
|
1035
|
+
creationSequence: nextExecutionCreationSequence++,
|
|
1036
|
+
updateSequence: 0,
|
|
1037
|
+
phase: 'RUNNING',
|
|
1038
|
+
orphaned: false,
|
|
1039
|
+
retained: true,
|
|
1040
|
+
sample: {
|
|
1041
|
+
writeSequence: 0,
|
|
1042
|
+
value: identity.cache.currentValue,
|
|
1043
|
+
velocity: identity.cache.velocity,
|
|
1044
|
+
done: false,
|
|
1045
|
+
},
|
|
1046
|
+
transitionOnUpdateCallbackId: null,
|
|
1047
|
+
transitionOnCompleteCallbackId: null,
|
|
1048
|
+
},
|
|
1049
|
+
})
|
|
1050
|
+
authority.slotByExecutionId.set(executionId, {
|
|
1051
|
+
controlId,
|
|
1052
|
+
executionId,
|
|
1053
|
+
valueCompletionSequence,
|
|
1054
|
+
})
|
|
1055
|
+
const lifecycleCallbackId = nextCallbackId++
|
|
1056
|
+
lifecycleCallbacks.set(lifecycleCallbackId, () =>
|
|
1057
|
+
emitIdentityValueEvent(authority.record.value, 'animationComplete'),
|
|
1058
|
+
)
|
|
1059
|
+
updateIdentity(authority, (current) => ({
|
|
1060
|
+
...current,
|
|
1061
|
+
pendingValueCompletions: [
|
|
1062
|
+
...current.pendingValueCompletions,
|
|
1063
|
+
{
|
|
1064
|
+
completionSequence: valueCompletionSequence,
|
|
1065
|
+
executionId,
|
|
1066
|
+
lifecycleCallbackId,
|
|
1067
|
+
publicStartPromiseId: null,
|
|
1068
|
+
armed: true,
|
|
1069
|
+
},
|
|
1070
|
+
],
|
|
1071
|
+
}))
|
|
1072
|
+
const factory = identityTrajectoryFactory(target, transition)
|
|
1073
|
+
const execution: IdentityValueChannelExecutionRecord = {
|
|
1074
|
+
executionId,
|
|
1075
|
+
controlId,
|
|
1076
|
+
valueCompletionSequence,
|
|
1077
|
+
lifecycleCallbackId,
|
|
1078
|
+
directOperationEpoch,
|
|
1079
|
+
executionLease,
|
|
1080
|
+
owner: element,
|
|
1081
|
+
generator: null as unknown as Generator,
|
|
1082
|
+
handle: null as unknown as Animation,
|
|
1083
|
+
elapsedMs: 0,
|
|
1084
|
+
done: false,
|
|
1085
|
+
stopped: false,
|
|
1086
|
+
}
|
|
1087
|
+
const handle = channel.graph.animate(authority.record.value, (seed) => {
|
|
1088
|
+
const generator = factory(seed)
|
|
1089
|
+
execution.generator = generator
|
|
1090
|
+
return generator
|
|
1091
|
+
})
|
|
1092
|
+
execution.handle = handle
|
|
1093
|
+
authority.executions.push(execution)
|
|
1094
|
+
// A stop through the graph's own stopper during the animationStart stack (no port installation
|
|
1095
|
+
// existed yet) leaves no continuation hook; reconcile it from the finished handle.
|
|
1096
|
+
if (handle.done && !execution.stopped) onExecutionStopped(authority, executionId)
|
|
1097
|
+
// The port slot installs only while the runtime slot still holds THIS execution; a listener
|
|
1098
|
+
// that replaced or stopped it keeps its own outcome.
|
|
1099
|
+
if (currentIdentity(authority).publicControlExecutionId === executionId) {
|
|
1100
|
+
authority.record.port.installPublicControlSlot({
|
|
1101
|
+
controlId,
|
|
1102
|
+
executionId,
|
|
1103
|
+
stopPhysicalExecution: () => {
|
|
1104
|
+
handle.stop()
|
|
1105
|
+
onExecutionStopped(authority, executionId)
|
|
1106
|
+
},
|
|
1107
|
+
})
|
|
1108
|
+
}
|
|
1109
|
+
if (preStoppedExecutionIds.size > 0) {
|
|
1110
|
+
updateIdentity(authority, (current) => ({
|
|
1111
|
+
...current,
|
|
1112
|
+
physicalExecutions: current.physicalExecutions.filter(
|
|
1113
|
+
(candidate) => !preStoppedExecutionIds.has(candidate.executionId),
|
|
1114
|
+
),
|
|
1115
|
+
}))
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
/** The canonical target-command flow: one batch per command, one capture/carrier per key. */
|
|
1120
|
+
function runIdentityTargetCommandFlow(
|
|
1121
|
+
element: IdentityValueChannelElementAuthority,
|
|
1122
|
+
command: DriverCommand & { readonly targets: object },
|
|
1123
|
+
identityKeys: readonly string[],
|
|
1124
|
+
carrierClass: boolean,
|
|
1125
|
+
): void {
|
|
1126
|
+
targetBatchState = advanceGlobalTargetBatch(targetBatchState, {
|
|
1127
|
+
kind: 'OPEN_TARGET_BATCH',
|
|
1128
|
+
creationSequence: nextBatchCreationSequence++,
|
|
1129
|
+
ownerDriverId: element.ownerDriverId,
|
|
1130
|
+
ownerGeneration: element.ownerGeneration,
|
|
1131
|
+
orchestrationDependencyCount: 0,
|
|
1132
|
+
}).state
|
|
1133
|
+
const batchId = targetBatchState.batches[targetBatchState.batches.length - 1]!.batchId
|
|
1134
|
+
const deferredCarriers: {
|
|
1135
|
+
readonly authority: IdentityValueChannelIdentityAuthority
|
|
1136
|
+
readonly key: string
|
|
1137
|
+
readonly authoredIndex: number
|
|
1138
|
+
readonly carrierId: number
|
|
1139
|
+
readonly value: number
|
|
1140
|
+
}[] = []
|
|
1141
|
+
identityKeys.forEach((key, authoredIndex) => {
|
|
1142
|
+
const authority = element.identityBindings.get(key)
|
|
1143
|
+
if (authority === undefined) return
|
|
1144
|
+
const { target, transition } = resolveIdentityTarget(command, key)
|
|
1145
|
+
// The host-resolved carrier class (a gesture-state/presence retarget of any transition
|
|
1146
|
+
// shape) rides the apparent-instant lane: no control, no flight — the carrier commit below
|
|
1147
|
+
// is the write — while a declarative/transition-defined target keeps its timed trajectory.
|
|
1148
|
+
const instant = carrierClass || isInstantIdentityTransition(transition)
|
|
1149
|
+
if (instant) {
|
|
1150
|
+
// The apparent-instant lane leaves no slot and emits no cancel: no runtime control, no
|
|
1151
|
+
// physical execution; the start still publishes so listeners observe the absent slot.
|
|
1152
|
+
if (currentIdentity(authority).publicControlExecutionId !== null) {
|
|
1153
|
+
authority.record.value.stop()
|
|
1154
|
+
}
|
|
1155
|
+
emitIdentityValueEvent(authority.record.value, 'animationStart')
|
|
1156
|
+
} else {
|
|
1157
|
+
startIdentityExecution(element, authority, target, transition)
|
|
1158
|
+
}
|
|
1159
|
+
const captureId = nextCaptureId++
|
|
1160
|
+
const listenerStackCompletionSequence = nextEdgeSequence++
|
|
1161
|
+
orderedEdges.push({
|
|
1162
|
+
kind: 'LISTENER_STACK_COMPLETED',
|
|
1163
|
+
sequence: listenerStackCompletionSequence,
|
|
1164
|
+
batchId,
|
|
1165
|
+
})
|
|
1166
|
+
const dependencyId = nextDependencyId++
|
|
1167
|
+
const constructed = constructGlobalTargetBatchFromPostListenerMotionValueSlot(
|
|
1168
|
+
targetBatchState,
|
|
1169
|
+
{
|
|
1170
|
+
kind: instant
|
|
1171
|
+
? 'CONSTRUCT_APPARENT_INSTANT_GLOBAL_TARGET_BATCH_CAPTURE'
|
|
1172
|
+
: 'CONSTRUCT_REPLACED_ORDINARY_GLOBAL_TARGET_BATCH_CAPTURE',
|
|
1173
|
+
batchId,
|
|
1174
|
+
identityId: authority.identityId,
|
|
1175
|
+
captureId,
|
|
1176
|
+
authoredIndex,
|
|
1177
|
+
listenerStackCompletionSequence,
|
|
1178
|
+
dependencyId,
|
|
1179
|
+
},
|
|
1180
|
+
{
|
|
1181
|
+
value: authority.record.value,
|
|
1182
|
+
readPublicControlSlot: () => readPublicControlSlot(authority),
|
|
1183
|
+
},
|
|
1184
|
+
)
|
|
1185
|
+
targetBatchState = constructed.state
|
|
1186
|
+
const batch = targetBatchState.batches.find((candidate) => candidate.batchId === batchId)
|
|
1187
|
+
const capture = batch?.capturedControls.find((candidate) => candidate.captureId === captureId)
|
|
1188
|
+
const carrierValue = instantFinalKeyframe(target)
|
|
1189
|
+
if (capture?.kind === 'PUBLIC_IDENTITY') {
|
|
1190
|
+
orderedEdges.push({
|
|
1191
|
+
kind: 'TARGET_BATCH_DEPENDENCY_APPENDED',
|
|
1192
|
+
sequence: nextEdgeSequence++,
|
|
1193
|
+
batchId,
|
|
1194
|
+
dependencyId,
|
|
1195
|
+
})
|
|
1196
|
+
const carrierId = nextCarrierId++
|
|
1197
|
+
const receipt = authority.controller.schedule({
|
|
1198
|
+
kind: 'SCHEDULE_IDENTITY_GLOBAL_VALUE_CHANNEL_CARRIER',
|
|
1199
|
+
carrierId,
|
|
1200
|
+
authoredIndex,
|
|
1201
|
+
key,
|
|
1202
|
+
value: carrierValue,
|
|
1203
|
+
})
|
|
1204
|
+
targetBatchState = advanceGlobalTargetBatch(targetBatchState, {
|
|
1205
|
+
kind: 'APPEND_AUTHORED_TARGET_BATCH_CARRIER',
|
|
1206
|
+
batchId,
|
|
1207
|
+
carrier: {
|
|
1208
|
+
carrierId,
|
|
1209
|
+
authoredIndex,
|
|
1210
|
+
identityId: authority.identityId,
|
|
1211
|
+
key,
|
|
1212
|
+
taskSequence: receipt.scheduledTask.taskSequence,
|
|
1213
|
+
},
|
|
1214
|
+
}).state
|
|
1215
|
+
carrierValues.set(carrierId, carrierValue)
|
|
1216
|
+
// A raw-waiting carrier's delivery records when it actually drains at the reaction.
|
|
1217
|
+
return
|
|
1218
|
+
}
|
|
1219
|
+
deferredCarriers.push({
|
|
1220
|
+
authority,
|
|
1221
|
+
key,
|
|
1222
|
+
authoredIndex,
|
|
1223
|
+
carrierId: nextCarrierId++,
|
|
1224
|
+
value: carrierValue,
|
|
1225
|
+
})
|
|
1226
|
+
})
|
|
1227
|
+
const sealed = targetBatchState.batches.find((candidate) => candidate.batchId === batchId)
|
|
1228
|
+
if (sealed !== undefined && sealed.dependencies.length === 0) {
|
|
1229
|
+
// Zero-dependency seal: the carrier tasks schedule inside the production boundary, and the
|
|
1230
|
+
// delivery records land now while the element delivery waits for the reaction.
|
|
1231
|
+
const produced = produceIdentityGlobalValueChannelSealedEmptyBatch(
|
|
1232
|
+
targetBatchState,
|
|
1233
|
+
runtime,
|
|
1234
|
+
{
|
|
1235
|
+
kind: 'PRODUCE_IDENTITY_GLOBAL_VALUE_CHANNEL_SEALED_EMPTY_BATCH',
|
|
1236
|
+
batchId,
|
|
1237
|
+
preparedTargets: [],
|
|
1238
|
+
authoredCarriers: deferredCarriers.map(
|
|
1239
|
+
({ authority, authoredIndex, carrierId, key, value }) => ({
|
|
1240
|
+
identityId: authority.identityId,
|
|
1241
|
+
command: {
|
|
1242
|
+
kind: 'SCHEDULE_IDENTITY_GLOBAL_VALUE_CHANNEL_CARRIER',
|
|
1243
|
+
carrierId,
|
|
1244
|
+
authoredIndex,
|
|
1245
|
+
key,
|
|
1246
|
+
value,
|
|
1247
|
+
},
|
|
1248
|
+
}),
|
|
1249
|
+
),
|
|
1250
|
+
},
|
|
1251
|
+
)
|
|
1252
|
+
targetBatchState = produced.batchState
|
|
1253
|
+
produced.carrierReceipts.forEach((receipt, index) => {
|
|
1254
|
+
const deferred = deferredCarriers[index]!
|
|
1255
|
+
carrierValues.set(deferred.carrierId, deferred.value)
|
|
1256
|
+
// A sealed-empty carrier queued synchronously at seal: its delivery records now, while
|
|
1257
|
+
// the element delivery still waits for the reaction.
|
|
1258
|
+
recordCarrierDelivery({
|
|
1259
|
+
batchId,
|
|
1260
|
+
carrierId: deferred.carrierId,
|
|
1261
|
+
authoredIndex: deferred.authoredIndex,
|
|
1262
|
+
identityId: deferred.authority.identityId,
|
|
1263
|
+
key: deferred.key,
|
|
1264
|
+
taskSequence: receipt.scheduledTask.taskSequence,
|
|
1265
|
+
})
|
|
1266
|
+
})
|
|
1267
|
+
return
|
|
1268
|
+
}
|
|
1269
|
+
targetBatchState = advanceGlobalTargetBatch(targetBatchState, {
|
|
1270
|
+
kind: 'SEAL_TARGET_BATCH',
|
|
1271
|
+
batchId,
|
|
1272
|
+
}).state
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
/**
|
|
1276
|
+
* A completing execution: the lifecycle emits while the post-listener slot is still installed,
|
|
1277
|
+
* then the slot clears token-blind WITHOUT stopping the retained physical execution (the
|
|
1278
|
+
* successor keeps sampling as an orphan), and only then do the batch dependency events stage.
|
|
1279
|
+
*/
|
|
1280
|
+
function completeIdentityExecution(
|
|
1281
|
+
authority: IdentityValueChannelIdentityAuthority,
|
|
1282
|
+
execution: IdentityValueChannelExecutionRecord,
|
|
1283
|
+
openEntries: ChannelMailboxEntry[],
|
|
1284
|
+
): void {
|
|
1285
|
+
const descriptor = {
|
|
1286
|
+
controlId: execution.controlId,
|
|
1287
|
+
executionId: execution.executionId,
|
|
1288
|
+
valueCompletionSequence: execution.valueCompletionSequence,
|
|
1289
|
+
target: currentIdentity(authority).cache.currentValue,
|
|
1290
|
+
}
|
|
1291
|
+
const slot = readPublicControlSlot(authority)
|
|
1292
|
+
completeIdentityGlobalValueChannelExecution(
|
|
1293
|
+
{
|
|
1294
|
+
value: authority.record.value,
|
|
1295
|
+
readPublicControlSlot: () => readPublicControlSlot(authority),
|
|
1296
|
+
startExecution: () => undefined,
|
|
1297
|
+
invokeDetachedCallback: () => undefined,
|
|
1298
|
+
invokeCompletionListener: (listenerId) => {
|
|
1299
|
+
transitionCallbacks.get(listenerId)?.(currentIdentity(authority).cache.currentValue)
|
|
1300
|
+
},
|
|
1301
|
+
emitAnimationCancel: () => undefined,
|
|
1302
|
+
emitAnimationComplete: () =>
|
|
1303
|
+
emitIdentityValueEvent(authority.record.value, 'animationComplete'),
|
|
1304
|
+
clearPublicControlSlotTokenBlind: (causedByExecutionId) => {
|
|
1305
|
+
const cleared = currentIdentity(authority).publicControlExecutionId
|
|
1306
|
+
// Token-blind (pinned completion/cancellation continuation): the post-listener slot
|
|
1307
|
+
// clears unconditionally and the retained physical execution is NEVER stopped.
|
|
1308
|
+
authority.record.port.clearPublicControlSlotTokenBlind(causedByExecutionId)
|
|
1309
|
+
updateIdentity(authority, (current) => ({
|
|
1310
|
+
...current,
|
|
1311
|
+
publicControlExecutionId: null,
|
|
1312
|
+
physicalExecutions: current.physicalExecutions.map((candidate) =>
|
|
1313
|
+
candidate.executionId === cleared && cleared !== causedByExecutionId
|
|
1314
|
+
? { ...candidate, orphaned: true }
|
|
1315
|
+
: candidate,
|
|
1316
|
+
),
|
|
1317
|
+
pendingValueCompletions: current.pendingValueCompletions.filter(
|
|
1318
|
+
(candidate) => candidate.completionSequence !== execution.valueCompletionSequence,
|
|
1319
|
+
),
|
|
1320
|
+
}))
|
|
1321
|
+
},
|
|
1322
|
+
stopPublicControl: () => authority.record.value.stop(),
|
|
1323
|
+
adoptPassiveTerminalCache: () => undefined,
|
|
1324
|
+
readSynchronousUiSnapshot: () => {
|
|
1325
|
+
const cache = currentIdentity(authority).cache
|
|
1326
|
+
return { value: cache.currentValue, velocity: cache.velocity }
|
|
1327
|
+
},
|
|
1328
|
+
},
|
|
1329
|
+
{
|
|
1330
|
+
kind: 'DELIVER_ORPHAN_IDENTITY_COMPLETION_AGAINST_SUCCESSOR_SLOT',
|
|
1331
|
+
identityId: authority.identityId,
|
|
1332
|
+
orphanExecution: descriptor,
|
|
1333
|
+
installedSuccessor:
|
|
1334
|
+
slot === null
|
|
1335
|
+
? descriptor
|
|
1336
|
+
: {
|
|
1337
|
+
controlId: slot.controlId,
|
|
1338
|
+
executionId: slot.executionId,
|
|
1339
|
+
valueCompletionSequence: slot.valueCompletionSequence,
|
|
1340
|
+
target: currentIdentity(authority).cache.currentValue,
|
|
1341
|
+
},
|
|
1342
|
+
listenerId: execution.lifecycleCallbackId,
|
|
1343
|
+
},
|
|
1344
|
+
)
|
|
1345
|
+
execution.done = true
|
|
1346
|
+
const drainId = runtime.state.nextMailboxDrainId
|
|
1347
|
+
const stage = (
|
|
1348
|
+
batchId: number,
|
|
1349
|
+
dependencyId: number,
|
|
1350
|
+
dependencyEventKind: TargetBatchDependencyEventKind,
|
|
1351
|
+
): void => {
|
|
1352
|
+
openEntries.push({
|
|
1353
|
+
kind: 'TARGET_BATCH_DEPENDENCY_EVENT',
|
|
1354
|
+
entryId: drainId * 1000 + openEntries.length,
|
|
1355
|
+
index: openEntries.length,
|
|
1356
|
+
batchId,
|
|
1357
|
+
dependencyId,
|
|
1358
|
+
source: {
|
|
1359
|
+
kind: 'PUBLIC_IDENTITY',
|
|
1360
|
+
identityId: authority.identityId,
|
|
1361
|
+
channelId: authority.channelId,
|
|
1362
|
+
},
|
|
1363
|
+
dependencyEventKind,
|
|
1364
|
+
eventSequence: nextDependencyEventSequence++,
|
|
1365
|
+
})
|
|
1366
|
+
}
|
|
1367
|
+
for (const batch of targetBatchState.batches) {
|
|
1368
|
+
for (const dependency of batch.dependencies) {
|
|
1369
|
+
if (dependency.source.kind !== 'PUBLIC_IDENTITY') continue
|
|
1370
|
+
if (
|
|
1371
|
+
dependency.source.identityId !== authority.identityId ||
|
|
1372
|
+
dependency.source.executionId !== execution.executionId
|
|
1373
|
+
) {
|
|
1374
|
+
continue
|
|
1375
|
+
}
|
|
1376
|
+
if (dependency.rawResolvedEventSequence === null) {
|
|
1377
|
+
stage(batch.batchId, dependency.dependencyId, 'RAW_RESOLVED')
|
|
1378
|
+
}
|
|
1379
|
+
if (dependency.valueAcknowledgedEventSequence === null) {
|
|
1380
|
+
stage(batch.batchId, dependency.dependencyId, 'VALUE_ACKNOWLEDGED')
|
|
1381
|
+
}
|
|
1382
|
+
}
|
|
1383
|
+
}
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1386
|
+
function buildDrainRuntime(envelope: ChannelMailboxTurnEnvelope): ChannelMailboxDrainRuntime {
|
|
1387
|
+
return {
|
|
1388
|
+
transferStatusValidator: channelMailboxTransferStatusValidator,
|
|
1389
|
+
transferStatusHandler: channelMailboxTransferStatusHandler,
|
|
1390
|
+
transferStatusTimestampMs: 0,
|
|
1391
|
+
canonicalTransferStatusState: unexpectedIdentityTransferStatus,
|
|
1392
|
+
persistTransferStatusTransition: unexpectedIdentityTransferStatus,
|
|
1393
|
+
publishTransferStatus: unexpectedIdentityTransferStatus,
|
|
1394
|
+
reportTransferStatusRefusal: unexpectedIdentityTransferStatus,
|
|
1395
|
+
entryHandlers: {
|
|
1396
|
+
VALUE_WRITE: unexpectedIdentityMailboxEntry,
|
|
1397
|
+
TOKENLESS_TASK_CALLBACK: unexpectedIdentityMailboxEntry,
|
|
1398
|
+
ELEMENT_HOST_COMMIT: unexpectedIdentityMailboxEntry,
|
|
1399
|
+
TRANSITION_CALLBACK: unexpectedIdentityMailboxEntry,
|
|
1400
|
+
LIFECYCLE_CALLBACK: unexpectedIdentityMailboxEntry,
|
|
1401
|
+
DESTINATION_COMMIT_ACKNOWLEDGEMENT: unexpectedIdentityMailboxEntry,
|
|
1402
|
+
TARGET_BATCH_DEPENDENCY_EVENT(entry) {
|
|
1403
|
+
const reported = advanceGlobalTargetBatch(targetBatchState, {
|
|
1404
|
+
kind: 'REPORT_DEPENDENCY_EVENT',
|
|
1405
|
+
mailboxDrainId: envelope.mailboxDrainId,
|
|
1406
|
+
event: {
|
|
1407
|
+
batchId: entry.batchId,
|
|
1408
|
+
dependencyId: entry.dependencyId,
|
|
1409
|
+
kind: entry.dependencyEventKind,
|
|
1410
|
+
eventSequence: entry.eventSequence,
|
|
1411
|
+
},
|
|
1412
|
+
})
|
|
1413
|
+
targetBatchState = reported.state
|
|
1414
|
+
// One drained-completion edge per dependency: the acknowledgement report that advanced
|
|
1415
|
+
// (never the raw half, never a replayed dedupe).
|
|
1416
|
+
if (
|
|
1417
|
+
entry.dependencyEventKind === 'VALUE_ACKNOWLEDGED' &&
|
|
1418
|
+
reported.orderedEffects.some((effect) => effect.kind === 'ADVANCE_DEPENDENCY')
|
|
1419
|
+
) {
|
|
1420
|
+
orderedEdges.push({
|
|
1421
|
+
kind: 'ASYNCHRONOUS_COMPLETION_DRAINED',
|
|
1422
|
+
sequence: nextEdgeSequence++,
|
|
1423
|
+
batchId: entry.batchId,
|
|
1424
|
+
dependencyId: entry.dependencyId,
|
|
1425
|
+
})
|
|
1426
|
+
}
|
|
1427
|
+
},
|
|
1428
|
+
TARGET_BATCH_DEPENDENCY_HANDOFF: unexpectedIdentityMailboxEntry,
|
|
1429
|
+
TARGET_BATCH_DEPENDENCY_OUTBOX_ACKNOWLEDGEMENT: unexpectedIdentityMailboxEntry,
|
|
1430
|
+
LAYOUT_RELEASE: unexpectedIdentityMailboxEntry,
|
|
1431
|
+
PROJECTION_STALE: unexpectedIdentityMailboxEntry,
|
|
1432
|
+
LAYOUT_SETTLE: unexpectedIdentityMailboxEntry,
|
|
1433
|
+
DRIVER_SETTLE: unexpectedIdentityMailboxEntry,
|
|
1434
|
+
},
|
|
1435
|
+
transportedSamplePort(channelId) {
|
|
1436
|
+
const authority = identities.find((candidate) => candidate.channelId === channelId)
|
|
1437
|
+
if (authority === undefined) {
|
|
1438
|
+
throw new IdentityGlobalValueChannelInvariantError('identity-unknown')
|
|
1439
|
+
}
|
|
1440
|
+
return authority.record.port
|
|
1441
|
+
},
|
|
1442
|
+
invokeTransitionCallback(callbackId) {
|
|
1443
|
+
transitionCallbacks.get(callbackId)?.(0)
|
|
1444
|
+
},
|
|
1445
|
+
invokeLifecycleCallback(callbackId) {
|
|
1446
|
+
lifecycleCallbacks.get(callbackId)?.()
|
|
1447
|
+
},
|
|
1448
|
+
abortAppliedPrefixAndResolveReadyAggregates: () => undefined,
|
|
1449
|
+
finishAuthoritativeMailboxDrainAndTargetBatchTail(mailboxDrainId) {
|
|
1450
|
+
targetBatchState = advanceGlobalTargetBatch(targetBatchState, {
|
|
1451
|
+
kind: 'FINISH_MAILBOX_TAIL',
|
|
1452
|
+
mailboxDrainId,
|
|
1453
|
+
}).state
|
|
1454
|
+
},
|
|
1455
|
+
}
|
|
1456
|
+
}
|
|
1457
|
+
|
|
1458
|
+
function recordCarrierDelivery(delivery: IdentityValueChannelCarrierDelivery): void {
|
|
1459
|
+
if (recordedCarrierDeliveryIds.has(delivery.carrierId)) return
|
|
1460
|
+
recordedCarrierDeliveryIds.add(delivery.carrierId)
|
|
1461
|
+
carrierDeliveries.push(delivery)
|
|
1462
|
+
}
|
|
1463
|
+
|
|
1464
|
+
function drainEnvelope(
|
|
1465
|
+
envelope: ChannelMailboxTurnEnvelope,
|
|
1466
|
+
state: ChannelMailboxDrainState,
|
|
1467
|
+
): ChannelMailboxDrainState {
|
|
1468
|
+
return drainChannelMailboxTurnEnvelope(
|
|
1469
|
+
state,
|
|
1470
|
+
envelope,
|
|
1471
|
+
buildDrainRuntime(envelope),
|
|
1472
|
+
new IdentityValueChannelDrainProgressWriter(),
|
|
1473
|
+
).state
|
|
1474
|
+
}
|
|
1475
|
+
|
|
1476
|
+
function createElementBinding(
|
|
1477
|
+
element: IdentityValueChannelElementAuthority,
|
|
1478
|
+
): WorkletDriverBinding {
|
|
1479
|
+
const delegate = element.delegate
|
|
1480
|
+
// The scheduled lane's dual-authority refusal cannot reach writeDelegateValues' synchronous
|
|
1481
|
+
// catch — the crossing is fire-and-forget (REQ-DRIVER-013) — so the delegate delivers it
|
|
1482
|
+
// here instead of retiring the admitted flight. Same terminal as the catch: latch, and owned
|
|
1483
|
+
// writes stay with the flight.
|
|
1484
|
+
const unsubscribeBoundValueRefusal = delegate.onBoundValueWriteRefusal((refusedHandle) => {
|
|
1485
|
+
if (element.handle === null) return
|
|
1486
|
+
if ((element.handle as number) !== (refusedHandle as number)) return
|
|
1487
|
+
element.delegateRefusesOwnedWrites = true
|
|
1488
|
+
})
|
|
1489
|
+
const driver: WorkletDriverBinding['driver'] = {
|
|
1490
|
+
register(initial) {
|
|
1491
|
+
const handle = delegate.driver.register(initial)
|
|
1492
|
+
element.handle = handle
|
|
1493
|
+
// Sink attachment precedes any controller command (register returns first): every eligible
|
|
1494
|
+
// key resolves its canonical identity and appends its sink before mount() can issue one.
|
|
1495
|
+
for (const key of element.eligibleKeys) {
|
|
1496
|
+
bindIdentityKey(element, key)
|
|
1497
|
+
}
|
|
1498
|
+
mountedElements.push(element)
|
|
1499
|
+
// First-paint constructor writes (two-pass law): with every sink registered, each
|
|
1500
|
+
// eligible identity receives its per-key initial record value as an ordinary write —
|
|
1501
|
+
// no lifecycle, the value's own identical-write suppression keeps a resting key silent.
|
|
1502
|
+
for (const key of element.eligibleKeys) {
|
|
1503
|
+
const authority = element.identityBindings.get(key)
|
|
1504
|
+
const startValue = initial[key]
|
|
1505
|
+
if (authority === undefined || typeof startValue !== 'number') continue
|
|
1506
|
+
authority.record.value.set(startValue)
|
|
1507
|
+
}
|
|
1508
|
+
return handle
|
|
1509
|
+
},
|
|
1510
|
+
command(handle, command) {
|
|
1511
|
+
if (command.kind === 'stop') {
|
|
1512
|
+
for (const authority of element.identityBindings.values()) {
|
|
1513
|
+
authority.record.value.stop()
|
|
1514
|
+
}
|
|
1515
|
+
for (const key of element.identityBindings.keys()) element.commandOwnedKeys.delete(key)
|
|
1516
|
+
delegate.driver.command(handle, command)
|
|
1517
|
+
return
|
|
1518
|
+
}
|
|
1519
|
+
const { identityKeys } = partitionIdentityCommandKeys(
|
|
1520
|
+
element.identityBindings,
|
|
1521
|
+
command.targets,
|
|
1522
|
+
)
|
|
1523
|
+
// The delegate is the host authority for the property: every command crosses unchanged,
|
|
1524
|
+
// identity keys included (one trajectory family, REQ-DRIVER-003).
|
|
1525
|
+
delegate.driver.command(handle, command)
|
|
1526
|
+
if (identityKeys.length === 0) return
|
|
1527
|
+
for (const key of identityKeys) element.commandOwnedKeys.add(key)
|
|
1528
|
+
// The host-resolved carrier law (the carrier keyed to the host's own returned controls):
|
|
1529
|
+
// a settled element whose delegate did not take a flight commits the target's ordinary
|
|
1530
|
+
// set right away — gesture-state/presence retargets through the apparent-instant lane,
|
|
1531
|
+
// declarative updates through their timed trajectory plus the same carrier write.
|
|
1532
|
+
const hostResolved = element.mountSettled && !delegate.driver.isActive(handle)
|
|
1533
|
+
const declarative =
|
|
1534
|
+
(command as unknown as Record<symbol, unknown>)[IDENTITY_DECLARATIVE_LANE_COMMAND] ===
|
|
1535
|
+
true
|
|
1536
|
+
runIdentityTargetCommandFlow(element, command, identityKeys, hostResolved && !declarative)
|
|
1537
|
+
if (!hostResolved) return
|
|
1538
|
+
for (const key of identityKeys) {
|
|
1539
|
+
const authority = element.identityBindings.get(key)
|
|
1540
|
+
if (authority === undefined) continue
|
|
1541
|
+
const finalValue = instantFinalKeyframe(resolveIdentityTarget(command, key).target)
|
|
1542
|
+
if (typeof finalValue === 'number') authority.record.value.set(finalValue)
|
|
1543
|
+
}
|
|
1544
|
+
},
|
|
1545
|
+
step(elapsedMs) {
|
|
1546
|
+
delegate.driver.step(elapsedMs)
|
|
1547
|
+
},
|
|
1548
|
+
committed(committedHandle) {
|
|
1549
|
+
return delegate.driver.committed(committedHandle)
|
|
1550
|
+
},
|
|
1551
|
+
setActive(activeHandle, active) {
|
|
1552
|
+
if (!active) {
|
|
1553
|
+
// Quiesce-before-unmount: the element is leaving, so its in-flight identity flights
|
|
1554
|
+
// halt silently with it (teardown is not a cancellation) — the value rests where it is.
|
|
1555
|
+
for (const authority of element.identityBindings.values()) {
|
|
1556
|
+
for (const execution of authority.executions) {
|
|
1557
|
+
if (execution.owner !== element || execution.done || execution.stopped) continue
|
|
1558
|
+
execution.handle.stop()
|
|
1559
|
+
onExecutionStopped(authority, execution.executionId)
|
|
1560
|
+
}
|
|
1561
|
+
}
|
|
1562
|
+
}
|
|
1563
|
+
delegate.driver.setActive(activeHandle, active)
|
|
1564
|
+
},
|
|
1565
|
+
isActive(activeHandle) {
|
|
1566
|
+
if (delegate.driver.isActive(activeHandle)) return true
|
|
1567
|
+
return identities.some((authority) =>
|
|
1568
|
+
authority.executions.some(
|
|
1569
|
+
(execution) => execution.owner === element && !execution.done && !execution.stopped,
|
|
1570
|
+
),
|
|
1571
|
+
)
|
|
1572
|
+
},
|
|
1573
|
+
write(writeHandle, values, velocities, eventShape) {
|
|
1574
|
+
delegate.driver.write(writeHandle, values, velocities, eventShape)
|
|
1575
|
+
},
|
|
1576
|
+
liveFor(liveHandle, key) {
|
|
1577
|
+
return delegate.driver.liveFor(liveHandle, key)
|
|
1578
|
+
},
|
|
1579
|
+
}
|
|
1580
|
+
return {
|
|
1581
|
+
driver,
|
|
1582
|
+
sharedFor(handle, key) {
|
|
1583
|
+
return delegate.sharedFor(handle, key)
|
|
1584
|
+
},
|
|
1585
|
+
writeBoundValue(handle, values) {
|
|
1586
|
+
const identityValues: Record<string, number> = {}
|
|
1587
|
+
const restValues: Record<string, number> = {}
|
|
1588
|
+
for (const [key, value] of Object.entries(values)) {
|
|
1589
|
+
const authority = element.identityBindings.get(key)
|
|
1590
|
+
if (authority === undefined) {
|
|
1591
|
+
restValues[key] = value
|
|
1592
|
+
continue
|
|
1593
|
+
}
|
|
1594
|
+
authority.controller.schedule({
|
|
1595
|
+
kind: 'SCHEDULE_IDENTITY_GLOBAL_VALUE_CHANNEL_DIRECT_WRITE',
|
|
1596
|
+
operation: 'SET',
|
|
1597
|
+
value,
|
|
1598
|
+
})
|
|
1599
|
+
identityValues[key] = value
|
|
1600
|
+
}
|
|
1601
|
+
if (Object.keys(identityValues).length > 0) {
|
|
1602
|
+
writeDelegateValues(element, identityValues)
|
|
1603
|
+
element.frameValuesListener?.(delegate.driver.committed(handle))
|
|
1604
|
+
}
|
|
1605
|
+
if (Object.keys(restValues).length > 0) delegate.writeBoundValue(handle, restValues)
|
|
1606
|
+
},
|
|
1607
|
+
...(delegate.propertyStateFor === undefined
|
|
1608
|
+
? {}
|
|
1609
|
+
: { propertyStateFor: delegate.propertyStateFor.bind(delegate) }),
|
|
1610
|
+
setFrameValuesListener(handle, listener) {
|
|
1611
|
+
element.frameValuesListener = listener
|
|
1612
|
+
delegate.setFrameValuesListener(handle, listener)
|
|
1613
|
+
},
|
|
1614
|
+
onBoundValueWriteRefusal(listener) {
|
|
1615
|
+
return delegate.onBoundValueWriteRefusal(listener)
|
|
1616
|
+
},
|
|
1617
|
+
layout: delegate.layout,
|
|
1618
|
+
dispose() {
|
|
1619
|
+
// Teardown stops this element's in-flight identity executions silently (teardown is not
|
|
1620
|
+
// a cancellation), then detaches its sinks and change lanes before the delegate tears down.
|
|
1621
|
+
unsubscribeBoundValueRefusal()
|
|
1622
|
+
for (const authority of element.identityBindings.values()) {
|
|
1623
|
+
for (const execution of authority.executions) {
|
|
1624
|
+
if (execution.owner !== element || execution.done || execution.stopped) continue
|
|
1625
|
+
execution.handle.stop()
|
|
1626
|
+
onExecutionStopped(authority, execution.executionId)
|
|
1627
|
+
}
|
|
1628
|
+
}
|
|
1629
|
+
for (const [key, authority] of element.identityBindings) {
|
|
1630
|
+
const handle = element.handle as number
|
|
1631
|
+
const sink = authority.record.sinks.find(
|
|
1632
|
+
(candidate) =>
|
|
1633
|
+
candidate.driverId === element.ownerDriverId &&
|
|
1634
|
+
candidate.handle === handle &&
|
|
1635
|
+
candidate.key === key,
|
|
1636
|
+
)
|
|
1637
|
+
if (sink !== undefined) {
|
|
1638
|
+
identityRegistry.detachBinding({ value: authority.record.value, sink })
|
|
1639
|
+
}
|
|
1640
|
+
updateIdentity(authority, (current) => ({
|
|
1641
|
+
...current,
|
|
1642
|
+
sinks: current.sinks.map((candidate) =>
|
|
1643
|
+
candidate.driverId === element.ownerDriverId &&
|
|
1644
|
+
candidate.handleId === handle &&
|
|
1645
|
+
candidate.key === key
|
|
1646
|
+
? { ...candidate, attached: false }
|
|
1647
|
+
: candidate,
|
|
1648
|
+
),
|
|
1649
|
+
}))
|
|
1650
|
+
authority.attachedSinkCount -= 1
|
|
1651
|
+
if (authority.attachedSinkCount === 0) authority.unsubscribeChangeLane()
|
|
1652
|
+
}
|
|
1653
|
+
delegate.dispose()
|
|
1654
|
+
},
|
|
1655
|
+
isDisposed: () => delegate.isDisposed(),
|
|
1656
|
+
}
|
|
1657
|
+
}
|
|
1658
|
+
|
|
1659
|
+
const runtimeInterface: IdentityValueChannelControllerRuntime = {
|
|
1660
|
+
bindingForElement(bindingOptions) {
|
|
1661
|
+
const snapshot = identityChannelEligibleBoundStyleSnapshot(bindingOptions.stylePreparation)
|
|
1662
|
+
const element: IdentityValueChannelElementAuthority = {
|
|
1663
|
+
adapter: null as unknown as IdentityValueChannelControllerElementAdapter,
|
|
1664
|
+
delegate: bindingOptions.delegate,
|
|
1665
|
+
ownerDriverId: bindingOptions.ownerDriverId,
|
|
1666
|
+
ownerGeneration: bindingOptions.ownerGeneration,
|
|
1667
|
+
eligibleKeys: snapshot.keys,
|
|
1668
|
+
resolvedStyleValues: snapshot.resolvedStyleValues,
|
|
1669
|
+
identityBindings: new Map(),
|
|
1670
|
+
commandOwnedKeys: new Set(),
|
|
1671
|
+
delegateRefusesOwnedWrites: false,
|
|
1672
|
+
handle: null,
|
|
1673
|
+
frameValuesListener: null,
|
|
1674
|
+
mountSettled: false,
|
|
1675
|
+
}
|
|
1676
|
+
// The mount commit's synchronous block owns every entrance command; the first task boundary
|
|
1677
|
+
// after it flips the element to its settled (carrier-commit) law.
|
|
1678
|
+
queueMicrotask(() => {
|
|
1679
|
+
element.mountSettled = true
|
|
1680
|
+
})
|
|
1681
|
+
elements.push(element)
|
|
1682
|
+
const binding = createElementBinding(element)
|
|
1683
|
+
const adapter = {
|
|
1684
|
+
ownerDriverId: bindingOptions.ownerDriverId,
|
|
1685
|
+
ownerGeneration: bindingOptions.ownerGeneration,
|
|
1686
|
+
stylePreparation: bindingOptions.stylePreparation,
|
|
1687
|
+
delegate: bindingOptions.delegate,
|
|
1688
|
+
binding,
|
|
1689
|
+
} as IdentityValueChannelControllerElementAdapter
|
|
1690
|
+
;(element as { adapter: IdentityValueChannelControllerElementAdapter }).adapter = adapter
|
|
1691
|
+
elementByAdapter.set(adapter, element)
|
|
1692
|
+
return adapter
|
|
1693
|
+
},
|
|
1694
|
+
|
|
1695
|
+
advanceFrame(elapsedMs) {
|
|
1696
|
+
frameTimeMs += elapsedMs
|
|
1697
|
+
for (const element of mountedElements) {
|
|
1698
|
+
if (element.handle === null || element.delegate.isDisposed()) continue
|
|
1699
|
+
element.delegate.driver.step(elapsedMs)
|
|
1700
|
+
}
|
|
1701
|
+
// Exactly one mailbox turn per frame across every identity; it seals only when the frame
|
|
1702
|
+
// actually staged dependency events (an empty turn has no envelope to close).
|
|
1703
|
+
const openEntries: ChannelMailboxEntry[] = []
|
|
1704
|
+
for (const authority of identities) {
|
|
1705
|
+
const running = authority.executions.filter((execution) => !execution.done)
|
|
1706
|
+
for (const execution of running) {
|
|
1707
|
+
const record = currentIdentity(authority).physicalExecutions.find(
|
|
1708
|
+
(candidate) => candidate.executionId === execution.executionId,
|
|
1709
|
+
)
|
|
1710
|
+
if (record === undefined || record.phase === 'STOPPED') {
|
|
1711
|
+
execution.stopped = true
|
|
1712
|
+
continue
|
|
1713
|
+
}
|
|
1714
|
+
execution.elapsedMs += elapsedMs
|
|
1715
|
+
const sample = execution.generator.sample(execution.elapsedMs)
|
|
1716
|
+
changeLaneSuppressionDepth += 1
|
|
1717
|
+
try {
|
|
1718
|
+
authority.controller.dispatch({
|
|
1719
|
+
kind: 'HOST_APPLY_IDENTITY_GLOBAL_VALUE_CHANNEL_WRITE',
|
|
1720
|
+
identityId: authority.identityId,
|
|
1721
|
+
executionId: execution.executionId,
|
|
1722
|
+
executionLease: execution.executionLease,
|
|
1723
|
+
directOperationEpoch: execution.directOperationEpoch,
|
|
1724
|
+
source: 'GENERATOR',
|
|
1725
|
+
writeSequence: nextWriteSequence++,
|
|
1726
|
+
value: sample.value,
|
|
1727
|
+
velocity: sample.velocity,
|
|
1728
|
+
absoluteTimestampMs: frameTimeMs,
|
|
1729
|
+
})
|
|
1730
|
+
} finally {
|
|
1731
|
+
changeLaneSuppressionDepth -= 1
|
|
1732
|
+
}
|
|
1733
|
+
// A completion whose host fanout reentrantly stopped the control still completes: the
|
|
1734
|
+
// lifecycle emission observes the post-listener slot before the token-blind clear.
|
|
1735
|
+
if (sample.done) completeIdentityExecution(authority, execution, openEntries)
|
|
1736
|
+
}
|
|
1737
|
+
}
|
|
1738
|
+
if (openEntries.length === 0) return
|
|
1739
|
+
const mailboxDrainId = runtime.state.nextMailboxDrainId
|
|
1740
|
+
const envelope = sealChannelMailboxTurnEnvelope({
|
|
1741
|
+
mailboxOrigin: 'UI',
|
|
1742
|
+
mailboxDrainId,
|
|
1743
|
+
stagedEntries: openEntries,
|
|
1744
|
+
sealed: false,
|
|
1745
|
+
})
|
|
1746
|
+
;(runtime.state as { nextMailboxDrainId: number }).nextMailboxDrainId = mailboxDrainId + 1
|
|
1747
|
+
closedMailboxTurns.push(envelope)
|
|
1748
|
+
},
|
|
1749
|
+
|
|
1750
|
+
drainNextMailboxTurn() {
|
|
1751
|
+
const envelope = closedMailboxTurns[mailboxDrainCursor]
|
|
1752
|
+
if (envelope === undefined) return
|
|
1753
|
+
mailboxDrainCursor += 1
|
|
1754
|
+
drainState = drainEnvelope(envelope, drainState)
|
|
1755
|
+
},
|
|
1756
|
+
|
|
1757
|
+
replayMailboxTurn(envelope) {
|
|
1758
|
+
// The replay lane keeps its own drain cell: a valid replay applies (dedupe-inert through the
|
|
1759
|
+
// batch controller), while a malformed candidate refuses before anything mutates.
|
|
1760
|
+
replayDrainState = drainEnvelope(envelope, replayDrainState)
|
|
1761
|
+
},
|
|
1762
|
+
|
|
1763
|
+
runReadyAggregateReactions() {
|
|
1764
|
+
const ready = targetBatchState.batches
|
|
1765
|
+
.filter(
|
|
1766
|
+
(batch) =>
|
|
1767
|
+
batch.stage === 'READY_REACTION_REGISTERED' ||
|
|
1768
|
+
batch.stage === 'SEALED_EMPTY_REACTION_REGISTERED',
|
|
1769
|
+
)
|
|
1770
|
+
.sort(
|
|
1771
|
+
(left, right) =>
|
|
1772
|
+
(left.aggregateReactionSequence ?? 0) - (right.aggregateReactionSequence ?? 0),
|
|
1773
|
+
)
|
|
1774
|
+
for (const batch of ready) {
|
|
1775
|
+
const reaction = advanceGlobalTargetBatch(targetBatchState, {
|
|
1776
|
+
kind: 'RUN_AGGREGATE_REACTION',
|
|
1777
|
+
batchId: batch.batchId,
|
|
1778
|
+
})
|
|
1779
|
+
targetBatchState = reaction.state
|
|
1780
|
+
if (
|
|
1781
|
+
!reaction.orderedEffects.some((effect) => effect.kind === 'DELIVER_LIVE_OWNER_ELEMENT')
|
|
1782
|
+
) {
|
|
1783
|
+
continue
|
|
1784
|
+
}
|
|
1785
|
+
elementDeliveries.push({
|
|
1786
|
+
batchId: batch.batchId,
|
|
1787
|
+
ownerDriverId: batch.ownerDriverId,
|
|
1788
|
+
ownerGeneration: batch.ownerGeneration,
|
|
1789
|
+
})
|
|
1790
|
+
const element = mountedElements.find(
|
|
1791
|
+
(candidate) =>
|
|
1792
|
+
candidate.ownerDriverId === batch.ownerDriverId &&
|
|
1793
|
+
candidate.ownerGeneration === batch.ownerGeneration,
|
|
1794
|
+
)
|
|
1795
|
+
const delivered = targetBatchState.batches.find(
|
|
1796
|
+
(candidate) => candidate.batchId === batch.batchId,
|
|
1797
|
+
)
|
|
1798
|
+
if (element === undefined || delivered === undefined || element.handle === null) continue
|
|
1799
|
+
for (const carrier of delivered.carriers) {
|
|
1800
|
+
if (!carrier.enqueued || carrier.pendingFrameActivityReleased) continue
|
|
1801
|
+
const authority = identities.find(
|
|
1802
|
+
(candidate) => candidate.identityId === carrier.identityId,
|
|
1803
|
+
)
|
|
1804
|
+
const value = carrierValues.get(carrier.carrierId)
|
|
1805
|
+
if (authority === undefined || value === undefined) continue
|
|
1806
|
+
// Enqueued carriers drain their tasks to the delegate host, then settle in order.
|
|
1807
|
+
recordCarrierDelivery({
|
|
1808
|
+
batchId: batch.batchId,
|
|
1809
|
+
carrierId: carrier.carrierId,
|
|
1810
|
+
authoredIndex: carrier.authoredIndex,
|
|
1811
|
+
identityId: carrier.identityId,
|
|
1812
|
+
key: carrier.key,
|
|
1813
|
+
taskSequence: carrier.taskSequence,
|
|
1814
|
+
})
|
|
1815
|
+
if (!delegateOwnsKey(element, carrier.key)) {
|
|
1816
|
+
writeDelegateValues(element, { [carrier.key]: value })
|
|
1817
|
+
}
|
|
1818
|
+
updateIdentity(authority, (current) => ({
|
|
1819
|
+
...current,
|
|
1820
|
+
pendingTasks: current.pendingTasks.filter(
|
|
1821
|
+
(task) => task.taskSequence !== carrier.taskSequence,
|
|
1822
|
+
),
|
|
1823
|
+
}))
|
|
1824
|
+
targetBatchState = advanceGlobalTargetBatch(targetBatchState, {
|
|
1825
|
+
kind: 'SETTLE_TARGET_BATCH_CARRIER',
|
|
1826
|
+
batchId: batch.batchId,
|
|
1827
|
+
carrierId: carrier.carrierId,
|
|
1828
|
+
identityId: carrier.identityId,
|
|
1829
|
+
taskSequence: carrier.taskSequence,
|
|
1830
|
+
}).state
|
|
1831
|
+
}
|
|
1832
|
+
}
|
|
1833
|
+
},
|
|
1834
|
+
|
|
1835
|
+
observe() {
|
|
1836
|
+
return {
|
|
1837
|
+
identities: identities.map((authority) => ({
|
|
1838
|
+
record: authority.record,
|
|
1839
|
+
host: authority.controller.observe(),
|
|
1840
|
+
publicControlSlot: readPublicControlSlot(authority),
|
|
1841
|
+
})),
|
|
1842
|
+
mountedElements: mountedElements.map((element) => ({
|
|
1843
|
+
adapter: element.adapter,
|
|
1844
|
+
handle: element.handle as ElementHandle,
|
|
1845
|
+
sinkRegistrations: identities.flatMap((authority) =>
|
|
1846
|
+
authority.record.sinks.filter(
|
|
1847
|
+
(sink) =>
|
|
1848
|
+
sink.driverId === element.ownerDriverId &&
|
|
1849
|
+
sink.handle === (element.handle as number),
|
|
1850
|
+
),
|
|
1851
|
+
),
|
|
1852
|
+
delegateCommittedValues: element.delegate.driver.committed(
|
|
1853
|
+
element.handle as ElementHandle,
|
|
1854
|
+
),
|
|
1855
|
+
})),
|
|
1856
|
+
runtimeState: runtime.state,
|
|
1857
|
+
targetBatchState,
|
|
1858
|
+
orderedEdges: [...orderedEdges],
|
|
1859
|
+
closedMailboxTurns: [...closedMailboxTurns],
|
|
1860
|
+
carrierDeliveries: [...carrierDeliveries],
|
|
1861
|
+
elementDeliveries: [...elementDeliveries],
|
|
1862
|
+
}
|
|
1863
|
+
},
|
|
1864
|
+
}
|
|
1865
|
+
|
|
1866
|
+
controllerRuntimeInternals.set(runtimeInterface, {
|
|
1867
|
+
findElementByAdapter(adapter) {
|
|
1868
|
+
return elements.find((candidate) => candidate.adapter === adapter) ?? null
|
|
1869
|
+
},
|
|
1870
|
+
ensureElement(ensureOptions) {
|
|
1871
|
+
const existing = elements.find((candidate) => candidate.adapter === ensureOptions.adapter)
|
|
1872
|
+
if (existing !== undefined) {
|
|
1873
|
+
// A reconcile-created element carries its already-registered handle straight into the
|
|
1874
|
+
// mounted observation; no second register crossing exists for it.
|
|
1875
|
+
if (existing.handle === null) existing.handle = ensureOptions.handle
|
|
1876
|
+
if (!mountedElements.includes(existing)) mountedElements.push(existing)
|
|
1877
|
+
return existing
|
|
1878
|
+
}
|
|
1879
|
+
const element: IdentityValueChannelElementAuthority = {
|
|
1880
|
+
adapter: ensureOptions.adapter,
|
|
1881
|
+
delegate: ensureOptions.delegate,
|
|
1882
|
+
ownerDriverId: ensureOptions.ownerDriverId,
|
|
1883
|
+
ownerGeneration: ensureOptions.ownerGeneration,
|
|
1884
|
+
eligibleKeys: [],
|
|
1885
|
+
resolvedStyleValues: {},
|
|
1886
|
+
identityBindings: new Map(),
|
|
1887
|
+
commandOwnedKeys: new Set(),
|
|
1888
|
+
delegateRefusesOwnedWrites: false,
|
|
1889
|
+
handle: ensureOptions.handle,
|
|
1890
|
+
frameValuesListener: null,
|
|
1891
|
+
mountSettled: false,
|
|
1892
|
+
}
|
|
1893
|
+
queueMicrotask(() => {
|
|
1894
|
+
element.mountSettled = true
|
|
1895
|
+
})
|
|
1896
|
+
elements.push(element)
|
|
1897
|
+
mountedElements.push(element)
|
|
1898
|
+
return element
|
|
1899
|
+
},
|
|
1900
|
+
detachIdentityValue: detachIdentityValueFromElement,
|
|
1901
|
+
attachIdentityValue: attachIdentityValueToElement,
|
|
1902
|
+
})
|
|
1903
|
+
return runtimeInterface
|
|
1904
|
+
}
|
|
1905
|
+
|
|
1906
|
+
const canonicalRuntimes = new WeakMap<ValueChannel, IdentityValueChannelControllerRuntime>()
|
|
1907
|
+
const ownerGenerationByDelegate = new WeakMap<WorkletDriverBinding, number>()
|
|
1908
|
+
|
|
1909
|
+
/** The one JS-global identity runtime for an exact public ValueChannel graph. */
|
|
1910
|
+
export function resolveIdentityValueChannelControllerRuntime(
|
|
1911
|
+
channel: ValueChannel,
|
|
1912
|
+
): IdentityValueChannelControllerRuntime {
|
|
1913
|
+
const existing = canonicalRuntimes.get(channel)
|
|
1914
|
+
if (existing !== undefined) return existing
|
|
1915
|
+
const runtime = createIdentityValueChannelControllerRuntime({
|
|
1916
|
+
channel,
|
|
1917
|
+
identityRegistry: createDriverValueChannelIdentityRegistry<number>(),
|
|
1918
|
+
})
|
|
1919
|
+
canonicalRuntimes.set(channel, runtime)
|
|
1920
|
+
return runtime
|
|
1921
|
+
}
|
|
1922
|
+
|
|
1923
|
+
/**
|
|
1924
|
+
* Production MotionView entrance. Legacy/derived/color bindings stay on their existing delegate;
|
|
1925
|
+
* an exact finite direct-public numeric value resolves the channel-global runtime and receives the
|
|
1926
|
+
* next engine-owned controller generation before `driver.register` can issue a command.
|
|
1927
|
+
*/
|
|
1928
|
+
export function attachIdentityValueChannelControllerElement(
|
|
1929
|
+
options: AttachIdentityValueChannelControllerElementOptions,
|
|
1930
|
+
): IdentityValueChannelControllerElementAdapter | null {
|
|
1931
|
+
const hasEligibleIdentity = Object.entries(options.stylePreparation.boundStyleValues).some(
|
|
1932
|
+
([key, value]) => isDirectPublicNumericMotionValue(value, options.resolvedStyleValues[key]),
|
|
1933
|
+
)
|
|
1934
|
+
if (!hasEligibleIdentity) return null
|
|
1935
|
+
|
|
1936
|
+
const ownerDriverId = identityValueChannelDriverIdForBinding(options.delegate)
|
|
1937
|
+
if (ownerDriverId === null) {
|
|
1938
|
+
throw new IdentityValueChannelControllerAdapterInvariantError(
|
|
1939
|
+
'UNREGISTERED_WORKLET_DRIVER_BINDING',
|
|
1940
|
+
)
|
|
1941
|
+
}
|
|
1942
|
+
const runtime = resolveIdentityValueChannelControllerRuntime(options.channel)
|
|
1943
|
+
const ownerGeneration = (ownerGenerationByDelegate.get(options.delegate) ?? 0) + 1
|
|
1944
|
+
ownerGenerationByDelegate.set(options.delegate, ownerGeneration)
|
|
1945
|
+
return runtime.bindingForElement({
|
|
1946
|
+
ownerDriverId,
|
|
1947
|
+
ownerGeneration,
|
|
1948
|
+
stylePreparation: options.stylePreparation,
|
|
1949
|
+
delegate: options.delegate,
|
|
1950
|
+
})
|
|
1951
|
+
}
|
|
1952
|
+
|
|
1953
|
+
/**
|
|
1954
|
+
* Production commit entrance for exact bound-value replacement and removal. The complete ordered
|
|
1955
|
+
* plan is consumed inside MotionView's first insertion effect — detach-before-attach per
|
|
1956
|
+
* replacement — before the same commit's controller update command may start through the key.
|
|
1957
|
+
*/
|
|
1958
|
+
export function reconcileIdentityValueChannelControllerElement(
|
|
1959
|
+
options: ReconcileIdentityValueChannelControllerElementOptions,
|
|
1960
|
+
): void {
|
|
1961
|
+
if (options.plan.changes.length === 0) return
|
|
1962
|
+
const runtime = resolveIdentityValueChannelControllerRuntime(options.channel)
|
|
1963
|
+
const internals = controllerRuntimeInternals.get(runtime)
|
|
1964
|
+
if (internals === undefined) {
|
|
1965
|
+
throw new IdentityValueChannelControllerAdapterInvariantError(
|
|
1966
|
+
'UNREGISTERED_WORKLET_DRIVER_BINDING',
|
|
1967
|
+
)
|
|
1968
|
+
}
|
|
1969
|
+
let adapter = options.adapter
|
|
1970
|
+
if (adapter === null) {
|
|
1971
|
+
// The mount had no eligible identity, so no adapter exists yet: the attach still lands, on
|
|
1972
|
+
// the next engine-owned generation for this exact delegate.
|
|
1973
|
+
const ownerDriverId = identityValueChannelDriverIdForBinding(options.delegate)
|
|
1974
|
+
if (ownerDriverId === null) {
|
|
1975
|
+
throw new IdentityValueChannelControllerAdapterInvariantError(
|
|
1976
|
+
'UNREGISTERED_WORKLET_DRIVER_BINDING',
|
|
1977
|
+
)
|
|
1978
|
+
}
|
|
1979
|
+
const ownerGeneration = (ownerGenerationByDelegate.get(options.delegate) ?? 0) + 1
|
|
1980
|
+
ownerGenerationByDelegate.set(options.delegate, ownerGeneration)
|
|
1981
|
+
adapter = runtime.bindingForElement({
|
|
1982
|
+
ownerDriverId,
|
|
1983
|
+
ownerGeneration,
|
|
1984
|
+
stylePreparation: EMPTY_MOUNT_STYLE_PREPARATION,
|
|
1985
|
+
delegate: options.delegate,
|
|
1986
|
+
})
|
|
1987
|
+
}
|
|
1988
|
+
const element = internals.ensureElement({
|
|
1989
|
+
adapter,
|
|
1990
|
+
delegate: options.delegate,
|
|
1991
|
+
ownerDriverId: adapter.ownerDriverId,
|
|
1992
|
+
ownerGeneration: adapter.ownerGeneration,
|
|
1993
|
+
handle: options.handle,
|
|
1994
|
+
})
|
|
1995
|
+
|
|
1996
|
+
for (const change of options.plan.changes) {
|
|
1997
|
+
if (change.kind === 'REPLACE_BOUND_VALUE') {
|
|
1998
|
+
const [detachEdge, attachEdge] = change.orderedEdges
|
|
1999
|
+
// Detach-then-attach in that order; the registry transition rides rebind for an exact
|
|
2000
|
+
// replacement, so the previous record and its successor never fork. An eligible edge is a
|
|
2001
|
+
// direct-public numeric identity by the plan's own admission.
|
|
2002
|
+
const previousSink = detachEdge.identityEligible
|
|
2003
|
+
? internals.detachIdentityValue(
|
|
2004
|
+
element,
|
|
2005
|
+
detachEdge.key,
|
|
2006
|
+
detachEdge.value as MotionValue<number>,
|
|
2007
|
+
false,
|
|
2008
|
+
)
|
|
2009
|
+
: null
|
|
2010
|
+
if (!attachEdge.identityEligible || typeof attachEdge.resolvedValue !== 'number') continue
|
|
2011
|
+
internals.attachIdentityValue(
|
|
2012
|
+
element,
|
|
2013
|
+
attachEdge.key,
|
|
2014
|
+
attachEdge.value as MotionValue<number>,
|
|
2015
|
+
attachEdge.resolvedValue,
|
|
2016
|
+
previousSink === null
|
|
2017
|
+
? null
|
|
2018
|
+
: { previousValue: detachEdge.value as MotionValue<number>, previousSink },
|
|
2019
|
+
)
|
|
2020
|
+
writeDelegateValues(element, { [attachEdge.key]: attachEdge.resolvedValue })
|
|
2021
|
+
continue
|
|
2022
|
+
}
|
|
2023
|
+
for (const edge of change.orderedEdges) {
|
|
2024
|
+
if (edge.kind === 'DETACH_BOUND_VALUE') {
|
|
2025
|
+
if (edge.identityEligible) {
|
|
2026
|
+
internals.detachIdentityValue(element, edge.key, edge.value as MotionValue<number>, true)
|
|
2027
|
+
}
|
|
2028
|
+
continue
|
|
2029
|
+
}
|
|
2030
|
+
if (!edge.identityEligible || typeof edge.resolvedValue !== 'number') continue
|
|
2031
|
+
internals.attachIdentityValue(
|
|
2032
|
+
element,
|
|
2033
|
+
edge.key,
|
|
2034
|
+
edge.value as MotionValue<number>,
|
|
2035
|
+
edge.resolvedValue,
|
|
2036
|
+
null,
|
|
2037
|
+
)
|
|
2038
|
+
// The rebind push law: the freshly attached source's live value writes through immediately,
|
|
2039
|
+
// so the host never shows the stale source for one commit.
|
|
2040
|
+
writeDelegateValues(element, { [edge.key]: edge.resolvedValue })
|
|
2041
|
+
}
|
|
2042
|
+
}
|
|
2043
|
+
}
|