Add server timestamps to version mark resolution APIs - #28037
Add server timestamps to version mark resolution APIs#28037Mark Fields (markfields) wants to merge 15 commits into
Conversation
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
|
Hi! Thank you for opening this PR. Want me to review it? Based on the diff (446 lines, 10 files), I've queued these reviewers:
How this works
|
There was a problem hiding this comment.
Pull request overview
This PR extends the container-runtime’s legacy/beta version mark resolution surface area to propagate the server timestamp alongside the resolved sequence number, ensuring consumers can associate a resolved mark with the exact server-ticketed op time.
Changes:
- Add
timestampto resolved results forResolveResultandVersionMarkCapture, and thread it through live + historical resolution paths. - Extend
IVersionMarkResolver.onBatchSequenced()to provide(batchId, sequenceNumber, timestamp)and carry the timestamp through inbound batch processing. - Update runtime wiring, API reports, type-compat baselines, tests, and publish a legacy changeset noting the breaking API change.
Reviewed changes
Copilot reviewed 11 out of 11 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/runtime/container-runtime/src/versionMarks/versionMarkResolver.ts | Adds timestamp to resolved types, tracks {sequenceNumber, timestamp} per batch, and extends listener + runtime hooks. |
| packages/runtime/container-runtime/src/versionMarks/inboundBatch.ts | Carries the last op’s timestamp when deriving sequenced batch completion updates. |
| packages/runtime/container-runtime/src/test/versionMarks/versionMarkResolver.spec.ts | Updates unit tests to validate timestamp propagation across capture/resolve/listener paths. |
| packages/runtime/container-runtime/src/test/versionMarks/inboundBatch.spec.ts | Updates helper tests to validate timestamp extraction from inbound messages. |
| packages/runtime/container-runtime/src/test/types/validateContainerRuntimePrevious.generated.ts | Marks expected forward-compat breaks for updated type aliases. |
| packages/runtime/container-runtime/src/test/containerRuntime.spec.ts | Updates runtime-level test expectations to include resolved timestamp. |
| packages/runtime/container-runtime/src/containerRuntime.ts | Wires deltaManager.lastMessage?.timestamp into resolver hooks and forwards inbound timestamps into processInboundBatch. |
| packages/runtime/container-runtime/package.json | Updates type validation “broken” config for the changed public type aliases. |
| packages/runtime/container-runtime/api-report/container-runtime.legacy.beta.api.md | Updates generated API report to reflect new timestamp-bearing signatures/types. |
| packages/runtime/container-runtime/api-report/container-runtime.legacy.alpha.api.md | Updates generated API report to reflect new timestamp-bearing signatures/types. |
| .changeset/lazy-teams-tan.md | Adds a minor legacy changeset describing the timestamp additions and callback signature change. |
Suppressed comments (1)
packages/runtime/container-runtime/src/versionMarks/versionMarkResolver.ts:84
- The
@returnsdocs state the resolved path always includes a "last processed op timestamp", but the returnedtimestampcan beundefinedper theVersionMarkCapturetype. Document that explicitly so consumers know to handle it.
* @returns The pending batch identity and exclusive sequence number lower bound, or the current sequence number
* and last processed op timestamp when there is no pending local batch.
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Fleet Review — In progressRunning reviewers: correctness, security, api-compatibility |
…resolved sequenceNumber bug in sealAndCaptureVersionMark Clarify that the resolved capture's timestamp can be undefined not only for back-compat with previously stored captures, but also when no op has been processed yet (e.g. a freshly loaded container). Also fix a bug where the resolved branch returned sequenceNumberLowerBound (out of scope, a different method`'s parameter) instead of referenceSequenceNumber, which contradicted existing unit test expectations. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 0add8302-8442-4f31-8596-1d0d7bcebf4b
The doc comments described only the last op's sequence number, but the sequenced field/return value also carries the last op's server timestamp. Update the comments to match. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 0add8302-8442-4f31-8596-1d0d7bcebf4b
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
…' into markfields-version-mark-timestamps
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
|
Deep Review is feeling its way through the dark.
|
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
|
Deep Review: Update on the |
| * @param sequenceNumberLowerBound - The inclusive lower bound for the historical op search. | ||
| * @returns The resolved sequence number, or a result indicating that the batch remains pending or can | ||
| * no longer be resolved. | ||
| * @param sequenceNumberLowerBound - The exclusive lower bound for the historical op search. |
There was a problem hiding this comment.
Deep Review: This JSDoc now describes sequenceNumberLowerBound as exclusive — line 85 (@returns ...exclusive sequence number lower bound...) and line 96 (@param sequenceNumberLowerBound - The exclusive lower bound...) — but line 92 in the same block still says inclusive, and the implementation is inclusive. sealAndCaptureVersionMark returns sequenceNumberLowerBound: referenceSequenceNumber + 1 with the comment "Store that as an inclusive lower bound so resolve() scans directly from it," and resolveFromHistory fetches from sequenceNumberLowerBound directly (commented "(inclusive)"). DEV.md and the container-runtime test both reaffirm an inclusive lower bound, and the PR's own edited test moves resolve("targetBatch", 11) → resolve("targetBatch", 10) and expects fetchMessages to start at exactly 10 — an exclusive bound would skip the boundary op.
Revert the two "exclusive" edits (lines 85 and 96) back to "inclusive" so the public @beta/@legacy JSDoc matches the implementation, line 92, DEV.md, and the edited test.
| * unacked local batch, or a `resolved` capture (`sequenceNumber` + the last processed op's server | ||
| * `timestamp`) when there is no in-flight local work. The timestamp property is optional both for | ||
| * compatibility with previously stored captures and because no op may have been processed yet (e.g. | ||
| * on a freshly loaded container), in which case it is `undefined`. |
There was a problem hiding this comment.
Deep Review: The sealAndCaptureVersionMark JSDoc says the timestamp is undefined when "no op may have been processed yet (e.g. on a freshly loaded container)." But the PR's own test "captures the summary timestamp after load before any new ops arrive" (containerRuntime.spec.ts:2894-2938) shows it retrieves the timestamp (123456) from summary metadata on a freshly loaded container, because getCurrentReferenceTimestampMs() falls back to messageAtLastSummary?.timestamp (containerRuntime.ts:4477-4480). It is undefined only when neither a last message nor a last-summary message exists.
Separately, DEV.md (line 681, also lines 45/58) documents the hook as getCurrentTimestamp -> deltaManager.lastMessage?.timestamp, omitting the ?? messageAtLastSummary?.timestamp fallback the code actually wires.
Correct the JSDoc to reflect that a freshly loaded container does obtain a timestamp via summary metadata, and update DEV.md to state the hook is getCurrentReferenceTimestampMs() (lastMessage?.timestamp ?? messageAtLastSummary?.timestamp). While in DEV.md, add a one-sentence rationale that reusing getCurrentReferenceTimestampMs() keeps the seq/timestamp pairing aligned at load — on load messageAtLastSummary.sequenceNumber === initialSequenceNumber, so with no new ops the fallback timestamp and the reported sequence number come from the same op.
|
Deep Review: Following up on the The hard-fault reading is resolved: the Open question for a |
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
|
🔗 Found some broken links! 💔 Run a link check locally to find them. See Checking for Broken Links for more information. linkcheck output |
Bundle size comparisonBase commit: unresolved The PR's CI build failed — fix the build and the comment will update once the next run succeeds. |
Deep ReviewReviewed commit Readiness: 8/10 — ALMOST READY Not signed off, but close. The resolved op's server Path to Ready
Context for Reviewers
For human reviewer
Review history (2 prior reviews)
|
| onBatchSequenced(listener: (batchId: string, sequenceNumber: number) => void): () => void; | ||
| onBatchSequenced( | ||
| listener: (batchId: string, sequenceNumber: number, timestamp?: number) => void, | ||
| ): () => void; |
There was a problem hiding this comment.
Deep Review: The @legacy @beta listener signature types timestamp as optional — listener: (batchId: string, sequenceNumber: number, timestamp?: number) => void — but the sole runtime caller always supplies a concrete value: processInboundBatch(batchId, sequenceNumber, timestamp: number) takes a required timestamp and invokes listener(batchId, sequenceNumber, timestamp). The doc comment itself describes the listener firing with "the final op's server timestamp," implying no optionality.
Since the only producer never omits the argument, tightening the parameter to timestamp: number removes defensive dead code with no runtime change. If the optionality is intentional (source-compatibility with existing two-arg listeners), keep it and note that rationale at the declaration so the width is a documented decision rather than an apparent oversight on a release-blocking public surface.
| ): () => void; | |
| onBatchSequenced( | |
| listener: (batchId: string, sequenceNumber: number, timestamp: number) => void, | |
| ): () => void; |
| const fetchOps = (context as IContainerContextInternal).fetchOps; | ||
| this.versionMarkResolverInternal = new VersionMarkResolver({ | ||
| getCurrentSequenceNumber: () => this.deltaManager.lastSequenceNumber, | ||
| getCurrentTimestamp: () => this.getCurrentReferenceTimestampMs(), |
There was a problem hiding this comment.
Deep Review: The two capture hooks read different DeltaManager members — getCurrentSequenceNumber: () => this.deltaManager.lastSequenceNumber (line 1945) and getCurrentTimestamp: () => this.getCurrentReferenceTimestampMs() (line 1946), where getCurrentReferenceTimestampMs() returns this.deltaManager.lastMessage?.timestamp ?? this.messageAtLastSummary?.timestamp. sealAndCaptureVersionMark() pairs these two independently-read values into one { sequenceNumber, timestamp } capture.
#22508's stated motivation was that lastSequenceNumber can advance ahead of the last surfaced message, so the pairing is a caller discipline rather than a structural guarantee. Load-time consistency is verified (messageAtLastSummary?.sequenceNumber vs initialSequenceNumber) and the added test covers the load/summary fallback, but the mid-batch skew case is not covered, and the PR's own Reviewer Guidance asks to confirm each API receives the timestamp from the exact op represented by its resolved sequence number.
Confirm no synchronous window exists where deltaManager.lastSequenceNumber refers to an op whose timestamp is not lastMessage.timestamp at capture time (validate across fullBatch vs piecemeal inbound shapes). If the invariant holds, optionally harden by deriving both values from a single combined hook returning { sequenceNumber, timestamp }, making the pairing atomic by construction rather than a documented caller invariant.
Description
Version mark consumers need the server timestamp associated with a resolved mark, not only its sequence number. This change carries the final op's timestamp through each resolution path:
sealAndCaptureVersionMark()andresolve()includetimestamp.onBatchSequenced()supplies the timestamp as its third listener argument when an incoming local batch resolves a pending mark.API reports, type compatibility baselines, and a legacy API changeset are included.
Breaking Changes
None - the changes are all backwards compatible.
Reviewer Guidance
The review process is outlined in the pull request guidelines.
Please verify that each API receives the timestamp from the exact op represented by its resolved sequence number, particularly the final op of grouped and piecemeal batches.