@napi-rs/cli 3.10.6 → 3.10.7
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/dist/cli.js +199 -4
- package/dist/index.cjs +199 -4
- package/dist/index.d.cts +1 -1
- package/dist/index.js +199 -4
- package/docs/wasi.md +68 -0
- package/package.json +1 -1
- package/src/api/__tests__/templates.spec.ts +692 -5
- package/src/api/templates/load-wasi-template.ts +213 -1
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
} from './binding-target.js'
|
|
6
6
|
|
|
7
7
|
const WASI_DISPOSE_SYMBOL = 'napi.rs.wasi.dispose'
|
|
8
|
+
const WASI_THREAD_POOL_RECONCILE_SYMBOL = 'napi.rs.wasi.reconcileThreadPool'
|
|
8
9
|
const WASI_ROLLBACK_REGISTRY_SYMBOL = 'napi.rs.wasi.rollback.registry.v1'
|
|
9
10
|
|
|
10
11
|
/**
|
|
@@ -4005,6 +4006,11 @@ function __createWasiWorker(filename) {
|
|
|
4005
4006
|
threads && asyncRuntime
|
|
4006
4007
|
? `${indent}__releaseCurrentThreadHostTimers()\n`
|
|
4007
4008
|
: ''
|
|
4009
|
+
// The threaded context defers emnapi's calls into wasm through
|
|
4010
|
+
// `__wasiSetImmediate`, which a crash disposal closes.
|
|
4011
|
+
const emnapiContextOptions = threads
|
|
4012
|
+
? '{ autoDestroy: false, features: { setImmediate: __wasiSetImmediate } }'
|
|
4013
|
+
: '{ autoDestroy: false }'
|
|
4008
4014
|
// Only the threaded flavor has pool workers whose wasm thread can die under
|
|
4009
4015
|
// this one. See `__disposeWasiBindingAtExit`.
|
|
4010
4016
|
const threadCrashLatch = threads
|
|
@@ -4050,6 +4056,34 @@ function __hasWasiThreadCrashed() {
|
|
|
4050
4056
|
}
|
|
4051
4057
|
|
|
4052
4058
|
let __wasiThreadCrashDisposePromise
|
|
4059
|
+
// Raised by the crash disposal right before it terminates the workers. See
|
|
4060
|
+
// \`__wasiSetImmediate\`.
|
|
4061
|
+
let __wasiReentryClosed = false
|
|
4062
|
+
|
|
4063
|
+
/**
|
|
4064
|
+
* emnapi's \`features.setImmediate\` for this binding's context. emnapi defers
|
|
4065
|
+
* its calls back into wasm through it: \`_emnapi_set_immediate\` (libuv handle
|
|
4066
|
+
* closes, threadsafe-function finalizers), threadsafe-function dispatch
|
|
4067
|
+
* (\`async-send\`) and the finalizer queue. One queued before a crash disposal
|
|
4068
|
+
* terminates the workers still runs after it, and a worker terminated while it
|
|
4069
|
+
* held a lock in the wasm heap (napi's heap-sync allocator lock spins and never
|
|
4070
|
+
* gives up) leaves that call spinning on this thread for good. The binding is
|
|
4071
|
+
* unusable after a crash disposal, so those calls are dropped.
|
|
4072
|
+
*
|
|
4073
|
+
* Known residual, still able to enter wasm after a crash disposal because emnapi
|
|
4074
|
+
* offers no hook for them: the \`FinalizationRegistry\` callbacks that free
|
|
4075
|
+
* external memory when GC collects a value (\`_free\`, the shared-buffer meta
|
|
4076
|
+
* release), threadsafe-function dispatch of \`async-send\` type 1 and
|
|
4077
|
+
* \`_emnapi_next_tick\` (both \`Promise.resolve().then\`), and every deferred
|
|
4078
|
+
* call under emnapi 1.x, whose \`createContext\` ignores \`features\`.
|
|
4079
|
+
*/
|
|
4080
|
+
function __wasiSetImmediate(callback) {
|
|
4081
|
+
return setImmediate(function () {
|
|
4082
|
+
if (!__wasiReentryClosed) {
|
|
4083
|
+
callback()
|
|
4084
|
+
}
|
|
4085
|
+
})
|
|
4086
|
+
}
|
|
4053
4087
|
|
|
4054
4088
|
/**
|
|
4055
4089
|
* Stores the first error a pool worker reported, with the worker's id. Called
|
|
@@ -4200,6 +4234,8 @@ function __disposeWasiBindingAfterThreadCrash() {
|
|
|
4200
4234
|
}
|
|
4201
4235
|
__releaseEmnapiWaitingRequestHandle()
|
|
4202
4236
|
${releaseCurrentThreadHostTimers(' ')}\
|
|
4237
|
+
// No call into wasm after this: see \`__wasiSetImmediate\`.
|
|
4238
|
+
__wasiReentryClosed = true
|
|
4203
4239
|
let workerResult
|
|
4204
4240
|
try {
|
|
4205
4241
|
workerResult = __terminateWasiWorkers()
|
|
@@ -4299,6 +4335,170 @@ ${releaseCurrentThreadHostTimers(' ')}\
|
|
|
4299
4335
|
}
|
|
4300
4336
|
`
|
|
4301
4337
|
: ''
|
|
4338
|
+
// Threaded flavor only: keeps emnapi's idle reuse pool at the addon's
|
|
4339
|
+
// configured MultiThread worker count. See docs/wasi.md, "Thread pool
|
|
4340
|
+
// preload".
|
|
4341
|
+
const threadPoolReconcile = threads
|
|
4342
|
+
? `
|
|
4343
|
+
const __wasiThreadPoolReconcileSymbol = Symbol.for('${WASI_THREAD_POOL_RECONCILE_SYMBOL}')
|
|
4344
|
+
|
|
4345
|
+
/**
|
|
4346
|
+
* Takes a Worker out of emnapi's reuse pool, if it is still there. emnapi
|
|
4347
|
+
* terminates a pooled Worker that failed to load but (up to
|
|
4348
|
+
* @emnapi/wasi-threads 2.1.0) leaves it in the pool, where the next thread
|
|
4349
|
+
* spawn would pop it. A newer emnapi removes it itself, so this is a no-op
|
|
4350
|
+
* then.
|
|
4351
|
+
*/
|
|
4352
|
+
function __removeWasiPoolWorker(manager, worker) {
|
|
4353
|
+
const index = manager.unusedWorkers.indexOf(worker)
|
|
4354
|
+
if (index !== -1) {
|
|
4355
|
+
manager.unusedWorkers.splice(index, 1)
|
|
4356
|
+
}
|
|
4357
|
+
}
|
|
4358
|
+
|
|
4359
|
+
/**
|
|
4360
|
+
* Takes a Worker the thread manager just terminated out of \`__wasiWorkers\`
|
|
4361
|
+
* once it has exited, not before. \`terminateWorker\` only starts Node's
|
|
4362
|
+
* asynchronous \`worker.terminate()\` and drops its promise, so a disposal that
|
|
4363
|
+
* begins before the exit has to find this Worker in the set and wait for it
|
|
4364
|
+
* like any other. A second \`terminate()\` settles when the Worker has exited,
|
|
4365
|
+
* the way \`__terminateWasiWorkers\` waits.
|
|
4366
|
+
*/
|
|
4367
|
+
function __untrackWasiWorkerOnExit(worker) {
|
|
4368
|
+
const terminated = worker.terminate()
|
|
4369
|
+
if (__isThenable(terminated)) {
|
|
4370
|
+
Promise.resolve(terminated).then(
|
|
4371
|
+
() => {
|
|
4372
|
+
__wasiWorkers.delete(worker)
|
|
4373
|
+
},
|
|
4374
|
+
// Left tracked: disposal terminates it again and reports the error.
|
|
4375
|
+
() => {},
|
|
4376
|
+
)
|
|
4377
|
+
} else {
|
|
4378
|
+
__wasiWorkers.delete(worker)
|
|
4379
|
+
}
|
|
4380
|
+
}
|
|
4381
|
+
|
|
4382
|
+
/**
|
|
4383
|
+
* Matches emnapi's idle reuse pool to the addon's configured MultiThread worker
|
|
4384
|
+
* count, which the addon exports as \`napi_wasm_runtime_pool_workers\`
|
|
4385
|
+
* (napi-async-runtime; 0 under CurrentThread). \`reuseWorker: true\` starts the
|
|
4386
|
+
* pool empty, so without this every pool thread the first async call spawns
|
|
4387
|
+
* boots a Worker and loads the wasm into it first. Here each missing Worker is
|
|
4388
|
+
* created and starts loading now; a spawn later pops one that is already
|
|
4389
|
+
* booting. Idle Workers above the count are terminated, last in first out,
|
|
4390
|
+
* the way a spawn takes them. A Worker a spawn already took is not in the pool
|
|
4391
|
+
* and is left alone.
|
|
4392
|
+
*
|
|
4393
|
+
* Runs once after a successful load and, when the loader wraps it, after every
|
|
4394
|
+
* successful \`configureAsyncRuntime\`. Also reachable as
|
|
4395
|
+
* binding[Symbol.for('${WASI_THREAD_POOL_RECONCILE_SYMBOL}')]().
|
|
4396
|
+
* It never throws and never waits on a Worker: a Worker whose load fails is
|
|
4397
|
+
* dropped from the pool when its load rejects. Nothing at all happens after a
|
|
4398
|
+
* thread crash, once disposal started, or for an addon without the export.
|
|
4399
|
+
*/
|
|
4400
|
+
function __reconcileWasiThreadPool() {
|
|
4401
|
+
try {
|
|
4402
|
+
if (__wasiDisposed || __wasiDisposePromise || __hasWasiThreadCrashed()) {
|
|
4403
|
+
return
|
|
4404
|
+
}
|
|
4405
|
+
const read = __napiInstance?.exports?.napi_wasm_runtime_pool_workers
|
|
4406
|
+
if (typeof read !== 'function') {
|
|
4407
|
+
return
|
|
4408
|
+
}
|
|
4409
|
+
const count = read() >>> 0
|
|
4410
|
+
const manager = __getWasiThreadManager()
|
|
4411
|
+
if (
|
|
4412
|
+
!manager ||
|
|
4413
|
+
!Array.isArray(manager.unusedWorkers) ||
|
|
4414
|
+
typeof manager.allocateUnusedWorker !== 'function' ||
|
|
4415
|
+
typeof manager.loadWasmModuleToWorker !== 'function'
|
|
4416
|
+
) {
|
|
4417
|
+
return
|
|
4418
|
+
}
|
|
4419
|
+
// Both loops are bounded by the difference they start from, so a manager
|
|
4420
|
+
// that does not update \`unusedWorkers\` the way emnapi does cannot spin.
|
|
4421
|
+
for (let excess = manager.unusedWorkers.length - count; excess > 0; excess--) {
|
|
4422
|
+
const worker = manager.unusedWorkers[manager.unusedWorkers.length - 1]
|
|
4423
|
+
manager.terminateWorker(worker)
|
|
4424
|
+
__removeWasiPoolWorker(manager, worker)
|
|
4425
|
+
__untrackWasiWorkerOnExit(worker)
|
|
4426
|
+
// Compatibility with @emnapi/wasi-threads 2.1.0 and older:
|
|
4427
|
+
// \`terminateWorker\` installs a reporter that logs every emnapi message
|
|
4428
|
+
// still queued on the port, so a Worker that finished loading just
|
|
4429
|
+
// before it was terminated prints 'received "loaded" command from
|
|
4430
|
+
// terminated worker'. Nothing listens for that Worker any more, and a
|
|
4431
|
+
// newer emnapi ignores the late 'loaded' itself, so this is harmless
|
|
4432
|
+
// there. \`__terminateWasiWorkers\` does the same.
|
|
4433
|
+
worker.onmessage = undefined
|
|
4434
|
+
}
|
|
4435
|
+
for (let missing = count - manager.unusedWorkers.length; missing > 0; missing--) {
|
|
4436
|
+
let worker
|
|
4437
|
+
try {
|
|
4438
|
+
// Through \`onCreateWorker\`: tracked in \`__wasiWorkers\`, unref'd, and
|
|
4439
|
+
// handed the crash flags like any pool Worker.
|
|
4440
|
+
worker = manager.allocateUnusedWorker()
|
|
4441
|
+
manager
|
|
4442
|
+
.loadWasmModuleToWorker(worker)
|
|
4443
|
+
.then(undefined, () => __removeWasiPoolWorker(manager, worker))
|
|
4444
|
+
} catch {
|
|
4445
|
+
if (worker !== undefined) {
|
|
4446
|
+
__removeWasiPoolWorker(manager, worker)
|
|
4447
|
+
try {
|
|
4448
|
+
manager.terminateWorker(worker)
|
|
4449
|
+
__untrackWasiWorkerOnExit(worker)
|
|
4450
|
+
} catch {}
|
|
4451
|
+
}
|
|
4452
|
+
return
|
|
4453
|
+
}
|
|
4454
|
+
}
|
|
4455
|
+
} catch {}
|
|
4456
|
+
}
|
|
4457
|
+
|
|
4458
|
+
function __publishWasiThreadPoolReconcile(exports) {
|
|
4459
|
+
Object.defineProperty(exports, __wasiThreadPoolReconcileSymbol, {
|
|
4460
|
+
configurable: false,
|
|
4461
|
+
enumerable: false,
|
|
4462
|
+
value: __reconcileWasiThreadPool,
|
|
4463
|
+
writable: false,
|
|
4464
|
+
})
|
|
4465
|
+
}
|
|
4466
|
+
${
|
|
4467
|
+
asyncRuntime
|
|
4468
|
+
? `
|
|
4469
|
+
/**
|
|
4470
|
+
* Replaces the addon's \`configureAsyncRuntime\` export, if it has one, with a
|
|
4471
|
+
* wrapper that reconciles the pool after every successful call: a configure
|
|
4472
|
+
* changes the count the pool was preloaded for. A configure that throws changed
|
|
4473
|
+
* nothing, so its error propagates and the pool stays as it is. Matched by
|
|
4474
|
+
* name, like the CurrentThread host install: it is the export
|
|
4475
|
+
* napi-async-runtime's adapter defines, and an addon that defines its own under
|
|
4476
|
+
* that name gets the same reconcile. Never fails the load.
|
|
4477
|
+
*/
|
|
4478
|
+
function __wrapWasiConfigureAsyncRuntime(binding) {
|
|
4479
|
+
let configure
|
|
4480
|
+
try {
|
|
4481
|
+
configure = binding.configureAsyncRuntime
|
|
4482
|
+
} catch {
|
|
4483
|
+
return
|
|
4484
|
+
}
|
|
4485
|
+
if (typeof configure !== 'function') {
|
|
4486
|
+
return
|
|
4487
|
+
}
|
|
4488
|
+
try {
|
|
4489
|
+
binding.configureAsyncRuntime = function configureAsyncRuntime(...args) {
|
|
4490
|
+
const result = Reflect.apply(configure, this, args)
|
|
4491
|
+
try {
|
|
4492
|
+
__reconcileWasiThreadPool()
|
|
4493
|
+
} catch {}
|
|
4494
|
+
return result
|
|
4495
|
+
}
|
|
4496
|
+
} catch {}
|
|
4497
|
+
}
|
|
4498
|
+
`
|
|
4499
|
+
: ''
|
|
4500
|
+
}`
|
|
4501
|
+
: ''
|
|
4302
4502
|
const workerRuntimeImport = threads
|
|
4303
4503
|
? ` createOnMessage: __wasmCreateOnMessageForFsProxy,\n`
|
|
4304
4504
|
: ''
|
|
@@ -4410,6 +4610,7 @@ const { createContext: __emnapiCreateContext } = require('@emnapi/runtime')
|
|
|
4410
4610
|
${asyncRuntimeImport}\
|
|
4411
4611
|
${workerExecArgv}\
|
|
4412
4612
|
${threadCrashLatch}\
|
|
4613
|
+
${threadPoolReconcile}\
|
|
4413
4614
|
|
|
4414
4615
|
const __cwd = process.cwd()
|
|
4415
4616
|
const __rootDir = __nodePath.parse(__cwd).root
|
|
@@ -4631,7 +4832,7 @@ try {
|
|
|
4631
4832
|
const __finishAutoDestroyCapture = __captureEmnapiAutoDestroyListener()
|
|
4632
4833
|
try {
|
|
4633
4834
|
__emnapiContext = __wrapEmnapiContextDestroyForSettlement(
|
|
4634
|
-
__emnapiCreateContext({
|
|
4835
|
+
__emnapiCreateContext(${emnapiContextOptions}),
|
|
4635
4836
|
__prepareWasmEnvCleanup,
|
|
4636
4837
|
__isPreparingWasmEnvCleanup,
|
|
4637
4838
|
)
|
|
@@ -4675,7 +4876,9 @@ ${captureAddonCrashFlag}\
|
|
|
4675
4876
|
},
|
|
4676
4877
|
}))
|
|
4677
4878
|
__publishWasiDispose(__napiModule.exports)
|
|
4879
|
+
${threads ? ' __publishWasiThreadPoolReconcile(__napiModule.exports)\n' : ''}\
|
|
4678
4880
|
${installAsyncRuntimeHosts}\
|
|
4881
|
+
${threads && asyncRuntime ? ' __wrapWasiConfigureAsyncRuntime(__napiModule.exports)\n' : ''}\
|
|
4679
4882
|
// The CommonJS tail below aliases \`__napiModule.exports\`; a named module
|
|
4680
4883
|
// export does not travel with it, so carry the marker on the binding itself
|
|
4681
4884
|
// too. Three things pin the stamp to exactly this spot:
|
|
@@ -4704,5 +4907,14 @@ ${installAsyncRuntimeHosts}\
|
|
|
4704
4907
|
__runWasiInitializationRollback(rollback)
|
|
4705
4908
|
throw rollback.error
|
|
4706
4909
|
}
|
|
4910
|
+
${
|
|
4911
|
+
threads
|
|
4912
|
+
? `// Preload the pool for the count the addon configured during registration.
|
|
4913
|
+
// See \`__reconcileWasiThreadPool\`.
|
|
4914
|
+
try {
|
|
4915
|
+
__reconcileWasiThreadPool()
|
|
4916
|
+
} catch {}
|
|
4707
4917
|
`
|
|
4918
|
+
: ''
|
|
4919
|
+
}`
|
|
4708
4920
|
}
|