@assistant-ui/tap 0.5.11 → 0.5.13
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 +21 -364
- package/dist/core/ResourceFiber.d.ts +9 -5
- package/dist/core/ResourceFiber.d.ts.map +1 -1
- package/dist/core/ResourceFiber.js +47 -45
- package/dist/core/ResourceFiber.js.map +1 -1
- package/dist/core/context.d.ts +7 -5
- package/dist/core/context.d.ts.map +1 -1
- package/dist/core/context.js +16 -15
- package/dist/core/context.js.map +1 -1
- package/dist/core/createResourceRoot.d.ts +8 -4
- package/dist/core/createResourceRoot.d.ts.map +1 -1
- package/dist/core/createResourceRoot.js +25 -26
- package/dist/core/createResourceRoot.js.map +1 -1
- package/dist/core/helpers/callResourceFn.d.ts +1 -2
- package/dist/core/helpers/callResourceFn.js +15 -13
- package/dist/core/helpers/callResourceFn.js.map +1 -1
- package/dist/core/helpers/commit.d.ts +7 -3
- package/dist/core/helpers/commit.d.ts.map +1 -1
- package/dist/core/helpers/commit.js +33 -50
- package/dist/core/helpers/commit.js.map +1 -1
- package/dist/core/helpers/env.d.ts +4 -1
- package/dist/core/helpers/env.d.ts.map +1 -1
- package/dist/core/helpers/env.js +5 -2
- package/dist/core/helpers/env.js.map +1 -1
- package/dist/core/helpers/execution-context.d.ts +8 -4
- package/dist/core/helpers/execution-context.d.ts.map +1 -1
- package/dist/core/helpers/execution-context.js +22 -27
- package/dist/core/helpers/execution-context.js.map +1 -1
- package/dist/core/helpers/root.d.ts +10 -6
- package/dist/core/helpers/root.d.ts.map +1 -1
- package/dist/core/helpers/root.js +45 -48
- package/dist/core/helpers/root.js.map +1 -1
- package/dist/core/resource.d.ts +8 -4
- package/dist/core/resource.d.ts.map +1 -1
- package/dist/core/resource.js +13 -9
- package/dist/core/resource.js.map +1 -1
- package/dist/core/scheduler.d.ts +11 -9
- package/dist/core/scheduler.d.ts.map +1 -1
- package/dist/core/scheduler.js +70 -83
- package/dist/core/scheduler.js.map +1 -1
- package/dist/core/types.d.ts +56 -55
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js +0 -2
- package/dist/core/withKey.d.ts +6 -2
- package/dist/core/withKey.d.ts.map +1 -1
- package/dist/core/withKey.js +9 -2
- package/dist/core/withKey.js.map +1 -1
- package/dist/hooks/tap-callback.d.ts +4 -1
- package/dist/hooks/tap-callback.d.ts.map +1 -1
- package/dist/hooks/tap-callback.js +6 -3
- package/dist/hooks/tap-callback.js.map +1 -1
- package/dist/hooks/tap-const.d.ts +4 -1
- package/dist/hooks/tap-const.d.ts.map +1 -1
- package/dist/hooks/tap-const.js +7 -3
- package/dist/hooks/tap-const.js.map +1 -1
- package/dist/hooks/tap-effect-event.d.ts +4 -1
- package/dist/hooks/tap-effect-event.d.ts.map +1 -1
- package/dist/hooks/tap-effect-event.js +31 -29
- package/dist/hooks/tap-effect-event.js.map +1 -1
- package/dist/hooks/tap-effect.d.ts +8 -5
- package/dist/hooks/tap-effect.d.ts.map +1 -1
- package/dist/hooks/tap-effect.js +35 -45
- package/dist/hooks/tap-effect.js.map +1 -1
- package/dist/hooks/tap-memo.d.ts +4 -1
- package/dist/hooks/tap-memo.d.ts.map +1 -1
- package/dist/hooks/tap-memo.js +17 -13
- package/dist/hooks/tap-memo.js.map +1 -1
- package/dist/hooks/tap-reducer.d.ts +7 -5
- package/dist/hooks/tap-reducer.d.ts.map +1 -1
- package/dist/hooks/tap-reducer.js +70 -76
- package/dist/hooks/tap-reducer.js.map +1 -1
- package/dist/hooks/tap-ref.d.ts +9 -6
- package/dist/hooks/tap-ref.d.ts.map +1 -1
- package/dist/hooks/tap-ref.js +7 -5
- package/dist/hooks/tap-ref.js.map +1 -1
- package/dist/hooks/tap-resource.d.ts +7 -3
- package/dist/hooks/tap-resource.d.ts.map +1 -1
- package/dist/hooks/tap-resource.js +31 -22
- package/dist/hooks/tap-resource.js.map +1 -1
- package/dist/hooks/tap-resources.d.ts +6 -2
- package/dist/hooks/tap-resources.d.ts.map +1 -1
- package/dist/hooks/tap-resources.js +74 -96
- package/dist/hooks/tap-resources.js.map +1 -1
- package/dist/hooks/tap-state.d.ts +7 -7
- package/dist/hooks/tap-state.d.ts.map +1 -1
- package/dist/hooks/tap-state.js +7 -5
- package/dist/hooks/tap-state.js.map +1 -1
- package/dist/hooks/utils/depsShallowEqual.d.ts +4 -1
- package/dist/hooks/utils/depsShallowEqual.d.ts.map +1 -1
- package/dist/hooks/utils/depsShallowEqual.js +8 -8
- package/dist/hooks/utils/depsShallowEqual.js.map +1 -1
- package/dist/hooks/utils/tapHook.d.ts +8 -4
- package/dist/hooks/utils/tapHook.d.ts.map +1 -1
- package/dist/hooks/utils/tapHook.js +17 -20
- package/dist/hooks/utils/tapHook.js.map +1 -1
- package/dist/index.d.ts +18 -18
- package/dist/index.js +17 -23
- package/dist/react/index.d.ts +2 -2
- package/dist/react/index.js +2 -2
- package/dist/react/use-resource.d.ts +6 -2
- package/dist/react/use-resource.d.ts.map +1 -1
- package/dist/react/use-resource.js +41 -36
- package/dist/react/use-resource.js.map +1 -1
- package/dist/tapResourceRoot.d.ts +18 -14
- package/dist/tapResourceRoot.d.ts.map +1 -1
- package/dist/tapResourceRoot.js +68 -77
- package/dist/tapResourceRoot.js.map +1 -1
- package/package.json +8 -8
- package/src/__tests__/basic/tapEffect.basic.test.ts +4 -1
- package/src/__tests__/errors/errors.effect-errors.test.ts +3 -4
- package/src/__tests__/lifecycle/lifecycle.dependencies.test.ts +1 -1
- package/src/__tests__/strictmode/react-strictmode-behavior.test.tsx +1 -0
- package/src/__tests__/strictmode/react-strictmode-rerender-sources.test.tsx +17 -5
- package/src/__tests__/strictmode/tap-strictmode-rerender-sources.test.ts +1 -0
- package/src/core/types.ts +1 -2
- package/src/hooks/tap-callback.ts +1 -1
- package/src/hooks/tap-resource.ts +2 -2
- package/src/hooks/tap-resources.ts +1 -1
- package/src/react/use-resource.ts +2 -2
- package/dist/core/helpers/callResourceFn.d.ts.map +0 -1
- package/dist/core/types.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/react/index.d.ts.map +0 -1
- package/dist/react/index.js.map +0 -1
package/README.md
CHANGED
|
@@ -1,6 +1,13 @@
|
|
|
1
|
-
#
|
|
1
|
+
# `@assistant-ui/tap`
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://www.npmjs.com/package/@assistant-ui/tap)
|
|
4
|
+
[](https://www.npmjs.com/package/@assistant-ui/tap)
|
|
5
|
+
[](https://bundlephobia.com/package/@assistant-ui/tap)
|
|
6
|
+
[](https://github.com/assistant-ui/assistant-ui)
|
|
7
|
+
|
|
8
|
+
Reactive primitives that bring React's hook mental model outside of components. The core has zero runtime dependencies and works in vanilla JS, on a server, or in React via the optional `/react` sub-path. Define self-contained units of state and effects (Resources) using `tapState`, `tapEffect`, `tapMemo`, and friends, and consume them via `useResource`.
|
|
9
|
+
|
|
10
|
+
`tap` powers the runtime layer of assistant-ui. Most users do not install it directly; reach for `@assistant-ui/react` instead.
|
|
4
11
|
|
|
5
12
|
## Installation
|
|
6
13
|
|
|
@@ -8,400 +15,50 @@
|
|
|
8
15
|
npm install @assistant-ui/tap
|
|
9
16
|
```
|
|
10
17
|
|
|
11
|
-
##
|
|
12
|
-
|
|
13
|
-
Instead of limiting hooks to React components, tap lets you use the same familiar hooks pattern (`useState`, `useEffect`, `useMemo`, etc.) to create self-contained, reusable units of reactive state and logic called **Resources** that can be used anywhere - in vanilla JavaScript, servers, or outside of React.
|
|
14
|
-
|
|
15
|
-
## Philosophy
|
|
16
|
-
|
|
17
|
-
- **Unified mental model**: Use the same hooks pattern everywhere
|
|
18
|
-
- **Framework agnostic**: Zero dependencies, works with or without React
|
|
19
|
-
- **Lifecycle management**: Resources handle their own cleanup automatically
|
|
20
|
-
- **Type-safe**: Full TypeScript support with proper type inference
|
|
21
|
-
|
|
22
|
-
## How It Works
|
|
23
|
-
|
|
24
|
-
tap implements a **render-commit pattern** similar to React:
|
|
25
|
-
|
|
26
|
-
### Render Phase
|
|
27
|
-
|
|
28
|
-
1. Each resource instance has a "fiber" that tracks state and effects
|
|
29
|
-
2. When a resource function runs, hooks record their data in the fiber
|
|
30
|
-
3. The library maintains an execution context to track which fiber's hooks are being called
|
|
31
|
-
4. Each hook stores its data in cells indexed by call order (enforcing React's rules)
|
|
32
|
-
|
|
33
|
-
### Commit Phase
|
|
34
|
-
|
|
35
|
-
1. After render, collected effect tasks are processed
|
|
36
|
-
2. Effects check if dependencies changed using shallow equality
|
|
37
|
-
3. Old effects are cleaned up before new ones run
|
|
38
|
-
4. Updates are batched using microtasks to prevent excessive re-renders
|
|
39
|
-
|
|
40
|
-
## Core Concepts
|
|
41
|
-
|
|
42
|
-
### Resources
|
|
43
|
-
|
|
44
|
-
Resources are self-contained units of reactive state and logic. They follow the same rules as React hooks:
|
|
45
|
-
|
|
46
|
-
- **Hook Order**: Hooks must be called in the same order in every render
|
|
47
|
-
- **No Conditional Hooks**: Can't call hooks inside conditionals or loops
|
|
48
|
-
- **No Async Hooks**: Hooks must be called synchronously during render
|
|
49
|
-
- Resources automatically handle cleanup and lifecycle
|
|
50
|
-
|
|
51
|
-
### Creating Resources
|
|
18
|
+
## Usage
|
|
52
19
|
|
|
53
20
|
```typescript
|
|
54
|
-
import {
|
|
21
|
+
import { resource, tapState, tapEffect, createResourceRoot } from "@assistant-ui/tap";
|
|
55
22
|
|
|
56
|
-
// Define a resource using familiar hook patterns
|
|
57
23
|
const Counter = resource(({ incrementBy = 1 }: { incrementBy?: number }) => {
|
|
58
24
|
const [count, setCount] = tapState(0);
|
|
59
25
|
|
|
60
26
|
tapEffect(() => {
|
|
61
|
-
console.log(
|
|
27
|
+
console.log("count:", count);
|
|
62
28
|
}, [count]);
|
|
63
29
|
|
|
64
30
|
return {
|
|
65
31
|
count,
|
|
66
32
|
increment: () => setCount((c) => c + incrementBy),
|
|
67
|
-
decrement: () => setCount((c) => c - incrementBy),
|
|
68
33
|
};
|
|
69
34
|
});
|
|
70
35
|
|
|
71
|
-
// Create an instance
|
|
72
36
|
const root = createResourceRoot();
|
|
73
37
|
const counter = root.render(Counter({ incrementBy: 2 }));
|
|
74
38
|
|
|
75
|
-
// Subscribe to changes
|
|
76
39
|
const unsubscribe = counter.subscribe(() => {
|
|
77
|
-
console.log("
|
|
40
|
+
console.log("counter updated:", counter.getValue().count);
|
|
78
41
|
});
|
|
79
42
|
|
|
80
|
-
// Use the resource
|
|
81
43
|
counter.getValue().increment();
|
|
82
44
|
```
|
|
83
45
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
Creates a resource element factory. Resource elements are plain objects of the type `{ type: ResourceFn<R, P>, props: P, key?: string | number }`.
|
|
87
|
-
|
|
88
|
-
```typescript
|
|
89
|
-
const Counter = resource(({ incrementBy = 1 }: { incrementBy?: number }) => {
|
|
90
|
-
const [count, setCount] = tapState(0);
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
// create a Counter element
|
|
94
|
-
const counterEl = Counter({ incrementBy: 2 });
|
|
95
|
-
|
|
96
|
-
// create a Counter instance
|
|
97
|
-
const root = createResourceRoot();
|
|
98
|
-
root.render(counterEl);
|
|
99
|
-
root.unmount();
|
|
100
|
-
```
|
|
101
|
-
|
|
102
|
-
## Hook APIs
|
|
46
|
+
In React, use the `useResource` hook from the `/react` sub-path:
|
|
103
47
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
Manages local state within a resource, exactly like React's `useState`.
|
|
107
|
-
|
|
108
|
-
```typescript
|
|
109
|
-
const [value, setValue] = tapState(initialValue);
|
|
110
|
-
const [value, setValue] = tapState(() => computeInitialValue());
|
|
111
|
-
```
|
|
112
|
-
|
|
113
|
-
### `tapEffect`
|
|
114
|
-
|
|
115
|
-
Runs side effects with automatic cleanup, exactly like React's `useEffect`.
|
|
116
|
-
|
|
117
|
-
```typescript
|
|
118
|
-
tapEffect(() => {
|
|
119
|
-
// Effect logic
|
|
120
|
-
return () => {
|
|
121
|
-
// Cleanup logic
|
|
122
|
-
};
|
|
123
|
-
}, [dependencies]);
|
|
124
|
-
```
|
|
125
|
-
|
|
126
|
-
### `tapMemo`
|
|
127
|
-
|
|
128
|
-
Memoizes expensive computations, exactly like React's `useMemo`.
|
|
129
|
-
|
|
130
|
-
```typescript
|
|
131
|
-
const expensiveValue = tapMemo(() => {
|
|
132
|
-
return computeExpensiveValue(dep1, dep2);
|
|
133
|
-
}, [dep1, dep2]);
|
|
134
|
-
```
|
|
135
|
-
|
|
136
|
-
### `tapCallback`
|
|
137
|
-
|
|
138
|
-
Memoizes callbacks to prevent unnecessary re-renders, exactly like React's `useCallback`.
|
|
139
|
-
|
|
140
|
-
```typescript
|
|
141
|
-
const stableCallback = tapCallback(() => {
|
|
142
|
-
doSomething(value);
|
|
143
|
-
}, [value]);
|
|
144
|
-
```
|
|
145
|
-
|
|
146
|
-
### `tapRef`
|
|
147
|
-
|
|
148
|
-
Creates a mutable reference that persists across renders, exactly like React's `useRef`.
|
|
149
|
-
|
|
150
|
-
```typescript
|
|
151
|
-
// With initial value
|
|
152
|
-
const ref = tapRef(initialValue);
|
|
153
|
-
ref.current = newValue;
|
|
154
|
-
|
|
155
|
-
// Without initial value
|
|
156
|
-
const ref = tapRef<string>(); // ref.current is undefined
|
|
157
|
-
ref.current = "hello";
|
|
158
|
-
```
|
|
159
|
-
|
|
160
|
-
### `tapResource`
|
|
161
|
-
|
|
162
|
-
Composes resources together - resources can render other resources.
|
|
163
|
-
|
|
164
|
-
```typescript
|
|
165
|
-
const Timer = resource(() => {
|
|
166
|
-
const counter = tapResource({ type: Counter, props: { incrementBy: 1 } });
|
|
167
|
-
|
|
168
|
-
tapEffect(() => {
|
|
169
|
-
const interval = setInterval(() => {
|
|
170
|
-
counter.increment();
|
|
171
|
-
}, 1000);
|
|
172
|
-
return () => clearInterval(interval);
|
|
173
|
-
}, []);
|
|
174
|
-
|
|
175
|
-
return counter.count;
|
|
176
|
-
});
|
|
177
|
-
```
|
|
178
|
-
|
|
179
|
-
### `tapResources`
|
|
180
|
-
|
|
181
|
-
Renders multiple resources from an array, similar to React's list rendering. Returns an array with each resource's result.
|
|
182
|
-
|
|
183
|
-
```typescript
|
|
184
|
-
tapResources<E extends ResourceElement<any, any>>(
|
|
185
|
-
getElements: () => readonly E[],
|
|
186
|
-
getElementsDeps: readonly any[]
|
|
187
|
-
): ExtractResourceReturnType<E>[]
|
|
188
|
-
```
|
|
189
|
-
|
|
190
|
-
**Parameters:**
|
|
191
|
-
- `getElements`: A function that returns an array of ResourceElements
|
|
192
|
-
- `getElementsDeps`: Dependency array for memoizing the getElements function
|
|
193
|
-
|
|
194
|
-
**Example:**
|
|
195
|
-
|
|
196
|
-
```typescript
|
|
197
|
-
const TodoItem = resource((props: { text: string }) => {
|
|
198
|
-
const [completed, setCompleted] = tapState(false);
|
|
199
|
-
return { text: props.text, completed, setCompleted };
|
|
200
|
-
});
|
|
201
|
-
|
|
202
|
-
const TodoList = resource(() => {
|
|
203
|
-
const todos = tapMemo(
|
|
204
|
-
() => [
|
|
205
|
-
{ id: "1", text: "Learn tap" },
|
|
206
|
-
{ id: "2", text: "Build something awesome" },
|
|
207
|
-
],
|
|
208
|
-
[],
|
|
209
|
-
);
|
|
210
|
-
|
|
211
|
-
// Returns Array<{ text, completed, setCompleted }>
|
|
212
|
-
const todoItems = tapResources(
|
|
213
|
-
() => todos.map((todo) => TodoItem({ text: todo.text })),
|
|
214
|
-
[todos]
|
|
215
|
-
);
|
|
216
|
-
|
|
217
|
-
return todoItems;
|
|
218
|
-
});
|
|
219
|
-
```
|
|
220
|
-
|
|
221
|
-
**Key features:**
|
|
222
|
-
- Resource instances are preserved when keys remain the same (use `withKey()` to provide stable keys)
|
|
223
|
-
- Automatically cleans up resources when removed from the array
|
|
224
|
-
- Handles resource type changes (recreates fiber if type changes)
|
|
225
|
-
|
|
226
|
-
### `tap` and Context Support
|
|
227
|
-
|
|
228
|
-
Create and use context to pass values through resource boundaries without prop drilling.
|
|
229
|
-
|
|
230
|
-
```typescript
|
|
231
|
-
import {
|
|
232
|
-
createResourceContext,
|
|
233
|
-
tap,
|
|
234
|
-
withContextProvider,
|
|
235
|
-
} from "@assistant-ui/tap";
|
|
236
|
-
|
|
237
|
-
const MyContext = createResourceContext(defaultValue);
|
|
238
|
-
|
|
239
|
-
// Provide context
|
|
240
|
-
withContextProvider(MyContext, value, () => {
|
|
241
|
-
// Inside this function, tap can access the value
|
|
242
|
-
});
|
|
243
|
-
|
|
244
|
-
// Access context in a resource
|
|
245
|
-
const value = tap(MyContext);
|
|
246
|
-
```
|
|
247
|
-
|
|
248
|
-
## Resource Management
|
|
249
|
-
|
|
250
|
-
### `createResourceRoot`
|
|
251
|
-
|
|
252
|
-
Create an instance of a resource. Call `render()` with a resource element to render it and mount effects. Returns a `SubscribableResource` with `getValue()` and `subscribe()`.
|
|
253
|
-
|
|
254
|
-
```typescript
|
|
255
|
-
import { createResourceRoot } from "@assistant-ui/tap";
|
|
256
|
-
|
|
257
|
-
const root = createResourceRoot();
|
|
258
|
-
const counter = root.render(Counter({ incrementBy: 1 }));
|
|
259
|
-
|
|
260
|
-
// Access current value
|
|
261
|
-
console.log(counter.getValue().count);
|
|
262
|
-
|
|
263
|
-
// Subscribe to changes
|
|
264
|
-
const unsubscribe = counter.subscribe(() => {
|
|
265
|
-
console.log("Counter updated:", counter.getValue());
|
|
266
|
-
});
|
|
267
|
-
|
|
268
|
-
// Update props by calling render again
|
|
269
|
-
root.render(Counter({ incrementBy: 2 }));
|
|
270
|
-
|
|
271
|
-
// Cleanup
|
|
272
|
-
root.unmount();
|
|
273
|
-
unsubscribe();
|
|
274
|
-
```
|
|
275
|
-
|
|
276
|
-
## React Integration
|
|
277
|
-
|
|
278
|
-
Use resources directly in React components with the `useResource` hook:
|
|
279
|
-
|
|
280
|
-
```typescript
|
|
48
|
+
```tsx
|
|
281
49
|
import { useResource } from "@assistant-ui/tap/react";
|
|
282
50
|
|
|
283
|
-
function
|
|
284
|
-
const
|
|
285
|
-
return
|
|
286
|
-
<div>
|
|
287
|
-
<p>Count: {state.count}</p>
|
|
288
|
-
<button onClick={state.increment}>Increment</button>
|
|
289
|
-
</div>
|
|
290
|
-
);
|
|
51
|
+
function CounterButton() {
|
|
52
|
+
const { count, increment } = useResource(Counter({ incrementBy: 1 }));
|
|
53
|
+
return <button onClick={increment}>{count}</button>;
|
|
291
54
|
}
|
|
292
55
|
```
|
|
293
56
|
|
|
294
|
-
##
|
|
295
|
-
|
|
296
|
-
### Automatic Cleanup
|
|
297
|
-
|
|
298
|
-
Resources automatically clean up after themselves when unmounted:
|
|
299
|
-
|
|
300
|
-
```typescript
|
|
301
|
-
const WebSocketResource = resource(() => {
|
|
302
|
-
const [messages, setMessages] = tapState<string[]>([]);
|
|
303
|
-
|
|
304
|
-
tapEffect(() => {
|
|
305
|
-
const ws = new WebSocket("ws://localhost:8080");
|
|
306
|
-
|
|
307
|
-
ws.onmessage = (event) => {
|
|
308
|
-
setMessages((prev) => [...prev, event.data]);
|
|
309
|
-
};
|
|
310
|
-
|
|
311
|
-
// Cleanup happens automatically when resource unmounts
|
|
312
|
-
return () => ws.close();
|
|
313
|
-
}, []);
|
|
314
|
-
|
|
315
|
-
return messages;
|
|
316
|
-
});
|
|
317
|
-
```
|
|
318
|
-
|
|
319
|
-
### API Wrapper Pattern
|
|
320
|
-
|
|
321
|
-
A common pattern in assistant-ui is to wrap resource state in a stable API object:
|
|
322
|
-
|
|
323
|
-
```typescript
|
|
324
|
-
export const tapApi = <TApi extends ApiObject & { getState: () => any }>(
|
|
325
|
-
api: TApi,
|
|
326
|
-
) => {
|
|
327
|
-
const ref = tapRef(api);
|
|
328
|
-
|
|
329
|
-
tapEffect(() => {
|
|
330
|
-
ref.current = api;
|
|
331
|
-
});
|
|
332
|
-
|
|
333
|
-
const apiProxy = tapMemo(
|
|
334
|
-
() =>
|
|
335
|
-
new Proxy<TApi>({} as TApi, new ReadonlyApiHandler(() => ref.current)),
|
|
336
|
-
[],
|
|
337
|
-
);
|
|
338
|
-
|
|
339
|
-
return tapMemo(
|
|
340
|
-
() => ({
|
|
341
|
-
state: api.getState(),
|
|
342
|
-
api: apiProxy,
|
|
343
|
-
}),
|
|
344
|
-
[api.getState()],
|
|
345
|
-
);
|
|
346
|
-
};
|
|
347
|
-
```
|
|
348
|
-
|
|
349
|
-
## Use Cases
|
|
350
|
-
|
|
351
|
-
tap is used throughout assistant-ui for:
|
|
352
|
-
|
|
353
|
-
1. **State Management**: Application-wide state without Redux/Zustand
|
|
354
|
-
2. **Event Handling**: Managing event subscriptions and cleanup
|
|
355
|
-
3. **Resource Lifecycle**: Auto-cleanup of WebSockets, timers, subscriptions
|
|
356
|
-
4. **Composition**: Nested resource management (threads, messages, tools)
|
|
357
|
-
5. **Context Injection**: Passing values through resource boundaries without prop drilling
|
|
358
|
-
6. **API Wrapping**: Creating reactive API objects with `getState()` and `subscribe()`
|
|
359
|
-
|
|
360
|
-
### Example: Tools Management
|
|
361
|
-
|
|
362
|
-
```typescript
|
|
363
|
-
export const Tools = resource(({ toolkit }: { toolkit?: Toolkit }) => {
|
|
364
|
-
const [state, setState] = tapState<ToolsState>(() => ({
|
|
365
|
-
tools: {},
|
|
366
|
-
}));
|
|
367
|
-
|
|
368
|
-
const modelContext = tapModelContext();
|
|
369
|
-
|
|
370
|
-
tapEffect(() => {
|
|
371
|
-
if (!toolkit) return;
|
|
372
|
-
|
|
373
|
-
// Register tools and setup subscriptions
|
|
374
|
-
const unsubscribes: (() => void)[] = [];
|
|
375
|
-
// ... registration logic
|
|
376
|
-
|
|
377
|
-
return () => unsubscribes.forEach((fn) => fn());
|
|
378
|
-
}, [toolkit, modelContext]);
|
|
379
|
-
|
|
380
|
-
return tapApi<ToolsApi>({
|
|
381
|
-
getState: () => state,
|
|
382
|
-
setToolUI,
|
|
383
|
-
});
|
|
384
|
-
});
|
|
385
|
-
```
|
|
386
|
-
|
|
387
|
-
## Why tap?
|
|
388
|
-
|
|
389
|
-
- **Reuse React knowledge**: Developers already familiar with hooks can immediately work with tap
|
|
390
|
-
- **Framework flexibility**: Core logic can work outside React components
|
|
391
|
-
- **Automatic cleanup**: No memory leaks from forgotten unsubscribes
|
|
392
|
-
- **Composability**: Resources can nest and combine naturally
|
|
393
|
-
- **Type safety**: Full TypeScript inference for state and APIs
|
|
394
|
-
- **Zero dependencies**: Lightweight and portable
|
|
57
|
+
## Hooks
|
|
395
58
|
|
|
396
|
-
|
|
59
|
+
`tapState`, `tapEffect`, `tapMemo`, `tapCallback`, `tapRef` mirror their React counterparts. Additional primitives include `tapResource` and `tapResources` for composition, plus `createResourceContext` / `tap` / `withContextProvider` for context.
|
|
397
60
|
|
|
398
|
-
|
|
399
|
-
| ------------- | ----------------- | --------- |
|
|
400
|
-
| `useState` | `tapState` | Identical |
|
|
401
|
-
| `useEffect` | `tapEffect` | Identical |
|
|
402
|
-
| `useMemo` | `tapMemo` | Identical |
|
|
403
|
-
| `useCallback` | `tapCallback` | Identical |
|
|
404
|
-
| `useRef` | `tapRef` | Identical |
|
|
61
|
+
Full API reference at [assistant-ui.com/tap/docs](https://www.assistant-ui.com/tap/docs).
|
|
405
62
|
|
|
406
63
|
## License
|
|
407
64
|
|
|
@@ -1,6 +1,10 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
import { RenderResult, Resource, ResourceFiber, ResourceFiberRoot } from "./types.js";
|
|
2
|
+
|
|
3
|
+
//#region src/core/ResourceFiber.d.ts
|
|
4
|
+
declare function createResourceFiber<R, P>(type: Resource<R, P>, root: ResourceFiberRoot, markDirty?: (() => void) | undefined, strictMode?: "root" | "child" | null): ResourceFiber<R, P>;
|
|
5
|
+
declare function unmountResourceFiber<R, P>(fiber: ResourceFiber<R, P>): void;
|
|
6
|
+
declare function renderResourceFiber<R, P>(fiber: ResourceFiber<R, P>, props: P): RenderResult;
|
|
7
|
+
declare function commitResourceFiber<R, P>(fiber: ResourceFiber<R, P>, result: RenderResult): void;
|
|
8
|
+
//#endregion
|
|
9
|
+
export { commitResourceFiber, createResourceFiber, renderResourceFiber, unmountResourceFiber };
|
|
6
10
|
//# sourceMappingURL=ResourceFiber.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ResourceFiber.d.ts","
|
|
1
|
+
{"version":3,"file":"ResourceFiber.d.ts","names":[],"sources":["../../src/core/ResourceFiber.ts"],"mappings":";;;iBAcgB,mBAAA,MAAA,CACd,IAAA,EAAM,QAAA,CAAS,CAAA,EAAG,CAAA,GAClB,IAAA,EAAM,iBAAA,EACN,SAAA,6BACA,UAAA,6BACC,aAAA,CAAc,CAAA,EAAG,CAAA;AAAA,iBAeJ,oBAAA,MAAA,CAA2B,KAAA,EAAO,aAAA,CAAc,CAAA,EAAG,CAAA;AAAA,iBAQnD,mBAAA,MAAA,CACd,KAAA,EAAO,aAAA,CAAc,CAAA,EAAG,CAAA,GACxB,KAAA,EAAO,CAAA,GACN,YAAA;AAAA,iBAmBa,mBAAA,MAAA,CACd,KAAA,EAAO,aAAA,CAAc,CAAA,EAAG,CAAA,GACxB,MAAA,EAAQ,YAAA"}
|
|
@@ -1,52 +1,54 @@
|
|
|
1
|
-
import { commitAllEffects, cleanupAllEffects } from "./helpers/commit.js";
|
|
2
|
-
import { getDevStrictMode, withResourceFiber, } from "./helpers/execution-context.js";
|
|
3
1
|
import { callResourceFn } from "./helpers/callResourceFn.js";
|
|
4
2
|
import { isDevelopment } from "./helpers/env.js";
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
3
|
+
import { getDevStrictMode, withResourceFiber } from "./helpers/execution-context.js";
|
|
4
|
+
import { cleanupAllEffects, commitAllEffects } from "./helpers/commit.js";
|
|
5
|
+
//#region src/core/ResourceFiber.ts
|
|
6
|
+
function createResourceFiber(type, root, markDirty = void 0, strictMode = getDevStrictMode(false)) {
|
|
7
|
+
return {
|
|
8
|
+
type,
|
|
9
|
+
root,
|
|
10
|
+
markDirty,
|
|
11
|
+
devStrictMode: strictMode,
|
|
12
|
+
cells: [],
|
|
13
|
+
currentIndex: 0,
|
|
14
|
+
renderContext: void 0,
|
|
15
|
+
isFirstRender: true,
|
|
16
|
+
isMounted: false,
|
|
17
|
+
isNeverMounted: true
|
|
18
|
+
};
|
|
18
19
|
}
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
cleanupAllEffects(fiber);
|
|
20
|
+
function unmountResourceFiber(fiber) {
|
|
21
|
+
if (!fiber.isMounted) throw new Error("Tried to unmount a fiber that is already unmounted");
|
|
22
|
+
fiber.isMounted = false;
|
|
23
|
+
cleanupAllEffects(fiber);
|
|
24
24
|
}
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
return result;
|
|
25
|
+
function renderResourceFiber(fiber, props) {
|
|
26
|
+
const result = {
|
|
27
|
+
effectTasks: [],
|
|
28
|
+
props,
|
|
29
|
+
output: void 0
|
|
30
|
+
};
|
|
31
|
+
withResourceFiber(fiber, () => {
|
|
32
|
+
fiber.renderContext = result;
|
|
33
|
+
try {
|
|
34
|
+
result.output = callResourceFn(fiber.type, props);
|
|
35
|
+
} finally {
|
|
36
|
+
fiber.renderContext = void 0;
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
return result;
|
|
41
40
|
}
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
41
|
+
function commitResourceFiber(fiber, result) {
|
|
42
|
+
fiber.isMounted = true;
|
|
43
|
+
if (isDevelopment && fiber.isNeverMounted && fiber.devStrictMode === "root") {
|
|
44
|
+
fiber.isNeverMounted = false;
|
|
45
|
+
commitAllEffects(result);
|
|
46
|
+
cleanupAllEffects(fiber);
|
|
47
|
+
}
|
|
48
|
+
fiber.isNeverMounted = false;
|
|
49
|
+
commitAllEffects(result);
|
|
51
50
|
}
|
|
51
|
+
//#endregion
|
|
52
|
+
export { commitResourceFiber, createResourceFiber, renderResourceFiber, unmountResourceFiber };
|
|
53
|
+
|
|
52
54
|
//# sourceMappingURL=ResourceFiber.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ResourceFiber.js","
|
|
1
|
+
{"version":3,"file":"ResourceFiber.js","names":[],"sources":["../../src/core/ResourceFiber.ts"],"sourcesContent":["import type {\n ResourceFiber,\n RenderResult,\n Resource,\n ResourceFiberRoot,\n} from \"./types\";\nimport { commitAllEffects, cleanupAllEffects } from \"./helpers/commit\";\nimport {\n getDevStrictMode,\n withResourceFiber,\n} from \"./helpers/execution-context\";\nimport { callResourceFn } from \"./helpers/callResourceFn\";\nimport { isDevelopment } from \"./helpers/env\";\n\nexport function createResourceFiber<R, P>(\n type: Resource<R, P>,\n root: ResourceFiberRoot,\n markDirty: (() => void) | undefined = undefined,\n strictMode: \"root\" | \"child\" | null = getDevStrictMode(false),\n): ResourceFiber<R, P> {\n return {\n type,\n root,\n markDirty,\n devStrictMode: strictMode,\n cells: [],\n currentIndex: 0,\n renderContext: undefined,\n isFirstRender: true,\n isMounted: false,\n isNeverMounted: true,\n };\n}\n\nexport function unmountResourceFiber<R, P>(fiber: ResourceFiber<R, P>): void {\n if (!fiber.isMounted)\n throw new Error(\"Tried to unmount a fiber that is already unmounted\");\n\n fiber.isMounted = false;\n cleanupAllEffects(fiber);\n}\n\nexport function renderResourceFiber<R, P>(\n fiber: ResourceFiber<R, P>,\n props: P,\n): RenderResult {\n const result = {\n effectTasks: [],\n props,\n output: undefined as R | undefined,\n };\n\n withResourceFiber(fiber, () => {\n fiber.renderContext = result;\n try {\n result.output = callResourceFn(fiber.type, props);\n } finally {\n fiber.renderContext = undefined;\n }\n });\n\n return result;\n}\n\nexport function commitResourceFiber<R, P>(\n fiber: ResourceFiber<R, P>,\n result: RenderResult,\n): void {\n fiber.isMounted = true;\n\n if (isDevelopment && fiber.isNeverMounted && fiber.devStrictMode === \"root\") {\n fiber.isNeverMounted = false;\n\n commitAllEffects(result);\n cleanupAllEffects(fiber);\n }\n\n fiber.isNeverMounted = false;\n commitAllEffects(result);\n}\n"],"mappings":";;;;;AAcA,SAAgB,oBACd,MACA,MACA,YAAsC,KAAA,GACtC,aAAsC,iBAAiB,KAAK,GACvC;CACrB,OAAO;EACL;EACA;EACA;EACA,eAAe;EACf,OAAO,CAAC;EACR,cAAc;EACd,eAAe,KAAA;EACf,eAAe;EACf,WAAW;EACX,gBAAgB;CAClB;AACF;AAEA,SAAgB,qBAA2B,OAAkC;CAC3E,IAAI,CAAC,MAAM,WACT,MAAM,IAAI,MAAM,oDAAoD;CAEtE,MAAM,YAAY;CAClB,kBAAkB,KAAK;AACzB;AAEA,SAAgB,oBACd,OACA,OACc;CACd,MAAM,SAAS;EACb,aAAa,CAAC;EACd;EACA,QAAQ,KAAA;CACV;CAEA,kBAAkB,aAAa;EAC7B,MAAM,gBAAgB;EACtB,IAAI;GACF,OAAO,SAAS,eAAe,MAAM,MAAM,KAAK;EAClD,UAAU;GACR,MAAM,gBAAgB,KAAA;EACxB;CACF,CAAC;CAED,OAAO;AACT;AAEA,SAAgB,oBACd,OACA,QACM;CACN,MAAM,YAAY;CAElB,IAAI,iBAAiB,MAAM,kBAAkB,MAAM,kBAAkB,QAAQ;EAC3E,MAAM,iBAAiB;EAEvB,iBAAiB,MAAM;EACvB,kBAAkB,KAAK;CACzB;CAEA,MAAM,iBAAiB;CACvB,iBAAiB,MAAM;AACzB"}
|
package/dist/core/context.d.ts
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
|
+
//#region src/core/context.d.ts
|
|
1
2
|
declare const contextValue: unique symbol;
|
|
2
3
|
type Context<T> = {
|
|
3
|
-
|
|
4
|
+
[contextValue]: T;
|
|
4
5
|
};
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
declare const createResourceContext: <T>(defaultValue: T) => Context<T>;
|
|
7
|
+
declare const withContextProvider: <T, TResult>(context: Context<T>, value: T, fn: () => TResult) => TResult;
|
|
8
|
+
declare const tap: <T>(context: Context<T>) => T;
|
|
9
|
+
//#endregion
|
|
10
|
+
export { createResourceContext, tap, withContextProvider };
|
|
9
11
|
//# sourceMappingURL=context.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"context.d.ts","
|
|
1
|
+
{"version":3,"file":"context.d.ts","names":[],"sources":["../../src/core/context.ts"],"mappings":";cAAM,YAAA;AAAA,KACD,OAAA;EAAA,CACF,YAAY,GAAG,CAAA;AAAA;AAAA,cAGL,qBAAA,MAA4B,YAAA,EAAc,CAAA,KAAI,OAAA,CAAQ,CAAA;AAAA,cAMtD,mBAAA,eACX,OAAA,EAAS,OAAA,CAAQ,CAAA,GACjB,KAAA,EAAO,CAAA,EACP,EAAA,QAAU,OAAA,KAAO,OAAA;AAAA,cAWN,GAAA,MAAU,OAAA,EAAS,OAAA,CAAQ,CAAA,MAAE,CAAA"}
|
package/dist/core/context.js
CHANGED
|
@@ -1,20 +1,21 @@
|
|
|
1
|
+
//#region src/core/context.ts
|
|
1
2
|
const contextValue = Symbol("tap.Context");
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
[contextValue]: defaultValue,
|
|
5
|
-
};
|
|
3
|
+
const createResourceContext = (defaultValue) => {
|
|
4
|
+
return { [contextValue]: defaultValue };
|
|
6
5
|
};
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
}
|
|
6
|
+
const withContextProvider = (context, value, fn) => {
|
|
7
|
+
const previousValue = context[contextValue];
|
|
8
|
+
context[contextValue] = value;
|
|
9
|
+
try {
|
|
10
|
+
return fn();
|
|
11
|
+
} finally {
|
|
12
|
+
context[contextValue] = previousValue;
|
|
13
|
+
}
|
|
16
14
|
};
|
|
17
|
-
|
|
18
|
-
|
|
15
|
+
const tap = (context) => {
|
|
16
|
+
return context[contextValue];
|
|
19
17
|
};
|
|
18
|
+
//#endregion
|
|
19
|
+
export { createResourceContext, tap, withContextProvider };
|
|
20
|
+
|
|
20
21
|
//# sourceMappingURL=context.js.map
|
package/dist/core/context.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"context.js","
|
|
1
|
+
{"version":3,"file":"context.js","names":[],"sources":["../../src/core/context.ts"],"sourcesContent":["const contextValue: unique symbol = Symbol(\"tap.Context\");\ntype Context<T> = {\n [contextValue]: T;\n};\n\nexport const createResourceContext = <T>(defaultValue: T): Context<T> => {\n return {\n [contextValue]: defaultValue,\n };\n};\n\nexport const withContextProvider = <T, TResult>(\n context: Context<T>,\n value: T,\n fn: () => TResult,\n) => {\n const previousValue = context[contextValue];\n context[contextValue] = value;\n try {\n return fn();\n } finally {\n context[contextValue] = previousValue;\n }\n};\n\nexport const tap = <T>(context: Context<T>) => {\n return context[contextValue];\n};\n"],"mappings":";AAAA,MAAM,eAA8B,OAAO,aAAa;AAKxD,MAAa,yBAA4B,iBAAgC;CACvE,OAAO,GACJ,eAAe,aAClB;AACF;AAEA,MAAa,uBACX,SACA,OACA,OACG;CACH,MAAM,gBAAgB,QAAQ;CAC9B,QAAQ,gBAAgB;CACxB,IAAI;EACF,OAAO,GAAG;CACZ,UAAU;EACR,QAAQ,gBAAgB;CAC1B;AACF;AAEA,MAAa,OAAU,YAAwB;CAC7C,OAAO,QAAQ;AACjB"}
|
|
@@ -1,6 +1,10 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
import { ResourceElement } from "./types.js";
|
|
2
|
+
|
|
3
|
+
//#region src/core/createResourceRoot.d.ts
|
|
4
|
+
declare const createResourceRoot: () => {
|
|
5
|
+
render: <R, P>(element: ResourceElement<R, P>) => any;
|
|
6
|
+
unmount: () => void;
|
|
5
7
|
};
|
|
8
|
+
//#endregion
|
|
9
|
+
export { createResourceRoot };
|
|
6
10
|
//# sourceMappingURL=createResourceRoot.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"createResourceRoot.d.ts","
|
|
1
|
+
{"version":3,"file":"createResourceRoot.d.ts","names":[],"sources":["../../src/core/createResourceRoot.ts"],"mappings":";;;cAea,kBAAA;iBAqBI,OAAA,EAAW,eAAA,CAAgB,CAAA,EAAG,CAAA"}
|