@praxisjs/core 1.4.1 → 1.6.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/CHANGELOG.md +28 -11
- package/dist/__tests__/resource.test.js +35 -0
- package/dist/__tests__/resource.test.js.map +1 -1
- package/dist/__tests__/synced.test.d.ts +2 -0
- package/dist/__tests__/synced.test.d.ts.map +1 -0
- package/dist/__tests__/synced.test.js +143 -0
- package/dist/__tests__/synced.test.js.map +1 -0
- package/dist/internal.d.ts +1 -1
- package/dist/internal.d.ts.map +1 -1
- package/dist/internal.js +1 -1
- package/dist/internal.js.map +1 -1
- package/dist/signal/batch.d.ts.map +1 -1
- package/dist/signal/batch.js +11 -11
- package/dist/signal/batch.js.map +1 -1
- package/dist/signal/computed.d.ts +1 -1
- package/dist/signal/computed.d.ts.map +1 -1
- package/dist/signal/computed.js +46 -36
- package/dist/signal/computed.js.map +1 -1
- package/dist/signal/effect.d.ts.map +1 -1
- package/dist/signal/effect.js +11 -12
- package/dist/signal/effect.js.map +1 -1
- package/dist/signal/index.d.ts +1 -0
- package/dist/signal/index.d.ts.map +1 -1
- package/dist/signal/index.js +1 -0
- package/dist/signal/index.js.map +1 -1
- package/dist/signal/signal.d.ts +9 -0
- package/dist/signal/signal.d.ts.map +1 -1
- package/dist/signal/signal.js +71 -37
- package/dist/signal/signal.js.map +1 -1
- package/dist/signal/synced.d.ts +6 -0
- package/dist/signal/synced.d.ts.map +1 -0
- package/dist/signal/synced.js +43 -0
- package/dist/signal/synced.js.map +1 -0
- package/package.json +1 -1
- package/src/__tests__/resource.test.ts +42 -0
- package/src/__tests__/synced.test.ts +173 -0
- package/src/internal.ts +2 -0
- package/src/signal/batch.ts +9 -11
- package/src/signal/computed.ts +42 -39
- package/src/signal/effect.ts +11 -12
- package/src/signal/index.ts +1 -0
- package/src/signal/signal.ts +62 -37
- package/src/signal/synced.ts +56 -0
package/dist/signal/index.js
CHANGED
|
@@ -3,5 +3,6 @@ export { peek } from "./peek";
|
|
|
3
3
|
export { signal } from "./signal";
|
|
4
4
|
export { computed } from "./computed";
|
|
5
5
|
export { persistedSignal } from "./persisted";
|
|
6
|
+
export { syncedSignal } from "./synced";
|
|
6
7
|
export { effect, untrack } from "./effect";
|
|
7
8
|
//# sourceMappingURL=index.js.map
|
package/dist/signal/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/signal/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAChC,OAAO,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAC;AAC9B,OAAO,EAAE,MAAM,EAAE,MAAM,UAAU,CAAC;AAClC,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AACtC,OAAO,EAA+B,eAAe,EAAE,MAAM,aAAa,CAAC;AAC3E,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,UAAU,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/signal/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAChC,OAAO,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAC;AAC9B,OAAO,EAAE,MAAM,EAAE,MAAM,UAAU,CAAC;AAClC,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AACtC,OAAO,EAA+B,eAAe,EAAE,MAAM,aAAa,CAAC;AAC3E,OAAO,EAAqB,YAAY,EAAE,MAAM,UAAU,CAAC;AAC3D,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,UAAU,CAAC"}
|
package/dist/signal/signal.d.ts
CHANGED
|
@@ -1,3 +1,12 @@
|
|
|
1
1
|
import type { Signal } from "@praxisjs/shared";
|
|
2
|
+
import { type Effect } from "./effect";
|
|
3
|
+
export type SubList = Effect | Effect[] | null;
|
|
4
|
+
export declare function addSub(holder: {
|
|
5
|
+
subs: SubList;
|
|
6
|
+
}, eff: Effect): void;
|
|
7
|
+
export declare function removeSub(holder: {
|
|
8
|
+
subs: SubList;
|
|
9
|
+
}, eff: Effect): void;
|
|
10
|
+
export declare function notifySubs(subs: Exclude<SubList, null>, batching: boolean): void;
|
|
2
11
|
export declare function signal<T>(initialValue: T): Signal<T>;
|
|
3
12
|
//# sourceMappingURL=signal.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"signal.d.ts","sourceRoot":"","sources":["../../src/signal/signal.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;
|
|
1
|
+
{"version":3,"file":"signal.d.ts","sourceRoot":"","sources":["../../src/signal/signal.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAG/C,OAAO,EAAgB,KAAK,MAAM,EAAE,MAAM,UAAU,CAAC;AAErD,MAAM,MAAM,OAAO,GAAG,MAAM,GAAG,MAAM,EAAE,GAAG,IAAI,CAAC;AAE/C,wBAAgB,MAAM,CAAC,MAAM,EAAE;IAAE,IAAI,EAAE,OAAO,CAAA;CAAE,EAAE,GAAG,EAAE,MAAM,GAAG,IAAI,CASnE;AAED,wBAAgB,SAAS,CAAC,MAAM,EAAE;IAAE,IAAI,EAAE,OAAO,CAAA;CAAE,EAAE,GAAG,EAAE,MAAM,GAAG,IAAI,CAStE;AAED,wBAAgB,UAAU,CAAC,IAAI,EAAE,OAAO,CAAC,OAAO,EAAE,IAAI,CAAC,EAAE,QAAQ,EAAE,OAAO,GAAG,IAAI,CAoBhF;AAED,wBAAgB,MAAM,CAAC,CAAC,EAAE,YAAY,EAAE,CAAC,GAAG,MAAM,CAAC,CAAC,CAAC,CA4BpD"}
|
package/dist/signal/signal.js
CHANGED
|
@@ -1,50 +1,84 @@
|
|
|
1
1
|
import { isBatching, enqueueEffect } from "./batch";
|
|
2
2
|
import { activeEffect } from "./effect";
|
|
3
|
+
export function addSub(holder, eff) {
|
|
4
|
+
const subs = holder.subs;
|
|
5
|
+
if (subs === null) {
|
|
6
|
+
holder.subs = eff;
|
|
7
|
+
}
|
|
8
|
+
else if (typeof subs === "function") {
|
|
9
|
+
if (subs !== eff)
|
|
10
|
+
holder.subs = [subs, eff];
|
|
11
|
+
}
|
|
12
|
+
else {
|
|
13
|
+
if (!subs.includes(eff))
|
|
14
|
+
subs.push(eff);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
export function removeSub(holder, eff) {
|
|
18
|
+
const subs = holder.subs;
|
|
19
|
+
if (subs === null)
|
|
20
|
+
return;
|
|
21
|
+
if (typeof subs === "function") {
|
|
22
|
+
if (subs === eff)
|
|
23
|
+
holder.subs = null;
|
|
24
|
+
}
|
|
25
|
+
else {
|
|
26
|
+
const idx = subs.indexOf(eff);
|
|
27
|
+
if (idx >= 0)
|
|
28
|
+
subs.splice(idx, 1);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
export function notifySubs(subs, batching) {
|
|
32
|
+
if (typeof subs === "function") {
|
|
33
|
+
if (batching)
|
|
34
|
+
enqueueEffect(subs);
|
|
35
|
+
else
|
|
36
|
+
subs();
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
if (batching) {
|
|
40
|
+
for (const sub of subs)
|
|
41
|
+
enqueueEffect(sub);
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
let lastError;
|
|
45
|
+
let hasError = false;
|
|
46
|
+
for (const sub of subs) {
|
|
47
|
+
try {
|
|
48
|
+
sub();
|
|
49
|
+
}
|
|
50
|
+
catch (e) {
|
|
51
|
+
lastError = e;
|
|
52
|
+
hasError = true;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
if (hasError)
|
|
56
|
+
throw lastError;
|
|
57
|
+
}
|
|
3
58
|
export function signal(initialValue) {
|
|
4
59
|
let value = initialValue;
|
|
5
|
-
const
|
|
60
|
+
const holder = { subs: null };
|
|
6
61
|
function read() {
|
|
7
|
-
if (activeEffect)
|
|
8
|
-
|
|
9
|
-
}
|
|
62
|
+
if (activeEffect !== null)
|
|
63
|
+
addSub(holder, activeEffect);
|
|
10
64
|
return value;
|
|
11
65
|
}
|
|
12
|
-
|
|
66
|
+
const s = read;
|
|
67
|
+
s.set = (newValue) => {
|
|
13
68
|
if (Object.is(value, newValue))
|
|
14
69
|
return;
|
|
15
70
|
value = newValue;
|
|
16
|
-
if (
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
sub();
|
|
24
|
-
}
|
|
25
|
-
catch (e) {
|
|
26
|
-
errors.push(e);
|
|
27
|
-
}
|
|
28
|
-
});
|
|
29
|
-
if (errors.length > 0)
|
|
30
|
-
throw errors[errors.length - 1];
|
|
31
|
-
}
|
|
32
|
-
function update(fn) {
|
|
33
|
-
set(fn(value));
|
|
34
|
-
}
|
|
35
|
-
function subscribe(fn) {
|
|
36
|
-
const wrapped = () => {
|
|
37
|
-
fn(value);
|
|
38
|
-
};
|
|
39
|
-
subscribers.add(wrapped);
|
|
71
|
+
if (holder.subs !== null)
|
|
72
|
+
notifySubs(holder.subs, isBatching());
|
|
73
|
+
};
|
|
74
|
+
s.update = (fn) => { s.set(fn(value)); };
|
|
75
|
+
s.subscribe = (fn) => {
|
|
76
|
+
const wrapped = () => { fn(value); };
|
|
77
|
+
addSub(holder, wrapped);
|
|
40
78
|
wrapped();
|
|
41
|
-
return () =>
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
signal.update = update;
|
|
46
|
-
signal.subscribe = subscribe;
|
|
47
|
-
signal.__isSignal = true;
|
|
48
|
-
return signal;
|
|
79
|
+
return () => { removeSub(holder, wrapped); };
|
|
80
|
+
};
|
|
81
|
+
s.__isSignal = true;
|
|
82
|
+
return s;
|
|
49
83
|
}
|
|
50
84
|
//# sourceMappingURL=signal.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"signal.js","sourceRoot":"","sources":["../../src/signal/signal.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AACpD,OAAO,EAAE,YAAY,EAAe,MAAM,UAAU,CAAC;
|
|
1
|
+
{"version":3,"file":"signal.js","sourceRoot":"","sources":["../../src/signal/signal.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"synced.d.ts","sourceRoot":"","sources":["../../src/signal/synced.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAI/C,MAAM,WAAW,YAAY,CAAC,CAAC,CAAE,SAAQ,MAAM,CAAC,CAAC,CAAC;IAChD,KAAK,IAAI,IAAI,CAAC;CACf;AAED,wBAAgB,YAAY,CAAC,CAAC,EAC5B,WAAW,EAAE,MAAM,EACnB,YAAY,EAAE,CAAC,GACd,YAAY,CAAC,CAAC,CAAC,CA4CjB"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { signal } from "./signal";
|
|
2
|
+
export function syncedSignal(channelName, initialValue) {
|
|
3
|
+
const inner = signal(initialValue);
|
|
4
|
+
const channel = new BroadcastChannel(channelName);
|
|
5
|
+
let posting = false;
|
|
6
|
+
channel.onmessage = (event) => {
|
|
7
|
+
if (posting)
|
|
8
|
+
return;
|
|
9
|
+
try {
|
|
10
|
+
inner.set(JSON.parse(String(event.data)));
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
// ignore malformed messages
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
function read() {
|
|
17
|
+
return inner();
|
|
18
|
+
}
|
|
19
|
+
function set(value) {
|
|
20
|
+
posting = true;
|
|
21
|
+
try {
|
|
22
|
+
inner.set(value);
|
|
23
|
+
channel.postMessage(JSON.stringify(value));
|
|
24
|
+
}
|
|
25
|
+
finally {
|
|
26
|
+
posting = false;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
function update(fn) {
|
|
30
|
+
set(fn(inner()));
|
|
31
|
+
}
|
|
32
|
+
function close() {
|
|
33
|
+
channel.close();
|
|
34
|
+
}
|
|
35
|
+
const source = read;
|
|
36
|
+
source.set = set;
|
|
37
|
+
source.update = update;
|
|
38
|
+
source.subscribe = inner.subscribe.bind(inner);
|
|
39
|
+
source.__isSignal = true;
|
|
40
|
+
source.close = close;
|
|
41
|
+
return source;
|
|
42
|
+
}
|
|
43
|
+
//# sourceMappingURL=synced.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"synced.js","sourceRoot":"","sources":["../../src/signal/synced.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,MAAM,EAAE,MAAM,UAAU,CAAC;AAMlC,MAAM,UAAU,YAAY,CAC1B,WAAmB,EACnB,YAAe;IAEf,MAAM,KAAK,GAAG,MAAM,CAAC,YAAY,CAAC,CAAC;IACnC,MAAM,OAAO,GAAG,IAAI,gBAAgB,CAAC,WAAW,CAAC,CAAC;IAClD,IAAI,OAAO,GAAG,KAAK,CAAC;IAEpB,OAAO,CAAC,SAAS,GAAG,CAAC,KAAmB,EAAE,EAAE;QAC1C,IAAI,OAAO;YAAE,OAAO;QACpB,IAAI,CAAC;YACH,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,CAAM,CAAC,CAAC;QACjD,CAAC;QAAC,MAAM,CAAC;YACP,4BAA4B;QAC9B,CAAC;IACH,CAAC,CAAC;IAEF,SAAS,IAAI;QACX,OAAO,KAAK,EAAE,CAAC;IACjB,CAAC;IAED,SAAS,GAAG,CAAC,KAAQ;QACnB,OAAO,GAAG,IAAI,CAAC;QACf,IAAI,CAAC;YACH,KAAK,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;YACjB,OAAO,CAAC,WAAW,CAAC,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC,CAAC;QAC7C,CAAC;gBAAS,CAAC;YACT,OAAO,GAAG,KAAK,CAAC;QAClB,CAAC;IACH,CAAC;IAED,SAAS,MAAM,CAAC,EAAkB;QAChC,GAAG,CAAC,EAAE,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC;IACnB,CAAC;IAED,SAAS,KAAK;QACZ,OAAO,CAAC,KAAK,EAAE,CAAC;IAClB,CAAC;IAED,MAAM,MAAM,GAAG,IAAuB,CAAC;IACvC,MAAM,CAAC,GAAG,GAAG,GAAG,CAAC;IACjB,MAAM,CAAC,MAAM,GAAG,MAAM,CAAC;IACvB,MAAM,CAAC,SAAS,GAAG,KAAK,CAAC,SAAS,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IAC/C,MAAM,CAAC,UAAU,GAAG,IAAI,CAAC;IACzB,MAAM,CAAC,KAAK,GAAG,KAAK,CAAC;IAErB,OAAO,MAAM,CAAC;AAChB,CAAC"}
|
package/package.json
CHANGED
|
@@ -109,6 +109,48 @@ describe("createResource", () => {
|
|
|
109
109
|
});
|
|
110
110
|
});
|
|
111
111
|
|
|
112
|
+
describe("resource — execute() sync throw via refetch()", () => {
|
|
113
|
+
it("refetch() captures a synchronous throw from the fetcher", async () => {
|
|
114
|
+
const r = resource(() => Promise.resolve(1), { immediate: false });
|
|
115
|
+
let calls = 0;
|
|
116
|
+
const r2 = resource(
|
|
117
|
+
() => {
|
|
118
|
+
calls++;
|
|
119
|
+
if (calls >= 2) throw new Error("sync in refetch");
|
|
120
|
+
return Promise.resolve(1);
|
|
121
|
+
},
|
|
122
|
+
{ immediate: false },
|
|
123
|
+
);
|
|
124
|
+
r2.refetch(); // first call succeeds
|
|
125
|
+
await vi.waitFor(() => r2.status() === "success");
|
|
126
|
+
r2.refetch(); // second call throws synchronously
|
|
127
|
+
await vi.waitFor(() => r2.status() === "error");
|
|
128
|
+
expect((r2.error() as Error).message).toBe("sync in refetch");
|
|
129
|
+
void r;
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it("refetch() wraps non-Error sync throws in Error", async () => {
|
|
133
|
+
let calls = 0;
|
|
134
|
+
const r = resource(
|
|
135
|
+
() => {
|
|
136
|
+
calls++;
|
|
137
|
+
if (calls >= 2) {
|
|
138
|
+
// eslint-disable-next-line @typescript-eslint/only-throw-error
|
|
139
|
+
throw "plain string sync";
|
|
140
|
+
}
|
|
141
|
+
return Promise.resolve(1);
|
|
142
|
+
},
|
|
143
|
+
{ immediate: false },
|
|
144
|
+
);
|
|
145
|
+
r.refetch();
|
|
146
|
+
await vi.waitFor(() => r.status() === "success");
|
|
147
|
+
r.refetch();
|
|
148
|
+
await vi.waitFor(() => r.status() === "error");
|
|
149
|
+
expect(r.error()).toBeInstanceOf(Error);
|
|
150
|
+
expect((r.error() as Error).message).toBe("plain string sync");
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
|
|
112
154
|
describe("resource — additional cases", () => {
|
|
113
155
|
it("two concurrent refetch() calls — last result wins, stale result is discarded", async () => {
|
|
114
156
|
let resolveFirst!: (v: string) => void;
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
|
+
|
|
3
|
+
import { syncedSignal } from "../signal/synced";
|
|
4
|
+
import { effect } from "../signal/effect";
|
|
5
|
+
|
|
6
|
+
// BroadcastChannel mock
|
|
7
|
+
class MockBroadcastChannel {
|
|
8
|
+
static instances: MockBroadcastChannel[] = [];
|
|
9
|
+
static byChannel: Map<string, MockBroadcastChannel[]> = new Map();
|
|
10
|
+
|
|
11
|
+
onmessage: ((event: MessageEvent) => void) | null = null;
|
|
12
|
+
readonly name: string;
|
|
13
|
+
closed = false;
|
|
14
|
+
|
|
15
|
+
constructor(name: string) {
|
|
16
|
+
this.name = name;
|
|
17
|
+
MockBroadcastChannel.instances.push(this);
|
|
18
|
+
if (!MockBroadcastChannel.byChannel.has(name)) {
|
|
19
|
+
MockBroadcastChannel.byChannel.set(name, []);
|
|
20
|
+
}
|
|
21
|
+
MockBroadcastChannel.byChannel.get(name)!.push(this);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
postMessage(data: unknown) {
|
|
25
|
+
// Deliver to all OTHER instances on same channel
|
|
26
|
+
MockBroadcastChannel.byChannel.get(this.name)?.forEach((ch) => {
|
|
27
|
+
if (ch !== this && ch.onmessage) {
|
|
28
|
+
ch.onmessage(new MessageEvent("message", { data }));
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
close() {
|
|
34
|
+
this.closed = true;
|
|
35
|
+
const list = MockBroadcastChannel.byChannel.get(this.name);
|
|
36
|
+
if (list) {
|
|
37
|
+
const idx = list.indexOf(this);
|
|
38
|
+
if (idx !== -1) list.splice(idx, 1);
|
|
39
|
+
}
|
|
40
|
+
MockBroadcastChannel.instances.splice(
|
|
41
|
+
MockBroadcastChannel.instances.indexOf(this),
|
|
42
|
+
1,
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
beforeEach(() => {
|
|
48
|
+
MockBroadcastChannel.instances = [];
|
|
49
|
+
MockBroadcastChannel.byChannel = new Map();
|
|
50
|
+
vi.stubGlobal("BroadcastChannel", MockBroadcastChannel);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
describe("syncedSignal", () => {
|
|
54
|
+
it("reads initial value", () => {
|
|
55
|
+
const s = syncedSignal("ch", 42);
|
|
56
|
+
expect(s()).toBe(42);
|
|
57
|
+
s.close();
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("set() updates the signal value", () => {
|
|
61
|
+
const s = syncedSignal("ch", 0);
|
|
62
|
+
s.set(10);
|
|
63
|
+
expect(s()).toBe(10);
|
|
64
|
+
s.close();
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("update() applies a transform", () => {
|
|
68
|
+
const s = syncedSignal("ch", 5);
|
|
69
|
+
s.update((v) => v * 2);
|
|
70
|
+
expect(s()).toBe(10);
|
|
71
|
+
s.close();
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("set() posts a JSON message to the channel", () => {
|
|
75
|
+
const s = syncedSignal("ch", 0);
|
|
76
|
+
const postSpy = vi.spyOn(
|
|
77
|
+
MockBroadcastChannel.instances[0],
|
|
78
|
+
"postMessage",
|
|
79
|
+
);
|
|
80
|
+
s.set(99);
|
|
81
|
+
expect(postSpy).toHaveBeenCalledWith(JSON.stringify(99));
|
|
82
|
+
s.close();
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("receiving a message from another tab updates the signal", () => {
|
|
86
|
+
const s = syncedSignal("ch", 0);
|
|
87
|
+
const channel = MockBroadcastChannel.instances[0];
|
|
88
|
+
|
|
89
|
+
channel.onmessage!(new MessageEvent("message", { data: JSON.stringify(7) }));
|
|
90
|
+
expect(s()).toBe(7);
|
|
91
|
+
s.close();
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("does not echo own set() back to itself", () => {
|
|
95
|
+
const s = syncedSignal("ch", 0);
|
|
96
|
+
|
|
97
|
+
// Simulate two instances on the same channel
|
|
98
|
+
const s2 = syncedSignal("ch", 0);
|
|
99
|
+
const received: number[] = [];
|
|
100
|
+
effect(() => { received.push(s2()); });
|
|
101
|
+
|
|
102
|
+
s.set(5);
|
|
103
|
+
// s2 should receive the message because it came from a different instance
|
|
104
|
+
expect(s2()).toBe(5);
|
|
105
|
+
|
|
106
|
+
// s itself should NOT trigger its own onmessage handler (no echo)
|
|
107
|
+
const channel = MockBroadcastChannel.instances[0];
|
|
108
|
+
const receivedBySelf: number[] = [];
|
|
109
|
+
effect(() => { receivedBySelf.push(s()); });
|
|
110
|
+
const beforeCount = receivedBySelf.length;
|
|
111
|
+
// Manually simulate echo — should be ignored because posting=false after set
|
|
112
|
+
channel.onmessage!(new MessageEvent("message", { data: JSON.stringify(5) }));
|
|
113
|
+
// The value stays 5 (no spurious extra notification tested via subscribe count)
|
|
114
|
+
expect(s()).toBe(5);
|
|
115
|
+
expect(receivedBySelf.length).toBe(beforeCount); // no extra effect run (Object.is equality)
|
|
116
|
+
|
|
117
|
+
s.close();
|
|
118
|
+
s2.close();
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it("subscribe fires immediately then on each remote change", () => {
|
|
122
|
+
const s = syncedSignal("ch", 0);
|
|
123
|
+
const channel = MockBroadcastChannel.instances[0];
|
|
124
|
+
const received: number[] = [];
|
|
125
|
+
s.subscribe((v) => received.push(v));
|
|
126
|
+
|
|
127
|
+
expect(received).toEqual([0]);
|
|
128
|
+
|
|
129
|
+
channel.onmessage!(new MessageEvent("message", { data: JSON.stringify(3) }));
|
|
130
|
+
expect(received).toEqual([0, 3]);
|
|
131
|
+
s.close();
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it("close() shuts down the BroadcastChannel", () => {
|
|
135
|
+
const s = syncedSignal("ch", 0);
|
|
136
|
+
const channel = MockBroadcastChannel.instances[0];
|
|
137
|
+
expect(channel.closed).toBe(false);
|
|
138
|
+
s.close();
|
|
139
|
+
expect(channel.closed).toBe(true);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it("has __isSignal marker", () => {
|
|
143
|
+
const s = syncedSignal("ch", 0);
|
|
144
|
+
expect(s.__isSignal).toBe(true);
|
|
145
|
+
s.close();
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it("does not update value when onmessage fires while posting (posting guard)", () => {
|
|
149
|
+
// Use a self-delivering channel that echoes to itself during postMessage.
|
|
150
|
+
// This covers the `if (posting) return;` branch in synced.ts.
|
|
151
|
+
class SelfEchoChannel {
|
|
152
|
+
onmessage: ((e: MessageEvent) => void) | null = null;
|
|
153
|
+
postMessage(data: unknown) {
|
|
154
|
+
this.onmessage?.(new MessageEvent("message", { data }));
|
|
155
|
+
}
|
|
156
|
+
close() {}
|
|
157
|
+
}
|
|
158
|
+
vi.stubGlobal("BroadcastChannel", SelfEchoChannel);
|
|
159
|
+
|
|
160
|
+
const s = syncedSignal("echo-ch", 0);
|
|
161
|
+
const received: number[] = [];
|
|
162
|
+
s.subscribe((v) => received.push(v));
|
|
163
|
+
|
|
164
|
+
// set() triggers postMessage → onmessage fires → posting=true → early return
|
|
165
|
+
// Inner value should not be double-set
|
|
166
|
+
s.set(42);
|
|
167
|
+
expect(s()).toBe(42);
|
|
168
|
+
// Only two notifications: initial (0) + the set (42), no echo re-set
|
|
169
|
+
expect(received).toEqual([0, 42]);
|
|
170
|
+
|
|
171
|
+
s.close();
|
|
172
|
+
});
|
|
173
|
+
});
|
package/src/internal.ts
CHANGED
|
@@ -8,12 +8,14 @@ export {
|
|
|
8
8
|
export {
|
|
9
9
|
signal,
|
|
10
10
|
persistedSignal,
|
|
11
|
+
syncedSignal,
|
|
11
12
|
peek,
|
|
12
13
|
computed,
|
|
13
14
|
batch,
|
|
14
15
|
effect,
|
|
15
16
|
untrack,
|
|
16
17
|
type PersistedSignalOptions,
|
|
18
|
+
type SyncedSignal,
|
|
17
19
|
} from "./signal";
|
|
18
20
|
export { RootComponent } from "./component";
|
|
19
21
|
export {
|
package/src/signal/batch.ts
CHANGED
|
@@ -1,27 +1,25 @@
|
|
|
1
1
|
import type { Effect } from "./effect";
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
const batchEffects: Effect[] = [];
|
|
4
|
+
let batchDepth = 0;
|
|
4
5
|
|
|
5
6
|
export function isBatching(): boolean {
|
|
6
|
-
return
|
|
7
|
+
return batchDepth > 0;
|
|
7
8
|
}
|
|
8
9
|
|
|
9
10
|
export function enqueueEffect(effect: Effect): void {
|
|
10
|
-
|
|
11
|
+
if (!batchEffects.includes(effect)) batchEffects.push(effect);
|
|
11
12
|
}
|
|
12
13
|
|
|
13
14
|
export function batch(fn: () => void) {
|
|
14
|
-
|
|
15
|
-
if (isOuter) {
|
|
16
|
-
batchQueue = new Set();
|
|
17
|
-
}
|
|
15
|
+
batchDepth++;
|
|
18
16
|
try {
|
|
19
17
|
fn();
|
|
20
18
|
} finally {
|
|
21
|
-
if (
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
19
|
+
if (--batchDepth === 0) {
|
|
20
|
+
const n = batchEffects.length;
|
|
21
|
+
for (let i = 0; i < n; i++) batchEffects[i]();
|
|
22
|
+
batchEffects.length = 0;
|
|
25
23
|
}
|
|
26
24
|
}
|
|
27
25
|
}
|
package/src/signal/computed.ts
CHANGED
|
@@ -1,69 +1,72 @@
|
|
|
1
1
|
import type { Computed } from "@praxisjs/shared";
|
|
2
2
|
|
|
3
3
|
import { activeEffect, runEffect, type Effect } from "./effect";
|
|
4
|
+
import { addSub, removeSub, notifySubs, type SubList } from "./signal";
|
|
4
5
|
|
|
5
6
|
type ComputedRecompute = Effect & { __isComputedRecompute: true };
|
|
6
7
|
|
|
7
|
-
|
|
8
|
-
|
|
8
|
+
function isComputedRecompute(eff: Effect): boolean {
|
|
9
|
+
return (eff as Partial<ComputedRecompute>).__isComputedRecompute === true;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function computed<T>(getter: () => T): Computed<T> {
|
|
13
|
+
let cachedValue: T | undefined;
|
|
9
14
|
let dirty = true;
|
|
10
|
-
|
|
15
|
+
|
|
16
|
+
const computedHolder = { subs: null as SubList };
|
|
17
|
+
const leafHolder = { subs: null as SubList };
|
|
11
18
|
let scheduled = false;
|
|
12
19
|
|
|
13
|
-
|
|
14
|
-
|
|
20
|
+
let recompute: ComputedRecompute | null = null;
|
|
21
|
+
|
|
22
|
+
function getRecompute(): ComputedRecompute {
|
|
23
|
+
if (recompute !== null) return recompute;
|
|
24
|
+
recompute = function markDirty(): void {
|
|
25
|
+
if (dirty) return;
|
|
15
26
|
dirty = true;
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
sub(); // propagate dirty synchronously through computed chain
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
if (!scheduled) {
|
|
27
|
+
if (computedHolder.subs !== null) notifySubs(computedHolder.subs, false);
|
|
28
|
+
if (leafHolder.subs !== null && !scheduled) {
|
|
22
29
|
scheduled = true;
|
|
23
30
|
queueMicrotask(() => {
|
|
24
31
|
scheduled = false;
|
|
25
|
-
|
|
26
|
-
if (!(sub as Partial<ComputedRecompute>).__isComputedRecompute) {
|
|
27
|
-
sub(); // notify leaf effects (DOM, @Watch, subscribe)
|
|
28
|
-
}
|
|
29
|
-
}
|
|
32
|
+
if (leafHolder.subs !== null) notifySubs(leafHolder.subs, false);
|
|
30
33
|
});
|
|
31
34
|
}
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
+
} as ComputedRecompute;
|
|
36
|
+
recompute.__isComputedRecompute = true;
|
|
37
|
+
return recompute;
|
|
38
|
+
}
|
|
35
39
|
|
|
36
|
-
function read() {
|
|
37
|
-
if (activeEffect) {
|
|
38
|
-
|
|
40
|
+
function read(): T {
|
|
41
|
+
if (activeEffect !== null) {
|
|
42
|
+
if (isComputedRecompute(activeEffect)) {
|
|
43
|
+
addSub(computedHolder, activeEffect);
|
|
44
|
+
} else {
|
|
45
|
+
addSub(leafHolder, activeEffect);
|
|
46
|
+
}
|
|
39
47
|
}
|
|
40
|
-
|
|
41
48
|
if (dirty) {
|
|
42
49
|
const prevEffect = activeEffect;
|
|
43
|
-
runEffect(
|
|
50
|
+
runEffect(getRecompute());
|
|
44
51
|
try {
|
|
45
|
-
cachedValue =
|
|
52
|
+
cachedValue = getter();
|
|
46
53
|
dirty = false;
|
|
47
54
|
} finally {
|
|
48
55
|
runEffect(prevEffect);
|
|
49
56
|
}
|
|
50
57
|
}
|
|
51
|
-
|
|
52
|
-
return cachedValue;
|
|
58
|
+
return cachedValue as T;
|
|
53
59
|
}
|
|
54
60
|
|
|
55
|
-
|
|
56
|
-
const wrappedEffect = () => {
|
|
57
|
-
fn(read());
|
|
58
|
-
};
|
|
59
|
-
subscribers.add(wrappedEffect);
|
|
60
|
-
wrappedEffect();
|
|
61
|
-
return () => subscribers.delete(wrappedEffect);
|
|
62
|
-
}
|
|
61
|
+
const c = read as Computed<T>;
|
|
63
62
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
63
|
+
c.subscribe = (fn: (value: T) => void) => {
|
|
64
|
+
const wrapped: Effect = () => { fn(read()); };
|
|
65
|
+
addSub(leafHolder, wrapped);
|
|
66
|
+
wrapped();
|
|
67
|
+
return () => { removeSub(leafHolder, wrapped); };
|
|
68
|
+
};
|
|
67
69
|
|
|
68
|
-
|
|
70
|
+
c.__isComputed = true;
|
|
71
|
+
return c;
|
|
69
72
|
}
|