@nadrif/thread 0.1.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.
@@ -0,0 +1,268 @@
1
+ /**
2
+ * @file Framework resolver for the thread config system.
3
+ *
4
+ * Maps a framework name (e.g. `'preact'`, `'react'`) to a dynamic
5
+ * `import()` that provides the five hooks the library needs:
6
+ *
7
+ * - **`useState`** — reactive state
8
+ * - **`useEffect`** — side effects and cleanup
9
+ * - **`useRef`** — mutable references
10
+ * - **`useCallback`** — memoized callbacks
11
+ * - **`useMemo`** — memoized values
12
+ *
13
+ * ## How it works
14
+ *
15
+ * Each framework has a **path** (the module to import) and an **extract**
16
+ * function (which normalizes the module's exports into the expected shape).
17
+ *
18
+ * For example:
19
+ * - **Preact** exports hooks directly from `preact/hooks`
20
+ * - **React** exports hooks from `react`
21
+ * - **Svelte 5** uses `svelte/reactivity` with different function names
22
+ * - **Vue 3** uses the composition API with `ref()`, `watchEffect()`, etc.
23
+ *
24
+ * The `extract` function handles these differences, returning a consistent
25
+ * `{ useState, useEffect, useRef, useCallback, useMemo }` shape.
26
+ *
27
+ * ## Adding a new framework
28
+ *
29
+ * To add a new framework:
30
+ * 1. Add an entry to `FRAMEWORK_MAP` with the import path and extract function
31
+ * 2. If the framework doesn't export React-style hooks, write an adapter
32
+ * in the `extract` function that wraps the framework's API
33
+ *
34
+ * @example
35
+ * ```js
36
+ * // Add Solid.js support (already included)
37
+ * const FRAMEWORK_MAP = {
38
+ * solid: {
39
+ * path: 'solid-js',
40
+ * extract: (m) => m, // Solid exports hooks directly
41
+ * },
42
+ * };
43
+ * ```
44
+ *
45
+ * @module config/frameworks
46
+ */
47
+
48
+ // ---------------------------------------------------------------------------
49
+ // Framework map
50
+ // ---------------------------------------------------------------------------
51
+
52
+ /**
53
+ * Mapping of framework names to their import configuration.
54
+ *
55
+ * Each entry has:
56
+ * - `path`: the npm package/module to `import()` (e.g. `'preact/hooks'`)
57
+ * - `extract`: function that receives the imported module and returns
58
+ * a normalized hooks object `{ useState, useEffect, useRef, useCallback, useMemo }`
59
+ *
60
+ * The `extract` function is needed because different frameworks export
61
+ * their hooks differently:
62
+ * - Preact: `import { useState } from 'preact/hooks'`
63
+ * - React: `import { useState } from 'react'`
64
+ * - Svelte: `import { state } from 'svelte/reactivity'`
65
+ * - Vue: `import { ref, watchEffect } from 'vue'`
66
+ *
67
+ * @type {Record<string, { path: string|null, extract: Function|null }>}
68
+ * @private
69
+ */
70
+ const FRAMEWORK_MAP = {
71
+ preact: {
72
+ path: 'preact/hooks',
73
+ extract: (m) => m,
74
+ },
75
+ react: {
76
+ path: 'react',
77
+ extract: (m) => m,
78
+ },
79
+ svelte: {
80
+ // Svelte 5 reactivity primitives — different API shape
81
+ path: 'svelte/reactivity',
82
+ extract: (m) => ({
83
+ // Svelte uses `state()` instead of `useState()`
84
+ useState: m.state ?? m.writable ?? m.ref,
85
+ // Svelte uses `effect()` instead of `useEffect()`
86
+ useEffect: m.effect ?? (() => {}),
87
+ // Svelte's `ref()` returns { value } not { current }
88
+ useRef: m.ref ?? m.state ?? ((v) => ({ current: v })),
89
+ // Svelte doesn't have useCallback — identity function
90
+ useCallback: (fn) => fn,
91
+ // Svelte doesn't have useMemo — call immediately
92
+ useMemo: (fn) => fn(),
93
+ }),
94
+ },
95
+ vue: {
96
+ // Vue 3 Composition API — uses ref() and watchEffect()
97
+ path: 'vue',
98
+ extract: (m) => ({
99
+ // Vue uses `ref()` for reactive state
100
+ useState: m.ref ?? ((v) => ({ value: v, current: v })),
101
+ // Vue uses `watchEffect()` or `watch()`
102
+ useEffect: m.watchEffect ?? m.watch ?? (() => {}),
103
+ // Vue's `ref()` returns { value } not { current }
104
+ useRef: m.ref ?? ((v) => ({ current: v })),
105
+ // Vue doesn't have useCallback — identity function
106
+ useCallback: (fn) => fn,
107
+ // Vue uses `computed()` for memoization
108
+ useMemo: m.computed ?? ((fn) => ({ value: fn() })),
109
+ }),
110
+ },
111
+ solid: {
112
+ // Solid JS exports hooks directly (same API as React)
113
+ path: 'solid-js',
114
+ extract: (m) => m,
115
+ },
116
+ angular: {
117
+ // Angular 16+ uses signals, not React-style hooks.
118
+ // Not yet supported — users should use `custom` with a hook shim.
119
+ path: null,
120
+ extract: null,
121
+ },
122
+ };
123
+
124
+ // ---------------------------------------------------------------------------
125
+ // Required hooks
126
+ // ---------------------------------------------------------------------------
127
+
128
+ /**
129
+ * The five hooks that thread requires from any framework.
130
+ *
131
+ * If a framework's `extract` function returns an object missing any
132
+ * of these, `resolveHooks()` throws a descriptive error.
133
+ *
134
+ * @type {string[]}
135
+ * @private
136
+ */
137
+ const REQUIRED_HOOKS = ['useState', 'useEffect', 'useRef', 'useCallback', 'useMemo'];
138
+
139
+ // ---------------------------------------------------------------------------
140
+ // Public API
141
+ // ---------------------------------------------------------------------------
142
+
143
+ /**
144
+ * Resolve framework hooks by name or custom source.
145
+ *
146
+ * This is the core of the framework switching system. Given a framework
147
+ * name (from the config), it dynamically imports the right module and
148
+ * returns normalized hooks.
149
+ *
150
+ * @param {string} framework
151
+ * Framework name. Must be one of the values in {@link FRAMEWORKS}.
152
+ * @param {Function|null} [customHookSource=null]
153
+ * Optional custom function that returns the hooks object. If provided,
154
+ * it overrides the framework name entirely — no import is performed.
155
+ *
156
+ * @returns {Promise<FrameworkHooks>}
157
+ * A normalized hooks object with `useState`, `useEffect`, `useRef`,
158
+ * `useCallback`, and `useMemo`.
159
+ *
160
+ * @throws {Error} If the framework is unknown, not yet supported, or
161
+ * missing required hooks.
162
+ *
163
+ * @example
164
+ * ```js
165
+ * // Resolve React hooks
166
+ * const hooks = await resolveHooks('react');
167
+ * const { useState, useEffect } = hooks;
168
+ *
169
+ * // Use in a component
170
+ * function Counter() {
171
+ * const [count, setCount] = useState(0);
172
+ * useEffect(() => console.log(count), [count]);
173
+ * }
174
+ * ```
175
+ *
176
+ * @example
177
+ * ```js
178
+ * // Use a custom hook source
179
+ * const hooks = await resolveHooks('custom', async () => {
180
+ * const mod = await import('my-framework/hooks');
181
+ * return {
182
+ * useState: mod.useState,
183
+ * useEffect: mod.useEffect,
184
+ * useRef: mod.useRef,
185
+ * useCallback: mod.useCallback,
186
+ * useMemo: mod.useMemo,
187
+ * };
188
+ * });
189
+ * ```
190
+ *
191
+ * @example
192
+ * ```js
193
+ * // Error handling
194
+ * try {
195
+ * await resolveHooks('angular');
196
+ * } catch (err) {
197
+ * console.error(err.message);
198
+ * // '[thread] Framework "angular" is not supported yet. Use customHookSource to provide your own hooks.'
199
+ * }
200
+ * ```
201
+ */
202
+ export async function resolveHooks(framework, customHookSource = null) {
203
+ // Custom hook source overrides framework lookup
204
+ if (typeof customHookSource === 'function') {
205
+ const hooks = await customHookSource();
206
+ validateHooks(hooks, 'custom');
207
+ return hooks;
208
+ }
209
+
210
+ const entry = FRAMEWORK_MAP[framework];
211
+
212
+ if (!entry) {
213
+ throw new Error(
214
+ `[thread] Unknown framework: "${framework}". ` +
215
+ `Valid options: ${Object.keys(FRAMEWORK_MAP).join(', ')}`
216
+ );
217
+ }
218
+
219
+ if (!entry.path) {
220
+ throw new Error(
221
+ `[thread] Framework "${framework}" is not supported yet. ` +
222
+ `Use customHookSource to provide your own hooks.`
223
+ );
224
+ }
225
+
226
+ const mod = await import(entry.path);
227
+ const hooks = entry.extract(mod);
228
+ validateHooks(hooks, framework);
229
+ return hooks;
230
+ }
231
+
232
+ // ---------------------------------------------------------------------------
233
+ // Internal
234
+ // ---------------------------------------------------------------------------
235
+
236
+ /**
237
+ * Validate that a hooks object has all required hooks.
238
+ *
239
+ * @param {Object} hooks - The hooks object to validate.
240
+ * @param {string} source - Framework name for error messages.
241
+ * @throws {Error} If any required hook is missing.
242
+ * @private
243
+ */
244
+ function validateHooks(hooks, source) {
245
+ if (!hooks || typeof hooks !== 'object') {
246
+ throw new Error(
247
+ `[thread] Framework "${source}" did not return a valid hooks object.`
248
+ );
249
+ }
250
+
251
+ for (const name of REQUIRED_HOOKS) {
252
+ if (typeof hooks[name] !== 'function') {
253
+ throw new Error(
254
+ `[thread] Framework "${source}" is missing required hook: ${name}. ` +
255
+ `Use customHookSource to provide your own implementation.`
256
+ );
257
+ }
258
+ }
259
+ }
260
+
261
+ /**
262
+ * @typedef {Object} FrameworkHooks
263
+ * @property {Function} useState - Create reactive state.
264
+ * @property {Function} useEffect - Run side effects with cleanup.
265
+ * @property {Function} useRef - Create a mutable reference.
266
+ * @property {Function} useCallback - Memoize a callback function.
267
+ * @property {Function} useMemo - Memoize a computed value.
268
+ */
@@ -0,0 +1,256 @@
1
+ /**
2
+ * @file Core config loader for the thread library.
3
+ *
4
+ * This is the central hub of the configuration system. It:
5
+ *
6
+ * 1. **Loads** `thread.config.js` from the user's project root
7
+ * 2. **Merges** it with built-in defaults
8
+ * 3. **Caches** the resolved config for fast access
9
+ * 4. **Resolves** framework hooks and state manager adapters on demand
10
+ *
11
+ * ## How config loading works
12
+ *
13
+ * The config file is loaded **synchronously** using `fs.readFileSync`.
14
+ * This is intentional — the config must be available before any hooks
15
+ * are called (hooks use top-level `await` to resolve the framework).
16
+ *
17
+ * If `thread.config.js` doesn't exist, the library uses built-in defaults
18
+ * (Preact + Zustand). This means **thread works out of the box** with
19
+ * zero configuration.
20
+ *
21
+ * ## Caching
22
+ *
23
+ * - `getConfig()` — caches the resolved config object
24
+ * - `getHooks()` — caches the resolved framework hooks (async import)
25
+ * - `getResolvedAdapter()` — caches the resolved adapter constructors
26
+ *
27
+ * All caches are cleared by `setConfig()`, which should be called during
28
+ * HMR (Hot Module Reload) to pick up config changes.
29
+ *
30
+ * @example
31
+ * ```js
32
+ * // Get the config
33
+ * import { getConfig } from 'thread/config';
34
+ *
35
+ * const config = getConfig();
36
+ * console.log(config.framework); // 'react'
37
+ * console.log(config.gpu.workgroupSize); // 256
38
+ * ```
39
+ *
40
+ * @example
41
+ * ```js
42
+ * // Force re-resolution (e.g. during HMR)
43
+ * import { setConfig, getConfig } from 'thread/config';
44
+ *
45
+ * // User edits thread.config.js...
46
+ * setConfig(); // Clear all caches
47
+ * const fresh = getConfig(); // Re-reads the file
48
+ * ```
49
+ *
50
+ * @module config/index
51
+ */
52
+
53
+ import { mergeWithDefaults, validateConfig } from './schema.js';
54
+ import { resolveHooks } from './frameworks.js';
55
+ import { getAdapter } from './adapters.js';
56
+
57
+ // Re-export for consumers who need direct access
58
+ export { mergeWithDefaults, validateConfig } from './schema.js';
59
+ export { resolveHooks } from './frameworks.js';
60
+ export { getAdapter } from './adapters.js';
61
+ import { loadConfigFile, loadConfigWithOverride, setProgrammaticConfig, getProgrammaticConfig, clearProgrammaticConfig } from './env.js';
62
+
63
+ // ---------------------------------------------------------------------------
64
+ // Cache state
65
+ // ---------------------------------------------------------------------------
66
+
67
+ /** @type {import('../types.js').threadConfig|null} Resolved config cache. */
68
+ let _config = null;
69
+
70
+ /** @type {Object|null} Cached resolved framework hooks. */
71
+ let _hooks = null;
72
+
73
+ /** @type {Object|null} Cached resolved adapter constructors. */
74
+ let _adapter = null;
75
+
76
+ // ---------------------------------------------------------------------------
77
+ // Internal: config file loading (delegated to config/env.js)
78
+ // ---------------------------------------------------------------------------
79
+
80
+ /**
81
+ * Load configuration from the environment.
82
+ *
83
+ * Delegates to the cross-environment config loader which handles
84
+ * Node.js, Bun, Deno, and browser/edge environments. In browser/edge,
85
+ * returns the programmatic config if set, otherwise `null`.
86
+ *
87
+ * @returns {Record<string, any>|null}
88
+ * The parsed config object, or `null` if not found.
89
+ * @private
90
+ */
91
+ function _loadConfig() {
92
+ return loadConfigWithOverride();
93
+ }
94
+
95
+ /**
96
+ * Resolve the full configuration.
97
+ *
98
+ * Loads `thread.config.js` from the project root, validates it, and
99
+ * merges it with defaults. If the file doesn't exist, returns
100
+ * the default config.
101
+ *
102
+ * @returns {import('../types.js').threadConfig}
103
+ * Fully resolved configuration.
104
+ * @private
105
+ */
106
+ export function resolveConfig() {
107
+ const file = _loadConfig();
108
+ const userConfig = file || {};
109
+
110
+ validateConfig(userConfig);
111
+ return mergeWithDefaults(userConfig);
112
+ }
113
+
114
+ // ---------------------------------------------------------------------------
115
+ // Public API
116
+ // ---------------------------------------------------------------------------
117
+
118
+ /**
119
+ * Get the resolved thread configuration.
120
+ *
121
+ * On first call, loads and resolves `thread.config.js` from the project root.
122
+ * Subsequent calls return the cached config (fast, no file I/O).
123
+ *
124
+ * If `thread.config.js` doesn't exist, returns the default config
125
+ * (Preact + Zustand + default GPU/thread/pool settings).
126
+ *
127
+ * @returns {import('../types.js').threadConfig}
128
+ * The resolved configuration object.
129
+ *
130
+ * @example
131
+ * ```js
132
+ * import { getConfig } from 'thread/config';
133
+ *
134
+ * const config = getConfig();
135
+ *
136
+ * // Use config to decide behavior
137
+ * if (config.framework === 'react') {
138
+ * // React-specific code
139
+ * }
140
+ *
141
+ * // Access GPU defaults
142
+ * console.log(config.gpu.workgroupSize); // 256
143
+ * ```
144
+ *
145
+ * @example
146
+ * ```js
147
+ * // Config is frozen — mutation attempts silently fail
148
+ * const config = getConfig();
149
+ * config.framework = 'vue'; // Error in strict mode, silent in sloppy
150
+ * console.log(config.framework); // Still 'preact'
151
+ * ```
152
+ */
153
+ export function getConfig() {
154
+ if (!_config) {
155
+ _config = resolveConfig();
156
+ }
157
+ return _config;
158
+ }
159
+
160
+ /**
161
+ * Force re-resolution of the configuration.
162
+ *
163
+ * Clears all cached state (config, hooks, adapters). The next call to
164
+ * `getConfig()` will re-read `thread.config.js` from disk.
165
+ *
166
+ * **When to call this:**
167
+ * - During HMR (Hot Module Reload) when the user edits `thread.config.js`
168
+ * - In test teardown to reset state between tests
169
+ * - When you need to programmatically change the config
170
+ *
171
+ * @example
172
+ * ```js
173
+ * import { setConfig, getConfig } from 'thread/config';
174
+ *
175
+ * // User edits thread.config.js...
176
+ * setConfig(); // Clear caches
177
+ *
178
+ * // Next getConfig() reads the fresh file
179
+ * const updated = getConfig();
180
+ * ```
181
+ */
182
+ export function setConfig() {
183
+ _config = null;
184
+ _hooks = null;
185
+ _adapter = null;
186
+ }
187
+
188
+ /**
189
+ * Get resolved framework hooks.
190
+ *
191
+ * On first call, dynamically imports the configured framework's hooks
192
+ * module (e.g. `import('preact/hooks')`). Subsequent calls return the
193
+ * cached hooks (fast, no import overhead).
194
+ *
195
+ * This function is used by `src/hooks.js` and `src/gpu/hooks.js` at
196
+ * module load time via top-level `await`:
197
+ *
198
+ * ```js
199
+ * const { useState, useEffect, ... } = await getHooks();
200
+ * ```
201
+ *
202
+ * @returns {Promise<import('./frameworks.js').FrameworkHooks>}
203
+ * The resolved hooks object with `useState`, `useEffect`, `useRef`,
204
+ * `useCallback`, and `useMemo`.
205
+ *
206
+ * @throws {Error} If the framework is unsupported or missing required hooks.
207
+ *
208
+ * @example
209
+ * ```js
210
+ * const hooks = await getHooks();
211
+ * const { useState } = hooks;
212
+ *
213
+ * function Counter() {
214
+ * const [count, setCount] = useState(0);
215
+ * return <button onClick={() => setCount(count + 1)}>{count}</button>;
216
+ * }
217
+ * ```
218
+ */
219
+ export async function getHooks() {
220
+ if (!_hooks) {
221
+ const config = getConfig();
222
+ _hooks = await resolveHooks(config.framework, config.customHookSource);
223
+ }
224
+ return _hooks;
225
+ }
226
+
227
+ /**
228
+ * Get resolved adapter constructors for the configured state manager.
229
+ *
230
+ * Returns the adapter constructors that match the configured state
231
+ * manager, ready to bind threads/GPUs to your store.
232
+ *
233
+ * @returns {{ thread: Function, gpu: Function, type: string }}
234
+ * Adapter constructors and type hint.
235
+ *
236
+ * @example
237
+ * ```js
238
+ * import { getResolvedAdapter } from 'thread/config';
239
+ *
240
+ * const { thread: createBinder, gpu: createGPUBinder, type } = getResolvedAdapter();
241
+ *
242
+ * // Bind a thread to a store
243
+ * const binder = createBinder(myThread, useStore, 'setData');
244
+ * await binder.run('some data');
245
+ * ```
246
+ */
247
+ export function getResolvedAdapter() {
248
+ if (!_adapter) {
249
+ const config = getConfig();
250
+ _adapter = getAdapter(config.stateManager, config.customAdapter);
251
+ }
252
+ return _adapter;
253
+ }
254
+
255
+ // Re-export programmatic config helpers for browser/edge environments
256
+ export { setProgrammaticConfig, getProgrammaticConfig, clearProgrammaticConfig } from './env.js';