@esportsplus/reactivity 0.4.7 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/build/signal.js CHANGED
@@ -1,280 +1,318 @@
1
- import { isArray } from '@esportsplus/utilities';
2
- import { CHECK, CLEAN, COMPUTED, DIRTY, DISPOSED, EFFECT, ROOT, SIGNAL } from './constants.js';
3
- let index = 0, observer = null, observers = null, scope = null;
4
- class Reactive {
5
- changed = null;
6
- fn = null;
7
- listeners = null;
8
- observers = null;
9
- root;
10
- scheduler = null;
11
- sources = null;
12
- state;
13
- task = null;
14
- tracking = null;
15
- type;
16
- value;
17
- constructor(state, type, value) {
18
- let root = null;
19
- if (type !== ROOT) {
20
- if (scope !== null) {
21
- root = scope;
22
- }
23
- else if (observer !== null) {
24
- root = observer.root;
25
- }
26
- if (root == null) {
27
- if (type === EFFECT) {
28
- throw new Error(`@esportsplus/reactivity: 'effect' cannot be created without a reactive root`);
29
- }
30
- }
31
- else if (root.tracking) {
32
- root.on('dispose', () => this.dispose());
33
- }
34
- }
35
- this.root = root;
36
- this.state = state;
37
- this.type = type;
38
- this.value = value;
1
+ import { isArray, isObject } from '@esportsplus/utilities';
2
+ import { REACTIVE, STATE_CHECK, STATE_DIRTY, STATE_IN_HEAP, STATE_NONE, STATE_RECOMPUTING } from './constants.js';
3
+ let context = null, dirtyHeap = new Array(2000), maxDirty = 0, markedHeap = false, minDirty = 0;
4
+ function cleanup(node) {
5
+ if (!node.cleanup) {
6
+ return;
39
7
  }
40
- dispatch(event, data) {
41
- if (this.listeners === null || this.listeners[event] === undefined) {
42
- return;
43
- }
44
- let listeners = this.listeners[event];
45
- for (let i = 0, n = listeners.length; i < n; i++) {
46
- let listener = listeners[i];
47
- if (listener === null) {
48
- continue;
49
- }
50
- try {
51
- listener(data, this.value);
52
- if (listener.once !== undefined) {
53
- listeners[i] = null;
54
- }
55
- }
56
- catch {
57
- listeners[i] = null;
58
- }
8
+ if (isArray(node.cleanup)) {
9
+ for (let i = 0; i < node.cleanup.length; i++) {
10
+ node.cleanup[i]();
59
11
  }
60
12
  }
61
- dispose() {
62
- if (this.state === DISPOSED) {
63
- return;
64
- }
65
- this.dispatch('cleanup', this);
66
- this.dispatch('dispose', this);
67
- removeSourceObservers(this, 0);
68
- this.listeners = null;
69
- this.observers = null;
70
- this.sources = null;
71
- this.state = DISPOSED;
72
- }
73
- get() {
74
- if (this.state === DISPOSED) {
75
- return this.value;
76
- }
77
- if (observer !== null) {
78
- if (observers === null) {
79
- if (observer.sources !== null && observer.sources[index] == this) {
80
- index++;
81
- }
82
- else {
83
- observers = [this];
84
- }
85
- }
86
- else {
87
- observers.push(this);
88
- }
89
- }
90
- if (this.type === COMPUTED || this.type === EFFECT) {
91
- sync(this);
92
- }
93
- return this.value;
13
+ else {
14
+ node.cleanup();
94
15
  }
95
- on(event, listener) {
96
- if (this.state === DIRTY) {
97
- if (event !== 'cleanup') {
98
- throw new Error(`@esportsplus/reactivity: events set within computed or effects must use the 'cleanup' event name`);
99
- }
100
- listener.once = true;
101
- }
102
- if (this.listeners === null) {
103
- this.listeners = { [event]: [listener] };
16
+ node.cleanup = null;
17
+ }
18
+ function deleteFromHeap(n) {
19
+ let state = n.state;
20
+ if (!(state & STATE_IN_HEAP)) {
21
+ return;
22
+ }
23
+ n.state = state & ~STATE_IN_HEAP;
24
+ let height = n.height;
25
+ if (n.prevHeap === n) {
26
+ dirtyHeap[height] = undefined;
27
+ }
28
+ else {
29
+ let next = n.nextHeap, dhh = dirtyHeap[height], end = next ?? dhh;
30
+ if (n === dhh) {
31
+ dirtyHeap[height] = next;
104
32
  }
105
33
  else {
106
- let listeners = this.listeners[event];
107
- if (listeners === undefined) {
108
- this.listeners[event] = [listener];
109
- }
110
- else if (listeners.indexOf(listener) === -1) {
111
- let i = listeners.indexOf(null);
112
- if (i === -1) {
113
- listeners.push(listener);
114
- }
115
- else {
116
- listeners[i] = listener;
117
- }
118
- }
34
+ n.prevHeap.nextHeap = next;
119
35
  }
36
+ end.prevHeap = n.prevHeap;
37
+ }
38
+ n.nextHeap = undefined;
39
+ n.prevHeap = n;
40
+ }
41
+ function insertIntoHeap(n) {
42
+ let state = n.state;
43
+ if (state & STATE_IN_HEAP) {
44
+ return;
120
45
  }
121
- once(event, listener) {
122
- listener.once = true;
123
- this.on(event, listener);
46
+ n.state = state | STATE_IN_HEAP;
47
+ let height = n.height, heapAtHeight = dirtyHeap[height];
48
+ if (heapAtHeight === undefined) {
49
+ dirtyHeap[height] = n;
124
50
  }
125
- set(value) {
126
- if (this.type !== SIGNAL && observer !== this) {
127
- throw new Error(`@esportsplus/reactivity: 'set' method is only available on signals`);
128
- }
129
- if (this.changed(this.value, value)) {
130
- this.value = value;
131
- notify(this.observers, DIRTY);
51
+ else {
52
+ let tail = heapAtHeight.prevHeap;
53
+ tail.nextHeap = n;
54
+ n.prevHeap = tail;
55
+ heapAtHeight.prevHeap = n;
56
+ }
57
+ if (height > maxDirty) {
58
+ maxDirty = height;
59
+ if (height >= dirtyHeap.length) {
60
+ dirtyHeap.length += 250;
132
61
  }
133
- return this.value;
134
62
  }
135
63
  }
136
- function changed(a, b) {
137
- return a !== b;
138
- }
139
- function notify(nodes, state) {
140
- if (nodes === null) {
64
+ function link(dep, sub) {
65
+ let prevDep = sub.depsTail;
66
+ if (prevDep !== null && prevDep.dep === dep) {
141
67
  return;
142
68
  }
143
- for (let i = 0, n = nodes.length; i < n; i++) {
144
- let node = nodes[i];
145
- if (node.state < state) {
146
- if (node.type === EFFECT && node.state === CLEAN) {
147
- node.root.scheduler(node.task);
148
- }
149
- node.state = state;
150
- notify(node.observers, CHECK);
69
+ let nextDep = null;
70
+ if (sub.state & STATE_RECOMPUTING) {
71
+ nextDep = prevDep !== null ? prevDep.nextDep : sub.deps;
72
+ if (nextDep !== null && nextDep.dep === dep) {
73
+ sub.depsTail = nextDep;
74
+ return;
151
75
  }
152
76
  }
77
+ let prevSub = dep.subsTail, newLink = sub.depsTail =
78
+ dep.subsTail = {
79
+ dep,
80
+ sub,
81
+ nextDep,
82
+ prevSub,
83
+ nextSub: null,
84
+ };
85
+ if (prevDep !== null) {
86
+ prevDep.nextDep = newLink;
87
+ }
88
+ else {
89
+ sub.deps = newLink;
90
+ }
91
+ if (prevSub !== null) {
92
+ prevSub.nextSub = newLink;
93
+ }
94
+ else {
95
+ dep.subs = newLink;
96
+ }
153
97
  }
154
- function removeSourceObservers(node, start) {
155
- if (node.sources === null) {
98
+ function markHeap() {
99
+ if (markedHeap) {
156
100
  return;
157
101
  }
158
- for (let i = start, n = node.sources.length; i < n; i++) {
159
- let observers = node.sources[i].observers;
160
- if (observers === null) {
161
- continue;
102
+ markedHeap = true;
103
+ for (let i = 0; i <= maxDirty; i++) {
104
+ for (let el = dirtyHeap[i]; el !== undefined; el = el.nextHeap) {
105
+ markNode(el);
162
106
  }
163
- observers[observers.indexOf(node)] = observers[observers.length - 1];
164
- observers.pop();
165
107
  }
166
108
  }
167
- function sync(node) {
168
- if (node.state === CHECK && node.sources !== null) {
169
- for (let i = 0, n = node.sources.length; i < n; i++) {
170
- sync(node.sources[i]);
171
- if (node.state === DIRTY) {
172
- break;
109
+ function markNode(el, newState = STATE_DIRTY) {
110
+ let state = el.state;
111
+ if ((state & (STATE_CHECK | STATE_DIRTY)) >= newState) {
112
+ return;
113
+ }
114
+ el.state = state | newState;
115
+ for (let link = el.subs; link !== null; link = link.nextSub) {
116
+ markNode(link.sub, STATE_CHECK);
117
+ }
118
+ }
119
+ function recompute(el, del) {
120
+ if (del) {
121
+ deleteFromHeap(el);
122
+ }
123
+ else {
124
+ el.nextHeap = undefined;
125
+ el.prevHeap = el;
126
+ }
127
+ cleanup(el);
128
+ let oldcontext = context, ok = true, value;
129
+ context = el;
130
+ el.depsTail = null;
131
+ el.state = STATE_RECOMPUTING;
132
+ try {
133
+ value = el.fn(oncleanup);
134
+ }
135
+ catch (e) {
136
+ ok = false;
137
+ }
138
+ context = oldcontext;
139
+ el.state = STATE_NONE;
140
+ let depsTail = el.depsTail, toRemove = depsTail !== null ? depsTail.nextDep : el.deps;
141
+ if (toRemove !== null) {
142
+ do {
143
+ toRemove = unlink(toRemove);
144
+ } while (toRemove !== null);
145
+ if (depsTail !== null) {
146
+ depsTail.nextDep = null;
147
+ }
148
+ else {
149
+ el.deps = null;
150
+ }
151
+ }
152
+ if (ok && value !== el.value) {
153
+ el.value = value;
154
+ for (let s = el.subs; s !== null; s = s.nextSub) {
155
+ let o = s.sub, state = o.state;
156
+ if (state & STATE_CHECK) {
157
+ o.state = state | STATE_DIRTY;
173
158
  }
159
+ insertIntoHeap(o);
174
160
  }
175
161
  }
176
- if (node.state === DIRTY) {
177
- update(node);
162
+ }
163
+ function unlink(link) {
164
+ let dep = link.dep, nextDep = link.nextDep, nextSub = link.nextSub, prevSub = link.prevSub;
165
+ if (nextSub !== null) {
166
+ nextSub.prevSub = prevSub;
167
+ }
168
+ else {
169
+ dep.subsTail = prevSub;
170
+ }
171
+ if (prevSub !== null) {
172
+ prevSub.nextSub = nextSub;
178
173
  }
179
174
  else {
180
- node.state = CLEAN;
175
+ dep.subs = nextSub;
176
+ if (nextSub === null && 'fn' in dep) {
177
+ dispose(dep);
178
+ }
181
179
  }
180
+ return nextDep;
182
181
  }
183
- function update(node) {
184
- let i = index, o = observer, os = observers;
185
- index = 0;
186
- observer = node;
187
- observers = null;
188
- try {
189
- node.dispatch('cleanup');
190
- node.dispatch('update');
191
- let value = node.fn.call(null, node);
192
- if (observers) {
193
- removeSourceObservers(node, index);
194
- if (node.sources !== null && index > 0) {
195
- node.sources.length = index + observers.length;
196
- for (let i = 0, n = observers.length; i < n; i++) {
197
- node.sources[index + i] = observers[i];
198
- }
182
+ function update(el) {
183
+ if (el.state & STATE_CHECK) {
184
+ for (let d = el.deps; d; d = d.nextDep) {
185
+ let dep = d.dep;
186
+ if ('fn' in dep) {
187
+ update(dep);
199
188
  }
200
- else {
201
- node.sources = observers;
202
- }
203
- for (let i = index, n = node.sources.length; i < n; i++) {
204
- let source = node.sources[i];
205
- if (source.observers === null) {
206
- source.observers = [node];
207
- }
208
- else {
209
- source.observers.push(node);
210
- }
189
+ if (el.state & STATE_DIRTY) {
190
+ break;
211
191
  }
212
192
  }
213
- else if (node.sources !== null && index < node.sources.length) {
214
- removeSourceObservers(node, index);
215
- node.sources.length = index;
193
+ }
194
+ if (el.state & STATE_DIRTY) {
195
+ recompute(el, true);
196
+ }
197
+ el.state = STATE_NONE;
198
+ }
199
+ const computed = (fn) => {
200
+ let self = {
201
+ [REACTIVE]: true,
202
+ cleanup: null,
203
+ deps: null,
204
+ depsTail: null,
205
+ fn: fn,
206
+ height: 0,
207
+ nextHeap: undefined,
208
+ prevHeap: null,
209
+ state: STATE_NONE,
210
+ subs: null,
211
+ subsTail: null,
212
+ value: undefined,
213
+ };
214
+ self.prevHeap = self;
215
+ if (context) {
216
+ if (context.depsTail === null) {
217
+ self.height = context.height;
218
+ recompute(self, false);
216
219
  }
217
- if (node.type === COMPUTED) {
218
- node.set(value);
220
+ else {
221
+ self.height = context.height + 1;
222
+ insertIntoHeap(self);
219
223
  }
224
+ link(self, context);
220
225
  }
221
- finally {
222
- index = i;
223
- observer = o;
224
- observers = os;
226
+ else {
227
+ recompute(self, false);
225
228
  }
226
- node.state = CLEAN;
227
- }
228
- const computed = (fn, options) => {
229
- let instance = new Reactive(DIRTY, COMPUTED, undefined);
230
- instance.changed = options?.changed || changed;
231
- instance.fn = fn;
232
- return instance;
229
+ return self;
233
230
  };
234
- const dispose = (dispose) => {
235
- if (dispose == null) {
231
+ const dispose = (el) => {
232
+ deleteFromHeap(el);
233
+ let dep = el.deps;
234
+ while (dep !== null) {
235
+ dep = unlink(dep);
236
236
  }
237
- else if (isArray(dispose)) {
238
- for (let i = 0, n = dispose.length; i < n; i++) {
239
- dispose[i].dispose();
240
- }
237
+ el.deps = null;
238
+ cleanup(el);
239
+ };
240
+ const isComputed = (value) => {
241
+ return isObject(value) && REACTIVE in value && 'fn' in value;
242
+ };
243
+ const isReactive = (value) => {
244
+ return isObject(value) && REACTIVE in value;
245
+ };
246
+ const isSignal = (value) => {
247
+ return isObject(value) && REACTIVE in value && 'fn' in value === false;
248
+ };
249
+ const oncleanup = (fn) => {
250
+ if (!context) {
251
+ return fn;
252
+ }
253
+ let node = context;
254
+ if (!node.cleanup) {
255
+ node.cleanup = fn;
256
+ }
257
+ else if (isArray(node.cleanup)) {
258
+ node.cleanup.push(fn);
241
259
  }
242
260
  else {
243
- dispose.dispose();
261
+ node.cleanup = [node.cleanup, fn];
244
262
  }
245
- return dispose;
246
- };
247
- const effect = (fn) => {
248
- let instance = new Reactive(DIRTY, EFFECT, null);
249
- instance.fn = fn;
250
- instance.task = () => instance.get();
251
- update(instance);
252
- return instance;
263
+ return fn;
253
264
  };
254
- const root = (fn, scheduler) => {
255
- let o = observer, s = scope;
256
- if (scheduler === undefined) {
257
- if (o?.type === EFFECT) {
258
- scope = o.root;
259
- }
260
- if (scope === null) {
261
- throw new Error('@esportsplus/reactivity: `root` cannot be created without a task scheduler');
265
+ const read = (el) => {
266
+ if (context) {
267
+ link(el, context);
268
+ if ('fn' in el) {
269
+ let height = el.height;
270
+ if (height >= context.height) {
271
+ context.height = height + 1;
272
+ }
273
+ if (height >= minDirty ||
274
+ el.state & (STATE_DIRTY | STATE_CHECK)) {
275
+ markHeap();
276
+ update(el);
277
+ }
262
278
  }
263
- scheduler = scope.scheduler;
264
- }
265
- observer = null;
266
- scope = new Reactive(CLEAN, ROOT, null);
267
- scope.scheduler = scheduler;
268
- scope.tracking = fn.length > 0;
269
- let result = fn.call(null, scope);
270
- observer = o;
271
- scope = s;
272
- return result;
279
+ }
280
+ return el.value;
273
281
  };
274
- const signal = (value, options) => {
275
- let instance = new Reactive(CLEAN, SIGNAL, value);
276
- instance.changed = options?.changed || changed;
277
- return instance;
282
+ const root = (fn) => {
283
+ let c = context;
284
+ context = null;
285
+ let value = fn();
286
+ context = c;
287
+ return value;
288
+ };
289
+ const signal = (value) => {
290
+ return {
291
+ [REACTIVE]: true,
292
+ subs: null,
293
+ subsTail: null,
294
+ value,
295
+ };
296
+ };
297
+ signal.set = (el, v) => {
298
+ if (el.value === v) {
299
+ return;
300
+ }
301
+ el.value = v;
302
+ for (let link = el.subs; link !== null; link = link.nextSub) {
303
+ markedHeap = false;
304
+ insertIntoHeap(link.sub);
305
+ }
306
+ };
307
+ const stabilize = () => {
308
+ for (minDirty = 0; minDirty <= maxDirty; minDirty++) {
309
+ let el = dirtyHeap[minDirty];
310
+ dirtyHeap[minDirty] = undefined;
311
+ while (el !== undefined) {
312
+ let next = el.nextHeap;
313
+ recompute(el, false);
314
+ el = next;
315
+ }
316
+ }
278
317
  };
279
- export { computed, dispose, effect, root, signal };
280
- export { Reactive };
318
+ export { computed, dispose, isComputed, isReactive, isSignal, oncleanup, read, root, signal, stabilize };
package/build/types.d.ts CHANGED
@@ -1,43 +1,33 @@
1
- import { Function, NeverAsync, Prettify } from '@esportsplus/utilities';
1
+ import { REACTIVE, STATE_CHECK, STATE_DIRTY, STATE_IN_HEAP, STATE_NONE, STATE_RECOMPUTING } from './constants.js';
2
+ import { oncleanup } from './signal.js';
2
3
  import { ReactiveArray } from './reactive/array.js';
3
4
  import { ReactiveObject } from './reactive/object.js';
4
- import { CHECK, CLEAN, COMPUTED, DIRTY, DISPOSED, EFFECT, ROOT, SIGNAL } from './constants.js';
5
- import { Reactive as ReactiveBase } from './signal.js';
6
- type Base<T> = Omit<ReactiveBase<T>, 'changed' | 'fn' | 'get' | 'scheduler' | 'set' | 'task' | 'tracking'>;
7
- type Changed = (a: unknown, b: unknown) => boolean;
8
- type Computed<T> = {
9
- changed: Changed;
10
- fn: NeverAsync<(instance: Computed<T>) => T>;
11
- get(): T;
12
- } & Base<T>;
13
- type Effect = {
14
- fn: NeverAsync<(instance: Effect) => void>;
15
- root: Root;
16
- task: Function;
17
- } & Omit<Base<void>, 'value'>;
18
- type Infer<T> = T extends (...args: unknown[]) => unknown ? ReturnType<T> : T extends (infer U)[] ? ReactiveArray<U> : T extends ReactiveObject<T> ? ReactiveObject<T> : T extends Record<PropertyKey, unknown> ? {
5
+ interface Computed<T> extends Signal<T> {
6
+ [REACTIVE]: true;
7
+ cleanup: VoidFunction | VoidFunction[] | null;
8
+ deps: Link | null;
9
+ depsTail: Link | null;
10
+ fn: (oc?: typeof oncleanup) => T;
11
+ height: number;
12
+ nextHeap: Computed<unknown> | undefined;
13
+ prevHeap: Computed<unknown>;
14
+ state: typeof STATE_CHECK | typeof STATE_DIRTY | typeof STATE_IN_HEAP | typeof STATE_NONE | typeof STATE_RECOMPUTING;
15
+ }
16
+ type Infer<T> = T extends (...args: unknown[]) => unknown ? ReturnType<T> : T extends (infer U)[] ? ReactiveArray<U> : T extends ReactiveObject<any> ? T : T extends Record<PropertyKey, unknown> ? {
19
17
  [K in keyof T]: T[K];
20
18
  } : T;
21
- type Event = 'cleanup' | 'dispose' | 'update' | string;
22
- type Listener<D> = {
23
- once?: boolean;
24
- <V>(data: D, value: V): void;
25
- };
26
- type Options = {
27
- changed?: Changed;
28
- };
19
+ interface Link {
20
+ dep: Signal<unknown> | Computed<unknown>;
21
+ sub: Computed<unknown>;
22
+ nextDep: Link | null;
23
+ nextSub: Link | null;
24
+ prevSub: Link | null;
25
+ }
29
26
  type Reactive<T> = T extends Record<PropertyKey, unknown> ? ReactiveObject<T> : ReactiveArray<T>;
30
- type Root = {
31
- scheduler: Scheduler;
32
- tracking: boolean;
33
- value: void;
34
- } & Omit<ReactiveBase<void>, 'root'>;
35
- type Scheduler = (fn: Function) => unknown;
36
27
  type Signal<T> = {
37
- changed: Changed;
38
- get(): T;
39
- set(value: T): T;
40
- } & Base<T>;
41
- type State = typeof CHECK | typeof CLEAN | typeof DIRTY | typeof DISPOSED;
42
- type Type = typeof COMPUTED | typeof EFFECT | typeof ROOT | typeof SIGNAL;
43
- export type { Changed, Computed, Effect, Event, Function, Infer, Listener, NeverAsync, Options, Prettify, Reactive, ReactiveArray, ReactiveObject, Root, Scheduler, Signal, State, Type };
28
+ [REACTIVE]: true;
29
+ subs: Link | null;
30
+ subsTail: Link | null;
31
+ value: T;
32
+ };
33
+ export type { Computed, Infer, Link, Signal, Reactive, ReactiveArray, ReactiveObject };
package/build/types.js CHANGED
@@ -1 +1 @@
1
- export {};
1
+ import { REACTIVE } from './constants.js';
package/package.json CHANGED
@@ -1,18 +1,18 @@
1
1
  {
2
2
  "author": "ICJR",
3
3
  "dependencies": {
4
- "@esportsplus/custom-function": "^0.0.12",
5
- "@esportsplus/utilities": "^0.15.0"
4
+ "@esportsplus/custom-function": "^0.0.13",
5
+ "@esportsplus/utilities": "^0.19.1"
6
6
  },
7
7
  "devDependencies": {
8
- "@esportsplus/typescript": "^0.9.1"
8
+ "@esportsplus/typescript": "^0.9.2"
9
9
  },
10
10
  "main": "build/index.js",
11
11
  "name": "@esportsplus/reactivity",
12
12
  "private": false,
13
13
  "type": "module",
14
14
  "types": "build/index.d.ts",
15
- "version": "0.4.7",
15
+ "version": "0.5.0",
16
16
  "scripts": {
17
17
  "build": "tsc && tsc-alias",
18
18
  "-": "-"
package/readme.md CHANGED
@@ -1,2 +1,2 @@
1
- https://github.com/maverick-js/signals
2
- https://github.com/modderme123/reactively
1
+ https://github.com/milomg/r3
2
+ https://github.com/stackblitz/alien-signals