rask-ui 0.4.0 → 0.4.1

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/README.md CHANGED
@@ -62,9 +62,7 @@ RASK gives you:
62
62
 
63
63
  - **Simple state management** - No reconciler interference with your state management
64
64
  - **Full reconciler power** - Express complex UIs naturally with the language
65
- - **No special syntax** - Access state properties directly, no function calls
66
- - **No compiler magic** - Plain JavaScript/TypeScript
67
- - **Simple mental model** - Just implement state and UI. No manual optimizations, special syntax or compiler magic
65
+ - **No compiler magic** - Plain JavaScript/TypeScript, it runs as you write it
68
66
 
69
67
  :fire: Built on [Inferno JS](https://github.com/infernojs/inferno).
70
68
 
@@ -76,6 +74,19 @@ RASK gives you:
76
74
  npm install rask-ui
77
75
  ```
78
76
 
77
+ ### Configuration
78
+
79
+ Configure TypeScript to use RASK's JSX runtime:
80
+
81
+ ```json
82
+ {
83
+ "compilerOptions": {
84
+ "jsx": "react-jsx",
85
+ "jsxImportSource": "rask-ui"
86
+ }
87
+ }
88
+ ```
89
+
79
90
  ### Basic Example
80
91
 
81
92
  ```tsx
@@ -449,8 +460,7 @@ function ShoppingCart() {
449
460
  state.items.reduce((sum, item) => sum + item.price * item.quantity, 0),
450
461
  tax: () => computed.subtotal * state.taxRate,
451
462
  total: () => computed.subtotal + computed.tax,
452
- itemCount: () =>
453
- state.items.reduce((sum, item) => sum + item.quantity, 0),
463
+ itemCount: () => state.items.reduce((sum, item) => sum + item.quantity, 0),
454
464
  });
455
465
 
