@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,3288 @@
|
|
|
1
|
+
// Private JS-side value-channel handoff (REQ-VALUE-014/015, REQ-DRIVER-034): the exact-object
|
|
2
|
+
// identity registry and its retention reconciliation, the serializable handoff protocol machine
|
|
3
|
+
// (freeze → prepare → source-confirmed release/abort → immutable COMMIT → destination
|
|
4
|
+
// acknowledgement), tuple validation at every producer/consumer boundary, the JS destination
|
|
5
|
+
// COMMIT consumer with its preallocated acknowledgement slot, and the bounded post-RELEASE
|
|
6
|
+
// refusal drain. Every transition replaces state immutably; malformed, conflicting, or
|
|
7
|
+
// out-of-sequence input is refused with the file's typed vocabulary and never mutates retained
|
|
8
|
+
// protocol state.
|
|
9
|
+
|
|
10
|
+
import type { MotionValue } from '@unrulysystems/native-motion-core'
|
|
11
|
+
import type {
|
|
12
|
+
MotionValueDriverBindingEligibility,
|
|
13
|
+
MotionValueDriverPort,
|
|
14
|
+
MotionValueDriverPortObservation,
|
|
15
|
+
} from '@unrulysystems/native-motion-core/internal-driver'
|
|
16
|
+
|
|
17
|
+
export type ValueChannelSide = 'UI' | 'JS'
|
|
18
|
+
export type ValueHandoffDirection = 'UI_TO_JS' | 'JS_TO_UI'
|
|
19
|
+
|
|
20
|
+
export type ValueHandoffOwnership =
|
|
21
|
+
| 'IDLE'
|
|
22
|
+
| 'UI_RUNNING'
|
|
23
|
+
| 'UI_FROZEN_TO_JS'
|
|
24
|
+
| 'JS_PREPARED'
|
|
25
|
+
| 'UI_RELEASE_PENDING_TO_JS'
|
|
26
|
+
| 'UI_RELEASED_TO_JS'
|
|
27
|
+
| 'JS_RUNNING'
|
|
28
|
+
| 'JS_FROZEN_TO_UI'
|
|
29
|
+
| 'UI_PREPARED'
|
|
30
|
+
| 'JS_RELEASE_PENDING_TO_UI'
|
|
31
|
+
| 'JS_RELEASED_TO_UI'
|
|
32
|
+
| 'UI_DRAINING'
|
|
33
|
+
| 'JS_DRAINING'
|
|
34
|
+
| 'BOUNDED_REFUSAL_DRAINING'
|
|
35
|
+
| 'TERMINAL_REFUSED'
|
|
36
|
+
|
|
37
|
+
export interface ValueChannelHistoryState {
|
|
38
|
+
readonly currentValue: number
|
|
39
|
+
readonly previousValue: number
|
|
40
|
+
readonly currentTimestampMs: number
|
|
41
|
+
readonly previousTimestampMs: number
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface ValueChannelSequenceState {
|
|
45
|
+
readonly authoritativeSequence: number
|
|
46
|
+
readonly commandSequence: number
|
|
47
|
+
readonly writeSequence: number
|
|
48
|
+
readonly eventSequence: number
|
|
49
|
+
readonly frameSequence: number
|
|
50
|
+
readonly dueWorkSequence: number
|
|
51
|
+
readonly valueCompletionSequence: number
|
|
52
|
+
readonly batchDependencyEventSequence: number
|
|
53
|
+
readonly dependencyOutboxAckSequence: number
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export type NormalizedValueChannelEasingPlan =
|
|
57
|
+
| { readonly kind: 'LINEAR' }
|
|
58
|
+
| {
|
|
59
|
+
readonly kind: 'CUBIC_BEZIER'
|
|
60
|
+
readonly x1: number
|
|
61
|
+
readonly y1: number
|
|
62
|
+
readonly x2: number
|
|
63
|
+
readonly y2: number
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export type NormalizedValueChannelGeneratorPlan =
|
|
67
|
+
| {
|
|
68
|
+
readonly kind: 'SPRING'
|
|
69
|
+
readonly from: number
|
|
70
|
+
readonly target: number
|
|
71
|
+
readonly initialVelocity: number
|
|
72
|
+
readonly stiffness: number
|
|
73
|
+
readonly damping: number
|
|
74
|
+
readonly mass: number
|
|
75
|
+
readonly restSpeed: number
|
|
76
|
+
readonly restDelta: number
|
|
77
|
+
readonly delayMs: number
|
|
78
|
+
}
|
|
79
|
+
| {
|
|
80
|
+
readonly kind: 'TIMING'
|
|
81
|
+
readonly keyframes: readonly number[]
|
|
82
|
+
readonly times: readonly number[]
|
|
83
|
+
readonly easing: readonly NormalizedValueChannelEasingPlan[]
|
|
84
|
+
readonly durationMs: number
|
|
85
|
+
readonly delayMs: number
|
|
86
|
+
readonly repeatCount: number
|
|
87
|
+
readonly repeatType: 'loop' | 'reverse' | 'mirror'
|
|
88
|
+
readonly repeatDelayMs: number
|
|
89
|
+
}
|
|
90
|
+
| {
|
|
91
|
+
readonly kind: 'INSTANT'
|
|
92
|
+
readonly target: number
|
|
93
|
+
readonly delayMs: number
|
|
94
|
+
}
|
|
95
|
+
| {
|
|
96
|
+
readonly kind: 'INERTIA'
|
|
97
|
+
readonly from: number
|
|
98
|
+
readonly initialVelocity: number
|
|
99
|
+
readonly power: number
|
|
100
|
+
readonly timeConstant: number
|
|
101
|
+
readonly bounceStiffness: number
|
|
102
|
+
readonly bounceDamping: number
|
|
103
|
+
readonly restDelta: number
|
|
104
|
+
readonly restSpeed: number
|
|
105
|
+
readonly min: number | null
|
|
106
|
+
readonly max: number | null
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export interface NormalizedValueChannelWritePayload {
|
|
110
|
+
readonly writeSequence: number
|
|
111
|
+
readonly frameSequence: number
|
|
112
|
+
readonly value: number
|
|
113
|
+
readonly velocity: number
|
|
114
|
+
readonly previousValue: number
|
|
115
|
+
readonly previousTimestampMs: number
|
|
116
|
+
readonly currentTimestampMs: number
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export type NormalizedValueChannelTaskCallbackPayload =
|
|
120
|
+
| {
|
|
121
|
+
readonly callbackKind: 'transition-on-update' | 'transition-on-complete'
|
|
122
|
+
readonly callbackId: number
|
|
123
|
+
readonly eventSequence: number
|
|
124
|
+
readonly callbackDeliverySequence: number
|
|
125
|
+
}
|
|
126
|
+
| {
|
|
127
|
+
readonly callbackKind: 'animation-complete'
|
|
128
|
+
readonly callbackId: number
|
|
129
|
+
readonly eventSequence: number
|
|
130
|
+
readonly callbackDeliverySequence: number
|
|
131
|
+
readonly completionSequence: number
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export type NormalizedValueChannelCarrierValue =
|
|
135
|
+
| { readonly kind: 'number'; readonly value: number }
|
|
136
|
+
| { readonly kind: 'keyword'; readonly value: string }
|
|
137
|
+
|
|
138
|
+
export type ValueHandoffDestinationLedgerKind =
|
|
139
|
+
| 'PUBLIC_CONTROL'
|
|
140
|
+
| 'RETENTION'
|
|
141
|
+
| 'EXECUTION'
|
|
142
|
+
| 'DUE_WORK'
|
|
143
|
+
| 'COMPLETION'
|
|
144
|
+
| 'DEPENDENCY_EVENT'
|
|
145
|
+
| 'GENERATION'
|
|
146
|
+
| 'SEQUENCE'
|
|
147
|
+
|
|
148
|
+
interface ValueHandoffDestinationLedgerSlot<Ledger extends ValueHandoffDestinationLedgerKind> {
|
|
149
|
+
readonly ledger: Ledger
|
|
150
|
+
readonly slotId: number
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export interface ValueChannelHandoffResourceState {
|
|
154
|
+
readonly preparedStateRetained: boolean
|
|
155
|
+
readonly preparedCloneRetained: boolean
|
|
156
|
+
readonly transferActivityRetained: boolean
|
|
157
|
+
readonly transferTimerArmed: boolean
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export interface DriverValueChannelIdentityRecordState {
|
|
161
|
+
readonly channelId: number
|
|
162
|
+
readonly jsRecordId: number
|
|
163
|
+
readonly uiRecordId: number
|
|
164
|
+
readonly creationKind: 'direct-public-finite-number'
|
|
165
|
+
readonly publicReferenceRetained: boolean
|
|
166
|
+
readonly sinkCount: number
|
|
167
|
+
readonly changeObserverCount: number
|
|
168
|
+
readonly publicControlSlotId: number | null
|
|
169
|
+
readonly physicalExecutionIds: readonly number[]
|
|
170
|
+
readonly dueWorkSequences: readonly number[]
|
|
171
|
+
readonly valueCompletionSequences: readonly number[]
|
|
172
|
+
readonly targetBatchDependencyIds: readonly number[]
|
|
173
|
+
readonly dependencyEventOutboxSequences: readonly number[]
|
|
174
|
+
readonly dependencyEventAckWatermark: number
|
|
175
|
+
readonly sharedBatchReferenceCount: number
|
|
176
|
+
readonly pendingFrameActivity: boolean
|
|
177
|
+
readonly currentBindingGeneration: number
|
|
178
|
+
readonly retiredBindingGeneration: number
|
|
179
|
+
readonly directOperationEpoch: number
|
|
180
|
+
readonly executionLeaseEpoch: number
|
|
181
|
+
readonly history: ValueChannelHistoryState
|
|
182
|
+
readonly sequences: ValueChannelSequenceState
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export interface ValueHandoffPreparedMutableTuple {
|
|
186
|
+
readonly publicControlSlot:
|
|
187
|
+
| { readonly kind: 'empty' }
|
|
188
|
+
| {
|
|
189
|
+
readonly kind: 'installed'
|
|
190
|
+
readonly controlId: number
|
|
191
|
+
readonly executionId: number
|
|
192
|
+
readonly directOperationEpoch: number
|
|
193
|
+
}
|
|
194
|
+
readonly physicalExecutionIds: readonly number[]
|
|
195
|
+
readonly dueWorkSequences: readonly number[]
|
|
196
|
+
readonly valueCompletionSequences: readonly number[]
|
|
197
|
+
readonly targetBatchDependencyIds: readonly number[]
|
|
198
|
+
readonly dependencyEventOutboxSequences: readonly number[]
|
|
199
|
+
readonly currentBindingGeneration: number
|
|
200
|
+
readonly retiredBindingGeneration: number
|
|
201
|
+
readonly directOperationEpoch: number
|
|
202
|
+
readonly dueWorkSequence: number
|
|
203
|
+
readonly valueCompletionSequence: number
|
|
204
|
+
readonly batchDependencyEventSequence: number
|
|
205
|
+
readonly dependencyOutboxAckSequence: number
|
|
206
|
+
readonly authoritativeSequence: number
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export type NormalizedRetainedWorkEntry =
|
|
210
|
+
| {
|
|
211
|
+
readonly kind: 'public-control-slot'
|
|
212
|
+
readonly destinationSlot: ValueHandoffDestinationLedgerSlot<'PUBLIC_CONTROL'>
|
|
213
|
+
readonly authoritativeSequence: number
|
|
214
|
+
readonly state:
|
|
215
|
+
| { readonly kind: 'EMPTY' }
|
|
216
|
+
| {
|
|
217
|
+
readonly kind: 'INSTALLED'
|
|
218
|
+
readonly controlId: number
|
|
219
|
+
readonly executionId: number
|
|
220
|
+
readonly directOperationEpoch: number
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
| {
|
|
224
|
+
readonly kind: 'retention-edge'
|
|
225
|
+
readonly destinationSlot: ValueHandoffDestinationLedgerSlot<'RETENTION'>
|
|
226
|
+
readonly authoritativeSequence: number
|
|
227
|
+
readonly registrationKind: 'SINK' | 'CHANGE_OBSERVER' | 'PUBLIC_REFERENCE'
|
|
228
|
+
readonly registrationId: number
|
|
229
|
+
readonly bindingGeneration: number
|
|
230
|
+
readonly edge: 'ATTACH' | 'DETACH'
|
|
231
|
+
}
|
|
232
|
+
| {
|
|
233
|
+
readonly kind: 'generator-execution'
|
|
234
|
+
readonly destinationSlot: ValueHandoffDestinationLedgerSlot<'EXECUTION'>
|
|
235
|
+
readonly authoritativeSequence: number
|
|
236
|
+
readonly executionId: number
|
|
237
|
+
readonly controlId: number
|
|
238
|
+
readonly directOperationEpoch: number
|
|
239
|
+
readonly executionLease: number
|
|
240
|
+
readonly plan: NormalizedValueChannelGeneratorPlan
|
|
241
|
+
readonly elapsedMs: number
|
|
242
|
+
readonly sample: NormalizedValueChannelWritePayload & { readonly done: boolean }
|
|
243
|
+
readonly phase: 'RUNNING' | 'DONE_DECIDED' | 'STOPPED'
|
|
244
|
+
readonly orphaned: boolean
|
|
245
|
+
}
|
|
246
|
+
| {
|
|
247
|
+
readonly kind: 'update-task'
|
|
248
|
+
readonly destinationSlot: ValueHandoffDestinationLedgerSlot<'DUE_WORK'>
|
|
249
|
+
readonly authoritativeSequence: number
|
|
250
|
+
readonly taskId: number
|
|
251
|
+
readonly taskSequence: number
|
|
252
|
+
readonly taskClass: 'instant' | 'equal' | 'idle-direct-write'
|
|
253
|
+
readonly state: 'QUEUED' | 'WRITE_RESOLVED' | 'CALLBACKS_PENDING'
|
|
254
|
+
readonly write: NormalizedValueChannelWritePayload
|
|
255
|
+
readonly callbacks: readonly NormalizedValueChannelTaskCallbackPayload[]
|
|
256
|
+
}
|
|
257
|
+
| {
|
|
258
|
+
readonly kind: 'carrier-task'
|
|
259
|
+
readonly destinationSlot: ValueHandoffDestinationLedgerSlot<'DUE_WORK'>
|
|
260
|
+
readonly authoritativeSequence: number
|
|
261
|
+
readonly taskId: number
|
|
262
|
+
readonly taskSequence: number
|
|
263
|
+
readonly taskClass: 'transition-end-carrier' | 'exit-carrier' | 'packet-b-carrier'
|
|
264
|
+
readonly carrier: {
|
|
265
|
+
readonly carrierId: number
|
|
266
|
+
readonly identityId: number
|
|
267
|
+
readonly authoredIndex: number
|
|
268
|
+
readonly key: string
|
|
269
|
+
readonly value: NormalizedValueChannelCarrierValue
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
| {
|
|
273
|
+
readonly kind: 'direct-write'
|
|
274
|
+
readonly destinationSlot: ValueHandoffDestinationLedgerSlot<'DUE_WORK'>
|
|
275
|
+
readonly authoritativeSequence: number
|
|
276
|
+
readonly operation: 'SET' | 'JUMP'
|
|
277
|
+
readonly commandSequence: number
|
|
278
|
+
readonly directOperationEpoch: number
|
|
279
|
+
readonly write: NormalizedValueChannelWritePayload
|
|
280
|
+
}
|
|
281
|
+
| {
|
|
282
|
+
readonly kind: 'value-completion'
|
|
283
|
+
readonly destinationSlot: ValueHandoffDestinationLedgerSlot<'COMPLETION'>
|
|
284
|
+
readonly authoritativeSequence: number
|
|
285
|
+
readonly executionId: number
|
|
286
|
+
readonly completionSequence: number
|
|
287
|
+
readonly eventSequence: number
|
|
288
|
+
readonly bindingGeneration: number
|
|
289
|
+
readonly state: 'ARMED' | 'RESOLVED' | 'DELIVERED'
|
|
290
|
+
readonly transitionOnCompleteCallbackId: number | null
|
|
291
|
+
readonly lifecycleCallbackIds: readonly number[]
|
|
292
|
+
readonly publicStartPromiseId: number | null
|
|
293
|
+
readonly clearControlIdAtArm: number | null
|
|
294
|
+
}
|
|
295
|
+
| {
|
|
296
|
+
readonly kind: 'target-batch-dependency-event'
|
|
297
|
+
readonly destinationSlot: ValueHandoffDestinationLedgerSlot<'DEPENDENCY_EVENT'>
|
|
298
|
+
readonly authoritativeSequence: number
|
|
299
|
+
readonly batchId: number
|
|
300
|
+
readonly dependencyId: number
|
|
301
|
+
readonly identityId: number
|
|
302
|
+
readonly dependencyEventKind: 'RAW_RESOLVED' | 'VALUE_ACKNOWLEDGED' | 'PERMANENTLY_UNRESOLVED'
|
|
303
|
+
readonly eventSequence: number
|
|
304
|
+
readonly outboxSequence: number
|
|
305
|
+
readonly outboxAcknowledgementWatermark: number
|
|
306
|
+
readonly state: 'UNACKNOWLEDGED' | 'ACKNOWLEDGED'
|
|
307
|
+
}
|
|
308
|
+
| {
|
|
309
|
+
readonly kind: 'binding-generation'
|
|
310
|
+
readonly destinationSlot: ValueHandoffDestinationLedgerSlot<'GENERATION'>
|
|
311
|
+
readonly authoritativeSequence: number
|
|
312
|
+
readonly currentBindingGeneration: number
|
|
313
|
+
readonly retiredBindingGeneration: number
|
|
314
|
+
}
|
|
315
|
+
| {
|
|
316
|
+
readonly kind: 'sequence-state'
|
|
317
|
+
readonly destinationSlot: ValueHandoffDestinationLedgerSlot<'SEQUENCE'>
|
|
318
|
+
readonly authoritativeSequence: number
|
|
319
|
+
readonly sequences: ValueChannelSequenceState
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
export type ValueHandoffDestinationLedgerSlotState =
|
|
323
|
+
| {
|
|
324
|
+
readonly ledger: 'PUBLIC_CONTROL'
|
|
325
|
+
readonly slotId: number
|
|
326
|
+
readonly entries: readonly Extract<
|
|
327
|
+
NormalizedRetainedWorkEntry,
|
|
328
|
+
{ readonly kind: 'public-control-slot' }
|
|
329
|
+
>[]
|
|
330
|
+
}
|
|
331
|
+
| {
|
|
332
|
+
readonly ledger: 'RETENTION'
|
|
333
|
+
readonly slotId: number
|
|
334
|
+
readonly entries: readonly Extract<
|
|
335
|
+
NormalizedRetainedWorkEntry,
|
|
336
|
+
{ readonly kind: 'retention-edge' }
|
|
337
|
+
>[]
|
|
338
|
+
}
|
|
339
|
+
| {
|
|
340
|
+
readonly ledger: 'EXECUTION'
|
|
341
|
+
readonly slotId: number
|
|
342
|
+
readonly entries: readonly Extract<
|
|
343
|
+
NormalizedRetainedWorkEntry,
|
|
344
|
+
{ readonly kind: 'generator-execution' }
|
|
345
|
+
>[]
|
|
346
|
+
}
|
|
347
|
+
| {
|
|
348
|
+
readonly ledger: 'DUE_WORK'
|
|
349
|
+
readonly slotId: number
|
|
350
|
+
readonly entries: readonly Extract<
|
|
351
|
+
NormalizedRetainedWorkEntry,
|
|
352
|
+
{ readonly kind: 'update-task' | 'carrier-task' | 'direct-write' }
|
|
353
|
+
>[]
|
|
354
|
+
}
|
|
355
|
+
| {
|
|
356
|
+
readonly ledger: 'COMPLETION'
|
|
357
|
+
readonly slotId: number
|
|
358
|
+
readonly entries: readonly Extract<
|
|
359
|
+
NormalizedRetainedWorkEntry,
|
|
360
|
+
{ readonly kind: 'value-completion' }
|
|
361
|
+
>[]
|
|
362
|
+
}
|
|
363
|
+
| {
|
|
364
|
+
readonly ledger: 'DEPENDENCY_EVENT'
|
|
365
|
+
readonly slotId: number
|
|
366
|
+
readonly entries: readonly Extract<
|
|
367
|
+
NormalizedRetainedWorkEntry,
|
|
368
|
+
{ readonly kind: 'target-batch-dependency-event' }
|
|
369
|
+
>[]
|
|
370
|
+
}
|
|
371
|
+
| {
|
|
372
|
+
readonly ledger: 'GENERATION'
|
|
373
|
+
readonly slotId: number
|
|
374
|
+
readonly entries: readonly Extract<
|
|
375
|
+
NormalizedRetainedWorkEntry,
|
|
376
|
+
{ readonly kind: 'binding-generation' }
|
|
377
|
+
>[]
|
|
378
|
+
}
|
|
379
|
+
| {
|
|
380
|
+
readonly ledger: 'SEQUENCE'
|
|
381
|
+
readonly slotId: number
|
|
382
|
+
readonly entries: readonly Extract<
|
|
383
|
+
NormalizedRetainedWorkEntry,
|
|
384
|
+
{ readonly kind: 'sequence-state' }
|
|
385
|
+
>[]
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
export interface ValueHandoffDestinationLedgerState {
|
|
389
|
+
readonly slots: readonly ValueHandoffDestinationLedgerSlotState[]
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
export type ValueHandoffPhase = 'PREPARED' | 'RELEASE_PENDING_PRE_FENCE' | 'RELEASED_POST_FENCE'
|
|
393
|
+
|
|
394
|
+
interface ValueHandoffSequencedPhaseEvent {
|
|
395
|
+
readonly authoritativeSequence: number
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
export type ValueHandoffPhaseMutationEvent =
|
|
399
|
+
| (ValueHandoffSequencedPhaseEvent & {
|
|
400
|
+
readonly kind: 'sink-attach'
|
|
401
|
+
readonly sink: DriverValueChannelSinkRegistration
|
|
402
|
+
})
|
|
403
|
+
| (ValueHandoffSequencedPhaseEvent & {
|
|
404
|
+
readonly kind: 'sink-detach'
|
|
405
|
+
readonly sink: DriverValueChannelSinkRegistration
|
|
406
|
+
})
|
|
407
|
+
| (ValueHandoffSequencedPhaseEvent & {
|
|
408
|
+
readonly kind: 'observer-arm'
|
|
409
|
+
readonly observerId: number
|
|
410
|
+
})
|
|
411
|
+
| (ValueHandoffSequencedPhaseEvent & {
|
|
412
|
+
readonly kind: 'observer-disarm'
|
|
413
|
+
readonly observerId: number
|
|
414
|
+
})
|
|
415
|
+
| (ValueHandoffSequencedPhaseEvent & {
|
|
416
|
+
readonly kind: 'start'
|
|
417
|
+
readonly executionId: number
|
|
418
|
+
readonly controlId: number
|
|
419
|
+
readonly directOperationEpoch: number
|
|
420
|
+
})
|
|
421
|
+
| (ValueHandoffSequencedPhaseEvent & {
|
|
422
|
+
readonly kind: 'retarget'
|
|
423
|
+
readonly previousControlId: number
|
|
424
|
+
readonly executionId: number
|
|
425
|
+
readonly controlId: number
|
|
426
|
+
readonly directOperationEpoch: number
|
|
427
|
+
})
|
|
428
|
+
| (ValueHandoffSequencedPhaseEvent & {
|
|
429
|
+
readonly kind: 'set'
|
|
430
|
+
readonly value: number
|
|
431
|
+
readonly commandSequence: number
|
|
432
|
+
readonly directOperationEpoch: number
|
|
433
|
+
})
|
|
434
|
+
| (ValueHandoffSequencedPhaseEvent & {
|
|
435
|
+
readonly kind: 'jump'
|
|
436
|
+
readonly value: number
|
|
437
|
+
readonly commandSequence: number
|
|
438
|
+
readonly directOperationEpoch: number
|
|
439
|
+
})
|
|
440
|
+
| (ValueHandoffSequencedPhaseEvent & {
|
|
441
|
+
readonly kind: 'stop'
|
|
442
|
+
readonly controlId: number | null
|
|
443
|
+
readonly commandSequence: number
|
|
444
|
+
readonly directOperationEpoch: number
|
|
445
|
+
})
|
|
446
|
+
| (ValueHandoffSequencedPhaseEvent & {
|
|
447
|
+
readonly kind: 'destroy'
|
|
448
|
+
readonly retiredBindingGeneration: number
|
|
449
|
+
})
|
|
450
|
+
| (ValueHandoffSequencedPhaseEvent & {
|
|
451
|
+
readonly kind: 'public-slot'
|
|
452
|
+
readonly controlId: number | null
|
|
453
|
+
readonly executionId: number | null
|
|
454
|
+
readonly directOperationEpoch: number
|
|
455
|
+
})
|
|
456
|
+
| (ValueHandoffSequencedPhaseEvent & {
|
|
457
|
+
readonly kind: 'physical-sample'
|
|
458
|
+
readonly executionId: number
|
|
459
|
+
readonly writeSequence: number
|
|
460
|
+
readonly value: number
|
|
461
|
+
})
|
|
462
|
+
| (ValueHandoffSequencedPhaseEvent & {
|
|
463
|
+
readonly kind: 'physical-done'
|
|
464
|
+
readonly executionId: number
|
|
465
|
+
readonly writeSequence: number
|
|
466
|
+
readonly completionSequence: number
|
|
467
|
+
})
|
|
468
|
+
| (ValueHandoffSequencedPhaseEvent & {
|
|
469
|
+
readonly kind: 'completion-arm'
|
|
470
|
+
readonly executionId: number
|
|
471
|
+
readonly completionSequence: number
|
|
472
|
+
})
|
|
473
|
+
| (ValueHandoffSequencedPhaseEvent & {
|
|
474
|
+
readonly kind: 'completion-resolved'
|
|
475
|
+
readonly executionId: number
|
|
476
|
+
readonly completionSequence: number
|
|
477
|
+
readonly eventSequence: number
|
|
478
|
+
})
|
|
479
|
+
| (ValueHandoffSequencedPhaseEvent & {
|
|
480
|
+
readonly kind: 'batch-dependency'
|
|
481
|
+
readonly batchId: number
|
|
482
|
+
readonly dependencyId: number
|
|
483
|
+
readonly referenceDelta: -1 | 1
|
|
484
|
+
})
|
|
485
|
+
| (ValueHandoffSequencedPhaseEvent & {
|
|
486
|
+
readonly kind: 'batch-outbox'
|
|
487
|
+
readonly batchId: number
|
|
488
|
+
readonly dependencyId: number
|
|
489
|
+
readonly status: 'RAW_RESOLVED' | 'VALUE_ACKNOWLEDGED' | 'PERMANENTLY_UNRESOLVED'
|
|
490
|
+
readonly eventSequence: number
|
|
491
|
+
})
|
|
492
|
+
| (ValueHandoffSequencedPhaseEvent & {
|
|
493
|
+
readonly kind: 'batch-outbox-ack'
|
|
494
|
+
readonly acknowledgedThroughSequence: number
|
|
495
|
+
})
|
|
496
|
+
| (ValueHandoffSequencedPhaseEvent & {
|
|
497
|
+
readonly kind: 'fifo-task'
|
|
498
|
+
readonly taskSequence: number
|
|
499
|
+
readonly taskClass: 'instant' | 'equal' | 'carrier' | 'idle'
|
|
500
|
+
})
|
|
501
|
+
| (ValueHandoffSequencedPhaseEvent & {
|
|
502
|
+
readonly kind: 'binding-generation'
|
|
503
|
+
readonly currentBindingGeneration: number
|
|
504
|
+
readonly retiredBindingGeneration: number
|
|
505
|
+
})
|
|
506
|
+
|
|
507
|
+
export type ValueHandoffPhaseReadEvent =
|
|
508
|
+
| { readonly kind: 'synchronous-snapshot' }
|
|
509
|
+
| { readonly kind: 'synchronous-get' }
|
|
510
|
+
| { readonly kind: 'synchronous-get-velocity' }
|
|
511
|
+
|
|
512
|
+
export type ValueHandoffPhaseEvent = ValueHandoffPhaseMutationEvent | ValueHandoffPhaseReadEvent
|
|
513
|
+
|
|
514
|
+
export type ValueHandoffPhaseEventKind = ValueHandoffPhaseEvent['kind']
|
|
515
|
+
export type ValueHandoffPhaseEventOperation = `phase-event-${ValueHandoffPhaseEventKind}`
|
|
516
|
+
|
|
517
|
+
export interface ValueHandoffPhaseEventEnvelope {
|
|
518
|
+
readonly direction: ValueHandoffDirection
|
|
519
|
+
readonly transferLease: number
|
|
520
|
+
readonly phaseToken: number
|
|
521
|
+
readonly phase: ValueHandoffPhase
|
|
522
|
+
readonly event: ValueHandoffPhaseEvent
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
export type ValueHandoffPhaseReadView =
|
|
526
|
+
| 'AUTHORITATIVE_SOURCE'
|
|
527
|
+
| 'SOURCE_WITH_DIRECT_OPERATION_OVERLAY'
|
|
528
|
+
| 'PREPARED_DESTINATION_WITH_DIRECT_OPERATION_OVERLAY'
|
|
529
|
+
|
|
530
|
+
export type ValueHandoffPhaseEventDisposition =
|
|
531
|
+
| {
|
|
532
|
+
readonly kind: 'ABORT_AND_REPREPARE'
|
|
533
|
+
readonly phase: 'PREPARED' | 'RELEASE_PENDING_PRE_FENCE'
|
|
534
|
+
readonly eventKind: ValueHandoffPhaseMutationEvent['kind']
|
|
535
|
+
readonly status: Extract<ValueHandoffSourceStatus, { readonly kind: 'ABORTED' }>
|
|
536
|
+
}
|
|
537
|
+
| {
|
|
538
|
+
readonly kind: 'APPEND_IMMUTABLE_COMMIT_SUFFIX'
|
|
539
|
+
readonly phase: 'RELEASED_POST_FENCE'
|
|
540
|
+
readonly eventKind: ValueHandoffPhaseMutationEvent['kind']
|
|
541
|
+
readonly commit: ImmutableDestinationCommit
|
|
542
|
+
readonly authoredSuffix: readonly ValueHandoffPhaseMutationEvent[]
|
|
543
|
+
}
|
|
544
|
+
| {
|
|
545
|
+
readonly kind: 'SYNCHRONOUS_READ_WITHOUT_PROTOCOL_MUTATION'
|
|
546
|
+
readonly phase: ValueHandoffPhase
|
|
547
|
+
readonly eventKind: ValueHandoffPhaseReadEvent['kind']
|
|
548
|
+
readonly view: ValueHandoffPhaseReadView
|
|
549
|
+
readonly history: ValueChannelHistoryState
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
export type ValueHandoffProtocolCheckpoint =
|
|
553
|
+
| 'FREEZE'
|
|
554
|
+
| 'PREPARE_READY'
|
|
555
|
+
| 'RELEASE_PENDING_SOURCE_UNKNOWN'
|
|
556
|
+
| 'SOURCE_CONFIRMED_ABORTED'
|
|
557
|
+
| 'SOURCE_CONFIRMED_RELEASED'
|
|
558
|
+
| 'DESTINATION_COMMIT_CONSUMPTION'
|
|
559
|
+
| 'DESTINATION_ACKNOWLEDGED'
|
|
560
|
+
|
|
561
|
+
interface ValueHandoffProtocolRetainedWorkPayload {
|
|
562
|
+
readonly retainedWorkEntry: NormalizedRetainedWorkEntry
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
export type ValueHandoffProtocolMutationEvent =
|
|
566
|
+
| (ValueHandoffPhaseMutationEvent & ValueHandoffProtocolRetainedWorkPayload)
|
|
567
|
+
| (ValueHandoffSequencedPhaseEvent &
|
|
568
|
+
ValueHandoffProtocolRetainedWorkPayload & {
|
|
569
|
+
readonly kind: 'tokenless-task-enqueue'
|
|
570
|
+
readonly taskSequence: number
|
|
571
|
+
readonly taskClass: 'instant' | 'equal' | 'carrier' | 'idle'
|
|
572
|
+
})
|
|
573
|
+
| (ValueHandoffSequencedPhaseEvent &
|
|
574
|
+
ValueHandoffProtocolRetainedWorkPayload & {
|
|
575
|
+
readonly kind: 'lifecycle-enqueue'
|
|
576
|
+
readonly lifecycleSequence: number
|
|
577
|
+
readonly lifecycleKind: 'animationStart' | 'animationCancel' | 'animationComplete'
|
|
578
|
+
readonly bindingGeneration: number
|
|
579
|
+
})
|
|
580
|
+
|
|
581
|
+
export type ValueHandoffProtocolEvent =
|
|
582
|
+
| ValueHandoffProtocolMutationEvent
|
|
583
|
+
| ValueHandoffPhaseReadEvent
|
|
584
|
+
|
|
585
|
+
export type ValueHandoffProtocolEventKind = ValueHandoffProtocolEvent['kind']
|
|
586
|
+
export type ValueHandoffProtocolMutationEventKind = ValueHandoffProtocolMutationEvent['kind']
|
|
587
|
+
export type ValueHandoffProtocolCheckpointOperation =
|
|
588
|
+
`protocol-checkpoint-${ValueHandoffProtocolCheckpoint}-${ValueHandoffProtocolEventKind}`
|
|
589
|
+
|
|
590
|
+
export interface ValueHandoffProtocolCheckpointEnvelope {
|
|
591
|
+
readonly direction: ValueHandoffDirection
|
|
592
|
+
readonly transferLease: number
|
|
593
|
+
readonly phaseToken: number
|
|
594
|
+
readonly checkpoint: ValueHandoffProtocolCheckpoint
|
|
595
|
+
readonly event: ValueHandoffProtocolEvent
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
export type ValueHandoffProtocolMutationHandling =
|
|
599
|
+
| 'INVALIDATE_FREEZE_AND_REPREPARE_SOURCE'
|
|
600
|
+
| 'ABORT_PREPARE_AND_REPREPARE_SOURCE'
|
|
601
|
+
| 'SEQUENCE_WITHOUT_INFERRING_SOURCE_STATUS'
|
|
602
|
+
| 'APPLY_CONFIRMED_ABORT_AND_REPREPARE_SOURCE'
|
|
603
|
+
| 'APPEND_TO_IMMUTABLE_RELEASED_COMMIT'
|
|
604
|
+
| 'APPLY_IN_DESTINATION_COMMIT_PREFIX'
|
|
605
|
+
| 'APPLY_TO_ACKNOWLEDGED_DESTINATION_OR_REVERSE'
|
|
606
|
+
|
|
607
|
+
export type ValueHandoffProtocolReadAuthority =
|
|
608
|
+
| 'AUTHORITATIVE_SOURCE'
|
|
609
|
+
| 'SOURCE_WITH_DIRECT_OPERATION_OVERLAY'
|
|
610
|
+
| 'PREPARED_DESTINATION_WITH_DIRECT_OPERATION_OVERLAY'
|
|
611
|
+
| 'ACKNOWLEDGED_DESTINATION'
|
|
612
|
+
|
|
613
|
+
export type ValueHandoffProtocolRetainedWorkDisposition =
|
|
614
|
+
| {
|
|
615
|
+
readonly kind: 'CARRIED_IN_INVALIDATED_FREEZE_REPREPARE'
|
|
616
|
+
readonly carriedEntry: NormalizedRetainedWorkEntry
|
|
617
|
+
}
|
|
618
|
+
| {
|
|
619
|
+
readonly kind: 'CARRIED_IN_ABORTED_PREPARE_REPREPARE'
|
|
620
|
+
readonly carriedEntry: NormalizedRetainedWorkEntry
|
|
621
|
+
}
|
|
622
|
+
| {
|
|
623
|
+
readonly kind: 'QUEUED_BEHIND_SOURCE_DECISION'
|
|
624
|
+
readonly queuedEntry: NormalizedRetainedWorkEntry
|
|
625
|
+
}
|
|
626
|
+
| {
|
|
627
|
+
readonly kind: 'APPLIED_TO_CONFIRMED_SOURCE_BEFORE_REPREPARE'
|
|
628
|
+
readonly appliedEntry: NormalizedRetainedWorkEntry
|
|
629
|
+
}
|
|
630
|
+
| {
|
|
631
|
+
readonly kind: 'APPENDED_TO_IMMUTABLE_RELEASED_COMMIT'
|
|
632
|
+
readonly appendedEntry: NormalizedRetainedWorkEntry
|
|
633
|
+
}
|
|
634
|
+
| {
|
|
635
|
+
readonly kind: 'APPLIED_TO_PREFILLED_DESTINATION_SLOT'
|
|
636
|
+
readonly destinationSlot: NormalizedRetainedWorkEntry['destinationSlot']
|
|
637
|
+
readonly appliedEntry: NormalizedRetainedWorkEntry
|
|
638
|
+
}
|
|
639
|
+
| {
|
|
640
|
+
readonly kind: 'APPLIED_TO_ACKNOWLEDGED_DESTINATION_OR_REVERSE'
|
|
641
|
+
readonly destinationSlot: NormalizedRetainedWorkEntry['destinationSlot']
|
|
642
|
+
readonly appliedEntry: NormalizedRetainedWorkEntry
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
export type ValueHandoffProtocolCheckpointDisposition =
|
|
646
|
+
| {
|
|
647
|
+
readonly kind: 'MUTATION'
|
|
648
|
+
readonly checkpoint: ValueHandoffProtocolCheckpoint
|
|
649
|
+
readonly eventKind: ValueHandoffProtocolMutationEvent['kind']
|
|
650
|
+
readonly operation: ValueHandoffProtocolMutationEvent
|
|
651
|
+
readonly handling: ValueHandoffProtocolMutationHandling
|
|
652
|
+
readonly retainedWorkDisposition: ValueHandoffProtocolRetainedWorkDisposition
|
|
653
|
+
readonly absoluteTransferDeadlineMs: number
|
|
654
|
+
readonly absoluteNoConsumerDeadlineMs: number
|
|
655
|
+
readonly resources: ValueChannelHandoffResourceState
|
|
656
|
+
}
|
|
657
|
+
| {
|
|
658
|
+
readonly kind: 'SYNCHRONOUS_READ_WITHOUT_PROTOCOL_MUTATION'
|
|
659
|
+
readonly checkpoint: ValueHandoffProtocolCheckpoint
|
|
660
|
+
readonly eventKind: ValueHandoffPhaseReadEvent['kind']
|
|
661
|
+
readonly authority: ValueHandoffProtocolReadAuthority
|
|
662
|
+
readonly history: ValueChannelHistoryState
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
export interface ValueHandoffProtocolCheckpointTransition {
|
|
666
|
+
readonly state: DriverValueChannelState
|
|
667
|
+
readonly observation: DriverValueChannelObservation
|
|
668
|
+
readonly disposition: ValueHandoffProtocolCheckpointDisposition
|
|
669
|
+
readonly orderedEffects: readonly ValueHandoffEffect[]
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
export interface ImmutableDestinationCommit {
|
|
673
|
+
readonly kind: 'DESTINATION_COMMIT'
|
|
674
|
+
readonly channelId: number
|
|
675
|
+
readonly sourceRecordId: number
|
|
676
|
+
readonly destinationRecordId: number
|
|
677
|
+
readonly direction: ValueHandoffDirection
|
|
678
|
+
readonly transferLease: number
|
|
679
|
+
readonly releaseFenceSequence: number
|
|
680
|
+
readonly sentThroughAuthoritativeSequence: number
|
|
681
|
+
readonly includedPostFencePrefix: readonly NormalizedRetainedWorkEntry[]
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
export interface DestinationCommitAcknowledgement {
|
|
685
|
+
readonly kind: 'DESTINATION_COMMIT_ACKNOWLEDGEMENT'
|
|
686
|
+
readonly channelId: number
|
|
687
|
+
readonly sourceRecordId: number
|
|
688
|
+
readonly destinationRecordId: number
|
|
689
|
+
readonly direction: ValueHandoffDirection
|
|
690
|
+
readonly transferLease: number
|
|
691
|
+
readonly releaseFenceSequence: number
|
|
692
|
+
readonly drainedThroughAuthoritativeSequence: number
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
export type JsDestinationRoot =
|
|
696
|
+
| 'js-local-graph-frame'
|
|
697
|
+
| 'js-fallback-frame'
|
|
698
|
+
| 'js-pending-update'
|
|
699
|
+
| 'js-orphan-drain-frame'
|
|
700
|
+
|
|
701
|
+
export type DestinationRuntime =
|
|
702
|
+
| { readonly kind: 'live-ui'; readonly driverId: number }
|
|
703
|
+
| { readonly kind: 'manual-ui'; readonly manualFrameGroupId: number }
|
|
704
|
+
| { readonly kind: 'js'; readonly root: JsDestinationRoot }
|
|
705
|
+
|
|
706
|
+
export interface JsPreparedDestinationRecordState {
|
|
707
|
+
readonly destinationRecordId: number
|
|
708
|
+
readonly channelId: number
|
|
709
|
+
readonly sourceRecordId: number
|
|
710
|
+
readonly direction: ValueHandoffDirection
|
|
711
|
+
readonly transferLease: number
|
|
712
|
+
readonly releaseFenceSequence: number
|
|
713
|
+
readonly adoptedCommitSlotId: number | null
|
|
714
|
+
readonly adoptedSentThroughAuthoritativeSequence: number
|
|
715
|
+
readonly appliedThroughAuthoritativeSequence: number
|
|
716
|
+
readonly ledgers: ValueHandoffDestinationLedgerState
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
export interface JsDestinationCommitAcknowledgementSlotState {
|
|
720
|
+
readonly acknowledgementSlotId: number
|
|
721
|
+
readonly commitSlotId: number
|
|
722
|
+
readonly state: 'EMPTY' | 'FILLED' | 'STAGED_IN_CURRENT_ENVELOPE'
|
|
723
|
+
readonly acknowledgement: DestinationCommitAcknowledgement | null
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
export type JsDestinationCommitEnvelopeEntry =
|
|
727
|
+
| {
|
|
728
|
+
readonly kind: 'DESTINATION_COMMIT_ACKNOWLEDGEMENT'
|
|
729
|
+
readonly acknowledgementSlotId: number
|
|
730
|
+
readonly acknowledgement: DestinationCommitAcknowledgement
|
|
731
|
+
}
|
|
732
|
+
| {
|
|
733
|
+
readonly kind: 'ROOT_SIBLING_DUE_WORK'
|
|
734
|
+
readonly dueWorkSequence: number
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
export interface JsDestinationCommitEnvelopeState {
|
|
738
|
+
readonly root: JsDestinationRoot
|
|
739
|
+
readonly mailboxDrainId: number
|
|
740
|
+
readonly state: 'NOT_BEGUN' | 'OPEN' | 'SEALED'
|
|
741
|
+
readonly beginCount: number
|
|
742
|
+
readonly entries: readonly JsDestinationCommitEnvelopeEntry[]
|
|
743
|
+
readonly sealCount: number
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
export interface JsDestinationCommitConsumeState {
|
|
747
|
+
readonly preparedDestinationRecords: readonly JsPreparedDestinationRecordState[]
|
|
748
|
+
readonly acknowledgementSlots: readonly JsDestinationCommitAcknowledgementSlotState[]
|
|
749
|
+
readonly envelope: JsDestinationCommitEnvelopeState
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
export interface JsDestinationCommitConsumeRequest {
|
|
753
|
+
readonly root: JsDestinationRoot
|
|
754
|
+
readonly commitSlotId: number
|
|
755
|
+
readonly acknowledgementSlotId: number
|
|
756
|
+
readonly commit: ImmutableDestinationCommit
|
|
757
|
+
readonly siblingDueWorkSequences: readonly number[]
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
export type JsDestinationCommitRefusalReason =
|
|
761
|
+
| 'root-mismatch'
|
|
762
|
+
| 'envelope-already-begun'
|
|
763
|
+
| 'missing-prepared-destination-record'
|
|
764
|
+
| 'missing-acknowledgement-slot'
|
|
765
|
+
| 'wrong-channel'
|
|
766
|
+
| 'wrong-source-record'
|
|
767
|
+
| 'wrong-destination-record'
|
|
768
|
+
| 'wrong-direction'
|
|
769
|
+
| 'wrong-transfer-lease'
|
|
770
|
+
| 'wrong-release-fence'
|
|
771
|
+
| 'ahead-of-sent-watermark'
|
|
772
|
+
| 'sent-watermark-regressed'
|
|
773
|
+
| 'prefix-not-contiguous'
|
|
774
|
+
|
|
775
|
+
export type JsDestinationCommitConsumeOutcome =
|
|
776
|
+
| {
|
|
777
|
+
readonly kind: 'APPLIED'
|
|
778
|
+
readonly commitSlotId: number
|
|
779
|
+
readonly acknowledgement: DestinationCommitAcknowledgement
|
|
780
|
+
}
|
|
781
|
+
| {
|
|
782
|
+
readonly kind: 'DUPLICATE_REACKNOWLEDGED'
|
|
783
|
+
readonly commitSlotId: number
|
|
784
|
+
readonly acknowledgement: DestinationCommitAcknowledgement
|
|
785
|
+
}
|
|
786
|
+
| {
|
|
787
|
+
readonly kind: 'REFUSED'
|
|
788
|
+
readonly commitSlotId: number
|
|
789
|
+
readonly reason: JsDestinationCommitRefusalReason
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
export type JsDestinationCommitEffect =
|
|
793
|
+
| { readonly kind: 'begin-js-destination-envelope'; readonly root: JsDestinationRoot }
|
|
794
|
+
| {
|
|
795
|
+
readonly kind: 'lookup-js-prepared-destination-record'
|
|
796
|
+
readonly destinationRecordId: number
|
|
797
|
+
}
|
|
798
|
+
| {
|
|
799
|
+
readonly kind: 'validate-js-immutable-destination-commit'
|
|
800
|
+
readonly commitSlotId: number
|
|
801
|
+
}
|
|
802
|
+
| {
|
|
803
|
+
readonly kind: 'adopt-js-immutable-destination-commit'
|
|
804
|
+
readonly commitSlotId: number
|
|
805
|
+
}
|
|
806
|
+
| {
|
|
807
|
+
readonly kind: 'apply-js-destination-ledger-entry-in-place'
|
|
808
|
+
readonly commitSlotId: number
|
|
809
|
+
readonly prefixIndex: number
|
|
810
|
+
readonly destinationSlot: NormalizedRetainedWorkEntry['destinationSlot']
|
|
811
|
+
readonly entry: NormalizedRetainedWorkEntry
|
|
812
|
+
}
|
|
813
|
+
| {
|
|
814
|
+
readonly kind: 'dedupe-js-destination-commit-application'
|
|
815
|
+
readonly commitSlotId: number
|
|
816
|
+
readonly drainedThroughAuthoritativeSequence: number
|
|
817
|
+
}
|
|
818
|
+
| {
|
|
819
|
+
readonly kind: 'fill-js-destination-acknowledgement-slot'
|
|
820
|
+
readonly commitSlotId: number
|
|
821
|
+
readonly acknowledgementSlotId: number
|
|
822
|
+
}
|
|
823
|
+
| {
|
|
824
|
+
readonly kind: 'stage-js-destination-acknowledgement-in-current-envelope'
|
|
825
|
+
readonly acknowledgementSlotId: number
|
|
826
|
+
readonly mailboxDrainId: number
|
|
827
|
+
}
|
|
828
|
+
| { readonly kind: 'run-js-root-sibling-due-work'; readonly dueWorkSequence: number }
|
|
829
|
+
| {
|
|
830
|
+
readonly kind: 'refuse-malformed-js-destination-commit'
|
|
831
|
+
readonly commitSlotId: number
|
|
832
|
+
readonly reason: JsDestinationCommitRefusalReason
|
|
833
|
+
}
|
|
834
|
+
| {
|
|
835
|
+
readonly kind: 'seal-js-destination-envelope-once'
|
|
836
|
+
readonly mailboxDrainId: number
|
|
837
|
+
readonly entryCount: number
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
export interface JsDestinationCommitTransition {
|
|
841
|
+
readonly state: JsDestinationCommitConsumeState
|
|
842
|
+
readonly outcome: JsDestinationCommitConsumeOutcome
|
|
843
|
+
readonly orderedEffects: readonly JsDestinationCommitEffect[]
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
export interface ValueHandoffProtocolState {
|
|
847
|
+
readonly ownership: ValueHandoffOwnership
|
|
848
|
+
readonly desiredSide: ValueChannelSide | null
|
|
849
|
+
readonly direction: ValueHandoffDirection | null
|
|
850
|
+
readonly transferLease: number
|
|
851
|
+
readonly phaseToken: number
|
|
852
|
+
readonly sourceRecordId: number | null
|
|
853
|
+
readonly destinationRecordId: number | null
|
|
854
|
+
readonly absoluteTransferDeadlineMs: number | null
|
|
855
|
+
readonly absoluteNoConsumerDeadlineMs: number | null
|
|
856
|
+
readonly releaseFenceSequence: number | null
|
|
857
|
+
readonly sentThroughAuthoritativeSequence: number
|
|
858
|
+
readonly drainedThroughAuthoritativeSequence: number
|
|
859
|
+
readonly preparedMutableTuple: ValueHandoffPreparedMutableTuple | null
|
|
860
|
+
readonly destinationRuntime: DestinationRuntime | null
|
|
861
|
+
readonly sourceMutationQueue: readonly NormalizedRetainedWorkEntry[]
|
|
862
|
+
readonly commitQueue: readonly ImmutableDestinationCommit[]
|
|
863
|
+
readonly resources: ValueChannelHandoffResourceState
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
export interface DriverValueChannelState {
|
|
867
|
+
readonly identity: DriverValueChannelIdentityRecordState
|
|
868
|
+
readonly handoff: ValueHandoffProtocolState
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
interface ValueHandoffCommitLossRetainedWorkCommon {
|
|
872
|
+
readonly workSequence: number
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
export type ValueHandoffCommitLossRetainedWork =
|
|
876
|
+
| (ValueHandoffCommitLossRetainedWorkCommon & {
|
|
877
|
+
readonly kind: 'cache-adoption'
|
|
878
|
+
readonly authoritativeSequence: number
|
|
879
|
+
readonly value: number
|
|
880
|
+
})
|
|
881
|
+
| (ValueHandoffCommitLossRetainedWorkCommon & {
|
|
882
|
+
readonly kind: 'transition-on-update'
|
|
883
|
+
readonly callbackId: number
|
|
884
|
+
readonly value: number
|
|
885
|
+
})
|
|
886
|
+
| (ValueHandoffCommitLossRetainedWorkCommon & {
|
|
887
|
+
readonly kind: 'transition-on-complete'
|
|
888
|
+
readonly callbackId: number
|
|
889
|
+
})
|
|
890
|
+
| (ValueHandoffCommitLossRetainedWorkCommon & {
|
|
891
|
+
readonly kind: 'current-animation-complete'
|
|
892
|
+
readonly listenerId: number
|
|
893
|
+
readonly bindingGeneration: number
|
|
894
|
+
})
|
|
895
|
+
| (ValueHandoffCommitLossRetainedWorkCommon & {
|
|
896
|
+
readonly kind: 'token-blind-public-slot-clear'
|
|
897
|
+
readonly initiallyInstalledControlId: number
|
|
898
|
+
readonly installedControlIdAtClear: number | null
|
|
899
|
+
})
|
|
900
|
+
| (ValueHandoffCommitLossRetainedWorkCommon & {
|
|
901
|
+
readonly kind: 'public-start-promise-resolution'
|
|
902
|
+
readonly promiseId: number
|
|
903
|
+
})
|
|
904
|
+
| (ValueHandoffCommitLossRetainedWorkCommon & {
|
|
905
|
+
readonly kind: 'target-batch-dependency-delivery'
|
|
906
|
+
readonly batchId: number
|
|
907
|
+
readonly dependencyId: number
|
|
908
|
+
readonly eventSequence: number
|
|
909
|
+
})
|
|
910
|
+
| (ValueHandoffCommitLossRetainedWorkCommon & {
|
|
911
|
+
readonly kind: 'target-batch-dependency-acknowledgement'
|
|
912
|
+
readonly batchId: number
|
|
913
|
+
readonly dependencyId: number
|
|
914
|
+
readonly acknowledgedEventSequence: number
|
|
915
|
+
})
|
|
916
|
+
| (ValueHandoffCommitLossRetainedWorkCommon & {
|
|
917
|
+
readonly kind: 'settle-release'
|
|
918
|
+
readonly settleEntryId: number
|
|
919
|
+
})
|
|
920
|
+
|
|
921
|
+
export interface ValueHandoffCommitLossCleanupCounts {
|
|
922
|
+
readonly transferTimerCancellationCount: number
|
|
923
|
+
readonly preparedStateReleaseCount: number
|
|
924
|
+
readonly preparedCloneReleaseCount: number
|
|
925
|
+
readonly transferActivityReleaseCount: number
|
|
926
|
+
readonly terminalFinalizationCount: number
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
export interface ValueHandoffCommitLossDrainState {
|
|
930
|
+
readonly channel: DriverValueChannelState
|
|
931
|
+
readonly retainedWork: readonly ValueHandoffCommitLossRetainedWork[]
|
|
932
|
+
readonly drainedWorkSequences: readonly number[]
|
|
933
|
+
readonly retiredListenerIds: readonly number[]
|
|
934
|
+
readonly retiredSinkRegistrationIds: readonly number[]
|
|
935
|
+
readonly cleanupCounts: ValueHandoffCommitLossCleanupCounts
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
export interface ValueHandoffCommitLossDrainObservation {
|
|
939
|
+
readonly remainingRetainedWorkCount: number
|
|
940
|
+
readonly drainedWorkCount: number
|
|
941
|
+
readonly retiredListenerDeliveryCount: number
|
|
942
|
+
readonly retiredSinkPublicationCount: number
|
|
943
|
+
readonly hostOnUpdatePublicationCount: number
|
|
944
|
+
readonly dependencyAcknowledgementCount: number
|
|
945
|
+
readonly settleReleaseCount: number
|
|
946
|
+
readonly destinationRunPermitCount: number
|
|
947
|
+
readonly sourceRevivalCount: number
|
|
948
|
+
readonly reverseTransferCount: number
|
|
949
|
+
readonly cleanupCounts: ValueHandoffCommitLossCleanupCounts
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
export type ValueHandoffCommitLossDrainEffect =
|
|
953
|
+
| {
|
|
954
|
+
readonly kind: 'adopt-bounded-destination-cache'
|
|
955
|
+
readonly workSequence: number
|
|
956
|
+
readonly authoritativeSequence: number
|
|
957
|
+
readonly value: number
|
|
958
|
+
}
|
|
959
|
+
| {
|
|
960
|
+
readonly kind: 'deliver-bounded-transition-on-update'
|
|
961
|
+
readonly workSequence: number
|
|
962
|
+
readonly callbackId: number
|
|
963
|
+
readonly value: number
|
|
964
|
+
}
|
|
965
|
+
| {
|
|
966
|
+
readonly kind: 'deliver-bounded-transition-on-complete'
|
|
967
|
+
readonly workSequence: number
|
|
968
|
+
readonly callbackId: number
|
|
969
|
+
}
|
|
970
|
+
| {
|
|
971
|
+
readonly kind: 'deliver-current-animation-complete'
|
|
972
|
+
readonly workSequence: number
|
|
973
|
+
readonly listenerId: number
|
|
974
|
+
readonly bindingGeneration: number
|
|
975
|
+
}
|
|
976
|
+
| {
|
|
977
|
+
readonly kind: 'clear-public-control-slot-token-blind'
|
|
978
|
+
readonly workSequence: number
|
|
979
|
+
readonly initiallyInstalledControlId: number
|
|
980
|
+
readonly installedControlIdAtClear: number | null
|
|
981
|
+
}
|
|
982
|
+
| {
|
|
983
|
+
readonly kind: 'resolve-public-start-promise'
|
|
984
|
+
readonly workSequence: number
|
|
985
|
+
readonly promiseId: number
|
|
986
|
+
}
|
|
987
|
+
| {
|
|
988
|
+
readonly kind: 'deliver-target-batch-dependency-event'
|
|
989
|
+
readonly workSequence: number
|
|
990
|
+
readonly batchId: number
|
|
991
|
+
readonly dependencyId: number
|
|
992
|
+
readonly eventSequence: number
|
|
993
|
+
}
|
|
994
|
+
| {
|
|
995
|
+
readonly kind: 'acknowledge-target-batch-dependency-event'
|
|
996
|
+
readonly workSequence: number
|
|
997
|
+
readonly batchId: number
|
|
998
|
+
readonly dependencyId: number
|
|
999
|
+
readonly acknowledgedEventSequence: number
|
|
1000
|
+
}
|
|
1001
|
+
| {
|
|
1002
|
+
readonly kind: 'release-settle-entry'
|
|
1003
|
+
readonly workSequence: number
|
|
1004
|
+
readonly settleEntryId: number
|
|
1005
|
+
}
|
|
1006
|
+
| { readonly kind: 'cancel-transfer-timer-once' }
|
|
1007
|
+
| { readonly kind: 'release-prepared-state-once' }
|
|
1008
|
+
| { readonly kind: 'release-prepared-clone-once' }
|
|
1009
|
+
| { readonly kind: 'release-transfer-activity-once' }
|
|
1010
|
+
| { readonly kind: 'finalize-bounded-refusal-without-run-or-reverse-once' }
|
|
1011
|
+
|
|
1012
|
+
export interface ValueHandoffCommitLossDrainTransition {
|
|
1013
|
+
readonly state: ValueHandoffCommitLossDrainState
|
|
1014
|
+
readonly observation: ValueHandoffCommitLossDrainObservation
|
|
1015
|
+
readonly orderedEffects: readonly ValueHandoffCommitLossDrainEffect[]
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
export interface DriverValueChannelSinkRegistration {
|
|
1019
|
+
readonly driverId: number
|
|
1020
|
+
readonly handle: number
|
|
1021
|
+
readonly key: string
|
|
1022
|
+
readonly bindingGeneration: number
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
/**
|
|
1026
|
+
* Exact-object registry record. The public reference and private CORE port are intentionally
|
|
1027
|
+
* available only on this private seam; primitive observation below never exposes either object.
|
|
1028
|
+
* Cross-identity target-batch authority is deliberately absent from this shape.
|
|
1029
|
+
*/
|
|
1030
|
+
export interface DriverValueChannelIdentityRegistryRecord<T> {
|
|
1031
|
+
readonly value: MotionValue<T>
|
|
1032
|
+
readonly port: MotionValueDriverPort<T>
|
|
1033
|
+
readonly state: DriverValueChannelState
|
|
1034
|
+
readonly sinks: readonly DriverValueChannelSinkRegistration[]
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
export interface DriverValueChannelResolveBindingRequest<T> {
|
|
1038
|
+
readonly value: MotionValue<T>
|
|
1039
|
+
readonly port: MotionValueDriverPort<T>
|
|
1040
|
+
readonly currentValue: unknown
|
|
1041
|
+
readonly requestedState: DriverValueChannelState
|
|
1042
|
+
readonly sink: DriverValueChannelSinkRegistration
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
export type DriverValueChannelBindingResolution<T> =
|
|
1046
|
+
| {
|
|
1047
|
+
readonly kind: 'eligible'
|
|
1048
|
+
readonly record: DriverValueChannelIdentityRegistryRecord<T>
|
|
1049
|
+
readonly created: boolean
|
|
1050
|
+
readonly attachedSink: DriverValueChannelSinkRegistration
|
|
1051
|
+
}
|
|
1052
|
+
| {
|
|
1053
|
+
readonly kind: 'ineligible'
|
|
1054
|
+
readonly eligibility: Extract<
|
|
1055
|
+
MotionValueDriverBindingEligibility,
|
|
1056
|
+
{ readonly kind: 'ineligible' }
|
|
1057
|
+
>
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
export interface DriverValueChannelDetachBindingRequest<T> {
|
|
1061
|
+
readonly value: MotionValue<T>
|
|
1062
|
+
readonly sink: DriverValueChannelSinkRegistration
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
export interface DriverValueChannelRebindRequest<T> {
|
|
1066
|
+
readonly previousValue: MotionValue<T>
|
|
1067
|
+
readonly previousSink: DriverValueChannelSinkRegistration
|
|
1068
|
+
readonly replacement: DriverValueChannelResolveBindingRequest<T>
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
export interface DriverValueChannelRebindResult<T> {
|
|
1072
|
+
readonly previousRecord: DriverValueChannelIdentityRegistryRecord<T>
|
|
1073
|
+
readonly replacement: DriverValueChannelBindingResolution<T>
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
export interface DriverValueChannelRetentionReconciliation<T> {
|
|
1077
|
+
readonly value: MotionValue<T>
|
|
1078
|
+
readonly nextState: DriverValueChannelState
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
export interface DriverValueChannelIdentityRegistryRecordObservation {
|
|
1082
|
+
readonly channelId: number
|
|
1083
|
+
readonly jsRecordId: number
|
|
1084
|
+
readonly uiRecordId: number
|
|
1085
|
+
readonly portChannelId: number
|
|
1086
|
+
readonly creationKind: MotionValueDriverPortObservation['creationKind']
|
|
1087
|
+
readonly initializerProvenance: MotionValueDriverPortObservation['initializerProvenance']
|
|
1088
|
+
readonly sinkCount: number
|
|
1089
|
+
readonly sinks: readonly DriverValueChannelSinkRegistration[]
|
|
1090
|
+
readonly currentBindingGeneration: number
|
|
1091
|
+
readonly retiredBindingGeneration: number
|
|
1092
|
+
readonly retentionReasons: readonly DriverValueChannelRetentionReason[]
|
|
1093
|
+
readonly retained: boolean
|
|
1094
|
+
readonly stronglyRegistered: boolean
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
export interface DriverValueChannelIdentityRegistryObservation {
|
|
1098
|
+
readonly retainedRecordCount: number
|
|
1099
|
+
readonly retainedChannelIds: readonly number[]
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
export interface CreateDriverValueChannelIdentityRegistryOptions<T> {
|
|
1103
|
+
readonly initialRecords?: readonly DriverValueChannelIdentityRegistryRecord<T>[]
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
export interface DriverValueChannelCanonicalOperationObservation<T> {
|
|
1107
|
+
readonly record: DriverValueChannelIdentityRegistryRecord<T>
|
|
1108
|
+
readonly port: MotionValueDriverPortObservation
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
export interface DriverValueChannelIdentityRegistry<T> {
|
|
1112
|
+
observe(): DriverValueChannelIdentityRegistryObservation
|
|
1113
|
+
peekExact(value: MotionValue<T>): DriverValueChannelIdentityRegistryRecordObservation | null
|
|
1114
|
+
peekById(channelId: number): DriverValueChannelIdentityRegistryRecordObservation | null
|
|
1115
|
+
resolveBinding(
|
|
1116
|
+
request: DriverValueChannelResolveBindingRequest<T>,
|
|
1117
|
+
): DriverValueChannelBindingResolution<T>
|
|
1118
|
+
detachBinding(
|
|
1119
|
+
request: DriverValueChannelDetachBindingRequest<T>,
|
|
1120
|
+
): DriverValueChannelIdentityRegistryRecord<T> | null
|
|
1121
|
+
rebind(request: DriverValueChannelRebindRequest<T>): DriverValueChannelRebindResult<T>
|
|
1122
|
+
reconcileRetention(
|
|
1123
|
+
reconciliation: DriverValueChannelRetentionReconciliation<T>,
|
|
1124
|
+
): DriverValueChannelIdentityRegistryObservation
|
|
1125
|
+
snapshotForHandoff(value: MotionValue<T>): DriverValueChannelState
|
|
1126
|
+
observeCanonicalOperation(
|
|
1127
|
+
value: MotionValue<T>,
|
|
1128
|
+
): DriverValueChannelCanonicalOperationObservation<T>
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
export type DriverValueChannelRetentionReason =
|
|
1132
|
+
| 'public-reference'
|
|
1133
|
+
| 'sink'
|
|
1134
|
+
| 'change-observer'
|
|
1135
|
+
| 'public-control-slot'
|
|
1136
|
+
| 'physical-execution'
|
|
1137
|
+
| 'due-work'
|
|
1138
|
+
| 'value-completion'
|
|
1139
|
+
| 'target-batch-dependency'
|
|
1140
|
+
| 'dependency-event-outbox'
|
|
1141
|
+
| 'shared-batch-reference'
|
|
1142
|
+
| 'pending-frame-activity'
|
|
1143
|
+
| 'prepared-state'
|
|
1144
|
+
| 'prepared-clone'
|
|
1145
|
+
| 'transfer-activity'
|
|
1146
|
+
| 'transfer-timer'
|
|
1147
|
+
|
|
1148
|
+
export interface DriverValueChannelObservation {
|
|
1149
|
+
readonly channelId: number
|
|
1150
|
+
readonly jsRecordId: number
|
|
1151
|
+
readonly uiRecordId: number
|
|
1152
|
+
readonly ownership: ValueHandoffOwnership
|
|
1153
|
+
readonly ownerSide: ValueChannelSide | null
|
|
1154
|
+
readonly desiredSide: ValueChannelSide | null
|
|
1155
|
+
readonly transferLease: number
|
|
1156
|
+
readonly currentBindingGeneration: number
|
|
1157
|
+
readonly retiredBindingGeneration: number
|
|
1158
|
+
readonly publicControlSlotId: number | null
|
|
1159
|
+
readonly physicalExecutionCount: number
|
|
1160
|
+
readonly dueWorkCount: number
|
|
1161
|
+
readonly valueCompletionCount: number
|
|
1162
|
+
readonly targetBatchDependencyCount: number
|
|
1163
|
+
readonly dependencyEventOutboxCount: number
|
|
1164
|
+
readonly dependencyEventAckWatermark: number
|
|
1165
|
+
readonly commitQueueLength: number
|
|
1166
|
+
readonly sourceMutationQueueLength: number
|
|
1167
|
+
readonly sentThroughAuthoritativeSequence: number
|
|
1168
|
+
readonly drainedThroughAuthoritativeSequence: number
|
|
1169
|
+
readonly retentionReasons: readonly DriverValueChannelRetentionReason[]
|
|
1170
|
+
readonly retained: boolean
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
export type DestinationCommitValidation =
|
|
1174
|
+
| { readonly kind: 'accepted'; readonly duplicate: boolean }
|
|
1175
|
+
| {
|
|
1176
|
+
readonly kind: 'rejected'
|
|
1177
|
+
readonly reason:
|
|
1178
|
+
| 'wrong-channel'
|
|
1179
|
+
| 'wrong-source-record'
|
|
1180
|
+
| 'wrong-destination-record'
|
|
1181
|
+
| 'wrong-direction'
|
|
1182
|
+
| 'wrong-transfer-lease'
|
|
1183
|
+
| 'wrong-release-fence'
|
|
1184
|
+
| 'ahead-of-sent-watermark'
|
|
1185
|
+
| 'sent-watermark-regressed'
|
|
1186
|
+
| 'prefix-not-contiguous'
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
export type DestinationCommitAcknowledgementValidation =
|
|
1190
|
+
| {
|
|
1191
|
+
readonly kind: 'accepted'
|
|
1192
|
+
readonly extent: 'duplicate' | 'partial' | 'through-current-tail'
|
|
1193
|
+
}
|
|
1194
|
+
| {
|
|
1195
|
+
readonly kind: 'rejected'
|
|
1196
|
+
readonly reason:
|
|
1197
|
+
| 'wrong-channel'
|
|
1198
|
+
| 'wrong-source-record'
|
|
1199
|
+
| 'wrong-destination-record'
|
|
1200
|
+
| 'wrong-direction'
|
|
1201
|
+
| 'wrong-transfer-lease'
|
|
1202
|
+
| 'wrong-release-fence'
|
|
1203
|
+
| 'before-release-fence-watermark'
|
|
1204
|
+
| 'ahead-of-sent-watermark'
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
export interface ValueHandoffSourceStatusIdentity {
|
|
1208
|
+
readonly channelId: number
|
|
1209
|
+
readonly sourceRecordId: number
|
|
1210
|
+
readonly destinationRecordId: number
|
|
1211
|
+
readonly direction: ValueHandoffDirection
|
|
1212
|
+
readonly transferLease: number
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
export type ValueHandoffSourceStatus = ValueHandoffSourceStatusIdentity &
|
|
1216
|
+
(
|
|
1217
|
+
| {
|
|
1218
|
+
readonly kind: 'ABORTED'
|
|
1219
|
+
readonly authoritativeSequence: number
|
|
1220
|
+
}
|
|
1221
|
+
| {
|
|
1222
|
+
readonly kind: 'RELEASED'
|
|
1223
|
+
readonly releaseFenceSequence: number
|
|
1224
|
+
}
|
|
1225
|
+
)
|
|
1226
|
+
|
|
1227
|
+
export type ValueHandoffSourceStatusRefusalReason =
|
|
1228
|
+
| 'wrong-channel'
|
|
1229
|
+
| 'wrong-source-record'
|
|
1230
|
+
| 'wrong-destination-record'
|
|
1231
|
+
| 'wrong-direction'
|
|
1232
|
+
| 'wrong-transfer-lease'
|
|
1233
|
+
|
|
1234
|
+
export type ValueHandoffSourceStatusValidation =
|
|
1235
|
+
| { readonly kind: 'accepted' }
|
|
1236
|
+
| { readonly kind: 'rejected'; readonly reason: ValueHandoffSourceStatusRefusalReason }
|
|
1237
|
+
|
|
1238
|
+
export type ValueHandoffSourceStatusProductionRequest = ValueHandoffSourceStatusIdentity &
|
|
1239
|
+
(
|
|
1240
|
+
| { readonly outcome: 'ABORTED'; readonly authoritativeSequence: number }
|
|
1241
|
+
| { readonly outcome: 'RELEASED'; readonly releaseFenceSequence: number }
|
|
1242
|
+
)
|
|
1243
|
+
|
|
1244
|
+
export interface ValueHandoffSourceStatusProductionTransition {
|
|
1245
|
+
readonly state: DriverValueChannelState
|
|
1246
|
+
readonly validation: ValueHandoffSourceStatusValidation
|
|
1247
|
+
readonly producedStatus: ValueHandoffSourceStatus | null
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
export type ValueHandoffEvent =
|
|
1251
|
+
| {
|
|
1252
|
+
readonly kind: 'begin-handoff'
|
|
1253
|
+
readonly direction: ValueHandoffDirection
|
|
1254
|
+
readonly destinationRecordId: number
|
|
1255
|
+
readonly destinationRuntime: DestinationRuntime
|
|
1256
|
+
readonly transferLease: number
|
|
1257
|
+
readonly phaseToken: number
|
|
1258
|
+
readonly absoluteTransferDeadlineMs: number
|
|
1259
|
+
readonly absoluteNoConsumerDeadlineMs: number
|
|
1260
|
+
}
|
|
1261
|
+
| {
|
|
1262
|
+
readonly kind: 'destination-ready'
|
|
1263
|
+
readonly direction: ValueHandoffDirection
|
|
1264
|
+
readonly transferLease: number
|
|
1265
|
+
readonly preparedMutableTuple: ValueHandoffPreparedMutableTuple
|
|
1266
|
+
}
|
|
1267
|
+
| {
|
|
1268
|
+
readonly kind: 'source-mutation'
|
|
1269
|
+
readonly entry: NormalizedRetainedWorkEntry
|
|
1270
|
+
}
|
|
1271
|
+
| { readonly kind: 'source-status'; readonly status: ValueHandoffSourceStatus }
|
|
1272
|
+
| {
|
|
1273
|
+
readonly kind: 'enqueue-destination-commit'
|
|
1274
|
+
readonly commit: ImmutableDestinationCommit
|
|
1275
|
+
}
|
|
1276
|
+
| {
|
|
1277
|
+
readonly kind: 'destination-commit-acknowledgement'
|
|
1278
|
+
readonly acknowledgement: DestinationCommitAcknowledgement
|
|
1279
|
+
}
|
|
1280
|
+
| {
|
|
1281
|
+
readonly kind: 'phase-timeout'
|
|
1282
|
+
readonly direction: ValueHandoffDirection
|
|
1283
|
+
readonly transferLease: number
|
|
1284
|
+
readonly phaseToken: number
|
|
1285
|
+
}
|
|
1286
|
+
| {
|
|
1287
|
+
readonly kind: 'bounded-terminal'
|
|
1288
|
+
readonly direction: ValueHandoffDirection
|
|
1289
|
+
readonly transferLease: number
|
|
1290
|
+
readonly phaseToken: number
|
|
1291
|
+
}
|
|
1292
|
+
| {
|
|
1293
|
+
readonly kind: 'finish-bounded-refusal-drain'
|
|
1294
|
+
readonly direction: ValueHandoffDirection
|
|
1295
|
+
readonly transferLease: number
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1298
|
+
export type ValueHandoffEffect =
|
|
1299
|
+
| { readonly kind: 'send-prepare'; readonly direction: ValueHandoffDirection }
|
|
1300
|
+
| { readonly kind: 'invalidate-prepared-clone' }
|
|
1301
|
+
| { readonly kind: 'resume-authoritative-source'; readonly side: ValueChannelSide }
|
|
1302
|
+
| { readonly kind: 'request-source-release-status' }
|
|
1303
|
+
| { readonly kind: 'enqueue-immutable-destination-commit' }
|
|
1304
|
+
| {
|
|
1305
|
+
readonly kind: 'request-autonomous-destination-root'
|
|
1306
|
+
readonly root: 'live-ui-frame' | JsDestinationRoot
|
|
1307
|
+
}
|
|
1308
|
+
| { readonly kind: 'arm-manual-frame-group'; readonly manualFrameGroupId: number }
|
|
1309
|
+
| { readonly kind: 'rebuild-same-commit-with-suffix'; readonly suffixStartSequence: number }
|
|
1310
|
+
| { readonly kind: 'cancel-transfer-timer' }
|
|
1311
|
+
| { readonly kind: 'release-prepared-state' }
|
|
1312
|
+
| { readonly kind: 'release-prepared-clone' }
|
|
1313
|
+
| { readonly kind: 'release-transfer-activity' }
|
|
1314
|
+
| { readonly kind: 'permit-destination-run-or-reverse' }
|
|
1315
|
+
| { readonly kind: 'enter-change-host-silent-drain' }
|
|
1316
|
+
| { readonly kind: 'refuse-live-channel-property' }
|
|
1317
|
+
| { readonly kind: 'finalize-bounded-refusal-without-run-or-reverse' }
|
|
1318
|
+
|
|
1319
|
+
export interface ValueHandoffTransition {
|
|
1320
|
+
readonly state: DriverValueChannelState
|
|
1321
|
+
readonly observation: DriverValueChannelObservation
|
|
1322
|
+
readonly orderedEffects: readonly ValueHandoffEffect[]
|
|
1323
|
+
readonly outcome:
|
|
1324
|
+
| { readonly kind: 'APPLIED' }
|
|
1325
|
+
| { readonly kind: 'IGNORED_STALE_EVENT' }
|
|
1326
|
+
| {
|
|
1327
|
+
readonly kind: 'SOURCE_STATUS_REFUSED'
|
|
1328
|
+
readonly reason: ValueHandoffSourceStatusRefusalReason
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1332
|
+
export interface ValueHandoffPhaseEventTransition {
|
|
1333
|
+
readonly state: DriverValueChannelState
|
|
1334
|
+
readonly observation: DriverValueChannelObservation
|
|
1335
|
+
readonly disposition: ValueHandoffPhaseEventDisposition
|
|
1336
|
+
readonly orderedEffects: readonly ValueHandoffEffect[]
|
|
1337
|
+
}
|
|
1338
|
+
|
|
1339
|
+
export type DriverValueChannelOperation =
|
|
1340
|
+
| ValueHandoffEvent['kind']
|
|
1341
|
+
| ValueHandoffPhaseEventOperation
|
|
1342
|
+
| ValueHandoffProtocolCheckpointOperation
|
|
1343
|
+
| `consume-js-destination-commit-${JsDestinationRoot}`
|
|
1344
|
+
| 'validate-destination-commit'
|
|
1345
|
+
| 'validate-destination-commit-acknowledgement'
|
|
1346
|
+
| 'produce-source-status'
|
|
1347
|
+
| 'registry-resolve-binding'
|
|
1348
|
+
| 'registry-detach-binding'
|
|
1349
|
+
| 'registry-rebind'
|
|
1350
|
+
| 'registry-reconcile-retention'
|
|
1351
|
+
| 'registry-snapshot-for-handoff'
|
|
1352
|
+
| 'registry-observe-canonical-operation'
|
|
1353
|
+
| 'drain-post-release-commit-loss'
|
|
1354
|
+
|
|
1355
|
+
export const DRIVER_VALUE_CHANNEL_UNIMPLEMENTED =
|
|
1356
|
+
'NATIVE_MOTION_DRIVER_VALUE_CHANNEL_UNIMPLEMENTED' as const
|
|
1357
|
+
|
|
1358
|
+
export class DriverValueChannelUnimplementedError extends Error {
|
|
1359
|
+
readonly code = DRIVER_VALUE_CHANNEL_UNIMPLEMENTED
|
|
1360
|
+
readonly operation: DriverValueChannelOperation
|
|
1361
|
+
|
|
1362
|
+
constructor(operation: DriverValueChannelOperation) {
|
|
1363
|
+
super(
|
|
1364
|
+
`native-motion: driver value-channel operation '${operation}' is unimplemented ` +
|
|
1365
|
+
'(REQ-VALUE-014/015, REQ-DRIVER-034)',
|
|
1366
|
+
)
|
|
1367
|
+
this.name = 'DriverValueChannelUnimplementedError'
|
|
1368
|
+
this.operation = operation
|
|
1369
|
+
}
|
|
1370
|
+
}
|
|
1371
|
+
|
|
1372
|
+
export const DRIVER_VALUE_CHANNEL_INVARIANT =
|
|
1373
|
+
'NATIVE_MOTION_DRIVER_VALUE_CHANNEL_INVARIANT' as const
|
|
1374
|
+
|
|
1375
|
+
export type DriverValueChannelInvariant =
|
|
1376
|
+
| 'registry-record-missing'
|
|
1377
|
+
| 'registry-rebind-previous-record-missing'
|
|
1378
|
+
| 'handoff-event-kind-unrecognized'
|
|
1379
|
+
| 'phase-event-kind-unrecognized'
|
|
1380
|
+
| 'phase-event-phase-unrecognized'
|
|
1381
|
+
| 'protocol-checkpoint-unrecognized'
|
|
1382
|
+
| 'protocol-event-kind-unrecognized'
|
|
1383
|
+
| 'commit-loss-work-kind-unrecognized'
|
|
1384
|
+
|
|
1385
|
+
/** Typed fail-closed refusal for malformed, conflicting, or out-of-sequence channel input. */
|
|
1386
|
+
export class DriverValueChannelInvariantError extends Error {
|
|
1387
|
+
readonly code = DRIVER_VALUE_CHANNEL_INVARIANT
|
|
1388
|
+
readonly invariant: DriverValueChannelInvariant
|
|
1389
|
+
readonly operation: DriverValueChannelOperation | null
|
|
1390
|
+
|
|
1391
|
+
constructor(invariant: DriverValueChannelInvariant, operation?: DriverValueChannelOperation) {
|
|
1392
|
+
super(
|
|
1393
|
+
`native-motion: driver value-channel invariant '${invariant}' was violated` +
|
|
1394
|
+
(operation === undefined ? '' : ` by operation '${operation}'`) +
|
|
1395
|
+
' (REQ-VALUE-014/015, REQ-DRIVER-034)',
|
|
1396
|
+
)
|
|
1397
|
+
this.name = 'DriverValueChannelInvariantError'
|
|
1398
|
+
this.invariant = invariant
|
|
1399
|
+
this.operation = operation ?? null
|
|
1400
|
+
}
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1403
|
+
function ownerSide(
|
|
1404
|
+
ownership: ValueHandoffOwnership,
|
|
1405
|
+
direction: ValueHandoffDirection | null,
|
|
1406
|
+
): ValueChannelSide | null {
|
|
1407
|
+
switch (ownership) {
|
|
1408
|
+
case 'UI_RUNNING':
|
|
1409
|
+
case 'UI_FROZEN_TO_JS':
|
|
1410
|
+
case 'JS_PREPARED':
|
|
1411
|
+
case 'UI_RELEASE_PENDING_TO_JS':
|
|
1412
|
+
case 'UI_DRAINING':
|
|
1413
|
+
return 'UI'
|
|
1414
|
+
case 'JS_RUNNING':
|
|
1415
|
+
case 'JS_FROZEN_TO_UI':
|
|
1416
|
+
case 'UI_PREPARED':
|
|
1417
|
+
case 'JS_RELEASE_PENDING_TO_UI':
|
|
1418
|
+
case 'JS_DRAINING':
|
|
1419
|
+
return 'JS'
|
|
1420
|
+
case 'BOUNDED_REFUSAL_DRAINING':
|
|
1421
|
+
return direction === 'UI_TO_JS' ? 'JS' : direction === 'JS_TO_UI' ? 'UI' : null
|
|
1422
|
+
case 'UI_RELEASED_TO_JS':
|
|
1423
|
+
case 'JS_RELEASED_TO_UI':
|
|
1424
|
+
case 'IDLE':
|
|
1425
|
+
case 'TERMINAL_REFUSED':
|
|
1426
|
+
return null
|
|
1427
|
+
default: {
|
|
1428
|
+
const exhaustive: never = ownership
|
|
1429
|
+
return exhaustive
|
|
1430
|
+
}
|
|
1431
|
+
}
|
|
1432
|
+
}
|
|
1433
|
+
|
|
1434
|
+
export function observeDriverValueChannel(
|
|
1435
|
+
state: DriverValueChannelState,
|
|
1436
|
+
): DriverValueChannelObservation {
|
|
1437
|
+
const { handoff, identity } = state
|
|
1438
|
+
const reasons: DriverValueChannelRetentionReason[] = []
|
|
1439
|
+
if (identity.publicReferenceRetained) reasons.push('public-reference')
|
|
1440
|
+
if (identity.sinkCount > 0) reasons.push('sink')
|
|
1441
|
+
if (identity.changeObserverCount > 0) reasons.push('change-observer')
|
|
1442
|
+
if (identity.publicControlSlotId !== null) reasons.push('public-control-slot')
|
|
1443
|
+
if (identity.physicalExecutionIds.length > 0) reasons.push('physical-execution')
|
|
1444
|
+
if (identity.dueWorkSequences.length > 0) reasons.push('due-work')
|
|
1445
|
+
if (identity.valueCompletionSequences.length > 0) reasons.push('value-completion')
|
|
1446
|
+
if (identity.targetBatchDependencyIds.length > 0) reasons.push('target-batch-dependency')
|
|
1447
|
+
if (identity.dependencyEventOutboxSequences.length > 0) {
|
|
1448
|
+
reasons.push('dependency-event-outbox')
|
|
1449
|
+
}
|
|
1450
|
+
if (identity.sharedBatchReferenceCount > 0) reasons.push('shared-batch-reference')
|
|
1451
|
+
if (identity.pendingFrameActivity) reasons.push('pending-frame-activity')
|
|
1452
|
+
if (handoff.resources.preparedStateRetained) reasons.push('prepared-state')
|
|
1453
|
+
if (handoff.resources.preparedCloneRetained) reasons.push('prepared-clone')
|
|
1454
|
+
if (handoff.resources.transferActivityRetained) reasons.push('transfer-activity')
|
|
1455
|
+
if (handoff.resources.transferTimerArmed) reasons.push('transfer-timer')
|
|
1456
|
+
|
|
1457
|
+
return {
|
|
1458
|
+
channelId: identity.channelId,
|
|
1459
|
+
jsRecordId: identity.jsRecordId,
|
|
1460
|
+
uiRecordId: identity.uiRecordId,
|
|
1461
|
+
ownership: handoff.ownership,
|
|
1462
|
+
ownerSide: ownerSide(handoff.ownership, handoff.direction),
|
|
1463
|
+
desiredSide: handoff.desiredSide,
|
|
1464
|
+
transferLease: handoff.transferLease,
|
|
1465
|
+
currentBindingGeneration: identity.currentBindingGeneration,
|
|
1466
|
+
retiredBindingGeneration: identity.retiredBindingGeneration,
|
|
1467
|
+
publicControlSlotId: identity.publicControlSlotId,
|
|
1468
|
+
physicalExecutionCount: identity.physicalExecutionIds.length,
|
|
1469
|
+
dueWorkCount: identity.dueWorkSequences.length,
|
|
1470
|
+
valueCompletionCount: identity.valueCompletionSequences.length,
|
|
1471
|
+
targetBatchDependencyCount: identity.targetBatchDependencyIds.length,
|
|
1472
|
+
dependencyEventOutboxCount: identity.dependencyEventOutboxSequences.length,
|
|
1473
|
+
dependencyEventAckWatermark: identity.dependencyEventAckWatermark,
|
|
1474
|
+
commitQueueLength: handoff.commitQueue.length,
|
|
1475
|
+
sourceMutationQueueLength: handoff.sourceMutationQueue.length,
|
|
1476
|
+
sentThroughAuthoritativeSequence: handoff.sentThroughAuthoritativeSequence,
|
|
1477
|
+
drainedThroughAuthoritativeSequence: handoff.drainedThroughAuthoritativeSequence,
|
|
1478
|
+
retentionReasons: reasons,
|
|
1479
|
+
retained: reasons.length > 0,
|
|
1480
|
+
}
|
|
1481
|
+
}
|
|
1482
|
+
|
|
1483
|
+
function frozenOwnershipFor(direction: ValueHandoffDirection): ValueHandoffOwnership {
|
|
1484
|
+
return direction === 'UI_TO_JS' ? 'UI_FROZEN_TO_JS' : 'JS_FROZEN_TO_UI'
|
|
1485
|
+
}
|
|
1486
|
+
|
|
1487
|
+
function preparedOwnershipFor(direction: ValueHandoffDirection): ValueHandoffOwnership {
|
|
1488
|
+
return direction === 'UI_TO_JS' ? 'JS_PREPARED' : 'UI_PREPARED'
|
|
1489
|
+
}
|
|
1490
|
+
|
|
1491
|
+
function releasePendingOwnershipFor(direction: ValueHandoffDirection): ValueHandoffOwnership {
|
|
1492
|
+
return direction === 'UI_TO_JS' ? 'UI_RELEASE_PENDING_TO_JS' : 'JS_RELEASE_PENDING_TO_UI'
|
|
1493
|
+
}
|
|
1494
|
+
|
|
1495
|
+
function releasedOwnershipFor(direction: ValueHandoffDirection): ValueHandoffOwnership {
|
|
1496
|
+
return direction === 'UI_TO_JS' ? 'UI_RELEASED_TO_JS' : 'JS_RELEASED_TO_UI'
|
|
1497
|
+
}
|
|
1498
|
+
|
|
1499
|
+
function sourceRunningOwnershipFor(direction: ValueHandoffDirection): ValueHandoffOwnership {
|
|
1500
|
+
return direction === 'UI_TO_JS' ? 'UI_RUNNING' : 'JS_RUNNING'
|
|
1501
|
+
}
|
|
1502
|
+
|
|
1503
|
+
function destinationRunningOwnershipFor(direction: ValueHandoffDirection): ValueHandoffOwnership {
|
|
1504
|
+
return direction === 'UI_TO_JS' ? 'JS_RUNNING' : 'UI_RUNNING'
|
|
1505
|
+
}
|
|
1506
|
+
|
|
1507
|
+
function sourceSideFor(direction: ValueHandoffDirection): ValueChannelSide {
|
|
1508
|
+
return direction === 'UI_TO_JS' ? 'UI' : 'JS'
|
|
1509
|
+
}
|
|
1510
|
+
|
|
1511
|
+
/** The one already-declared autonomous destination turn a successful COMMIT enqueue activates. */
|
|
1512
|
+
function destinationActivationEffect(
|
|
1513
|
+
runtime: DestinationRuntime | null,
|
|
1514
|
+
): ValueHandoffEffect | null {
|
|
1515
|
+
if (runtime === null) return null
|
|
1516
|
+
switch (runtime.kind) {
|
|
1517
|
+
case 'live-ui':
|
|
1518
|
+
return { kind: 'request-autonomous-destination-root', root: 'live-ui-frame' }
|
|
1519
|
+
case 'manual-ui':
|
|
1520
|
+
return { kind: 'arm-manual-frame-group', manualFrameGroupId: runtime.manualFrameGroupId }
|
|
1521
|
+
case 'js':
|
|
1522
|
+
return { kind: 'request-autonomous-destination-root', root: runtime.root }
|
|
1523
|
+
default: {
|
|
1524
|
+
const exhaustive: never = runtime
|
|
1525
|
+
return exhaustive
|
|
1526
|
+
}
|
|
1527
|
+
}
|
|
1528
|
+
}
|
|
1529
|
+
|
|
1530
|
+
function withHandoff(
|
|
1531
|
+
state: DriverValueChannelState,
|
|
1532
|
+
handoff: ValueHandoffProtocolState,
|
|
1533
|
+
): DriverValueChannelState {
|
|
1534
|
+
return { identity: state.identity, handoff }
|
|
1535
|
+
}
|
|
1536
|
+
|
|
1537
|
+
function appliedTransition(
|
|
1538
|
+
state: DriverValueChannelState,
|
|
1539
|
+
orderedEffects: readonly ValueHandoffEffect[],
|
|
1540
|
+
): ValueHandoffTransition {
|
|
1541
|
+
return {
|
|
1542
|
+
state,
|
|
1543
|
+
observation: observeDriverValueChannel(state),
|
|
1544
|
+
orderedEffects,
|
|
1545
|
+
outcome: { kind: 'APPLIED' },
|
|
1546
|
+
}
|
|
1547
|
+
}
|
|
1548
|
+
|
|
1549
|
+
function ignoredTransition(state: DriverValueChannelState): ValueHandoffTransition {
|
|
1550
|
+
return {
|
|
1551
|
+
state,
|
|
1552
|
+
observation: observeDriverValueChannel(state),
|
|
1553
|
+
orderedEffects: [],
|
|
1554
|
+
outcome: { kind: 'IGNORED_STALE_EVENT' },
|
|
1555
|
+
}
|
|
1556
|
+
}
|
|
1557
|
+
|
|
1558
|
+
/**
|
|
1559
|
+
* The immutable five-field transfer identity every producer/consumer boundary revalidates; a
|
|
1560
|
+
* mismatch is refused by name and never mutates retained protocol state.
|
|
1561
|
+
*/
|
|
1562
|
+
function validateTransferIdentityTuple(
|
|
1563
|
+
state: DriverValueChannelState,
|
|
1564
|
+
candidate: ValueHandoffSourceStatusIdentity,
|
|
1565
|
+
): ValueHandoffSourceStatusRefusalReason | null {
|
|
1566
|
+
if (candidate.channelId !== state.identity.channelId) return 'wrong-channel'
|
|
1567
|
+
if (candidate.sourceRecordId !== state.handoff.sourceRecordId) return 'wrong-source-record'
|
|
1568
|
+
if (candidate.destinationRecordId !== state.handoff.destinationRecordId) {
|
|
1569
|
+
return 'wrong-destination-record'
|
|
1570
|
+
}
|
|
1571
|
+
if (candidate.direction !== state.handoff.direction) return 'wrong-direction'
|
|
1572
|
+
if (candidate.transferLease !== state.handoff.transferLease) return 'wrong-transfer-lease'
|
|
1573
|
+
return null
|
|
1574
|
+
}
|
|
1575
|
+
|
|
1576
|
+
function releasedResourceState(): ValueChannelHandoffResourceState {
|
|
1577
|
+
return {
|
|
1578
|
+
preparedStateRetained: false,
|
|
1579
|
+
preparedCloneRetained: false,
|
|
1580
|
+
transferActivityRetained: false,
|
|
1581
|
+
transferTimerArmed: false,
|
|
1582
|
+
}
|
|
1583
|
+
}
|
|
1584
|
+
|
|
1585
|
+
function baseDestinationCommit(
|
|
1586
|
+
state: DriverValueChannelState,
|
|
1587
|
+
direction: ValueHandoffDirection,
|
|
1588
|
+
): ImmutableDestinationCommit {
|
|
1589
|
+
return {
|
|
1590
|
+
kind: 'DESTINATION_COMMIT',
|
|
1591
|
+
channelId: state.identity.channelId,
|
|
1592
|
+
sourceRecordId: state.handoff.sourceRecordId ?? -1,
|
|
1593
|
+
destinationRecordId: state.handoff.destinationRecordId ?? -1,
|
|
1594
|
+
direction,
|
|
1595
|
+
transferLease: state.handoff.transferLease,
|
|
1596
|
+
releaseFenceSequence: state.handoff.releaseFenceSequence ?? -1,
|
|
1597
|
+
sentThroughAuthoritativeSequence: state.handoff.sentThroughAuthoritativeSequence,
|
|
1598
|
+
includedPostFencePrefix: [],
|
|
1599
|
+
}
|
|
1600
|
+
}
|
|
1601
|
+
|
|
1602
|
+
/** Append one retained-work entry to the current immutable COMMIT, creating it when absent. */
|
|
1603
|
+
function appendEntryToCommitQueue(
|
|
1604
|
+
state: DriverValueChannelState,
|
|
1605
|
+
entry: NormalizedRetainedWorkEntry,
|
|
1606
|
+
): readonly ImmutableDestinationCommit[] {
|
|
1607
|
+
const queue = state.handoff.commitQueue
|
|
1608
|
+
const current = queue.at(-1)
|
|
1609
|
+
if (current === undefined) {
|
|
1610
|
+
const direction = state.handoff.direction
|
|
1611
|
+
if (direction === null) return queue
|
|
1612
|
+
return [
|
|
1613
|
+
{
|
|
1614
|
+
...baseDestinationCommit(state, direction),
|
|
1615
|
+
sentThroughAuthoritativeSequence: entry.authoritativeSequence,
|
|
1616
|
+
includedPostFencePrefix: [entry],
|
|
1617
|
+
},
|
|
1618
|
+
]
|
|
1619
|
+
}
|
|
1620
|
+
return [
|
|
1621
|
+
...queue.slice(0, -1),
|
|
1622
|
+
{
|
|
1623
|
+
...current,
|
|
1624
|
+
sentThroughAuthoritativeSequence: entry.authoritativeSequence,
|
|
1625
|
+
includedPostFencePrefix: [...current.includedPostFencePrefix, entry],
|
|
1626
|
+
},
|
|
1627
|
+
]
|
|
1628
|
+
}
|
|
1629
|
+
|
|
1630
|
+
function isProtocolReadEvent(
|
|
1631
|
+
event: ValueHandoffPhaseEvent | ValueHandoffProtocolEvent,
|
|
1632
|
+
): event is ValueHandoffPhaseReadEvent {
|
|
1633
|
+
return (
|
|
1634
|
+
event.kind === 'synchronous-snapshot' ||
|
|
1635
|
+
event.kind === 'synchronous-get' ||
|
|
1636
|
+
event.kind === 'synchronous-get-velocity'
|
|
1637
|
+
)
|
|
1638
|
+
}
|
|
1639
|
+
|
|
1640
|
+
/**
|
|
1641
|
+
* Fail closed on any phase outside the closed union BEFORE a phase-keyed transition runs; an
|
|
1642
|
+
* untyped caller must never fall into the pre-fence branch by default.
|
|
1643
|
+
*/
|
|
1644
|
+
function requireValueHandoffPhase(phase: ValueHandoffPhase): void {
|
|
1645
|
+
switch (phase) {
|
|
1646
|
+
case 'PREPARED':
|
|
1647
|
+
case 'RELEASE_PENDING_PRE_FENCE':
|
|
1648
|
+
case 'RELEASED_POST_FENCE':
|
|
1649
|
+
return
|
|
1650
|
+
default: {
|
|
1651
|
+
const exhaustive: never = phase
|
|
1652
|
+
void exhaustive
|
|
1653
|
+
throw new DriverValueChannelInvariantError('phase-event-phase-unrecognized')
|
|
1654
|
+
}
|
|
1655
|
+
}
|
|
1656
|
+
}
|
|
1657
|
+
|
|
1658
|
+
/** Fail closed on any phase event kind outside the closed union before mutation dispatch. */
|
|
1659
|
+
function requirePhaseEventKind(kind: ValueHandoffPhaseEventKind): void {
|
|
1660
|
+
switch (kind) {
|
|
1661
|
+
case 'sink-attach':
|
|
1662
|
+
case 'sink-detach':
|
|
1663
|
+
case 'observer-arm':
|
|
1664
|
+
case 'observer-disarm':
|
|
1665
|
+
case 'start':
|
|
1666
|
+
case 'retarget':
|
|
1667
|
+
case 'set':
|
|
1668
|
+
case 'jump':
|
|
1669
|
+
case 'stop':
|
|
1670
|
+
case 'destroy':
|
|
1671
|
+
case 'public-slot':
|
|
1672
|
+
case 'physical-sample':
|
|
1673
|
+
case 'physical-done':
|
|
1674
|
+
case 'completion-arm':
|
|
1675
|
+
case 'completion-resolved':
|
|
1676
|
+
case 'batch-dependency':
|
|
1677
|
+
case 'batch-outbox':
|
|
1678
|
+
case 'batch-outbox-ack':
|
|
1679
|
+
case 'fifo-task':
|
|
1680
|
+
case 'binding-generation':
|
|
1681
|
+
case 'synchronous-snapshot':
|
|
1682
|
+
case 'synchronous-get':
|
|
1683
|
+
case 'synchronous-get-velocity':
|
|
1684
|
+
return
|
|
1685
|
+
default: {
|
|
1686
|
+
const exhaustive: never = kind
|
|
1687
|
+
void exhaustive
|
|
1688
|
+
throw new DriverValueChannelInvariantError('phase-event-kind-unrecognized')
|
|
1689
|
+
}
|
|
1690
|
+
}
|
|
1691
|
+
}
|
|
1692
|
+
|
|
1693
|
+
/** Fail closed on any checkpoint outside the closed union before a keyed transition runs. */
|
|
1694
|
+
function requireProtocolCheckpoint(checkpoint: ValueHandoffProtocolCheckpoint): void {
|
|
1695
|
+
switch (checkpoint) {
|
|
1696
|
+
case 'FREEZE':
|
|
1697
|
+
case 'PREPARE_READY':
|
|
1698
|
+
case 'RELEASE_PENDING_SOURCE_UNKNOWN':
|
|
1699
|
+
case 'SOURCE_CONFIRMED_ABORTED':
|
|
1700
|
+
case 'SOURCE_CONFIRMED_RELEASED':
|
|
1701
|
+
case 'DESTINATION_COMMIT_CONSUMPTION':
|
|
1702
|
+
case 'DESTINATION_ACKNOWLEDGED':
|
|
1703
|
+
return
|
|
1704
|
+
default: {
|
|
1705
|
+
const exhaustive: never = checkpoint
|
|
1706
|
+
void exhaustive
|
|
1707
|
+
throw new DriverValueChannelInvariantError('protocol-checkpoint-unrecognized')
|
|
1708
|
+
}
|
|
1709
|
+
}
|
|
1710
|
+
}
|
|
1711
|
+
|
|
1712
|
+
/** Fail closed on any protocol event kind outside the closed union before mutation dispatch. */
|
|
1713
|
+
function requireProtocolEventKind(kind: ValueHandoffProtocolEventKind): void {
|
|
1714
|
+
switch (kind) {
|
|
1715
|
+
case 'sink-attach':
|
|
1716
|
+
case 'sink-detach':
|
|
1717
|
+
case 'observer-arm':
|
|
1718
|
+
case 'observer-disarm':
|
|
1719
|
+
case 'start':
|
|
1720
|
+
case 'retarget':
|
|
1721
|
+
case 'set':
|
|
1722
|
+
case 'jump':
|
|
1723
|
+
case 'stop':
|
|
1724
|
+
case 'destroy':
|
|
1725
|
+
case 'public-slot':
|
|
1726
|
+
case 'physical-sample':
|
|
1727
|
+
case 'physical-done':
|
|
1728
|
+
case 'completion-arm':
|
|
1729
|
+
case 'completion-resolved':
|
|
1730
|
+
case 'batch-dependency':
|
|
1731
|
+
case 'batch-outbox':
|
|
1732
|
+
case 'batch-outbox-ack':
|
|
1733
|
+
case 'fifo-task':
|
|
1734
|
+
case 'binding-generation':
|
|
1735
|
+
case 'tokenless-task-enqueue':
|
|
1736
|
+
case 'lifecycle-enqueue':
|
|
1737
|
+
case 'synchronous-snapshot':
|
|
1738
|
+
case 'synchronous-get':
|
|
1739
|
+
case 'synchronous-get-velocity':
|
|
1740
|
+
return
|
|
1741
|
+
default: {
|
|
1742
|
+
const exhaustive: never = kind
|
|
1743
|
+
void exhaustive
|
|
1744
|
+
throw new DriverValueChannelInvariantError('protocol-event-kind-unrecognized')
|
|
1745
|
+
}
|
|
1746
|
+
}
|
|
1747
|
+
}
|
|
1748
|
+
|
|
1749
|
+
function readViewForPhase(phase: ValueHandoffPhase): ValueHandoffPhaseReadView {
|
|
1750
|
+
switch (phase) {
|
|
1751
|
+
case 'PREPARED':
|
|
1752
|
+
return 'AUTHORITATIVE_SOURCE'
|
|
1753
|
+
case 'RELEASE_PENDING_PRE_FENCE':
|
|
1754
|
+
return 'SOURCE_WITH_DIRECT_OPERATION_OVERLAY'
|
|
1755
|
+
case 'RELEASED_POST_FENCE':
|
|
1756
|
+
return 'PREPARED_DESTINATION_WITH_DIRECT_OPERATION_OVERLAY'
|
|
1757
|
+
default: {
|
|
1758
|
+
const exhaustive: never = phase
|
|
1759
|
+
return exhaustive
|
|
1760
|
+
}
|
|
1761
|
+
}
|
|
1762
|
+
}
|
|
1763
|
+
|
|
1764
|
+
function readAuthorityForCheckpoint(
|
|
1765
|
+
checkpoint: ValueHandoffProtocolCheckpoint,
|
|
1766
|
+
): ValueHandoffProtocolReadAuthority {
|
|
1767
|
+
switch (checkpoint) {
|
|
1768
|
+
case 'FREEZE':
|
|
1769
|
+
case 'PREPARE_READY':
|
|
1770
|
+
case 'SOURCE_CONFIRMED_ABORTED':
|
|
1771
|
+
return 'AUTHORITATIVE_SOURCE'
|
|
1772
|
+
case 'RELEASE_PENDING_SOURCE_UNKNOWN':
|
|
1773
|
+
return 'SOURCE_WITH_DIRECT_OPERATION_OVERLAY'
|
|
1774
|
+
case 'SOURCE_CONFIRMED_RELEASED':
|
|
1775
|
+
case 'DESTINATION_COMMIT_CONSUMPTION':
|
|
1776
|
+
return 'PREPARED_DESTINATION_WITH_DIRECT_OPERATION_OVERLAY'
|
|
1777
|
+
case 'DESTINATION_ACKNOWLEDGED':
|
|
1778
|
+
return 'ACKNOWLEDGED_DESTINATION'
|
|
1779
|
+
default: {
|
|
1780
|
+
const exhaustive: never = checkpoint
|
|
1781
|
+
return exhaustive
|
|
1782
|
+
}
|
|
1783
|
+
}
|
|
1784
|
+
}
|
|
1785
|
+
|
|
1786
|
+
function mutationHandlingForCheckpoint(
|
|
1787
|
+
checkpoint: ValueHandoffProtocolCheckpoint,
|
|
1788
|
+
): ValueHandoffProtocolMutationHandling {
|
|
1789
|
+
switch (checkpoint) {
|
|
1790
|
+
case 'FREEZE':
|
|
1791
|
+
return 'INVALIDATE_FREEZE_AND_REPREPARE_SOURCE'
|
|
1792
|
+
case 'PREPARE_READY':
|
|
1793
|
+
return 'ABORT_PREPARE_AND_REPREPARE_SOURCE'
|
|
1794
|
+
case 'RELEASE_PENDING_SOURCE_UNKNOWN':
|
|
1795
|
+
return 'SEQUENCE_WITHOUT_INFERRING_SOURCE_STATUS'
|
|
1796
|
+
case 'SOURCE_CONFIRMED_ABORTED':
|
|
1797
|
+
return 'APPLY_CONFIRMED_ABORT_AND_REPREPARE_SOURCE'
|
|
1798
|
+
case 'SOURCE_CONFIRMED_RELEASED':
|
|
1799
|
+
return 'APPEND_TO_IMMUTABLE_RELEASED_COMMIT'
|
|
1800
|
+
case 'DESTINATION_COMMIT_CONSUMPTION':
|
|
1801
|
+
return 'APPLY_IN_DESTINATION_COMMIT_PREFIX'
|
|
1802
|
+
case 'DESTINATION_ACKNOWLEDGED':
|
|
1803
|
+
return 'APPLY_TO_ACKNOWLEDGED_DESTINATION_OR_REVERSE'
|
|
1804
|
+
default: {
|
|
1805
|
+
const exhaustive: never = checkpoint
|
|
1806
|
+
return exhaustive
|
|
1807
|
+
}
|
|
1808
|
+
}
|
|
1809
|
+
}
|
|
1810
|
+
|
|
1811
|
+
function retainedWorkDispositionFor(
|
|
1812
|
+
checkpoint: ValueHandoffProtocolCheckpoint,
|
|
1813
|
+
event: ValueHandoffProtocolMutationEvent,
|
|
1814
|
+
): ValueHandoffProtocolRetainedWorkDisposition {
|
|
1815
|
+
switch (checkpoint) {
|
|
1816
|
+
case 'FREEZE':
|
|
1817
|
+
return {
|
|
1818
|
+
kind: 'CARRIED_IN_INVALIDATED_FREEZE_REPREPARE',
|
|
1819
|
+
carriedEntry: event.retainedWorkEntry,
|
|
1820
|
+
}
|
|
1821
|
+
case 'PREPARE_READY':
|
|
1822
|
+
return {
|
|
1823
|
+
kind: 'CARRIED_IN_ABORTED_PREPARE_REPREPARE',
|
|
1824
|
+
carriedEntry: event.retainedWorkEntry,
|
|
1825
|
+
}
|
|
1826
|
+
case 'RELEASE_PENDING_SOURCE_UNKNOWN':
|
|
1827
|
+
return {
|
|
1828
|
+
kind: 'QUEUED_BEHIND_SOURCE_DECISION',
|
|
1829
|
+
queuedEntry: event.retainedWorkEntry,
|
|
1830
|
+
}
|
|
1831
|
+
case 'SOURCE_CONFIRMED_ABORTED':
|
|
1832
|
+
return {
|
|
1833
|
+
kind: 'APPLIED_TO_CONFIRMED_SOURCE_BEFORE_REPREPARE',
|
|
1834
|
+
appliedEntry: event.retainedWorkEntry,
|
|
1835
|
+
}
|
|
1836
|
+
case 'SOURCE_CONFIRMED_RELEASED':
|
|
1837
|
+
return {
|
|
1838
|
+
kind: 'APPENDED_TO_IMMUTABLE_RELEASED_COMMIT',
|
|
1839
|
+
appendedEntry: event.retainedWorkEntry,
|
|
1840
|
+
}
|
|
1841
|
+
case 'DESTINATION_COMMIT_CONSUMPTION':
|
|
1842
|
+
return {
|
|
1843
|
+
kind: 'APPLIED_TO_PREFILLED_DESTINATION_SLOT',
|
|
1844
|
+
destinationSlot: event.retainedWorkEntry.destinationSlot,
|
|
1845
|
+
appliedEntry: event.retainedWorkEntry,
|
|
1846
|
+
}
|
|
1847
|
+
case 'DESTINATION_ACKNOWLEDGED':
|
|
1848
|
+
return {
|
|
1849
|
+
kind: 'APPLIED_TO_ACKNOWLEDGED_DESTINATION_OR_REVERSE',
|
|
1850
|
+
destinationSlot: event.retainedWorkEntry.destinationSlot,
|
|
1851
|
+
appliedEntry: event.retainedWorkEntry,
|
|
1852
|
+
}
|
|
1853
|
+
default: {
|
|
1854
|
+
const exhaustive: never = checkpoint
|
|
1855
|
+
return exhaustive
|
|
1856
|
+
}
|
|
1857
|
+
}
|
|
1858
|
+
}
|
|
1859
|
+
|
|
1860
|
+
/**
|
|
1861
|
+
* Apply one confirmed retained-work entry to the identity record. Sequence-state entries adopt
|
|
1862
|
+
* their exact sequences and advance the dependency-outbox acknowledgement watermark; outbox
|
|
1863
|
+
* dependency events append their event sequence; destroy retires the current binding generation
|
|
1864
|
+
* and clears the current public reference, sinks, and observers (channel-generation retirement,
|
|
1865
|
+
* never public-object death).
|
|
1866
|
+
*/
|
|
1867
|
+
function applyProtocolMutationToIdentity(
|
|
1868
|
+
identity: DriverValueChannelIdentityRecordState,
|
|
1869
|
+
event: ValueHandoffProtocolMutationEvent,
|
|
1870
|
+
): DriverValueChannelIdentityRecordState {
|
|
1871
|
+
const entry = event.retainedWorkEntry
|
|
1872
|
+
switch (entry.kind) {
|
|
1873
|
+
case 'sequence-state':
|
|
1874
|
+
return {
|
|
1875
|
+
...identity,
|
|
1876
|
+
dependencyEventAckWatermark: entry.sequences.dependencyOutboxAckSequence,
|
|
1877
|
+
sequences: entry.sequences,
|
|
1878
|
+
}
|
|
1879
|
+
case 'binding-generation':
|
|
1880
|
+
if (event.kind === 'destroy') {
|
|
1881
|
+
return {
|
|
1882
|
+
...identity,
|
|
1883
|
+
publicReferenceRetained: false,
|
|
1884
|
+
sinkCount: 0,
|
|
1885
|
+
changeObserverCount: 0,
|
|
1886
|
+
currentBindingGeneration: entry.currentBindingGeneration,
|
|
1887
|
+
retiredBindingGeneration: entry.retiredBindingGeneration,
|
|
1888
|
+
}
|
|
1889
|
+
}
|
|
1890
|
+
return {
|
|
1891
|
+
...identity,
|
|
1892
|
+
currentBindingGeneration: entry.currentBindingGeneration,
|
|
1893
|
+
retiredBindingGeneration: entry.retiredBindingGeneration,
|
|
1894
|
+
}
|
|
1895
|
+
case 'target-batch-dependency-event':
|
|
1896
|
+
return {
|
|
1897
|
+
...identity,
|
|
1898
|
+
dependencyEventOutboxSequences: identity.dependencyEventOutboxSequences.includes(
|
|
1899
|
+
entry.eventSequence,
|
|
1900
|
+
)
|
|
1901
|
+
? identity.dependencyEventOutboxSequences
|
|
1902
|
+
: [...identity.dependencyEventOutboxSequences, entry.eventSequence],
|
|
1903
|
+
sequences: {
|
|
1904
|
+
...identity.sequences,
|
|
1905
|
+
batchDependencyEventSequence: entry.eventSequence,
|
|
1906
|
+
},
|
|
1907
|
+
}
|
|
1908
|
+
default:
|
|
1909
|
+
return identity
|
|
1910
|
+
}
|
|
1911
|
+
}
|
|
1912
|
+
|
|
1913
|
+
/** Append one COMMIT entry to its prefilled destination ledger slot, in place by slot identity. */
|
|
1914
|
+
function appendLedgerEntry(
|
|
1915
|
+
slot: ValueHandoffDestinationLedgerSlotState,
|
|
1916
|
+
entry: NormalizedRetainedWorkEntry,
|
|
1917
|
+
): ValueHandoffDestinationLedgerSlotState {
|
|
1918
|
+
switch (slot.ledger) {
|
|
1919
|
+
case 'PUBLIC_CONTROL':
|
|
1920
|
+
return entry.kind === 'public-control-slot'
|
|
1921
|
+
? { ...slot, entries: [...slot.entries, entry] }
|
|
1922
|
+
: slot
|
|
1923
|
+
case 'RETENTION':
|
|
1924
|
+
return entry.kind === 'retention-edge' ? { ...slot, entries: [...slot.entries, entry] } : slot
|
|
1925
|
+
case 'EXECUTION':
|
|
1926
|
+
return entry.kind === 'generator-execution'
|
|
1927
|
+
? { ...slot, entries: [...slot.entries, entry] }
|
|
1928
|
+
: slot
|
|
1929
|
+
case 'DUE_WORK':
|
|
1930
|
+
return entry.kind === 'update-task' ||
|
|
1931
|
+
entry.kind === 'carrier-task' ||
|
|
1932
|
+
entry.kind === 'direct-write'
|
|
1933
|
+
? { ...slot, entries: [...slot.entries, entry] }
|
|
1934
|
+
: slot
|
|
1935
|
+
case 'COMPLETION':
|
|
1936
|
+
return entry.kind === 'value-completion'
|
|
1937
|
+
? { ...slot, entries: [...slot.entries, entry] }
|
|
1938
|
+
: slot
|
|
1939
|
+
case 'DEPENDENCY_EVENT':
|
|
1940
|
+
return entry.kind === 'target-batch-dependency-event'
|
|
1941
|
+
? { ...slot, entries: [...slot.entries, entry] }
|
|
1942
|
+
: slot
|
|
1943
|
+
case 'GENERATION':
|
|
1944
|
+
return entry.kind === 'binding-generation'
|
|
1945
|
+
? { ...slot, entries: [...slot.entries, entry] }
|
|
1946
|
+
: slot
|
|
1947
|
+
case 'SEQUENCE':
|
|
1948
|
+
return entry.kind === 'sequence-state' ? { ...slot, entries: [...slot.entries, entry] } : slot
|
|
1949
|
+
default: {
|
|
1950
|
+
const exhaustive: never = slot
|
|
1951
|
+
return exhaustive
|
|
1952
|
+
}
|
|
1953
|
+
}
|
|
1954
|
+
}
|
|
1955
|
+
|
|
1956
|
+
/** Grow one destination ledger slot at the consumption edge when the prefix declares a new slot. */
|
|
1957
|
+
function createLedgerSlot(
|
|
1958
|
+
entry: NormalizedRetainedWorkEntry,
|
|
1959
|
+
): ValueHandoffDestinationLedgerSlotState {
|
|
1960
|
+
switch (entry.kind) {
|
|
1961
|
+
case 'public-control-slot':
|
|
1962
|
+
return { ledger: 'PUBLIC_CONTROL', slotId: entry.destinationSlot.slotId, entries: [entry] }
|
|
1963
|
+
case 'retention-edge':
|
|
1964
|
+
return { ledger: 'RETENTION', slotId: entry.destinationSlot.slotId, entries: [entry] }
|
|
1965
|
+
case 'generator-execution':
|
|
1966
|
+
return { ledger: 'EXECUTION', slotId: entry.destinationSlot.slotId, entries: [entry] }
|
|
1967
|
+
case 'update-task':
|
|
1968
|
+
case 'carrier-task':
|
|
1969
|
+
case 'direct-write':
|
|
1970
|
+
return { ledger: 'DUE_WORK', slotId: entry.destinationSlot.slotId, entries: [entry] }
|
|
1971
|
+
case 'value-completion':
|
|
1972
|
+
return { ledger: 'COMPLETION', slotId: entry.destinationSlot.slotId, entries: [entry] }
|
|
1973
|
+
case 'target-batch-dependency-event':
|
|
1974
|
+
return {
|
|
1975
|
+
ledger: 'DEPENDENCY_EVENT',
|
|
1976
|
+
slotId: entry.destinationSlot.slotId,
|
|
1977
|
+
entries: [entry],
|
|
1978
|
+
}
|
|
1979
|
+
case 'binding-generation':
|
|
1980
|
+
return { ledger: 'GENERATION', slotId: entry.destinationSlot.slotId, entries: [entry] }
|
|
1981
|
+
case 'sequence-state':
|
|
1982
|
+
return { ledger: 'SEQUENCE', slotId: entry.destinationSlot.slotId, entries: [entry] }
|
|
1983
|
+
default: {
|
|
1984
|
+
const exhaustive: never = entry
|
|
1985
|
+
return exhaustive
|
|
1986
|
+
}
|
|
1987
|
+
}
|
|
1988
|
+
}
|
|
1989
|
+
|
|
1990
|
+
function applyEntriesToLedgers(
|
|
1991
|
+
ledgers: ValueHandoffDestinationLedgerState,
|
|
1992
|
+
entries: readonly NormalizedRetainedWorkEntry[],
|
|
1993
|
+
): ValueHandoffDestinationLedgerState {
|
|
1994
|
+
let slots = ledgers.slots
|
|
1995
|
+
for (const entry of entries) {
|
|
1996
|
+
const index = slots.findIndex(
|
|
1997
|
+
(slot) =>
|
|
1998
|
+
slot.ledger === entry.destinationSlot.ledger &&
|
|
1999
|
+
slot.slotId === entry.destinationSlot.slotId,
|
|
2000
|
+
)
|
|
2001
|
+
if (index < 0) {
|
|
2002
|
+
slots = [...slots, createLedgerSlot(entry)]
|
|
2003
|
+
continue
|
|
2004
|
+
}
|
|
2005
|
+
const slot = slots[index]
|
|
2006
|
+
if (slot === undefined) continue
|
|
2007
|
+
slots = [...slots.slice(0, index), appendLedgerEntry(slot, entry), ...slots.slice(index + 1)]
|
|
2008
|
+
}
|
|
2009
|
+
return { slots }
|
|
2010
|
+
}
|
|
2011
|
+
|
|
2012
|
+
class DriverValueChannelIdentityRegistryImpl<T> implements DriverValueChannelIdentityRegistry<T> {
|
|
2013
|
+
readonly #recordsByExactValue = new WeakMap<
|
|
2014
|
+
MotionValue<T>,
|
|
2015
|
+
DriverValueChannelIdentityRegistryRecord<T>
|
|
2016
|
+
>()
|
|
2017
|
+
readonly #retainedRecordsByChannelId = new Map<
|
|
2018
|
+
number,
|
|
2019
|
+
DriverValueChannelIdentityRegistryRecord<T>
|
|
2020
|
+
>()
|
|
2021
|
+
|
|
2022
|
+
constructor(options: CreateDriverValueChannelIdentityRegistryOptions<T>) {
|
|
2023
|
+
for (const record of options.initialRecords ?? []) {
|
|
2024
|
+
this.#recordsByExactValue.set(record.value, record)
|
|
2025
|
+
if (observeDriverValueChannel(record.state).retained) {
|
|
2026
|
+
this.#retainedRecordsByChannelId.set(record.state.identity.channelId, record)
|
|
2027
|
+
}
|
|
2028
|
+
}
|
|
2029
|
+
}
|
|
2030
|
+
|
|
2031
|
+
observe(): DriverValueChannelIdentityRegistryObservation {
|
|
2032
|
+
const retainedChannelIds = [...this.#retainedRecordsByChannelId.keys()].reduce<number[]>(
|
|
2033
|
+
(ordered, channelId) => {
|
|
2034
|
+
const insertionIndex = ordered.findIndex((existing) => existing > channelId)
|
|
2035
|
+
if (insertionIndex < 0) return [...ordered, channelId]
|
|
2036
|
+
return [...ordered.slice(0, insertionIndex), channelId, ...ordered.slice(insertionIndex)]
|
|
2037
|
+
},
|
|
2038
|
+
[],
|
|
2039
|
+
)
|
|
2040
|
+
return {
|
|
2041
|
+
retainedRecordCount: this.#retainedRecordsByChannelId.size,
|
|
2042
|
+
retainedChannelIds,
|
|
2043
|
+
}
|
|
2044
|
+
}
|
|
2045
|
+
|
|
2046
|
+
peekExact(value: MotionValue<T>): DriverValueChannelIdentityRegistryRecordObservation | null {
|
|
2047
|
+
const record = this.#recordsByExactValue.get(value)
|
|
2048
|
+
return record === undefined ? null : this.#observeRecord(record)
|
|
2049
|
+
}
|
|
2050
|
+
|
|
2051
|
+
peekById(channelId: number): DriverValueChannelIdentityRegistryRecordObservation | null {
|
|
2052
|
+
const record = this.#retainedRecordsByChannelId.get(channelId)
|
|
2053
|
+
return record === undefined ? null : this.#observeRecord(record)
|
|
2054
|
+
}
|
|
2055
|
+
|
|
2056
|
+
resolveBinding(
|
|
2057
|
+
request: DriverValueChannelResolveBindingRequest<T>,
|
|
2058
|
+
): DriverValueChannelBindingResolution<T> {
|
|
2059
|
+
const existing = this.#recordsByExactValue.get(request.value)
|
|
2060
|
+
if (existing !== undefined) {
|
|
2061
|
+
// Exact-object identity is the law: conflicting caller ports, snapshots, or ids never fork
|
|
2062
|
+
// the canonical record; the binding only appends its sink registration.
|
|
2063
|
+
;(existing.sinks as DriverValueChannelSinkRegistration[]).push(request.sink)
|
|
2064
|
+
return { kind: 'eligible', record: existing, created: false, attachedSink: request.sink }
|
|
2065
|
+
}
|
|
2066
|
+
const eligibility = request.port.evaluateBindingEligibility(request.currentValue)
|
|
2067
|
+
if (eligibility.kind === 'ineligible') {
|
|
2068
|
+
return { kind: 'ineligible', eligibility }
|
|
2069
|
+
}
|
|
2070
|
+
const record: DriverValueChannelIdentityRegistryRecord<T> = {
|
|
2071
|
+
value: request.value,
|
|
2072
|
+
port: request.port,
|
|
2073
|
+
state: request.requestedState,
|
|
2074
|
+
sinks: [request.sink],
|
|
2075
|
+
}
|
|
2076
|
+
this.#recordsByExactValue.set(request.value, record)
|
|
2077
|
+
if (observeDriverValueChannel(record.state).retained) {
|
|
2078
|
+
this.#retainedRecordsByChannelId.set(record.state.identity.channelId, record)
|
|
2079
|
+
}
|
|
2080
|
+
return { kind: 'eligible', record, created: true, attachedSink: request.sink }
|
|
2081
|
+
}
|
|
2082
|
+
|
|
2083
|
+
detachBinding(
|
|
2084
|
+
request: DriverValueChannelDetachBindingRequest<T>,
|
|
2085
|
+
): DriverValueChannelIdentityRegistryRecord<T> | null {
|
|
2086
|
+
const record = this.#recordsByExactValue.get(request.value)
|
|
2087
|
+
if (record === undefined) return null
|
|
2088
|
+
const sinks = record.sinks as DriverValueChannelSinkRegistration[]
|
|
2089
|
+
let index = sinks.indexOf(request.sink)
|
|
2090
|
+
if (index < 0) {
|
|
2091
|
+
index = sinks.findIndex(
|
|
2092
|
+
(candidate) =>
|
|
2093
|
+
candidate.driverId === request.sink.driverId &&
|
|
2094
|
+
candidate.handle === request.sink.handle &&
|
|
2095
|
+
candidate.key === request.sink.key &&
|
|
2096
|
+
candidate.bindingGeneration === request.sink.bindingGeneration,
|
|
2097
|
+
)
|
|
2098
|
+
}
|
|
2099
|
+
if (index >= 0) sinks.splice(index, 1)
|
|
2100
|
+
return record
|
|
2101
|
+
}
|
|
2102
|
+
|
|
2103
|
+
rebind(request: DriverValueChannelRebindRequest<T>): DriverValueChannelRebindResult<T> {
|
|
2104
|
+
const previousRecord = this.detachBinding({
|
|
2105
|
+
value: request.previousValue,
|
|
2106
|
+
sink: request.previousSink,
|
|
2107
|
+
})
|
|
2108
|
+
if (previousRecord === null) {
|
|
2109
|
+
throw new DriverValueChannelInvariantError(
|
|
2110
|
+
'registry-rebind-previous-record-missing',
|
|
2111
|
+
'registry-rebind',
|
|
2112
|
+
)
|
|
2113
|
+
}
|
|
2114
|
+
return { previousRecord, replacement: this.resolveBinding(request.replacement) }
|
|
2115
|
+
}
|
|
2116
|
+
|
|
2117
|
+
reconcileRetention(
|
|
2118
|
+
reconciliation: DriverValueChannelRetentionReconciliation<T>,
|
|
2119
|
+
): DriverValueChannelIdentityRegistryObservation {
|
|
2120
|
+
const record = this.#recordsByExactValue.get(reconciliation.value)
|
|
2121
|
+
if (record === undefined) {
|
|
2122
|
+
throw new DriverValueChannelInvariantError(
|
|
2123
|
+
'registry-record-missing',
|
|
2124
|
+
'registry-reconcile-retention',
|
|
2125
|
+
)
|
|
2126
|
+
}
|
|
2127
|
+
const nextRecord: DriverValueChannelIdentityRegistryRecord<T> = {
|
|
2128
|
+
value: record.value,
|
|
2129
|
+
port: record.port,
|
|
2130
|
+
state: reconciliation.nextState,
|
|
2131
|
+
sinks: record.sinks,
|
|
2132
|
+
}
|
|
2133
|
+
this.#recordsByExactValue.set(record.value, nextRecord)
|
|
2134
|
+
for (const [channelId, retained] of this.#retainedRecordsByChannelId) {
|
|
2135
|
+
if (retained === record) this.#retainedRecordsByChannelId.delete(channelId)
|
|
2136
|
+
}
|
|
2137
|
+
if (observeDriverValueChannel(nextRecord.state).retained) {
|
|
2138
|
+
this.#retainedRecordsByChannelId.set(nextRecord.state.identity.channelId, nextRecord)
|
|
2139
|
+
}
|
|
2140
|
+
return this.observe()
|
|
2141
|
+
}
|
|
2142
|
+
|
|
2143
|
+
snapshotForHandoff(value: MotionValue<T>): DriverValueChannelState {
|
|
2144
|
+
const record = this.#recordsByExactValue.get(value)
|
|
2145
|
+
if (record === undefined) {
|
|
2146
|
+
throw new DriverValueChannelInvariantError(
|
|
2147
|
+
'registry-record-missing',
|
|
2148
|
+
'registry-snapshot-for-handoff',
|
|
2149
|
+
)
|
|
2150
|
+
}
|
|
2151
|
+
// Identity-local state only: the JS-global batch controller is never stored or cloned here.
|
|
2152
|
+
return record.state
|
|
2153
|
+
}
|
|
2154
|
+
|
|
2155
|
+
observeCanonicalOperation(
|
|
2156
|
+
value: MotionValue<T>,
|
|
2157
|
+
): DriverValueChannelCanonicalOperationObservation<T> {
|
|
2158
|
+
const record = this.#recordsByExactValue.get(value)
|
|
2159
|
+
if (record === undefined) {
|
|
2160
|
+
throw new DriverValueChannelInvariantError(
|
|
2161
|
+
'registry-record-missing',
|
|
2162
|
+
'registry-observe-canonical-operation',
|
|
2163
|
+
)
|
|
2164
|
+
}
|
|
2165
|
+
const port = record.port.observe()
|
|
2166
|
+
if (port.currentExecutionId !== null || !record.value.isAnimating()) {
|
|
2167
|
+
return { record, port }
|
|
2168
|
+
}
|
|
2169
|
+
// A public start that bypassed the port is still observable through the canonical record:
|
|
2170
|
+
// report the record's latest retained physical execution identity.
|
|
2171
|
+
const currentExecutionId = record.state.identity.physicalExecutionIds.at(-1) ?? null
|
|
2172
|
+
if (currentExecutionId === null) {
|
|
2173
|
+
return { record, port }
|
|
2174
|
+
}
|
|
2175
|
+
return { record, port: { ...port, currentExecutionId } }
|
|
2176
|
+
}
|
|
2177
|
+
|
|
2178
|
+
#observeRecord(
|
|
2179
|
+
record: DriverValueChannelIdentityRegistryRecord<T>,
|
|
2180
|
+
): DriverValueChannelIdentityRegistryRecordObservation {
|
|
2181
|
+
const state = observeDriverValueChannel(record.state)
|
|
2182
|
+
const port = record.port.observe()
|
|
2183
|
+
return {
|
|
2184
|
+
channelId: state.channelId,
|
|
2185
|
+
jsRecordId: state.jsRecordId,
|
|
2186
|
+
uiRecordId: state.uiRecordId,
|
|
2187
|
+
portChannelId: port.channelId,
|
|
2188
|
+
creationKind: port.creationKind,
|
|
2189
|
+
initializerProvenance: port.initializerProvenance,
|
|
2190
|
+
sinkCount: record.sinks.length,
|
|
2191
|
+
sinks: record.sinks,
|
|
2192
|
+
currentBindingGeneration: state.currentBindingGeneration,
|
|
2193
|
+
retiredBindingGeneration: state.retiredBindingGeneration,
|
|
2194
|
+
retentionReasons: state.retentionReasons,
|
|
2195
|
+
retained: state.retained,
|
|
2196
|
+
stronglyRegistered: this.#retainedRecordsByChannelId.get(state.channelId) === record,
|
|
2197
|
+
}
|
|
2198
|
+
}
|
|
2199
|
+
}
|
|
2200
|
+
|
|
2201
|
+
export function createDriverValueChannelIdentityRegistry<T>(
|
|
2202
|
+
options: CreateDriverValueChannelIdentityRegistryOptions<T> = {},
|
|
2203
|
+
): DriverValueChannelIdentityRegistry<T> {
|
|
2204
|
+
return new DriverValueChannelIdentityRegistryImpl(options)
|
|
2205
|
+
}
|
|
2206
|
+
|
|
2207
|
+
export function validateDestinationCommit(
|
|
2208
|
+
state: DriverValueChannelState,
|
|
2209
|
+
commit: ImmutableDestinationCommit,
|
|
2210
|
+
): DestinationCommitValidation {
|
|
2211
|
+
const refusal = validateTransferIdentityTuple(state, commit)
|
|
2212
|
+
if (refusal !== null) return { kind: 'rejected', reason: refusal }
|
|
2213
|
+
const fence = state.handoff.releaseFenceSequence
|
|
2214
|
+
if (fence === null || commit.releaseFenceSequence !== fence) {
|
|
2215
|
+
return { kind: 'rejected', reason: 'wrong-release-fence' }
|
|
2216
|
+
}
|
|
2217
|
+
if (commit.sentThroughAuthoritativeSequence > state.handoff.sentThroughAuthoritativeSequence) {
|
|
2218
|
+
return { kind: 'rejected', reason: 'ahead-of-sent-watermark' }
|
|
2219
|
+
}
|
|
2220
|
+
if (commit.sentThroughAuthoritativeSequence < state.handoff.sentThroughAuthoritativeSequence) {
|
|
2221
|
+
return { kind: 'rejected', reason: 'sent-watermark-regressed' }
|
|
2222
|
+
}
|
|
2223
|
+
const prefix = commit.includedPostFencePrefix
|
|
2224
|
+
for (let index = 1; index < prefix.length; index += 1) {
|
|
2225
|
+
const previous = prefix[index - 1]
|
|
2226
|
+
const current = prefix[index]
|
|
2227
|
+
if (previous === undefined || current === undefined) break
|
|
2228
|
+
if (current.authoritativeSequence <= previous.authoritativeSequence) {
|
|
2229
|
+
return { kind: 'rejected', reason: 'prefix-not-contiguous' }
|
|
2230
|
+
}
|
|
2231
|
+
}
|
|
2232
|
+
const last = prefix.at(-1)
|
|
2233
|
+
if (last !== undefined && last.authoritativeSequence > commit.sentThroughAuthoritativeSequence) {
|
|
2234
|
+
return { kind: 'rejected', reason: 'prefix-not-contiguous' }
|
|
2235
|
+
}
|
|
2236
|
+
return {
|
|
2237
|
+
kind: 'accepted',
|
|
2238
|
+
duplicate:
|
|
2239
|
+
commit.sentThroughAuthoritativeSequence <= state.handoff.drainedThroughAuthoritativeSequence,
|
|
2240
|
+
}
|
|
2241
|
+
}
|
|
2242
|
+
|
|
2243
|
+
export function validateDestinationCommitAcknowledgement(
|
|
2244
|
+
state: DriverValueChannelState,
|
|
2245
|
+
acknowledgement: DestinationCommitAcknowledgement,
|
|
2246
|
+
): DestinationCommitAcknowledgementValidation {
|
|
2247
|
+
const refusal = validateTransferIdentityTuple(state, acknowledgement)
|
|
2248
|
+
if (refusal !== null) return { kind: 'rejected', reason: refusal }
|
|
2249
|
+
const fence = state.handoff.releaseFenceSequence
|
|
2250
|
+
if (fence === null || acknowledgement.releaseFenceSequence !== fence) {
|
|
2251
|
+
return { kind: 'rejected', reason: 'wrong-release-fence' }
|
|
2252
|
+
}
|
|
2253
|
+
if (acknowledgement.drainedThroughAuthoritativeSequence < fence) {
|
|
2254
|
+
return { kind: 'rejected', reason: 'before-release-fence-watermark' }
|
|
2255
|
+
}
|
|
2256
|
+
if (
|
|
2257
|
+
acknowledgement.drainedThroughAuthoritativeSequence >
|
|
2258
|
+
state.handoff.sentThroughAuthoritativeSequence
|
|
2259
|
+
) {
|
|
2260
|
+
return { kind: 'rejected', reason: 'ahead-of-sent-watermark' }
|
|
2261
|
+
}
|
|
2262
|
+
if (
|
|
2263
|
+
acknowledgement.drainedThroughAuthoritativeSequence <=
|
|
2264
|
+
state.handoff.drainedThroughAuthoritativeSequence
|
|
2265
|
+
) {
|
|
2266
|
+
return { kind: 'accepted', extent: 'duplicate' }
|
|
2267
|
+
}
|
|
2268
|
+
if (
|
|
2269
|
+
acknowledgement.drainedThroughAuthoritativeSequence <
|
|
2270
|
+
state.handoff.sentThroughAuthoritativeSequence
|
|
2271
|
+
) {
|
|
2272
|
+
return { kind: 'accepted', extent: 'partial' }
|
|
2273
|
+
}
|
|
2274
|
+
return { kind: 'accepted', extent: 'through-current-tail' }
|
|
2275
|
+
}
|
|
2276
|
+
|
|
2277
|
+
export function produceValueHandoffSourceStatus(
|
|
2278
|
+
state: DriverValueChannelState,
|
|
2279
|
+
request: ValueHandoffSourceStatusProductionRequest,
|
|
2280
|
+
): ValueHandoffSourceStatusProductionTransition {
|
|
2281
|
+
const refusal = validateTransferIdentityTuple(state, request)
|
|
2282
|
+
if (refusal !== null) {
|
|
2283
|
+
return { state, validation: { kind: 'rejected', reason: refusal }, producedStatus: null }
|
|
2284
|
+
}
|
|
2285
|
+
const identity: ValueHandoffSourceStatusIdentity = {
|
|
2286
|
+
channelId: request.channelId,
|
|
2287
|
+
sourceRecordId: request.sourceRecordId,
|
|
2288
|
+
destinationRecordId: request.destinationRecordId,
|
|
2289
|
+
direction: request.direction,
|
|
2290
|
+
transferLease: request.transferLease,
|
|
2291
|
+
}
|
|
2292
|
+
const producedStatus: ValueHandoffSourceStatus =
|
|
2293
|
+
request.outcome === 'ABORTED'
|
|
2294
|
+
? { ...identity, kind: 'ABORTED', authoritativeSequence: request.authoritativeSequence }
|
|
2295
|
+
: { ...identity, kind: 'RELEASED', releaseFenceSequence: request.releaseFenceSequence }
|
|
2296
|
+
return { state, validation: { kind: 'accepted' }, producedStatus }
|
|
2297
|
+
}
|
|
2298
|
+
|
|
2299
|
+
function beginHandoff(
|
|
2300
|
+
state: DriverValueChannelState,
|
|
2301
|
+
event: Extract<ValueHandoffEvent, { readonly kind: 'begin-handoff' }>,
|
|
2302
|
+
): ValueHandoffTransition {
|
|
2303
|
+
const direction = event.direction
|
|
2304
|
+
const next = withHandoff(state, {
|
|
2305
|
+
...state.handoff,
|
|
2306
|
+
ownership: frozenOwnershipFor(direction),
|
|
2307
|
+
direction,
|
|
2308
|
+
transferLease: event.transferLease,
|
|
2309
|
+
phaseToken: event.phaseToken,
|
|
2310
|
+
destinationRecordId: event.destinationRecordId,
|
|
2311
|
+
destinationRuntime: event.destinationRuntime,
|
|
2312
|
+
absoluteTransferDeadlineMs: event.absoluteTransferDeadlineMs,
|
|
2313
|
+
absoluteNoConsumerDeadlineMs: event.absoluteNoConsumerDeadlineMs,
|
|
2314
|
+
releaseFenceSequence: null,
|
|
2315
|
+
preparedMutableTuple: null,
|
|
2316
|
+
sourceMutationQueue: [],
|
|
2317
|
+
commitQueue: [],
|
|
2318
|
+
})
|
|
2319
|
+
return appliedTransition(next, [{ kind: 'send-prepare', direction }])
|
|
2320
|
+
}
|
|
2321
|
+
|
|
2322
|
+
function destinationReady(
|
|
2323
|
+
state: DriverValueChannelState,
|
|
2324
|
+
event: Extract<ValueHandoffEvent, { readonly kind: 'destination-ready' }>,
|
|
2325
|
+
): ValueHandoffTransition {
|
|
2326
|
+
const handoff = state.handoff
|
|
2327
|
+
if (
|
|
2328
|
+
handoff.direction !== event.direction ||
|
|
2329
|
+
handoff.transferLease !== event.transferLease ||
|
|
2330
|
+
handoff.ownership !== frozenOwnershipFor(event.direction)
|
|
2331
|
+
) {
|
|
2332
|
+
return ignoredTransition(state)
|
|
2333
|
+
}
|
|
2334
|
+
const next = withHandoff(state, {
|
|
2335
|
+
...handoff,
|
|
2336
|
+
ownership: preparedOwnershipFor(event.direction),
|
|
2337
|
+
preparedMutableTuple: event.preparedMutableTuple,
|
|
2338
|
+
releaseFenceSequence: null,
|
|
2339
|
+
})
|
|
2340
|
+
return appliedTransition(next, [{ kind: 'request-source-release-status' }])
|
|
2341
|
+
}
|
|
2342
|
+
|
|
2343
|
+
function sourceMutation(
|
|
2344
|
+
state: DriverValueChannelState,
|
|
2345
|
+
event: Extract<ValueHandoffEvent, { readonly kind: 'source-mutation' }>,
|
|
2346
|
+
): ValueHandoffTransition {
|
|
2347
|
+
const handoff = state.handoff
|
|
2348
|
+
const entry = event.entry
|
|
2349
|
+
switch (handoff.ownership) {
|
|
2350
|
+
case 'UI_FROZEN_TO_JS':
|
|
2351
|
+
case 'JS_FROZEN_TO_UI':
|
|
2352
|
+
case 'JS_PREPARED':
|
|
2353
|
+
case 'UI_PREPARED': {
|
|
2354
|
+
const direction = handoff.direction
|
|
2355
|
+
if (direction === null) return ignoredTransition(state)
|
|
2356
|
+
// Pre-fence: the prepared clone is invalidated and the authoritative source resumes.
|
|
2357
|
+
const next = withHandoff(state, {
|
|
2358
|
+
...handoff,
|
|
2359
|
+
ownership: sourceRunningOwnershipFor(direction),
|
|
2360
|
+
releaseFenceSequence: null,
|
|
2361
|
+
preparedMutableTuple: null,
|
|
2362
|
+
commitQueue: [],
|
|
2363
|
+
})
|
|
2364
|
+
return appliedTransition(next, [
|
|
2365
|
+
{ kind: 'invalidate-prepared-clone' },
|
|
2366
|
+
{ kind: 'resume-authoritative-source', side: sourceSideFor(direction) },
|
|
2367
|
+
])
|
|
2368
|
+
}
|
|
2369
|
+
case 'UI_RELEASE_PENDING_TO_JS':
|
|
2370
|
+
case 'JS_RELEASE_PENDING_TO_UI': {
|
|
2371
|
+
// The source RELEASE/ABORT status is still unknown: sequence, never infer.
|
|
2372
|
+
const next = withHandoff(state, {
|
|
2373
|
+
...handoff,
|
|
2374
|
+
sourceMutationQueue: [...handoff.sourceMutationQueue, entry],
|
|
2375
|
+
})
|
|
2376
|
+
return appliedTransition(next, [])
|
|
2377
|
+
}
|
|
2378
|
+
case 'UI_RELEASED_TO_JS':
|
|
2379
|
+
case 'JS_RELEASED_TO_UI': {
|
|
2380
|
+
// Post-fence: the fence and lease are immutable; the mutation joins the COMMIT prefix.
|
|
2381
|
+
const next = withHandoff(state, {
|
|
2382
|
+
...handoff,
|
|
2383
|
+
sentThroughAuthoritativeSequence: entry.authoritativeSequence,
|
|
2384
|
+
commitQueue: appendEntryToCommitQueue(state, entry),
|
|
2385
|
+
})
|
|
2386
|
+
return appliedTransition(next, [])
|
|
2387
|
+
}
|
|
2388
|
+
default:
|
|
2389
|
+
return ignoredTransition(state)
|
|
2390
|
+
}
|
|
2391
|
+
}
|
|
2392
|
+
|
|
2393
|
+
function sourceStatus(
|
|
2394
|
+
state: DriverValueChannelState,
|
|
2395
|
+
event: Extract<ValueHandoffEvent, { readonly kind: 'source-status' }>,
|
|
2396
|
+
): ValueHandoffTransition {
|
|
2397
|
+
const refusal = validateTransferIdentityTuple(state, event.status)
|
|
2398
|
+
if (refusal !== null) {
|
|
2399
|
+
return {
|
|
2400
|
+
state,
|
|
2401
|
+
observation: observeDriverValueChannel(state),
|
|
2402
|
+
orderedEffects: [],
|
|
2403
|
+
outcome: { kind: 'SOURCE_STATUS_REFUSED', reason: refusal },
|
|
2404
|
+
}
|
|
2405
|
+
}
|
|
2406
|
+
const handoff = state.handoff
|
|
2407
|
+
const status = event.status
|
|
2408
|
+
const direction = status.direction
|
|
2409
|
+
if (handoff.ownership !== releasePendingOwnershipFor(direction)) {
|
|
2410
|
+
return ignoredTransition(state)
|
|
2411
|
+
}
|
|
2412
|
+
if (status.kind === 'ABORTED') {
|
|
2413
|
+
const next = withHandoff(state, {
|
|
2414
|
+
...handoff,
|
|
2415
|
+
ownership: sourceRunningOwnershipFor(direction),
|
|
2416
|
+
releaseFenceSequence: null,
|
|
2417
|
+
preparedMutableTuple: null,
|
|
2418
|
+
commitQueue: [],
|
|
2419
|
+
})
|
|
2420
|
+
return appliedTransition(next, [
|
|
2421
|
+
{ kind: 'invalidate-prepared-clone' },
|
|
2422
|
+
{ kind: 'resume-authoritative-source', side: sourceSideFor(direction) },
|
|
2423
|
+
])
|
|
2424
|
+
}
|
|
2425
|
+
// Source-confirmed RELEASE is the handoff linearization point: mutations queued behind the
|
|
2426
|
+
// source decision drain into the one immutable COMMIT, which is enqueued and activates the
|
|
2427
|
+
// declared destination turn without permitting the destination to run on its own yet.
|
|
2428
|
+
const drainedPrefix = handoff.sourceMutationQueue
|
|
2429
|
+
const sentThrough =
|
|
2430
|
+
drainedPrefix.at(-1)?.authoritativeSequence ?? handoff.sentThroughAuthoritativeSequence
|
|
2431
|
+
const commit: ImmutableDestinationCommit = {
|
|
2432
|
+
kind: 'DESTINATION_COMMIT',
|
|
2433
|
+
channelId: status.channelId,
|
|
2434
|
+
sourceRecordId: status.sourceRecordId,
|
|
2435
|
+
destinationRecordId: status.destinationRecordId,
|
|
2436
|
+
direction,
|
|
2437
|
+
transferLease: status.transferLease,
|
|
2438
|
+
releaseFenceSequence: status.releaseFenceSequence,
|
|
2439
|
+
sentThroughAuthoritativeSequence: sentThrough,
|
|
2440
|
+
includedPostFencePrefix: drainedPrefix,
|
|
2441
|
+
}
|
|
2442
|
+
const activation = destinationActivationEffect(handoff.destinationRuntime)
|
|
2443
|
+
const next = withHandoff(state, {
|
|
2444
|
+
...handoff,
|
|
2445
|
+
ownership: releasedOwnershipFor(direction),
|
|
2446
|
+
releaseFenceSequence: status.releaseFenceSequence,
|
|
2447
|
+
sentThroughAuthoritativeSequence: sentThrough,
|
|
2448
|
+
sourceMutationQueue: [],
|
|
2449
|
+
commitQueue: [commit],
|
|
2450
|
+
})
|
|
2451
|
+
return appliedTransition(
|
|
2452
|
+
next,
|
|
2453
|
+
activation === null
|
|
2454
|
+
? [{ kind: 'enqueue-immutable-destination-commit' }]
|
|
2455
|
+
: [{ kind: 'enqueue-immutable-destination-commit' }, activation],
|
|
2456
|
+
)
|
|
2457
|
+
}
|
|
2458
|
+
|
|
2459
|
+
function enqueueDestinationCommit(
|
|
2460
|
+
state: DriverValueChannelState,
|
|
2461
|
+
event: Extract<ValueHandoffEvent, { readonly kind: 'enqueue-destination-commit' }>,
|
|
2462
|
+
): ValueHandoffTransition {
|
|
2463
|
+
const handoff = state.handoff
|
|
2464
|
+
const activation = destinationActivationEffect(handoff.destinationRuntime)
|
|
2465
|
+
const next = withHandoff(state, {
|
|
2466
|
+
...handoff,
|
|
2467
|
+
commitQueue: [...handoff.commitQueue, event.commit],
|
|
2468
|
+
})
|
|
2469
|
+
return appliedTransition(
|
|
2470
|
+
next,
|
|
2471
|
+
activation === null
|
|
2472
|
+
? [{ kind: 'enqueue-immutable-destination-commit' }]
|
|
2473
|
+
: [{ kind: 'enqueue-immutable-destination-commit' }, activation],
|
|
2474
|
+
)
|
|
2475
|
+
}
|
|
2476
|
+
|
|
2477
|
+
function destinationCommitAcknowledgement(
|
|
2478
|
+
state: DriverValueChannelState,
|
|
2479
|
+
event: Extract<ValueHandoffEvent, { readonly kind: 'destination-commit-acknowledgement' }>,
|
|
2480
|
+
): ValueHandoffTransition {
|
|
2481
|
+
const validation = validateDestinationCommitAcknowledgement(state, event.acknowledgement)
|
|
2482
|
+
if (validation.kind === 'rejected' || validation.extent === 'duplicate') {
|
|
2483
|
+
// Malformed candidates and completed-prefix duplicates are inert: cleanup never repeats.
|
|
2484
|
+
return ignoredTransition(state)
|
|
2485
|
+
}
|
|
2486
|
+
const handoff = state.handoff
|
|
2487
|
+
const acknowledgement = event.acknowledgement
|
|
2488
|
+
if (validation.extent === 'partial') {
|
|
2489
|
+
const current = handoff.commitQueue.at(-1)
|
|
2490
|
+
if (current === undefined) return ignoredTransition(state)
|
|
2491
|
+
// A partial acknowledgement rebuilds and re-enqueues the same immutable COMMIT shape with
|
|
2492
|
+
// only the remaining ordered suffix under the same lease, fence, and activation rule.
|
|
2493
|
+
const suffix = current.includedPostFencePrefix.filter(
|
|
2494
|
+
(entry) => entry.authoritativeSequence > acknowledgement.drainedThroughAuthoritativeSequence,
|
|
2495
|
+
)
|
|
2496
|
+
const firstSuffix = suffix[0]
|
|
2497
|
+
const effects: ValueHandoffEffect[] = [
|
|
2498
|
+
{
|
|
2499
|
+
kind: 'rebuild-same-commit-with-suffix',
|
|
2500
|
+
suffixStartSequence:
|
|
2501
|
+
firstSuffix?.authoritativeSequence ?? acknowledgement.drainedThroughAuthoritativeSequence,
|
|
2502
|
+
},
|
|
2503
|
+
]
|
|
2504
|
+
const activation = destinationActivationEffect(handoff.destinationRuntime)
|
|
2505
|
+
if (activation !== null) effects.push(activation)
|
|
2506
|
+
const next = withHandoff(state, {
|
|
2507
|
+
...handoff,
|
|
2508
|
+
drainedThroughAuthoritativeSequence: acknowledgement.drainedThroughAuthoritativeSequence,
|
|
2509
|
+
commitQueue: [
|
|
2510
|
+
...handoff.commitQueue.slice(0, -1),
|
|
2511
|
+
{ ...current, includedPostFencePrefix: suffix },
|
|
2512
|
+
],
|
|
2513
|
+
})
|
|
2514
|
+
return appliedTransition(next, effects)
|
|
2515
|
+
}
|
|
2516
|
+
// Acknowledged through the current tail: cancel the timer, release every retained protocol
|
|
2517
|
+
// resource exactly once, then permit the destination to run or reverse.
|
|
2518
|
+
const next = withHandoff(state, {
|
|
2519
|
+
...handoff,
|
|
2520
|
+
ownership: destinationRunningOwnershipFor(acknowledgement.direction),
|
|
2521
|
+
drainedThroughAuthoritativeSequence: acknowledgement.drainedThroughAuthoritativeSequence,
|
|
2522
|
+
commitQueue: [],
|
|
2523
|
+
resources: releasedResourceState(),
|
|
2524
|
+
})
|
|
2525
|
+
return appliedTransition(next, [
|
|
2526
|
+
{ kind: 'cancel-transfer-timer' },
|
|
2527
|
+
{ kind: 'release-prepared-state' },
|
|
2528
|
+
{ kind: 'release-prepared-clone' },
|
|
2529
|
+
{ kind: 'release-transfer-activity' },
|
|
2530
|
+
{ kind: 'permit-destination-run-or-reverse' },
|
|
2531
|
+
])
|
|
2532
|
+
}
|
|
2533
|
+
|
|
2534
|
+
function phaseTimeout(
|
|
2535
|
+
state: DriverValueChannelState,
|
|
2536
|
+
event: Extract<ValueHandoffEvent, { readonly kind: 'phase-timeout' }>,
|
|
2537
|
+
): ValueHandoffTransition {
|
|
2538
|
+
const handoff = state.handoff
|
|
2539
|
+
if (
|
|
2540
|
+
handoff.direction !== event.direction ||
|
|
2541
|
+
handoff.transferLease !== event.transferLease ||
|
|
2542
|
+
handoff.phaseToken !== event.phaseToken
|
|
2543
|
+
) {
|
|
2544
|
+
// A timeout from a retired lease is stale; the successor lease is never touched.
|
|
2545
|
+
return ignoredTransition(state)
|
|
2546
|
+
}
|
|
2547
|
+
const direction = event.direction
|
|
2548
|
+
switch (handoff.ownership) {
|
|
2549
|
+
case 'UI_FROZEN_TO_JS':
|
|
2550
|
+
case 'JS_FROZEN_TO_UI':
|
|
2551
|
+
case 'JS_PREPARED':
|
|
2552
|
+
case 'UI_PREPARED': {
|
|
2553
|
+
const next = withHandoff(state, {
|
|
2554
|
+
...handoff,
|
|
2555
|
+
ownership: sourceRunningOwnershipFor(direction),
|
|
2556
|
+
releaseFenceSequence: null,
|
|
2557
|
+
preparedMutableTuple: null,
|
|
2558
|
+
commitQueue: [],
|
|
2559
|
+
})
|
|
2560
|
+
return appliedTransition(next, [
|
|
2561
|
+
{ kind: 'invalidate-prepared-clone' },
|
|
2562
|
+
{ kind: 'resume-authoritative-source', side: sourceSideFor(direction) },
|
|
2563
|
+
])
|
|
2564
|
+
}
|
|
2565
|
+
case 'UI_RELEASE_PENDING_TO_JS':
|
|
2566
|
+
case 'JS_RELEASE_PENDING_TO_UI':
|
|
2567
|
+
// The source status is still unknown: retransmit only the query, never guess ownership.
|
|
2568
|
+
return appliedTransition(state, [{ kind: 'request-source-release-status' }])
|
|
2569
|
+
case 'UI_RELEASED_TO_JS':
|
|
2570
|
+
case 'JS_RELEASED_TO_UI': {
|
|
2571
|
+
const activation = destinationActivationEffect(handoff.destinationRuntime)
|
|
2572
|
+
return appliedTransition(
|
|
2573
|
+
state,
|
|
2574
|
+
activation === null
|
|
2575
|
+
? [{ kind: 'enqueue-immutable-destination-commit' }]
|
|
2576
|
+
: [{ kind: 'enqueue-immutable-destination-commit' }, activation],
|
|
2577
|
+
)
|
|
2578
|
+
}
|
|
2579
|
+
default:
|
|
2580
|
+
return ignoredTransition(state)
|
|
2581
|
+
}
|
|
2582
|
+
}
|
|
2583
|
+
|
|
2584
|
+
function boundedTerminal(
|
|
2585
|
+
state: DriverValueChannelState,
|
|
2586
|
+
event: Extract<ValueHandoffEvent, { readonly kind: 'bounded-terminal' }>,
|
|
2587
|
+
): ValueHandoffTransition {
|
|
2588
|
+
const handoff = state.handoff
|
|
2589
|
+
if (
|
|
2590
|
+
handoff.direction !== event.direction ||
|
|
2591
|
+
handoff.transferLease !== event.transferLease ||
|
|
2592
|
+
handoff.phaseToken !== event.phaseToken ||
|
|
2593
|
+
handoff.ownership !== releasedOwnershipFor(event.direction)
|
|
2594
|
+
) {
|
|
2595
|
+
return ignoredTransition(state)
|
|
2596
|
+
}
|
|
2597
|
+
// Expiry after confirmed release enters the bounded-refusal drain with every protocol resource
|
|
2598
|
+
// still retained; cleanup waits for the retained pin-visible work to finish.
|
|
2599
|
+
const next = withHandoff(state, { ...handoff, ownership: 'BOUNDED_REFUSAL_DRAINING' })
|
|
2600
|
+
return appliedTransition(next, [
|
|
2601
|
+
{ kind: 'enter-change-host-silent-drain' },
|
|
2602
|
+
{ kind: 'refuse-live-channel-property' },
|
|
2603
|
+
])
|
|
2604
|
+
}
|
|
2605
|
+
|
|
2606
|
+
function finishBoundedRefusalDrain(
|
|
2607
|
+
state: DriverValueChannelState,
|
|
2608
|
+
event: Extract<ValueHandoffEvent, { readonly kind: 'finish-bounded-refusal-drain' }>,
|
|
2609
|
+
): ValueHandoffTransition {
|
|
2610
|
+
const handoff = state.handoff
|
|
2611
|
+
if (
|
|
2612
|
+
handoff.direction !== event.direction ||
|
|
2613
|
+
handoff.transferLease !== event.transferLease ||
|
|
2614
|
+
handoff.ownership !== 'BOUNDED_REFUSAL_DRAINING'
|
|
2615
|
+
) {
|
|
2616
|
+
return ignoredTransition(state)
|
|
2617
|
+
}
|
|
2618
|
+
const next = withHandoff(state, {
|
|
2619
|
+
...handoff,
|
|
2620
|
+
ownership: 'TERMINAL_REFUSED',
|
|
2621
|
+
resources: releasedResourceState(),
|
|
2622
|
+
})
|
|
2623
|
+
return appliedTransition(next, [
|
|
2624
|
+
{ kind: 'cancel-transfer-timer' },
|
|
2625
|
+
{ kind: 'release-prepared-state' },
|
|
2626
|
+
{ kind: 'release-prepared-clone' },
|
|
2627
|
+
{ kind: 'release-transfer-activity' },
|
|
2628
|
+
{ kind: 'finalize-bounded-refusal-without-run-or-reverse' },
|
|
2629
|
+
])
|
|
2630
|
+
}
|
|
2631
|
+
|
|
2632
|
+
export function advanceValueHandoff(
|
|
2633
|
+
state: DriverValueChannelState,
|
|
2634
|
+
event: ValueHandoffEvent,
|
|
2635
|
+
now: number,
|
|
2636
|
+
): ValueHandoffTransition {
|
|
2637
|
+
void now
|
|
2638
|
+
switch (event.kind) {
|
|
2639
|
+
case 'begin-handoff':
|
|
2640
|
+
return beginHandoff(state, event)
|
|
2641
|
+
case 'destination-ready':
|
|
2642
|
+
return destinationReady(state, event)
|
|
2643
|
+
case 'source-mutation':
|
|
2644
|
+
return sourceMutation(state, event)
|
|
2645
|
+
case 'source-status':
|
|
2646
|
+
return sourceStatus(state, event)
|
|
2647
|
+
case 'enqueue-destination-commit':
|
|
2648
|
+
return enqueueDestinationCommit(state, event)
|
|
2649
|
+
case 'destination-commit-acknowledgement':
|
|
2650
|
+
return destinationCommitAcknowledgement(state, event)
|
|
2651
|
+
case 'phase-timeout':
|
|
2652
|
+
return phaseTimeout(state, event)
|
|
2653
|
+
case 'bounded-terminal':
|
|
2654
|
+
return boundedTerminal(state, event)
|
|
2655
|
+
case 'finish-bounded-refusal-drain':
|
|
2656
|
+
return finishBoundedRefusalDrain(state, event)
|
|
2657
|
+
default: {
|
|
2658
|
+
// An untyped/JS caller can reach this with a kind outside the union: fail closed with the
|
|
2659
|
+
// typed invariant (never return the event as a transition — that would corrupt state).
|
|
2660
|
+
const exhaustive: never = event
|
|
2661
|
+
void exhaustive
|
|
2662
|
+
throw new DriverValueChannelInvariantError('handoff-event-kind-unrecognized')
|
|
2663
|
+
}
|
|
2664
|
+
}
|
|
2665
|
+
}
|
|
2666
|
+
|
|
2667
|
+
export function advanceValueHandoffPhaseEvent(
|
|
2668
|
+
state: DriverValueChannelState,
|
|
2669
|
+
envelope: ValueHandoffPhaseEventEnvelope,
|
|
2670
|
+
now: number,
|
|
2671
|
+
): ValueHandoffPhaseEventTransition {
|
|
2672
|
+
void now
|
|
2673
|
+
const { phase, event } = envelope
|
|
2674
|
+
requireValueHandoffPhase(phase)
|
|
2675
|
+
requirePhaseEventKind(event.kind)
|
|
2676
|
+
if (isProtocolReadEvent(event)) {
|
|
2677
|
+
return {
|
|
2678
|
+
state,
|
|
2679
|
+
observation: observeDriverValueChannel(state),
|
|
2680
|
+
disposition: {
|
|
2681
|
+
kind: 'SYNCHRONOUS_READ_WITHOUT_PROTOCOL_MUTATION',
|
|
2682
|
+
phase,
|
|
2683
|
+
eventKind: event.kind,
|
|
2684
|
+
view: readViewForPhase(phase),
|
|
2685
|
+
history: state.identity.history,
|
|
2686
|
+
},
|
|
2687
|
+
orderedEffects: [],
|
|
2688
|
+
}
|
|
2689
|
+
}
|
|
2690
|
+
if (phase === 'RELEASED_POST_FENCE') {
|
|
2691
|
+
// Post-fence: the fence is immutable; the authored suffix rides the same COMMIT shape.
|
|
2692
|
+
const current = state.handoff.commitQueue.at(-1)
|
|
2693
|
+
const commit: ImmutableDestinationCommit = {
|
|
2694
|
+
kind: 'DESTINATION_COMMIT',
|
|
2695
|
+
channelId: state.identity.channelId,
|
|
2696
|
+
sourceRecordId: state.handoff.sourceRecordId ?? -1,
|
|
2697
|
+
destinationRecordId: state.handoff.destinationRecordId ?? -1,
|
|
2698
|
+
direction: envelope.direction,
|
|
2699
|
+
transferLease: envelope.transferLease,
|
|
2700
|
+
releaseFenceSequence:
|
|
2701
|
+
state.handoff.releaseFenceSequence ?? current?.releaseFenceSequence ?? -1,
|
|
2702
|
+
sentThroughAuthoritativeSequence: event.authoritativeSequence,
|
|
2703
|
+
includedPostFencePrefix: current?.includedPostFencePrefix ?? [],
|
|
2704
|
+
}
|
|
2705
|
+
const next = withHandoff(state, {
|
|
2706
|
+
...state.handoff,
|
|
2707
|
+
sentThroughAuthoritativeSequence: event.authoritativeSequence,
|
|
2708
|
+
})
|
|
2709
|
+
return {
|
|
2710
|
+
state: next,
|
|
2711
|
+
observation: observeDriverValueChannel(next),
|
|
2712
|
+
disposition: {
|
|
2713
|
+
kind: 'APPEND_IMMUTABLE_COMMIT_SUFFIX',
|
|
2714
|
+
phase,
|
|
2715
|
+
eventKind: event.kind,
|
|
2716
|
+
commit,
|
|
2717
|
+
authoredSuffix: [event],
|
|
2718
|
+
},
|
|
2719
|
+
orderedEffects: [],
|
|
2720
|
+
}
|
|
2721
|
+
}
|
|
2722
|
+
const direction = envelope.direction
|
|
2723
|
+
const status: ValueHandoffSourceStatus = {
|
|
2724
|
+
kind: 'ABORTED',
|
|
2725
|
+
channelId: state.identity.channelId,
|
|
2726
|
+
sourceRecordId: state.handoff.sourceRecordId ?? -1,
|
|
2727
|
+
destinationRecordId: state.handoff.destinationRecordId ?? -1,
|
|
2728
|
+
direction,
|
|
2729
|
+
transferLease: envelope.transferLease,
|
|
2730
|
+
authoritativeSequence: event.authoritativeSequence,
|
|
2731
|
+
}
|
|
2732
|
+
const next = withHandoff(state, {
|
|
2733
|
+
...state.handoff,
|
|
2734
|
+
ownership: frozenOwnershipFor(direction),
|
|
2735
|
+
releaseFenceSequence: null,
|
|
2736
|
+
preparedMutableTuple: null,
|
|
2737
|
+
commitQueue: [],
|
|
2738
|
+
})
|
|
2739
|
+
return {
|
|
2740
|
+
state: next,
|
|
2741
|
+
observation: observeDriverValueChannel(next),
|
|
2742
|
+
disposition: { kind: 'ABORT_AND_REPREPARE', phase, eventKind: event.kind, status },
|
|
2743
|
+
orderedEffects: [
|
|
2744
|
+
{ kind: 'invalidate-prepared-clone' },
|
|
2745
|
+
{ kind: 'resume-authoritative-source', side: sourceSideFor(direction) },
|
|
2746
|
+
{ kind: 'send-prepare', direction },
|
|
2747
|
+
],
|
|
2748
|
+
}
|
|
2749
|
+
}
|
|
2750
|
+
|
|
2751
|
+
function applyProtocolCheckpointMutation(
|
|
2752
|
+
state: DriverValueChannelState,
|
|
2753
|
+
envelope: ValueHandoffProtocolCheckpointEnvelope,
|
|
2754
|
+
event: ValueHandoffProtocolMutationEvent,
|
|
2755
|
+
): {
|
|
2756
|
+
readonly state: DriverValueChannelState
|
|
2757
|
+
readonly orderedEffects: readonly ValueHandoffEffect[]
|
|
2758
|
+
} {
|
|
2759
|
+
const handoff = state.handoff
|
|
2760
|
+
const direction = envelope.direction
|
|
2761
|
+
switch (envelope.checkpoint) {
|
|
2762
|
+
case 'FREEZE':
|
|
2763
|
+
return { state, orderedEffects: [{ kind: 'send-prepare', direction }] }
|
|
2764
|
+
case 'PREPARE_READY': {
|
|
2765
|
+
const next = withHandoff(state, {
|
|
2766
|
+
...handoff,
|
|
2767
|
+
ownership: frozenOwnershipFor(direction),
|
|
2768
|
+
releaseFenceSequence: null,
|
|
2769
|
+
preparedMutableTuple: null,
|
|
2770
|
+
commitQueue: [],
|
|
2771
|
+
})
|
|
2772
|
+
return {
|
|
2773
|
+
state: next,
|
|
2774
|
+
orderedEffects: [
|
|
2775
|
+
{ kind: 'invalidate-prepared-clone' },
|
|
2776
|
+
{ kind: 'send-prepare', direction },
|
|
2777
|
+
],
|
|
2778
|
+
}
|
|
2779
|
+
}
|
|
2780
|
+
case 'RELEASE_PENDING_SOURCE_UNKNOWN': {
|
|
2781
|
+
const next = withHandoff(state, {
|
|
2782
|
+
...handoff,
|
|
2783
|
+
sourceMutationQueue: [...handoff.sourceMutationQueue, event.retainedWorkEntry],
|
|
2784
|
+
})
|
|
2785
|
+
return { state: next, orderedEffects: [] }
|
|
2786
|
+
}
|
|
2787
|
+
case 'SOURCE_CONFIRMED_ABORTED':
|
|
2788
|
+
return {
|
|
2789
|
+
state: {
|
|
2790
|
+
identity: applyProtocolMutationToIdentity(state.identity, event),
|
|
2791
|
+
handoff,
|
|
2792
|
+
},
|
|
2793
|
+
orderedEffects: [{ kind: 'send-prepare', direction }],
|
|
2794
|
+
}
|
|
2795
|
+
case 'SOURCE_CONFIRMED_RELEASED':
|
|
2796
|
+
case 'DESTINATION_COMMIT_CONSUMPTION': {
|
|
2797
|
+
const next = {
|
|
2798
|
+
identity: applyProtocolMutationToIdentity(state.identity, event),
|
|
2799
|
+
handoff: {
|
|
2800
|
+
...handoff,
|
|
2801
|
+
sentThroughAuthoritativeSequence: event.retainedWorkEntry.authoritativeSequence,
|
|
2802
|
+
commitQueue: appendEntryToCommitQueue(state, event.retainedWorkEntry),
|
|
2803
|
+
},
|
|
2804
|
+
}
|
|
2805
|
+
return { state: next, orderedEffects: [] }
|
|
2806
|
+
}
|
|
2807
|
+
case 'DESTINATION_ACKNOWLEDGED':
|
|
2808
|
+
return {
|
|
2809
|
+
state: {
|
|
2810
|
+
identity: applyProtocolMutationToIdentity(state.identity, event),
|
|
2811
|
+
handoff,
|
|
2812
|
+
},
|
|
2813
|
+
orderedEffects: [],
|
|
2814
|
+
}
|
|
2815
|
+
default: {
|
|
2816
|
+
const exhaustive: never = envelope.checkpoint
|
|
2817
|
+
return exhaustive
|
|
2818
|
+
}
|
|
2819
|
+
}
|
|
2820
|
+
}
|
|
2821
|
+
|
|
2822
|
+
export function advanceValueHandoffProtocolCheckpoint(
|
|
2823
|
+
state: DriverValueChannelState,
|
|
2824
|
+
envelope: ValueHandoffProtocolCheckpointEnvelope,
|
|
2825
|
+
now: number,
|
|
2826
|
+
): ValueHandoffProtocolCheckpointTransition {
|
|
2827
|
+
void now
|
|
2828
|
+
const { checkpoint, event } = envelope
|
|
2829
|
+
requireProtocolCheckpoint(checkpoint)
|
|
2830
|
+
requireProtocolEventKind(event.kind)
|
|
2831
|
+
if (isProtocolReadEvent(event)) {
|
|
2832
|
+
return {
|
|
2833
|
+
state,
|
|
2834
|
+
observation: observeDriverValueChannel(state),
|
|
2835
|
+
disposition: {
|
|
2836
|
+
kind: 'SYNCHRONOUS_READ_WITHOUT_PROTOCOL_MUTATION',
|
|
2837
|
+
checkpoint,
|
|
2838
|
+
eventKind: event.kind,
|
|
2839
|
+
authority: readAuthorityForCheckpoint(checkpoint),
|
|
2840
|
+
history: state.identity.history,
|
|
2841
|
+
},
|
|
2842
|
+
orderedEffects: [],
|
|
2843
|
+
}
|
|
2844
|
+
}
|
|
2845
|
+
const mutation = applyProtocolCheckpointMutation(state, envelope, event)
|
|
2846
|
+
return {
|
|
2847
|
+
state: mutation.state,
|
|
2848
|
+
observation: observeDriverValueChannel(mutation.state),
|
|
2849
|
+
disposition: {
|
|
2850
|
+
kind: 'MUTATION',
|
|
2851
|
+
checkpoint,
|
|
2852
|
+
eventKind: event.kind,
|
|
2853
|
+
operation: event,
|
|
2854
|
+
handling: mutationHandlingForCheckpoint(checkpoint),
|
|
2855
|
+
retainedWorkDisposition: retainedWorkDispositionFor(checkpoint, event),
|
|
2856
|
+
absoluteTransferDeadlineMs: state.handoff.absoluteTransferDeadlineMs ?? -1,
|
|
2857
|
+
absoluteNoConsumerDeadlineMs: state.handoff.absoluteNoConsumerDeadlineMs ?? -1,
|
|
2858
|
+
resources: state.handoff.resources,
|
|
2859
|
+
},
|
|
2860
|
+
orderedEffects: mutation.orderedEffects,
|
|
2861
|
+
}
|
|
2862
|
+
}
|
|
2863
|
+
|
|
2864
|
+
function refuseJsDestinationCommit(
|
|
2865
|
+
state: JsDestinationCommitConsumeState,
|
|
2866
|
+
request: JsDestinationCommitConsumeRequest,
|
|
2867
|
+
reason: JsDestinationCommitRefusalReason,
|
|
2868
|
+
priorEffects: readonly JsDestinationCommitEffect[],
|
|
2869
|
+
envelope: JsDestinationCommitEnvelopeState | null,
|
|
2870
|
+
): JsDestinationCommitTransition {
|
|
2871
|
+
if (envelope === null) {
|
|
2872
|
+
// The envelope never began: refuse without begin/seal or any destination mutation.
|
|
2873
|
+
return {
|
|
2874
|
+
state,
|
|
2875
|
+
outcome: { kind: 'REFUSED', commitSlotId: request.commitSlotId, reason },
|
|
2876
|
+
orderedEffects: [
|
|
2877
|
+
{
|
|
2878
|
+
kind: 'refuse-malformed-js-destination-commit',
|
|
2879
|
+
commitSlotId: request.commitSlotId,
|
|
2880
|
+
reason,
|
|
2881
|
+
},
|
|
2882
|
+
],
|
|
2883
|
+
}
|
|
2884
|
+
}
|
|
2885
|
+
const sealedEnvelope: JsDestinationCommitEnvelopeState = {
|
|
2886
|
+
...envelope,
|
|
2887
|
+
state: 'SEALED',
|
|
2888
|
+
sealCount: envelope.sealCount + 1,
|
|
2889
|
+
}
|
|
2890
|
+
return {
|
|
2891
|
+
state: {
|
|
2892
|
+
preparedDestinationRecords: state.preparedDestinationRecords,
|
|
2893
|
+
acknowledgementSlots: state.acknowledgementSlots,
|
|
2894
|
+
envelope: sealedEnvelope,
|
|
2895
|
+
},
|
|
2896
|
+
outcome: { kind: 'REFUSED', commitSlotId: request.commitSlotId, reason },
|
|
2897
|
+
orderedEffects: [
|
|
2898
|
+
...priorEffects,
|
|
2899
|
+
{
|
|
2900
|
+
kind: 'refuse-malformed-js-destination-commit',
|
|
2901
|
+
commitSlotId: request.commitSlotId,
|
|
2902
|
+
reason,
|
|
2903
|
+
},
|
|
2904
|
+
{
|
|
2905
|
+
kind: 'seal-js-destination-envelope-once',
|
|
2906
|
+
mailboxDrainId: sealedEnvelope.mailboxDrainId,
|
|
2907
|
+
entryCount: 0,
|
|
2908
|
+
},
|
|
2909
|
+
],
|
|
2910
|
+
}
|
|
2911
|
+
}
|
|
2912
|
+
|
|
2913
|
+
function validateJsCommitAgainstRecord(
|
|
2914
|
+
record: JsPreparedDestinationRecordState,
|
|
2915
|
+
commit: ImmutableDestinationCommit,
|
|
2916
|
+
): JsDestinationCommitRefusalReason | null {
|
|
2917
|
+
if (commit.channelId !== record.channelId) return 'wrong-channel'
|
|
2918
|
+
if (commit.sourceRecordId !== record.sourceRecordId) return 'wrong-source-record'
|
|
2919
|
+
if (commit.destinationRecordId !== record.destinationRecordId) return 'wrong-destination-record'
|
|
2920
|
+
if (commit.direction !== record.direction) return 'wrong-direction'
|
|
2921
|
+
if (commit.transferLease !== record.transferLease) return 'wrong-transfer-lease'
|
|
2922
|
+
if (commit.releaseFenceSequence !== record.releaseFenceSequence) return 'wrong-release-fence'
|
|
2923
|
+
if (
|
|
2924
|
+
commit.includedPostFencePrefix.some(
|
|
2925
|
+
(entry) => entry.authoritativeSequence > commit.sentThroughAuthoritativeSequence,
|
|
2926
|
+
)
|
|
2927
|
+
) {
|
|
2928
|
+
return 'ahead-of-sent-watermark'
|
|
2929
|
+
}
|
|
2930
|
+
if (commit.sentThroughAuthoritativeSequence < record.adoptedSentThroughAuthoritativeSequence) {
|
|
2931
|
+
return 'sent-watermark-regressed'
|
|
2932
|
+
}
|
|
2933
|
+
const newEntries = commit.includedPostFencePrefix.filter(
|
|
2934
|
+
(entry) => entry.authoritativeSequence > record.appliedThroughAuthoritativeSequence,
|
|
2935
|
+
)
|
|
2936
|
+
for (let index = 0; index < newEntries.length; index += 1) {
|
|
2937
|
+
const entry = newEntries[index]
|
|
2938
|
+
if (entry === undefined) break
|
|
2939
|
+
if (entry.authoritativeSequence !== record.appliedThroughAuthoritativeSequence + index + 1) {
|
|
2940
|
+
return 'prefix-not-contiguous'
|
|
2941
|
+
}
|
|
2942
|
+
}
|
|
2943
|
+
return null
|
|
2944
|
+
}
|
|
2945
|
+
|
|
2946
|
+
export function consumeJsDestinationCommit(
|
|
2947
|
+
state: JsDestinationCommitConsumeState,
|
|
2948
|
+
request: JsDestinationCommitConsumeRequest,
|
|
2949
|
+
): JsDestinationCommitTransition {
|
|
2950
|
+
const envelope = state.envelope
|
|
2951
|
+
if (envelope.root !== request.root) {
|
|
2952
|
+
return refuseJsDestinationCommit(state, request, 'root-mismatch', [], null)
|
|
2953
|
+
}
|
|
2954
|
+
if (envelope.state !== 'NOT_BEGUN') {
|
|
2955
|
+
return refuseJsDestinationCommit(state, request, 'envelope-already-begun', [], null)
|
|
2956
|
+
}
|
|
2957
|
+
const commit = request.commit
|
|
2958
|
+
const openEnvelope: JsDestinationCommitEnvelopeState = {
|
|
2959
|
+
...envelope,
|
|
2960
|
+
state: 'OPEN',
|
|
2961
|
+
beginCount: envelope.beginCount + 1,
|
|
2962
|
+
}
|
|
2963
|
+
const effects: JsDestinationCommitEffect[] = [
|
|
2964
|
+
{ kind: 'begin-js-destination-envelope', root: request.root },
|
|
2965
|
+
{
|
|
2966
|
+
kind: 'lookup-js-prepared-destination-record',
|
|
2967
|
+
destinationRecordId: commit.destinationRecordId,
|
|
2968
|
+
},
|
|
2969
|
+
]
|
|
2970
|
+
// The runtime keys its prepared records by its own destination record id; a commit naming an
|
|
2971
|
+
// unknown destination id for a known channel still resolves the channel's authority so the
|
|
2972
|
+
// validator can diagnose the exact tuple field rather than report a missing record.
|
|
2973
|
+
const record =
|
|
2974
|
+
state.preparedDestinationRecords.find(
|
|
2975
|
+
(candidate) => candidate.destinationRecordId === commit.destinationRecordId,
|
|
2976
|
+
) ??
|
|
2977
|
+
state.preparedDestinationRecords.find((candidate) => candidate.channelId === commit.channelId)
|
|
2978
|
+
if (record === undefined) {
|
|
2979
|
+
return refuseJsDestinationCommit(
|
|
2980
|
+
state,
|
|
2981
|
+
request,
|
|
2982
|
+
'missing-prepared-destination-record',
|
|
2983
|
+
effects,
|
|
2984
|
+
openEnvelope,
|
|
2985
|
+
)
|
|
2986
|
+
}
|
|
2987
|
+
effects.push({
|
|
2988
|
+
kind: 'validate-js-immutable-destination-commit',
|
|
2989
|
+
commitSlotId: request.commitSlotId,
|
|
2990
|
+
})
|
|
2991
|
+
const refusal = validateJsCommitAgainstRecord(record, commit)
|
|
2992
|
+
if (refusal !== null) {
|
|
2993
|
+
return refuseJsDestinationCommit(state, request, refusal, effects, openEnvelope)
|
|
2994
|
+
}
|
|
2995
|
+
const acknowledgementSlot = state.acknowledgementSlots.find(
|
|
2996
|
+
(candidate) => candidate.acknowledgementSlotId === request.acknowledgementSlotId,
|
|
2997
|
+
)
|
|
2998
|
+
if (acknowledgementSlot === undefined) {
|
|
2999
|
+
return refuseJsDestinationCommit(
|
|
3000
|
+
state,
|
|
3001
|
+
request,
|
|
3002
|
+
'missing-acknowledgement-slot',
|
|
3003
|
+
effects,
|
|
3004
|
+
openEnvelope,
|
|
3005
|
+
)
|
|
3006
|
+
}
|
|
3007
|
+
|
|
3008
|
+
// Validation is complete before any entry applies: only the entries beyond the applied
|
|
3009
|
+
// watermark are new; a fully-applied candidate dedupes without a second application.
|
|
3010
|
+
const newEntries = commit.includedPostFencePrefix.filter(
|
|
3011
|
+
(entry) => entry.authoritativeSequence > record.appliedThroughAuthoritativeSequence,
|
|
3012
|
+
)
|
|
3013
|
+
const appliedThrough =
|
|
3014
|
+
newEntries.at(-1)?.authoritativeSequence ?? record.appliedThroughAuthoritativeSequence
|
|
3015
|
+
const acknowledgement: DestinationCommitAcknowledgement = {
|
|
3016
|
+
kind: 'DESTINATION_COMMIT_ACKNOWLEDGEMENT',
|
|
3017
|
+
channelId: commit.channelId,
|
|
3018
|
+
sourceRecordId: commit.sourceRecordId,
|
|
3019
|
+
destinationRecordId: commit.destinationRecordId,
|
|
3020
|
+
direction: commit.direction,
|
|
3021
|
+
transferLease: commit.transferLease,
|
|
3022
|
+
releaseFenceSequence: commit.releaseFenceSequence,
|
|
3023
|
+
drainedThroughAuthoritativeSequence: appliedThrough,
|
|
3024
|
+
}
|
|
3025
|
+
const duplicate = newEntries.length === 0
|
|
3026
|
+
let nextRecord = record
|
|
3027
|
+
if (duplicate) {
|
|
3028
|
+
effects.push({
|
|
3029
|
+
kind: 'dedupe-js-destination-commit-application',
|
|
3030
|
+
commitSlotId: request.commitSlotId,
|
|
3031
|
+
drainedThroughAuthoritativeSequence: record.appliedThroughAuthoritativeSequence,
|
|
3032
|
+
})
|
|
3033
|
+
} else {
|
|
3034
|
+
effects.push({
|
|
3035
|
+
kind: 'adopt-js-immutable-destination-commit',
|
|
3036
|
+
commitSlotId: request.commitSlotId,
|
|
3037
|
+
})
|
|
3038
|
+
for (const entry of newEntries) {
|
|
3039
|
+
effects.push({
|
|
3040
|
+
kind: 'apply-js-destination-ledger-entry-in-place',
|
|
3041
|
+
commitSlotId: request.commitSlotId,
|
|
3042
|
+
prefixIndex: entry.authoritativeSequence - commit.releaseFenceSequence - 1,
|
|
3043
|
+
destinationSlot: entry.destinationSlot,
|
|
3044
|
+
entry,
|
|
3045
|
+
})
|
|
3046
|
+
}
|
|
3047
|
+
nextRecord = {
|
|
3048
|
+
...record,
|
|
3049
|
+
adoptedCommitSlotId: request.commitSlotId,
|
|
3050
|
+
adoptedSentThroughAuthoritativeSequence: commit.sentThroughAuthoritativeSequence,
|
|
3051
|
+
appliedThroughAuthoritativeSequence: appliedThrough,
|
|
3052
|
+
ledgers: applyEntriesToLedgers(record.ledgers, newEntries),
|
|
3053
|
+
}
|
|
3054
|
+
}
|
|
3055
|
+
effects.push({
|
|
3056
|
+
kind: 'fill-js-destination-acknowledgement-slot',
|
|
3057
|
+
commitSlotId: request.commitSlotId,
|
|
3058
|
+
acknowledgementSlotId: request.acknowledgementSlotId,
|
|
3059
|
+
})
|
|
3060
|
+
effects.push({
|
|
3061
|
+
kind: 'stage-js-destination-acknowledgement-in-current-envelope',
|
|
3062
|
+
acknowledgementSlotId: request.acknowledgementSlotId,
|
|
3063
|
+
mailboxDrainId: openEnvelope.mailboxDrainId,
|
|
3064
|
+
})
|
|
3065
|
+
const entries: JsDestinationCommitEnvelopeEntry[] = [
|
|
3066
|
+
{
|
|
3067
|
+
kind: 'DESTINATION_COMMIT_ACKNOWLEDGEMENT',
|
|
3068
|
+
acknowledgementSlotId: request.acknowledgementSlotId,
|
|
3069
|
+
acknowledgement,
|
|
3070
|
+
},
|
|
3071
|
+
]
|
|
3072
|
+
for (const dueWorkSequence of request.siblingDueWorkSequences) {
|
|
3073
|
+
effects.push({ kind: 'run-js-root-sibling-due-work', dueWorkSequence })
|
|
3074
|
+
entries.push({ kind: 'ROOT_SIBLING_DUE_WORK', dueWorkSequence })
|
|
3075
|
+
}
|
|
3076
|
+
const sealedEnvelope: JsDestinationCommitEnvelopeState = {
|
|
3077
|
+
...openEnvelope,
|
|
3078
|
+
state: 'SEALED',
|
|
3079
|
+
entries,
|
|
3080
|
+
sealCount: openEnvelope.sealCount + 1,
|
|
3081
|
+
}
|
|
3082
|
+
effects.push({
|
|
3083
|
+
kind: 'seal-js-destination-envelope-once',
|
|
3084
|
+
mailboxDrainId: sealedEnvelope.mailboxDrainId,
|
|
3085
|
+
entryCount: entries.length,
|
|
3086
|
+
})
|
|
3087
|
+
const filledSlot: JsDestinationCommitAcknowledgementSlotState = {
|
|
3088
|
+
...acknowledgementSlot,
|
|
3089
|
+
state: 'STAGED_IN_CURRENT_ENVELOPE',
|
|
3090
|
+
acknowledgement,
|
|
3091
|
+
}
|
|
3092
|
+
return {
|
|
3093
|
+
state: {
|
|
3094
|
+
preparedDestinationRecords: duplicate
|
|
3095
|
+
? state.preparedDestinationRecords
|
|
3096
|
+
: state.preparedDestinationRecords.map((candidate) =>
|
|
3097
|
+
candidate === record ? nextRecord : candidate,
|
|
3098
|
+
),
|
|
3099
|
+
acknowledgementSlots: state.acknowledgementSlots.map((candidate) =>
|
|
3100
|
+
candidate === acknowledgementSlot ? filledSlot : candidate,
|
|
3101
|
+
),
|
|
3102
|
+
envelope: sealedEnvelope,
|
|
3103
|
+
},
|
|
3104
|
+
outcome: duplicate
|
|
3105
|
+
? { kind: 'DUPLICATE_REACKNOWLEDGED', commitSlotId: request.commitSlotId, acknowledgement }
|
|
3106
|
+
: { kind: 'APPLIED', commitSlotId: request.commitSlotId, acknowledgement },
|
|
3107
|
+
orderedEffects: effects,
|
|
3108
|
+
}
|
|
3109
|
+
}
|
|
3110
|
+
|
|
3111
|
+
export function drainPostReleaseCommitLoss(
|
|
3112
|
+
state: ValueHandoffCommitLossDrainState,
|
|
3113
|
+
now: number,
|
|
3114
|
+
): ValueHandoffCommitLossDrainTransition {
|
|
3115
|
+
void now
|
|
3116
|
+
// The bounded drain is change/host-silent: every retained public obligation still runs in
|
|
3117
|
+
// packet order; retired listeners, retired sinks, and host publication stay silent.
|
|
3118
|
+
const orderedEffects: ValueHandoffCommitLossDrainEffect[] = []
|
|
3119
|
+
const drainedWorkSequences: number[] = []
|
|
3120
|
+
let dependencyAcknowledgementCount = 0
|
|
3121
|
+
let settleReleaseCount = 0
|
|
3122
|
+
for (const work of state.retainedWork) {
|
|
3123
|
+
drainedWorkSequences.push(work.workSequence)
|
|
3124
|
+
switch (work.kind) {
|
|
3125
|
+
case 'cache-adoption':
|
|
3126
|
+
orderedEffects.push({
|
|
3127
|
+
kind: 'adopt-bounded-destination-cache',
|
|
3128
|
+
workSequence: work.workSequence,
|
|
3129
|
+
authoritativeSequence: work.authoritativeSequence,
|
|
3130
|
+
value: work.value,
|
|
3131
|
+
})
|
|
3132
|
+
break
|
|
3133
|
+
case 'transition-on-update':
|
|
3134
|
+
orderedEffects.push({
|
|
3135
|
+
kind: 'deliver-bounded-transition-on-update',
|
|
3136
|
+
workSequence: work.workSequence,
|
|
3137
|
+
callbackId: work.callbackId,
|
|
3138
|
+
value: work.value,
|
|
3139
|
+
})
|
|
3140
|
+
break
|
|
3141
|
+
case 'transition-on-complete':
|
|
3142
|
+
orderedEffects.push({
|
|
3143
|
+
kind: 'deliver-bounded-transition-on-complete',
|
|
3144
|
+
workSequence: work.workSequence,
|
|
3145
|
+
callbackId: work.callbackId,
|
|
3146
|
+
})
|
|
3147
|
+
break
|
|
3148
|
+
case 'current-animation-complete':
|
|
3149
|
+
orderedEffects.push({
|
|
3150
|
+
kind: 'deliver-current-animation-complete',
|
|
3151
|
+
workSequence: work.workSequence,
|
|
3152
|
+
listenerId: work.listenerId,
|
|
3153
|
+
bindingGeneration: work.bindingGeneration,
|
|
3154
|
+
})
|
|
3155
|
+
break
|
|
3156
|
+
case 'token-blind-public-slot-clear':
|
|
3157
|
+
orderedEffects.push({
|
|
3158
|
+
kind: 'clear-public-control-slot-token-blind',
|
|
3159
|
+
workSequence: work.workSequence,
|
|
3160
|
+
initiallyInstalledControlId: work.initiallyInstalledControlId,
|
|
3161
|
+
installedControlIdAtClear: work.installedControlIdAtClear,
|
|
3162
|
+
})
|
|
3163
|
+
break
|
|
3164
|
+
case 'public-start-promise-resolution':
|
|
3165
|
+
orderedEffects.push({
|
|
3166
|
+
kind: 'resolve-public-start-promise',
|
|
3167
|
+
workSequence: work.workSequence,
|
|
3168
|
+
promiseId: work.promiseId,
|
|
3169
|
+
})
|
|
3170
|
+
break
|
|
3171
|
+
case 'target-batch-dependency-delivery':
|
|
3172
|
+
orderedEffects.push({
|
|
3173
|
+
kind: 'deliver-target-batch-dependency-event',
|
|
3174
|
+
workSequence: work.workSequence,
|
|
3175
|
+
batchId: work.batchId,
|
|
3176
|
+
dependencyId: work.dependencyId,
|
|
3177
|
+
eventSequence: work.eventSequence,
|
|
3178
|
+
})
|
|
3179
|
+
break
|
|
3180
|
+
case 'target-batch-dependency-acknowledgement':
|
|
3181
|
+
dependencyAcknowledgementCount += 1
|
|
3182
|
+
orderedEffects.push({
|
|
3183
|
+
kind: 'acknowledge-target-batch-dependency-event',
|
|
3184
|
+
workSequence: work.workSequence,
|
|
3185
|
+
batchId: work.batchId,
|
|
3186
|
+
dependencyId: work.dependencyId,
|
|
3187
|
+
acknowledgedEventSequence: work.acknowledgedEventSequence,
|
|
3188
|
+
})
|
|
3189
|
+
break
|
|
3190
|
+
case 'settle-release':
|
|
3191
|
+
settleReleaseCount += 1
|
|
3192
|
+
orderedEffects.push({
|
|
3193
|
+
kind: 'release-settle-entry',
|
|
3194
|
+
workSequence: work.workSequence,
|
|
3195
|
+
settleEntryId: work.settleEntryId,
|
|
3196
|
+
})
|
|
3197
|
+
break
|
|
3198
|
+
default: {
|
|
3199
|
+
// An untyped caller can reach this with a work kind outside the union: fail closed with
|
|
3200
|
+
// the typed invariant (never return the work entry as a drain transition).
|
|
3201
|
+
const exhaustive: never = work
|
|
3202
|
+
void exhaustive
|
|
3203
|
+
throw new DriverValueChannelInvariantError('commit-loss-work-kind-unrecognized')
|
|
3204
|
+
}
|
|
3205
|
+
}
|
|
3206
|
+
}
|
|
3207
|
+
|
|
3208
|
+
// Only when the due-work set ends does the refusal release its protocol resources, each
|
|
3209
|
+
// exactly once, finalizing without permitting either side to run or reverse.
|
|
3210
|
+
let transferTimerCancellationCount = state.cleanupCounts.transferTimerCancellationCount
|
|
3211
|
+
let preparedStateReleaseCount = state.cleanupCounts.preparedStateReleaseCount
|
|
3212
|
+
let preparedCloneReleaseCount = state.cleanupCounts.preparedCloneReleaseCount
|
|
3213
|
+
let transferActivityReleaseCount = state.cleanupCounts.transferActivityReleaseCount
|
|
3214
|
+
let terminalFinalizationCount = state.cleanupCounts.terminalFinalizationCount
|
|
3215
|
+
let preparedStateRetained = state.channel.handoff.resources.preparedStateRetained
|
|
3216
|
+
let preparedCloneRetained = state.channel.handoff.resources.preparedCloneRetained
|
|
3217
|
+
let transferActivityRetained = state.channel.handoff.resources.transferActivityRetained
|
|
3218
|
+
let transferTimerArmed = state.channel.handoff.resources.transferTimerArmed
|
|
3219
|
+
let ownership = state.channel.handoff.ownership
|
|
3220
|
+
if (transferTimerCancellationCount === 0 && transferTimerArmed) {
|
|
3221
|
+
orderedEffects.push({ kind: 'cancel-transfer-timer-once' })
|
|
3222
|
+
transferTimerCancellationCount = 1
|
|
3223
|
+
transferTimerArmed = false
|
|
3224
|
+
}
|
|
3225
|
+
if (preparedStateReleaseCount === 0 && preparedStateRetained) {
|
|
3226
|
+
orderedEffects.push({ kind: 'release-prepared-state-once' })
|
|
3227
|
+
preparedStateReleaseCount = 1
|
|
3228
|
+
preparedStateRetained = false
|
|
3229
|
+
}
|
|
3230
|
+
if (preparedCloneReleaseCount === 0 && preparedCloneRetained) {
|
|
3231
|
+
orderedEffects.push({ kind: 'release-prepared-clone-once' })
|
|
3232
|
+
preparedCloneReleaseCount = 1
|
|
3233
|
+
preparedCloneRetained = false
|
|
3234
|
+
}
|
|
3235
|
+
if (transferActivityReleaseCount === 0 && transferActivityRetained) {
|
|
3236
|
+
orderedEffects.push({ kind: 'release-transfer-activity-once' })
|
|
3237
|
+
transferActivityReleaseCount = 1
|
|
3238
|
+
transferActivityRetained = false
|
|
3239
|
+
}
|
|
3240
|
+
if (terminalFinalizationCount === 0) {
|
|
3241
|
+
orderedEffects.push({ kind: 'finalize-bounded-refusal-without-run-or-reverse-once' })
|
|
3242
|
+
terminalFinalizationCount = 1
|
|
3243
|
+
ownership = 'TERMINAL_REFUSED'
|
|
3244
|
+
}
|
|
3245
|
+
|
|
3246
|
+
const cleanupCounts: ValueHandoffCommitLossCleanupCounts = {
|
|
3247
|
+
transferTimerCancellationCount,
|
|
3248
|
+
preparedStateReleaseCount,
|
|
3249
|
+
preparedCloneReleaseCount,
|
|
3250
|
+
transferActivityReleaseCount,
|
|
3251
|
+
terminalFinalizationCount,
|
|
3252
|
+
}
|
|
3253
|
+
const resources: ValueChannelHandoffResourceState = {
|
|
3254
|
+
preparedStateRetained,
|
|
3255
|
+
preparedCloneRetained,
|
|
3256
|
+
transferActivityRetained,
|
|
3257
|
+
transferTimerArmed,
|
|
3258
|
+
}
|
|
3259
|
+
|
|
3260
|
+
const nextState: ValueHandoffCommitLossDrainState = {
|
|
3261
|
+
channel: {
|
|
3262
|
+
identity: state.channel.identity,
|
|
3263
|
+
handoff: { ...state.channel.handoff, ownership, resources },
|
|
3264
|
+
},
|
|
3265
|
+
retainedWork: [],
|
|
3266
|
+
drainedWorkSequences: [...state.drainedWorkSequences, ...drainedWorkSequences],
|
|
3267
|
+
retiredListenerIds: state.retiredListenerIds,
|
|
3268
|
+
retiredSinkRegistrationIds: state.retiredSinkRegistrationIds,
|
|
3269
|
+
cleanupCounts,
|
|
3270
|
+
}
|
|
3271
|
+
return {
|
|
3272
|
+
state: nextState,
|
|
3273
|
+
observation: {
|
|
3274
|
+
remainingRetainedWorkCount: nextState.retainedWork.length,
|
|
3275
|
+
drainedWorkCount: drainedWorkSequences.length,
|
|
3276
|
+
retiredListenerDeliveryCount: 0,
|
|
3277
|
+
retiredSinkPublicationCount: 0,
|
|
3278
|
+
hostOnUpdatePublicationCount: 0,
|
|
3279
|
+
dependencyAcknowledgementCount,
|
|
3280
|
+
settleReleaseCount,
|
|
3281
|
+
destinationRunPermitCount: 0,
|
|
3282
|
+
sourceRevivalCount: 0,
|
|
3283
|
+
reverseTransferCount: 0,
|
|
3284
|
+
cleanupCounts,
|
|
3285
|
+
},
|
|
3286
|
+
orderedEffects,
|
|
3287
|
+
}
|
|
3288
|
+
}
|