Skip to content

host: Add prefixed clientRequestIds to code mode editor requests #2399

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

Merged
merged 35 commits into from
Apr 22, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
fff8dd3
Add assertions for code mode clientRequestIds
backspace Apr 8, 2025
5e44827
Add logging for store reloads
backspace Apr 8, 2025
42f928d
Add clientRequestId in saveSource
backspace Apr 8, 2025
477a50c
Add logger service
backspace Apr 8, 2025
e3a77f2
Add another logger function
backspace Apr 8, 2025
65b9bf8
Add logging re code mode refresh
backspace Apr 10, 2025
0a08904
Add more logging
backspace Apr 10, 2025
7c4681b
Add application global in preview deployments
backspace Apr 10, 2025
f292f36
Remove request id on source updates
backspace Apr 11, 2025
55ea715
Add more time for server debounce
backspace Apr 11, 2025
12410b7
Add logging
backspace Apr 11, 2025
14aeb8f
Change logging
backspace Apr 11, 2025
59c5f41
Change server echo debounce
backspace Apr 11, 2025
8b9a723
Add prefix experiment
backspace Apr 11, 2025
dbf31c8
Merge branch 'main' into host/code-mode-clientRequestId-cs-8339
backspace Apr 11, 2025
1cf2433
Fix to reload on blank request id
backspace Apr 14, 2025
97aeaca
Add missing reload call
backspace Apr 14, 2025
2e1774c
Merge branch 'main' into host/code-mode-clientRequestId-cs-8339
backspace Apr 14, 2025
ccef4e6
Merge remote-tracking branch 'origin/cs-8313-card-resource-still-does…
backspace Apr 15, 2025
1b49962
Fix merge error
backspace Apr 15, 2025
430331f
Remove Monaco modifier complexity
backspace Apr 15, 2025
e341183
Revert modifier changes
backspace Apr 16, 2025
cb36be3
Merge branch 'main' into host/code-mode-clientRequestId-cs-8339
backspace Apr 16, 2025
9ff0393
Add test to exercise card-to-source
backspace Apr 16, 2025
f371a83
Fix nesting and logging of file reloads
backspace Apr 17, 2025
2cfd179
Remove currently-broken test
backspace Apr 17, 2025
8de5667
Remove exportApplicationGlobal
backspace Apr 17, 2025
6b0bc14
Remove superficial tests
backspace Apr 17, 2025
ab772c0
Change to use realm events logger
backspace Apr 17, 2025
08a83da
Add another clientRequestId prefix
backspace Apr 17, 2025
0f23091
Add another bot-patch
backspace Apr 17, 2025
d71d770
Change saveSource type to be required
backspace Apr 17, 2025
19f1c3d
Remove outdated note
backspace Apr 17, 2025
af4b6e1
Change logging and what causes store reload
backspace Apr 17, 2025
fb38911
Merge branch 'main' into host/code-mode-clientRequestId-cs-8339
backspace Apr 22, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,11 @@ export default class FormattedMessage extends Component<FormattedMessageSignatur
});
patchedCode = patchedCodeResult;
}
await this.cardService.saveSource(new URL(fileUrl), patchedCode);
await this.cardService.saveSource(
new URL(fileUrl),
patchedCode,
'bot-patch',
);
codePatchActionsGroupedByFileUrl[fileUrl].forEach((codePatchAction) => {
codePatchAction.patchCodeTaskState = 'applied';
});
Expand Down
2 changes: 1 addition & 1 deletion packages/host/app/components/operator-mode/container.gts
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ export default class OperatorModeContainer extends Component<Signature> {

private saveSource = task(async (url: URL, content: string) => {
await this.withTestWaiters(async () => {
await this.cardService.saveSource(url, content);
await this.cardService.saveSource(url, content, 'editor');
});
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -674,7 +674,11 @@ export class ${className} extends ${exportName} {
src.push(`}`);

try {
await this.cardService.saveSource(url, src.join('\n').trim());
await this.cardService.saveSource(
url,
src.join('\n').trim(),
'create-file',
);
this.currentRequest.newFileDeferred.fulfill(url);
} catch (e: any) {
let fieldOrCard = isField ? 'field' : 'card';
Expand Down
6 changes: 5 additions & 1 deletion packages/host/app/lib/formatted-message/code-patch-action.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,11 @@ export class CodePatchAction {
codeBlock: this.searchReplaceBlock,
});

await this.cardService.saveSource(new URL(this.fileUrl), patchedCode);
await this.cardService.saveSource(
new URL(this.fileUrl),
patchedCode,
'bot-patch',
);

this.patchCodeTaskState = 'applied';
} catch (error) {
Expand Down
41 changes: 40 additions & 1 deletion packages/host/app/resources/file.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ import type MessageService from '../services/message-service';
import type NetworkService from '../services/network';

const log = logger('resource:file');
const realmEventsLogger = logger('realm:events');

const utf8 = new TextDecoder();
const encoder = new TextEncoder();

Expand Down Expand Up @@ -214,8 +216,44 @@ class _FileResource extends Resource<Args> {
let normalizedURL = this.url.endsWith('.json')
? this.url.replace(/\.json$/, '')
: this.url;

if (invalidations.includes(normalizedURL)) {
this.read.perform();
realmEventsLogger.trace(
`file resource ${normalizedURL} processing invalidation`,
event,
);

let clientRequestId = event.clientRequestId;
let reloadFile = false;

if (!clientRequestId) {
reloadFile = true;
realmEventsLogger.debug(
`reloading file resource ${normalizedURL} because realm event has no clientRequestId`,
);
} else if (clientRequestId.startsWith('editor:')) {
if (this.cardService.clientRequestIds.has(clientRequestId)) {
realmEventsLogger.debug(
`ignoring because request id is contained in known clientRequestIds`,
event.clientRequestId,
);
} else {
reloadFile = true;
realmEventsLogger.debug(
`reloading file resource ${normalizedURL} because request id is ${clientRequestId}, not contained within known clientRequestIds`,
Object.keys(this.cardService.clientRequestIds),
);
}
} else if (clientRequestId.startsWith('bot-patch:')) {
reloadFile = true;
realmEventsLogger.debug(
`reloading file resource ${normalizedURL} because request id is ${clientRequestId}`,
);
}

if (reloadFile) {
this.read.perform();
}
}
});
});
Expand All @@ -225,6 +263,7 @@ class _FileResource extends Resource<Args> {
let response = await this.cardService.saveSource(
new URL(this._url),
content,
'editor',
);
if (this.innerState.state === 'not-found') {
// TODO think about the "unauthorized" scenario
Expand Down
10 changes: 7 additions & 3 deletions packages/host/app/services/card-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ export default class CardService extends Service {
'QUERY');

if (!isReadOperation) {
let clientRequestId = uuidv4();
let clientRequestId = `instance:${uuidv4()}`;
this.clientRequestIds.add(clientRequestId);
headers = { ...headers, 'X-Boxel-Client-Request-Id': clientRequestId };
}
Expand Down Expand Up @@ -156,11 +156,15 @@ export default class CardService extends Service {
return response.text();
}

async saveSource(url: URL, content: string) {
async saveSource(url: URL, content: string, type: string) {
let clientRequestId = `${type}:${uuidv4()}`;
this.clientRequestIds.add(clientRequestId);

let response = await this.network.authedFetch(url, {
method: 'POST',
headers: {
Accept: 'application/vnd.card+source',
'X-Boxel-Client-Request-Id': clientRequestId,
},
body: content,
});
Expand All @@ -185,7 +189,7 @@ export default class CardService extends Service {
});

const content = await response.text();
await this.saveSource(toUrl, content);
await this.saveSource(toUrl, content, 'copy');
return response;
}

Expand Down
52 changes: 38 additions & 14 deletions packages/host/app/services/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,11 @@ import {
isCardCollectionDocument,
Deferred,
delay,
isCardError,
mergeRelationships,
realmURL as realmURLSymbol,
localId as localIdSymbol,
isCardError,
logger,
type Store as StoreInterface,
type Query,
type PatchData,
Expand Down Expand Up @@ -68,6 +69,8 @@ export { CardErrorJSONAPI, CardSaveSubscriber };

let waiter = buildWaiter('store-service');

const realmEventsLogger = logger('realm:events');

export default class StoreService extends Service implements StoreInterface {
@service declare private realm: RealmService;
@service declare private loaderService: LoaderService;
Expand Down Expand Up @@ -478,24 +481,45 @@ export default class StoreService extends Service implements StoreInterface {
}
let instance = this.identityContext.get(invalidation);
if (instance) {
// Do not reload if the event is a result of a request that we made. Otherwise we risk overwriting
// the inputs with past values. This can happen if the user makes edits in the time between the auto
// save request and the arrival realm event.
if (
!event.clientRequestId ||
!this.cardService.clientRequestIds.has(event.clientRequestId)
) {
this.reloadTask.perform(instance);
} else {
if (this.cardService.clientRequestIds.has(event.clientRequestId)) {
console.debug(
'ignoring invalidation for card because clientRequestId is ours',
event,
// Do not reload if the event is a result of an instance-editing request that we made. Otherwise we risk
// overwriting the inputs with past values. This can happen if the user makes edits in the time between
// the auto save request and the arrival realm event.

let clientRequestId = event.clientRequestId;
let reloadFile = false;

if (!clientRequestId) {
reloadFile = true;
realmEventsLogger.debug(
`reloading file resource ${invalidation} because event has no clientRequestId`,
);
} else if (this.cardService.clientRequestIds.has(clientRequestId)) {
if (clientRequestId.startsWith('instance:')) {
realmEventsLogger.debug(
`ignoring invalidation for card ${invalidation} because request id ${clientRequestId} is ours and an instance type`,
);
} else {
reloadFile = true;
realmEventsLogger.debug(
`reloading file resource ${invalidation} because request id ${clientRequestId} is not instance type`,
);
}
} else {
reloadFile = true;
realmEventsLogger.debug(
`reloading file resource ${invalidation} because request id ${clientRequestId} is not contained within known clientRequestIds`,
Array.from(this.cardService.clientRequestIds.values()),
);
}

if (reloadFile) {
this.reloadTask.perform(instance);
}
} else if (!this.identityContext.get(invalidation)) {
// load the card using just the ID because we don't have a running card on hand
realmEventsLogger.debug(
`reloading file resource ${invalidation} because it is not found in the identity context`,
);
this.loadInstanceTask.perform(invalidation);
}
}
Expand Down
31 changes: 31 additions & 0 deletions packages/matrix/tests/live-cards.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -196,4 +196,35 @@ test.describe('Live Cards', () => {

await expect(page.locator('[data-test-file="hello.gts"]')).toHaveCount(1);
});

test.skip('updating a card in code mode edit updates its source', async ({
page,
}) => {
await clearLocalStorage(page, serverIndexUrl);
await login(page, 'user1', 'pass', {
url: serverIndexUrl,
skipOpeningAssistant: true,
});
await createRealm(page, realmName);

await page.goto(
`${realmURL}?operatorModeState=${encodeURIComponent(
JSON.stringify({
stacks: [],
codePath: `${realmURL}HelloWorld/47c0fc54-5099-4e9c-ad0d-8a58572d05c0`,
fileView: 'browser',
submode: 'code',
}),
)}`,
);

await page.locator('[data-test-format-chooser="edit"]').click();
await page
.locator('[data-test-field="fullName"] input')
.fill('Replacement');

await expect(
page.locator('[data-test-monaco-container-operator-mode]'),
).toContainText('Replacement');
});
});