Skip to content

Commit 25933af

Browse files
authored
Batch asset invalidations (#11937)
- Close enso-org/cloud-v2#1627 - Batch asset invalidations for: - Bulk deletes - Bulk undo deletes (restores) - Bulk copy/move - Bulk download - This avoids flickering when the directory list is invalidated multiple times (once for the mutation corresponding to each asset) Codebase changes: - Remove all `AssetEvent`s and `AssetListEvent`s. Remaining events have been moved to TanStack Query mutations in this PR, as it is neccessary for batch invalidation functionality. - Remove `key` and `directoryKey` from `AssetTreeNode`, and `key`s in general in favor of `id`s. Not *strictly* necessary, but it was causing logic issues and is (IMO) a nice simplification to be able to do. # Important Notes None
1 parent 787372e commit 25933af

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

65 files changed

+2683
-3230
lines changed

.gitignore

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,3 +191,9 @@ test-results/
191191
test-traces/
192192
playwright-report/
193193
playwright/.cache/
194+
195+
#########
196+
## Git ##
197+
#########
198+
199+
/.mailmap

app/common/src/backendQuery.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@ export type BackendMethods = object.ExtractKeys<Backend, object.MethodOf<Backend
1111

1212
/** For each backend method, an optional function defining how to create a query key from its arguments. */
1313
type BackendQueryNormalizers = {
14-
[Method in BackendMethods]?: (...args: Parameters<Backend[Method]>) => queryCore.QueryKey
14+
[Method in BackendMethods]?: (
15+
...args: Readonly<Parameters<Backend[Method]>>
16+
) => queryCore.QueryKey
1517
}
1618

1719
const NORMALIZE_METHOD_QUERY: BackendQueryNormalizers = {
@@ -22,7 +24,7 @@ const NORMALIZE_METHOD_QUERY: BackendQueryNormalizers = {
2224
/** Creates a partial query key representing the given method and arguments. */
2325
function normalizeMethodQuery<Method extends BackendMethods>(
2426
method: Method,
25-
args: Parameters<Backend[Method]>,
27+
args: Readonly<Parameters<Backend[Method]>>,
2628
) {
2729
return NORMALIZE_METHOD_QUERY[method]?.(...args) ?? args
2830
}
@@ -31,7 +33,7 @@ function normalizeMethodQuery<Method extends BackendMethods>(
3133
export function backendQueryOptions<Method extends BackendMethods>(
3234
backend: Backend | null,
3335
method: Method,
34-
args: Parameters<Backend[Method]>,
36+
args: Readonly<Parameters<Backend[Method]>>,
3537
keyExtra?: queryCore.QueryKey | undefined,
3638
): {
3739
queryKey: queryCore.QueryKey
@@ -47,7 +49,7 @@ export function backendQueryOptions<Method extends BackendMethods>(
4749
export function backendQueryKey<Method extends BackendMethods>(
4850
backend: Backend | null,
4951
method: Method,
50-
args: Parameters<Backend[Method]>,
52+
args: Readonly<Parameters<Backend[Method]>>,
5153
keyExtra?: queryCore.QueryKey | undefined,
5254
): queryCore.QueryKey {
5355
return [backend?.type, method, ...normalizeMethodQuery(method, args), ...(keyExtra ?? [])]

app/common/src/queryClient.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ declare module '@tanstack/query-core' {
3939
* @default false
4040
*/
4141
readonly awaitInvalidates?: queryCore.QueryKey[] | boolean
42+
readonly refetchType?: queryCore.InvalidateQueryFilters['refetchType']
4243
}
4344

4445
readonly queryMeta: {
@@ -98,6 +99,7 @@ export function createQueryClient<TStorageValue = string>(
9899
mutationCache: new queryCore.MutationCache({
99100
onSuccess: (_data, _variables, _context, mutation) => {
100101
const shouldAwaitInvalidates = mutation.meta?.awaitInvalidates ?? false
102+
const refetchType = mutation.meta?.refetchType ?? 'active'
101103
const invalidates = mutation.meta?.invalidates ?? []
102104
const invalidatesToAwait = (() => {
103105
if (Array.isArray(shouldAwaitInvalidates)) {
@@ -113,6 +115,7 @@ export function createQueryClient<TStorageValue = string>(
113115
for (const queryKey of invalidatesToIgnore) {
114116
void queryClient.invalidateQueries({
115117
predicate: query => queryCore.matchQuery({ queryKey }, query),
118+
refetchType,
116119
})
117120
}
118121

@@ -121,6 +124,7 @@ export function createQueryClient<TStorageValue = string>(
121124
invalidatesToAwait.map(queryKey =>
122125
queryClient.invalidateQueries({
123126
predicate: query => queryCore.matchQuery({ queryKey }, query),
127+
refetchType,
124128
}),
125129
),
126130
)

app/common/src/services/Backend.ts

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1070,6 +1070,18 @@ export function assetIsType<Type extends AssetType>(type: Type) {
10701070
return (asset: AnyAsset): asset is Extract<AnyAsset, Asset<Type>> => asset.type === type
10711071
}
10721072

1073+
/** Extract the type of an id and return a discriminated union containing both id and type. */
1074+
export function extractTypeFromId(id: AssetId): AnyAsset extends infer T ?
1075+
T extends T ?
1076+
Pick<T, ('id' | 'type') & keyof T>
1077+
: never
1078+
: never {
1079+
return {
1080+
type: id.match(/^(.+?)-/)?.[1],
1081+
id,
1082+
} as never
1083+
}
1084+
10731085
/** Creates a new placeholder asset id for the given asset type. */
10741086
export function createPlaceholderAssetId<Type extends AssetType>(
10751087
type: Type,
@@ -1674,11 +1686,7 @@ export default abstract class Backend {
16741686
title: string,
16751687
): Promise<CreatedProject>
16761688
/** Return project details. */
1677-
abstract getProjectDetails(
1678-
projectId: ProjectId,
1679-
directoryId: DirectoryId | null,
1680-
getPresignedUrl?: boolean,
1681-
): Promise<Project>
1689+
abstract getProjectDetails(projectId: ProjectId, getPresignedUrl?: boolean): Promise<Project>
16821690
/** Return Language Server logs for a project session. */
16831691
abstract getProjectSessionLogs(
16841692
projectSessionId: ProjectSessionId,
@@ -1767,8 +1775,8 @@ export default abstract class Backend {
17671775
projectId?: string | null,
17681776
metadata?: object | null,
17691777
): Promise<void>
1770-
/** Download from an arbitrary URL that is assumed to originate from this backend. */
1771-
abstract download(url: string, name?: string): Promise<void>
1778+
/** Download an asset. */
1779+
abstract download(assetId: AssetId, title: string): Promise<void>
17721780

17731781
/**
17741782
* Get the URL for the customer portal.

app/common/src/text/english.json

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,6 @@
3939
"editDescriptionError": "Could not edit description",
4040
"canOnlyDownloadFilesError": "You currently can only download files.",
4141
"noProjectSelectedError": "First select a project to download.",
42-
"downloadInvalidTypeError": "You can only download files, projects, and Datalinks",
4342
"downloadProjectError": "Could not download project '$0'",
4443
"downloadFileError": "Could not download file '$0'",
4544
"downloadDatalinkError": "Could not download Datalink '$0'",
@@ -64,9 +63,6 @@
6463
"nameShouldNotContainInvalidCharacters": "Name should not contain invalid characters",
6564
"invalidEmailValidationError": "Please enter a valid email address",
6665

67-
"projectHasNoSourceFilesPhrase": "project has no source files",
68-
"fileNotFoundPhrase": "file not found",
69-
7066
"noNewProfilePictureError": "Could not upload a new profile picture because no image was found",
7167

7268
"registrationError": "Something went wrong! Please try again or contact the administrators.",

app/common/src/utilities/data/array.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/** @file Utilities for manipulating arrays. */
22

3-
export const EMPTY_ARRAY: readonly never[] = []
3+
export const EMPTY_ARRAY: readonly [] = []
44

55
// ====================
66
// === shallowEqual ===

app/common/src/utilities/data/object.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,3 +219,12 @@ export function useObjectId() {
219219
* can be used to splice together objects without the risk of collisions.
220220
*/
221221
export type DisjointKeysUnion<A, B> = keyof A & keyof B extends never ? A & B : never
222+
223+
/**
224+
* Merge types of values of an object union. Useful to return an object that UNSAFELY
225+
* (at runtime) conforms to the shape of a discriminated union.
226+
* Especially useful for things like Tanstack Query results.
227+
*/
228+
export type MergeValuesOfObjectUnion<T> = {
229+
[K in `${keyof T & string}`]: T[K & keyof T]
230+
}

app/gui/integration-test/dashboard/delete.spec.ts

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,6 @@ test('delete and restore', ({ page }) =>
2121
.contextMenu.restoreFromTrash()
2222
.driveTable.expectTrashPlaceholderRow()
2323
.goToCategory.cloud()
24-
.expectStartModal()
25-
.withStartModal(async (startModal) => {
26-
await expect(startModal).toBeVisible()
27-
})
28-
.close()
2924
.driveTable.withRows(async (rows) => {
3025
await expect(rows).toHaveCount(1)
3126
}))
@@ -50,8 +45,6 @@ test('delete and restore (keyboard)', ({ page }) =>
5045
.press('Mod+R')
5146
.driveTable.expectTrashPlaceholderRow()
5247
.goToCategory.cloud()
53-
.expectStartModal()
54-
.close()
5548
.driveTable.withRows(async (rows) => {
5649
await expect(rows).toHaveCount(1)
5750
}))

app/gui/integration-test/dashboard/labelsPanel.spec.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,12 +84,13 @@ test('labels', ({ page }) =>
8484
// Labels panel with one entry
8585
await locateCreateButton(locateNewLabelModal(page)).click()
8686
await expect(locateLabelsPanel(page)).toBeVisible()
87+
expect(await locateLabelsPanelLabels(page).count()).toBe(1)
8788

8889
// Empty labels panel again, after deleting the only entry
8990
await locateLabelsPanelLabels(page).first().hover()
9091

9192
const labelsPanel = locateLabelsPanel(page)
9293
await labelsPanel.getByRole('button').and(labelsPanel.getByLabel(TEXT.delete)).click()
9394
await page.getByRole('button', { name: TEXT.delete }).getByText(TEXT.delete).click()
94-
expect(await locateLabelsPanelLabels(page).count()).toBeGreaterThanOrEqual(1)
95+
expect(await locateLabelsPanelLabels(page).count()).toBe(0)
9596
}))

app/gui/src/dashboard/App.tsx

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -560,10 +560,6 @@ function AppRouter(props: AppRouterProps) {
560560
)
561561
}
562562

563-
// ====================================
564-
// === LocalBackendPathSynchronizer ===
565-
// ====================================
566-
567563
/** Keep `localBackend.rootPath` in sync with the saved root path state. */
568564
function LocalBackendPathSynchronizer() {
569565
const [localRootDirectory] = localStorageProvider.useLocalStorageState('localRootDirectory')
@@ -575,5 +571,6 @@ function LocalBackendPathSynchronizer() {
575571
localBackend.resetRootPath()
576572
}
577573
}
574+
578575
return null
579576
}

0 commit comments

Comments
 (0)