@fluidframework/container-runtime 2.118.0 → 3.0.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 +68 -4
- package/README.md +53 -50
- package/{api-extractor.json → api-extractor/api-extractor-model.json} +1 -1
- package/api-report/container-runtime.legacy.alpha.api.md +3 -1
- package/api-report/container-runtime.legacy.beta.api.md +3 -1
- package/container-runtime.test-files.tar +0 -0
- package/dist/batchTracker.d.ts.map +1 -1
- package/dist/batchTracker.js +5 -1
- package/dist/batchTracker.js.map +1 -1
- package/dist/blobManager/blobManager.d.ts +1 -1
- package/dist/blobManager/blobManager.d.ts.map +1 -1
- package/dist/blobManager/blobManager.js +264 -231
- package/dist/blobManager/blobManager.js.map +1 -1
- package/dist/blobManager/blobManagerSnapSum.d.ts.map +1 -1
- package/dist/channelCollection.d.ts.map +1 -1
- package/dist/channelCollection.js +70 -58
- package/dist/channelCollection.js.map +1 -1
- package/dist/connectionTelemetry.js +38 -20
- package/dist/connectionTelemetry.js.map +1 -1
- package/dist/containerCompatibility.d.ts +3 -3
- package/dist/containerCompatibility.d.ts.map +1 -1
- package/dist/containerCompatibility.js +36 -45
- package/dist/containerCompatibility.js.map +1 -1
- package/dist/containerHandleContext.d.ts.map +1 -1
- package/dist/containerHandleContext.js +4 -0
- package/dist/containerHandleContext.js.map +1 -1
- package/dist/containerRuntime.d.ts +4 -4
- package/dist/containerRuntime.d.ts.map +1 -1
- package/dist/containerRuntime.js +346 -223
- package/dist/containerRuntime.js.map +1 -1
- package/dist/dataStore.d.ts.map +1 -1
- package/dist/dataStore.js +9 -1
- package/dist/dataStore.js.map +1 -1
- package/dist/dataStoreContext.d.ts.map +1 -1
- package/dist/dataStoreContext.js +184 -127
- package/dist/dataStoreContext.js.map +1 -1
- package/dist/dataStoreContexts.js +43 -42
- package/dist/dataStoreContexts.js.map +1 -1
- package/dist/dataStoreRegistry.js +1 -0
- package/dist/dataStoreRegistry.js.map +1 -1
- package/dist/deltaManagerProxies.js +5 -0
- package/dist/deltaManagerProxies.js.map +1 -1
- package/dist/deltaScheduler.js +104 -99
- package/dist/deltaScheduler.js.map +1 -1
- package/dist/error.js +2 -1
- package/dist/error.js.map +1 -1
- package/dist/gc/garbageCollection.js +85 -55
- package/dist/gc/garbageCollection.js.map +1 -1
- package/dist/gc/gcConfigs.js +2 -3
- package/dist/gc/gcConfigs.js.map +1 -1
- package/dist/gc/gcHelpers.js +12 -13
- package/dist/gc/gcHelpers.js.map +1 -1
- package/dist/gc/gcReferenceGraphAlgorithm.js +1 -2
- package/dist/gc/gcReferenceGraphAlgorithm.js.map +1 -1
- package/dist/gc/gcSummaryStateTracker.js +8 -3
- package/dist/gc/gcSummaryStateTracker.js.map +1 -1
- package/dist/gc/gcTelemetry.js +12 -5
- package/dist/gc/gcTelemetry.js.map +1 -1
- package/dist/gc/gcUnreferencedStateTracker.js +18 -1
- package/dist/gc/gcUnreferencedStateTracker.js.map +1 -1
- package/dist/inboundBatchAggregator.js +122 -117
- package/dist/inboundBatchAggregator.js.map +1 -1
- package/dist/metadata.d.ts.map +1 -1
- package/dist/metadata.js +3 -3
- package/dist/metadata.js.map +1 -1
- package/dist/opLifecycle/batchManager.d.ts.map +1 -1
- package/dist/opLifecycle/batchManager.js +11 -5
- package/dist/opLifecycle/batchManager.js.map +1 -1
- package/dist/opLifecycle/duplicateBatchDetector.js +18 -18
- package/dist/opLifecycle/duplicateBatchDetector.js.map +1 -1
- package/dist/opLifecycle/index.d.ts +2 -2
- package/dist/opLifecycle/index.d.ts.map +1 -1
- package/dist/opLifecycle/index.js +2 -1
- package/dist/opLifecycle/index.js.map +1 -1
- package/dist/opLifecycle/opCompressor.js +2 -1
- package/dist/opLifecycle/opCompressor.js.map +1 -1
- package/dist/opLifecycle/opDecompressor.js +10 -6
- package/dist/opLifecycle/opDecompressor.js.map +1 -1
- package/dist/opLifecycle/opGroupingManager.js +5 -3
- package/dist/opLifecycle/opGroupingManager.js.map +1 -1
- package/dist/opLifecycle/opSerialization.d.ts +11 -1
- package/dist/opLifecycle/opSerialization.d.ts.map +1 -1
- package/dist/opLifecycle/opSerialization.js +22 -6
- package/dist/opLifecycle/opSerialization.js.map +1 -1
- package/dist/opLifecycle/opSplitter.d.ts +17 -1
- package/dist/opLifecycle/opSplitter.d.ts.map +1 -1
- package/dist/opLifecycle/opSplitter.js +46 -9
- package/dist/opLifecycle/opSplitter.js.map +1 -1
- package/dist/opLifecycle/outbox.d.ts.map +1 -1
- package/dist/opLifecycle/outbox.js +17 -13
- package/dist/opLifecycle/outbox.js.map +1 -1
- package/dist/opLifecycle/remoteMessageProcessor.js +6 -3
- package/dist/opLifecycle/remoteMessageProcessor.js.map +1 -1
- package/dist/opProperties.d.ts.map +1 -1
- package/dist/packageVersion.d.ts +1 -1
- package/dist/packageVersion.d.ts.map +1 -1
- package/dist/packageVersion.js +1 -1
- package/dist/packageVersion.js.map +1 -1
- package/dist/pendingStateManager.js +54 -47
- package/dist/pendingStateManager.js.map +1 -1
- package/dist/runCounter.js +10 -30
- package/dist/runCounter.js.map +1 -1
- package/dist/runtimeLayerCompatState.d.ts +1 -1
- package/dist/runtimeLayerCompatState.d.ts.map +1 -1
- package/dist/runtimeLayerCompatState.js +31 -11
- package/dist/runtimeLayerCompatState.js.map +1 -1
- package/dist/signalTelemetryProcessing.js +15 -17
- package/dist/signalTelemetryProcessing.js.map +1 -1
- package/dist/storageServiceWithAttachBlobs.js +2 -0
- package/dist/storageServiceWithAttachBlobs.js.map +1 -1
- package/dist/summary/documentSchema.d.ts.map +1 -1
- package/dist/summary/documentSchema.js +51 -13
- package/dist/summary/documentSchema.js.map +1 -1
- package/dist/summary/orderedClientElection.js +25 -17
- package/dist/summary/orderedClientElection.js.map +1 -1
- package/dist/summary/summarizerClientElection.d.ts.map +1 -1
- package/dist/summary/summarizerClientElection.js +18 -7
- package/dist/summary/summarizerClientElection.js.map +1 -1
- package/dist/summary/summarizerNode/summarizerNode.d.ts.map +1 -1
- package/dist/summary/summarizerNode/summarizerNode.js +22 -13
- package/dist/summary/summarizerNode/summarizerNode.js.map +1 -1
- package/dist/summary/summarizerNode/summarizerNodeWithGc.d.ts.map +1 -1
- package/dist/summary/summarizerNode/summarizerNodeWithGc.js +20 -6
- package/dist/summary/summarizerNode/summarizerNodeWithGc.js.map +1 -1
- package/dist/summary/summarizerUtils.d.ts.map +1 -1
- package/dist/summary/summarizerUtils.js +4 -3
- package/dist/summary/summarizerUtils.js.map +1 -1
- package/dist/summary/summaryCollection.js +23 -11
- package/dist/summary/summaryCollection.js.map +1 -1
- package/dist/summary/summaryDelayLoadedModule/runWhileConnectedCoordinator.js +4 -2
- package/dist/summary/summaryDelayLoadedModule/runWhileConnectedCoordinator.js.map +1 -1
- package/dist/summary/summaryDelayLoadedModule/runningSummarizer.d.ts.map +1 -1
- package/dist/summary/summaryDelayLoadedModule/runningSummarizer.js +65 -46
- package/dist/summary/summaryDelayLoadedModule/runningSummarizer.js.map +1 -1
- package/dist/summary/summaryDelayLoadedModule/summarizer.d.ts.map +1 -1
- package/dist/summary/summaryDelayLoadedModule/summarizer.js +15 -6
- package/dist/summary/summaryDelayLoadedModule/summarizer.js.map +1 -1
- package/dist/summary/summaryDelayLoadedModule/summarizerHeuristics.js +30 -25
- package/dist/summary/summaryDelayLoadedModule/summarizerHeuristics.js.map +1 -1
- package/dist/summary/summaryDelayLoadedModule/summaryGenerator.js +9 -0
- package/dist/summary/summaryDelayLoadedModule/summaryGenerator.js.map +1 -1
- package/dist/summary/summaryDelayLoadedModule/summaryResultBuilder.js +3 -5
- package/dist/summary/summaryDelayLoadedModule/summaryResultBuilder.js.map +1 -1
- package/dist/summary/summaryFormat.d.ts.map +1 -1
- package/dist/summary/summaryFormat.js +7 -7
- package/dist/summary/summaryFormat.js.map +1 -1
- package/dist/summary/summaryHelpers.js +5 -5
- package/dist/summary/summaryHelpers.js.map +1 -1
- package/dist/summary/summaryManager.js +66 -54
- package/dist/summary/summaryManager.js.map +1 -1
- package/dist/throttler.d.ts.map +1 -1
- package/dist/throttler.js +4 -1
- package/dist/throttler.js.map +1 -1
- package/dist/versionMarks/inboundBatch.d.ts +8 -6
- package/dist/versionMarks/inboundBatch.d.ts.map +1 -1
- package/dist/versionMarks/inboundBatch.js +11 -7
- package/dist/versionMarks/inboundBatch.js.map +1 -1
- package/dist/versionMarks/versionMarkResolver.d.ts +30 -23
- package/dist/versionMarks/versionMarkResolver.d.ts.map +1 -1
- package/dist/versionMarks/versionMarkResolver.js +112 -63
- package/dist/versionMarks/versionMarkResolver.js.map +1 -1
- package/lib/batchTracker.d.ts.map +1 -1
- package/lib/batchTracker.js +5 -1
- package/lib/batchTracker.js.map +1 -1
- package/lib/blobManager/blobManager.d.ts +1 -1
- package/lib/blobManager/blobManager.d.ts.map +1 -1
- package/lib/blobManager/blobManager.js +264 -231
- package/lib/blobManager/blobManager.js.map +1 -1
- package/lib/blobManager/blobManagerSnapSum.d.ts.map +1 -1
- package/lib/channelCollection.d.ts.map +1 -1
- package/lib/channelCollection.js +65 -53
- package/lib/channelCollection.js.map +1 -1
- package/lib/connectionTelemetry.js +36 -18
- package/lib/connectionTelemetry.js.map +1 -1
- package/lib/containerCompatibility.d.ts +3 -3
- package/lib/containerCompatibility.d.ts.map +1 -1
- package/lib/containerCompatibility.js +33 -42
- package/lib/containerCompatibility.js.map +1 -1
- package/lib/containerHandleContext.d.ts.map +1 -1
- package/lib/containerHandleContext.js +4 -0
- package/lib/containerHandleContext.js.map +1 -1
- package/lib/containerRuntime.d.ts +4 -4
- package/lib/containerRuntime.d.ts.map +1 -1
- package/lib/containerRuntime.js +341 -218
- package/lib/containerRuntime.js.map +1 -1
- package/lib/dataStore.d.ts.map +1 -1
- package/lib/dataStore.js +9 -1
- package/lib/dataStore.js.map +1 -1
- package/lib/dataStoreContext.d.ts.map +1 -1
- package/lib/dataStoreContext.js +182 -125
- package/lib/dataStoreContext.js.map +1 -1
- package/lib/dataStoreContexts.js +43 -42
- package/lib/dataStoreContexts.js.map +1 -1
- package/lib/dataStoreRegistry.js +1 -0
- package/lib/dataStoreRegistry.js.map +1 -1
- package/lib/deltaManagerProxies.js +5 -0
- package/lib/deltaManagerProxies.js.map +1 -1
- package/lib/deltaScheduler.js +104 -99
- package/lib/deltaScheduler.js.map +1 -1
- package/lib/error.js +2 -1
- package/lib/error.js.map +1 -1
- package/lib/gc/garbageCollection.js +85 -55
- package/lib/gc/garbageCollection.js.map +1 -1
- package/lib/gc/gcSummaryStateTracker.js +8 -3
- package/lib/gc/gcSummaryStateTracker.js.map +1 -1
- package/lib/gc/gcTelemetry.js +12 -5
- package/lib/gc/gcTelemetry.js.map +1 -1
- package/lib/gc/gcUnreferencedStateTracker.js +18 -1
- package/lib/gc/gcUnreferencedStateTracker.js.map +1 -1
- package/lib/inboundBatchAggregator.js +122 -117
- package/lib/inboundBatchAggregator.js.map +1 -1
- package/lib/legacy.d.ts +1 -1
- package/lib/legacyAlpha.d.ts +1 -1
- package/lib/metadata.d.ts.map +1 -1
- package/lib/opLifecycle/batchManager.d.ts.map +1 -1
- package/lib/opLifecycle/batchManager.js +8 -2
- package/lib/opLifecycle/batchManager.js.map +1 -1
- package/lib/opLifecycle/duplicateBatchDetector.js +18 -18
- package/lib/opLifecycle/duplicateBatchDetector.js.map +1 -1
- package/lib/opLifecycle/index.d.ts +2 -2
- package/lib/opLifecycle/index.d.ts.map +1 -1
- package/lib/opLifecycle/index.js +2 -2
- package/lib/opLifecycle/index.js.map +1 -1
- package/lib/opLifecycle/opCompressor.js +3 -2
- package/lib/opLifecycle/opCompressor.js.map +1 -1
- package/lib/opLifecycle/opDecompressor.js +10 -6
- package/lib/opLifecycle/opDecompressor.js.map +1 -1
- package/lib/opLifecycle/opGroupingManager.js +3 -1
- package/lib/opLifecycle/opGroupingManager.js.map +1 -1
- package/lib/opLifecycle/opSerialization.d.ts +11 -1
- package/lib/opLifecycle/opSerialization.d.ts.map +1 -1
- package/lib/opLifecycle/opSerialization.js +16 -0
- package/lib/opLifecycle/opSerialization.js.map +1 -1
- package/lib/opLifecycle/opSplitter.d.ts +17 -1
- package/lib/opLifecycle/opSplitter.d.ts.map +1 -1
- package/lib/opLifecycle/opSplitter.js +44 -7
- package/lib/opLifecycle/opSplitter.js.map +1 -1
- package/lib/opLifecycle/outbox.d.ts.map +1 -1
- package/lib/opLifecycle/outbox.js +14 -10
- package/lib/opLifecycle/outbox.js.map +1 -1
- package/lib/opLifecycle/remoteMessageProcessor.js +4 -1
- package/lib/opLifecycle/remoteMessageProcessor.js.map +1 -1
- package/lib/opProperties.d.ts.map +1 -1
- package/lib/packageVersion.d.ts +1 -1
- package/lib/packageVersion.d.ts.map +1 -1
- package/lib/packageVersion.js +1 -1
- package/lib/packageVersion.js.map +1 -1
- package/lib/pendingStateManager.js +52 -45
- package/lib/pendingStateManager.js.map +1 -1
- package/lib/public.d.ts +1 -1
- package/lib/runCounter.js +10 -30
- package/lib/runCounter.js.map +1 -1
- package/lib/runtimeLayerCompatState.d.ts +1 -1
- package/lib/runtimeLayerCompatState.d.ts.map +1 -1
- package/lib/runtimeLayerCompatState.js +28 -8
- package/lib/runtimeLayerCompatState.js.map +1 -1
- package/lib/signalTelemetryProcessing.js +15 -17
- package/lib/signalTelemetryProcessing.js.map +1 -1
- package/lib/storageServiceWithAttachBlobs.js +2 -0
- package/lib/storageServiceWithAttachBlobs.js.map +1 -1
- package/lib/summary/documentSchema.d.ts.map +1 -1
- package/lib/summary/documentSchema.js +51 -13
- package/lib/summary/documentSchema.js.map +1 -1
- package/lib/summary/orderedClientElection.js +25 -17
- package/lib/summary/orderedClientElection.js.map +1 -1
- package/lib/summary/summarizerClientElection.d.ts.map +1 -1
- package/lib/summary/summarizerClientElection.js +18 -7
- package/lib/summary/summarizerClientElection.js.map +1 -1
- package/lib/summary/summarizerNode/summarizerNode.d.ts.map +1 -1
- package/lib/summary/summarizerNode/summarizerNode.js +22 -13
- package/lib/summary/summarizerNode/summarizerNode.js.map +1 -1
- package/lib/summary/summarizerNode/summarizerNodeWithGc.d.ts.map +1 -1
- package/lib/summary/summarizerNode/summarizerNodeWithGc.js +20 -6
- package/lib/summary/summarizerNode/summarizerNodeWithGc.js.map +1 -1
- package/lib/summary/summarizerUtils.d.ts.map +1 -1
- package/lib/summary/summarizerUtils.js +1 -0
- package/lib/summary/summarizerUtils.js.map +1 -1
- package/lib/summary/summaryCollection.js +23 -11
- package/lib/summary/summaryCollection.js.map +1 -1
- package/lib/summary/summaryDelayLoadedModule/runWhileConnectedCoordinator.js +4 -2
- package/lib/summary/summaryDelayLoadedModule/runWhileConnectedCoordinator.js.map +1 -1
- package/lib/summary/summaryDelayLoadedModule/runningSummarizer.d.ts.map +1 -1
- package/lib/summary/summaryDelayLoadedModule/runningSummarizer.js +65 -46
- package/lib/summary/summaryDelayLoadedModule/runningSummarizer.js.map +1 -1
- package/lib/summary/summaryDelayLoadedModule/summarizer.d.ts.map +1 -1
- package/lib/summary/summaryDelayLoadedModule/summarizer.js +15 -6
- package/lib/summary/summaryDelayLoadedModule/summarizer.js.map +1 -1
- package/lib/summary/summaryDelayLoadedModule/summarizerHeuristics.js +30 -25
- package/lib/summary/summaryDelayLoadedModule/summarizerHeuristics.js.map +1 -1
- package/lib/summary/summaryDelayLoadedModule/summaryGenerator.js +9 -0
- package/lib/summary/summaryDelayLoadedModule/summaryGenerator.js.map +1 -1
- package/lib/summary/summaryDelayLoadedModule/summaryResultBuilder.js +3 -5
- package/lib/summary/summaryDelayLoadedModule/summaryResultBuilder.js.map +1 -1
- package/lib/summary/summaryFormat.d.ts.map +1 -1
- package/lib/summary/summaryManager.js +66 -54
- package/lib/summary/summaryManager.js.map +1 -1
- package/lib/throttler.d.ts.map +1 -1
- package/lib/throttler.js +4 -1
- package/lib/throttler.js.map +1 -1
- package/lib/versionMarks/inboundBatch.d.ts +8 -6
- package/lib/versionMarks/inboundBatch.d.ts.map +1 -1
- package/lib/versionMarks/inboundBatch.js +10 -5
- package/lib/versionMarks/inboundBatch.js.map +1 -1
- package/lib/versionMarks/versionMarkResolver.d.ts +30 -23
- package/lib/versionMarks/versionMarkResolver.d.ts.map +1 -1
- package/lib/versionMarks/versionMarkResolver.js +112 -63
- package/lib/versionMarks/versionMarkResolver.js.map +1 -1
- package/package.json +60 -102
- package/src/containerCompatibility.ts +33 -42
- package/src/containerRuntime.ts +15 -12
- package/src/opLifecycle/index.ts +7 -1
- package/src/opLifecycle/opCompressor.ts +2 -2
- package/src/opLifecycle/opSerialization.ts +22 -1
- package/src/opLifecycle/opSplitter.ts +63 -6
- package/src/packageVersion.ts +1 -1
- package/src/runtimeLayerCompatState.ts +28 -21
- package/src/summary/documentSchema.ts +31 -6
- package/src/versionMarks/DEV.md +595 -157
- package/src/versionMarks/inboundBatch.ts +18 -8
- package/src/versionMarks/versionMarkResolver.ts +144 -75
- package/tsconfig.json +1 -1
- package/internal.d.ts +0 -11
- package/legacy/alpha.d.ts +0 -11
- package/legacy.d.ts +0 -11
- /package/api-extractor/{api-extractor.current.json → api-extractor-report.current.json} +0 -0
- /package/api-extractor/{api-extractor.legacy.json → api-extractor-report.legacy.json} +0 -0
package/src/versionMarks/DEV.md
CHANGED
|
@@ -1,90 +1,171 @@
|
|
|
1
1
|
# Version marks runtime resolver
|
|
2
2
|
|
|
3
|
-
Version marks keep mark storage out of the Fluid runtime. The app owns mark records, labels, timestamps,
|
|
3
|
+
Version marks keep mark storage out of the Fluid runtime. The app owns mark records, labels, app-authored timestamps,
|
|
4
|
+
retention, promotion, **and the stored locator shape**. Fluid owns only a runtime resolver that can turn a pending
|
|
5
|
+
batchId into a durable global sequence number and the corresponding server-generated op timestamp, either when the batch
|
|
6
|
+
is observed live or by scanning retained historical ops.
|
|
4
7
|
|
|
5
8
|
## Implementation map
|
|
6
9
|
|
|
7
|
-
| File
|
|
8
|
-
|
|
|
9
|
-
| `packages/common/container-definitions/src/runtime.ts`
|
|
10
|
-
| `packages/loader/container-loader/src/container.ts`
|
|
11
|
-
| `packages/loader/container-loader/src/containerContext.ts`
|
|
12
|
-
| `packages/runtime/container-runtime/src/pendingStateManager.ts`
|
|
13
|
-
| `packages/runtime/container-runtime/src/versionMarks/inboundBatch.ts`
|
|
14
|
-
| `packages/runtime/container-runtime/src/versionMarks/versionMarkResolver.ts`
|
|
15
|
-
| `packages/runtime/container-runtime/src/containerRuntime.ts`
|
|
16
|
-
| `packages/runtime/container-runtime/src/versionMarks/index.ts` and `src/index.ts` | Export the internal implementation types and the host-consumable resolver interface/result types.
|
|
10
|
+
| File | Responsibility |
|
|
11
|
+
| --------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
12
|
+
| `packages/common/container-definitions/src/runtime.ts` | Defines the internal loader-to-runtime extension `IContainerContextInternal` and its optional `fetchOps` capability. |
|
|
13
|
+
| `packages/loader/container-loader/src/container.ts` | Implements `fetchOps` by connecting to the current document delta-storage service and forwarding the requested range. |
|
|
14
|
+
| `packages/loader/container-loader/src/containerContext.ts` | Carries `fetchOps` through `ContainerContext`. The config key is required so support is explicit, but its value may be `undefined` because the capability itself is optional. |
|
|
15
|
+
| `packages/runtime/container-runtime/src/pendingStateManager.ts` | Supplies the reconnect-stable id from the batch-start message of the most recently flushed pending batch for capture. |
|
|
16
|
+
| `packages/runtime/container-runtime/src/versionMarks/inboundBatch.ts` | Converts live or historically unpacked `InboundMessageResult` values into completed batch identities and carries identity across piecemeal batches. |
|
|
17
|
+
| `packages/runtime/container-runtime/src/versionMarks/versionMarkResolver.ts` | Implements capture, live promotion, the session fast-path cache, historical resolution, miss classification, listener isolation, and cache eviction. |
|
|
18
|
+
| `packages/runtime/container-runtime/src/containerRuntime.ts` | Constructs the resolver, wires runtime hooks, creates the historical unpack pipeline, exposes the host-facing resolver, and invokes live batch tracking after pending-state validation. |
|
|
19
|
+
| `packages/runtime/container-runtime/src/versionMarks/index.ts` and `src/index.ts` | Export the internal implementation types and the host-consumable resolver interface/result types. |
|
|
17
20
|
|
|
18
21
|
## Locator format
|
|
19
22
|
|
|
20
|
-
Fluid does not define or export a locator type — the resolver works in primitives (`batchId`,
|
|
23
|
+
Fluid does not define or export a locator type — the resolver works in primitives (`batchId`,
|
|
24
|
+
`sequenceNumberLowerBound`, `sequenceNumber`, `timestamp`), and the app packs/unpacks its own stored records. A typical
|
|
25
|
+
app-owned shape is:
|
|
21
26
|
|
|
22
27
|
```ts
|
|
23
28
|
// App-owned type (not provided by Fluid)
|
|
24
29
|
type MarkLocator =
|
|
25
|
-
| { kind: "resolved"; sequenceNumber: number }
|
|
30
|
+
| { kind: "resolved"; sequenceNumber: number; timestamp?: number }
|
|
26
31
|
| { kind: "pending"; batchId: string; sequenceNumberLowerBound: number };
|
|
27
32
|
```
|
|
28
33
|
|
|
34
|
+
`timestamp` is the server-generated timestamp of the op at `sequenceNumber`. It is optional in the API for compatibility
|
|
35
|
+
with records and callers using the earlier resolved shape, but current runtime-produced resolved values populate it
|
|
36
|
+
whenever the corresponding op timestamp is available.
|
|
37
|
+
|
|
29
38
|
There is no runtime `expired` locator kind. Expiration or user-visible failure states are app-side policy.
|
|
30
39
|
|
|
31
|
-
A `pending` locator carries two things: `batchId` identifies which op the mark points at (the reconnect-stable batch
|
|
40
|
+
A `pending` locator carries two things: `batchId` identifies which op the mark points at (the reconnect-stable batch
|
|
41
|
+
identity), and `sequenceNumberLowerBound` is the first possible sequence number of the pending batch —
|
|
42
|
+
`referenceSequenceNumber + 1`, where the reference is the last globally-sequenced point at capture (the batch's ops are
|
|
43
|
+
sequenced after it). This is an **inclusive** lower bound for an out-of-session history read. Since `batchStartCsn` is a
|
|
44
|
+
per-connection counter, not a global seq, it gives no location hint on its own; `sequenceNumberLowerBound` is the scan
|
|
45
|
+
anchor for resolving `batchId -> seq` by reading ops starting at `sequenceNumberLowerBound` (see Resolution paths).
|
|
32
46
|
|
|
33
47
|
## Batch identity
|
|
34
48
|
|
|
35
|
-
`BatchManager.generateBatchId(originalClientId, batchStartCsn)` produces `${originalClientId}_[${batchStartCsn}]`.
|
|
49
|
+
`BatchManager.generateBatchId(originalClientId, batchStartCsn)` produces `${originalClientId}_[${batchStartCsn}]`.
|
|
50
|
+
`getEffectiveBatchId(...)` returns explicit batch metadata on resubmit, or derives the same id from the original wire
|
|
51
|
+
client/csn for first submission. `PendingStateManager` preserves that batch info across reconnect and stamps the batchId
|
|
52
|
+
during resubmit.
|
|
36
53
|
|
|
37
|
-
For capture, `PendingStateManager.getMostRecentPendingBatchId()` locates the start of the most recently flushed pending
|
|
54
|
+
For capture, `PendingStateManager.getMostRecentPendingBatchId()` locates the start of the most recently flushed pending
|
|
55
|
+
batch using its recorded batch length, then derives the effective id from that start message. This matters for
|
|
56
|
+
resubmitted multi-op batches because the explicit reconnect-stable `batchId` is stamped only on the first message;
|
|
57
|
+
reading the last message would incorrectly derive a new id from the current client and CSN. Stashed `initialMessages`
|
|
58
|
+
are ignored until they are applied into the current session's pending queue.
|
|
38
59
|
|
|
39
60
|
## Resolver API
|
|
40
61
|
|
|
41
62
|
`VersionMarkResolver` implements `IVersionMarkResolver`:
|
|
42
63
|
|
|
43
|
-
- `sealAndCaptureVersionMark()` synchronously seals the current outbound batch (flushes the runtime) and captures a mark
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
64
|
+
- `sealAndCaptureVersionMark()` synchronously seals the current outbound batch (flushes the runtime) and captures a mark
|
|
65
|
+
at the resulting point, returning a `VersionMarkCapture`: either
|
|
66
|
+
`{ kind: "pending", batchId, sequenceNumberLowerBound }` (an unacked local edit, resolve it later) or
|
|
67
|
+
`{ kind: "resolved", sequenceNumber, timestamp? }` (no in-flight local work). The resolved timestamp comes from
|
|
68
|
+
`getCurrentReferenceTimestampMs()`, which uses the DeltaManager's last processed message and falls back to the
|
|
69
|
+
last-summary message. A batch's `batchId` is only assigned when it is flushed into `PendingStateManager` — see
|
|
70
|
+
[Batch identity](#batch-identity). Combining sealing and capture prevents a caller from reading an older batch or
|
|
71
|
+
`undefined` immediately after an edit and prevents the batch id and lower bound from being read at different points.
|
|
72
|
+
The app packs its own stored record from the result — the runtime does not define the stored locator shape. Call it at
|
|
73
|
+
savepoint boundaries, not per keystroke, because sealing submits the pending batch.
|
|
74
|
+
- `resolve(batchId, sequenceNumberLowerBound)` resolves live-then-history: (1) the ephemeral in-session
|
|
75
|
+
`batchId -> { sequenceNumber, timestamp }` map (batch seen live this session); (2) on a miss, an **out-of-session
|
|
76
|
+
scan** — reads ops starting at `sequenceNumberLowerBound` (an inclusive lower bound) via an injected
|
|
77
|
+
`IHistoricalOpReader`, routes each op through the **same unpack pipeline the live inbound path uses** (chunk
|
|
78
|
+
reassembly, ungroup, decompress) and derives batch identity with the shared `inboundVersionMarkUpdate` helper,
|
|
79
|
+
returning the matched batch's **last** op sequence number and server timestamp, or (when not found) `pending` /
|
|
80
|
+
`unresolvable` distinguished by a read-derived availability check (see Resolution behavior). The reader is generic
|
|
81
|
+
(backed by any driver's `IDocumentDeltaStorageService.fetchMessages`); when it is not wired, an unknown batchId is
|
|
82
|
+
reported `pending`.
|
|
83
|
+
- `onBatchSequenced(listener)` broadcasts `(batchId, sequenceNumber, timestamp?)` as each batch is processed inbound, so
|
|
84
|
+
any connected client can promote a matching pending mark in its own store (resolution is not tied to the capturing
|
|
85
|
+
client). The timestamp is from the batch's final op and is optional in the callback type for source compatibility with
|
|
86
|
+
existing listeners. Returns an unsubscribe. Listeners run synchronously on the inbound op path, so each invocation is
|
|
87
|
+
isolated: a throwing listener is caught, logged (`VersionMarkListenerException`), and skipped — it cannot abort op
|
|
88
|
+
processing or starve later listeners (mirroring the container's `EventEmitterWithErrorHandling`). A missed live
|
|
89
|
+
promotion is recoverable — the app can still resolve that mark later via `resolve()`'s history scan — so a listener
|
|
90
|
+
fault logs and continues rather than faulting the container.
|
|
91
|
+
|
|
92
|
+
Host exposure: `ContainerRuntime` exposes an `@internal` `versionMarkResolver` getter backed by the concrete
|
|
93
|
+
`versionMarkResolverInternal`. An app gets it from the runtime instance passed to `provideEntryPoint`, or exposes it
|
|
94
|
+
from its own entryPoint. A future public API may move this onto container-runtime definitions rather than the concrete
|
|
95
|
+
runtime class.
|
|
48
96
|
|
|
49
97
|
### Capture implementation
|
|
50
98
|
|
|
51
99
|
`VersionMarkResolver.sealAndCaptureVersionMark()` executes synchronously in this order:
|
|
52
100
|
|
|
53
|
-
1. Call the `flushPendingBatch` hook (`ContainerRuntime.flush`) so the current outbox batch is moved into
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
101
|
+
1. Call the `flushPendingBatch` hook (`ContainerRuntime.flush`) so the current outbox batch is moved into
|
|
102
|
+
`PendingStateManager` and assigned stable batch information.
|
|
103
|
+
1. Read `getCurrentSequenceNumber()` (`deltaManager.lastSequenceNumber`) as the reference sequence number — the last
|
|
104
|
+
globally-sequenced point at capture.
|
|
105
|
+
1. Read `getCurrentPendingBatchId()` (`PendingStateManager.getMostRecentPendingBatchId()`).
|
|
106
|
+
1. If no pending batch exists, return `{ kind: "resolved", sequenceNumber, timestamp }` using the reference sequence
|
|
107
|
+
number and `getCurrentReferenceTimestampMs()`
|
|
108
|
+
(`deltaManager.lastMessage?.timestamp ?? messageAtLastSummary?.timestamp`). This path does not enable inbound
|
|
109
|
+
tracking because there is no pending batch to promote.
|
|
110
|
+
1. If a pending batch exists, set the sticky `tracking` flag and return
|
|
111
|
+
`{ kind: "pending", batchId, sequenceNumberLowerBound }`, where
|
|
112
|
+
`sequenceNumberLowerBound = referenceSequenceNumber + 1` (the pending batch's first possible sequence number — an
|
|
113
|
+
inclusive lower bound).
|
|
114
|
+
|
|
115
|
+
Keeping sealing and capture in one synchronous operation prevents callers from observing or persisting intermediate
|
|
116
|
+
state between flushing, reading the sequence number, and reading the batch id.
|
|
60
117
|
|
|
61
118
|
## Consumer surface & public-API graduation
|
|
62
119
|
|
|
63
|
-
An app (e.g. the Loop/office-bohemia host) consumes a small `@legacy @
|
|
120
|
+
An app (e.g. the Loop/office-bohemia host) consumes a small `@legacy @beta` surface:
|
|
64
121
|
|
|
65
122
|
- Get the resolver: `ContainerRuntime.versionMarkResolver` -> `IVersionMarkResolver`.
|
|
66
|
-
- `IVersionMarkResolver` methods: `sealAndCaptureVersionMark()` -> `VersionMarkCapture` (seals the batch and returns the
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
-
|
|
70
|
-
|
|
123
|
+
- `IVersionMarkResolver` methods: `sealAndCaptureVersionMark()` -> `VersionMarkCapture` (seals the batch and returns the
|
|
124
|
+
locator data atomically), `onBatchSequenced(listener)` (live promotion), `resolve(batchId, sequenceNumberLowerBound)`
|
|
125
|
+
-> `ResolveResult` (load-time sweep / restore).
|
|
126
|
+
- Types `IVersionMarkResolver`, `ResolveResult`, and `VersionMarkCapture` are exported from
|
|
127
|
+
`@fluidframework/container-runtime/legacy`.
|
|
128
|
+
- Restore side: `loadContainerToSequenceNumber` and `ILoadContainerToSequenceNumberProps` (`@legacy @beta`) are exported
|
|
129
|
+
from `@fluidframework/container-loader/legacy`, fed the `resolved` sequence number.
|
|
130
|
+
- ODSP point-in-time support: `createOdspDocumentServiceFactory` accepts the implementation
|
|
131
|
+
(`getOdspPointInTimeDocumentServiceFactory` / `IPointInTimeDocumentServiceFactory`) currently exported from the
|
|
132
|
+
`@fluidframework/odsp-driver/legacy/point-in-time` subpath. AB#81443 tracks moving it to the normal
|
|
133
|
+
`@fluidframework/odsp-driver/legacy` entrypoint without retaining the unused implementation graph in consumer bundles.
|
|
71
134
|
|
|
72
135
|
### Capturing a mark (app side)
|
|
73
136
|
|
|
74
|
-
Capture is a single `resolver.sealAndCaptureVersionMark()` call returning a `VersionMarkCapture` — either
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
- **
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
137
|
+
Capture is a single `resolver.sealAndCaptureVersionMark()` call returning a `VersionMarkCapture` — either
|
|
138
|
+
`{ kind: "pending", batchId, sequenceNumberLowerBound }` (an unacked local edit, resolve it later) or
|
|
139
|
+
`{ kind: "resolved", sequenceNumber, timestamp? }` (no in-flight local work). Notes for consumers:
|
|
140
|
+
|
|
141
|
+
- **It seals the current outbound batch synchronously** (flushes the runtime) so the just-submitted edit has a stable
|
|
142
|
+
`batchId` before it is read. Call it at savepoint boundaries (e.g. an explicit "snapshot this version"), not per
|
|
143
|
+
keystroke — it submits the pending batch as a side effect.
|
|
144
|
+
- **The runtime composes the pending-vs-resolved result atomically.** The app no longer reads a batchId and a sequence
|
|
145
|
+
number lower bound separately or decides pending-vs-resolved itself; that removes the earlier race where a batchId
|
|
146
|
+
still in the outbox came back stale/`undefined` and got paired with a mismatched lower bound, persisting a wrong
|
|
147
|
+
coordinate.
|
|
148
|
+
- **The app still owns storage.** `VersionMarkCapture` is a transient result, not a persisted locator type — the app
|
|
149
|
+
maps it into its own stored record.
|
|
150
|
+
- **Persist the timestamp with a resolved locator when present.** The runtime populates the server-generated timestamp,
|
|
151
|
+
while the optional API property keeps existing stored records and callers compatible.
|
|
152
|
+
- **Works while disconnected.** A disconnected flush stamps a stable placeholder `batchId` (carried across resubmit), so
|
|
153
|
+
capture returns a usable `pending` mark offline; no connection is required to capture.
|
|
154
|
+
|
|
155
|
+
Not consumed by the app (internal plumbing): `IContainerContextInternal.fetchOps`, the concrete `VersionMarkResolver`,
|
|
156
|
+
`IHistoricalOpReader`, `VersionMarkResolverRuntimeHooks` (including `getHistoricalOpReader` and
|
|
157
|
+
`createHistoricalOpUnpacker`), `inboundVersionMarkUpdate`, and `processInboundBatch`.
|
|
158
|
+
|
|
159
|
+
Before promotion beyond `@legacy @beta`, move the access point off the concrete `@internal` `ContainerRuntime` class
|
|
160
|
+
onto a public runtime interface (container-runtime-definitions) or the entryPoint / `FluidObject` provider pattern, and
|
|
161
|
+
resolve the API-shape questions in [Future work](#future-work). The interface is primitive-typed (no `MarkLocator` or
|
|
162
|
+
driver types leak). The `fetchOps` plumbing remains loader→runtime internal wiring on `IContainerContextInternal`; the
|
|
163
|
+
resolver, loader helper, and ODSP factory are the host-facing touchpoints.
|
|
84
164
|
|
|
85
165
|
## Loader-to-runtime wiring
|
|
86
166
|
|
|
87
|
-
`IContainerContextInternal` extends the public/legacy `IContainerContext` only inside the loader/runtime implementation
|
|
167
|
+
`IContainerContextInternal` extends the public/legacy `IContainerContext` only inside the loader/runtime implementation
|
|
168
|
+
boundary. Its optional function is:
|
|
88
169
|
|
|
89
170
|
```ts
|
|
90
171
|
fetchOps(
|
|
@@ -94,15 +175,25 @@ fetchOps(
|
|
|
94
175
|
): Promise<IStream<ISequencedDocumentMessage[]>>
|
|
95
176
|
```
|
|
96
177
|
|
|
97
|
-
The range follows delta-storage semantics, `[from, to)`: `from` is inclusive, `to` is exclusive, and an undefined `to`
|
|
178
|
+
The range follows delta-storage semantics, `[from, to)`: `from` is inclusive, `to` is exclusive, and an undefined `to`
|
|
179
|
+
means there is no fixed upper bound. `ContainerContext` stores the function unchanged. Its config requires a `fetchOps`
|
|
180
|
+
key even though the value may be `undefined`; this makes each constructor call explicitly state whether the host
|
|
181
|
+
provides historical reads.
|
|
98
182
|
|
|
99
|
-
The `Container.fetchOps` helper connects to delta storage on every call through `service.connectToDeltaStorage()`. This
|
|
183
|
+
The `Container.fetchOps` helper connects to delta storage on every call through `service.connectToDeltaStorage()`. This
|
|
184
|
+
avoids retaining a handle across reconnects, epoch changes, or service replacement. If the current service cannot
|
|
185
|
+
provide delta storage, it throws `"Cannot fetch ops: delta storage is unavailable"`. Otherwise it forwards `from`, `to`,
|
|
186
|
+
and `abortSignal` directly to `IDocumentDeltaStorageService.fetchMessages`.
|
|
100
187
|
|
|
101
|
-
During `ContainerRuntime` construction, the context is narrowed to `IContainerContextInternal`. The runtime stores the
|
|
188
|
+
During `ContainerRuntime` construction, the context is narrowed to `IContainerContextInternal`. The runtime stores the
|
|
189
|
+
concrete `VersionMarkResolver` in `versionMarkResolverInternal` and exposes it through the host-facing
|
|
190
|
+
`versionMarkResolver` getter typed as `IVersionMarkResolver`.
|
|
102
191
|
|
|
103
192
|
The runtime hooks are wired as follows:
|
|
104
193
|
|
|
105
194
|
- `getCurrentSequenceNumber` -> `deltaManager.lastSequenceNumber`.
|
|
195
|
+
- `getCurrentTimestamp` -> `getCurrentReferenceTimestampMs()`
|
|
196
|
+
(`deltaManager.lastMessage?.timestamp ?? messageAtLastSummary?.timestamp`).
|
|
106
197
|
- `getCurrentMinimumSequenceNumber` -> `deltaManager.minimumSequenceNumber`.
|
|
107
198
|
- `getCurrentPendingBatchId` -> `pendingStateManager.getMostRecentPendingBatchId()`.
|
|
108
199
|
- `flushPendingBatch` -> `ContainerRuntime.flush()`.
|
|
@@ -110,78 +201,189 @@ The runtime hooks are wired as follows:
|
|
|
110
201
|
- `getHistoricalOpReader` -> a lightweight `{ fetchMessages: fetchOps }` adapter when `fetchOps` exists.
|
|
111
202
|
- `createHistoricalOpUnpacker` -> a factory for a fresh `RemoteMessageProcessor` when `fetchOps` exists.
|
|
112
203
|
|
|
113
|
-
|
|
204
|
+
Reusing `getCurrentReferenceTimestampMs()` keeps the sequence number and timestamp paired at load:
|
|
205
|
+
`messageAtLastSummary.sequenceNumber` equals `deltaManager.initialSequenceNumber`, so before any new ops arrive the
|
|
206
|
+
fallback timestamp and reported sequence number come from the same op.
|
|
207
|
+
|
|
208
|
+
Each historical scan gets its own `RemoteMessageProcessor` because `OpSplitter` keeps chunk-reassembly state. The
|
|
209
|
+
processor is built with the runtime's chunk-size and max-batch-size options, an `OpDecompressor`, and an
|
|
210
|
+
`OpGroupingManager` configured with the runtime's grouped-batching setting. The returned unpack function filters
|
|
211
|
+
system/server messages, clones the op, deserializes string contents with `ensureContentsDeserialized`, and runs the
|
|
212
|
+
clone through `RemoteMessageProcessor.process`.
|
|
114
213
|
|
|
115
|
-
If `fetchOps` is absent, both historical hooks are absent. Live resolution still works, while an unknown id
|
|
214
|
+
If `fetchOps` is absent, both historical hooks are absent. Live resolution still works, while an unknown id
|
|
215
|
+
conservatively resolves to `pending`.
|
|
116
216
|
|
|
117
217
|
## Resolution behavior
|
|
118
218
|
|
|
119
219
|
### Live inbound tracking
|
|
120
220
|
|
|
121
|
-
`ContainerRuntime` owns `versionMarkInboundBatchId`, which carries a batch id between piecemeal inbound messages. After
|
|
221
|
+
`ContainerRuntime` owns `versionMarkInboundBatchId`, which carries a batch id between piecemeal inbound messages. After
|
|
222
|
+
`PendingStateManager.processInboundMessages` successfully validates an inbound result, the runtime checks
|
|
223
|
+
`versionMarkResolverInternal.isTracking`. If tracking is disabled, it skips all version-mark work on the hot path. If
|
|
224
|
+
tracking is enabled, it calls `inboundVersionMarkUpdate(inboundResult, versionMarkInboundBatchId)`, records any
|
|
225
|
+
completed batch through `processInboundBatch`, and stores the returned `carriedBatchId` for the next message.
|
|
122
226
|
|
|
123
|
-
The version-mark update runs **after** pending-state validation because that validation throws for a batch that must be
|
|
227
|
+
The version-mark update runs **after** pending-state validation because that validation throws for a batch that must be
|
|
228
|
+
rejected (fork detection or pending-content mismatch). `processInboundBatch` synchronously fires `onBatchSequenced`,
|
|
229
|
+
which an app may use to promote a mark in an external store; sequencing the update after validation ensures a rejected
|
|
230
|
+
batch never causes that irreversible side effect.
|
|
124
231
|
|
|
125
232
|
`inboundVersionMarkUpdate` handles every `InboundMessageResult` shape:
|
|
126
233
|
|
|
127
|
-
- `fullBatch`: derive the effective id from `batchStart`; resolve at the last message's sequence number
|
|
234
|
+
- `fullBatch`: derive the effective id from `batchStart`; resolve at the last message's sequence number and server
|
|
235
|
+
timestamp. An empty grouped batch has no messages, so it uses the batch-start key message's sequence number and
|
|
236
|
+
timestamp.
|
|
128
237
|
- `batchStartingMessage`: derive and carry the batch id without recording a sequence number yet.
|
|
129
|
-
- `nextBatchMessage` with `batchEnd: true`: if an id is being carried, resolve it at this final message's sequence
|
|
238
|
+
- `nextBatchMessage` with `batchEnd: true`: if an id is being carried, resolve it at this final message's sequence
|
|
239
|
+
number and server timestamp, then clear the carry.
|
|
130
240
|
- Mid-batch messages, or an end message without a carried id: preserve the current carry and emit no completed batch.
|
|
131
241
|
|
|
132
|
-
`VersionMarkResolver.processInboundBatch`
|
|
242
|
+
`VersionMarkResolver.processInboundBatch` records and broadcasts only the first resolved point observed for each
|
|
243
|
+
`batchId`. A later update for the same id is ignored, even if a spurious service redelivery assigns it a different
|
|
244
|
+
sequence number or timestamp; retaining the first-landed point avoids turning a tolerated duplicate into a container
|
|
245
|
+
fault or remapping an already-promoted mark. For a new id, the resolver inserts the resolved point, evicts entries below
|
|
246
|
+
the current MSN, and synchronously invokes every subscribed listener. Each listener has its own `try/catch`; a fault
|
|
247
|
+
emits `VersionMarkListenerException` and iteration continues.
|
|
133
248
|
|
|
134
|
-
The runtime does not store or mutate app marks. The listener only lets the app replace its own pending locator with the
|
|
249
|
+
The runtime does not store or mutate app marks. The listener only lets the app replace its own pending locator with the
|
|
250
|
+
supplied sequence number and server timestamp.
|
|
135
251
|
|
|
136
252
|
### Resolve control flow
|
|
137
253
|
|
|
138
254
|
`resolve(batchId, sequenceNumberLowerBound)` performs:
|
|
139
255
|
|
|
256
|
+
1. Enable tracking so inbound batches are recorded from here on (see [Tracking gate](#tracking-gate-istracking)).
|
|
140
257
|
1. Look up `batchId` in the session map. A hit immediately returns `resolved` and never consults storage.
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
258
|
+
1. Call `getHistoricalOpReader` on a miss. If no reader is available, return `pending`.
|
|
259
|
+
1. Set `from = sequenceNumberLowerBound`, create a fresh unpacker, and create an `AbortController`.
|
|
260
|
+
1. Request `fetchMessages(from, undefined, abortSignal)`.
|
|
261
|
+
1. Read every stream chunk until the target is found or the stream returns `done`.
|
|
262
|
+
1. Return the matched completed batch's last sequence number and server timestamp, or classify the miss.
|
|
263
|
+
1. Abort the controller in `finally`, both on success and on exhaustion/error, so the underlying fetch can stop any
|
|
264
|
+
remaining work.
|
|
265
|
+
1. `resolve()` emits one `Resolve` telemetry event (`outcome`, `path`, `durationMs`) before returning — on every path,
|
|
266
|
+
including a thrown scan (`outcome: "error"`, emitted via `finally`). See [Telemetry](#telemetry).
|
|
267
|
+
|
|
268
|
+
For each raw op, the scan records the first returned sequence number before filtering because that value is also the
|
|
269
|
+
trim-availability signal. It then:
|
|
270
|
+
|
|
271
|
+
1. Passes the op to the injected historical unpacker. `undefined` means the op produced no complete inbound result.
|
|
272
|
+
1. Calls `inboundVersionMarkUpdate`, carrying the batch id across piecemeal results exactly as the live path does.
|
|
273
|
+
1. Returns immediately when the completed batch id matches the requested id.
|
|
274
|
+
|
|
275
|
+
Routing scanned ops through the live unpack pipeline is required for **chunked batches**. Chunking strips the `batchId`
|
|
276
|
+
from the final chunk's wire metadata and restores it only after `OpSplitter` reassembly. A raw metadata scan would
|
|
277
|
+
therefore miss a resubmitted chunked batch. The shared pipeline also keeps grouped and compressed batch handling
|
|
278
|
+
consistent with live processing.
|
|
279
|
+
|
|
280
|
+
**Scan-anchor handling (chunk streams).** Ordinary multi-op batches are not split by the capture anchor:
|
|
281
|
+
`InboundBatchAggregator` keeps them atomic when delivering them to the runtime. Chunk streams are different. The
|
|
282
|
+
DeltaManager advances `lastSequenceNumber` for each intermediate chunk before `OpSplitter` has reconstructed the
|
|
283
|
+
original message, so `sequenceNumberLowerBound` can start in the middle of a chunk stream.
|
|
284
|
+
|
|
285
|
+
Each historical scan uses a fresh `OpSplitter` (chunk-reassembly state must not be shared across scans) configured to
|
|
286
|
+
allow the first observed stream for each client to begin after chunk 1. It discards the unreconstructable remainder of
|
|
287
|
+
that stream, then enforces normal chunk ordering for that client. The live inbound `OpSplitter` remains strict. This
|
|
288
|
+
behavior stays within the injected unpack pipeline so `VersionMarkResolver` does not inspect or track virtualization
|
|
289
|
+
details. `opSplitter.spec.ts` covers partial and complete streams interleaved across clients, and
|
|
290
|
+
`versionMarkResolver.spec.ts` retains one integration regression through the real unpack pipeline.
|
|
291
|
+
|
|
292
|
+
An **ordinary (non-chunked) batch tail is never clipped**, on the live queue or the replayed `fetchMessages` stream, so
|
|
293
|
+
the scan needs no ordinary-batch orphan-end guard. `InboundBatchAggregator` aggregates a runtime-observed batch
|
|
294
|
+
atomically and `DeltaQueue` drains it synchronously, so a runtime-observed `lastSequenceNumber` never lands
|
|
295
|
+
mid-ordinary-batch; the capture anchor (`sequenceNumberLowerBound`) therefore cannot start between an ordinary batch's
|
|
296
|
+
first and last op. This holds for the replay path because it feeds the same `RemoteMessageProcessor`/`OpSplitter`
|
|
297
|
+
pipeline, and delta storage returns whole batches (only chunk streams expose intermediate sequence numbers). If a
|
|
298
|
+
clipped ordinary-batch tail ever did reach the unpacker, it would not resolve silently:
|
|
299
|
+
`RemoteMessageProcessor.getResultBasedOnBatchMetadata` still asserts `0x9d5` on a `batch: false` marker with no batch in
|
|
300
|
+
progress, so the invariant fails loudly rather than masking a regression from a future replay/capture-path change.
|
|
301
|
+
|
|
302
|
+
This orphan-tail premise relies on every persisted locator using the current inclusive scheme
|
|
303
|
+
(`sequenceNumberLowerBound = referenceSequenceNumber + 1`), so a scan always begins at the pending batch's own first op,
|
|
304
|
+
never at the last op `S` of a preceding batch. That holds because the earlier exclusive scheme (which stored `S` and
|
|
305
|
+
scanned from `S + 1` at read time) was **never shipped** — there are no pre-PR persisted locators. If one somehow
|
|
306
|
+
existed, scanning from `S` could start on a preceding batch's `batch: false` orphan tail (tripping `0x9d5`) or
|
|
307
|
+
reclassify a miss from `pending` to `unresolvable`; because the old format never shipped, neither case can arise and no
|
|
308
|
+
compat guard is needed. Any future change that alters the stored anchor's meaning must revisit this (see
|
|
309
|
+
[Persisted-locator format compatibility](#resolver-correctness-and-robustness-gaps)).
|
|
310
|
+
|
|
311
|
+
### `pending` vs `unresolvable` on a miss (read-derived availability)
|
|
312
|
+
|
|
313
|
+
Here, **trimmed** means that older sequenced ops are no longer retained or returned by the service's delta storage. It
|
|
314
|
+
does not refer to eviction from the resolver's fast-path cache or to an op falling below the minimum sequence number
|
|
315
|
+
(MSN).
|
|
316
|
+
|
|
317
|
+
When the scan does not find the batch, the result distinguishes **`pending`** ("not sequenced yet — retry later") from
|
|
318
|
+
**`unresolvable`** ("its ops were trimmed — gone forever"). Both look identical from the batch id alone (the batch is
|
|
319
|
+
simply absent), so the distinction uses a **read-derived availability signal**: the current tip
|
|
320
|
+
(`getCurrentSequenceNumber`) plus where the scan's first op landed relative to `from = sequenceNumberLowerBound`:
|
|
321
|
+
|
|
322
|
+
- `from > tip` — nothing is sequenced at/after the lower bound yet, so the batch cannot have landed → **`pending`**.
|
|
323
|
+
- Empty read while ops should exist (`from <= tip`) — the requested range came back empty, which for a strict driver
|
|
324
|
+
(ODSP's `validateMessages` empties a from-misaligned trimmed range) means the range was trimmed → **`unresolvable`**.
|
|
325
|
+
- First available op is past `from` — a trim gap at the anchor; the mark's batch (sequenced just after the reference
|
|
326
|
+
point) was trimmed → **`unresolvable`**. A found batch resolves before this check, so a gap **on a miss** genuinely
|
|
327
|
+
means the batch's ops are gone, not merely preceded by other clients' ops (which are still present at `from`).
|
|
328
|
+
- Ops present from `from` but the batch is not among them — it has not been sequenced yet → **`pending`**.
|
|
147
329
|
|
|
148
|
-
|
|
330
|
+
This is an **interim, read-derived** signal: it infers availability from how the driver responds to a trimmed range,
|
|
331
|
+
which is driver-behavior-dependent (strict-empty vs return-from-earliest) and degrades to the conservative outcome when
|
|
332
|
+
ambiguous. A dedicated driver op-availability / retention API (e.g. an explicit earliest-retained-sequence-number query
|
|
333
|
+
on `IDocumentDeltaStorageService`, coordinated across drivers) would replace it with a precise, contractual signal — a
|
|
334
|
+
separate follow-up.
|
|
149
335
|
|
|
150
|
-
|
|
151
|
-
2. Passes the op to the unpacker. `undefined` means a filtered system op or an incomplete chunk waiting for more fragments.
|
|
152
|
-
3. Mirrors the unpacker's batch-in-progress state from `batchStartingMessage` and final `nextBatchMessage` results.
|
|
153
|
-
4. Calls `inboundVersionMarkUpdate`, carrying the batch id across piecemeal results exactly as the live path does.
|
|
154
|
-
5. Returns immediately when the completed batch id matches the requested id.
|
|
336
|
+
### Error handling and invariants
|
|
155
337
|
|
|
156
|
-
|
|
338
|
+
- A historical reader must never return an op below `from`. `classifyMiss` asserts this because trim classification is
|
|
339
|
+
invalid if the range contract is violated.
|
|
340
|
+
- Delta-storage connection/fetch failures and unpacking failures propagate to the caller. They are operational failures,
|
|
341
|
+
not legitimate `pending` or `unresolvable` results.
|
|
342
|
+
- The `AbortController` is aborted in `finally`, including when a reader, stream, or unpacker throws.
|
|
343
|
+
- Listener failures are isolated, logged, and skipped because a missed live promotion remains recoverable through
|
|
344
|
+
history.
|
|
345
|
+
- Inbound pending-state validation runs before notification. A rejected or forked batch cannot cause an app-side
|
|
346
|
+
promotion.
|
|
347
|
+
- `resolvedBatchById` is only a session cache. Correctness must not depend on an entry remaining present; a miss can
|
|
348
|
+
fall back to retained history.
|
|
157
349
|
|
|
158
|
-
|
|
350
|
+
## Telemetry
|
|
159
351
|
|
|
160
|
-
|
|
352
|
+
Version-mark telemetry is greenfield; this section is the **contract** every event (FF-side and app-side) codes against,
|
|
353
|
+
so all apps can feed one shared dashboard. Design events around the questions a dashboard asks, not around code lines.
|
|
161
354
|
|
|
162
|
-
|
|
355
|
+
### Principles
|
|
163
356
|
|
|
164
|
-
|
|
357
|
+
- **One event per logical operation, with a low-cardinality `outcome`/`path` enum** — not many scattered events.
|
|
358
|
+
Group-by dimensions must stay low-cardinality (enums, booleans, counts, durations).
|
|
359
|
+
- **Never put high-cardinality/PII values (`batchId`, `clientId`, `docId`) in group-by dimensions.** When a correlation
|
|
360
|
+
key is needed, emit it as a **tagged detail** (`TelemetryDataTag`), not a dimension.
|
|
361
|
+
- **Severity discipline:** informational (`sendTelemetryEvent`) for usage/health and _smells_; `sendErrorEvent` only for
|
|
362
|
+
provable faults (corruption is already covered by asserts).
|
|
363
|
+
- **Instrument the infrequent control points** (capture, resolve) directly — they are savepoint/load-time, not hot.
|
|
364
|
+
Never emit per-op/per-batch events on `onBatchSequenced`; aggregate (counters or `SampledTelemetryHelper`) if
|
|
365
|
+
per-batch signal is ever needed.
|
|
366
|
+
- The resolver's logger is namespaced `VersionMarkResolver`, so event names below are emitted as
|
|
367
|
+
`VersionMarkResolver:<name>`.
|
|
165
368
|
|
|
166
|
-
|
|
369
|
+
### Events
|
|
167
370
|
|
|
168
|
-
|
|
371
|
+
| Event | When | Dimensions | Answers |
|
|
372
|
+
| -------------------------------------- | ---------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
373
|
+
| `Resolve` **(implemented)** | end of `resolve()` (via `finally`, so a thrown scan is still reported) | `outcome` (`resolved`\|`pending`\|`unresolvable`\|`error`), `path` (`session`\|`history`\|`noReader`), `durationMs`, `sequenceNumber` (when resolved) | success rate; how often history is needed; latency; `unresolvable` = data-loss KPI; `error` = the scan threw. `session` means the final result came from the live map, including a live resolution found by the post-history recheck. |
|
|
374
|
+
| `Capture` **(implemented — AB#80270)** | `sealAndCaptureVersionMark()` | `kind` (`pending`\|`resolved`) | capture volume; pending ratio |
|
|
169
375
|
|
|
170
|
-
|
|
171
|
-
- Empty read while ops should exist (`from <= tip`) — the requested range came back empty, which for a strict driver (ODSP's `validateMessages` empties a from-misaligned trimmed range) means the range was trimmed → **`unresolvable`**.
|
|
172
|
-
- First available op is past `from` — a trim gap at the anchor; the mark's batch (sequenced just after the reference point) was trimmed → **`unresolvable`**. A found batch resolves before this check, so a gap **on a miss** genuinely means the batch's ops are gone, not merely preceded by other clients' ops (which are still present at `from`).
|
|
173
|
-
- Ops present from `from` but the batch is not among them — it has not been sequenced yet → **`pending`**.
|
|
376
|
+
### Correlation and the app funnel (planned)
|
|
174
377
|
|
|
175
|
-
|
|
378
|
+
FF emits only what it can observe (the runtime mechanism). The product funnel — locator persisted, restore
|
|
379
|
+
initiated/applied, trigger source (user vs. NiTL agent), user accept/discard — is owned by the app/host (office-bohemia;
|
|
380
|
+
AB#80271) and cannot be emitted from FF. For a shared cross-app dashboard, both layers must:
|
|
176
381
|
|
|
177
|
-
|
|
382
|
+
- use this **stable, app-agnostic schema** plus an `app`/`host` dimension (the host logger supplies host context), and
|
|
383
|
+
- stamp shared **correlation keys** — `containerId` (auto-tagged by the FF logger) and the mark's `batchId` (as a tagged
|
|
384
|
+
detail) — so FF `Resolve` events join to the app's restore events for the same mark.
|
|
178
385
|
|
|
179
|
-
|
|
180
|
-
- Delta-storage connection/fetch failures and unpacking failures propagate to the caller. They are operational failures, not legitimate `pending` or `unresolvable` results.
|
|
181
|
-
- The `AbortController` is aborted in `finally`, including when a reader, stream, or unpacker throws.
|
|
182
|
-
- Listener failures are isolated, logged, and skipped because a missed live promotion remains recoverable through history.
|
|
183
|
-
- Inbound pending-state validation runs before notification. A rejected or forked batch cannot cause an app-side promotion.
|
|
184
|
-
- `sequenceNumberByBatchId` is only a session cache. Correctness must not depend on an entry remaining present; a miss can fall back to retained history.
|
|
386
|
+
The shared rollout dashboard is tracked in AB#80150.
|
|
185
387
|
|
|
186
388
|
## Loading a mark
|
|
187
389
|
|
|
@@ -191,138 +393,374 @@ Loading (restoring) a mark is two explicit steps, and the resolver is the bridge
|
|
|
191
393
|
locator --resolve()--> sequenceNumber --loadContainerToSequenceNumber()--> IContainer
|
|
192
394
|
```
|
|
193
395
|
|
|
194
|
-
1. `resolve(batchId, sequenceNumberLowerBound)` turns the locator into a concrete `sequenceNumber` (a `resolved` mark
|
|
195
|
-
|
|
396
|
+
1. `resolve(batchId, sequenceNumberLowerBound)` turns the locator into a concrete `sequenceNumber` (a `resolved` mark
|
|
397
|
+
already carries its `sequenceNumber`, so it skips this step entirely).
|
|
398
|
+
1. `loadContainerToSequenceNumber({ request, loadToSequenceNumber, ... })` (the loader's point-in-time primitive)
|
|
399
|
+
materializes a read-only container at that sequence number.
|
|
196
400
|
|
|
197
|
-
The load primitive stays **mark-agnostic** — it takes a raw `sequenceNumber` and never learns what a "mark" is. This is
|
|
198
|
-
|
|
199
|
-
|
|
401
|
+
The load primitive stays **mark-agnostic** — it takes a raw `sequenceNumber` and never learns what a "mark" is. This is
|
|
402
|
+
the deliberate design choice (decision A): the resolver owns the locator→sequence translation, the loader owns
|
|
403
|
+
materialization, and the two do not merge. Its current container-loader placement is prototype-era ownership; the
|
|
404
|
+
planned extraction of the host-facing load orchestration into a dedicated feature package is documented in the
|
|
200
405
|
[point-in-time loading guide](../../../../loader/container-loader/src/pointInTime/DEV.md#package-ownership-and-planned-extraction).
|
|
201
406
|
|
|
407
|
+
The ODSP point-in-time implementation creates the recoverable (historical snapshot) document service before the live
|
|
408
|
+
document service used for bounded forward replay. If live-service creation fails, it disposes the already-created
|
|
409
|
+
recoverable service and rethrows the original live-service error (AB#77964). If cleanup itself throws, that secondary
|
|
410
|
+
failure is logged without replacing the creation error.
|
|
411
|
+
|
|
202
412
|
### Why the load takes a sequence number, not a locator
|
|
203
413
|
|
|
204
|
-
This mirrors `IUrlResolver.resolve(request): Promise<IResolvedUrl | undefined>` — resolution is a **separate step that
|
|
414
|
+
This mirrors `IUrlResolver.resolve(request): Promise<IResolvedUrl | undefined>` — resolution is a **separate step that
|
|
415
|
+
returns a value** (including "can't resolve" as a value, not a throw), and load consumes the resolved form. It also
|
|
416
|
+
matches how `IFluidHandle` surfaces a pending payload state rather than hiding it: when resolution has a legitimate
|
|
417
|
+
non-error "not yet" outcome, the codebase exposes it as a first-class value.
|
|
205
418
|
|
|
206
419
|
A locator-taking load (a single `loadContainerToMark(locator)` call) was considered and rejected:
|
|
207
420
|
|
|
208
|
-
- Return-type impedance: such a call returns `Promise<IContainer>`, but two of the three resolve outcomes (`pending`,
|
|
209
|
-
|
|
210
|
-
|
|
421
|
+
- Return-type impedance: such a call returns `Promise<IContainer>`, but two of the three resolve outcomes (`pending`,
|
|
422
|
+
`unresolvable`) yield no container. It would have to either throw for both (collapsing "retry later" and "gone
|
|
423
|
+
forever" into one error path) or return a union the caller must branch on anyway.
|
|
424
|
+
- Lifecycle mismatch: the resolver is a **live-session** object bound to an already-open container's runtime and its op
|
|
425
|
+
reader, while the load creates a **new read-only historical** container. A one-call wrapper would need a live resolver
|
|
426
|
+
injected into a load that produces a different container instance, conflating two container lifecycles.
|
|
427
|
+
- Redundancy: `resolve()` must exist as a standalone call regardless — the app uses it for live promotion
|
|
428
|
+
(`onBatchSequenced`), for the load-time sweep, and to render pending / unresolvable state in the UI. A wrapper would
|
|
429
|
+
just duplicate it.
|
|
211
430
|
|
|
212
|
-
If one-call ergonomics are ever wanted, the right shape is a thin wrapper that **returns the three-state result**
|
|
431
|
+
If one-call ergonomics are ever wanted, the right shape is a thin wrapper that **returns the three-state result**
|
|
432
|
+
(`IContainer` on `resolved`, else `pending` / `unresolvable`) layered on top of these two primitives — not a change to
|
|
433
|
+
the primitives.
|
|
213
434
|
|
|
214
435
|
## Removed runtime persistence
|
|
215
436
|
|
|
216
|
-
The previous runtime-owned marks map and `.versionMarks` summary blob were removed. There is no runtime durable mark
|
|
437
|
+
The previous runtime-owned marks map and `.versionMarks` summary blob were removed. There is no runtime durable mark
|
|
438
|
+
store and no summarized durable `batchId -> resolved point` index. Once a batch resolves, the app must persist the
|
|
439
|
+
sequence number and, when present, its server timestamp in its own store.
|
|
217
440
|
|
|
218
441
|
## Fast-path cache bounding (MSN eviction)
|
|
219
442
|
|
|
220
|
-
`VersionMarkResolver.
|
|
443
|
+
`VersionMarkResolver.resolvedBatchById` (`Map<batchId, { sequenceNumber, timestamp }>`) is a **live-session fast-path
|
|
444
|
+
cache**, not a source of truth: `processInboundBatch` inserts the first entry observed for each batch id, and
|
|
445
|
+
`resolve()` reads it only as the fast path before falling back to the historical-op scan (`resolveFromHistory`).
|
|
446
|
+
Although a batch id normally maps to one resolved point, a spurious service redelivery can reuse an effective batch id
|
|
447
|
+
at a different sequence number. The implementation keeps the first-landed resolution and ignores later updates for that
|
|
448
|
+
id. A miss degrades to the history scan when a reader is available, so eviction affects speed rather than correctness
|
|
449
|
+
while the ops remain retained.
|
|
221
450
|
|
|
222
451
|
### What MSN means here
|
|
223
452
|
|
|
224
|
-
The minimum sequence number (MSN) is the protocol's collaboration-window floor. When an op is below the current MSN,
|
|
453
|
+
The minimum sequence number (MSN) is the protocol's collaboration-window floor. When an op is below the current MSN,
|
|
454
|
+
every active client has advanced far enough to have processed it. The resolver reads the value from
|
|
455
|
+
`deltaManager.minimumSequenceNumber` through `getCurrentMinimumSequenceNumber`.
|
|
225
456
|
|
|
226
|
-
MSN is **not** part of a stored mark and is not used to create, resolve, expire, or validate marks. Marks come only from
|
|
457
|
+
MSN is **not** part of a stored mark and is not used to create, resolve, expire, or validate marks. Marks come only from
|
|
458
|
+
an app calling `sealAndCaptureVersionMark()` and storing the returned locator. MSN also is **not** the service's
|
|
459
|
+
op-retention boundary: an op can be below MSN and still be available from delta storage, or can later be trimmed
|
|
460
|
+
according to service policy. `unresolvable` is about delta-storage retention, not MSN.
|
|
227
461
|
|
|
228
|
-
The resolver uses MSN only to answer a cache-lifetime question: how long should a live container retain every observed
|
|
462
|
+
The resolver uses MSN only to answer a cache-lifetime question: how long should a live container retain every observed
|
|
463
|
+
`batchId -> { sequenceNumber, timestamp }` mapping? Without eviction, a long-running container would add one entry for
|
|
464
|
+
every tracked inbound batch and the map would grow without bound. MSN provides a protocol-derived, workload-sensitive
|
|
465
|
+
boundary instead of a fixed entry count or timeout.
|
|
229
466
|
|
|
230
467
|
### Why eviction below MSN is useful
|
|
231
468
|
|
|
232
|
-
For the normal live-promotion path, a batch below MSN has already passed every active client. If tracking was enabled,
|
|
469
|
+
For the normal live-promotion path, a batch below MSN has already passed every active client. If tracking was enabled,
|
|
470
|
+
`processInboundBatch` has already fired `onBatchSequenced`, giving the app an opportunity to replace its stored pending
|
|
471
|
+
locator with the durable sequence number and server timestamp. Keeping that batch in the resolver's session cache after
|
|
472
|
+
it leaves the collaboration window is therefore only an optimization for repeated lookups.
|
|
233
473
|
|
|
234
|
-
Eviction does not delete an app-owned mark or its resolved sequence number. A later `resolve()` cache miss scans
|
|
474
|
+
Eviction does not delete an app-owned mark or its resolved sequence number. A later `resolve()` cache miss scans
|
|
475
|
+
retained historical ops when `fetchOps` is available. If no historical reader is wired, an evicted id returns `pending`;
|
|
476
|
+
in that configuration the consumer must rely on the live `onBatchSequenced` promotion having been persisted. Likewise,
|
|
477
|
+
listener failure is recoverable only when historical reads remain available.
|
|
235
478
|
|
|
236
|
-
MSN speaks only about active clients in the current collaboration window. A disconnected client, a client loading much
|
|
479
|
+
MSN speaks only about active clients in the current collaboration window. A disconnected client, a client loading much
|
|
480
|
+
later, or a host that subscribes after a batch was processed cannot rely on the live cache or notification; those cases
|
|
481
|
+
are why the stored locator includes a history anchor and why historical resolution exists.
|
|
237
482
|
|
|
238
483
|
### Eviction algorithm and invariants
|
|
239
484
|
|
|
240
|
-
`processInboundBatch` inserts the completed batch, then calls `evictBelowMinimumSequenceNumber()`. Entries are observed
|
|
485
|
+
`processInboundBatch` inserts the completed batch, then calls `evictBelowMinimumSequenceNumber()`. Entries are observed
|
|
486
|
+
and inserted in sequence order. The eviction loop walks the `Map` from its oldest insertion:
|
|
241
487
|
|
|
242
488
|
1. Delete each entry whose `sequenceNumber < minimumSequenceNumber`.
|
|
243
|
-
|
|
489
|
+
1. Stop at the first entry whose `sequenceNumber >= minimumSequenceNumber`; all later entries are also expected to be in
|
|
490
|
+
the collaboration window.
|
|
244
491
|
|
|
245
|
-
This makes cleanup proportional to the number of entries actually evicted (amortized O(evicted)). The just-recorded
|
|
492
|
+
This makes cleanup proportional to the number of entries actually evicted (amortized O(evicted)). The just-recorded
|
|
493
|
+
inbound batch is at or above the current MSN, so it is retained. The implementation also relies on the invariant that a
|
|
494
|
+
stable `batchId` never remaps to a different sequence number (the protocol guarantees this); `processInboundBatch`
|
|
495
|
+
**asserts** it rather than silently overwriting, because changing an existing key without moving its insertion position
|
|
496
|
+
would break the ordered early-exit assumption.
|
|
246
497
|
|
|
247
498
|
### Tracking gate (`isTracking`)
|
|
248
499
|
|
|
249
|
-
Per-inbound-batch work (deriving the batch identity and populating the map/notifying listeners) is **gated on a sticky
|
|
500
|
+
Per-inbound-batch work (deriving the batch identity and populating the map/notifying listeners) is **gated on a sticky
|
|
501
|
+
`isTracking` flag**, so a container that never uses version marks does no version-mark work on the hot path. This
|
|
502
|
+
mirrors #22497, which gated `DuplicateBatchDetector` on offline load being enabled even though its cost was small —
|
|
503
|
+
there is no reason to pay a predictable per-batch cost for a feature that can't do anything. Tracking flips on (and
|
|
504
|
+
stays on) the first time the feature is actually used this session: a **pending** `sealAndCaptureVersionMark()` (a
|
|
505
|
+
resolved capture needs no tracking), an `onBatchSequenced` subscription, or a `resolve()` call. `resolve()` enables it
|
|
506
|
+
up front so that a batch which sequences during the history scan (or live, in the no-reader case) is recorded and can be
|
|
507
|
+
recovered by the post-scan session-map recheck, even when the caller never captured or subscribed. The runtime reads
|
|
508
|
+
`versionMarkResolverInternal.isTracking` and skips the whole update block while it is false. A batch in flight at the
|
|
509
|
+
moment tracking flips on may be missed, which is harmless: an app's own captured mark is for a not-yet-sequenced edit
|
|
510
|
+
(tracked once it lands), and cross-session resolution uses the history scan regardless.
|
|
250
511
|
|
|
251
512
|
## Current test map
|
|
252
513
|
|
|
253
514
|
- `src/test/versionMarks/inboundBatch.spec.ts` covers full, empty, derived-id, explicit-id, and piecemeal batch updates.
|
|
254
|
-
- `src/test/versionMarks/versionMarkResolver.spec.ts` covers capture ordering/results, the tracking gate, live-map
|
|
255
|
-
-
|
|
256
|
-
|
|
515
|
+
- `src/test/versionMarks/versionMarkResolver.spec.ts` covers capture ordering/results, the tracking gate, live-map
|
|
516
|
+
precedence, no-reader behavior, fresh and resubmitted batches, multi-op batches across stream reads, chunk reassembly,
|
|
517
|
+
a partial initial stream interleaved with a complete chunked target through the real unpack pipeline, miss
|
|
518
|
+
classifications, range arguments, reader-contract assertion, abort behavior, listener
|
|
519
|
+
isolation/unsubscribe/deduplication, MSN eviction, and the `Resolve` telemetry event.
|
|
520
|
+
- `src/test/opLifecycle/opSplitter.spec.ts` covers strict chunk ordering plus the historical configuration that discards
|
|
521
|
+
only a client's first partial stream while independently reconstructing complete interleaved streams.
|
|
522
|
+
- `src/test/opLifecycle/opSerialization.spec.ts` covers `tryGetDeserializedRuntimeOpCopy` (runtime-op copy with
|
|
523
|
+
deserialized contents; non-runtime and clientless ops return undefined without deserializing).
|
|
524
|
+
- `src/test/pendingStateManager.spec.ts` covers ignoring unapplied stashed messages and reading an explicit
|
|
525
|
+
reconnect-stable id from the start of the most recently flushed multi-op batch.
|
|
526
|
+
- `src/test/containerRuntime.spec.ts` covers the complete context `fetchOps` -> historical unpack -> resolver path
|
|
527
|
+
(including system/server-op filtering and abort-after-match), plus the ordering guarantee that failed inbound
|
|
528
|
+
validation does not notify listeners.
|
|
257
529
|
|
|
258
530
|
## Future work
|
|
259
531
|
|
|
260
532
|
### Missing end-to-end coverage
|
|
261
533
|
|
|
262
|
-
The existing real-service ODSP suites under `packages/test/test-end-to-end-tests/src/test/pointInTime/` begin with a
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
534
|
+
The existing real-service ODSP suites under `packages/test/test-end-to-end-tests/src/test/pointInTime/` begin with a
|
|
535
|
+
known sequence number and exercise loading. They do not create that sequence number through the version-mark API. Extend
|
|
536
|
+
`pointInTimeTestUtils.ts` with a host entry point that exposes `IVersionMarkResolver`, then add:
|
|
537
|
+
|
|
538
|
+
1. **Pending mark to historical load:** Make a local edit, call `sealAndCaptureVersionMark()`, persist the pending
|
|
539
|
+
locator outside the runtime, sequence the batch, resolve the locator, and load the resulting sequence number. Verify
|
|
540
|
+
the loaded state includes the marked edit and excludes later edits.
|
|
541
|
+
1. **Already-resolved capture:** Capture with no local pending batch and load the returned sequence number directly,
|
|
542
|
+
proving the no-resolution path produces the expected historical state.
|
|
543
|
+
1. **Live promotion from another client:** Capture on one client and use `onBatchSequenced` on another connected client
|
|
544
|
+
to promote the stored locator, proving batch identity is observable across clients.
|
|
545
|
+
1. **Reconnect and resubmission:** Capture before disconnect, reconnect and resubmit the multi-op batch with its
|
|
546
|
+
original explicit `batchId`, then verify both live and historical resolution still find the mark.
|
|
547
|
+
1. **Capturing-client loss:** Close the capturing client before its acknowledgement, open a fresh client, resolve from
|
|
548
|
+
retained historical ops, and load the marked state. This is the primary cross-session recovery scenario.
|
|
549
|
+
1. **Transformed batches:** Capture edits that produce grouped, compressed, and chunked batches and verify the real
|
|
550
|
+
inbound/history pipelines recover the same effective batch identity.
|
|
551
|
+
1. **Pending and retention outcomes:** Resolve before the batch sequences and observe `pending`; when the test
|
|
552
|
+
environment can deterministically trim the target ops, verify the same stored locator becomes `unresolvable`.
|
|
553
|
+
1. **Offline and staging lifecycles:** Cover stash/rehydration plus staging commit and discard once those capture
|
|
554
|
+
contracts are finalized.
|
|
555
|
+
1. **Repeated capture and multiple pending batches:** Capture twice while the same batch remains unacknowledged, then
|
|
556
|
+
capture after a second local batch is flushed. Verify each mark identifies the latest batch whose state it includes
|
|
557
|
+
and that changing remote sequence numbers between captures does not produce an invalid lower bound.
|
|
558
|
+
1. **Tracking activation boundaries:** Subscribe while an inbound batch is already being processed and immediately after
|
|
559
|
+
a batch completed. Verify the documented behavior at each boundary and prove that a missed notification remains
|
|
560
|
+
recoverable through `resolve()`.
|
|
561
|
+
1. **Sequenced-before-persisted restore:** Resolve a mark from the live map as soon as its batch sequences, then
|
|
562
|
+
immediately start a point-in-time load before ODSP has flushed that op to durable delta storage. Define whether the
|
|
563
|
+
host waits, explicitly requests an op flush, or retries later, and verify a resolved locator never implies that the
|
|
564
|
+
target is already materializable by a storage-only loader.
|
|
275
565
|
|
|
276
566
|
### Resolver correctness and robustness gaps
|
|
277
567
|
|
|
278
|
-
1. **
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
568
|
+
1. **Concurrent history scans:** AB#82260 fixes the stale-miss race by rechecking the live map after a non-resolved
|
|
569
|
+
history result and preferring a batch that sequenced during the scan. Still decide whether concurrent calls for the
|
|
570
|
+
same locator should share one scan rather than issuing duplicate delta-storage reads.
|
|
571
|
+
1. **Finite and cancelable history reads:** The current scan requests `[lowerBound, undefined)`, has no caller-supplied
|
|
572
|
+
cancellation signal, and can inherit long driver retry behavior. Consider snapshotting an upper bound from the
|
|
573
|
+
observed tip, accepting an `AbortSignal`, and canceling outstanding scans when the runtime is disposed. Cover
|
|
574
|
+
cancellation before fetch, during `fetchMessages`, during `stream.read()`, and after a match races cancellation.
|
|
575
|
+
1. **Stored-locator validation:** `batchId` and `sequenceNumberLowerBound` may come from app-owned persisted data.
|
|
576
|
+
Define whether `resolve()` trusts that data or rejects an empty batch ID and negative/fractional/non-safe sequence
|
|
577
|
+
numbers. Invalid persisted coordinates must not silently scan the wrong range or be classified as a legitimate mark
|
|
578
|
+
state. The safe-integer `+ 1` consideration now applies at **capture** rather than read: `sealAndCaptureVersionMark`
|
|
579
|
+
stores `referenceSequenceNumber + 1`, so that addition (not a read-time `lowerBound + 1`) is the arithmetic subject
|
|
580
|
+
to safe-integer precision.
|
|
581
|
+
1. **Persisted-locator format compatibility:** The app persists the `pending` locator (`batchId` +
|
|
582
|
+
`sequenceNumberLowerBound`) and may resolve it much later, so the meaning of those fields is a compatibility
|
|
583
|
+
contract. The risk is that `sequenceNumberLowerBound` is a bare number with no format/version marker, so a semantic
|
|
584
|
+
change is invisible to a reader: e.g. the exclusive→inclusive change would make old code reading a new-format locator
|
|
585
|
+
compute `stored + 1` and silently scan past the target. This is not a concern today because the exclusive format was
|
|
586
|
+
**never shipped** (see [Resolve control flow](#resolve-control-flow)) — the current inclusive format is the implicit
|
|
587
|
+
v1 baseline and there are no other persisted formats. **Decision (deferred):** add nothing now; treat today's format
|
|
588
|
+
as v1 and only do compatibility work _if/when_ the locator's meaning or shape actually changes. If it never changes,
|
|
589
|
+
there is nothing to do. Marks are resolved cross-client (whichever client picks one up first) across a mixed-version
|
|
590
|
+
fleet, so a future change must be introduced compat-safely — following FF precedent, either (a) gate the new format's
|
|
591
|
+
**write** behind `minVersionForCollab`/`OldestSupportedClientVersion` so older readers in the compat window never
|
|
592
|
+
encounter it (as versioned codecs pick their write version), or (b) add an explicit `formatVersion` field and detect
|
|
593
|
+
it on read, defaulting a missing marker to v1 and failing closed on an unrecognized one (as
|
|
594
|
+
`summaryFormat.getAttributesFormatVersion` maps a missing version to legacy v0). A tiny two-field locator suits the
|
|
595
|
+
lightweight `summaryFormat`-style field inspection over the full versioned-codec subsystem in
|
|
596
|
+
`dds/tree/src/codec/versioned`. Because the app owns the stored record (`VersionMarkCapture` is transient), decide at
|
|
597
|
+
that point whether FF stamps the version into the capture result (preferred, since FF owns the semantics) or
|
|
598
|
+
documents that the app must version its own record.
|
|
599
|
+
1. **Historical stream contract:** Add direct coverage for duplicated, decreasing, gapped, and malformed batch/chunk
|
|
600
|
+
sequences, plus a target found after a trim gap. Define which violations are rejected by the driver, which are
|
|
601
|
+
rejected by the resolver, and which conservatively return `pending`; never infer `unresolvable` from a stream that
|
|
602
|
+
violated the requested range contract.
|
|
284
603
|
|
|
285
604
|
### API and design follow-ups
|
|
286
605
|
|
|
287
|
-
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
606
|
+
- **Loader/runtime compatibility pattern (resolved with Navin).** `fetchOps` is a new loader-provided API consumed by
|
|
607
|
+
runtime logic. Making the member optional and using its presence as the capability signal is the correct cross-layer
|
|
608
|
+
pattern; a separate `supportedFeatures` negotiation is unnecessary for a new API. Fluid supports a 12-month Runtime ->
|
|
609
|
+
Loader compatibility window, so a newer runtime must continue to work with an older loader that predates `fetchOps`.
|
|
610
|
+
The capability first shipped in client 2.115.0 at layer generation 9. Keep the optional branch through generation 20;
|
|
611
|
+
at generation 21 every supported loader is guaranteed to provide it, and AB#81034 tracks making the member required
|
|
612
|
+
and removing the branch.
|
|
613
|
+
- Consider merging `getCurrentPendingBatchId` into `flushPendingBatch` so sealing the batch returns its resulting
|
|
614
|
+
`batchId`. This would keep the ordered flush-then-read operation inside one runtime hook instead of requiring the
|
|
615
|
+
resolver to call two hooks in sequence.
|
|
616
|
+
- Reevaluate whether distinguishing `pending` from `unresolvable` is valuable enough to justify the driver-dependent
|
|
617
|
+
heuristics in `classifyMiss`. The current implementation makes educated guesses from empty reads and sequence gaps, so
|
|
618
|
+
its confidence depends on how each driver's delta storage reports trimmed ranges. Consider returning the conservative
|
|
619
|
+
`pending` result for ambiguous misses, collapsing the states, or deferring a definitive `unresolvable` result until
|
|
620
|
+
delta storage exposes an explicit retention boundary. This is separate from a missing historical reader, described
|
|
621
|
+
below.
|
|
622
|
+
- In a future beta-breaking release, make `timestamp` required on the resolved variants of `VersionMarkCapture` and
|
|
623
|
+
`ResolveResult`, and make the third `onBatchSequenced` listener argument required. It is optional today only so
|
|
624
|
+
existing callers, callbacks, mocks, and stored resolved records remain source-compatible. Before taking the break,
|
|
625
|
+
verify all supported resolution paths always produce a server timestamp, migrate known consumers, and follow the
|
|
626
|
+
beta-breaking release process.
|
|
627
|
+
- **`batchEnd` vs `onBatchSequenced` (deferred).** The version-mark APIs are already `@legacy @beta`, so this is no
|
|
628
|
+
longer a pre-beta prerequisite. `onBatchSequenced` broadcasts `(batchId, sequenceNumber)` for live mark promotion; the
|
|
629
|
+
open question is whether to instead extend the existing `batchEnd` event (on `IContainerRuntimeBaseEvents`) to expose
|
|
630
|
+
the effective stable batch ID and reuse it, retiring `onBatchSequenced`.
|
|
631
|
+
|
|
632
|
+
**Case for consolidating (Mark's original point):** the new event is largely a subset of `batchEnd` plus one
|
|
633
|
+
generally-applicable field (the stable batch id). If API changes were free we would not add a second parallel
|
|
634
|
+
notification API; every extra public API is long-term surface we are then responsible for maintaining, documenting,
|
|
635
|
+
and evolving. Avoiding that cruft is the benefit.
|
|
636
|
+
|
|
637
|
+
**Case for keeping `onBatchSequenced` separate:** it is **not** a strict subset of `batchEnd` — its firing contract is
|
|
638
|
+
correctness-load-bearing and differs in three ways: (1) it fires **only after pending-state validation**, so a
|
|
639
|
+
rejected/forked batch never triggers promotion, whereas `batchEnd` fires even on error; (2) it is **gated on
|
|
640
|
+
`isTracking`**, firing only for containers actually using version marks, whereas `batchEnd` fires on all clients; (3)
|
|
641
|
+
the stable-batch-id derivation is itself tracking-gated. A consumer migrated onto `batchEnd` would have to re-derive
|
|
642
|
+
that validated-only + tracking-gated filtering itself, and getting it wrong would promote a mark on a batch that was
|
|
643
|
+
never durably sequenced. There is also cost on the emit side (deriving/exposing the id for every `batchEnd`, including
|
|
644
|
+
on untracked clients).
|
|
645
|
+
|
|
646
|
+
**If we do decide to consolidate, the phased (non-breaking-until-last-step) path is:**
|
|
647
|
+
1. **Add the stable batch id to `batchEnd`** — additive event-payload change, non-breaking, lands in any minor
|
|
648
|
+
(optional/nullable field, since `batchEnd` fires when no stable id exists).
|
|
649
|
+
1. **Deprecate `onBatchSequenced`** (`@deprecated` TSDoc pointing at `batchEnd`, with removal version + tracking
|
|
650
|
+
issue) — non-breaking, signals the migration and starts the partner lead-time clock.
|
|
651
|
+
1. **Remove `onBatchSequenced`** — the only beta-breaking step; requires **both** an x.x0 (or major) break window
|
|
652
|
+
_and_ the ~12-week partner lead time (office-bohemia consumes this API), staged on a `test/breaks/client/#.#0/`
|
|
653
|
+
branch.
|
|
654
|
+
|
|
655
|
+
**Gate on step 3:** removal is only safe if a `batchEnd` consumer can faithfully reproduce `onBatchSequenced`'s
|
|
656
|
+
validated-only + tracking-gated semantics. If it cannot, keep `onBatchSequenced` and do not finalize the
|
|
657
|
+
consolidation. **Decision: deferred** — none of this is required for the current beta, it does not block
|
|
658
|
+
office-bohemia testing (the feature works today via `sealAndCaptureVersionMark`/`resolve` plus `onBatchSequenced`, and
|
|
659
|
+
a missed live promotion is recoverable via the history scan), and the only breaking step is gated to a future x.x0
|
|
660
|
+
regardless.
|
|
661
|
+
|
|
662
|
+
- Define the teardown contract for listeners and in-flight `resolve()` calls. Subscriptions should not retain app
|
|
663
|
+
objects after runtime disposal, and a resolver obtained from a closed runtime should fail predictably rather than
|
|
664
|
+
starting new storage work.
|
|
665
|
+
|
|
666
|
+
### Missing `fetchOps` and resolve-result semantics
|
|
667
|
+
|
|
668
|
+
When a newer runtime is paired with an older loader, `fetchOps` is absent. The runtime already handles that pairing
|
|
669
|
+
safely: it does not construct the historical reader/unpacker, and a live-map miss returns `{ kind: "pending" }` with
|
|
670
|
+
telemetry path `noReader`. The process does not crash, and a batch observed later in the same live session can still
|
|
671
|
+
resolve through `processInboundBatch`.
|
|
672
|
+
|
|
673
|
+
The open production-design question is semantic clarity. `pending` currently covers both:
|
|
674
|
+
|
|
675
|
+
1. the batch has not sequenced yet and a normal retry may resolve it; and
|
|
676
|
+
1. this loader cannot inspect retained history, so the runtime cannot determine whether the batch sequenced in another
|
|
677
|
+
session.
|
|
678
|
+
|
|
679
|
+
This union is consumed by the host/service layer, not displayed directly to an end user. It can still affect end-user
|
|
680
|
+
behavior indirectly if the host leaves a version unresolved or retries forever. `unresolvable` is not an exact
|
|
681
|
+
substitute: its current contract means retained history was available but the target ops were trimmed or otherwise
|
|
682
|
+
proven gone. A later container load with a newer loader could resolve a mark that the old-loader pairing could not
|
|
683
|
+
inspect.
|
|
684
|
+
|
|
685
|
+
Before production, choose and document one policy:
|
|
686
|
+
|
|
687
|
+
- **Non-breaking clarification (preferred if sufficient):** add an optional reason to the existing `pending` member,
|
|
688
|
+
such as `reason?: "awaitingSequence" | "historicalOpsUnavailable"`. Existing consumers and older runtime results
|
|
689
|
+
remain valid, while an updated host can choose a different retry policy.
|
|
690
|
+
- **Clean distinct outcome:** add an `unavailable`/`indeterminate` result, or make a reason required. This is a breaking
|
|
691
|
+
change to the exported `@legacy @beta` union because exhaustive consumers must handle the new shape. It requires a
|
|
692
|
+
changeset, regenerated API reports, API Council approval, an allowed beta-break window, and office-bohemia partner
|
|
693
|
+
lead time/integration testing.
|
|
694
|
+
- **Reuse `unresolvable` (not recommended without redefining it):** this avoids a new union member, but would blur a
|
|
695
|
+
terminal retained-history result with a capability-limited result that may become resolvable after loading with a
|
|
696
|
+
newer loader. If selected, its contract and host retry behavior must be deliberately changed and documented.
|
|
697
|
+
- **Keep current behavior:** explicitly define no-reader `pending` as a conservative compatibility result and require
|
|
698
|
+
the host to bound retries or rely on live promotion. This avoids an API change but preserves the ambiguity.
|
|
699
|
+
|
|
700
|
+
Whichever policy is selected, test both a current loader and an old-loader-shaped context with no `fetchOps`, and
|
|
701
|
+
document whether a host should retain and retry the mark after a later deployment/reload. Do not remove the optional
|
|
702
|
+
branch as part of this decision; that cleanup remains independently gated by AB#81034 and generation 21.
|
|
292
703
|
|
|
293
704
|
### Flush side effect and corner cases
|
|
294
705
|
|
|
295
|
-
The flush side effect in `sealAndCaptureVersionMark()` is acceptable, but it creates corner cases that should be handled
|
|
706
|
+
The flush side effect in `sealAndCaptureVersionMark()` is acceptable, but it creates corner cases that should be handled
|
|
707
|
+
explicitly and covered in tests and consumer documentation.
|
|
296
708
|
|
|
297
|
-
Consider adding a `notCaptured` result to `VersionMarkCapture` for expected caller-state conditions where capture cannot
|
|
709
|
+
Consider adding a `notCaptured` result to `VersionMarkCapture` for expected caller-state conditions where capture cannot
|
|
710
|
+
safely begin:
|
|
298
711
|
|
|
299
712
|
```ts
|
|
300
713
|
type VersionMarkCapture =
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
714
|
+
| { kind: "pending"; batchId: string; sequenceNumberLowerBound: number }
|
|
715
|
+
| { kind: "resolved"; sequenceNumber: number }
|
|
716
|
+
| {
|
|
717
|
+
kind: "notCaptured";
|
|
718
|
+
reason: "unsafeToFlush" | "stagingNotSupported";
|
|
719
|
+
};
|
|
307
720
|
```
|
|
308
721
|
|
|
309
|
-
This is preferable to throwing when the runtime can detect the condition before changing batch state. In particular,
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
722
|
+
This is preferable to throwing when the runtime can detect the condition before changing batch state. In particular,
|
|
723
|
+
capture should preflight reentrant/inbound processing, manual batch accumulation inside `orderSequentially`, and staging
|
|
724
|
+
mode before calling `flush()`. Today `ContainerRuntime.flush()` treats failures as critical: for example, flushing
|
|
725
|
+
inside `orderSequentially` asserts, closes the container, and rethrows. A mark request made at an inconvenient but
|
|
726
|
+
recoverable time should instead return `notCaptured`, perform no flush, create no locator, and leave the container
|
|
727
|
+
usable.
|
|
728
|
+
|
|
729
|
+
`notCaptured` should not become a catch-all for failures after flushing starts. Unexpected submission failures, an
|
|
730
|
+
oversized or invalid batch, a closed/disposed runtime, and other operational or data-processing errors should continue
|
|
731
|
+
to throw and follow their existing container lifecycle. Returning a normal result after partial mutation would hide an
|
|
732
|
+
indeterminate capture. Keep the reason union small and actionable; callers can retry `unsafeToFlush` after leaving the
|
|
733
|
+
current callback, while `stagingNotSupported` means they must wait for commit/discard or use a future staging-aware
|
|
734
|
+
capture contract.
|
|
735
|
+
|
|
736
|
+
Adding this variant is an API change. Before promotion, decide whether `sealAndCaptureVersionMark()` should always
|
|
737
|
+
return the three-way union or whether unsafe contexts should remain programmer errors. If `notCaptured` is adopted,
|
|
738
|
+
document that it guarantees no mark was produced and no capture-triggered flush occurred.
|
|
739
|
+
|
|
740
|
+
Staging mode also needs an explicit contract for both commit and discard. Capturing staged edits should not send them
|
|
741
|
+
immediately. Committing should preserve the captured batch identity through submission/resubmission so the mark resolves
|
|
742
|
+
normally. After discard, the captured batch will never sequence, so the resulting mark behavior must be defined and
|
|
743
|
+
documented.
|
|
316
744
|
|
|
317
745
|
Suggested test coverage:
|
|
318
746
|
|
|
319
|
-
1. **Real batch cut:** Submit an op through `ContainerRuntime`, call capture before the TurnBased flush, and verify that
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
747
|
+
1. **Real batch cut:** Submit an op through `ContainerRuntime`, call capture before the TurnBased flush, and verify that
|
|
748
|
+
capture flushes the op into `PendingStateManager` and returns that exact batch's ID rather than a mocked ID.
|
|
749
|
+
1. **Unsafe contexts:** Call capture during inbound processing and inside `orderSequentially`; verify `notCaptured` with
|
|
750
|
+
`reason: "unsafeToFlush"`, no flush, and no container closure. Also verify a later retry succeeds.
|
|
751
|
+
1. **Staging commit and discard:** Capture staged edits and verify nothing is sent immediately. Verify that commit
|
|
752
|
+
preserves the captured ID through resubmit and resolution, and define and test the result after discard.
|
|
753
|
+
1. **Offline rehydration:** Capture while disconnected, stash and rehydrate, resubmit from the new client, and resolve
|
|
754
|
+
using the original ID. This also covers end-to-end preservation and stamping of the batch identity through
|
|
755
|
+
pending-state rehydration, beyond the existing unit tests for explicit original `batchId` metadata.
|
|
323
756
|
|
|
324
757
|
## Historical-op retention limitation
|
|
325
758
|
|
|
326
|
-
Cross-client or headless resolution of an old pending mark falls back to the historical-op scan. This covers the case
|
|
759
|
+
Cross-client or headless resolution of an old pending mark falls back to the historical-op scan. This covers the case
|
|
760
|
+
where the capturing client dies before its own ack and no other live client promoted the mark: a fresh client can
|
|
761
|
+
resolve the stored `batchId` from retained ops even though that op will not reappear on the live inbound stream.
|
|
327
762
|
|
|
328
|
-
Resolution is still bounded by op retention. Once the target range has been trimmed, there is no runtime-owned durable
|
|
763
|
+
Resolution is still bounded by op retention. Once the target range has been trimmed, there is no runtime-owned durable
|
|
764
|
+
`batchId -> { sequenceNumber, timestamp }` index to recover it, so the resolver returns `unresolvable`. As described
|
|
765
|
+
above, the current trim detection is read-derived and driver-dependent; a future explicit op-availability API would make
|
|
766
|
+
that classification contractual.
|