rask-ui 0.4.0 → 0.4.2

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,6 @@
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
- export declare function installEventBatching(target?: EventTarget): void;
4
6
  //# 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":"AAAA,MAAM,MAAM,cAAc,GAAG,CAAC,MAAM,IAAI,CAAC,GAAG;IAAE,QAAQ,EAAE,OAAO,CAAA;CAAE,CAAC;AAgElE,wBAAgB,KAAK,CAAC,EAAE,EAAE,cAAc,QAevC;AAED,wBAAgB,SAAS,CAAC,EAAE,EAAE,MAAM,IAAI,QAgBvC"}
package/dist/batch.js CHANGED
@@ -1,86 +1,89 @@
1
- const INTERACTIVE_EVENTS = [
2
- // DISCRETE
3
- "beforeinput",
4
- "input",
5
- "change",
6
- "compositionend",
7
- "keydown",
8
- "keyup",
9
- "click",
10
- "contextmenu",
11
- "submit",
12
- "reset",
13
- // GESTURE START
14
- "pointerdown",
15
- "mousedown",
16
- "touchstart",
17
- // GESTURE END
18
- "pointerup",
19
- "mouseup",
20
- "touchend",
21
- "touchcancel",
22
- ];
1
+ const asyncQueue = [];
2
+ const syncQueue = [];
23
3
  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) {
4
+ let asyncScheduled = false;
5
+ let inSyncBatch = 0;
6
+ // New: guards against re-entrant flushing
7
+ let inAsyncFlush = false;
8
+ let inSyncFlush = false;
9
+ function scheduleAsyncFlush() {
10
+ if (asyncScheduled)
30
11
  return;
12
+ asyncScheduled = true;
13
+ queueMicrotask(flushAsyncQueue);
14
+ }
15
+ function flushAsyncQueue() {
16
+ if (inAsyncFlush)
17
+ return;
18
+ inAsyncFlush = true;
19
+ asyncScheduled = false;
20
+ try {
21
+ if (!asyncQueue.length)
22
+ return;
23
+ // Note: we intentionally DO NOT snapshot.
24
+ // If callbacks queue more async work, it gets picked up
25
+ // in this same loop because length grows.
26
+ for (let i = 0; i < asyncQueue.length; i++) {
27
+ const cb = asyncQueue[i];
28
+ asyncQueue[i] = undefined;
29
+ cb();
30
+ cb.__queued = false;
31
+ }
32
+ asyncQueue.length = 0;
31
33
  }
32
- hasAsyncQueue = true;
33
- queueMicrotask(() => {
34
- hasAsyncQueue = false;
35
- if (!flushQueue.size) {
34
+ finally {
35
+ inAsyncFlush = false;
36
+ }
37
+ }
38
+ function flushSyncQueue() {
39
+ if (inSyncFlush)
40
+ return;
41
+ inSyncFlush = true;
42
+ try {
43
+ if (!syncQueue.length)
36
44
  return;
45
+ // Same pattern as async: no snapshot, just iterate.
46
+ // New callbacks queued via syncBatch inside this flush
47
+ // will be pushed to syncQueue and picked up by this loop.
48
+ for (let i = 0; i < syncQueue.length; i++) {
49
+ const cb = syncQueue[i];
50
+ syncQueue[i] = undefined;
51
+ cb();
52
+ cb.__queued = false;
37
53
  }
38
- const queued = Array.from(flushQueue);
39
- flushQueue.clear();
40
- queued.forEach((cb) => cb());
41
- });
54
+ syncQueue.length = 0;
55
+ }
56
+ finally {
57
+ inSyncFlush = false;
58
+ }
42
59
  }
43
60
  export function queue(cb) {
44
- if (hasSyncQueue) {
45
- syncFlushQueue.add(cb);
61
+ // Optional: uncomment this if you want deduping:
62
+ // if (cb.__queued) return;
63
+ cb.__queued = true;
64
+ if (inSyncBatch) {
65
+ syncQueue.push(cb);
46
66
  return;
47
67
  }
48
- flushQueue.add(cb);
68
+ asyncQueue.push(cb);
49
69
  if (!inInteractive) {
50
- queueAsync();
70
+ scheduleAsyncFlush();
51
71
  }
52
72
  }
53
73
  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());
60
- }
61
- export function installEventBatching(target = document) {
62
- const captureOptions = { capture: true, passive: true };
63
- const bubbleOptions = { passive: true };
64
- const onCapture = () => {
65
- inInteractive++;
66
- // Backup in case of stop propagation
67
- queueAsync();
68
- };
69
- const onBubble = () => {
70
- if (--inInteractive === 0 && flushQueue.size) {
71
- const queued = Array.from(flushQueue);
72
- flushQueue.clear();
73
- queued.forEach((cb) => cb());
74
- }
75
- };
76
- // 1) open scope before handlers
77
- INTERACTIVE_EVENTS.forEach((type) => {
78
- target.addEventListener(type, onCapture, captureOptions);
79
- });
80
- queueMicrotask(() => {
81
- // 2) close + flush after handlers (bubble on window/document)
82
- INTERACTIVE_EVENTS.forEach((type) => {
83
- target.addEventListener(type, onBubble, bubbleOptions);
84
- });
85
- });
74
+ inSyncBatch++;
75
+ try {
76
+ cb();
77
+ }
78
+ catch (e) {
79
+ inSyncBatch--;
80
+ throw e; // no flush on error
81
+ }
82
+ inSyncBatch--;
83
+ if (!inSyncBatch) {
84
+ // Only the outermost syncBatch triggers a flush.
85
+ // If this happens *inside* an ongoing flushSyncQueue,
86
+ // inSyncFlush will be true and flushSyncQueue will no-op.
87
+ flushSyncQueue();
88
+ }
86
89
  }
@@ -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) {
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { render as infernoRender } from "inferno";
1
+ export { render } from "./render";
2
2
  export { onCleanup, onMount } from "./component";
3
3
  export { createContext } from "./createContext";
4
4
  export { createState } from "./createState";
@@ -10,5 +10,5 @@ export { createRef } from "inferno";
10
10
  export { createView } from "./createView";
11
11
  export { createEffect } from "./createEffect";
12
12
  export { createComputed } from "./createComputed";
13
- export declare function render(...params: Parameters<typeof infernoRender>): void;
13
+ export { syncBatch } from "./batch";
14
14
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,IAAI,aAAa,EAAE,MAAM,SAAS,CAAC;AAElD,OAAO,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,aAAa,CAAC;AACjD,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAChD,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAC5C,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAC5C,OAAO,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AACxC,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAC5C,OAAO,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAClD,OAAO,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AACpC,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC1C,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAC9C,OAAO,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAClD,wBAAgB,MAAM,CAAC,GAAG,MAAM,EAAE,UAAU,CAAC,OAAO,aAAa,CAAC,QAMjE"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,UAAU,CAAC;AAClC,OAAO,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,aAAa,CAAC;AACjD,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAChD,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAC5C,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAC5C,OAAO,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AACxC,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAC5C,OAAO,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAClD,OAAO,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AACpC,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC1C,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAC9C,OAAO,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAClD,OAAO,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC"}
package/dist/index.js CHANGED
@@ -1,5 +1,4 @@
1
- import { render as infernoRender } from "inferno";
2
- import { installEventBatching } from "./batch";
1
+ export { render } from "./render";
3
2
  export { onCleanup, onMount } from "./component";
