@assistant-ui/tap 0.3.4 → 0.3.5

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 (107) hide show
  1. package/dist/core/ResourceFiber.d.ts +1 -1
  2. package/dist/core/ResourceFiber.d.ts.map +1 -1
  3. package/dist/core/ResourceFiber.js +35 -40
  4. package/dist/core/ResourceFiber.js.map +1 -1
  5. package/dist/core/callResourceFn.js +15 -12
  6. package/dist/core/callResourceFn.js.map +1 -1
  7. package/dist/core/commit.d.ts +1 -1
  8. package/dist/core/commit.d.ts.map +1 -1
  9. package/dist/core/commit.js +57 -54
  10. package/dist/core/commit.js.map +1 -1
  11. package/dist/core/context.js +16 -21
  12. package/dist/core/context.js.map +1 -1
  13. package/dist/core/createResource.d.ts +1 -1
  14. package/dist/core/createResource.d.ts.map +1 -1
  15. package/dist/core/createResource.js +54 -67
  16. package/dist/core/createResource.js.map +1 -1
  17. package/dist/core/execution-context.d.ts +1 -1
  18. package/dist/core/execution-context.d.ts.map +1 -1
  19. package/dist/core/execution-context.js +21 -25
  20. package/dist/core/execution-context.js.map +1 -1
  21. package/dist/core/resource.d.ts +1 -1
  22. package/dist/core/resource.d.ts.map +1 -1
  23. package/dist/core/resource.js +8 -12
  24. package/dist/core/resource.js.map +1 -1
  25. package/dist/core/scheduler.js +73 -72
  26. package/dist/core/scheduler.js.map +1 -1
  27. package/dist/core/types.d.ts +3 -3
  28. package/dist/core/types.d.ts.map +1 -1
  29. package/dist/core/types.js +1 -0
  30. package/dist/core/types.js.map +1 -1
  31. package/dist/hooks/depsShallowEqual.js +8 -10
  32. package/dist/hooks/depsShallowEqual.js.map +1 -1
  33. package/dist/hooks/tap-callback.js +2 -6
  34. package/dist/hooks/tap-callback.js.map +1 -1
  35. package/dist/hooks/tap-effect-event.js +21 -10
  36. package/dist/hooks/tap-effect-event.js.map +1 -1
  37. package/dist/hooks/tap-effect.js +30 -31
  38. package/dist/hooks/tap-effect.js.map +1 -1
  39. package/dist/hooks/tap-inline-resource.d.ts +1 -1
  40. package/dist/hooks/tap-inline-resource.d.ts.map +1 -1
  41. package/dist/hooks/tap-inline-resource.js +2 -6
  42. package/dist/hooks/tap-inline-resource.js.map +1 -1
  43. package/dist/hooks/tap-memo.js +10 -14
  44. package/dist/hooks/tap-memo.js.map +1 -1
  45. package/dist/hooks/tap-ref.js +5 -9
  46. package/dist/hooks/tap-ref.js.map +1 -1
  47. package/dist/hooks/tap-resource.d.ts +1 -1
  48. package/dist/hooks/tap-resource.d.ts.map +1 -1
  49. package/dist/hooks/tap-resource.js +13 -28
  50. package/dist/hooks/tap-resource.js.map +1 -1
  51. package/dist/hooks/tap-resources.d.ts +1 -1
  52. package/dist/hooks/tap-resources.d.ts.map +1 -1
  53. package/dist/hooks/tap-resources.js +63 -64
  54. package/dist/hooks/tap-resources.js.map +1 -1
  55. package/dist/hooks/tap-state.js +47 -44
  56. package/dist/hooks/tap-state.js.map +1 -1
  57. package/dist/index.d.ts +14 -14
  58. package/dist/index.d.ts.map +1 -1
  59. package/dist/index.js +18 -31
  60. package/dist/index.js.map +1 -1
  61. package/dist/react/index.d.ts +1 -1
  62. package/dist/react/index.d.ts.map +1 -1
  63. package/dist/react/index.js +1 -5
  64. package/dist/react/index.js.map +1 -1
  65. package/dist/react/use-resource.d.ts +1 -1
  66. package/dist/react/use-resource.d.ts.map +1 -1
  67. package/dist/react/use-resource.js +16 -26
  68. package/dist/react/use-resource.js.map +1 -1
  69. package/package.json +44 -30
  70. package/react/package.json +5 -0
  71. package/src/__tests__/basic/resourceHandle.test.ts +56 -0
  72. package/src/__tests__/basic/tapEffect.basic.test.ts +247 -0
  73. package/src/__tests__/basic/tapResources.basic.test.ts +222 -0
  74. package/src/__tests__/basic/tapState.basic.test.ts +240 -0
  75. package/src/__tests__/errors/errors.effect-errors.test.ts +222 -0
  76. package/src/__tests__/errors/errors.render-errors.test.ts +190 -0
  77. package/src/__tests__/lifecycle/lifecycle.dependencies.test.ts +241 -0
  78. package/src/__tests__/lifecycle/lifecycle.mount-unmount.test.ts +211 -0
  79. package/src/__tests__/rules/rules.hook-count.test.ts +200 -0
  80. package/src/__tests__/rules/rules.hook-order.test.ts +192 -0
  81. package/src/__tests__/test-utils.ts +219 -0
  82. package/src/core/ResourceFiber.ts +58 -0
  83. package/src/core/callResourceFn.ts +21 -0
  84. package/src/core/commit.ts +73 -0
  85. package/src/core/context.ts +28 -0
  86. package/src/core/createResource.ts +116 -0
  87. package/src/core/execution-context.ts +34 -0
  88. package/src/core/resource.ts +16 -0
  89. package/src/core/scheduler.ts +95 -0
  90. package/src/core/types.ts +59 -0
  91. package/src/hooks/depsShallowEqual.ts +10 -0
  92. package/src/hooks/tap-callback.ts +8 -0
  93. package/src/hooks/tap-effect-event.ts +29 -0
  94. package/src/hooks/tap-effect.ts +59 -0
  95. package/src/hooks/tap-inline-resource.ts +8 -0
  96. package/src/hooks/tap-memo.ts +16 -0
  97. package/src/hooks/tap-ref.ts +16 -0
  98. package/src/hooks/tap-resource.ts +44 -0
  99. package/src/hooks/tap-resources.ts +112 -0
  100. package/src/hooks/tap-state.ts +83 -0
  101. package/src/index.ts +31 -0
  102. package/src/react/index.ts +1 -0
  103. package/src/react/use-resource.ts +35 -0
  104. package/dist/__tests__/test-utils.d.ts +0 -79
  105. package/dist/__tests__/test-utils.d.ts.map +0 -1
  106. package/dist/__tests__/test-utils.js +0 -138
  107. package/dist/__tests__/test-utils.js.map +0 -1
