@norskvideo/ctl-sdk 0.1.50 → 0.1.52
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 +76 -0
- package/fleet.js +70 -0
- package/package.json +5 -1
- package/product-service.d.ts +5 -0
- package/product-service.js +9 -3
package/fleet.d.ts
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
export interface FleetEndpoint {
|
|
2
|
+
transport: string;
|
|
3
|
+
address: string;
|
|
4
|
+
params: Record<string, string>;
|
|
5
|
+
}
|
|
6
|
+
/** What norsk-discovery holds. */
|
|
7
|
+
export interface FleetResource {
|
|
8
|
+
name: string;
|
|
9
|
+
kind: string;
|
|
10
|
+
labels: Record<string, string>;
|
|
11
|
+
endpoints: FleetEndpoint[];
|
|
12
|
+
}
|
|
13
|
+
/** A runtime as the platform knows it: under norsk-mgr its job id,
|
|
14
|
+
* production, replica and job name; under norsk-ctl alone its instance id
|
|
15
|
+
* and product. */
|
|
16
|
+
export interface FleetRuntime {
|
|
17
|
+
id: string;
|
|
18
|
+
namespace: string;
|
|
19
|
+
[field: string]: unknown;
|
|
20
|
+
}
|
|
21
|
+
export interface FleetItem {
|
|
22
|
+
runtime: FleetRuntime;
|
|
23
|
+
/** The platform's own registration of the runtime (`norsk.runtime`), with
|
|
24
|
+
* its `norsk.state`. */
|
|
25
|
+
presence?: FleetResource;
|
|
26
|
+
entries: FleetResource[];
|
|
27
|
+
}
|
|
28
|
+
export interface FleetDirectory {
|
|
29
|
+
items: FleetItem[];
|
|
30
|
+
/** Whether discovery's replay has landed: before it, entries are partial. */
|
|
31
|
+
synced: boolean;
|
|
32
|
+
}
|
|
33
|
+
export interface FleetSelection {
|
|
34
|
+
/** The kinds to read, as literals. `norsk.runtime` lists runtimes by their
|
|
35
|
+
* own registration. */
|
|
36
|
+
kind: string | readonly string[];
|
|
37
|
+
/** Every one present on an entry, with the same value. */
|
|
38
|
+
labels?: Readonly<Record<string, string>>;
|
|
39
|
+
}
|
|
40
|
+
export interface FleetWatchHandlers {
|
|
41
|
+
/** The whole current set, after the snapshot and after every change. */
|
|
42
|
+
onChange?: (items: FleetItem[]) => void;
|
|
43
|
+
onSynced?: (synced: boolean) => void;
|
|
44
|
+
/** The stream failed; the browser retries it. */
|
|
45
|
+
onError?: () => void;
|
|
46
|
+
}
|
|
47
|
+
export interface FleetWatch {
|
|
48
|
+
current(): FleetItem[];
|
|
49
|
+
synced(): boolean;
|
|
50
|
+
close(): void;
|
|
51
|
+
}
|
|
52
|
+
interface EventSourceLike {
|
|
53
|
+
addEventListener(name: string, listener: (e: {
|
|
54
|
+
data: string;
|
|
55
|
+
}) => void): void;
|
|
56
|
+
close(): void;
|
|
57
|
+
}
|
|
58
|
+
export interface FleetOptions {
|
|
59
|
+
/** Where the directory is: `/_fleet` on a dashboard host (the default),
|
|
60
|
+
* `/api/v1/fleet` on the platform's own origin. */
|
|
61
|
+
base?: string;
|
|
62
|
+
fetch?: (url: string, init?: {
|
|
63
|
+
credentials?: "same-origin";
|
|
64
|
+
}) => Promise<Response>;
|
|
65
|
+
EventSource?: new (url: string, init?: {
|
|
66
|
+
withCredentials?: boolean;
|
|
67
|
+
}) => EventSourceLike;
|
|
68
|
+
}
|
|
69
|
+
export interface Fleet {
|
|
70
|
+
query(selection: FleetSelection): Promise<FleetDirectory>;
|
|
71
|
+
watch(selection: FleetSelection, handlers?: FleetWatchHandlers): FleetWatch;
|
|
72
|
+
}
|
|
73
|
+
export declare function fleetClient(options?: FleetOptions): Fleet;
|
|
74
|
+
/** The directory on the page's own origin, as a dashboard host serves it. */
|
|
75
|
+
export declare const fleet: Fleet;
|
|
76
|
+
export {};
|
package/fleet.js
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// The directory, for a dashboard (RFC 0006 §4.5, task 122 K): the runtimes
|
|
2
|
+
// the signed-in user may see, each with what discovery holds under it, read
|
|
3
|
+
// from the page's own origin and filtered by the platform, never by the
|
|
4
|
+
// dashboard.
|
|
5
|
+
//
|
|
6
|
+
// On a dashboard host the directory is `/_fleet/`; in a product's own UI or a
|
|
7
|
+
// dashboard's dev loop it is the platform's API, `/api/v1/fleet/`. The calls
|
|
8
|
+
// are the same either way.
|
|
9
|
+
//
|
|
10
|
+
// Name the kinds a call reads with literals: ctl-dev-kit's dashboard build
|
|
11
|
+
// derives the registration's `uses` from them, and refuses one it cannot read.
|
|
12
|
+
//
|
|
13
|
+
// Browser-only, with no imports, so a dashboard's bundle carries nothing else.
|
|
14
|
+
function queryString(selection) {
|
|
15
|
+
const kinds = typeof selection.kind === "string" ? [selection.kind] : [...selection.kind];
|
|
16
|
+
const params = new URLSearchParams({ kinds: kinds.join(",") });
|
|
17
|
+
const labels = Object.entries(selection.labels ?? {}).map(([k, v]) => `${k}=${v}`);
|
|
18
|
+
if (labels.length > 0)
|
|
19
|
+
params.set("labels", labels.join(","));
|
|
20
|
+
return params.toString();
|
|
21
|
+
}
|
|
22
|
+
export function fleetClient(options = {}) {
|
|
23
|
+
const base = (options.base ?? "/_fleet").replace(/\/+$/, "");
|
|
24
|
+
return {
|
|
25
|
+
async query(selection) {
|
|
26
|
+
const doFetch = options.fetch ?? ((url, init) => globalThis.fetch(url, init));
|
|
27
|
+
const res = await doFetch(`${base}/directory?${queryString(selection)}`, { credentials: "same-origin" });
|
|
28
|
+
if (!res.ok)
|
|
29
|
+
throw new Error(`the directory answered ${res.status}`);
|
|
30
|
+
return (await res.json());
|
|
31
|
+
},
|
|
32
|
+
watch(selection, handlers = {}) {
|
|
33
|
+
const Source = options.EventSource ?? globalThis.EventSource;
|
|
34
|
+
if (Source === undefined)
|
|
35
|
+
throw new Error("this environment has no EventSource");
|
|
36
|
+
const items = new Map();
|
|
37
|
+
let isSynced = false;
|
|
38
|
+
const source = new Source(`${base}/directory/watch?${queryString(selection)}`, { withCredentials: true });
|
|
39
|
+
const changed = () => handlers.onChange?.([...items.values()]);
|
|
40
|
+
const on = (name, apply) => source.addEventListener(name, (e) => apply(JSON.parse(e.data)));
|
|
41
|
+
on("snapshot", (data) => {
|
|
42
|
+
items.clear();
|
|
43
|
+
for (const item of data.items)
|
|
44
|
+
items.set(item.runtime.id, item);
|
|
45
|
+
isSynced = data.synced;
|
|
46
|
+
changed();
|
|
47
|
+
});
|
|
48
|
+
on("upsert", (item) => {
|
|
49
|
+
items.set(item.runtime.id, item);
|
|
50
|
+
changed();
|
|
51
|
+
});
|
|
52
|
+
on("remove", (data) => {
|
|
53
|
+
items.delete(data.id);
|
|
54
|
+
changed();
|
|
55
|
+
});
|
|
56
|
+
on("synced", (data) => {
|
|
57
|
+
isSynced = data.synced;
|
|
58
|
+
handlers.onSynced?.(isSynced);
|
|
59
|
+
});
|
|
60
|
+
source.addEventListener("error", () => handlers.onError?.());
|
|
61
|
+
return {
|
|
62
|
+
current: () => [...items.values()],
|
|
63
|
+
synced: () => isSynced,
|
|
64
|
+
close: () => source.close(),
|
|
65
|
+
};
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
/** The directory on the page's own origin, as a dashboard host serves it. */
|
|
70
|
+
export const fleet = fleetClient();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@norskvideo/ctl-sdk",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.52",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": {
|
|
@@ -15,6 +15,10 @@
|
|
|
15
15
|
"types": "./components/index.d.ts",
|
|
16
16
|
"default": "./components/index.js"
|
|
17
17
|
},
|
|
18
|
+
"./fleet": {
|
|
19
|
+
"types": "./fleet.d.ts",
|
|
20
|
+
"default": "./fleet.js"
|
|
21
|
+
},
|
|
18
22
|
"./frontend-spec": {
|
|
19
23
|
"types": "./frontend-spec.d.ts",
|
|
20
24
|
"default": "./frontend-spec.js"
|
package/product-service.d.ts
CHANGED
|
@@ -171,6 +171,9 @@ export interface ProductServiceOptions {
|
|
|
171
171
|
/** Told once a registration is removed, so the host can drop whatever it
|
|
172
172
|
* holds against the product (role assignments on `product/<name>`). */
|
|
173
173
|
onRemoved?: (productName: string) => void;
|
|
174
|
+
/** Told after every add, remove and reload that succeeds: the host keeps a
|
|
175
|
+
* view of its products elsewhere (norsk-ctl's discovery entries). */
|
|
176
|
+
onChanged?: () => void;
|
|
174
177
|
/**
|
|
175
178
|
* What the daemon asserts when it dials a REGISTERED product's own routes —
|
|
176
179
|
* the config-screen probe and the product-template fetches (RFC 0003 D7's
|
|
@@ -228,6 +231,7 @@ export declare class ProductService {
|
|
|
228
231
|
private reachHostFor;
|
|
229
232
|
private readonly onRolesDeclared?;
|
|
230
233
|
private readonly onRemoved?;
|
|
234
|
+
private readonly onChanged?;
|
|
231
235
|
private readonly reservedRoleNames?;
|
|
232
236
|
/**
|
|
233
237
|
* Product roles travel bare, so a name means one thing on a host. Two
|
|
@@ -248,6 +252,7 @@ export declare class ProductService {
|
|
|
248
252
|
* tracked containerId means running. Dev-mode: externally owned, so probe
|
|
249
253
|
* the dev URL — registered no longer implies running. */
|
|
250
254
|
isRunning(reg: ProductRegistration): Promise<boolean>;
|
|
255
|
+
private changed;
|
|
251
256
|
add(spec: ProductSpec, opts?: AddProductOpts): Promise<AddProductResult>;
|
|
252
257
|
private addSerialised;
|
|
253
258
|
remove(name: string): Promise<void>;
|
package/product-service.js
CHANGED
|
@@ -76,6 +76,7 @@ export class ProductService {
|
|
|
76
76
|
this.onRolesDeclared = opts.onRolesDeclared;
|
|
77
77
|
this.reservedRoleNames = opts.reservedRoleNames;
|
|
78
78
|
this.onRemoved = opts.onRemoved;
|
|
79
|
+
this.onChanged = opts.onChanged;
|
|
79
80
|
this.forgetIdentity = opts.forgetIdentity;
|
|
80
81
|
this.probeHeadersFor = opts.probeHeadersFor;
|
|
81
82
|
}
|
|
@@ -99,6 +100,7 @@ export class ProductService {
|
|
|
99
100
|
}
|
|
100
101
|
onRolesDeclared;
|
|
101
102
|
onRemoved;
|
|
103
|
+
onChanged;
|
|
102
104
|
reservedRoleNames;
|
|
103
105
|
/**
|
|
104
106
|
* Product roles travel bare, so a name means one thing on a host. Two
|
|
@@ -165,8 +167,12 @@ export class ProductService {
|
|
|
165
167
|
return reg.containerId !== undefined;
|
|
166
168
|
return this.isDevUrlAlive(productBaseUrl(reg));
|
|
167
169
|
}
|
|
170
|
+
changed(result) {
|
|
171
|
+
this.onChanged?.();
|
|
172
|
+
return result;
|
|
173
|
+
}
|
|
168
174
|
async add(spec, opts = {}) {
|
|
169
|
-
return this.mutations.run(() => this.addSerialised(spec, opts));
|
|
175
|
+
return this.changed(await this.mutations.run(() => this.addSerialised(spec, opts)));
|
|
170
176
|
}
|
|
171
177
|
async addSerialised(spec, opts) {
|
|
172
178
|
const existing = this.store.read();
|
|
@@ -373,7 +379,7 @@ export class ProductService {
|
|
|
373
379
|
return { registration, warnings };
|
|
374
380
|
}
|
|
375
381
|
async remove(name) {
|
|
376
|
-
return this.mutations.run(() => this.removeSerialised(name));
|
|
382
|
+
return this.changed(await this.mutations.run(() => this.removeSerialised(name)));
|
|
377
383
|
}
|
|
378
384
|
async removeSerialised(name) {
|
|
379
385
|
const existing = this.store.read();
|
|
@@ -400,7 +406,7 @@ export class ProductService {
|
|
|
400
406
|
* stood in for. Like restart, the new container id is persisted before the
|
|
401
407
|
* readiness wait so a timed-out reload leaves a tracked container. */
|
|
402
408
|
async reload(name) {
|
|
403
|
-
return this.mutations.run(() => this.reloadSerialised(name));
|
|
409
|
+
return this.changed(await this.mutations.run(() => this.reloadSerialised(name)));
|
|
404
410
|
}
|
|
405
411
|
async reloadSerialised(name) {
|
|
406
412
|
const target = this.store.read().find((p) => p.name === name);
|