env-runner 0.2.3 → 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.
@@ -1,6 +1,16 @@
1
- import { resolveVirtualModules } from "./virtual-loader.mjs";
1
+ import { encodeVirtualModules, normalizeVirtualModules, resolveVirtualModules, warnVirtualPathCollisions } from "./virtual-loader.mjs";
2
2
  import { rm } from "node:fs/promises";
3
3
  import { proxyFetch, proxyUpgrade } from "httpxy";
4
+ function hostEnv(extra) {
5
+ const env = {
6
+ ...process.env,
7
+ ...extra
8
+ };
9
+ const stdout = process.stdout;
10
+ if (!("FORCE_COLOR" in env) && !env.NO_COLOR && stdout?.isTTY) env.FORCE_COLOR = "1";
11
+ if (!("COLUMNS" in env) && stdout?.columns) env.COLUMNS = String(stdout.columns);
12
+ return env;
13
+ }
4
14
  var BaseEnvRunner = class {
5
15
  closed = false;
6
16
  _name;
@@ -11,7 +21,11 @@ var BaseEnvRunner = class {
11
21
  _address;
12
22
  _messageListeners;
13
23
  _pendingRequests;
24
+ _closeCause;
14
25
  _virtualResolved;
26
+ _virtualUpdates = Promise.resolve();
27
+ #virtualUpdateId = 0;
28
+ _processData;
15
29
  constructor(opts) {
16
30
  this._name = opts.name;
17
31
  this._workerEntry = opts.workerEntry;
@@ -49,24 +63,29 @@ var BaseEnvRunner = class {
49
63
  }
50
64
  waitForReady(timeout = 15e3) {
51
65
  if (this.ready) return Promise.resolve();
52
- if (this.closed) return Promise.reject(/* @__PURE__ */ new Error("Runner closed before becoming ready"));
66
+ if (this.closed) return Promise.reject(closedBeforeReadyError(this._closeCause));
53
67
  return new Promise((resolve, reject) => {
54
68
  const timer = setTimeout(() => {
55
- this._messageListeners.delete(listener);
69
+ cleanup();
56
70
  reject(/* @__PURE__ */ new Error("Runner did not become ready in time"));
57
71
  }, timeout);
58
72
  const listener = () => {
59
73
  if (this.ready) {
60
- clearTimeout(timer);
61
- this._messageListeners.delete(listener);
74
+ cleanup();
62
75
  resolve();
63
- } else if (this.closed) {
64
- clearTimeout(timer);
65
- this._messageListeners.delete(listener);
66
- reject(/* @__PURE__ */ new Error("Runner closed before becoming ready"));
67
76
  }
68
77
  };
78
+ const onClose = (cause) => {
79
+ cleanup();
80
+ reject(closedBeforeReadyError(cause));
81
+ };
82
+ const cleanup = () => {
83
+ clearTimeout(timer);
84
+ this._messageListeners.delete(listener);
85
+ this._pendingRequests.delete(onClose);
86
+ };
69
87
  this._messageListeners.add(listener);
88
+ this._pendingRequests.add(onClose);
70
89
  });
71
90
  }
72
91
  rpc(name, data, opts) {
@@ -82,27 +101,27 @@ var BaseEnvRunner = class {
82
101
  }).then((msg) => msg.data);
83
102
  }
84
103
  async reloadModule(timeout = 5e3) {
104
+ await this._virtualUpdates;
85
105
  await this._request({ event: "reload-module" }, {
86
106
  match: (msg) => msg?.event === "module-reloaded",
87
107
  timeout,
88
108
  timeoutError: "Module reload timed out"
89
109
  });
90
110
  }
91
- async invalidateModule(specifier, timeout = 5e3) {
92
- const source = await this._refreshVirtualSource(specifier);
93
- await this._request({
94
- event: "invalidate-module",
95
- specifier,
96
- source
97
- }, {
98
- match: (msg) => msg?.event === "module-invalidated" && msg.specifier === specifier,
99
- timeout,
100
- timeoutError: `Module invalidation timed out for "${specifier}"`
101
- });
111
+ updateVirtualModules(changes, timeout = 5e3) {
112
+ return this._enqueueVirtualUpdate(() => changes, timeout);
113
+ }
114
+ invalidateModule(specifier, timeout = 5e3) {
115
+ return this._enqueueVirtualUpdate(() => {
116
+ const source = this._virtualSources?.[specifier];
117
+ if (source === void 0) throw new Error(`Cannot invalidate "${specifier}" (not a registered virtual module)`);
118
+ return { [specifier]: source };
119
+ }, timeout);
102
120
  }
103
121
  async close(cause) {
104
122
  if (this.closed) return;
105
123
  this.closed = true;
124
+ this._closeCause = cause;
106
125
  for (const rejectPending of this._pendingRequests) rejectPending(cause);
107
126
  this._pendingRequests.clear();
108
127
  this._hooks.onClose?.(this, cause);
@@ -133,6 +152,36 @@ var BaseEnvRunner = class {
133
152
  if (message?.event === "init-error" && !this.ready && !this.closed) this.close(new Error(String(message.error || "Worker initialization failed")));
134
153
  for (const listener of this._messageListeners) listener(message);
135
154
  }
155
+ _processEnv() {
156
+ const data = this._data || {};
157
+ const virtual = data.virtual;
158
+ try {
159
+ this._processData = JSON.stringify(virtual ? {
160
+ ...data,
161
+ virtual: encodeVirtualModules(virtual)
162
+ } : data);
163
+ } catch (error) {
164
+ throw new TypeError(`Runner data must be JSON-serializable: ${error?.message || error}`, { cause: error });
165
+ }
166
+ return hostEnv({ ENV_RUNNER_NAME: this._name });
167
+ }
168
+ _handleProcessMessage(message) {
169
+ if (message?.event !== "request-init-data") {
170
+ this._handleMessage(message);
171
+ return;
172
+ }
173
+ if (this.closed) return;
174
+ try {
175
+ this.sendMessage({
176
+ event: "init-data",
177
+ data: this._processData ?? "{}"
178
+ });
179
+ } catch (error) {
180
+ const cause = new Error(`Failed to send runner data to the worker over IPC: ${error?.message || error}`, { cause: error });
181
+ console.error(`[env-runner] ${cause.message}`);
182
+ this.close(cause);
183
+ }
184
+ }
136
185
  _request(message, opts) {
137
186
  if (this.closed) return Promise.reject(/* @__PURE__ */ new Error("Runner is closed"));
138
187
  return new Promise((resolve, reject) => {
@@ -168,8 +217,17 @@ var BaseEnvRunner = class {
168
217
  }
169
218
  _resolveVirtualData() {
170
219
  const virtual = this._data?.virtual;
171
- this._virtualSources = virtual;
172
- if (!virtual || !Object.values(virtual).some((v) => typeof v === "function")) return;
220
+ this._virtualSources = { ...virtual };
221
+ this._data = { ...this._data };
222
+ warnVirtualPathCollisions(Object.keys(virtual ?? {}));
223
+ if (!virtual) return;
224
+ if (!Object.values(virtual).some((v) => typeof v === "function")) try {
225
+ this._data.virtual = normalizeVirtualModules(virtual);
226
+ return;
227
+ } catch (error) {
228
+ this._virtualResolved = Promise.reject(error);
229
+ return this._virtualResolved;
230
+ }
173
231
  this._virtualResolved = resolveVirtualModules(virtual).then((resolved) => {
174
232
  this._data = {
175
233
  ...this._data,
@@ -178,20 +236,50 @@ var BaseEnvRunner = class {
178
236
  });
179
237
  return this._virtualResolved;
180
238
  }
181
- async _refreshVirtualSource(specifier) {
182
- await this._virtualResolved?.catch(() => {});
183
- const original = this._virtualSources?.[specifier];
184
- if (typeof original !== "function") return;
185
- const source = await original();
186
- const resolved = this._data?.virtual;
187
- if (resolved) resolved[specifier] = source;
188
- return source;
239
+ _enqueueVirtualUpdate(changes, timeout) {
240
+ const update = this._virtualUpdates.then(async () => {
241
+ await this._virtualResolved;
242
+ if (this.closed) throw new Error("Runner is closed");
243
+ const entries = Object.entries(changes()).filter(([, source]) => source !== void 0);
244
+ if (entries.length === 0) return;
245
+ const sets = Object.fromEntries(entries.filter((entry) => entry[1] !== null));
246
+ const resolved = await resolveVirtualModules(sets);
247
+ const sources = this._virtualSources ??= {};
248
+ const virtual = (this._data ??= {}).virtual ??= {};
249
+ let added = false;
250
+ for (const [key, source] of entries) if (source === null) {
251
+ resolved[key] = null;
252
+ delete sources[key];
253
+ delete virtual[key];
254
+ } else {
255
+ added ||= !Object.hasOwn(sources, key);
256
+ sources[key] = source;
257
+ virtual[key] = resolved[key];
258
+ }
259
+ if (added) warnVirtualPathCollisions(Object.keys(virtual));
260
+ if (!this.ready) await this.waitForReady();
261
+ await this._applyVirtualUpdates(resolved, timeout);
262
+ });
263
+ this._virtualUpdates = update.catch(() => {});
264
+ return update;
265
+ }
266
+ async _applyVirtualUpdates(changes, timeout) {
267
+ const id = ++this.#virtualUpdateId;
268
+ await this._request({
269
+ event: "update-virtual-modules",
270
+ id,
271
+ changes: encodeVirtualModules(changes)
272
+ }, {
273
+ match: (msg) => msg?.event === "virtual-modules-updated" && msg.id === id,
274
+ timeout,
275
+ timeoutError: "Virtual module update timed out"
276
+ });
189
277
  }
190
278
  _initWithVirtualData(init) {
191
279
  const pending = this._resolveVirtualData();
192
280
  if (pending) pending.then(() => {
193
281
  if (!this.closed) init();
194
- }, (error) => this.close(error));
282
+ }).catch((error) => this.close(error));
195
283
  else init();
196
284
  }
197
285
  async _closeSocket() {
@@ -200,4 +288,7 @@ var BaseEnvRunner = class {
200
288
  this._address = void 0;
201
289
  }
202
290
  };
203
- export { BaseEnvRunner };
291
+ function closedBeforeReadyError(cause) {
292
+ return new Error("Runner closed before becoming ready", cause ? { cause } : void 0);
293
+ }
294
+ export { BaseEnvRunner, hostEnv };
@@ -0,0 +1,13 @@
1
+ function receiveProcessData() {
2
+ return new Promise((resolve) => {
3
+ const onMessage = (message) => {
4
+ if (message?.event === "init-data") {
5
+ process.off("message", onMessage);
6
+ resolve(message.data);
7
+ }
8
+ };
9
+ process.on("message", onMessage);
10
+ process.send({ event: "request-init-data" });
11
+ }).then((json) => JSON.parse(json));
12
+ }
13
+ export { receiveProcessData };