@@ -0,0 +1,34 @@
1
+ import { ResourceFiber } from "./types";
2
+
3
+ let currentResourceFiber: ResourceFiber<any, any> | null = null;
4
+
5
+ export function withResourceFiber<R, P>(
6
+ fiber: ResourceFiber<R, P>,
7
+ fn: () => void,
8
+ ): void {
9
+ fiber.currentIndex = 0;
10
+
11
+ const previousContext = currentResourceFiber;
12
+ currentResourceFiber = fiber;
13
+ try {
14
+ fn();
15
+
16
+ fiber.isFirstRender = false;
17
+
18
+ // ensure hook count matches
19
+ if (fiber.cells.length !== fiber.currentIndex) {
20
+ throw new Error(
21
+ `Rendered ${fiber.currentIndex} hooks but expected ${fiber.cells.length}. ` +
22
+ "Hooks must be called in the exact same order in every render.",
23
+ );
24
+ }
25
+ } finally {
26
+ currentResourceFiber = previousContext;
27
+ }
28
+ }
29
+ export function getCurrentResourceFiber(): ResourceFiber<unknown, unknown> {
30
+ if (!currentResourceFiber) {
31
+ throw new Error("No resource fiber available");
32
+ }
33
+ return currentResourceFiber;
34
+ }
@@ -0,0 +1,16 @@
1
+ import { Resource, ResourceElement } from "./types";
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>;
5
+ export function resource<R, P = undefined>(fn: (props: P) => R) {
6
+ const type = (props?: P) => {
7
+ return {
8
+ type,
9
+ props: props!,
10
+ } satisfies ResourceElement<R, P>;
11
+ };
12
+
13
+ type[fnSymbol] = fn;
14
+
15
+ return type;
16
+ }
@@ -0,0 +1,95 @@
1
+ type Task = () => void;
2
+
3
+ type GlobalFlushState = {
4
+ schedulers: Set<UpdateScheduler>;
5
+ isScheduled: boolean;
6
+ };
7
+
8
+ const MAX_FLUSH_LIMIT = 50;
9
+ let flushState: GlobalFlushState = {
10
+ schedulers: new Set([]),
11
+ isScheduled: false,
12
+ };
13
+
14
+ export class UpdateScheduler {
15
+ private _isDirty = false;
16
+
17
+ constructor(private readonly _task: Task) {}
18
+
19
+ get isDirty() {
20
+ return this._isDirty;
21
+ }
22
+
23
+ markDirty() {
24
+ this._isDirty = true;
25
+
26
+ flushState.schedulers.add(this);
27
+ scheduleFlush();
28
+ }
29
+
30
+ runTask() {
31
+ this._isDirty = false;
32
+ this._task();
33
+ }
34
+ }
35
+
36
+ const scheduleFlush = () => {
37
+ if (flushState.isScheduled) return;
38
+ flushState.isScheduled = true;
39
+ queueMicrotask(flushScheduled);
40
+ };
41
+
42
+ const flushScheduled = () => {
43
+ try {
44
+ const errors = [];
45
+ let flushDepth = 0;
46
+
47
+ for (const scheduler of flushState.schedulers) {
48
+ flushState.schedulers.delete(scheduler);
49
+ if (!scheduler.isDirty) continue;
50
+
51
+ flushDepth++;
52
+
53
+ if (flushDepth > MAX_FLUSH_LIMIT) {
54
+ throw new Error(
55
+ `Maximum update depth exceeded. This can happen when a resource ` +
56
+ `repeatedly calls setState inside tapEffect.`,
57
+ );
58
+ }
59
+
60
+ try {
61
+ scheduler.runTask();
62
+ } catch (error) {
63
+ errors.push(error);
64
+ }
65
+ }
66
+
67
+ if (errors.length > 0) {
68
+ if (errors.length === 1) {
69
+ throw errors[0];
70
+ } else {
71
+ throw new AggregateError(errors, "Errors occurred during flushSync");
72
+ }
73
+ }
74
+ } finally {
75
+ flushState.schedulers.clear();
76
+ flushState.isScheduled = false;
77
+ }
78
+ };
79
+
80
+ export const flushSync = <T>(callback: () => T): T => {
81
+ const prev = flushState;
82
+ flushState = {
83
+ schedulers: new Set([]),
84
+ isScheduled: true,
85
+ };
86
+
87
+ try {
88
+ const result = callback();
89
+ flushScheduled();
90
+
91
+ return result;
92
+ } finally {
93
+ flushState = prev;
94
+ }
95
+ };
@@ -0,0 +1,59 @@
1
+ import type { tapEffect } from "../hooks/tap-effect";
2
+ import type { tapState } from "../hooks/tap-state";
3
+ import { fnSymbol } from "./callResourceFn";
4
+
5
+ export type ResourceElement<R, P = any> = {
6
+ type: Resource<R, P> & { [fnSymbol]: (props: P) => R };
7
+ props: P;
8
+ };
9
+
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>;
14
+
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;
21
+
22
+ export type Cell =
23
+ | {
24
+ type: "state";
25
+ value: any;
26
+ set: (updater: tapState.StateUpdater<any>) => void;
27
+ }
28
+ | {
29
+ type: "effect";
30
+ mounted: boolean;
31
+ cleanup?: tapEffect.Destructor | undefined;
32
+ deps?: readonly unknown[] | undefined;
33
+ };
34
+
35
+ export interface EffectTask {
36
+ effect: tapEffect.EffectCallback;
37
+ deps?: readonly unknown[] | undefined;
38
+ cellIndex: number;
39
+ }
40
+
41
+ export interface RenderResult {
42
+ state: any;
43
+ props: any;
44
+ commitTasks: EffectTask[];
45
+ }
46
+
47
+ export interface ResourceFiber<R, P> {
48
+ readonly scheduleRerender: () => void;
49
+ readonly resource: Resource<R, P>;
50
+
51
+ cells: Cell[];
52
+ currentIndex: number;
53
+
54
+ renderContext: RenderResult | undefined; // set during render
55
+
56
+ isMounted: boolean;
57
+ isFirstRender: boolean;
58
+ isNeverMounted: boolean;
59
+ }
@@ -0,0 +1,10 @@
1
+ export const depsShallowEqual = (
2
+ a: readonly unknown[],
3
+ b: readonly unknown[],
4
+ ) => {
5
+ if (a.length !== b.length) return false;
6
+ for (let i = 0; i < a.length; i++) {
7
+ if (!Object.is(a[i], b[i])) return false;
8
+ }
9
+ return true;
10
+ };
@@ -0,0 +1,8 @@
1
+ import { tapMemo } from "./tap-memo";
2
+
3
+ export const tapCallback = <T extends (...args: any[]) => any>(
4
+ fn: T,
5
+ deps: readonly unknown[],
6
+ ): T => {
7
+ return tapMemo(() => fn, deps);
8
+ };
@@ -0,0 +1,29 @@
1
+ import { tapRef } from "./tap-ref";
2
+ import { tapEffect } from "./tap-effect";
3
+
4
+ /**
5
+ * Creates a stable function reference that always calls the most recent version of the callback.
6
+ * Similar to React's useEffectEvent hook.
7
+ *
8
+ * @param callback - The callback function to wrap
9
+ * @returns A stable function reference that always calls the latest callback
10
+ *
11
+ * @example
12
+ * ```typescript
13
+ * const handleClick = tapEffectEvent((value: string) => {
14
+ * console.log(value);
15
+ * });
16
+ * // handleClick reference is stable, but always calls the latest version
17
+ * ```
18
+ */
19
+ export function tapEffectEvent<T extends (...args: any[]) => any>(
20
+ callback: T,
21
+ ): T {
22
+ const callbackRef = tapRef(callback);
23
+
24
+ tapEffect(() => {
25
+ callbackRef.current = callback;
26
+ });
27
+
28
+ return callbackRef.current;
29
+ }
@@ -0,0 +1,59 @@
1
+ import { getCurrentResourceFiber } from "../core/execution-context";
2
+ import { Cell } from "../core/types";
3
+
4
+ function getEffectCell(): number {
5
+ const fiber = getCurrentResourceFiber();
6
+ const index = fiber.currentIndex++;
7
+
8
+ // Check if we're trying to use more hooks than in previous renders
9
+ if (!fiber.isFirstRender && index >= fiber.cells.length) {
10
+ throw new Error(
11
+ "Rendered more hooks than during the previous render. " +
12
+ "Hooks must be called in the exact same order in every render.",
13
+ );
14
+ }
15
+
16
+ if (!fiber.cells[index]) {
17
+ // Create the effect cell
18
+ const cell: Cell & { type: "effect" } = {
19
+ type: "effect",
20
+ mounted: false,
21
+ };
22
+
23
+ fiber.cells[index] = cell;
24
+ }
25
+
26
+ const cell = fiber.cells[index];
27
+ if (cell.type !== "effect") {
28
+ throw new Error("Hook order changed between renders");
29
+ }
30
+
31
+ return index;
32
+ }
33
+
34
+ export namespace tapEffect {
35
+ export type Destructor = () => void;
36
+ export type EffectCallback = () => void | Destructor;
37
+ }
38
+
39
+ export function tapEffect(effect: tapEffect.EffectCallback): void;
40
+ export function tapEffect(
41
+ effect: tapEffect.EffectCallback,
42
+ deps: readonly unknown[],
43
+ ): void;
44
+ export function tapEffect(
45
+ effect: tapEffect.EffectCallback,
46
+ deps?: readonly unknown[],
47
+ ): void {
48
+ const fiber = getCurrentResourceFiber();
49
+
50
+ // Reserve a spot for the effect cell and get its index
51
+ const cellIndex = getEffectCell();
52
+
53
+ // Add task to render context for execution in commit phase
54
+ fiber.renderContext!.commitTasks.push({
55
+ effect,
56
+ deps,
57
+ cellIndex,
58
+ });
59
+ }
@@ -0,0 +1,8 @@
1
+ import { ExtractResourceOutput, ResourceElement } from "../core/types";
2
+ import { callResourceFn } from "../core/callResourceFn";
3
+
4
+ export function tapInlineResource<E extends ResourceElement<any, any>>(
5
+ element: E,
6
+ ): ExtractResourceOutput<E> {
7
+ return callResourceFn(element.type, element.props);
8
+ }
@@ -0,0 +1,16 @@
1
+ import { tapRef } from "./tap-ref";
2
+ import { depsShallowEqual } from "./depsShallowEqual";
3
+
4
+ export const tapMemo = <T>(fn: () => T, deps: readonly unknown[]) => {
5
+ const dataRef = tapRef<{ value: T; deps: readonly unknown[] }>();
6
+ if (!dataRef.current) {
7
+ dataRef.current = { value: fn(), deps };
8
+ }
9
+
10
+ if (!depsShallowEqual(dataRef.current.deps, deps)) {
11
+ dataRef.current.value = fn();
12
+ dataRef.current.deps = deps;
13
+ }
14
+
15
+ return dataRef.current.value;
16
+ };
@@ -0,0 +1,16 @@
1
+ import { tapState } from "./tap-state";
2
+
3
+ export namespace tapRef {
4
+ export interface RefObject<T> {
5
+ current: T;
6
+ }
7
+ }
8
+
9
+ export function tapRef<T>(initialValue: T): tapRef.RefObject<T>;
10
+ export function tapRef<T = undefined>(): tapRef.RefObject<T | undefined>;
11
+ export function tapRef<T>(initialValue?: T): tapRef.RefObject<T | undefined> {
12
+ const [state] = tapState(() => ({
13
+ current: initialValue,
14
+ }));
15
+ return state;
16
+ }
@@ -0,0 +1,44 @@
1
+ import { ExtractResourceOutput, ResourceElement } from "../core/types";
2
+ import { tapEffect } from "./tap-effect";
3
+ import {
4
+ createResourceFiber,
5
+ unmountResourceFiber,
6
+ renderResourceFiber,
7
+ commitResourceFiber,
8
+ } from "../core/ResourceFiber";
9
+ import { tapMemo } from "./tap-memo";
10
+ import { tapState } from "./tap-state";
11
+
12
+ export function tapResource<E extends ResourceElement<any, any>>(
13
+ element: E,
14
+ ): ExtractResourceOutput<E>;
15
+ export function tapResource<E extends ResourceElement<any, any>>(
16
+ element: E,
17
+ deps: readonly unknown[],
18
+ ): ExtractResourceOutput<E>;
19
+ export function tapResource<E extends ResourceElement<any, any>>(
20
+ element: E,
21
+ deps?: readonly unknown[],
22
+ ): ExtractResourceOutput<E> {
23
+ const [stateVersion, rerender] = tapState({});
24
+ const fiber = tapMemo(
25
+ () => createResourceFiber(element.type, () => rerender({})),
26
+ [element.type],
27
+ );
28
+
29
+ const props = deps ? tapMemo(() => element.props, deps) : element.props;
30
+ const result = tapMemo(
31
+ () => renderResourceFiber(fiber, props),
32
+ [fiber, props, stateVersion],
33
+ );
34
+
35
+ tapEffect(() => {
36
+ return () => unmountResourceFiber(fiber);
37
+ }, [fiber]);
38
+
39
+ tapEffect(() => {
40
+ commitResourceFiber(fiber, result);
41
+ }, [fiber, result]);
42
+
43
+ return result.state;
44
+ }
@@ -0,0 +1,112 @@
1
+ import {
2
+ ExtractResourceOutput,
3
+ RenderResult,
4
+ ResourceElement,
5
+ ResourceFiber,
6
+ } from "../core/types";
7
+ import { tapEffect } from "./tap-effect";
8
+ import { tapMemo } from "./tap-memo";
9
+ import { tapState } from "./tap-state";
10
+ import { tapCallback } from "./tap-callback";
11
+ import {
12
+ createResourceFiber,
13
+ unmountResourceFiber,
14
+ renderResourceFiber,
15
+ commitResourceFiber,
16
+ } from "../core/ResourceFiber";
17
+
18
+ export type TapResourcesRenderResult<R, K extends string | number | symbol> = {
19
+ add: [K, ResourceFiber<R, any>][];
20
+ remove: K[];
21
+ commit: [K, RenderResult][];
22
+ return: Record<K, R>;
23
+ };
24
+
25
+ export function tapResources<
26
+ M extends Record<string | number | symbol, any>,
27
+ E extends ResourceElement<any, any>,
28
+ >(
29
+ map: M,
30
+ getElement: (t: M[keyof M], key: keyof M) => E,
31
+ getElementDeps: any[],
32
+ ): { [K in keyof M]: ExtractResourceOutput<E> } {
33
+ type R = ExtractResourceOutput<E>;
34
+ const [version, setVersion] = tapState(0);
35
+ const rerender = tapCallback(() => setVersion((v) => v + 1), []);
36
+
37
+ type K = keyof M;
38
+ const [fibers] = tapState(() => new Map<K, ResourceFiber<R, any>>());
39
+
40
+ const getElementMemo = tapMemo(() => getElement, getElementDeps);
41
+
42
+ // Process each element
43
+
44
+ const results = tapMemo(() => {
45
+ const results: TapResourcesRenderResult<R, K> = {
46
+ remove: [],
47
+ add: [],
48
+ commit: [],
49
+ return: {} as Record<K, R>,
50
+ };
51
+
52
+ // Create/update fibers and render
53
+ for (const key in map) {
54
+ const value = map[key as K];
55
+ const element = getElementMemo(value, key);
56
+
57
+ let fiber = fibers.get(key);
58
+
59
+ // Create new fiber if needed or type changed
60
+ if (!fiber || fiber.resource !== element.type) {
61
+ if (fiber) results.remove.push(key);
62
+ fiber = createResourceFiber(element.type, rerender);
63
+ results.add.push([key, fiber]);
64
+ }
65
+
66
+ // Render with current props
67
+ const renderResult = renderResourceFiber(fiber, element.props);
68
+ results.commit.push([key, renderResult]);
69
+
70
+ results.return[key] = renderResult.state;
71
+ }
72
+
73
+ // Clean up removed fibers (only if there might be stale ones)
74
+ if (
75
+ fibers.size >
76
+ results.commit.length - results.add.length + results.remove.length
77
+ ) {
78
+ for (const key of fibers.keys()) {
79
+ if (!(key in map)) {
80
+ results.remove.push(key);
81
+ }
82
+ }
83
+ }
84
+
85
+ return results;
86
+ }, [map, getElementMemo, version]);
87
+
88
+ // Cleanup on unmount
89
+ tapEffect(() => {
90
+ return () => {
91
+ for (const key of fibers.keys()) {
92
+ unmountResourceFiber(fibers.get(key)!);
93
+ fibers.delete(key);
94
+ }
95
+ };
96
+ }, []);
97
+
98
+ tapEffect(() => {
99
+ for (const key of results.remove) {
100
+ unmountResourceFiber(fibers.get(key)!);
101
+ fibers.delete(key);
102
+ }
103
+ for (const [key, fiber] of results.add) {
104
+ fibers.set(key, fiber);
105
+ }
106
+ for (const [key, result] of results.commit) {
107
+ commitResourceFiber(fibers.get(key)!, result);
108
+ }
109
+ }, [results]);
110
+
111
+ return results.return;
112
+ }
@@ -0,0 +1,83 @@
1
+ import { getCurrentResourceFiber } from "../core/execution-context";
2
+ import { Cell, ResourceFiber } from "../core/types";
3
+
4
+ export namespace tapState {
5
+ export type StateUpdater<S> = S | ((prev: S) => S);
6
+ }
7
+
8
+ const rerender = (fiber: ResourceFiber<any, any>) => {
9
+ if (fiber.renderContext) {
10
+ throw new Error("Resource updated during render");
11
+ }
12
+
13
+ if (fiber.isMounted) {
14
+ // Only schedule rerender if currently mounted
15
+ fiber.scheduleRerender();
16
+ } else if (fiber.isNeverMounted) {
17
+ throw new Error("Resource updated before mount");
18
+ }
19
+ };
20
+
21
+ function getStateCell<T>(
22
+ initialValue: T | (() => T),
23
+ ): Cell & { type: "state" } {
24
+ const fiber = getCurrentResourceFiber();
25
+ const index = fiber.currentIndex++;
26
+
27
+ // Check if we're trying to use more hooks than in previous renders
28
+ if (!fiber.isFirstRender && index >= fiber.cells.length) {
29
+ throw new Error(
30
+ "Rendered more hooks than during the previous render. " +
31
+ "Hooks must be called in the exact same order in every render.",
32
+ );
33
+ }
34
+
35
+ if (!fiber.cells[index]) {
36
+ // Initialize the value immediately
37
+ const value =
38
+ typeof initialValue === "function"
39
+ ? (initialValue as () => T)()
40
+ : initialValue;
41
+
42
+ const cell: Cell & { type: "state" } = {
43
+ type: "state",
44
+ value,
45
+ set: (updater: tapState.StateUpdater<T>) => {
46
+ const currentValue = cell.value;
47
+ const nextValue =
48
+ typeof updater === "function"
49
+ ? (updater as (prev: T) => T)(currentValue)
50
+ : updater;
51
+
52
+ if (!Object.is(currentValue, nextValue)) {
53
+ cell.value = nextValue;
54
+ rerender(fiber);
55
+ }
56
+ },
57
+ };
58
+
59
+ fiber.cells[index] = cell;
60
+ }
61
+
62
+ const cell = fiber.cells[index];
63
+ if (cell.type !== "state") {
64
+ throw new Error("Hook order changed between renders");
65
+ }
66
+
67
+ return cell as Cell & { type: "state" };
68
+ }
69
+
70
+ export function tapState<S = undefined>(): [
71
+ S | undefined,
72
+ (updater: tapState.StateUpdater<S>) => void,
73
+ ];
74
+ export function tapState<S>(
75
+ initial: S | (() => S),
76
+ ): [S, (updater: tapState.StateUpdater<S>) => void];
77
+ export function tapState<S>(
78
+ initial?: S | (() => S),
79
+ ): [S | undefined, (updater: tapState.StateUpdater<S>) => void] {
80
+ const cell = getStateCell(initial as S | (() => S));
81
+
82
+ return [cell.value, cell.set];
83
+ }
package/src/index.ts ADDED
@@ -0,0 +1,31 @@
1
+ export { resource } from "./core/resource";
2
+
3
+ // primitive hooks
4
+ export { tapState } from "./hooks/tap-state";
5
+ export { tapEffect } from "./hooks/tap-effect";
6
+
7
+ // utility hooks
8
+ export { tapRef } from "./hooks/tap-ref";
9
+ export { tapMemo } from "./hooks/tap-memo";
10
+ export { tapCallback } from "./hooks/tap-callback";
11
+ export { tapEffectEvent } from "./hooks/tap-effect-event";
12
+
13
+ // resources
14
+ export { tapResource } from "./hooks/tap-resource";
15
+ export { tapInlineResource } from "./hooks/tap-inline-resource";
16
+ export { tapResources } from "./hooks/tap-resources";
17
+
18
+ // imperative
19
+ export { createResource } from "./core/createResource";
20
+ export { flushSync } from "./core/scheduler";
21
+
22
+ // context
23
+ export { createContext, tapContext, withContextProvider } from "./core/context";
24
+
25
+ // types
26
+ export type {
27
+ Resource,
28
+ ContravariantResource,
29
+ ResourceElement,
30
+ ExtractResourceOutput,
31
+ } from "./core/types";
@@ -0,0 +1 @@
1
+ export { useResource } from "./use-resource";
@@ -0,0 +1,35 @@
1
+ import { useEffect, useLayoutEffect, useMemo, useState } from "react";
2
+ import { ExtractResourceOutput, ResourceElement } from "../core/types";
3
+ import {
4
+ createResourceFiber,
5
+ unmountResourceFiber,
6
+ renderResourceFiber,
7
+ commitResourceFiber,
8
+ } from "../core/ResourceFiber";
9
+
10
+ const shouldAvoidLayoutEffect =
11
+ (globalThis as any).__ASSISTANT_UI_DISABLE_LAYOUT_EFFECT__ === true;
12
+
13
+ const useIsomorphicLayoutEffect = shouldAvoidLayoutEffect
14
+ ? useEffect
15
+ : useLayoutEffect;
16
+
17
+ export function useResource<E extends ResourceElement<any, any>>(
18
+ element: E,
19
+ ): ExtractResourceOutput<E> {
20
+ const [, rerender] = useState({});
21
+ const fiber = useMemo(
22
+ () => createResourceFiber(element.type, () => rerender({})),
23
+ [element.type],
24
+ );
25
+
26
+ const result = renderResourceFiber(fiber, element.props);
27
+ useIsomorphicLayoutEffect(() => {
28
+ return () => unmountResourceFiber(fiber);
29
+ }, [fiber]);
30
+ useIsomorphicLayoutEffect(() => {
31
+ commitResourceFiber(fiber, result);
32
+ });
33
+
34
+ return result.state;
35
+ }