@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/internal/modules.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
// Node's synchronous module hooks have no Effect equivalent.
|
|
2
|
-
import {
|
|
2
|
+
import { digest } from "./digest"
|
|
3
3
|
import { registerHooks } from "node:module"
|
|
4
4
|
import { fileURLToPath, pathToFileURL } from "node:url"
|
|
5
5
|
import {
|
|
6
6
|
Array,
|
|
7
|
+
Chunk,
|
|
7
8
|
HashMap,
|
|
8
9
|
HashSet,
|
|
9
10
|
Option,
|
|
@@ -31,11 +32,12 @@ export const isMissingEntry = (url: string, cause: unknown): boolean =>
|
|
|
31
32
|
// Allocated only when a session starts; importing this module performs no I/O.
|
|
32
33
|
let sessions = 0
|
|
33
34
|
|
|
35
|
+
/** A resolution view that allocates fresh URLs for new or invalidated instances. */
|
|
34
36
|
export interface Generation {
|
|
35
37
|
readonly id: number
|
|
36
38
|
}
|
|
37
39
|
|
|
38
|
-
/**
|
|
40
|
+
/** Digest of the bytes actually loaded by a concrete module instance. */
|
|
39
41
|
export interface FileVersion {
|
|
40
42
|
readonly generation: number
|
|
41
43
|
readonly content: string | undefined
|
|
@@ -49,7 +51,7 @@ export interface Attempt {
|
|
|
49
51
|
}
|
|
50
52
|
|
|
51
53
|
interface Dependency extends Attempt {
|
|
52
|
-
readonly
|
|
54
|
+
readonly node: string
|
|
53
55
|
}
|
|
54
56
|
|
|
55
57
|
/** Resolve local requests lexically; Node's cached physical target is a separate fact. */
|
|
@@ -71,7 +73,6 @@ const fileRequest = (
|
|
|
71
73
|
export const modules = (base: string, include: (url: string) => boolean) => {
|
|
72
74
|
const key = `graft-hmr-${++sessions}`
|
|
73
75
|
const prefix = `${key}:`
|
|
74
|
-
/** What `request` produces: the generation that imports a specifier. */
|
|
75
76
|
const decodeRequest = Schema.decodeUnknownOption(
|
|
76
77
|
Schema.TemplateLiteralParser([
|
|
77
78
|
prefix,
|
|
@@ -83,213 +84,256 @@ export const modules = (base: string, include: (url: string) => boolean) => {
|
|
|
83
84
|
let revision = 0
|
|
84
85
|
let importing:
|
|
85
86
|
{ readonly generation: Generation; readonly reload: boolean } | undefined
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
)
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
87
|
+
let resolved: string | undefined
|
|
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>()
|
|
99
|
+
const clean = (url: string): string => {
|
|
100
|
+
const value = new URL(url)
|
|
101
|
+
value.searchParams.delete(key)
|
|
102
|
+
return value.href
|
|
103
|
+
}
|
|
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,
|
|
118
119
|
),
|
|
120
|
+
() => tag(url, id),
|
|
119
121
|
)
|
|
120
|
-
|
|
121
|
-
const
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
HashMap.
|
|
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>,
|
|
125
128
|
)
|
|
129
|
+
const reachable = (node: string) => {
|
|
130
|
+
const cached = HashMap.get(reachCache, node)
|
|
131
|
+
if (Option.isSome(cached)) return cached.value
|
|
126
132
|
let seen = HashSet.empty<string>()
|
|
127
|
-
let pending =
|
|
128
|
-
while (
|
|
129
|
-
const
|
|
130
|
-
pending =
|
|
131
|
-
if (HashSet.has(seen,
|
|
132
|
-
seen = HashSet.add(seen,
|
|
133
|
-
|
|
134
|
-
pending,
|
|
135
|
-
Array.map(
|
|
136
|
-
HashMap.toValues(
|
|
137
|
-
Option.getOrElse(
|
|
138
|
-
HashMap.get(graph, url),
|
|
139
|
-
HashMap.empty<string, Dependency>,
|
|
140
|
-
),
|
|
141
|
-
),
|
|
142
|
-
(edge) => edge.url,
|
|
143
|
-
),
|
|
144
|
-
)
|
|
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)
|
|
145
141
|
}
|
|
142
|
+
reachCache = HashMap.set(reachCache, node, seen)
|
|
146
143
|
return seen
|
|
147
144
|
}
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
const used =
|
|
152
|
-
|
|
153
|
-
HashMap.get(edges, id),
|
|
154
|
-
HashMap.empty<string, HashMap.HashMap<string, Dependency>>,
|
|
155
|
-
)
|
|
156
|
-
return [
|
|
157
|
-
...(attempt.request === undefined ? [] : [attempt]),
|
|
158
|
-
...Array.flatMap(HashMap.toValues(graph), (children) =>
|
|
159
|
-
Array.filter(
|
|
160
|
-
HashMap.toValues(children),
|
|
161
|
-
(edge) =>
|
|
162
|
-
edge.request !== undefined && HashSet.has(used, edge.parent),
|
|
163
|
-
),
|
|
164
|
-
),
|
|
165
|
-
]
|
|
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
|
|
166
150
|
}
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
const
|
|
176
|
-
|
|
177
|
-
|
|
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()
|
|
178
180
|
}
|
|
179
|
-
const
|
|
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
|
+
]
|
|
181
192
|
const unsupported = (url: string, format: string | null | undefined) =>
|
|
182
193
|
format === "commonjs" ||
|
|
183
194
|
format === "commonjs-typescript" ||
|
|
184
195
|
format === "addon" ||
|
|
185
196
|
String.endsWith(".node")(new URL(url).pathname)
|
|
186
|
-
const requireRestart = (
|
|
187
|
-
|
|
188
|
-
dependencies,
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
Option.getOrElse(HashMap.get(dependencies, id), HashSet.empty<string>),
|
|
192
|
-
url,
|
|
193
|
-
),
|
|
194
|
-
)
|
|
195
|
-
if (
|
|
196
|
-
importing !== undefined &&
|
|
197
|
-
String.String(importing.generation.id) === id &&
|
|
198
|
-
importing.reload
|
|
199
|
-
)
|
|
200
|
-
throw RestartRequired.make({ url })
|
|
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) })
|
|
201
202
|
}
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
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>,
|
|
207
210
|
) => {
|
|
208
|
-
|
|
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
|
+
}
|
|
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
|
|
209
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
|
+
}
|
|
210
275
|
|
|
276
|
+
views = HashMap.set(views, id, view)
|
|
277
|
+
return { id }
|
|
278
|
+
}
|
|
211
279
|
const hooks = registerHooks({
|
|
212
280
|
resolve(specifier, context, nextResolve) {
|
|
281
|
+
const parentNode = context.parentURL
|
|
213
282
|
const request = decodeRequest(specifier)
|
|
214
283
|
const entry = Option.isSome(request)
|
|
215
284
|
const id = Option.match(request, {
|
|
216
285
|
onNone: () => version(context.parentURL),
|
|
217
|
-
onSome: ([, id]) => id,
|
|
286
|
+
onSome: ([, id]) => Number(id),
|
|
218
287
|
})
|
|
219
|
-
if (id ===
|
|
220
|
-
const
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
{
|
|
227
|
-
...context,
|
|
228
|
-
parentURL: entry ? base : clean(context.parentURL!),
|
|
229
|
-
},
|
|
230
|
-
)
|
|
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))
|
|
231
295
|
const result = (() => {
|
|
232
296
|
try {
|
|
233
|
-
return
|
|
297
|
+
return nextResolve(target, { ...context, parentURL: parent })
|
|
234
298
|
} catch (cause) {
|
|
235
|
-
// Node reports the missing file even though resolution did not complete.
|
|
236
299
|
if (
|
|
237
300
|
Predicate.hasProperty(cause, "url") &&
|
|
238
301
|
typeof cause.url === "string" &&
|
|
239
302
|
String.startsWith("file:")(cause.url)
|
|
240
303
|
) {
|
|
241
304
|
if (entry) resolved = cause.url
|
|
242
|
-
else if (
|
|
243
|
-
link(
|
|
244
|
-
id,
|
|
245
|
-
clean(context.parentURL),
|
|
246
|
-
cause.url,
|
|
247
|
-
Option.getOrUndefined(
|
|
248
|
-
fileRequest(specifier, clean(context.parentURL)),
|
|
249
|
-
),
|
|
250
|
-
)
|
|
305
|
+
else if (include(cause.url))
|
|
306
|
+
link(parentNode!, route(cause.url, id), lexical)
|
|
251
307
|
}
|
|
252
308
|
throw cause
|
|
253
309
|
}
|
|
254
310
|
})()
|
|
255
311
|
if (entry) resolved = result.url
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
result.url === "node:module" &&
|
|
261
|
-
context.parentURL !== undefined
|
|
262
|
-
) {
|
|
263
|
-
link(id, clean(context.parentURL), result.url)
|
|
264
|
-
requireRestart(result.url, id)
|
|
312
|
+
if (!entry && result.url === "node:module") {
|
|
313
|
+
const node = route(result.url, id)
|
|
314
|
+
link(parentNode!, node)
|
|
315
|
+
requireRestart(node)
|
|
265
316
|
}
|
|
266
317
|
if (
|
|
267
318
|
!String.startsWith("file:")(result.url) ||
|
|
268
319
|
(!entry && !include(result.url))
|
|
269
320
|
)
|
|
270
321
|
return result
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
id,
|
|
274
|
-
clean(context.parentURL),
|
|
275
|
-
result.url,
|
|
276
|
-
Option.getOrUndefined(
|
|
277
|
-
fileRequest(specifier, clean(context.parentURL)),
|
|
278
|
-
),
|
|
279
|
-
)
|
|
322
|
+
const node = route(result.url, id)
|
|
323
|
+
if (!entry) link(parentNode!, node, lexical)
|
|
280
324
|
const required = Array.contains(context.conditions, "require")
|
|
281
|
-
if (required) requireRestart(
|
|
282
|
-
else record(
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
return { ...result, url: url.href }
|
|
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 }
|
|
287
330
|
},
|
|
288
331
|
load(url, context, nextLoad) {
|
|
289
332
|
const id = version(url)
|
|
290
|
-
if (id ===
|
|
333
|
+
if (id === undefined) return nextLoad(url, context)
|
|
334
|
+
instantiatedViews = HashSet.add(instantiatedViews, id)
|
|
291
335
|
const original = clean(url)
|
|
292
|
-
record(
|
|
336
|
+
record(url, context.format)
|
|
293
337
|
const result = nextLoad(original, context)
|
|
294
338
|
const source = result.source
|
|
295
339
|
if (source !== undefined && source !== null) {
|
|
@@ -303,103 +347,103 @@ export const modules = (base: string, include: (url: string) => boolean) => {
|
|
|
303
347
|
source.byteLength,
|
|
304
348
|
)
|
|
305
349
|
: new Uint8Array(source)
|
|
306
|
-
|
|
307
|
-
sources,
|
|
308
|
-
id,
|
|
309
|
-
HashMap.set(
|
|
310
|
-
Option.getOrElse(
|
|
311
|
-
HashMap.get(sources, id),
|
|
312
|
-
HashMap.empty<string, string>,
|
|
313
|
-
),
|
|
314
|
-
original,
|
|
315
|
-
Base64.encode(bytes),
|
|
316
|
-
),
|
|
317
|
-
)
|
|
350
|
+
if (importing !== undefined || HashSet.has(retained, url))
|
|
351
|
+
sources = HashMap.set(sources, url, digest(bytes))
|
|
318
352
|
}
|
|
319
|
-
record(
|
|
353
|
+
record(url, result.format)
|
|
320
354
|
return result
|
|
321
355
|
},
|
|
322
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
|
+
}
|
|
323
390
|
return {
|
|
324
391
|
prepare,
|
|
392
|
+
files,
|
|
393
|
+
resolutions,
|
|
394
|
+
invalidated,
|
|
395
|
+
affected: (attempt: Attempt, invalid: HashSet.HashSet<string>) =>
|
|
396
|
+
HashSet.has(invalid, root(attempt)),
|
|
325
397
|
fileRequest: (specifier: string) => fileRequest(specifier, base),
|
|
326
|
-
resolutions: (attempts: ReadonlyArray<Attempt>) =>
|
|
327
|
-
Array.flatMap(attempts, resolutionsFor),
|
|
328
|
-
files: (attempts: ReadonlyArray<Attempt>) =>
|
|
329
|
-
Array.reduce(
|
|
330
|
-
attempts,
|
|
331
|
-
HashMap.empty<string, FileVersion>(),
|
|
332
|
-
(files, { generation, url, request }) => {
|
|
333
|
-
const contents = Option.getOrElse(
|
|
334
|
-
HashMap.get(sources, String.String(generation.id)),
|
|
335
|
-
HashMap.empty<string, string>,
|
|
336
|
-
)
|
|
337
|
-
const unversioned = Option.getOrElse(
|
|
338
|
-
HashMap.get(dependencies, String.String(generation.id)),
|
|
339
|
-
HashSet.empty<string>,
|
|
340
|
-
)
|
|
341
|
-
const tracked = Array.reduce(
|
|
342
|
-
Array.fromIterable(reachable(String.String(generation.id), [url])),
|
|
343
|
-
files,
|
|
344
|
-
(files, url) => {
|
|
345
|
-
if (
|
|
346
|
-
!String.startsWith("file:")(url) ||
|
|
347
|
-
HashSet.has(unversioned, url)
|
|
348
|
-
)
|
|
349
|
-
return files
|
|
350
|
-
// A retained failed lease must not override a newer import
|
|
351
|
-
// of the same file just because its owner is visited last.
|
|
352
|
-
if (
|
|
353
|
-
Option.exists(
|
|
354
|
-
HashMap.get(files, url),
|
|
355
|
-
(current) =>
|
|
356
|
-
current.generation > generation.id ||
|
|
357
|
-
(current.generation === generation.id &&
|
|
358
|
-
current.resolution !== undefined),
|
|
359
|
-
)
|
|
360
|
-
)
|
|
361
|
-
return files
|
|
362
|
-
return HashMap.set(files, url, {
|
|
363
|
-
generation: generation.id,
|
|
364
|
-
content: Option.getOrUndefined(HashMap.get(contents, url)),
|
|
365
|
-
})
|
|
366
|
-
},
|
|
367
|
-
)
|
|
368
|
-
return Array.reduce(
|
|
369
|
-
resolutionsFor({
|
|
370
|
-
generation,
|
|
371
|
-
url,
|
|
372
|
-
...(request === undefined ? {} : { request }),
|
|
373
|
-
}),
|
|
374
|
-
tracked,
|
|
375
|
-
(files, edge) => {
|
|
376
|
-
if (
|
|
377
|
-
edge.request === undefined ||
|
|
378
|
-
HashSet.has(unversioned, edge.url) ||
|
|
379
|
-
Option.exists(
|
|
380
|
-
HashMap.get(files, edge.request),
|
|
381
|
-
(file) => file.generation > generation.id,
|
|
382
|
-
)
|
|
383
|
-
)
|
|
384
|
-
return files
|
|
385
|
-
return HashMap.set(files, edge.request, {
|
|
386
|
-
generation: generation.id,
|
|
387
|
-
content: Option.getOrUndefined(HashMap.get(contents, edge.url)),
|
|
388
|
-
resolution: pathToFileURL(fileURLToPath(edge.url)).href,
|
|
389
|
-
})
|
|
390
|
-
},
|
|
391
|
-
)
|
|
392
|
-
},
|
|
393
|
-
),
|
|
394
398
|
unsupported: (generation: Generation, roots?: ReadonlyArray<string>) => {
|
|
395
|
-
const
|
|
396
|
-
|
|
397
|
-
|
|
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
|
+
),
|
|
398
412
|
)
|
|
399
|
-
if (roots === undefined) return found
|
|
400
|
-
const used = reachable(String.String(generation.id), roots)
|
|
401
|
-
return HashSet.filter(found, (url) => HashSet.has(used, url))
|
|
402
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
|
+
}),
|
|
403
447
|
start: (generation: Generation, reload = false) => {
|
|
404
448
|
importing = { generation, reload }
|
|
405
449
|
},
|
|
@@ -411,6 +455,15 @@ export const modules = (base: string, include: (url: string) => boolean) => {
|
|
|
411
455
|
return `${prefix}${importing!.generation.id}:${specifier}`
|
|
412
456
|
},
|
|
413
457
|
resolved: () => Option.fromUndefinedOr(resolved),
|
|
414
|
-
close: () =>
|
|
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()
|
|
467
|
+
},
|
|
415
468
|
}
|
|
416
469
|
}
|