@helioslx/core 0.2.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/CHANGELOG.md +44 -0
- package/CONTRIBUTING.md +83 -0
- package/LICENSE +256 -0
- package/README.md +159 -0
- package/SECURITY.md +67 -0
- package/dist/contracts-C4kpAdZq.d.ts +265 -0
- package/dist/contracts-C4kpAdZq.d.ts.map +1 -0
- package/dist/http.d.ts +643 -0
- package/dist/http.d.ts.map +1 -0
- package/dist/http.js +670 -0
- package/dist/http.js.map +1 -0
- package/dist/index.d.ts +40 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +270 -0
- package/dist/index.js.map +1 -0
- package/dist/node.d.ts +114 -0
- package/dist/node.d.ts.map +1 -0
- package/dist/node.js +334 -0
- package/dist/node.js.map +1 -0
- package/dist/redis.d.ts +46 -0
- package/dist/redis.d.ts.map +1 -0
- package/dist/redis.js +174 -0
- package/dist/redis.js.map +1 -0
- package/dist/source-DB1oq2GT.js +884 -0
- package/dist/source-DB1oq2GT.js.map +1 -0
- package/dist/source-D_XNWXS9.d.ts +76 -0
- package/dist/source-D_XNWXS9.d.ts.map +1 -0
- package/dist/testing.d.ts +38 -0
- package/dist/testing.d.ts.map +1 -0
- package/dist/testing.js +98 -0
- package/dist/testing.js.map +1 -0
- package/dist/validation-BdsVeyAE.js +151 -0
- package/dist/validation-BdsVeyAE.js.map +1 -0
- package/examples/embedded-elysia-http.ts +30 -0
- package/examples/full-frame-fade.ts +25 -0
- package/examples/graceful-shutdown.ts +19 -0
- package/examples/quickstart.ts +30 -0
- package/examples/redis.ts +30 -0
- package/examples/sparse-channels.ts +23 -0
- package/examples/viewer-subscription.ts +38 -0
- package/examples/viewer-tui.ts +201 -0
- package/package.json +108 -0
- package/src/contracts.ts +302 -0
- package/src/http.ts +1101 -0
- package/src/index.ts +61 -0
- package/src/memory-store.ts +45 -0
- package/src/node.ts +578 -0
- package/src/output-engine.ts +778 -0
- package/src/redis.ts +258 -0
- package/src/source.ts +502 -0
- package/src/testing.ts +139 -0
- package/src/validation.ts +328 -0
- package/src/viewer.ts +368 -0
package/dist/http.js
ADDED
|
@@ -0,0 +1,670 @@
|
|
|
1
|
+
import { a as SacnValidationError, i as SacnLifecycleError, n as PersistenceError, o as TransportError, t as DependencyUnavailableError } from "./validation-BdsVeyAE.js";
|
|
2
|
+
import { Elysia, t } from "elysia";
|
|
3
|
+
import { openapi } from "@elysiajs/openapi";
|
|
4
|
+
import { node } from "@elysiajs/node";
|
|
5
|
+
//#region src/http.ts
|
|
6
|
+
const outputParamsSchema = t.Object({
|
|
7
|
+
universe: t.String({ pattern: "^-?\\d+$" }),
|
|
8
|
+
priority: t.String({ pattern: "^-?\\d+$" })
|
|
9
|
+
});
|
|
10
|
+
const frameValuesSchema = t.Array(t.Integer({
|
|
11
|
+
minimum: 0,
|
|
12
|
+
maximum: 255
|
|
13
|
+
}), {
|
|
14
|
+
minItems: 512,
|
|
15
|
+
maxItems: 512
|
|
16
|
+
});
|
|
17
|
+
const outputOptionsSchema = {
|
|
18
|
+
cid: t.Optional(t.String()),
|
|
19
|
+
idleFps: t.Optional(t.Number({
|
|
20
|
+
exclusiveMinimum: 0,
|
|
21
|
+
maximum: 1e3
|
|
22
|
+
})),
|
|
23
|
+
sourceName: t.Optional(t.String({
|
|
24
|
+
minLength: 1,
|
|
25
|
+
maxLength: 64
|
|
26
|
+
}))
|
|
27
|
+
};
|
|
28
|
+
const frameBodySchema = t.Object({
|
|
29
|
+
values: frameValuesSchema,
|
|
30
|
+
durationMs: t.Optional(t.Number({ minimum: 0 })),
|
|
31
|
+
...outputOptionsSchema
|
|
32
|
+
});
|
|
33
|
+
const upsertBodySchema = t.Object({
|
|
34
|
+
initialValues: t.Optional(frameValuesSchema),
|
|
35
|
+
values: t.Optional(frameValuesSchema),
|
|
36
|
+
durationMs: t.Optional(t.Number({ minimum: 0 })),
|
|
37
|
+
...outputOptionsSchema
|
|
38
|
+
});
|
|
39
|
+
const channelWritesSchema = t.Array(t.Object({
|
|
40
|
+
channel: t.Integer({
|
|
41
|
+
minimum: 1,
|
|
42
|
+
maximum: 512
|
|
43
|
+
}),
|
|
44
|
+
value: t.Integer({
|
|
45
|
+
minimum: 0,
|
|
46
|
+
maximum: 255
|
|
47
|
+
}),
|
|
48
|
+
durationMs: t.Optional(t.Number({ minimum: 0 }))
|
|
49
|
+
}), { minItems: 1 });
|
|
50
|
+
const channelsBodySchema = t.Union([t.Object({
|
|
51
|
+
channels: t.Union([channelWritesSchema, t.Record(t.String(), t.Integer({
|
|
52
|
+
minimum: 0,
|
|
53
|
+
maximum: 255
|
|
54
|
+
}))]),
|
|
55
|
+
durationMs: t.Optional(t.Number({ minimum: 0 })),
|
|
56
|
+
...outputOptionsSchema
|
|
57
|
+
}), t.Object({
|
|
58
|
+
changes: channelWritesSchema,
|
|
59
|
+
durationMs: t.Optional(t.Number({ minimum: 0 })),
|
|
60
|
+
...outputOptionsSchema
|
|
61
|
+
})]);
|
|
62
|
+
const outputSnapshotSchema = t.Object({
|
|
63
|
+
activeTransitions: t.Integer({ minimum: 0 }),
|
|
64
|
+
cid: t.String(),
|
|
65
|
+
current: frameValuesSchema,
|
|
66
|
+
dirty: t.Boolean(),
|
|
67
|
+
frameMode: t.Union([t.Literal("active"), t.Literal("idle")]),
|
|
68
|
+
idleFps: t.Number(),
|
|
69
|
+
lastError: t.Union([t.String(), t.Null()]),
|
|
70
|
+
lastSent: frameValuesSchema,
|
|
71
|
+
lastSentAt: t.Union([t.Number(), t.Null()]),
|
|
72
|
+
nextDueAt: t.Number(),
|
|
73
|
+
priority: t.Integer(),
|
|
74
|
+
sequence: t.Integer(),
|
|
75
|
+
sourceName: t.Union([t.String(), t.Null()]),
|
|
76
|
+
target: frameValuesSchema,
|
|
77
|
+
universe: t.Integer(),
|
|
78
|
+
updatedAt: t.Number()
|
|
79
|
+
});
|
|
80
|
+
const errorSchema = t.Object({ error: t.String() });
|
|
81
|
+
const viewerStateSchema = t.Object({ universes: t.Array(t.Integer({
|
|
82
|
+
minimum: 1,
|
|
83
|
+
maximum: 63999
|
|
84
|
+
})) });
|
|
85
|
+
const engineTelemetrySchema = t.Object({
|
|
86
|
+
closed: t.Boolean(),
|
|
87
|
+
running: t.Boolean(),
|
|
88
|
+
outputCount: t.Integer({ minimum: 0 }),
|
|
89
|
+
outputs: t.Array(outputSnapshotSchema),
|
|
90
|
+
loopIterations: t.Integer({ minimum: 0 }),
|
|
91
|
+
lastLoopStartedAt: t.Union([t.Number(), t.Null()]),
|
|
92
|
+
lastLoopCompletedAt: t.Union([t.Number(), t.Null()]),
|
|
93
|
+
lastLoopDurationMs: t.Union([t.Number(), t.Null()]),
|
|
94
|
+
transport: t.Object({
|
|
95
|
+
sendAttempts: t.Integer({ minimum: 0 }),
|
|
96
|
+
sendFailures: t.Integer({ minimum: 0 }),
|
|
97
|
+
sendRetries: t.Integer({ minimum: 0 }),
|
|
98
|
+
sendSuccesses: t.Integer({ minimum: 0 }),
|
|
99
|
+
sendTimeouts: t.Integer({ minimum: 0 })
|
|
100
|
+
})
|
|
101
|
+
});
|
|
102
|
+
const viewerTelemetrySchema = t.Object({
|
|
103
|
+
droppedUpdates: t.Integer({ minimum: 0 }),
|
|
104
|
+
packetsReceived: t.Integer({ minimum: 0 }),
|
|
105
|
+
selectedUniverses: t.Array(t.Integer()),
|
|
106
|
+
streamCount: t.Integer({ minimum: 0 })
|
|
107
|
+
});
|
|
108
|
+
const viewerUniverseParamsSchema = t.Object({ universe: t.String({ pattern: "^-?\\d+$" }) });
|
|
109
|
+
const errorResponseDocument = (description) => ({
|
|
110
|
+
description,
|
|
111
|
+
content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } }
|
|
112
|
+
});
|
|
113
|
+
const outputResponseDocument = {
|
|
114
|
+
200: {
|
|
115
|
+
description: "Output snapshot",
|
|
116
|
+
content: { "application/json": { schema: { $ref: "#/components/schemas/OutputSnapshot" } } }
|
|
117
|
+
},
|
|
118
|
+
400: errorResponseDocument("Invalid request"),
|
|
119
|
+
409: errorResponseDocument("Lifecycle conflict"),
|
|
120
|
+
503: errorResponseDocument("Transport or persistence unavailable")
|
|
121
|
+
};
|
|
122
|
+
const outputLookupResponseDocument = {
|
|
123
|
+
...outputResponseDocument,
|
|
124
|
+
404: errorResponseDocument("Output not found")
|
|
125
|
+
};
|
|
126
|
+
const clearResponseDocument = {
|
|
127
|
+
200: {
|
|
128
|
+
description: "Output cleared",
|
|
129
|
+
content: { "application/json": { schema: {
|
|
130
|
+
type: "object",
|
|
131
|
+
required: ["ok"],
|
|
132
|
+
properties: { ok: {
|
|
133
|
+
type: "boolean",
|
|
134
|
+
const: true
|
|
135
|
+
} }
|
|
136
|
+
} } }
|
|
137
|
+
},
|
|
138
|
+
400: errorResponseDocument("Invalid request"),
|
|
139
|
+
404: errorResponseDocument("Output not found"),
|
|
140
|
+
409: errorResponseDocument("Lifecycle conflict"),
|
|
141
|
+
503: errorResponseDocument("Transport or persistence unavailable")
|
|
142
|
+
};
|
|
143
|
+
const componentResponse = (schema, description) => ({
|
|
144
|
+
200: {
|
|
145
|
+
description,
|
|
146
|
+
content: { "application/json": { schema: { $ref: `#/components/schemas/${schema}` } } }
|
|
147
|
+
},
|
|
148
|
+
400: errorResponseDocument("Invalid request"),
|
|
149
|
+
409: errorResponseDocument("Lifecycle conflict"),
|
|
150
|
+
503: errorResponseDocument("Dependency unavailable")
|
|
151
|
+
});
|
|
152
|
+
const integerParam = (value, label) => {
|
|
153
|
+
if (!/^-?\d+$/.test(value)) throw new SacnValidationError(`${label} must be an integer.`, label === "Universe" ? "INVALID_UNIVERSE" : "INVALID_PRIORITY");
|
|
154
|
+
return Number(value);
|
|
155
|
+
};
|
|
156
|
+
const addressOf = (params) => ({
|
|
157
|
+
universe: integerParam(params.universe, "Universe"),
|
|
158
|
+
priority: integerParam(params.priority, "Priority")
|
|
159
|
+
});
|
|
160
|
+
const bodyObject = (body) => {
|
|
161
|
+
if (!body || typeof body !== "object" || Array.isArray(body)) throw new SacnValidationError("Request body must be an object.", "INVALID_FRAME");
|
|
162
|
+
return body;
|
|
163
|
+
};
|
|
164
|
+
const messageOf = (error) => error instanceof Error ? error.message : "Internal server error.";
|
|
165
|
+
const statusOf = (error) => {
|
|
166
|
+
if (error instanceof SacnValidationError) return 400;
|
|
167
|
+
if (error instanceof SacnLifecycleError) return 409;
|
|
168
|
+
if (error instanceof DependencyUnavailableError || error instanceof PersistenceError || error instanceof TransportError) return 503;
|
|
169
|
+
return 500;
|
|
170
|
+
};
|
|
171
|
+
const fail = (set, error) => {
|
|
172
|
+
set.status = statusOf(error);
|
|
173
|
+
return { error: messageOf(error) };
|
|
174
|
+
};
|
|
175
|
+
const deprecated = (set, successor, sunset) => {
|
|
176
|
+
set.headers.deprecation = "true";
|
|
177
|
+
set.headers.link = `<${successor}>; rel="successor-version"`;
|
|
178
|
+
set.headers.sunset = sunset;
|
|
179
|
+
};
|
|
180
|
+
const channelsFrom = (body) => {
|
|
181
|
+
const record = bodyObject(body);
|
|
182
|
+
const sharedDuration = typeof record.durationMs === "number" ? record.durationMs : void 0;
|
|
183
|
+
const value = record.channels ?? record.changes;
|
|
184
|
+
const options = {
|
|
185
|
+
...sharedDuration === void 0 ? {} : { durationMs: sharedDuration },
|
|
186
|
+
...record.idleFps === void 0 ? {} : { idleFps: record.idleFps },
|
|
187
|
+
...record.sourceName === void 0 ? {} : { sourceName: record.sourceName },
|
|
188
|
+
...record.cid === void 0 ? {} : { cid: record.cid }
|
|
189
|
+
};
|
|
190
|
+
if (value !== null && typeof value === "object" && !Array.isArray(value)) {
|
|
191
|
+
const values = {};
|
|
192
|
+
for (const [key, channelValue] of Object.entries(value)) values[Number(key)] = channelValue;
|
|
193
|
+
if (sharedDuration !== void 0 && sharedDuration > 0) return {
|
|
194
|
+
mode: "fade",
|
|
195
|
+
values,
|
|
196
|
+
...options
|
|
197
|
+
};
|
|
198
|
+
return {
|
|
199
|
+
mode: "set",
|
|
200
|
+
values,
|
|
201
|
+
...options
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
if (!Array.isArray(value)) throw new SacnValidationError("Body must contain a channels array or map.", "INVALID_CHANNEL");
|
|
205
|
+
const writes = value.map((item) => {
|
|
206
|
+
const change = bodyObject(item);
|
|
207
|
+
const durationMs = typeof change.durationMs === "number" ? change.durationMs : void 0;
|
|
208
|
+
return {
|
|
209
|
+
channel: change.channel,
|
|
210
|
+
value: change.value,
|
|
211
|
+
...durationMs === void 0 ? {} : { durationMs }
|
|
212
|
+
};
|
|
213
|
+
});
|
|
214
|
+
if (writes.some((write) => write.durationMs !== void 0 && write.durationMs > 0)) return {
|
|
215
|
+
mode: "transition",
|
|
216
|
+
writes: writes.map((write) => ({
|
|
217
|
+
channel: write.channel,
|
|
218
|
+
value: write.value,
|
|
219
|
+
durationMs: write.durationMs ?? sharedDuration ?? 0
|
|
220
|
+
})),
|
|
221
|
+
...options
|
|
222
|
+
};
|
|
223
|
+
const values = {};
|
|
224
|
+
for (const write of writes) values[write.channel] = write.value;
|
|
225
|
+
if (sharedDuration !== void 0 && sharedDuration > 0) return {
|
|
226
|
+
mode: "fade",
|
|
227
|
+
values,
|
|
228
|
+
...options
|
|
229
|
+
};
|
|
230
|
+
return {
|
|
231
|
+
mode: "set",
|
|
232
|
+
values,
|
|
233
|
+
...options
|
|
234
|
+
};
|
|
235
|
+
};
|
|
236
|
+
const frameFrom = (body) => {
|
|
237
|
+
const record = bodyObject(body);
|
|
238
|
+
const values = record.values ?? record.initialValues;
|
|
239
|
+
if (!Array.isArray(values) && !(values instanceof Uint8Array)) throw new SacnValidationError("Body must contain a values array.", "INVALID_FRAME");
|
|
240
|
+
const durationMs = record.durationMs;
|
|
241
|
+
return {
|
|
242
|
+
values,
|
|
243
|
+
...durationMs === void 0 ? {} : { durationMs },
|
|
244
|
+
...record.idleFps === void 0 ? {} : { idleFps: record.idleFps },
|
|
245
|
+
...record.sourceName === void 0 ? {} : { sourceName: record.sourceName },
|
|
246
|
+
...record.cid === void 0 ? {} : { cid: record.cid }
|
|
247
|
+
};
|
|
248
|
+
};
|
|
249
|
+
const universeOf = (source, params, extras = {}) => {
|
|
250
|
+
const address = addressOf(params);
|
|
251
|
+
return source.universe(address.universe, {
|
|
252
|
+
priority: address.priority,
|
|
253
|
+
...extras
|
|
254
|
+
});
|
|
255
|
+
};
|
|
256
|
+
const applyChannels = async (universe, parsed) => {
|
|
257
|
+
if (parsed.mode === "fade" && parsed.values) return universe.fadeChannels(parsed.values, { durationMs: parsed.durationMs ?? 0 });
|
|
258
|
+
if (parsed.mode === "transition" && parsed.writes) return universe.transition(parsed.writes);
|
|
259
|
+
if (parsed.values) return universe.setChannels(parsed.values);
|
|
260
|
+
throw new SacnValidationError("Body must contain channel values.", "INVALID_CHANNEL");
|
|
261
|
+
};
|
|
262
|
+
/**
|
|
263
|
+
* Creates an unbound Elysia route plugin. CORS and server binding deliberately
|
|
264
|
+
* remain the host application's responsibility.
|
|
265
|
+
*/
|
|
266
|
+
const createSacnHttpAdapter = (options) => {
|
|
267
|
+
const prefix = options.prefix ?? "/sacn";
|
|
268
|
+
const sunset = options.sunset ?? "Wed, 01 Jul 2027 00:00:00 GMT";
|
|
269
|
+
const maxWebSocketClients = options.maxWebSocketClients ?? 64;
|
|
270
|
+
const webSocketQueueCapacity = options.webSocketQueueCapacity ?? 32;
|
|
271
|
+
if (!Number.isInteger(maxWebSocketClients) || maxWebSocketClients < 1) throw new SacnValidationError("Maximum WebSocket clients must be a positive integer.", "INVALID_FRAME");
|
|
272
|
+
if (!Number.isInteger(webSocketQueueCapacity) || webSocketQueueCapacity < 1) throw new SacnValidationError("WebSocket queue capacity must be a positive integer.", "INVALID_FRAME");
|
|
273
|
+
const clients = /* @__PURE__ */ new Set();
|
|
274
|
+
const closeClient = (client) => {
|
|
275
|
+
clients.delete(client);
|
|
276
|
+
client.pending.clear();
|
|
277
|
+
try {
|
|
278
|
+
client.socket.close();
|
|
279
|
+
} catch {}
|
|
280
|
+
};
|
|
281
|
+
const flushClient = (client) => {
|
|
282
|
+
client.scheduled = false;
|
|
283
|
+
const packets = [...client.pending.values()];
|
|
284
|
+
client.pending.clear();
|
|
285
|
+
for (const packet of packets) try {
|
|
286
|
+
const status = client.socket.send({
|
|
287
|
+
packet,
|
|
288
|
+
type: "viewer-packet"
|
|
289
|
+
});
|
|
290
|
+
if (typeof status === "number" && status <= 0) {
|
|
291
|
+
closeClient(client);
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
} catch {
|
|
295
|
+
closeClient(client);
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
};
|
|
299
|
+
const dispatchViewerPacket = (packet) => {
|
|
300
|
+
try {
|
|
301
|
+
options.onViewerPacket?.(packet);
|
|
302
|
+
} catch {}
|
|
303
|
+
for (const client of clients) {
|
|
304
|
+
if (client.pending.has(packet.universe)) client.pending.delete(packet.universe);
|
|
305
|
+
else if (client.pending.size >= webSocketQueueCapacity) {
|
|
306
|
+
const oldestUniverse = client.pending.keys().next().value;
|
|
307
|
+
if (oldestUniverse !== void 0) client.pending.delete(oldestUniverse);
|
|
308
|
+
}
|
|
309
|
+
client.pending.set(packet.universe, packet);
|
|
310
|
+
if (!client.scheduled) {
|
|
311
|
+
client.scheduled = true;
|
|
312
|
+
queueMicrotask(() => flushClient(client));
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
};
|
|
316
|
+
const unsubscribePackets = options.viewer ? options.viewer.subscribe(dispatchViewerPacket) : void 0;
|
|
317
|
+
const app = new Elysia({
|
|
318
|
+
adapter: node(),
|
|
319
|
+
prefix
|
|
320
|
+
}).use(openapi({
|
|
321
|
+
path: options.openapiPath ?? "/openapi",
|
|
322
|
+
documentation: {
|
|
323
|
+
components: { schemas: {
|
|
324
|
+
Error: errorSchema,
|
|
325
|
+
EngineTelemetry: engineTelemetrySchema,
|
|
326
|
+
OutputSnapshot: outputSnapshotSchema,
|
|
327
|
+
ViewerState: viewerStateSchema,
|
|
328
|
+
ViewerTelemetry: viewerTelemetrySchema
|
|
329
|
+
} },
|
|
330
|
+
info: {
|
|
331
|
+
title: "@helioslx/core sACN API",
|
|
332
|
+
version: "0.1.0"
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
})).onBeforeHandle(async ({ request, set }) => {
|
|
336
|
+
try {
|
|
337
|
+
const result = await options.auth?.({ request });
|
|
338
|
+
if (result instanceof Response) return result;
|
|
339
|
+
if (result === false) {
|
|
340
|
+
set.status = 401;
|
|
341
|
+
return { error: "Unauthorized." };
|
|
342
|
+
}
|
|
343
|
+
} catch (error) {
|
|
344
|
+
return fail(set, error);
|
|
345
|
+
}
|
|
346
|
+
}).onError(({ code, error, set }) => {
|
|
347
|
+
if (code === "VALIDATION") {
|
|
348
|
+
set.status = 400;
|
|
349
|
+
return { error: error.message };
|
|
350
|
+
}
|
|
351
|
+
}).onStop(() => {
|
|
352
|
+
unsubscribePackets?.();
|
|
353
|
+
clients.clear();
|
|
354
|
+
}).get("/health/live", () => ({ status: "ok" }), { response: { 200: t.Object({ status: t.Literal("ok") }) } }).get("/health/ready", ({ set }) => {
|
|
355
|
+
const telemetry = options.source.getTelemetry();
|
|
356
|
+
if (!telemetry.running || telemetry.closed) {
|
|
357
|
+
set.status = 503;
|
|
358
|
+
return { status: "not-ready" };
|
|
359
|
+
}
|
|
360
|
+
return { status: "ready" };
|
|
361
|
+
}, { response: {
|
|
362
|
+
200: t.Object({ status: t.Literal("ready") }),
|
|
363
|
+
503: t.Object({ status: t.Literal("not-ready") })
|
|
364
|
+
} }).get("/universes", async ({ set }) => {
|
|
365
|
+
try {
|
|
366
|
+
return await options.source.listUniverses();
|
|
367
|
+
} catch (error) {
|
|
368
|
+
return fail(set, error);
|
|
369
|
+
}
|
|
370
|
+
}, { detail: { responses: {
|
|
371
|
+
200: {
|
|
372
|
+
description: "Active outputs",
|
|
373
|
+
content: { "application/json": { schema: {
|
|
374
|
+
type: "array",
|
|
375
|
+
items: { $ref: "#/components/schemas/OutputSnapshot" }
|
|
376
|
+
} } }
|
|
377
|
+
},
|
|
378
|
+
409: errorResponseDocument("Lifecycle conflict"),
|
|
379
|
+
503: errorResponseDocument("Persistence unavailable")
|
|
380
|
+
} } }).get("/engine/telemetry", () => options.source.getTelemetry(), { detail: { responses: componentResponse("EngineTelemetry", "Engine telemetry") } }).get("/universes/:universe/priorities/:priority", async ({ params, set }) => {
|
|
381
|
+
try {
|
|
382
|
+
const output = await universeOf(options.source, params).get();
|
|
383
|
+
if (!output) {
|
|
384
|
+
set.status = 404;
|
|
385
|
+
return { error: "Output not found." };
|
|
386
|
+
}
|
|
387
|
+
return output;
|
|
388
|
+
} catch (error) {
|
|
389
|
+
return fail(set, error);
|
|
390
|
+
}
|
|
391
|
+
}, {
|
|
392
|
+
detail: { responses: outputLookupResponseDocument },
|
|
393
|
+
params: outputParamsSchema
|
|
394
|
+
}).put("/universes/:universe/priorities/:priority", async ({ body, params, set }) => {
|
|
395
|
+
try {
|
|
396
|
+
const record = bodyObject(body);
|
|
397
|
+
const frame = frameFrom({
|
|
398
|
+
...record,
|
|
399
|
+
values: record.initialValues ?? record.values ?? Array.from({ length: 512 }, () => 0)
|
|
400
|
+
});
|
|
401
|
+
const universe = universeOf(options.source, params, {
|
|
402
|
+
...frame.cid === void 0 ? {} : { cid: frame.cid },
|
|
403
|
+
...frame.idleFps === void 0 ? {} : { idleFps: frame.idleFps },
|
|
404
|
+
...frame.sourceName === void 0 ? {} : { sourceName: frame.sourceName }
|
|
405
|
+
});
|
|
406
|
+
const existing = await universe.get();
|
|
407
|
+
const supplied = record.initialValues ?? record.values;
|
|
408
|
+
const values = Array.isArray(supplied) || supplied instanceof Uint8Array ? supplied : existing?.target ?? Array.from({ length: 512 }, () => 0);
|
|
409
|
+
return await universe.write(values, { ...frame.durationMs === void 0 ? {} : { durationMs: frame.durationMs } });
|
|
410
|
+
} catch (error) {
|
|
411
|
+
return fail(set, error);
|
|
412
|
+
}
|
|
413
|
+
}, {
|
|
414
|
+
body: upsertBodySchema,
|
|
415
|
+
detail: { responses: outputResponseDocument },
|
|
416
|
+
params: outputParamsSchema
|
|
417
|
+
}).post("/universes/:universe/priorities/:priority/channels", async ({ body, params, set }) => {
|
|
418
|
+
try {
|
|
419
|
+
const parsed = channelsFrom(body);
|
|
420
|
+
const universe = universeOf(options.source, params, {
|
|
421
|
+
...parsed.cid === void 0 ? {} : { cid: parsed.cid },
|
|
422
|
+
...parsed.idleFps === void 0 ? {} : { idleFps: parsed.idleFps },
|
|
423
|
+
...parsed.sourceName === void 0 ? {} : { sourceName: parsed.sourceName }
|
|
424
|
+
});
|
|
425
|
+
return await applyChannels(universe, parsed);
|
|
426
|
+
} catch (error) {
|
|
427
|
+
return fail(set, error);
|
|
428
|
+
}
|
|
429
|
+
}, {
|
|
430
|
+
body: channelsBodySchema,
|
|
431
|
+
detail: { responses: outputResponseDocument },
|
|
432
|
+
params: outputParamsSchema
|
|
433
|
+
}).post("/universes/:universe/priorities/:priority/frame", async ({ body, params, set }) => {
|
|
434
|
+
try {
|
|
435
|
+
const frame = frameFrom(body);
|
|
436
|
+
return await universeOf(options.source, params, {
|
|
437
|
+
...frame.cid === void 0 ? {} : { cid: frame.cid },
|
|
438
|
+
...frame.idleFps === void 0 ? {} : { idleFps: frame.idleFps },
|
|
439
|
+
...frame.sourceName === void 0 ? {} : { sourceName: frame.sourceName }
|
|
440
|
+
}).write(frame.values, { ...frame.durationMs === void 0 ? {} : { durationMs: frame.durationMs } });
|
|
441
|
+
} catch (error) {
|
|
442
|
+
return fail(set, error);
|
|
443
|
+
}
|
|
444
|
+
}, {
|
|
445
|
+
body: frameBodySchema,
|
|
446
|
+
detail: { responses: outputResponseDocument },
|
|
447
|
+
params: outputParamsSchema
|
|
448
|
+
}).delete("/universes/:universe/priorities/:priority", async ({ params, set }) => {
|
|
449
|
+
try {
|
|
450
|
+
if (!await universeOf(options.source, params).clear()) {
|
|
451
|
+
set.status = 404;
|
|
452
|
+
return { error: "Output not found." };
|
|
453
|
+
}
|
|
454
|
+
return { ok: true };
|
|
455
|
+
} catch (error) {
|
|
456
|
+
return fail(set, error);
|
|
457
|
+
}
|
|
458
|
+
}, {
|
|
459
|
+
detail: { responses: {
|
|
460
|
+
200: {
|
|
461
|
+
description: "Output cleared",
|
|
462
|
+
content: { "application/json": { schema: {
|
|
463
|
+
type: "object",
|
|
464
|
+
required: ["ok"],
|
|
465
|
+
properties: { ok: {
|
|
466
|
+
type: "boolean",
|
|
467
|
+
const: true
|
|
468
|
+
} }
|
|
469
|
+
} } }
|
|
470
|
+
},
|
|
471
|
+
400: errorResponseDocument("Invalid request"),
|
|
472
|
+
404: errorResponseDocument("Output not found"),
|
|
473
|
+
409: errorResponseDocument("Lifecycle conflict"),
|
|
474
|
+
503: errorResponseDocument("Transport or persistence unavailable")
|
|
475
|
+
} },
|
|
476
|
+
params: outputParamsSchema
|
|
477
|
+
});
|
|
478
|
+
if (options.viewer) app.get("/viewer/universes", () => ({ universes: options.viewer?.getSelectedUniverses() ?? [] }), { detail: { responses: componentResponse("ViewerState", "Selected viewer universes") } }).get("/viewer/telemetry", () => options.viewer?.getTelemetry(), { detail: { responses: componentResponse("ViewerTelemetry", "Viewer telemetry") } }).put("/viewer/universes", async ({ body, set }) => {
|
|
479
|
+
try {
|
|
480
|
+
const record = bodyObject(body);
|
|
481
|
+
if (!Array.isArray(record.universes)) throw new SacnValidationError("Body must contain a universes array.", "INVALID_UNIVERSE");
|
|
482
|
+
return { universes: await options.viewer?.setSelectedUniverses(record.universes) };
|
|
483
|
+
} catch (error) {
|
|
484
|
+
return fail(set, error);
|
|
485
|
+
}
|
|
486
|
+
}, {
|
|
487
|
+
body: viewerStateSchema,
|
|
488
|
+
detail: { responses: componentResponse("ViewerState", "Updated viewer universes") }
|
|
489
|
+
}).post("/viewer/universes/:universe", async ({ params, set }) => {
|
|
490
|
+
try {
|
|
491
|
+
return { universes: await options.viewer?.addUniverse(integerParam(params.universe, "Universe")) };
|
|
492
|
+
} catch (error) {
|
|
493
|
+
return fail(set, error);
|
|
494
|
+
}
|
|
495
|
+
}, {
|
|
496
|
+
detail: { responses: componentResponse("ViewerState", "Updated viewer universes") },
|
|
497
|
+
params: viewerUniverseParamsSchema
|
|
498
|
+
}).delete("/viewer/universes/:universe", async ({ params, set }) => {
|
|
499
|
+
try {
|
|
500
|
+
return { universes: await options.viewer?.removeUniverse(integerParam(params.universe, "Universe")) };
|
|
501
|
+
} catch (error) {
|
|
502
|
+
return fail(set, error);
|
|
503
|
+
}
|
|
504
|
+
}, {
|
|
505
|
+
detail: { responses: componentResponse("ViewerState", "Updated viewer universes") },
|
|
506
|
+
params: viewerUniverseParamsSchema
|
|
507
|
+
}).delete("/viewer/universes", async ({ set }) => {
|
|
508
|
+
try {
|
|
509
|
+
return { universes: await options.viewer?.setSelectedUniverses([]) };
|
|
510
|
+
} catch (error) {
|
|
511
|
+
return fail(set, error);
|
|
512
|
+
}
|
|
513
|
+
}, { detail: { responses: componentResponse("ViewerState", "Cleared viewer universes") } }).ws("/viewer/ws", {
|
|
514
|
+
open(ws) {
|
|
515
|
+
if (clients.size >= maxWebSocketClients) {
|
|
516
|
+
ws.close();
|
|
517
|
+
return;
|
|
518
|
+
}
|
|
519
|
+
const client = {
|
|
520
|
+
socket: ws,
|
|
521
|
+
pending: /* @__PURE__ */ new Map(),
|
|
522
|
+
scheduled: false
|
|
523
|
+
};
|
|
524
|
+
clients.add(client);
|
|
525
|
+
ws.send({
|
|
526
|
+
type: "viewer-state",
|
|
527
|
+
viewer: { universes: options.viewer?.getSelectedUniverses() ?? [] }
|
|
528
|
+
});
|
|
529
|
+
},
|
|
530
|
+
close(ws) {
|
|
531
|
+
for (const client of clients) if (client.socket === ws) {
|
|
532
|
+
clients.delete(client);
|
|
533
|
+
client.pending.clear();
|
|
534
|
+
break;
|
|
535
|
+
}
|
|
536
|
+
},
|
|
537
|
+
message() {}
|
|
538
|
+
});
|
|
539
|
+
const mark = (set, successor) => deprecated(set, `${prefix}${successor}`, sunset);
|
|
540
|
+
return app.get("/outputs", async ({ set }) => {
|
|
541
|
+
mark(set, "/universes");
|
|
542
|
+
try {
|
|
543
|
+
return await options.source.listUniverses();
|
|
544
|
+
} catch (error) {
|
|
545
|
+
return fail(set, error);
|
|
546
|
+
}
|
|
547
|
+
}, { detail: {
|
|
548
|
+
deprecated: true,
|
|
549
|
+
responses: { 200: {
|
|
550
|
+
description: "Active outputs",
|
|
551
|
+
content: { "application/json": { schema: {
|
|
552
|
+
type: "array",
|
|
553
|
+
items: { $ref: "#/components/schemas/OutputSnapshot" }
|
|
554
|
+
} } }
|
|
555
|
+
} }
|
|
556
|
+
} }).get("/telemetry", ({ set }) => {
|
|
557
|
+
mark(set, "/engine/telemetry");
|
|
558
|
+
return options.source.getTelemetry();
|
|
559
|
+
}, { detail: {
|
|
560
|
+
deprecated: true,
|
|
561
|
+
responses: componentResponse("EngineTelemetry", "Engine telemetry")
|
|
562
|
+
} }).get("/outputs/:universe/:priority", async ({ params, set }) => {
|
|
563
|
+
mark(set, `/universes/${params.universe}/priorities/${params.priority}`);
|
|
564
|
+
try {
|
|
565
|
+
const output = await universeOf(options.source, params).get();
|
|
566
|
+
if (!output) {
|
|
567
|
+
set.status = 404;
|
|
568
|
+
return { error: "Output not found." };
|
|
569
|
+
}
|
|
570
|
+
return output;
|
|
571
|
+
} catch (error) {
|
|
572
|
+
return fail(set, error);
|
|
573
|
+
}
|
|
574
|
+
}, {
|
|
575
|
+
detail: {
|
|
576
|
+
deprecated: true,
|
|
577
|
+
responses: outputLookupResponseDocument
|
|
578
|
+
},
|
|
579
|
+
params: outputParamsSchema
|
|
580
|
+
}).put("/outputs/:universe/:priority", async ({ body, params, set }) => {
|
|
581
|
+
mark(set, `/universes/${params.universe}/priorities/${params.priority}`);
|
|
582
|
+
try {
|
|
583
|
+
const record = bodyObject(body);
|
|
584
|
+
const frame = frameFrom({
|
|
585
|
+
...record,
|
|
586
|
+
values: record.initialValues ?? record.values ?? Array.from({ length: 512 }, () => 0)
|
|
587
|
+
});
|
|
588
|
+
const universe = universeOf(options.source, params, {
|
|
589
|
+
...frame.cid === void 0 ? {} : { cid: frame.cid },
|
|
590
|
+
...frame.idleFps === void 0 ? {} : { idleFps: frame.idleFps },
|
|
591
|
+
...frame.sourceName === void 0 ? {} : { sourceName: frame.sourceName }
|
|
592
|
+
});
|
|
593
|
+
const existing = await universe.get();
|
|
594
|
+
const supplied = record.initialValues ?? record.values;
|
|
595
|
+
const values = Array.isArray(supplied) || supplied instanceof Uint8Array ? supplied : existing?.target ?? Array.from({ length: 512 }, () => 0);
|
|
596
|
+
return await universe.write(values, { ...frame.durationMs === void 0 ? {} : { durationMs: frame.durationMs } });
|
|
597
|
+
} catch (error) {
|
|
598
|
+
return fail(set, error);
|
|
599
|
+
}
|
|
600
|
+
}, {
|
|
601
|
+
body: upsertBodySchema,
|
|
602
|
+
detail: {
|
|
603
|
+
deprecated: true,
|
|
604
|
+
responses: outputResponseDocument
|
|
605
|
+
},
|
|
606
|
+
params: outputParamsSchema
|
|
607
|
+
}).post("/outputs/:universe/:priority/channels", async ({ body, params, set }) => {
|
|
608
|
+
mark(set, `/universes/${params.universe}/priorities/${params.priority}/channels`);
|
|
609
|
+
try {
|
|
610
|
+
const parsed = channelsFrom(body);
|
|
611
|
+
const universe = universeOf(options.source, params, {
|
|
612
|
+
...parsed.cid === void 0 ? {} : { cid: parsed.cid },
|
|
613
|
+
...parsed.idleFps === void 0 ? {} : { idleFps: parsed.idleFps },
|
|
614
|
+
...parsed.sourceName === void 0 ? {} : { sourceName: parsed.sourceName }
|
|
615
|
+
});
|
|
616
|
+
return await applyChannels(universe, parsed);
|
|
617
|
+
} catch (error) {
|
|
618
|
+
return fail(set, error);
|
|
619
|
+
}
|
|
620
|
+
}, {
|
|
621
|
+
body: channelsBodySchema,
|
|
622
|
+
detail: {
|
|
623
|
+
deprecated: true,
|
|
624
|
+
responses: outputResponseDocument
|
|
625
|
+
},
|
|
626
|
+
params: outputParamsSchema
|
|
627
|
+
}).post("/outputs/:universe/:priority/frame", async ({ body, params, set }) => {
|
|
628
|
+
mark(set, `/universes/${params.universe}/priorities/${params.priority}/frame`);
|
|
629
|
+
try {
|
|
630
|
+
const frame = frameFrom(body);
|
|
631
|
+
return await universeOf(options.source, params, {
|
|
632
|
+
...frame.cid === void 0 ? {} : { cid: frame.cid },
|
|
633
|
+
...frame.idleFps === void 0 ? {} : { idleFps: frame.idleFps },
|
|
634
|
+
...frame.sourceName === void 0 ? {} : { sourceName: frame.sourceName }
|
|
635
|
+
}).write(frame.values, { ...frame.durationMs === void 0 ? {} : { durationMs: frame.durationMs } });
|
|
636
|
+
} catch (error) {
|
|
637
|
+
return fail(set, error);
|
|
638
|
+
}
|
|
639
|
+
}, {
|
|
640
|
+
body: frameBodySchema,
|
|
641
|
+
detail: {
|
|
642
|
+
deprecated: true,
|
|
643
|
+
responses: outputResponseDocument
|
|
644
|
+
},
|
|
645
|
+
params: outputParamsSchema
|
|
646
|
+
}).delete("/outputs/:universe/:priority", async ({ params, set }) => {
|
|
647
|
+
mark(set, `/universes/${params.universe}/priorities/${params.priority}`);
|
|
648
|
+
try {
|
|
649
|
+
if (!await universeOf(options.source, params).clear()) {
|
|
650
|
+
set.status = 404;
|
|
651
|
+
return { error: "Output not found." };
|
|
652
|
+
}
|
|
653
|
+
return { ok: true };
|
|
654
|
+
} catch (error) {
|
|
655
|
+
return fail(set, error);
|
|
656
|
+
}
|
|
657
|
+
}, {
|
|
658
|
+
detail: {
|
|
659
|
+
deprecated: true,
|
|
660
|
+
responses: clearResponseDocument
|
|
661
|
+
},
|
|
662
|
+
params: outputParamsSchema
|
|
663
|
+
});
|
|
664
|
+
};
|
|
665
|
+
/** Connects viewer packets to a host-owned WebSocket or SSE implementation. */
|
|
666
|
+
const subscribeViewerPackets = (viewer, listener) => viewer.subscribe(listener);
|
|
667
|
+
//#endregion
|
|
668
|
+
export { createSacnHttpAdapter, subscribeViewerPackets };
|
|
669
|
+
|
|
670
|
+
//# sourceMappingURL=http.js.map
|