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