@maravilla-labs/platform 0.2.4 → 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/dist/config.d.ts +12 -1
- package/dist/config.js.map +1 -1
- package/dist/events.d.ts +45 -2
- package/dist/events.js +5 -1
- package/dist/events.js.map +1 -1
- package/dist/index.d.ts +89 -1
- package/dist/index.js +167 -1
- package/dist/index.js.map +1 -1
- package/dist/transforms-KzpoYW43.d.ts +156 -0
- package/package.json +5 -3
- package/src/config.ts +10 -0
- package/src/events.ts +52 -0
- package/src/index.ts +1 -0
- package/src/remote-client.ts +139 -1
- package/src/ren.ts +6 -0
- package/src/transforms.ts +243 -0
- package/src/types.ts +99 -0
- package/tests/derive-key.test.ts +68 -0
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Media transforms — async ffmpeg / image / OCR jobs.
|
|
3
|
+
*
|
|
4
|
+
* Mirrors the Rust `platform.media.transforms` surface. All mutating
|
|
5
|
+
* methods (transcode/thumbnail/resize/ocr) return a {@link JobHandle}
|
|
6
|
+
* whose `outputKey` is content-addressed via {@link keyFor} and known
|
|
7
|
+
* up front — clients can render placeholder UI for the derived asset
|
|
8
|
+
* before the worker even starts the job.
|
|
9
|
+
*
|
|
10
|
+
* The runtime injects the live JS object as `platform.media.transforms`
|
|
11
|
+
* (see `crates/runtime/src/ops/platform/js_bindings/platform_js.rs`).
|
|
12
|
+
* The static helper {@link keyFor} is exported separately and runs
|
|
13
|
+
* client-side — it must produce byte-for-byte identical output to the
|
|
14
|
+
* Rust `derive_key`. The shared golden-vector fixture
|
|
15
|
+
* `crates/platform/tests/derive_key_vectors.json` is consumed from both
|
|
16
|
+
* the Rust integration test (`crates/platform/tests/derive_key_golden.rs`)
|
|
17
|
+
* and the TS test (`packages/platform/tests/derive-key.test.ts`) to keep
|
|
18
|
+
* the two in lockstep.
|
|
19
|
+
*/
|
|
20
|
+
/** Video container targets supported by v1. */
|
|
21
|
+
type VideoFormat = 'mp4' | 'webm';
|
|
22
|
+
/** Image output formats supported by v1. */
|
|
23
|
+
type ImageFormat = 'jpg' | 'png' | 'webp';
|
|
24
|
+
/** Lifecycle state of a transform job. */
|
|
25
|
+
type JobStatus = 'pending' | 'running' | 'complete' | 'failed';
|
|
26
|
+
/** Options for `transforms.transcode`. */
|
|
27
|
+
interface TranscodeOpts {
|
|
28
|
+
format: VideoFormat;
|
|
29
|
+
codec?: string;
|
|
30
|
+
max_width?: number;
|
|
31
|
+
max_height?: number;
|
|
32
|
+
audio_codec?: string;
|
|
33
|
+
bitrate_kbps?: number;
|
|
34
|
+
}
|
|
35
|
+
/** Options for `transforms.thumbnail` — extract a single video frame. */
|
|
36
|
+
interface ThumbnailOpts {
|
|
37
|
+
/** Offset into the source. Accepts `"00:00:01"`, `"1s"`, or a plain number-as-string. */
|
|
38
|
+
at: string;
|
|
39
|
+
width?: number;
|
|
40
|
+
height?: number;
|
|
41
|
+
/** Defaults to `"jpg"` server-side when omitted. */
|
|
42
|
+
format?: ImageFormat;
|
|
43
|
+
quality?: number;
|
|
44
|
+
}
|
|
45
|
+
/** Options for `transforms.resize`. */
|
|
46
|
+
interface ResizeOpts {
|
|
47
|
+
width?: number;
|
|
48
|
+
height?: number;
|
|
49
|
+
format: ImageFormat;
|
|
50
|
+
quality?: number;
|
|
51
|
+
strip_metadata?: boolean;
|
|
52
|
+
}
|
|
53
|
+
/** Options for `transforms.ocr`. */
|
|
54
|
+
interface OcrOpts {
|
|
55
|
+
/** Tesseract language code(s). Defaults to `"eng"` server-side when omitted. */
|
|
56
|
+
lang?: string;
|
|
57
|
+
}
|
|
58
|
+
/** Tagged union of all transform requests — matches the Rust `TransformSpec`. */
|
|
59
|
+
type TransformSpec = ({
|
|
60
|
+
kind: 'transcode';
|
|
61
|
+
} & TranscodeOpts) | ({
|
|
62
|
+
kind: 'thumbnail';
|
|
63
|
+
} & ThumbnailOpts) | ({
|
|
64
|
+
kind: 'resize';
|
|
65
|
+
} & ResizeOpts) | ({
|
|
66
|
+
kind: 'ocr';
|
|
67
|
+
} & OcrOpts);
|
|
68
|
+
/** Probe output. All fields optional — ffprobe doesn't fill every one for every input. */
|
|
69
|
+
interface MediaInfo {
|
|
70
|
+
duration_secs?: number;
|
|
71
|
+
width?: number;
|
|
72
|
+
height?: number;
|
|
73
|
+
video_codec?: string;
|
|
74
|
+
audio_codec?: string;
|
|
75
|
+
bitrate_bps?: number;
|
|
76
|
+
container?: string;
|
|
77
|
+
}
|
|
78
|
+
/** Returned by every mutating transforms method. */
|
|
79
|
+
interface JobHandle {
|
|
80
|
+
id: string;
|
|
81
|
+
src_key: string;
|
|
82
|
+
output_key: string;
|
|
83
|
+
status: JobStatus;
|
|
84
|
+
}
|
|
85
|
+
/** Returned by `transforms.job(id)`. */
|
|
86
|
+
interface JobStatusResponse {
|
|
87
|
+
id: string;
|
|
88
|
+
status: JobStatus;
|
|
89
|
+
}
|
|
90
|
+
interface TransformsService {
|
|
91
|
+
transcode(srcKey: string, opts: TranscodeOpts): Promise<JobHandle>;
|
|
92
|
+
thumbnail(srcKey: string, opts: ThumbnailOpts): Promise<JobHandle>;
|
|
93
|
+
resize(srcKey: string, opts: ResizeOpts): Promise<JobHandle>;
|
|
94
|
+
probe(srcKey: string): Promise<MediaInfo>;
|
|
95
|
+
ocr(srcKey: string, opts?: OcrOpts | null): Promise<JobHandle>;
|
|
96
|
+
job(id: string): Promise<JobStatusResponse>;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Compute the canonical derived-asset key for `(srcKey, spec)`.
|
|
100
|
+
*
|
|
101
|
+
* Shape: `__derived/<srcHash>/<variantHash>.<ext>` where each hash is
|
|
102
|
+
* the first 16 hex chars (8 bytes / 64 bits) of `SHA-256(...)`.
|
|
103
|
+
*
|
|
104
|
+
* **MUST** match Rust `platform::media::transforms::derive_key` byte-for-byte.
|
|
105
|
+
* Cross-language golden vectors live at
|
|
106
|
+
* `crates/platform/tests/derive_key_vectors.json`.
|
|
107
|
+
*/
|
|
108
|
+
declare function keyFor(srcKey: string, spec: TransformSpec): string;
|
|
109
|
+
/** Convenience namespace mirroring the Rust `transforms::keyFor` re-export. */
|
|
110
|
+
declare const transforms: {
|
|
111
|
+
keyFor: typeof keyFor;
|
|
112
|
+
};
|
|
113
|
+
/**
|
|
114
|
+
* Per-pattern transforms declaration used inside the `transforms` block of
|
|
115
|
+
* `maravilla.config.ts`. Each field accepts a single opts object or an array.
|
|
116
|
+
*
|
|
117
|
+
* `variants` is sugar for `resize` arrays — convenient for the common
|
|
118
|
+
* "image → multiple resized renditions" case.
|
|
119
|
+
*/
|
|
120
|
+
interface TransformsPatternSpec {
|
|
121
|
+
transcode?: TranscodeOpts | TranscodeOpts[];
|
|
122
|
+
thumbnail?: ThumbnailOpts | ThumbnailOpts[];
|
|
123
|
+
resize?: ResizeOpts | ResizeOpts[];
|
|
124
|
+
/** Sugar for `resize` arrays — same shape, same semantics. */
|
|
125
|
+
variants?: ResizeOpts[];
|
|
126
|
+
ocr?: OcrOpts | OcrOpts[];
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Top-level `transforms` block. Keys are glob path patterns matched against
|
|
130
|
+
* the storage key of every uploaded object. The adapter compiles each entry
|
|
131
|
+
* into a synthetic `storage.put` event handler that fans out the declared
|
|
132
|
+
* transforms via `Promise.all`.
|
|
133
|
+
*
|
|
134
|
+
* @example
|
|
135
|
+
* ```ts
|
|
136
|
+
* import { defineConfig } from '@maravilla-labs/platform/config';
|
|
137
|
+
*
|
|
138
|
+
* export default defineConfig({
|
|
139
|
+
* transforms: {
|
|
140
|
+
* 'uploads/videos/**': {
|
|
141
|
+
* transcode: [{ format: 'mp4' }, { format: 'webm' }],
|
|
142
|
+
* thumbnail: { at: '1s', width: 640, format: 'jpg' },
|
|
143
|
+
* },
|
|
144
|
+
* 'uploads/photos/**': {
|
|
145
|
+
* variants: [
|
|
146
|
+
* { width: 1600, format: 'webp', quality: 85 },
|
|
147
|
+
* { width: 400, format: 'webp', quality: 80 },
|
|
148
|
+
* ],
|
|
149
|
+
* },
|
|
150
|
+
* },
|
|
151
|
+
* });
|
|
152
|
+
* ```
|
|
153
|
+
*/
|
|
154
|
+
type TransformsConfig = Record<string, TransformsPatternSpec>;
|
|
155
|
+
|
|
156
|
+
export { type ImageFormat as I, type JobHandle as J, type MediaInfo as M, type OcrOpts as O, type ResizeOpts as R, type TransformsConfig as T, type VideoFormat as V, type TransformsPatternSpec as a, type JobStatus as b, type JobStatusResponse as c, type ThumbnailOpts as d, type TranscodeOpts as e, type TransformSpec as f, type TransformsService as g, keyFor as k, transforms as t };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@maravilla-labs/platform",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Universal platform client for Maravilla runtime",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -27,12 +27,14 @@
|
|
|
27
27
|
"scripts": {
|
|
28
28
|
"build": "tsup",
|
|
29
29
|
"dev": "tsup --watch",
|
|
30
|
-
"typecheck": "tsc --noEmit"
|
|
30
|
+
"typecheck": "tsc --noEmit",
|
|
31
|
+
"test": "vitest run"
|
|
31
32
|
},
|
|
32
33
|
"devDependencies": {
|
|
33
34
|
"@types/node": "^22.10.6",
|
|
34
35
|
"tsup": "^8.5.0",
|
|
35
|
-
"typescript": "^5.9.2"
|
|
36
|
+
"typescript": "^5.9.2",
|
|
37
|
+
"vitest": "^1.6.0"
|
|
36
38
|
},
|
|
37
39
|
"peerDependencies": {
|
|
38
40
|
"livekit-client": "^2.18.1"
|
package/src/config.ts
CHANGED
|
@@ -38,6 +38,9 @@
|
|
|
38
38
|
*/
|
|
39
39
|
export type SecretRef = string | { env: string };
|
|
40
40
|
|
|
41
|
+
import type { TransformsConfig } from './transforms.js';
|
|
42
|
+
export type { TransformsConfig, TransformsPatternSpec } from './transforms.js';
|
|
43
|
+
|
|
41
44
|
// ── Resources + policies ──
|
|
42
45
|
|
|
43
46
|
export interface ResourceDefinition {
|
|
@@ -198,6 +201,13 @@ export interface MaravillaConfig {
|
|
|
198
201
|
auth?: AuthConfigBlock;
|
|
199
202
|
/** Declarative database indexes (regular + vector). Reconciled upsert-only on deploy. */
|
|
200
203
|
database?: DatabaseConfigBlock;
|
|
204
|
+
/**
|
|
205
|
+
* Declarative media transforms — each entry compiles into a synthetic
|
|
206
|
+
* `storage.put` event handler that fans out the declared
|
|
207
|
+
* `platform.media.transforms.*` calls (via `Promise.all`) for every
|
|
208
|
+
* matching upload. See {@link TransformsConfig}.
|
|
209
|
+
*/
|
|
210
|
+
transforms?: TransformsConfig;
|
|
201
211
|
}
|
|
202
212
|
|
|
203
213
|
// ── Database block ──
|
package/src/events.ts
CHANGED
|
@@ -33,6 +33,7 @@ export type EventTrigger =
|
|
|
33
33
|
| EventTriggerQueue
|
|
34
34
|
| EventTriggerChannel
|
|
35
35
|
| EventTriggerDeploy
|
|
36
|
+
| EventTriggerStorage
|
|
36
37
|
| EventTriggerRen;
|
|
37
38
|
|
|
38
39
|
export interface EventTriggerKv {
|
|
@@ -84,6 +85,20 @@ export interface EventTriggerDeploy {
|
|
|
84
85
|
phase: 'ready' | 'draining' | 'stopped';
|
|
85
86
|
}
|
|
86
87
|
|
|
88
|
+
/**
|
|
89
|
+
* Object storage trigger — fires when an object is created or removed
|
|
90
|
+
* under the matching `keyPattern`. Used by both hand-written `onStorage`
|
|
91
|
+
* handlers and by the synthesized handlers that the adapter compiles
|
|
92
|
+
* from a `transforms` block in `maravilla.config.ts`.
|
|
93
|
+
*/
|
|
94
|
+
export interface EventTriggerStorage {
|
|
95
|
+
kind: 'storage';
|
|
96
|
+
/** Glob-style storage key pattern (e.g. `"uploads/videos/**"`). Omit to match all objects. */
|
|
97
|
+
keyPattern?: string;
|
|
98
|
+
/** Operation filter; omit to match both `put` and `delete`. */
|
|
99
|
+
op?: 'put' | 'delete';
|
|
100
|
+
}
|
|
101
|
+
|
|
87
102
|
export interface EventTriggerRen {
|
|
88
103
|
kind: 'ren';
|
|
89
104
|
match: { r?: string; t?: string; ns?: string };
|
|
@@ -157,6 +172,21 @@ export interface DeployEvent {
|
|
|
157
172
|
ts: number;
|
|
158
173
|
}
|
|
159
174
|
|
|
175
|
+
/**
|
|
176
|
+
* Storage event payload — emitted by the dev-server / runtime when an
|
|
177
|
+
* object is put or deleted. The synthesized transforms handlers consume
|
|
178
|
+
* the `key` field to look up the source object.
|
|
179
|
+
*/
|
|
180
|
+
export interface StorageEvent {
|
|
181
|
+
op: 'put' | 'delete';
|
|
182
|
+
key: string;
|
|
183
|
+
/** Present on `put` — content type from the upload metadata, when known. */
|
|
184
|
+
contentType?: string;
|
|
185
|
+
/** Present on `put` — size in bytes, when known. */
|
|
186
|
+
size?: number;
|
|
187
|
+
ts: number;
|
|
188
|
+
}
|
|
189
|
+
|
|
160
190
|
// ============ Handler context ============
|
|
161
191
|
|
|
162
192
|
export interface EventCtx {
|
|
@@ -264,6 +294,28 @@ export function onDeploy(
|
|
|
264
294
|
return register({ kind: 'deploy', phase }, handler);
|
|
265
295
|
}
|
|
266
296
|
|
|
297
|
+
/**
|
|
298
|
+
* React to object-storage `put` / `delete` events. `keyPattern` is a
|
|
299
|
+
* glob (e.g. `"uploads/videos/**"`). Omit `op` to match both put and
|
|
300
|
+
* delete; omit `keyPattern` to match every object in the tenant's
|
|
301
|
+
* bucket.
|
|
302
|
+
*
|
|
303
|
+
* ```ts
|
|
304
|
+
* export const onUpload = onStorage(
|
|
305
|
+
* { keyPattern: 'uploads/photos/**', op: 'put' },
|
|
306
|
+
* async (event, ctx) => {
|
|
307
|
+
* await ctx.platform.media.transforms.resize(event.key, { width: 1600, format: 'webp' });
|
|
308
|
+
* },
|
|
309
|
+
* );
|
|
310
|
+
* ```
|
|
311
|
+
*/
|
|
312
|
+
export function onStorage(
|
|
313
|
+
config: Omit<EventTriggerStorage, 'kind'>,
|
|
314
|
+
handler: (event: StorageEvent, ctx: EventCtx) => unknown | Promise<unknown>,
|
|
315
|
+
): RegisteredHandler<StorageEvent> {
|
|
316
|
+
return register({ kind: 'storage', ...config }, handler);
|
|
317
|
+
}
|
|
318
|
+
|
|
267
319
|
/** Escape hatch for arbitrary `RenEvent` matches. */
|
|
268
320
|
export function defineEvent(
|
|
269
321
|
config: Omit<EventTriggerRen, 'kind'>,
|
package/src/index.ts
CHANGED
package/src/remote-client.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { KvNamespace, KvListResult, Database, DbFindOptions, Storage, RealtimeService, PresenceService, AuthService, AuthCaller, AuthUser, AuthSession, AuthField, RegisterOptions, LoginOptions, UserListFilter, UserListResponse, UpdateUserOptions, PolicyService, VectorIndexSpec, VectorIndexDescriptor, VectorQueryWithFilter, VectorSearchHit, IndexSpec, IndexDescriptor } from './types.js';
|
|
1
|
+
import type { KvNamespace, KvListResult, Database, DbFindOptions, Storage, RealtimeService, PresenceService, AuthService, AuthCaller, AuthUser, AuthSession, AuthField, RegisterOptions, LoginOptions, UserListFilter, UserListResponse, UpdateUserOptions, PolicyService, VectorIndexSpec, VectorIndexDescriptor, VectorQueryWithFilter, VectorSearchHit, IndexSpec, IndexDescriptor, Workflows, WorkflowHandle, WorkflowRun, WorkflowStepRecord } from './types.js';
|
|
2
2
|
import { RemoteMediaService } from './media.js';
|
|
3
3
|
|
|
4
4
|
/**
|
|
@@ -396,6 +396,21 @@ class RemoteStorage implements Storage {
|
|
|
396
396
|
if (response.status === 404) return null;
|
|
397
397
|
return response.json();
|
|
398
398
|
}
|
|
399
|
+
|
|
400
|
+
async confirm(key: string): Promise<void> {
|
|
401
|
+
// Mirrors the dev-server route registered in
|
|
402
|
+
// `crates/dev-server/src/router.rs`:
|
|
403
|
+
// POST /api/storage/confirm body: { key: "..." }
|
|
404
|
+
// The handler verifies the object actually landed in the backing store
|
|
405
|
+
// and publishes the synthesized `storage.put` RenEvent that any
|
|
406
|
+
// registered handlers (including those synthesized from a `transforms`
|
|
407
|
+
// config block) react to.
|
|
408
|
+
await this.fetch(`${this.baseUrl}/api/storage/confirm`, {
|
|
409
|
+
method: 'POST',
|
|
410
|
+
headers: { 'content-type': 'application/json' },
|
|
411
|
+
body: JSON.stringify({ key }),
|
|
412
|
+
});
|
|
413
|
+
}
|
|
399
414
|
}
|
|
400
415
|
|
|
401
416
|
/**
|
|
@@ -628,6 +643,127 @@ class RemotePolicyService implements PolicyService {
|
|
|
628
643
|
}
|
|
629
644
|
}
|
|
630
645
|
|
|
646
|
+
/**
|
|
647
|
+
* Remote Workflows service — HTTP adaptor over the dev-server's
|
|
648
|
+
* `/api/workflows/*` routes. The runtime (isolate) path uses `Deno.core.ops`
|
|
649
|
+
* instead; both produce handles with the same shape.
|
|
650
|
+
*
|
|
651
|
+
* @internal
|
|
652
|
+
*/
|
|
653
|
+
class RemoteWorkflows implements Workflows {
|
|
654
|
+
constructor(
|
|
655
|
+
private baseUrl: string,
|
|
656
|
+
private headers: Record<string, string>,
|
|
657
|
+
) {}
|
|
658
|
+
|
|
659
|
+
private async request(path: string, options: RequestInit = {}): Promise<Response> {
|
|
660
|
+
const response = await fetch(`${this.baseUrl}${path}`, {
|
|
661
|
+
...options,
|
|
662
|
+
headers: { ...this.headers, ...options.headers },
|
|
663
|
+
});
|
|
664
|
+
if (!response.ok && response.status !== 404) {
|
|
665
|
+
const error = await response.text();
|
|
666
|
+
throw new Error(`Platform API error: ${response.status} - ${error}`);
|
|
667
|
+
}
|
|
668
|
+
return response;
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
async start(workflowId: string, input: unknown = {}): Promise<WorkflowHandle> {
|
|
672
|
+
if (typeof workflowId !== 'string' || !workflowId) {
|
|
673
|
+
throw new Error('workflows.start: workflowId must be a non-empty string');
|
|
674
|
+
}
|
|
675
|
+
const res = await this.request(
|
|
676
|
+
`/api/workflows/${encodeURIComponent(workflowId)}/start`,
|
|
677
|
+
{ method: 'POST', body: JSON.stringify(input ?? {}) },
|
|
678
|
+
);
|
|
679
|
+
const { runId } = (await res.json()) as { runId: string };
|
|
680
|
+
return makeRemoteWorkflowHandle(this.baseUrl, this.headers, runId);
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
handle(runId: string): WorkflowHandle {
|
|
684
|
+
if (typeof runId !== 'string' || !runId) {
|
|
685
|
+
throw new Error('workflows.handle: runId must be a non-empty string');
|
|
686
|
+
}
|
|
687
|
+
return makeRemoteWorkflowHandle(this.baseUrl, this.headers, runId);
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
async sendEvent(eventType: string, payload: unknown = null): Promise<number> {
|
|
691
|
+
if (typeof eventType !== 'string' || !eventType) {
|
|
692
|
+
throw new Error('workflows.sendEvent: eventType must be a non-empty string');
|
|
693
|
+
}
|
|
694
|
+
const res = await this.request(`/api/workflows/signal`, {
|
|
695
|
+
method: 'POST',
|
|
696
|
+
body: JSON.stringify({ eventType, payload }),
|
|
697
|
+
});
|
|
698
|
+
const data = (await res.json()) as { count: number };
|
|
699
|
+
return data.count;
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
function makeRemoteWorkflowHandle(
|
|
704
|
+
baseUrl: string,
|
|
705
|
+
headers: Record<string, string>,
|
|
706
|
+
runId: string,
|
|
707
|
+
): WorkflowHandle {
|
|
708
|
+
const doFetch = async (path: string, init: RequestInit = {}): Promise<Response> => {
|
|
709
|
+
const res = await fetch(`${baseUrl}${path}`, {
|
|
710
|
+
...init,
|
|
711
|
+
headers: { ...headers, ...init.headers },
|
|
712
|
+
});
|
|
713
|
+
if (!res.ok && res.status !== 404) {
|
|
714
|
+
throw new Error(`Platform API error: ${res.status} - ${await res.text()}`);
|
|
715
|
+
}
|
|
716
|
+
return res;
|
|
717
|
+
};
|
|
718
|
+
|
|
719
|
+
const handle: WorkflowHandle = {
|
|
720
|
+
runId,
|
|
721
|
+
async status(): Promise<WorkflowRun | null> {
|
|
722
|
+
const res = await doFetch(`/api/workflows/runs/${encodeURIComponent(runId)}`);
|
|
723
|
+
if (res.status === 404) return null;
|
|
724
|
+
return (await res.json()) as WorkflowRun;
|
|
725
|
+
},
|
|
726
|
+
async history(): Promise<WorkflowStepRecord[]> {
|
|
727
|
+
const res = await doFetch(
|
|
728
|
+
`/api/workflows/runs/${encodeURIComponent(runId)}/history`,
|
|
729
|
+
);
|
|
730
|
+
return (await res.json()) as WorkflowStepRecord[];
|
|
731
|
+
},
|
|
732
|
+
async cancel(): Promise<boolean> {
|
|
733
|
+
const res = await doFetch(
|
|
734
|
+
`/api/workflows/runs/${encodeURIComponent(runId)}/cancel`,
|
|
735
|
+
{ method: 'POST' },
|
|
736
|
+
);
|
|
737
|
+
const data = (await res.json()) as { cancelled: boolean };
|
|
738
|
+
return data.cancelled;
|
|
739
|
+
},
|
|
740
|
+
async result(options?: { timeoutMs?: number }): Promise<unknown> {
|
|
741
|
+
const timeoutMs = options?.timeoutMs;
|
|
742
|
+
const deadline =
|
|
743
|
+
typeof timeoutMs === 'number' && Number.isFinite(timeoutMs) && timeoutMs > 0
|
|
744
|
+
? Date.now() + timeoutMs
|
|
745
|
+
: null;
|
|
746
|
+
let delay = 100;
|
|
747
|
+
while (true) {
|
|
748
|
+
const run = await handle.status();
|
|
749
|
+
if (!run) throw new Error(`workflow run not found: ${runId}`);
|
|
750
|
+
if (run.status === 'completed') return run.output;
|
|
751
|
+
if (run.status === 'failed' || run.status === 'cancelled') {
|
|
752
|
+
const err = new Error(`workflow ${run.status}`) as Error & { run: WorkflowRun };
|
|
753
|
+
err.run = run;
|
|
754
|
+
throw err;
|
|
755
|
+
}
|
|
756
|
+
if (deadline !== null && Date.now() >= deadline) {
|
|
757
|
+
throw new Error('workflow result timeout');
|
|
758
|
+
}
|
|
759
|
+
await new Promise((r) => setTimeout(r, delay));
|
|
760
|
+
delay = Math.min(delay * 2, 5000);
|
|
761
|
+
}
|
|
762
|
+
},
|
|
763
|
+
};
|
|
764
|
+
return handle;
|
|
765
|
+
}
|
|
766
|
+
|
|
631
767
|
/**
|
|
632
768
|
* Create a remote platform client for development environments.
|
|
633
769
|
*
|
|
@@ -671,6 +807,7 @@ export function createRemoteClient(baseUrl: string, tenant: string) {
|
|
|
671
807
|
const realtime = new RemoteRealtimeService(baseUrl, headers);
|
|
672
808
|
const auth = new RemoteAuthService(baseUrl, headers);
|
|
673
809
|
const policy = new RemotePolicyService();
|
|
810
|
+
const workflows = new RemoteWorkflows(baseUrl, headers);
|
|
674
811
|
|
|
675
812
|
return {
|
|
676
813
|
env: {
|
|
@@ -682,5 +819,6 @@ export function createRemoteClient(baseUrl: string, tenant: string) {
|
|
|
682
819
|
realtime,
|
|
683
820
|
auth,
|
|
684
821
|
policy,
|
|
822
|
+
workflows,
|
|
685
823
|
};
|
|
686
824
|
}
|
package/src/ren.ts
CHANGED
|
@@ -122,6 +122,12 @@ export class RenClient {
|
|
|
122
122
|
'presence.join',
|
|
123
123
|
'presence.leave',
|
|
124
124
|
'presence.update',
|
|
125
|
+
// Media transform lifecycle (emitted by media-transforms-worker)
|
|
126
|
+
'transform.queued',
|
|
127
|
+
'transform.started',
|
|
128
|
+
'transform.progress',
|
|
129
|
+
'transform.complete',
|
|
130
|
+
'transform.failed',
|
|
125
131
|
// Meta events
|
|
126
132
|
'ren.meta'
|
|
127
133
|
];
|