@assistant-ui/tap 0.3.6 → 0.4.2
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 +24 -23
- package/dist/core/ResourceFiber.d.ts +1 -1
- package/dist/core/ResourceFiber.d.ts.map +1 -1
- package/dist/core/ResourceFiber.js +15 -8
- package/dist/core/ResourceFiber.js.map +1 -1
- package/dist/core/commit.d.ts +1 -1
- package/dist/core/commit.d.ts.map +1 -1
- package/dist/core/commit.js +40 -50
- package/dist/core/commit.js.map +1 -1
- package/dist/core/context.d.ts +2 -2
- package/dist/core/context.d.ts.map +1 -1
- package/dist/core/context.js +2 -2
- package/dist/core/context.js.map +1 -1
- package/dist/core/createResource.d.ts +3 -2
- package/dist/core/createResource.d.ts.map +1 -1
- package/dist/core/createResource.js +48 -22
- package/dist/core/createResource.js.map +1 -1
- package/dist/core/env.d.ts +2 -0
- package/dist/core/env.d.ts.map +1 -0
- package/dist/core/env.js +3 -0
- package/dist/core/env.js.map +1 -0
- package/dist/core/execution-context.d.ts +1 -0
- package/dist/core/execution-context.d.ts.map +1 -1
- package/dist/core/execution-context.js +8 -0
- package/dist/core/execution-context.js.map +1 -1
- package/dist/core/resource.d.ts +4 -3
- package/dist/core/resource.d.ts.map +1 -1
- package/dist/core/resource.js.map +1 -1
- package/dist/core/scheduler.d.ts +1 -1
- package/dist/core/scheduler.d.ts.map +1 -1
- package/dist/core/scheduler.js +4 -1
- package/dist/core/scheduler.js.map +1 -1
- package/dist/core/types.d.ts +22 -21
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js +1 -1
- package/dist/core/types.js.map +1 -1
- package/dist/core/withKey.d.ts +3 -0
- package/dist/core/withKey.d.ts.map +1 -0
- package/dist/core/withKey.js +4 -0
- package/dist/core/withKey.js.map +1 -0
- package/dist/hooks/tap-callback.d.ts.map +1 -1
- package/dist/hooks/tap-callback.js +1 -0
- package/dist/hooks/tap-callback.js.map +1 -1
- package/dist/hooks/tap-const.d.ts +2 -0
- package/dist/hooks/tap-const.d.ts.map +1 -0
- package/dist/hooks/tap-const.js +6 -0
- package/dist/hooks/tap-const.js.map +1 -0
- package/dist/hooks/tap-effect-event.d.ts.map +1 -1
- package/dist/hooks/tap-effect-event.js +11 -0
- package/dist/hooks/tap-effect-event.js.map +1 -1
- package/dist/hooks/tap-effect.d.ts.map +1 -1
- package/dist/hooks/tap-effect.js +46 -31
- package/dist/hooks/tap-effect.js.map +1 -1
- package/dist/hooks/tap-inline-resource.d.ts +2 -2
- package/dist/hooks/tap-inline-resource.d.ts.map +1 -1
- package/dist/hooks/tap-memo.d.ts.map +1 -1
- package/dist/hooks/tap-memo.js +9 -1
- package/dist/hooks/tap-memo.js.map +1 -1
- package/dist/hooks/tap-resource.d.ts +3 -3
- package/dist/hooks/tap-resource.d.ts.map +1 -1
- package/dist/hooks/tap-resource.js +17 -9
- package/dist/hooks/tap-resource.js.map +1 -1
- package/dist/hooks/tap-resources.d.ts +2 -10
- package/dist/hooks/tap-resources.d.ts.map +1 -1
- package/dist/hooks/tap-resources.js +74 -43
- package/dist/hooks/tap-resources.js.map +1 -1
- package/dist/hooks/tap-state.d.ts.map +1 -1
- package/dist/hooks/tap-state.js +37 -24
- package/dist/hooks/tap-state.js.map +1 -1
- package/dist/hooks/utils/depsShallowEqual.d.ts.map +1 -0
- package/dist/hooks/utils/depsShallowEqual.js.map +1 -0
- package/dist/hooks/utils/tapHook.d.ts +6 -0
- package/dist/hooks/utils/tapHook.d.ts.map +1 -0
- package/dist/hooks/utils/tapHook.js +24 -0
- package/dist/hooks/utils/tapHook.js.map +1 -0
- package/dist/index.d.ts +5 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -2
- package/dist/index.js.map +1 -1
- package/dist/react/use-resource.d.ts +2 -2
- package/dist/react/use-resource.d.ts.map +1 -1
- package/dist/react/use-resource.js +24 -10
- package/dist/react/use-resource.js.map +1 -1
- package/package.json +10 -3
- package/src/__tests__/basic/resourceHandle.test.ts +4 -4
- package/src/__tests__/basic/tapEffect.basic.test.ts +3 -2
- package/src/__tests__/basic/tapResources.basic.test.ts +84 -64
- package/src/__tests__/basic/tapState.basic.test.ts +8 -8
- package/src/__tests__/errors/errors.effect-errors.test.ts +8 -3
- package/src/__tests__/lifecycle/lifecycle.dependencies.test.ts +3 -2
- package/src/__tests__/lifecycle/lifecycle.mount-unmount.test.ts +2 -2
- package/src/__tests__/react/concurrent-mode.test.tsx +243 -0
- package/src/__tests__/strictmode/react-strictmode-behavior.test.tsx +709 -0
- package/src/__tests__/strictmode/react-strictmode-rerender-sources.test.tsx +392 -0
- package/src/__tests__/strictmode/strictmode.test.ts +274 -0
- package/src/__tests__/strictmode/tap-strictmode-rerender-sources.test.ts +723 -0
- package/src/__tests__/test-utils.ts +8 -6
- package/src/core/ResourceFiber.ts +21 -11
- package/src/core/commit.ts +37 -57
- package/src/core/context.ts +2 -2
- package/src/core/createResource.ts +64 -25
- package/src/core/env.ts +3 -0
- package/src/core/execution-context.ts +9 -0
- package/src/core/resource.ts +9 -3
- package/src/core/scheduler.ts +4 -1
- package/src/core/types.ts +25 -26
- package/src/core/withKey.ts +8 -0
- package/src/hooks/tap-callback.ts +1 -0
- package/src/hooks/tap-const.ts +6 -0
- package/src/hooks/tap-effect-event.ts +15 -0
- package/src/hooks/tap-effect.ts +51 -38
- package/src/hooks/tap-inline-resource.ts +2 -2
- package/src/hooks/tap-memo.ts +10 -1
- package/src/hooks/tap-resource.ts +24 -20
- package/src/hooks/tap-resources.ts +86 -63
- package/src/hooks/tap-state.ts +49 -26
- package/src/hooks/utils/tapHook.ts +35 -0
- package/src/index.ts +8 -3
- package/src/react/use-resource.ts +27 -16
- package/dist/hooks/depsShallowEqual.d.ts.map +0 -1
- package/dist/hooks/depsShallowEqual.js.map +0 -1
- /package/dist/hooks/{depsShallowEqual.d.ts → utils/depsShallowEqual.d.ts} +0 -0
- /package/dist/hooks/{depsShallowEqual.js → utils/depsShallowEqual.js} +0 -0
- /package/src/hooks/{depsShallowEqual.ts → utils/depsShallowEqual.ts} +0 -0
|
@@ -18,7 +18,9 @@ import { tapState } from "../hooks/tap-state";
|
|
|
18
18
|
* Sets up a rerender callback that automatically re-renders when state changes.
|
|
19
19
|
*/
|
|
20
20
|
export function createTestResource<R, P>(fn: (props: P) => R) {
|
|
21
|
-
const rerenderCallback = () => {
|
|
21
|
+
const rerenderCallback = (callback: () => boolean) => {
|
|
22
|
+
if (!callback()) return;
|
|
23
|
+
|
|
22
24
|
// Re-render when state changes
|
|
23
25
|
if (activeResources.has(fiber)) {
|
|
24
26
|
const lastProps = propsMap.get(fiber);
|
|
@@ -59,7 +61,7 @@ export function renderTest<R, P>(fiber: ResourceFiber<R, P>, props: P): R {
|
|
|
59
61
|
|
|
60
62
|
// Return the committed state from the result
|
|
61
63
|
// This accounts for any re-renders that happened during commit
|
|
62
|
-
return result.
|
|
64
|
+
return result.output;
|
|
63
65
|
}
|
|
64
66
|
|
|
65
67
|
/**
|
|
@@ -84,14 +86,14 @@ export function cleanupAllResources() {
|
|
|
84
86
|
* Gets the current committed state of a resource fiber.
|
|
85
87
|
* Returns the state from the last render/commit cycle.
|
|
86
88
|
*/
|
|
87
|
-
export function
|
|
89
|
+
export function getCommittedOutput<R, P>(fiber: ResourceFiber<R, P>): R {
|
|
88
90
|
const lastResult = lastRenderResultMap.get(fiber);
|
|
89
91
|
if (!lastResult) {
|
|
90
92
|
throw new Error(
|
|
91
93
|
"No render result found for fiber. Make sure to call renderResource first.",
|
|
92
94
|
);
|
|
93
95
|
}
|
|
94
|
-
return lastResult.
|
|
96
|
+
return lastResult.output;
|
|
95
97
|
}
|
|
96
98
|
|
|
97
99
|
// ============================================================================
|
|
@@ -113,7 +115,7 @@ export class TestSubscriber<T> {
|
|
|
113
115
|
const lastProps = propsMap.get(fiber) ?? undefined;
|
|
114
116
|
const initialResult = renderResourceFiber(fiber, lastProps as any);
|
|
115
117
|
commitResourceFiber(fiber, initialResult);
|
|
116
|
-
this.lastState = initialResult.
|
|
118
|
+
this.lastState = initialResult.output;
|
|
117
119
|
lastRenderResultMap.set(fiber, initialResult);
|
|
118
120
|
activeResources.add(fiber);
|
|
119
121
|
}
|
|
@@ -146,7 +148,7 @@ export class TestResourceManager<R, P> {
|
|
|
146
148
|
const result = renderResourceFiber(this.fiber, props);
|
|
147
149
|
commitResourceFiber(this.fiber, result);
|
|
148
150
|
lastRenderResultMap.set(this.fiber, result);
|
|
149
|
-
return result.
|
|
151
|
+
return result.output;
|
|
150
152
|
}
|
|
151
153
|
|
|
152
154
|
cleanup() {
|
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
import { ResourceFiber, RenderResult, Resource } from "./types";
|
|
2
2
|
import { commitRender, cleanupAllEffects } from "./commit";
|
|
3
|
-
import { withResourceFiber } from "./execution-context";
|
|
3
|
+
import { getDevStrictMode, withResourceFiber } from "./execution-context";
|
|
4
4
|
import { callResourceFn } from "./callResourceFn";
|
|
5
|
+
import { isDevelopment } from "./env";
|
|
5
6
|
|
|
6
7
|
export function createResourceFiber<R, P>(
|
|
7
|
-
|
|
8
|
-
|
|
8
|
+
type: Resource<R, P>,
|
|
9
|
+
dispatchUpdate: (callback: () => boolean) => void,
|
|
10
|
+
strictMode: "root" | "child" | null = getDevStrictMode(false),
|
|
9
11
|
): ResourceFiber<R, P> {
|
|
10
12
|
return {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
+
type,
|
|
14
|
+
dispatchUpdate,
|
|
15
|
+
devStrictMode: strictMode,
|
|
13
16
|
cells: [],
|
|
14
17
|
currentIndex: 0,
|
|
15
18
|
renderContext: undefined,
|
|
@@ -20,7 +23,9 @@ export function createResourceFiber<R, P>(
|
|
|
20
23
|
}
|
|
21
24
|
|
|
22
25
|
export function unmountResourceFiber<R, P>(fiber: ResourceFiber<R, P>): void {
|
|
23
|
-
|
|
26
|
+
if (!fiber.isMounted)
|
|
27
|
+
throw new Error("Tried to unmount a fiber that is already unmounted");
|
|
28
|
+
|
|
24
29
|
fiber.isMounted = false;
|
|
25
30
|
cleanupAllEffects(fiber);
|
|
26
31
|
}
|
|
@@ -29,16 +34,16 @@ export function renderResourceFiber<R, P>(
|
|
|
29
34
|
fiber: ResourceFiber<R, P>,
|
|
30
35
|
props: P,
|
|
31
36
|
): RenderResult {
|
|
32
|
-
const result
|
|
37
|
+
const result = {
|
|
33
38
|
commitTasks: [],
|
|
34
39
|
props,
|
|
35
|
-
|
|
40
|
+
output: undefined as R | undefined,
|
|
36
41
|
};
|
|
37
42
|
|
|
38
43
|
withResourceFiber(fiber, () => {
|
|
39
44
|
fiber.renderContext = result;
|
|
40
45
|
try {
|
|
41
|
-
result.
|
|
46
|
+
result.output = callResourceFn(fiber.type, props);
|
|
42
47
|
} finally {
|
|
43
48
|
fiber.renderContext = undefined;
|
|
44
49
|
}
|
|
@@ -52,7 +57,12 @@ export function commitResourceFiber<R, P>(
|
|
|
52
57
|
result: RenderResult,
|
|
53
58
|
): void {
|
|
54
59
|
fiber.isMounted = true;
|
|
55
|
-
fiber.isNeverMounted = false;
|
|
56
60
|
|
|
57
|
-
|
|
61
|
+
if (isDevelopment && fiber.isNeverMounted && fiber.devStrictMode === "root") {
|
|
62
|
+
commitRender(result);
|
|
63
|
+
cleanupAllEffects(fiber);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
fiber.isNeverMounted = false;
|
|
67
|
+
commitRender(result);
|
|
58
68
|
}
|
package/src/core/commit.ts
CHANGED
|
@@ -1,73 +1,53 @@
|
|
|
1
1
|
import { ResourceFiber, RenderResult } from "./types";
|
|
2
2
|
|
|
3
|
-
export function commitRender
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
)
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
if (effectCell.type !== "effect") {
|
|
12
|
-
throw new Error("Cannot find effect cell");
|
|
3
|
+
export function commitRender(renderResult: RenderResult): void {
|
|
4
|
+
const errors: unknown[] = [];
|
|
5
|
+
|
|
6
|
+
for (const task of renderResult.commitTasks) {
|
|
7
|
+
try {
|
|
8
|
+
task();
|
|
9
|
+
} catch (error) {
|
|
10
|
+
errors.push(error);
|
|
13
11
|
}
|
|
12
|
+
}
|
|
14
13
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
14
|
+
if (errors.length > 0) {
|
|
15
|
+
if (errors.length === 1) {
|
|
16
|
+
throw errors[0];
|
|
17
|
+
} else {
|
|
18
|
+
for (const error of errors) {
|
|
19
|
+
console.error(error);
|
|
20
|
+
}
|
|
21
|
+
throw new AggregateError(errors, "Errors during commit");
|
|
22
22
|
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
23
25
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
"tapEffect called with and without dependencies across re-renders",
|
|
30
|
-
);
|
|
31
|
-
}
|
|
26
|
+
export function cleanupAllEffects<R, P>(executionContext: ResourceFiber<R, P>) {
|
|
27
|
+
const errors: unknown[] = [];
|
|
28
|
+
for (const cell of executionContext.cells) {
|
|
29
|
+
if (cell?.type === "effect") {
|
|
30
|
+
cell.deps = null; // Reset deps so effect runs again on next mount
|
|
32
31
|
|
|
32
|
+
if (cell.cleanup) {
|
|
33
33
|
try {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
34
|
+
cell.cleanup?.();
|
|
35
|
+
} catch (e) {
|
|
36
|
+
errors.push(e);
|
|
37
37
|
} finally {
|
|
38
|
-
|
|
38
|
+
cell.cleanup = undefined;
|
|
39
39
|
}
|
|
40
40
|
}
|
|
41
|
-
const cleanup = task.effect();
|
|
42
|
-
|
|
43
|
-
if (cleanup !== undefined && typeof cleanup !== "function") {
|
|
44
|
-
throw new Error(
|
|
45
|
-
"An effect function must either return a cleanup function or nothing. " +
|
|
46
|
-
`Received: ${typeof cleanup}`,
|
|
47
|
-
);
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
effectCell.mounted = true;
|
|
51
|
-
effectCell.cleanup = typeof cleanup === "function" ? cleanup : undefined;
|
|
52
|
-
effectCell.deps = task.deps;
|
|
53
41
|
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
const cell = executionContext.cells[i];
|
|
62
|
-
if (cell?.type === "effect" && cell.mounted && cell.cleanup) {
|
|
63
|
-
try {
|
|
64
|
-
cell.cleanup();
|
|
65
|
-
} catch (e) {
|
|
66
|
-
if (firstError == null) firstError = e;
|
|
67
|
-
} finally {
|
|
68
|
-
cell.mounted = false;
|
|
42
|
+
}
|
|
43
|
+
if (errors.length > 0) {
|
|
44
|
+
if (errors.length === 1) {
|
|
45
|
+
throw errors[0];
|
|
46
|
+
} else {
|
|
47
|
+
for (const error of errors) {
|
|
48
|
+
console.error(error);
|
|
69
49
|
}
|
|
50
|
+
throw new AggregateError(errors, "Errors during cleanup");
|
|
70
51
|
}
|
|
71
52
|
}
|
|
72
|
-
if (firstError != null) throw firstError;
|
|
73
53
|
}
|
package/src/core/context.ts
CHANGED
|
@@ -3,7 +3,7 @@ type Context<T> = {
|
|
|
3
3
|
[contextValue]: T;
|
|
4
4
|
};
|
|
5
5
|
|
|
6
|
-
export const
|
|
6
|
+
export const createResourceContext = <T>(defaultValue: T): Context<T> => {
|
|
7
7
|
return {
|
|
8
8
|
[contextValue]: defaultValue,
|
|
9
9
|
};
|
|
@@ -23,6 +23,6 @@ export const withContextProvider = <T, TResult>(
|
|
|
23
23
|
}
|
|
24
24
|
};
|
|
25
25
|
|
|
26
|
-
export const
|
|
26
|
+
export const tap = <T>(context: Context<T>) => {
|
|
27
27
|
return context[contextValue];
|
|
28
28
|
};
|
|
@@ -5,19 +5,22 @@ import {
|
|
|
5
5
|
renderResourceFiber,
|
|
6
6
|
commitResourceFiber,
|
|
7
7
|
} from "./ResourceFiber";
|
|
8
|
-
import {
|
|
8
|
+
import { flushResourcesSync, UpdateScheduler } from "./scheduler";
|
|
9
9
|
import { tapRef } from "../hooks/tap-ref";
|
|
10
10
|
import { tapState } from "../hooks/tap-state";
|
|
11
11
|
import { tapMemo } from "../hooks/tap-memo";
|
|
12
12
|
import { tapEffect } from "../hooks/tap-effect";
|
|
13
13
|
import { resource } from "./resource";
|
|
14
14
|
import { tapResource } from "../hooks/tap-resource";
|
|
15
|
+
import { tapConst } from "../hooks/tap-const";
|
|
16
|
+
import { getDevStrictMode } from "./execution-context";
|
|
17
|
+
import { isDevelopment } from "./env";
|
|
15
18
|
|
|
16
19
|
export namespace createResource {
|
|
17
20
|
export type Unsubscribe = () => void;
|
|
18
21
|
|
|
19
22
|
export interface Handle<R, P> {
|
|
20
|
-
|
|
23
|
+
getValue(): R;
|
|
21
24
|
subscribe(callback: () => void): Unsubscribe;
|
|
22
25
|
render(element: ResourceElement<R, P>): void;
|
|
23
26
|
unmount(): void;
|
|
@@ -26,40 +29,44 @@ export namespace createResource {
|
|
|
26
29
|
|
|
27
30
|
const HandleWrapperResource = resource(
|
|
28
31
|
<R, P>(state: {
|
|
29
|
-
|
|
32
|
+
elementRef: {
|
|
33
|
+
current: ResourceElement<R, P>;
|
|
34
|
+
};
|
|
30
35
|
onRender: (changed: boolean) => boolean;
|
|
31
36
|
onUnmount: () => void;
|
|
32
37
|
}): createResource.Handle<R, P> => {
|
|
33
|
-
const [, setElement] = tapState(state.
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
const
|
|
38
|
+
const [, setElement] = tapState(state.elementRef.current);
|
|
39
|
+
const output = tapResource(state.elementRef.current);
|
|
40
|
+
|
|
41
|
+
const subscribers = tapConst(() => new Set<() => void>(), []);
|
|
42
|
+
const valueRef = tapRef(output);
|
|
37
43
|
|
|
38
44
|
tapEffect(() => {
|
|
39
|
-
if (
|
|
40
|
-
valueRef.current =
|
|
45
|
+
if (output !== valueRef.current) {
|
|
46
|
+
valueRef.current = output;
|
|
41
47
|
subscribers.forEach((callback) => callback());
|
|
42
48
|
}
|
|
43
49
|
});
|
|
44
50
|
|
|
45
51
|
const handle = tapMemo(
|
|
46
52
|
() => ({
|
|
47
|
-
|
|
53
|
+
getValue: () => valueRef.current,
|
|
48
54
|
subscribe: (callback: () => void) => {
|
|
49
55
|
subscribers.add(callback);
|
|
50
56
|
return () => subscribers.delete(callback);
|
|
51
57
|
},
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
state.
|
|
58
|
+
|
|
59
|
+
render: (el: ResourceElement<R, P>) => {
|
|
60
|
+
const changed = state.elementRef.current !== el;
|
|
61
|
+
state.elementRef.current = el;
|
|
55
62
|
|
|
56
63
|
if (state.onRender(changed)) {
|
|
57
|
-
setElement(
|
|
64
|
+
setElement(el);
|
|
58
65
|
}
|
|
59
66
|
},
|
|
60
67
|
unmount: state.onUnmount,
|
|
61
68
|
}),
|
|
62
|
-
[],
|
|
69
|
+
[state],
|
|
63
70
|
);
|
|
64
71
|
|
|
65
72
|
return handle;
|
|
@@ -68,24 +75,43 @@ const HandleWrapperResource = resource(
|
|
|
68
75
|
|
|
69
76
|
export const createResource = <R, P>(
|
|
70
77
|
element: ResourceElement<R, P>,
|
|
71
|
-
{
|
|
78
|
+
{
|
|
79
|
+
mount = true,
|
|
80
|
+
devStrictMode = false,
|
|
81
|
+
}: { mount?: boolean; devStrictMode?: boolean } = {},
|
|
72
82
|
): createResource.Handle<R, P> => {
|
|
73
83
|
let isMounted = mount;
|
|
74
84
|
let render: RenderResult;
|
|
75
85
|
const props = {
|
|
76
|
-
element,
|
|
86
|
+
elementRef: { current: element },
|
|
77
87
|
onRender: (changed: boolean) => {
|
|
78
88
|
if (isMounted) return changed;
|
|
79
89
|
isMounted = true;
|
|
80
90
|
|
|
81
|
-
|
|
91
|
+
if (
|
|
92
|
+
isDevelopment &&
|
|
93
|
+
fiber.isNeverMounted &&
|
|
94
|
+
fiber.devStrictMode === "child"
|
|
95
|
+
) {
|
|
82
96
|
if (changed) {
|
|
83
97
|
render = renderResourceFiber(fiber, props);
|
|
84
98
|
}
|
|
99
|
+
commitResourceFiber(fiber, render);
|
|
100
|
+
} else {
|
|
101
|
+
flushResourcesSync(() => {
|
|
102
|
+
if (changed) {
|
|
103
|
+
// In strict mode, render twice to detect side effects
|
|
104
|
+
if (isDevelopment && fiber.devStrictMode === "root") {
|
|
105
|
+
void renderResourceFiber(fiber, props);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
render = renderResourceFiber(fiber, props);
|
|
109
|
+
}
|
|
85
110
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
111
|
+
if (scheduler.isDirty) return;
|
|
112
|
+
commitResourceFiber(fiber, render!);
|
|
113
|
+
});
|
|
114
|
+
}
|
|
89
115
|
|
|
90
116
|
return false;
|
|
91
117
|
},
|
|
@@ -98,19 +124,32 @@ export const createResource = <R, P>(
|
|
|
98
124
|
};
|
|
99
125
|
|
|
100
126
|
const scheduler = new UpdateScheduler(() => {
|
|
127
|
+
// In strict mode, render twice to detect side effects
|
|
128
|
+
if (
|
|
129
|
+
isDevelopment &&
|
|
130
|
+
(fiber.devStrictMode === "root" ||
|
|
131
|
+
(fiber.devStrictMode && !fiber.isFirstRender))
|
|
132
|
+
) {
|
|
133
|
+
void renderResourceFiber(fiber, props);
|
|
134
|
+
}
|
|
135
|
+
|
|
101
136
|
render = renderResourceFiber(fiber, props);
|
|
102
137
|
|
|
103
138
|
if (scheduler.isDirty || !isMounted) return;
|
|
104
139
|
commitResourceFiber(fiber, render);
|
|
105
140
|
});
|
|
106
141
|
|
|
107
|
-
const fiber = createResourceFiber(
|
|
108
|
-
|
|
142
|
+
const fiber = createResourceFiber(
|
|
143
|
+
HandleWrapperResource<R, P>,
|
|
144
|
+
(callback) => {
|
|
145
|
+
if (callback()) scheduler.markDirty();
|
|
146
|
+
},
|
|
147
|
+
getDevStrictMode(devStrictMode),
|
|
109
148
|
);
|
|
110
149
|
|
|
111
|
-
|
|
150
|
+
flushResourcesSync(() => {
|
|
112
151
|
scheduler.markDirty();
|
|
113
152
|
});
|
|
114
153
|
|
|
115
|
-
return render!.
|
|
154
|
+
return render!.output;
|
|
116
155
|
};
|
package/src/core/env.ts
ADDED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isDevelopment } from "./env";
|
|
1
2
|
import { ResourceFiber } from "./types";
|
|
2
3
|
|
|
3
4
|
let currentResourceFiber: ResourceFiber<any, any> | null = null;
|
|
@@ -32,3 +33,11 @@ export function getCurrentResourceFiber(): ResourceFiber<unknown, unknown> {
|
|
|
32
33
|
}
|
|
33
34
|
return currentResourceFiber;
|
|
34
35
|
}
|
|
36
|
+
|
|
37
|
+
export function getDevStrictMode(enable: boolean) {
|
|
38
|
+
if (!isDevelopment) return null;
|
|
39
|
+
if (currentResourceFiber?.devStrictMode)
|
|
40
|
+
return currentResourceFiber.isFirstRender ? "child" : "root";
|
|
41
|
+
|
|
42
|
+
return enable ? "root" : null;
|
|
43
|
+
}
|
package/src/core/resource.ts
CHANGED
|
@@ -1,7 +1,13 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { ResourceElement } from "./types";
|
|
2
2
|
import { fnSymbol } from "./callResourceFn";
|
|
3
|
-
|
|
4
|
-
export function resource<R
|
|
3
|
+
|
|
4
|
+
export function resource<R>(fn: () => R): () => ResourceElement<R, undefined>;
|
|
5
|
+
export function resource<R, P>(
|
|
6
|
+
fn: (props: P) => R,
|
|
7
|
+
): (props: P) => ResourceElement<R, P>;
|
|
8
|
+
export function resource<R, P>(
|
|
9
|
+
fn: (props?: P) => R,
|
|
10
|
+
): (props?: P) => ResourceElement<R, P | undefined>;
|
|
5
11
|
export function resource<R, P = undefined>(fn: (props: P) => R) {
|
|
6
12
|
const type = (props?: P) => {
|
|
7
13
|
return {
|
package/src/core/scheduler.ts
CHANGED
|
@@ -68,6 +68,9 @@ const flushScheduled = () => {
|
|
|
68
68
|
if (errors.length === 1) {
|
|
69
69
|
throw errors[0];
|
|
70
70
|
} else {
|
|
71
|
+
for (const error of errors) {
|
|
72
|
+
console.error(error);
|
|
73
|
+
}
|
|
71
74
|
throw new AggregateError(errors, "Errors occurred during flushSync");
|
|
72
75
|
}
|
|
73
76
|
}
|
|
@@ -77,7 +80,7 @@ const flushScheduled = () => {
|
|
|
77
80
|
}
|
|
78
81
|
};
|
|
79
82
|
|
|
80
|
-
export const
|
|
83
|
+
export const flushResourcesSync = <T>(callback: () => T): T => {
|
|
81
84
|
const prev = flushState;
|
|
82
85
|
flushState = {
|
|
83
86
|
schedulers: new Set([]),
|
package/src/core/types.ts
CHANGED
|
@@ -1,52 +1,51 @@
|
|
|
1
1
|
import type { tapEffect } from "../hooks/tap-effect";
|
|
2
2
|
import type { tapState } from "../hooks/tap-state";
|
|
3
|
-
import { fnSymbol } from "./callResourceFn";
|
|
3
|
+
import type { fnSymbol } from "./callResourceFn";
|
|
4
4
|
|
|
5
5
|
export type ResourceElement<R, P = any> = {
|
|
6
|
-
type: Resource<R, P> & { [fnSymbol]: (props: P) => R };
|
|
7
|
-
props: P;
|
|
6
|
+
readonly type: Resource<R, P> & { [fnSymbol]: (props: P) => R };
|
|
7
|
+
readonly props: P;
|
|
8
|
+
readonly key?: string | number;
|
|
8
9
|
};
|
|
9
10
|
|
|
10
|
-
type
|
|
11
|
-
export type
|
|
12
|
-
...args: ResourceArgs<P>
|
|
13
|
-
) => ResourceElement<R, P>;
|
|
11
|
+
export type Resource<R, P> = (props: P) => ResourceElement<R, P>;
|
|
12
|
+
export type ContravariantResource<R, P> = (props: P) => ResourceElement<R>;
|
|
14
13
|
|
|
15
|
-
export type
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
14
|
+
export type ExtractResourceReturnType<T> =
|
|
15
|
+
T extends ResourceElement<infer R, any>
|
|
16
|
+
? R
|
|
17
|
+
: T extends Resource<infer R, any>
|
|
18
|
+
? R
|
|
19
|
+
: never;
|
|
21
20
|
|
|
22
21
|
export type Cell =
|
|
23
22
|
| {
|
|
24
|
-
type: "state";
|
|
23
|
+
readonly type: "state";
|
|
25
24
|
value: any;
|
|
26
25
|
set: (updater: tapState.StateUpdater<any>) => void;
|
|
27
26
|
}
|
|
28
27
|
| {
|
|
29
|
-
type: "effect";
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
deps?: readonly unknown[] | undefined;
|
|
28
|
+
readonly type: "effect";
|
|
29
|
+
cleanup: tapEffect.Destructor | void;
|
|
30
|
+
deps: readonly unknown[] | null | undefined;
|
|
33
31
|
};
|
|
34
32
|
|
|
35
33
|
export interface EffectTask {
|
|
36
|
-
effect: tapEffect.EffectCallback;
|
|
37
|
-
deps
|
|
38
|
-
|
|
34
|
+
readonly effect: tapEffect.EffectCallback;
|
|
35
|
+
readonly deps: readonly unknown[] | undefined;
|
|
36
|
+
readonly cell: Cell & { type: "effect" };
|
|
39
37
|
}
|
|
40
38
|
|
|
41
39
|
export interface RenderResult {
|
|
42
|
-
|
|
43
|
-
props: any;
|
|
44
|
-
commitTasks:
|
|
40
|
+
readonly output: any;
|
|
41
|
+
readonly props: any;
|
|
42
|
+
readonly commitTasks: (() => void)[];
|
|
45
43
|
}
|
|
46
44
|
|
|
47
45
|
export interface ResourceFiber<R, P> {
|
|
48
|
-
readonly
|
|
49
|
-
readonly
|
|
46
|
+
readonly dispatchUpdate: (callback: () => boolean) => void;
|
|
47
|
+
readonly type: Resource<R, P>;
|
|
48
|
+
readonly devStrictMode: "root" | "child" | null;
|
|
50
49
|
|
|
51
50
|
cells: Cell[];
|
|
52
51
|
currentIndex: number;
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { tapRef } from "./tap-ref";
|
|
2
2
|
import { tapEffect } from "./tap-effect";
|
|
3
|
+
import { isDevelopment } from "../core/env";
|
|
4
|
+
import { tapCallback } from "./tap-callback";
|
|
5
|
+
import { getCurrentResourceFiber } from "../core/execution-context";
|
|
3
6
|
|
|
4
7
|
/**
|
|
5
8
|
* Creates a stable function reference that always calls the most recent version of the callback.
|
|
@@ -25,5 +28,17 @@ export function tapEffectEvent<T extends (...args: any[]) => any>(
|
|
|
25
28
|
callbackRef.current = callback;
|
|
26
29
|
});
|
|
27
30
|
|
|
31
|
+
if (isDevelopment) {
|
|
32
|
+
const fiber = getCurrentResourceFiber();
|
|
33
|
+
return tapCallback(
|
|
34
|
+
((...args: Parameters<T>) => {
|
|
35
|
+
if (fiber.renderContext)
|
|
36
|
+
throw new Error("tapEffectEvent cannot be called during render");
|
|
37
|
+
return callbackRef.current(...args);
|
|
38
|
+
}) as T,
|
|
39
|
+
[fiber],
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
28
43
|
return callbackRef.current;
|
|
29
44
|
}
|