Add script asset sub-assets

This commit is contained in:
2026-01-09 18:42:37 +01:00
parent c4354782a8
commit 96b1cf501c
16 changed files with 770 additions and 17 deletions

View File

@@ -88,6 +88,9 @@ export class BroadcastRenderer {
const wasExisting = this.state.assets.has(asset.id);
this.state.assets.set(asset.id, asset);
ensureLayerPosition(this.state, asset.id, placement);
if (isCodeAsset(asset)) {
this.updateScriptWorkerAttachments(asset);
}
if (!wasExisting && !this.state.visibilityStates.has(asset.id)) {
const initialAlpha = 0; // Fade in newly discovered assets
this.state.visibilityStates.set(asset.id, {
@@ -484,6 +487,20 @@ export class BroadcastRenderer {
payload: {
id: asset.id,
source: assetSource,
attachments: asset.scriptAttachments || [],
},
});
}
updateScriptWorkerAttachments(asset) {
if (!this.scriptWorker || !this.scriptWorkerReady || !asset?.id) {
return;
}
this.scriptWorker.postMessage({
type: "updateAttachments",
payload: {
id: asset.id,
attachments: asset.scriptAttachments || [],
},
});
}

View File

@@ -1,4 +1,5 @@
const scripts = new Map();
const allowedFetchUrls = new Set();
let canvas = null;
let ctx = null;
let channelName = "";
@@ -9,9 +10,18 @@ const tickIntervalMs = 1000 / 60;
const errorKeys = new Set();
function disableNetworkApis() {
const nativeFetch = typeof self.fetch === "function" ? self.fetch.bind(self) : null;
const blockedApis = {
fetch: () => {
throw new Error("Network access is disabled in asset scripts.");
fetch: (...args) => {
if (!nativeFetch) {
throw new Error("Network access is disabled in asset scripts.");
}
const request = new Request(...args);
const url = normalizeUrl(request.url);
if (!allowedFetchUrls.has(url)) {
throw new Error("Network access is disabled in asset scripts.");
}
return nativeFetch(request);
},
XMLHttpRequest: undefined,
WebSocket: undefined,
@@ -43,6 +53,32 @@ function disableNetworkApis() {
disableNetworkApis();
function normalizeUrl(url) {
try {
return new URL(url, self.location?.href || "http://localhost").toString();
} catch (_error) {
return "";
}
}
function refreshAllowedFetchUrls() {
allowedFetchUrls.clear();
scripts.forEach((script) => {
const assets = script?.context?.assets;
if (!Array.isArray(assets)) {
return;
}
assets.forEach((asset) => {
if (asset?.url) {
const normalized = normalizeUrl(asset.url);
if (normalized) {
allowedFetchUrls.add(normalized);
}
}
});
});
}
function reportScriptError(id, stage, error) {
if (!id) {
return;
@@ -115,7 +151,8 @@ function stopTickLoopIfIdle() {
}
function createScriptHandlers(source, context, state, sourceLabel = "") {
const contextPrelude = "const { canvas, ctx, channelName, width, height, now, deltaMs, elapsedMs } = context;";
const contextPrelude =
"const { canvas, ctx, channelName, width, height, now, deltaMs, elapsedMs, assets } = context;";
const sourceUrl = sourceLabel ? `\n//# sourceURL=${sourceLabel}` : "";
const factory = new Function(
"context",
@@ -172,6 +209,7 @@ self.addEventListener("message", (event) => {
now: 0,
deltaMs: 0,
elapsedMs: 0,
assets: Array.isArray(payload.attachments) ? payload.attachments : [],
};
let handlers = {};
try {
@@ -189,6 +227,7 @@ self.addEventListener("message", (event) => {
tick: handlers.tick,
};
scripts.set(payload.id, script);
refreshAllowedFetchUrls();
if (script.init) {
try {
script.init(script.context, script.state);
@@ -206,6 +245,19 @@ self.addEventListener("message", (event) => {
return;
}
scripts.delete(payload.id);
refreshAllowedFetchUrls();
stopTickLoopIfIdle();
}
if (type === "updateAttachments") {
if (!payload?.id) {
return;
}
const script = scripts.get(payload.id);
if (!script) {
return;
}
script.context.assets = Array.isArray(payload.attachments) ? payload.attachments : [];
refreshAllowedFetchUrls();
}
});