@graftland/hmr 0.1.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 +195 -2
- package/README.zh-CN.md +137 -0
- package/README.zh-TW.md +137 -0
- package/dist/index.d.ts +39 -12
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +250 -48
- package/dist/index.js.map +1 -1
- package/dist/internal/changes.js +92 -0
- package/dist/internal/changes.js.map +1 -0
- package/dist/internal/digest.js +6 -0
- package/dist/internal/digest.js.map +1 -0
- package/dist/internal/modules.js +237 -39
- package/dist/internal/modules.js.map +1 -1
- package/package.json +7 -5
- package/src/index.ts +632 -108
- package/src/internal/changes.ts +274 -0
- package/src/internal/digest.ts +5 -0
- package/src/internal/modules.ts +403 -57
package/src/internal/modules.ts
CHANGED
|
@@ -1,7 +1,17 @@
|
|
|
1
1
|
// Node's synchronous module hooks have no Effect equivalent.
|
|
2
|
+
import { digest } from "./digest"
|
|
2
3
|
import { registerHooks } from "node:module"
|
|
3
|
-
import { fileURLToPath } from "node:url"
|
|
4
|
-
import {
|
|
4
|
+
import { fileURLToPath, pathToFileURL } from "node:url"
|
|
5
|
+
import {
|
|
6
|
+
Array,
|
|
7
|
+
Chunk,
|
|
8
|
+
HashMap,
|
|
9
|
+
HashSet,
|
|
10
|
+
Option,
|
|
11
|
+
Predicate,
|
|
12
|
+
Schema,
|
|
13
|
+
String,
|
|
14
|
+
} from "effect"
|
|
5
15
|
|
|
6
16
|
export class RestartRequired extends Schema.TaggedError<
|
|
7
17
|
RestartRequired,
|
|
@@ -10,7 +20,7 @@ export class RestartRequired extends Schema.TaggedError<
|
|
|
10
20
|
|
|
11
21
|
// Only a missing entry is disposable; missing dependencies still fail the reload.
|
|
12
22
|
export const isMissingEntry = (url: string, cause: unknown): boolean =>
|
|
13
|
-
|
|
23
|
+
String.startsWith("file:")(url) &&
|
|
14
24
|
Predicate.hasProperty(cause, "code") &&
|
|
15
25
|
((cause.code === "ERR_MODULE_NOT_FOUND" &&
|
|
16
26
|
Predicate.hasProperty(cause, "url") &&
|
|
@@ -22,102 +32,438 @@ export const isMissingEntry = (url: string, cause: unknown): boolean =>
|
|
|
22
32
|
// Allocated only when a session starts; importing this module performs no I/O.
|
|
23
33
|
let sessions = 0
|
|
24
34
|
|
|
35
|
+
/** A resolution view that allocates fresh URLs for new or invalidated instances. */
|
|
25
36
|
export interface Generation {
|
|
26
|
-
readonly id:
|
|
27
|
-
|
|
37
|
+
readonly id: number
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Digest of the bytes actually loaded by a concrete module instance. */
|
|
41
|
+
export interface FileVersion {
|
|
42
|
+
readonly generation: number
|
|
43
|
+
readonly content: string | undefined
|
|
44
|
+
readonly resolution?: string
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface Attempt {
|
|
48
|
+
readonly generation: Generation
|
|
49
|
+
readonly url: string
|
|
50
|
+
readonly request?: string
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
interface Dependency extends Attempt {
|
|
54
|
+
readonly node: string
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Resolve local requests lexically; Node's cached physical target is a separate fact. */
|
|
58
|
+
const fileRequest = (
|
|
59
|
+
specifier: string,
|
|
60
|
+
parent: string,
|
|
61
|
+
): Option.Option<string> => {
|
|
62
|
+
if (
|
|
63
|
+
!Array.some(["file:", "./", "../", "/"], (prefix) =>
|
|
64
|
+
String.startsWith(prefix)(specifier),
|
|
65
|
+
)
|
|
66
|
+
)
|
|
67
|
+
return Option.none()
|
|
68
|
+
const url = new URL(specifier, parent)
|
|
69
|
+
return url.protocol === "file:" ? Option.some(url.href) : Option.none()
|
|
28
70
|
}
|
|
29
71
|
|
|
30
72
|
/** A scoped Node hook. Mutable bindings bridge synchronous hooks to Effect. */
|
|
31
73
|
export const modules = (base: string, include: (url: string) => boolean) => {
|
|
32
74
|
const key = `graft-hmr-${++sessions}`
|
|
33
75
|
const prefix = `${key}:`
|
|
76
|
+
const decodeRequest = Schema.decodeUnknownOption(
|
|
77
|
+
Schema.TemplateLiteralParser([
|
|
78
|
+
prefix,
|
|
79
|
+
Schema.String.check(Schema.isPattern(/^\d+$/)),
|
|
80
|
+
":",
|
|
81
|
+
Schema.String,
|
|
82
|
+
]),
|
|
83
|
+
)
|
|
34
84
|
let revision = 0
|
|
35
|
-
let
|
|
36
|
-
|
|
85
|
+
let importing:
|
|
86
|
+
{ readonly generation: Generation; readonly reload: boolean } | undefined
|
|
37
87
|
let resolved: string | undefined
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
88
|
+
// Resolution views contain only routing identities. Old code can execute after
|
|
89
|
+
// replacement (including teardown), so these persistent maps live until close.
|
|
90
|
+
// Cached instances also retain their facts: Node does not replay static imports.
|
|
91
|
+
// Leases restrict observation, not the lifetime of old routing targets.
|
|
92
|
+
let views = HashMap.empty<number, HashMap.HashMap<string, string>>()
|
|
93
|
+
let edges = HashMap.empty<string, HashMap.HashMap<string, Dependency>>()
|
|
94
|
+
let sources = HashMap.empty<string, string>()
|
|
95
|
+
let dependencies = HashSet.empty<string>()
|
|
96
|
+
let reachCache = HashMap.empty<string, HashSet.HashSet<string>>()
|
|
97
|
+
let retained = HashSet.empty<string>()
|
|
98
|
+
let instantiatedViews = HashSet.empty<number>()
|
|
43
99
|
const clean = (url: string): string => {
|
|
44
100
|
const value = new URL(url)
|
|
45
101
|
value.searchParams.delete(key)
|
|
46
102
|
return value.href
|
|
47
103
|
}
|
|
48
|
-
const version = (url: string | undefined):
|
|
49
|
-
url === undefined
|
|
104
|
+
const version = (url: string | undefined): number | undefined => {
|
|
105
|
+
if (url === undefined) return undefined
|
|
106
|
+
const value = new URL(url).searchParams.get(key)
|
|
107
|
+
return value === null ? undefined : Number(value)
|
|
108
|
+
}
|
|
109
|
+
const tag = (url: string, id: number) => {
|
|
110
|
+
const value = new URL(url)
|
|
111
|
+
value.searchParams.set(key, String.String(id))
|
|
112
|
+
return value.href
|
|
113
|
+
}
|
|
114
|
+
const route = (url: string, id: number): string =>
|
|
115
|
+
Option.getOrElse(
|
|
116
|
+
HashMap.get(
|
|
117
|
+
Option.getOrElse(HashMap.get(views, id), HashMap.empty<string, string>),
|
|
118
|
+
url,
|
|
119
|
+
),
|
|
120
|
+
() => tag(url, id),
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
const root = (attempt: Attempt) => route(attempt.url, attempt.generation.id)
|
|
124
|
+
const children = (node: string) =>
|
|
125
|
+
Option.getOrElse(
|
|
126
|
+
HashMap.get(edges, node),
|
|
127
|
+
HashMap.empty<string, Dependency>,
|
|
128
|
+
)
|
|
129
|
+
const reachable = (node: string) => {
|
|
130
|
+
const cached = HashMap.get(reachCache, node)
|
|
131
|
+
if (Option.isSome(cached)) return cached.value
|
|
132
|
+
let seen = HashSet.empty<string>()
|
|
133
|
+
let pending: Chunk.Chunk<string> = Chunk.of(node)
|
|
134
|
+
while (Chunk.isNonEmpty(pending)) {
|
|
135
|
+
const current = Chunk.headNonEmpty(pending)
|
|
136
|
+
pending = Chunk.drop(pending, 1)
|
|
137
|
+
if (HashSet.has(seen, current)) continue
|
|
138
|
+
seen = HashSet.add(seen, current)
|
|
139
|
+
for (const edge of HashMap.values(children(current)))
|
|
140
|
+
pending = Chunk.prepend(pending, edge.node)
|
|
141
|
+
}
|
|
142
|
+
reachCache = HashMap.set(reachCache, node, seen)
|
|
143
|
+
return seen
|
|
144
|
+
}
|
|
145
|
+
const usedBy = (attempts: ReadonlyArray<Attempt>) => {
|
|
146
|
+
const roots = HashSet.fromIterable(Array.map(attempts, root))
|
|
147
|
+
let used = HashSet.empty<string>()
|
|
148
|
+
for (const node of roots) used = HashSet.union(used, reachable(node))
|
|
149
|
+
return used
|
|
150
|
+
}
|
|
151
|
+
const link = (parent: string, node: string, request?: string) => {
|
|
152
|
+
if (
|
|
153
|
+
importing === undefined &&
|
|
154
|
+
!HashSet.has(retained, parent) &&
|
|
155
|
+
!HashSet.has(instantiatedViews, version(parent)!)
|
|
156
|
+
)
|
|
157
|
+
return
|
|
158
|
+
retained = HashSet.add(retained, node)
|
|
159
|
+
const url = clean(node)
|
|
160
|
+
const edge = {
|
|
161
|
+
node,
|
|
162
|
+
url,
|
|
163
|
+
generation: { id: version(node)! },
|
|
164
|
+
...(request === undefined ? {} : { request }),
|
|
165
|
+
}
|
|
166
|
+
const previous = HashMap.get(children(parent), request ?? url)
|
|
167
|
+
if (
|
|
168
|
+
Option.exists(
|
|
169
|
+
previous,
|
|
170
|
+
(previous) => previous.node === node && previous.request === request,
|
|
171
|
+
)
|
|
172
|
+
)
|
|
173
|
+
return
|
|
174
|
+
edges = HashMap.set(
|
|
175
|
+
edges,
|
|
176
|
+
parent,
|
|
177
|
+
HashMap.set(children(parent), request ?? url, edge),
|
|
178
|
+
)
|
|
179
|
+
reachCache = HashMap.empty()
|
|
180
|
+
}
|
|
181
|
+
const resolutions = (
|
|
182
|
+
attempts: ReadonlyArray<Attempt>,
|
|
183
|
+
): ReadonlyArray<Attempt> => [
|
|
184
|
+
...Array.filter(attempts, (attempt) => attempt.request !== undefined),
|
|
185
|
+
...Array.flatMap(Array.fromIterable(usedBy(attempts)), (node) =>
|
|
186
|
+
Array.filter(
|
|
187
|
+
HashMap.toValues(children(node)),
|
|
188
|
+
(edge) => edge.request !== undefined,
|
|
189
|
+
),
|
|
190
|
+
),
|
|
191
|
+
]
|
|
50
192
|
const unsupported = (url: string, format: string | null | undefined) =>
|
|
51
193
|
format === "commonjs" ||
|
|
52
194
|
format === "commonjs-typescript" ||
|
|
53
195
|
format === "addon" ||
|
|
54
|
-
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
196
|
+
String.endsWith(".node")(new URL(url).pathname)
|
|
197
|
+
const requireRestart = (node: string) => {
|
|
198
|
+
if (importing !== undefined || HashSet.has(retained, node))
|
|
199
|
+
dependencies = HashSet.add(dependencies, node)
|
|
200
|
+
if (importing?.reload === true && importing.generation.id === version(node))
|
|
201
|
+
throw RestartRequired.make({ url: clean(node) })
|
|
202
|
+
}
|
|
203
|
+
const record = (node: string, format: string | null | undefined) => {
|
|
204
|
+
if (unsupported(clean(node), format)) requireRestart(node)
|
|
205
|
+
}
|
|
206
|
+
// One reverse traversal closes cycles and all importers of every dirty instance.
|
|
207
|
+
const invalidated = (
|
|
208
|
+
attempts: ReadonlyArray<Attempt>,
|
|
209
|
+
dirty: HashSet.HashSet<string>,
|
|
59
210
|
) => {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
211
|
+
const used = usedBy(attempts)
|
|
212
|
+
let parents = HashMap.empty<string, HashSet.HashSet<string>>()
|
|
213
|
+
let pending = Chunk.empty<string>()
|
|
214
|
+
for (const node of used) {
|
|
215
|
+
if (HashSet.has(dirty, clean(node)))
|
|
216
|
+
pending = Chunk.prepend(pending, node)
|
|
217
|
+
for (const edge of HashMap.values(children(node))) {
|
|
218
|
+
parents = HashMap.set(
|
|
219
|
+
parents,
|
|
220
|
+
edge.node,
|
|
221
|
+
HashSet.add(
|
|
222
|
+
Option.getOrElse(
|
|
223
|
+
HashMap.get(parents, edge.node),
|
|
224
|
+
HashSet.empty<string>,
|
|
225
|
+
),
|
|
226
|
+
node,
|
|
227
|
+
),
|
|
228
|
+
)
|
|
229
|
+
if (edge.request !== undefined && HashSet.has(dirty, edge.request))
|
|
230
|
+
pending = Chunk.prepend(pending, edge.node)
|
|
231
|
+
}
|
|
64
232
|
}
|
|
233
|
+
for (const attempt of attempts)
|
|
234
|
+
if (attempt.request !== undefined && HashSet.has(dirty, attempt.request))
|
|
235
|
+
pending = Chunk.prepend(pending, root(attempt))
|
|
236
|
+
let invalid = HashSet.empty<string>()
|
|
237
|
+
while (Chunk.isNonEmpty(pending)) {
|
|
238
|
+
const node = Chunk.headNonEmpty(pending)
|
|
239
|
+
pending = Chunk.drop(pending, 1)
|
|
240
|
+
if (HashSet.has(invalid, node)) continue
|
|
241
|
+
invalid = HashSet.add(invalid, node)
|
|
242
|
+
for (const parent of Option.getOrElse(
|
|
243
|
+
HashMap.get(parents, node),
|
|
244
|
+
HashSet.empty<string>,
|
|
245
|
+
))
|
|
246
|
+
pending = Chunk.prepend(pending, parent)
|
|
247
|
+
}
|
|
248
|
+
return invalid
|
|
65
249
|
}
|
|
250
|
+
const prepare = (
|
|
251
|
+
attempts: ReadonlyArray<Attempt> = [],
|
|
252
|
+
invalid = HashSet.empty<string>(),
|
|
253
|
+
): Generation => {
|
|
254
|
+
const id = ++revision
|
|
255
|
+
const seed = Array.reduce(attempts, 0, (id, attempt) =>
|
|
256
|
+
Math.max(id, attempt.generation.id),
|
|
257
|
+
)
|
|
258
|
+
let view = Option.getOrElse(
|
|
259
|
+
HashMap.get(views, seed),
|
|
260
|
+
HashMap.empty<string, string>,
|
|
261
|
+
)
|
|
262
|
+
const used = usedBy(attempts)
|
|
263
|
+
// Retain HAMT structure between views. Default routes need no stored entry;
|
|
264
|
+
// only modules reused from older views need an explicit override.
|
|
265
|
+
for (const [url, node] of view)
|
|
266
|
+
if (!HashSet.has(used, node) || HashSet.has(invalid, node))
|
|
267
|
+
view = HashMap.remove(view, url)
|
|
268
|
+
for (const node of used) {
|
|
269
|
+
if (HashSet.has(invalid, node)) continue
|
|
270
|
+
const url = clean(node)
|
|
271
|
+
const previous = HashMap.get(view, url)
|
|
272
|
+
if (Option.isNone(previous) || version(previous.value)! < version(node)!)
|
|
273
|
+
view = HashMap.set(view, url, node)
|
|
274
|
+
}
|
|
66
275
|
|
|
276
|
+
views = HashMap.set(views, id, view)
|
|
277
|
+
return { id }
|
|
278
|
+
}
|
|
67
279
|
const hooks = registerHooks({
|
|
68
280
|
resolve(specifier, context, nextResolve) {
|
|
69
|
-
const
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
:
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
const
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
}
|
|
87
|
-
|
|
281
|
+
const parentNode = context.parentURL
|
|
282
|
+
const request = decodeRequest(specifier)
|
|
283
|
+
const entry = Option.isSome(request)
|
|
284
|
+
const id = Option.match(request, {
|
|
285
|
+
onNone: () => version(context.parentURL),
|
|
286
|
+
onSome: ([, id]) => Number(id),
|
|
287
|
+
})
|
|
288
|
+
if (id === undefined) return nextResolve(specifier, context)
|
|
289
|
+
const target = Option.match(request, {
|
|
290
|
+
onNone: () => specifier,
|
|
291
|
+
onSome: ([, , , target]) => target,
|
|
292
|
+
})
|
|
293
|
+
const parent = entry ? base : clean(context.parentURL!)
|
|
294
|
+
const lexical = Option.getOrUndefined(fileRequest(target, parent))
|
|
295
|
+
const result = (() => {
|
|
296
|
+
try {
|
|
297
|
+
return nextResolve(target, { ...context, parentURL: parent })
|
|
298
|
+
} catch (cause) {
|
|
299
|
+
if (
|
|
300
|
+
Predicate.hasProperty(cause, "url") &&
|
|
301
|
+
typeof cause.url === "string" &&
|
|
302
|
+
String.startsWith("file:")(cause.url)
|
|
303
|
+
) {
|
|
304
|
+
if (entry) resolved = cause.url
|
|
305
|
+
else if (include(cause.url))
|
|
306
|
+
link(parentNode!, route(cause.url, id), lexical)
|
|
307
|
+
}
|
|
308
|
+
throw cause
|
|
309
|
+
}
|
|
310
|
+
})()
|
|
88
311
|
if (entry) resolved = result.url
|
|
312
|
+
if (!entry && result.url === "node:module") {
|
|
313
|
+
const node = route(result.url, id)
|
|
314
|
+
link(parentNode!, node)
|
|
315
|
+
requireRestart(node)
|
|
316
|
+
}
|
|
89
317
|
if (
|
|
90
|
-
!
|
|
318
|
+
!String.startsWith("file:")(result.url) ||
|
|
91
319
|
(!entry && !include(result.url))
|
|
92
320
|
)
|
|
93
321
|
return result
|
|
94
|
-
|
|
95
|
-
if (
|
|
96
|
-
const
|
|
97
|
-
|
|
98
|
-
|
|
322
|
+
const node = route(result.url, id)
|
|
323
|
+
if (!entry) link(parentNode!, node, lexical)
|
|
324
|
+
const required = Array.contains(context.conditions, "require")
|
|
325
|
+
if (required) requireRestart(node)
|
|
326
|
+
else record(node, result.format)
|
|
327
|
+
return required || unsupported(result.url, result.format)
|
|
328
|
+
? result
|
|
329
|
+
: { ...result, url: node }
|
|
99
330
|
},
|
|
100
331
|
load(url, context, nextLoad) {
|
|
101
332
|
const id = version(url)
|
|
102
|
-
if (id ===
|
|
333
|
+
if (id === undefined) return nextLoad(url, context)
|
|
334
|
+
instantiatedViews = HashSet.add(instantiatedViews, id)
|
|
103
335
|
const original = clean(url)
|
|
104
|
-
record(
|
|
336
|
+
record(url, context.format)
|
|
105
337
|
const result = nextLoad(original, context)
|
|
106
|
-
|
|
338
|
+
const source = result.source
|
|
339
|
+
if (source !== undefined && source !== null) {
|
|
340
|
+
const bytes =
|
|
341
|
+
typeof source === "string"
|
|
342
|
+
? source
|
|
343
|
+
: ArrayBuffer.isView(source)
|
|
344
|
+
? new Uint8Array(
|
|
345
|
+
source.buffer,
|
|
346
|
+
source.byteOffset,
|
|
347
|
+
source.byteLength,
|
|
348
|
+
)
|
|
349
|
+
: new Uint8Array(source)
|
|
350
|
+
if (importing !== undefined || HashSet.has(retained, url))
|
|
351
|
+
sources = HashMap.set(sources, url, digest(bytes))
|
|
352
|
+
}
|
|
353
|
+
record(url, result.format)
|
|
107
354
|
return result
|
|
108
355
|
},
|
|
109
356
|
})
|
|
357
|
+
const files = (attempts: ReadonlyArray<Attempt>) => {
|
|
358
|
+
let files = HashMap.empty<string, FileVersion>()
|
|
359
|
+
const put = (url: string, node: string, resolution?: string) => {
|
|
360
|
+
if (!String.startsWith("file:")(url) || HashSet.has(dependencies, node))
|
|
361
|
+
return
|
|
362
|
+
const generation = version(node)!
|
|
363
|
+
const previous = HashMap.get(files, url)
|
|
364
|
+
if (
|
|
365
|
+
Option.exists(
|
|
366
|
+
previous,
|
|
367
|
+
(previous) =>
|
|
368
|
+
previous.generation > generation ||
|
|
369
|
+
(previous.generation === generation &&
|
|
370
|
+
previous.resolution !== undefined),
|
|
371
|
+
)
|
|
372
|
+
)
|
|
373
|
+
return
|
|
374
|
+
files = HashMap.set(files, url, {
|
|
375
|
+
generation,
|
|
376
|
+
content: Option.getOrUndefined(HashMap.get(sources, node)),
|
|
377
|
+
...(resolution === undefined ? {} : { resolution }),
|
|
378
|
+
})
|
|
379
|
+
}
|
|
380
|
+
for (const node of usedBy(attempts)) put(clean(node), node)
|
|
381
|
+
for (const attempt of resolutions(attempts))
|
|
382
|
+
if (attempt.request !== undefined)
|
|
383
|
+
put(
|
|
384
|
+
attempt.request,
|
|
385
|
+
root(attempt),
|
|
386
|
+
pathToFileURL(fileURLToPath(attempt.url)).href,
|
|
387
|
+
)
|
|
388
|
+
return files
|
|
389
|
+
}
|
|
110
390
|
return {
|
|
111
391
|
prepare,
|
|
392
|
+
files,
|
|
393
|
+
resolutions,
|
|
394
|
+
invalidated,
|
|
395
|
+
affected: (attempt: Attempt, invalid: HashSet.HashSet<string>) =>
|
|
396
|
+
HashSet.has(invalid, root(attempt)),
|
|
397
|
+
fileRequest: (specifier: string) => fileRequest(specifier, base),
|
|
398
|
+
unsupported: (generation: Generation, roots?: ReadonlyArray<string>) => {
|
|
399
|
+
const used =
|
|
400
|
+
roots === undefined
|
|
401
|
+
? undefined
|
|
402
|
+
: usedBy(Array.map(roots, (url) => ({ url, generation })))
|
|
403
|
+
return HashSet.fromIterable(
|
|
404
|
+
Array.map(
|
|
405
|
+
Array.filter(Array.fromIterable(dependencies), (node) =>
|
|
406
|
+
used === undefined
|
|
407
|
+
? version(node) === generation.id
|
|
408
|
+
: HashSet.has(used, node),
|
|
409
|
+
),
|
|
410
|
+
clean,
|
|
411
|
+
),
|
|
412
|
+
)
|
|
413
|
+
},
|
|
414
|
+
collect: (attempts: ReadonlyArray<Attempt>) => {
|
|
415
|
+
const used = usedBy(attempts)
|
|
416
|
+
const ownedViews = HashSet.fromIterable(
|
|
417
|
+
Array.map(attempts, (attempt) => attempt.generation.id),
|
|
418
|
+
)
|
|
419
|
+
views = HashMap.filter(
|
|
420
|
+
views,
|
|
421
|
+
(_, id) =>
|
|
422
|
+
id === revision ||
|
|
423
|
+
HashSet.has(instantiatedViews, id) ||
|
|
424
|
+
HashSet.has(ownedViews, id),
|
|
425
|
+
)
|
|
426
|
+
// Old code can reach any default route in its instantiated view, even
|
|
427
|
+
// after every lease on that target closed. Preserve its complete graph,
|
|
428
|
+
// source baseline and native capabilities until the session closes.
|
|
429
|
+
const keep = (node: string) =>
|
|
430
|
+
HashSet.has(used, node) ||
|
|
431
|
+
HashSet.has(instantiatedViews, version(node)!)
|
|
432
|
+
retained = used
|
|
433
|
+
sources = HashMap.filter(sources, (_, node) => keep(node))
|
|
434
|
+
edges = HashMap.filter(edges, (_, node) => keep(node))
|
|
435
|
+
dependencies = HashSet.filter(dependencies, keep)
|
|
436
|
+
reachCache = HashMap.filter(reachCache, (_, node) =>
|
|
437
|
+
HashSet.has(used, node),
|
|
438
|
+
)
|
|
439
|
+
},
|
|
440
|
+
// Internal counters support deterministic retention tests and external benchmarks.
|
|
441
|
+
counts: () => ({
|
|
442
|
+
sources: HashMap.size(sources),
|
|
443
|
+
edges: HashMap.size(edges),
|
|
444
|
+
dependencies: HashSet.size(dependencies),
|
|
445
|
+
views: HashMap.size(views),
|
|
446
|
+
}),
|
|
112
447
|
start: (generation: Generation, reload = false) => {
|
|
113
|
-
|
|
114
|
-
reloading = reload
|
|
448
|
+
importing = { generation, reload }
|
|
115
449
|
},
|
|
116
450
|
discard: () => {
|
|
117
|
-
|
|
451
|
+
importing = undefined
|
|
452
|
+
},
|
|
453
|
+
request: (specifier: string) => {
|
|
454
|
+
resolved = undefined
|
|
455
|
+
return `${prefix}${importing!.generation.id}:${specifier}`
|
|
456
|
+
},
|
|
457
|
+
resolved: () => Option.fromUndefinedOr(resolved),
|
|
458
|
+
close: () => {
|
|
459
|
+
hooks.deregister()
|
|
460
|
+
views = HashMap.empty()
|
|
461
|
+
sources = HashMap.empty()
|
|
462
|
+
edges = HashMap.empty()
|
|
463
|
+
dependencies = HashSet.empty()
|
|
464
|
+
reachCache = HashMap.empty()
|
|
465
|
+
retained = HashSet.empty()
|
|
466
|
+
instantiatedViews = HashSet.empty()
|
|
118
467
|
},
|
|
119
|
-
request: (specifier: string) => `${prefix}${recording!.id}:${specifier}`,
|
|
120
|
-
resolved: () => resolved,
|
|
121
|
-
close: () => hooks.deregister(),
|
|
122
468
|
}
|
|
123
469
|
}
|