Repository navigation
feat(webview): view-local buffer reads and setValue persistence (N3, split 3/5 of #1546) #2235
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
e67ab35
f28c486
7906df6
f560872
6340998
9849018
1357d5b
9d88599
c59f82f
afa9ab3
b620419
86c0288
b296635
576f452
14a8d40
b4ca1bc
b82c904
ebe5a6a
0f341c6
f79870c
d52cd47
7e49dae
57acc73
f652d7e
2a91e73
7a48eed
3f2a32c
00c0f8d
5a6693b
589531a
2b8270a
18b687c
61189bb
38d9fee
9a6521d
f577f15
cfce4b9
75ebe31
1da2756
ea9015e
563c607
c1529c0
a93fbe4
88dcfa1
9de36be
d46d7b2
aefbdbb
96d2957
3f57651
acf8ccb
f3ca33b
f2d4336
f061de5
515cc02
1d4f28e
3f1983f
0a77cf8
ddda5ce
53d6fe1
48e9bcb
f95d164
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -67,17 +67,22 @@ const globalSettingsExportSchema = globalSettingsSchema.omit({ | |||||||||||||||||||||||||||||||||||||||||||||||
| taskHistory: true, | ||||||||||||||||||||||||||||||||||||||||||||||||
| listApiConfigMeta: true, | ||||||||||||||||||||||||||||||||||||||||||||||||
| currentApiConfigName: true, | ||||||||||||||||||||||||||||||||||||||||||||||||
| // Per-view selection state is machine-local: it keeps flowing through the | ||||||||||||||||||||||||||||||||||||||||||||||||
| // normal runtime and pruning paths but must not transfer between settings. | ||||||||||||||||||||||||||||||||||||||||||||||||
| viewStates: true, | ||||||||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| export class ContextProxy { | ||||||||||||||||||||||||||||||||||||||||||||||||
| private readonly originalContext: vscode.ExtensionContext | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| private stateCache: GlobalState | ||||||||||||||||||||||||||||||||||||||||||||||||
| private stateWriteGenerations = new Map<GlobalStateKey, number>() | ||||||||||||||||||||||||||||||||||||||||||||||||
| private secretCache: SecretState | ||||||||||||||||||||||||||||||||||||||||||||||||
| private _isInitialized = false | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| constructor(context: vscode.ExtensionContext) { | ||||||||||||||||||||||||||||||||||||||||||||||||
| this.originalContext = context | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| this.stateCache = {} | ||||||||||||||||||||||||||||||||||||||||||||||||
| this.secretCache = {} | ||||||||||||||||||||||||||||||||||||||||||||||||
| this._isInitialized = false | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -397,6 +402,15 @@ export class ContextProxy { | |||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||
| private readonly stateWriteOwner = new Map<string, object>() | ||||||||||||||||||||||||||||||||||||||||||||||||
| private readonly secretWriteOwner = new Map<string, object>() | ||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||
| * The secret writes that are still in flight, each paired with the claim it took, | ||||||||||||||||||||||||||||||||||||||||||||||||
| * so a reset can wait for them to settle before it deletes the keys. Without the | ||||||||||||||||||||||||||||||||||||||||||||||||
| * pairing a reset cannot tell a settled-but-orphaned write from a live one. | ||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||
| private readonly pendingSecretWrites = new Map<string, Set<{ settle: Promise<void> }>>() | ||||||||||||||||||||||||||||||||||||||||||||||||
| // A reset is in progress: the caches are meaningless mid-reset, and a secret write begun | ||||||||||||||||||||||||||||||||||||||||||||||||
| // inside it would commit after the delete pass and survive the erase. | ||||||||||||||||||||||||||||||||||||||||||||||||
| private resetting = false | ||||||||||||||||||||||||||||||||||||||||||||||||
| /** How many writes of each secret were started. Unlike the owners, it is not cleared when a write is over. */ | ||||||||||||||||||||||||||||||||||||||||||||||||
| private readonly secretWriteCount = new Map<string, number>() | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -413,6 +427,8 @@ export class ContextProxy { | |||||||||||||||||||||||||||||||||||||||||||||||
| // writes that are in flight. An update that throws at once is turned into a rejection, so it is handled the same. | ||||||||||||||||||||||||||||||||||||||||||||||||
| const owner = {} | ||||||||||||||||||||||||||||||||||||||||||||||||
| this.stateWriteOwner.set(key, owner) | ||||||||||||||||||||||||||||||||||||||||||||||||
| const generation = (this.stateWriteGenerations.get(key) ?? 0) + 1 | ||||||||||||||||||||||||||||||||||||||||||||||||
| this.stateWriteGenerations.set(key, generation) | ||||||||||||||||||||||||||||||||||||||||||||||||
| this.stateCache[key] = value | ||||||||||||||||||||||||||||||||||||||||||||||||
| const release = () => { | ||||||||||||||||||||||||||||||||||||||||||||||||
| if (this.stateWriteOwner.get(key) === owner) this.stateWriteOwner.delete(key) | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -429,6 +445,11 @@ export class ContextProxy { | |||||||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| /** Current per-key write generation, used by callers whose rollback must not clobber a newer write. */ | ||||||||||||||||||||||||||||||||||||||||||||||||
| public getStateWriteGeneration<K extends GlobalStateKey>(key: K): number { | ||||||||||||||||||||||||||||||||||||||||||||||||
| return this.stateWriteGenerations.get(key) ?? 0 | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| private getAllGlobalState(): GlobalState { | ||||||||||||||||||||||||||||||||||||||||||||||||
| return Object.fromEntries(GLOBAL_STATE_KEYS.map((key) => [key, this.getGlobalState(key)])) | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -443,6 +464,13 @@ export class ContextProxy { | |||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| storeSecret(key: SecretStateKey, value?: string) { | ||||||||||||||||||||||||||||||||||||||||||||||||
| if (this.resetting) { | ||||||||||||||||||||||||||||||||||||||||||||||||
| // Refused rather than deferred: a deferred secret write would re-store exactly the | ||||||||||||||||||||||||||||||||||||||||||||||||
| // value the user asked to erase, and a silently dropped one would lose the save. | ||||||||||||||||||||||||||||||||||||||||||||||||
| return Promise.reject( | ||||||||||||||||||||||||||||||||||||||||||||||||
| new SettingsWriteError([], [key], new Error("secret writes are refused while state is being reset")), | ||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
| // Update cache. | ||||||||||||||||||||||||||||||||||||||||||||||||
| const owner = {} | ||||||||||||||||||||||||||||||||||||||||||||||||
| this.secretWriteOwner.set(key, owner) | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -454,7 +482,19 @@ export class ContextProxy { | |||||||||||||||||||||||||||||||||||||||||||||||
| // cache (it stays empty until the next write, `refreshSecrets` or a restart) rather than being served. | ||||||||||||||||||||||||||||||||||||||||||||||||
| // The entry of the key is removed when the write and its read-back are over (if it is still the latest). A store | ||||||||||||||||||||||||||||||||||||||||||||||||
| // that throws at once is turned into a rejection, so it is handled the same. | ||||||||||||||||||||||||||||||||||||||||||||||||
| const pending = this.pendingSecretWrites.get(key) ?? new Set<{ settle: Promise<void> }>() | ||||||||||||||||||||||||||||||||||||||||||||||||
| let settleWrite: () => void = () => {} | ||||||||||||||||||||||||||||||||||||||||||||||||
| const entry: { settle: Promise<void> } = { | ||||||||||||||||||||||||||||||||||||||||||||||||
| settle: new Promise<void>((resolve) => { | ||||||||||||||||||||||||||||||||||||||||||||||||
| settleWrite = resolve | ||||||||||||||||||||||||||||||||||||||||||||||||
| }), | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
| pending.add(entry) | ||||||||||||||||||||||||||||||||||||||||||||||||
| this.pendingSecretWrites.set(key, pending) | ||||||||||||||||||||||||||||||||||||||||||||||||
| const release = () => { | ||||||||||||||||||||||||||||||||||||||||||||||||
| pending.delete(entry) | ||||||||||||||||||||||||||||||||||||||||||||||||
| if (pending.size === 0) this.pendingSecretWrites.delete(key) | ||||||||||||||||||||||||||||||||||||||||||||||||
| settleWrite() | ||||||||||||||||||||||||||||||||||||||||||||||||
| if (this.secretWriteOwner.get(key) === owner) this.secretWriteOwner.delete(key) | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
| const stored = (async () => | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -716,17 +756,42 @@ export class ContextProxy { | |||||||||||||||||||||||||||||||||||||||||||||||
| * @returns A promise that resolves when all reset operations are complete | ||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||
| public async resetAllState() { | ||||||||||||||||||||||||||||||||||||||||||||||||
| // Clear in-memory caches | ||||||||||||||||||||||||||||||||||||||||||||||||
| this.stateCache = {} | ||||||||||||||||||||||||||||||||||||||||||||||||
| this.secretCache = {} | ||||||||||||||||||||||||||||||||||||||||||||||||
| this.resetting = true | ||||||||||||||||||||||||||||||||||||||||||||||||
|
easonLiangWorldedtech marked this conversation as resolved.
Resolved
|
||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||
| // A secret write that is in flight at this moment has already committed nothing, but it will | ||||||||||||||||||||||||||||||||||||||||||||||||
| // commit as soon as it settles, and the deletes below cannot un-commit a write that lands | ||||||||||||||||||||||||||||||||||||||||||||||||
| // after them. Wait for the pending writes first, then end the claims, then delete, so the | ||||||||||||||||||||||||||||||||||||||||||||||||
| // cleared copy is the last one to land in storage and the cache stays empty afterwards. | ||||||||||||||||||||||||||||||||||||||||||||||||
| const inFlight = [...this.pendingSecretWrites.values()].flatMap((writes) => | ||||||||||||||||||||||||||||||||||||||||||||||||
| [...writes].map((w) => w.settle), | ||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||
| await Promise.allSettled(inFlight) | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| // Clear in-memory caches | ||||||||||||||||||||||||||||||||||||||||||||||||
| this.stateCache = {} | ||||||||||||||||||||||||||||||||||||||||||||||||
| this.secretCache = {} | ||||||||||||||||||||||||||||||||||||||||||||||||
| // A write that started before this reset must not restore its pre-reset value when it | ||||||||||||||||||||||||||||||||||||||||||||||||
| // rejects afterwards, so advance every generation the pending writes still hold. | ||||||||||||||||||||||||||||||||||||||||||||||||
| for (const key of this.stateWriteGenerations.keys()) { | ||||||||||||||||||||||||||||||||||||||||||||||||
| this.stateWriteGenerations.set(key, (this.stateWriteGenerations.get(key) ?? 0) + 1) | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
| // A reset also ends write ownership: a write that is still in flight reads the Memento back | ||||||||||||||||||||||||||||||||||||||||||||||||
| // into the cache it just lost, so clearing the caches is not enough to keep pre-reset state | ||||||||||||||||||||||||||||||||||||||||||||||||
| // out. Ownership is what the rejection handlers gate on. | ||||||||||||||||||||||||||||||||||||||||||||||||
| this.stateWriteOwner.clear() | ||||||||||||||||||||||||||||||||||||||||||||||||
| this.secretWriteOwner.clear() | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| await Promise.all([ | ||||||||||||||||||||||||||||||||||||||||||||||||
| ...GLOBAL_STATE_KEYS.map((key) => this.originalContext.globalState.update(key, undefined)), | ||||||||||||||||||||||||||||||||||||||||||||||||
| ...SECRET_STATE_KEYS.map((key) => this.originalContext.secrets.delete(key)), | ||||||||||||||||||||||||||||||||||||||||||||||||
| ...GLOBAL_SECRET_KEYS.map((key) => this.originalContext.secrets.delete(key)), | ||||||||||||||||||||||||||||||||||||||||||||||||
| ]) | ||||||||||||||||||||||||||||||||||||||||||||||||
| await Promise.all([ | ||||||||||||||||||||||||||||||||||||||||||||||||
| ...GLOBAL_STATE_KEYS.map((key) => this.originalContext.globalState.update(key, undefined)), | ||||||||||||||||||||||||||||||||||||||||||||||||
| ...SECRET_STATE_KEYS.map((key) => this.originalContext.secrets.delete(key)), | ||||||||||||||||||||||||||||||||||||||||||||||||
| ...GLOBAL_SECRET_KEYS.map((key) => this.originalContext.secrets.delete(key)), | ||||||||||||||||||||||||||||||||||||||||||||||||
| ]) | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| await this.initialize() | ||||||||||||||||||||||||||||||||||||||||||||||||
| await this.initialize() | ||||||||||||||||||||||||||||||||||||||||||||||||
| this.resetting = false | ||||||||||||||||||||||||||||||||||||||||||||||||
| } finally { | ||||||||||||||||||||||||||||||||||||||||||||||||
| this.resetting = false | ||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+784
to
+793
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Wait for every reset deletion to settle before the
Failure sequence:
This is the ordering limit raised in the Line 759 thread. Commit Fix:
Proposed fix
Regression test: reject the Committable suggestion
|
||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| private static _instance: ContextProxy | null = null | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
Uh oh!
There was an error while loading. Please reload this page.