signalium 0.2.8 → 0.3.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/.turbo/turbo-build.log +12 -0
- package/CHANGELOG.md +12 -0
- package/dist/cjs/config.d.ts +14 -5
- package/dist/cjs/config.d.ts.map +1 -1
- package/dist/cjs/config.js +23 -14
- package/dist/cjs/config.js.map +1 -1
- package/dist/cjs/debug.d.ts +3 -0
- package/dist/cjs/debug.d.ts.map +1 -0
- package/dist/cjs/debug.js +16 -0
- package/dist/cjs/debug.js.map +1 -0
- package/dist/cjs/hooks.d.ts +45 -0
- package/dist/cjs/hooks.d.ts.map +1 -0
- package/dist/cjs/hooks.js +263 -0
- package/dist/cjs/hooks.js.map +1 -0
- package/dist/cjs/index.d.ts +5 -3
- package/dist/cjs/index.d.ts.map +1 -1
- package/dist/cjs/index.js +21 -8
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/react/context.d.ts +4 -0
- package/dist/cjs/react/context.d.ts.map +1 -0
- package/dist/cjs/react/context.js +10 -0
- package/dist/cjs/react/context.js.map +1 -0
- package/dist/cjs/react/index.d.ts +5 -0
- package/dist/cjs/react/index.d.ts.map +1 -0
- package/dist/cjs/react/index.js +12 -0
- package/dist/cjs/react/index.js.map +1 -0
- package/dist/cjs/react/provider.d.ts +7 -0
- package/dist/cjs/react/provider.d.ts.map +1 -0
- package/dist/cjs/react/provider.js +13 -0
- package/dist/cjs/react/provider.js.map +1 -0
- package/dist/cjs/react/setup.d.ts +2 -0
- package/dist/cjs/react/setup.d.ts.map +1 -0
- package/dist/cjs/react/setup.js +13 -0
- package/dist/cjs/react/setup.js.map +1 -0
- package/dist/cjs/react/signal-value.d.ts +2 -0
- package/dist/cjs/react/signal-value.d.ts.map +1 -0
- package/dist/cjs/react/signal-value.js +35 -0
- package/dist/cjs/react/signal-value.js.map +1 -0
- package/dist/cjs/react/state.d.ts +3 -0
- package/dist/cjs/react/state.d.ts.map +1 -0
- package/dist/cjs/react/state.js +13 -0
- package/dist/cjs/react/state.js.map +1 -0
- package/dist/cjs/scheduling.d.ts +5 -0
- package/dist/cjs/scheduling.d.ts.map +1 -1
- package/dist/cjs/scheduling.js +59 -5
- package/dist/cjs/scheduling.js.map +1 -1
- package/dist/cjs/signals.d.ts +28 -68
- package/dist/cjs/signals.d.ts.map +1 -1
- package/dist/cjs/signals.js +223 -76
- package/dist/cjs/signals.js.map +1 -1
- package/dist/cjs/trace.d.ts +127 -0
- package/dist/cjs/trace.d.ts.map +1 -0
- package/dist/cjs/trace.js +319 -0
- package/dist/cjs/trace.js.map +1 -0
- package/dist/cjs/types.d.ts +66 -0
- package/dist/cjs/types.d.ts.map +1 -0
- package/dist/cjs/types.js +3 -0
- package/dist/cjs/types.js.map +1 -0
- package/dist/cjs/utils.d.ts +4 -0
- package/dist/cjs/utils.d.ts.map +1 -0
- package/dist/cjs/utils.js +80 -0
- package/dist/cjs/utils.js.map +1 -0
- package/dist/esm/config.d.ts +14 -5
- package/dist/esm/config.d.ts.map +1 -1
- package/dist/esm/config.js +19 -11
- package/dist/esm/config.js.map +1 -1
- package/dist/esm/debug.d.ts +3 -0
- package/dist/esm/debug.d.ts.map +1 -0
- package/dist/esm/debug.js +3 -0
- package/dist/esm/debug.js.map +1 -0
- package/dist/esm/hooks.d.ts +45 -0
- package/dist/esm/hooks.d.ts.map +1 -0
- package/dist/esm/hooks.js +246 -0
- package/dist/esm/hooks.js.map +1 -0
- package/dist/esm/index.d.ts +5 -3
- package/dist/esm/index.d.ts.map +1 -1
- package/dist/esm/index.js +4 -2
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/react/context.d.ts +4 -0
- package/dist/esm/react/context.d.ts.map +1 -0
- package/dist/esm/react/context.js +6 -0
- package/dist/esm/react/context.js.map +1 -0
- package/dist/esm/react/index.d.ts +5 -0
- package/dist/esm/react/index.d.ts.map +1 -0
- package/dist/esm/react/index.js +5 -0
- package/dist/esm/react/index.js.map +1 -0
- package/dist/esm/react/provider.d.ts +7 -0
- package/dist/esm/react/provider.d.ts.map +1 -0
- package/dist/esm/react/provider.js +10 -0
- package/dist/esm/react/provider.js.map +1 -0
- package/dist/esm/react/setup.d.ts +2 -0
- package/dist/esm/react/setup.d.ts.map +1 -0
- package/dist/esm/react/setup.js +10 -0
- package/dist/esm/react/setup.js.map +1 -0
- package/dist/esm/react/signal-value.d.ts +2 -0
- package/dist/esm/react/signal-value.d.ts.map +1 -0
- package/dist/esm/react/signal-value.js +32 -0
- package/dist/esm/react/signal-value.js.map +1 -0
- package/dist/esm/react/state.d.ts +3 -0
- package/dist/esm/react/state.d.ts.map +1 -0
- package/dist/esm/react/state.js +10 -0
- package/dist/esm/react/state.js.map +1 -0
- package/dist/esm/scheduling.d.ts +5 -0
- package/dist/esm/scheduling.d.ts.map +1 -1
- package/dist/esm/scheduling.js +51 -1
- package/dist/esm/scheduling.js.map +1 -1
- package/dist/esm/signals.d.ts +28 -68
- package/dist/esm/signals.d.ts.map +1 -1
- package/dist/esm/signals.js +215 -72
- package/dist/esm/signals.js.map +1 -1
- package/dist/esm/trace.d.ts +127 -0
- package/dist/esm/trace.d.ts.map +1 -0
- package/dist/esm/trace.js +311 -0
- package/dist/esm/trace.js.map +1 -0
- package/dist/esm/types.d.ts +66 -0
- package/dist/esm/types.d.ts.map +1 -0
- package/dist/esm/types.js +2 -0
- package/dist/esm/types.js.map +1 -0
- package/dist/esm/utils.d.ts +4 -0
- package/dist/esm/utils.d.ts.map +1 -0
- package/dist/esm/utils.js +75 -0
- package/dist/esm/utils.js.map +1 -0
- package/package.json +43 -2
- package/src/__tests__/hooks/async-computed.test.ts +190 -0
- package/src/__tests__/hooks/async-task.test.ts +227 -0
- package/src/__tests__/hooks/computed.test.ts +126 -0
- package/src/__tests__/hooks/context.test.ts +527 -0
- package/src/__tests__/hooks/nesting.test.ts +303 -0
- package/src/__tests__/hooks/params-and-state.test.ts +168 -0
- package/src/__tests__/hooks/subscription.test.ts +97 -0
- package/src/__tests__/signals/async.test.ts +416 -0
- package/src/__tests__/signals/basic.test.ts +399 -0
- package/src/__tests__/signals/subscription.test.ts +632 -0
- package/src/__tests__/signals/watcher.test.ts +253 -0
- package/src/__tests__/utils/async.ts +6 -0
- package/src/__tests__/utils/builders.ts +22 -0
- package/src/__tests__/utils/instrumented-hooks.ts +309 -0
- package/src/__tests__/utils/instrumented-signals.ts +281 -0
- package/src/__tests__/utils/permute.ts +74 -0
- package/src/config.ts +32 -17
- package/src/debug.ts +14 -0
- package/src/hooks.ts +433 -0
- package/src/index.ts +28 -3
- package/src/react/__tests__/react.test.tsx +227 -0
- package/src/react/context.ts +8 -0
- package/src/react/index.ts +4 -0
- package/src/react/provider.tsx +18 -0
- package/src/react/setup.ts +10 -0
- package/src/react/signal-value.ts +49 -0
- package/src/react/state.ts +13 -0
- package/src/scheduling.ts +69 -1
- package/src/signals.ts +328 -169
- package/src/trace.ts +449 -0
- package/src/types.ts +86 -0
- package/src/utils.ts +83 -0
- package/tsconfig.json +2 -1
- package/vitest.workspace.ts +24 -0
- package/src/__tests__/async.test.ts +0 -426
- package/src/__tests__/basic.test.ts +0 -378
- package/src/__tests__/subscription.test.ts +0 -645
- package/src/__tests__/utils/instrumented.ts +0 -326
package/src/trace.ts
ADDED
@@ -0,0 +1,449 @@
|
|
1
|
+
import { scheduleTracer } from './scheduling.js';
|
2
|
+
import { ComputedSignal } from './signals.js';
|
3
|
+
import { Signal, Watcher } from './types.js';
|
4
|
+
|
5
|
+
export let TRACER: TracerProxy | undefined;
|
6
|
+
|
7
|
+
export enum VisualizerNodeType {
|
8
|
+
State,
|
9
|
+
Computed,
|
10
|
+
AsyncComputed,
|
11
|
+
Subscription,
|
12
|
+
Watcher,
|
13
|
+
}
|
14
|
+
|
15
|
+
export interface VisualizerLink {
|
16
|
+
connected: boolean;
|
17
|
+
version: number;
|
18
|
+
node: VisualizerNode;
|
19
|
+
}
|
20
|
+
|
21
|
+
export enum TracerEventType {
|
22
|
+
StartUpdate = 'StartUpdate',
|
23
|
+
EndUpdate = 'EndUpdate',
|
24
|
+
StartLoading = 'StartLoading',
|
25
|
+
EndLoading = 'EndLoading',
|
26
|
+
|
27
|
+
Connected = 'Connected',
|
28
|
+
Disconnected = 'Disconnected',
|
29
|
+
|
30
|
+
ConsumeState = 'ConsumeState',
|
31
|
+
}
|
32
|
+
|
33
|
+
type StartUpdateEvent = {
|
34
|
+
type: TracerEventType.StartUpdate;
|
35
|
+
id: string;
|
36
|
+
};
|
37
|
+
|
38
|
+
type EndUpdateEvent = {
|
39
|
+
type: TracerEventType.EndUpdate;
|
40
|
+
id: string;
|
41
|
+
value: unknown;
|
42
|
+
preserveChildren?: boolean;
|
43
|
+
};
|
44
|
+
|
45
|
+
type StartLoadingEvent = {
|
46
|
+
type: TracerEventType.StartLoading;
|
47
|
+
id: string;
|
48
|
+
};
|
49
|
+
|
50
|
+
type EndLoadingEvent = {
|
51
|
+
type: TracerEventType.EndLoading;
|
52
|
+
id: string;
|
53
|
+
value: unknown;
|
54
|
+
};
|
55
|
+
|
56
|
+
type ConnectedEvent = {
|
57
|
+
type: TracerEventType.Connected;
|
58
|
+
id: string;
|
59
|
+
childId: string;
|
60
|
+
nodeType: VisualizerNodeType;
|
61
|
+
name?: string;
|
62
|
+
params?: string;
|
63
|
+
};
|
64
|
+
|
65
|
+
type DisconnectedEvent = {
|
66
|
+
type: TracerEventType.Disconnected;
|
67
|
+
id: string;
|
68
|
+
childId: string;
|
69
|
+
};
|
70
|
+
|
71
|
+
type ConsumeStateEvent = {
|
72
|
+
type: TracerEventType.ConsumeState;
|
73
|
+
id: string;
|
74
|
+
childId: string;
|
75
|
+
value: unknown;
|
76
|
+
setValue: (value: unknown) => void;
|
77
|
+
};
|
78
|
+
|
79
|
+
type TracerEvent =
|
80
|
+
| StartUpdateEvent
|
81
|
+
| EndUpdateEvent
|
82
|
+
| StartLoadingEvent
|
83
|
+
| EndLoadingEvent
|
84
|
+
| ConnectedEvent
|
85
|
+
| DisconnectedEvent
|
86
|
+
| ConsumeStateEvent;
|
87
|
+
|
88
|
+
export class VisualizerNode {
|
89
|
+
private subscribers: (() => void)[] = [];
|
90
|
+
|
91
|
+
private nextStateChildren: VisualizerNode[] = [];
|
92
|
+
|
93
|
+
public stateChildren: VisualizerNode[] = [];
|
94
|
+
public children: VisualizerLink[] = [];
|
95
|
+
public updating = true;
|
96
|
+
public loading = false;
|
97
|
+
public version = 0;
|
98
|
+
|
99
|
+
private updatingVersion = 0;
|
100
|
+
private didConnect = false;
|
101
|
+
|
102
|
+
constructor(
|
103
|
+
private tracer: Tracer,
|
104
|
+
public depth: number,
|
105
|
+
public type: VisualizerNodeType,
|
106
|
+
public id: string,
|
107
|
+
public value: unknown,
|
108
|
+
public name?: string,
|
109
|
+
public params?: string,
|
110
|
+
private _setValue?: (value: unknown) => void,
|
111
|
+
) {
|
112
|
+
this.tracer.maxDepth = Math.max(this.tracer.maxDepth, this.depth);
|
113
|
+
}
|
114
|
+
|
115
|
+
get showParams() {
|
116
|
+
return this.tracer.showParams;
|
117
|
+
}
|
118
|
+
|
119
|
+
get showValue() {
|
120
|
+
return this.tracer.showValue;
|
121
|
+
}
|
122
|
+
|
123
|
+
get interactive() {
|
124
|
+
return this.tracer.interactive;
|
125
|
+
}
|
126
|
+
|
127
|
+
setValue(value: unknown) {
|
128
|
+
if (this.type !== VisualizerNodeType.State) {
|
129
|
+
throw new Error('setValue is only allowed on state nodes');
|
130
|
+
}
|
131
|
+
|
132
|
+
this._setValue?.(value);
|
133
|
+
this.notify();
|
134
|
+
scheduleTracer(this.tracer);
|
135
|
+
}
|
136
|
+
|
137
|
+
connectChild(child: VisualizerNode): boolean {
|
138
|
+
let childLink = this.children.find(
|
139
|
+
link => link.node.id === child.id || (link.node.name === child.name && link.version !== this.updatingVersion),
|
140
|
+
);
|
141
|
+
|
142
|
+
let shouldSkip = false;
|
143
|
+
|
144
|
+
if (childLink) {
|
145
|
+
if (!child.didConnect) {
|
146
|
+
child.value = childLink.node.value;
|
147
|
+
child.children = childLink.node.children.map(link => ({
|
148
|
+
...link,
|
149
|
+
version: child.version,
|
150
|
+
}));
|
151
|
+
}
|
152
|
+
|
153
|
+
childLink.node = child;
|
154
|
+
childLink.connected = true;
|
155
|
+
childLink.version = this.updatingVersion;
|
156
|
+
shouldSkip = true;
|
157
|
+
} else {
|
158
|
+
this.children.push({
|
159
|
+
connected: true,
|
160
|
+
node: child,
|
161
|
+
version: this.updatingVersion,
|
162
|
+
});
|
163
|
+
}
|
164
|
+
|
165
|
+
child.didConnect = true;
|
166
|
+
this.notify();
|
167
|
+
|
168
|
+
return shouldSkip;
|
169
|
+
}
|
170
|
+
|
171
|
+
disconnectChild(childId: string) {
|
172
|
+
const childLink = this.children.find(link => link.node.id === childId);
|
173
|
+
|
174
|
+
if (!childLink) {
|
175
|
+
return;
|
176
|
+
}
|
177
|
+
|
178
|
+
childLink.connected = false;
|
179
|
+
|
180
|
+
this.notify();
|
181
|
+
}
|
182
|
+
|
183
|
+
startUpdate() {
|
184
|
+
this.updating = true;
|
185
|
+
this.updatingVersion++;
|
186
|
+
|
187
|
+
this.notify();
|
188
|
+
}
|
189
|
+
|
190
|
+
endUpdate(value: unknown, preserveChildren = false) {
|
191
|
+
this.updating = false;
|
192
|
+
this.value = value;
|
193
|
+
if (!preserveChildren) {
|
194
|
+
this.stateChildren = this.nextStateChildren;
|
195
|
+
}
|
196
|
+
this.nextStateChildren = [];
|
197
|
+
this.notify();
|
198
|
+
}
|
199
|
+
|
200
|
+
startLoading() {
|
201
|
+
this.loading = true;
|
202
|
+
this.notify();
|
203
|
+
}
|
204
|
+
|
205
|
+
endLoading(value: unknown) {
|
206
|
+
this.loading = false;
|
207
|
+
this.value = value;
|
208
|
+
this.notify();
|
209
|
+
}
|
210
|
+
|
211
|
+
consumeState(id: string, value: unknown, setValue: (value: unknown) => void) {
|
212
|
+
const existing = this.stateChildren.find(child => child.id === id);
|
213
|
+
|
214
|
+
if (existing) {
|
215
|
+
existing.value = value;
|
216
|
+
this.nextStateChildren.push(existing);
|
217
|
+
existing.notify();
|
218
|
+
} else {
|
219
|
+
const node = new VisualizerNode(
|
220
|
+
this.tracer,
|
221
|
+
this.depth + 1,
|
222
|
+
VisualizerNodeType.State,
|
223
|
+
id,
|
224
|
+
value,
|
225
|
+
undefined,
|
226
|
+
undefined,
|
227
|
+
setValue,
|
228
|
+
);
|
229
|
+
node.updating = false;
|
230
|
+
this.nextStateChildren.push(node);
|
231
|
+
}
|
232
|
+
}
|
233
|
+
|
234
|
+
notify() {
|
235
|
+
this.version++;
|
236
|
+
for (const subscriber of this.subscribers) {
|
237
|
+
subscriber();
|
238
|
+
}
|
239
|
+
}
|
240
|
+
|
241
|
+
subscribe(subscriber: () => void) {
|
242
|
+
this.subscribers.push(subscriber);
|
243
|
+
|
244
|
+
return () => {
|
245
|
+
this.subscribers = this.subscribers.filter(s => s !== subscriber);
|
246
|
+
};
|
247
|
+
}
|
248
|
+
}
|
249
|
+
|
250
|
+
let ID = 0;
|
251
|
+
|
252
|
+
class TraceFlush {
|
253
|
+
private forceComplete = false;
|
254
|
+
public promise: Promise<void>;
|
255
|
+
public id = ID++;
|
256
|
+
constructor(tracer: Tracer, queue: TracerEvent[], previousFlush?: TraceFlush) {
|
257
|
+
this.promise = this.runFlush(tracer, queue, previousFlush);
|
258
|
+
}
|
259
|
+
|
260
|
+
complete() {
|
261
|
+
this.forceComplete = true;
|
262
|
+
return this.promise;
|
263
|
+
}
|
264
|
+
|
265
|
+
private async runFlush(tracer: Tracer, queue: TracerEvent[], previousFlush?: TraceFlush) {
|
266
|
+
if (previousFlush) {
|
267
|
+
await previousFlush.complete();
|
268
|
+
}
|
269
|
+
|
270
|
+
for (let i = 0; i < queue.length; i++) {
|
271
|
+
const event = queue[i];
|
272
|
+
const nextEvent = queue[i + 1];
|
273
|
+
|
274
|
+
const skipDelay = tracer.handleEvent(event, nextEvent);
|
275
|
+
|
276
|
+
if (!this.forceComplete && !skipDelay && tracer.delay > 0 && !document.hidden) {
|
277
|
+
await new Promise(resolve => setTimeout(resolve, tracer.delay));
|
278
|
+
}
|
279
|
+
}
|
280
|
+
}
|
281
|
+
}
|
282
|
+
|
283
|
+
export class Tracer {
|
284
|
+
private nodeMap = new Map<string, VisualizerNode>();
|
285
|
+
|
286
|
+
delay = 200;
|
287
|
+
maxDepth = 0;
|
288
|
+
|
289
|
+
private initialized = false;
|
290
|
+
|
291
|
+
constructor(
|
292
|
+
id: string,
|
293
|
+
immediate = false,
|
294
|
+
public showParams = true,
|
295
|
+
public showValue = true,
|
296
|
+
public interactive = true,
|
297
|
+
) {
|
298
|
+
// If it's immediate, we should run the first flush immediately, skipping animations
|
299
|
+
this.initialized = !immediate;
|
300
|
+
|
301
|
+
const node = new VisualizerNode(this, 0, VisualizerNodeType.Watcher, id, '');
|
302
|
+
|
303
|
+
this.rootNode = node;
|
304
|
+
this.nodeMap.set(id, node);
|
305
|
+
}
|
306
|
+
|
307
|
+
public rootNode: VisualizerNode;
|
308
|
+
|
309
|
+
private eventQueue: TracerEvent[] = [];
|
310
|
+
private currentFlush: TraceFlush | undefined;
|
311
|
+
|
312
|
+
emit(event: TracerEvent) {
|
313
|
+
if (event.type === TracerEventType.Connected || event.type === TracerEventType.ConsumeState) {
|
314
|
+
const node = this.nodeMap.get(event.id);
|
315
|
+
|
316
|
+
if (!node) {
|
317
|
+
return;
|
318
|
+
}
|
319
|
+
|
320
|
+
if (!this.nodeMap.has(event.childId)) {
|
321
|
+
const name = event.type === TracerEventType.Connected ? event.name : undefined;
|
322
|
+
const params = event.type === TracerEventType.Connected ? event.params : undefined;
|
323
|
+
const nodeType = event.type === TracerEventType.Connected ? event.nodeType : VisualizerNodeType.State;
|
324
|
+
|
325
|
+
this.nodeMap.set(
|
326
|
+
event.childId,
|
327
|
+
new VisualizerNode(this, node.depth + 1, nodeType, event.childId, '', name, params),
|
328
|
+
);
|
329
|
+
}
|
330
|
+
}
|
331
|
+
|
332
|
+
if (this.initialized) {
|
333
|
+
this.eventQueue.push(event);
|
334
|
+
} else {
|
335
|
+
this.handleEvent(event);
|
336
|
+
}
|
337
|
+
}
|
338
|
+
|
339
|
+
handleEvent(event: TracerEvent, nextEvent?: TracerEvent) {
|
340
|
+
const node = this.nodeMap.get(event.id);
|
341
|
+
|
342
|
+
if (!node) {
|
343
|
+
return true;
|
344
|
+
}
|
345
|
+
|
346
|
+
let skipDelay = nextEvent?.type === TracerEventType.StartLoading;
|
347
|
+
|
348
|
+
if (event.type === TracerEventType.Connected) {
|
349
|
+
let child = this.nodeMap.get(event.childId);
|
350
|
+
|
351
|
+
if (!child) {
|
352
|
+
throw new Error(`Child node ${event.childId} not found`);
|
353
|
+
}
|
354
|
+
|
355
|
+
skipDelay = node.connectChild(child);
|
356
|
+
} else if (event.type === TracerEventType.Disconnected) {
|
357
|
+
node.disconnectChild(event.childId);
|
358
|
+
} else if (event.type === TracerEventType.StartUpdate) {
|
359
|
+
node.startUpdate();
|
360
|
+
if (
|
361
|
+
nextEvent &&
|
362
|
+
nextEvent.id === event.id &&
|
363
|
+
(nextEvent.type === TracerEventType.EndUpdate || nextEvent.type === TracerEventType.StartLoading)
|
364
|
+
) {
|
365
|
+
skipDelay = true;
|
366
|
+
}
|
367
|
+
} else if (event.type === TracerEventType.EndUpdate) {
|
368
|
+
node.endUpdate(event.value, event.preserveChildren);
|
369
|
+
} else if (event.type === TracerEventType.StartLoading) {
|
370
|
+
node.startLoading();
|
371
|
+
skipDelay = true;
|
372
|
+
} else if (event.type === TracerEventType.EndLoading) {
|
373
|
+
node.endLoading(event.value);
|
374
|
+
} else if (event.type === TracerEventType.ConsumeState) {
|
375
|
+
node.consumeState(event.childId, event.value, event.setValue);
|
376
|
+
}
|
377
|
+
|
378
|
+
return skipDelay;
|
379
|
+
}
|
380
|
+
|
381
|
+
async flush() {
|
382
|
+
if (this.eventQueue.length === 0) {
|
383
|
+
return;
|
384
|
+
}
|
385
|
+
|
386
|
+
this.currentFlush = new TraceFlush(this, this.eventQueue, this.currentFlush);
|
387
|
+
this.eventQueue = [];
|
388
|
+
}
|
389
|
+
|
390
|
+
addListener(listener: () => void) {
|
391
|
+
this.initialized = true;
|
392
|
+
return this.rootNode.subscribe(listener);
|
393
|
+
}
|
394
|
+
}
|
395
|
+
|
396
|
+
class TracerProxy {
|
397
|
+
private tracers: Tracer[] = [];
|
398
|
+
|
399
|
+
constructor() {}
|
400
|
+
|
401
|
+
emit(event: TracerEvent) {
|
402
|
+
this.tracers.forEach(tracer => tracer.emit(event));
|
403
|
+
}
|
404
|
+
|
405
|
+
createTracer(id: string, immediate = false): Tracer {
|
406
|
+
const tracer = new Tracer(id, immediate);
|
407
|
+
|
408
|
+
this.tracers.push(tracer);
|
409
|
+
|
410
|
+
return tracer;
|
411
|
+
}
|
412
|
+
|
413
|
+
removeTracer(tracer: Tracer) {
|
414
|
+
this.tracers = this.tracers.filter(t => t !== tracer);
|
415
|
+
}
|
416
|
+
|
417
|
+
flush() {
|
418
|
+
this.tracers.forEach(tracer => tracer.flush());
|
419
|
+
}
|
420
|
+
}
|
421
|
+
|
422
|
+
export function setTracing(enabled: boolean) {
|
423
|
+
if (enabled) {
|
424
|
+
TRACER = new TracerProxy();
|
425
|
+
} else {
|
426
|
+
TRACER = undefined;
|
427
|
+
}
|
428
|
+
}
|
429
|
+
|
430
|
+
export function createTracer(_signal: Signal<unknown> | Watcher<unknown>, immediate = false) {
|
431
|
+
const signal = _signal as ComputedSignal<unknown>;
|
432
|
+
return createTracerFromId(signal._opts.id, immediate);
|
433
|
+
}
|
434
|
+
|
435
|
+
export function createTracerFromId(id: string, immediate = false) {
|
436
|
+
if (!TRACER) {
|
437
|
+
throw new Error('Tracing is not enabled');
|
438
|
+
}
|
439
|
+
|
440
|
+
return TRACER.createTracer(id, immediate);
|
441
|
+
}
|
442
|
+
|
443
|
+
export function removeTracer(tracer: Tracer) {
|
444
|
+
if (!TRACER) {
|
445
|
+
throw new Error('Tracing is not enabled');
|
446
|
+
}
|
447
|
+
|
448
|
+
TRACER.removeTracer(tracer);
|
449
|
+
}
|
package/src/types.ts
ADDED
@@ -0,0 +1,86 @@
|
|
1
|
+
import type { SignalScope } from './hooks.js';
|
2
|
+
|
3
|
+
export interface Signal<T = unknown> {
|
4
|
+
get(): T;
|
5
|
+
}
|
6
|
+
|
7
|
+
export interface WriteableSignal<T> extends Signal<T> {
|
8
|
+
set(value: T): void;
|
9
|
+
}
|
10
|
+
|
11
|
+
export type AsyncSignal<T> = Signal<AsyncResult<T>>;
|
12
|
+
|
13
|
+
export type SignalCompute<T> = (prev: T | undefined) => T;
|
14
|
+
|
15
|
+
export type SignalAsyncCompute<T> = (prev: T | undefined) => T | Promise<T>;
|
16
|
+
|
17
|
+
export type SignalEquals<T> = (prev: T, next: T) => boolean;
|
18
|
+
|
19
|
+
export type SignalSubscription = {
|
20
|
+
update?(): void;
|
21
|
+
unsubscribe?(): void;
|
22
|
+
};
|
23
|
+
|
24
|
+
export type SignalSubscribe<T> = (
|
25
|
+
get: () => T | undefined,
|
26
|
+
set: (value: T) => void,
|
27
|
+
) => SignalSubscription | undefined | void;
|
28
|
+
|
29
|
+
export interface SignalOptions<T, Args extends unknown[]> {
|
30
|
+
equals?: SignalEquals<T> | false;
|
31
|
+
id?: string;
|
32
|
+
desc?: string;
|
33
|
+
params?: string;
|
34
|
+
scope?: SignalScope;
|
35
|
+
paramKey?: (...args: Args) => string;
|
36
|
+
}
|
37
|
+
|
38
|
+
export interface SignalOptionsWithInit<T, Args extends unknown[]> extends SignalOptions<T, Args> {
|
39
|
+
initValue: T;
|
40
|
+
}
|
41
|
+
|
42
|
+
export interface AsyncBaseResult<T> {
|
43
|
+
invalidate(): void;
|
44
|
+
await(): T;
|
45
|
+
}
|
46
|
+
|
47
|
+
export interface AsyncPending<T> extends AsyncBaseResult<T> {
|
48
|
+
result: undefined;
|
49
|
+
error: unknown;
|
50
|
+
isPending: boolean;
|
51
|
+
isReady: false;
|
52
|
+
isError: boolean;
|
53
|
+
isSuccess: boolean;
|
54
|
+
didResolve: boolean;
|
55
|
+
}
|
56
|
+
|
57
|
+
export interface AsyncReady<T> extends AsyncBaseResult<T> {
|
58
|
+
result: T;
|
59
|
+
error: unknown;
|
60
|
+
isPending: boolean;
|
61
|
+
isReady: true;
|
62
|
+
isError: boolean;
|
63
|
+
isSuccess: boolean;
|
64
|
+
didResolve: boolean;
|
65
|
+
}
|
66
|
+
|
67
|
+
export type AsyncResult<T> = AsyncPending<T> | AsyncReady<T>;
|
68
|
+
|
69
|
+
export interface AsyncTask<T, Args extends unknown[] = unknown[]> {
|
70
|
+
result: T | undefined;
|
71
|
+
error: unknown;
|
72
|
+
isPending: boolean;
|
73
|
+
isSuccess: boolean;
|
74
|
+
isError: boolean;
|
75
|
+
isReady: boolean;
|
76
|
+
|
77
|
+
run(...args: Args): Promise<T>;
|
78
|
+
}
|
79
|
+
|
80
|
+
export interface WatcherListenerOptions {
|
81
|
+
immediate?: boolean;
|
82
|
+
}
|
83
|
+
|
84
|
+
export interface Watcher<T> {
|
85
|
+
addListener(listener: (value: T) => void, opts?: WatcherListenerOptions): () => void;
|
86
|
+
}
|
package/src/utils.ts
ADDED
@@ -0,0 +1,83 @@
|
|
1
|
+
import { createComputedSignal, createSubscriptionSignal } from './signals.js';
|
2
|
+
|
3
|
+
const objectToIdMap = new WeakMap<object, string>();
|
4
|
+
let nextId = 1;
|
5
|
+
|
6
|
+
export function getObjectId(obj: object): string {
|
7
|
+
let id = objectToIdMap.get(obj);
|
8
|
+
if (id === undefined) {
|
9
|
+
id = `obj-${nextId++}`;
|
10
|
+
objectToIdMap.set(obj, id);
|
11
|
+
}
|
12
|
+
return id;
|
13
|
+
}
|
14
|
+
|
15
|
+
// Handle basic POJOs and arrays recursively
|
16
|
+
function isPOJO(obj: object): boolean {
|
17
|
+
return Object.getPrototypeOf(obj) === Object.prototype;
|
18
|
+
}
|
19
|
+
|
20
|
+
function isPlainArray(arr: unknown): arr is unknown[] {
|
21
|
+
return Array.isArray(arr);
|
22
|
+
}
|
23
|
+
|
24
|
+
export function hashValue(value: unknown): string {
|
25
|
+
if (value === null) return 'null';
|
26
|
+
if (value === undefined) return 'undefined';
|
27
|
+
|
28
|
+
switch (typeof value) {
|
29
|
+
case 'number':
|
30
|
+
case 'boolean':
|
31
|
+
case 'string':
|
32
|
+
return String(value);
|
33
|
+
case 'bigint':
|
34
|
+
return value.toString();
|
35
|
+
case 'symbol':
|
36
|
+
return String(value);
|
37
|
+
case 'object': {
|
38
|
+
if (value instanceof Date) {
|
39
|
+
return value.toISOString();
|
40
|
+
}
|
41
|
+
if (isPlainArray(value)) {
|
42
|
+
return `[${value.map(hashValue).join(',')}]`;
|
43
|
+
}
|
44
|
+
if (isPOJO(value)) {
|
45
|
+
const entries = [
|
46
|
+
...Object.entries(value),
|
47
|
+
...Object.getOwnPropertySymbols(value).map(sym => [sym, value[sym as keyof typeof value]]),
|
48
|
+
].sort(([a], [b]) => (String(a) < String(b) ? -1 : String(a) > String(b) ? 1 : 0));
|
49
|
+
|
50
|
+
return `{ ${entries.map(([k, v]) => `${String(k)}: ${hashValue(v)}`).join(', ')} }`;
|
51
|
+
}
|
52
|
+
return getObjectId(value);
|
53
|
+
}
|
54
|
+
case 'function':
|
55
|
+
return getObjectId(value);
|
56
|
+
default:
|
57
|
+
return getObjectId(value as object);
|
58
|
+
}
|
59
|
+
}
|
60
|
+
|
61
|
+
let UNKNOWN_SUBSCRIPTION_ID = 0;
|
62
|
+
let UNKNOWN_COMPUTED_ID = 0;
|
63
|
+
let UNKNOWN_ASYNC_COMPUTED_ID = 0;
|
64
|
+
|
65
|
+
const UNKNOWN_SIGNAL_NAMES = new Map<object, string>();
|
66
|
+
|
67
|
+
export function getUnknownSignalFnName(fn: object, makeSignal: unknown) {
|
68
|
+
let name = UNKNOWN_SIGNAL_NAMES.get(fn);
|
69
|
+
|
70
|
+
if (name === undefined) {
|
71
|
+
if (makeSignal === createSubscriptionSignal) {
|
72
|
+
name = `unknownSubscription${UNKNOWN_SUBSCRIPTION_ID++}`;
|
73
|
+
} else if (makeSignal === createComputedSignal) {
|
74
|
+
name = `unknownComputed${UNKNOWN_COMPUTED_ID++}`;
|
75
|
+
} else {
|
76
|
+
name = `unknownAsyncComputed${UNKNOWN_ASYNC_COMPUTED_ID++}`;
|
77
|
+
}
|
78
|
+
|
79
|
+
UNKNOWN_SIGNAL_NAMES.set(fn, name);
|
80
|
+
}
|
81
|
+
|
82
|
+
return name;
|
83
|
+
}
|
package/tsconfig.json
CHANGED
@@ -0,0 +1,24 @@
|
|
1
|
+
/// <reference types="@vitest/browser/providers/playwright" />
|
2
|
+
|
3
|
+
import { defineWorkspace } from 'vitest/config';
|
4
|
+
import react from '@vitejs/plugin-react';
|
5
|
+
|
6
|
+
export default defineWorkspace([
|
7
|
+
{
|
8
|
+
test: {
|
9
|
+
include: ['src/__tests__/**/*.test.ts'],
|
10
|
+
name: 'unit',
|
11
|
+
environment: 'node',
|
12
|
+
},
|
13
|
+
},
|
14
|
+
{
|
15
|
+
plugins: [react()],
|
16
|
+
test: {
|
17
|
+
browser: {
|
18
|
+
enabled: true,
|
19
|
+
provider: 'playwright',
|
20
|
+
instances: [{ browser: 'chromium' }],
|
21
|
+
},
|
22
|
+
},
|
23
|
+
},
|
24
|
+
]);
|