@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/node.js
ADDED
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
import { a as SacnValidationError, c as assertCid, d as assertPort, f as assertSourceName, g as toValidatedFrame, h as normalizeAddress, o as TransportError } from "./validation-BdsVeyAE.js";
|
|
2
|
+
import { r as createSacnSource$1 } from "./source-DB1oq2GT.js";
|
|
3
|
+
import { Buffer } from "node:buffer";
|
|
4
|
+
import { createSocket } from "node:dgram";
|
|
5
|
+
import { Packet, Receiver } from "sacn";
|
|
6
|
+
//#region src/node.ts
|
|
7
|
+
const activeSources = /* @__PURE__ */ new Set();
|
|
8
|
+
let processHandlersInstalled = false;
|
|
9
|
+
let shuttingDown = false;
|
|
10
|
+
const unregisterSource = (source) => {
|
|
11
|
+
activeSources.delete(source);
|
|
12
|
+
};
|
|
13
|
+
const closeRegisteredSources = async () => {
|
|
14
|
+
const sources = [...activeSources];
|
|
15
|
+
activeSources.clear();
|
|
16
|
+
await Promise.allSettled(sources.map((source) => source.close()));
|
|
17
|
+
};
|
|
18
|
+
const onProcessShutdown = (signal) => {
|
|
19
|
+
if (shuttingDown) return;
|
|
20
|
+
shuttingDown = true;
|
|
21
|
+
closeRegisteredSources().finally(() => {
|
|
22
|
+
if (signal === "beforeExit") return;
|
|
23
|
+
if (process.listenerCount(signal) === 0) process.exit(signal === "SIGINT" ? 130 : 143);
|
|
24
|
+
});
|
|
25
|
+
};
|
|
26
|
+
const installProcessHandlers = () => {
|
|
27
|
+
if (processHandlersInstalled) return;
|
|
28
|
+
processHandlersInstalled = true;
|
|
29
|
+
process.once("SIGINT", () => onProcessShutdown("SIGINT"));
|
|
30
|
+
process.once("SIGTERM", () => onProcessShutdown("SIGTERM"));
|
|
31
|
+
process.once("beforeExit", () => onProcessShutdown("beforeExit"));
|
|
32
|
+
};
|
|
33
|
+
/** Test helper: close and clear the Node active-source registry. */
|
|
34
|
+
const flushActiveSacnSourcesForTests = async () => {
|
|
35
|
+
await closeRegisteredSources();
|
|
36
|
+
shuttingDown = false;
|
|
37
|
+
};
|
|
38
|
+
/** Test helper: whether process signal handlers are installed. */
|
|
39
|
+
const areProcessHandlersInstalledForTests = () => processHandlersInstalled;
|
|
40
|
+
const getNodeRuntimeTelemetry = () => {
|
|
41
|
+
const cpu = process.cpuUsage();
|
|
42
|
+
const memory = process.memoryUsage();
|
|
43
|
+
return Object.freeze({
|
|
44
|
+
cpuUsage: Object.freeze({
|
|
45
|
+
systemMicroseconds: cpu.system,
|
|
46
|
+
userMicroseconds: cpu.user
|
|
47
|
+
}),
|
|
48
|
+
memoryUsage: Object.freeze({
|
|
49
|
+
arrayBuffersBytes: memory.arrayBuffers,
|
|
50
|
+
externalBytes: memory.external,
|
|
51
|
+
heapTotalBytes: memory.heapTotal,
|
|
52
|
+
heapUsedBytes: memory.heapUsed,
|
|
53
|
+
rssBytes: memory.rss
|
|
54
|
+
}),
|
|
55
|
+
pid: process.pid,
|
|
56
|
+
uptimeSeconds: process.uptime()
|
|
57
|
+
});
|
|
58
|
+
};
|
|
59
|
+
const createSacnSource = (options = {}) => {
|
|
60
|
+
const { name, transportOptions, installProcessHandlers: shouldInstallHandlers = true, ...sourceOptions } = options;
|
|
61
|
+
const transport = new NodeSacnTransport({
|
|
62
|
+
...transportOptions,
|
|
63
|
+
...name === void 0 ? {} : { sourceName: transportOptions?.sourceName ?? name }
|
|
64
|
+
});
|
|
65
|
+
const source = createSacnSource$1({
|
|
66
|
+
...sourceOptions,
|
|
67
|
+
transport,
|
|
68
|
+
ownsTransport: true
|
|
69
|
+
});
|
|
70
|
+
activeSources.add(source);
|
|
71
|
+
source.subscribe((event) => {
|
|
72
|
+
if (event.type === "closed") unregisterSource(source);
|
|
73
|
+
});
|
|
74
|
+
if (shouldInstallHandlers) installProcessHandlers();
|
|
75
|
+
return source;
|
|
76
|
+
};
|
|
77
|
+
const multicastGroup = (universe) => `239.255.${universe >> 8 & 255}.${universe & 255}`;
|
|
78
|
+
var DefaultNodeSender = class {
|
|
79
|
+
#socket;
|
|
80
|
+
#ready;
|
|
81
|
+
#universe;
|
|
82
|
+
#port;
|
|
83
|
+
#destination;
|
|
84
|
+
#closed = false;
|
|
85
|
+
constructor(options) {
|
|
86
|
+
this.#universe = options.universe;
|
|
87
|
+
this.#port = options.port;
|
|
88
|
+
this.#destination = options.useUnicastDestination ?? multicastGroup(options.universe);
|
|
89
|
+
this.#socket = createSocket({
|
|
90
|
+
type: "udp4",
|
|
91
|
+
reuseAddr: true
|
|
92
|
+
});
|
|
93
|
+
this.#ready = new Promise((resolve, reject) => {
|
|
94
|
+
const onError = (error) => {
|
|
95
|
+
this.#socket.off("listening", onListening);
|
|
96
|
+
reject(error);
|
|
97
|
+
};
|
|
98
|
+
const onListening = () => {
|
|
99
|
+
this.#socket.off("error", onError);
|
|
100
|
+
if (options.iface && !options.useUnicastDestination) this.#socket.setMulticastInterface(options.iface);
|
|
101
|
+
resolve();
|
|
102
|
+
};
|
|
103
|
+
this.#socket.once("error", onError);
|
|
104
|
+
this.#socket.once("listening", onListening);
|
|
105
|
+
this.#socket.bind(0);
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
on(event, listener) {
|
|
109
|
+
this.#socket.on(event, listener);
|
|
110
|
+
return this;
|
|
111
|
+
}
|
|
112
|
+
async send(packet) {
|
|
113
|
+
await this.#ready;
|
|
114
|
+
if (this.#closed) throw new Error("sACN sender is closed.");
|
|
115
|
+
const wire = new Packet({
|
|
116
|
+
cid: packet.cid,
|
|
117
|
+
payload: packet.payload,
|
|
118
|
+
priority: packet.priority || 1,
|
|
119
|
+
sequence: packet.sequence,
|
|
120
|
+
sourceName: packet.sourceName,
|
|
121
|
+
universe: this.#universe,
|
|
122
|
+
useRawDmxValues: true
|
|
123
|
+
}).buffer;
|
|
124
|
+
wire[108] = packet.priority;
|
|
125
|
+
wire[111] = packet.sequence;
|
|
126
|
+
await new Promise((resolve, reject) => {
|
|
127
|
+
this.#socket.send(wire, this.#port, this.#destination, (error) => error ? reject(error) : resolve());
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
close() {
|
|
131
|
+
if (this.#closed) return;
|
|
132
|
+
this.#closed = true;
|
|
133
|
+
this.#socket.close();
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
const uuidBuffer = (cid) => Buffer.from(cid.replaceAll("-", ""), "hex");
|
|
137
|
+
const truncateUtf8 = (value, maximumBytes) => {
|
|
138
|
+
let result = value;
|
|
139
|
+
while (Buffer.byteLength(result, "utf8") > maximumBytes) result = result.slice(0, -1);
|
|
140
|
+
return result;
|
|
141
|
+
};
|
|
142
|
+
const sourceNameFor = (base, suffix, universe, priority) => truncateUtf8([
|
|
143
|
+
base,
|
|
144
|
+
suffix,
|
|
145
|
+
`${universe}/${priority}`
|
|
146
|
+
].filter(Boolean).join(" ").replace(/[^\x20-\x7e]/g, "?"), 64);
|
|
147
|
+
const createPayload = () => {
|
|
148
|
+
const payload = {};
|
|
149
|
+
for (let channel = 1; channel <= 512; channel += 1) payload[channel] = 0;
|
|
150
|
+
return payload;
|
|
151
|
+
};
|
|
152
|
+
const updatePayload = (payload, data) => {
|
|
153
|
+
for (let channel = 1; channel <= 512; channel += 1) payload[channel] = data[channel - 1] ?? 0;
|
|
154
|
+
};
|
|
155
|
+
var NodeSacnTransport = class {
|
|
156
|
+
#options;
|
|
157
|
+
#senders = /* @__PURE__ */ new Map();
|
|
158
|
+
#closed = false;
|
|
159
|
+
constructor(options = {}) {
|
|
160
|
+
assertPort(options.port ?? 5568);
|
|
161
|
+
if (options.sourceName !== void 0) assertSourceName(options.sourceName);
|
|
162
|
+
this.#options = options;
|
|
163
|
+
}
|
|
164
|
+
async send(packet, signal) {
|
|
165
|
+
signal.throwIfAborted();
|
|
166
|
+
if (this.#closed) throw new TransportError("Node sACN transport is closed.");
|
|
167
|
+
normalizeAddress(packet);
|
|
168
|
+
assertCid(packet.cid);
|
|
169
|
+
if (packet.sourceName !== null) assertSourceName(packet.sourceName);
|
|
170
|
+
if (!Number.isInteger(packet.sequence) || packet.sequence < 0 || packet.sequence > 255) throw new SacnValidationError("Sequence must be an integer between 0 and 255.", "INVALID_FRAME");
|
|
171
|
+
const frame = toValidatedFrame(packet.data);
|
|
172
|
+
const key = `${packet.universe}:${packet.priority}`;
|
|
173
|
+
const sourceName = sourceNameFor(this.#options.sourceName ?? "@helioslx/core", packet.sourceName, packet.universe, packet.priority);
|
|
174
|
+
let context = this.#senders.get(key);
|
|
175
|
+
if (context && (context.cid !== packet.cid || context.sourceName !== sourceName)) {
|
|
176
|
+
context.sender.close();
|
|
177
|
+
this.#senders.delete(key);
|
|
178
|
+
context = void 0;
|
|
179
|
+
}
|
|
180
|
+
if (!context) {
|
|
181
|
+
const senderOptions = {
|
|
182
|
+
universe: packet.universe,
|
|
183
|
+
port: this.#options.port ?? 5568,
|
|
184
|
+
...this.#options.iface === void 0 ? {} : { iface: this.#options.iface },
|
|
185
|
+
...this.#options.unicastDestination === void 0 ? {} : { useUnicastDestination: this.#options.unicastDestination }
|
|
186
|
+
};
|
|
187
|
+
const sender = this.#options.senderFactory?.(senderOptions) ?? new DefaultNodeSender(senderOptions);
|
|
188
|
+
sender.on?.("error", (error) => {
|
|
189
|
+
this.#options.logger?.error?.("sACN sender error.", {
|
|
190
|
+
error,
|
|
191
|
+
universe: packet.universe,
|
|
192
|
+
priority: packet.priority
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
context = {
|
|
196
|
+
sender,
|
|
197
|
+
cid: packet.cid,
|
|
198
|
+
sourceName,
|
|
199
|
+
payload: createPayload()
|
|
200
|
+
};
|
|
201
|
+
this.#senders.set(key, context);
|
|
202
|
+
}
|
|
203
|
+
updatePayload(context.payload, frame);
|
|
204
|
+
try {
|
|
205
|
+
await context.sender.send({
|
|
206
|
+
payload: context.payload,
|
|
207
|
+
cid: uuidBuffer(packet.cid),
|
|
208
|
+
priority: packet.priority,
|
|
209
|
+
sequence: packet.sequence,
|
|
210
|
+
sourceName,
|
|
211
|
+
useRawDmxValues: true
|
|
212
|
+
});
|
|
213
|
+
signal.throwIfAborted();
|
|
214
|
+
} catch (error) {
|
|
215
|
+
if (signal.aborted) throw signal.reason;
|
|
216
|
+
this.#senders.delete(key);
|
|
217
|
+
try {
|
|
218
|
+
context.sender.close();
|
|
219
|
+
} catch {}
|
|
220
|
+
throw new TransportError(`Node sACN sender failed for universe ${packet.universe}.`, error);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
async closeOutput(address) {
|
|
224
|
+
const key = `${address.universe}:${address.priority}`;
|
|
225
|
+
const context = this.#senders.get(key);
|
|
226
|
+
if (!context) return;
|
|
227
|
+
this.#senders.delete(key);
|
|
228
|
+
context.sender.close();
|
|
229
|
+
}
|
|
230
|
+
async close() {
|
|
231
|
+
if (this.#closed) return;
|
|
232
|
+
this.#closed = true;
|
|
233
|
+
for (const context of this.#senders.values()) context.sender.close();
|
|
234
|
+
this.#senders.clear();
|
|
235
|
+
}
|
|
236
|
+
};
|
|
237
|
+
const cidString = (cid) => {
|
|
238
|
+
const hex = cid.toString("hex").padEnd(32, "0").slice(0, 32);
|
|
239
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
240
|
+
};
|
|
241
|
+
var NodeSacnReceiver = class {
|
|
242
|
+
#receiver;
|
|
243
|
+
#listeners = /* @__PURE__ */ new Set();
|
|
244
|
+
#onPacket;
|
|
245
|
+
#onError;
|
|
246
|
+
#onPacketWarning;
|
|
247
|
+
#ready;
|
|
248
|
+
#closed = false;
|
|
249
|
+
constructor(options = {}) {
|
|
250
|
+
assertPort(options.port ?? 5568);
|
|
251
|
+
const receiverOptions = {
|
|
252
|
+
universes: (options.universes ?? []).map((universe) => normalizeAddress({ universe }).universe),
|
|
253
|
+
port: options.port ?? 5568,
|
|
254
|
+
...options.iface === void 0 ? {} : { iface: options.iface }
|
|
255
|
+
};
|
|
256
|
+
this.#receiver = options.receiverFactory?.(receiverOptions) ?? new Receiver({
|
|
257
|
+
...receiverOptions,
|
|
258
|
+
reuseAddr: true
|
|
259
|
+
});
|
|
260
|
+
const ready = this.#receiver.ready ?? (this.#receiver.socket ? new Promise((resolve, reject) => {
|
|
261
|
+
const onReadyError = (error) => {
|
|
262
|
+
this.#receiver.off?.("error", onReadyError);
|
|
263
|
+
reject(error);
|
|
264
|
+
};
|
|
265
|
+
const onListening = () => {
|
|
266
|
+
this.#receiver.off?.("error", onReadyError);
|
|
267
|
+
resolve();
|
|
268
|
+
};
|
|
269
|
+
this.#receiver.on("error", onReadyError);
|
|
270
|
+
try {
|
|
271
|
+
this.#receiver.socket?.address();
|
|
272
|
+
onListening();
|
|
273
|
+
} catch {
|
|
274
|
+
this.#receiver.socket?.once("listening", onListening);
|
|
275
|
+
}
|
|
276
|
+
}) : Promise.resolve());
|
|
277
|
+
ready.catch(() => void 0);
|
|
278
|
+
this.#ready = ready;
|
|
279
|
+
this.#onPacket = (packet) => {
|
|
280
|
+
const values = /* @__PURE__ */ new Uint8Array(512);
|
|
281
|
+
if (packet.payloadAsBuffer) values.set(packet.payloadAsBuffer.subarray(0, 512));
|
|
282
|
+
else for (let channel = 1; channel <= 512; channel += 1) values[channel - 1] = packet.payload[channel] ?? 0;
|
|
283
|
+
const normalized = Object.freeze({
|
|
284
|
+
universe: packet.universe,
|
|
285
|
+
priority: packet.priority,
|
|
286
|
+
sequence: packet.sequence,
|
|
287
|
+
cid: cidString(packet.cid),
|
|
288
|
+
sourceName: packet.sourceName || null,
|
|
289
|
+
sourceAddress: packet.sourceAddress ?? null,
|
|
290
|
+
values
|
|
291
|
+
});
|
|
292
|
+
for (const listener of this.#listeners) listener(normalized);
|
|
293
|
+
};
|
|
294
|
+
this.#onError = (error) => {
|
|
295
|
+
options.logger?.error?.("sACN receiver error.", { error });
|
|
296
|
+
};
|
|
297
|
+
this.#onPacketWarning = (error) => {
|
|
298
|
+
options.logger?.warn?.("sACN receiver dropped or reordered a packet.", { error });
|
|
299
|
+
};
|
|
300
|
+
this.#receiver.on("packet", this.#onPacket);
|
|
301
|
+
this.#receiver.on("error", this.#onError);
|
|
302
|
+
this.#receiver.on("PacketCorruption", this.#onPacketWarning);
|
|
303
|
+
this.#receiver.on("PacketOutOfOrder", this.#onPacketWarning);
|
|
304
|
+
}
|
|
305
|
+
async addUniverse(universe) {
|
|
306
|
+
await this.#ready;
|
|
307
|
+
await this.#receiver.addUniverse(normalizeAddress({ universe }).universe);
|
|
308
|
+
}
|
|
309
|
+
async removeUniverse(universe) {
|
|
310
|
+
await this.#ready;
|
|
311
|
+
await this.#receiver.removeUniverse(normalizeAddress({ universe }).universe);
|
|
312
|
+
}
|
|
313
|
+
subscribe(listener) {
|
|
314
|
+
if (this.#closed) throw new TransportError("Node sACN receiver is closed.");
|
|
315
|
+
this.#listeners.add(listener);
|
|
316
|
+
return () => this.#listeners.delete(listener);
|
|
317
|
+
}
|
|
318
|
+
async close() {
|
|
319
|
+
if (this.#closed) return;
|
|
320
|
+
this.#closed = true;
|
|
321
|
+
this.#receiver.off?.("packet", this.#onPacket);
|
|
322
|
+
this.#receiver.off?.("error", this.#onError);
|
|
323
|
+
this.#receiver.off?.("PacketCorruption", this.#onPacketWarning);
|
|
324
|
+
this.#receiver.off?.("PacketOutOfOrder", this.#onPacketWarning);
|
|
325
|
+
this.#listeners.clear();
|
|
326
|
+
await new Promise((resolve) => {
|
|
327
|
+
this.#receiver.close(resolve);
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
};
|
|
331
|
+
//#endregion
|
|
332
|
+
export { NodeSacnReceiver, NodeSacnTransport, areProcessHandlersInstalledForTests, createSacnSource, flushActiveSacnSourcesForTests, getNodeRuntimeTelemetry };
|
|
333
|
+
|
|
334
|
+
//# sourceMappingURL=node.js.map
|
package/dist/node.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"node.js","names":["createCoreSacnSource","#socket","#ready","#universe","#port","#destination","#closed","#options","#senders","#receiver","#listeners","#onPacket","#onError","#onPacketWarning","SacnReceiver"],"sources":["../src/node.ts"],"sourcesContent":["import { Buffer } from \"node:buffer\";\nimport { createSocket, type Socket } from \"node:dgram\";\nimport { Packet, Receiver as SacnReceiver } from \"sacn\";\nimport {\n\ttype Logger,\n\ttype OutputAddress,\n\ttype OutputPacket,\n\ttype OutputTransport,\n\ttype Receiver,\n\ttype ReceiverPacket,\n\ttype ReceiverPacketListener,\n\ttype SacnSourceOptions,\n\tSLOT_COUNT,\n} from \"./contracts.js\";\nimport { createSacnSource as createCoreSacnSource, type SacnSource } from \"./source.js\";\nimport {\n\tassertCid,\n\tassertPort,\n\tassertSourceName,\n\tnormalizeAddress,\n\tSacnValidationError,\n\tTransportError,\n\ttoValidatedFrame,\n} from \"./validation.js\";\n\nconst activeSources = new Set<SacnSource>();\nlet processHandlersInstalled = false;\nlet shuttingDown = false;\n\nconst unregisterSource = (source: SacnSource): void => {\n\tactiveSources.delete(source);\n};\n\nconst closeRegisteredSources = async (): Promise<void> => {\n\tconst sources = [...activeSources];\n\tactiveSources.clear();\n\tawait Promise.allSettled(sources.map((source) => source.close()));\n};\n\nconst onProcessShutdown = (signal: NodeJS.Signals | \"beforeExit\"): void => {\n\tif (shuttingDown) return;\n\tshuttingDown = true;\n\tvoid closeRegisteredSources().finally(() => {\n\t\tif (signal === \"beforeExit\") return;\n\t\t// Allow default signal behavior after teardown when no other listeners remain.\n\t\tif (process.listenerCount(signal) === 0) {\n\t\t\tprocess.exit(signal === \"SIGINT\" ? 130 : 143);\n\t\t}\n\t});\n};\n\nconst installProcessHandlers = (): void => {\n\tif (processHandlersInstalled) return;\n\tprocessHandlersInstalled = true;\n\tprocess.once(\"SIGINT\", () => onProcessShutdown(\"SIGINT\"));\n\tprocess.once(\"SIGTERM\", () => onProcessShutdown(\"SIGTERM\"));\n\tprocess.once(\"beforeExit\", () => onProcessShutdown(\"beforeExit\"));\n};\n\n/** Test helper: close and clear the Node active-source registry. */\nexport const flushActiveSacnSourcesForTests = async (): Promise<void> => {\n\tawait closeRegisteredSources();\n\tshuttingDown = false;\n};\n\n/** Test helper: whether process signal handlers are installed. */\nexport const areProcessHandlersInstalledForTests = (): boolean =>\n\tprocessHandlersInstalled;\n\nexport interface NodeRuntimeTelemetry {\n\treadonly cpuUsage: {\n\t\treadonly systemMicroseconds: number;\n\t\treadonly userMicroseconds: number;\n\t};\n\treadonly memoryUsage: {\n\t\treadonly arrayBuffersBytes: number;\n\t\treadonly externalBytes: number;\n\t\treadonly heapTotalBytes: number;\n\t\treadonly heapUsedBytes: number;\n\t\treadonly rssBytes: number;\n\t};\n\treadonly pid: number;\n\treadonly uptimeSeconds: number;\n}\n\nexport const getNodeRuntimeTelemetry = (): NodeRuntimeTelemetry => {\n\tconst cpu = process.cpuUsage();\n\tconst memory = process.memoryUsage();\n\treturn Object.freeze({\n\t\tcpuUsage: Object.freeze({\n\t\t\tsystemMicroseconds: cpu.system,\n\t\t\tuserMicroseconds: cpu.user,\n\t\t}),\n\t\tmemoryUsage: Object.freeze({\n\t\t\tarrayBuffersBytes: memory.arrayBuffers,\n\t\t\texternalBytes: memory.external,\n\t\t\theapTotalBytes: memory.heapTotal,\n\t\t\theapUsedBytes: memory.heapUsed,\n\t\t\trssBytes: memory.rss,\n\t\t}),\n\t\tpid: process.pid,\n\t\tuptimeSeconds: process.uptime(),\n\t});\n};\n\nexport interface NodeSender {\n\tsend(packet: {\n\t\tcid: Buffer;\n\t\tpayload: Record<number, number>;\n\t\tpriority: number;\n\t\tsequence: number;\n\t\tsourceName: string;\n\t\tuseRawDmxValues: true;\n\t}): Promise<void>;\n\tclose(): unknown;\n\ton?(event: \"error\", listener: (error: Error) => void): unknown;\n}\n\nexport interface NodeSenderOptions {\n\treadonly universe: number;\n\treadonly port: number;\n\treadonly iface?: string;\n\treadonly useUnicastDestination?: string;\n}\n\nexport interface NodeSacnTransportOptions {\n\treadonly iface?: string;\n\treadonly port?: number;\n\treadonly sourceName?: string;\n\t/** Optional unicast destination for testing or networks that require it. */\n\treadonly unicastDestination?: string;\n\treadonly senderFactory?: (options: NodeSenderOptions) => NodeSender;\n\treadonly logger?: Logger;\n}\n\nexport interface CreateSacnSourceOptions\n\textends Omit<SacnSourceOptions, \"ownsTransport\" | \"transport\"> {\n\treadonly name?: string;\n\treadonly transportOptions?: NodeSacnTransportOptions;\n\t/**\n\t * Install process SIGINT/SIGTERM/beforeExit handlers that close all\n\t * registered Node sources. Defaults to true.\n\t */\n\treadonly installProcessHandlers?: boolean;\n}\n\nexport const createSacnSource = (\n\toptions: CreateSacnSourceOptions = {},\n): SacnSource => {\n\tconst {\n\t\tname,\n\t\ttransportOptions,\n\t\tinstallProcessHandlers: shouldInstallHandlers = true,\n\t\t...sourceOptions\n\t} = options;\n\tconst transport = new NodeSacnTransport({\n\t\t...transportOptions,\n\t\t...(name === undefined\n\t\t\t? {}\n\t\t\t: { sourceName: transportOptions?.sourceName ?? name }),\n\t});\n\tconst source = createCoreSacnSource({\n\t\t...sourceOptions,\n\t\ttransport,\n\t\townsTransport: true,\n\t});\n\tactiveSources.add(source);\n\tsource.subscribe((event) => {\n\t\tif (event.type === \"closed\") unregisterSource(source);\n\t});\n\tif (shouldInstallHandlers) installProcessHandlers();\n\treturn source;\n};\n\nconst multicastGroup = (universe: number): string =>\n\t`239.255.${(universe >> 8) & 0xff}.${universe & 0xff}`;\n\nclass DefaultNodeSender implements NodeSender {\n\treadonly #socket: Socket;\n\treadonly #ready: Promise<void>;\n\treadonly #universe: number;\n\treadonly #port: number;\n\treadonly #destination: string;\n\t#closed = false;\n\n\tconstructor(options: NodeSenderOptions) {\n\t\tthis.#universe = options.universe;\n\t\tthis.#port = options.port;\n\t\tthis.#destination =\n\t\t\toptions.useUnicastDestination ?? multicastGroup(options.universe);\n\t\tthis.#socket = createSocket({ type: \"udp4\", reuseAddr: true });\n\t\tthis.#ready = new Promise((resolve, reject) => {\n\t\t\tconst onError = (error: Error): void => {\n\t\t\t\tthis.#socket.off(\"listening\", onListening);\n\t\t\t\treject(error);\n\t\t\t};\n\t\t\tconst onListening = (): void => {\n\t\t\t\tthis.#socket.off(\"error\", onError);\n\t\t\t\tif (options.iface && !options.useUnicastDestination) {\n\t\t\t\t\tthis.#socket.setMulticastInterface(options.iface);\n\t\t\t\t}\n\t\t\t\tresolve();\n\t\t\t};\n\t\t\tthis.#socket.once(\"error\", onError);\n\t\t\tthis.#socket.once(\"listening\", onListening);\n\t\t\tthis.#socket.bind(0);\n\t\t});\n\t}\n\n\ton(event: \"error\", listener: (error: Error) => void): unknown {\n\t\tthis.#socket.on(event, listener);\n\t\treturn this;\n\t}\n\n\tasync send(packet: {\n\t\tcid: Buffer;\n\t\tpayload: Record<number, number>;\n\t\tpriority: number;\n\t\tsequence: number;\n\t\tsourceName: string;\n\t\tuseRawDmxValues: true;\n\t}): Promise<void> {\n\t\tawait this.#ready;\n\t\tif (this.#closed) throw new Error(\"sACN sender is closed.\");\n\t\tconst wire = new Packet({\n\t\t\tcid: packet.cid,\n\t\t\tpayload: packet.payload,\n\t\t\tpriority: packet.priority || 1,\n\t\t\tsequence: packet.sequence,\n\t\t\tsourceName: packet.sourceName,\n\t\t\tuniverse: this.#universe,\n\t\t\tuseRawDmxValues: true,\n\t\t}).buffer;\n\t\t// sacn 4.x applies `priority || 100`; restore standards-valid priority 0.\n\t\twire[108] = packet.priority;\n\t\t// Preserve the engine's sequence even if the encoder changes its policy.\n\t\twire[111] = packet.sequence;\n\t\tawait new Promise<void>((resolve, reject) => {\n\t\t\tthis.#socket.send(wire, this.#port, this.#destination, (error) =>\n\t\t\t\terror ? reject(error) : resolve(),\n\t\t\t);\n\t\t});\n\t}\n\n\tclose(): void {\n\t\tif (this.#closed) return;\n\t\tthis.#closed = true;\n\t\tthis.#socket.close();\n\t}\n}\n\nconst uuidBuffer = (cid: string): Buffer =>\n\tBuffer.from(cid.replaceAll(\"-\", \"\"), \"hex\");\n\nconst truncateUtf8 = (value: string, maximumBytes: number): string => {\n\tlet result = value;\n\twhile (Buffer.byteLength(result, \"utf8\") > maximumBytes) {\n\t\tresult = result.slice(0, -1);\n\t}\n\treturn result;\n};\n\nconst sourceNameFor = (\n\tbase: string,\n\tsuffix: string | null,\n\tuniverse: number,\n\tpriority: number,\n): string =>\n\ttruncateUtf8(\n\t\t[base, suffix, `${universe}/${priority}`]\n\t\t\t.filter(Boolean)\n\t\t\t.join(\" \")\n\t\t\t.replace(/[^\\x20-\\x7e]/g, \"?\"),\n\t\t64,\n\t);\n\ninterface SenderContext {\n\treadonly sender: NodeSender;\n\treadonly cid: string;\n\treadonly sourceName: string;\n\treadonly payload: Record<number, number>;\n}\n\nconst createPayload = (): Record<number, number> => {\n\tconst payload: Record<number, number> = {};\n\tfor (let channel = 1; channel <= SLOT_COUNT; channel += 1) {\n\t\tpayload[channel] = 0;\n\t}\n\treturn payload;\n};\n\nconst updatePayload = (\n\tpayload: Record<number, number>,\n\tdata: Uint8Array,\n): void => {\n\tfor (let channel = 1; channel <= SLOT_COUNT; channel += 1) {\n\t\tpayload[channel] = data[channel - 1] ?? 0;\n\t}\n};\n\nexport class NodeSacnTransport implements OutputTransport {\n\treadonly #options: NodeSacnTransportOptions;\n\treadonly #senders = new Map<string, SenderContext>();\n\t#closed = false;\n\n\tconstructor(options: NodeSacnTransportOptions = {}) {\n\t\tassertPort(options.port ?? 5568);\n\t\tif (options.sourceName !== undefined) assertSourceName(options.sourceName);\n\t\tthis.#options = options;\n\t}\n\n\tasync send(packet: OutputPacket, signal: AbortSignal): Promise<void> {\n\t\tsignal.throwIfAborted();\n\t\tif (this.#closed)\n\t\t\tthrow new TransportError(\"Node sACN transport is closed.\");\n\t\tnormalizeAddress(packet);\n\t\tassertCid(packet.cid);\n\t\tif (packet.sourceName !== null) assertSourceName(packet.sourceName);\n\t\tif (\n\t\t\t!Number.isInteger(packet.sequence) ||\n\t\t\tpacket.sequence < 0 ||\n\t\t\tpacket.sequence > 255\n\t\t) {\n\t\t\tthrow new SacnValidationError(\n\t\t\t\t\"Sequence must be an integer between 0 and 255.\",\n\t\t\t\t\"INVALID_FRAME\",\n\t\t\t);\n\t\t}\n\t\tconst frame = toValidatedFrame(packet.data);\n\t\tconst key = `${packet.universe}:${packet.priority}`;\n\t\tconst sourceName = sourceNameFor(\n\t\t\tthis.#options.sourceName ?? \"@helioslx/core\",\n\t\t\tpacket.sourceName,\n\t\t\tpacket.universe,\n\t\t\tpacket.priority,\n\t\t);\n\t\tlet context = this.#senders.get(key);\n\t\tif (\n\t\t\tcontext &&\n\t\t\t(context.cid !== packet.cid || context.sourceName !== sourceName)\n\t\t) {\n\t\t\tcontext.sender.close();\n\t\t\tthis.#senders.delete(key);\n\t\t\tcontext = undefined;\n\t\t}\n\t\tif (!context) {\n\t\t\tconst senderOptions: NodeSenderOptions = {\n\t\t\t\tuniverse: packet.universe,\n\t\t\t\tport: this.#options.port ?? 5568,\n\t\t\t\t...(this.#options.iface === undefined\n\t\t\t\t\t? {}\n\t\t\t\t\t: { iface: this.#options.iface }),\n\t\t\t\t...(this.#options.unicastDestination === undefined\n\t\t\t\t\t? {}\n\t\t\t\t\t: { useUnicastDestination: this.#options.unicastDestination }),\n\t\t\t};\n\t\t\tconst sender =\n\t\t\t\tthis.#options.senderFactory?.(senderOptions) ??\n\t\t\t\tnew DefaultNodeSender(senderOptions);\n\t\t\tsender.on?.(\"error\", (error) => {\n\t\t\t\tthis.#options.logger?.error?.(\"sACN sender error.\", {\n\t\t\t\t\terror,\n\t\t\t\t\tuniverse: packet.universe,\n\t\t\t\t\tpriority: packet.priority,\n\t\t\t\t});\n\t\t\t});\n\t\t\tcontext = {\n\t\t\t\tsender,\n\t\t\t\tcid: packet.cid,\n\t\t\t\tsourceName,\n\t\t\t\tpayload: createPayload(),\n\t\t\t};\n\t\t\tthis.#senders.set(key, context);\n\t\t}\n\t\tupdatePayload(context.payload, frame);\n\t\ttry {\n\t\t\tawait context.sender.send({\n\t\t\t\tpayload: context.payload,\n\t\t\t\tcid: uuidBuffer(packet.cid),\n\t\t\t\tpriority: packet.priority,\n\t\t\t\tsequence: packet.sequence,\n\t\t\t\tsourceName,\n\t\t\t\tuseRawDmxValues: true,\n\t\t\t});\n\t\t\tsignal.throwIfAborted();\n\t\t} catch (error) {\n\t\t\tif (signal.aborted) throw signal.reason;\n\t\t\tthis.#senders.delete(key);\n\t\t\ttry {\n\t\t\t\tcontext.sender.close();\n\t\t\t} catch {\n\t\t\t\t// A failed sender is discarded even when its close also fails.\n\t\t\t}\n\t\t\tthrow new TransportError(\n\t\t\t\t`Node sACN sender failed for universe ${packet.universe}.`,\n\t\t\t\terror,\n\t\t\t);\n\t\t}\n\t}\n\n\tasync closeOutput(address: Required<OutputAddress>): Promise<void> {\n\t\tconst key = `${address.universe}:${address.priority}`;\n\t\tconst context = this.#senders.get(key);\n\t\tif (!context) return;\n\t\tthis.#senders.delete(key);\n\t\tcontext.sender.close();\n\t}\n\n\tasync close(): Promise<void> {\n\t\tif (this.#closed) return;\n\t\tthis.#closed = true;\n\t\tfor (const context of this.#senders.values()) context.sender.close();\n\t\tthis.#senders.clear();\n\t}\n}\n\ninterface SacnPacketLike {\n\treadonly cid: Buffer;\n\treadonly payload: Readonly<Record<number, number>>;\n\treadonly payloadAsBuffer?: Buffer | null;\n\treadonly priority: number;\n\treadonly sequence: number;\n\treadonly sourceAddress?: string;\n\treadonly sourceName: string;\n\treadonly universe: number;\n}\n\nexport interface NodeReceiverInstance {\n\taddUniverse(universe: number): Promise<unknown> | unknown;\n\tremoveUniverse(universe: number): Promise<unknown> | unknown;\n\ton(event: \"packet\", listener: (packet: SacnPacketLike) => void): unknown;\n\ton(\n\t\tevent: \"error\" | \"PacketCorruption\" | \"PacketOutOfOrder\",\n\t\tlistener: (error: Error) => void,\n\t): unknown;\n\toff?(event: \"packet\", listener: (packet: SacnPacketLike) => void): unknown;\n\toff?(\n\t\tevent: \"error\" | \"PacketCorruption\" | \"PacketOutOfOrder\",\n\t\tlistener: (error: Error) => void,\n\t): unknown;\n\tclose(callback?: () => void): unknown;\n\treadonly ready?: Promise<void>;\n\treadonly socket?: {\n\t\taddress(): unknown;\n\t\tonce(event: \"listening\", listener: () => void): unknown;\n\t};\n}\n\nexport interface NodeSacnReceiverOptions {\n\treadonly universes?: readonly number[];\n\treadonly iface?: string;\n\treadonly port?: number;\n\treadonly receiverFactory?: (options: {\n\t\tuniverses: number[];\n\t\tiface?: string;\n\t\tport: number;\n\t}) => NodeReceiverInstance;\n\treadonly logger?: Logger;\n}\n\nconst cidString = (cid: Buffer): string => {\n\tconst hex = cid.toString(\"hex\").padEnd(32, \"0\").slice(0, 32);\n\treturn `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;\n};\n\nexport class NodeSacnReceiver implements Receiver {\n\treadonly #receiver: NodeReceiverInstance;\n\treadonly #listeners = new Set<ReceiverPacketListener>();\n\treadonly #onPacket: (packet: SacnPacketLike) => void;\n\treadonly #onError: (error: Error) => void;\n\treadonly #onPacketWarning: (error: Error) => void;\n\treadonly #ready: Promise<void>;\n\t#closed = false;\n\n\tconstructor(options: NodeSacnReceiverOptions = {}) {\n\t\tassertPort(options.port ?? 5568);\n\t\tconst universes = (options.universes ?? []).map(\n\t\t\t(universe) => normalizeAddress({ universe }).universe,\n\t\t);\n\t\tconst receiverOptions = {\n\t\t\tuniverses,\n\t\t\tport: options.port ?? 5568,\n\t\t\t...(options.iface === undefined ? {} : { iface: options.iface }),\n\t\t};\n\t\tthis.#receiver =\n\t\t\toptions.receiverFactory?.(receiverOptions) ??\n\t\t\t(new SacnReceiver({\n\t\t\t\t...receiverOptions,\n\t\t\t\treuseAddr: true,\n\t\t\t}) as unknown as NodeReceiverInstance);\n\t\tconst ready =\n\t\t\tthis.#receiver.ready ??\n\t\t\t(this.#receiver.socket\n\t\t\t\t? new Promise<void>((resolve, reject) => {\n\t\t\t\t\t\tconst onReadyError = (error: Error): void => {\n\t\t\t\t\t\t\tthis.#receiver.off?.(\"error\", onReadyError);\n\t\t\t\t\t\t\treject(error);\n\t\t\t\t\t\t};\n\t\t\t\t\t\tconst onListening = (): void => {\n\t\t\t\t\t\t\tthis.#receiver.off?.(\"error\", onReadyError);\n\t\t\t\t\t\t\tresolve();\n\t\t\t\t\t\t};\n\t\t\t\t\t\tthis.#receiver.on(\"error\", onReadyError);\n\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\tthis.#receiver.socket?.address();\n\t\t\t\t\t\t\tonListening();\n\t\t\t\t\t\t} catch {\n\t\t\t\t\t\t\tthis.#receiver.socket?.once(\"listening\", onListening);\n\t\t\t\t\t\t}\n\t\t\t\t\t})\n\t\t\t\t: Promise.resolve());\n\t\tvoid ready.catch(() => undefined);\n\t\tthis.#ready = ready;\n\t\tthis.#onPacket = (packet) => {\n\t\t\tconst values = new Uint8Array(SLOT_COUNT);\n\t\t\tif (packet.payloadAsBuffer) {\n\t\t\t\tvalues.set(packet.payloadAsBuffer.subarray(0, SLOT_COUNT));\n\t\t\t} else {\n\t\t\t\tfor (let channel = 1; channel <= SLOT_COUNT; channel += 1) {\n\t\t\t\t\tvalues[channel - 1] = packet.payload[channel] ?? 0;\n\t\t\t\t}\n\t\t\t}\n\t\t\tconst normalized: ReceiverPacket = Object.freeze({\n\t\t\t\tuniverse: packet.universe,\n\t\t\t\tpriority: packet.priority,\n\t\t\t\tsequence: packet.sequence,\n\t\t\t\tcid: cidString(packet.cid),\n\t\t\t\tsourceName: packet.sourceName || null,\n\t\t\t\tsourceAddress: packet.sourceAddress ?? null,\n\t\t\t\tvalues,\n\t\t\t});\n\t\t\tfor (const listener of this.#listeners) listener(normalized);\n\t\t};\n\t\tthis.#onError = (error) => {\n\t\t\toptions.logger?.error?.(\"sACN receiver error.\", { error });\n\t\t};\n\t\tthis.#onPacketWarning = (error) => {\n\t\t\toptions.logger?.warn?.(\"sACN receiver dropped or reordered a packet.\", {\n\t\t\t\terror,\n\t\t\t});\n\t\t};\n\t\tthis.#receiver.on(\"packet\", this.#onPacket);\n\t\tthis.#receiver.on(\"error\", this.#onError);\n\t\tthis.#receiver.on(\"PacketCorruption\", this.#onPacketWarning);\n\t\tthis.#receiver.on(\"PacketOutOfOrder\", this.#onPacketWarning);\n\t}\n\n\tasync addUniverse(universe: number): Promise<void> {\n\t\tawait this.#ready;\n\t\tawait this.#receiver.addUniverse(normalizeAddress({ universe }).universe);\n\t}\n\n\tasync removeUniverse(universe: number): Promise<void> {\n\t\tawait this.#ready;\n\t\tawait this.#receiver.removeUniverse(\n\t\t\tnormalizeAddress({ universe }).universe,\n\t\t);\n\t}\n\n\tsubscribe(listener: ReceiverPacketListener): () => void {\n\t\tif (this.#closed) throw new TransportError(\"Node sACN receiver is closed.\");\n\t\tthis.#listeners.add(listener);\n\t\treturn () => this.#listeners.delete(listener);\n\t}\n\n\tasync close(): Promise<void> {\n\t\tif (this.#closed) return;\n\t\tthis.#closed = true;\n\t\tthis.#receiver.off?.(\"packet\", this.#onPacket);\n\t\tthis.#receiver.off?.(\"error\", this.#onError);\n\t\tthis.#receiver.off?.(\"PacketCorruption\", this.#onPacketWarning);\n\t\tthis.#receiver.off?.(\"PacketOutOfOrder\", this.#onPacketWarning);\n\t\tthis.#listeners.clear();\n\t\tawait new Promise<void>((resolve) => {\n\t\t\tthis.#receiver.close(resolve);\n\t\t});\n\t}\n}\n"],"mappings":"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"}
|
package/dist/redis.d.ts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { L as ViewerRecord, V as ViewerStore, g as OutputAddress, x as OutputStore, y as OutputRecord } from "./contracts-C4kpAdZq.js";
|
|
2
|
+
//#region src/redis.d.ts
|
|
3
|
+
interface RedisClientCompatible {
|
|
4
|
+
get(key: string): Promise<string | null>;
|
|
5
|
+
set(key: string, value: string): Promise<unknown>;
|
|
6
|
+
del(key: string): Promise<unknown>;
|
|
7
|
+
scanIterator(options: {
|
|
8
|
+
MATCH: string;
|
|
9
|
+
COUNT?: number;
|
|
10
|
+
}): AsyncIterable<string | string[]>;
|
|
11
|
+
connect?(): Promise<unknown>;
|
|
12
|
+
quit?(): Promise<unknown>;
|
|
13
|
+
disconnect?(): unknown;
|
|
14
|
+
}
|
|
15
|
+
interface RedisStoreOptions {
|
|
16
|
+
readonly client?: RedisClientCompatible;
|
|
17
|
+
readonly url?: string;
|
|
18
|
+
readonly namespace?: string;
|
|
19
|
+
readonly version?: string | number;
|
|
20
|
+
/** Defaults to true for an internally-created client and false for an injected client. */
|
|
21
|
+
readonly closeClient?: boolean;
|
|
22
|
+
}
|
|
23
|
+
declare abstract class RedisStoreBase {
|
|
24
|
+
#private;
|
|
25
|
+
readonly prefix: string;
|
|
26
|
+
constructor(options: RedisStoreOptions);
|
|
27
|
+
protected ordered<T>(action: string, operation: (client: RedisClientCompatible) => Promise<T>): Promise<T>;
|
|
28
|
+
close(): Promise<void>;
|
|
29
|
+
}
|
|
30
|
+
declare class RedisOutputStore extends RedisStoreBase implements OutputStore {
|
|
31
|
+
#private;
|
|
32
|
+
constructor(options?: RedisStoreOptions);
|
|
33
|
+
get(address: Required<OutputAddress>): Promise<OutputRecord | null>;
|
|
34
|
+
list(): Promise<readonly OutputRecord[]>;
|
|
35
|
+
remove(address: Required<OutputAddress>): Promise<void>;
|
|
36
|
+
save(record: OutputRecord): Promise<void>;
|
|
37
|
+
}
|
|
38
|
+
declare class RedisViewerStore extends RedisStoreBase implements ViewerStore {
|
|
39
|
+
#private;
|
|
40
|
+
constructor(options?: RedisStoreOptions);
|
|
41
|
+
load(): Promise<ViewerRecord | null>;
|
|
42
|
+
save(record: ViewerRecord): Promise<void>;
|
|
43
|
+
}
|
|
44
|
+
//#endregion
|
|
45
|
+
export { RedisClientCompatible, RedisOutputStore, RedisStoreOptions, RedisViewerStore };
|
|
46
|
+
//# sourceMappingURL=redis.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"redis.d.ts","names":[],"sources":["../src/redis.ts"],"mappings":";;UAkBiB;EACf,IAAI,cAAc;EAClB,IAAI,aAAa,gBAAgB;EACjC,IAAI,cAAc;EAClB,aAAa;IACX;IACA;MACE;EACJ,YAAY;EACZ,SAAS;EACT;;UAGe;WACN,SAAS;WACT;WACA;WACA;;WAEA;;uBAGI;;WACJ;EAQT,YAAY,SAAS;YAsBX,QAAQ,GAAG,gBAAgB,YAAY,QAAQ,0BAA0B,QAAQ,KAAK,QAAQ;EAgBlG,SAAS;;cAyDJ,yBAAyB,0BAA0B;;EAC9D,YAAY,UAAS;EAQrB,IAAI,SAAS,SAAS,iBAAiB,QAAQ;EAO/C,QAAQ,iBAAiB;EA6BzB,OAAO,SAAS,SAAS,iBAAiB;EAM1C,KAAK,QAAQ,eAAe;;cAyCjB,yBAAyB,0BAA0B;;EAG9D,YAAY,UAAS;EAKrB,QAAQ,QAAQ;EAOhB,KAAK,QAAQ,eAAe"}
|
package/dist/redis.js
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { a as SacnValidationError, c as assertCid, f as assertSourceName, g as toValidatedFrame, h as normalizeAddress, n as PersistenceError, u as assertFps } from "./validation-BdsVeyAE.js";
|
|
2
|
+
import { createClient } from "redis";
|
|
3
|
+
//#region src/redis.ts
|
|
4
|
+
var RedisStoreBase = class {
|
|
5
|
+
prefix;
|
|
6
|
+
#client;
|
|
7
|
+
#closeClient;
|
|
8
|
+
#queue = Promise.resolve();
|
|
9
|
+
#closing = false;
|
|
10
|
+
#closed = false;
|
|
11
|
+
#closePromise = null;
|
|
12
|
+
constructor(options) {
|
|
13
|
+
const namespace = options.namespace ?? "helioslx";
|
|
14
|
+
const version = String(options.version ?? 1);
|
|
15
|
+
if (!/^[A-Za-z0-9:_-]{1,128}$/.test(namespace) || !/^[A-Za-z0-9._-]{1,32}$/.test(version)) throw new SacnValidationError("Redis namespace or schema version contains unsupported characters.", "INVALID_NAMESPACE");
|
|
16
|
+
this.prefix = `${namespace}:v${version}`;
|
|
17
|
+
this.#closeClient = options.closeClient ?? options.client === void 0;
|
|
18
|
+
if (options.client) this.#client = Promise.resolve(options.client);
|
|
19
|
+
else {
|
|
20
|
+
const client = createClient(options.url ? { url: options.url } : {});
|
|
21
|
+
this.#client = client.connect().then(() => client);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
ordered(action, operation) {
|
|
25
|
+
if (this.#closing || this.#closed) return Promise.reject(new PersistenceError("Redis store is closed."));
|
|
26
|
+
const result = this.#queue.then(async () => {
|
|
27
|
+
try {
|
|
28
|
+
return await operation(await this.#client);
|
|
29
|
+
} catch (error) {
|
|
30
|
+
if (error instanceof PersistenceError) throw error;
|
|
31
|
+
throw new PersistenceError(`Redis failed to ${action}.`, error);
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
this.#queue = result.then(() => void 0, () => void 0);
|
|
35
|
+
return result;
|
|
36
|
+
}
|
|
37
|
+
async close() {
|
|
38
|
+
if (this.#closePromise) return this.#closePromise;
|
|
39
|
+
this.#closing = true;
|
|
40
|
+
this.#closePromise = this.#queue.then(async () => {
|
|
41
|
+
try {
|
|
42
|
+
if (this.#closeClient) {
|
|
43
|
+
const client = await this.#client;
|
|
44
|
+
if (client.quit) await client.quit();
|
|
45
|
+
else client.disconnect?.();
|
|
46
|
+
}
|
|
47
|
+
} catch (error) {
|
|
48
|
+
throw new PersistenceError("Redis failed to close client.", error);
|
|
49
|
+
} finally {
|
|
50
|
+
this.#closed = true;
|
|
51
|
+
this.#closing = false;
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
return this.#closePromise;
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
const outputRecord = (value) => {
|
|
58
|
+
if (!value || typeof value !== "object") throw new PersistenceError("Stored Redis output record is not an object.");
|
|
59
|
+
const record = value;
|
|
60
|
+
const address = normalizeAddress({
|
|
61
|
+
universe: record.universe,
|
|
62
|
+
priority: record.priority
|
|
63
|
+
});
|
|
64
|
+
const cid = assertCid(record.cid);
|
|
65
|
+
assertFps(record.idleFps, "Idle FPS");
|
|
66
|
+
const sourceName = record.sourceName === null ? null : assertSourceName(record.sourceName);
|
|
67
|
+
const target = toValidatedFrame(record.target);
|
|
68
|
+
if (typeof record.updatedAt !== "number" || !Number.isFinite(record.updatedAt)) throw new PersistenceError("Stored Redis output updatedAt is invalid.");
|
|
69
|
+
return Object.freeze({
|
|
70
|
+
...address,
|
|
71
|
+
cid,
|
|
72
|
+
idleFps: record.idleFps,
|
|
73
|
+
sourceName,
|
|
74
|
+
target: Object.freeze(Array.from(target)),
|
|
75
|
+
updatedAt: record.updatedAt
|
|
76
|
+
});
|
|
77
|
+
};
|
|
78
|
+
const validateOutputRecord = (value) => {
|
|
79
|
+
try {
|
|
80
|
+
return outputRecord(value);
|
|
81
|
+
} catch (error) {
|
|
82
|
+
if (error instanceof PersistenceError) throw error;
|
|
83
|
+
throw new PersistenceError("Redis output record is invalid.", error);
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
var RedisOutputStore = class extends RedisStoreBase {
|
|
87
|
+
constructor(options = {}) {
|
|
88
|
+
super(options);
|
|
89
|
+
}
|
|
90
|
+
#key(address) {
|
|
91
|
+
return `${this.prefix}:output:${address.universe}:${address.priority}`;
|
|
92
|
+
}
|
|
93
|
+
get(address) {
|
|
94
|
+
return this.ordered("get output", async (client) => {
|
|
95
|
+
const value = await client.get(this.#key(address));
|
|
96
|
+
return value === null ? null : validateOutputRecord(JSON.parse(value));
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
list() {
|
|
100
|
+
return this.ordered("list outputs", async (client) => {
|
|
101
|
+
const pattern = `${this.prefix}:output:*`;
|
|
102
|
+
const keys = [];
|
|
103
|
+
for await (const entry of client.scanIterator({
|
|
104
|
+
MATCH: pattern,
|
|
105
|
+
COUNT: 100
|
|
106
|
+
})) if (Array.isArray(entry)) keys.push(...entry);
|
|
107
|
+
else keys.push(entry);
|
|
108
|
+
const records = await Promise.all(keys.sort().map(async (key) => {
|
|
109
|
+
const value = await client.get(key);
|
|
110
|
+
if (value === null) throw new PersistenceError(`Redis output key disappeared: ${key}.`);
|
|
111
|
+
return validateOutputRecord(JSON.parse(value));
|
|
112
|
+
}));
|
|
113
|
+
return Object.freeze(records.sort((left, right) => left.universe - right.universe || left.priority - right.priority));
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
remove(address) {
|
|
117
|
+
return this.ordered("remove output", async (client) => {
|
|
118
|
+
await client.del(this.#key(address));
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
save(record) {
|
|
122
|
+
const validated = validateOutputRecord(record);
|
|
123
|
+
return this.ordered("save output", async (client) => {
|
|
124
|
+
await client.set(this.#key({
|
|
125
|
+
universe: validated.universe,
|
|
126
|
+
priority: validated.priority
|
|
127
|
+
}), JSON.stringify(validated));
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
const viewerRecord = (value) => {
|
|
132
|
+
if (!value || typeof value !== "object") throw new PersistenceError("Stored Redis viewer record is not an object.");
|
|
133
|
+
const record = value;
|
|
134
|
+
if (!Array.isArray(record.selectedUniverses)) throw new PersistenceError("Stored Redis viewer universes are invalid.");
|
|
135
|
+
const universes = [...new Set(record.selectedUniverses)].map((universe) => {
|
|
136
|
+
return normalizeAddress({ universe }).universe;
|
|
137
|
+
});
|
|
138
|
+
if (typeof record.updatedAt !== "number" || !Number.isFinite(record.updatedAt)) throw new PersistenceError("Stored Redis viewer updatedAt is invalid.");
|
|
139
|
+
return Object.freeze({
|
|
140
|
+
selectedUniverses: Object.freeze(universes.sort((a, b) => a - b)),
|
|
141
|
+
updatedAt: record.updatedAt
|
|
142
|
+
});
|
|
143
|
+
};
|
|
144
|
+
const validateViewerRecord = (value) => {
|
|
145
|
+
try {
|
|
146
|
+
return viewerRecord(value);
|
|
147
|
+
} catch (error) {
|
|
148
|
+
if (error instanceof PersistenceError) throw error;
|
|
149
|
+
throw new PersistenceError("Redis viewer record is invalid.", error);
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
var RedisViewerStore = class extends RedisStoreBase {
|
|
153
|
+
#key;
|
|
154
|
+
constructor(options = {}) {
|
|
155
|
+
super(options);
|
|
156
|
+
this.#key = `${this.prefix}:viewer`;
|
|
157
|
+
}
|
|
158
|
+
load() {
|
|
159
|
+
return this.ordered("load viewer state", async (client) => {
|
|
160
|
+
const value = await client.get(this.#key);
|
|
161
|
+
return value === null ? null : validateViewerRecord(JSON.parse(value));
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
save(record) {
|
|
165
|
+
const validated = validateViewerRecord(record);
|
|
166
|
+
return this.ordered("save viewer state", async (client) => {
|
|
167
|
+
await client.set(this.#key, JSON.stringify(validated));
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
//#endregion
|
|
172
|
+
export { RedisOutputStore, RedisViewerStore };
|
|
173
|
+
|
|
174
|
+
//# sourceMappingURL=redis.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"redis.js","names":["#client","#closeClient","#closing","#closed","#queue","#closePromise","#key"],"sources":["../src/redis.ts"],"sourcesContent":["import { createClient } from \"redis\";\nimport {\n type OutputAddress,\n type OutputRecord,\n type OutputStore,\n type ViewerRecord,\n type ViewerStore,\n} from \"./contracts.js\";\nimport {\n PersistenceError,\n SacnValidationError,\n assertCid,\n assertFps,\n assertSourceName,\n normalizeAddress,\n toValidatedFrame,\n} from \"./validation.js\";\n\nexport interface RedisClientCompatible {\n get(key: string): Promise<string | null>;\n set(key: string, value: string): Promise<unknown>;\n del(key: string): Promise<unknown>;\n scanIterator(options: {\n MATCH: string;\n COUNT?: number;\n }): AsyncIterable<string | string[]>;\n connect?(): Promise<unknown>;\n quit?(): Promise<unknown>;\n disconnect?(): unknown;\n}\n\nexport interface RedisStoreOptions {\n readonly client?: RedisClientCompatible;\n readonly url?: string;\n readonly namespace?: string;\n readonly version?: string | number;\n /** Defaults to true for an internally-created client and false for an injected client. */\n readonly closeClient?: boolean;\n}\n\nabstract class RedisStoreBase {\n readonly prefix: string;\n readonly #client: Promise<RedisClientCompatible>;\n readonly #closeClient: boolean;\n #queue: Promise<void> = Promise.resolve();\n #closing = false;\n #closed = false;\n #closePromise: Promise<void> | null = null;\n\n constructor(options: RedisStoreOptions) {\n const namespace = options.namespace ?? \"helioslx\";\n const version = String(options.version ?? 1);\n if (\n !/^[A-Za-z0-9:_-]{1,128}$/.test(namespace) ||\n !/^[A-Za-z0-9._-]{1,32}$/.test(version)\n ) {\n throw new SacnValidationError(\n \"Redis namespace or schema version contains unsupported characters.\",\n \"INVALID_NAMESPACE\",\n );\n }\n this.prefix = `${namespace}:v${version}`;\n this.#closeClient = options.closeClient ?? options.client === undefined;\n if (options.client) {\n this.#client = Promise.resolve(options.client);\n } else {\n const client = createClient(options.url ? { url: options.url } : {});\n this.#client = client.connect().then(() => client as RedisClientCompatible);\n }\n }\n\n protected ordered<T>(action: string, operation: (client: RedisClientCompatible) => Promise<T>): Promise<T> {\n if (this.#closing || this.#closed) {\n return Promise.reject(new PersistenceError(\"Redis store is closed.\"));\n }\n const result = this.#queue.then(async () => {\n try {\n return await operation(await this.#client);\n } catch (error) {\n if (error instanceof PersistenceError) throw error;\n throw new PersistenceError(`Redis failed to ${action}.`, error);\n }\n });\n this.#queue = result.then(() => undefined, () => undefined);\n return result;\n }\n\n async close(): Promise<void> {\n if (this.#closePromise) return this.#closePromise;\n this.#closing = true;\n this.#closePromise = this.#queue.then(async () => {\n try {\n if (this.#closeClient) {\n const client = await this.#client;\n if (client.quit) await client.quit();\n else client.disconnect?.();\n }\n } catch (error) {\n throw new PersistenceError(\"Redis failed to close client.\", error);\n } finally {\n this.#closed = true;\n this.#closing = false;\n }\n });\n return this.#closePromise;\n }\n}\n\nconst outputRecord = (value: unknown): OutputRecord => {\n if (!value || typeof value !== \"object\") {\n throw new PersistenceError(\"Stored Redis output record is not an object.\");\n }\n const record = value as Record<string, unknown>;\n const address = normalizeAddress({\n universe: record.universe as number,\n priority: record.priority as number,\n });\n const cid = assertCid(record.cid as string);\n assertFps(record.idleFps as number, \"Idle FPS\");\n const sourceName =\n record.sourceName === null ? null : assertSourceName(record.sourceName as string);\n const target = toValidatedFrame(record.target as number[]);\n if (typeof record.updatedAt !== \"number\" || !Number.isFinite(record.updatedAt)) {\n throw new PersistenceError(\"Stored Redis output updatedAt is invalid.\");\n }\n return Object.freeze({\n ...address,\n cid,\n idleFps: record.idleFps as number,\n sourceName,\n target: Object.freeze(Array.from(target)),\n updatedAt: record.updatedAt,\n });\n};\n\nconst validateOutputRecord = (value: unknown): OutputRecord => {\n try {\n return outputRecord(value);\n } catch (error) {\n if (error instanceof PersistenceError) throw error;\n throw new PersistenceError(\"Redis output record is invalid.\", error);\n }\n};\n\nexport class RedisOutputStore extends RedisStoreBase implements OutputStore {\n constructor(options: RedisStoreOptions = {}) {\n super(options);\n }\n\n #key(address: Required<OutputAddress>): string {\n return `${this.prefix}:output:${address.universe}:${address.priority}`;\n }\n\n get(address: Required<OutputAddress>): Promise<OutputRecord | null> {\n return this.ordered(\"get output\", async (client) => {\n const value = await client.get(this.#key(address));\n return value === null ? null : validateOutputRecord(JSON.parse(value));\n });\n }\n\n list(): Promise<readonly OutputRecord[]> {\n return this.ordered(\"list outputs\", async (client) => {\n const pattern = `${this.prefix}:output:*`;\n const keys: string[] = [];\n for await (const entry of client.scanIterator({\n MATCH: pattern,\n COUNT: 100,\n })) {\n if (Array.isArray(entry)) keys.push(...entry);\n else keys.push(entry);\n }\n const records = await Promise.all(\n keys.sort().map(async (key) => {\n const value = await client.get(key);\n if (value === null) {\n throw new PersistenceError(`Redis output key disappeared: ${key}.`);\n }\n return validateOutputRecord(JSON.parse(value));\n }),\n );\n return Object.freeze(\n records.sort(\n (left, right) =>\n left.universe - right.universe || left.priority - right.priority,\n ),\n );\n });\n }\n\n remove(address: Required<OutputAddress>): Promise<void> {\n return this.ordered(\"remove output\", async (client) => {\n await client.del(this.#key(address));\n });\n }\n\n save(record: OutputRecord): Promise<void> {\n const validated = validateOutputRecord(record);\n return this.ordered(\"save output\", async (client) => {\n await client.set(\n this.#key({ universe: validated.universe, priority: validated.priority }),\n JSON.stringify(validated),\n );\n });\n }\n}\n\nconst viewerRecord = (value: unknown): ViewerRecord => {\n if (!value || typeof value !== \"object\") {\n throw new PersistenceError(\"Stored Redis viewer record is not an object.\");\n }\n const record = value as Record<string, unknown>;\n if (!Array.isArray(record.selectedUniverses)) {\n throw new PersistenceError(\"Stored Redis viewer universes are invalid.\");\n }\n const universes = [...new Set(record.selectedUniverses)].map((universe) => {\n const normalized = normalizeAddress({ universe: universe as number });\n return normalized.universe;\n });\n if (typeof record.updatedAt !== \"number\" || !Number.isFinite(record.updatedAt)) {\n throw new PersistenceError(\"Stored Redis viewer updatedAt is invalid.\");\n }\n return Object.freeze({\n selectedUniverses: Object.freeze(universes.sort((a, b) => a - b)),\n updatedAt: record.updatedAt,\n });\n};\n\nconst validateViewerRecord = (value: unknown): ViewerRecord => {\n try {\n return viewerRecord(value);\n } catch (error) {\n if (error instanceof PersistenceError) throw error;\n throw new PersistenceError(\"Redis viewer record is invalid.\", error);\n }\n};\n\nexport class RedisViewerStore extends RedisStoreBase implements ViewerStore {\n readonly #key: string;\n\n constructor(options: RedisStoreOptions = {}) {\n super(options);\n this.#key = `${this.prefix}:viewer`;\n }\n\n load(): Promise<ViewerRecord | null> {\n return this.ordered(\"load viewer state\", async (client) => {\n const value = await client.get(this.#key);\n return value === null ? null : validateViewerRecord(JSON.parse(value));\n });\n }\n\n save(record: ViewerRecord): Promise<void> {\n const validated = validateViewerRecord(record);\n return this.ordered(\"save viewer state\", async (client) => {\n await client.set(this.#key, JSON.stringify(validated));\n });\n }\n}\n"],"mappings":"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"}
|