storion 0.6.0 → 0.7.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.
@@ -1,3 +1,4 @@
1
+ import { e as emitter, w as withHooks, h as hasReadHook, t as trackRead, a as hasWriteHook, c as trackWrite, s as scheduleNotification, b as batch } from "./emitter-BA44OHdL.js";
1
2
  const STORION_TYPE = Symbol("STORION");
2
3
  function is$1(value, kind) {
3
4
  return value !== null && (typeof value === "object" || typeof value === "function") && STORION_TYPE in value && value[STORION_TYPE] === kind;
@@ -1930,6 +1931,11 @@ function createStoreContext(options) {
1930
1931
  return mixin(ctx, ...args);
1931
1932
  },
1932
1933
  focus(path, options2) {
1934
+ if (!isSetupPhase()) {
1935
+ throw new Error(
1936
+ `focus() can only be called during setup phase. Do not call focus() inside actions or async callbacks. Use the .to() method on an existing focus for dynamic sub-paths.`
1937
+ );
1938
+ }
1933
1939
  const focusCtx = {
1934
1940
  getState: getMutableState,
1935
1941
  update: (updater) => {
@@ -1942,75 +1948,6 @@ function createStoreContext(options) {
1942
1948
  };
1943
1949
  return ctx;
1944
1950
  }
1945
- let globalHooks = {
1946
- scheduleNotification(notify) {
1947
- notify();
1948
- },
1949
- scheduleEffect(runEffect) {
1950
- runEffect();
1951
- }
1952
- };
1953
- function getHooks() {
1954
- return globalHooks;
1955
- }
1956
- function hasReadHook() {
1957
- return globalHooks.onRead !== void 0;
1958
- }
1959
- function hasWriteHook() {
1960
- return globalHooks.onWrite !== void 0;
1961
- }
1962
- function withHooks(hooksOrSetup, fn, onFinish) {
1963
- const prev = globalHooks;
1964
- if (typeof hooksOrSetup === "function") {
1965
- globalHooks = {
1966
- ...globalHooks,
1967
- ...hooksOrSetup(prev)
1968
- };
1969
- } else {
1970
- globalHooks = { ...prev, ...hooksOrSetup };
1971
- }
1972
- try {
1973
- return fn();
1974
- } finally {
1975
- globalHooks = prev;
1976
- onFinish == null ? void 0 : onFinish();
1977
- }
1978
- }
1979
- function trackRead(storeId, prop, value, subscribe) {
1980
- var _a;
1981
- const key = `${storeId}.${prop}`;
1982
- (_a = globalHooks.onRead) == null ? void 0 : _a.call(globalHooks, { key, value, subscribe });
1983
- }
1984
- function trackWrite(storeId, prop, next, prev) {
1985
- var _a;
1986
- const key = `${storeId}.${prop}`;
1987
- (_a = globalHooks.onWrite) == null ? void 0 : _a.call(globalHooks, { key, next, prev });
1988
- }
1989
- function untrack(fn) {
1990
- return withHooks({ onRead: void 0, onWrite: void 0 }, fn);
1991
- }
1992
- function scheduleNotification(notify, key) {
1993
- globalHooks.scheduleNotification(notify, key);
1994
- }
1995
- function batch(fn) {
1996
- const pending = /* @__PURE__ */ new Map();
1997
- return withHooks(
1998
- (current2) => ({
1999
- ...current2,
2000
- scheduleNotification: (notify, key) => {
2001
- const actualKey = key ?? notify;
2002
- pending.set(actualKey, notify);
2003
- }
2004
- }),
2005
- fn,
2006
- // Flush on finish
2007
- () => {
2008
- for (const notify of pending.values()) {
2009
- notify();
2010
- }
2011
- }
2012
- );
2013
- }
2014
1951
  let specIdCounter = 0;
2015
1952
  function generateSpecName() {
2016
1953
  return `spec-${++specIdCounter}`;
@@ -2032,153 +1969,6 @@ function unwrapFn(fn) {
2032
1969
  function isWrappedFn(fn) {
2033
1970
  return ORIGINAL_FN in fn;
2034
1971
  }
2035
- function emitter(initialListeners) {
2036
- const listeners = /* @__PURE__ */ new Set([]);
2037
- let settledPayload;
2038
- let isSettled = false;
2039
- const noop = () => {
2040
- };
2041
- const doEmit = (payload, clear, lifo = false) => {
2042
- const copy = Array.from(listeners);
2043
- if (clear) {
2044
- listeners.clear();
2045
- }
2046
- const len = copy.length;
2047
- if (lifo) {
2048
- for (let i = len - 1; i >= 0; i--) {
2049
- copy[i](payload);
2050
- }
2051
- } else {
2052
- for (let i = 0; i < len; i++) {
2053
- copy[i](payload);
2054
- }
2055
- }
2056
- };
2057
- return {
2058
- get size() {
2059
- return listeners.size;
2060
- },
2061
- get settled() {
2062
- return isSettled;
2063
- },
2064
- /**
2065
- * Adds one or more listeners to the emitter.
2066
- *
2067
- * The listener(s) will be called whenever `emit()` is called.
2068
- * Returns an unsubscribe function that removes the listener(s).
2069
- *
2070
- * **Important**: The unsubscribe function is idempotent - calling it multiple
2071
- * times is safe and won't cause errors. If the same listener is added multiple
2072
- * times, it will only be called once per emit (Set deduplication).
2073
- *
2074
- * **Settled behavior**: If the emitter is settled, listeners are called
2075
- * immediately with the settled payload and a no-op unsubscribe is returned.
2076
- *
2077
- * @param newListeners - Single listener or array of listeners to add
2078
- * @returns An unsubscribe function that removes the listener(s)
2079
- */
2080
- on(...args) {
2081
- let newListeners = [];
2082
- if (args.length < 2) {
2083
- newListeners = Array.isArray(args[0]) ? args[0] : [args[0]];
2084
- } else {
2085
- const map = args[0];
2086
- const sourceListeners = Array.isArray(args[1]) ? args[1] : [args[1]];
2087
- newListeners = [
2088
- (value) => {
2089
- const mappedValue = map(value);
2090
- if (mappedValue) {
2091
- for (const listener of sourceListeners) {
2092
- listener(mappedValue.value);
2093
- }
2094
- }
2095
- }
2096
- ];
2097
- }
2098
- if (isSettled) {
2099
- for (const listener of newListeners) {
2100
- listener(settledPayload);
2101
- }
2102
- return noop;
2103
- }
2104
- for (const listener of newListeners) {
2105
- listeners.add(listener);
2106
- }
2107
- return () => {
2108
- for (const listener of newListeners) {
2109
- listeners.delete(listener);
2110
- }
2111
- };
2112
- },
2113
- /**
2114
- * Emits an event to all registered listeners.
2115
- *
2116
- * **Important**: Creates a snapshot of listeners before iterating to ensure
2117
- * that modifications during emission (adding/removing listeners) don't affect
2118
- * the current emission cycle. This prevents:
2119
- * - New listeners added during emission from being called immediately
2120
- * - Issues with listeners that unsubscribe during emission
2121
- *
2122
- * Performance: For typical use cases (< 20 listeners), Array.from() overhead
2123
- * is negligible compared to calling the listener functions themselves.
2124
- *
2125
- * **Settled behavior**: After `settle()` is called, `emit()` becomes a no-op.
2126
- *
2127
- * @param payload - The value to pass to all listeners
2128
- */
2129
- emit(payload) {
2130
- if (isSettled) return;
2131
- doEmit(payload, false);
2132
- },
2133
- emitLifo(payload) {
2134
- if (isSettled) return;
2135
- doEmit(payload, false, true);
2136
- },
2137
- /**
2138
- * Removes all registered listeners.
2139
- *
2140
- * After calling `clear()`, no listeners will be notified until new ones
2141
- * are added via `on()`.
2142
- */
2143
- clear() {
2144
- listeners.clear();
2145
- },
2146
- /**
2147
- * Emits an event to all registered listeners and then clears all listeners.
2148
- *
2149
- * **Settled behavior**: After `settle()` is called, `emitAndClear()` becomes a no-op.
2150
- *
2151
- * @param payload - The value to pass to all listeners
2152
- */
2153
- emitAndClear(payload) {
2154
- if (isSettled) return;
2155
- doEmit(payload, true);
2156
- },
2157
- emitAndClearLifo(payload) {
2158
- if (isSettled) return;
2159
- doEmit(payload, true, true);
2160
- },
2161
- /**
2162
- * Emit to all listeners, clear, and "settle" the emitter.
2163
- *
2164
- * After settling:
2165
- * - Any new `on()` call immediately invokes the listener with the settled payload
2166
- * - Returns a no-op unsubscribe function
2167
- * - `emit()` and `emitAndClear()` become no-ops
2168
- *
2169
- * **Important**: `isSettled` is set BEFORE emitting so that listeners
2170
- * added during emission see the settled state and get called immediately.
2171
- *
2172
- * @param payload - The final value to pass to all listeners
2173
- */
2174
- settle(payload) {
2175
- if (isSettled) return;
2176
- settledPayload = payload;
2177
- isSettled = true;
2178
- doEmit(payload, true);
2179
- }
2180
- };
2181
- }
2182
1972
  function collection(createItem, initialItems) {
2183
1973
  const map = /* @__PURE__ */ new Map([]);
2184
1974
  return {
@@ -2655,32 +2445,26 @@ function createStoreInstance(spec, resolver, instanceOptions = {}) {
2655
2445
  return instance;
2656
2446
  }
2657
2447
  export {
2658
- scheduleNotification as A,
2659
2448
  STORION_TYPE as S,
2660
2449
  is$1 as a,
2661
2450
  isStorion as b,
2662
2451
  createStoreInstance as c,
2663
2452
  isContainer as d,
2664
- emitter as e,
2665
- isStore as f,
2453
+ isStore as e,
2454
+ isFocus as f,
2666
2455
  getKind as g,
2667
- isFocus as h,
2456
+ isAction as h,
2668
2457
  isSpec as i,
2669
- isAction as j,
2670
- isStoreContext as k,
2671
- isSelectorContext as l,
2672
- batch as m,
2673
- equality as n,
2674
- shallowEqual as o,
2675
- deepEqual as p,
2676
- strictEqual as q,
2458
+ isStoreContext as j,
2459
+ isSelectorContext as k,
2460
+ equality as l,
2461
+ shallowEqual as m,
2462
+ deepEqual as n,
2463
+ strictEqual as o,
2464
+ isWrappedFn as p,
2677
2465
  resolveEquality as r,
2678
2466
  store as s,
2679
- wrapFn as t,
2680
- untrack as u,
2681
- unwrapFn as v,
2682
- withHooks as w,
2683
- isWrappedFn as x,
2684
- tryDispose as y,
2685
- getHooks as z
2467
+ tryDispose as t,
2468
+ unwrapFn as u,
2469
+ wrapFn as w
2686
2470
  };
package/dist/storion.js CHANGED
@@ -1,5 +1,8 @@
1
- import { y as tryDispose, i as isSpec, e as emitter, u as untrack, S as STORION_TYPE, z as getHooks, w as withHooks, r as resolveEquality, A as scheduleNotification, v as unwrapFn, o as shallowEqual } from "./store-DS-4XdM6.js";
2
- import { m, p, n, g, a, j, d, h, l, f, k, b, x, s, q, t } from "./store-DS-4XdM6.js";
1
+ import { t as tryDispose, i as isSpec, S as STORION_TYPE, r as resolveEquality, u as unwrapFn, m as shallowEqual } from "./store-BroaE7p4.js";
2
+ import { n, l, g, a, h, d, f, k, e, j, b, p, s, o, w } from "./store-BroaE7p4.js";
3
+ import { e as emitter, u as untrack, g as getHooks, w as withHooks } from "./emitter-BA44OHdL.js";
4
+ import { b as b2 } from "./emitter-BA44OHdL.js";
5
+ import { e as e2 } from "./effect-DPAYSuW3.js";
3
6
  function extractDisplayName(factory) {
4
7
  if (isSpec(factory)) {
5
8
  return factory.displayName;
@@ -323,279 +326,6 @@ function pick(selector, equality) {
323
326
  });
324
327
  return currentValue;
325
328
  }
326
- function isPromiseLike(value) {
327
- return typeof value === "object" && value !== null && "then" in value && typeof value.then === "function";
328
- }
329
- function createEffectContext(nth) {
330
- let cleanupEmitter = null;
331
- let abortController = null;
332
- let isStale = false;
333
- const runCleanups = () => {
334
- if (isStale) return;
335
- isStale = true;
336
- if (abortController) {
337
- abortController.abort();
338
- abortController = null;
339
- }
340
- if (cleanupEmitter && cleanupEmitter.size > 0) {
341
- cleanupEmitter.emitAndClearLifo();
342
- }
343
- };
344
- function wrapPromise(promise) {
345
- return new Promise((resolve, reject) => {
346
- promise.then(
347
- (value) => {
348
- if (!isStale) {
349
- resolve(value);
350
- }
351
- },
352
- (error) => {
353
- if (!isStale) {
354
- reject(error);
355
- }
356
- }
357
- );
358
- });
359
- }
360
- const context = {
361
- nth,
362
- get signal() {
363
- if (!abortController) {
364
- abortController = new AbortController();
365
- }
366
- return abortController.signal;
367
- },
368
- onCleanup(listener) {
369
- if (!cleanupEmitter) {
370
- cleanupEmitter = emitter();
371
- }
372
- return cleanupEmitter.on(listener);
373
- },
374
- safe(promiseOrCallback) {
375
- if (promiseOrCallback instanceof Promise) {
376
- return wrapPromise(promiseOrCallback);
377
- }
378
- return (...args) => {
379
- if (!isStale) {
380
- const result = promiseOrCallback(
381
- ...args
382
- );
383
- if (isPromiseLike(result)) {
384
- return wrapPromise(result);
385
- }
386
- return result;
387
- }
388
- return void 0;
389
- };
390
- }
391
- };
392
- return Object.assign(context, { _runCleanups: runCleanups });
393
- }
394
- function resolveErrorStrategy(effectOptions) {
395
- return (effectOptions == null ? void 0 : effectOptions.onError) ?? "keepAlive";
396
- }
397
- function getRetryDelay(config, attempt) {
398
- if (typeof config.delay === "function") {
399
- return config.delay(attempt);
400
- }
401
- return config.delay ?? 100 * Math.pow(2, attempt);
402
- }
403
- function effect(fn, options) {
404
- let currentContext = null;
405
- let subscriptionEmitter = null;
406
- let isStarted = false;
407
- let isRunning = false;
408
- let isDisposed = false;
409
- let runGeneration = 0;
410
- let retryCount = 0;
411
- let retryTimeout = null;
412
- let errorStrategy = "keepAlive";
413
- let onErrorCallback = null;
414
- let prevTrackedDeps = /* @__PURE__ */ new Map();
415
- let prevSubscriptionEmitter = null;
416
- let trackedDeps = /* @__PURE__ */ new Map();
417
- const writtenProps = /* @__PURE__ */ new Set();
418
- let newTrackedDeps = null;
419
- const getSubscriptionEmitter = () => {
420
- if (!subscriptionEmitter) {
421
- subscriptionEmitter = emitter();
422
- }
423
- return subscriptionEmitter;
424
- };
425
- const clearSubscriptions = () => {
426
- if (subscriptionEmitter && subscriptionEmitter.size > 0) {
427
- subscriptionEmitter.emitAndClear();
428
- }
429
- };
430
- const areDepsChanged = (prev, next) => {
431
- if (prev.size !== next.size) return true;
432
- for (const key of next.keys()) {
433
- if (!prev.has(key)) return true;
434
- }
435
- return false;
436
- };
437
- const subscribeToTrackedDeps = () => {
438
- for (const [key, dep] of trackedDeps) {
439
- if (writtenProps.has(key)) continue;
440
- const unsub = dep.subscribe(() => {
441
- scheduleNotification(execute, fn);
442
- });
443
- getSubscriptionEmitter().on(unsub);
444
- }
445
- };
446
- const handleError = (error) => {
447
- onErrorCallback == null ? void 0 : onErrorCallback(error);
448
- if (errorStrategy === "failFast") {
449
- throw error;
450
- }
451
- if (errorStrategy === "keepAlive") {
452
- console.error("Effect error (keepAlive):", error);
453
- if (prevSubscriptionEmitter && prevSubscriptionEmitter.size > 0) {
454
- trackedDeps = new Map(prevTrackedDeps);
455
- subscriptionEmitter = prevSubscriptionEmitter;
456
- prevSubscriptionEmitter = null;
457
- return;
458
- }
459
- if (newTrackedDeps && newTrackedDeps.size > 0) {
460
- trackedDeps = newTrackedDeps;
461
- }
462
- subscribeToTrackedDeps();
463
- return;
464
- }
465
- if (typeof errorStrategy === "function") {
466
- const retry = () => {
467
- retryCount++;
468
- execute();
469
- };
470
- errorStrategy({ error, retry, retryCount });
471
- return;
472
- }
473
- const retryConfig = errorStrategy;
474
- if (retryCount < retryConfig.maxRetries) {
475
- const delay = getRetryDelay(retryConfig, retryCount);
476
- retryCount++;
477
- retryTimeout = setTimeout(() => {
478
- retryTimeout = null;
479
- execute();
480
- }, delay);
481
- } else {
482
- console.error(
483
- `Effect failed after ${retryConfig.maxRetries} retries:`,
484
- error
485
- );
486
- }
487
- };
488
- let cachedHooks = null;
489
- const getTrackingHooks = (current) => {
490
- if (!cachedHooks) {
491
- cachedHooks = {
492
- ...current,
493
- onRead: (event) => {
494
- var _a;
495
- (_a = current.onRead) == null ? void 0 : _a.call(current, event);
496
- if (!newTrackedDeps.has(event.key)) {
497
- newTrackedDeps.set(event.key, {
498
- key: event.key,
499
- value: event.value,
500
- subscribe: event.subscribe
501
- });
502
- }
503
- },
504
- onWrite: (event) => {
505
- var _a;
506
- (_a = current.onWrite) == null ? void 0 : _a.call(current, event);
507
- writtenProps.add(event.key);
508
- },
509
- scheduleNotification: current.scheduleNotification,
510
- scheduleEffect: current.scheduleEffect
511
- };
512
- }
513
- return cachedHooks;
514
- };
515
- const execute = () => {
516
- if (isDisposed || isRunning) return;
517
- isRunning = true;
518
- const currentGeneration = ++runGeneration;
519
- try {
520
- currentContext == null ? void 0 : currentContext._runCleanups();
521
- currentContext = null;
522
- if (subscriptionEmitter && subscriptionEmitter.size > 0) {
523
- prevTrackedDeps = new Map(trackedDeps);
524
- prevSubscriptionEmitter = subscriptionEmitter;
525
- subscriptionEmitter = null;
526
- }
527
- newTrackedDeps = /* @__PURE__ */ new Map();
528
- writtenProps.clear();
529
- let lazyContext = null;
530
- const getOrCreateContext = () => {
531
- if (!lazyContext) {
532
- lazyContext = createEffectContext(currentGeneration);
533
- }
534
- return lazyContext;
535
- };
536
- const lazyContextProxy = new Proxy({}, {
537
- get(_, prop) {
538
- return getOrCreateContext()[prop];
539
- }
540
- });
541
- withHooks(getTrackingHooks, () => {
542
- const result = fn(lazyContextProxy);
543
- if (result !== null && result !== void 0 && typeof result.then === "function") {
544
- throw new Error(
545
- "Effect function must be synchronous. Use ctx.safe(promise) for async operations instead of returning a Promise."
546
- );
547
- }
548
- });
549
- currentContext = lazyContext;
550
- retryCount = 0;
551
- const depsChanged = areDepsChanged(trackedDeps, newTrackedDeps);
552
- if (depsChanged) {
553
- if (prevSubscriptionEmitter && prevSubscriptionEmitter.size > 0) {
554
- prevSubscriptionEmitter.emitAndClear();
555
- }
556
- trackedDeps = newTrackedDeps;
557
- subscribeToTrackedDeps();
558
- } else {
559
- if (prevSubscriptionEmitter) {
560
- subscriptionEmitter = prevSubscriptionEmitter;
561
- }
562
- }
563
- prevTrackedDeps.clear();
564
- prevSubscriptionEmitter = null;
565
- } catch (error) {
566
- handleError(error);
567
- } finally {
568
- isRunning = false;
569
- }
570
- };
571
- const dispose = () => {
572
- if (isDisposed) return;
573
- isDisposed = true;
574
- runGeneration++;
575
- if (retryTimeout) {
576
- clearTimeout(retryTimeout);
577
- retryTimeout = null;
578
- }
579
- currentContext == null ? void 0 : currentContext._runCleanups();
580
- currentContext = null;
581
- clearSubscriptions();
582
- if (prevSubscriptionEmitter && prevSubscriptionEmitter.size > 0) {
583
- prevSubscriptionEmitter.emitAndClear();
584
- }
585
- };
586
- const start = (runOptions) => {
587
- if (isStarted) return;
588
- isStarted = true;
589
- errorStrategy = resolveErrorStrategy(options);
590
- onErrorCallback = (runOptions == null ? void 0 : runOptions.onError) ?? null;
591
- execute();
592
- };
593
- getHooks().scheduleEffect((runOptions) => {
594
- start(runOptions);
595
- return dispose;
596
- });
597
- return dispose;
598
- }
599
329
  function patternToPredicate(pattern) {
600
330
  if (pattern instanceof RegExp) {
601
331
  return (name) => pattern.test(name);
@@ -723,34 +453,34 @@ export {
723
453
  STORION_TYPE,
724
454
  applyExcept,
725
455
  applyFor,
726
- m as batch,
456
+ b2 as batch,
727
457
  compose,
728
458
  container,
729
459
  createLoggingMiddleware,
730
460
  createResolver,
731
461
  createValidationMiddleware,
732
- p as deepEqual,
733
- effect,
734
- n as equality,
462
+ n as deepEqual,
463
+ e2 as effect,
464
+ l as equality,
735
465
  forStores,
736
466
  g as getKind,
737
467
  a as is,
738
- j as isAction,
468
+ h as isAction,
739
469
  d as isContainer,
740
- h as isFocus,
741
- l as isSelectorContext,
470
+ f as isFocus,
471
+ k as isSelectorContext,
742
472
  isSpec,
743
- f as isStore,
744
- k as isStoreContext,
473
+ e as isStore,
474
+ j as isStoreContext,
745
475
  b as isStorion,
746
- x as isWrappedFn,
476
+ p as isWrappedFn,
747
477
  pick,
748
478
  shallowEqual,
749
479
  s as store,
750
- q as strictEqual,
480
+ o as strictEqual,
751
481
  trigger,
752
482
  untrack,
753
483
  unwrapFn,
754
484
  when,
755
- t as wrapFn
485
+ w as wrapFn
756
486
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "storion",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "description": "Reactive stores for modern apps. Type-safe. Auto-tracked. Effortlessly composable",
5
5
  "type": "module",
6
6
  "main": "./dist/storion.js",