@editframe/cli 0.10.0-beta.4 → 0.10.0-beta.6

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.
Files changed (59) hide show
  1. package/dist/VERSION.d.ts +1 -1
  2. package/dist/VERSION.js +1 -1
  3. package/dist/commands/render.d.ts +1 -1
  4. package/dist/commands/render.js +77 -51
  5. package/dist/commands/sync.js +5 -2
  6. package/dist/operations/getRenderInfo.d.ts +40 -4
  7. package/dist/operations/getRenderInfo.js +13 -0
  8. package/dist/operations/processRenderInfo.js +3 -0
  9. package/dist/operations/syncAssetsDirectory/SubAssetSync.d.ts +20 -0
  10. package/dist/operations/syncAssetsDirectory/SubAssetSync.js +26 -0
  11. package/dist/operations/syncAssetsDirectory/SyncCaption.d.ts +19 -0
  12. package/dist/operations/syncAssetsDirectory/SyncCaption.js +66 -0
  13. package/dist/operations/syncAssetsDirectory/SyncCaption.test.d.ts +1 -0
  14. package/dist/operations/syncAssetsDirectory/SyncFragmentIndex.d.ts +20 -0
  15. package/dist/operations/syncAssetsDirectory/SyncFragmentIndex.js +79 -0
  16. package/dist/operations/syncAssetsDirectory/SyncFragmentIndex.test.d.ts +1 -0
  17. package/dist/operations/syncAssetsDirectory/SyncImage.d.ts +23 -0
  18. package/dist/operations/syncAssetsDirectory/SyncImage.js +95 -0
  19. package/dist/operations/syncAssetsDirectory/SyncImage.test.d.ts +1 -0
  20. package/dist/operations/syncAssetsDirectory/SyncStatus.d.ts +41 -0
  21. package/dist/operations/syncAssetsDirectory/SyncStatus.js +43 -0
  22. package/dist/operations/syncAssetsDirectory/SyncTrack.d.ts +70 -0
  23. package/dist/operations/syncAssetsDirectory/SyncTrack.js +138 -0
  24. package/dist/operations/syncAssetsDirectory/SyncTrack.test.d.ts +1 -0
  25. package/dist/operations/syncAssetsDirectory/doAssetSync.d.ts +5 -0
  26. package/dist/operations/syncAssetsDirectory/doAssetSync.js +48 -0
  27. package/dist/operations/syncAssetsDirectory/doAssetSync.test.d.ts +1 -0
  28. package/dist/operations/syncAssetsDirectory.d.ts +1 -1
  29. package/dist/operations/syncAssetsDirectory.js +20 -240
  30. package/dist/test-fixtures/fixture.d.ts +26 -0
  31. package/dist/utils/index.js +4 -1
  32. package/package.json +5 -5
  33. package/src/commands/process.ts +0 -1
  34. package/src/commands/render.ts +79 -58
  35. package/src/commands/sync.ts +5 -2
  36. package/src/operations/getRenderInfo.ts +14 -0
  37. package/src/operations/processRenderInfo.ts +3 -0
  38. package/src/operations/syncAssetsDirectory/SubAssetSync.ts +42 -0
  39. package/src/operations/syncAssetsDirectory/SyncCaption.test.ts +145 -0
  40. package/src/operations/syncAssetsDirectory/SyncCaption.ts +76 -0
  41. package/src/operations/syncAssetsDirectory/SyncFragmentIndex.test.ts +151 -0
  42. package/src/operations/syncAssetsDirectory/SyncFragmentIndex.ts +92 -0
  43. package/src/operations/syncAssetsDirectory/SyncImage.test.ts +131 -0
  44. package/src/operations/syncAssetsDirectory/SyncImage.ts +112 -0
  45. package/src/operations/syncAssetsDirectory/SyncStatus.ts +51 -0
  46. package/src/operations/syncAssetsDirectory/SyncTrack.test.ts +222 -0
  47. package/src/operations/syncAssetsDirectory/SyncTrack.ts +164 -0
  48. package/src/operations/syncAssetsDirectory/doAssetSync.test.ts +134 -0
  49. package/src/operations/syncAssetsDirectory/doAssetSync.ts +62 -0
  50. package/src/operations/syncAssetsDirectory.test.ts +482 -0
  51. package/src/operations/syncAssetsDirectory.ts +22 -283
  52. package/src/utils/index.ts +4 -1
  53. package/test-fixtures/fixture.ts +141 -0
  54. package/test-fixtures/network.ts +181 -0
  55. package/test-fixtures/test-captions.json +9 -0
  56. package/test-fixtures/test.mp4 +0 -0
  57. package/test-fixtures/test.png +0 -0
  58. package/src/commands/render.test.ts +0 -34
  59. /package/dist/{commands/render.test.d.ts → operations/syncAssetsDirectory.test.d.ts} +0 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@editframe/cli",
