@agent-native/core 0.81.3 → 0.82.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/corpus/README.md +1 -1
- package/corpus/core/CHANGELOG.md +6 -0
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/file-upload/builder.ts +199 -37
- package/corpus/core/src/file-upload/index.ts +2 -0
- package/corpus/core/src/file-upload/types.ts +38 -0
- package/corpus/templates/clips/actions/create-recording.ts +44 -0
- package/corpus/templates/clips/actions/finalize-recording.ts +198 -88
- package/corpus/templates/clips/actions/lib/create-recording-schema.ts +12 -0
- package/corpus/templates/clips/app/components/recorder/recorder-engine.ts +83 -29
- package/corpus/templates/clips/app/routes/record.tsx +12 -1
- package/corpus/templates/clips/server/lib/resumable-session.ts +39 -0
- package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/abort.post.ts +2 -0
- package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/chunk.post.ts +186 -29
- package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/reset-chunks.post.ts +4 -0
- package/corpus/templates/clips/shared/recording-core.ts +5 -0
- package/corpus/templates/content/app/components/editor/DocumentEditor.tsx +63 -16
- package/corpus/templates/content/app/components/sidebar/DocumentSidebar.tsx +28 -33
- package/corpus/templates/content/app/components/sidebar/document-sidebar-sections.ts +47 -0
- package/corpus/templates/content/app/hooks/use-documents.ts +37 -0
- package/corpus/templates/content/changelog/2026-06-30-sidebar-favorites-now-keep-long-titles-tidy-and-stay-in-sync.md +6 -0
- package/dist/collab/awareness.d.ts +2 -2
- package/dist/collab/awareness.d.ts.map +1 -1
- package/dist/collab/routes.d.ts +1 -1
- package/dist/file-upload/actions/upload-image.d.ts +2 -2
- package/dist/file-upload/builder.d.ts.map +1 -1
- package/dist/file-upload/builder.js +137 -25
- package/dist/file-upload/builder.js.map +1 -1
- package/dist/file-upload/index.d.ts +1 -1
- package/dist/file-upload/index.d.ts.map +1 -1
- package/dist/file-upload/index.js.map +1 -1
- package/dist/file-upload/types.d.ts +26 -0
- package/dist/file-upload/types.d.ts.map +1 -1
- package/dist/file-upload/types.js.map +1 -1
- package/dist/notifications/routes.d.ts +3 -3
- package/dist/observability/routes.d.ts +8 -8
- package/dist/progress/routes.d.ts +1 -1
- package/dist/resources/handlers.d.ts +3 -3
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/dist/server/transcribe-voice.d.ts +1 -1
- package/package.json +1 -1
package/corpus/README.md
CHANGED
package/corpus/core/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# @agent-native/core
|
|
2
2
|
|
|
3
|
+
## 0.82.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- fe9fd99: Add optional `resumable` capability to `FileUploadProvider` for streaming uploads. Providers that implement `startSession`, `relayChunk`, and `completeSession` can receive video chunks during recording instead of waiting for a fully assembled file after stop. The Builder.io provider implements this via the GCS resumable upload protocol. Also exports `ResumableUploadSession` and `ResumableChunkResult` types.
|
|
8
|
+
|
|
3
9
|
## 0.81.3
|
|
4
10
|
|
|
5
11
|
### Patch Changes
|
package/corpus/core/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agent-native/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.82.0",
|
|
4
4
|
"description": "Framework for agent-native application development — where AI agents and UI share SQL state, actions, and context",
|
|
5
5
|
"homepage": "https://github.com/BuilderIO/agent-native#readme",
|
|
6
6
|
"bugs": {
|
|
@@ -2,6 +2,8 @@ import type {
|
|
|
2
2
|
FileUploadProvider,
|
|
3
3
|
FileUploadInput,
|
|
4
4
|
FileUploadResult,
|
|
5
|
+
ResumableUploadSession,
|
|
6
|
+
ResumableChunkResult,
|
|
5
7
|
} from "./types.js";
|
|
6
8
|
|
|
7
9
|
const DEFAULT_BUILDER_APP_HOST = "https://builder.io";
|
|
@@ -46,8 +48,6 @@ async function uploadLargeFileViaSignedUrl(
|
|
|
46
48
|
bareMimeType: string,
|
|
47
49
|
bytes: Uint8Array,
|
|
48
50
|
): Promise<FileUploadResult> {
|
|
49
|
-
const host = builderUploadHost();
|
|
50
|
-
const authHeader = { Authorization: `Bearer ${privateKey}` };
|
|
51
51
|
const name = input.filename ?? "upload";
|
|
52
52
|
const mb = (bytes.byteLength / (1024 * 1024)).toFixed(1);
|
|
53
53
|
|
|
@@ -57,32 +57,12 @@ async function uploadLargeFileViaSignedUrl(
|
|
|
57
57
|
|
|
58
58
|
// Step 1 — request a signed URL.
|
|
59
59
|
console.log(`[builder-upload] step 1: requesting signed URL`);
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
body: JSON.stringify({
|
|
66
|
-
fileName: name,
|
|
67
|
-
contentType: bareMimeType,
|
|
68
|
-
size: bytes.byteLength,
|
|
69
|
-
}),
|
|
70
|
-
},
|
|
60
|
+
const { uploadUrl, assetId, requiredHeaders } = await requestBuilderSignedUrl(
|
|
61
|
+
privateKey,
|
|
62
|
+
name,
|
|
63
|
+
bareMimeType,
|
|
64
|
+
bytes.byteLength,
|
|
71
65
|
);
|
|
72
|
-
await assertOk(step1Res, "Builder.io signed-URL request failed");
|
|
73
|
-
|
|
74
|
-
const step1Json = (await step1Res.json()) as {
|
|
75
|
-
uploadUrl?: string;
|
|
76
|
-
assetId?: string;
|
|
77
|
-
expiresAt?: string;
|
|
78
|
-
requiredHeaders?: Record<string, string>;
|
|
79
|
-
};
|
|
80
|
-
const { uploadUrl, assetId, requiredHeaders } = step1Json;
|
|
81
|
-
if (!uploadUrl || !assetId || !requiredHeaders) {
|
|
82
|
-
throw new Error(
|
|
83
|
-
`Builder.io signed-URL response missing required fields: ${JSON.stringify(Object.keys(step1Json))}`,
|
|
84
|
-
);
|
|
85
|
-
}
|
|
86
66
|
console.log(`[builder-upload] step 1 ok: assetId=${assetId}`);
|
|
87
67
|
|
|
88
68
|
// Step 2 — PUT bytes directly to GCS. Only requiredHeaders; no Authorization
|
|
@@ -102,21 +82,80 @@ async function uploadLargeFileViaSignedUrl(
|
|
|
102
82
|
console.log(
|
|
103
83
|
`[builder-upload] step 3: registering asset - ${assetId}, ${input.filename}`,
|
|
104
84
|
);
|
|
105
|
-
const
|
|
85
|
+
const { url, id } = await completeBuilderUpload(
|
|
86
|
+
privateKey,
|
|
87
|
+
assetId,
|
|
88
|
+
input.filename,
|
|
89
|
+
);
|
|
90
|
+
console.log(`[builder-upload] done [${assetId}]: ${url}`);
|
|
91
|
+
return { url, id, provider: "builder" };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async function requestBuilderSignedUrl(
|
|
95
|
+
privateKey: string,
|
|
96
|
+
filename: string,
|
|
97
|
+
mimeType: string,
|
|
98
|
+
size: number,
|
|
99
|
+
resumable = false,
|
|
100
|
+
): Promise<{
|
|
101
|
+
uploadUrl: string;
|
|
102
|
+
assetId: string;
|
|
103
|
+
requiredHeaders: Record<string, string>;
|
|
104
|
+
}> {
|
|
105
|
+
const host = builderUploadHost();
|
|
106
|
+
const url = new URL("/api/v1/upload/signed-url", host);
|
|
107
|
+
const res = await fetchWithTimeout(url.toString(), {
|
|
108
|
+
method: "POST",
|
|
109
|
+
headers: {
|
|
110
|
+
Authorization: `Bearer ${privateKey}`,
|
|
111
|
+
"Content-Type": "application/json",
|
|
112
|
+
},
|
|
113
|
+
body: JSON.stringify({
|
|
114
|
+
fileName: filename,
|
|
115
|
+
contentType: mimeType,
|
|
116
|
+
size,
|
|
117
|
+
resumable,
|
|
118
|
+
}),
|
|
119
|
+
});
|
|
120
|
+
await assertOk(res, "Builder.io signed-URL request failed");
|
|
121
|
+
const json = (await res.json()) as {
|
|
122
|
+
uploadUrl?: string;
|
|
123
|
+
assetId?: string;
|
|
124
|
+
requiredHeaders?: Record<string, string>;
|
|
125
|
+
};
|
|
126
|
+
if (!json.uploadUrl || !json.assetId || !json.requiredHeaders) {
|
|
127
|
+
throw new Error(
|
|
128
|
+
`Builder.io signed-URL response missing required fields: ${JSON.stringify(Object.keys(json))}`,
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
return {
|
|
132
|
+
uploadUrl: json.uploadUrl,
|
|
133
|
+
assetId: json.assetId,
|
|
134
|
+
requiredHeaders: json.requiredHeaders,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async function completeBuilderUpload(
|
|
139
|
+
privateKey: string,
|
|
140
|
+
assetId: string,
|
|
141
|
+
filename: string | undefined,
|
|
142
|
+
): Promise<{ url: string; id?: string }> {
|
|
143
|
+
const host = builderUploadHost();
|
|
144
|
+
const res = await fetchWithTimeout(
|
|
106
145
|
new URL("/api/v1/upload/complete", host).toString(),
|
|
107
146
|
{
|
|
108
147
|
method: "POST",
|
|
109
|
-
headers: {
|
|
110
|
-
|
|
148
|
+
headers: {
|
|
149
|
+
Authorization: `Bearer ${privateKey}`,
|
|
150
|
+
"Content-Type": "application/json",
|
|
151
|
+
},
|
|
152
|
+
body: JSON.stringify({ assetId, name: filename }),
|
|
111
153
|
},
|
|
112
154
|
);
|
|
113
|
-
await assertOk(
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
console.log(`[builder-upload] done [${assetId}]: ${url}`);
|
|
119
|
-
return { url, id, provider: "builder" };
|
|
155
|
+
await assertOk(res, "Builder.io upload complete failed");
|
|
156
|
+
const json = (await res.json()) as { url?: string; id?: string };
|
|
157
|
+
if (!json.url) throw new Error("Builder.io upload/complete returned no URL");
|
|
158
|
+
return { url: json.url, id: json.id };
|
|
120
159
|
}
|
|
121
160
|
|
|
122
161
|
// Retry transient 5xx once with backoff. Builder.io's upload service
|
|
@@ -221,4 +260,127 @@ export const builderFileUploadProvider: FileUploadProvider = {
|
|
|
221
260
|
console.log(`[builder-upload] done: ${json.url}`);
|
|
222
261
|
return { url: json.url, id: json.id, provider: "builder" };
|
|
223
262
|
},
|
|
263
|
+
|
|
264
|
+
resumable: {
|
|
265
|
+
async startSession(filename, mimeType, maxBytes) {
|
|
266
|
+
const { resolveBuilderPrivateKey } =
|
|
267
|
+
await import("../server/credential-provider.js");
|
|
268
|
+
const privateKey = await resolveBuilderPrivateKey();
|
|
269
|
+
if (!privateKey) throw new Error("BUILDER_PRIVATE_KEY is not set");
|
|
270
|
+
|
|
271
|
+
console.log(
|
|
272
|
+
`[builder-resumable] starting session: ${filename} ${mimeType} ${maxBytes} bytes`,
|
|
273
|
+
);
|
|
274
|
+
const { uploadUrl, assetId, requiredHeaders } =
|
|
275
|
+
await requestBuilderSignedUrl(
|
|
276
|
+
privateKey,
|
|
277
|
+
filename,
|
|
278
|
+
mimeType,
|
|
279
|
+
maxBytes,
|
|
280
|
+
true,
|
|
281
|
+
);
|
|
282
|
+
console.log(`[builder-resumable] session step 1 ok: assetId=${assetId}`);
|
|
283
|
+
|
|
284
|
+
const initHeaders: Record<string, string> = {
|
|
285
|
+
"Content-Type": mimeType,
|
|
286
|
+
"x-goog-resumable": "start",
|
|
287
|
+
};
|
|
288
|
+
const contentLengthRange =
|
|
289
|
+
requiredHeaders?.["x-goog-content-length-range"];
|
|
290
|
+
if (contentLengthRange)
|
|
291
|
+
initHeaders["x-goog-content-length-range"] = contentLengthRange;
|
|
292
|
+
|
|
293
|
+
console.log(`[builder-resumable] session step 2: initiating GCS session`);
|
|
294
|
+
const initRes = await fetchWithTimeout(uploadUrl, {
|
|
295
|
+
method: "POST",
|
|
296
|
+
headers: initHeaders,
|
|
297
|
+
body: new Uint8Array(0),
|
|
298
|
+
});
|
|
299
|
+
if (!initRes.ok) {
|
|
300
|
+
const body = await initRes.text().catch(() => "");
|
|
301
|
+
throw new Error(
|
|
302
|
+
`GCS resumable session initiation failed (${initRes.status}): ${body}`,
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
const sessionUri = initRes.headers.get("location");
|
|
306
|
+
if (!sessionUri)
|
|
307
|
+
throw new Error(
|
|
308
|
+
"GCS did not return a Location header for the resumable session",
|
|
309
|
+
);
|
|
310
|
+
|
|
311
|
+
console.log(`[builder-resumable] session ready: assetId=${assetId}`);
|
|
312
|
+
return {
|
|
313
|
+
sessionId: sessionUri,
|
|
314
|
+
meta: { assetId, filename, mimeType },
|
|
315
|
+
} satisfies ResumableUploadSession;
|
|
316
|
+
},
|
|
317
|
+
|
|
318
|
+
async relayChunk(session, contentRange, bytes, options) {
|
|
319
|
+
const sessionUri = session.sessionId;
|
|
320
|
+
const MAX_ATTEMPTS = 4;
|
|
321
|
+
const RETRYABLE = new Set([408, 429, 500, 502, 503, 504]);
|
|
322
|
+
const delayMs = (attempt: number) =>
|
|
323
|
+
Math.min(2000, 300 * 2 ** (attempt - 1));
|
|
324
|
+
|
|
325
|
+
let lastError: unknown = null;
|
|
326
|
+
for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
|
|
327
|
+
try {
|
|
328
|
+
const headers: Record<string, string> = {
|
|
329
|
+
"Content-Range": contentRange,
|
|
330
|
+
};
|
|
331
|
+
if (options?.mimeType) headers["Content-Type"] = options.mimeType;
|
|
332
|
+
const res = await fetch(sessionUri, {
|
|
333
|
+
method: "PUT",
|
|
334
|
+
headers,
|
|
335
|
+
body: bytes as unknown as BodyInit,
|
|
336
|
+
});
|
|
337
|
+
if (res.status === 308 || res.ok)
|
|
338
|
+
return {
|
|
339
|
+
ok: true,
|
|
340
|
+
status: res.status,
|
|
341
|
+
} satisfies ResumableChunkResult;
|
|
342
|
+
if (RETRYABLE.has(res.status) && attempt < MAX_ATTEMPTS) {
|
|
343
|
+
await res.text().catch(() => "");
|
|
344
|
+
console.warn(
|
|
345
|
+
`[builder-resumable] transient ${res.status} on attempt ${attempt}, retrying`,
|
|
346
|
+
);
|
|
347
|
+
await new Promise((r) => setTimeout(r, delayMs(attempt)));
|
|
348
|
+
continue;
|
|
349
|
+
}
|
|
350
|
+
return {
|
|
351
|
+
ok: false,
|
|
352
|
+
status: res.status,
|
|
353
|
+
} satisfies ResumableChunkResult;
|
|
354
|
+
} catch (err) {
|
|
355
|
+
lastError = err;
|
|
356
|
+
if (attempt >= MAX_ATTEMPTS) break;
|
|
357
|
+
console.warn(
|
|
358
|
+
`[builder-resumable] network error on attempt ${attempt}:`,
|
|
359
|
+
err instanceof Error ? err.message : String(err),
|
|
360
|
+
);
|
|
361
|
+
await new Promise((r) => setTimeout(r, delayMs(attempt)));
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
throw lastError instanceof Error
|
|
365
|
+
? lastError
|
|
366
|
+
: new Error("GCS PUT failed after retries");
|
|
367
|
+
},
|
|
368
|
+
|
|
369
|
+
async completeSession(session, filename) {
|
|
370
|
+
const { resolveBuilderPrivateKey } =
|
|
371
|
+
await import("../server/credential-provider.js");
|
|
372
|
+
const privateKey = await resolveBuilderPrivateKey();
|
|
373
|
+
if (!privateKey) throw new Error("BUILDER_PRIVATE_KEY is not set");
|
|
374
|
+
|
|
375
|
+
const assetId = session.meta.assetId as string;
|
|
376
|
+
console.log(`[builder-resumable] completing upload: assetId=${assetId}`);
|
|
377
|
+
const { url } = await completeBuilderUpload(
|
|
378
|
+
privateKey,
|
|
379
|
+
assetId,
|
|
380
|
+
filename,
|
|
381
|
+
);
|
|
382
|
+
console.log(`[builder-resumable] upload complete: ${url}`);
|
|
383
|
+
return url;
|
|
384
|
+
},
|
|
385
|
+
},
|
|
224
386
|
};
|
|
@@ -27,6 +27,22 @@ export interface FileUploadResult {
|
|
|
27
27
|
provider: string;
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
/** Opaque session handle returned by {@link FileUploadProvider.resumable.startSession}.
|
|
31
|
+
* `sessionId` is provider-specific (GCS Location URI, S3 UploadId, etc.).
|
|
32
|
+
* `meta` holds any provider state needed for subsequent relay and complete calls. */
|
|
33
|
+
export interface ResumableUploadSession {
|
|
34
|
+
sessionId: string;
|
|
35
|
+
meta: Record<string, unknown>;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface ResumableChunkResult {
|
|
39
|
+
ok: boolean;
|
|
40
|
+
status: number;
|
|
41
|
+
/** Providers that need per-chunk state (e.g. S3 ETags) return updated meta
|
|
42
|
+
* here; the chunk route merges it back into the stored session. */
|
|
43
|
+
updatedMeta?: Record<string, unknown>;
|
|
44
|
+
}
|
|
45
|
+
|
|
30
46
|
export interface FileUploadProvider {
|
|
31
47
|
/** Unique id, e.g. "builder", "s3". */
|
|
32
48
|
id: string;
|
|
@@ -41,4 +57,26 @@ export interface FileUploadProvider {
|
|
|
41
57
|
isConfiguredForRequest?: () => Promise<boolean>;
|
|
42
58
|
/** Upload a file and return a URL. Throw on failure. */
|
|
43
59
|
upload: (input: FileUploadInput) => Promise<FileUploadResult>;
|
|
60
|
+
/**
|
|
61
|
+
* Optional resumable/streaming upload capability.
|
|
62
|
+
* When present, create-recording will initialise a session and stream chunks
|
|
63
|
+
* during recording instead of assembling the full blob after stop().
|
|
64
|
+
*/
|
|
65
|
+
resumable?: {
|
|
66
|
+
startSession(
|
|
67
|
+
filename: string,
|
|
68
|
+
mimeType: string,
|
|
69
|
+
maxBytes: number,
|
|
70
|
+
): Promise<ResumableUploadSession>;
|
|
71
|
+
relayChunk(
|
|
72
|
+
session: ResumableUploadSession,
|
|
73
|
+
contentRange: string,
|
|
74
|
+
bytes: Uint8Array,
|
|
75
|
+
options?: { mimeType?: string },
|
|
76
|
+
): Promise<ResumableChunkResult>;
|
|
77
|
+
completeSession(
|
|
78
|
+
session: ResumableUploadSession,
|
|
79
|
+
filename: string,
|
|
80
|
+
): Promise<string>;
|
|
81
|
+
};
|
|
44
82
|
}
|
|
@@ -11,6 +11,9 @@
|
|
|
11
11
|
|
|
12
12
|
import { defineAction } from "@agent-native/core";
|
|
13
13
|
import { writeAppState } from "@agent-native/core/application-state";
|
|
14
|
+
import { getActiveFileUploadProviderForRequest } from "@agent-native/core/file-upload";
|
|
15
|
+
import type { UploadMode } from "@shared/recording-core.js";
|
|
16
|
+
import { MAX_UPLOAD_BYTES } from "@shared/upload-limits.js";
|
|
14
17
|
|
|
15
18
|
import { getDb, schema } from "../server/db/index.js";
|
|
16
19
|
import {
|
|
@@ -19,6 +22,7 @@ import {
|
|
|
19
22
|
requireOrganizationAccess,
|
|
20
23
|
stringifySpaceIds,
|
|
21
24
|
} from "../server/lib/recordings.js";
|
|
25
|
+
import { setResumableSession } from "../server/lib/resumable-session.js";
|
|
22
26
|
import { createRecordingSchema } from "./lib/create-recording-schema.js";
|
|
23
27
|
import { DEFAULT_RECORDING_TITLE } from "./lib/title-source.js";
|
|
24
28
|
|
|
@@ -76,6 +80,45 @@ export default defineAction({
|
|
|
76
80
|
|
|
77
81
|
console.log(`Created recording "${title}" (${id})`);
|
|
78
82
|
|
|
83
|
+
// Initialize a resumable upload session so chunks are streamed to the
|
|
84
|
+
// provider during recording (no post-stop assembly). Falls back gracefully
|
|
85
|
+
// to the SQL chunk path when no provider supports resumable uploads or the
|
|
86
|
+
// init fails.
|
|
87
|
+
let uploadMode: UploadMode = "buffered";
|
|
88
|
+
const uploadProvider = await getActiveFileUploadProviderForRequest();
|
|
89
|
+
if (args.requestStreaming && uploadProvider?.resumable) {
|
|
90
|
+
try {
|
|
91
|
+
const recordingMimeType =
|
|
92
|
+
args.mimeType?.split(";")[0]?.trim() || "video/webm";
|
|
93
|
+
const ext = /mp4|quicktime/i.test(recordingMimeType) ? "mp4" : "webm";
|
|
94
|
+
const filename = `${id}.${ext}`;
|
|
95
|
+
console.log(
|
|
96
|
+
`[create-recording] starting resumable session: provider=${uploadProvider.id} mimeType=${recordingMimeType}`,
|
|
97
|
+
);
|
|
98
|
+
const session = await uploadProvider.resumable.startSession(
|
|
99
|
+
filename,
|
|
100
|
+
recordingMimeType,
|
|
101
|
+
MAX_UPLOAD_BYTES,
|
|
102
|
+
);
|
|
103
|
+
await setResumableSession(id, {
|
|
104
|
+
providerId: uploadProvider.id,
|
|
105
|
+
sessionId: session.sessionId,
|
|
106
|
+
meta: session.meta,
|
|
107
|
+
bytesUploaded: 0,
|
|
108
|
+
lastCommittedIndex: -1,
|
|
109
|
+
});
|
|
110
|
+
uploadMode = "streaming";
|
|
111
|
+
console.log(
|
|
112
|
+
`[create-recording] resumable session ready for ${id}: provider=${uploadProvider.id}`,
|
|
113
|
+
);
|
|
114
|
+
} catch (err) {
|
|
115
|
+
console.warn(
|
|
116
|
+
`[create-recording] resumable session init failed, falling back to buffered:`,
|
|
117
|
+
err instanceof Error ? err.message : String(err),
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
79
122
|
return {
|
|
80
123
|
id,
|
|
81
124
|
organizationId,
|
|
@@ -84,6 +127,7 @@ export default defineAction({
|
|
|
84
127
|
abortUrl: `/api/uploads/${id}/abort`,
|
|
85
128
|
// Frontend substitutes {index}/{total}/{isFinal}
|
|
86
129
|
uploadChunkUrlTemplate: `/api/uploads/${id}/chunk?index={index}&total={total}&isFinal={isFinal}`,
|
|
130
|
+
uploadMode,
|
|
87
131
|
};
|
|
88
132
|
},
|
|
89
133
|
});
|