@hyperframes/core 0.8.208 → 0.8.209

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,5 @@
1
+ /** Removes a scratch folder or file the mix is already done with. A refusal (Windows temp folders that deny
2
+ * delete) leaves it for the render's own cleanup and never fails the audio it served. */
3
+ export declare function removeScratch(path: string): void;
4
+ /** Runs `run`; whatever removeScratch left behind inside it (this run's only) goes to `report` once it ends. */
5
+ export declare function collectScratchLeftovers<T>(run: () => Promise<T>, report: (left: string[]) => void): Promise<T>;
@@ -0,0 +1,23 @@
1
+ import { AsyncLocalStorage } from "node:async_hooks";
2
+ import { rmSync } from "node:fs";
3
+ const collecting = new AsyncLocalStorage();
4
+ /** Removes a scratch folder or file the mix is already done with. A refusal (Windows temp folders that deny
5
+ * delete) leaves it for the render's own cleanup and never fails the audio it served. */
6
+ export function removeScratch(path) {
7
+ try {
8
+ rmSync(path, { recursive: true, force: true });
9
+ }
10
+ catch {
11
+ collecting.getStore()?.push(path);
12
+ }
13
+ }
14
+ /** Runs `run`; whatever removeScratch left behind inside it (this run's only) goes to `report` once it ends. */
15
+ export async function collectScratchLeftovers(run, report) {
16
+ const left = [];
17
+ try {
18
+ return await collecting.run(left, run);
19
+ }
20
+ finally {
21
+ report(left);
22
+ }
23
+ }
@@ -0,0 +1,16 @@
1
+ interface Options {
2
+ platform?: NodeJS.Platform;
3
+ /** Copy the bytes when Windows keeps refusing (a TEMP that denies delete). Not atomic: scratch files only. */
4
+ copyFallback?: boolean;
5
+ }
6
+ /** `renameSync` that rides out Windows' transient file locks; other platforms and errors throw at once. */
7
+ export declare function renameWithRetry(from: string, to: string, options?: Options & {
8
+ rename?: (from: string, to: string) => void;
9
+ copy?: (from: string, to: string) => void;
10
+ }): void;
11
+ /** Async twin of {@link renameWithRetry}. */
12
+ export declare function renameWithRetryAsync(from: string, to: string, options?: Options & {
13
+ rename?: (from: string, to: string) => Promise<void>;
14
+ copy?: (from: string, to: string) => Promise<void>;
15
+ }): Promise<void>;
16
+ export {};
@@ -0,0 +1,81 @@
1
+ import { copyFileSync, renameSync } from "node:fs";
2
+ import { copyFile, rename } from "node:fs/promises";
3
+ import { removeScratch } from "./removeScratch.js";
4
+ // Windows reports EPERM/EACCES/EBUSY while antivirus or a just-exited process still holds the
5
+ // file; the hold clears within milliseconds. Total wait before giving up: about 1.3 s.
6
+ const RETRYABLE = new Set(["EPERM", "EACCES", "EBUSY"]);
7
+ const BACKOFF_MS = [10, 20, 40, 80, 160, 320, 640];
8
+ function isRetryable(error, platform) {
9
+ if (platform !== "win32")
10
+ return false;
11
+ return RETRYABLE.has(error?.code ?? "");
12
+ }
13
+ /** What a refused rename does next: wait and try again, fall back to a copy, or give up with the error. */
14
+ function nextStep(error, attempt, options, platform) {
15
+ if (!isRetryable(error, platform))
16
+ return "throw";
17
+ if (attempt < BACKOFF_MS.length)
18
+ return "wait";
19
+ return options.copyFallback ? "copy" : "throw";
20
+ }
21
+ function sleepSync(ms) {
22
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
23
+ }
24
+ /** `renameSync` that rides out Windows' transient file locks; other platforms and errors throw at once. */
25
+ export function renameWithRetry(from, to, options = {}) {
26
+ const doRename = options.rename ?? renameSync;
27
+ const doCopy = options.copy ?? copyFileSync;
28
+ const platform = options.platform ?? process.platform;
29
+ for (let attempt = 0;; attempt++) {
30
+ try {
31
+ doRename(from, to);
32
+ return;
33
+ }
34
+ catch (error) {
35
+ const step = nextStep(error, attempt, options, platform);
36
+ if (step === "throw")
37
+ throw error;
38
+ if (step === "wait") {
39
+ sleepSync(BACKOFF_MS[attempt]);
40
+ continue;
41
+ }
42
+ try {
43
+ doCopy(from, to);
44
+ }
45
+ catch {
46
+ throw error;
47
+ }
48
+ removeScratch(from);
49
+ return;
50
+ }
51
+ }
52
+ }
53
+ /** Async twin of {@link renameWithRetry}. */
54
+ export async function renameWithRetryAsync(from, to, options = {}) {
55
+ const doRename = options.rename ?? rename;
56
+ const doCopy = options.copy ?? copyFile;
57
+ const platform = options.platform ?? process.platform;
58
+ for (let attempt = 0;; attempt++) {
59
+ try {
60
+ await doRename(from, to);
61
+ return;
62
+ }
63
+ catch (error) {
64
+ const step = nextStep(error, attempt, options, platform);
65
+ if (step === "throw")
66
+ throw error;
67
+ if (step === "wait") {
68
+ await new Promise((resolve) => setTimeout(resolve, BACKOFF_MS[attempt]));
69
+ continue;
70
+ }
71
+ try {
72
+ await doCopy(from, to);
73
+ }
74
+ catch {
75
+ throw error;
76
+ }
77
+ removeScratch(from);
78
+ return;
79
+ }
80
+ }
81
+ }
@@ -21,10 +21,10 @@
21
21
  * chain the color-grading runtime uses: `__hfVariablesByComp[compId]` for
