@norskvideo/ctl-sdk 0.1.54 → 0.1.55
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/fleet.d.ts +54 -0
- package/fleet.js +97 -3
- package/package.json +1 -1
package/fleet.d.ts
CHANGED
|
@@ -49,6 +49,44 @@ export interface FleetWatch {
|
|
|
49
49
|
synced(): boolean;
|
|
50
50
|
close(): void;
|
|
51
51
|
}
|
|
52
|
+
/** A declared operation the runtime offers this dashboard. */
|
|
53
|
+
export interface FleetOperation {
|
|
54
|
+
operationId: string;
|
|
55
|
+
method: string;
|
|
56
|
+
/** Under the runtime's instance root, with `{param}`s. */
|
|
57
|
+
path: string;
|
|
58
|
+
live: boolean;
|
|
59
|
+
}
|
|
60
|
+
export interface FleetCallInput {
|
|
61
|
+
params?: Readonly<Record<string, string | number>>;
|
|
62
|
+
query?: Readonly<Record<string, string>>;
|
|
63
|
+
body?: unknown;
|
|
64
|
+
}
|
|
65
|
+
export interface FleetLiveHandlers {
|
|
66
|
+
onOpen?: () => void;
|
|
67
|
+
onMessage?: (data: unknown) => void;
|
|
68
|
+
onClose?: () => void;
|
|
69
|
+
}
|
|
70
|
+
export interface FleetLive {
|
|
71
|
+
send(data: string | ArrayBufferLike | Blob | ArrayBufferView): void;
|
|
72
|
+
close(): void;
|
|
73
|
+
}
|
|
74
|
+
export interface FleetStateDocument<T = unknown> {
|
|
75
|
+
value: T;
|
|
76
|
+
revision: number;
|
|
77
|
+
}
|
|
78
|
+
/** Someone else wrote the document since it was read: `current` is theirs. */
|
|
79
|
+
export declare class FleetStateConflict extends Error {
|
|
80
|
+
readonly current: FleetStateDocument;
|
|
81
|
+
constructor(current: FleetStateDocument);
|
|
82
|
+
}
|
|
83
|
+
interface WebSocketLike {
|
|
84
|
+
addEventListener(name: string, listener: (e: {
|
|
85
|
+
data?: unknown;
|
|
86
|
+
}) => void): void;
|
|
87
|
+
send(data: string | ArrayBufferLike | Blob | ArrayBufferView): void;
|
|
88
|
+
close(): void;
|
|
89
|
+
}
|
|
52
90
|
interface EventSourceLike {
|
|
53
91
|
addEventListener(name: string, listener: (e: {
|
|
54
92
|
data: string;
|
|
@@ -60,15 +98,31 @@ export interface FleetOptions {
|
|
|
60
98
|
* `/api/v1/fleet` on the platform's own origin. */
|
|
61
99
|
base?: string;
|
|
62
100
|
fetch?: (url: string, init?: {
|
|
101
|
+
method?: string;
|
|
102
|
+
headers?: Record<string, string>;
|
|
103
|
+
body?: string;
|
|
63
104
|
credentials?: "same-origin";
|
|
64
105
|
}) => Promise<Response>;
|
|
65
106
|
EventSource?: new (url: string, init?: {
|
|
66
107
|
withCredentials?: boolean;
|
|
67
108
|
}) => EventSourceLike;
|
|
109
|
+
WebSocket?: new (url: string) => WebSocketLike;
|
|
110
|
+
/** The page's origin, for a live operation's ws(s) URL; the page's own by default. */
|
|
111
|
+
origin?: string;
|
|
68
112
|
}
|
|
69
113
|
export interface Fleet {
|
|
70
114
|
query(selection: FleetSelection): Promise<FleetDirectory>;
|
|
71
115
|
watch(selection: FleetSelection, handlers?: FleetWatchHandlers): FleetWatch;
|
|
116
|
+
/** Call a runtime's declared operation; its JSON answer, or its text. */
|
|
117
|
+
call<T = unknown>(runtimeId: string, operationId: string, input?: FleetCallInput): Promise<T>;
|
|
118
|
+
/** Open a runtime's declared live operation. */
|
|
119
|
+
live(runtimeId: string, operationId: string, handlers: FleetLiveHandlers, input?: FleetCallInput): Promise<FleetLive>;
|
|
120
|
+
state: {
|
|
121
|
+
get<T = unknown>(key: string): Promise<FleetStateDocument<T> | undefined>;
|
|
122
|
+
/** Write at `revision` (omit for a new key); a FleetStateConflict when
|
|
123
|
+
* someone else wrote since. */
|
|
124
|
+
put<T = unknown>(key: string, value: T, revision?: number): Promise<FleetStateDocument<T>>;
|
|
125
|
+
};
|
|
72
126
|
}
|
|
73
127
|
export declare function fleetClient(options?: FleetOptions): Fleet;
|
|
74
128
|
/** The directory on the page's own origin, as a dashboard host serves it. */
|
package/fleet.js
CHANGED
|
@@ -7,10 +7,34 @@
|
|
|
7
7
|
// dashboard's dev loop it is the platform's API, `/api/v1/fleet/`. The calls
|
|
8
8
|
// are the same either way.
|
|
9
9
|
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
10
|
+
// A dashboard also calls a runtime's declared operations (`fleet.call`),
|
|
11
|
+
// opens its live ones (`fleet.live`), and keeps shared state on its host
|
|
12
|
+
// (`fleet.state`); the platform decides each for the signed-in user.
|
|
13
|
+
//
|
|
14
|
+
// Name the kinds a query or watch reads, and the operation a call or live
|
|
15
|
+
// opens, with literals: ctl-dev-kit's dashboard build derives the
|
|
16
|
+
// registration's `uses` from them, and refuses one it cannot read.
|
|
12
17
|
//
|
|
13
18
|
// Browser-only, with no imports, so a dashboard's bundle carries nothing else.
|
|
19
|
+
/** Someone else wrote the document since it was read: `current` is theirs. */
|
|
20
|
+
export class FleetStateConflict extends Error {
|
|
21
|
+
current;
|
|
22
|
+
constructor(current) {
|
|
23
|
+
super("the dashboard state changed since it was read");
|
|
24
|
+
this.current = current;
|
|
25
|
+
this.name = "FleetStateConflict";
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
function resolvePath(template, input) {
|
|
29
|
+
const path = template.replace(/\{([^}]+)\}/g, (_m, name) => {
|
|
30
|
+
const value = input.params?.[name];
|
|
31
|
+
if (value === undefined)
|
|
32
|
+
throw new Error(`the operation needs parameter ${name}`);
|
|
33
|
+
return encodeURIComponent(String(value));
|
|
34
|
+
});
|
|
35
|
+
const query = input.query && Object.keys(input.query).length > 0 ? `?${new URLSearchParams(input.query)}` : "";
|
|
36
|
+
return `${path}${query}`;
|
|
37
|
+
}
|
|
14
38
|
function queryString(selection) {
|
|
15
39
|
const kinds = typeof selection.kind === "string" ? [selection.kind] : [...selection.kind];
|
|
16
40
|
const params = new URLSearchParams({ kinds: kinds.join(",") });
|
|
@@ -21,9 +45,79 @@ function queryString(selection) {
|
|
|
21
45
|
}
|
|
22
46
|
export function fleetClient(options = {}) {
|
|
23
47
|
const base = (options.base ?? "/_fleet").replace(/\/+$/, "");
|
|
48
|
+
const doFetch = options.fetch ?? ((url, init) => globalThis.fetch(url, init));
|
|
49
|
+
const tables = new Map();
|
|
50
|
+
const operationOf = async (runtimeId, operationId) => {
|
|
51
|
+
let table = tables.get(runtimeId);
|
|
52
|
+
if (table === undefined) {
|
|
53
|
+
table = doFetch(`${base}/operations/${encodeURIComponent(runtimeId)}`, { credentials: "same-origin" }).then(async (res) => {
|
|
54
|
+
if (!res.ok)
|
|
55
|
+
throw new Error(`the runtime's operations answered ${res.status}`);
|
|
56
|
+
return (await res.json()).operations;
|
|
57
|
+
});
|
|
58
|
+
tables.set(runtimeId, table);
|
|
59
|
+
table.catch(() => tables.delete(runtimeId));
|
|
60
|
+
}
|
|
61
|
+
const op = (await table).find((o) => o.operationId === operationId);
|
|
62
|
+
if (op === undefined)
|
|
63
|
+
throw new Error(`runtime ${runtimeId} offers this dashboard no operation ${operationId}`);
|
|
64
|
+
return op;
|
|
65
|
+
};
|
|
24
66
|
return {
|
|
67
|
+
async call(runtimeId, operationId, input = {}) {
|
|
68
|
+
const op = await operationOf(runtimeId, operationId);
|
|
69
|
+
if (op.live)
|
|
70
|
+
throw new Error(`${operationId} is a live operation: open it with fleet.live`);
|
|
71
|
+
const hasBody = input.body !== undefined && op.method !== "GET" && op.method !== "HEAD";
|
|
72
|
+
const res = await doFetch(`${base}/api/${encodeURIComponent(runtimeId)}${resolvePath(op.path, input)}`, {
|
|
73
|
+
method: op.method,
|
|
74
|
+
credentials: "same-origin",
|
|
75
|
+
...(hasBody ? { headers: { "content-type": "application/json" }, body: JSON.stringify(input.body) } : {}),
|
|
76
|
+
});
|
|
77
|
+
if (!res.ok)
|
|
78
|
+
throw new Error(`${operationId} answered ${res.status}: ${await res.text()}`);
|
|
79
|
+
const type = res.headers.get("content-type") ?? "";
|
|
80
|
+
return (type.includes("json") ? await res.json() : await res.text());
|
|
81
|
+
},
|
|
82
|
+
async live(runtimeId, operationId, handlers, input = {}) {
|
|
83
|
+
const op = await operationOf(runtimeId, operationId);
|
|
84
|
+
if (!op.live)
|
|
85
|
+
throw new Error(`${operationId} is not a live operation: call it with fleet.call`);
|
|
86
|
+
const Socket = options.WebSocket ?? globalThis.WebSocket;
|
|
87
|
+
if (Socket === undefined)
|
|
88
|
+
throw new Error("this environment has no WebSocket");
|
|
89
|
+
const origin = options.origin ?? globalThis.location?.origin ?? "";
|
|
90
|
+
const ws = origin.replace(/^http/, "ws");
|
|
91
|
+
const socket = new Socket(`${ws}${base}/live/${encodeURIComponent(runtimeId)}${resolvePath(op.path, input)}`);
|
|
92
|
+
socket.addEventListener("open", () => handlers.onOpen?.());
|
|
93
|
+
socket.addEventListener("message", (e) => handlers.onMessage?.(e.data));
|
|
94
|
+
socket.addEventListener("close", () => handlers.onClose?.());
|
|
95
|
+
return { send: (data) => socket.send(data), close: () => socket.close() };
|
|
96
|
+
},
|
|
97
|
+
state: {
|
|
98
|
+
async get(key) {
|
|
99
|
+
const res = await doFetch(`${base}/state/${encodeURIComponent(key)}`, { credentials: "same-origin" });
|
|
100
|
+
if (res.status === 404)
|
|
101
|
+
return undefined;
|
|
102
|
+
if (!res.ok)
|
|
103
|
+
throw new Error(`the dashboard state answered ${res.status}`);
|
|
104
|
+
return (await res.json());
|
|
105
|
+
},
|
|
106
|
+
async put(key, value, revision) {
|
|
107
|
+
const res = await doFetch(`${base}/state/${encodeURIComponent(key)}`, {
|
|
108
|
+
method: "PUT",
|
|
109
|
+
credentials: "same-origin",
|
|
110
|
+
headers: { "content-type": "application/json" },
|
|
111
|
+
body: JSON.stringify({ value, ...(revision !== undefined ? { revision } : {}) }),
|
|
112
|
+
});
|
|
113
|
+
if (res.status === 409)
|
|
114
|
+
throw new FleetStateConflict((await res.json()).current);
|
|
115
|
+
if (!res.ok)
|
|
116
|
+
throw new Error(`the dashboard state answered ${res.status}: ${await res.text()}`);
|
|
117
|
+
return (await res.json());
|
|
118
|
+
},
|
|
119
|
+
},
|
|
25
120
|
async query(selection) {
|
|
26
|
-
const doFetch = options.fetch ?? ((url, init) => globalThis.fetch(url, init));
|
|
27
121
|
const res = await doFetch(`${base}/directory?${queryString(selection)}`, { credentials: "same-origin" });
|
|
28
122
|
if (!res.ok)
|
|
29
123
|
throw new Error(`the directory answered ${res.status}`);
|