Skip to content

Commit 4e14c8a

Browse files
authored
Change virtual network parameters to be required (#5109)
1 parent 0537330 commit 4e14c8a

13 files changed

Lines changed: 190 additions & 118 deletions

File tree

packages/base/card-api.gts

Lines changed: 42 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -479,10 +479,10 @@ export type GetSearchResourceFunc<T extends CardDef | FileDef = CardDef> = (
479479

480480
export interface CardStore {
481481
// The VirtualNetwork that owns this store's realm mappings, used for
482-
// prefix/RRI resolution during (de)serialization. Optional so test doubles
483-
// don't need to implement it; resolution sites degrade — URL-form refs
484-
// still URL-join, prefix-form refs pass through unchanged.
485-
virtualNetwork?: VirtualNetwork;
482+
// prefix/RRI resolution during (de)serialization. Required — every store
483+
// implementation must supply one (production stores, test stubs, the
484+
// FallbackCardStore).
485+
virtualNetwork: VirtualNetwork;
486486
getCard(url: string): CardDef | undefined;
487487
getFileMeta(url: string): FileDef | undefined;
488488
setCard(url: string, instance: CardDef): void;
@@ -1450,7 +1450,7 @@ class LinksTo<CardT extends LinkableDefConstructor> implements Field<CardT> {
14501450
if (reference == null || reference === '') {
14511451
return null;
14521452
}
1453-
let href = resolveRef(store, reference, relativeTo);
1453+
let href = resolveRef(store.virtualNetwork, reference, relativeTo);
14541454
let cachedInstance = isFileDef(this.card)
14551455
? store.getFileMeta(href)
14561456
: store.getCard(href);
@@ -2030,7 +2030,11 @@ class LinksToMany<FieldT extends LinkableDefConstructor> implements Field<
20302030
if (reference == null) {
20312031
return null;
20322032
}
2033-
let normalizedReference = resolveRef(store, reference, relativeTo);
2033+
let normalizedReference = resolveRef(
2034+
store.virtualNetwork,
2035+
reference,
2036+
relativeTo,
2037+
);
20342038
let cachedInstance = isFileDef(this.card)
20352039
? store.getFileMeta(normalizedReference)
20362040
: store.getCard(normalizedReference);
@@ -2480,7 +2484,7 @@ export class BaseDef {
24802484
return maybeRelativeReference;
24812485
}
24822486
return resolveRef(
2483-
getStore(value),
2487+
getStore(value).virtualNetwork,
24842488
maybeRelativeReference,
24852489
value[relativeTo],
24862490
);
@@ -2507,7 +2511,7 @@ export class BaseDef {
25072511
let normalizedId = rawValue.reference;
25082512
if (value[relativeTo]) {
25092513
normalizedId = resolveRef(
2510-
getStore(value),
2514+
getStore(value).virtualNetwork,
25112515
normalizedId,
25122516
value[relativeTo],
25132517
);
@@ -3480,7 +3484,11 @@ function lazilyLoadLink(
34803484
inflightLinkLoads.set(instance, inflightLoads);
34813485
}
34823486
let store = getStore(instance);
3483-
let reference = resolveRef(store, link, instance.id ?? instance[relativeTo]);
3487+
let reference = resolveRef(
3488+
store.virtualNetwork,
3489+
link,
3490+
instance.id ?? instance[relativeTo],
3491+
);
34843492
let key = `${field.name}/${reference}`;
34853493
let promise = inflightLoads.get(key);
34863494
if (promise) {
@@ -3552,7 +3560,7 @@ function lazilyLoadLink(
35523560
continue;
35533561
}
35543562
let notLoadedRef = resolveRef(
3555-
store,
3563+
store.virtualNetwork,
35563564
item.reference,
35573565
instance.id ?? instance[relativeTo],
35583566
);
@@ -3640,7 +3648,7 @@ function lazilyLoadLink(
36403648
continue;
36413649
}
36423650
let notLoadedRef = resolveRef(
3643-
store,
3651+
store.virtualNetwork,
36443652
item.reference,
36453653
instance.id ?? instance[relativeTo],
36463654
);
@@ -4682,30 +4690,34 @@ function getStore(instance: BaseDef): CardStore {
46824690
}
46834691

46844692
// The VirtualNetwork associated with an instance's store, for prefix/RRI
4685-
// resolution outside this module. Returns undefined when the store can't
4686-
// supply one — callers handle that by degrading to URL math or throwing.
4693+
// resolution outside this module. Returns undefined when the instance is
4694+
// detached (no store, no loader-attached VN) — callers handle that by
4695+
// degrading to URL math or throwing.
46874696
export function virtualNetworkFor(
46884697
instance: BaseDef,
46894698
): VirtualNetwork | undefined {
4690-
return getStore(instance).virtualNetwork;
4699+
try {
4700+
return getStore(instance).virtualNetwork;
4701+
} catch {
4702+
return undefined;
4703+
}
46914704
}
46924705

46934706
// Resolve a (possibly prefix-form or relative) reference to an absolute URL
4694-
// string through the store's VirtualNetwork. When the store doesn't carry
4695-
// a VN (test stubs, detached instances), fall back to plain URL math: it
4707+
// string through the supplied VirtualNetwork. When the caller can't supply
4708+
// one (test stubs, detached instances), fall back to plain URL math: it
46964709
// covers URL-form refs and relative refs against URL-form bases. Prefix-form
46974710
// refs and refs against prefix-form bases can't be resolved without a VN —
46984711
// `new URL()` throws on those, so we return the raw reference unchanged
46994712
// instead of bubbling the error to callers (e.g. relationship deserialize
47004713
// uses the returned string as a "did this resolve?" signal).
47014714
function resolveRef(
4702-
store: CardStore | undefined,
4715+
virtualNetwork: VirtualNetwork | undefined,
47034716
reference: string,
47044717
relativeTo: RealmResourceIdentifier | URL | undefined,
47054718
): string {
4706-
let vn = store?.virtualNetwork;
4707-
if (vn) {
4708-
return vn.resolveURL(reference, relativeTo).href;
4719+
if (virtualNetwork) {
4720+
return virtualNetwork.resolveURL(reference, relativeTo).href;
47094721
}
47104722
let base: URL | string | undefined;
47114723
if (relativeTo instanceof URL) {
@@ -4742,8 +4754,14 @@ class FallbackCardStore implements CardStore {
47424754
#inFlight: Set<Promise<unknown>> = new Set();
47434755
#loadGeneration = 0; // mirrors host store tracking to detect new loads
47444756

4745-
get virtualNetwork(): VirtualNetwork | undefined {
4746-
return myLoader().getVirtualNetwork();
4757+
get virtualNetwork(): VirtualNetwork {
4758+
let vn = myLoader().getVirtualNetwork();
4759+
if (!vn) {
4760+
throw new Error(
4761+
`FallbackCardStore.virtualNetwork requires the active Loader to have a VirtualNetwork`,
4762+
);
4763+
}
4764+
return vn;
47474765
}
47484766

47494767
getCard(id: string) {
@@ -4804,13 +4822,7 @@ class FallbackCardStore implements CardStore {
48044822
opts?: { dependencyTrackingContext?: RuntimeDependencyTrackingContext },
48054823
) {
48064824
trackRuntimeInstanceDependency(url, opts?.dependencyTrackingContext);
4807-
let vn = this.virtualNetwork;
4808-
if (!vn) {
4809-
throw new Error(
4810-
`CardStore.loadCardDocument requires a Loader with a VirtualNetwork`,
4811-
);
4812-
}
4813-
let promise = loadCardDocument(fetch, url, vn);
4825+
let promise = loadCardDocument(fetch, url, this.virtualNetwork);
48144826
this.trackLoad(promise);
48154827
return await promise;
48164828
}
@@ -4820,13 +4832,7 @@ class FallbackCardStore implements CardStore {
48204832
opts?: { dependencyTrackingContext?: RuntimeDependencyTrackingContext },
48214833
) {
48224834
trackRuntimeFileDependency(url, opts?.dependencyTrackingContext);
4823-
let vn = this.virtualNetwork;
4824-
if (!vn) {
4825-
throw new Error(
4826-
`CardStore.loadFileMetaDocument requires a Loader with a VirtualNetwork`,
4827-
);
4828-
}
4829-
let promise = loadFileMetaDocument(fetch, url, vn);
4835+
let promise = loadFileMetaDocument(fetch, url, this.virtualNetwork);
48304836
this.trackLoad(promise);
48314837
return await promise;
48324838
}

packages/base/card-serialization.ts

Lines changed: 23 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -73,10 +73,8 @@ export interface SerializeOpts {
7373
maybeRelativeReference?: (possibleReference: string) => string;
7474
overrides?: Map<string, typeof BaseDef>;
7575
// The VirtualNetwork to consult for prefix/RRI resolution during
76-
// serialization. Optional: when absent, the `maybeRelativeReference`
77-
// closures degrade — prefix-form refs pass through unchanged and only
78-
// URL-form bases support URL math.
79-
virtualNetwork?: VirtualNetwork;
76+
// serialization. Required — every caller must thread a VN.
77+
virtualNetwork: VirtualNetwork;
8078
}
8179

8280
export interface DeserializeOpts {
@@ -212,7 +210,7 @@ export function resourceFrom(
212210

213211
export function serializeCard(
214212
model: CardDef,
215-
opts?: SerializeOpts,
213+
opts: SerializeOpts,
216214
): LooseSingleCardDocument {
217215
let doc = {
218216
data: {
@@ -222,40 +220,22 @@ export function serializeCard(
222220
};
223221
let modelRelativeTo: RealmResourceIdentifier | URL | undefined =
224222
model.id ?? model[relativeTo];
223+
let vn = opts.virtualNetwork;
225224
let data = serializeCardResource(model, doc, {
226225
...opts,
227226
...{
228227
maybeRelativeReference(possibleReference: string) {
229-
let vn = opts?.virtualNetwork;
230228
// Registered prefix refs (e.g. @cardstack/catalog/foo) are already
231-
// in their canonical portable form — return as-is. Without a VN
232-
// we can't know which prefixes are registered, so the most we can
233-
// do for prefix-form refs is pass them through unchanged.
234-
if (vn ? vn.isRegisteredPrefix(possibleReference) : false) {
229+
// in their canonical portable form — return as-is.
230+
if (vn.isRegisteredPrefix(possibleReference)) {
235231
return possibleReference;
236232
}
237-
let modelRelativeToForURL: URL | undefined;
238-
if (typeof modelRelativeTo === 'string') {
239-
if (vn) {
240-
modelRelativeToForURL = vn.toURL(modelRelativeTo);
241-
} else if (
242-
modelRelativeTo.startsWith('http://') ||
243-
modelRelativeTo.startsWith('https://')
244-
) {
245-
modelRelativeToForURL = new URL(modelRelativeTo);
246-
}
247-
} else {
248-
modelRelativeToForURL = modelRelativeTo;
249-
}
233+
let modelRelativeToForURL: URL | undefined =
234+
typeof modelRelativeTo === 'string'
235+
? vn.toURL(modelRelativeTo)
236+
: modelRelativeTo;
250237
let url = maybeURL(possibleReference, modelRelativeToForURL);
251238
if (!url) {
252-
if (!vn) {
253-
// Without a VN we can't resolve a prefix-form reference. Pass
254-
// through unchanged so callers that don't need relativization
255-
// (e.g. test adapters serializing same-realm cards by URL)
256-
// don't blow up on portable refs.
257-
return possibleReference;
258-
}
259239
throw new Error(
260240
`could not determine url from '${possibleReference}' relative to ${modelRelativeTo}`,
261241
);
@@ -302,7 +282,12 @@ export function serializeCardResource(
302282
usedLinksToFieldsOnly: !opts?.includeUnrenderedFields,
303283
});
304284
let overrides = getFieldOverrides(model);
305-
opts = { ...(opts ?? {}), overrides };
285+
// `serializeCardResource` is reachable from the recursive field-serialize
286+
// symbol path without opts (e.g. callSerializeHook with no opts arg).
287+
// That path doesn't read `opts.virtualNetwork`, so the synthesized
288+
// working opts can lack it; cast through SerializeOpts | undefined to
289+
// satisfy the required-VN type while preserving runtime behavior.
290+
opts = { ...(opts ?? {}), overrides } as SerializeOpts | undefined;
306291
let fieldResources = Object.entries(fields)
307292
.filter(
308293
([_fieldName, field]) =>
@@ -330,7 +315,7 @@ export function serializeCardResource(
330315

331316
export function serializeFileDef(
332317
model: FileDef,
333-
opts?: SerializeOpts,
318+
opts: SerializeOpts,
334319
): LooseSingleFileMetaDocument {
335320
let doc = {
336321
data: {
@@ -340,40 +325,25 @@ export function serializeFileDef(
340325
};
341326
let modelRelativeTo: RealmResourceIdentifier | URL | undefined =
342327
model.id ?? model[relativeTo];
328+
let vn = opts.virtualNetwork;
343329
let data = serializeCardResource(
344330
model,
345331
doc,
346332
{
347333
...opts,
348334
...{
349335
maybeRelativeReference(possibleReference: string) {
350-
let vn = opts?.virtualNetwork;
351336
// Registered prefix refs (e.g. @cardstack/catalog/foo) are
352337
// already in their canonical portable form — return as-is.
353-
// Without a VN we can't know which prefixes are registered,
354-
// so the most we can do for prefix-form refs is pass them
355-
// through unchanged.
356-
if (vn ? vn.isRegisteredPrefix(possibleReference) : false) {
338+
if (vn.isRegisteredPrefix(possibleReference)) {
357339
return possibleReference;
358340
}
359-
let modelRelativeToForURL: URL | undefined;
360-
if (typeof modelRelativeTo === 'string') {
361-
if (vn) {
362-
modelRelativeToForURL = vn.toURL(modelRelativeTo);
363-
} else if (
364-
modelRelativeTo.startsWith('http://') ||
365-
modelRelativeTo.startsWith('https://')
366-
) {
367-
modelRelativeToForURL = new URL(modelRelativeTo);
368-
}
369-
} else {
370-
modelRelativeToForURL = modelRelativeTo;
371-
}
341+
let modelRelativeToForURL: URL | undefined =
342+
typeof modelRelativeTo === 'string'
343+
? vn.toURL(modelRelativeTo)
344+
: modelRelativeTo;
372345
let url = maybeURL(possibleReference, modelRelativeToForURL);
373346
if (!url) {
374-
if (!vn) {
375-
return possibleReference;
376-
}
377347
throw new Error(
378348
`could not determine url from '${possibleReference}' relative to ${modelRelativeTo}`,
379349
);

packages/host/app/commands/copy-and-edit.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -279,7 +279,13 @@ export default class CopyAndEditCommand extends HostBaseCommand<
279279
fieldName: string,
280280
): string | undefined {
281281
try {
282-
let serialized = this.#cardAPI?.serializeCard(card);
282+
let vn = this.loaderService.loader.getVirtualNetwork();
283+
if (!vn) {
284+
return undefined;
285+
}
286+
let serialized = this.#cardAPI?.serializeCard(card, {
287+
virtualNetwork: vn,
288+
});
283289
let relationships = (serialized?.data as any)?.relationships ?? {};
284290
return Object.keys(relationships).find(
285291
(key) => key === fieldName || key.endsWith(`.${fieldName}`),

packages/host/app/lib/file-def-manager.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -315,7 +315,7 @@ export default class FileDefManagerImpl
315315
serialization: LooseSingleCardDocument;
316316
}[] = await Promise.all(
317317
cards.map(async (card) => {
318-
let opts: CardAPI.SerializeOpts = {
318+
let opts: Omit<CardAPI.SerializeOpts, 'virtualNetwork'> = {
319319
useAbsoluteURL: true,
320320
includeComputeds: true,
321321
};

packages/host/app/services/card-service.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -200,7 +200,7 @@ export default class CardService extends Service {
200200

201201
async serializeCard(
202202
card: CardDef,
203-
opts?: SerializeOpts & { withIncluded?: true },
203+
opts?: Omit<SerializeOpts, 'virtualNetwork'> & { withIncluded?: true },
204204
): Promise<LooseSingleCardDocument> {
205205
let api = await this.getAPI();
206206
let serialized = api.serializeCard(card, {

packages/host/app/services/render-service.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,10 @@ export class CardStoreWithErrors implements CardStore {
6868
this.#virtualNetwork = virtualNetwork;
6969
}
7070

71+
get virtualNetwork(): VirtualNetwork {
72+
return this.#virtualNetwork;
73+
}
74+
7175
getCard(id: string): CardDef | undefined {
7276
id = this.normalizeKey(id);
7377
return this.#cards.get(id);

packages/host/tests/helpers/adapter.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -265,9 +265,13 @@ export class TestRealmAdapter implements RealmAdapter {
265265
`${baseRealm.url}card-api`,
266266
);
267267
if (cardApi.isCard(value)) {
268-
let doc = cardApi.serializeCard(value, {
269-
virtualNetwork: this.#loader.getVirtualNetwork(),
270-
});
268+
let vn = this.#loader.getVirtualNetwork();
269+
if (!vn) {
270+
throw new Error(
271+
`TestRealmAdapter.openFile needs the test loader to have a VirtualNetwork to serialize ${path}`,
272+
);
273+
}
274+
let doc = cardApi.serializeCard(value, { virtualNetwork: vn });
271275
fileRefContent = JSON.stringify(doc);
272276
} else {
273277
fileRefContent =

0 commit comments

Comments
 (0)