3
- "version": "0.10.0-beta.4",
3
+ "version": "0.10.0-beta.6",
4
4
  "description": "Command line interface for EditFrame",
5
5
  "bin": {
6
6
  "editframe": "./dist/index.js"
@@ -23,10 +23,10 @@
23
23
  "vite-tsconfig-paths": "^4.3.2"
24
24
  },
25
25
  "dependencies": {
26
- "@editframe/api": "0.10.0-beta.4",
27
- "@editframe/assets": "0.10.0-beta.4",
28
- "@editframe/elements": "0.10.0-beta.4",
29
- "@editframe/vite-plugin": "0.10.0-beta.4",
26
+ "@editframe/api": "0.10.0-beta.6",
27
+ "@editframe/assets": "0.10.0-beta.6",
28
+ "@editframe/elements": "0.10.0-beta.6",
29
+ "@editframe/vite-plugin": "0.10.0-beta.6",
30
30
  "@inquirer/prompts": "^5.3.8",
31
31
  "axios": "^1.6.8",
32
32
  "chalk": "^5.3.0",
@@ -17,7 +17,6 @@ program
17
17
  .action(async (directory) => {
18
18
  directory ??= ".";
19
19
 
20
- // const srcDir = path.join(directory, "src");
21
20
  const distDir = path.join(directory, "dist");
22
21
  await withSpinner("Building\n", async () => {
23
22
  spawnSync("npx", ["vite", "build", directory], {
@@ -1,53 +1,47 @@
1
- import path from "node:path";
2
1
  import { readFile, writeFile } from "node:fs/promises";
3
- import { inspect } from "node:util";
2
+ import path, { basename, join } from "node:path";
4
3
  import { PassThrough } from "node:stream";
4
+ import { inspect } from "node:util";
5
5
 
6
- import * as tar from "tar";
7
- import { program, Option } from "commander";
6
+ import { Option, program } from "commander";
8
7
  import { parse as parseHTML } from "node-html-parser";
9
- import { build } from "vite";
8
+ import * as tar from "tar";
10
9
 
11
10
  import { md5Directory, md5FilePath } from "@editframe/assets";
12
11
 
13
- import { withSpinner } from "../utils/withSpinner.ts";
14
- import { syncAssetDirectory } from "../operations/syncAssetsDirectory.ts";
12
+ import { spawnSync } from "node:child_process";
15
13
  import { createRender, uploadRender } from "@editframe/api";
14
+ import debug from "debug";
15
+ import { RenderInfo, getRenderInfo } from "../operations/getRenderInfo.ts";
16
+ import { processRenderInfo } from "../operations/processRenderInfo.ts";
17
+ import { syncAssetDirectory } from "../operations/syncAssetsDirectory.ts";
18
+ import { SyncStatus } from "../operations/syncAssetsDirectory/SyncStatus.ts";
19
+ import { getFolderSize } from "../utils/getFolderSize.ts";
20
+ import { getClient } from "../utils/index.ts";
16
21
  import { launchBrowserAndWaitForSDK } from "../utils/launchBrowserAndWaitForSDK.ts";
17
22
  import { PreviewServer } from "../utils/startPreviewServer.ts";
18
- import { getClient } from "../utils/index.ts";
19
- import { getRenderInfo } from "../operations/getRenderInfo.ts";
20
- import { processRenderInfo } from "../operations/processRenderInfo.ts";
21
23
  import { validateVideoResolution } from "../utils/validateVideoResolution.ts";
22
- import { getFolderSize } from "../utils/getFolderSize.ts";
23
-
24
- interface StrategyBuilder {
25
- buildAssetId: (assetPath: string) => Promise<string>;
26
- }
27
-
28
- export const buildAssetId = async (assetPath: string) => {
29
- const md5Sum = await md5FilePath(assetPath);
30
-
31
- const basename = path.basename(assetPath);
32
-
33
- return `${md5Sum}:${basename}`;
34
- };
35
-
36
- class V1Builder implements StrategyBuilder {
37
- async buildAssetId(assetPath: string) {
38
- return buildAssetId(assetPath);
39
- }
40
- }
24
+ import { withSpinner } from "../utils/withSpinner.ts";
41
25
 
42
- class V2Builder implements StrategyBuilder {
43
- async buildAssetId(assetPath: string) {
44
- return buildAssetId(assetPath);
26
+ const log = debug("ef:cli:render");
27
+
28
+ export const buildAssetId = async (
29
+ srcDir: string,
30
+ src: string,
31
+ basename: string,
32
+ ) => {
33
+ log(`Building image asset id for ${src}\n`);
34
+ const assetPath = path.join(srcDir, src);
35
+ const assetMd5 = await md5FilePath(assetPath);
36
+ const syncStatus = new SyncStatus(
37
+ join(srcDir, "assets", ".cache", assetMd5, basename),
38
+ );
39
+ const info = await syncStatus.readInfo();
40
+ if (!info) {
41
+ throw new Error(`SyncStatus info is not found for ${syncStatus.infoPath}`);
45
42
  }
46
- }
47
43
 
48
- const strategyBuilders: Record<string, StrategyBuilder> = {
49
- v1: new V1Builder(),
50
- v2: new V2Builder(),
44
+ return info.asset_id;
51
45
  };
52
46
 
53
47
  program
@@ -57,27 +51,33 @@ program
57
51
  )
58
52
  .addOption(
59
53
  new Option("-s, --strategy <strategy>", "Render strategy")
60
- .choices(["v1", "v2"])
54
+ .choices(["v1"])
61
55
  .default("v1"),
62
56
  )
63
57
  .action(async (directory, options) => {
64
58
  directory ??= ".";
65
59
 
66
- await syncAssetDirectory(directory);
60
+ await syncAssetDirectory(
61
+ join(process.cwd(), directory, "src", "assets", ".cache"),
62
+ );
67
63
 
68
64
  const srcDir = path.join(directory, "src");
69
65
  const distDir = path.join(directory, "dist");
70
- const builder = strategyBuilders[options.strategy];
71
- if (!builder) {
72
- console.error("Invalid strategy", options.strategy);
73
- return;
74
- }
75
66
  await withSpinner("Building\n", async () => {
76
67
  try {
77
- await build({
78
- root: directory,
79
- logLevel: "info", // Optional: adjust log level as needed
80
- clearScreen: false, // Optional: keep console output clean
68
+ await withSpinner("Building\n", async () => {
69
+ spawnSync(
70
+ "npx",
71
+ // biome-ignore format: Grouping CLI arguments
72
+ [
73
+ "vite", "build", directory,
74
+ "--clearScreen", "false",
75
+ "--logLevel", "debug",
76
+ ],
77
+ {
78
+ stdio: "inherit",
79
+ },
80
+ );
81
81
  });
82
82
  } catch (error) {
83
83
  console.error("Build failed:", error);
@@ -96,7 +96,7 @@ program
96
96
  headless: true,
97
97
  },
98
98
  async (page) => {
99
- const renderInfo = await page.evaluate(getRenderInfo);
99
+ const renderInfo = RenderInfo.parse(await page.evaluate(getRenderInfo));
100
100
 
101
101
  validateVideoResolution({
102
102
  width: renderInfo.width,
@@ -108,20 +108,41 @@ program
108
108
  const doc = parseHTML(
109
109
  await readFile(path.join(distDir, "index.html"), "utf-8"),
110
110
  );
111
- const elements = doc.querySelectorAll(
112
- "ef-audio, ef-video, ef-image, ef-captions",
113
- );
114
- for (const element of elements) {
111
+
112
+ log("Building asset IDs");
113
+ for (const element of doc.querySelectorAll(
114
+ "ef-image, ef-audio, ef-video",
115
+ )) {
116
+ log(`Processing ${element.tagName}`);
117
+ if (element.hasAttribute("asset-id")) {
118
+ log(
119
+ `Asset ID for ${element.tagName} ${element.getAttribute("src")} is ${element.getAttribute("asset-id")}`,
120
+ );
121
+ continue;
122
+ }
115
123
  const src = element.getAttribute("src");
116
124
  if (!src) {
125
+ log(`No src attribute for ${element.tagName}`);
117
126
  continue;
118
127
  }
119
- const assetPath = path.join(srcDir, src);
120
128
 
121
- element.setAttribute(
122
- "asset-id",
123
- await builder.buildAssetId(assetPath),
124
- );
129
+ switch (element.tagName) {
130
+ case "EF-IMAGE":
131
+ element.setAttribute(
132
+ "asset-id",
133
+ await buildAssetId(srcDir, src, basename(src)),
134
+ );
135
+ break;
136
+ case "EF-AUDIO":
137
+ case "EF-VIDEO":
138
+ element.setAttribute(
139
+ "asset-id",
140
+ await buildAssetId(srcDir, src, "isobmff"),
141
+ );
142
+ break;
143
+ default:
144
+ log(`Unknown element type: ${element.tagName}`);
145
+ }
125
146
  }
126
147
 
127
148
  await writeFile(path.join(distDir, "index.html"), doc.toString());
@@ -159,7 +180,7 @@ program
159
180
  const readable = new PassThrough();
160
181
  tarStream.pipe(readable);
161
182
  const folderSize = await getFolderSize(distDir);
162
- await uploadRender(getClient(), md5, readable, folderSize);
183
+ await uploadRender(getClient(), render.id, readable, folderSize);
163
184
  process.stderr.write("Render assets uploaded\n");
164
185
  process.stderr.write(inspect(render));
165
186
  process.stderr.write("\n");
@@ -1,3 +1,4 @@
1
+ import { join } from "node:path";
1
2
  import { program } from "commander";
2
3
  import { syncAssetDirectory } from "../operations/syncAssetsDirectory.ts";
3
4
 
@@ -5,6 +6,8 @@ program
5
6
  .command("sync")
6
7
  .description("Sync assets to Editframe servers for rendering")
7
8
  .argument("[directory]", "Path to project directory to sync.")
8
- .action(async (projectDirectory = ".") => {
9
- await syncAssetDirectory(projectDirectory);
9
+ .action(async (directory = ".") => {
10
+ await syncAssetDirectory(
11
+ join(process.cwd(), directory, "src", "assets", ".cache"),
12
+ );
10
13
  });
@@ -3,6 +3,8 @@
3
3
  * RUN IN A WEB BROWSER. LOGS ARE CAPTURED AND RE-LOGGED.
4
4
  */
5
5
 
6
+ import { z } from "zod";
7
+
6
8
  import type {
7
9
  EFCaptions,
8
10
  EFImage,
@@ -10,6 +12,18 @@ import type {
10
12
  EFTimegroup,
11
13
  } from "@editframe/elements";
12
14
 
15
+ export const RenderInfo = z.object({
16
+ width: z.number().positive(),
17
+ height: z.number().positive(),
18
+ fps: z.number().positive(),
19
+ durationMs: z.number().positive(),
20
+ assets: z.object({
21
+ efMedia: z.record(z.any()),
22
+ efCaptions: z.array(z.string()),
23
+ efImage: z.array(z.string()),
24
+ }),
25
+ });
26
+
13
27
  export const getRenderInfo = async () => {
14
28
  const rootTimeGroup = document.querySelector("ef-timegroup") as
15
29
  | EFTimegroup
@@ -13,6 +13,9 @@ export const processRenderInfo = async (
13
13
  process.stderr.write(src);
14
14
  process.stderr.write("\n");
15
15
  for (const trackId in tracks) {
16
+ process.stderr.write("Generating track: ");
17
+ process.stderr.write(trackId);
18
+ process.stderr.write("\n");
16
19
  await generateTrack(
17
20
  "./src/assets",
18
21
  `./src${src}`,
@@ -0,0 +1,42 @@
1
+ import type { SyncStatus } from "./SyncStatus.ts";
2
+
3
+ import { SyncCaption } from "./SyncCaption.ts";
4
+ import { SyncFragmentIndex } from "./SyncFragmentIndex.ts";
5
+ import { SyncImage } from "./SyncImage.ts";
6
+ import { SyncTrack } from "./SyncTrack.ts";
7
+
8
+ export interface SubAssetSync<CreationType> {
9
+ icon: string;
10
+ label: string;
11
+ path: string;
12
+ md5: string;
13
+ prepare: () => Promise<void>;
14
+ validate: () => Promise<void>;
15
+ create: () => Promise<void>;
16
+ upload: () => Promise<void>;
17
+ syncStatus: SyncStatus;
18
+ isComplete: () => boolean;
19
+ markSynced: () => Promise<void>;
20
+ created: CreationType | null;
21
+ }
22
+
23
+ const trackMatch = /\.track-[\d]+.mp4$/i;
24
+ const fragmentIndexMatch = /\.tracks.json$/i;
25
+ const captionsMatch = /\.captions.json$/i;
26
+ const imageMatch = /\.(png|jpe?g|gif|webp)$/i;
27
+
28
+ export const getAssetSync = (subAssetPath: string, md5: string) => {
29
+ if (imageMatch.test(subAssetPath)) {
30
+ return new SyncImage(subAssetPath, md5);
31
+ }
32
+ if (trackMatch.test(subAssetPath)) {
33
+ return new SyncTrack(subAssetPath, md5);
34
+ }
35
+ if (fragmentIndexMatch.test(subAssetPath)) {
36
+ return new SyncFragmentIndex(subAssetPath, md5);
37
+ }
38
+ if (captionsMatch.test(subAssetPath)) {
39
+ return new SyncCaption(subAssetPath, md5);
40
+ }
41
+ throw new Error(`Unrecognized sub-asset type: ${subAssetPath}`);
42
+ };
@@ -0,0 +1,145 @@
1
+ import { describe, expect, test } from "vitest";
2
+ import { fixture, withFixtures } from "../../../test-fixtures/fixture.ts";
3
+ import {
4
+ mockCreateCaptionFile,
5
+ mockUploadCaptionFile,
6
+ useMSW,
7
+ } from "../../../test-fixtures/network.ts";
8
+ import { SyncCaption } from "./SyncCaption.ts";
9
+
10
+ describe("SyncCaption", async () => {
11
+ const server = useMSW();
12
+ await withFixtures(
13
+ [fixture("test.mp4", "test.mp4")],
14
+ async ({ files: [video], generateCaptions }) => {
15
+ test("Reads byte size", async () => {
16
+ const syncCaption = new SyncCaption(
17
+ await generateCaptions(video!),
18
+ video!.md5,
19
+ );
20
+ await expect(syncCaption.byteSize()).resolves.toEqual(35);
21
+ });
22
+
23
+ test("prepare() is noop", async () => {
24
+ const syncCaption = new SyncCaption(
25
+ await generateCaptions(video!),
26
+ video!.md5,
27
+ );
28
+ await expect(syncCaption.prepare()).resolves.toBeUndefined();
29
+ });
30
+
31
+ test("validate() is noop", async () => {
32
+ const syncCaption = new SyncCaption(
33
+ await generateCaptions(video!),
34
+ video!.md5,
35
+ );
36
+ await expect(syncCaption.validate()).resolves.toBeUndefined();
37
+ });
38
+
39
+ describe(".create()", () => {
40
+ test("isComplete() returns false when not created", async () => {
41
+ server.use(
42
+ mockCreateCaptionFile({
43
+ complete: false,
44
+ id: "123",
45
+ filename: "test.mp4",
46
+ fixture: video!,
47
+ }),
48
+ );
49
+ const syncCaption = new SyncCaption(
50
+ await generateCaptions(video!),
51
+ video!.md5,
52
+ );
53
+ await syncCaption.create();
54
+ expect(syncCaption.isComplete()).toBe(false);
55
+ });
56
+
57
+ test("isComplete() returns true when created", async () => {
58
+ server.use(
59
+ mockCreateCaptionFile({
60
+ complete: true,
61
+ id: "123",
62
+ filename: "test.mp4",
63
+ fixture: video!,
64
+ }),
65
+ );
66
+ const syncCaption = new SyncCaption(
67
+ await generateCaptions(video!),
68
+ video!.md5,
69
+ );
70
+ await syncCaption.create();
71
+ expect(syncCaption.isComplete()).toBe(true);
72
+ });
73
+ });
74
+
75
+ describe(".upload()", () => {
76
+ test("throws when not created", async () => {
77
+ const syncCaption = new SyncCaption(
78
+ await generateCaptions(video!),
79
+ video!.md5,
80
+ );
81
+ await expect(syncCaption.upload()).rejects.toThrow();
82
+ });
83
+
84
+ test("uploads caption", async () => {
85
+ server.use(
86
+ mockCreateCaptionFile({
87
+ complete: true,
88
+ id: "123",
89
+ filename: "test.mp4",
90
+ fixture: video!,
91
+ }),
92
+ mockUploadCaptionFile({
93
+ id: "123",
94
+ filename: "test.mp4",
95
+ fixture: video!,
96
+ }),
97
+ );
98
+ const syncCaption = new SyncCaption(
99
+ await generateCaptions(video!),
100
+ video!.md5,
101
+ );
102
+ await syncCaption.create();
103
+ await expect(syncCaption.upload()).resolves.toBeUndefined();
104
+ });
105
+ });
106
+
107
+ describe(".markSynced()", () => {
108
+ test("throws when not created", async () => {
109
+ const syncCaption = new SyncCaption(
110
+ await generateCaptions(video!),
111
+ video!.md5,
112
+ );
113
+ await expect(syncCaption.markSynced()).rejects.toThrow();
114
+ });
115
+
116
+ test("marks synced", async () => {
117
+ server.use(
118
+ mockCreateCaptionFile({
119
+ complete: true,
120
+ id: "123",
121
+ filename: "test.mp4",
122
+ fixture: video!,
123
+ }),
124
+ );
125
+ const syncCaption = new SyncCaption(
126
+ await generateCaptions(video!),
127
+ video!.md5,
128
+ );
129
+ await syncCaption.create();
130
+ await syncCaption.markSynced();
131
+
132
+ await expect(syncCaption.syncStatus.isSynced()).resolves.toBe(true);
133
+ await expect(syncCaption.syncStatus.readInfo()).resolves.toEqual({
134
+ version: "1",
135
+ complete: true,
136
+ id: "123",
137
+ md5: video!.md5,
138
+ asset_id: `${video!.md5}:test.mp4`,
139
+ byte_size: 35,
140
+ });
141
+ });
142
+ });
143
+ },
144
+ );
145
+ });
@@ -0,0 +1,76 @@
1
+ import fs from "node:fs/promises";
2
+
3
+ import {
4
+ type CreateCaptionFileResult,
5
+ createCaptionFile,
6
+ uploadCaptionFile,
7
+ } from "@editframe/api";
8
+
9
+ import { Readable } from "node:stream";
10
+ import { getClient } from "../../utils/index.ts";
11
+ import type { SubAssetSync } from "./SubAssetSync.ts";
12
+ import { SyncStatus } from "./SyncStatus.ts";
13
+
14
+ export class SyncCaption implements SubAssetSync<CreateCaptionFileResult> {
15
+ icon = "📝";
16
+ label = "captions";
17
+ syncStatus: SyncStatus = new SyncStatus(this.path);
18
+ created: CreateCaptionFileResult | null = null;
19
+ constructor(
20
+ public path: string,
21
+ public md5: string,
22
+ ) {}
23
+
24
+ async byteSize() {
25
+ return (await fs.stat(this.path)).size;
26
+ }
27
+
28
+ async prepare() {}
29
+
30
+ async validate() {}
31
+
32
+ async create() {
33
+ this.created = await createCaptionFile(getClient(), {
34
+ md5: this.md5,
35
+ filename: this.path.replace(/\.captions.json$/, ""),
36
+ byte_size: await this.byteSize(),
37
+ });
38
+ }
39
+
40
+ isComplete() {
41
+ return !!this.created?.complete;
42
+ }
43
+
44
+ async upload() {
45
+ if (!this.created) {
46
+ throw new Error(
47
+ "Caption not created. Should have been prevented by .isComplete()",
48
+ );
49
+ }
50
+ await uploadCaptionFile(
51
+ getClient(),
52
+ this.created.id,
53
+ // It's not clear why we need to use Readable.from here, but it seems
54
+ // to fix an issue where the request is closed early in tests
55
+ Readable.from(await fs.readFile(this.path)),
56
+ await this.byteSize(),
57
+ );
58
+ }
59
+
60
+ async markSynced() {
61
+ if (!this.created) {
62
+ throw new Error(
63
+ "Caption not created. Should have been prevented by .isComplete()",
64
+ );
65
+ }
66
+ const byteSize = await this.byteSize();
67
+ await this.syncStatus.markSynced({
68
+ version: "1",
69
+ complete: true,
70
+ id: this.created.id,
71
+ md5: this.md5,
72
+ asset_id: this.created.asset_id,
73
+ byte_size: byteSize,
74
+ });
75
+ }
76
+ }