@jongleberry/vurst-runtime 0.0.1

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.
Files changed (3) hide show
  1. package/index.d.ts +19 -0
  2. package/index.js +167 -0
  3. package/package.json +13 -0
package/index.d.ts ADDED
@@ -0,0 +1,19 @@
1
+ export interface NativeRuntime {
2
+ beginShutdown(): void
3
+ getPendingCount(): number
4
+ isShuttingDown(): boolean
5
+ resetForTests(): void
6
+ runNativeFunction(fn: Function, receiver: unknown, args: unknown[]): unknown
7
+ waitForDrain(): Promise<void>
8
+ }
9
+
10
+ export declare function createNativeRuntime(): NativeRuntime
11
+
12
+ export declare function wrapNativeAddon<T extends Record<PropertyKey, unknown>>(
13
+ addon: T,
14
+ runtime?: NativeRuntime,
15
+ ): T
16
+
17
+ export declare function beginNativeAddonShutdown(): void
18
+ export declare function isNativeAddonShuttingDown(): boolean
19
+ export declare function waitForNativeAddonWorkToDrain(): Promise<void>
package/index.js ADDED
@@ -0,0 +1,167 @@
1
+ 'use strict'
2
+
3
+ // Shutdown drain wrapper for @jongleberry/vurst-* N-API packages.
4
+ //
5
+ // Provides a keep-alive handle while native work is in flight, shutdown
6
+ // signalling, and a drain-wait primitive for graceful process exit.
7
+
8
+ const SHUTDOWN_ERROR_MESSAGE =
9
+ 'Rust N-API runtime is shutting down; refusing to start new native work.'
10
+
11
+ /**
12
+ * @param {unknown} value
13
+ * @returns {value is PromiseLike<unknown>}
14
+ */
15
+ function isPromiseLike(value) {
16
+ if (value == null) return false
17
+ if (typeof value !== 'object' && typeof value !== 'function') return false
18
+ return typeof (/** @type {any} */ (value).then) === 'function'
19
+ }
20
+
21
+ /**
22
+ * Create an isolated runtime instance.
23
+ *
24
+ * Returns an object with:
25
+ * - `beginShutdown()` — mark runtime as shutting down
26
+ * - `getPendingCount()` — number of in-flight native calls
27
+ * - `isShuttingDown()` — whether shutdown has been signalled
28
+ * - `resetForTests()` — reset all state (use only in tests)
29
+ * - `runNativeFunction(fn, receiver, args)` — track and call a native fn
30
+ * - `waitForDrain()` — Promise that resolves when pendingCount reaches 0
31
+ */
32
+ function createNativeRuntime() {
33
+ let pendingCount = 0
34
+ let shuttingDown = false
35
+ /** @type {NodeJS.Timeout | null} */
36
+ let keepAliveHandle = null
37
+ /** @type {Set<() => void>} */
38
+ const drainResolvers = new Set()
39
+
40
+ function ensureKeepAliveHandle() {
41
+ if (keepAliveHandle != null) return
42
+ // Keep the event loop alive while native work is in flight. Some N-API async
43
+ // completions do not show up as ordinary JS handles, so without this guard a
44
+ // worker can start tearing down before the Promise settles back into JS.
45
+ keepAliveHandle = setInterval(() => {}, 60_000)
46
+ }
47
+
48
+ function releaseKeepAliveHandle() {
49
+ if (pendingCount !== 0 || keepAliveHandle == null) return
50
+ clearInterval(keepAliveHandle)
51
+ keepAliveHandle = null
52
+ }
53
+
54
+ function resolveDrainWaiters() {
55
+ if (pendingCount !== 0) return
56
+ for (const resolve of drainResolvers) resolve()
57
+ drainResolvers.clear()
58
+ }
59
+
60
+ /**
61
+ * @template T
62
+ * @param {PromiseLike<T>} promise
63
+ * @returns {Promise<T>}
64
+ */
65
+ function trackPromise(promise) {
66
+ pendingCount += 1
67
+ ensureKeepAliveHandle()
68
+
69
+ return Promise.resolve(promise).finally(() => {
70
+ pendingCount -= 1
71
+ releaseKeepAliveHandle()
72
+ resolveDrainWaiters()
73
+ })
74
+ }
75
+
76
+ /**
77
+ * @param {Function} fn
78
+ * @param {unknown} receiver
79
+ * @param {unknown[]} args
80
+ * @returns {unknown}
81
+ */
82
+ function runNativeFunction(fn, receiver, args) {
83
+ if (shuttingDown) throw new Error(SHUTDOWN_ERROR_MESSAGE)
84
+ const result = Reflect.apply(fn, receiver, args)
85
+ if (!isPromiseLike(result)) return result
86
+ return trackPromise(result)
87
+ }
88
+
89
+ /** @returns {Promise<void>} */
90
+ function waitForDrain() {
91
+ if (pendingCount === 0) return Promise.resolve()
92
+ return new Promise(resolve => drainResolvers.add(resolve))
93
+ }
94
+
95
+ function beginShutdown() {
96
+ shuttingDown = true
97
+ }
98
+
99
+ function resetForTests() {
100
+ shuttingDown = false
101
+ pendingCount = 0
102
+ drainResolvers.clear()
103
+ if (keepAliveHandle != null) clearInterval(keepAliveHandle)
104
+ keepAliveHandle = null
105
+ }
106
+
107
+ return {
108
+ beginShutdown,
109
+ getPendingCount: () => pendingCount,
110
+ isShuttingDown: () => shuttingDown,
111
+ resetForTests,
112
+ runNativeFunction,
113
+ waitForDrain,
114
+ }
115
+ }
116
+
117
+ const sharedRuntime = createNativeRuntime()
118
+
119
+ /**
120
+ * Wrap a native N-API addon object so every function call is tracked through
121
+ * the runtime for shutdown/drain handling.
122
+ *
123
+ * @template {Record<PropertyKey, unknown>} T
124
+ * @param {T} addon
125
+ * @param {ReturnType<typeof createNativeRuntime>} [runtime]
126
+ * @returns {T}
127
+ */
128
+ function wrapNativeAddon(addon, runtime) {
129
+ const rt = runtime ?? sharedRuntime
130
+ /** @type {Map<PropertyKey, { original: Function, wrapped: Function }>} */
131
+ const wrappedFunctions = new Map()
132
+
133
+ return new Proxy(addon, {
134
+ get(target, prop, receiver) {
135
+ const value = Reflect.get(target, prop, receiver)
136
+ if (typeof value !== 'function') return value
137
+
138
+ const cached = wrappedFunctions.get(prop)
139
+ if (cached != null && cached.original === value) return cached.wrapped
140
+
141
+ const wrapped = (...args) => rt.runNativeFunction(value, target, args)
142
+ wrappedFunctions.set(prop, { original: value, wrapped })
143
+ return wrapped
144
+ },
145
+ })
146
+ }
147
+
148
+ /** Signal the shared runtime to stop accepting new native work. */
149
+ function beginNativeAddonShutdown() {
150
+ sharedRuntime.beginShutdown()
151
+ }
152
+
153
+ /** @returns {boolean} */
154
+ function isNativeAddonShuttingDown() {
155
+ return sharedRuntime.isShuttingDown()
156
+ }
157
+
158
+ /** @returns {Promise<void>} */
159
+ function waitForNativeAddonWorkToDrain() {
160
+ return sharedRuntime.waitForDrain()
161
+ }
162
+
163
+ module.exports.createNativeRuntime = createNativeRuntime
164
+ module.exports.wrapNativeAddon = wrapNativeAddon
165
+ module.exports.beginNativeAddonShutdown = beginNativeAddonShutdown
166
+ module.exports.isNativeAddonShuttingDown = isNativeAddonShuttingDown
167
+ module.exports.waitForNativeAddonWorkToDrain = waitForNativeAddonWorkToDrain
package/package.json ADDED
@@ -0,0 +1,13 @@
1
+ {
2
+ "name": "@jongleberry/vurst-runtime",
3
+ "version": "0.0.1",
4
+ "description": "Shutdown drain wrapper for @jongleberry/vurst-* N-API packages.",
5
+ "main": "index.js",
6
+ "types": "index.d.ts",
7
+ "license": "MIT",
8
+ "engines": { "node": ">= 18" },
9
+ "repository": { "type": "git", "url": "git+https://github.com/jonathanong/vurst.git" },
10
+ "files": ["index.js", "index.d.ts", "README.md"],
11
+ "keywords": ["rust", "napi", "shutdown", "drain", "graceful"],
12
+ "publishConfig": { "access": "public", "provenance": true }
13
+ }