@peisar/peisar-wasm32-wasi 0.2.21 → 0.3.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@peisar/peisar-wasm32-wasi",
3
- "version": "0.2.21",
3
+ "version": "0.3.0",
4
4
  "main": "peisar.wasi.cjs",
5
5
  "files": [
6
6
  "peisar.wasm32-wasi.wasm",
@@ -30,8 +30,8 @@
30
30
  "browser": "peisar.wasi-browser.js",
31
31
  "type": "module",
32
32
  "dependencies": {
33
- "@napi-rs/wasm-runtime": "~1.2.4",
34
- "@emnapi/core": "2.0.0-alpha.5",
35
- "@emnapi/runtime": "2.0.0-alpha.5"
33
+ "@napi-rs/wasm-runtime": "~1.2.5",
34
+ "@emnapi/core": "2.0.0-alpha.6",
35
+ "@emnapi/runtime": "2.0.0-alpha.6"
36
36
  }
37
37
  }
@@ -1441,6 +1441,7 @@ try {
1441
1441
  }
1442
1442
  export default __napiModule.exports
1443
1443
  export const Peisar = __napiModule.exports.Peisar
1444
+ export const PeisarCache = __napiModule.exports.PeisarCache
1444
1445
  export const EmphasisLevel = __napiModule.exports.EmphasisLevel
1445
1446
  export const TableCellAlignment = __napiModule.exports.TableCellAlignment
1446
1447
  export const TaskState = __napiModule.exports.TaskState
package/peisar.wasi.cjs CHANGED
@@ -1,4 +1,4 @@
1
- // napi-rs-artifact-metadata:{"version":2,"rootEntry":"index.js","exports":["Peisar","EmphasisLevel","TableCellAlignment","TaskState"],"managedRootEntries":["browser.js","index.js","peisar.wasm","peisar.debug.wasm"]}
1
+ // napi-rs-artifact-metadata:{"version":2,"rootEntry":"index.js","exports":["Peisar","PeisarCache","EmphasisLevel","TableCellAlignment","TaskState"],"managedRootEntries":["browser.js","index.js","peisar.wasm","peisar.debug.wasm"]}
2
2
  /* eslint-disable */
3
3
  /* auto-generated by NAPI-RS */
4
4
 
@@ -222,6 +222,34 @@ function __hasWasiThreadCrashed() {
222
222
  }
223
223
 
224
224
  let __wasiThreadCrashDisposePromise
225
+ // Raised by the crash disposal right before it terminates the workers. See
226
+ // `__wasiSetImmediate`.
227
+ let __wasiReentryClosed = false
228
+
229
+ /**
230
+ * emnapi's `features.setImmediate` for this binding's context. emnapi defers
231
+ * its calls back into wasm through it: `_emnapi_set_immediate` (libuv handle
232
+ * closes, threadsafe-function finalizers), threadsafe-function dispatch
233
+ * (`async-send`) and the finalizer queue. One queued before a crash disposal
234
+ * terminates the workers still runs after it, and a worker terminated while it
235
+ * held a lock in the wasm heap (napi's heap-sync allocator lock spins and never
236
+ * gives up) leaves that call spinning on this thread for good. The binding is
237
+ * unusable after a crash disposal, so those calls are dropped.
238
+ *
239
+ * Known residual, still able to enter wasm after a crash disposal because emnapi
240
+ * offers no hook for them: the `FinalizationRegistry` callbacks that free
241
+ * external memory when GC collects a value (`_free`, the shared-buffer meta
242
+ * release), threadsafe-function dispatch of `async-send` type 1 and
243
+ * `_emnapi_next_tick` (both `Promise.resolve().then`), and every deferred
244
+ * call under emnapi 1.x, whose `createContext` ignores `features`.
245
+ */
246
+ function __wasiSetImmediate(callback) {
247
+ return setImmediate(function () {
248
+ if (!__wasiReentryClosed) {
249
+ callback()
250
+ }
251
+ })
252
+ }
225
253
 
