@holo-js/broadcast 0.1.3

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.
@@ -0,0 +1,528 @@
1
+ import {
2
+ formatChannelPattern,
3
+ isBroadcastDefinition,
4
+ normalizeBroadcastDefinition
5
+ } from "./chunk-QW6MHEWS.mjs";
6
+
7
+ // src/runtime.ts
8
+ import { randomUUID } from "crypto";
9
+ import { holoBroadcastDefaults } from "@holo-js/config";
10
+
11
+ // src/registry.ts
12
+ var registeredDrivers = /* @__PURE__ */ new Map();
13
+ function normalizeDriverName(name) {
14
+ const normalized = name.trim();
15
+ if (!normalized) {
16
+ throw new Error("[Holo Broadcast] Broadcast driver names must be non-empty strings.");
17
+ }
18
+ return normalized;
19
+ }
20
+ function registerBroadcastDriver(name, driver, options = {}) {
21
+ const normalizedName = normalizeDriverName(name);
22
+ if (typeof driver?.send !== "function") {
23
+ throw new Error("[Holo Broadcast] Broadcast drivers must define a send(...) function.");
24
+ }
25
+ if (registeredDrivers.has(normalizedName) && options.replace !== true) {
26
+ throw new Error(`[Holo Broadcast] Broadcast driver "${normalizedName}" is already registered.`);
27
+ }
28
+ registeredDrivers.set(normalizedName, driver);
29
+ return Object.freeze({
30
+ name: normalizedName,
31
+ driver
32
+ });
33
+ }
34
+ function getRegisteredBroadcastDriver(name) {
35
+ return registeredDrivers.get(normalizeDriverName(name));
36
+ }
37
+ function listRegisteredBroadcastDrivers() {
38
+ return Object.freeze(
39
+ [...registeredDrivers.entries()].map(([name, driver]) => Object.freeze({ name, driver }))
40
+ );
41
+ }
42
+ function resetBroadcastDriverRegistry() {
43
+ registeredDrivers.clear();
44
+ }
45
+ var broadcastRegistryInternals = {
46
+ normalizeDriverName
47
+ };
48
+
49
+ // src/runtime.ts
50
+ var HOLO_BROADCAST_DELIVER_JOB = "holo.broadcast.deliver";
51
+ function getRuntimeState() {
52
+ const runtime = globalThis;
53
+ runtime.__holoBroadcastRuntime__ ??= {};
54
+ return runtime.__holoBroadcastRuntime__;
55
+ }
56
+ function getRuntimeBindings() {
57
+ return getRuntimeState().bindings ?? {};
58
+ }
59
+ function dynamicImport(specifier) {
60
+ return import(specifier);
61
+ }
62
+ async function loadQueueModule() {
63
+ const load = getRuntimeState().loadQueueModule ?? (async () => await dynamicImport("@holo-js/queue"));
64
+ try {
65
+ return await load();
66
+ } catch (error) {
67
+ if (error && typeof error === "object" && "code" in error && error.code === "ERR_MODULE_NOT_FOUND") {
68
+ throw new Error("[@holo-js/broadcast] Queued or delayed broadcasts require @holo-js/queue to be installed.");
69
+ }
70
+ throw error;
71
+ }
72
+ }
73
+ async function loadDbModule() {
74
+ const load = getRuntimeState().loadDbModule ?? (async () => await dynamicImport("@holo-js/db"));
75
+ try {
76
+ return await load();
77
+ } catch (error) {
78
+ if (error && typeof error === "object" && "code" in error && error.code === "ERR_MODULE_NOT_FOUND") {
79
+ return null;
80
+ }
81
+ throw error;
82
+ }
83
+ }
84
+ function normalizeOptionalString(value, label) {
85
+ if (typeof value === "undefined") {
86
+ return void 0;
87
+ }
88
+ const normalized = value.trim();
89
+ if (!normalized) {
90
+ throw new Error(`[@holo-js/broadcast] ${label} must be a non-empty string when provided.`);
91
+ }
92
+ return normalized;
93
+ }
94
+ function normalizeRequiredString(value, label) {
95
+ const normalized = value.trim();
96
+ if (!normalized) {
97
+ throw new Error(`[@holo-js/broadcast] ${label} must be a non-empty string.`);
98
+ }
99
+ return normalized;
100
+ }
101
+ function normalizeDelayValue(value, label) {
102
+ if (typeof value === "number") {
103
+ if (!Number.isFinite(value) || value < 0) {
104
+ throw new Error(`[@holo-js/broadcast] ${label} must be a finite number greater than or equal to 0.`);
105
+ }
106
+ return value;
107
+ }
108
+ if (!(value instanceof Date) || Number.isNaN(value.getTime())) {
109
+ throw new Error(`[@holo-js/broadcast] ${label} dates must be valid Date instances.`);
110
+ }
111
+ return value;
112
+ }
113
+ function isRecord(value) {
114
+ return !!value && typeof value === "object" && !Array.isArray(value) && (Object.getPrototypeOf(value) === Object.prototype || Object.getPrototypeOf(value) === null);
115
+ }
116
+ function normalizeJsonValue(value, path) {
117
+ if (value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
118
+ return value;
119
+ }
120
+ if (Array.isArray(value)) {
121
+ return Object.freeze(value.map((entry, index) => normalizeJsonValue(entry, `${path}[${index}]`)));
122
+ }
123
+ if (!isRecord(value)) {
124
+ throw new Error(`[@holo-js/broadcast] ${path} must be JSON-serializable.`);
125
+ }
126
+ return Object.freeze(Object.fromEntries(
127
+ Object.entries(value).map(([key, entry]) => [key, normalizeJsonValue(entry, `${path}.${key}`)])
128
+ ));
129
+ }
130
+ function normalizePayload(payload) {
131
+ if (!isRecord(payload)) {
132
+ throw new Error("[@holo-js/broadcast] Broadcast payload must be a plain object.");
133
+ }
134
+ return normalizeJsonValue(payload, "Broadcast payload");
135
+ }
136
+ function normalizeRawChannels(channels) {
137
+ if (!Array.isArray(channels) || channels.length === 0) {
138
+ throw new Error("[@holo-js/broadcast] Raw broadcasts must target at least one channel.");
139
+ }
140
+ const normalized = channels.map((channel) => normalizeRequiredString(channel, "Broadcast channel"));
141
+ return Object.freeze(normalized);
142
+ }
143
+ function resolveBroadcastDefinition(definition) {
144
+ return isBroadcastDefinition(definition) ? definition : normalizeBroadcastDefinition(definition);
145
+ }
146
+ function resolveBroadcastConnection(selectedConnection) {
147
+ const config = getRuntimeBindings().config ?? holoBroadcastDefaults;
148
+ const connectionName = normalizeOptionalString(selectedConnection, "Broadcast connection") ?? config.default;
149
+ const connection = config.connections[connectionName];
150
+ if (!connection) {
151
+ throw new Error(`[@holo-js/broadcast] Broadcast connection "${connectionName}" is not configured.`);
152
+ }
153
+ return Object.freeze({
154
+ name: connection.name,
155
+ driver: connection.driver,
156
+ ..."options" in connection ? { options: connection.options } : {}
157
+ });
158
+ }
159
+ function normalizeRawBroadcastInput(input, selectedConnection) {
160
+ const connection = resolveBroadcastConnection(selectedConnection ?? input.connection);
161
+ const event = normalizeRequiredString(input.event, "Broadcast event");
162
+ const socketId = normalizeOptionalString(input.socketId, "Broadcast socket id");
163
+ return Object.freeze({
164
+ connection: connection.name,
165
+ event,
166
+ channels: normalizeRawChannels(input.channels),
167
+ payload: normalizePayload(input.payload),
168
+ ...typeof socketId === "undefined" ? {} : { socketId }
169
+ });
170
+ }
171
+ function createRawInputFromDefinition(definition, selectedConnection) {
172
+ if (typeof definition.name !== "string" || !definition.name.trim()) {
173
+ throw new Error("[@holo-js/broadcast] Broadcast definitions must resolve a public event name before dispatch.");
174
+ }
175
+ return normalizeRawBroadcastInput({
176
+ connection: selectedConnection,
177
+ event: definition.name,
178
+ channels: definition.channels.map((channel) => {
179
+ const resolved = formatChannelPattern(
180
+ channel.pattern,
181
+ channel.params
182
+ );
183
+ if (channel.type === "private") {
184
+ return `private-${resolved}`;
185
+ }
186
+ if (channel.type === "presence") {
187
+ return `presence-${resolved}`;
188
+ }
189
+ return resolved;
190
+ }),
191
+ payload: definition.payload
192
+ });
193
+ }
194
+ function resolveQueuePlan(definition, options) {
195
+ const queueDefaults = definition?.queue;
196
+ const queued = queueDefaults?.queued === true || typeof options.connection !== "undefined" || typeof options.queue !== "undefined" || typeof options.delay !== "undefined" || typeof definition?.delay !== "undefined";
197
+ return Object.freeze({
198
+ queued,
199
+ connection: options.connection ?? queueDefaults?.connection,
200
+ queue: options.queue ?? queueDefaults?.queue,
201
+ delay: options.delay ?? definition?.delay,
202
+ afterCommit: options.afterCommit ?? queueDefaults?.afterCommit ?? false
203
+ });
204
+ }
205
+ function normalizeDispatchOptions(options) {
206
+ return Object.freeze({
207
+ broadcaster: options.broadcaster,
208
+ connection: options.connection,
209
+ queue: options.queue,
210
+ delay: options.delay,
211
+ afterCommit: options.afterCommit
212
+ });
213
+ }
214
+ function createExecutionContext(messageId, driver, queued, deferred = false) {
215
+ return Object.freeze({
216
+ connection: driver.connection.name,
217
+ driver: driver.driver,
218
+ queued,
219
+ delayed: deferred,
220
+ messageId
221
+ });
222
+ }
223
+ function createBaseResult(context, channels) {
224
+ return Object.freeze({
225
+ connection: context.connection,
226
+ driver: context.driver,
227
+ queued: context.queued,
228
+ publishedChannels: Object.freeze([...channels]),
229
+ messageId: context.messageId
230
+ });
231
+ }
232
+ function normalizeDriverResult(result, context, channels) {
233
+ const publishedChannels = Array.isArray(result.publishedChannels) ? Object.freeze(result.publishedChannels.map((channel) => normalizeRequiredString(channel, "Published channel"))) : Object.freeze([...channels]);
234
+ const provider = result.provider && isRecord(result.provider) ? Object.freeze({ ...result.provider }) : void 0;
235
+ return Object.freeze({
236
+ connection: normalizeOptionalString(result.connection, "Broadcast result connection") ?? context.connection,
237
+ driver: normalizeOptionalString(result.driver, "Broadcast result driver") ?? context.driver,
238
+ queued: typeof result.queued === "boolean" ? result.queued : context.queued,
239
+ publishedChannels,
240
+ messageId: normalizeOptionalString(result.messageId, "Broadcast result messageId") ?? context.messageId,
241
+ ...provider ? { provider } : {}
242
+ });
243
+ }
244
+ function createTransportDriver(driverName) {
245
+ return {
246
+ async send(input, context) {
247
+ const publish = getRuntimeBindings().publish;
248
+ if (!publish) {
249
+ throw new Error(`[@holo-js/broadcast] The "${driverName}" driver requires a publish runtime binding.`);
250
+ }
251
+ return await publish(input, context);
252
+ }
253
+ };
254
+ }
255
+ var builtInDrivers = Object.freeze({
256
+ holo: createTransportDriver("holo"),
257
+ pusher: createTransportDriver("pusher"),
258
+ ably: createTransportDriver("ably"),
259
+ log: {
260
+ send(input, context) {
261
+ console.warn("[@holo-js/broadcast]", {
262
+ connection: context.connection,
263
+ driver: context.driver,
264
+ event: input.event,
265
+ channels: input.channels,
266
+ hasSocketId: typeof input.socketId === "string"
267
+ });
268
+ return createBaseResult({ ...context, messageId: randomUUID() }, input.channels);
269
+ }
270
+ },
271
+ null: {
272
+ send(input, context) {
273
+ return createBaseResult({ ...context, messageId: randomUUID() }, input.channels);
274
+ }
275
+ }
276
+ });
277
+ function resolveDriver(connectionName) {
278
+ const connection = resolveBroadcastConnection(connectionName);
279
+ const implementation = getRegisteredBroadcastDriver(connection.driver) ?? builtInDrivers[connection.driver];
280
+ if (!implementation) {
281
+ throw new Error(`[@holo-js/broadcast] Broadcast driver "${connection.driver}" is not registered.`);
282
+ }
283
+ return Object.freeze({
284
+ connection,
285
+ driver: connection.driver,
286
+ implementation
287
+ });
288
+ }
289
+ async function runQueuedBroadcastDelivery(payload) {
290
+ const driver = resolveDriver(payload.context.connection);
291
+ const context = createExecutionContext(payload.messageId, driver, true);
292
+ return await deliverResolvedRawBroadcast(payload.raw, driver, context);
293
+ }
294
+ async function ensureBroadcastQueueJobRegistered(queueModule) {
295
+ const state = getRuntimeState();
296
+ if (queueModule?.getRegisteredQueueJob(HOLO_BROADCAST_DELIVER_JOB)) {
297
+ return queueModule;
298
+ }
299
+ if (state.queueJobRegistration) {
300
+ return await state.queueJobRegistration;
301
+ }
302
+ const registration = (async () => {
303
+ const resolvedQueueModule = queueModule ?? await loadQueueModule();
304
+ if (!resolvedQueueModule.getRegisteredQueueJob(HOLO_BROADCAST_DELIVER_JOB)) {
305
+ resolvedQueueModule.registerQueueJob(
306
+ resolvedQueueModule.defineJob({
307
+ async handle(payload) {
308
+ return await runQueuedBroadcastDelivery(payload);
309
+ }
310
+ }),
311
+ { name: HOLO_BROADCAST_DELIVER_JOB }
312
+ );
313
+ }
314
+ return resolvedQueueModule;
315
+ })();
316
+ state.queueJobRegistration = registration;
317
+ try {
318
+ return await registration;
319
+ } finally {
320
+ if (state.queueJobRegistration === registration) {
321
+ state.queueJobRegistration = void 0;
322
+ }
323
+ }
324
+ }
325
+ function createQueuedPayload(input, context) {
326
+ return Object.freeze({
327
+ messageId: context.messageId,
328
+ raw: input,
329
+ context: Object.freeze({
330
+ connection: context.connection,
331
+ driver: context.driver
332
+ })
333
+ });
334
+ }
335
+ async function dispatchQueuedBroadcast(input, context, plan) {
336
+ const queueModule = await ensureBroadcastQueueJobRegistered();
337
+ let pending = queueModule.dispatch(
338
+ HOLO_BROADCAST_DELIVER_JOB,
339
+ createQueuedPayload(input, context)
340
+ );
341
+ if (typeof plan.connection !== "undefined") {
342
+ pending = pending.onConnection(plan.connection);
343
+ }
344
+ if (typeof plan.queue !== "undefined") {
345
+ pending = pending.onQueue(plan.queue);
346
+ }
347
+ if (typeof plan.delay !== "undefined") {
348
+ pending = pending.delay(plan.delay);
349
+ }
350
+ await pending.dispatch();
351
+ return createBaseResult(context, input.channels);
352
+ }
353
+ async function deferDispatchUntilCommit(context, channels, callback) {
354
+ const dbModule = await loadDbModule();
355
+ const active = dbModule?.connectionAsyncContext.getActive()?.connection;
356
+ if (!active || active.getScope().kind === "root") {
357
+ return null;
358
+ }
359
+ active.afterCommit(async () => {
360
+ await callback();
361
+ });
362
+ return createBaseResult(context, channels);
363
+ }
364
+ async function deliverResolvedRawBroadcast(input, driver, context) {
365
+ const frozenInput = Object.freeze({
366
+ connection: input.connection,
367
+ event: input.event,
368
+ channels: Object.freeze([...input.channels]),
369
+ payload: input.payload,
370
+ ...typeof input.socketId === "undefined" ? {} : { socketId: input.socketId }
371
+ });
372
+ const result = await driver.implementation.send(frozenInput, context);
373
+ return normalizeDriverResult(result, context, frozenInput.channels);
374
+ }
375
+ async function executeResolvedRawBroadcast(input, definition, options) {
376
+ const driver = resolveDriver(input.connection);
377
+ const plan = resolveQueuePlan(definition, options);
378
+ const context = createExecutionContext(randomUUID(), driver, plan.queued);
379
+ if (plan.afterCommit) {
380
+ const deferred = await deferDispatchUntilCommit(
381
+ createExecutionContext(context.messageId, driver, plan.queued, true),
382
+ input.channels,
383
+ async () => {
384
+ if (plan.queued) {
385
+ return await dispatchQueuedBroadcast(
386
+ input,
387
+ createExecutionContext(context.messageId, driver, true),
388
+ plan
389
+ );
390
+ }
391
+ return await deliverResolvedRawBroadcast(
392
+ input,
393
+ driver,
394
+ createExecutionContext(context.messageId, driver, false)
395
+ );
396
+ }
397
+ );
398
+ if (deferred) {
399
+ return deferred;
400
+ }
401
+ }
402
+ if (plan.queued) {
403
+ return await dispatchQueuedBroadcast(
404
+ input,
405
+ createExecutionContext(context.messageId, driver, true),
406
+ plan
407
+ );
408
+ }
409
+ return await deliverResolvedRawBroadcast(
410
+ input,
411
+ driver,
412
+ createExecutionContext(context.messageId, driver, false)
413
+ );
414
+ }
415
+ var PendingDispatch = class {
416
+ constructor(executor, options = {}) {
417
+ this.executor = executor;
418
+ this.options = options;
419
+ }
420
+ #promise;
421
+ using(name) {
422
+ this.options.broadcaster = normalizeRequiredString(name, "Broadcast connection");
423
+ return this;
424
+ }
425
+ onConnection(name) {
426
+ this.options.connection = normalizeRequiredString(name, "Broadcast queue connection");
427
+ return this;
428
+ }
429
+ onQueue(name) {
430
+ this.options.queue = normalizeRequiredString(name, "Broadcast queue name");
431
+ return this;
432
+ }
433
+ delay(value) {
434
+ this.options.delay = normalizeDelayValue(value, "Broadcast delay");
435
+ return this;
436
+ }
437
+ afterCommit() {
438
+ this.options.afterCommit = true;
439
+ return this;
440
+ }
441
+ then(onfulfilled, onrejected) {
442
+ return this.execute().then(onfulfilled, onrejected);
443
+ }
444
+ catch(onrejected) {
445
+ return this.execute().catch(onrejected);
446
+ }
447
+ finally(onfinally) {
448
+ return this.execute().finally(onfinally ?? void 0);
449
+ }
450
+ execute() {
451
+ if (!this.#promise) {
452
+ this.#promise = this.executor(normalizeDispatchOptions(this.options));
453
+ }
454
+ return this.#promise;
455
+ }
456
+ };
457
+ function configureBroadcastRuntime(bindings) {
458
+ getRuntimeState().bindings = bindings;
459
+ }
460
+ function getBroadcastRuntimeBindings() {
461
+ return getRuntimeBindings();
462
+ }
463
+ function resetBroadcastRuntime() {
464
+ const state = getRuntimeState();
465
+ state.bindings = void 0;
466
+ state.loadQueueModule = void 0;
467
+ state.loadDbModule = void 0;
468
+ state.queueJobRegistration = void 0;
469
+ }
470
+ function broadcast(definition) {
471
+ return new PendingDispatch(async (options) => {
472
+ const resolvedDefinition = resolveBroadcastDefinition(definition);
473
+ const raw = createRawInputFromDefinition(resolvedDefinition, options.broadcaster);
474
+ const input = Object.freeze({
475
+ broadcast: resolvedDefinition,
476
+ raw,
477
+ options
478
+ });
479
+ void input;
480
+ return await executeResolvedRawBroadcast(raw, resolvedDefinition, options);
481
+ });
482
+ }
483
+ function broadcastRaw(input) {
484
+ return new PendingDispatch(async (options) => {
485
+ const resolvedInput = normalizeRawBroadcastInput(input, options.broadcaster);
486
+ return await executeResolvedRawBroadcast(resolvedInput, null, options);
487
+ });
488
+ }
489
+ function getBroadcastRuntime() {
490
+ return Object.freeze({
491
+ broadcast,
492
+ broadcastRaw
493
+ });
494
+ }
495
+ var broadcastRuntimeInternals = {
496
+ createRawInputFromDefinition,
497
+ ensureBroadcastQueueJobRegistered,
498
+ normalizeDelayValue,
499
+ normalizeRawBroadcastInput,
500
+ resolveBroadcastConnection,
501
+ resolveDriver,
502
+ resolveQueuePlan,
503
+ resetBroadcastRuntime,
504
+ async runQueuedBroadcastDelivery(payload) {
505
+ return await runQueuedBroadcastDelivery(payload);
506
+ },
507
+ setLoadDbModuleForTesting(loader) {
508
+ getRuntimeState().loadDbModule = loader;
509
+ },
510
+ setLoadQueueModuleForTesting(loader) {
511
+ getRuntimeState().loadQueueModule = loader;
512
+ }
513
+ };
514
+
515
+ export {
516
+ registerBroadcastDriver,
517
+ getRegisteredBroadcastDriver,
518
+ listRegisteredBroadcastDrivers,
519
+ resetBroadcastDriverRegistry,
520
+ broadcastRegistryInternals,
521
+ configureBroadcastRuntime,
522
+ getBroadcastRuntimeBindings,
523
+ resetBroadcastRuntime,
524
+ broadcast,
525
+ broadcastRaw,
526
+ getBroadcastRuntime,
527
+ broadcastRuntimeInternals
528
+ };