@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/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,6 +73,7 @@ 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}:`
|
|
76
|
+
const rawPrefix = `${key}:raw:`
|
|
74
77
|
/** What `request` produces: the generation that imports a specifier. */
|
|
75
78
|
const decodeRequest = Schema.decodeUnknownOption(
|
|
76
79
|
Schema.TemplateLiteralParser([
|
|
@@ -83,213 +86,261 @@ export const modules = (base: string, include: (url: string) => boolean) => {
|
|
|
83
86
|
let revision = 0
|
|
84
87
|
let importing:
|
|
85
88
|
{ 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
|
-
|
|
89
|
+
let resolved: string | undefined
|
|
90
|
+
// Resolution views contain only routing identities. Old code can execute after
|
|
91
|
+
// replacement (including teardown), so these persistent maps live until close.
|
|
92
|
+
// Cached instances also retain their facts: Node does not replay static imports.
|
|
93
|
+
// Leases restrict observation, not the lifetime of old routing targets.
|
|
94
|
+
let views = HashMap.empty<number, HashMap.HashMap<string, string>>()
|
|
95
|
+
let edges = HashMap.empty<string, HashMap.HashMap<string, Dependency>>()
|
|
96
|
+
let sources = HashMap.empty<string, string>()
|
|
97
|
+
let dependencies = HashSet.empty<string>()
|
|
98
|
+
let reachCache = HashMap.empty<string, HashSet.HashSet<string>>()
|
|
99
|
+
let retained = HashSet.empty<string>()
|
|
100
|
+
let instantiatedViews = HashSet.empty<number>()
|
|
101
|
+
const clean = (url: string): string => {
|
|
102
|
+
const value = new URL(url)
|
|
103
|
+
value.searchParams.delete(key)
|
|
104
|
+
return value.href
|
|
105
|
+
}
|
|
106
|
+
const version = (url: string | undefined): number | undefined => {
|
|
107
|
+
if (url === undefined) return undefined
|
|
108
|
+
const value = new URL(url).searchParams.get(key)
|
|
109
|
+
return value === null ? undefined : Number(value)
|
|
110
|
+
}
|
|
111
|
+
const tag = (url: string, id: number) => {
|
|
112
|
+
const value = new URL(url)
|
|
113
|
+
value.searchParams.set(key, String.String(id))
|
|
114
|
+
return value.href
|
|
115
|
+
}
|
|
116
|
+
const route = (url: string, id: number): string =>
|
|
117
|
+
Option.getOrElse(
|
|
118
|
+
HashMap.get(
|
|
119
|
+
Option.getOrElse(HashMap.get(views, id), HashMap.empty<string, string>),
|
|
120
|
+
url,
|
|
118
121
|
),
|
|
122
|
+
() => tag(url, id),
|
|
119
123
|
)
|
|
120
|
-
|
|
121
|
-
const
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
HashMap.
|
|
124
|
+
|
|
125
|
+
const root = (attempt: Attempt) => route(attempt.url, attempt.generation.id)
|
|
126
|
+
const children = (node: string) =>
|
|
127
|
+
Option.getOrElse(
|
|
128
|
+
HashMap.get(edges, node),
|
|
129
|
+
HashMap.empty<string, Dependency>,
|
|
125
130
|
)
|
|
131
|
+
const reachable = (node: string) => {
|
|
132
|
+
const cached = HashMap.get(reachCache, node)
|
|
133
|
+
if (Option.isSome(cached)) return cached.value
|
|
126
134
|
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
|
-
)
|
|
135
|
+
let pending: Chunk.Chunk<string> = Chunk.of(node)
|
|
136
|
+
while (Chunk.isNonEmpty(pending)) {
|
|
137
|
+
const current = Chunk.headNonEmpty(pending)
|
|
138
|
+
pending = Chunk.drop(pending, 1)
|
|
139
|
+
if (HashSet.has(seen, current)) continue
|
|
140
|
+
seen = HashSet.add(seen, current)
|
|
141
|
+
for (const edge of HashMap.values(children(current)))
|
|
142
|
+
pending = Chunk.prepend(pending, edge.node)
|
|
145
143
|
}
|
|
144
|
+
reachCache = HashMap.set(reachCache, node, seen)
|
|
146
145
|
return seen
|
|
147
146
|
}
|
|
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
|
-
]
|
|
147
|
+
const usedBy = (attempts: ReadonlyArray<Attempt>) => {
|
|
148
|
+
const roots = HashSet.fromIterable(Array.map(attempts, root))
|
|
149
|
+
let used = HashSet.empty<string>()
|
|
150
|
+
for (const node of roots) used = HashSet.union(used, reachable(node))
|
|
151
|
+
return used
|
|
166
152
|
}
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
const
|
|
176
|
-
|
|
177
|
-
|
|
153
|
+
const link = (parent: string, node: string, request?: string) => {
|
|
154
|
+
if (
|
|
155
|
+
importing === undefined &&
|
|
156
|
+
!HashSet.has(retained, parent) &&
|
|
157
|
+
!HashSet.has(instantiatedViews, version(parent)!)
|
|
158
|
+
)
|
|
159
|
+
return
|
|
160
|
+
retained = HashSet.add(retained, node)
|
|
161
|
+
const url = clean(node)
|
|
162
|
+
const edge = {
|
|
163
|
+
node,
|
|
164
|
+
url,
|
|
165
|
+
generation: { id: version(node)! },
|
|
166
|
+
...(request === undefined ? {} : { request }),
|
|
167
|
+
}
|
|
168
|
+
const previous = HashMap.get(children(parent), request ?? url)
|
|
169
|
+
if (
|
|
170
|
+
Option.exists(
|
|
171
|
+
previous,
|
|
172
|
+
(previous) => previous.node === node && previous.request === request,
|
|
173
|
+
)
|
|
174
|
+
)
|
|
175
|
+
return
|
|
176
|
+
edges = HashMap.set(
|
|
177
|
+
edges,
|
|
178
|
+
parent,
|
|
179
|
+
HashMap.set(children(parent), request ?? url, edge),
|
|
180
|
+
)
|
|
181
|
+
reachCache = HashMap.empty()
|
|
178
182
|
}
|
|
179
|
-
const
|
|
180
|
-
|
|
183
|
+
const resolutions = (
|
|
184
|
+
attempts: ReadonlyArray<Attempt>,
|
|
185
|
+
): ReadonlyArray<Attempt> => [
|
|
186
|
+
...Array.filter(attempts, (attempt) => attempt.request !== undefined),
|
|
187
|
+
...Array.flatMap(Array.fromIterable(usedBy(attempts)), (node) =>
|
|
188
|
+
Array.filter(
|
|
189
|
+
HashMap.toValues(children(node)),
|
|
190
|
+
(edge) => edge.request !== undefined,
|
|
191
|
+
),
|
|
192
|
+
),
|
|
193
|
+
]
|
|
181
194
|
const unsupported = (url: string, format: string | null | undefined) =>
|
|
182
195
|
format === "commonjs" ||
|
|
183
196
|
format === "commonjs-typescript" ||
|
|
184
197
|
format === "addon" ||
|
|
185
198
|
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 })
|
|
199
|
+
const requireRestart = (node: string) => {
|
|
200
|
+
if (importing !== undefined || HashSet.has(retained, node))
|
|
201
|
+
dependencies = HashSet.add(dependencies, node)
|
|
202
|
+
if (importing?.reload === true && importing.generation.id === version(node))
|
|
203
|
+
throw RestartRequired.make({ url: clean(node) })
|
|
201
204
|
}
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
205
|
+
const record = (node: string, format: string | null | undefined) => {
|
|
206
|
+
if (unsupported(clean(node), format)) requireRestart(node)
|
|
207
|
+
}
|
|
208
|
+
// One reverse traversal closes cycles and all importers of every dirty instance.
|
|
209
|
+
const invalidated = (
|
|
210
|
+
attempts: ReadonlyArray<Attempt>,
|
|
211
|
+
dirty: HashSet.HashSet<string>,
|
|
207
212
|
) => {
|
|
208
|
-
|
|
213
|
+
const used = usedBy(attempts)
|
|
214
|
+
let parents = HashMap.empty<string, HashSet.HashSet<string>>()
|
|
215
|
+
let pending = Chunk.empty<string>()
|
|
216
|
+
for (const node of used) {
|
|
217
|
+
if (HashSet.has(dirty, clean(node)))
|
|
218
|
+
pending = Chunk.prepend(pending, node)
|
|
219
|
+
for (const edge of HashMap.values(children(node))) {
|
|
220
|
+
parents = HashMap.set(
|
|
221
|
+
parents,
|
|
222
|
+
edge.node,
|
|
223
|
+
HashSet.add(
|
|
224
|
+
Option.getOrElse(
|
|
225
|
+
HashMap.get(parents, edge.node),
|
|
226
|
+
HashSet.empty<string>,
|
|
227
|
+
),
|
|
228
|
+
node,
|
|
229
|
+
),
|
|
230
|
+
)
|
|
231
|
+
if (edge.request !== undefined && HashSet.has(dirty, edge.request))
|
|
232
|
+
pending = Chunk.prepend(pending, edge.node)
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
for (const attempt of attempts)
|
|
236
|
+
if (attempt.request !== undefined && HashSet.has(dirty, attempt.request))
|
|
237
|
+
pending = Chunk.prepend(pending, root(attempt))
|
|
238
|
+
let invalid = HashSet.empty<string>()
|
|
239
|
+
while (Chunk.isNonEmpty(pending)) {
|
|
240
|
+
const node = Chunk.headNonEmpty(pending)
|
|
241
|
+
pending = Chunk.drop(pending, 1)
|
|
242
|
+
if (HashSet.has(invalid, node)) continue
|
|
243
|
+
invalid = HashSet.add(invalid, node)
|
|
244
|
+
for (const parent of Option.getOrElse(
|
|
245
|
+
HashMap.get(parents, node),
|
|
246
|
+
HashSet.empty<string>,
|
|
247
|
+
))
|
|
248
|
+
pending = Chunk.prepend(pending, parent)
|
|
249
|
+
}
|
|
250
|
+
return invalid
|
|
209
251
|
}
|
|
252
|
+
const prepare = (
|
|
253
|
+
attempts: ReadonlyArray<Attempt> = [],
|
|
254
|
+
invalid = HashSet.empty<string>(),
|
|
255
|
+
): Generation => {
|
|
256
|
+
const id = ++revision
|
|
257
|
+
const seed = Array.reduce(attempts, 0, (id, attempt) =>
|
|
258
|
+
Math.max(id, attempt.generation.id),
|
|
259
|
+
)
|
|
260
|
+
let view = Option.getOrElse(
|
|
261
|
+
HashMap.get(views, seed),
|
|
262
|
+
HashMap.empty<string, string>,
|
|
263
|
+
)
|
|
264
|
+
const used = usedBy(attempts)
|
|
265
|
+
// Retain HAMT structure between views. Default routes need no stored entry;
|
|
266
|
+
// only modules reused from older views need an explicit override.
|
|
267
|
+
for (const [url, node] of view)
|
|
268
|
+
if (!HashSet.has(used, node) || HashSet.has(invalid, node))
|
|
269
|
+
view = HashMap.remove(view, url)
|
|
270
|
+
for (const node of used) {
|
|
271
|
+
if (HashSet.has(invalid, node)) continue
|
|
272
|
+
const url = clean(node)
|
|
273
|
+
const previous = HashMap.get(view, url)
|
|
274
|
+
if (Option.isNone(previous) || version(previous.value)! < version(node)!)
|
|
275
|
+
view = HashMap.set(view, url, node)
|
|
276
|
+
}
|
|
210
277
|
|
|
278
|
+
views = HashMap.set(views, id, view)
|
|
279
|
+
return { id }
|
|
280
|
+
}
|
|
211
281
|
const hooks = registerHooks({
|
|
212
282
|
resolve(specifier, context, nextResolve) {
|
|
283
|
+
if (String.startsWith(rawPrefix)(specifier))
|
|
284
|
+
return nextResolve(String.slice(rawPrefix.length)(specifier), {
|
|
285
|
+
...context,
|
|
286
|
+
parentURL: base,
|
|
287
|
+
})
|
|
288
|
+
const parentNode = context.parentURL
|
|
213
289
|
const request = decodeRequest(specifier)
|
|
214
290
|
const entry = Option.isSome(request)
|
|
215
291
|
const id = Option.match(request, {
|
|
216
292
|
onNone: () => version(context.parentURL),
|
|
217
|
-
onSome: ([, id]) => id,
|
|
293
|
+
onSome: ([, id]) => Number(id),
|
|
218
294
|
})
|
|
219
|
-
if (id ===
|
|
220
|
-
const
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
{
|
|
227
|
-
...context,
|
|
228
|
-
parentURL: entry ? base : clean(context.parentURL!),
|
|
229
|
-
},
|
|
230
|
-
)
|
|
295
|
+
if (id === undefined) return nextResolve(specifier, context)
|
|
296
|
+
const target = Option.match(request, {
|
|
297
|
+
onNone: () => specifier,
|
|
298
|
+
onSome: ([, , , target]) => target,
|
|
299
|
+
})
|
|
300
|
+
const parent = entry ? base : clean(context.parentURL!)
|
|
301
|
+
const lexical = Option.getOrUndefined(fileRequest(target, parent))
|
|
231
302
|
const result = (() => {
|
|
232
303
|
try {
|
|
233
|
-
return
|
|
304
|
+
return nextResolve(target, { ...context, parentURL: parent })
|
|
234
305
|
} catch (cause) {
|
|
235
|
-
// Node reports the missing file even though resolution did not complete.
|
|
236
306
|
if (
|
|
237
307
|
Predicate.hasProperty(cause, "url") &&
|
|
238
308
|
typeof cause.url === "string" &&
|
|
239
309
|
String.startsWith("file:")(cause.url)
|
|
240
310
|
) {
|
|
241
311
|
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
|
-
)
|
|
312
|
+
else if (include(cause.url))
|
|
313
|
+
link(parentNode!, route(cause.url, id), lexical)
|
|
251
314
|
}
|
|
252
315
|
throw cause
|
|
253
316
|
}
|
|
254
317
|
})()
|
|
255
318
|
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)
|
|
319
|
+
if (!entry && result.url === "node:module") {
|
|
320
|
+
const node = route(result.url, id)
|
|
321
|
+
link(parentNode!, node)
|
|
322
|
+
requireRestart(node)
|
|
265
323
|
}
|
|
266
324
|
if (
|
|
267
325
|
!String.startsWith("file:")(result.url) ||
|
|
268
326
|
(!entry && !include(result.url))
|
|
269
327
|
)
|
|
270
328
|
return result
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
id,
|
|
274
|
-
clean(context.parentURL),
|
|
275
|
-
result.url,
|
|
276
|
-
Option.getOrUndefined(
|
|
277
|
-
fileRequest(specifier, clean(context.parentURL)),
|
|
278
|
-
),
|
|
279
|
-
)
|
|
329
|
+
const node = route(result.url, id)
|
|
330
|
+
if (!entry) link(parentNode!, node, lexical)
|
|
280
331
|
const required = Array.contains(context.conditions, "require")
|
|
281
|
-
if (required) requireRestart(
|
|
282
|
-
else record(
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
return { ...result, url: url.href }
|
|
332
|
+
if (required) requireRestart(node)
|
|
333
|
+
else record(node, result.format)
|
|
334
|
+
return required || unsupported(result.url, result.format)
|
|
335
|
+
? result
|
|
336
|
+
: { ...result, url: node }
|
|
287
337
|
},
|
|
288
338
|
load(url, context, nextLoad) {
|
|
289
339
|
const id = version(url)
|
|
290
|
-
if (id ===
|
|
340
|
+
if (id === undefined) return nextLoad(url, context)
|
|
341
|
+
instantiatedViews = HashSet.add(instantiatedViews, id)
|
|
291
342
|
const original = clean(url)
|
|
292
|
-
record(
|
|
343
|
+
record(url, context.format)
|
|
293
344
|
const result = nextLoad(original, context)
|
|
294
345
|
const source = result.source
|
|
295
346
|
if (source !== undefined && source !== null) {
|
|
@@ -303,103 +354,103 @@ export const modules = (base: string, include: (url: string) => boolean) => {
|
|
|
303
354
|
source.byteLength,
|
|
304
355
|
)
|
|
305
356
|
: 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
|
-
)
|
|
357
|
+
if (importing !== undefined || HashSet.has(retained, url))
|
|
358
|
+
sources = HashMap.set(sources, url, digest(bytes))
|
|
318
359
|
}
|
|
319
|
-
record(
|
|
360
|
+
record(url, result.format)
|
|
320
361
|
return result
|
|
321
362
|
},
|
|
322
363
|
})
|
|
364
|
+
const files = (attempts: ReadonlyArray<Attempt>) => {
|
|
365
|
+
let files = HashMap.empty<string, FileVersion>()
|
|
366
|
+
const put = (url: string, node: string, resolution?: string) => {
|
|
367
|
+
if (!String.startsWith("file:")(url) || HashSet.has(dependencies, node))
|
|
368
|
+
return
|
|
369
|
+
const generation = version(node)!
|
|
370
|
+
const previous = HashMap.get(files, url)
|
|
371
|
+
if (
|
|
372
|
+
Option.exists(
|
|
373
|
+
previous,
|
|
374
|
+
(previous) =>
|
|
375
|
+
previous.generation > generation ||
|
|
376
|
+
(previous.generation === generation &&
|
|
377
|
+
previous.resolution !== undefined),
|
|
378
|
+
)
|
|
379
|
+
)
|
|
380
|
+
return
|
|
381
|
+
files = HashMap.set(files, url, {
|
|
382
|
+
generation,
|
|
383
|
+
content: Option.getOrUndefined(HashMap.get(sources, node)),
|
|
384
|
+
...(resolution === undefined ? {} : { resolution }),
|
|
385
|
+
})
|
|
386
|
+
}
|
|
387
|
+
for (const node of usedBy(attempts)) put(clean(node), node)
|
|
388
|
+
for (const attempt of resolutions(attempts))
|
|
389
|
+
if (attempt.request !== undefined)
|
|
390
|
+
put(
|
|
391
|
+
attempt.request,
|
|
392
|
+
root(attempt),
|
|
393
|
+
pathToFileURL(fileURLToPath(attempt.url)).href,
|
|
394
|
+
)
|
|
395
|
+
return files
|
|
396
|
+
}
|
|
323
397
|
return {
|
|
324
398
|
prepare,
|
|
399
|
+
files,
|
|
400
|
+
resolutions,
|
|
401
|
+
invalidated,
|
|
402
|
+
affected: (attempt: Attempt, invalid: HashSet.HashSet<string>) =>
|
|
403
|
+
HashSet.has(invalid, root(attempt)),
|
|
325
404
|
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
405
|
unsupported: (generation: Generation, roots?: ReadonlyArray<string>) => {
|
|
395
|
-
const
|
|
396
|
-
|
|
397
|
-
|
|
406
|
+
const used =
|
|
407
|
+
roots === undefined
|
|
408
|
+
? undefined
|
|
409
|
+
: usedBy(Array.map(roots, (url) => ({ url, generation })))
|
|
410
|
+
return HashSet.fromIterable(
|
|
411
|
+
Array.map(
|
|
412
|
+
Array.filter(Array.fromIterable(dependencies), (node) =>
|
|
413
|
+
used === undefined
|
|
414
|
+
? version(node) === generation.id
|
|
415
|
+
: HashSet.has(used, node),
|
|
416
|
+
),
|
|
417
|
+
clean,
|
|
418
|
+
),
|
|
398
419
|
)
|
|
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
420
|
},
|
|
421
|
+
collect: (attempts: ReadonlyArray<Attempt>) => {
|
|
422
|
+
const used = usedBy(attempts)
|
|
423
|
+
const ownedViews = HashSet.fromIterable(
|
|
424
|
+
Array.map(attempts, (attempt) => attempt.generation.id),
|
|
425
|
+
)
|
|
426
|
+
views = HashMap.filter(
|
|
427
|
+
views,
|
|
428
|
+
(_, id) =>
|
|
429
|
+
id === revision ||
|
|
430
|
+
HashSet.has(instantiatedViews, id) ||
|
|
431
|
+
HashSet.has(ownedViews, id),
|
|
432
|
+
)
|
|
433
|
+
// Old code can reach any default route in its instantiated view, even
|
|
434
|
+
// after every lease on that target closed. Preserve its complete graph,
|
|
435
|
+
// source baseline and native capabilities until the session closes.
|
|
436
|
+
const keep = (node: string) =>
|
|
437
|
+
HashSet.has(used, node) ||
|
|
438
|
+
HashSet.has(instantiatedViews, version(node)!)
|
|
439
|
+
retained = used
|
|
440
|
+
sources = HashMap.filter(sources, (_, node) => keep(node))
|
|
441
|
+
edges = HashMap.filter(edges, (_, node) => keep(node))
|
|
442
|
+
dependencies = HashSet.filter(dependencies, keep)
|
|
443
|
+
reachCache = HashMap.filter(reachCache, (_, node) =>
|
|
444
|
+
HashSet.has(used, node),
|
|
445
|
+
)
|
|
446
|
+
},
|
|
447
|
+
// Internal counters support deterministic retention tests and external benchmarks.
|
|
448
|
+
counts: () => ({
|
|
449
|
+
sources: HashMap.size(sources),
|
|
450
|
+
edges: HashMap.size(edges),
|
|
451
|
+
dependencies: HashSet.size(dependencies),
|
|
452
|
+
views: HashMap.size(views),
|
|
453
|
+
}),
|
|
403
454
|
start: (generation: Generation, reload = false) => {
|
|
404
455
|
importing = { generation, reload }
|
|
405
456
|
},
|
|
@@ -410,7 +461,17 @@ export const modules = (base: string, include: (url: string) => boolean) => {
|
|
|
410
461
|
resolved = undefined
|
|
411
462
|
return `${prefix}${importing!.generation.id}:${specifier}`
|
|
412
463
|
},
|
|
464
|
+
rawRequest: (specifier: string) => `${rawPrefix}${specifier}`,
|
|
413
465
|
resolved: () => Option.fromUndefinedOr(resolved),
|
|
414
|
-
close: () =>
|
|
466
|
+
close: () => {
|
|
467
|
+
hooks.deregister()
|
|
468
|
+
views = HashMap.empty()
|
|
469
|
+
sources = HashMap.empty()
|
|
470
|
+
edges = HashMap.empty()
|
|
471
|
+
dependencies = HashSet.empty()
|
|
472
|
+
reachCache = HashMap.empty()
|
|
473
|
+
retained = HashSet.empty()
|
|
474
|
+
instantiatedViews = HashSet.empty()
|
|
475
|
+
},
|
|
415
476
|
}
|
|
416
477
|
}
|