@rplx/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/dist/index.mjs ADDED
@@ -0,0 +1,1096 @@
1
+ // src/modules/registrar.ts
2
+ function createRegistrar(options) {
3
+ const handlers = /* @__PURE__ */ new Map();
4
+ const warnOnOverwrite = options?.warnOnOverwrite ?? true;
5
+ return {
6
+ register(kind, id, handler) {
7
+ let kindMap = handlers.get(kind);
8
+ if (!kindMap) {
9
+ kindMap = /* @__PURE__ */ new Map();
10
+ handlers.set(kind, kindMap);
11
+ }
12
+ if (warnOnOverwrite && kindMap.has(id)) {
13
+ console.warn(`re-frame: overwriting ${kind} handler for: ${id}`);
14
+ }
15
+ kindMap.set(id, handler);
16
+ return handler;
17
+ },
18
+ get(kind, id) {
19
+ const kindMap = handlers.get(kind);
20
+ if (!kindMap) {
21
+ return void 0;
22
+ }
23
+ return kindMap.get(id);
24
+ },
25
+ has(kind, id) {
26
+ const kindMap = handlers.get(kind);
27
+ if (!kindMap) {
28
+ return false;
29
+ }
30
+ return kindMap.has(id);
31
+ },
32
+ clear(kind, id) {
33
+ if (kind === void 0) {
34
+ handlers.clear();
35
+ } else if (id === void 0) {
36
+ handlers.delete(kind);
37
+ } else {
38
+ const kindMap = handlers.get(kind);
39
+ if (kindMap) {
40
+ if (kindMap.has(id)) {
41
+ kindMap.delete(id);
42
+ } else {
43
+ console.warn(`re-frame: can't clear ${kind} handler for ${id}. Handler not found.`);
44
+ }
45
+ }
46
+ }
47
+ }
48
+ };
49
+ }
50
+
51
+ // src/modules/state.ts
52
+ function createStateManager(initialState, onStateChange, onStateChangeForSubscriptions) {
53
+ let state = initialState;
54
+ const stateChanges = [];
55
+ let rafId = null;
56
+ const scheduleNotification = () => {
57
+ if (rafId !== null) return;
58
+ rafId = requestAnimationFrame(() => {
59
+ rafId = null;
60
+ if (stateChanges.length > 0) {
61
+ const latestState = stateChanges[stateChanges.length - 1];
62
+ stateChanges.length = 0;
63
+ if (onStateChange) {
64
+ onStateChange(latestState);
65
+ }
66
+ if (onStateChangeForSubscriptions) {
67
+ onStateChangeForSubscriptions(latestState);
68
+ }
69
+ }
70
+ });
71
+ };
72
+ return {
73
+ getState() {
74
+ return state;
75
+ },
76
+ setState(newState) {
77
+ const oldState = state;
78
+ if (!Object.is(oldState, newState)) {
79
+ state = newState;
80
+ stateChanges.push(newState);
81
+ scheduleNotification();
82
+ }
83
+ },
84
+ scheduleNotification
85
+ };
86
+ }
87
+
88
+ // src/modules/tracing.ts
89
+ function createTracer(config = {}) {
90
+ const traceEnabled = config.enabled ?? false;
91
+ const traceCallbacks = /* @__PURE__ */ new Map();
92
+ const traces = [];
93
+ let traceIdCounter = 0;
94
+ const debounceTime = config.debounceTime ?? 50;
95
+ let debounceTimer = null;
96
+ let nextDeliveryTime = 0;
97
+ const deliverTraces = () => {
98
+ if (traces.length === 0) return;
99
+ const tracesToDeliver = [...traces];
100
+ traces.length = 0;
101
+ for (const [key, callback] of traceCallbacks.entries()) {
102
+ try {
103
+ callback(tracesToDeliver);
104
+ } catch (error) {
105
+ console.error(`Error in trace callback "${key}":`, error);
106
+ }
107
+ }
108
+ debounceTimer = null;
109
+ nextDeliveryTime = 0;
110
+ };
111
+ const scheduleTraceDelivery = () => {
112
+ const now = Date.now();
113
+ if (now < nextDeliveryTime - 25) {
114
+ return;
115
+ }
116
+ if (debounceTimer !== null) {
117
+ clearTimeout(debounceTimer);
118
+ }
119
+ nextDeliveryTime = now + debounceTime;
120
+ debounceTimer = setTimeout(() => {
121
+ deliverTraces();
122
+ }, debounceTime);
123
+ };
124
+ const emitEventTrace = (trace) => {
125
+ if (!traceEnabled) return;
126
+ const traceWithId = {
127
+ ...trace,
128
+ id: ++traceIdCounter
129
+ };
130
+ traces.push(traceWithId);
131
+ scheduleTraceDelivery();
132
+ };
133
+ const registerTraceCallback = (key, callback) => {
134
+ if (!traceEnabled) {
135
+ console.warn("Tracing is not enabled. Set tracing.enabled to true in StoreConfig.");
136
+ return;
137
+ }
138
+ traceCallbacks.set(key, callback);
139
+ };
140
+ const removeTraceCallback = (key) => {
141
+ traceCallbacks.delete(key);
142
+ };
143
+ return {
144
+ registerTraceCallback,
145
+ removeTraceCallback,
146
+ emitEventTrace
147
+ };
148
+ }
149
+
150
+ // src/modules/errorHandler.ts
151
+ function createErrorHandler(initialHandler, initialConfig) {
152
+ let errorHandler = initialHandler || defaultErrorHandler;
153
+ let errorHandlerConfig = {
154
+ rethrow: false,
155
+ ...initialConfig
156
+ };
157
+ return {
158
+ register(handler, config) {
159
+ errorHandler = handler;
160
+ if (config) {
161
+ errorHandlerConfig = { ...errorHandlerConfig, ...config };
162
+ }
163
+ },
164
+ async handle(error, context) {
165
+ try {
166
+ await errorHandler(error, context, errorHandlerConfig);
167
+ if (errorHandlerConfig.rethrow) {
168
+ throw error;
169
+ }
170
+ } catch (handlerError) {
171
+ console.error("Error in error handler:", handlerError);
172
+ if (errorHandlerConfig.rethrow) {
173
+ throw error;
174
+ }
175
+ }
176
+ }
177
+ };
178
+ }
179
+ function defaultErrorHandler(error, context, config) {
180
+ const { eventKey, phase, interceptor } = context;
181
+ if (phase === "interceptor" && interceptor) {
182
+ console.error(
183
+ `Error in ${interceptor.direction} phase of interceptor "${interceptor.id || "unnamed"}" while handling event "${eventKey}":`,
184
+ error
185
+ );
186
+ } else if (phase === "effect" && interceptor) {
187
+ console.error(
188
+ `Error executing effect "${interceptor.id}" for event "${eventKey}":`,
189
+ error
190
+ );
191
+ } else if (phase === "subscription") {
192
+ console.error(
193
+ `Error in subscription "${eventKey}":`,
194
+ error
195
+ );
196
+ } else {
197
+ console.error(
198
+ `Error handling event "${eventKey}":`,
199
+ error
200
+ );
201
+ }
202
+ }
203
+
204
+ // src/modules/effects.ts
205
+ function createEffectExecutor(deps) {
206
+ const { registrar, stateManager, errorHandler, dispatch, deregisterEvent, registerEffect } = deps;
207
+ async function execute(effectMap, eventKey, payload, effectsExecuted) {
208
+ if (effectMap.db !== void 0) {
209
+ const dbHandler = registrar.get("effect", "db");
210
+ if (dbHandler) {
211
+ const effectStart = Date.now();
212
+ try {
213
+ await dbHandler(effectMap.db, deps);
214
+ const effectEnd = Date.now();
215
+ if (effectsExecuted) {
216
+ effectsExecuted.push({
217
+ effectType: "db",
218
+ config: effectMap.db,
219
+ start: effectStart,
220
+ end: effectEnd,
221
+ duration: effectEnd - effectStart
222
+ });
223
+ }
224
+ } catch (error) {
225
+ const effectEnd = Date.now();
226
+ if (effectsExecuted) {
227
+ effectsExecuted.push({
228
+ effectType: "db",
229
+ config: effectMap.db,
230
+ start: effectStart,
231
+ end: effectEnd,
232
+ duration: effectEnd - effectStart,
233
+ error
234
+ });
235
+ }
236
+ await errorHandler.handle(error, {
237
+ eventKey,
238
+ payload,
239
+ phase: "effect",
240
+ interceptor: {
241
+ id: "db",
242
+ direction: "after"
243
+ }
244
+ });
245
+ }
246
+ }
247
+ }
248
+ const effectsWithoutDb = Object.entries(effectMap).filter(
249
+ ([key]) => key !== "db" && key !== "fx"
250
+ );
251
+ await Promise.all(
252
+ effectsWithoutDb.map(async ([effectType, config]) => {
253
+ if (config === void 0 || config === null) {
254
+ return;
255
+ }
256
+ const handler = registrar.get("effect", effectType);
257
+ if (!handler) {
258
+ console.warn(`No effect handler registered for "${String(effectType)}"`);
259
+ return;
260
+ }
261
+ const effectStart = Date.now();
262
+ try {
263
+ await handler(config, deps);
264
+ const effectEnd = Date.now();
265
+ if (effectsExecuted) {
266
+ effectsExecuted.push({
267
+ effectType,
268
+ config,
269
+ start: effectStart,
270
+ end: effectEnd,
271
+ duration: effectEnd - effectStart
272
+ });
273
+ }
274
+ } catch (error) {
275
+ const effectEnd = Date.now();
276
+ if (effectsExecuted) {
277
+ effectsExecuted.push({
278
+ effectType,
279
+ config,
280
+ start: effectStart,
281
+ end: effectEnd,
282
+ duration: effectEnd - effectStart,
283
+ error
284
+ });
285
+ }
286
+ await errorHandler.handle(error, {
287
+ eventKey,
288
+ payload,
289
+ phase: "effect",
290
+ interceptor: {
291
+ id: effectType,
292
+ direction: "after"
293
+ }
294
+ });
295
+ }
296
+ })
297
+ );
298
+ if (effectMap.fx !== void 0) {
299
+ const fxHandler = registrar.get("effect", "fx");
300
+ if (fxHandler) {
301
+ const effectStart = Date.now();
302
+ try {
303
+ await fxHandler(effectMap.fx, deps);
304
+ const effectEnd = Date.now();
305
+ if (effectsExecuted) {
306
+ effectsExecuted.push({
307
+ effectType: "fx",
308
+ config: effectMap.fx,
309
+ start: effectStart,
310
+ end: effectEnd,
311
+ duration: effectEnd - effectStart
312
+ });
313
+ }
314
+ } catch (error) {
315
+ const effectEnd = Date.now();
316
+ if (effectsExecuted) {
317
+ effectsExecuted.push({
318
+ effectType: "fx",
319
+ config: effectMap.fx,
320
+ start: effectStart,
321
+ end: effectEnd,
322
+ duration: effectEnd - effectStart,
323
+ error
324
+ });
325
+ }
326
+ await errorHandler.handle(error, {
327
+ eventKey,
328
+ payload,
329
+ phase: "effect",
330
+ interceptor: {
331
+ id: "fx",
332
+ direction: "after"
333
+ }
334
+ });
335
+ }
336
+ }
337
+ }
338
+ }
339
+ function registerBuiltInEffects() {
340
+ registerEffect("db", (newState) => {
341
+ stateManager.setState(newState);
342
+ });
343
+ registerEffect("dispatch", async (config) => {
344
+ if (!config || typeof config.event !== "string") {
345
+ console.error("re-frame: ignoring bad :dispatch value. Expected {event: string, payload: any}, but got:", config);
346
+ return;
347
+ }
348
+ await dispatch(config.event, config.payload);
349
+ });
350
+ registerEffect("dispatch-n", async (configs) => {
351
+ if (!Array.isArray(configs)) {
352
+ console.error("re-frame: ignoring bad :dispatch-n value. Expected an array, but got:", configs);
353
+ return;
354
+ }
355
+ for (const config of configs) {
356
+ if (config && config.event) {
357
+ await dispatch(config.event, config.payload);
358
+ }
359
+ }
360
+ });
361
+ registerEffect("dispatch-later", (value) => {
362
+ if (!Array.isArray(value)) {
363
+ console.error("re-frame: ignoring bad :dispatch-later value. Expected an array, but got:", value);
364
+ return;
365
+ }
366
+ for (const effect of value.filter((e) => e != null)) {
367
+ if (!effect || typeof effect.ms !== "number" || !effect.event) {
368
+ console.error("re-frame: ignoring bad :dispatch-later entry:", effect);
369
+ continue;
370
+ }
371
+ setTimeout(() => {
372
+ dispatch(effect.event, effect.payload);
373
+ }, effect.ms);
374
+ }
375
+ });
376
+ registerEffect("fx", async (seqOfEffects) => {
377
+ if (!Array.isArray(seqOfEffects)) {
378
+ console.warn('re-frame: ":fx" effect expects an array, but was given', typeof seqOfEffects);
379
+ return;
380
+ }
381
+ for (const effectTuple of seqOfEffects) {
382
+ if (effectTuple == null) {
383
+ continue;
384
+ }
385
+ const [effectKey, effectValue] = effectTuple;
386
+ if (effectKey === "db") {
387
+ console.warn('re-frame: ":fx" effect should not contain a :db effect. Use top-level :db instead.');
388
+ continue;
389
+ }
390
+ const handler = registrar.get("effect", effectKey);
391
+ if (!handler) {
392
+ console.warn(`re-frame: in ":fx" effect found "${effectKey}" which has no associated handler. Ignoring.`);
393
+ continue;
394
+ }
395
+ try {
396
+ await handler(effectValue, deps);
397
+ } catch (error) {
398
+ await errorHandler.handle(error, {
399
+ eventKey: `fx:${effectKey}`,
400
+ payload: effectValue,
401
+ phase: "effect",
402
+ interceptor: {
403
+ id: effectKey,
404
+ direction: "after"
405
+ }
406
+ });
407
+ }
408
+ }
409
+ });
410
+ registerEffect("deregister-event-handler", (value) => {
411
+ if (Array.isArray(value)) {
412
+ for (const eventKey of value) {
413
+ if (typeof eventKey === "string") {
414
+ deregisterEvent(eventKey);
415
+ }
416
+ }
417
+ } else if (typeof value === "string") {
418
+ deregisterEvent(value);
419
+ }
420
+ });
421
+ }
422
+ return {
423
+ execute,
424
+ registerBuiltInEffects
425
+ };
426
+ }
427
+ function mergeEffects(...effects) {
428
+ const result = {};
429
+ for (const effect of effects) {
430
+ for (const [key, value] of Object.entries(effect)) {
431
+ if (key === "dispatch-n" && Array.isArray(value)) {
432
+ result["dispatch-n"] = [
433
+ ...result["dispatch-n"] || [],
434
+ ...value
435
+ ];
436
+ } else if (key === "dispatch-later" && Array.isArray(value)) {
437
+ result["dispatch-later"] = [
438
+ ...result["dispatch-later"] || [],
439
+ ...value
440
+ ];
441
+ } else if (key === "fx" && Array.isArray(value)) {
442
+ result.fx = [
443
+ ...result.fx || [],
444
+ ...value
445
+ ];
446
+ } else if (key === "deregister-event-handler") {
447
+ if (Array.isArray(value)) {
448
+ const existing = result["deregister-event-handler"];
449
+ if (Array.isArray(existing)) {
450
+ result["deregister-event-handler"] = [...existing, ...value];
451
+ } else if (typeof existing === "string") {
452
+ result["deregister-event-handler"] = [existing, ...value];
453
+ } else {
454
+ result["deregister-event-handler"] = value;
455
+ }
456
+ } else if (typeof value === "string") {
457
+ const existing = result["deregister-event-handler"];
458
+ if (Array.isArray(existing)) {
459
+ result["deregister-event-handler"] = [...existing, value];
460
+ } else if (typeof existing === "string") {
461
+ result["deregister-event-handler"] = [existing, value];
462
+ } else {
463
+ result["deregister-event-handler"] = value;
464
+ }
465
+ }
466
+ } else {
467
+ result[key] = value;
468
+ }
469
+ }
470
+ }
471
+ return result;
472
+ }
473
+
474
+ // src/modules/events.ts
475
+ function createEventManager(deps) {
476
+ const { registrar, stateManager, effectExecutor, errorHandler, tracer, coeffectProviders } = deps;
477
+ function dbHandlerToInterceptor(handler) {
478
+ return {
479
+ id: "db-handler",
480
+ before: (context) => {
481
+ const event = context.coeffects.event;
482
+ const newState = handler(context.coeffects, event);
483
+ return {
484
+ ...context,
485
+ effects: {
486
+ ...context.effects,
487
+ db: newState
488
+ }
489
+ };
490
+ }
491
+ };
492
+ }
493
+ function fxHandlerToInterceptor(handler) {
494
+ return {
495
+ id: "fx-handler",
496
+ before: (context) => {
497
+ const event = context.coeffects.event;
498
+ const effects = handler(context.coeffects, event);
499
+ return {
500
+ ...context,
501
+ effects: {
502
+ ...context.effects,
503
+ ...effects
504
+ }
505
+ };
506
+ }
507
+ };
508
+ }
509
+ function registerEventDb(eventKey, handler, interceptors) {
510
+ if (registrar.has("event", eventKey)) {
511
+ console.warn(`Event handler for "${eventKey}" is being overwritten`);
512
+ }
513
+ const handlerInterceptor = dbHandlerToInterceptor(handler);
514
+ const chain = interceptors ? [...interceptors, handlerInterceptor] : [handlerInterceptor];
515
+ registrar.register("event", eventKey, chain);
516
+ }
517
+ function registerEvent(eventKey, handler, interceptors) {
518
+ if (registrar.has("event", eventKey)) {
519
+ console.warn(`Event handler for "${eventKey}" is being overwritten`);
520
+ }
521
+ const handlerInterceptor = fxHandlerToInterceptor(handler);
522
+ const chain = interceptors ? [...interceptors, handlerInterceptor] : [handlerInterceptor];
523
+ registrar.register("event", eventKey, chain);
524
+ }
525
+ function deregisterEvent(eventKey) {
526
+ registrar.clear("event", eventKey);
527
+ }
528
+ async function handleEvent(eventKey, payload) {
529
+ const startTime = Date.now();
530
+ const stateBefore = stateManager.getState();
531
+ const interceptors = registrar.get("event", eventKey);
532
+ if (!interceptors || interceptors.length === 0) {
533
+ console.warn(`No handler registered for event "${eventKey}"`);
534
+ return;
535
+ }
536
+ const computedCoeffects = {};
537
+ for (const key in coeffectProviders) {
538
+ const provider = coeffectProviders[key];
539
+ computedCoeffects[key] = provider();
540
+ }
541
+ const initialContext = {
542
+ db: stateManager.getState(),
543
+ event: payload,
544
+ ...computedCoeffects
545
+ };
546
+ let context = {
547
+ coeffects: initialContext,
548
+ effects: {},
549
+ queue: [...interceptors],
550
+ // Copy to avoid mutation
551
+ stack: []
552
+ };
553
+ const effectsExecuted = [];
554
+ let eventError;
555
+ let effectMap = {};
556
+ try {
557
+ while (context.queue.length > 0) {
558
+ const interceptor = context.queue.shift();
559
+ context.stack.push(interceptor);
560
+ if (interceptor.before) {
561
+ try {
562
+ context = interceptor.before(context);
563
+ } catch (error) {
564
+ eventError = error;
565
+ await errorHandler.handle(error, {
566
+ eventKey,
567
+ payload,
568
+ phase: "interceptor",
569
+ interceptor: {
570
+ id: interceptor.id,
571
+ direction: "before"
572
+ }
573
+ });
574
+ break;
575
+ }
576
+ }
577
+ }
578
+ if (!eventError) {
579
+ while (context.stack.length > 0) {
580
+ const interceptor = context.stack.pop();
581
+ if (interceptor.after) {
582
+ try {
583
+ context = interceptor.after(context);
584
+ } catch (error) {
585
+ eventError = error;
586
+ await errorHandler.handle(error, {
587
+ eventKey,
588
+ payload,
589
+ phase: "interceptor",
590
+ interceptor: {
591
+ id: interceptor.id,
592
+ direction: "after"
593
+ }
594
+ });
595
+ break;
596
+ }
597
+ }
598
+ }
599
+ }
600
+ effectMap = context.effects;
601
+ if (!eventError) {
602
+ await effectExecutor.execute(context.effects, eventKey, payload, effectsExecuted);
603
+ }
604
+ } catch (error) {
605
+ eventError = error;
606
+ await errorHandler.handle(error, {
607
+ eventKey,
608
+ payload,
609
+ phase: "interceptor"
610
+ });
611
+ }
612
+ const stateAfter = stateManager.getState();
613
+ const duration = Date.now() - startTime;
614
+ const interceptorInfo = interceptors.map((interceptor, index) => ({
615
+ id: interceptor.id,
616
+ order: index
617
+ }));
618
+ tracer.emitEventTrace({
619
+ eventKey,
620
+ payload,
621
+ timestamp: startTime,
622
+ stateBefore,
623
+ stateAfter,
624
+ interceptors: interceptorInfo,
625
+ effectMap: eventError ? {} : effectMap,
626
+ effectsExecuted,
627
+ duration,
628
+ error: eventError
629
+ });
630
+ }
631
+ return {
632
+ registerEventDb,
633
+ registerEvent,
634
+ deregisterEvent,
635
+ handleEvent
636
+ };
637
+ }
638
+
639
+ // src/modules/router.ts
640
+ function createRouter(deps) {
641
+ const { eventManager } = deps;
642
+ const eventQueue = [];
643
+ let isProcessing = false;
644
+ async function processEvent(eventKey, payload) {
645
+ await eventManager.handleEvent(eventKey, payload);
646
+ }
647
+ async function processQueue() {
648
+ if (isProcessing) return;
649
+ isProcessing = true;
650
+ try {
651
+ while (eventQueue.length > 0) {
652
+ const event = eventQueue.shift();
653
+ await processEvent(event.eventKey, event.payload);
654
+ }
655
+ } finally {
656
+ isProcessing = false;
657
+ }
658
+ }
659
+ function dispatch(eventKey, payload) {
660
+ return new Promise((resolve, reject) => {
661
+ eventQueue.push({ eventKey, payload });
662
+ if (!isProcessing) {
663
+ processQueue().then(resolve).catch(reject);
664
+ } else {
665
+ resolve();
666
+ }
667
+ });
668
+ }
669
+ async function flush() {
670
+ await processQueue();
671
+ }
672
+ return {
673
+ dispatch,
674
+ flush
675
+ };
676
+ }
677
+
678
+ // src/modules/subscription.ts
679
+ var Subscription = class {
680
+ constructor(key, params) {
681
+ this.key = key;
682
+ this.params = params;
683
+ }
684
+ };
685
+ var SubscriptionRegistry = class {
686
+ constructor() {
687
+ this.subscriptions = /* @__PURE__ */ new Map();
688
+ // Map of cache keys to shared Subscription objects
689
+ this.subscriptionCache = /* @__PURE__ */ new Map();
690
+ // WeakMap for computed results - automatically GC'd when Subscription is no longer referenced
691
+ this.resultCache = /* @__PURE__ */ new WeakMap();
692
+ // Reference counting: track how many active subscriptions exist for each Subscription object
693
+ this.refCounts = /* @__PURE__ */ new WeakMap();
694
+ // Listeners for each subscription
695
+ this.listeners = /* @__PURE__ */ new WeakMap();
696
+ }
697
+ register(key, config) {
698
+ this.subscriptions.set(key, config);
699
+ }
700
+ /**
701
+ * Get or create a shared Subscription object for the given key+params
702
+ * Returns the same object for the same key+params (like re-frame)
703
+ */
704
+ getSubscription(key, params) {
705
+ const cacheKey = this.getCacheKey(key, params);
706
+ let subscription = this.subscriptionCache.get(cacheKey);
707
+ if (!subscription) {
708
+ subscription = new Subscription(key, params);
709
+ this.subscriptionCache.set(cacheKey, subscription);
710
+ this.refCounts.set(subscription, 0);
711
+ this.listeners.set(subscription, /* @__PURE__ */ new Set());
712
+ }
713
+ return subscription;
714
+ }
715
+ subscribe(state, key, params, callback, onError) {
716
+ const subscription = this.getSubscription(key, params);
717
+ const currentCount = this.refCounts.get(subscription) || 0;
718
+ this.refCounts.set(subscription, currentCount + 1);
719
+ const result = this.query(state, key, params, onError);
720
+ callback(result);
721
+ const listeners = this.listeners.get(subscription);
722
+ listeners.add(callback);
723
+ return () => {
724
+ listeners.delete(callback);
725
+ const count = this.refCounts.get(subscription) || 0;
726
+ const newCount = Math.max(0, count - 1);
727
+ this.refCounts.set(subscription, newCount);
728
+ if (newCount <= 0 && listeners.size === 0) {
729
+ const cacheKey = this.getCacheKey(key, params);
730
+ this.subscriptionCache.delete(cacheKey);
731
+ }
732
+ };
733
+ }
734
+ query(state, key, params, onError) {
735
+ const config = this.subscriptions.get(key);
736
+ if (!config) {
737
+ const error = new Error(`Subscription "${key}" not registered`);
738
+ if (onError) {
739
+ onError(error, key, params);
740
+ return void 0;
741
+ }
742
+ console.error(error.message);
743
+ return void 0;
744
+ }
745
+ const subscription = this.getSubscription(key, params);
746
+ const cached = this.resultCache.get(subscription);
747
+ if (cached && cached.state === state) {
748
+ return cached.result;
749
+ }
750
+ let result;
751
+ try {
752
+ if (config.compute) {
753
+ result = config.compute(state, ...params);
754
+ } else if (config.deps && config.combine) {
755
+ const depResults = config.deps.map((depKey) => this.query(state, depKey, [], onError));
756
+ result = config.combine(depResults, ...params);
757
+ } else {
758
+ const error = new Error(`Invalid subscription config for "${key}"`);
759
+ if (onError) {
760
+ onError(error, key, params);
761
+ return void 0;
762
+ }
763
+ console.error(error.message);
764
+ return void 0;
765
+ }
766
+ } catch (error) {
767
+ if (onError) {
768
+ onError(error, key, params);
769
+ } else {
770
+ console.error(`Error computing subscription "${key}":`, error);
771
+ }
772
+ return cached?.result ?? void 0;
773
+ }
774
+ this.resultCache.set(subscription, { state, result });
775
+ return result;
776
+ }
777
+ notifyListeners(newState) {
778
+ for (const subscription of this.subscriptionCache.values()) {
779
+ const listeners = this.listeners.get(subscription);
780
+ if (!listeners || listeners.size === 0) {
781
+ continue;
782
+ }
783
+ const oldCached = this.resultCache.get(subscription);
784
+ const oldResult = oldCached?.result;
785
+ const newResult = this.query(newState, subscription.key, subscription.params, void 0);
786
+ if (!oldCached || !this.deepEqual(oldResult, newResult)) {
787
+ listeners.forEach((callback) => callback(newResult));
788
+ }
789
+ }
790
+ }
791
+ getCacheKey(key, params) {
792
+ return `${key}:${JSON.stringify(params)}`;
793
+ }
794
+ deepEqual(a, b) {
795
+ return JSON.stringify(a) === JSON.stringify(b);
796
+ }
797
+ };
798
+
799
+ // src/modules/subscriptions.ts
800
+ function createSubscriptionManager(deps) {
801
+ const { stateManager, errorHandler } = deps;
802
+ const registry = new SubscriptionRegistry();
803
+ function createSubscriptionErrorHandler() {
804
+ return (error, subKey, subParams) => {
805
+ errorHandler.handle(error, {
806
+ eventKey: `subscription:${subKey}`,
807
+ payload: subParams,
808
+ phase: "subscription"
809
+ }).catch(() => {
810
+ });
811
+ };
812
+ }
813
+ function registerSubscription(key, config) {
814
+ registry.register(key, config);
815
+ }
816
+ function subscribe(key, params, callback) {
817
+ const onError = createSubscriptionErrorHandler();
818
+ return registry.subscribe(
819
+ stateManager.getState(),
820
+ key,
821
+ params,
822
+ callback,
823
+ onError
824
+ );
825
+ }
826
+ function query(key, params) {
827
+ const onError = createSubscriptionErrorHandler();
828
+ return registry.query(
829
+ stateManager.getState(),
830
+ key,
831
+ params,
832
+ onError
833
+ );
834
+ }
835
+ function getSubscription(key, params) {
836
+ return registry.getSubscription(key, params);
837
+ }
838
+ function notifyListeners(newState) {
839
+ registry.notifyListeners(newState);
840
+ }
841
+ return {
842
+ registerSubscription,
843
+ subscribe,
844
+ query,
845
+ getSubscription,
846
+ notifyListeners
847
+ };
848
+ }
849
+
850
+ // src/modules/store.ts
851
+ function createStore(config) {
852
+ const registrar = createRegistrar();
853
+ const coeffectProviders = config.coeffects || {};
854
+ const errorHandlerManager = createErrorHandler(
855
+ config.errorHandler?.handler,
856
+ config.errorHandler?.rethrow !== void 0 ? { rethrow: config.errorHandler.rethrow } : void 0
857
+ );
858
+ let subscriptionManagerRef = null;
859
+ const stateManager = createStateManager(
860
+ config.initialState,
861
+ config.onStateChange,
862
+ (state) => {
863
+ if (subscriptionManagerRef) {
864
+ subscriptionManagerRef.notifyListeners(state);
865
+ }
866
+ }
867
+ );
868
+ const subscriptionManager = createSubscriptionManager({
869
+ stateManager,
870
+ errorHandler: errorHandlerManager
871
+ });
872
+ subscriptionManagerRef = subscriptionManager;
873
+ const tracer = createTracer(config.tracing);
874
+ let dispatchFn = () => {
875
+ throw new Error("Router not initialized yet. This should not happen during store creation.");
876
+ };
877
+ let deregisterEventFn = () => {
878
+ throw new Error("Event manager not initialized yet.");
879
+ };
880
+ let registerEffectFn = () => {
881
+ throw new Error("Event manager not initialized yet.");
882
+ };
883
+ const effectExecutor = createEffectExecutor({
884
+ registrar,
885
+ stateManager,
886
+ errorHandler: errorHandlerManager,
887
+ dispatch: (eventKey, payload) => {
888
+ return dispatchFn(eventKey, payload);
889
+ },
890
+ deregisterEvent: (eventKey) => {
891
+ deregisterEventFn(eventKey);
892
+ },
893
+ registerEffect: (effectType, handler) => {
894
+ registerEffectFn(effectType, handler);
895
+ }
896
+ });
897
+ const eventManager = createEventManager({
898
+ registrar,
899
+ stateManager,
900
+ effectExecutor,
901
+ errorHandler: errorHandlerManager,
902
+ tracer,
903
+ coeffectProviders
904
+ });
905
+ deregisterEventFn = (eventKey) => {
906
+ eventManager.deregisterEvent(eventKey);
907
+ };
908
+ registerEffectFn = (effectType, handler) => {
909
+ registrar.register("effect", effectType, handler);
910
+ };
911
+ const router = createRouter({
912
+ eventManager
913
+ });
914
+ dispatchFn = (eventKey, payload) => {
915
+ return router.dispatch(eventKey, payload);
916
+ };
917
+ effectExecutor.registerBuiltInEffects();
918
+ return {
919
+ // Event registration
920
+ registerEventDb: (eventKey, handler, interceptors) => {
921
+ eventManager.registerEventDb(eventKey, handler, interceptors);
922
+ },
923
+ registerEvent: (eventKey, handler, interceptors) => {
924
+ eventManager.registerEvent(eventKey, handler, interceptors);
925
+ },
926
+ deregisterEvent: (eventKey) => {
927
+ eventManager.deregisterEvent(eventKey);
928
+ },
929
+ // Effect registration
930
+ registerEffect: (effectType, handler) => {
931
+ if (registrar.has("effect", effectType)) {
932
+ console.warn(`Effect handler for "${effectType}" is being overwritten`);
933
+ }
934
+ registrar.register("effect", effectType, handler);
935
+ },
936
+ // Event dispatching
937
+ dispatch: (eventKey, payload) => {
938
+ return router.dispatch(eventKey, payload);
939
+ },
940
+ flush: async () => {
941
+ await router.flush();
942
+ },
943
+ // State access
944
+ getState: () => {
945
+ return stateManager.getState();
946
+ },
947
+ // Interceptor inspection
948
+ getInterceptors: (eventKey) => {
949
+ return registrar.get("event", eventKey);
950
+ },
951
+ // Error handling
952
+ registerErrorHandler: (handler, config2) => {
953
+ errorHandlerManager.register(handler, config2);
954
+ },
955
+ // Subscriptions
956
+ registerSubscription: (key, config2) => {
957
+ subscriptionManager.registerSubscription(key, config2);
958
+ },
959
+ subscribe: (key, params, callback) => {
960
+ return subscriptionManager.subscribe(key, params, callback);
961
+ },
962
+ query: (key, params) => {
963
+ return subscriptionManager.query(key, params);
964
+ },
965
+ getSubscription: (key, params) => {
966
+ return subscriptionManager.getSubscription(key, params);
967
+ },
968
+ // Tracing
969
+ registerTraceCallback: (key, callback) => {
970
+ tracer.registerTraceCallback(key, callback);
971
+ },
972
+ removeTraceCallback: (key) => {
973
+ tracer.removeTraceCallback(key);
974
+ }
975
+ };
976
+ }
977
+
978
+ // src/modules/interceptor.ts
979
+ function path(pathKeys) {
980
+ return {
981
+ id: `path-${pathKeys.join(".")}`,
982
+ before: (context) => {
983
+ let value = context.coeffects.db;
984
+ for (const key of pathKeys) {
985
+ value = value?.[key];
986
+ }
987
+ return {
988
+ ...context,
989
+ coeffects: {
990
+ ...context.coeffects,
991
+ _originalDb: context.coeffects.db,
992
+ _pathKeys: pathKeys,
993
+ db: value
994
+ }
995
+ };
996
+ },
997
+ after: (context) => {
998
+ const pathKeys2 = context.coeffects._pathKeys;
999
+ const originalDb = context.coeffects._originalDb;
1000
+ const focusedState = context.effects.db;
1001
+ if (pathKeys2 && originalDb !== void 0 && focusedState !== void 0) {
1002
+ let newDb = { ...originalDb };
1003
+ let current = newDb;
1004
+ for (let i = 0; i < pathKeys2.length - 1; i++) {
1005
+ current[pathKeys2[i]] = { ...current[pathKeys2[i]] };
1006
+ current = current[pathKeys2[i]];
1007
+ }
1008
+ current[pathKeys2[pathKeys2.length - 1]] = focusedState;
1009
+ return {
1010
+ ...context,
1011
+ coeffects: {
1012
+ ...context.coeffects,
1013
+ db: originalDb
1014
+ },
1015
+ effects: {
1016
+ ...context.effects,
1017
+ db: newDb
1018
+ }
1019
+ };
1020
+ }
1021
+ if (pathKeys2 && originalDb !== void 0) {
1022
+ return {
1023
+ ...context,
1024
+ coeffects: {
1025
+ ...context.coeffects,
1026
+ db: originalDb
1027
+ }
1028
+ };
1029
+ }
1030
+ return context;
1031
+ }
1032
+ };
1033
+ }
1034
+ function debug() {
1035
+ return {
1036
+ id: "debug",
1037
+ before: (context) => {
1038
+ console.group("Event");
1039
+ console.log("Coeffects:", context.coeffects);
1040
+ return context;
1041
+ },
1042
+ after: (context) => {
1043
+ console.log("New State:", context.coeffects.db);
1044
+ console.log("Effects:", context.effects);
1045
+ console.groupEnd();
1046
+ return context;
1047
+ }
1048
+ };
1049
+ }
1050
+ function after(fn) {
1051
+ return {
1052
+ id: "after",
1053
+ after: (context) => {
1054
+ fn(context.coeffects.db, context.effects);
1055
+ return context;
1056
+ }
1057
+ };
1058
+ }
1059
+ function injectCofx(key, value) {
1060
+ return {
1061
+ id: `inject-${key}`,
1062
+ before: (context) => {
1063
+ return {
1064
+ ...context,
1065
+ coeffects: {
1066
+ ...context.coeffects,
1067
+ [key]: value
1068
+ }
1069
+ };
1070
+ }
1071
+ };
1072
+ }
1073
+ function validate(schema) {
1074
+ return {
1075
+ id: "validate",
1076
+ after: (context) => {
1077
+ const result = schema(context.coeffects.db);
1078
+ if (result !== true) {
1079
+ console.error("State validation failed:", result);
1080
+ }
1081
+ return context;
1082
+ }
1083
+ };
1084
+ }
1085
+ export {
1086
+ Subscription,
1087
+ SubscriptionRegistry,
1088
+ after,
1089
+ createStore,
1090
+ debug,
1091
+ defaultErrorHandler,
1092
+ injectCofx,
1093
+ mergeEffects,
1094
+ path,
1095
+ validate
1096
+ };