226
254
  /**
227
255
  * Stores the first error a pool worker reported, with the worker's id. Called
@@ -371,6 +399,8 @@ function __disposeWasiBindingAfterThreadCrash() {
371
399
  return __wasiThreadCrashDisposePromise
372
400
  }
373
401
  __releaseEmnapiWaitingRequestHandle()
402
+ // No call into wasm after this: see `__wasiSetImmediate`.
403
+ __wasiReentryClosed = true
374
404
  let workerResult
375
405
  try {
376
406
  workerResult = __terminateWasiWorkers()
@@ -455,6 +485,130 @@ function __rollbackWasiInitializationAfterThreadCrash() {
455
485
  return [crashError]
456
486
  }
457
487
 
488
+ const __wasiThreadPoolReconcileSymbol = Symbol.for('napi.rs.wasi.reconcileThreadPool')
489
+
490
+ /**
491
+ * Takes a Worker out of emnapi's reuse pool, if it is still there. emnapi
492
+ * terminates a pooled Worker that failed to load but (up to
493
+ * @emnapi/wasi-threads 2.1.0) leaves it in the pool, where the next thread
494
+ * spawn would pop it. A newer emnapi removes it itself, so this is a no-op
495
+ * then.
496
+ */
497
+ function __removeWasiPoolWorker(manager, worker) {
498
+ const index = manager.unusedWorkers.indexOf(worker)
499
+ if (index !== -1) {
500
+ manager.unusedWorkers.splice(index, 1)
501
+ }
502
+ }
503
+
504
+ /**
505
+ * Takes a Worker the thread manager just terminated out of `__wasiWorkers`
506
+ * once it has exited, not before. `terminateWorker` only starts Node's
507
+ * asynchronous `worker.terminate()` and drops its promise, so a disposal that
508
+ * begins before the exit has to find this Worker in the set and wait for it
509
+ * like any other. A second `terminate()` settles when the Worker has exited,
510
+ * the way `__terminateWasiWorkers` waits.
511
+ */
512
+ function __untrackWasiWorkerOnExit(worker) {
513
+ const terminated = worker.terminate()
514
+ if (__isThenable(terminated)) {
515
+ Promise.resolve(terminated).then(
516
+ () => {
517
+ __wasiWorkers.delete(worker)
518
+ },
519
+ // Left tracked: disposal terminates it again and reports the error.
520
+ () => {},
521
+ )
522
+ } else {
523
+ __wasiWorkers.delete(worker)
524
+ }
525
+ }
526
+
527
+ /**
528
+ * Matches emnapi's idle reuse pool to the addon's configured MultiThread worker
529
+ * count, which the addon exports as `napi_wasm_runtime_pool_workers`
530
+ * (napi-async-runtime; 0 under CurrentThread). `reuseWorker: true` starts the
531
+ * pool empty, so without this every pool thread the first async call spawns
532
+ * boots a Worker and loads the wasm into it first. Here each missing Worker is
533
+ * created and starts loading now; a spawn later pops one that is already
534
+ * booting. Idle Workers above the count are terminated, last in first out,
535
+ * the way a spawn takes them. A Worker a spawn already took is not in the pool
536
+ * and is left alone.
537
+ *
538
+ * Runs once after a successful load and, when the loader wraps it, after every
539
+ * successful `configureAsyncRuntime`. Also reachable as
540
+ * binding[Symbol.for('napi.rs.wasi.reconcileThreadPool')]().
541
+ * It never throws and never waits on a Worker: a Worker whose load fails is
542
+ * dropped from the pool when its load rejects. Nothing at all happens after a
543
+ * thread crash, once disposal started, or for an addon without the export.
544
+ */
545
+ function __reconcileWasiThreadPool() {
546
+ try {
547
+ if (__wasiDisposed || __wasiDisposePromise || __hasWasiThreadCrashed()) {
548
+ return
549
+ }
550
+ const read = __napiInstance?.exports?.napi_wasm_runtime_pool_workers
551
+ if (typeof read !== 'function') {
552
+ return
553
+ }
554
+ const count = read() >>> 0
555
+ const manager = __getWasiThreadManager()
556
+ if (
557
+ !manager ||
558
+ !Array.isArray(manager.unusedWorkers) ||
559
+ typeof manager.allocateUnusedWorker !== 'function' ||
560
+ typeof manager.loadWasmModuleToWorker !== 'function'
561
+ ) {
562
+ return
563
+ }
564
+ // Both loops are bounded by the difference they start from, so a manager
565
+ // that does not update `unusedWorkers` the way emnapi does cannot spin.
566
+ for (let excess = manager.unusedWorkers.length - count; excess > 0; excess--) {
567
+ const worker = manager.unusedWorkers[manager.unusedWorkers.length - 1]
568
+ manager.terminateWorker(worker)
569
+ __removeWasiPoolWorker(manager, worker)
570
+ __untrackWasiWorkerOnExit(worker)
571
+ // Compatibility with @emnapi/wasi-threads 2.1.0 and older:
572
+ // `terminateWorker` installs a reporter that logs every emnapi message
573
+ // still queued on the port, so a Worker that finished loading just
574
+ // before it was terminated prints 'received "loaded" command from
575
+ // terminated worker'. Nothing listens for that Worker any more, and a
576
+ // newer emnapi ignores the late 'loaded' itself, so this is harmless
577
+ // there. `__terminateWasiWorkers` does the same.
578
+ worker.onmessage = undefined
579
+ }
580
+ for (let missing = count - manager.unusedWorkers.length; missing > 0; missing--) {
581
+ let worker
582
+ try {
583
+ // Through `onCreateWorker`: tracked in `__wasiWorkers`, unref'd, and
584
+ // handed the crash flags like any pool Worker.
585
+ worker = manager.allocateUnusedWorker()
586
+ manager
587
+ .loadWasmModuleToWorker(worker)
588
+ .then(undefined, () => __removeWasiPoolWorker(manager, worker))
589
+ } catch {
590
+ if (worker !== undefined) {
591
+ __removeWasiPoolWorker(manager, worker)
592
+ try {
593
+ manager.terminateWorker(worker)
594
+ __untrackWasiWorkerOnExit(worker)
595
+ } catch {}
596
+ }
597
+ return
598
+ }
599
+ }
600
+ } catch {}
601
+ }
602
+
603
+ function __publishWasiThreadPoolReconcile(exports) {
604
+ Object.defineProperty(exports, __wasiThreadPoolReconcileSymbol, {
605
+ configurable: false,
606
+ enumerable: false,
607
+ value: __reconcileWasiThreadPool,
608
+ writable: false,
609
+ })
610
+ }
611
+
458
612
  const __cwd = process.cwd()
