Files
Oleg Isonen 905103c739 feat: When asset is deleted or uploaded - refresh asset resource (#5568)
## Description

When user is using asset resource and binding it on canvas, for example
video file bound to video tag to the src attribute and then deletes the
asset, it is expected that canvas reflects the deleted asset as well as
the variable.

## Steps for reproduction

1. click button
2. expect xyz

## Code Review

- [ ] hi @kof, I need you to do
  - conceptual review (architecture, feature-correctness)
  - detailed review (read every line)
  - test it on preview

## Before requesting a review

- [ ] made a self-review
- [ ] added inline comments where things may be not obvious (the "why",
not "what")

## Before merging

- [ ] tested locally and on preview environment (preview dev login:
0000)
- [ ] updated [test
cases](https://github.com/webstudio-is/webstudio/blob/main/apps/builder/docs/test-cases.md)
document
- [ ] added tests
- [ ] if any new env variables are added, added them to `.env` file
2026-01-16 21:05:16 +01:00

127 lines
3.5 KiB
TypeScript

import { atom, computed } from "nanostores";
import type { DataSource, Resource, ResourceRequest } from "@webstudio-is/sdk";
import { restResourcesLoader } from "./router-utils";
import { computeExpression } from "./data-variables";
import { fetch } from "./fetch.client";
import { getResourceKey } from "./resource-utils";
const MAX_PENDING_RESOURCES = 5;
export { getResourceKey };
const queue = new Map<string, ResourceRequest>();
const pending = new Map<string, ResourceRequest>();
const cache = new Map<string, unknown>();
export const $resourcesCache = atom(cache);
const updateCache = () => {
$resourcesCache.set(new Map(cache));
};
const $pendingUpdater = atom({});
const updatePending = () => {
$pendingUpdater.set({});
};
export const $hasPendingResources = computed(
$pendingUpdater,
() => queue.size > 0 || pending.size > 0
);
const loadResources = async () => {
const list = Array.from(queue.values()).slice(0, MAX_PENDING_RESOURCES);
for (const resource of list) {
const key = getResourceKey(resource);
queue.delete(key);
pending.set(key, resource);
}
updatePending();
const response = await fetch(restResourcesLoader(), {
method: "POST",
body: JSON.stringify(Array.from(pending.values())),
});
if (response.ok) {
const results = new Map<string, unknown>(await response.json());
for (const [key, result] of results) {
cache.set(key, result);
pending.delete(key);
}
}
updateCache();
updatePending();
// restart loading until queue is empty
scheduleLoading();
};
let timeoutId: undefined | number;
const scheduleLoading = () => {
// scheduling will be restarted after finishing pending one
// skip when there is nothing in queue
if (pending.size > 0 || queue.size === 0) {
return;
}
window.clearTimeout(timeoutId);
// start loading after one second of "preload" call
timeoutId = window.setTimeout(loadResources, 1000);
};
export const preloadResource = (resource: ResourceRequest) => {
const key = getResourceKey(resource);
if (queue.has(key) || pending.has(key) || cache.has(key)) {
return;
}
// deduplicate resources in queue
queue.set(key, resource);
updatePending();
scheduleLoading();
};
export const invalidateResource = (resource: ResourceRequest) => {
const key = getResourceKey(resource);
cache.delete(key);
preloadResource(resource);
};
/**
* Invalidate the assets system resource.
* Call this when assets are uploaded, deleted, or modified to refresh expressions using assets.
*/
export const invalidateAssets = () => {
const url = "/$resources/assets";
// System resources always use GET with no params/headers/body
const systemResourceRequest: ResourceRequest = {
name: "assets",
method: "get",
url,
searchParams: [],
headers: [],
};
invalidateResource(systemResourceRequest);
};
export const computeResourceRequest = (
resource: Resource,
values: Map<DataSource["id"], unknown>
): ResourceRequest => {
const request: ResourceRequest = {
name: resource.name,
method: resource.method,
url: computeExpression(resource.url, values),
searchParams: (resource.searchParams ?? []).map(({ name, value }) => ({
name,
value: computeExpression(value, values),
})),
headers: resource.headers.map(({ name, value }) => ({
name,
value: computeExpression(value, values),
})),
};
if (resource.body !== undefined) {
request.body = computeExpression(resource.body, values);
}
return request;
};