versioncam 0.2.0 → 0.3.0
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/CHANGELOG.md +50 -0
- package/README.md +36 -2
- package/dist/cli/commands/check.js +45 -4
- package/dist/cli/commands/check.js.map +1 -1
- package/dist/cli/commands/publish.d.ts +17 -0
- package/dist/cli/commands/publish.js +203 -0
- package/dist/cli/commands/publish.js.map +1 -0
- package/dist/cli/commands/record.js +2 -0
- package/dist/cli/commands/record.js.map +1 -1
- package/dist/cli/commands/render.js +22 -2
- package/dist/cli/commands/render.js.map +1 -1
- package/dist/cli/main.js +8 -0
- package/dist/cli/main.js.map +1 -1
- package/dist/cli/usage.d.ts +1 -1
- package/dist/cli/usage.js +8 -0
- package/dist/cli/usage.js.map +1 -1
- package/dist/config.d.ts +11 -1
- package/dist/config.js +19 -0
- package/dist/config.js.map +1 -1
- package/dist/driver/clip.d.ts +6 -0
- package/dist/driver/clip.js +6 -3
- package/dist/driver/clip.js.map +1 -1
- package/dist/driver/reports.d.ts +34 -2
- package/dist/driver/reports.js +31 -3
- package/dist/driver/reports.js.map +1 -1
- package/dist/driver/session.d.ts +14 -0
- package/dist/driver/session.js +24 -2
- package/dist/driver/session.js.map +1 -1
- package/dist/index.d.ts +9 -1
- package/dist/index.js +5 -1
- package/dist/index.js.map +1 -1
- package/dist/publish/client.d.ts +38 -0
- package/dist/publish/client.js +118 -0
- package/dist/publish/client.js.map +1 -0
- package/dist/publish/index.d.ts +12 -0
- package/dist/publish/index.js +11 -0
- package/dist/publish/index.js.map +1 -0
- package/dist/publish/manifest.d.ts +46 -0
- package/dist/publish/manifest.js +133 -0
- package/dist/publish/manifest.js.map +1 -0
- package/dist/publish/protocol.d.ts +203 -0
- package/dist/publish/protocol.js +56 -0
- package/dist/publish/protocol.js.map +1 -0
- package/dist/render/encode.d.ts +15 -0
- package/dist/render/encode.js +23 -4
- package/dist/render/encode.js.map +1 -1
- package/dist/render/render.js +1 -0
- package/dist/render/render.js.map +1 -1
- package/dist/render/sequence.d.ts +3 -1
- package/dist/render/sequence.js +4 -1
- package/dist/render/sequence.js.map +1 -1
- package/package.json +6 -2
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAgBxE,OAAO,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,iBAAiB,CAAC;AAGrD,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,kBAAkB,CAAC;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAgBxE,OAAO,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,iBAAiB,CAAC;AAGrD,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AASnE,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAG9C,OAAO,EAAE,aAAa,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAC;AACvE,OAAO,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAGhD,OAAO,EAAE,IAAI,EAAE,gBAAgB,EAAE,mBAAmB,EAAE,MAAM,gBAAgB,CAAC;AAE7E,OAAO,EAAE,OAAO,EAAE,MAAM,eAAe,CAAC;AAExC,OAAO,EAAE,MAAM,EAAE,MAAM,2BAA2B,CAAC;AAenD,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAChF,OAAO,EACL,YAAY,EACZ,WAAW,EACX,eAAe,EACf,WAAW,EACX,WAAW,EACX,YAAY,GACb,MAAM,oBAAoB,CAAC;AAE5B,OAAO,EACL,SAAS,EACT,UAAU,EACV,WAAW,EACX,YAAY,EACZ,aAAa,EACb,WAAW,GACZ,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EACL,iBAAiB,EACjB,WAAW,EACX,aAAa,EACb,WAAW,GACZ,MAAM,qBAAqB,CAAC;AAE7B,OAAO,EACL,YAAY,EACZ,eAAe,EACf,cAAc,GACf,MAAM,oBAAoB,CAAC;AAQ5B,OAAO,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AACrD,OAAO,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAEtD,OAAO,EAAE,iBAAiB,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAExE,OAAO,EACL,qBAAqB,EACrB,QAAQ,EACR,OAAO,EACP,SAAS,GACV,MAAM,uBAAuB,CAAC;AAE/B,cAAc,uBAAuB,CAAC"}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { type PublishBegin, type PublishBeginResponse, type PublishCommitResponse } from "./protocol.js";
|
|
2
|
+
/**
|
|
3
|
+
* The client half of the publish protocol: begin, upload what the service
|
|
4
|
+
* does not have yet, commit.
|
|
5
|
+
*
|
|
6
|
+
* It knows nothing about recordings on disk. `files` hands it bytes on
|
|
7
|
+
* request, which keeps it testable against a fake `fetch` and keeps a state
|
|
8
|
+
* the service already has from ever being read off the disk.
|
|
9
|
+
*/
|
|
10
|
+
/** Bytes for what the service asks for, looked up lazily. */
|
|
11
|
+
export type PublishFiles = {
|
|
12
|
+
state(hash: string): Promise<Uint8Array>;
|
|
13
|
+
timeline(clip: string): Promise<Uint8Array>;
|
|
14
|
+
targets(clip: string): Promise<Uint8Array>;
|
|
15
|
+
failure(clip: string): Promise<Uint8Array>;
|
|
16
|
+
};
|
|
17
|
+
export type PublishOptions = {
|
|
18
|
+
/** The API's origin, e.g. https://api.version.cam. */
|
|
19
|
+
api: string;
|
|
20
|
+
/** A GitHub OpenID Connect token, or a `vc_` project token. */
|
|
21
|
+
credential: string;
|
|
22
|
+
fetch?: typeof fetch;
|
|
23
|
+
/** How many uploads run at once. */
|
|
24
|
+
concurrency?: number;
|
|
25
|
+
/** Waits between retries, in milliseconds; the last one is the last try. */
|
|
26
|
+
backoffMs?: readonly number[];
|
|
27
|
+
log?: (line: string) => void;
|
|
28
|
+
};
|
|
29
|
+
/** An answer from the service that no retry will change. */
|
|
30
|
+
export declare class PublishRefused extends Error {
|
|
31
|
+
readonly status: number;
|
|
32
|
+
readonly code: string;
|
|
33
|
+
constructor(message: string, status: number, code: string);
|
|
34
|
+
}
|
|
35
|
+
export declare function publishRecordings(begin: PublishBegin, files: PublishFiles, options: PublishOptions): Promise<{
|
|
36
|
+
begun: PublishBeginResponse;
|
|
37
|
+
committed: PublishCommitResponse;
|
|
38
|
+
}>;
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { PUBLISH_PATHS, } from "./protocol.js";
|
|
2
|
+
/** An answer from the service that no retry will change. */
|
|
3
|
+
export class PublishRefused extends Error {
|
|
4
|
+
status;
|
|
5
|
+
code;
|
|
6
|
+
constructor(message, status, code) {
|
|
7
|
+
super(message);
|
|
8
|
+
this.status = status;
|
|
9
|
+
this.code = code;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
export async function publishRecordings(begin, files, options) {
|
|
13
|
+
const call = caller(options);
|
|
14
|
+
const begun = (await call("POST", PUBLISH_PATHS.begin, {
|
|
15
|
+
body: JSON.stringify(begin),
|
|
16
|
+
type: "application/json",
|
|
17
|
+
}));
|
|
18
|
+
const uploads = [
|
|
19
|
+
...begun.needStates.map((hash) => async () => call("PUT", PUBLISH_PATHS.state(begun.publishId, hash), {
|
|
20
|
+
body: await files.state(hash),
|
|
21
|
+
type: "image/png",
|
|
22
|
+
})),
|
|
23
|
+
...begun.needTimelines.map((clip) => async () => call("PUT", PUBLISH_PATHS.timeline(begun.publishId, clip), {
|
|
24
|
+
body: await files.timeline(clip),
|
|
25
|
+
type: "application/json",
|
|
26
|
+
})),
|
|
27
|
+
...begun.needTargets.map((clip) => async () => call("PUT", PUBLISH_PATHS.targets(begun.publishId, clip), {
|
|
28
|
+
body: await files.targets(clip),
|
|
29
|
+
type: "application/json",
|
|
30
|
+
})),
|
|
31
|
+
...begun.needFailures.map((clip) => async () => call("PUT", PUBLISH_PATHS.failure(begun.publishId, clip), {
|
|
32
|
+
body: await files.failure(clip),
|
|
33
|
+
type: "image/png",
|
|
34
|
+
})),
|
|
35
|
+
];
|
|
36
|
+
options.log?.(` uploading ${begun.needStates.length} new state(s) of ` +
|
|
37
|
+
`${begin.clips.reduce((n, c) => n + (c.recording?.states.length ?? 0), 0)}`);
|
|
38
|
+
await pool(uploads, options.concurrency ?? 6);
|
|
39
|
+
const committed = (await call("POST", PUBLISH_PATHS.commit(begun.publishId), {
|
|
40
|
+
body: "{}",
|
|
41
|
+
type: "application/json",
|
|
42
|
+
}));
|
|
43
|
+
return { begun, committed };
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* One request, retried on what a retry can fix: a network error, a 429 or a
|
|
47
|
+
* 5xx. Anything else is the service's considered answer, and its sentence is
|
|
48
|
+
* passed on unchanged, because it names the limit or the problem better than
|
|
49
|
+
* anything the client could add.
|
|
50
|
+
*/
|
|
51
|
+
function caller(options) {
|
|
52
|
+
const doFetch = options.fetch ?? fetch;
|
|
53
|
+
const backoff = options.backoffMs ?? [1000, 2000, 4000];
|
|
54
|
+
const origin = options.api.replace(/\/+$/, "");
|
|
55
|
+
return async (method, path, payload) => {
|
|
56
|
+
let lastError;
|
|
57
|
+
for (let attempt = 0; attempt <= backoff.length; attempt += 1) {
|
|
58
|
+
let response;
|
|
59
|
+
try {
|
|
60
|
+
response = await doFetch(`${origin}${path}`, {
|
|
61
|
+
method,
|
|
62
|
+
headers: {
|
|
63
|
+
authorization: `Bearer ${options.credential}`,
|
|
64
|
+
"content-type": payload.type,
|
|
65
|
+
},
|
|
66
|
+
// A copy for a byte body: fetch wants an ArrayBuffer-backed view,
|
|
67
|
+
// and a Buffer read from disk may sit in a shared pool.
|
|
68
|
+
body: typeof payload.body === "string"
|
|
69
|
+
? payload.body
|
|
70
|
+
: new Uint8Array(payload.body),
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
catch (error) {
|
|
74
|
+
lastError = error;
|
|
75
|
+
if (attempt < backoff.length)
|
|
76
|
+
await sleep(backoff[attempt]);
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
if (response.ok) {
|
|
80
|
+
const text = await response.text();
|
|
81
|
+
return text ? JSON.parse(text) : null;
|
|
82
|
+
}
|
|
83
|
+
if (response.status === 429 || response.status >= 500) {
|
|
84
|
+
lastError = new Error(`${method} ${path}: ${response.status} ${await response.text()}`);
|
|
85
|
+
if (attempt < backoff.length)
|
|
86
|
+
await sleep(backoff[attempt]);
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
const body = await response.text();
|
|
90
|
+
let parsed = null;
|
|
91
|
+
try {
|
|
92
|
+
parsed = JSON.parse(body);
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
// Not the service's JSON: a proxy or a misconfigured API address.
|
|
96
|
+
}
|
|
97
|
+
throw new PublishRefused(parsed?.error ??
|
|
98
|
+
`${method} ${path} answered ${response.status}: ${body.slice(0, 300)}`, response.status, parsed?.code ?? "http");
|
|
99
|
+
}
|
|
100
|
+
throw new Error(`version.cam did not answer after ${backoff.length + 1} tries: ${lastError?.message ?? lastError}`);
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
/** Run tasks with at most `size` in flight; the first failure fails the lot. */
|
|
104
|
+
async function pool(tasks, size) {
|
|
105
|
+
let next = 0;
|
|
106
|
+
const workers = Array.from({ length: Math.min(size, tasks.length) }, async () => {
|
|
107
|
+
while (next < tasks.length) {
|
|
108
|
+
const task = tasks[next];
|
|
109
|
+
next += 1;
|
|
110
|
+
await task();
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
await Promise.all(workers);
|
|
114
|
+
}
|
|
115
|
+
function sleep(ms) {
|
|
116
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
117
|
+
}
|
|
118
|
+
//# sourceMappingURL=client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.js","sourceRoot":"","sources":["../../src/publish/client.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `versioncam/publish`: what the version.cam service imports.
|
|
3
|
+
*
|
|
4
|
+
* Nothing reachable from here may import Node, Playwright or ffmpeg: the
|
|
5
|
+
* service runs on Cloudflare Workers, and one stray import of the driver
|
|
6
|
+
* would pull a browser into its bundle. The recorder's own publish code
|
|
7
|
+
* (manifests, the client) lives beside this file and is not re-exported.
|
|
8
|
+
*/
|
|
9
|
+
export * from "./protocol.js";
|
|
10
|
+
export { validateTimeline } from "../core/timeline.js";
|
|
11
|
+
export type { Timeline, Step, Rect, Mark, Caption } from "../core/timeline.js";
|
|
12
|
+
export type { Motion, Presentation } from "../config.js";
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `versioncam/publish`: what the version.cam service imports.
|
|
3
|
+
*
|
|
4
|
+
* Nothing reachable from here may import Node, Playwright or ffmpeg: the
|
|
5
|
+
* service runs on Cloudflare Workers, and one stray import of the driver
|
|
6
|
+
* would pull a browser into its bundle. The recorder's own publish code
|
|
7
|
+
* (manifests, the client) lives beside this file and is not re-exported.
|
|
8
|
+
*/
|
|
9
|
+
export * from "./protocol.js";
|
|
10
|
+
export { validateTimeline } from "../core/timeline.js";
|
|
11
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/publish/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,cAAc,eAAe,CAAC;AAC9B,OAAO,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { ResolvedConfig } from "../config.js";
|
|
2
|
+
import { type ClipManifest, type PublishedRenderConfig } from "./protocol.js";
|
|
3
|
+
/**
|
|
4
|
+
* A recording directory, read into what a publish says about it.
|
|
5
|
+
*
|
|
6
|
+
* Everything the service is told is computed here from the files, never from
|
|
7
|
+
* the run that made them, so `publish` can run in a later CI step (or a later
|
|
8
|
+
* job) than the recording did and say exactly the same thing.
|
|
9
|
+
*/
|
|
10
|
+
export declare function sha256Hex(bytes: Uint8Array): string;
|
|
11
|
+
/**
|
|
12
|
+
* A PNG's size from its header. The signature is eight bytes and the first
|
|
13
|
+
* chunk is always IHDR, whose data starts with the width and height as
|
|
14
|
+
* big-endian 32-bit integers, so no decoder is needed to tell the service
|
|
15
|
+
* what it is about to receive.
|
|
16
|
+
*/
|
|
17
|
+
export declare function pngSize(bytes: Uint8Array): {
|
|
18
|
+
width: number;
|
|
19
|
+
height: number;
|
|
20
|
+
};
|
|
21
|
+
/** What the service renders with: the render page's config, and its clock. */
|
|
22
|
+
export declare function publishedRenderConfig(config: ResolvedConfig): PublishedRenderConfig;
|
|
23
|
+
/** The files a recording's manifest was computed from, to upload on request. */
|
|
24
|
+
export type RecordingFiles = {
|
|
25
|
+
timeline: Uint8Array;
|
|
26
|
+
targets: Uint8Array | null;
|
|
27
|
+
/** By hash, for the states the service asks for. */
|
|
28
|
+
states: Map<string, string>;
|
|
29
|
+
failure: Uint8Array | null;
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* One clip's manifest, from its recording directory.
|
|
33
|
+
*
|
|
34
|
+
* A directory holding `failure.json` failed its last run: the recorder
|
|
35
|
+
* clears a clip's directory before recording it, so a failure there is the
|
|
36
|
+
* latest word on the clip, and what it recorded before failing is not a
|
|
37
|
+
* recording anyone should see. Otherwise `timeline.json` is a complete
|
|
38
|
+
* recording that passed. With neither, the clip was never recorded here.
|
|
39
|
+
*/
|
|
40
|
+
export declare function readClip(dir: string, clip: {
|
|
41
|
+
id: string;
|
|
42
|
+
title: string;
|
|
43
|
+
}, renderConfig: PublishedRenderConfig): {
|
|
44
|
+
manifest: ClipManifest;
|
|
45
|
+
files: RecordingFiles;
|
|
46
|
+
} | null;
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { renderConfigOf } from "../config.js";
|
|
5
|
+
import { FAILURE_JSON, FAILURE_PNG, TARGETS_FILE, } from "../driver/reports.js";
|
|
6
|
+
import { contentOfTimeline, } from "./protocol.js";
|
|
7
|
+
/**
|
|
8
|
+
* A recording directory, read into what a publish says about it.
|
|
9
|
+
*
|
|
10
|
+
* Everything the service is told is computed here from the files, never from
|
|
11
|
+
* the run that made them, so `publish` can run in a later CI step (or a later
|
|
12
|
+
* job) than the recording did and say exactly the same thing.
|
|
13
|
+
*/
|
|
14
|
+
export function sha256Hex(bytes) {
|
|
15
|
+
return createHash("sha256").update(bytes).digest("hex");
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* A PNG's size from its header. The signature is eight bytes and the first
|
|
19
|
+
* chunk is always IHDR, whose data starts with the width and height as
|
|
20
|
+
* big-endian 32-bit integers, so no decoder is needed to tell the service
|
|
21
|
+
* what it is about to receive.
|
|
22
|
+
*/
|
|
23
|
+
export function pngSize(bytes) {
|
|
24
|
+
const view = Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
25
|
+
const signature = "89504e470d0a1a0a";
|
|
26
|
+
if (view.length < 24 || view.subarray(0, 8).toString("hex") !== signature) {
|
|
27
|
+
throw new Error("not a PNG");
|
|
28
|
+
}
|
|
29
|
+
if (view.subarray(12, 16).toString("latin1") !== "IHDR") {
|
|
30
|
+
throw new Error("a PNG whose first chunk is not IHDR");
|
|
31
|
+
}
|
|
32
|
+
return { width: view.readUInt32BE(16), height: view.readUInt32BE(20) };
|
|
33
|
+
}
|
|
34
|
+
/** What the service renders with: the render page's config, and its clock. */
|
|
35
|
+
export function publishedRenderConfig(config) {
|
|
36
|
+
const { scenes: _scenes, ...render } = renderConfigOf(config);
|
|
37
|
+
return {
|
|
38
|
+
...render,
|
|
39
|
+
clock: {
|
|
40
|
+
timezoneId: config.clock.timezoneId,
|
|
41
|
+
locale: config.clock.locale,
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* One clip's manifest, from its recording directory.
|
|
47
|
+
*
|
|
48
|
+
* A directory holding `failure.json` failed its last run: the recorder
|
|
49
|
+
* clears a clip's directory before recording it, so a failure there is the
|
|
50
|
+
* latest word on the clip, and what it recorded before failing is not a
|
|
51
|
+
* recording anyone should see. Otherwise `timeline.json` is a complete
|
|
52
|
+
* recording that passed. With neither, the clip was never recorded here.
|
|
53
|
+
*/
|
|
54
|
+
export function readClip(dir, clip, renderConfig) {
|
|
55
|
+
const failureJson = join(dir, FAILURE_JSON);
|
|
56
|
+
const timelineFile = join(dir, "timeline.json");
|
|
57
|
+
if (existsSync(failureJson)) {
|
|
58
|
+
const failure = JSON.parse(readFileSync(failureJson, "utf8"));
|
|
59
|
+
const png = join(dir, FAILURE_PNG);
|
|
60
|
+
const picture = existsSync(png) ? readFileSync(png) : null;
|
|
61
|
+
const check = {
|
|
62
|
+
status: "fail",
|
|
63
|
+
message: failure.message,
|
|
64
|
+
step: failure.step ?? null,
|
|
65
|
+
frame: failure.frame,
|
|
66
|
+
t: failure.t,
|
|
67
|
+
};
|
|
68
|
+
return {
|
|
69
|
+
manifest: {
|
|
70
|
+
id: clip.id,
|
|
71
|
+
title: clip.title,
|
|
72
|
+
check,
|
|
73
|
+
recording: null,
|
|
74
|
+
failureHash: picture ? sha256Hex(picture) : null,
|
|
75
|
+
},
|
|
76
|
+
files: {
|
|
77
|
+
timeline: new Uint8Array(),
|
|
78
|
+
targets: null,
|
|
79
|
+
states: new Map(),
|
|
80
|
+
failure: picture,
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
if (!existsSync(timelineFile))
|
|
85
|
+
return null;
|
|
86
|
+
const timelineBytes = readFileSync(timelineFile);
|
|
87
|
+
const timeline = JSON.parse(timelineBytes.toString("utf8"));
|
|
88
|
+
const targetsFile = join(dir, TARGETS_FILE);
|
|
89
|
+
const targets = existsSync(targetsFile) ? readFileSync(targetsFile) : null;
|
|
90
|
+
const statePaths = new Map();
|
|
91
|
+
const states = timeline.states.map((name) => {
|
|
92
|
+
const path = join(dir, "states", name);
|
|
93
|
+
const bytes = readFileSync(path);
|
|
94
|
+
const hash = sha256Hex(bytes);
|
|
95
|
+
statePaths.set(hash, path);
|
|
96
|
+
return { hash, bytes: bytes.length, ...pngSize(bytes) };
|
|
97
|
+
});
|
|
98
|
+
const targetsHash = targets ? sha256Hex(targets) : null;
|
|
99
|
+
const contentHash = sha256Hex(Buffer.from(JSON.stringify({
|
|
100
|
+
timeline: contentOfTimeline(timeline),
|
|
101
|
+
states: states.map((s) => s.hash),
|
|
102
|
+
targets: targetsHash,
|
|
103
|
+
renderConfig,
|
|
104
|
+
})));
|
|
105
|
+
const recording = {
|
|
106
|
+
contentHash,
|
|
107
|
+
timelineHash: sha256Hex(timelineBytes),
|
|
108
|
+
timelineBytes: timelineBytes.length,
|
|
109
|
+
targetsHash,
|
|
110
|
+
draft: timeline.meta.draft === true,
|
|
111
|
+
fps: timeline.fps,
|
|
112
|
+
durationFrames: timeline.durationFrames,
|
|
113
|
+
viewport: timeline.viewport,
|
|
114
|
+
states,
|
|
115
|
+
renderConfig,
|
|
116
|
+
};
|
|
117
|
+
return {
|
|
118
|
+
manifest: {
|
|
119
|
+
id: clip.id,
|
|
120
|
+
title: clip.title || timeline.meta.title,
|
|
121
|
+
check: { status: "pass" },
|
|
122
|
+
recording,
|
|
123
|
+
failureHash: null,
|
|
124
|
+
},
|
|
125
|
+
files: {
|
|
126
|
+
timeline: timelineBytes,
|
|
127
|
+
targets,
|
|
128
|
+
states: statePaths,
|
|
129
|
+
failure: null,
|
|
130
|
+
},
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
//# sourceMappingURL=manifest.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"manifest.js","sourceRoot":"","sources":["../../src/publish/manifest.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The publish protocol: what `versioncam publish` sends to version.cam, and
|
|
3
|
+
* what comes back.
|
|
4
|
+
*
|
|
5
|
+
* Both ends import this file. The recorder builds a publish from it and the
|
|
6
|
+
* service validates one against it, through `versioncam/publish`, an entry
|
|
7
|
+
* that pulls in nothing that needs Node. A field added on one side is a type
|
|
8
|
+
* error on the other until both agree, which is the only kind of protocol
|
|
9
|
+
* drift that cannot reach production.
|
|
10
|
+
*
|
|
11
|
+
* The shape follows what a recording is on disk. A recording is a timeline
|
|
12
|
+
* plus a directory of distinct states, so a publish is a timeline plus the
|
|
13
|
+
* states' content hashes: the service asks for the states it has never seen,
|
|
14
|
+
* and a version that changed one screen uploads one screen.
|
|
15
|
+
*/
|
|
16
|
+
/** Bumped when either side could misread the other. */
|
|
17
|
+
export declare const PUBLISH_PROTOCOL = 1;
|
|
18
|
+
/** Where the service is, unless `VERSIONCAM_API` says otherwise. */
|
|
19
|
+
export declare const DEFAULT_API = "https://api.version.cam";
|
|
20
|
+
/**
|
|
21
|
+
* The audience a GitHub OpenID Connect token must be minted for. The Action
|
|
22
|
+
* asks for exactly this, and the service refuses any other, so a token
|
|
23
|
+
* minted for some other service cannot be replayed here.
|
|
24
|
+
*/
|
|
25
|
+
export declare const OIDC_AUDIENCE = "version.cam";
|
|
26
|
+
/**
|
|
27
|
+
* Limits that hold on every plan. The service enforces them; they are here so
|
|
28
|
+
* the client can say which one a recording breaks before uploading anything.
|
|
29
|
+
*/
|
|
30
|
+
export declare const HARD_CAPS: {
|
|
31
|
+
readonly statesPerVersion: 400;
|
|
32
|
+
readonly stateBytes: number;
|
|
33
|
+
readonly timelineBytes: number;
|
|
34
|
+
readonly targetsBytes: number;
|
|
35
|
+
readonly failureBytes: number;
|
|
36
|
+
readonly publishesPerHour: 20;
|
|
37
|
+
};
|
|
38
|
+
/** The step that was running when a clip failed: its index and its call. */
|
|
39
|
+
export type FailedStep = {
|
|
40
|
+
index: number;
|
|
41
|
+
call: string;
|
|
42
|
+
};
|
|
43
|
+
/** What the last run of a clip said about it. */
|
|
44
|
+
export type CheckResult = {
|
|
45
|
+
status: "pass";
|
|
46
|
+
} | {
|
|
47
|
+
status: "fail";
|
|
48
|
+
message: string;
|
|
49
|
+
step: FailedStep | null;
|
|
50
|
+
frame: number;
|
|
51
|
+
t: number;
|
|
52
|
+
};
|
|
53
|
+
/** One distinct state: a PNG, by the SHA-256 of its bytes. */
|
|
54
|
+
export type StateRef = {
|
|
55
|
+
hash: string;
|
|
56
|
+
bytes: number;
|
|
57
|
+
width: number;
|
|
58
|
+
height: number;
|
|
59
|
+
};
|
|
60
|
+
/**
|
|
61
|
+
* What the service needs to render a recording exactly as `versioncam render`
|
|
62
|
+
* would have: the render page's config without its scenes (a scenes module is
|
|
63
|
+
* the app's own code, and nothing runs an app's code but the app's own CI),
|
|
64
|
+
* and the clock the render page is opened with.
|
|
65
|
+
*/
|
|
66
|
+
export type PublishedRenderConfig = {
|
|
67
|
+
output: {
|
|
68
|
+
width: number;
|
|
69
|
+
height: number;
|
|
70
|
+
};
|
|
71
|
+
theme: {
|
|
72
|
+
accent: string;
|
|
73
|
+
cursorScale: number;
|
|
74
|
+
/** The config's `theme.presentation`, or null when it is off. */
|
|
75
|
+
presentation: Record<string, unknown> | null;
|
|
76
|
+
};
|
|
77
|
+
/** The config's motion, every default filled in. */
|
|
78
|
+
motion: Record<string, unknown>;
|
|
79
|
+
clock: {
|
|
80
|
+
timezoneId: string;
|
|
81
|
+
locale: string;
|
|
82
|
+
};
|
|
83
|
+
};
|
|
84
|
+
/** A complete recording, described without its bytes. */
|
|
85
|
+
export type RecordingManifest = {
|
|
86
|
+
/**
|
|
87
|
+
* The SHA-256 of what makes this recording look the way it does: the
|
|
88
|
+
* timeline without the fields that change on every run (when it was
|
|
89
|
+
* recorded, the app's commit), the states' hashes in order, the targets
|
|
90
|
+
* and the render config. Two runs that would render the same bytes have
|
|
91
|
+
* the same content hash, which is how the service knows a version is
|
|
92
|
+
* unchanged since the last one without being sent it.
|
|
93
|
+
*/
|
|
94
|
+
contentHash: string;
|
|
95
|
+
/** The SHA-256 of `timeline.json` exactly as it will be uploaded. */
|
|
96
|
+
timelineHash: string;
|
|
97
|
+
timelineBytes: number;
|
|
98
|
+
/** The SHA-256 of `targets.json`, or null for a recording without one. */
|
|
99
|
+
targetsHash: string | null;
|
|
100
|
+
/** Recorded with `--draft`: 15 frames a second, at CSS scale. */
|
|
101
|
+
draft: boolean;
|
|
102
|
+
fps: number;
|
|
103
|
+
durationFrames: number;
|
|
104
|
+
viewport: {
|
|
105
|
+
width: number;
|
|
106
|
+
height: number;
|
|
107
|
+
dpr: number;
|
|
108
|
+
};
|
|
109
|
+
/** Every state the timeline names, in the timeline's order. */
|
|
110
|
+
states: StateRef[];
|
|
111
|
+
renderConfig: PublishedRenderConfig;
|
|
112
|
+
};
|
|
113
|
+
/** One clip in a publish: its last check, and its recording if it passed. */
|
|
114
|
+
export type ClipManifest = {
|
|
115
|
+
id: string;
|
|
116
|
+
title: string;
|
|
117
|
+
check: CheckResult;
|
|
118
|
+
/** Present when the check passed and the recording is complete. */
|
|
119
|
+
recording: RecordingManifest | null;
|
|
120
|
+
/** The SHA-256 of `failure.png`, when the check failed and left one. */
|
|
121
|
+
failureHash: string | null;
|
|
122
|
+
};
|
|
123
|
+
/**
|
|
124
|
+
* The first request of a publish.
|
|
125
|
+
*
|
|
126
|
+
* With a GitHub OpenID Connect token, the repository, commit, ref and run
|
|
127
|
+
* come from the token and these fields are checked against it: what the
|
|
128
|
+
* player says a clip was recorded against is GitHub's word, not the CLI's.
|
|
129
|
+
* With a project token they are the only word there is.
|
|
130
|
+
*/
|
|
131
|
+
export type PublishBegin = {
|
|
132
|
+
protocol: typeof PUBLISH_PROTOCOL;
|
|
133
|
+
/** The versioncam version that made the recordings. */
|
|
134
|
+
recorder: string;
|
|
135
|
+
/** `owner/repo`. Optional with a project token, which names its project. */
|
|
136
|
+
project: string | null;
|
|
137
|
+
commit: string;
|
|
138
|
+
branch: string;
|
|
139
|
+
ref: string;
|
|
140
|
+
runId: string | null;
|
|
141
|
+
clips: ClipManifest[];
|
|
142
|
+
};
|
|
143
|
+
/** A clip the service will not take a recording of, and why, in a sentence. */
|
|
144
|
+
export type Refusal = {
|
|
145
|
+
clip: string;
|
|
146
|
+
reason: string;
|
|
147
|
+
};
|
|
148
|
+
export type PublishBeginResponse = {
|
|
149
|
+
publishId: string;
|
|
150
|
+
/** `owner/repo`, as the service knows the project. */
|
|
151
|
+
project: string;
|
|
152
|
+
needStates: string[];
|
|
153
|
+
needTimelines: string[];
|
|
154
|
+
needTargets: string[];
|
|
155
|
+
needFailures: string[];
|
|
156
|
+
/**
|
|
157
|
+
* Recordings refused, by a cap or a plan. Their check results are recorded
|
|
158
|
+
* all the same: a limit gates storage and renders, never the truth.
|
|
159
|
+
*/
|
|
160
|
+
refused: Refusal[];
|
|
161
|
+
};
|
|
162
|
+
/** What a publish made, per clip. */
|
|
163
|
+
export type PublishedVersion = {
|
|
164
|
+
clip: string;
|
|
165
|
+
version: string;
|
|
166
|
+
/** The living URL of the clip on this branch. */
|
|
167
|
+
url: string;
|
|
168
|
+
/** This version's own page. */
|
|
169
|
+
versionUrl: string;
|
|
170
|
+
check: "pass" | "fail";
|
|
171
|
+
/** Nothing about how it looks changed since the version before it. */
|
|
172
|
+
unchanged: boolean;
|
|
173
|
+
/** The clip is stale on the default branch after this publish. */
|
|
174
|
+
stale: boolean;
|
|
175
|
+
/** Why its recording was not stored, when it was not. */
|
|
176
|
+
refused: string | null;
|
|
177
|
+
};
|
|
178
|
+
export type PublishCommitResponse = {
|
|
179
|
+
project: string;
|
|
180
|
+
versions: PublishedVersion[];
|
|
181
|
+
};
|
|
182
|
+
/** Every error the service answers with has a sentence meant for a person. */
|
|
183
|
+
export type PublishErrorBody = {
|
|
184
|
+
error: string;
|
|
185
|
+
code: string;
|
|
186
|
+
};
|
|
187
|
+
/** Paths, relative to the API's origin. */
|
|
188
|
+
export declare const PUBLISH_PATHS: {
|
|
189
|
+
readonly begin: "/v1/publish/begin";
|
|
190
|
+
readonly state: (id: string, hash: string) => string;
|
|
191
|
+
readonly timeline: (id: string, clip: string) => string;
|
|
192
|
+
readonly targets: (id: string, clip: string) => string;
|
|
193
|
+
readonly failure: (id: string, clip: string) => string;
|
|
194
|
+
readonly commit: (id: string) => string;
|
|
195
|
+
};
|
|
196
|
+
/**
|
|
197
|
+
* The timeline as its content hash sees it: without when it was recorded or
|
|
198
|
+
* which commit of the app it ran against, which differ on every run and say
|
|
199
|
+
* nothing about what the recording shows.
|
|
200
|
+
*/
|
|
201
|
+
export declare function contentOfTimeline(timeline: {
|
|
202
|
+
meta: Record<string, unknown>;
|
|
203
|
+
}): unknown;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The publish protocol: what `versioncam publish` sends to version.cam, and
|
|
3
|
+
* what comes back.
|
|
4
|
+
*
|
|
5
|
+
* Both ends import this file. The recorder builds a publish from it and the
|
|
6
|
+
* service validates one against it, through `versioncam/publish`, an entry
|
|
7
|
+
* that pulls in nothing that needs Node. A field added on one side is a type
|
|
8
|
+
* error on the other until both agree, which is the only kind of protocol
|
|
9
|
+
* drift that cannot reach production.
|
|
10
|
+
*
|
|
11
|
+
* The shape follows what a recording is on disk. A recording is a timeline
|
|
12
|
+
* plus a directory of distinct states, so a publish is a timeline plus the
|
|
13
|
+
* states' content hashes: the service asks for the states it has never seen,
|
|
14
|
+
* and a version that changed one screen uploads one screen.
|
|
15
|
+
*/
|
|
16
|
+
/** Bumped when either side could misread the other. */
|
|
17
|
+
export const PUBLISH_PROTOCOL = 1;
|
|
18
|
+
/** Where the service is, unless `VERSIONCAM_API` says otherwise. */
|
|
19
|
+
export const DEFAULT_API = "https://api.version.cam";
|
|
20
|
+
/**
|
|
21
|
+
* The audience a GitHub OpenID Connect token must be minted for. The Action
|
|
22
|
+
* asks for exactly this, and the service refuses any other, so a token
|
|
23
|
+
* minted for some other service cannot be replayed here.
|
|
24
|
+
*/
|
|
25
|
+
export const OIDC_AUDIENCE = "version.cam";
|
|
26
|
+
/**
|
|
27
|
+
* Limits that hold on every plan. The service enforces them; they are here so
|
|
28
|
+
* the client can say which one a recording breaks before uploading anything.
|
|
29
|
+
*/
|
|
30
|
+
export const HARD_CAPS = {
|
|
31
|
+
statesPerVersion: 400,
|
|
32
|
+
stateBytes: 4 * 1024 * 1024,
|
|
33
|
+
timelineBytes: 2 * 1024 * 1024,
|
|
34
|
+
targetsBytes: 256 * 1024,
|
|
35
|
+
failureBytes: 4 * 1024 * 1024,
|
|
36
|
+
publishesPerHour: 20,
|
|
37
|
+
};
|
|
38
|
+
/** Paths, relative to the API's origin. */
|
|
39
|
+
export const PUBLISH_PATHS = {
|
|
40
|
+
begin: "/v1/publish/begin",
|
|
41
|
+
state: (id, hash) => `/v1/publish/${id}/state/${hash}`,
|
|
42
|
+
timeline: (id, clip) => `/v1/publish/${id}/timeline/${encodeURIComponent(clip)}`,
|
|
43
|
+
targets: (id, clip) => `/v1/publish/${id}/targets/${encodeURIComponent(clip)}`,
|
|
44
|
+
failure: (id, clip) => `/v1/publish/${id}/failure/${encodeURIComponent(clip)}`,
|
|
45
|
+
commit: (id) => `/v1/publish/${id}/commit`,
|
|
46
|
+
};
|
|
47
|
+
/**
|
|
48
|
+
* The timeline as its content hash sees it: without when it was recorded or
|
|
49
|
+
* which commit of the app it ran against, which differ on every run and say
|
|
50
|
+
* nothing about what the recording shows.
|
|
51
|
+
*/
|
|
52
|
+
export function contentOfTimeline(timeline) {
|
|
53
|
+
const { recordedAt: _recordedAt, appCommit: _appCommit, ...meta } = timeline.meta;
|
|
54
|
+
return { ...timeline, meta };
|
|
55
|
+
}
|
|
56
|
+
//# sourceMappingURL=protocol.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"protocol.js","sourceRoot":"","sources":["../../src/publish/protocol.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,uDAAuD;AACvD,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAAC,CAAC;AAElC,oEAAoE;AACpE,MAAM,CAAC,MAAM,WAAW,GAAG,yBAAyB,CAAC;AAErD;;;;GAIG;AACH,MAAM,CAAC,MAAM,aAAa,GAAG,aAAa,CAAC;AAE3C;;;GAGG;AACH,MAAM,CAAC,MAAM,SAAS,GAAG;IACvB,gBAAgB,EAAE,GAAG;IACrB,UAAU,EAAE,CAAC,GAAG,IAAI,GAAG,IAAI;IAC3B,aAAa,EAAE,CAAC,GAAG,IAAI,GAAG,IAAI;IAC9B,YAAY,EAAE,GAAG,GAAG,IAAI;IACxB,YAAY,EAAE,CAAC,GAAG,IAAI,GAAG,IAAI;IAC7B,gBAAgB,EAAE,EAAE;CACZ,CAAC;AAoJX,2CAA2C;AAC3C,MAAM,CAAC,MAAM,aAAa,GAAG;IAC3B,KAAK,EAAE,mBAAmB;IAC1B,KAAK,EAAE,CAAC,EAAU,EAAE,IAAY,EAAE,EAAE,CAAC,eAAe,EAAE,UAAU,IAAI,EAAE;IACtE,QAAQ,EAAE,CAAC,EAAU,EAAE,IAAY,EAAE,EAAE,CACrC,eAAe,EAAE,aAAa,kBAAkB,CAAC,IAAI,CAAC,EAAE;IAC1D,OAAO,EAAE,CAAC,EAAU,EAAE,IAAY,EAAE,EAAE,CACpC,eAAe,EAAE,YAAY,kBAAkB,CAAC,IAAI,CAAC,EAAE;IACzD,OAAO,EAAE,CAAC,EAAU,EAAE,IAAY,EAAE,EAAE,CACpC,eAAe,EAAE,YAAY,kBAAkB,CAAC,IAAI,CAAC,EAAE;IACzD,MAAM,EAAE,CAAC,EAAU,EAAE,EAAE,CAAC,eAAe,EAAE,SAAS;CAC1C,CAAC;AAEX;;;;GAIG;AACH,MAAM,UAAU,iBAAiB,CAAC,QAEjC;IACC,MAAM,EACJ,UAAU,EAAE,WAAW,EACvB,SAAS,EAAE,UAAU,EACrB,GAAG,IAAI,EACR,GAAG,QAAQ,CAAC,IAAI,CAAC;IAClB,OAAO,EAAE,GAAG,QAAQ,EAAE,IAAI,EAAE,CAAC;AAC/B,CAAC"}
|