22
22
  * inlined sub-compositions, then the top-level merged `getVariables()`.
23
23
  *
24
- * Applied at init AND re-applied after the composition loader inlines
25
- * external / template sub-compositions (their DOM and per-instance scoped
26
- * values don't exist at init). Idempotent: re-applying writes the same
27
- * values.
24
+ * Text is bound before composition scripts run; all of it at init and after
25
+ * the composition loader inlines external / template sub-compositions (their
26
+ * DOM and per-instance scoped values don't exist at init). Idempotent:
27
+ * re-applying writes the same values.
28
28
  */
29
29
  /**
30
30
  * Strip characters that could smuggle additional declarations or markup out of
@@ -45,3 +45,5 @@ export declare function sanitizeCssValue(value: string): string;
45
45
  export declare function unproxiedMediaSrc(el: Element): string | null;
46
46
  /** Applies the bindings in `doc`, or only those inside `within` and `within` itself. */
47
47
  export declare function applyVariableBindings(doc: Document, within?: Element): void;
48
+ /** Binds only text, in `doc` or inside `within`: what composition scripts read before they run. */
49
+ export declare function applyTextBindings(doc: Document, within?: Element): void;
@@ -21,10 +21,10 @@
21
21
  * chain the color-grading runtime uses: `__hfVariablesByComp[compId]` for
22
22
  * inlined sub-compositions, then the top-level merged `getVariables()`.
23
23
  *
24
- * Applied at init AND re-applied after the composition loader inlines
25
- * external / template sub-compositions (their DOM and per-instance scoped
26
- * values don't exist at init). Idempotent: re-applying writes the same
27
- * values.
24
+ * Text is bound before composition scripts run; all of it at init and after
25
+ * the composition loader inlines external / template sub-compositions (their
26
+ * DOM and per-instance scoped values don't exist at init). Idempotent:
27
+ * re-applying writes the same values.
28
28
  */
29
29
  import { readVariablesForElement } from "./variableScope.js";
30
30
  import { isScalarVariableValue as isScalar, isSafeMediaUrl, } from "@hyperframes/parsers/composition";
@@ -154,12 +154,13 @@ function variableSrcFor(el, cache, warn = true) {
154
154
  export function unproxiedMediaSrc(el) {
155
155
  return variableSrcFor(el, new Map(), false) ?? unproxiedSrc(el);
156
156
  }
157
+ const matching = (doc, within, selector) => within
158
+ ? [...(within.matches(selector) ? [within] : []), ...within.querySelectorAll(selector)]
159
+ : Array.from(doc.querySelectorAll(selector));
157
160
  /** Applies the bindings in `doc`, or only those inside `within` and `within` itself. */
158
161
  export function applyVariableBindings(doc, within) {
159
162
  const cache = new Map();
160
- const all = (selector) => within
161
- ? [...(within.matches(selector) ? [within] : []), ...within.querySelectorAll(selector)]
162
- : Array.from(doc.querySelectorAll(selector));
163
+ const all = (selector) => matching(doc, within, selector);
163
164
  const topRoot = within ? null : findTopRoot(doc);
164
165
  applyCssCustomProperties([...(topRoot ? [topRoot] : []), ...all("[data-composition-id]")], cache);
165
166
  for (const el of all("[data-var-src]")) {
@@ -167,12 +168,23 @@ export function applyVariableBindings(doc, within) {
167
168
  if (url !== null && unproxiedSrc(el) !== url)
168
169
  el.setAttribute("src", url);
169
170
  }
170
- for (const el of all("[data-var-text]")) {
171
+ bindText(all("[data-var-text]"), cache);
172
+ }
173
+ /** Binds only text, in `doc` or inside `within`: what composition scripts read before they run. */
174
+ export function applyTextBindings(doc, within) {
175
+ bindText(matching(doc, within, "[data-var-text]"), new Map());
176
+ }
177
+ const textWrittenOnce = new WeakSet();
178
+ /** Writes each binding once: a script may then reshape the text, which a second write would duplicate. */
179
+ function bindText(elements, cache) {
180
+ for (const el of elements) {
171
181
  const id = el.getAttribute("data-var-text")?.trim();
172
- if (!id)
182
+ if (!id || textWrittenOnce.has(el))
173
183
  continue;
174
184
  const value = valuesForElement(el, cache)[id];
175
- if (isScalar(value))
176
- setOwnTextPreservingChildren(el, String(value));
185
+ if (!isScalar(value))
186
+ continue;
187
+ setOwnTextPreservingChildren(el, String(value));
188
+ textWrittenOnce.add(el);
177
189
  }
178
190
  }
@@ -20,18 +20,19 @@
20
20
  * const { title = "Untitled", theme = "light" } = getVariables<MyVars>();
21
21
  */
22
22
  import { cssVariableName, detectSlugCollisions } from "../tokenSlug.js";
23
+ import { scopedVariablesFor } from "./variableScope.js";
23
24
  export function getVariables() {
24
25
  if (typeof document === "undefined")
25
26
  return {};
26
- // Same collection the CSS-variable injection uses: <html> first, then any
27
- // composition element carrying the attribute (later declarers win), then
28
- // render-time overrides.
27
+ // <html> first, then any element carrying the attribute (later declarers win), then render-time
28
+ // overrides. A sub-composition's declaration is its own scope's, read from its scoped table.
29
29
  const declarers = new Set();
30
30
  if (document.documentElement?.hasAttribute("data-composition-variables")) {
31
31
  declarers.add(document.documentElement);
32
32
  }
33
33
  for (const el of Array.from(document.querySelectorAll("[data-composition-variables]"))) {
34
- declarers.add(el);
34
+ if (!scopedVariablesFor(el))
35
+ declarers.add(el);
35
36
  }
36
37
  const declaredDefaults = {};
37
38
  for (const el of declarers) {
@@ -5,4 +5,6 @@
5
5
  * runtime consumer of variables (color grading, declarative bindings) so the
6
6
  * scope chain can never diverge between channels.
7
7
  */
8
+ /** The per-instance values of the inlined sub-composition an element belongs to, if any. */
9
+ export declare function scopedVariablesFor(element: Element): Record<string, unknown> | undefined;
8
10
  export declare function readVariablesForElement(element: Element): Record<string, unknown>;
@@ -5,11 +5,17 @@
5
5
  * runtime consumer of variables (color grading, declarative bindings) so the
6
6
  * scope chain can never diverge between channels.
7
7
  */
8
- export function readVariablesForElement(element) {
9
- const win = window;
8
+ /** The per-instance values of the inlined sub-composition an element belongs to, if any. */
9
+ export function scopedVariablesFor(element) {
10
10
  const scope = element.closest("[data-composition-id]");
11
11
  const compositionId = scope?.getAttribute("data-composition-id")?.trim() ?? "";
12
- const scoped = compositionId ? win.__hfVariablesByComp?.[compositionId] : undefined;
12
+ return compositionId
13
+ ? window.__hfVariablesByComp?.[compositionId]
14
+ : undefined;
15
+ }
16
+ export function readVariablesForElement(element) {
17
+ const win = window;
18
+ const scoped = scopedVariablesFor(element);
13
19
  if (scoped)
14
20
  return scoped;
15
21
  const fromHelper = win.__hyperframes?.getVariables?.();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hyperframes/core",
3
- "version": "0.8.208",
3
+ "version": "0.8.209",
4
4
  "description": "",
5
5
  "repository": {
6
6
  "type": "git",
@@ -64,6 +64,14 @@
64
64
  "import": "./dist/removeTree.js",
65
65
  "types": "./dist/removeTree.d.ts"
66
66
  },
67
+ "./rename-retry": {
68
+ "import": "./dist/renameWithRetry.js",
69
+ "types": "./dist/renameWithRetry.d.ts"
70
+ },
71
+ "./remove-scratch": {
72
+ "import": "./dist/removeScratch.js",
73
+ "types": "./dist/removeScratch.d.ts"
74
+ },
67
75
  "./project-folders": {
68
76
  "import": "./dist/projectFolders.js",
69
77
  "types": "./dist/projectFolders.d.ts"
@@ -417,9 +425,9 @@
417
425
  "linkedom": "^0.18.12",
418
426
  "postcss": "^8.5.8",
419
427
  "wawoff2": "^2.0.1",
420
- "@hyperframes/lint": "0.8.208",
421
- "@hyperframes/studio-server": "0.8.208",
422
- "@hyperframes/parsers": "0.8.208"
428
+ "@hyperframes/lint": "0.8.209",
429
+ "@hyperframes/parsers": "0.8.209",
430
+ "@hyperframes/studio-server": "0.8.209"
423
431
  },
424
432
  "devDependencies": {
425
433
  "@fontsource/archivo-black": "^5.2.8",