459
613
  const __rootDir = __nodePath.parse(__cwd).root
460
614
  const __hostRoot =
@@ -2011,7 +2165,7 @@ try {
2011
2165
  const __finishAutoDestroyCapture = __captureEmnapiAutoDestroyListener()
2012
2166
  try {
2013
2167
  __emnapiContext = __wrapEmnapiContextDestroyForSettlement(
2014
- __emnapiCreateContext({ autoDestroy: false }),
2168
+ __emnapiCreateContext({ autoDestroy: false, features: { setImmediate: __wasiSetImmediate } }),
2015
2169
  __prepareWasmEnvCleanup,
2016
2170
  __isPreparingWasmEnvCleanup,
2017
2171
  )
@@ -2108,6 +2262,7 @@ try {
2108
2262
  },
2109
2263
  }))
2110
2264
  __publishWasiDispose(__napiModule.exports)
2265
+ __publishWasiThreadPoolReconcile(__napiModule.exports)
2111
2266
  // The CommonJS tail below aliases `__napiModule.exports`; a named module
2112
2267
  // export does not travel with it, so carry the marker on the binding itself
2113
2268
  // too. Three things pin the stamp to exactly this spot:
@@ -2136,8 +2291,14 @@ try {
2136
2291
  __runWasiInitializationRollback(rollback)
2137
2292
  throw rollback.error
2138
2293
  }
2294
+ // Preload the pool for the count the addon configured during registration.
2295
+ // See `__reconcileWasiThreadPool`.
2296
+ try {
2297
+ __reconcileWasiThreadPool()
2298
+ } catch {}
2139
2299
  module.exports = __napiModule.exports
2140
2300
  module.exports.Peisar = __napiModule.exports.Peisar
2301
+ module.exports.PeisarCache = __napiModule.exports.PeisarCache
2141
2302
  module.exports.EmphasisLevel = __napiModule.exports.EmphasisLevel
2142
2303
  module.exports.TableCellAlignment = __napiModule.exports.TableCellAlignment
2143
2304
  module.exports.TaskState = __napiModule.exports.TaskState