4
3
  export { createContext } from "./createContext";
5
4
  export { createState } from "./createState";
@@ -11,10 +10,4 @@ export { createRef } from "inferno";
11
10
  export { createView } from "./createView";
12
11
  export { createEffect } from "./createEffect";
13
12
  export { createComputed } from "./createComputed";
14
- export function render(...params) {
15
- if (!params[1]) {
16
- throw new Error("You need a target container");
17
- }
18
- installEventBatching(params[1]);
19
- return infernoRender(...params);
20
- }
13
+ export { syncBatch } from "./batch";
@@ -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
  }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Temporarily patches document.addEventListener during render to capture
3
+ * and wrap Inferno's delegated event listeners with syncBatch
4
+ */
5
+ export declare function patchInfernoEventHandling(renderFn: () => void): void;
6
+ //# sourceMappingURL=patchInferno.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"patchInferno.d.ts","sourceRoot":"","sources":["../src/patchInferno.ts"],"names":[],"mappings":"AAEA;;;GAGG;AACH,wBAAgB,yBAAyB,CAAC,QAAQ,EAAE,MAAM,IAAI,QA0D7D"}
@@ -0,0 +1,53 @@
1
+ import { syncBatch } from "./batch";
2
+ /**
3
+ * Temporarily patches document.addEventListener during render to capture
4
+ * and wrap Inferno's delegated event listeners with syncBatch
5
+ */
6
+ export function patchInfernoEventHandling(renderFn) {
7
+ const originalAddEventListener = document.addEventListener.bind(document);
8
+ const patchedEvents = new Set();
9
+ // Inferno's delegated events
10
+ const INFERNO_EVENTS = [
11
+ "click",
12
+ "dblclick",
13
+ "focusin",
14
+ "focusout",
15
+ "keydown",
16
+ "keypress",
17
+ "keyup",
18
+ "mousedown",
19
+ "mousemove",
20
+ "mouseup",
21
+ "touchend",
22
+ "touchmove",
23
+ "touchstart",
24
+ "change",
25
+ "input",
26
+ "submit",
27
+ ];
28
+ // Temporarily replace addEventListener
29
+ document.addEventListener = function (type, listener, options) {
30
+ // Only wrap Inferno's delegated event listeners
31
+ if (INFERNO_EVENTS.includes(type) &&
32
+ typeof listener === "function" &&
33
+ !patchedEvents.has(type)) {
34
+ patchedEvents.add(type);
35
+ const wrappedListener = function (event) {
36
+ syncBatch(() => {
37
+ listener.call(this, event);
38
+ });
39
+ };
40
+ return originalAddEventListener(type, wrappedListener, options);
41
+ }
42
+ // @ts-ignore
43
+ return originalAddEventListener(type, listener, options);
44
+ };
45
+ try {
46
+ // Call render - Inferno will synchronously attach its listeners
47
+ renderFn();
48
+ }
49
+ finally {
50
+ // Restore original addEventListener
51
+ document.addEventListener = originalAddEventListener;
52
+ }
53
+ }
@@ -0,0 +1,8 @@
1
+ import { render as infernoRender } from "inferno";
2
+ /**
3
+ * Renders a component with automatic event batching.
4
+ * Temporarily patches document.addEventListener to wrap
5
+ * Inferno's delegated event listeners with syncBatch.
6
+ */
7
+ export declare function render(...params: Parameters<typeof infernoRender>): void;
8
+ //# sourceMappingURL=render.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"render.d.ts","sourceRoot":"","sources":["../src/render.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,IAAI,aAAa,EAAE,MAAM,SAAS,CAAC;AAGlD;;;;GAIG;AACH,wBAAgB,MAAM,CAAC,GAAG,MAAM,EAAE,UAAU,CAAC,OAAO,aAAa,CAAC,QAkEjE"}
package/dist/render.js ADDED
@@ -0,0 +1,62 @@
1
+ import { render as infernoRender } from "inferno";
2
+ import { syncBatch } from "./batch";
3
+ /**
4
+ * Renders a component with automatic event batching.
5
+ * Temporarily patches document.addEventListener to wrap
6
+ * Inferno's delegated event listeners with syncBatch.
7
+ */
8
+ export function render(...params) {
9
+ if (!params[1]) {
10
+ throw new Error("You need a target container");
11
+ }
12
+ /**
13
+ * Temporarily patches document.addEventListener during render to capture
14
+ * and wrap Inferno's delegated event listeners with syncBatch
15
+ */
16
+ const originalAddEventListener = document.addEventListener.bind(document);
17
+ const patchedEvents = new Set();
18
+ // Inferno's delegated events
19
+ const INFERNO_EVENTS = [
20
+ "click",
21
+ "dblclick",
22
+ "focusin",
23
+ "focusout",
24
+ "keydown",
25
+ "keypress",
26
+ "keyup",
27
+ "mousedown",
28
+ "mousemove",
29
+ "mouseup",
30
+ "touchend",
31
+ "touchmove",
32
+ "touchstart",
33
+ "change",
34
+ "input",
35
+ "submit",
36
+ ];
37
+ // Temporarily replace addEventListener
38
+ document.addEventListener = function (type, listener, options) {
39
+ // Only wrap Inferno's delegated event listeners
40
+ if (INFERNO_EVENTS.includes(type) &&
41
+ typeof listener === "function" &&
42
+ !patchedEvents.has(type)) {
43
+ patchedEvents.add(type);
44
+ const wrappedListener = function (event) {
45
+ syncBatch(() => {
46
+ listener.call(this, event);
47
+ });
48
+ };
49
+ return originalAddEventListener(type, wrappedListener, options);
50
+ }
51
+ // @ts-ignore
52
+ return originalAddEventListener(type, listener, options);
53
+ };
54
+ try {
55
+ // Call render - Inferno will synchronously attach its listeners
56
+ return infernoRender(...params);
57
+ }
58
+ finally {
59
+ // Restore original addEventListener
60
+ document.addEventListener = originalAddEventListener;
61
+ }
62
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rask-ui",
3
- "version": "0.4.0",
3
+ "version": "0.4.2",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",