rask-ui 0.3.4 → 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 +266 -96
- package/dist/batch.d.ts +7 -0
- package/dist/batch.d.ts.map +1 -0
- package/dist/batch.js +107 -0
- package/dist/component.d.ts +9 -1
- package/dist/component.d.ts.map +1 -1
- package/dist/component.js +42 -12
- package/dist/createComputed.d.ts +4 -0
- package/dist/createComputed.d.ts.map +1 -0
- package/dist/createComputed.js +41 -0
- package/dist/createEffect.d.ts +2 -0
- package/dist/createEffect.d.ts.map +1 -0
- package/dist/createEffect.js +25 -0
- package/dist/createState.d.ts +1 -0
- package/dist/createState.d.ts.map +1 -1
- package/dist/createState.js +1 -1
- package/dist/index.d.ts +4 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +11 -1
- package/dist/observation.d.ts +82 -6
- package/dist/observation.d.ts.map +1 -1
- package/dist/observation.js +171 -21
- package/dist/plugin.d.ts +8 -0
- package/dist/plugin.d.ts.map +1 -0
- package/dist/plugin.js +195 -0
- package/dist/scheduler.d.ts +4 -0
- package/dist/scheduler.d.ts.map +1 -0
- package/dist/scheduler.js +107 -0
- package/package.json +1 -1
- package/dist/createRef.d.ts +0 -5
- package/dist/createRef.d.ts.map +0 -1
- package/dist/createRef.js +0 -7
- package/dist/test-setup.d.ts +0 -16
- package/dist/test-setup.d.ts.map +0 -1
- package/dist/test-setup.js +0 -40
- package/dist/vdom/AbstractVNode.d.ts +0 -44
- package/dist/vdom/AbstractVNode.d.ts.map +0 -1
- package/dist/vdom/AbstractVNode.js +0 -256
- package/dist/vdom/ComponentVNode.d.ts +0 -48
- package/dist/vdom/ComponentVNode.d.ts.map +0 -1
- package/dist/vdom/ComponentVNode.js +0 -221
- package/dist/vdom/ElementVNode.d.ts +0 -27
- package/dist/vdom/ElementVNode.d.ts.map +0 -1
- package/dist/vdom/ElementVNode.js +0 -220
- package/dist/vdom/FragmentVNode.d.ts +0 -13
- package/dist/vdom/FragmentVNode.d.ts.map +0 -1
- package/dist/vdom/FragmentVNode.js +0 -49
- package/dist/vdom/RootVNode.d.ts +0 -25
- package/dist/vdom/RootVNode.d.ts.map +0 -1
- package/dist/vdom/RootVNode.js +0 -79
- package/dist/vdom/TextVNode.d.ts +0 -11
- package/dist/vdom/TextVNode.d.ts.map +0 -1
- package/dist/vdom/TextVNode.js +0 -35
- package/dist/vdom/dom-utils.d.ts +0 -14
- package/dist/vdom/dom-utils.d.ts.map +0 -1
- package/dist/vdom/dom-utils.js +0 -103
- package/dist/vdom/index.d.ts +0 -10
- package/dist/vdom/index.d.ts.map +0 -1
- package/dist/vdom/index.js +0 -26
- package/dist/vdom/types.d.ts +0 -20
- package/dist/vdom/types.d.ts.map +0 -1
- package/dist/vdom/types.js +0 -1
- package/dist/vdom/utils.d.ts +0 -6
- package/dist/vdom/utils.d.ts.map +0 -1
- package/dist/vdom/utils.js +0 -63
package/dist/component.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createComponentVNode, Component, } from "inferno";
|
|
2
2
|
import { getCurrentObserver, Observer, Signal } from "./observation";
|
|
3
|
+
import { syncBatch } from "./batch";
|
|
3
4
|
let currentComponent;
|
|
4
5
|
export function getCurrentComponent() {
|
|
5
6
|
if (!currentComponent) {
|
|
@@ -26,6 +27,7 @@ class RaskComponent extends Component {
|
|
|
26
27
|
this.forceUpdate();
|
|
27
28
|
});
|
|
28
29
|
isRendering = false;
|
|
30
|
+
effects = [];
|
|
29
31
|
contexts = new Map();
|
|
30
32
|
getChildContext() {
|
|
31
33
|
const parentGetContext = this.context.getContext;
|
|
@@ -41,15 +43,36 @@ class RaskComponent extends Component {
|
|
|
41
43
|
createReactiveProps() {
|
|
42
44
|
const reactiveProps = {};
|
|
43
45
|
const self = this;
|
|
46
|
+
const signals = new Map();
|
|
44
47
|
for (const prop in this.props) {
|
|
45
|
-
const
|
|
46
|
-
//
|
|
47
|
-
|
|
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
|
|
48
65
|
Object.defineProperty(reactiveProps, prop, {
|
|
49
66
|
get() {
|
|
50
67
|
if (!self.isRendering) {
|
|
51
68
|
const observer = getCurrentObserver();
|
|
52
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
|
+
}
|
|
53
76
|
observer.subscribeSignal(signal);
|
|
54
77
|
}
|
|
55
78
|
}
|
|
@@ -57,8 +80,9 @@ class RaskComponent extends Component {
|
|
|
57
80
|
return self.props[prop];
|
|
58
81
|
},
|
|
59
82
|
set(value) {
|
|
60
|
-
if (
|
|
61
|
-
|
|
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) {
|
|
62
86
|
signal.notify();
|
|
63
87
|
}
|
|
64
88
|
},
|
|
@@ -72,15 +96,21 @@ class RaskComponent extends Component {
|
|
|
72
96
|
componentWillUnmount() {
|
|
73
97
|
this.onCleanups.forEach((cb) => cb());
|
|
74
98
|
}
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
99
|
+
/**
|
|
100
|
+
*
|
|
101
|
+
*/
|
|
102
|
+
componentWillUpdate(nextProps) {
|
|
103
|
+
syncBatch(() => {
|
|
104
|
+
for (const prop in nextProps) {
|
|
105
|
+
if (prop === "__component" || prop === "children") {
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
// @ts-ignore
|
|
109
|
+
this.reactiveProps[prop] = nextProps[prop];
|
|
79
110
|
}
|
|
80
|
-
|
|
81
|
-
this.reactiveProps[prop] = this.props[prop];
|
|
82
|
-
}
|
|
111
|
+
});
|
|
83
112
|
}
|
|
113
|
+
componentWillReceiveProps() { }
|
|
84
114
|
shouldComponentUpdate(nextProps) {
|
|
85
115
|
// Shallow comparison of props, excluding internal props
|
|
86
116
|
for (const prop in nextProps) {
|
|
@@ -0,0 +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,CA4CA"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { getCurrentComponent, onCleanup } from "./component";
|
|
2
|
+
import { getCurrentObserver, Observer, Signal } from "./observation";
|
|
3
|
+
export function createComputed(computed) {
|
|
4
|
+
let currentComponent;
|
|
5
|
+
try {
|
|
6
|
+
currentComponent = getCurrentComponent();
|
|
7
|
+
}
|
|
8
|
+
catch {
|
|
9
|
+
currentComponent = undefined;
|
|
10
|
+
}
|
|
11
|
+
const proxy = {};
|
|
12
|
+
for (const prop in computed) {
|
|
13
|
+
let isDirty = true;
|
|
14
|
+
let value;
|
|
15
|
+
const signal = new Signal();
|
|
16
|
+
const computedObserver = new Observer(() => {
|
|
17
|
+
isDirty = true;
|
|
18
|
+
signal.notify();
|
|
19
|
+
});
|
|
20
|
+
if (currentComponent) {
|
|
21
|
+
onCleanup(() => computedObserver.dispose());
|
|
22
|
+
}
|
|
23
|
+
Object.defineProperty(proxy, prop, {
|
|
24
|
+
get() {
|
|
25
|
+
const currentObserver = getCurrentObserver();
|
|
26
|
+
if (currentObserver) {
|
|
27
|
+
currentObserver.subscribeSignal(signal);
|
|
28
|
+
}
|
|
29
|
+
if (isDirty) {
|
|
30
|
+
const stopObserving = computedObserver.observe();
|
|
31
|
+
value = computed[prop]();
|
|
32
|
+
stopObserving();
|
|
33
|
+
isDirty = false;
|
|
34
|
+
return value;
|
|
35
|
+
}
|
|
36
|
+
return value;
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
return proxy;
|
|
41
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"createEffect.d.ts","sourceRoot":"","sources":["../src/createEffect.ts"],"names":[],"mappings":"AAGA,wBAAgB,YAAY,CAAC,EAAE,EAAE,MAAM,IAAI,QAwB1C"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { getCurrentComponent, onCleanup } from "./component";
|
|
2
|
+
import { Observer } from "./observation";
|
|
3
|
+
export function createEffect(cb) {
|
|
4
|
+
let currentComponent;
|
|
5
|
+
try {
|
|
6
|
+
currentComponent = getCurrentComponent();
|
|
7
|
+
}
|
|
8
|
+
catch {
|
|
9
|
+
currentComponent = undefined;
|
|
10
|
+
}
|
|
11
|
+
const observer = new Observer(() => {
|
|
12
|
+
// We trigger effects on micro task as synchronous observer notifications
|
|
13
|
+
// (Like when components sets props) should not synchronously trigger effects
|
|
14
|
+
queueMicrotask(runEffect);
|
|
15
|
+
});
|
|
16
|
+
const runEffect = () => {
|
|
17
|
+
const stopObserving = observer.observe();
|
|
18
|
+
cb();
|
|
19
|
+
stopObserving();
|
|
20
|
+
};
|
|
21
|
+
if (currentComponent) {
|
|
22
|
+
onCleanup(() => observer.dispose());
|
|
23
|
+
}
|
|
24
|
+
runEffect();
|
|
25
|
+
}
|
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/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
import { render as infernoRender } from "inferno";
|
|
2
2
|
export { onCleanup, onMount } from "./component";
|
|
3
3
|
export { createContext } from "./createContext";
|
|
4
4
|
export { createState } from "./createState";
|
|
@@ -8,4 +8,7 @@ export { createQuery } from "./createQuery";
|
|
|
8
8
|
export { createMutation } from "./createMutation";
|
|
9
9
|
export { createRef } from "inferno";
|
|
10
10
|
export { createView } from "./createView";
|
|
11
|
+
export { createEffect } from "./createEffect";
|
|
12
|
+
export { createComputed } from "./createComputed";
|
|
13
|
+
export declare function render(...params: Parameters<typeof infernoRender>): void;
|
|
11
14
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"
|
|
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"}
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
|
|
1
|
+
import { render as infernoRender } from "inferno";
|
|
2
|
+
import { installEventBatching } from "./batch";
|
|
2
3
|
export { onCleanup, onMount } from "./component";
|
|
3
4
|
export { createContext } from "./createContext";
|
|
4
5
|
export { createState } from "./createState";
|
|
@@ -8,3 +9,12 @@ export { createQuery } from "./createQuery";
|
|
|
8
9
|
export { createMutation } from "./createMutation";
|
|
9
10
|
export { createRef } from "inferno";
|
|
10
11
|
export { createView } from "./createView";
|
|
12
|
+
export { createEffect } from "./createEffect";
|
|
13
|
+
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
|
+
}
|
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,45 +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
|
+
*/
|
|
39
|
+
import { queue } from "./batch";
|
|
40
|
+
// GLOBAL OBSERVER STACK (for dependency tracking)
|
|
1
41
|
const observerStack = [];
|
|
42
|
+
let stackTop = -1;
|
|
43
|
+
// Get the active observer during a render/compute
|
|
2
44
|
export function getCurrentObserver() {
|
|
3
|
-
return observerStack[
|
|
45
|
+
return stackTop >= 0 ? observerStack[stackTop] : undefined;
|
|
4
46
|
}
|
|
5
|
-
|
|
6
|
-
|
|
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
|
+
*/
|
|
7
73
|
export class Signal {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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;
|
|
14
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;
|
|
107
|
+
}
|
|
108
|
+
/** Notify all observers.
|
|
109
|
+
* Safe even if observers unsubscribe themselves or subscribe new ones mid-run.
|
|
110
|
+
*/
|
|
15
111
|
notify() {
|
|
16
|
-
|
|
17
|
-
|
|
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
|
+
}
|
|
18
123
|
}
|
|
19
124
|
}
|
|
125
|
+
/**
|
|
126
|
+
* OBSERVER — Reacts to signal changes.
|
|
127
|
+
* Typically wraps a computation or component.
|
|
128
|
+
*/
|
|
20
129
|
export class Observer {
|
|
21
130
|
isDisposed = false;
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
}
|
|
131
|
+
// Doubly-linked list of all subscriptions from this observer
|
|
132
|
+
subsHead = null;
|
|
133
|
+
subsTail = null;
|
|
134
|
+
// Only ONE notify callback closure per observer
|
|
27
135
|
onNotify;
|
|
28
136
|
constructor(onNotify) {
|
|
29
|
-
|
|
137
|
+
const onNotifyQueued = onNotify;
|
|
138
|
+
onNotifyQueued.__queued = false;
|
|
139
|
+
this.onNotify = () => {
|
|
140
|
+
if (onNotifyQueued.__queued)
|
|
141
|
+
return;
|
|
142
|
+
queue(onNotify);
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
/** Called from Signal.notify() */
|
|
146
|
+
_notify() {
|
|
147
|
+
if (!this.isDisposed) {
|
|
148
|
+
this.onNotify();
|
|
149
|
+
}
|
|
30
150
|
}
|
|
151
|
+
/** Subscribe this observer to a signal */
|
|
31
152
|
subscribeSignal(signal) {
|
|
32
|
-
|
|
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
|
+
}
|
|
33
178
|
}
|
|
179
|
+
/** Begin dependency collection */
|
|
34
180
|
observe() {
|
|
35
181
|
this.clearSignals();
|
|
36
|
-
observerStack
|
|
182
|
+
observerStack[++stackTop] = this;
|
|
183
|
+
// Return a disposer for this observation frame
|
|
37
184
|
return () => {
|
|
38
|
-
|
|
185
|
+
stackTop--;
|
|
39
186
|
};
|
|
40
187
|
}
|
|
188
|
+
/** Dispose the observer completely */
|
|
41
189
|
dispose() {
|
|
42
|
-
this.
|
|
190
|
+
if (this.isDisposed)
|
|
191
|
+
return;
|
|
43
192
|
this.isDisposed = true;
|
|
193
|
+
this.clearSignals();
|
|
44
194
|
}
|
|
45
195
|
}
|
package/dist/plugin.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,MAAM,CAAC;AAMnC,UAAU,iBAAiB;IACzB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAyDD,wBAAgB,UAAU,CAAC,OAAO,GAAE,iBAAsB,GAAG,MAAM,CAsPlE"}
|