@graftland/hmr 0.2.0-beta.0 → 0.2.0-beta.2
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/README.md +62 -16
- package/README.zh-CN.md +24 -7
- package/README.zh-TW.md +24 -7
- package/dist/index.d.ts +4 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +88 -12
- package/dist/index.js.map +1 -1
- package/dist/internal/changes.js +78 -36
- package/dist/internal/changes.js.map +1 -1
- package/dist/internal/digest.js +6 -0
- package/dist/internal/digest.js.map +1 -0
- package/dist/internal/modules.js +184 -92
- package/dist/internal/modules.js.map +1 -1
- package/package.json +2 -2
- package/src/index.ts +356 -166
- package/src/internal/changes.ts +225 -123
- package/src/internal/digest.ts +5 -0
- package/src/internal/modules.ts +309 -248
package/src/index.ts
CHANGED
|
@@ -82,6 +82,8 @@ export interface ImportOptions {
|
|
|
82
82
|
export interface Hmr {
|
|
83
83
|
/** Keeps imported entries registered until this HMR session closes. */
|
|
84
84
|
readonly importer: (specifier: string) => Promise<unknown>
|
|
85
|
+
/** Uses the application context and Node cache, without watching or replacing the module. */
|
|
86
|
+
readonly importModule: (specifier: string) => Effect.Effect<unknown, HmrError>
|
|
85
87
|
/** Registers an observation lease in observationScope, or the caller's scope when omitted. */
|
|
86
88
|
readonly importerScoped: (
|
|
87
89
|
specifier: string,
|
|
@@ -90,8 +92,8 @@ export interface Hmr {
|
|
|
90
92
|
/** Imports every entry before replacing anything. Activation failures belong to Graft. */
|
|
91
93
|
readonly reload: Effect.Effect<ReloadResult, HmrError>
|
|
92
94
|
/**
|
|
93
|
-
* Consuming this stream installs
|
|
94
|
-
* per second to recover missed events. Known native loading limitations emit
|
|
95
|
+
* Consuming this stream installs graph-driven directory watchers, polls stat once
|
|
96
|
+
* per second, and audits contents every ten seconds to recover missed events. Known native loading limitations emit
|
|
95
97
|
* Skipped even without a file event. The consumer owns the stream's lifetimes.
|
|
96
98
|
*/
|
|
97
99
|
readonly watch: (
|
|
@@ -169,6 +171,32 @@ export const make = (
|
|
|
169
171
|
Ref.set(closed, true).pipe(Effect.andThen(Effect.sync(hooks.close))),
|
|
170
172
|
),
|
|
171
173
|
)
|
|
174
|
+
// Node can return a cached entry after its last observation lease closes.
|
|
175
|
+
// Keep current entry attempts without polling or reloading unleased entries.
|
|
176
|
+
// The hooks also retain cached instance facts for older resolution views.
|
|
177
|
+
const cached = yield* Ref.make(HashMap.empty<string, Attempt>())
|
|
178
|
+
|
|
179
|
+
const retryDirty = yield* Ref.make(HashSet.empty<string>())
|
|
180
|
+
const retryAll = yield* Ref.make(false)
|
|
181
|
+
const observations = Ref.get(owners).pipe(
|
|
182
|
+
Effect.map((owners) =>
|
|
183
|
+
Array.flatMap(HashMap.toValues(owners), (owner) =>
|
|
184
|
+
Array.getSomes([owner.active, owner.pending]),
|
|
185
|
+
),
|
|
186
|
+
),
|
|
187
|
+
)
|
|
188
|
+
const collect = Effect.gen(function* () {
|
|
189
|
+
const attempts = yield* observations
|
|
190
|
+
const currentCache = yield* Ref.get(cached)
|
|
191
|
+
hooks.collect([...attempts, ...HashMap.toValues(currentCache)])
|
|
192
|
+
const files = hooks.files(attempts)
|
|
193
|
+
yield* Ref.update(
|
|
194
|
+
retryDirty,
|
|
195
|
+
HashSet.filter((url) => HashMap.has(files, url)),
|
|
196
|
+
)
|
|
197
|
+
if (attempts.length === 0) yield* Ref.set(retryAll, false)
|
|
198
|
+
})
|
|
199
|
+
|
|
172
200
|
const state = yield* Ref.make({
|
|
173
201
|
generation: hooks.prepare(),
|
|
174
202
|
entries: HashMap.empty<
|
|
@@ -223,21 +251,62 @@ export const make = (
|
|
|
223
251
|
return Option.some(request)
|
|
224
252
|
})
|
|
225
253
|
|
|
226
|
-
const changedResolutions =
|
|
227
|
-
|
|
254
|
+
const changedResolutions = Effect.gen(function* () {
|
|
255
|
+
const current = yield* Ref.get(owners)
|
|
256
|
+
const attempts = hooks.resolutions(
|
|
257
|
+
Array.flatMap(HashMap.toValues(current), (owner) =>
|
|
258
|
+
Array.getSomes([owner.active, owner.pending]),
|
|
259
|
+
),
|
|
260
|
+
)
|
|
261
|
+
const requests = HashSet.fromIterable(
|
|
262
|
+
Array.filterMap(attempts, (attempt) =>
|
|
263
|
+
attempt.request === undefined
|
|
264
|
+
? Result.fail(undefined)
|
|
265
|
+
: Result.succeed(attempt.request),
|
|
266
|
+
),
|
|
267
|
+
)
|
|
268
|
+
const resolveAll = (requests: HashSet.HashSet<string>) =>
|
|
228
269
|
Effect.forEach(
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
270
|
+
requests,
|
|
271
|
+
(request) =>
|
|
272
|
+
physicalTarget(request).pipe(
|
|
273
|
+
Effect.map((target) => [request, target] as const),
|
|
232
274
|
),
|
|
233
|
-
|
|
234
|
-
|
|
275
|
+
{ concurrency: 8 },
|
|
276
|
+
).pipe(Effect.map(HashMap.fromIterable))
|
|
277
|
+
const actual = yield* resolveAll(requests)
|
|
278
|
+
const missingTargets = HashSet.fromIterable(
|
|
279
|
+
Array.filterMap(attempts, (attempt) =>
|
|
280
|
+
attempt.request !== undefined &&
|
|
281
|
+
Option.getOrUndefined(HashMap.get(actual, attempt.request)) ===
|
|
282
|
+
undefined &&
|
|
283
|
+
!HashMap.has(actual, attempt.url)
|
|
284
|
+
? Result.succeed(attempt.url)
|
|
285
|
+
: Result.fail(undefined),
|
|
235
286
|
),
|
|
236
|
-
)
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
287
|
+
)
|
|
288
|
+
const remaining = yield* resolveAll(missingTargets)
|
|
289
|
+
const targets = HashMap.union(actual, remaining)
|
|
290
|
+
|
|
291
|
+
return Array.fromIterable(
|
|
292
|
+
HashSet.fromIterable(
|
|
293
|
+
Array.filterMap(attempts, (attempt) => {
|
|
294
|
+
if (attempt.request === undefined) return Result.fail(undefined)
|
|
295
|
+
const actual = Option.getOrUndefined(
|
|
296
|
+
HashMap.get(targets, attempt.request),
|
|
297
|
+
)
|
|
298
|
+
const expected = pathToFileURL(fileURLToPath(attempt.url)).href
|
|
299
|
+
return actual === expected ||
|
|
300
|
+
(actual === undefined &&
|
|
301
|
+
Option.getOrUndefined(HashMap.get(targets, attempt.url)) ===
|
|
302
|
+
undefined)
|
|
303
|
+
? Result.fail(undefined)
|
|
304
|
+
: Result.succeed(attempt.request)
|
|
305
|
+
}),
|
|
306
|
+
),
|
|
307
|
+
)
|
|
308
|
+
})
|
|
309
|
+
|
|
241
310
|
const load = Effect.fnUntraced(function* (specifier: string) {
|
|
242
311
|
const namespace = yield* Effect.tryPromise({
|
|
243
312
|
try: () => import(/* @vite-ignore */ hooks.request(specifier)),
|
|
@@ -296,6 +365,11 @@ export const make = (
|
|
|
296
365
|
onSome: (request) => ({ request }),
|
|
297
366
|
}),
|
|
298
367
|
}))
|
|
368
|
+
if (Option.isSome(attempt))
|
|
369
|
+
yield* Ref.update(
|
|
370
|
+
cached,
|
|
371
|
+
HashMap.set(attempt.value.url, attempt.value),
|
|
372
|
+
)
|
|
299
373
|
yield* Ref.update(
|
|
300
374
|
owners,
|
|
301
375
|
HashMap.set(token, {
|
|
@@ -319,7 +393,10 @@ export const make = (
|
|
|
319
393
|
})
|
|
320
394
|
}
|
|
321
395
|
return { token, exit: Exit.map(exit, (entry) => entry.namespace) }
|
|
322
|
-
}).pipe(
|
|
396
|
+
}).pipe(
|
|
397
|
+
Effect.ensuring(Effect.sync(hooks.discard)),
|
|
398
|
+
Effect.ensuring(collect),
|
|
399
|
+
),
|
|
323
400
|
),
|
|
324
401
|
({ token }) =>
|
|
325
402
|
serial(
|
|
@@ -334,13 +411,26 @@ export const make = (
|
|
|
334
411
|
: Result.succeed({ ...entry, leases })
|
|
335
412
|
}),
|
|
336
413
|
}))
|
|
337
|
-
}),
|
|
414
|
+
}).pipe(Effect.ensuring(collect)),
|
|
338
415
|
),
|
|
339
416
|
).pipe(
|
|
340
417
|
Effect.flatMap(({ exit }) => exit),
|
|
341
418
|
preserveExit,
|
|
342
419
|
)
|
|
343
420
|
|
|
421
|
+
const importModule: Hmr["importModule"] = (specifier) =>
|
|
422
|
+
serial(
|
|
423
|
+
checkOpen.pipe(
|
|
424
|
+
Effect.andThen(
|
|
425
|
+
Effect.tryPromise({
|
|
426
|
+
try: () => import(/* @vite-ignore */ hooks.rawRequest(specifier)),
|
|
427
|
+
catch: (cause) =>
|
|
428
|
+
HmrError.make({ stage: "import", specifier, cause }),
|
|
429
|
+
}),
|
|
430
|
+
),
|
|
431
|
+
),
|
|
432
|
+
)
|
|
433
|
+
|
|
344
434
|
const importerScoped: Hmr["importerScoped"] = (specifier, options) =>
|
|
345
435
|
options === undefined
|
|
346
436
|
? importWithLease(specifier)
|
|
@@ -358,166 +448,265 @@ export const make = (
|
|
|
358
448
|
),
|
|
359
449
|
)
|
|
360
450
|
|
|
361
|
-
const
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
451
|
+
const unsupportedAttempts = (attempts: ReadonlyArray<Attempt>) =>
|
|
452
|
+
Array.reduce(attempts, HashSet.empty<string>(), (all, attempt) =>
|
|
453
|
+
HashSet.union(
|
|
454
|
+
all,
|
|
455
|
+
hooks.unsupported(attempt.generation, [attempt.url]),
|
|
456
|
+
),
|
|
457
|
+
)
|
|
458
|
+
const unsupportedModules = Ref.get(owners).pipe(
|
|
459
|
+
Effect.map((owned) =>
|
|
460
|
+
unsupportedAttempts(
|
|
461
|
+
Array.getSomes(
|
|
462
|
+
Array.map(HashMap.toValues(owned), (owner) => owner.active),
|
|
463
|
+
),
|
|
366
464
|
),
|
|
367
465
|
),
|
|
368
466
|
)
|
|
369
467
|
|
|
370
|
-
const
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
modules: Array.fromIterable(unsupported),
|
|
384
|
-
reason: "restart-required",
|
|
385
|
-
})
|
|
386
|
-
// Advance even without entries so retries bypass cached failed imports.
|
|
387
|
-
const candidate = hooks.prepare()
|
|
388
|
-
hooks.start(candidate, true)
|
|
389
|
-
return yield* Effect.gen(function* () {
|
|
390
|
-
const loaded = yield* Effect.forEach(entries, ([url, previous]) =>
|
|
391
|
-
Ref.update(
|
|
392
|
-
owners,
|
|
393
|
-
HashMap.map((owner, token) =>
|
|
394
|
-
HashSet.has(previous.leases, token)
|
|
395
|
-
? {
|
|
396
|
-
...owner,
|
|
397
|
-
pending: Option.map(owner.active, (attempt) => ({
|
|
398
|
-
...attempt,
|
|
399
|
-
url,
|
|
400
|
-
generation: candidate,
|
|
401
|
-
})),
|
|
402
|
-
}
|
|
403
|
-
: owner,
|
|
404
|
-
),
|
|
405
|
-
).pipe(
|
|
406
|
-
Effect.andThen(load(url)),
|
|
407
|
-
Effect.map((entry) =>
|
|
408
|
-
Option.some({
|
|
409
|
-
url,
|
|
410
|
-
previous: previous.plugin,
|
|
411
|
-
leases: previous.leases,
|
|
412
|
-
plugin: entry.plugin,
|
|
413
|
-
}),
|
|
414
|
-
),
|
|
415
|
-
Effect.catchIf(
|
|
416
|
-
(error) =>
|
|
417
|
-
error.stage === "import" && isMissingEntry(url, error.cause),
|
|
418
|
-
() => Effect.succeedNone,
|
|
419
|
-
),
|
|
420
|
-
),
|
|
421
|
-
)
|
|
422
|
-
const candidates = Array.getSomes(loaded)
|
|
423
|
-
const alternatives = Array.reduce(
|
|
424
|
-
candidates,
|
|
425
|
-
HashMap.empty<Plugin.Plugin, HashSet.HashSet<Plugin.Plugin>>(),
|
|
426
|
-
(groups, { previous, plugin }) =>
|
|
427
|
-
HashMap.set(
|
|
428
|
-
groups,
|
|
429
|
-
previous,
|
|
430
|
-
HashSet.add(
|
|
431
|
-
Option.getOrElse(
|
|
432
|
-
HashMap.get(groups, previous),
|
|
433
|
-
HashSet.empty<Plugin.Plugin>,
|
|
434
|
-
),
|
|
435
|
-
plugin,
|
|
436
|
-
),
|
|
437
|
-
),
|
|
438
|
-
)
|
|
439
|
-
const ambiguous = Array.filter(candidates, ({ previous }) =>
|
|
440
|
-
Option.exists(
|
|
441
|
-
HashMap.get(alternatives, previous),
|
|
442
|
-
(plugins) => HashSet.size(plugins) > 1,
|
|
443
|
-
),
|
|
444
|
-
)
|
|
445
|
-
if (ambiguous.length > 0)
|
|
446
|
-
return WatchResult.Skipped({
|
|
447
|
-
reason: "ambiguous-exports",
|
|
448
|
-
modules: Array.map(ambiguous, ({ url }) => url),
|
|
449
|
-
})
|
|
450
|
-
const next = HashMap.fromIterable(
|
|
451
|
-
Array.map(
|
|
452
|
-
candidates,
|
|
453
|
-
({ url, plugin, leases }) => [url, { plugin, leases }] as const,
|
|
454
|
-
),
|
|
455
|
-
)
|
|
456
|
-
const pairs = Array.map(
|
|
457
|
-
candidates,
|
|
458
|
-
({ previous, plugin }) => [previous, plugin] as const,
|
|
459
|
-
)
|
|
468
|
+
const reloadChanged = (dirty?: HashSet.HashSet<string>) =>
|
|
469
|
+
serial(
|
|
470
|
+
Effect.gen(function* () {
|
|
471
|
+
yield* checkOpen
|
|
472
|
+
const fullRetry = yield* Ref.get(retryAll)
|
|
473
|
+
const force = dirty === undefined || fullRetry
|
|
474
|
+
const pendingDirty = yield* Ref.get(retryDirty)
|
|
475
|
+
const changedFiles =
|
|
476
|
+
dirty === undefined
|
|
477
|
+
? pendingDirty
|
|
478
|
+
: HashSet.union(pendingDirty, dirty)
|
|
479
|
+
yield* Ref.set(retryDirty, changedFiles)
|
|
480
|
+
yield* Ref.set(retryAll, force)
|
|
460
481
|
const changed = yield* changedResolutions
|
|
461
482
|
if (changed.length > 0)
|
|
462
483
|
return WatchResult.Skipped({
|
|
463
484
|
reason: "restart-required",
|
|
464
485
|
modules: changed,
|
|
465
486
|
})
|
|
466
|
-
const
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
)
|
|
470
|
-
if (Result.isFailure(replacement))
|
|
487
|
+
const { entries } = yield* Ref.get(state)
|
|
488
|
+
const unsupported = yield* unsupportedModules
|
|
489
|
+
if (HashSet.size(unsupported) > 0)
|
|
471
490
|
return WatchResult.Skipped({
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
Array.contains(replacement.failure.indices, index)
|
|
475
|
-
? Result.succeed(url)
|
|
476
|
-
: Result.fail(undefined),
|
|
477
|
-
),
|
|
491
|
+
modules: Array.fromIterable(unsupported),
|
|
492
|
+
reason: "restart-required",
|
|
478
493
|
})
|
|
479
|
-
yield* Ref.
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
HashSet.has(leases, token)
|
|
483
|
-
? {
|
|
484
|
-
active: Option.map(owner.pending, (attempt) => ({
|
|
485
|
-
...attempt,
|
|
486
|
-
url,
|
|
487
|
-
generation: candidate,
|
|
488
|
-
})),
|
|
489
|
-
pending: Option.none(),
|
|
490
|
-
}
|
|
491
|
-
: owner,
|
|
492
|
-
),
|
|
493
|
-
),
|
|
494
|
+
const owned = yield* Ref.get(owners)
|
|
495
|
+
const attempts = Array.flatMap(HashMap.toValues(owned), (owner) =>
|
|
496
|
+
Array.getSomes([owner.active, owner.pending]),
|
|
494
497
|
)
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
498
|
+
const active = Array.getSomes(
|
|
499
|
+
Array.map(HashMap.toValues(owned), (owner) => owner.active),
|
|
500
|
+
)
|
|
501
|
+
let selected = entries
|
|
502
|
+
let invalid = HashSet.empty<string>()
|
|
503
|
+
if (!force) {
|
|
504
|
+
let affected = changedFiles
|
|
505
|
+
// Plugin identity groups and importer closure can expand each other.
|
|
506
|
+
// Reach a fixed point before allocating any candidate instance.
|
|
507
|
+
while (true) {
|
|
508
|
+
invalid = hooks.invalidated(attempts, affected)
|
|
509
|
+
const plugins = HashSet.fromIterable(
|
|
510
|
+
Array.filterMap(Array.fromIterable(entries), ([url, entry]) =>
|
|
511
|
+
HashSet.has(affected, url) ||
|
|
512
|
+
Array.some(Array.fromIterable(entry.leases), (token) =>
|
|
513
|
+
Option.exists(HashMap.get(owned, token), (owner) =>
|
|
514
|
+
Array.some(
|
|
515
|
+
Array.getSomes([owner.active, owner.pending]),
|
|
516
|
+
(attempt) => hooks.affected(attempt, invalid),
|
|
517
|
+
),
|
|
518
|
+
),
|
|
519
|
+
)
|
|
520
|
+
? Result.succeed(entry.plugin)
|
|
521
|
+
: Result.fail(undefined),
|
|
522
|
+
),
|
|
523
|
+
)
|
|
524
|
+
selected = HashMap.filter(entries, (entry) =>
|
|
525
|
+
HashSet.has(plugins, entry.plugin),
|
|
526
|
+
)
|
|
527
|
+
const expanded = HashSet.union(
|
|
528
|
+
affected,
|
|
529
|
+
HashSet.fromIterable(HashMap.keys(selected)),
|
|
530
|
+
)
|
|
531
|
+
if (HashSet.size(expanded) === HashSet.size(affected)) break
|
|
532
|
+
affected = expanded
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
// Advance even without entries so retries bypass cached failed imports.
|
|
536
|
+
const candidate = force
|
|
537
|
+
? hooks.prepare()
|
|
538
|
+
: hooks.prepare(active, invalid)
|
|
539
|
+
|
|
540
|
+
hooks.start(candidate, true)
|
|
541
|
+
return yield* Effect.gen(function* () {
|
|
542
|
+
const loaded = yield* Effect.forEach(selected, ([url, previous]) =>
|
|
543
|
+
Ref.update(
|
|
544
|
+
owners,
|
|
545
|
+
HashMap.map((owner, token) =>
|
|
546
|
+
HashSet.has(previous.leases, token)
|
|
547
|
+
? {
|
|
548
|
+
...owner,
|
|
549
|
+
pending: Option.map(owner.active, (attempt) => ({
|
|
550
|
+
...attempt,
|
|
551
|
+
url,
|
|
552
|
+
generation: candidate,
|
|
553
|
+
})),
|
|
554
|
+
}
|
|
555
|
+
: owner,
|
|
556
|
+
),
|
|
557
|
+
).pipe(
|
|
558
|
+
Effect.andThen(load(url)),
|
|
559
|
+
Effect.map((entry) =>
|
|
560
|
+
Option.some({
|
|
561
|
+
url,
|
|
562
|
+
previous: previous.plugin,
|
|
563
|
+
leases: previous.leases,
|
|
564
|
+
plugin: entry.plugin,
|
|
565
|
+
}),
|
|
566
|
+
),
|
|
567
|
+
Effect.catchIf(
|
|
568
|
+
(error) =>
|
|
569
|
+
error.stage === "import" &&
|
|
570
|
+
isMissingEntry(url, error.cause),
|
|
571
|
+
() => Effect.succeedNone,
|
|
572
|
+
),
|
|
573
|
+
),
|
|
574
|
+
)
|
|
575
|
+
const candidates = Array.getSomes(loaded)
|
|
576
|
+
const alternatives = Array.reduce(
|
|
577
|
+
candidates,
|
|
578
|
+
HashMap.empty<Plugin.Plugin, HashSet.HashSet<Plugin.Plugin>>(),
|
|
579
|
+
(groups, { previous, plugin }) =>
|
|
580
|
+
HashMap.set(
|
|
581
|
+
groups,
|
|
582
|
+
previous,
|
|
583
|
+
HashSet.add(
|
|
584
|
+
Option.getOrElse(
|
|
585
|
+
HashMap.get(groups, previous),
|
|
586
|
+
HashSet.empty<Plugin.Plugin>,
|
|
587
|
+
),
|
|
588
|
+
plugin,
|
|
508
589
|
),
|
|
509
|
-
|
|
510
|
-
|
|
590
|
+
),
|
|
591
|
+
)
|
|
592
|
+
const ambiguous = Array.filter(candidates, ({ previous }) =>
|
|
593
|
+
Option.exists(
|
|
594
|
+
HashMap.get(alternatives, previous),
|
|
595
|
+
(plugins) => HashSet.size(plugins) > 1,
|
|
511
596
|
),
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
597
|
+
)
|
|
598
|
+
if (ambiguous.length > 0)
|
|
599
|
+
return WatchResult.Skipped({
|
|
600
|
+
reason: "ambiguous-exports",
|
|
601
|
+
modules: Array.map(ambiguous, ({ url }) => url),
|
|
602
|
+
})
|
|
603
|
+
const replacements = HashMap.fromIterable(
|
|
604
|
+
Array.map(
|
|
605
|
+
candidates,
|
|
606
|
+
({ url, plugin, leases }) => [url, { plugin, leases }] as const,
|
|
607
|
+
),
|
|
608
|
+
)
|
|
609
|
+
// A missing entry still owns its running plugin until the last lease closes.
|
|
610
|
+
const next = HashMap.union(entries, replacements)
|
|
611
|
+
|
|
612
|
+
const pairs = Array.map(
|
|
613
|
+
candidates,
|
|
614
|
+
({ previous, plugin }) => [previous, plugin] as const,
|
|
615
|
+
)
|
|
616
|
+
const changed = yield* changedResolutions
|
|
617
|
+
if (changed.length > 0)
|
|
618
|
+
return WatchResult.Skipped({
|
|
619
|
+
reason: "restart-required",
|
|
620
|
+
modules: changed,
|
|
621
|
+
})
|
|
622
|
+
// A reused instance can acquire a native loading capability while the
|
|
623
|
+
// candidate imports. Observe it without throwing into concurrent old code.
|
|
624
|
+
const unsafe = unsupportedAttempts(
|
|
625
|
+
Array.map(active, (attempt) =>
|
|
626
|
+
HashMap.has(replacements, attempt.url)
|
|
627
|
+
? { ...attempt, generation: candidate }
|
|
628
|
+
: attempt,
|
|
629
|
+
),
|
|
630
|
+
)
|
|
631
|
+
if (HashSet.size(unsafe) > 0)
|
|
632
|
+
return WatchResult.Skipped({
|
|
633
|
+
reason: "restart-required",
|
|
634
|
+
modules: Array.fromIterable(unsafe),
|
|
635
|
+
})
|
|
636
|
+
|
|
637
|
+
const replacement = yield* Graft.replace(pairs).pipe(
|
|
638
|
+
Effect.provideService(Graft.Graft, graft),
|
|
639
|
+
Effect.result,
|
|
640
|
+
)
|
|
641
|
+
if (Result.isFailure(replacement))
|
|
642
|
+
return WatchResult.Skipped({
|
|
643
|
+
reason: "identity-conflict",
|
|
644
|
+
modules: Array.filterMap(candidates, ({ url }, index) =>
|
|
645
|
+
Array.contains(replacement.failure.indices, index)
|
|
646
|
+
? Result.succeed(url)
|
|
647
|
+
: Result.fail(undefined),
|
|
648
|
+
),
|
|
649
|
+
})
|
|
650
|
+
yield* Ref.update(owners, (owners) =>
|
|
651
|
+
Array.reduce(candidates, owners, (owners, { url, leases }) =>
|
|
652
|
+
HashMap.map(owners, (owner, token) =>
|
|
653
|
+
HashSet.has(leases, token)
|
|
654
|
+
? {
|
|
655
|
+
active: Option.map(owner.pending, (attempt) => ({
|
|
656
|
+
...attempt,
|
|
657
|
+
url,
|
|
658
|
+
generation: candidate,
|
|
659
|
+
})),
|
|
660
|
+
pending: Option.none(),
|
|
661
|
+
}
|
|
662
|
+
: owner,
|
|
663
|
+
),
|
|
664
|
+
),
|
|
665
|
+
)
|
|
666
|
+
yield* Ref.set(state, { entries: next, generation: candidate })
|
|
667
|
+
yield* Ref.update(cached, (cached) =>
|
|
668
|
+
HashMap.union(
|
|
669
|
+
HashMap.filter(cached, (_, url) => HashMap.has(next, url)),
|
|
670
|
+
HashMap.map(replacements, (_, url) => ({
|
|
671
|
+
url,
|
|
672
|
+
generation: candidate,
|
|
673
|
+
})),
|
|
674
|
+
),
|
|
675
|
+
)
|
|
676
|
+
yield* Ref.set(retryDirty, HashSet.empty())
|
|
677
|
+
yield* Ref.set(retryAll, false)
|
|
678
|
+
return WatchResult.Reloaded({
|
|
679
|
+
entries: Array.map(candidates, (candidate) => candidate.url),
|
|
680
|
+
})
|
|
681
|
+
}).pipe(
|
|
682
|
+
Effect.catchIf(
|
|
683
|
+
(
|
|
684
|
+
error,
|
|
685
|
+
): error is HmrError & { readonly cause: RestartRequired } =>
|
|
686
|
+
Schema.is(RestartRequired)(error.cause),
|
|
687
|
+
(error) =>
|
|
688
|
+
Effect.succeed(
|
|
689
|
+
WatchResult.Skipped({
|
|
690
|
+
modules: Array.fromIterable(
|
|
691
|
+
HashSet.add(
|
|
692
|
+
hooks.unsupported(candidate),
|
|
693
|
+
error.cause.url,
|
|
694
|
+
),
|
|
695
|
+
),
|
|
696
|
+
reason: "restart-required",
|
|
697
|
+
}),
|
|
698
|
+
),
|
|
699
|
+
),
|
|
700
|
+
Effect.ensuring(Effect.sync(hooks.discard)),
|
|
701
|
+
Effect.ensuring(collect),
|
|
702
|
+
)
|
|
703
|
+
}),
|
|
704
|
+
)
|
|
517
705
|
return {
|
|
518
706
|
importer,
|
|
519
707
|
importerScoped,
|
|
520
|
-
|
|
708
|
+
importModule,
|
|
709
|
+
reload: reloadChanged(),
|
|
521
710
|
watch: (paths, watchOptions) =>
|
|
522
711
|
paths.length === 0
|
|
523
712
|
? Stream.empty
|
|
@@ -541,19 +730,20 @@ export const make = (
|
|
|
541
730
|
]),
|
|
542
731
|
),
|
|
543
732
|
Effect.flatMap(use),
|
|
544
|
-
//
|
|
545
|
-
//
|
|
733
|
+
// Only snapshot and validation callbacks run under the lock.
|
|
734
|
+
// File reads operate on the retained immutable snapshot outside it.
|
|
546
735
|
semaphore.withPermits(1),
|
|
547
736
|
),
|
|
548
737
|
paths,
|
|
549
738
|
watchOptions,
|
|
550
739
|
).pipe(
|
|
551
|
-
Stream.
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
740
|
+
Stream.mapEffect((dirty) =>
|
|
741
|
+
Effect.map(
|
|
742
|
+
Effect.exit(reloadChanged(dirty)),
|
|
743
|
+
(exit): WatchResult =>
|
|
744
|
+
Exit.isSuccess(exit)
|
|
745
|
+
? exit.value
|
|
746
|
+
: WatchResult.Failed({ cause: exit.cause }),
|
|
557
747
|
),
|
|
558
748
|
),
|
|
559
749
|
// Native caches may hide both dependency edges and the first save.
|