@graftland/hmr 0.3.0 → 0.3.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/dist/index.d.ts.map +1 -1
- package/dist/index.js +20 -18
- package/dist/index.js.map +1 -1
- package/dist/internal/changes.js +4 -4
- package/dist/internal/changes.js.map +1 -1
- package/package.json +2 -2
- package/src/index.ts +328 -335
- package/src/internal/changes.ts +104 -102
package/src/index.ts
CHANGED
|
@@ -110,10 +110,10 @@ export interface Hmr {
|
|
|
110
110
|
* Installs Node module hooks until the enclosing scope closes. Module evaluation
|
|
111
111
|
* must be side-effect free. Versioned ESM cache entries are not reclaimed.
|
|
112
112
|
*/
|
|
113
|
-
export const make
|
|
113
|
+
export const make: (
|
|
114
114
|
options: Options,
|
|
115
|
-
)
|
|
116
|
-
|
|
115
|
+
) => Effect.Effect<Hmr, never, Graft.Graft | Scope.Scope> = Effect.fnUntraced(
|
|
116
|
+
function* (options: Options) {
|
|
117
117
|
const graft = yield* Graft.Graft
|
|
118
118
|
const semaphore = yield* Semaphore.make(1)
|
|
119
119
|
// A lease owns both the running graph and its latest uncommitted attempt.
|
|
@@ -130,30 +130,30 @@ export const make = (
|
|
|
130
130
|
const session = yield* Effect.scope
|
|
131
131
|
// Transfer the complete Exit before restoring interruption. Effect may replace
|
|
132
132
|
// a failed masked operation with pending interruption at that boundary.
|
|
133
|
-
const preserveExit =
|
|
134
|
-
Effect.
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
133
|
+
const preserveExit = Effect.fnUntraced(function* <A, E, R>(
|
|
134
|
+
effect: Effect.Effect<A, E, R>,
|
|
135
|
+
): Effect.fn.Return<A, E, R> {
|
|
136
|
+
const completed = yield* Deferred.make<Exit.Exit<A, E>>()
|
|
137
|
+
return yield* effect.pipe(
|
|
138
|
+
Effect.exit,
|
|
139
|
+
Effect.tap((exit) => Deferred.succeed(completed, exit)),
|
|
140
|
+
Effect.uninterruptible,
|
|
141
|
+
Effect.flatten,
|
|
142
|
+
Effect.onExit(
|
|
143
|
+
Effect.fnUntraced(function* (exit: Exit.Exit<A, E>) {
|
|
144
|
+
if (Exit.isSuccess(exit) || !Cause.hasInterrupts(exit.cause)) return
|
|
145
|
+
const saved = yield* Deferred.poll(completed)
|
|
146
|
+
if (Option.isNone(saved)) return
|
|
147
|
+
const original = yield* saved.value
|
|
148
|
+
if (Exit.isFailure(original))
|
|
149
|
+
yield* Effect.logError(
|
|
150
|
+
"HMR operation failed during cancellation",
|
|
151
|
+
original.cause,
|
|
152
|
+
)
|
|
153
|
+
}),
|
|
154
|
+
),
|
|
155
|
+
)
|
|
156
|
+
})
|
|
157
157
|
// Native imports cannot be cancelled: keep the lock until their promises settle.
|
|
158
158
|
const serial = <A, E, R>(effect: Effect.Effect<A, E, R>) =>
|
|
159
159
|
effect.pipe(preserveExit, semaphore.withPermits(1))
|
|
@@ -310,7 +310,11 @@ export const make = (
|
|
|
310
310
|
const load = Effect.fnUntraced(function* (specifier: string) {
|
|
311
311
|
const namespace = yield* Effect.tryPromise({
|
|
312
312
|
try: () => import(/* @vite-ignore */ hooks.request(specifier)),
|
|
313
|
-
|
|
313
|
+
// The module hooks throw RestartRequired through import(); keep its tag.
|
|
314
|
+
catch: (cause) =>
|
|
315
|
+
Schema.is(RestartRequired)(cause)
|
|
316
|
+
? cause
|
|
317
|
+
: HmrError.make({ stage: "import", specifier, cause }),
|
|
314
318
|
})
|
|
315
319
|
const plugin = Plugin.fromModule(namespace)
|
|
316
320
|
if (Option.isNone(plugin))
|
|
@@ -334,87 +338,87 @@ export const make = (
|
|
|
334
338
|
}
|
|
335
339
|
const changed = yield* changedResolution(attempt)
|
|
336
340
|
if (Option.isSome(changed))
|
|
337
|
-
return yield*
|
|
338
|
-
stage: "import",
|
|
339
|
-
specifier,
|
|
340
|
-
cause: RestartRequired.make({ url: changed.value }),
|
|
341
|
-
})
|
|
341
|
+
return yield* RestartRequired.make({ url: changed.value })
|
|
342
342
|
return { namespace, plugin: plugin.value, url }
|
|
343
343
|
})
|
|
344
|
+
const acquireLease = Effect.fnUntraced(
|
|
345
|
+
function* (specifier: string) {
|
|
346
|
+
yield* checkOpen
|
|
347
|
+
const current = yield* Ref.get(state)
|
|
348
|
+
hooks.start(current.generation)
|
|
349
|
+
// Acquire an owner for either Exit. Rethrow only after its finalizer
|
|
350
|
+
// is registered, so failed imports remain available for scoped retry.
|
|
351
|
+
const exit = yield* Effect.exit(load(specifier))
|
|
352
|
+
const token: symbol = Symbol()
|
|
353
|
+
const root = Option.orElse(hooks.resolved(), () =>
|
|
354
|
+
String.startsWith("file:")(specifier)
|
|
355
|
+
? Option.some(specifier)
|
|
356
|
+
: Option.none(),
|
|
357
|
+
)
|
|
358
|
+
const attempt = Option.map(root, (url) => ({
|
|
359
|
+
url,
|
|
360
|
+
generation: current.generation,
|
|
361
|
+
...Option.match(hooks.fileRequest(specifier), {
|
|
362
|
+
onNone: () => ({}),
|
|
363
|
+
onSome: (request) => ({ request }),
|
|
364
|
+
}),
|
|
365
|
+
}))
|
|
366
|
+
if (Option.isSome(attempt))
|
|
367
|
+
yield* Ref.update(
|
|
368
|
+
cached,
|
|
369
|
+
HashMap.set(attempt.value.url, attempt.value),
|
|
370
|
+
)
|
|
371
|
+
yield* Ref.update(
|
|
372
|
+
owners,
|
|
373
|
+
HashMap.set(token, {
|
|
374
|
+
active: Exit.isSuccess(exit) ? attempt : Option.none(),
|
|
375
|
+
pending: Exit.isFailure(exit) ? attempt : Option.none(),
|
|
376
|
+
}),
|
|
377
|
+
)
|
|
378
|
+
if (Exit.isSuccess(exit)) {
|
|
379
|
+
const entry = exit.value
|
|
380
|
+
const previous = HashMap.get(current.entries, entry.url)
|
|
381
|
+
const leases = Option.match(previous, {
|
|
382
|
+
onNone: () => HashSet.empty<symbol>(),
|
|
383
|
+
onSome: (entry) => entry.leases,
|
|
384
|
+
})
|
|
385
|
+
yield* Ref.set(state, {
|
|
386
|
+
...current,
|
|
387
|
+
entries: HashMap.set(current.entries, entry.url, {
|
|
388
|
+
plugin: entry.plugin,
|
|
389
|
+
leases: HashSet.add(leases, token),
|
|
390
|
+
}),
|
|
391
|
+
})
|
|
392
|
+
}
|
|
393
|
+
return { token, exit: Exit.map(exit, (entry) => entry.namespace) }
|
|
394
|
+
},
|
|
395
|
+
Effect.ensuring(Effect.sync(hooks.discard)),
|
|
396
|
+
Effect.ensuring(collect),
|
|
397
|
+
serial,
|
|
398
|
+
)
|
|
399
|
+
const releaseLease = Effect.fnUntraced(
|
|
400
|
+
function* ({ token }: { readonly token: symbol }) {
|
|
401
|
+
yield* Ref.update(owners, HashMap.remove(token))
|
|
402
|
+
yield* Ref.update(state, (current) => ({
|
|
403
|
+
...current,
|
|
404
|
+
entries: HashMap.filterMap(current.entries, (entry) => {
|
|
405
|
+
const leases = HashSet.remove(entry.leases, token)
|
|
406
|
+
return HashSet.size(leases) === 0
|
|
407
|
+
? Result.fail(undefined)
|
|
408
|
+
: Result.succeed({ ...entry, leases })
|
|
409
|
+
}),
|
|
410
|
+
}))
|
|
411
|
+
},
|
|
412
|
+
Effect.ensuring(collect),
|
|
413
|
+
serial,
|
|
414
|
+
)
|
|
344
415
|
const importWithLease = (specifier: string) =>
|
|
345
|
-
Effect.acquireRelease(
|
|
346
|
-
serial(
|
|
347
|
-
Effect.gen(function* () {
|
|
348
|
-
yield* checkOpen
|
|
349
|
-
const current = yield* Ref.get(state)
|
|
350
|
-
hooks.start(current.generation)
|
|
351
|
-
// Acquire an owner for either Exit. Rethrow only after its finalizer
|
|
352
|
-
// is registered, so failed imports remain available for scoped retry.
|
|
353
|
-
const exit = yield* Effect.exit(load(specifier))
|
|
354
|
-
const token: symbol = Symbol()
|
|
355
|
-
const root = Option.orElse(hooks.resolved(), () =>
|
|
356
|
-
String.startsWith("file:")(specifier)
|
|
357
|
-
? Option.some(specifier)
|
|
358
|
-
: Option.none(),
|
|
359
|
-
)
|
|
360
|
-
const attempt = Option.map(root, (url) => ({
|
|
361
|
-
url,
|
|
362
|
-
generation: current.generation,
|
|
363
|
-
...Option.match(hooks.fileRequest(specifier), {
|
|
364
|
-
onNone: () => ({}),
|
|
365
|
-
onSome: (request) => ({ request }),
|
|
366
|
-
}),
|
|
367
|
-
}))
|
|
368
|
-
if (Option.isSome(attempt))
|
|
369
|
-
yield* Ref.update(
|
|
370
|
-
cached,
|
|
371
|
-
HashMap.set(attempt.value.url, attempt.value),
|
|
372
|
-
)
|
|
373
|
-
yield* Ref.update(
|
|
374
|
-
owners,
|
|
375
|
-
HashMap.set(token, {
|
|
376
|
-
active: Exit.isSuccess(exit) ? attempt : Option.none(),
|
|
377
|
-
pending: Exit.isFailure(exit) ? attempt : Option.none(),
|
|
378
|
-
}),
|
|
379
|
-
)
|
|
380
|
-
if (Exit.isSuccess(exit)) {
|
|
381
|
-
const entry = exit.value
|
|
382
|
-
const previous = HashMap.get(current.entries, entry.url)
|
|
383
|
-
const leases = Option.match(previous, {
|
|
384
|
-
onNone: () => HashSet.empty<symbol>(),
|
|
385
|
-
onSome: (entry) => entry.leases,
|
|
386
|
-
})
|
|
387
|
-
yield* Ref.set(state, {
|
|
388
|
-
...current,
|
|
389
|
-
entries: HashMap.set(current.entries, entry.url, {
|
|
390
|
-
plugin: entry.plugin,
|
|
391
|
-
leases: HashSet.add(leases, token),
|
|
392
|
-
}),
|
|
393
|
-
})
|
|
394
|
-
}
|
|
395
|
-
return { token, exit: Exit.map(exit, (entry) => entry.namespace) }
|
|
396
|
-
}).pipe(
|
|
397
|
-
Effect.ensuring(Effect.sync(hooks.discard)),
|
|
398
|
-
Effect.ensuring(collect),
|
|
399
|
-
),
|
|
400
|
-
),
|
|
401
|
-
({ token }) =>
|
|
402
|
-
serial(
|
|
403
|
-
Effect.gen(function* () {
|
|
404
|
-
yield* Ref.update(owners, HashMap.remove(token))
|
|
405
|
-
yield* Ref.update(state, (current) => ({
|
|
406
|
-
...current,
|
|
407
|
-
entries: HashMap.filterMap(current.entries, (entry) => {
|
|
408
|
-
const leases = HashSet.remove(entry.leases, token)
|
|
409
|
-
return HashSet.size(leases) === 0
|
|
410
|
-
? Result.fail(undefined)
|
|
411
|
-
: Result.succeed({ ...entry, leases })
|
|
412
|
-
}),
|
|
413
|
-
}))
|
|
414
|
-
}).pipe(Effect.ensuring(collect)),
|
|
415
|
-
),
|
|
416
|
-
).pipe(
|
|
416
|
+
Effect.acquireRelease(acquireLease(specifier), releaseLease).pipe(
|
|
417
417
|
Effect.flatMap(({ exit }) => exit),
|
|
418
|
+
// Importers cannot act on a restart; reloadChanged handles it by tag.
|
|
419
|
+
Effect.catchTag("RestartRequired", (cause) =>
|
|
420
|
+
Effect.fail(HmrError.make({ stage: "import", specifier, cause })),
|
|
421
|
+
),
|
|
418
422
|
preserveExit,
|
|
419
423
|
)
|
|
420
424
|
|
|
@@ -465,248 +469,236 @@ export const make = (
|
|
|
465
469
|
),
|
|
466
470
|
)
|
|
467
471
|
|
|
468
|
-
const reloadChanged = (
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
Array.some(
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
(attempt) => hooks.affected(attempt, invalid),
|
|
517
|
-
),
|
|
518
|
-
),
|
|
519
|
-
)
|
|
520
|
-
? Result.succeed(entry.plugin)
|
|
521
|
-
: Result.fail(undefined),
|
|
472
|
+
const reloadChanged = Effect.fnUntraced(function* (
|
|
473
|
+
dirty: HashSet.HashSet<string> | undefined,
|
|
474
|
+
) {
|
|
475
|
+
yield* checkOpen
|
|
476
|
+
const fullRetry = yield* Ref.get(retryAll)
|
|
477
|
+
const force = dirty === undefined || fullRetry
|
|
478
|
+
const pendingDirty = yield* Ref.get(retryDirty)
|
|
479
|
+
const changedFiles =
|
|
480
|
+
dirty === undefined ? pendingDirty : HashSet.union(pendingDirty, dirty)
|
|
481
|
+
yield* Ref.set(retryDirty, changedFiles)
|
|
482
|
+
yield* Ref.set(retryAll, force)
|
|
483
|
+
const changed = yield* changedResolutions
|
|
484
|
+
if (changed.length > 0)
|
|
485
|
+
return WatchResult.Skipped({
|
|
486
|
+
reason: "restart-required",
|
|
487
|
+
modules: changed,
|
|
488
|
+
})
|
|
489
|
+
const { entries } = yield* Ref.get(state)
|
|
490
|
+
const unsupported = yield* unsupportedModules
|
|
491
|
+
if (HashSet.size(unsupported) > 0)
|
|
492
|
+
return WatchResult.Skipped({
|
|
493
|
+
modules: Array.fromIterable(unsupported),
|
|
494
|
+
reason: "restart-required",
|
|
495
|
+
})
|
|
496
|
+
const owned = yield* Ref.get(owners)
|
|
497
|
+
const attempts = Array.flatMap(HashMap.toValues(owned), (owner) =>
|
|
498
|
+
Array.getSomes([owner.active, owner.pending]),
|
|
499
|
+
)
|
|
500
|
+
const active = Array.getSomes(
|
|
501
|
+
Array.map(HashMap.toValues(owned), (owner) => owner.active),
|
|
502
|
+
)
|
|
503
|
+
let selected = entries
|
|
504
|
+
let invalid = HashSet.empty<string>()
|
|
505
|
+
if (!force) {
|
|
506
|
+
let affected = changedFiles
|
|
507
|
+
// Plugin identity groups and importer closure can expand each other.
|
|
508
|
+
// Reach a fixed point before allocating any candidate instance.
|
|
509
|
+
while (true) {
|
|
510
|
+
invalid = hooks.invalidated(attempts, affected)
|
|
511
|
+
const plugins = HashSet.fromIterable(
|
|
512
|
+
Array.filterMap(Array.fromIterable(entries), ([url, entry]) =>
|
|
513
|
+
HashSet.has(affected, url) ||
|
|
514
|
+
Array.some(Array.fromIterable(entry.leases), (token) =>
|
|
515
|
+
Option.exists(HashMap.get(owned, token), (owner) =>
|
|
516
|
+
Array.some(
|
|
517
|
+
Array.getSomes([owner.active, owner.pending]),
|
|
518
|
+
(attempt) => hooks.affected(attempt, invalid),
|
|
519
|
+
),
|
|
522
520
|
),
|
|
523
521
|
)
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
522
|
+
? Result.succeed(entry.plugin)
|
|
523
|
+
: Result.fail(undefined),
|
|
524
|
+
),
|
|
525
|
+
)
|
|
526
|
+
selected = HashMap.filter(entries, (entry) =>
|
|
527
|
+
HashSet.has(plugins, entry.plugin),
|
|
528
|
+
)
|
|
529
|
+
const expanded = HashSet.union(
|
|
530
|
+
affected,
|
|
531
|
+
HashSet.fromIterable(HashMap.keys(selected)),
|
|
532
|
+
)
|
|
533
|
+
if (HashSet.size(expanded) === HashSet.size(affected)) break
|
|
534
|
+
affected = expanded
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
// Advance even without entries so retries bypass cached failed imports.
|
|
538
|
+
const candidate = force ? hooks.prepare() : hooks.prepare(active, invalid)
|
|
539
539
|
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
plugin,
|
|
589
|
-
),
|
|
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._tag === "HmrError" &&
|
|
570
|
+
error.stage === "import" &&
|
|
571
|
+
isMissingEntry(url, error.cause),
|
|
572
|
+
() => Effect.succeedNone,
|
|
573
|
+
),
|
|
574
|
+
),
|
|
575
|
+
)
|
|
576
|
+
const candidates = Array.getSomes(loaded)
|
|
577
|
+
const alternatives = Array.reduce(
|
|
578
|
+
candidates,
|
|
579
|
+
HashMap.empty<Plugin.Plugin, HashSet.HashSet<Plugin.Plugin>>(),
|
|
580
|
+
(groups, { previous, plugin }) =>
|
|
581
|
+
HashMap.set(
|
|
582
|
+
groups,
|
|
583
|
+
previous,
|
|
584
|
+
HashSet.add(
|
|
585
|
+
Option.getOrElse(
|
|
586
|
+
HashMap.get(groups, previous),
|
|
587
|
+
HashSet.empty<Plugin.Plugin>,
|
|
590
588
|
),
|
|
591
|
-
|
|
592
|
-
const ambiguous = Array.filter(candidates, ({ previous }) =>
|
|
593
|
-
Option.exists(
|
|
594
|
-
HashMap.get(alternatives, previous),
|
|
595
|
-
(plugins) => HashSet.size(plugins) > 1,
|
|
589
|
+
plugin,
|
|
596
590
|
),
|
|
597
|
-
)
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
)
|
|
609
|
-
|
|
610
|
-
|
|
591
|
+
),
|
|
592
|
+
)
|
|
593
|
+
const ambiguous = Array.filter(candidates, ({ previous }) =>
|
|
594
|
+
Option.exists(
|
|
595
|
+
HashMap.get(alternatives, previous),
|
|
596
|
+
(plugins) => HashSet.size(plugins) > 1,
|
|
597
|
+
),
|
|
598
|
+
)
|
|
599
|
+
if (ambiguous.length > 0)
|
|
600
|
+
return WatchResult.Skipped({
|
|
601
|
+
reason: "ambiguous-exports",
|
|
602
|
+
modules: Array.map(ambiguous, ({ url }) => url),
|
|
603
|
+
})
|
|
604
|
+
const replacements = HashMap.fromIterable(
|
|
605
|
+
Array.map(
|
|
606
|
+
candidates,
|
|
607
|
+
({ url, plugin, leases }) => [url, { plugin, leases }] as const,
|
|
608
|
+
),
|
|
609
|
+
)
|
|
610
|
+
// A missing entry still owns its running plugin until the last lease closes.
|
|
611
|
+
const next = HashMap.union(entries, replacements)
|
|
611
612
|
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
613
|
+
const pairs = Array.map(
|
|
614
|
+
candidates,
|
|
615
|
+
({ previous, plugin }) => [previous, plugin] as const,
|
|
616
|
+
)
|
|
617
|
+
const changed = yield* changedResolutions
|
|
618
|
+
if (changed.length > 0)
|
|
619
|
+
return WatchResult.Skipped({
|
|
620
|
+
reason: "restart-required",
|
|
621
|
+
modules: changed,
|
|
622
|
+
})
|
|
623
|
+
// A reused instance can acquire a native loading capability while the
|
|
624
|
+
// candidate imports. Observe it without throwing into concurrent old code.
|
|
625
|
+
const unsafe = unsupportedAttempts(
|
|
626
|
+
Array.map(active, (attempt) =>
|
|
627
|
+
HashMap.has(replacements, attempt.url)
|
|
628
|
+
? { ...attempt, generation: candidate }
|
|
629
|
+
: attempt,
|
|
630
|
+
),
|
|
631
|
+
)
|
|
632
|
+
if (HashSet.size(unsafe) > 0)
|
|
633
|
+
return WatchResult.Skipped({
|
|
634
|
+
reason: "restart-required",
|
|
635
|
+
modules: Array.fromIterable(unsafe),
|
|
636
|
+
})
|
|
636
637
|
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
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
|
-
),
|
|
638
|
+
const replacement = yield* Graft.replace(pairs).pipe(
|
|
639
|
+
Effect.provideService(Graft.Graft, graft),
|
|
640
|
+
Effect.result,
|
|
641
|
+
)
|
|
642
|
+
if (Result.isFailure(replacement))
|
|
643
|
+
return WatchResult.Skipped({
|
|
644
|
+
reason: "identity-conflict",
|
|
645
|
+
modules: Array.filterMap(candidates, ({ url }, index) =>
|
|
646
|
+
Array.contains(replacement.failure.indices, index)
|
|
647
|
+
? Result.succeed(url)
|
|
648
|
+
: Result.fail(undefined),
|
|
699
649
|
),
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
)
|
|
703
|
-
|
|
650
|
+
})
|
|
651
|
+
yield* Ref.update(owners, (owners) =>
|
|
652
|
+
Array.reduce(candidates, owners, (owners, { url, leases }) =>
|
|
653
|
+
HashMap.map(owners, (owner, token) =>
|
|
654
|
+
HashSet.has(leases, token)
|
|
655
|
+
? {
|
|
656
|
+
active: Option.map(owner.pending, (attempt) => ({
|
|
657
|
+
...attempt,
|
|
658
|
+
url,
|
|
659
|
+
generation: candidate,
|
|
660
|
+
})),
|
|
661
|
+
pending: Option.none(),
|
|
662
|
+
}
|
|
663
|
+
: owner,
|
|
664
|
+
),
|
|
665
|
+
),
|
|
666
|
+
)
|
|
667
|
+
yield* Ref.set(state, { entries: next, generation: candidate })
|
|
668
|
+
yield* Ref.update(cached, (cached) =>
|
|
669
|
+
HashMap.union(
|
|
670
|
+
HashMap.filter(cached, (_, url) => HashMap.has(next, url)),
|
|
671
|
+
HashMap.map(replacements, (_, url) => ({
|
|
672
|
+
url,
|
|
673
|
+
generation: candidate,
|
|
674
|
+
})),
|
|
675
|
+
),
|
|
676
|
+
)
|
|
677
|
+
yield* Ref.set(retryDirty, HashSet.empty())
|
|
678
|
+
yield* Ref.set(retryAll, false)
|
|
679
|
+
return WatchResult.Reloaded({
|
|
680
|
+
entries: Array.map(candidates, (candidate) => candidate.url),
|
|
681
|
+
})
|
|
682
|
+
}).pipe(
|
|
683
|
+
Effect.catchTag("RestartRequired", (error) =>
|
|
684
|
+
Effect.succeed(
|
|
685
|
+
WatchResult.Skipped({
|
|
686
|
+
modules: Array.fromIterable(
|
|
687
|
+
HashSet.add(hooks.unsupported(candidate), error.url),
|
|
688
|
+
),
|
|
689
|
+
reason: "restart-required",
|
|
690
|
+
}),
|
|
691
|
+
),
|
|
692
|
+
),
|
|
693
|
+
Effect.ensuring(Effect.sync(hooks.discard)),
|
|
694
|
+
Effect.ensuring(collect),
|
|
704
695
|
)
|
|
696
|
+
}, serial)
|
|
705
697
|
return {
|
|
706
698
|
importer,
|
|
707
699
|
importerScoped,
|
|
708
700
|
importModule,
|
|
709
|
-
reload: reloadChanged(),
|
|
701
|
+
reload: reloadChanged(undefined),
|
|
710
702
|
watch: (paths, watchOptions) =>
|
|
711
703
|
paths.length === 0
|
|
712
704
|
? Stream.empty
|
|
@@ -765,4 +757,5 @@ export const make = (
|
|
|
765
757
|
),
|
|
766
758
|
),
|
|
767
759
|
}
|
|
768
|
-
}
|
|
760
|
+
},
|
|
761
|
+
)
|