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 +19 -21
- package/dist/batch.d.ts +4 -1
- package/dist/batch.d.ts.map +1 -1
- package/dist/batch.js +56 -35
- package/dist/component.d.ts.map +1 -1
- package/dist/component.js +27 -5
- package/dist/createComputed.d.ts.map +1 -1
- package/dist/createComputed.js +14 -7
- package/dist/createEffect.d.ts.map +1 -1
- package/dist/createEffect.js +7 -1
- package/dist/createState.d.ts +1 -0
- package/dist/createState.d.ts.map +1 -1
- package/dist/createState.js +1 -1
- package/dist/observation.d.ts +82 -6
- package/dist/observation.d.ts.map +1 -1
- package/dist/observation.js +167 -18
- package/package.json +1 -1
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
|
|
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>
|
|
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
|
|
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
|
package/dist/batch.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"batch.d.ts","sourceRoot":"","sources":["../src/batch.ts"],"names":[],"mappings":"
|
|
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
|
|
25
|
-
let
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
|
|
45
|
-
|
|
58
|
+
cb.__queued = true;
|
|
59
|
+
if (inSyncBatch) {
|
|
60
|
+
syncQueue.push(cb);
|
|
46
61
|
return;
|
|
47
62
|
}
|
|
48
|
-
|
|
63
|
+
asyncQueue.push(cb);
|
|
49
64
|
if (!inInteractive) {
|
|
50
|
-
|
|
65
|
+
scheduleAsyncFlush();
|
|
51
66
|
}
|
|
52
67
|
}
|
|
53
68
|
export function syncBatch(cb) {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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 = {
|
|
84
|
+
const captureOptions = {
|
|
85
|
+
capture: true,
|
|
86
|
+
passive: true,
|
|
87
|
+
};
|
|
63
88
|
const bubbleOptions = { passive: true };
|
|
64
89
|
const onCapture = () => {
|
|
65
90
|
inInteractive++;
|
|
66
|
-
//
|
|
67
|
-
queueAsync();
|
|
91
|
+
scheduleAsyncFlush(); // backup in case of stopPropagation
|
|
68
92
|
};
|
|
69
93
|
const onBubble = () => {
|
|
70
|
-
if (--inInteractive === 0 &&
|
|
71
|
-
|
|
72
|
-
|
|
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
|
});
|
package/dist/component.d.ts.map
CHANGED
|
@@ -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;
|
|
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
|
|
48
|
-
//
|
|
49
|
-
|
|
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 (
|
|
63
|
-
|
|
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,
|
|
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"}
|
package/dist/createComputed.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
16
|
+
const computedObserver = new Observer(() => {
|
|
11
17
|
isDirty = true;
|
|
12
18
|
signal.notify();
|
|
13
19
|
});
|
|
14
20
|
if (currentComponent) {
|
|
15
|
-
onCleanup(() =>
|
|
21
|
+
onCleanup(() => computedObserver.dispose());
|
|
16
22
|
}
|
|
17
23
|
Object.defineProperty(proxy, prop, {
|
|
18
24
|
get() {
|
|
19
|
-
const
|
|
20
|
-
if (
|
|
21
|
-
|
|
25
|
+
const currentObserver = getCurrentObserver();
|
|
26
|
+
if (currentObserver) {
|
|
27
|
+
currentObserver.subscribeSignal(signal);
|
|
22
28
|
}
|
|
23
29
|
if (isDirty) {
|
|
24
|
-
const stopObserving =
|
|
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,
|
|
1
|
+
{"version":3,"file":"createEffect.d.ts","sourceRoot":"","sources":["../src/createEffect.ts"],"names":[],"mappings":"AAGA,wBAAgB,YAAY,CAAC,EAAE,EAAE,MAAM,IAAI,QAwB1C"}
|
package/dist/createEffect.js
CHANGED
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
import { getCurrentComponent, onCleanup } from "./component";
|
|
2
2
|
import { Observer } from "./observation";
|
|
3
3
|
export function createEffect(cb) {
|
|
4
|
-
|
|
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
|
package/dist/createState.d.ts
CHANGED
|
@@ -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"}
|
package/dist/createState.js
CHANGED
|
@@ -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/observation.d.ts
CHANGED
|
@@ -1,17 +1,93 @@
|
|
|
1
|
-
|
|
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
|
|
4
|
-
|
|
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
|
|
10
|
-
private
|
|
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":"
|
|
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"}
|
package/dist/observation.js
CHANGED
|
@@ -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[
|
|
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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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
|
-
|
|
16
|
-
|
|
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
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
|
|
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
|
|
182
|
+
observerStack[++stackTop] = this;
|
|
183
|
+
// Return a disposer for this observation frame
|
|
38
184
|
return () => {
|
|
39
|
-
|
|
185
|
+
stackTop--;
|
|
40
186
|
};
|
|
41
187
|
}
|
|
188
|
+
/** Dispose the observer completely */
|
|
42
189
|
dispose() {
|
|
43
|
-
this.
|
|
190
|
+
if (this.isDisposed)
|
|
191
|
+
return;
|
|
44
192
|
this.isDisposed = true;
|
|
193
|
+
this.clearSignals();
|
|
45
194
|
}
|
|
46
195
|
}
|