@decohere/flux 0.0.1-dev.95 → 0.0.1-dev.96

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.
@@ -160,8 +160,10 @@ class HotReloadServer extends _nodeevents.EventEmitter {
160
160
  try {
161
161
  const mod = await this.loader.importEntry(entryPath);
162
162
  state.module = mod?.default;
163
+ state.failed = false;
163
164
  } catch (error) {
164
165
  state.module = undefined;
166
+ state.failed = true;
165
167
  this.logger.error("There was an error booting the entry point. Your application may not function as expected", error);
166
168
  }
167
169
  }
@@ -309,6 +311,7 @@ class HotReloadServer extends _nodeevents.EventEmitter {
309
311
  this.loader.setSourceFile(fullPath, project);
310
312
  this.moduleCache.set(fullPath, null);
311
313
  this.reloadIfAffected(fullPath);
314
+ this.retryFailedEntries(fullPath);
312
315
  }
313
316
  return;
314
317
  }
@@ -316,7 +319,25 @@ class HotReloadServer extends _nodeevents.EventEmitter {
316
319
  if (!project.isSourceFile(file)) return;
317
320
  this.loader.invalidateTranspile(fullPath);
318
321
  this.reloadIfAffected(fullPath);
322
+ this.retryFailedEntries(fullPath);
319
323
  };