package/peisar.wasi.d.cts CHANGED
@@ -65,6 +65,68 @@ export declare class Peisar {
65
65
  get astJson(): string
66
66
  }
67
67
 
68
+ /**
69
+ * In-memory cache of markdown files under a given directory.
70
+ * The cache maps absolute PathBuf -> raw file contents.
71
+ */
72
+ export declare class PeisarCache {
73
+ /**
74
+ * Construct a new PeisarCache using the default discovery behavior.
75
+ *
76
+ * JS: `new PeisarCache(entryDir, assetsDir?)` — both plain strings.
77
+ */
78
+ constructor(entryDir: string, assetsDir?: string | undefined | null)
79
+ /**
80
+ * JS: same as the constructor, for callers that prefer a factory shape.
81
+ * Kept non-generic so NAPI can export it.
82
+ */
83
+ static withConfigJs(entryDir: string, assetsDir?: string | undefined | null): PeisarCache
84
+ /**
85
+ * Start watching the entry_dir recursively. The watcher will update the
86
+ * in-memory cache on create/modify/remove events for markdown files and
87
+ * assets, and persist updates to disk.
88
+ *
89
+ * The returned Result is only for watcher setup errors; runtime errors are
90
+ * printed to stderr by the watch callback.
91
+ *
92
+ * JS: `cache.startWatchingJs()` — errors surface as JS exceptions.
93
+ */
94
+ startWatchingJs(): void
95
+ /**
96
+ * ----------------------------------------------------------------
97
+ * JavaScript (NAPI) surface
98
+ * ----------------------------------------------------------------
99
+ * All JS methods take/return plain strings because `&Path`/`PathBuf`
100
+ * do not cross the NAPI boundary.
101
+ * JS: `cache.getText(absPath)` — cached text of a file, or null.
102
+ */
103
+ getText(absPath: string): string | null
104
+ /** JS: `cache.getBinary(absPath)` — cached bytes of a binary asset, or null. */
105
+ getBinary(absPath: string): Array<number> | null
106
+ /** JS: `cache.listFiles()` — absolute paths of everything cached. */
107
+ listFiles(): Array<string>
108
+ /** JS: `cache.markdownFiles()` — absolute paths of cached markdown files. */
109
+ markdownFiles(): Array<string>
110
+ /** JS: `cache.assetFiles()` — absolute paths of cached non-markdown files. */
111
+ assetFiles(): Array<string>
112
+ /**
113
+ * JS: `cache.onChange(cb)` — invoke `cb(event)` on every file change the
114
+ * watcher detects while watching is active. Returns a subscription id
115
+ * that can be passed to `offChange(id)`.
116
+ *
117
+ * The callback is wrapped in a `ThreadsafeFunction` because notify
118
+ * events fire on the watcher thread, not the JS main thread.
119
+ */
120
+ onChange(callback: (event: CacheChangeEvent) => void): number
121
+ /** JS: `cache.offChange(id)` — remove a previously registered callback. */
122
+ offChange(id: number): void
123
+ /**
124
+ * JS: `cache.dispose()` — stop the watcher and drop JS change callbacks.
125
+ * Safe to call more than once.
126
+ */
127
+ dispose(): void
128
+ }
129
+
68
130
  /**
69
131
  * Options that control how Markdown is parsed.
70
132
  *
@@ -185,6 +247,25 @@ export interface BlockParseResultJs {
185
247
  consumed?: number
186
248
  }
187
249
 
250
+ /** A file-change event delivered to JavaScript `onChange` callbacks. */
251
+ export interface CacheChangeEvent {
252
+ /** Absolute path of the changed file. */
253
+ path: string
254
+ /** `"create"`, `"modify"`, `"remove"`, or `"other"`. */
255
+ kind: string
256
+ /** `true` when the file is under the markdown entry directory. */
257
+ isMarkdown: boolean
258
+ }
259
+
260
+ /**
261
+ * An entry stored in the cache: raw UTF-8 text (markdown) or raw bytes
262
+ * (binary assets). Exposed to JavaScript so JS consumers can branch on
263
+ * the variant.
264
+ */
265
+ export type CachedContent =
266
+ | { type: 'Text'; field0: string }
267
+ | { type: 'Binary'; field0: Array<number> }
268
+
188
269
  /**
189
270
  * A Markdown document — the root of the AST.
190
271
  *
Binary file