@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.
- package/LICENSE +21 -0
- package/README.md +1156 -0
- package/package.json +133 -0
- package/src/adapters.js +404 -0
- package/src/config/adapters.js +188 -0
- package/src/config/define.js +171 -0
- package/src/config/env.js +218 -0
- package/src/config/frameworks.js +268 -0
- package/src/config/index.js +256 -0
- package/src/config/schema.js +375 -0
- package/src/config.js +52 -0
- package/src/deno.js +29 -0
- package/src/edge.js +38 -0
- package/src/env.js +361 -0
- package/src/error.js +340 -0
- package/src/factory.js +591 -0
- package/src/gpu/adapters.js +227 -0
- package/src/gpu/chains.js +261 -0
- package/src/gpu/env.js +372 -0
- package/src/gpu/gpu.js +1199 -0
- package/src/gpu/helpers.js +240 -0
- package/src/gpu/hooks.js +284 -0
- package/src/gpu/index.js +16 -0
- package/src/gpu/shaders.js +834 -0
- package/src/gpu/special.js +493 -0
- package/src/hooks.js +448 -0
- package/src/index.js +557 -0
- package/src/metrix.js +222 -0
- package/src/node.js +36 -0
- package/src/pool.js +740 -0
- package/src/serializer.js +139 -0
- package/src/thread.js +1246 -0
- package/src/types.js +1354 -0
- package/src/worker-factory.js +347 -0
|
@@ -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
|
+
};
|