324
+ /**
325
+ * A failed boot (e.g. importing a file that doesn't exist yet) may have recorded no
326
+ * dependency edges, so the graph can't tell whether this save fixes it. Any source
327
+ * add or change retries failed entries (#512).
328
+ */ retryFailedEntries(fullPath) {
329
+ const failed = [
330
+ ...this.entries.values()
331
+ ].filter((entry)=>entry.failed);
332
+ if (failed.length === 0) return;
333
+ for (const entry of failed){
334
+ // New URL version: Node caches the failed load and would replay the same error.
335
+ this.invalidateModuleCaches(entry.path);
336
+ this.pendingEntryReboots.add(entry.path);
337
+ }
338
+ this.pendingTriggers.add(fullPath);
339
+ this.scheduleReboot();
340
+ }
320
341
  reloadIfAffected(fullPath) {
321
342
  const deps = this.getDependencyGraph(fullPath);
322
343
  const modules = [
@@ -345,30 +366,24 @@ class HotReloadServer extends _nodeevents.EventEmitter {
345
366
  return true;
346
367
  }
347
368
  // ── Dependency graph ─────────────────────────────────────────────────────
348
- getDependencyGraph(moduleId, parentModules = []) {
369
+ /**
370
+ * Every module that imports `moduleId`, directly or transitively. Visits each module
371
+ * once: import graphs share modules (diamonds) and may legally contain cycles (#515).
372
+ */ getDependencyGraph(moduleId) {
349
373
  if (this.isSelf(moduleId)) return [];
350
- const moduleDeps = this.dependencies.get(moduleId) ?? [];
351
- if (parentModules.includes(moduleId)) {
352
- this.logger.error(`Circular reference detected: ${moduleId}`);
353
- return [];
354
- }
355
- let subDeps = [];
356
- for (const dep of moduleDeps){
357
- if (!this.loader.hasSourceFile(dep)) continue;
358
- const subs = this.getDependencyGraph(dep, [
359
- moduleId,
360
- ...parentModules
361
- ]);
362
- subDeps = [
363
- ...subDeps,
364
- ...subs
365
- ];
374
+ const found = new Set();
375
+ const queue = [
376
+ moduleId
377
+ ];
378
+ for(let next = queue.shift(); next != null; next = queue.shift()){
379
+ for (const importer of this.dependencies.get(next) ?? []){
380
+ if (found.has(importer)) continue;
381
+ found.add(importer);
382
+ if (this.loader.hasSourceFile(importer) && !this.isSelf(importer)) queue.push(importer);
383
+ }
366
384
  }
367
385
  return [
368
- ...new Set([
369
- ...moduleDeps,
370
- ...subDeps
371
- ])
386
+ ...found
372
387
  ];
373
388
  }
374
389
  trackDependency(filePath, parentPath) {
@@ -128,8 +128,10 @@ export class HotReloadServer extends EventEmitter {
128
128
  try {
129
129
  const mod = await this.loader.importEntry(entryPath);
130
130
  state.module = mod?.default;
131
+ state.failed = false;
131
132
  } catch (error) {
132
133
  state.module = undefined;
134
+ state.failed = true;
133
135
  this.logger.error("There was an error booting the entry point. Your application may not function as expected", error);
134
136
  }
135
137
  }
@@ -277,6 +279,7 @@ export class HotReloadServer extends EventEmitter {
277
279
  this.loader.setSourceFile(fullPath, project);
278
280
  this.moduleCache.set(fullPath, null);
279
281
  this.reloadIfAffected(fullPath);
282
+ this.retryFailedEntries(fullPath);
280
283
  }
281
284
  return;
282
285
  }
@@ -284,7 +287,25 @@ export class HotReloadServer extends EventEmitter {
284
287
  if (!project.isSourceFile(file)) return;
285
288
  this.loader.invalidateTranspile(fullPath);
286
289
  this.reloadIfAffected(fullPath);
290
+ this.retryFailedEntries(fullPath);
287
291
  };
292
+ /**
293
+ * A failed boot (e.g. importing a file that doesn't exist yet) may have recorded no
294
+ * dependency edges, so the graph can't tell whether this save fixes it. Any source
295
+ * add or change retries failed entries (#512).
296
+ */ retryFailedEntries(fullPath) {
297
+ const failed = [
298
+ ...this.entries.values()
299
+ ].filter((entry)=>entry.failed);
300
+ if (failed.length === 0) return;
301
+ for (const entry of failed){
302
+ // New URL version: Node caches the failed load and would replay the same error.
303
+ this.invalidateModuleCaches(entry.path);
304
+ this.pendingEntryReboots.add(entry.path);
305
+ }
306
+ this.pendingTriggers.add(fullPath);
307
+ this.scheduleReboot();
308
+ }
288
309
  reloadIfAffected(fullPath) {
289
310
  const deps = this.getDependencyGraph(fullPath);
290
311
  const modules = [
@@ -313,30 +334,24 @@ export class HotReloadServer extends EventEmitter {
313
334
  return true;
314
335
  }
315
336
  // ── Dependency graph ─────────────────────────────────────────────────────
316
- getDependencyGraph(moduleId, parentModules = []) {
337
+ /**
338
+ * Every module that imports `moduleId`, directly or transitively. Visits each module
339
+ * once: import graphs share modules (diamonds) and may legally contain cycles (#515).
340
+ */ getDependencyGraph(moduleId) {
317
341
  if (this.isSelf(moduleId)) return [];
318
- const moduleDeps = this.dependencies.get(moduleId) ?? [];
319
- if (parentModules.includes(moduleId)) {
320
- this.logger.error(`Circular reference detected: ${moduleId}`);
321
- return [];
322
- }
323
- let subDeps = [];
324
- for (const dep of moduleDeps){
325
- if (!this.loader.hasSourceFile(dep)) continue;
326
- const subs = this.getDependencyGraph(dep, [
327
- moduleId,
328
- ...parentModules
329
- ]);
330
- subDeps = [
331
- ...subDeps,
332
- ...subs
333
- ];
342
+ const found = new Set();
343
+ const queue = [
344
+ moduleId
345
+ ];
346
+ for(let next = queue.shift(); next != null; next = queue.shift()){
347
+ for (const importer of this.dependencies.get(next) ?? []){
348
+ if (found.has(importer)) continue;
349
+ found.add(importer);
350
+ if (this.loader.hasSourceFile(importer) && !this.isSelf(importer)) queue.push(importer);
351
+ }
334
352
  }
335
353
  return [
336
- ...new Set([
337
- ...moduleDeps,
338
- ...subDeps
339
- ])
354
+ ...found
340
355
  ];
341
356
  }
342
357
  trackDependency(filePath, parentPath) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@decohere/flux",
3
- "version": "0.0.1-dev.95",
3
+ "version": "0.0.1-dev.96",
4
4
  "description": "Hot module reload server for Node.js ESM applications",
5
5
  "keywords": [
6
6
  "typescript",
@@ -25,11 +25,11 @@
25
25
  "test": "echo \"Error: no test specified\" && exit 1"
26
26
  },
27
27
  "dependencies": {
28
- "@decohere/crucible": "0.0.1-dev.95",
29
- "@decohere/forge": "0.0.1-dev.95",
30
- "@decohere/mesh": "0.0.1-dev.95",
31
- "@decohere/nexus": "0.0.1-dev.95",
32
- "@decohere/scribe": "0.0.1-dev.95",
28
+ "@decohere/crucible": "0.0.1-dev.96",
29
+ "@decohere/forge": "0.0.1-dev.96",
30
+ "@decohere/mesh": "0.0.1-dev.96",
31
+ "@decohere/nexus": "0.0.1-dev.96",
32
+ "@decohere/scribe": "0.0.1-dev.96",
33
33
  "commander": "^13.1.0",
34
34
  "rxjs": "^7.8.2"
35
35
  },
@@ -70,7 +70,17 @@ export declare class HotReloadServer extends EventEmitter<HotReloadServerEvents>
70
70
  */
71
71
  private onPostResolve;
72
72
  private onFileChange;
73
+ /**
74
+ * A failed boot (e.g. importing a file that doesn't exist yet) may have recorded no
75
+ * dependency edges, so the graph can't tell whether this save fixes it. Any source
76
+ * add or change retries failed entries (#512).
77
+ */
78
+ private retryFailedEntries;
73
79
  private reloadIfAffected;
80
+ /**
81
+ * Every module that imports `moduleId`, directly or transitively. Visits each module
82
+ * once: import graphs share modules (diamonds) and may legally contain cycles (#515).
83
+ */
74
84
  private getDependencyGraph;
75
85
  private trackDependency;
76
86
  private getCachedOrNew;