@felixfern/tunnel-fn 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +217 -0
- package/dist/index.cjs +207 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +30 -0
- package/dist/index.d.ts +30 -0
- package/dist/index.js +177 -0
- package/dist/index.js.map +1 -0
- package/package.json +56 -0
package/README.md
ADDED
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
# @felixfern/tunnel-fn
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/@felixfern/tunnel-fn)
|
|
4
|
+
|
|
5
|
+
Type-safe function tunneling across React components via context. Call functions registered in distant components without prop drilling.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install @felixfern/tunnel-fn
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
pnpm add @felixfern/tunnel-fn
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
yarn add @felixfern/tunnel-fn
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Requires React >= 16.8.0 as a peer dependency.
|
|
22
|
+
|
|
23
|
+
## Quick Start
|
|
24
|
+
|
|
25
|
+
### 1. Define your tunnel
|
|
26
|
+
|
|
27
|
+
```tsx
|
|
28
|
+
// tunnel.ts
|
|
29
|
+
import { createTunnel } from "@felixfern/tunnel-fn";
|
|
30
|
+
|
|
31
|
+
const { TunnelProvider, useTunnel, useTunnelFunction } = createTunnel<{
|
|
32
|
+
showAlert: (message: string) => void;
|
|
33
|
+
getCount: () => number;
|
|
34
|
+
}>();
|
|
35
|
+
|
|
36
|
+
export { TunnelProvider, useTunnel, useTunnelFunction };
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### 2. Wrap with Provider
|
|
40
|
+
|
|
41
|
+
```tsx
|
|
42
|
+
// App.tsx
|
|
43
|
+
import { TunnelProvider } from "./tunnel";
|
|
44
|
+
|
|
45
|
+
function App() {
|
|
46
|
+
return (
|
|
47
|
+
<TunnelProvider>
|
|
48
|
+
<ComponentA />
|
|
49
|
+
<ComponentB />
|
|
50
|
+
</TunnelProvider>
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### 3. Register functions
|
|
56
|
+
|
|
57
|
+
```tsx
|
|
58
|
+
// ComponentA.tsx
|
|
59
|
+
import { useTunnelFunction } from "./tunnel";
|
|
60
|
+
|
|
61
|
+
function ComponentA() {
|
|
62
|
+
useTunnelFunction("showAlert", (message: string) => {
|
|
63
|
+
alert(message);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
return <div>Component A</div>;
|
|
67
|
+
}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### 4. Call from anywhere
|
|
71
|
+
|
|
72
|
+
```tsx
|
|
73
|
+
// ComponentB.tsx
|
|
74
|
+
import { useTunnel } from "./tunnel";
|
|
75
|
+
|
|
76
|
+
function ComponentB() {
|
|
77
|
+
const { call } = useTunnel();
|
|
78
|
+
|
|
79
|
+
return (
|
|
80
|
+
<button onClick={() => call("showAlert", "Hello from B!")}>
|
|
81
|
+
Trigger Alert
|
|
82
|
+
</button>
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Everything is fully typed — `call("showAlert", 123)` is a type error because `showAlert` expects a `string`.
|
|
88
|
+
|
|
89
|
+
## API
|
|
90
|
+
|
|
91
|
+
### `createTunnel<T>()`
|
|
92
|
+
|
|
93
|
+
Creates a scoped tunnel instance. `T` is a record mapping function names to their signatures.
|
|
94
|
+
|
|
95
|
+
```tsx
|
|
96
|
+
const { TunnelProvider, useTunnel, useTunnelFunction } = createTunnel<{
|
|
97
|
+
onSave: (data: FormData) => void;
|
|
98
|
+
onCancel: () => void;
|
|
99
|
+
validate: (field: string) => boolean;
|
|
100
|
+
}>();
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Returns `{ TunnelProvider, useTunnel, useTunnelFunction }`.
|
|
104
|
+
|
|
105
|
+
### `TunnelProvider`
|
|
106
|
+
|
|
107
|
+
React component that scopes the tunnel. All `useTunnel` and `useTunnelFunction` calls must be within a Provider.
|
|
108
|
+
|
|
109
|
+
```tsx
|
|
110
|
+
<TunnelProvider>{children}</TunnelProvider>
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Multiple Providers create independent scopes — functions registered under one Provider are not visible to another.
|
|
114
|
+
|
|
115
|
+
### `useTunnel()`
|
|
116
|
+
|
|
117
|
+
Hook that returns `{ call, callAsync, has, onReady }` for invoking tunneled functions.
|
|
118
|
+
|
|
119
|
+
```tsx
|
|
120
|
+
const { call, callAsync, has, onReady } = useTunnel();
|
|
121
|
+
|
|
122
|
+
if (has("onSave")) {
|
|
123
|
+
call("onSave", formData);
|
|
124
|
+
}
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
- `call(key, ...args)` — Calls the registered function. Returns `undefined` if no function is registered for that key. In development, logs a warning.
|
|
128
|
+
- `callAsync(key, ...args)` — Like `call`, but returns a `Promise`. If the function isn't registered yet, waits until it is, then calls it. Useful when mount order is uncertain.
|
|
129
|
+
- `has(key)` — Returns `true` if a function is registered for that key.
|
|
130
|
+
- `onReady(key, callback)` — Calls `callback` with the registered function immediately if available, or when it becomes available. Returns an unsubscribe function.
|
|
131
|
+
|
|
132
|
+
All methods are fully typed — keys are autocompleted and arguments are type-checked.
|
|
133
|
+
|
|
134
|
+
#### `callAsync` — handling async functions and deferred calls
|
|
135
|
+
|
|
136
|
+
```tsx
|
|
137
|
+
// Call an async tunnel function and await the result
|
|
138
|
+
const data = await callAsync("fetchData", userId);
|
|
139
|
+
|
|
140
|
+
// Or call a function that hasn't been registered yet — it will wait
|
|
141
|
+
useEffect(() => {
|
|
142
|
+
callAsync("initialize", config).then(() => {
|
|
143
|
+
console.log("initialized");
|
|
144
|
+
});
|
|
145
|
+
}, [callAsync]);
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
#### `onReady` — react to function availability
|
|
149
|
+
|
|
150
|
+
```tsx
|
|
151
|
+
useEffect(() => {
|
|
152
|
+
const unsubscribe = onReady("initialize", (fn) => {
|
|
153
|
+
fn(config);
|
|
154
|
+
});
|
|
155
|
+
return unsubscribe;
|
|
156
|
+
}, [onReady]);
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
### `useTunnelFunction(key, fn)`
|
|
160
|
+
|
|
161
|
+
Hook that registers a function in the tunnel. Automatically unregisters on unmount.
|
|
162
|
+
|
|
163
|
+
```tsx
|
|
164
|
+
useTunnelFunction("onSave", (data: FormData) => {
|
|
165
|
+
console.log(data);
|
|
166
|
+
});
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
Returns the same function passed in, preserving its type.
|
|
170
|
+
|
|
171
|
+
## Multiple Tunnels
|
|
172
|
+
|
|
173
|
+
Create separate tunnels for different concerns:
|
|
174
|
+
|
|
175
|
+
```tsx
|
|
176
|
+
const formTunnel = createTunnel<{
|
|
177
|
+
submit: () => void;
|
|
178
|
+
reset: () => void;
|
|
179
|
+
}>();
|
|
180
|
+
|
|
181
|
+
const navTunnel = createTunnel<{
|
|
182
|
+
navigate: (path: string) => void;
|
|
183
|
+
}>();
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
Each tunnel has its own Provider and hooks — they don't interfere.
|
|
187
|
+
|
|
188
|
+
## When to Use tunnel-fn
|
|
189
|
+
|
|
190
|
+
**Good fit:**
|
|
191
|
+
|
|
192
|
+
- **Sibling communication** — Two components under the same Provider need to talk without lifting state to a common parent.
|
|
193
|
+
- **Scoped imperative actions** — `showToast()`, `openModal()`, `scrollToTop()` — fire-and-forget commands scoped to a subtree.
|
|
194
|
+
- **Decoupled feature modules** — A toolbar triggers actions defined in a content panel, without either knowing about the other's internals.
|
|
195
|
+
- **Plugin-style architectures** — A host component defines the tunnel type, and dynamically-loaded children register implementations.
|
|
196
|
+
|
|
197
|
+
**Not a good fit:**
|
|
198
|
+
|
|
199
|
+
- **Parent → child communication** — Use props. Tunnels are for sideways/upward communication.
|
|
200
|
+
- **Global cross-tree state** — If you need shared state across the entire app, use Zustand, Jotai, or Redux. Tunnels scope to a Provider subtree.
|
|
201
|
+
- **Replacing state management** — Tunnels pass _functions_, not _data_. If you need reactive state, use state management.
|
|
202
|
+
- **High-frequency calls** — Tunnels are ref-based (no re-renders), but rapid polling or animation-frame calls are better served by direct refs or pub/sub.
|
|
203
|
+
|
|
204
|
+
## Dev Warnings
|
|
205
|
+
|
|
206
|
+
In development (`NODE_ENV !== "production"`), tunnel-fn logs warnings to help catch common mistakes:
|
|
207
|
+
|
|
208
|
+
| Warning | Cause | Fix |
|
|
209
|
+
| -------------------------- | ------------------------------------------------------- | -------------------------------------------------------------------------- |
|
|
210
|
+
| **No function registered** | `call("key")` when no component has registered that key | Ensure the registering component is mounted before calling |
|
|
211
|
+
| **Duplicate registration** | Two components register the same key under one Provider | Use separate keys or separate tunnels |
|
|
212
|
+
| **Call during render** | `call()` invoked directly in the component body | Move to an event handler, `useEffect`, or callback |
|
|
213
|
+
| **Registration churn** | A function is re-registered rapidly (>3×/sec) | Wrap the function in `useCallback()` before passing to `useTunnelFunction` |
|
|
214
|
+
|
|
215
|
+
## License
|
|
216
|
+
|
|
217
|
+
ISC - [Felix Fernando](https://github.com/FelixFern)
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/index.ts
|
|
31
|
+
var index_exports = {};
|
|
32
|
+
__export(index_exports, {
|
|
33
|
+
createTunnel: () => createTunnel
|
|
34
|
+
});
|
|
35
|
+
module.exports = __toCommonJS(index_exports);
|
|
36
|
+
|
|
37
|
+
// src/tunnel.tsx
|
|
38
|
+
var React = __toESM(require("react"), 1);
|
|
39
|
+
var import_react = require("react");
|
|
40
|
+
function createTunnel() {
|
|
41
|
+
const TunnelContext = (0, import_react.createContext)(null);
|
|
42
|
+
function TunnelProvider({ children }) {
|
|
43
|
+
const storeRef = (0, import_react.useRef)(/* @__PURE__ */ new Map());
|
|
44
|
+
const listenersRef = (0, import_react.useRef)(/* @__PURE__ */ new Map());
|
|
45
|
+
const isRenderPhaseRef = (0, import_react.useRef)(false);
|
|
46
|
+
const registrationCountRef = (0, import_react.useRef)(/* @__PURE__ */ new Map());
|
|
47
|
+
const churnWarnedRef = (0, import_react.useRef)(/* @__PURE__ */ new Set());
|
|
48
|
+
if (process.env.NODE_ENV !== "production") {
|
|
49
|
+
isRenderPhaseRef.current = true;
|
|
50
|
+
}
|
|
51
|
+
(0, import_react.useLayoutEffect)(() => {
|
|
52
|
+
isRenderPhaseRef.current = false;
|
|
53
|
+
});
|
|
54
|
+
const store = (0, import_react.useRef)({
|
|
55
|
+
register(key, fn) {
|
|
56
|
+
var _a;
|
|
57
|
+
if (process.env.NODE_ENV !== "production") {
|
|
58
|
+
if (storeRef.current.has(key)) {
|
|
59
|
+
console.warn(
|
|
60
|
+
`[tunnel-fn] Duplicate registration for key "${String(key)}". Another component already registered this key. Only the last registration will be active \u2014 this is likely a bug.`
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
const now = Date.now();
|
|
64
|
+
const timestamps = (_a = registrationCountRef.current.get(key)) != null ? _a : [];
|
|
65
|
+
timestamps.push(now);
|
|
66
|
+
const recent = timestamps.filter((t) => now - t < 1e3);
|
|
67
|
+
registrationCountRef.current.set(key, recent);
|
|
68
|
+
if (recent.length > 3 && !churnWarnedRef.current.has(key)) {
|
|
69
|
+
churnWarnedRef.current.add(key);
|
|
70
|
+
console.warn(
|
|
71
|
+
`[tunnel-fn] Function for key "${String(key)}" is being re-registered rapidly (${recent.length} times in <1s). This usually means the function passed to useTunnelFunction is recreated every render. Wrap it in useCallback() to fix this:
|
|
72
|
+
|
|
73
|
+
const handler = useCallback((...) => { ... }, [deps]);
|
|
74
|
+
useTunnelFunction("${String(key)}", handler);`
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
storeRef.current.set(key, fn);
|
|
79
|
+
const listeners = listenersRef.current.get(key);
|
|
80
|
+
if (listeners) {
|
|
81
|
+
listeners.forEach((cb) => cb(fn));
|
|
82
|
+
listenersRef.current.delete(key);
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
unregister(key) {
|
|
86
|
+
storeRef.current.delete(key);
|
|
87
|
+
},
|
|
88
|
+
call(key, ...args) {
|
|
89
|
+
if (process.env.NODE_ENV !== "production") {
|
|
90
|
+
if (isRenderPhaseRef.current) {
|
|
91
|
+
console.warn(
|
|
92
|
+
`[tunnel-fn] call("${String(key)}") was invoked during the render phase. Tunnel functions should only be called from event handlers, useEffect, or callbacks \u2014 never directly during render. This can cause unpredictable behavior.`
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
const fn = storeRef.current.get(key);
|
|
97
|
+
if (!fn) {
|
|
98
|
+
if (process.env.NODE_ENV !== "production") {
|
|
99
|
+
console.warn(
|
|
100
|
+
`[tunnel-fn] No function registered for key "${String(key)}". Make sure a component with useTunnelFunction("${String(key)}", ...) is mounted.`
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
return void 0;
|
|
104
|
+
}
|
|
105
|
+
return fn(...args);
|
|
106
|
+
},
|
|
107
|
+
has(key) {
|
|
108
|
+
return storeRef.current.has(key);
|
|
109
|
+
},
|
|
110
|
+
callAsync(key, ...args) {
|
|
111
|
+
const fn = storeRef.current.get(key);
|
|
112
|
+
if (fn) {
|
|
113
|
+
return Promise.resolve(fn(...args)).then(
|
|
114
|
+
(v) => v
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
return new Promise((resolve) => {
|
|
118
|
+
const listener = (registeredFn) => {
|
|
119
|
+
resolve(
|
|
120
|
+
Promise.resolve(registeredFn(...args)).then(
|
|
121
|
+
(v) => v
|
|
122
|
+
)
|
|
123
|
+
);
|
|
124
|
+
};
|
|
125
|
+
let set = listenersRef.current.get(key);
|
|
126
|
+
if (!set) {
|
|
127
|
+
set = /* @__PURE__ */ new Set();
|
|
128
|
+
listenersRef.current.set(key, set);
|
|
129
|
+
}
|
|
130
|
+
set.add(listener);
|
|
131
|
+
});
|
|
132
|
+
},
|
|
133
|
+
onReady(key, callback) {
|
|
134
|
+
const existing = storeRef.current.get(key);
|
|
135
|
+
if (existing) {
|
|
136
|
+
callback(existing);
|
|
137
|
+
return () => {
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
const listener = (fn) => callback(fn);
|
|
141
|
+
let set = listenersRef.current.get(key);
|
|
142
|
+
if (!set) {
|
|
143
|
+
set = /* @__PURE__ */ new Set();
|
|
144
|
+
listenersRef.current.set(key, set);
|
|
145
|
+
}
|
|
146
|
+
set.add(listener);
|
|
147
|
+
return () => {
|
|
148
|
+
set.delete(listener);
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
}).current;
|
|
152
|
+
return /* @__PURE__ */ React.createElement(TunnelContext.Provider, { value: store }, children);
|
|
153
|
+
}
|
|
154
|
+
function useTunnelStore() {
|
|
155
|
+
const store = (0, import_react.useContext)(TunnelContext);
|
|
156
|
+
if (!store) {
|
|
157
|
+
throw new Error(
|
|
158
|
+
"[tunnel-fn] useTunnel/useTunnelFunction must be used within a <TunnelProvider>. Wrap your component tree with the TunnelProvider from createTunnel()."
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
return store;
|
|
162
|
+
}
|
|
163
|
+
function useTunnel() {
|
|
164
|
+
const store = useTunnelStore();
|
|
165
|
+
const call = (0, import_react.useCallback)(
|
|
166
|
+
(key, ...args) => {
|
|
167
|
+
return store.call(key, ...args);
|
|
168
|
+
},
|
|
169
|
+
[store]
|
|
170
|
+
);
|
|
171
|
+
const callAsync = (0, import_react.useCallback)(
|
|
172
|
+
(key, ...args) => {
|
|
173
|
+
return store.callAsync(key, ...args);
|
|
174
|
+
},
|
|
175
|
+
[store]
|
|
176
|
+
);
|
|
177
|
+
const has = (0, import_react.useCallback)(
|
|
178
|
+
(key) => {
|
|
179
|
+
return store.has(key);
|
|
180
|
+
},
|
|
181
|
+
[store]
|
|
182
|
+
);
|
|
183
|
+
const onReady = (0, import_react.useCallback)(
|
|
184
|
+
(key, callback) => {
|
|
185
|
+
return store.onReady(key, callback);
|
|
186
|
+
},
|
|
187
|
+
[store]
|
|
188
|
+
);
|
|
189
|
+
return { call, callAsync, has, onReady };
|
|
190
|
+
}
|
|
191
|
+
function useTunnelFunction(key, fn) {
|
|
192
|
+
const store = useTunnelStore();
|
|
193
|
+
(0, import_react.useEffect)(() => {
|
|
194
|
+
store.register(key, fn);
|
|
195
|
+
return () => {
|
|
196
|
+
store.unregister(key);
|
|
197
|
+
};
|
|
198
|
+
}, [store, key, fn]);
|
|
199
|
+
return fn;
|
|
200
|
+
}
|
|
201
|
+
return { TunnelProvider, useTunnel, useTunnelFunction };
|
|
202
|
+
}
|
|
203
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
204
|
+
0 && (module.exports = {
|
|
205
|
+
createTunnel
|
|
206
|
+
});
|
|
207
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/tunnel.tsx"],"sourcesContent":["export { createTunnel } from \"./tunnel\";\n","import * as React from \"react\";\nimport {\n\tcreateContext,\n\tuseCallback,\n\tuseContext,\n\tuseEffect,\n\tuseLayoutEffect,\n\tuseRef,\n\ttype ReactNode,\n} from \"react\";\n\ntype TunnelFunctionMap = Record<string, (...args: never[]) => unknown>;\n\ntype Awaited<T> = T extends Promise<infer U> ? U : T;\n\ntype TunnelStore<T extends TunnelFunctionMap> = {\n\tregister<K extends keyof T>(key: K, fn: T[K]): void;\n\tunregister<K extends keyof T>(key: K): void;\n\tcall<K extends keyof T>(\n\t\tkey: K,\n\t\t...args: Parameters<T[K]>\n\t): ReturnType<T[K]> | undefined;\n\tcallAsync<K extends keyof T>(\n\t\tkey: K,\n\t\t...args: Parameters<T[K]>\n\t): Promise<Awaited<ReturnType<T[K]>> | undefined>;\n\thas<K extends keyof T>(key: K): boolean;\n\tonReady<K extends keyof T>(key: K, callback: (fn: T[K]) => void): () => void;\n};\n\n/**\n * Creates a type-safe tunnel for passing functions across components via context.\n *\n * @example\n * ```tsx\n * const { TunnelProvider, useTunnel, useTunnelFunction } = createTunnel<{\n * onSave: (data: string) => void;\n * getCount: () => number;\n * }>();\n * ```\n */\nexport function createTunnel<T extends TunnelFunctionMap>() {\n\tconst TunnelContext = createContext<TunnelStore<T> | null>(null);\n\n\tfunction TunnelProvider({ children }: { children: ReactNode }) {\n\t\tconst storeRef = useRef<Map<keyof T, T[keyof T]>>(new Map());\n\t\tconst listenersRef = useRef<Map<keyof T, Set<(fn: T[keyof T]) => void>>>(new Map());\n\n\t\tconst isRenderPhaseRef = useRef(false);\n\t\tconst registrationCountRef = useRef<Map<keyof T, number[]>>(new Map());\n\t\tconst churnWarnedRef = useRef<Set<keyof T>>(new Set());\n\n\t\t// Track render phase: set true synchronously during render, false in effect\n\t\tif (process.env.NODE_ENV !== \"production\") {\n\t\t\tisRenderPhaseRef.current = true;\n\t\t}\n\n\t\tuseLayoutEffect(() => {\n\t\t\tisRenderPhaseRef.current = false;\n\t\t});\n\n\t\tconst store = useRef<TunnelStore<T>>({\n\t\t\tregister<K extends keyof T>(key: K, fn: T[K]) {\n\t\t\t\tif (process.env.NODE_ENV !== \"production\") {\n\t\t\t\t\t// Warn: duplicate key registration\n\t\t\t\t\tif (storeRef.current.has(key)) {\n\t\t\t\t\t\tconsole.warn(\n\t\t\t\t\t\t\t`[tunnel-fn] Duplicate registration for key \"${String(key)}\". ` +\n\t\t\t\t\t\t\t\t`Another component already registered this key. ` +\n\t\t\t\t\t\t\t\t`Only the last registration will be active — this is likely a bug.`\n\t\t\t\t\t\t);\n\t\t\t\t\t}\n\n\t\t\t\t\t// Track: registration churn (unstable fn reference)\n\t\t\t\t\tconst now = Date.now();\n\t\t\t\t\tconst timestamps = registrationCountRef.current.get(key) ?? [];\n\t\t\t\t\ttimestamps.push(now);\n\t\t\t\t\t// Keep only timestamps from the last second\n\t\t\t\t\tconst recent = timestamps.filter((t) => now - t < 1000);\n\t\t\t\t\tregistrationCountRef.current.set(key, recent);\n\n\t\t\t\t\tif (recent.length > 3 && !churnWarnedRef.current.has(key)) {\n\t\t\t\t\t\tchurnWarnedRef.current.add(key);\n\t\t\t\t\t\tconsole.warn(\n\t\t\t\t\t\t\t`[tunnel-fn] Function for key \"${String(key)}\" is being re-registered rapidly ` +\n\t\t\t\t\t\t\t\t`(${recent.length} times in <1s). This usually means the function passed to ` +\n\t\t\t\t\t\t\t\t`useTunnelFunction is recreated every render. Wrap it in useCallback() to fix this:\\n\\n` +\n\t\t\t\t\t\t\t\t` const handler = useCallback((...) => { ... }, [deps]);\\n` +\n\t\t\t\t\t\t\t\t` useTunnelFunction(\"${String(key)}\", handler);`\n\t\t\t\t\t\t);\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tstoreRef.current.set(key, fn);\n\n\t\t\t\tconst listeners = listenersRef.current.get(key);\n\t\t\t\tif (listeners) {\n\t\t\t\t\tlisteners.forEach((cb) => cb(fn as T[keyof T]));\n\t\t\t\t\tlistenersRef.current.delete(key);\n\t\t\t\t}\n\t\t\t},\n\t\t\tunregister<K extends keyof T>(key: K) {\n\t\t\t\tstoreRef.current.delete(key);\n\t\t\t},\n\t\t\tcall<K extends keyof T>(key: K, ...args: Parameters<T[K]>) {\n\t\t\t\tif (process.env.NODE_ENV !== \"production\") {\n\t\t\t\t\t// Warn: call() during render phase\n\t\t\t\t\tif (isRenderPhaseRef.current) {\n\t\t\t\t\t\tconsole.warn(\n\t\t\t\t\t\t\t`[tunnel-fn] call(\"${String(key)}\") was invoked during the render phase. ` +\n\t\t\t\t\t\t\t\t`Tunnel functions should only be called from event handlers, useEffect, or callbacks — ` +\n\t\t\t\t\t\t\t\t`never directly during render. This can cause unpredictable behavior.`\n\t\t\t\t\t\t);\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tconst fn = storeRef.current.get(key);\n\t\t\t\tif (!fn) {\n\t\t\t\t\tif (process.env.NODE_ENV !== \"production\") {\n\t\t\t\t\t\tconsole.warn(\n\t\t\t\t\t\t\t`[tunnel-fn] No function registered for key \"${String(key)}\". ` +\n\t\t\t\t\t\t\t\t`Make sure a component with useTunnelFunction(\"${String(key)}\", ...) is mounted.`\n\t\t\t\t\t\t);\n\t\t\t\t\t}\n\t\t\t\t\treturn undefined as ReturnType<T[K]> | undefined;\n\t\t\t\t}\n\t\t\t\treturn (fn as T[K])(...args) as ReturnType<T[K]>;\n\t\t\t},\n\t\t\thas<K extends keyof T>(key: K) {\n\t\t\t\treturn storeRef.current.has(key);\n\t\t\t},\n\t\t\tcallAsync<K extends keyof T>(key: K, ...args: Parameters<T[K]>) {\n\t\t\t\tconst fn = storeRef.current.get(key);\n\t\t\t\tif (fn) {\n\t\t\t\t\treturn Promise.resolve((fn as T[K])(...args)).then(\n\t\t\t\t\t\t(v) => v as Awaited<ReturnType<T[K]>>\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t\treturn new Promise<Awaited<ReturnType<T[K]>> | undefined>((resolve) => {\n\t\t\t\t\tconst listener = (registeredFn: T[keyof T]) => {\n\t\t\t\t\t\tresolve(\n\t\t\t\t\t\t\tPromise.resolve((registeredFn as T[K])(...args)).then(\n\t\t\t\t\t\t\t\t(v) => v as Awaited<ReturnType<T[K]>>\n\t\t\t\t\t\t\t) as unknown as Awaited<ReturnType<T[K]>>\n\t\t\t\t\t\t);\n\t\t\t\t\t};\n\t\t\t\t\tlet set = listenersRef.current.get(key);\n\t\t\t\t\tif (!set) {\n\t\t\t\t\t\tset = new Set();\n\t\t\t\t\t\tlistenersRef.current.set(key, set);\n\t\t\t\t\t}\n\t\t\t\t\tset.add(listener);\n\t\t\t\t});\n\t\t\t},\n\t\t\tonReady<K extends keyof T>(key: K, callback: (fn: T[K]) => void) {\n\t\t\t\tconst existing = storeRef.current.get(key);\n\t\t\t\tif (existing) {\n\t\t\t\t\tcallback(existing as T[K]);\n\t\t\t\t\treturn () => {};\n\t\t\t\t}\n\t\t\t\tconst listener = (fn: T[keyof T]) => callback(fn as T[K]);\n\t\t\t\tlet set = listenersRef.current.get(key);\n\t\t\t\tif (!set) {\n\t\t\t\t\tset = new Set();\n\t\t\t\t\tlistenersRef.current.set(key, set);\n\t\t\t\t}\n\t\t\t\tset.add(listener);\n\t\t\t\treturn () => {\n\t\t\t\t\tset!.delete(listener);\n\t\t\t\t};\n\t\t\t},\n\t\t}).current;\n\n\t\treturn (\n\t\t\t<TunnelContext.Provider value={store}>\n\t\t\t\t{children}\n\t\t\t</TunnelContext.Provider>\n\t\t);\n\t}\n\n\tfunction useTunnelStore(): TunnelStore<T> {\n\t\tconst store = useContext(TunnelContext);\n\t\tif (!store) {\n\t\t\tthrow new Error(\n\t\t\t\t\"[tunnel-fn] useTunnel/useTunnelFunction must be used within a <TunnelProvider>. \" +\n\t\t\t\t\t\"Wrap your component tree with the TunnelProvider from createTunnel().\"\n\t\t\t);\n\t\t}\n\t\treturn store;\n\t}\n\n\tfunction useTunnel() {\n\t\tconst store = useTunnelStore();\n\n\t\tconst call = useCallback(\n\t\t\t<K extends keyof T>(key: K, ...args: Parameters<T[K]>): ReturnType<T[K]> | undefined => {\n\t\t\t\treturn store.call(key, ...args);\n\t\t\t},\n\t\t\t[store]\n\t\t);\n\n\t\tconst callAsync = useCallback(\n\t\t\t<K extends keyof T>(key: K, ...args: Parameters<T[K]>): Promise<Awaited<ReturnType<T[K]>> | undefined> => {\n\t\t\t\treturn store.callAsync(key, ...args);\n\t\t\t},\n\t\t\t[store]\n\t\t);\n\n\t\tconst has = useCallback(\n\t\t\t<K extends keyof T>(key: K): boolean => {\n\t\t\t\treturn store.has(key);\n\t\t\t},\n\t\t\t[store]\n\t\t);\n\n\t\tconst onReady = useCallback(\n\t\t\t<K extends keyof T>(key: K, callback: (fn: T[K]) => void): (() => void) => {\n\t\t\t\treturn store.onReady(key, callback);\n\t\t\t},\n\t\t\t[store]\n\t\t);\n\n\t\treturn { call, callAsync, has, onReady } as const;\n\t}\n\n\tfunction useTunnelFunction<K extends keyof T>(key: K, fn: T[K]): T[K] {\n\t\tconst store = useTunnelStore();\n\n\t\tuseEffect(() => {\n\t\t\tstore.register(key, fn);\n\t\t\treturn () => {\n\t\t\t\tstore.unregister(key);\n\t\t\t};\n\t\t}, [store, key, fn]);\n\n\t\treturn fn;\n\t}\n\n\treturn { TunnelProvider, useTunnel, useTunnelFunction } as const;\n}\n"],"mappings":"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","names":[]}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { ReactNode } from 'react';
|
|
3
|
+
|
|
4
|
+
type TunnelFunctionMap = Record<string, (...args: never[]) => unknown>;
|
|
5
|
+
type Awaited<T> = T extends Promise<infer U> ? U : T;
|
|
6
|
+
/**
|
|
7
|
+
* Creates a type-safe tunnel for passing functions across components via context.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```tsx
|
|
11
|
+
* const { TunnelProvider, useTunnel, useTunnelFunction } = createTunnel<{
|
|
12
|
+
* onSave: (data: string) => void;
|
|
13
|
+
* getCount: () => number;
|
|
14
|
+
* }>();
|
|
15
|
+
* ```
|
|
16
|
+
*/
|
|
17
|
+
declare function createTunnel<T extends TunnelFunctionMap>(): {
|
|
18
|
+
readonly TunnelProvider: ({ children }: {
|
|
19
|
+
children: ReactNode;
|
|
20
|
+
}) => React.JSX.Element;
|
|
21
|
+
readonly useTunnel: () => {
|
|
22
|
+
readonly call: <K extends keyof T>(key: K, ...args: Parameters<T[K]>) => ReturnType<T[K]> | undefined;
|
|
23
|
+
readonly callAsync: <K extends keyof T>(key: K, ...args: Parameters<T[K]>) => Promise<Awaited<ReturnType<T[K]>> | undefined>;
|
|
24
|
+
readonly has: <K extends keyof T>(key: K) => boolean;
|
|
25
|
+
readonly onReady: <K extends keyof T>(key: K, callback: (fn: T[K]) => void) => (() => void);
|
|
26
|
+
};
|
|
27
|
+
readonly useTunnelFunction: <K extends keyof T>(key: K, fn: T[K]) => T[K];
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export { createTunnel };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { ReactNode } from 'react';
|
|
3
|
+
|
|
4
|
+
type TunnelFunctionMap = Record<string, (...args: never[]) => unknown>;
|
|
5
|
+
type Awaited<T> = T extends Promise<infer U> ? U : T;
|
|
6
|
+
/**
|
|
7
|
+
* Creates a type-safe tunnel for passing functions across components via context.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```tsx
|
|
11
|
+
* const { TunnelProvider, useTunnel, useTunnelFunction } = createTunnel<{
|
|
12
|
+
* onSave: (data: string) => void;
|
|
13
|
+
* getCount: () => number;
|
|
14
|
+
* }>();
|
|
15
|
+
* ```
|
|
16
|
+
*/
|
|
17
|
+
declare function createTunnel<T extends TunnelFunctionMap>(): {
|
|
18
|
+
readonly TunnelProvider: ({ children }: {
|
|
19
|
+
children: ReactNode;
|
|
20
|
+
}) => React.JSX.Element;
|
|
21
|
+
readonly useTunnel: () => {
|
|
22
|
+
readonly call: <K extends keyof T>(key: K, ...args: Parameters<T[K]>) => ReturnType<T[K]> | undefined;
|
|
23
|
+
readonly callAsync: <K extends keyof T>(key: K, ...args: Parameters<T[K]>) => Promise<Awaited<ReturnType<T[K]>> | undefined>;
|
|
24
|
+
readonly has: <K extends keyof T>(key: K) => boolean;
|
|
25
|
+
readonly onReady: <K extends keyof T>(key: K, callback: (fn: T[K]) => void) => (() => void);
|
|
26
|
+
};
|
|
27
|
+
readonly useTunnelFunction: <K extends keyof T>(key: K, fn: T[K]) => T[K];
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export { createTunnel };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
// src/tunnel.tsx
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
import {
|
|
4
|
+
createContext,
|
|
5
|
+
useCallback,
|
|
6
|
+
useContext,
|
|
7
|
+
useEffect,
|
|
8
|
+
useLayoutEffect,
|
|
9
|
+
useRef
|
|
10
|
+
} from "react";
|
|
11
|
+
function createTunnel() {
|
|
12
|
+
const TunnelContext = createContext(null);
|
|
13
|
+
function TunnelProvider({ children }) {
|
|
14
|
+
const storeRef = useRef(/* @__PURE__ */ new Map());
|
|
15
|
+
const listenersRef = useRef(/* @__PURE__ */ new Map());
|
|
16
|
+
const isRenderPhaseRef = useRef(false);
|
|
17
|
+
const registrationCountRef = useRef(/* @__PURE__ */ new Map());
|
|
18
|
+
const churnWarnedRef = useRef(/* @__PURE__ */ new Set());
|
|
19
|
+
if (process.env.NODE_ENV !== "production") {
|
|
20
|
+
isRenderPhaseRef.current = true;
|
|
21
|
+
}
|
|
22
|
+
useLayoutEffect(() => {
|
|
23
|
+
isRenderPhaseRef.current = false;
|
|
24
|
+
});
|
|
25
|
+
const store = useRef({
|
|
26
|
+
register(key, fn) {
|
|
27
|
+
var _a;
|
|
28
|
+
if (process.env.NODE_ENV !== "production") {
|
|
29
|
+
if (storeRef.current.has(key)) {
|
|
30
|
+
console.warn(
|
|
31
|
+
`[tunnel-fn] Duplicate registration for key "${String(key)}". Another component already registered this key. Only the last registration will be active \u2014 this is likely a bug.`
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
const now = Date.now();
|
|
35
|
+
const timestamps = (_a = registrationCountRef.current.get(key)) != null ? _a : [];
|
|
36
|
+
timestamps.push(now);
|
|
37
|
+
const recent = timestamps.filter((t) => now - t < 1e3);
|
|
38
|
+
registrationCountRef.current.set(key, recent);
|
|
39
|
+
if (recent.length > 3 && !churnWarnedRef.current.has(key)) {
|
|
40
|
+
churnWarnedRef.current.add(key);
|
|
41
|
+
console.warn(
|
|
42
|
+
`[tunnel-fn] Function for key "${String(key)}" is being re-registered rapidly (${recent.length} times in <1s). This usually means the function passed to useTunnelFunction is recreated every render. Wrap it in useCallback() to fix this:
|
|
43
|
+
|
|
44
|
+
const handler = useCallback((...) => { ... }, [deps]);
|
|
45
|
+
useTunnelFunction("${String(key)}", handler);`
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
storeRef.current.set(key, fn);
|
|
50
|
+
const listeners = listenersRef.current.get(key);
|
|
51
|
+
if (listeners) {
|
|
52
|
+
listeners.forEach((cb) => cb(fn));
|
|
53
|
+
listenersRef.current.delete(key);
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
unregister(key) {
|
|
57
|
+
storeRef.current.delete(key);
|
|
58
|
+
},
|
|
59
|
+
call(key, ...args) {
|
|
60
|
+
if (process.env.NODE_ENV !== "production") {
|
|
61
|
+
if (isRenderPhaseRef.current) {
|
|
62
|
+
console.warn(
|
|
63
|
+
`[tunnel-fn] call("${String(key)}") was invoked during the render phase. Tunnel functions should only be called from event handlers, useEffect, or callbacks \u2014 never directly during render. This can cause unpredictable behavior.`
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
const fn = storeRef.current.get(key);
|
|
68
|
+
if (!fn) {
|
|
69
|
+
if (process.env.NODE_ENV !== "production") {
|
|
70
|
+
console.warn(
|
|
71
|
+
`[tunnel-fn] No function registered for key "${String(key)}". Make sure a component with useTunnelFunction("${String(key)}", ...) is mounted.`
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
return void 0;
|
|
75
|
+
}
|
|
76
|
+
return fn(...args);
|
|
77
|
+
},
|
|
78
|
+
has(key) {
|
|
79
|
+
return storeRef.current.has(key);
|
|
80
|
+
},
|
|
81
|
+
callAsync(key, ...args) {
|
|
82
|
+
const fn = storeRef.current.get(key);
|
|
83
|
+
if (fn) {
|
|
84
|
+
return Promise.resolve(fn(...args)).then(
|
|
85
|
+
(v) => v
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
return new Promise((resolve) => {
|
|
89
|
+
const listener = (registeredFn) => {
|
|
90
|
+
resolve(
|
|
91
|
+
Promise.resolve(registeredFn(...args)).then(
|
|
92
|
+
(v) => v
|
|
93
|
+
)
|
|
94
|
+
);
|
|
95
|
+
};
|
|
96
|
+
let set = listenersRef.current.get(key);
|
|
97
|
+
if (!set) {
|
|
98
|
+
set = /* @__PURE__ */ new Set();
|
|
99
|
+
listenersRef.current.set(key, set);
|
|
100
|
+
}
|
|
101
|
+
set.add(listener);
|
|
102
|
+
});
|
|
103
|
+
},
|
|
104
|
+
onReady(key, callback) {
|
|
105
|
+
const existing = storeRef.current.get(key);
|
|
106
|
+
if (existing) {
|
|
107
|
+
callback(existing);
|
|
108
|
+
return () => {
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
const listener = (fn) => callback(fn);
|
|
112
|
+
let set = listenersRef.current.get(key);
|
|
113
|
+
if (!set) {
|
|
114
|
+
set = /* @__PURE__ */ new Set();
|
|
115
|
+
listenersRef.current.set(key, set);
|
|
116
|
+
}
|
|
117
|
+
set.add(listener);
|
|
118
|
+
return () => {
|
|
119
|
+
set.delete(listener);
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
}).current;
|
|
123
|
+
return /* @__PURE__ */ React.createElement(TunnelContext.Provider, { value: store }, children);
|
|
124
|
+
}
|
|
125
|
+
function useTunnelStore() {
|
|
126
|
+
const store = useContext(TunnelContext);
|
|
127
|
+
if (!store) {
|
|
128
|
+
throw new Error(
|
|
129
|
+
"[tunnel-fn] useTunnel/useTunnelFunction must be used within a <TunnelProvider>. Wrap your component tree with the TunnelProvider from createTunnel()."
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
return store;
|
|
133
|
+
}
|
|
134
|
+
function useTunnel() {
|
|
135
|
+
const store = useTunnelStore();
|
|
136
|
+
const call = useCallback(
|
|
137
|
+
(key, ...args) => {
|
|
138
|
+
return store.call(key, ...args);
|
|
139
|
+
},
|
|
140
|
+
[store]
|
|
141
|
+
);
|
|
142
|
+
const callAsync = useCallback(
|
|
143
|
+
(key, ...args) => {
|
|
144
|
+
return store.callAsync(key, ...args);
|
|
145
|
+
},
|
|
146
|
+
[store]
|
|
147
|
+
);
|
|
148
|
+
const has = useCallback(
|
|
149
|
+
(key) => {
|
|
150
|
+
return store.has(key);
|
|
151
|
+
},
|
|
152
|
+
[store]
|
|
153
|
+
);
|
|
154
|
+
const onReady = useCallback(
|
|
155
|
+
(key, callback) => {
|
|
156
|
+
return store.onReady(key, callback);
|
|
157
|
+
},
|
|
158
|
+
[store]
|
|
159
|
+
);
|
|
160
|
+
return { call, callAsync, has, onReady };
|
|
161
|
+
}
|
|
162
|
+
function useTunnelFunction(key, fn) {
|
|
163
|
+
const store = useTunnelStore();
|
|
164
|
+
useEffect(() => {
|
|
165
|
+
store.register(key, fn);
|
|
166
|
+
return () => {
|
|
167
|
+
store.unregister(key);
|
|
168
|
+
};
|
|
169
|
+
}, [store, key, fn]);
|
|
170
|
+
return fn;
|
|
171
|
+
}
|
|
172
|
+
return { TunnelProvider, useTunnel, useTunnelFunction };
|
|
173
|
+
}
|
|
174
|
+
export {
|
|
175
|
+
createTunnel
|
|
176
|
+
};
|
|
177
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/tunnel.tsx"],"sourcesContent":["import * as React from \"react\";\nimport {\n\tcreateContext,\n\tuseCallback,\n\tuseContext,\n\tuseEffect,\n\tuseLayoutEffect,\n\tuseRef,\n\ttype ReactNode,\n} from \"react\";\n\ntype TunnelFunctionMap = Record<string, (...args: never[]) => unknown>;\n\ntype Awaited<T> = T extends Promise<infer U> ? U : T;\n\ntype TunnelStore<T extends TunnelFunctionMap> = {\n\tregister<K extends keyof T>(key: K, fn: T[K]): void;\n\tunregister<K extends keyof T>(key: K): void;\n\tcall<K extends keyof T>(\n\t\tkey: K,\n\t\t...args: Parameters<T[K]>\n\t): ReturnType<T[K]> | undefined;\n\tcallAsync<K extends keyof T>(\n\t\tkey: K,\n\t\t...args: Parameters<T[K]>\n\t): Promise<Awaited<ReturnType<T[K]>> | undefined>;\n\thas<K extends keyof T>(key: K): boolean;\n\tonReady<K extends keyof T>(key: K, callback: (fn: T[K]) => void): () => void;\n};\n\n/**\n * Creates a type-safe tunnel for passing functions across components via context.\n *\n * @example\n * ```tsx\n * const { TunnelProvider, useTunnel, useTunnelFunction } = createTunnel<{\n * onSave: (data: string) => void;\n * getCount: () => number;\n * }>();\n * ```\n */\nexport function createTunnel<T extends TunnelFunctionMap>() {\n\tconst TunnelContext = createContext<TunnelStore<T> | null>(null);\n\n\tfunction TunnelProvider({ children }: { children: ReactNode }) {\n\t\tconst storeRef = useRef<Map<keyof T, T[keyof T]>>(new Map());\n\t\tconst listenersRef = useRef<Map<keyof T, Set<(fn: T[keyof T]) => void>>>(new Map());\n\n\t\tconst isRenderPhaseRef = useRef(false);\n\t\tconst registrationCountRef = useRef<Map<keyof T, number[]>>(new Map());\n\t\tconst churnWarnedRef = useRef<Set<keyof T>>(new Set());\n\n\t\t// Track render phase: set true synchronously during render, false in effect\n\t\tif (process.env.NODE_ENV !== \"production\") {\n\t\t\tisRenderPhaseRef.current = true;\n\t\t}\n\n\t\tuseLayoutEffect(() => {\n\t\t\tisRenderPhaseRef.current = false;\n\t\t});\n\n\t\tconst store = useRef<TunnelStore<T>>({\n\t\t\tregister<K extends keyof T>(key: K, fn: T[K]) {\n\t\t\t\tif (process.env.NODE_ENV !== \"production\") {\n\t\t\t\t\t// Warn: duplicate key registration\n\t\t\t\t\tif (storeRef.current.has(key)) {\n\t\t\t\t\t\tconsole.warn(\n\t\t\t\t\t\t\t`[tunnel-fn] Duplicate registration for key \"${String(key)}\". ` +\n\t\t\t\t\t\t\t\t`Another component already registered this key. ` +\n\t\t\t\t\t\t\t\t`Only the last registration will be active — this is likely a bug.`\n\t\t\t\t\t\t);\n\t\t\t\t\t}\n\n\t\t\t\t\t// Track: registration churn (unstable fn reference)\n\t\t\t\t\tconst now = Date.now();\n\t\t\t\t\tconst timestamps = registrationCountRef.current.get(key) ?? [];\n\t\t\t\t\ttimestamps.push(now);\n\t\t\t\t\t// Keep only timestamps from the last second\n\t\t\t\t\tconst recent = timestamps.filter((t) => now - t < 1000);\n\t\t\t\t\tregistrationCountRef.current.set(key, recent);\n\n\t\t\t\t\tif (recent.length > 3 && !churnWarnedRef.current.has(key)) {\n\t\t\t\t\t\tchurnWarnedRef.current.add(key);\n\t\t\t\t\t\tconsole.warn(\n\t\t\t\t\t\t\t`[tunnel-fn] Function for key \"${String(key)}\" is being re-registered rapidly ` +\n\t\t\t\t\t\t\t\t`(${recent.length} times in <1s). This usually means the function passed to ` +\n\t\t\t\t\t\t\t\t`useTunnelFunction is recreated every render. Wrap it in useCallback() to fix this:\\n\\n` +\n\t\t\t\t\t\t\t\t` const handler = useCallback((...) => { ... }, [deps]);\\n` +\n\t\t\t\t\t\t\t\t` useTunnelFunction(\"${String(key)}\", handler);`\n\t\t\t\t\t\t);\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tstoreRef.current.set(key, fn);\n\n\t\t\t\tconst listeners = listenersRef.current.get(key);\n\t\t\t\tif (listeners) {\n\t\t\t\t\tlisteners.forEach((cb) => cb(fn as T[keyof T]));\n\t\t\t\t\tlistenersRef.current.delete(key);\n\t\t\t\t}\n\t\t\t},\n\t\t\tunregister<K extends keyof T>(key: K) {\n\t\t\t\tstoreRef.current.delete(key);\n\t\t\t},\n\t\t\tcall<K extends keyof T>(key: K, ...args: Parameters<T[K]>) {\n\t\t\t\tif (process.env.NODE_ENV !== \"production\") {\n\t\t\t\t\t// Warn: call() during render phase\n\t\t\t\t\tif (isRenderPhaseRef.current) {\n\t\t\t\t\t\tconsole.warn(\n\t\t\t\t\t\t\t`[tunnel-fn] call(\"${String(key)}\") was invoked during the render phase. ` +\n\t\t\t\t\t\t\t\t`Tunnel functions should only be called from event handlers, useEffect, or callbacks — ` +\n\t\t\t\t\t\t\t\t`never directly during render. This can cause unpredictable behavior.`\n\t\t\t\t\t\t);\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tconst fn = storeRef.current.get(key);\n\t\t\t\tif (!fn) {\n\t\t\t\t\tif (process.env.NODE_ENV !== \"production\") {\n\t\t\t\t\t\tconsole.warn(\n\t\t\t\t\t\t\t`[tunnel-fn] No function registered for key \"${String(key)}\". ` +\n\t\t\t\t\t\t\t\t`Make sure a component with useTunnelFunction(\"${String(key)}\", ...) is mounted.`\n\t\t\t\t\t\t);\n\t\t\t\t\t}\n\t\t\t\t\treturn undefined as ReturnType<T[K]> | undefined;\n\t\t\t\t}\n\t\t\t\treturn (fn as T[K])(...args) as ReturnType<T[K]>;\n\t\t\t},\n\t\t\thas<K extends keyof T>(key: K) {\n\t\t\t\treturn storeRef.current.has(key);\n\t\t\t},\n\t\t\tcallAsync<K extends keyof T>(key: K, ...args: Parameters<T[K]>) {\n\t\t\t\tconst fn = storeRef.current.get(key);\n\t\t\t\tif (fn) {\n\t\t\t\t\treturn Promise.resolve((fn as T[K])(...args)).then(\n\t\t\t\t\t\t(v) => v as Awaited<ReturnType<T[K]>>\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t\treturn new Promise<Awaited<ReturnType<T[K]>> | undefined>((resolve) => {\n\t\t\t\t\tconst listener = (registeredFn: T[keyof T]) => {\n\t\t\t\t\t\tresolve(\n\t\t\t\t\t\t\tPromise.resolve((registeredFn as T[K])(...args)).then(\n\t\t\t\t\t\t\t\t(v) => v as Awaited<ReturnType<T[K]>>\n\t\t\t\t\t\t\t) as unknown as Awaited<ReturnType<T[K]>>\n\t\t\t\t\t\t);\n\t\t\t\t\t};\n\t\t\t\t\tlet set = listenersRef.current.get(key);\n\t\t\t\t\tif (!set) {\n\t\t\t\t\t\tset = new Set();\n\t\t\t\t\t\tlistenersRef.current.set(key, set);\n\t\t\t\t\t}\n\t\t\t\t\tset.add(listener);\n\t\t\t\t});\n\t\t\t},\n\t\t\tonReady<K extends keyof T>(key: K, callback: (fn: T[K]) => void) {\n\t\t\t\tconst existing = storeRef.current.get(key);\n\t\t\t\tif (existing) {\n\t\t\t\t\tcallback(existing as T[K]);\n\t\t\t\t\treturn () => {};\n\t\t\t\t}\n\t\t\t\tconst listener = (fn: T[keyof T]) => callback(fn as T[K]);\n\t\t\t\tlet set = listenersRef.current.get(key);\n\t\t\t\tif (!set) {\n\t\t\t\t\tset = new Set();\n\t\t\t\t\tlistenersRef.current.set(key, set);\n\t\t\t\t}\n\t\t\t\tset.add(listener);\n\t\t\t\treturn () => {\n\t\t\t\t\tset!.delete(listener);\n\t\t\t\t};\n\t\t\t},\n\t\t}).current;\n\n\t\treturn (\n\t\t\t<TunnelContext.Provider value={store}>\n\t\t\t\t{children}\n\t\t\t</TunnelContext.Provider>\n\t\t);\n\t}\n\n\tfunction useTunnelStore(): TunnelStore<T> {\n\t\tconst store = useContext(TunnelContext);\n\t\tif (!store) {\n\t\t\tthrow new Error(\n\t\t\t\t\"[tunnel-fn] useTunnel/useTunnelFunction must be used within a <TunnelProvider>. \" +\n\t\t\t\t\t\"Wrap your component tree with the TunnelProvider from createTunnel().\"\n\t\t\t);\n\t\t}\n\t\treturn store;\n\t}\n\n\tfunction useTunnel() {\n\t\tconst store = useTunnelStore();\n\n\t\tconst call = useCallback(\n\t\t\t<K extends keyof T>(key: K, ...args: Parameters<T[K]>): ReturnType<T[K]> | undefined => {\n\t\t\t\treturn store.call(key, ...args);\n\t\t\t},\n\t\t\t[store]\n\t\t);\n\n\t\tconst callAsync = useCallback(\n\t\t\t<K extends keyof T>(key: K, ...args: Parameters<T[K]>): Promise<Awaited<ReturnType<T[K]>> | undefined> => {\n\t\t\t\treturn store.callAsync(key, ...args);\n\t\t\t},\n\t\t\t[store]\n\t\t);\n\n\t\tconst has = useCallback(\n\t\t\t<K extends keyof T>(key: K): boolean => {\n\t\t\t\treturn store.has(key);\n\t\t\t},\n\t\t\t[store]\n\t\t);\n\n\t\tconst onReady = useCallback(\n\t\t\t<K extends keyof T>(key: K, callback: (fn: T[K]) => void): (() => void) => {\n\t\t\t\treturn store.onReady(key, callback);\n\t\t\t},\n\t\t\t[store]\n\t\t);\n\n\t\treturn { call, callAsync, has, onReady } as const;\n\t}\n\n\tfunction useTunnelFunction<K extends keyof T>(key: K, fn: T[K]): T[K] {\n\t\tconst store = useTunnelStore();\n\n\t\tuseEffect(() => {\n\t\t\tstore.register(key, fn);\n\t\t\treturn () => {\n\t\t\t\tstore.unregister(key);\n\t\t\t};\n\t\t}, [store, key, fn]);\n\n\t\treturn fn;\n\t}\n\n\treturn { TunnelProvider, useTunnel, useTunnelFunction } as const;\n}\n"],"mappings":";AAAA,YAAY,WAAW;AACvB;AAAA,EACC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAEM;AAgCA,SAAS,eAA4C;AAC3D,QAAM,gBAAgB,cAAqC,IAAI;AAE/D,WAAS,eAAe,EAAE,SAAS,GAA4B;AAC9D,UAAM,WAAW,OAAiC,oBAAI,IAAI,CAAC;AAC3D,UAAM,eAAe,OAAoD,oBAAI,IAAI,CAAC;AAElF,UAAM,mBAAmB,OAAO,KAAK;AACrC,UAAM,uBAAuB,OAA+B,oBAAI,IAAI,CAAC;AACrE,UAAM,iBAAiB,OAAqB,oBAAI,IAAI,CAAC;AAGrD,QAAI,QAAQ,IAAI,aAAa,cAAc;AAC1C,uBAAiB,UAAU;AAAA,IAC5B;AAEA,oBAAgB,MAAM;AACrB,uBAAiB,UAAU;AAAA,IAC5B,CAAC;AAED,UAAM,QAAQ,OAAuB;AAAA,MACpC,SAA4B,KAAQ,IAAU;AA9DjD;AA+DI,YAAI,QAAQ,IAAI,aAAa,cAAc;AAE1C,cAAI,SAAS,QAAQ,IAAI,GAAG,GAAG;AAC9B,oBAAQ;AAAA,cACP,+CAA+C,OAAO,GAAG,CAAC;AAAA,YAG3D;AAAA,UACD;AAGA,gBAAM,MAAM,KAAK,IAAI;AACrB,gBAAM,cAAa,0BAAqB,QAAQ,IAAI,GAAG,MAApC,YAAyC,CAAC;AAC7D,qBAAW,KAAK,GAAG;AAEnB,gBAAM,SAAS,WAAW,OAAO,CAAC,MAAM,MAAM,IAAI,GAAI;AACtD,+BAAqB,QAAQ,IAAI,KAAK,MAAM;AAE5C,cAAI,OAAO,SAAS,KAAK,CAAC,eAAe,QAAQ,IAAI,GAAG,GAAG;AAC1D,2BAAe,QAAQ,IAAI,GAAG;AAC9B,oBAAQ;AAAA,cACP,iCAAiC,OAAO,GAAG,CAAC,qCACvC,OAAO,MAAM;AAAA;AAAA;AAAA,uBAGO,OAAO,GAAG,CAAC;AAAA,YACrC;AAAA,UACD;AAAA,QACD;AAEA,iBAAS,QAAQ,IAAI,KAAK,EAAE;AAE5B,cAAM,YAAY,aAAa,QAAQ,IAAI,GAAG;AAC9C,YAAI,WAAW;AACd,oBAAU,QAAQ,CAAC,OAAO,GAAG,EAAgB,CAAC;AAC9C,uBAAa,QAAQ,OAAO,GAAG;AAAA,QAChC;AAAA,MACD;AAAA,MACA,WAA8B,KAAQ;AACrC,iBAAS,QAAQ,OAAO,GAAG;AAAA,MAC5B;AAAA,MACA,KAAwB,QAAW,MAAwB;AAC1D,YAAI,QAAQ,IAAI,aAAa,cAAc;AAE1C,cAAI,iBAAiB,SAAS;AAC7B,oBAAQ;AAAA,cACP,qBAAqB,OAAO,GAAG,CAAC;AAAA,YAGjC;AAAA,UACD;AAAA,QACD;AAEA,cAAM,KAAK,SAAS,QAAQ,IAAI,GAAG;AACnC,YAAI,CAAC,IAAI;AACR,cAAI,QAAQ,IAAI,aAAa,cAAc;AAC1C,oBAAQ;AAAA,cACP,+CAA+C,OAAO,GAAG,CAAC,oDACR,OAAO,GAAG,CAAC;AAAA,YAC9D;AAAA,UACD;AACA,iBAAO;AAAA,QACR;AACA,eAAQ,GAAY,GAAG,IAAI;AAAA,MAC5B;AAAA,MACA,IAAuB,KAAQ;AAC9B,eAAO,SAAS,QAAQ,IAAI,GAAG;AAAA,MAChC;AAAA,MACA,UAA6B,QAAW,MAAwB;AAC/D,cAAM,KAAK,SAAS,QAAQ,IAAI,GAAG;AACnC,YAAI,IAAI;AACP,iBAAO,QAAQ,QAAS,GAAY,GAAG,IAAI,CAAC,EAAE;AAAA,YAC7C,CAAC,MAAM;AAAA,UACR;AAAA,QACD;AACA,eAAO,IAAI,QAA+C,CAAC,YAAY;AACtE,gBAAM,WAAW,CAAC,iBAA6B;AAC9C;AAAA,cACC,QAAQ,QAAS,aAAsB,GAAG,IAAI,CAAC,EAAE;AAAA,gBAChD,CAAC,MAAM;AAAA,cACR;AAAA,YACD;AAAA,UACD;AACA,cAAI,MAAM,aAAa,QAAQ,IAAI,GAAG;AACtC,cAAI,CAAC,KAAK;AACT,kBAAM,oBAAI,IAAI;AACd,yBAAa,QAAQ,IAAI,KAAK,GAAG;AAAA,UAClC;AACA,cAAI,IAAI,QAAQ;AAAA,QACjB,CAAC;AAAA,MACF;AAAA,MACA,QAA2B,KAAQ,UAA8B;AAChE,cAAM,WAAW,SAAS,QAAQ,IAAI,GAAG;AACzC,YAAI,UAAU;AACb,mBAAS,QAAgB;AACzB,iBAAO,MAAM;AAAA,UAAC;AAAA,QACf;AACA,cAAM,WAAW,CAAC,OAAmB,SAAS,EAAU;AACxD,YAAI,MAAM,aAAa,QAAQ,IAAI,GAAG;AACtC,YAAI,CAAC,KAAK;AACT,gBAAM,oBAAI,IAAI;AACd,uBAAa,QAAQ,IAAI,KAAK,GAAG;AAAA,QAClC;AACA,YAAI,IAAI,QAAQ;AAChB,eAAO,MAAM;AACZ,cAAK,OAAO,QAAQ;AAAA,QACrB;AAAA,MACD;AAAA,IACD,CAAC,EAAE;AAEH,WACC,oCAAC,cAAc,UAAd,EAAuB,OAAO,SAC7B,QACF;AAAA,EAEF;AAEA,WAAS,iBAAiC;AACzC,UAAM,QAAQ,WAAW,aAAa;AACtC,QAAI,CAAC,OAAO;AACX,YAAM,IAAI;AAAA,QACT;AAAA,MAED;AAAA,IACD;AACA,WAAO;AAAA,EACR;AAEA,WAAS,YAAY;AACpB,UAAM,QAAQ,eAAe;AAE7B,UAAM,OAAO;AAAA,MACZ,CAAoB,QAAW,SAAyD;AACvF,eAAO,MAAM,KAAK,KAAK,GAAG,IAAI;AAAA,MAC/B;AAAA,MACA,CAAC,KAAK;AAAA,IACP;AAEA,UAAM,YAAY;AAAA,MACjB,CAAoB,QAAW,SAA2E;AACzG,eAAO,MAAM,UAAU,KAAK,GAAG,IAAI;AAAA,MACpC;AAAA,MACA,CAAC,KAAK;AAAA,IACP;AAEA,UAAM,MAAM;AAAA,MACX,CAAoB,QAAoB;AACvC,eAAO,MAAM,IAAI,GAAG;AAAA,MACrB;AAAA,MACA,CAAC,KAAK;AAAA,IACP;AAEA,UAAM,UAAU;AAAA,MACf,CAAoB,KAAQ,aAA+C;AAC1E,eAAO,MAAM,QAAQ,KAAK,QAAQ;AAAA,MACnC;AAAA,MACA,CAAC,KAAK;AAAA,IACP;AAEA,WAAO,EAAE,MAAM,WAAW,KAAK,QAAQ;AAAA,EACxC;AAEA,WAAS,kBAAqC,KAAQ,IAAgB;AACrE,UAAM,QAAQ,eAAe;AAE7B,cAAU,MAAM;AACf,YAAM,SAAS,KAAK,EAAE;AACtB,aAAO,MAAM;AACZ,cAAM,WAAW,GAAG;AAAA,MACrB;AAAA,IACD,GAAG,CAAC,OAAO,KAAK,EAAE,CAAC;AAEnB,WAAO;AAAA,EACR;AAEA,SAAO,EAAE,gBAAgB,WAAW,kBAAkB;AACvD;","names":[]}
|
package/package.json
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@felixfern/tunnel-fn",
|
|
3
|
+
"version": "2.0.0",
|
|
4
|
+
"description": "Type-safe function tunneling across React components via context",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.cjs",
|
|
7
|
+
"module": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": {
|
|
12
|
+
"import": "./dist/index.d.ts",
|
|
13
|
+
"require": "./dist/index.d.cts"
|
|
14
|
+
},
|
|
15
|
+
"import": "./dist/index.js",
|
|
16
|
+
"require": "./dist/index.cjs"
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"sideEffects": false,
|
|
20
|
+
"files": [
|
|
21
|
+
"dist"
|
|
22
|
+
],
|
|
23
|
+
"keywords": [
|
|
24
|
+
"react",
|
|
25
|
+
"typescript",
|
|
26
|
+
"type-safe",
|
|
27
|
+
"context",
|
|
28
|
+
"function-passing",
|
|
29
|
+
"callback",
|
|
30
|
+
"tunnel"
|
|
31
|
+
],
|
|
32
|
+
"author": "Felix Fernando",
|
|
33
|
+
"license": "ISC",
|
|
34
|
+
"publishConfig": {
|
|
35
|
+
"access": "public"
|
|
36
|
+
},
|
|
37
|
+
"peerDependencies": {
|
|
38
|
+
"react": ">=16.8.0"
|
|
39
|
+
},
|
|
40
|
+
"devDependencies": {
|
|
41
|
+
"@testing-library/jest-dom": "^6.9.1",
|
|
42
|
+
"@testing-library/react": "^16.3.2",
|
|
43
|
+
"@types/node": "^20.12.13",
|
|
44
|
+
"@types/react": "^19.0.1",
|
|
45
|
+
"@vitejs/plugin-react": "^5.1.4",
|
|
46
|
+
"jsdom": "^28.1.0",
|
|
47
|
+
"tsup": "^8.5.1",
|
|
48
|
+
"typescript": "^5.7.2",
|
|
49
|
+
"vitest": "^4.0.18"
|
|
50
|
+
},
|
|
51
|
+
"scripts": {
|
|
52
|
+
"build": "tsup",
|
|
53
|
+
"test": "vitest run",
|
|
54
|
+
"test:watch": "vitest"
|
|
55
|
+
}
|
|
56
|
+
}
|