@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,375 @@
1
+ /**
2
+ * @file Config schema, defaults, and validation for the thread library.
3
+ *
4
+ * This module is the source of truth for the thread configuration shape.
5
+ * It defines:
6
+ *
7
+ * - **`FRAMEWORKS`** — the list of supported UI frameworks
8
+ * - **`STATE_MANAGERS`** — the list of supported state managers
9
+ * - **`DEFAULTS`** — the default values for every config field
10
+ * - **`validateConfig()`** — warns on unknown/invalid keys
11
+ * - **`mergeWithDefaults()`** — deep-merges user config with defaults
12
+ *
13
+ * ## How validation works
14
+ *
15
+ * thread uses **soft validation** — invalid values don't throw. Instead:
16
+ *
17
+ * 1. Unknown keys produce a warning (catches typos)
18
+ * 2. Invalid enum values (e.g. `framework: 'Angular'`) warn and fall
19
+ * back to the default
20
+ * 3. Invalid types (e.g. `gpu: 'fast'` instead of an object) warn and
21
+ * ignore the section
22
+ *
23
+ * This keeps the library functional even with a partially broken config.
24
+ *
25
+ * ## How merging works
26
+ *
27
+ * `mergeWithDefaults()` performs a **shallow merge per section**:
28
+ *
29
+ * ```js
30
+ * // User config:
31
+ * { gpu: { workgroupSize: 64 } }
32
+ *
33
+ * // Merged with defaults:
34
+ * {
35
+ * gpu: {
36
+ * workgroupSize: 64, ← user value
37
+ * maxBufferSize: 268435456, ← default (preserved)
38
+ * entryPoint: 'main', ← default (preserved)
39
+ * ...
40
+ * }
41
+ * }
42
+ * ```
43
+ *
44
+ * The user's section object is spread over the defaults, so any omitted
45
+ * fields keep their default values.
46
+ *
47
+ * @module config/schema
48
+ */
49
+
50
+ // ---------------------------------------------------------------------------
51
+ // Constants
52
+ // ---------------------------------------------------------------------------
53
+
54
+ /**
55
+ * List of supported UI frameworks.
56
+ *
57
+ * Each value maps to a dynamic import in `config/frameworks.js`:
58
+ *
59
+ * | Framework | Import path | Notes |
60
+ * |-----------|------------|-------|
61
+ * | `preact` | `preact/hooks` | Default. Full hook support. |
62
+ * | `react` | `react` | Full hook support. |
63
+ * | `svelte` | `svelte/reactivity` | Svelte 5 runes. Partial. |
64
+ * | `vue` | `vue` | Composition API. Partial. |
65
+ * | `solid` | `solid-js` | Full hook support. |
66
+ * | `angular` | — | Not yet supported. Use `custom`. |
67
+ * | `custom` | — | User provides `customHookSource`. |
68
+ *
69
+ * @type {string[]}
70
+ *
71
+ * @example
72
+ * ```js
73
+ * import { FRAMEWORKS } from 'thread/config';
74
+ *
75
+ * console.log(FRAMEWORKS);
76
+ * // ['preact', 'react', 'svelte', 'vue', 'solid', 'angular', 'custom']
77
+ * ```
78
+ */
79
+ export const FRAMEWORKS = ['preact', 'react', 'svelte', 'vue', 'solid', 'angular', 'custom'];
80
+
81
+ /**
82
+ * List of supported state managers.
83
+ *
84
+ * Each value maps to adapter constructors in `config/adapters.js`:
85
+ *
86
+ * | State Manager | Adapter type | Thread adapter | GPU adapter |
87
+ * |--------------|-------------|---------------|-------------|
88
+ * | `zustand` | action | `createZustandBinder` | `createGPUBinder` |
89
+ * | `signals` | signal | `createSignalBinder` | `createGPUSignalBinder` |
90
+ * | `redux` | setter | `createStoreBinder` | `createGPUStoreBinder` |
91
+ * | `jotai` | setter | `createStoreBinder` | `createGPUStoreBinder` |
92
+ * | `mobx` | setter | `createStoreBinder` | `createGPUStoreBinder` |
93
+ * | `vanilla` | setter | `createStoreBinder` | `createGPUStoreBinder` |
94
+ * | `custom` | — | User provides `customAdapter` | — |
95
+ *
96
+ * @type {string[]}
97
+ *
98
+ * @example
99
+ * ```js
100
+ * import { STATE_MANAGERS } from 'thread/config';
101
+ *
102
+ * console.log(STATE_MANAGERS);
103
+ * // ['zustand', 'signals', 'redux', 'jotai', 'mobx', 'vanilla', 'custom']
104
+ * ```
105
+ */
106
+ export const STATE_MANAGERS = ['zustand', 'signals', 'redux', 'jotai', 'mobx', 'vanilla', 'custom'];
107
+
108
+ /** @type {Set<string>} Known top-level config keys. */
109
+ const KNOWN_KEYS = new Set([
110
+ 'framework',
111
+ 'stateManager',
112
+ 'customHookSource',
113
+ 'customAdapter',
114
+ 'gpu',
115
+ 'thread',
116
+ 'pool',
117
+ 'dev',
118
+ ]);
119
+
120
+ /** @type {Set<string>} Known keys inside the `gpu` section. */
121
+ const KNOWN_GPU_KEYS = new Set([
122
+ 'workgroupSize', 'maxBufferSize', 'entryPoint', 'powerPreference',
123
+ 'cpuFallback', 'adapterOptions', 'shader',
124
+ 'bunWebgpu', 'fallbackAdapter',
125
+ ]);
126
+
127
+ /** @type {Set<string>} Known keys inside the `thread` section. */
128
+ const KNOWN_THREAD_KEYS = new Set([
129
+ 'timeout', 'idleTimeout', 'healthCheckInterval', 'healthCheckTimeout',
130
+ 'concurrency', 'imports', 'initArgs', 'initTransfer',
131
+ ]);
132
+
133
+ /** @type {Set<string>} Known keys inside the `pool` section. */
134
+ const KNOWN_POOL_KEYS = new Set([
135
+ 'autoRestart', 'maxSize', 'enableStealing', 'keyHasher',
136
+ ]);
137
+
138
+ /** @type {Set<string>} Known keys inside the `dev` section. */
139
+ const KNOWN_DEV_KEYS = new Set(['log', 'metrics', 'warnOnLongTask']);
140
+
141
+ // ---------------------------------------------------------------------------
142
+ // Defaults
143
+ // ---------------------------------------------------------------------------
144
+
145
+ /**
146
+ * Built-in default configuration.
147
+ *
148
+ * Applied when the user omits a field. Every section is frozen to
149
+ * prevent accidental mutation.
150
+ *
151
+ * @type {Readonly<import('../types.js').threadConfig>}
152
+ *
153
+ * @example
154
+ * ```js
155
+ * import { DEFAULTS } from 'thread/config';
156
+ *
157
+ * console.log(DEFAULTS.framework); // 'preact'
158
+ * console.log(DEFAULTS.gpu.workgroupSize); // 256
159
+ * console.log(DEFAULTS.thread.timeout); // 30000
160
+ * console.log(DEFAULTS.pool.autoRestart); // true
161
+ * console.log(DEFAULTS.dev.log); // false
162
+ * ```
163
+ */
164
+ export const DEFAULTS = Object.freeze({
165
+ framework: 'preact',
166
+ stateManager: 'zustand',
167
+ customHookSource: null,
168
+ customAdapter: null,
169
+
170
+ gpu: Object.freeze({
171
+ workgroupSize: 256,
172
+ maxBufferSize: 256 * 1024 * 1024,
173
+ entryPoint: 'main',
174
+ powerPreference: 'high-performance',
175
+ cpuFallback: null,
176
+ adapterOptions: {},
177
+ shader: null,
178
+ bunWebgpu: true,
179
+ fallbackAdapter: true,
180
+ }),
181
+
182
+ thread: Object.freeze({
183
+ timeout: 30_000,
184
+ }),
185
+
186
+ pool: Object.freeze({
187
+ autoRestart: true,
188
+ enableStealing: true,
189
+ }),
190
+
191
+ dev: Object.freeze({
192
+ log: false,
193
+ metrics: false,
194
+ warnOnLongTask: 0,
195
+ }),
196
+ });
197
+
198
+ // ---------------------------------------------------------------------------
199
+ // Validation
200
+ // ---------------------------------------------------------------------------
201
+
202
+ /**
203
+ * Warn about unknown top-level or nested config keys.
204
+ *
205
+ * This is a **development aid** — it catches typos like `framwork`
206
+ * or `workgroup_Size` before they cause silent bugs.
207
+ *
208
+ * @param {Record<string, any>} config - The raw user config.
209
+ * @private
210
+ */
211
+ function warnUnknownKeys(config) {
212
+ for (const key of Object.keys(config)) {
213
+ if (!KNOWN_KEYS.has(key)) {
214
+ console.warn(`[thread] Unknown config key: "${key}". Did you mean one of: ${[...KNOWN_KEYS].join(', ')}?`);
215
+ }
216
+ }
217
+
218
+ if (config.gpu && typeof config.gpu === 'object') {
219
+ for (const key of Object.keys(config.gpu)) {
220
+ if (!KNOWN_GPU_KEYS.has(key)) {
221
+ console.warn(`[thread] Unknown gpu config key: "${key}". Known keys: ${[...KNOWN_GPU_KEYS].join(', ')}`);
222
+ }
223
+ }
224
+ }
225
+
226
+ if (config.thread && typeof config.thread === 'object') {
227
+ for (const key of Object.keys(config.thread)) {
228
+ if (!KNOWN_THREAD_KEYS.has(key)) {
229
+ console.warn(`[thread] Unknown thread config key: "${key}". Known keys: ${[...KNOWN_THREAD_KEYS].join(', ')}`);
230
+ }
231
+ }
232
+ }
233
+
234
+ if (config.pool && typeof config.pool === 'object') {
235
+ for (const key of Object.keys(config.pool)) {
236
+ if (!KNOWN_POOL_KEYS.has(key)) {
237
+ console.warn(`[thread] Unknown pool config key: "${key}". Known keys: ${[...KNOWN_POOL_KEYS].join(', ')}`);
238
+ }
239
+ }
240
+ }
241
+
242
+ if (config.dev && typeof config.dev === 'object') {
243
+ for (const key of Object.keys(config.dev)) {
244
+ if (!KNOWN_DEV_KEYS.has(key)) {
245
+ console.warn(`[thread] Unknown dev config key: "${key}". Known keys: ${[...KNOWN_DEV_KEYS].join(', ')}`);
246
+ }
247
+ }
248
+ }
249
+ }
250
+
251
+ /**
252
+ * Validate a config object, warning on invalid values.
253
+ *
254
+ * Uses **soft validation** — no errors are thrown. Invalid values are
255
+ * replaced with defaults and a warning is logged. This keeps the library
256
+ * functional even with a partially incorrect config.
257
+ *
258
+ * @param {Record<string, any>} config - The raw user config to validate.
259
+ *
260
+ * @example
261
+ * ```js
262
+ * // These all produce warnings but don't throw:
263
+ *
264
+ * validateConfig({ framework: ' angular' }); // typo in casing
265
+ * validateConfig({ stateManager: 'REDUX' }); // wrong casing
266
+ * validateConfig({ gpu: 'fast' }); // wrong type
267
+ * validateConfig({ unknownKey: true }); // unknown key
268
+ * ```
269
+ */
270
+ export function validateConfig(config) {
271
+ warnUnknownKeys(config);
272
+
273
+ if (config.framework !== undefined && !FRAMEWORKS.includes(config.framework)) {
274
+ console.warn(
275
+ `[thread] Invalid framework: "${config.framework}". Falling back to "preact". ` +
276
+ `Valid options: ${FRAMEWORKS.join(', ')}`
277
+ );
278
+ }
279
+
280
+ if (config.stateManager !== undefined && !STATE_MANAGERS.includes(config.stateManager)) {
281
+ console.warn(
282
+ `[thread] Invalid stateManager: "${config.stateManager}". Falling back to "zustand". ` +
283
+ `Valid options: ${STATE_MANAGERS.join(', ')}`
284
+ );
285
+ }
286
+
287
+ if (config.gpu !== undefined && typeof config.gpu !== 'object') {
288
+ console.warn('[thread] gpu config must be an object. Ignoring.');
289
+ }
290
+
291
+ if (config.thread !== undefined && typeof config.thread !== 'object') {
292
+ console.warn('[thread] thread config must be an object. Ignoring.');
293
+ }
294
+
295
+ if (config.pool !== undefined && typeof config.pool !== 'object') {
296
+ console.warn('[thread] pool config must be an object. Ignoring.');
297
+ }
298
+
299
+ if (config.dev !== undefined && typeof config.dev !== 'object') {
300
+ console.warn('[thread] dev config must be an object. Ignoring.');
301
+ }
302
+ }
303
+
304
+ // ---------------------------------------------------------------------------
305
+ // Merge
306
+ // ---------------------------------------------------------------------------
307
+
308
+ /**
309
+ * Deep-merge user config with built-in defaults.
310
+ *
311
+ * Merge rules:
312
+ * - **Scalars** (string, boolean, number): user value wins if valid
313
+ * - **Objects** (gpu, thread, pool, dev): merged recursively — user's
314
+ * fields override, omitted fields keep defaults
315
+ * - **Functions** (customHookSource, customAdapter): user value wins as-is
316
+ * - **Invalid values**: default wins
317
+ *
318
+ * @param {Record<string, any>} userConfig
319
+ * The raw user config (may be empty `{}`).
320
+ *
321
+ * @returns {import('../types.js').threadConfig}
322
+ * Fully resolved config with all defaults applied.
323
+ *
324
+ * @example
325
+ * ```js
326
+ * // Partial user config
327
+ * const user = { framework: 'react', gpu: { workgroupSize: 64 } };
328
+ *
329
+ * const merged = mergeWithDefaults(user);
330
+ *
331
+ * console.log(merged.framework); // 'react' (user value)
332
+ * console.log(merged.stateManager); // 'zustand' (default)
333
+ * console.log(merged.gpu.workgroupSize); // 64 (user value)
334
+ * console.log(merged.gpu.maxBufferSize); // 268435456 (default)
335
+ * console.log(merged.gpu.powerPreference); // 'high-performance' (default)
336
+ * ```
337
+ *
338
+ * @example
339
+ * ```js
340
+ * // Empty config — everything uses defaults
341
+ * const merged = mergeWithDefaults({});
342
+ * // equivalent to DEFAULTS
343
+ * ```
344
+ */
345
+ export function mergeWithDefaults(userConfig) {
346
+ const fw = userConfig.framework;
347
+ const sm = userConfig.stateManager;
348
+
349
+ return Object.freeze({
350
+ framework: (fw && FRAMEWORKS.includes(fw)) ? fw : DEFAULTS.framework,
351
+ stateManager: (sm && STATE_MANAGERS.includes(sm)) ? sm : DEFAULTS.stateManager,
352
+ customHookSource: userConfig.customHookSource ?? DEFAULTS.customHookSource,
353
+ customAdapter: userConfig.customAdapter ?? DEFAULTS.customAdapter,
354
+
355
+ gpu: Object.freeze({
356
+ ...DEFAULTS.gpu,
357
+ ...(typeof userConfig.gpu === 'object' ? userConfig.gpu : {}),
358
+ }),
359
+
360
+ thread: Object.freeze({
361
+ ...DEFAULTS.thread,
362
+ ...(typeof userConfig.thread === 'object' ? userConfig.thread : {}),
363
+ }),
364
+
365
+ pool: Object.freeze({
366
+ ...DEFAULTS.pool,
367
+ ...(typeof userConfig.pool === 'object' ? userConfig.pool : {}),
368
+ }),
369
+
370
+ dev: Object.freeze({
371
+ ...DEFAULTS.dev,
372
+ ...(typeof userConfig.dev === 'object' ? userConfig.dev : {}),
373
+ }),
374
+ });
375
+ }
package/src/config.js ADDED
@@ -0,0 +1,52 @@
1
+ /**
2
+ * @file Config entry point for `thread/config`.
3
+ *
4
+ * This is the public entry point for the thread configuration system.
5
+ * Import from `thread/config` to access configuration utilities:
6
+ *
7
+ * ```js
8
+ * import { defineConfig, getConfig, setConfig, DEFAULTS, FRAMEWORKS, STATE_MANAGERS } from 'thread/config';
9
+ * ```
10
+ *
11
+ * ## What's exported
12
+ *
13
+ * | Export | Purpose |
14
+ * |--------|---------|
15
+ * | `defineConfig()` | Create a validated, frozen config object |
16
+ * | `getConfig()` | Get the resolved config (cached) |
17
+ * | `setConfig()` | Clear caches and force re-resolution |
18
+ * | `getHooks()` | Get resolved framework hooks (async) |
19
+ * | `getResolvedAdapter()` | Get adapter constructors for the state manager |
20
+ * | `DEFAULTS` | Built-in default config values |
21
+ * | `FRAMEWORKS` | List of supported frameworks |
22
+ * | `STATE_MANAGERS` | List of supported state managers |
23
+ *
24
+ * ## Usage in `thread.config.js`
25
+ *
26
+ * ```js
27
+ * import { defineConfig } from 'thread/config';
28
+ *
29
+ * export default defineConfig({
30
+ * framework: 'react',
31
+ * stateManager: 'zustand',
32
+ * gpu: { workgroupSize: 256 },
33
+ * });
34
+ * ```
35
+ *
36
+ * ## Usage in application code
37
+ *
38
+ * ```js
39
+ * import { getConfig } from 'thread/config';
40
+ *
41
+ * const config = getConfig();
42
+ * if (config.dev.log) {
43
+ * console.log('thread is in debug mode');
44
+ * }
45
+ * ```
46
+ *
47
+ * @module config
48
+ */
49
+
50
+ export { defineConfig } from './config/define.js';
51
+ export { getConfig, setConfig, getHooks, getResolvedAdapter, mergeWithDefaults, validateConfig, resolveHooks, getAdapter } from './config/index.js';
52
+ export { DEFAULTS, FRAMEWORKS, STATE_MANAGERS } from './config/schema.js';
package/src/deno.js ADDED
@@ -0,0 +1,29 @@
1
+ /**
2
+ * @file Deno-specific entry point for the thread library.
3
+ *
4
+ * This entry point is optimised for Deno runtimes. It:
5
+ *
6
+ * 1. Uses Deno's Worker API (Blob URL-based)
7
+ * 2. Uses `Deno.readTextFileSync` for config loading
8
+ * 3. Uses `Deno.gpu` for WebGPU access
9
+ *
10
+ * **Usage:**
11
+ *
12
+ * ```ts
13
+ * // In your Deno project
14
+ * import { Thread, ThreadPool, GPUCompute } from 'jsr:@peach/thread/deno';
15
+ * // or from npm:
16
+ * import { Thread, ThreadPool, GPUCompute } from 'thread/deno';
17
+ * ```
18
+ *
19
+ * @module thread/deno
20
+ */
21
+
22
+ // Re-export everything from the main entry point
23
+ export * from './index.js';
24
+ export { default } from './index.js';
25
+
26
+ // Re-export environment-specific utilities
27
+ export { env } from './env.js';
28
+ export { createWorker, terminateWorker, supportsWorkers, workerInfo } from './worker-factory.js';
29
+ export { gpuEnv, isGPUAvailable, requestGPUAdapter, requestGPUDevice } from './gpu/env.js';
package/src/edge.js ADDED
@@ -0,0 +1,38 @@
1
+ /**
2
+ * @file Edge runtime entry point for the thread library.
3
+ *
4
+ * This entry point is optimised for edge runtimes (Cloudflare Workers,
5
+ * Vercel Edge, Netlify Edge, Deno Deploy). It:
6
+ *
7
+ * 1. Uses the standard Web Worker API (Blob URLs)
8
+ * 2. Skips file-based config loading (use `setProgrammaticConfig`)
9
+ * 3. Uses browser-compatible WebGPU detection
10
+ *
11
+ * **Usage:**
12
+ *
13
+ * ```js
14
+ * // In your edge function
15
+ * import { Thread, ThreadPool, GPUCompute } from 'thread/edge';
16
+ * import { setProgrammaticConfig } from 'thread/config';
17
+ *
18
+ * // Set config programmatically (no filesystem in edge)
19
+ * setProgrammaticConfig({
20
+ * framework: 'react',
21
+ * stateManager: 'zustand',
22
+ * });
23
+ * ```
24
+ *
25
+ * @module thread/edge
26
+ */
27
+
28
+ // Re-export everything from the main entry point
29
+ export * from './index.js';
30
+ export { default } from './index.js';
31
+
32
+ // Re-export environment-specific utilities
33
+ export { env } from './env.js';
34
+ export { createWorker, terminateWorker, supportsWorkers, workerInfo } from './worker-factory.js';
35
+ export { gpuEnv, isGPUAvailable, requestGPUAdapter, requestGPUDevice } from './gpu/env.js';
36
+
37
+ // Re-export programmatic config helpers (critical for edge environments)
38
+ export { setProgrammaticConfig, getProgrammaticConfig, clearProgrammaticConfig } from './config/index.js';