@onmark/cli 0.1.3
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.
- package/LICENSE +21 -0
- package/README.md +202 -0
- package/onmark-release.json +388 -0
- package/package.json +52 -0
- package/packages/authoring/dist/src/index.d.ts +3 -0
- package/packages/authoring/dist/src/index.js +4 -0
- package/packages/authoring/dist/src/motion.d.ts +23 -0
- package/packages/authoring/dist/src/motion.js +81 -0
- package/packages/authoring/dist/src/presentation.d.ts +12 -0
- package/packages/authoring/dist/src/presentation.js +226 -0
- package/packages/authoring/dist/src/resource.d.ts +32 -0
- package/packages/authoring/dist/src/resource.js +86 -0
- package/packages/authoring/dist/src/types.d.ts +1 -0
- package/packages/authoring/dist/src/types.js +2 -0
- package/packages/bundler/dist/src/authored_html.d.ts +41 -0
- package/packages/bundler/dist/src/authored_html.js +387 -0
- package/packages/bundler/dist/src/command.d.ts +2 -0
- package/packages/bundler/dist/src/command.js +138 -0
- package/packages/bundler/dist/src/generated/bundle-manifest.d.ts +65 -0
- package/packages/bundler/dist/src/generated/bundle-manifest.js +18 -0
- package/packages/bundler/dist/src/html_image.d.ts +20 -0
- package/packages/bundler/dist/src/html_image.js +337 -0
- package/packages/bundler/dist/src/image_admission.d.ts +2 -0
- package/packages/bundler/dist/src/image_admission.js +217 -0
- package/packages/bundler/dist/src/index.d.ts +2 -0
- package/packages/bundler/dist/src/index.js +3 -0
- package/packages/bundler/dist/src/presentation.d.ts +41 -0
- package/packages/bundler/dist/src/presentation.js +492 -0
- package/packages/launcher/dist/desktop-release.json +21 -0
- package/packages/launcher/dist/src/browser-command.d.ts +2 -0
- package/packages/launcher/dist/src/browser-command.js +43 -0
- package/packages/launcher/dist/src/browser.d.ts +14 -0
- package/packages/launcher/dist/src/browser.js +280 -0
- package/packages/launcher/dist/src/cache.d.ts +8 -0
- package/packages/launcher/dist/src/cache.js +21 -0
- package/packages/launcher/dist/src/capture-environment.d.ts +15 -0
- package/packages/launcher/dist/src/capture-environment.js +145 -0
- package/packages/launcher/dist/src/command.d.ts +2 -0
- package/packages/launcher/dist/src/command.js +58 -0
- package/packages/launcher/dist/src/native.d.ts +20 -0
- package/packages/launcher/dist/src/native.js +31 -0
- package/packages/launcher/dist/src/platform.d.ts +13 -0
- package/packages/launcher/dist/src/platform.js +29 -0
- package/packages/launcher/dist/src/release.d.ts +16 -0
- package/packages/launcher/dist/src/release.js +49 -0
- package/packages/motion-gsap/dist/src/index.d.ts +16 -0
- package/packages/motion-gsap/dist/src/index.js +139 -0
- package/packages/runtime/dist/src/clock.d.ts +10 -0
- package/packages/runtime/dist/src/clock.js +19 -0
- package/packages/runtime/dist/src/generated/browser-request.d.ts +143 -0
- package/packages/runtime/dist/src/generated/browser-request.js +2 -0
- package/packages/runtime/dist/src/generated/browser-response.d.ts +58 -0
- package/packages/runtime/dist/src/generated/browser-response.js +2 -0
- package/packages/runtime/dist/src/generated/bundle-layout.d.ts +7 -0
- package/packages/runtime/dist/src/generated/bundle-layout.js +14 -0
- package/packages/runtime/dist/src/generated/codec.d.ts +8 -0
- package/packages/runtime/dist/src/generated/codec.js +22 -0
- package/packages/runtime/dist/src/generated/runtime-contract.d.ts +7 -0
- package/packages/runtime/dist/src/generated/runtime-contract.js +8 -0
- package/packages/runtime/dist/src/generated/validators.d.ts +13 -0
- package/packages/runtime/dist/src/generated/validators.js +3327 -0
- package/packages/runtime/dist/src/host.d.ts +11 -0
- package/packages/runtime/dist/src/host.js +25 -0
- package/packages/runtime/dist/src/index.d.ts +13 -0
- package/packages/runtime/dist/src/index.js +12 -0
- package/packages/runtime/dist/src/media.d.ts +13 -0
- package/packages/runtime/dist/src/media.js +28 -0
- package/packages/runtime/dist/src/presentation.d.ts +66 -0
- package/packages/runtime/dist/src/presentation.js +379 -0
- package/packages/runtime/dist/src/resource.d.ts +23 -0
- package/packages/runtime/dist/src/resource.js +150 -0
- package/packages/runtime/dist/src/session.d.ts +43 -0
- package/packages/runtime/dist/src/session.js +450 -0
- package/packages/runtime/dist/src/types.d.ts +6 -0
- package/packages/runtime/dist/src/types.js +2 -0
- package/packages/runtime/dist/src/video.d.ts +40 -0
- package/packages/runtime/dist/src/video.js +343 -0
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
// Bounded readiness and terminal cleanup for presentation-owned resources.
|
|
2
|
+
// Resources carry browser effects only; Rust-owned plan facts remain authoritative.
|
|
3
|
+
import { MAX_PENDING_RESOURCE_CHARACTERS, MAX_PENDING_RESOURCES, } from "./generated/runtime-contract.js";
|
|
4
|
+
import { RuntimeAdapterError } from "./session.js";
|
|
5
|
+
// ── Public contract ──
|
|
6
|
+
const MAX_READINESS_TIMEOUT_MILLISECONDS = 24 * 60 * 60 * 1_000;
|
|
7
|
+
const MAX_RESOURCE_ID_CHARACTERS = 256;
|
|
8
|
+
export const PRESENTATION_RESOURCE_KINDS = Object.freeze([
|
|
9
|
+
"image",
|
|
10
|
+
"font",
|
|
11
|
+
"texture",
|
|
12
|
+
"custom",
|
|
13
|
+
]);
|
|
14
|
+
/** Maximum resources retained by one presentation adapter. */
|
|
15
|
+
export const MAX_PRESENTATION_RESOURCES = MAX_PENDING_RESOURCES;
|
|
16
|
+
// ── Collection lifecycle ──
|
|
17
|
+
/** Takes an immutable snapshot before validation or asynchronous work begins. */
|
|
18
|
+
export function ownPresentationResources(resources) {
|
|
19
|
+
return Object.freeze(resources.map(ownPresentationResource));
|
|
20
|
+
}
|
|
21
|
+
function ownPresentationResource(resource) {
|
|
22
|
+
return Object.freeze({
|
|
23
|
+
id: resource.id,
|
|
24
|
+
kind: resource.kind,
|
|
25
|
+
dispose: resource.dispose.bind(resource),
|
|
26
|
+
prepare: resource.prepare.bind(resource),
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
/** Rejects resource collections that cannot fit the runtime failure contract. */
|
|
30
|
+
export function validatePresentationResources(resources) {
|
|
31
|
+
if (resources.length > MAX_PRESENTATION_RESOURCES) {
|
|
32
|
+
throw new RuntimeAdapterError("operation", "presentation resource count exceeds its limit");
|
|
33
|
+
}
|
|
34
|
+
const identities = new Set();
|
|
35
|
+
for (const resource of resources) {
|
|
36
|
+
const identity = resourceIdentity(resource);
|
|
37
|
+
if (identities.has(identity)) {
|
|
38
|
+
throw new RuntimeAdapterError("operation", "presentation resource identity is duplicated");
|
|
39
|
+
}
|
|
40
|
+
identities.add(identity);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
/** Waits for every resource under one bounded, deterministic failure policy. */
|
|
44
|
+
export async function preparePresentationResources(resources, timeoutMilliseconds) {
|
|
45
|
+
const outcomes = await Promise.all(resources.map((resource) => prepareResource(resource, timeoutMilliseconds)));
|
|
46
|
+
let failure;
|
|
47
|
+
const pendingResources = [];
|
|
48
|
+
for (const outcome of outcomes) {
|
|
49
|
+
if (outcome.kind === "failed") {
|
|
50
|
+
failure ??= outcome.error;
|
|
51
|
+
}
|
|
52
|
+
else if (outcome.kind === "timedOut") {
|
|
53
|
+
pendingResources.push(outcome.pendingResource);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
if (failure !== undefined) {
|
|
57
|
+
throw withPendingResources(failure, pendingResources);
|
|
58
|
+
}
|
|
59
|
+
if (pendingResources.length > 0) {
|
|
60
|
+
throw new RuntimeAdapterError("readinessTimeout", "presentation resources did not become ready", pendingResources);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
function withPendingResources(failure, pendingResources) {
|
|
64
|
+
if (pendingResources.length === 0) {
|
|
65
|
+
return failure;
|
|
66
|
+
}
|
|
67
|
+
// Collection-owned identities take precedence when a custom failure already
|
|
68
|
+
// carries its own pending details; this keeps every declared resource within
|
|
69
|
+
// the protocol's fixed collection budget.
|
|
70
|
+
return new RuntimeAdapterError(failure.kind, failure.message, pendingResources);
|
|
71
|
+
}
|
|
72
|
+
/** Releases all owned resources while retaining the first cleanup failure. */
|
|
73
|
+
export async function releasePresentationResources(resources) {
|
|
74
|
+
let failure;
|
|
75
|
+
for (const resource of resources) {
|
|
76
|
+
try {
|
|
77
|
+
await resource.dispose();
|
|
78
|
+
}
|
|
79
|
+
catch (error) {
|
|
80
|
+
failure ??= error;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return failure;
|
|
84
|
+
}
|
|
85
|
+
/** Validates the shared browser-readiness deadline before resources are bound. */
|
|
86
|
+
export function requireReadinessTimeout(timeoutMilliseconds) {
|
|
87
|
+
if (!Number.isSafeInteger(timeoutMilliseconds) ||
|
|
88
|
+
timeoutMilliseconds <= 0 ||
|
|
89
|
+
timeoutMilliseconds > MAX_READINESS_TIMEOUT_MILLISECONDS) {
|
|
90
|
+
throw new TypeError("readiness timeout must be a positive integer no greater than one day");
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
// ── Individual readiness ──
|
|
94
|
+
async function prepareResource(resource, timeoutMilliseconds) {
|
|
95
|
+
const pendingResource = `${resourceIdentity(resource)}:prepare`;
|
|
96
|
+
let deadline;
|
|
97
|
+
const timeout = new Promise((resolve) => {
|
|
98
|
+
deadline = setTimeout(() => resolve({ kind: "timedOut", pendingResource }), timeoutMilliseconds);
|
|
99
|
+
});
|
|
100
|
+
const preparation = observePreparation(resource, pendingResource);
|
|
101
|
+
try {
|
|
102
|
+
return await Promise.race([preparation, timeout]);
|
|
103
|
+
}
|
|
104
|
+
finally {
|
|
105
|
+
clearTimeout(deadline);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
async function observePreparation(resource, pendingResource) {
|
|
109
|
+
try {
|
|
110
|
+
// Install every resource deadline before author code can run.
|
|
111
|
+
await Promise.resolve();
|
|
112
|
+
await resource.prepare();
|
|
113
|
+
return { kind: "ready" };
|
|
114
|
+
}
|
|
115
|
+
catch (error) {
|
|
116
|
+
return {
|
|
117
|
+
kind: "failed",
|
|
118
|
+
error: resourceFailure(error, pendingResource),
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
function resourceIdentity(resource) {
|
|
123
|
+
if (typeof resource.id !== "string") {
|
|
124
|
+
throw new RuntimeAdapterError("operation", "presentation resource id must be a string");
|
|
125
|
+
}
|
|
126
|
+
if (!PRESENTATION_RESOURCE_KINDS.includes(resource.kind)) {
|
|
127
|
+
throw new RuntimeAdapterError("operation", "presentation resource kind is invalid");
|
|
128
|
+
}
|
|
129
|
+
if (resource.id.length === 0 || resource.id !== resource.id.trim()) {
|
|
130
|
+
throw new RuntimeAdapterError("operation", "presentation resource id must be nonblank and trimmed");
|
|
131
|
+
}
|
|
132
|
+
let characters = 0;
|
|
133
|
+
for (const _character of resource.id) {
|
|
134
|
+
characters += 1;
|
|
135
|
+
if (characters > MAX_RESOURCE_ID_CHARACTERS) {
|
|
136
|
+
throw new RuntimeAdapterError("operation", "presentation resource id exceeds its character limit");
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
const identity = `${resource.kind}:${resource.id}`;
|
|
140
|
+
if ([...`${identity}:prepare`].length > MAX_PENDING_RESOURCE_CHARACTERS) {
|
|
141
|
+
throw new RuntimeAdapterError("operation", "presentation resource identity exceeds the protocol limit");
|
|
142
|
+
}
|
|
143
|
+
return identity;
|
|
144
|
+
}
|
|
145
|
+
function resourceFailure(error, pendingResource) {
|
|
146
|
+
if (error instanceof RuntimeAdapterError) {
|
|
147
|
+
return error;
|
|
148
|
+
}
|
|
149
|
+
return new RuntimeAdapterError("operation", `${pendingResource} failed to prepare`);
|
|
150
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { BrowserPlan, BrowserRequest } from "./generated/browser-request.js";
|
|
2
|
+
import type { BrowserResponse } from "./generated/browser-response.js";
|
|
3
|
+
import { type RuntimeFrame } from "./clock.js";
|
|
4
|
+
type Immutable<T> = T extends object ? {
|
|
5
|
+
readonly [Key in keyof T]: Immutable<T[Key]>;
|
|
6
|
+
} : T;
|
|
7
|
+
/** Immutable browser-plan view owned by one runtime session. */
|
|
8
|
+
export type RuntimePlan = Immutable<BrowserPlan>;
|
|
9
|
+
/**
|
|
10
|
+
* Browser-owned operations sequenced by one runtime session.
|
|
11
|
+
*
|
|
12
|
+
* Implementations bound every asynchronous wait and report expected browser
|
|
13
|
+
* failures through `RuntimeAdapterError`.
|
|
14
|
+
*/
|
|
15
|
+
export interface RuntimeAdapter {
|
|
16
|
+
/** Installs one owned snapshot of the immutable browser plan. */
|
|
17
|
+
load(plan: RuntimePlan): Promise<void>;
|
|
18
|
+
/** Resolves only after resources at the evaluation start are stable. */
|
|
19
|
+
prepare(frame: RuntimeFrame): Promise<void>;
|
|
20
|
+
/** Stages one exact frame and registers media presentation observers. */
|
|
21
|
+
seek(frame: RuntimeFrame): Promise<void>;
|
|
22
|
+
/** Verifies media presentation after native compositor capture. */
|
|
23
|
+
confirm(frame: RuntimeFrame): Promise<void>;
|
|
24
|
+
/** Releases all resources owned by this adapter. */
|
|
25
|
+
dispose(): Promise<void>;
|
|
26
|
+
}
|
|
27
|
+
export type RuntimeAdapterFailureKind = "operation" | "readinessTimeout";
|
|
28
|
+
/** Expected failure reported by a browser adapter. */
|
|
29
|
+
export declare class RuntimeAdapterError extends Error {
|
|
30
|
+
readonly kind: RuntimeAdapterFailureKind;
|
|
31
|
+
readonly pendingResources: readonly string[];
|
|
32
|
+
constructor(kind: RuntimeAdapterFailureKind, message: string, pendingResources?: readonly string[]);
|
|
33
|
+
/** Preserves typed failures and contains untyped browser exceptions. */
|
|
34
|
+
static fromUnknown(error: unknown, message: string): RuntimeAdapterError;
|
|
35
|
+
}
|
|
36
|
+
/** Sequential browser protocol session. */
|
|
37
|
+
export declare class RuntimeSession {
|
|
38
|
+
#private;
|
|
39
|
+
constructor(adapter: RuntimeAdapter);
|
|
40
|
+
/** Executes one decoded request without introducing an unbounded queue. */
|
|
41
|
+
dispatch(request: BrowserRequest): Promise<BrowserResponse>;
|
|
42
|
+
}
|
|
43
|
+
export {};
|
|
@@ -0,0 +1,450 @@
|
|
|
1
|
+
// Sequential browser protocol session.
|
|
2
|
+
// Owns command ordering and state while a narrow adapter owns browser effects;
|
|
3
|
+
// this split keeps protocol behavior deterministic and directly testable.
|
|
4
|
+
import { decodeBrowserResponse } from "./generated/codec.js";
|
|
5
|
+
import { MAX_FAILURE_MESSAGE_CHARACTERS, MAX_PENDING_RESOURCE_CHARACTERS, MAX_PENDING_RESOURCES, } from "./generated/runtime-contract.js";
|
|
6
|
+
import { runtimeFrameAt } from "./clock.js";
|
|
7
|
+
/** Expected failure reported by a browser adapter. */
|
|
8
|
+
export class RuntimeAdapterError extends Error {
|
|
9
|
+
kind;
|
|
10
|
+
pendingResources;
|
|
11
|
+
constructor(kind, message, pendingResources = []) {
|
|
12
|
+
const failureMessage = boundedText(message, "adapter failure message", MAX_FAILURE_MESSAGE_CHARACTERS);
|
|
13
|
+
super(failureMessage);
|
|
14
|
+
this.name = "RuntimeAdapterError";
|
|
15
|
+
this.kind = kind;
|
|
16
|
+
this.pendingResources = boundedPendingResources(pendingResources);
|
|
17
|
+
}
|
|
18
|
+
/** Preserves typed failures and contains untyped browser exceptions. */
|
|
19
|
+
static fromUnknown(error, message) {
|
|
20
|
+
return error instanceof RuntimeAdapterError
|
|
21
|
+
? error
|
|
22
|
+
: new RuntimeAdapterError("operation", message);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
// ── Protocol session ──
|
|
26
|
+
/** Sequential browser protocol session. */
|
|
27
|
+
export class RuntimeSession {
|
|
28
|
+
#adapter;
|
|
29
|
+
#state = { kind: "empty" };
|
|
30
|
+
#busy = false;
|
|
31
|
+
constructor(adapter) {
|
|
32
|
+
this.#adapter = adapter;
|
|
33
|
+
}
|
|
34
|
+
/** Executes one decoded request without introducing an unbounded queue. */
|
|
35
|
+
async dispatch(request) {
|
|
36
|
+
if (this.#busy) {
|
|
37
|
+
return invalidRequest(request.requestId, "another browser command is still in progress");
|
|
38
|
+
}
|
|
39
|
+
this.#busy = true;
|
|
40
|
+
try {
|
|
41
|
+
return await this.#execute(request);
|
|
42
|
+
}
|
|
43
|
+
finally {
|
|
44
|
+
this.#busy = false;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
#execute(request) {
|
|
48
|
+
switch (request.command.type) {
|
|
49
|
+
case "load":
|
|
50
|
+
return this.#load(request.requestId, request.command.plan);
|
|
51
|
+
case "prepare":
|
|
52
|
+
return this.#prepare(request.requestId, request.command.evaluationStart);
|
|
53
|
+
case "seek":
|
|
54
|
+
return this.#seek(request.requestId, request.command.frame);
|
|
55
|
+
case "confirm":
|
|
56
|
+
return this.#confirm(request.requestId, request.command.frame);
|
|
57
|
+
case "dispose":
|
|
58
|
+
return this.#dispose(request.requestId);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
async #load(requestId, plan) {
|
|
62
|
+
if (this.#state.kind !== "empty") {
|
|
63
|
+
return invalidRequest(requestId, "load requires an empty browser session");
|
|
64
|
+
}
|
|
65
|
+
const violation = planViolation(plan);
|
|
66
|
+
if (violation !== undefined) {
|
|
67
|
+
return invalidRequest(requestId, violation);
|
|
68
|
+
}
|
|
69
|
+
const ownedPlan = snapshotPlan(plan);
|
|
70
|
+
const nextState = loadedState(ownedPlan);
|
|
71
|
+
try {
|
|
72
|
+
await this.#adapter.load(ownedPlan);
|
|
73
|
+
}
|
|
74
|
+
catch (error) {
|
|
75
|
+
// Loading may have entered author code and partially mutated its owned
|
|
76
|
+
// browser state. Only terminal disposal is safe after that boundary.
|
|
77
|
+
this.#state = { kind: "failed" };
|
|
78
|
+
return operationFailure(requestId, "loadFailed", error);
|
|
79
|
+
}
|
|
80
|
+
this.#state = nextState;
|
|
81
|
+
return response(requestId, { type: "loaded" });
|
|
82
|
+
}
|
|
83
|
+
async #prepare(requestId, evaluationStart) {
|
|
84
|
+
if (this.#state.kind !== "loaded") {
|
|
85
|
+
return invalidRequest(requestId, "prepare requires a loaded browser plan");
|
|
86
|
+
}
|
|
87
|
+
if (evaluationStart !== this.#state.evaluationStart) {
|
|
88
|
+
return invalidRequest(requestId, "prepare must use the plan evaluation start");
|
|
89
|
+
}
|
|
90
|
+
try {
|
|
91
|
+
await this.#adapter.prepare(runtimeFrameAt(evaluationStart, this.#state.frameRate));
|
|
92
|
+
}
|
|
93
|
+
catch (error) {
|
|
94
|
+
// Preparation may have started author-owned asynchronous work that the
|
|
95
|
+
// generic adapter cannot cancel. Make the session terminal so a retry
|
|
96
|
+
// cannot overlap that work with a second preparation phase.
|
|
97
|
+
this.#state = { kind: "failed" };
|
|
98
|
+
return readinessFailure(requestId, "prepareFailed", error);
|
|
99
|
+
}
|
|
100
|
+
this.#state = { ...this.#state, kind: "ready" };
|
|
101
|
+
return response(requestId, { type: "prepared", evaluationStart });
|
|
102
|
+
}
|
|
103
|
+
async #seek(requestId, frame) {
|
|
104
|
+
if (this.#state.kind !== "ready") {
|
|
105
|
+
return invalidRequest(requestId, "seek requires a prepared browser session");
|
|
106
|
+
}
|
|
107
|
+
if (frame < this.#state.evaluationStart ||
|
|
108
|
+
frame >= this.#state.evaluationEnd) {
|
|
109
|
+
return invalidRequest(requestId, "seek frame falls outside the evaluation interval");
|
|
110
|
+
}
|
|
111
|
+
try {
|
|
112
|
+
await this.#adapter.seek(runtimeFrameAt(frame, this.#state.frameRate));
|
|
113
|
+
}
|
|
114
|
+
catch (error) {
|
|
115
|
+
// A failed adapter seek may have partially changed author-owned DOM or
|
|
116
|
+
// effect state. Only terminal disposal is safe after that boundary.
|
|
117
|
+
this.#state = { kind: "failed" };
|
|
118
|
+
return readinessFailure(requestId, "seekFailed", error);
|
|
119
|
+
}
|
|
120
|
+
this.#state = { ...this.#state, kind: "staged", frame };
|
|
121
|
+
return response(requestId, { type: "frameStaged", frame });
|
|
122
|
+
}
|
|
123
|
+
async #confirm(requestId, frame) {
|
|
124
|
+
if (this.#state.kind !== "staged") {
|
|
125
|
+
return invalidRequest(requestId, "confirm requires a staged browser frame");
|
|
126
|
+
}
|
|
127
|
+
if (frame !== this.#state.frame) {
|
|
128
|
+
return invalidRequest(requestId, "confirm must use the staged frame");
|
|
129
|
+
}
|
|
130
|
+
try {
|
|
131
|
+
await this.#adapter.confirm(runtimeFrameAt(frame, this.#state.frameRate));
|
|
132
|
+
}
|
|
133
|
+
catch (error) {
|
|
134
|
+
// Confirmation consumes staged media observers and may succeed for only
|
|
135
|
+
// a prefix of videos. Retrying cannot reconstruct that transaction.
|
|
136
|
+
this.#state = { kind: "failed" };
|
|
137
|
+
return readinessFailure(requestId, "confirmFailed", error);
|
|
138
|
+
}
|
|
139
|
+
this.#state = { ...this.#state, kind: "ready" };
|
|
140
|
+
return response(requestId, { type: "frameReady", frame });
|
|
141
|
+
}
|
|
142
|
+
async #dispose(requestId) {
|
|
143
|
+
if (this.#state.kind === "disposed") {
|
|
144
|
+
return invalidRequest(requestId, "browser session is already disposed");
|
|
145
|
+
}
|
|
146
|
+
// A disposal attempt is terminal even when browser cleanup reports an
|
|
147
|
+
// error; partially released resources must never be used again.
|
|
148
|
+
this.#state = { kind: "disposed" };
|
|
149
|
+
try {
|
|
150
|
+
await this.#adapter.dispose();
|
|
151
|
+
return response(requestId, { type: "disposed" });
|
|
152
|
+
}
|
|
153
|
+
catch (error) {
|
|
154
|
+
return operationFailure(requestId, "internal", error);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
function response(requestId, event) {
|
|
159
|
+
return decodeBrowserResponse({ version: 1, requestId, event });
|
|
160
|
+
}
|
|
161
|
+
function invalidRequest(requestId, message) {
|
|
162
|
+
return response(requestId, {
|
|
163
|
+
type: "failed",
|
|
164
|
+
code: "invalidRequest",
|
|
165
|
+
message,
|
|
166
|
+
pendingResources: [],
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
function operationFailure(requestId, operationCode, error) {
|
|
170
|
+
if (!(error instanceof RuntimeAdapterError)) {
|
|
171
|
+
return response(requestId, {
|
|
172
|
+
type: "failed",
|
|
173
|
+
code: "internal",
|
|
174
|
+
message: "runtime adapter threw an untyped error",
|
|
175
|
+
pendingResources: [],
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
return response(requestId, {
|
|
179
|
+
type: "failed",
|
|
180
|
+
code: operationCode,
|
|
181
|
+
message: error.message,
|
|
182
|
+
pendingResources: [...error.pendingResources],
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
function readinessFailure(requestId, operationCode, error) {
|
|
186
|
+
if (error instanceof RuntimeAdapterError &&
|
|
187
|
+
error.kind === "readinessTimeout") {
|
|
188
|
+
return response(requestId, {
|
|
189
|
+
type: "failed",
|
|
190
|
+
code: "readinessTimeout",
|
|
191
|
+
message: error.message,
|
|
192
|
+
pendingResources: [...error.pendingResources],
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
return operationFailure(requestId, operationCode, error);
|
|
196
|
+
}
|
|
197
|
+
function planViolation(plan) {
|
|
198
|
+
if (plan.timeline.start >= plan.timeline.end) {
|
|
199
|
+
return "plan timeline interval is empty or reversed";
|
|
200
|
+
}
|
|
201
|
+
if (plan.evaluation.start > plan.evaluation.end) {
|
|
202
|
+
return "plan evaluation interval is reversed";
|
|
203
|
+
}
|
|
204
|
+
if (!insideInterval(plan.evaluation, plan.timeline)) {
|
|
205
|
+
return "plan evaluation interval falls outside timeline";
|
|
206
|
+
}
|
|
207
|
+
if (plan.output.start >= plan.output.end) {
|
|
208
|
+
return "plan output interval is empty or reversed";
|
|
209
|
+
}
|
|
210
|
+
if (plan.output.start < plan.evaluation.start ||
|
|
211
|
+
plan.output.end > plan.evaluation.end) {
|
|
212
|
+
return "plan output interval falls outside evaluation";
|
|
213
|
+
}
|
|
214
|
+
if (!hasCanonicalNodeOrder(plan.scenes) ||
|
|
215
|
+
!hasCanonicalNodeOrder(plan.shots) ||
|
|
216
|
+
!hasCanonicalNodeOrder(plan.videos) ||
|
|
217
|
+
!hasCanonicalNodeOrder(plan.overlays)) {
|
|
218
|
+
return "plan node collection is not in canonical order";
|
|
219
|
+
}
|
|
220
|
+
const nodeIds = new Set();
|
|
221
|
+
const authoredIds = new Set();
|
|
222
|
+
const filmViolation = claimNode(plan.film, nodeIds, authoredIds);
|
|
223
|
+
if (filmViolation !== undefined) {
|
|
224
|
+
return filmViolation;
|
|
225
|
+
}
|
|
226
|
+
const sceneIntervals = new Map();
|
|
227
|
+
for (const scene of plan.scenes) {
|
|
228
|
+
const nodeViolation = claimNode(scene.node, nodeIds, authoredIds);
|
|
229
|
+
if (nodeViolation !== undefined) {
|
|
230
|
+
return nodeViolation;
|
|
231
|
+
}
|
|
232
|
+
if (!validPlacement(scene.interval, plan.timeline, plan.evaluation)) {
|
|
233
|
+
return "plan scene interval is invalid for this evaluation";
|
|
234
|
+
}
|
|
235
|
+
sceneIntervals.set(scene.node.nodeId, scene.interval);
|
|
236
|
+
}
|
|
237
|
+
const shotIntervals = new Map();
|
|
238
|
+
for (const shot of plan.shots) {
|
|
239
|
+
const nodeViolation = claimNode(shot.node, nodeIds, authoredIds);
|
|
240
|
+
if (nodeViolation !== undefined) {
|
|
241
|
+
return nodeViolation;
|
|
242
|
+
}
|
|
243
|
+
const sceneInterval = sceneIntervals.get(shot.sceneId);
|
|
244
|
+
if (sceneInterval === undefined) {
|
|
245
|
+
return "plan shot names an unknown scene";
|
|
246
|
+
}
|
|
247
|
+
if (!validPlacement(shot.interval, plan.timeline, plan.evaluation)) {
|
|
248
|
+
return "plan shot interval is invalid for this evaluation";
|
|
249
|
+
}
|
|
250
|
+
if (!insideInterval(shot.interval, sceneInterval)) {
|
|
251
|
+
return "plan shot interval falls outside its scene";
|
|
252
|
+
}
|
|
253
|
+
shotIntervals.set(shot.node.nodeId, shot.interval);
|
|
254
|
+
}
|
|
255
|
+
for (const video of plan.videos) {
|
|
256
|
+
const nodeViolation = claimNode(video.node, nodeIds, authoredIds);
|
|
257
|
+
if (nodeViolation !== undefined) {
|
|
258
|
+
return nodeViolation;
|
|
259
|
+
}
|
|
260
|
+
const shotInterval = shotIntervals.get(video.shotId);
|
|
261
|
+
if (shotInterval === undefined) {
|
|
262
|
+
return "plan video names an unknown shot";
|
|
263
|
+
}
|
|
264
|
+
if (!insideEvaluation(video.interval, plan.evaluation)) {
|
|
265
|
+
return "plan video interval falls outside evaluation";
|
|
266
|
+
}
|
|
267
|
+
if (!insideInterval(video.interval, shotInterval)) {
|
|
268
|
+
return "plan video interval falls outside its shot";
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
for (const overlay of plan.overlays) {
|
|
272
|
+
const nodeViolation = claimNode(overlay.node, nodeIds, authoredIds);
|
|
273
|
+
if (nodeViolation !== undefined) {
|
|
274
|
+
return nodeViolation;
|
|
275
|
+
}
|
|
276
|
+
const parentViolation = overlayParentViolation(overlay, shotIntervals);
|
|
277
|
+
if (parentViolation !== undefined) {
|
|
278
|
+
return parentViolation;
|
|
279
|
+
}
|
|
280
|
+
if (!validPlacement(overlay.interval, plan.timeline, plan.evaluation)) {
|
|
281
|
+
return "plan overlay interval is invalid for this evaluation";
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
if (!hasDenseNodeIdentity(nodeIds)) {
|
|
285
|
+
return "plan node identity is not dense";
|
|
286
|
+
}
|
|
287
|
+
return undefined;
|
|
288
|
+
}
|
|
289
|
+
function hasDenseNodeIdentity(identities) {
|
|
290
|
+
for (let expected = 0; expected < identities.size; expected += 1) {
|
|
291
|
+
if (!identities.has(expected)) {
|
|
292
|
+
return false;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
return true;
|
|
296
|
+
}
|
|
297
|
+
function hasCanonicalNodeOrder(entries) {
|
|
298
|
+
let previous;
|
|
299
|
+
for (const { node } of entries) {
|
|
300
|
+
if (previous !== undefined && node.nodeId <= previous) {
|
|
301
|
+
return false;
|
|
302
|
+
}
|
|
303
|
+
previous = node.nodeId;
|
|
304
|
+
}
|
|
305
|
+
return true;
|
|
306
|
+
}
|
|
307
|
+
function overlayParentViolation(overlay, shotIntervals) {
|
|
308
|
+
if (overlay.kind === "caption") {
|
|
309
|
+
return overlay.shotId === undefined || overlay.shotId === null
|
|
310
|
+
? undefined
|
|
311
|
+
: "plan caption names a structural parent";
|
|
312
|
+
}
|
|
313
|
+
if (overlay.shotId === undefined || overlay.shotId === null) {
|
|
314
|
+
return "plan overlay names an unknown shot";
|
|
315
|
+
}
|
|
316
|
+
const shotInterval = shotIntervals.get(overlay.shotId);
|
|
317
|
+
if (shotInterval === undefined) {
|
|
318
|
+
return "plan overlay names an unknown shot";
|
|
319
|
+
}
|
|
320
|
+
if (!insideInterval(overlay.interval, shotInterval)) {
|
|
321
|
+
return "plan overlay interval falls outside its shot";
|
|
322
|
+
}
|
|
323
|
+
return undefined;
|
|
324
|
+
}
|
|
325
|
+
function insideInterval(interval, parent) {
|
|
326
|
+
return interval.start >= parent.start && interval.end <= parent.end;
|
|
327
|
+
}
|
|
328
|
+
function claimNode(node, identities, authoredIdentities) {
|
|
329
|
+
if (identities.has(node.nodeId)) {
|
|
330
|
+
return "plan node identity is duplicated";
|
|
331
|
+
}
|
|
332
|
+
identities.add(node.nodeId);
|
|
333
|
+
if (node.authoredId === undefined || node.authoredId === null) {
|
|
334
|
+
return undefined;
|
|
335
|
+
}
|
|
336
|
+
if (node.authoredId.length === 0 || /[\t\n\f\r ]/u.test(node.authoredId)) {
|
|
337
|
+
return "plan authored node identity is invalid";
|
|
338
|
+
}
|
|
339
|
+
if (authoredIdentities.has(node.authoredId)) {
|
|
340
|
+
return "plan authored node identity is duplicated";
|
|
341
|
+
}
|
|
342
|
+
authoredIdentities.add(node.authoredId);
|
|
343
|
+
return undefined;
|
|
344
|
+
}
|
|
345
|
+
function insideEvaluation(interval, evaluation) {
|
|
346
|
+
return (interval.start < interval.end &&
|
|
347
|
+
interval.start >= evaluation.start &&
|
|
348
|
+
interval.end <= evaluation.end);
|
|
349
|
+
}
|
|
350
|
+
function validPlacement(interval, timeline, evaluation) {
|
|
351
|
+
return (interval.start < interval.end &&
|
|
352
|
+
insideInterval(interval, timeline) &&
|
|
353
|
+
intervalsIntersect(interval, evaluation));
|
|
354
|
+
}
|
|
355
|
+
function intervalsIntersect(left, right) {
|
|
356
|
+
return left.start < right.end && right.start < left.end;
|
|
357
|
+
}
|
|
358
|
+
function snapshotPlan(plan) {
|
|
359
|
+
// Listing every generated field makes a schema addition fail compilation
|
|
360
|
+
// instead of silently falling outside the immutable adapter snapshot.
|
|
361
|
+
const frameRate = Object.freeze({ ...plan.frameRate });
|
|
362
|
+
const timeline = Object.freeze({ ...plan.timeline });
|
|
363
|
+
const evaluation = Object.freeze({ ...plan.evaluation });
|
|
364
|
+
const output = Object.freeze({ ...plan.output });
|
|
365
|
+
const film = snapshotNode(plan.film);
|
|
366
|
+
const scenes = Object.freeze(plan.scenes.map(snapshotScene));
|
|
367
|
+
const shots = Object.freeze(plan.shots.map(snapshotShot));
|
|
368
|
+
const videos = Object.freeze(plan.videos.map(snapshotVideo));
|
|
369
|
+
const overlays = Object.freeze(plan.overlays.map(snapshotOverlay));
|
|
370
|
+
return Object.freeze({
|
|
371
|
+
timelineVersion: plan.timelineVersion,
|
|
372
|
+
frameRate,
|
|
373
|
+
timeline,
|
|
374
|
+
evaluation,
|
|
375
|
+
output,
|
|
376
|
+
film,
|
|
377
|
+
scenes,
|
|
378
|
+
shots,
|
|
379
|
+
videos,
|
|
380
|
+
overlays,
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
function snapshotScene(scene) {
|
|
384
|
+
return Object.freeze({
|
|
385
|
+
node: snapshotNode(scene.node),
|
|
386
|
+
interval: Object.freeze({ ...scene.interval }),
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
function snapshotShot(shot) {
|
|
390
|
+
return Object.freeze({
|
|
391
|
+
node: snapshotNode(shot.node),
|
|
392
|
+
sceneId: shot.sceneId,
|
|
393
|
+
interval: Object.freeze({ ...shot.interval }),
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
function snapshotVideo(video) {
|
|
397
|
+
return Object.freeze({
|
|
398
|
+
node: snapshotNode(video.node),
|
|
399
|
+
shotId: video.shotId,
|
|
400
|
+
assetId: video.assetId,
|
|
401
|
+
interval: Object.freeze({ ...video.interval }),
|
|
402
|
+
sourceFrameRate: Object.freeze({ ...video.sourceFrameRate }),
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
function snapshotOverlay(overlay) {
|
|
406
|
+
return Object.freeze({
|
|
407
|
+
node: snapshotNode(overlay.node),
|
|
408
|
+
shotId: overlay.shotId ?? null,
|
|
409
|
+
kind: overlay.kind,
|
|
410
|
+
text: overlay.text,
|
|
411
|
+
interval: Object.freeze({ ...overlay.interval }),
|
|
412
|
+
});
|
|
413
|
+
}
|
|
414
|
+
function snapshotNode(node) {
|
|
415
|
+
return Object.freeze({
|
|
416
|
+
nodeId: node.nodeId,
|
|
417
|
+
authoredId: node.authoredId ?? null,
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
function loadedState(plan) {
|
|
421
|
+
return {
|
|
422
|
+
kind: "loaded",
|
|
423
|
+
evaluationStart: plan.evaluation.start,
|
|
424
|
+
evaluationEnd: plan.evaluation.end,
|
|
425
|
+
frameRate: plan.frameRate,
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
function boundedPendingResources(resources) {
|
|
429
|
+
if (resources.length > MAX_PENDING_RESOURCES) {
|
|
430
|
+
throw new TypeError("adapter failure has too many pending resources");
|
|
431
|
+
}
|
|
432
|
+
return resources.map((resource) => boundedText(resource, "pending resource", MAX_PENDING_RESOURCE_CHARACTERS));
|
|
433
|
+
}
|
|
434
|
+
function boundedText(value, role, limit) {
|
|
435
|
+
const text = nonBlank(value, role);
|
|
436
|
+
let characters = 0;
|
|
437
|
+
for (const _character of text) {
|
|
438
|
+
characters += 1;
|
|
439
|
+
if (characters > limit) {
|
|
440
|
+
throw new TypeError(`${role} exceeds the protocol character limit`);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
return text;
|
|
444
|
+
}
|
|
445
|
+
function nonBlank(value, role) {
|
|
446
|
+
if (value.trim().length === 0) {
|
|
447
|
+
throw new TypeError(`${role} cannot be blank`);
|
|
448
|
+
}
|
|
449
|
+
return value;
|
|
450
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export type { RuntimeFrame } from "./clock.js";
|
|
2
|
+
export type { RuntimeVideo } from "./media.js";
|
|
3
|
+
export type { PresentationFrameBehavior, PresentationTemporalCapability, PresentationVisualCapability, } from "./generated/bundle-layout.js";
|
|
4
|
+
export type { RuntimePlan } from "./session.js";
|
|
5
|
+
export type { ContainerPresentation, FrameEffect, OverlayPresentation, PresentationBindings, PresentationExtensions, RuntimeOverlay, RuntimeNode, RuntimeScene, RuntimeShot, VideoPresentation, } from "./presentation.js";
|
|
6
|
+
export type { PresentationResource, PresentationResourceKind, } from "./resource.js";
|