456
466
  return () => (
@@ -467,7 +477,9 @@ function ShoppingCart() {
467
477
  </ul>
468
478
  <div>
469
479
  <p>Subtotal: ${computed.subtotal.toFixed(2)}</p>
470
- <p>Tax ({state.taxRate * 100}%): ${computed.tax.toFixed(2)}</p>
480
+ <p>
481
+ Tax ({state.taxRate * 100}%): ${computed.tax.toFixed(2)}
482
+ </p>
471
483
  <p>
472
484
  <strong>Total: ${computed.total.toFixed(2)}</strong>
473
485
  </p>
@@ -966,6 +978,7 @@ function ShoppingCart() {
966
978
  ```
967
979
 
968
980
  Benefits of `createComputed`:
981
+
969
982
  - **Cached** - Only recalculates when dependencies change
970
983
  - **Lazy** - Only calculates when accessed
971
984
  - **Composable** - Computed properties can depend on other computed properties
@@ -1117,21 +1130,6 @@ function TodoList() {
1117
1130
  }
1118
1131
  ```
1119
1132
 
1120
- ## Configuration
1121
-
1122
- ### JSX Setup
1123
-
1124
- Configure TypeScript to use RASK's JSX runtime:
1125
-
1126
- ```json
1127
- {
1128
- "compilerOptions": {
1129
- "jsx": "react-jsx",
1130
- "jsxImportSource": "rask-ui"
1131
- }
1132
- }
1133
- ```
1134
-
1135
1133
  ## Performance
1136
1134
 
1137
1135
  RASK is designed for performance:
package/dist/batch.d.ts CHANGED
@@ -1,4 +1,7 @@
1
- export declare function queue(cb: () => void): void;
1
+ export type QueuedCallback = (() => void) & {
2
+ __queued: boolean;
3
+ };
4
+ export declare function queue(cb: QueuedCallback): void;
2
5
  export declare function syncBatch(cb: () => void): void;
3
6
  export declare function installEventBatching(target?: EventTarget): void;
4
7
  //# sourceMappingURL=batch.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"batch.d.ts","sourceRoot":"","sources":["../src/batch.ts"],"names":[],"mappings":"AAmDA,wBAAgB,KAAK,CAAC,EAAE,EAAE,MAAM,IAAI,QAWnC;AAED,wBAAgB,SAAS,CAAC,EAAE,EAAE,MAAM,IAAI,QAOvC;AAED,wBAAgB,oBAAoB,CAAC,MAAM,GAAE,WAAsB,QA0BlE"}
1
+ {"version":3,"file":"batch.d.ts","sourceRoot":"","sources":["../src/batch.ts"],"names":[],"mappings":"AAyBA,MAAM,MAAM,cAAc,GAAG,CAAC,MAAM,IAAI,CAAC,GAAG;IAAE,QAAQ,EAAE,OAAO,CAAA;CAAE,CAAC;AAwClE,wBAAgB,KAAK,CAAC,EAAE,EAAE,cAAc,QAYvC;AAED,wBAAgB,SAAS,CAAC,EAAE,EAAE,MAAM,IAAI,QAavC;AAED,wBAAgB,oBAAoB,CAAC,MAAM,GAAE,WAAsB,QA4BlE"}
package/dist/batch.js CHANGED
@@ -20,65 +20,86 @@ const INTERACTIVE_EVENTS = [
20
20
  "touchend",
21
21
  "touchcancel",
22
22
  ];
23
+ const asyncQueue = [];
24
+ const syncQueue = [];
23
25
  let inInteractive = 0;
24
- let hasAsyncQueue = false;
25
- let hasSyncQueue = false;
26
- const flushQueue = new Set();
27
- const syncFlushQueue = new Set();
28
- function queueAsync() {
29
- if (hasAsyncQueue) {
26
+ let asyncScheduled = false;
27
+ let inSyncBatch = 0;
28
+ function scheduleAsyncFlush() {
29
+ if (asyncScheduled)
30
30
  return;
31
+ asyncScheduled = true;
32
+ queueMicrotask(flushAsyncQueue);
33
+ }
34
+ function flushAsyncQueue() {
35
+ asyncScheduled = false;
36
+ if (!asyncQueue.length)
37
+ return;
38
+ for (let i = 0; i < asyncQueue.length; i++) {
39
+ const cb = asyncQueue[i];
40
+ asyncQueue[i] = undefined;
41
+ cb();
42
+ cb.__queued = false;
31
43
  }
32
- hasAsyncQueue = true;
33
- queueMicrotask(() => {
34
- hasAsyncQueue = false;
35
- if (!flushQueue.size) {
36
- return;
37
- }
38
- const queued = Array.from(flushQueue);
39
- flushQueue.clear();
40
- queued.forEach((cb) => cb());
41
- });
44
+ asyncQueue.length = 0;
45
+ }
46
+ function flushSyncQueue() {
47
+ if (!syncQueue.length)
48
+ return;
49
+ for (let i = 0; i < syncQueue.length; i++) {
50
+ const cb = syncQueue[i];
51
+ syncQueue[i] = undefined;
52
+ cb();
53
+ cb.__queued = false;
54
+ }
55
+ syncQueue.length = 0;
42
56
  }
43
57
  export function queue(cb) {
44
- if (hasSyncQueue) {
45
- syncFlushQueue.add(cb);
58
+ cb.__queued = true;
59
+ if (inSyncBatch) {
60
+ syncQueue.push(cb);
46
61
  return;
47
62
  }
48
- flushQueue.add(cb);
63
+ asyncQueue.push(cb);
49
64
  if (!inInteractive) {
50
- queueAsync();
65
+ scheduleAsyncFlush();
51
66
  }
52
67
  }
53
68
  export function syncBatch(cb) {
54
- hasSyncQueue = true;
55
- cb();
56
- hasSyncQueue = false;
57
- const queued = Array.from(syncFlushQueue);
58
- syncFlushQueue.clear();
59
- queued.forEach((cb) => cb());
69
+ inSyncBatch++;
70
+ try {
71
+ cb();
72
+ // Only flush on successful completion
73
+ inSyncBatch--;
74
+ if (!inSyncBatch) {
75
+ flushSyncQueue();
76
+ }
77
+ }
78
+ catch (e) {
79
+ inSyncBatch--;
80
+ throw e; // Re-throw without flushing
81
+ }
60
82
  }
61
83
  export function installEventBatching(target = document) {
62
- const captureOptions = { capture: true, passive: true };
84
+ const captureOptions = {
85
+ capture: true,
86
+ passive: true,
87
+ };
63
88
  const bubbleOptions = { passive: true };
64
89
  const onCapture = () => {
65
90
  inInteractive++;
66
- // Backup in case of stop propagation
67
- queueAsync();
91
+ scheduleAsyncFlush(); // backup in case of stopPropagation
68
92
  };
69
93
  const onBubble = () => {
70
- if (--inInteractive === 0 && flushQueue.size) {
71
- const queued = Array.from(flushQueue);
72
- flushQueue.clear();
73
- queued.forEach((cb) => cb());
94
+ if (--inInteractive === 0 && asyncQueue.length) {
95
+ // Flush inline once outermost interactive event finishes
96
+ flushAsyncQueue();
74
97
  }
75
98
  };
76
- // 1) open scope before handlers
77
99
  INTERACTIVE_EVENTS.forEach((type) => {
78
100
  target.addEventListener(type, onCapture, captureOptions);
79
101
  });
80
102
  queueMicrotask(() => {
81
- // 2) close + flush after handlers (bubble on window/document)
82
103
  INTERACTIVE_EVENTS.forEach((type) => {
83
104
  target.addEventListener(type, onBubble, bubbleOptions);
84
105
  });
@@ -1 +1 @@
1
- {"version":3,"file":"component.d.ts","sourceRoot":"","sources":["../src/component.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,EACL,SAAS,EACT,KAAK,EAEN,MAAM,SAAS,CAAC;AAOjB,wBAAgB,mBAAmB,uBAMlC;AAED,wBAAgB,OAAO,CAAC,EAAE,EAAE,MAAM,IAAI,QAMrC;AAED,wBAAgB,SAAS,CAAC,EAAE,EAAE,MAAM,IAAI,QAMvC;AAED,MAAM,MAAM,qBAAqB,CAAC,CAAC,SAAS,KAAK,CAAC,GAAG,CAAC,IAClD,CAAC,MAAM,MAAM,KAAK,CAAC,GACnB,CAAC,CAAC,KAAK,EAAE,CAAC,KAAK,MAAM,KAAK,CAAC,CAAC;AAEhC,cAAM,aAAa,CAAC,CAAC,SAAS,KAAK,CAAC,GAAG,CAAC,CAAE,SAAQ,SAAS,CACzD,CAAC,GAAG;IAAE,WAAW,EAAE,qBAAqB,CAAC,CAAC,CAAC,CAAA;CAAE,CAC9C;IACC,OAAO,CAAC,QAAQ,CAAC,CAAc;IAC/B,OAAO,CAAC,aAAa,CAAC,CAAa;IACnC,OAAO,CAAC,QAAQ,CAEb;IACH,OAAO,CAAC,WAAW,CAAS;IAC5B,OAAO,EAAE,KAAK,CAAC;QAAE,OAAO,EAAE,OAAO,CAAC;QAAC,GAAG,EAAE,MAAM,IAAI,CAAA;KAAE,CAAC,CAAM;IAC3D,QAAQ,gBAAa;IACrB,eAAe;IAUf,QAAQ,EAAE,KAAK,CAAC,MAAM,IAAI,CAAC,CAAM;IACjC,UAAU,EAAE,KAAK,CAAC,MAAM,IAAI,CAAC,CAAM;IACnC,OAAO,CAAC,mBAAmB;IAgC3B,iBAAiB,IAAI,IAAI;IAGzB,oBAAoB,IAAI,IAAI;IAG5B;;OAEG;IACH,mBAAmB,CAAC,SAAS,EAAE,GAAG;IAYlC,yBAAyB,IAAI,IAAI;IACjC,qBAAqB,CAAC,SAAS,EAAE,KAAK,CAAC,GAAG,CAAC,GAAG,OAAO;IAerD,MAAM;CAyCP;AAED,wBAAgB,eAAe,CAAC,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,EAAE,GAAG,CAAC,EAAE,MAAM,SAO9D"}
1
+ {"version":3,"file":"component.d.ts","sourceRoot":"","sources":["../src/component.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,EACL,SAAS,EACT,KAAK,EAEN,MAAM,SAAS,CAAC;AAQjB,wBAAgB,mBAAmB,uBAMlC;AAED,wBAAgB,OAAO,CAAC,EAAE,EAAE,MAAM,IAAI,QAMrC;AAED,wBAAgB,SAAS,CAAC,EAAE,EAAE,MAAM,IAAI,QAMvC;AAED,MAAM,MAAM,qBAAqB,CAAC,CAAC,SAAS,KAAK,CAAC,GAAG,CAAC,IAClD,CAAC,MAAM,MAAM,KAAK,CAAC,GACnB,CAAC,CAAC,KAAK,EAAE,CAAC,KAAK,MAAM,KAAK,CAAC,CAAC;AAEhC,cAAM,aAAa,CAAC,CAAC,SAAS,KAAK,CAAC,GAAG,CAAC,CAAE,SAAQ,SAAS,CACzD,CAAC,GAAG;IAAE,WAAW,EAAE,qBAAqB,CAAC,CAAC,CAAC,CAAA;CAAE,CAC9C;IACC,OAAO,CAAC,QAAQ,CAAC,CAAc;IAC/B,OAAO,CAAC,aAAa,CAAC,CAAa;IACnC,OAAO,CAAC,QAAQ,CAEb;IACH,OAAO,CAAC,WAAW,CAAS;IAC5B,OAAO,EAAE,KAAK,CAAC;QAAE,OAAO,EAAE,OAAO,CAAC;QAAC,GAAG,EAAE,MAAM,IAAI,CAAA;KAAE,CAAC,CAAM;IAC3D,QAAQ,gBAAa;IACrB,eAAe;IAUf,QAAQ,EAAE,KAAK,CAAC,MAAM,IAAI,CAAC,CAAM;IACjC,UAAU,EAAE,KAAK,CAAC,MAAM,IAAI,CAAC,CAAM;IACnC,OAAO,CAAC,mBAAmB;IA4D3B,iBAAiB,IAAI,IAAI;IAGzB,oBAAoB,IAAI,IAAI;IAG5B;;OAEG;IACH,mBAAmB,CAAC,SAAS,EAAE,GAAG;IAYlC,yBAAyB,IAAI,IAAI;IACjC,qBAAqB,CAAC,SAAS,EAAE,KAAK,CAAC,GAAG,CAAC,GAAG,OAAO;IAerD,MAAM;CAyCP;AAED,wBAAgB,eAAe,CAAC,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,EAAE,GAAG,CAAC,EAAE,MAAM,SAO9D"}
package/dist/component.js CHANGED
@@ -43,15 +43,36 @@ class RaskComponent extends Component {
43
43
  createReactiveProps() {
44
44
  const reactiveProps = {};
45
45
  const self = this;
46
+ const signals = new Map();
46
47
  for (const prop in this.props) {
47
- const signal = new Signal();
48
- // @ts-ignore
49
- let reactiveValue = this.props[prop];
48
+ const value = this.props[prop];
49
+ // Skip known non-reactive props
50
+ if (typeof value === "function" ||
51
+ prop === "__component" ||
52
+ prop === "children" ||
53
+ prop === "key" ||
54
+ prop === "ref") {
55
+ reactiveProps[prop] = value;
56
+ continue;
57
+ }
58
+ // Skip objects/arrays - they're already reactive if they're proxies
59
+ // No need to wrap them in additional signals
60
+ if (typeof value === "object" && value !== null) {
61
+ reactiveProps[prop] = value;
62
+ continue;
63
+ }
64
+ // Only create reactive getters for primitives
50
65
  Object.defineProperty(reactiveProps, prop, {
51
66
  get() {
52
67
  if (!self.isRendering) {
53
68
  const observer = getCurrentObserver();
54
69
  if (observer) {
70
+ // Lazy create signal only when accessed in reactive context
71
+ let signal = signals.get(prop);
72
+ if (!signal) {
73
+ signal = new Signal();
74
+ signals.set(prop, signal);
75
+ }
55
76
  observer.subscribeSignal(signal);
56
77
  }
57
78
  }
@@ -59,8 +80,9 @@ class RaskComponent extends Component {
59
80
  return self.props[prop];
60
81
  },
61
82
  set(value) {
62
- if (reactiveValue !== value) {
63
- reactiveValue = value;
83
+ // Only notify if signal was created (i.e., prop was accessed reactively)
84
+ const signal = signals.get(prop);
85
+ if (signal && self.props[prop] !== value) {
64
86
  signal.notify();
65
87
  }
66
88
  },
@@ -1 +1 @@
1
- {"version":3,"file":"createComputed.d.ts","sourceRoot":"","sources":["../src/createComputed.ts"],"names":[],"mappings":"AAGA,wBAAgB,cAAc,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,CAAC,EAChE,QAAQ,EAAE,CAAC,GACV;KACA,CAAC,IAAI,MAAM,CAAC,GAAG,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;CACjC,CAsCA"}
1
+ {"version":3,"file":"createComputed.d.ts","sourceRoot":"","sources":["../src/createComputed.ts"],"names":[],"mappings":"AAGA,wBAAgB,cAAc,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,CAAC,EAChE,QAAQ,EAAE,CAAC,GACV;KACA,CAAC,IAAI,MAAM,CAAC,GAAG,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;CACjC,CA4CA"}
@@ -1,29 +1,36 @@
1
1
  import { getCurrentComponent, onCleanup } from "./component";
2
2
  import { getCurrentObserver, Observer, Signal } from "./observation";
3
3
  export function createComputed(computed) {
4
- const currentComponent = getCurrentComponent();
4
+ let currentComponent;
5
+ try {
6
+ currentComponent = getCurrentComponent();
7
+ }
8
+ catch {
9
+ currentComponent = undefined;
10
+ }
5
11
  const proxy = {};
6
12
  for (const prop in computed) {
7
13
  let isDirty = true;
8
14
  let value;
9
15
  const signal = new Signal();
10
- const observer = new Observer(() => {
16
+ const computedObserver = new Observer(() => {
11
17
  isDirty = true;
12
18
  signal.notify();
13
19
  });
14
20
  if (currentComponent) {
15
- onCleanup(() => observer.dispose());
21
+ onCleanup(() => computedObserver.dispose());
16
22
  }
17
23
  Object.defineProperty(proxy, prop, {
18
24
  get() {
19
- const observer = getCurrentObserver();
20
- if (observer) {
21
- observer.subscribeSignal(signal);
25
+ const currentObserver = getCurrentObserver();
26
+ if (currentObserver) {
27
+ currentObserver.subscribeSignal(signal);
22
28
  }
23
29
  if (isDirty) {
24
- const stopObserving = observer.observe();
30
+ const stopObserving = computedObserver.observe();
25
31
  value = computed[prop]();
26
32
  stopObserving();
33
+ isDirty = false;
27
34
  return value;
28
35
  }
29
36
  return value;
@@ -1 +1 @@
1
- {"version":3,"file":"createEffect.d.ts","sourceRoot":"","sources":["../src/createEffect.ts"],"names":[],"mappings":"AAGA,wBAAgB,YAAY,CAAC,EAAE,EAAE,MAAM,IAAI,QAmB1C"}
1
+ {"version":3,"file":"createEffect.d.ts","sourceRoot":"","sources":["../src/createEffect.ts"],"names":[],"mappings":"AAGA,wBAAgB,YAAY,CAAC,EAAE,EAAE,MAAM,IAAI,QAwB1C"}
@@ -1,7 +1,13 @@
1
1
  import { getCurrentComponent, onCleanup } from "./component";
2
2
  import { Observer } from "./observation";
3
3
  export function createEffect(cb) {
4
- const currentComponent = getCurrentComponent();
4
+ let currentComponent;
5
+ try {
6
+ currentComponent = getCurrentComponent();
7
+ }
8
+ catch {
9
+ currentComponent = undefined;
10
+ }
5
11
  const observer = new Observer(() => {
6
12
  // We trigger effects on micro task as synchronous observer notifications
7
13
  // (Like when components sets props) should not synchronously trigger effects
@@ -23,4 +23,5 @@
23
23
  * @returns A reactive proxy of the state object
24
24
  */
25
25
  export declare function createState<T extends object>(state: T): T;
26
+ export declare const PROXY_MARKER: unique symbol;
26
27
  //# sourceMappingURL=createState.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"createState.d.ts","sourceRoot":"","sources":["../src/createState.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAgB,WAAW,CAAC,CAAC,SAAS,MAAM,EAAE,KAAK,EAAE,CAAC,GAAG,CAAC,CAEzD"}
1
+ {"version":3,"file":"createState.d.ts","sourceRoot":"","sources":["../src/createState.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAgB,WAAW,CAAC,CAAC,SAAS,MAAM,EAAE,KAAK,EAAE,CAAC,GAAG,CAAC,CAEzD;AAGD,eAAO,MAAM,YAAY,eAAoB,CAAC"}
@@ -27,7 +27,7 @@ export function createState(state) {
27
27
  return getProxy(state);
28
28
  }
29
29
  const proxyCache = new WeakMap();
30
- const PROXY_MARKER = Symbol("isProxy");
30
+ export const PROXY_MARKER = Symbol("isProxy");
31
31
  function getProxy(value) {
32
32
  // Check if already a proxy to avoid double-wrapping
33
33
  if (PROXY_MARKER in value) {
@@ -1,17 +1,93 @@
1
- export declare function getCurrentObserver(): Observer;
1
+ /**
2
+ * ---------------------------------------------------------------------------
3
+ * OBSERVER–SIGNAL SYSTEM (OPTIMIZED / LOW MEMORY / ZERO PENDING ARRAYS)
4
+ * ---------------------------------------------------------------------------
5
+ *
6
+ * STRATEGY OVERVIEW
7
+ * -----------------
8
+ * Instead of storing subscriber callbacks and disposer closures,
9
+ * we model the connection between an Observer and a Signal using a
10
+ * lightweight Subscription node.
11
+ *
12
+ * Each Subscription is owned *by both sides*:
13
+ * - Signal keeps a doubly-linked list of all Subscriptions.
14
+ * - Observer keeps a doubly-linked list of all Subscriptions it has made.
15
+ *
16
+ * This gives several important advantages:
17
+ *
18
+ * ✔ NO per-subscription closures (disposers)
19
+ * ✔ NO Set allocations in Signal or Observer
20
+ * ✔ NO Array copies during notify()
21
+ * ✔ Unsubscribing while iterating is safe (linked-list + cached "next")
22
+ * ✔ New subscriptions inside notify() DO NOT fire in the same notify pass
23
+ * (using an epoch barrier)
24
+ * ✔ Observer.clearSignals() is O(n) with real O(1) unlink
25
+ * ✔ Memory overhead is extremely small (one node with 4 pointers)
26
+ *
27
+ * NOTIFY BARRIER
28
+ * --------------
29
+ * A global integer `epoch` increments for each notify().
30
+ * New subscriptions created during notify() store `createdAtEpoch = epoch+1`.
31
+ * During traversal, we only fire subscriptions where:
32
+ *
33
+ * sub.createdAtEpoch <= epoch
34
+ *
35
+ * This guarantees consistency and prevents "late" subscribers from firing too early.
36
+ *
37
+ * ---------------------------------------------------------------------------
38
+ */
39
+ export declare function getCurrentObserver(): Observer | undefined;
40
+ /**
41
+ * A lightweight link connecting ONE observer ↔ ONE signal.
42
+ * Stored in a linked list on both sides.
43
+ */
44
+ declare class Subscription {
45
+ signal: Signal;
46
+ observer: Observer;
47
+ prevInSignal: Subscription | null;
48
+ nextInSignal: Subscription | null;
49
+ prevInObserver: Subscription | null;
50
+ nextInObserver: Subscription | null;
51
+ active: boolean;
52
+ createdAtEpoch: number;
53
+ constructor(signal: Signal, observer: Observer, epoch: number);
54
+ }
55
+ /**
56
+ * SIGNAL — Notifies subscribed observers.
57
+ */
2
58
  export declare class Signal {
3
- private subscribers;
4
- subscribe(cb: () => void): () => void;
59
+ private head;
60
+ private tail;
61
+ private epoch;
62
+ /** INTERNAL: Create a subscription from observer → signal. */
63
+ _subscribe(observer: Observer): Subscription;
64
+ /** INTERNAL: Unlink a subscription from this signal. */
65
+ _unsubscribe(sub: Subscription): void;
66
+ /** Notify all observers.
67
+ * Safe even if observers unsubscribe themselves or subscribe new ones mid-run.
68
+ */
5
69
  notify(): void;
6
70
  }
71
+ /**
72
+ * OBSERVER — Reacts to signal changes.
73
+ * Typically wraps a computation or component.
74
+ */
7
75
  export declare class Observer {
8
76
  isDisposed: boolean;
9
- private signalDisposers;
10
- private clearSignals;
11
- private onNotify;
77
+ private subsHead;
78
+ private subsTail;
79
+ private readonly onNotify;
12
80
  constructor(onNotify: () => void);
81
+ /** Called from Signal.notify() */
82
+ _notify(): void;
83
+ /** Subscribe this observer to a signal */
13
84
  subscribeSignal(signal: Signal): void;
85
+ /** Remove all signal subscriptions (fast + safe) */
86
+ private clearSignals;
87
+ /** Begin dependency collection */
14
88
  observe(): () => void;
89
+ /** Dispose the observer completely */
15
90
  dispose(): void;
16
91
  }
92
+ export {};
17
93
  //# sourceMappingURL=observation.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"observation.d.ts","sourceRoot":"","sources":["../src/observation.ts"],"names":[],"mappings":"AAIA,wBAAgB,kBAAkB,aAEjC;AAED,qBAAa,MAAM;IACjB,OAAO,CAAC,WAAW,CAAyB;IAC5C,SAAS,CAAC,EAAE,EAAE,MAAM,IAAI;IAOxB,MAAM;CAIP;AAED,qBAAa,QAAQ;IACnB,UAAU,UAAS;IACnB,OAAO,CAAC,eAAe,CAAyB;IAChD,OAAO,CAAC,YAAY;IAIpB,OAAO,CAAC,QAAQ,CAAa;gBACjB,QAAQ,EAAE,MAAM,IAAI;IAKhC,eAAe,CAAC,MAAM,EAAE,MAAM;IAG9B,OAAO;IAOP,OAAO;CAIR"}
1
+ {"version":3,"file":"observation.d.ts","sourceRoot":"","sources":["../src/observation.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AASH,wBAAgB,kBAAkB,yBAEjC;AAED;;;GAGG;AACH,cAAM,YAAY;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,QAAQ,CAAC;IAGnB,YAAY,EAAE,YAAY,GAAG,IAAI,CAAQ;IACzC,YAAY,EAAE,YAAY,GAAG,IAAI,CAAQ;IAGzC,cAAc,EAAE,YAAY,GAAG,IAAI,CAAQ;IAC3C,cAAc,EAAE,YAAY,GAAG,IAAI,CAAQ;IAG3C,MAAM,UAAQ;IAGd,cAAc,EAAE,MAAM,CAAC;gBAEX,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM;CAK9D;AAED;;GAEG;AACH,qBAAa,MAAM;IACjB,OAAO,CAAC,IAAI,CAA6B;IACzC,OAAO,CAAC,IAAI,CAA6B;IAGzC,OAAO,CAAC,KAAK,CAAK;IAElB,8DAA8D;IAC9D,UAAU,CAAC,QAAQ,EAAE,QAAQ,GAAG,YAAY;IAe5C,wDAAwD;IACxD,YAAY,CAAC,GAAG,EAAE,YAAY;IAe9B;;OAEG;IACH,MAAM;CAgBP;AAED;;;GAGG;AACH,qBAAa,QAAQ;IACnB,UAAU,UAAS;IAGnB,OAAO,CAAC,QAAQ,CAA6B;IAC7C,OAAO,CAAC,QAAQ,CAA6B;IAG7C,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAa;gBAE1B,QAAQ,EAAE,MAAM,IAAI;IAUhC,kCAAkC;IAClC,OAAO;IAMP,0CAA0C;IAC1C,eAAe,CAAC,MAAM,EAAE,MAAM;IAe9B,oDAAoD;IACpD,OAAO,CAAC,YAAY;IAiBpB,kCAAkC;IAClC,OAAO;IAUP,sCAAsC;IACtC,OAAO;CAKR"}
@@ -1,46 +1,195 @@
1
+ /**
2
+ * ---------------------------------------------------------------------------
3
+ * OBSERVER–SIGNAL SYSTEM (OPTIMIZED / LOW MEMORY / ZERO PENDING ARRAYS)
4
+ * ---------------------------------------------------------------------------
5
+ *
6
+ * STRATEGY OVERVIEW
7
+ * -----------------
8
+ * Instead of storing subscriber callbacks and disposer closures,
9
+ * we model the connection between an Observer and a Signal using a
10
+ * lightweight Subscription node.
11
+ *
12
+ * Each Subscription is owned *by both sides*:
13
+ * - Signal keeps a doubly-linked list of all Subscriptions.
14
+ * - Observer keeps a doubly-linked list of all Subscriptions it has made.
15
+ *
16
+ * This gives several important advantages:
17
+ *
18
+ * ✔ NO per-subscription closures (disposers)
19
+ * ✔ NO Set allocations in Signal or Observer
20
+ * ✔ NO Array copies during notify()
21
+ * ✔ Unsubscribing while iterating is safe (linked-list + cached "next")
22
+ * ✔ New subscriptions inside notify() DO NOT fire in the same notify pass
23
+ * (using an epoch barrier)
24
+ * ✔ Observer.clearSignals() is O(n) with real O(1) unlink
25
+ * ✔ Memory overhead is extremely small (one node with 4 pointers)
26
+ *
27
+ * NOTIFY BARRIER
28
+ * --------------
29
+ * A global integer `epoch` increments for each notify().
30
+ * New subscriptions created during notify() store `createdAtEpoch = epoch+1`.
31
+ * During traversal, we only fire subscriptions where:
32
+ *
33
+ * sub.createdAtEpoch <= epoch
34
+ *
35
+ * This guarantees consistency and prevents "late" subscribers from firing too early.
36
+ *
37
+ * ---------------------------------------------------------------------------
38
+ */
1
39
  import { queue } from "./batch";
40
+ // GLOBAL OBSERVER STACK (for dependency tracking)
2
41
  const observerStack = [];
42
+ let stackTop = -1;
43
+ // Get the active observer during a render/compute
3
44
  export function getCurrentObserver() {
4
- return observerStack[0];
45
+ return stackTop >= 0 ? observerStack[stackTop] : undefined;
5
46
  }
47
+ /**
48
+ * A lightweight link connecting ONE observer ↔ ONE signal.
49
+ * Stored in a linked list on both sides.
50
+ */
51
+ class Subscription {
52
+ signal;
53
+ observer;
54
+ // Linked list pointers within the signal
55
+ prevInSignal = null;
56
+ nextInSignal = null;
57
+ // Linked list pointers within the observer
58
+ prevInObserver = null;
59
+ nextInObserver = null;
60
+ // Whether this subscription is active
61
+ active = true;
62
+ // Used for the notify barrier
63
+ createdAtEpoch;
64
+ constructor(signal, observer, epoch) {
65
+ this.signal = signal;
66
+ this.observer = observer;
67
+ this.createdAtEpoch = epoch;
68
+ }
69
+ }
70
+ /**
71
+ * SIGNAL — Notifies subscribed observers.
72
+ */
6
73
  export class Signal {
7
- subscribers = new Set();
8
- subscribe(cb) {
9
- this.subscribers.add(cb);
10
- return () => {
11
- this.subscribers.delete(cb);
12
- };
74
+ head = null;
75
+ tail = null;
76
+ // Incremented for every notify() call
77
+ epoch = 0;
78
+ /** INTERNAL: Create a subscription from observer → signal. */
79
+ _subscribe(observer) {
80
+ const sub = new Subscription(this, observer, this.epoch + 1);
81
+ // Attach to signal's linked list
82
+ if (this.tail) {
83
+ this.tail.nextInSignal = sub;
84
+ sub.prevInSignal = this.tail;
85
+ this.tail = sub;
86
+ }
87
+ else {
88
+ this.head = this.tail = sub;
89
+ }
90
+ return sub;
91
+ }
92
+ /** INTERNAL: Unlink a subscription from this signal. */
93
+ _unsubscribe(sub) {
94
+ if (!sub.active)
95
+ return;
96
+ sub.active = false;
97
+ const { prevInSignal, nextInSignal } = sub;
98
+ if (prevInSignal)
99
+ prevInSignal.nextInSignal = nextInSignal;
100
+ else
101
+ this.head = nextInSignal;
102
+ if (nextInSignal)
103
+ nextInSignal.prevInSignal = prevInSignal;
104
+ else
105
+ this.tail = prevInSignal;
106
+ sub.prevInSignal = sub.nextInSignal = null;
13
107
  }
108
+ /** Notify all observers.
109
+ * Safe even if observers unsubscribe themselves or subscribe new ones mid-run.
110
+ */
14
111
  notify() {
15
- const currentSubscribers = Array.from(this.subscribers);
16
- currentSubscribers.forEach((cb) => cb());
112
+ if (!this.head)
113
+ return;
114
+ const barrier = ++this.epoch; // new subs won't fire now
115
+ let sub = this.head;
116
+ while (sub) {
117
+ const next = sub.nextInSignal; // cache next → safe if sub unlinks itself
118
+ if (sub.active && sub.createdAtEpoch <= barrier) {
119
+ sub.observer._notify();
120
+ }
121
+ sub = next;
122
+ }
17
123
  }
18
124
  }
125
+ /**
126
+ * OBSERVER — Reacts to signal changes.
127
+ * Typically wraps a computation or component.
128
+ */
19
129
  export class Observer {
20
130
  isDisposed = false;
21
- signalDisposers = new Set();
22
- clearSignals() {
23
- this.signalDisposers.forEach((dispose) => dispose());
24
- this.signalDisposers.clear();
25
- }
131
+ // Doubly-linked list of all subscriptions from this observer
132
+ subsHead = null;
133
+ subsTail = null;
134
+ // Only ONE notify callback closure per observer
26
135
  onNotify;
27
136
  constructor(onNotify) {
137
+ const onNotifyQueued = onNotify;
138
+ onNotifyQueued.__queued = false;
28
139
  this.onNotify = () => {
140
+ if (onNotifyQueued.__queued)
141
+ return;
29
142
  queue(onNotify);
30
143
  };
31
144
  }
145
+ /** Called from Signal.notify() */
146
+ _notify() {
147
+ if (!this.isDisposed) {
148
+ this.onNotify();
149
+ }
150
+ }
151
+ /** Subscribe this observer to a signal */
32
152
  subscribeSignal(signal) {
33
- this.signalDisposers.add(signal.subscribe(this.onNotify));
153
+ if (this.isDisposed)
154
+ return;
155
+ const sub = signal._subscribe(this);
156
+ // Add to observer's linked list
157
+ if (this.subsTail) {
158
+ this.subsTail.nextInObserver = sub;
159
+ sub.prevInObserver = this.subsTail;
160
+ this.subsTail = sub;
161
+ }
162
+ else {
163
+ this.subsHead = this.subsTail = sub;
164
+ }
165
+ }
166
+ /** Remove all signal subscriptions (fast + safe) */
167
+ clearSignals() {
168
+ let sub = this.subsHead;
169
+ this.subsHead = this.subsTail = null;
170
+ while (sub) {
171
+ const next = sub.nextInObserver;
172
+ // Unlink from the signal
173
+ sub.signal._unsubscribe(sub);
174
+ // Clean up observer-side pointers
175
+ sub.prevInObserver = sub.nextInObserver = null;
176
+ sub = next;
177
+ }
34
178
  }
179
+ /** Begin dependency collection */
35
180
  observe() {
36
181
  this.clearSignals();
37
- observerStack.unshift(this);
182
+ observerStack[++stackTop] = this;
183
+ // Return a disposer for this observation frame
38
184
  return () => {
39
- observerStack.shift();
185
+ stackTop--;
40
186
  };
41
187
  }
188
+ /** Dispose the observer completely */
42
189
  dispose() {
43
- this.clearSignals();
190
+ if (this.isDisposed)
191
+ return;
44
192
  this.isDisposed = true;
193
+ this.clearSignals();
45
194
  }
46
195
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rask-ui",
3
- "version": "0.4.0",
3
+ "version": "0.4.1",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",