Skip to content

Commit 1b4c1af

Browse files
authored
feat(vnext): correlate completion refresh intent (#198)
## Summary - add an opaque completion refresh token that correlates loading results, follow-up catalog events, and the originating completion task - make completion tasks expose their refresh token synchronously so CodeMirror consumers can safely latch intent before caller-controlled reentrancy - define precise token lifetimes for soft and terminal catalog-loading leases, including expiry, cancellation, update, and disposal - harden `AbortSignal` handling against reentrant and throwing getters/listener methods without allowing an older invocation to erase newer work ## Why The upcoming CodeMirror adapter must refresh completion only for the request that originally produced a loading result. Revision checks alone cannot distinguish multiple requests at the same document revision, and asynchronous token publication leaves a race where catalog events can arrive before the consumer knows what to match. This contract makes that identity explicit and keeps latest-request-wins behavior correct across hostile caller boundaries. ## Validation - 2,003 tests pass; 1 intentional expected failure - changed-file coverage: 97.92% statements, 96.01% branches, 100% functions, 98.01% lines - all strict and loose-optional TypeScript configurations pass - oxlint and test-integrity checks pass - session completion benchmarks remain within budget - two independent exact-commit adversarial reviews approved `a0522c4` <!-- This is an auto-generated description by cubic. --> --- ## Summary by cubic Adds an opaque completion refresh token to correlate loading results, follow-up catalog events, and the originating request. `session.complete()` now returns a `SqlCompletionTask` that exposes the token synchronously, and `onDidChange` events include the token when relevant. - **New Features** - Introduced `SqlCompletionRefreshToken` and `SqlCompletionTask` (`complete()` returns a task with `refreshToken`). - `SqlSessionChangeEvent` now carries `refreshToken`: - `catalog-availability`: always includes the exact token for the active soft intent. - `catalog`: includes the matching token during an active intent lease, otherwise `null`. - `provider-configuration`: `refreshToken` is always `null`. - Ready results include `refreshToken` when a `catalog-loading` lease is active; otherwise `null`. - Hardened `AbortSignal` and timer handling to avoid reentrancy issues and prevent stale callbacks from erasing newer work. - **Migration** - Update callers to use the new task: - `const task = session.complete(...); const result = await task;` - Use `task.refreshToken` immediately to latch intent. - Update `onDidChange` listeners: - Compare `event.refreshToken` to the latched `task.refreshToken` before refreshing. - Treat `catalog` events with `refreshToken: null` as unrelated to the current intent. - Adjust types: - `complete()` now returns `SqlCompletionTask` (still awaitable). - Handle the refined `SqlSessionChangeEvent` union with per-reason `refreshToken` semantics. - Do not serialize or introspect tokens; compare by identity only. <sup>Written for commit a0522c4. Summary will update on new commits.</sup> <a href="https://cubic.dev/pr/marimo-team/codemirror-sql/pull/198?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. -->
1 parent 0f350ef commit 1b4c1af

7 files changed

Lines changed: 1340 additions & 106 deletions

File tree

docs/vnext/session-primitives.md

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -88,18 +88,25 @@ const session = service.openDocument({
8888
text: "SELECT * FROM ",
8989
});
9090

91-
const subscription = session.onDidChange(({ reason }) => {
92-
if (reason === "catalog" || reason === "catalog-availability") {
91+
let completionRefreshToken: SqlCompletionRefreshToken | null = null;
92+
const subscription = session.onDidChange(({ refreshToken }) => {
93+
if (
94+
refreshToken !== null &&
95+
refreshToken === completionRefreshToken
96+
) {
9397
// Ask the editor adapter to request completion again.
9498
}
9599
});
96100

97-
const result = await session.complete({
101+
const completionTask = session.complete({
98102
position: 14,
99103
trigger: { kind: "invoked" },
100104
});
105+
completionRefreshToken = completionTask.refreshToken;
106+
const result = await completionTask;
101107

102108
if (result.status === "ready" && session.isCurrent(result.revision)) {
109+
completionRefreshToken = result.refreshToken;
103110
for (const item of result.value.items) {
104111
// Apply item.edit in the original document's UTF-16 coordinates.
105112
}
@@ -118,10 +125,21 @@ exposing provider errors or internal epochs.
118125

119126
The interactive catalog wait is bounded from the start of `complete()`. When
120127
the budget expires, the session returns local evidence with a
121-
`catalog-loading` issue and a checked remaining intent lease. Compatible
122-
readiness advances the session revision and emits `catalog-availability`;
123-
higher provider epochs emit `catalog`. Consumers must request completion again
124-
and apply only results whose revision remains current.
128+
`catalog-loading` issue, a checked remaining intent lease, and an opaque
129+
`refreshToken`. Compatible readiness advances the session revision and emits
130+
`catalog-availability` with that exact token. A higher provider epoch emits
131+
`catalog` with the token only while the same soft or terminal loading intent
132+
remains leased; otherwise its token is `null`.
133+
134+
Consumers compare refresh tokens by identity. A matching token is necessary,
135+
not sufficient, to request completion again: the document, selection, context,
136+
and adapter-owned intent must also remain unchanged. Tokens are in-process,
137+
non-serializable control identities; they expose no provider epoch, query, or
138+
work identity. The completion task exposes its token synchronously before
139+
provider work starts, so consumers can latch a matching catalog event that
140+
races the result. Ready results retain that token only while
141+
`catalog-loading` has an unexpired lease; all other ready results use `null`.
142+
Consumers apply only results whose revision remains current.
125143

126144
## Dialect registration
127145

0 commit comments

Comments
 (0)