@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.
Files changed (124) hide show
  1. package/README.md +24 -23
  2. package/dist/core/ResourceFiber.d.ts +1 -1
  3. package/dist/core/ResourceFiber.d.ts.map +1 -1
  4. package/dist/core/ResourceFiber.js +15 -8
  5. package/dist/core/ResourceFiber.js.map +1 -1
  6. package/dist/core/commit.d.ts +1 -1
  7. package/dist/core/commit.d.ts.map +1 -1
  8. package/dist/core/commit.js +40 -50
  9. package/dist/core/commit.js.map +1 -1
  10. package/dist/core/context.d.ts +2 -2
  11. package/dist/core/context.d.ts.map +1 -1
  12. package/dist/core/context.js +2 -2
  13. package/dist/core/context.js.map +1 -1
  14. package/dist/core/createResource.d.ts +3 -2
  15. package/dist/core/createResource.d.ts.map +1 -1
  16. package/dist/core/createResource.js +48 -22
  17. package/dist/core/createResource.js.map +1 -1
  18. package/dist/core/env.d.ts +2 -0
  19. package/dist/core/env.d.ts.map +1 -0
  20. package/dist/core/env.js +3 -0
  21. package/dist/core/env.js.map +1 -0
  22. package/dist/core/execution-context.d.ts +1 -0
  23. package/dist/core/execution-context.d.ts.map +1 -1
  24. package/dist/core/execution-context.js +8 -0
  25. package/dist/core/execution-context.js.map +1 -1
  26. package/dist/core/resource.d.ts +4 -3
  27. package/dist/core/resource.d.ts.map +1 -1
  28. package/dist/core/resource.js.map +1 -1
  29. package/dist/core/scheduler.d.ts +1 -1
  30. package/dist/core/scheduler.d.ts.map +1 -1
  31. package/dist/core/scheduler.js +4 -1
  32. package/dist/core/scheduler.js.map +1 -1
  33. package/dist/core/types.d.ts +22 -21
  34. package/dist/core/types.d.ts.map +1 -1
  35. package/dist/core/types.js +1 -1
  36. package/dist/core/types.js.map +1 -1
  37. package/dist/core/withKey.d.ts +3 -0
  38. package/dist/core/withKey.d.ts.map +1 -0
  39. package/dist/core/withKey.js +4 -0
  40. package/dist/core/withKey.js.map +1 -0
  41. package/dist/hooks/tap-callback.d.ts.map +1 -1
  42. package/dist/hooks/tap-callback.js +1 -0
  43. package/dist/hooks/tap-callback.js.map +1 -1
  44. package/dist/hooks/tap-const.d.ts +2 -0
  45. package/dist/hooks/tap-const.d.ts.map +1 -0
  46. package/dist/hooks/tap-const.js +6 -0
  47. package/dist/hooks/tap-const.js.map +1 -0
  48. package/dist/hooks/tap-effect-event.d.ts.map +1 -1
  49. package/dist/hooks/tap-effect-event.js +11 -0
  50. package/dist/hooks/tap-effect-event.js.map +1 -1
  51. package/dist/hooks/tap-effect.d.ts.map +1 -1
  52. package/dist/hooks/tap-effect.js +46 -31
  53. package/dist/hooks/tap-effect.js.map +1 -1
  54. package/dist/hooks/tap-inline-resource.d.ts +2 -2
  55. package/dist/hooks/tap-inline-resource.d.ts.map +1 -1
  56. package/dist/hooks/tap-memo.d.ts.map +1 -1
  57. package/dist/hooks/tap-memo.js +9 -1
  58. package/dist/hooks/tap-memo.js.map +1 -1
  59. package/dist/hooks/tap-resource.d.ts +3 -3
  60. package/dist/hooks/tap-resource.d.ts.map +1 -1
  61. package/dist/hooks/tap-resource.js +17 -9
  62. package/dist/hooks/tap-resource.js.map +1 -1
  63. package/dist/hooks/tap-resources.d.ts +2 -10
  64. package/dist/hooks/tap-resources.d.ts.map +1 -1
  65. package/dist/hooks/tap-resources.js +74 -43
  66. package/dist/hooks/tap-resources.js.map +1 -1
  67. package/dist/hooks/tap-state.d.ts.map +1 -1
  68. package/dist/hooks/tap-state.js +37 -24
  69. package/dist/hooks/tap-state.js.map +1 -1
  70. package/dist/hooks/utils/depsShallowEqual.d.ts.map +1 -0
  71. package/dist/hooks/utils/depsShallowEqual.js.map +1 -0
  72. package/dist/hooks/utils/tapHook.d.ts +6 -0
  73. package/dist/hooks/utils/tapHook.d.ts.map +1 -0
  74. package/dist/hooks/utils/tapHook.js +24 -0
  75. package/dist/hooks/utils/tapHook.js.map +1 -0
  76. package/dist/index.d.ts +5 -3
  77. package/dist/index.d.ts.map +1 -1
  78. package/dist/index.js +4 -2
  79. package/dist/index.js.map +1 -1
  80. package/dist/react/use-resource.d.ts +2 -2
  81. package/dist/react/use-resource.d.ts.map +1 -1
  82. package/dist/react/use-resource.js +24 -10
  83. package/dist/react/use-resource.js.map +1 -1
  84. package/package.json +10 -3
  85. package/src/__tests__/basic/resourceHandle.test.ts +4 -4
  86. package/src/__tests__/basic/tapEffect.basic.test.ts +3 -2
  87. package/src/__tests__/basic/tapResources.basic.test.ts +84 -64
  88. package/src/__tests__/basic/tapState.basic.test.ts +8 -8
  89. package/src/__tests__/errors/errors.effect-errors.test.ts +8 -3
  90. package/src/__tests__/lifecycle/lifecycle.dependencies.test.ts +3 -2
  91. package/src/__tests__/lifecycle/lifecycle.mount-unmount.test.ts +2 -2
  92. package/src/__tests__/react/concurrent-mode.test.tsx +243 -0
  93. package/src/__tests__/strictmode/react-strictmode-behavior.test.tsx +709 -0
  94. package/src/__tests__/strictmode/react-strictmode-rerender-sources.test.tsx +392 -0
  95. package/src/__tests__/strictmode/strictmode.test.ts +274 -0
  96. package/src/__tests__/strictmode/tap-strictmode-rerender-sources.test.ts +723 -0
  97. package/src/__tests__/test-utils.ts +8 -6
  98. package/src/core/ResourceFiber.ts +21 -11
  99. package/src/core/commit.ts +37 -57
  100. package/src/core/context.ts +2 -2
  101. package/src/core/createResource.ts +64 -25
  102. package/src/core/env.ts +3 -0
  103. package/src/core/execution-context.ts +9 -0
  104. package/src/core/resource.ts +9 -3
  105. package/src/core/scheduler.ts +4 -1
  106. package/src/core/types.ts +25 -26
  107. package/src/core/withKey.ts +8 -0
  108. package/src/hooks/tap-callback.ts +1 -0
  109. package/src/hooks/tap-const.ts +6 -0
  110. package/src/hooks/tap-effect-event.ts +15 -0
  111. package/src/hooks/tap-effect.ts +51 -38
  112. package/src/hooks/tap-inline-resource.ts +2 -2
  113. package/src/hooks/tap-memo.ts +10 -1
  114. package/src/hooks/tap-resource.ts +24 -20
  115. package/src/hooks/tap-resources.ts +86 -63
  116. package/src/hooks/tap-state.ts +49 -26
  117. package/src/hooks/utils/tapHook.ts +35 -0
  118. package/src/index.ts +8 -3
  119. package/src/react/use-resource.ts +27 -16
  120. package/dist/hooks/depsShallowEqual.d.ts.map +0 -1
  121. package/dist/hooks/depsShallowEqual.js.map +0 -1
  122. /package/dist/hooks/{depsShallowEqual.d.ts → utils/depsShallowEqual.d.ts} +0 -0
  123. /package/dist/hooks/{depsShallowEqual.js → utils/depsShallowEqual.js} +0 -0
  124. /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.state;
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 getCommittedState<R, P>(fiber: ResourceFiber<R, P>): R {
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.state;
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.state;
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.state;
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
- resource: Resource<R, P>,
8
- scheduleRerender: () => void,
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
- resource,
12
- scheduleRerender,
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
- // Clean up all effects
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: RenderResult = {
37
+ const result = {
33
38
  commitTasks: [],
34
39
  props,
35
- state: undefined,
40
+ output: undefined as R | undefined,
36
41
  };
37
42
 
38
43
  withResourceFiber(fiber, () => {
39
44
  fiber.renderContext = result;
40
45
  try {
41
- result.state = callResourceFn(fiber.resource, props);
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
- commitRender(result, fiber);
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
  }
@@ -1,73 +1,53 @@
1
1
  import { ResourceFiber, RenderResult } from "./types";
2
2
 
3
- export function commitRender<R, P>(
4
- renderResult: RenderResult,
5
- fiber: ResourceFiber<R, P>,
6
- ): void {
7
- // Process all tasks collected during render
8
- renderResult.commitTasks.forEach((task) => {
9
- const cellIndex = task.cellIndex;
10
- const effectCell = fiber.cells[cellIndex]!;
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
- // Check if deps changed
16
- let shouldRunEffect = true;
17
-
18
- if (effectCell.deps !== undefined && task.deps !== undefined) {
19
- shouldRunEffect =
20
- effectCell.deps.length !== task.deps.length ||
21
- effectCell.deps.some((dep, j) => !Object.is(dep, task.deps![j]));
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
- // Run cleanup if effect will re-run
25
- if (shouldRunEffect) {
26
- if (effectCell.mounted) {
27
- if (typeof effectCell.deps !== typeof task.deps) {
28
- throw new Error(
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
- if (effectCell.mounted && effectCell.cleanup) {
35
- effectCell.cleanup();
36
- }
34
+ cell.cleanup?.();
35
+ } catch (e) {
36
+ errors.push(e);
37
37
  } finally {
38
- effectCell.mounted = false;
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
- export function cleanupAllEffects<R, P>(executionContext: ResourceFiber<R, P>) {
58
- let firstError: unknown | null = null;
59
- // Run cleanups in reverse order
60
- for (let i = executionContext.cells.length - 1; i >= 0; i--) {
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
  }
@@ -3,7 +3,7 @@ type Context<T> = {
3
3
  [contextValue]: T;
4
4
  };
5
5
 
6
- export const createContext = <T>(defaultValue: T): Context<T> => {
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 tapContext = <T>(context: Context<T>) => {
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 { flushSync, UpdateScheduler } from "./scheduler";
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
- getState(): R;
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
- element: ResourceElement<R, P>;
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.element);
34
- const value = tapResource(state.element);
35
- const subscribers = tapRef(new Set<() => void>()).current;
36
- const valueRef = tapRef(value);
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 (value !== valueRef.current) {
40
- valueRef.current = value;
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
- getState: () => valueRef.current,
53
+ getValue: () => valueRef.current,
48
54
  subscribe: (callback: () => void) => {
49
55
  subscribers.add(callback);
50
56
  return () => subscribers.delete(callback);
51
57
  },
52
- render: (element: ResourceElement<R, P>) => {
53
- const changed = state.element !== element;
54
- state.element = element;
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(element);
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
- { mount = true }: { mount?: boolean } = {},
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
- flushSync(() => {
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
- if (scheduler.isDirty) return;
87
- commitResourceFiber(fiber, render!);
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(HandleWrapperResource<R, P>, () =>
108
- scheduler.markDirty(),
142
+ const fiber = createResourceFiber(
143
+ HandleWrapperResource<R, P>,
144
+ (callback) => {
145
+ if (callback()) scheduler.markDirty();
146
+ },
147
+ getDevStrictMode(devStrictMode),
109
148
  );
110
149
 
111
- flushSync(() => {
150
+ flushResourcesSync(() => {
112
151
  scheduler.markDirty();
113
152
  });
114
153
 
115
- return render!.state;
154
+ return render!.output;
116
155
  };
@@ -0,0 +1,3 @@
1
+ export const isDevelopment =
2
+ process.env["NODE_ENV"] === "development" ||
3
+ process.env["NODE_ENV"] === "test";
@@ -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
+ }
@@ -1,7 +1,13 @@
1
- import { Resource, ResourceElement } from "./types";
1
+ import { ResourceElement } from "./types";
2
2
  import { fnSymbol } from "./callResourceFn";
3
- export function resource<R>(fn: () => R): Resource<R, undefined>;
4
- export function resource<R, P>(fn: (props: P) => R): Resource<R, P>;
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 {
@@ -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 flushSync = <T>(callback: () => T): T => {
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 ResourceArgs<P> = undefined extends P ? [props?: P] : [props: P];
11
- export type Resource<R, P> = (
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 ContravariantResource<R, P> = (
16
- ...args: ResourceArgs<P>
17
- ) => ResourceElement<R>;
18
-
19
- export type ExtractResourceOutput<T> =
20
- T extends ResourceElement<infer R, any> ? R : never;
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
- mounted: boolean;
31
- cleanup?: tapEffect.Destructor | undefined;
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?: readonly unknown[] | undefined;
38
- cellIndex: number;
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
- state: any;
43
- props: any;
44
- commitTasks: EffectTask[];
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 scheduleRerender: () => void;
49
- readonly resource: Resource<R, P>;
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;
@@ -0,0 +1,8 @@
1
+ import { ResourceElement } from "./types";
2
+
3
+ export function withKey<E extends ResourceElement<any, any>>(
4
+ key: string | number,
5
+ element: E,
6
+ ): E {
7
+ return { ...element, key };
8
+ }
@@ -4,5 +4,6 @@ export const tapCallback = <T extends (...args: any[]) => any>(
4
4
  fn: T,
5
5
  deps: readonly unknown[],
6
6
  ): T => {
7
+ // biome-ignore lint/correctness/useExhaustiveDependencies: user provided deps instead of callback identity
7
8
  return tapMemo(() => fn, deps);
8
9
  };
@@ -0,0 +1,6 @@
1
+ import { tapState } from "./tap-state";
2
+
3
+ export function tapConst<T>(getValue: () => T, _deps: readonly never[]): T {
4
+ const [state] = tapState(getValue);
5
+ return state;
6
+ }
@@ -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
  }