@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,188 @@
1
+ /**
2
+ * @file State manager adapter registry for the thread config system.
3
+ *
4
+ * Maps a state manager name (e.g. `'zustand'`, `'redux'`) to the
5
+ * appropriate adapter constructors for threads and GPU.
6
+ *
7
+ * ## How adapters work
8
+ *
9
+ * Adapters bridge thread's thread/pool/GPU instances to your state manager.
10
+ * They follow a consistent pattern:
11
+ *
12
+ * ```
13
+ * adapter(threadOrGpu, store, action, options) → { run, destroy }
14
+ * ```
15
+ *
16
+ * - `run(...args)` — execute the task **and** push the result to the store
17
+ * - `destroy()` — clean up event listeners
18
+ *
19
+ * Different state managers have different update patterns:
20
+ *
21
+ * | Type | Pattern | Used by |
22
+ * |------|---------|---------|
23
+ * | `action` | Call a store action by name: `store.getAction('setData')(result)` | Zustand |
24
+ * | `signal` | Write to a signal's `.value` property: `signal.value = result` | Preact Signals |
25
+ * | `setter` | Call a setter callback: `setter(result)` | Redux, Jotai, MobX, vanilla |
26
+ *
27
+ * ## Why some adapters are the same
28
+ *
29
+ * Redux, Jotai, MobX, and vanilla JS all use the same adapter
30
+ * (`createStoreBinder`) because they all accept a generic setter callback.
31
+ * The difference is in how you *create* the store, not in how thread
32
+ * pushes results to it.
33
+ *
34
+ * @example
35
+ * ```js
36
+ * // All of these use the same adapter under the hood:
37
+ * import { getAdapter } from './config/adapters.js';
38
+ *
39
+ * const reduxAdapter = getAdapter('redux');
40
+ * const jotaiAdapter = getAdapter('jotai');
41
+ * const mobxAdapter = getAdapter('mobx');
42
+ *
43
+ * // reduxAdapter.thread === jotaiAdapter.thread === mobxAdapter.thread
44
+ * // They're all `createStoreBinder`
45
+ * ```
46
+ *
47
+ * @module config/adapters
48
+ */
49
+
50
+ import {
51
+ createZustandBinder,
52
+ createSignalBinder,
53
+ createStoreBinder,
54
+ } from '../adapters.js';
55
+
56
+ import {
57
+ createGPUBinder,
58
+ createGPUSignalBinder,
59
+ createGPUStoreBinder,
60
+ } from '../gpu/adapters.js';
61
+
62
+ // ---------------------------------------------------------------------------
63
+ // Adapter map
64
+ // ---------------------------------------------------------------------------
65
+
66
+ /**
67
+ * Mapping of state manager names to their adapter constructors.
68
+ *
69
+ * Each entry provides:
70
+ * - `thread`: adapter constructor for thread/pool binding
71
+ * - `gpu`: adapter constructor for GPU binding
72
+ * - `type`: hint about the update pattern (`'action'` | `'signal'` | `'setter'`)
73
+ *
74
+ * @type {Record<string, { thread: Function, gpu: Function, type: string }>}
75
+ * @private
76
+ */
77
+ const ADAPTER_MAP = {
78
+ zustand: {
79
+ thread: createZustandBinder,
80
+ gpu: createGPUBinder,
81
+ type: 'action',
82
+ },
83
+ signals: {
84
+ thread: createSignalBinder,
85
+ gpu: createGPUSignalBinder,
86
+ type: 'signal',
87
+ },
88
+ redux: {
89
+ thread: createStoreBinder,
90
+ gpu: createGPUStoreBinder,
91
+ type: 'setter',
92
+ },
93
+ jotai: {
94
+ thread: createStoreBinder,
95
+ gpu: createGPUStoreBinder,
96
+ type: 'setter',
97
+ },
98
+ mobx: {
99
+ thread: createStoreBinder,
100
+ gpu: createGPUStoreBinder,
101
+ type: 'setter',
102
+ },
103
+ vanilla: {
104
+ thread: createStoreBinder,
105
+ gpu: createGPUStoreBinder,
106
+ type: 'setter',
107
+ },
108
+ };
109
+
110
+ // ---------------------------------------------------------------------------
111
+ // Public API
112
+ // ---------------------------------------------------------------------------
113
+
114
+ /**
115
+ * Get adapter constructors for a given state manager.
116
+ *
117
+ * Returns the thread and GPU adapter constructors, plus a type hint
118
+ * that tells you how the adapter works.
119
+ *
120
+ * @param {string} stateManager
121
+ * State manager name. Must be one of the values in {@link STATE_MANAGERS}.
122
+ * @param {Function|null} [customAdapter=null]
123
+ * Optional custom adapter factory. If provided, it is used for both
124
+ * thread and GPU adapters, overriding the registry lookup.
125
+ *
126
+ * @returns {{ thread: Function, gpu: Function, type: string }}
127
+ * Adapter constructors and type hint.
128
+ *
129
+ * @example
130
+ * ```js
131
+ * import { getAdapter } from './config/adapters.js';
132
+ *
133
+ * // Get Zustand adapters
134
+ * const { thread, gpu, type } = getAdapter('zustand');
135
+ * console.log(type); // 'action'
136
+ *
137
+ * // Use the thread adapter
138
+ * const binder = thread(myThread, useStore, 'setData', {
139
+ * errorAction: 'setError',
140
+ * });
141
+ *
142
+ * // Now every thread.run() result is pushed to the store
143
+ * await binder.run('some data');
144
+ * ```
145
+ *
146
+ * @example
147
+ * ```js
148
+ * // Get Redux adapters (generic setter pattern)
149
+ * const { thread, type } = getAdapter('redux');
150
+ * console.log(type); // 'setter'
151
+ *
152
+ * // The adapter works with any setter function:
153
+ * const binder = thread(myThread, (result) => {
154
+ * dispatch({ type: 'SET_DATA', payload: result });
155
+ * });
156
+ * ```
157
+ *
158
+ * @example
159
+ * ```js
160
+ * // Custom adapter
161
+ * const myAdapter = (instance, store, action) => ({
162
+ * run: async (...args) => {
163
+ * const result = await instance.run(...args);
164
+ * store.dispatch({ type: action, payload: result });
165
+ * },
166
+ * destroy: () => {},
167
+ * });
168
+ *
169
+ * const { thread } = getAdapter('custom', myAdapter);
170
+ * ```
171
+ */
172
+ export function getAdapter(stateManager, customAdapter = null) {
173
+ if (typeof customAdapter === 'function') {
174
+ return { thread: customAdapter, gpu: customAdapter, type: 'custom' };
175
+ }
176
+
177
+ const entry = ADAPTER_MAP[stateManager];
178
+
179
+ if (!entry) {
180
+ console.warn(
181
+ `[thread] Unknown stateManager: "${stateManager}". Falling back to "vanilla". ` +
182
+ `Valid options: ${Object.keys(ADAPTER_MAP).join(', ')}`
183
+ );
184
+ return ADAPTER_MAP.vanilla;
185
+ }
186
+
187
+ return entry;
188
+ }
@@ -0,0 +1,171 @@
1
+ /**
2
+ * @file Configuration helper for the thread library.
3
+ *
4
+ * Provides the `defineConfig()` function — the recommended way to create
5
+ * a `thread.config.js` file. It validates the config, warns on unknown
6
+ * keys (catching typos), applies sensible defaults for omitted fields,
7
+ * and returns a frozen (immutable) config object.
8
+ *
9
+ * ## Why `defineConfig()`?
10
+ *
11
+ * You *could* write `export default { ... }` directly, but `defineConfig()`
12
+ * gives you:
13
+ *
14
+ * - **Typo detection** — unknown keys trigger a console warning
15
+ * - **Automatic defaults** — omit any field and it falls back to a sensible value
16
+ * - **Immutability** — the returned config is frozen, preventing accidental mutation
17
+ * - **Validation** — invalid enum values (e.g. `framework: ' angular'`) warn and
18
+ * fall back to the default
19
+ *
20
+ * ## How it works
21
+ *
22
+ * 1. The user creates `thread.config.js` in their project root
23
+ * 2. They `import { defineConfig } from 'thread/config'` and call it
24
+ * 3. At library load time, thread reads the file, parses the config object,
25
+ * and applies it globally
26
+ * 4. All hooks and adapters use the resolved config to pick the right
27
+ * framework and state manager
28
+ *
29
+ * @example
30
+ * ```js
31
+ * // thread.config.js — in your project root
32
+ * import { defineConfig } from 'thread/config';
33
+ *
34
+ * export default defineConfig({
35
+ * // Pick your UI framework — hooks are auto-resolved
36
+ * framework: 'react',
37
+ *
38
+ * // Pick your state manager — adapter shortcuts are auto-selected
39
+ * stateManager: 'zustand',
40
+ *
41
+ * // GPU compute defaults (used by all GPUCompute instances)
42
+ * gpu: {
43
+ * workgroupSize: 256,
44
+ * powerPreference: 'high-performance',
45
+ * },
46
+ *
47
+ * // Thread defaults
48
+ * thread: { timeout: 10_000 },
49
+ *
50
+ * // Pool defaults
51
+ * pool: { autoRestart: true, enableStealing: true },
52
+ *
53
+ * // Development options
54
+ * dev: { log: true, metrics: true },
55
+ * });
56
+ * ```
57
+ *
58
+ * @example
59
+ * ```js
60
+ * // Minimal config — everything else uses defaults
61
+ * import { defineConfig } from 'thread/config';
62
+ *
63
+ * export default defineConfig({
64
+ * framework: 'svelte',
65
+ * stateManager: 'signals',
66
+ * });
67
+ * ```
68
+ *
69
+ * @example
70
+ * ```js
71
+ * // Custom framework with your own hooks
72
+ * import { defineConfig } from 'thread/config';
73
+ *
74
+ * export default defineConfig({
75
+ * framework: 'custom',
76
+ * customHookSource: async () => {
77
+ * const mod = await import('my-framework/hooks');
78
+ * return {
79
+ * useState: mod.useState,
80
+ * useEffect: mod.useEffect,
81
+ * useRef: mod.useRef,
82
+ * useCallback: mod.useCallback,
83
+ * useMemo: mod.useMemo,
84
+ * };
85
+ * },
86
+ * });
87
+ * ```
88
+ *
89
+ * @module config/define
90
+ */
91
+
92
+ import { mergeWithDefaults, validateConfig } from './schema.js';
93
+
94
+ // ---------------------------------------------------------------------------
95
+ // defineConfig
96
+ // ---------------------------------------------------------------------------
97
+
98
+ /**
99
+ * Define a thread configuration object.
100
+ *
101
+ * Returns a **frozen** (immutable) config with all defaults applied.
102
+ * Unknown top-level keys produce a console warning — this helps catch
103
+ * typos like `framwork` instead of `framework`.
104
+ *
105
+ * Invalid values (e.g. `framework: 'Angular'` with wrong casing) are
106
+ * replaced with the default and a warning is logged. This keeps the
107
+ * library functional even with a partially incorrect config.
108
+ *
109
+ * @param {Record<string, any>} [config={}]
110
+ * User configuration. All fields are optional. See {@link threadConfig}
111
+ * for the full schema.
112
+ *
113
+ * @returns {Readonly<import('../types.js').threadConfig>}
114
+ * Fully resolved, frozen configuration object. Contains the user's
115
+ * values where valid, and built-in defaults everywhere else.
116
+ *
117
+ * @throws {TypeError} Never — invalid inputs are handled gracefully with
118
+ * warnings and fallbacks.
119
+ *
120
+ * @see {@link threadConfig} for the full type definition.
121
+ * @see {@link DEFAULTS} for the default values.
122
+ * @see {@link FRAMEWORKS} for the list of supported frameworks.
123
+ * @see {@link STATE_MANAGERS} for the list of supported state managers.
124
+ *
125
+ * @example
126
+ * ```js
127
+ * import { defineConfig } from 'thread/config';
128
+ *
129
+ * export default defineConfig({
130
+ * framework: 'react',
131
+ * gpu: { powerPreference: 'low-power' },
132
+ * dev: { log: true },
133
+ * });
134
+ *
135
+ * // Result:
136
+ * // {
137
+ * // framework: 'react',
138
+ * // stateManager: 'zustand', ← default
139
+ * // customHookSource: null, ← default
140
+ * // customAdapter: null, ← default
141
+ * // gpu: { workgroupSize: 256, maxBufferSize: 268435456, ... },
142
+ * // thread: { timeout: 30000 }, ← default
143
+ * // pool: { autoRestart: true }, ← default
144
+ * // dev: { log: true, metrics: false, warnOnLongTask: 0 },
145
+ * // }
146
+ * ```
147
+ *
148
+ * @example
149
+ * ```js
150
+ * // TypeScript — the return type is threadConfig
151
+ * import { defineConfig } from 'thread/config';
152
+ * import type { threadConfig } from 'thread/types';
153
+ *
154
+ * const config: threadConfig = defineConfig({
155
+ * framework: 'vue',
156
+ * stateManager: 'redux',
157
+ * });
158
+ * ```
159
+ */
160
+ export function defineConfig(config = {}) {
161
+ if (config !== null && typeof config !== 'object') {
162
+ console.warn('[thread] defineConfig() expected an object, got:', typeof config);
163
+ return Object.freeze(mergeWithDefaults({}));
164
+ }
165
+
166
+ validateConfig(config);
167
+
168
+ const resolved = mergeWithDefaults(config);
169
+
170
+ return Object.freeze(resolved);
171
+ }
@@ -0,0 +1,218 @@
1
+ /**
2
+ * @file Cross-environment config loading for the thread library.
3
+ *
4
+ * Loads `thread.config.js` from the project root across different runtimes:
5
+ * - **Node.js / Bun** — `fs.readFileSync` (synchronous, works with top-level await)
6
+ * - **Deno** — `Deno.readTextFileSync`
7
+ * - **Browser** — Skips file loading (config must be set programmatically)
8
+ * - **Edge** — Skips file loading
9
+ *
10
+ * @module config/env
11
+ */
12
+
13
+ import { env } from '../env.js';
14
+
15
+ // ---------------------------------------------------------------------------
16
+ // Config file loading
17
+ // ---------------------------------------------------------------------------
18
+
19
+ /**
20
+ * Attempt to load `thread.config.js` from the project root.
21
+ *
22
+ * Uses the appropriate file system API for the current runtime.
23
+ * Returns `null` if the file doesn't exist or can't be read.
24
+ *
25
+ * @param {string} [filename='thread.config.js'] - Config filename to look for.
26
+ * @returns {Record<string, any>|null} Parsed config object, or `null`.
27
+ *
28
+ * @example
29
+ * ```js
30
+ * const file = loadConfigFile();
31
+ * if (file) {
32
+ * console.log('Loaded config:', file);
33
+ * } else {
34
+ * console.log('No config file found — using defaults');
35
+ * }
36
+ * ```
37
+ */
38
+ export function loadConfigFile(filename = 'thread.config.js') {
39
+ // --- Node.js / Bun ---
40
+ if (env.isNode || env.isBun) {
41
+ return _loadNodeConfig(filename);
42
+ }
43
+
44
+ // --- Deno ---
45
+ if (env.isDeno) {
46
+ return _loadDenoConfig(filename);
47
+ }
48
+
49
+ // --- Browser / Edge ---
50
+ // Config must be set programmatically via setConfig()
51
+ return null;
52
+ }
53
+
54
+ /**
55
+ * @private
56
+ */
57
+ function _loadNodeConfig(filename) {
58
+ try {
59
+ const fs = env.requireModule('node:fs');
60
+ const path = env.requireModule('node:path');
61
+ if (!fs || !path) return null;
62
+
63
+ const cwd = env.getCwd();
64
+ const configPath = path.resolve(cwd, filename);
65
+
66
+ if (!fs.existsSync(configPath)) return null;
67
+
68
+ const raw = fs.readFileSync(configPath, 'utf-8');
69
+ return _parseConfig(raw);
70
+ } catch {
71
+ return null;
72
+ }
73
+ }
74
+
75
+ /**
76
+ * @private
77
+ */
78
+ function _loadDenoConfig(filename) {
79
+ try {
80
+ if (typeof Deno === 'undefined' || typeof Deno.readTextFileSync !== 'function') {
81
+ return null;
82
+ }
83
+
84
+ const cwd = env.getCwd();
85
+ const configPath = `${cwd}/${filename}`;
86
+
87
+ // Check if file exists
88
+ try { Deno.statSync(configPath); } catch { return null; }
89
+
90
+ const raw = Deno.readTextFileSync(configPath);
91
+ return _parseConfig(raw);
92
+ } catch {
93
+ return null;
94
+ }
95
+ }
96
+
97
+ /**
98
+ * Parse a config file string into an object.
99
+ *
100
+ * Strips `import` and `export default` statements, then evaluates
101
+ * the remaining object literal.
102
+ *
103
+ * @param {string} raw - Raw file contents.
104
+ * @returns {Record<string, any>|null} Parsed object, or `null` on failure.
105
+ * @private
106
+ */
107
+ function _parseConfig(raw) {
108
+ try {
109
+ // Strip ESM syntax
110
+ const cleaned = raw
111
+ .replace(/^import\s+.*$/gm, '')
112
+ .replace(/^export\s+default\s+/, '')
113
+ .replace(/^export\s+\{[^}]*\};?\s*$/gm, '')
114
+ .trim();
115
+
116
+ if (!cleaned) return null;
117
+
118
+ // Evaluate the config object
119
+ const fn = new Function(`return (${cleaned})`);
120
+ const result = fn();
121
+
122
+ // Handle defineConfig() return value (frozen object)
123
+ if (result && typeof result === 'object' && typeof result[Symbol.iterator] === 'function') {
124
+ return Object.fromEntries(
125
+ Object.entries(result).filter(([k]) => k !== 'then')
126
+ );
127
+ }
128
+
129
+ return result && typeof result === 'object' ? result : null;
130
+ } catch {
131
+ return null;
132
+ }
133
+ }
134
+
135
+ // ---------------------------------------------------------------------------
136
+ // Programmatic config storage (for browser/edge environments)
137
+ // ---------------------------------------------------------------------------
138
+
139
+ /** @type {Record<string, any>|null} Programmatic config override. */
140
+ let _programmaticConfig = null;
141
+
142
+ /**
143
+ * Set the configuration programmatically.
144
+ *
145
+ * Use this in browser/edge environments where file system access
146
+ * is not available. This overrides any `thread.config.js` file.
147
+ *
148
+ * @param {Record<string, any>} config - Configuration object.
149
+ *
150
+ * @example
151
+ * ```js
152
+ * import { setProgrammaticConfig } from './config/env.js';
153
+ *
154
+ * setProgrammaticConfig({
155
+ * framework: 'react',
156
+ * stateManager: 'zustand',
157
+ * gpu: { workgroupSize: 512 },
158
+ * });
159
+ * ```
160
+ */
161
+ export function setProgrammaticConfig(config) {
162
+ _programmaticConfig = config && typeof config === 'object' ? { ...config } : null;
163
+ }
164
+
165
+ /**
166
+ * Get the programmatic config override (if set).
167
+ *
168
+ * @returns {Record<string, any>|null} The programmatic config, or `null`.
169
+ */
170
+ export function getProgrammaticConfig() {
171
+ return _programmaticConfig;
172
+ }
173
+
174
+ /**
175
+ * Clear the programmatic config override.
176
+ */
177
+ export function clearProgrammaticConfig() {
178
+ _programmaticConfig = null;
179
+ }
180
+
181
+ /**
182
+ * Load config with programmatic override support.
183
+ *
184
+ * Priority: programmatic config > file-based config > defaults.
185
+ *
186
+ * @param {string} [filename='thread.config.js'] - Config filename.
187
+ * @returns {Record<string, any>|null} Loaded config, or `null`.
188
+ *
189
+ * @example
190
+ * ```js
191
+ * // In a browser environment
192
+ * import { loadConfigWithOverride } from './config/env.js';
193
+ *
194
+ * // User sets config programmatically
195
+ * setProgrammaticConfig({ framework: 'svelte' });
196
+ *
197
+ * // Config loader uses programmatic override
198
+ * const config = loadConfigWithOverride();
199
+ * console.log(config.framework); // 'svelte'
200
+ * ```
201
+ */
202
+ export function loadConfigWithOverride(filename = 'thread.config.js') {
203
+ // Priority: programmatic > file
204
+ if (_programmaticConfig) return _programmaticConfig;
205
+ return loadConfigFile(filename);
206
+ }
207
+
208
+ // ---------------------------------------------------------------------------
209
+ // Exports
210
+ // ---------------------------------------------------------------------------
211
+
212
+ export default {
213
+ loadConfigFile,
214
+ setProgrammaticConfig,
215
+ getProgrammaticConfig,
216
+ clearProgrammaticConfig,
217
+ loadConfigWithOverride,
218
+ };