Skip to content

Commit

Permalink
Merge pull request #1080 from cardstack/virtual-network
Browse files Browse the repository at this point in the history
Virtual network with loader shimming
  • Loading branch information
jurgenwerk authored Mar 21, 2024
2 parents 4b42538 + d673908 commit f6cb3c4
Show file tree
Hide file tree
Showing 27 changed files with 617 additions and 497 deletions.
26 changes: 21 additions & 5 deletions packages/base/card-api.gts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ import {
type RealmInfo,
} from '@cardstack/runtime-common';
import type { ComponentLike } from '@glint/template';
import { initSharedState } from './shared-state';

export { primitive, isField, type BoxComponent };
export const serialize = Symbol.for('cardstack-serialize');
Expand Down Expand Up @@ -170,14 +171,29 @@ function isStaleValue(value: any): value is StaleValue {
return false;
}
}
const deserializedData = new WeakMap<BaseDef, Map<string, any>>();
const recomputePromises = new WeakMap<BaseDef, Promise<any>>();
const identityContexts = new WeakMap<BaseDef, IdentityContext>();
const subscribers = new WeakMap<BaseDef, Set<CardChangeSubscriber>>();
const deserializedData = initSharedState(
'deserializedData',
() => new WeakMap<BaseDef, Map<string, any>>(),
);
const recomputePromises = initSharedState(
'recomputePromises',
() => new WeakMap<BaseDef, Promise<any>>(),
);
const identityContexts = initSharedState(
'identityContexts',
() => new WeakMap<BaseDef, IdentityContext>(),
);
const subscribers = initSharedState(
'subscribers',
() => new WeakMap<BaseDef, Set<CardChangeSubscriber>>(),
);

// our place for notifying Glimmer when a card is ready to re-render (which will
// involve rerunning async computed fields)
const cardTracking = new TrackedWeakMap<object, any>();
const cardTracking = initSharedState(
'cardTracking',
() => new TrackedWeakMap<object, any>(),
);

const isBaseInstance = Symbol.for('isBaseInstance');

Expand Down
6 changes: 5 additions & 1 deletion packages/base/field-component.gts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { getField } from '@cardstack/runtime-common';
import type { ComponentLike } from '@glint/template';
import { CardContainer } from '@cardstack/boxel-ui/components';
import Modifier from 'ember-modifier';
import { initSharedState } from './shared-state';
import { eq } from '@cardstack/boxel-ui/helpers';

interface BoxComponentSignature {
Expand All @@ -25,7 +26,10 @@ interface BoxComponentSignature {

export type BoxComponent = ComponentLike<BoxComponentSignature>;

const componentCache = new WeakMap<Box<BaseDef>, BoxComponent>();
const componentCache = initSharedState(
'componentCache',
() => new WeakMap<Box<BaseDef>, BoxComponent>(),
);

export function getBoxComponent(
card: typeof BaseDef,
Expand Down
29 changes: 21 additions & 8 deletions packages/base/room.gts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
} from '@cardstack/runtime-common';
//@ts-expect-error cached type not available yet
import { cached } from '@glimmer/tracking';
import { initSharedState } from './shared-state';
import BooleanField from './boolean';

// this is so we can have triple equals equivalent room member cards
Expand Down Expand Up @@ -277,14 +278,26 @@ interface RoomState {

// in addition to acting as a cache, this also ensures we have
// triple equal equivalence for the interior cards of RoomField
const eventCache = new WeakMap<RoomField, Map<string, MatrixEvent>>();
const messageCache = new WeakMap<RoomField, Map<string, MessageField>>();
const roomMemberCache = new WeakMap<RoomField, Map<string, RoomMemberField>>();
const roomStateCache = new WeakMap<RoomField, RoomState>();
const fragmentCache = new WeakMap<
RoomField,
Map<string, CardFragmentContent>
>();
const eventCache = initSharedState(
'eventCache',
() => new WeakMap<RoomField, Map<string, MatrixEvent>>(),
);
const messageCache = initSharedState(
'messageCache',
() => new WeakMap<RoomField, Map<string, MessageField>>(),
);
const roomMemberCache = initSharedState(
'roomMemberCache',
() => new WeakMap<RoomField, Map<string, RoomMemberField>>(),
);
const roomStateCache = initSharedState(
'roomStateCache',
() => new WeakMap<RoomField, RoomState>(),
);
const fragmentCache = initSharedState(
'fragmentCache',
() => new WeakMap<RoomField, Map<string, CardFragmentContent>>(),
);

export class RoomField extends FieldDef {
static displayName = 'Room';
Expand Down
25 changes: 25 additions & 0 deletions packages/base/shared-state.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/*
This module needs to exist so long as we have multiple loaders cooperating in
the same environment. It ensures that any shared global state is really
global. Ultimately we would like to get to the point where the loader itself
is scoped so broadly that there's only one, and module-scoped state is safe to
treat as global.
*/

const bucket: Map<string, unknown> = (() => {
let g = globalThis as unknown as {
__card_api_shared_state: Map<string, unknown> | undefined;
};
if (!g.__card_api_shared_state) {
g.__card_api_shared_state = new Map();
}
return g.__card_api_shared_state;
})();

export function initSharedState<T>(key: string, fn: () => T): T {
if (bucket.has(key)) {
return bucket.get(key) as T;
}
bucket.set(key, fn());
return bucket.get(key) as T;
}
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ export default class InteractSubmode extends Component<Signature> {
doc,
relativeTo,
);
await here.cardService.saveModel(here, newCard);

let newItem = new StackItem({
owner: here,
card: newCard,
Expand All @@ -176,6 +176,13 @@ export default class InteractSubmode extends Component<Signature> {
isLinkedCard: opts?.isLinkedCard,
stackIndex,
});

// TODO: it is important saveModel happens after newItem because it
// looks like perhaps there is a race condition (or something else) when a
// new linked card is created, and when it is added to the stack and closed
// - the parent card is not updated with the new linked card
await here.cardService.saveModel(here, newCard);

await newItem.ready();
here.addToStack(newItem);
return await newItem.request?.promise;
Expand Down
65 changes: 35 additions & 30 deletions packages/host/app/lib/externals.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,41 +29,46 @@ import * as boxelUiHelpers from '@cardstack/boxel-ui/helpers';
import * as boxelUiIcons from '@cardstack/boxel-ui/icons';

import * as runtime from '@cardstack/runtime-common';
import { Loader } from '@cardstack/runtime-common/loader';
import { VirtualNetwork } from '@cardstack/runtime-common';

export function shimExternals(loader: Loader) {
loader.shimModule('@cardstack/runtime-common', runtime);
loader.shimModule('@cardstack/boxel-ui/components', boxelUiComponents);
loader.shimModule('@cardstack/boxel-ui/helpers', boxelUiHelpers);
loader.shimModule('@cardstack/boxel-ui/icons', boxelUiIcons);
loader.shimModule('@glimmer/component', glimmerComponent);
loader.shimModule('@ember/component', emberComponent);
loader.shimModule(
export function shimExternals(virtualNetwork: VirtualNetwork) {
virtualNetwork.shimModule('@cardstack/runtime-common', runtime);
virtualNetwork.shimModule(
'@cardstack/boxel-ui/components',
boxelUiComponents,
);
virtualNetwork.shimModule('@cardstack/boxel-ui/helpers', boxelUiHelpers);
virtualNetwork.shimModule('@cardstack/boxel-ui/icons', boxelUiIcons);
virtualNetwork.shimModule('@glimmer/component', glimmerComponent);
virtualNetwork.shimModule('@ember/component', emberComponent);
virtualNetwork.shimModule(
'@ember/component/template-only',
emberComponentTemplateOnly,
);
loader.shimModule('ember-css-url', cssUrl);
loader.shimModule('@ember/template-factory', emberTemplateFactory);
loader.shimModule('@ember/template', emberTemplate);
loader.shimModule('@glimmer/tracking', glimmerTracking);
loader.shimModule('@ember/object', emberObject);
loader.shimModule('@ember/object/internals', emberObjectInternals);
loader.shimModule('@ember/helper', emberHelper);
loader.shimModule('@ember/modifier', emberModifier);
loader.shimModule('ember-resources', emberResources);
loader.shimModule('ember-concurrency', emberConcurrency);
loader.shimModule(
virtualNetwork.shimModule('ember-css-url', cssUrl);
virtualNetwork.shimModule('@ember/template-factory', emberTemplateFactory);
virtualNetwork.shimModule('@ember/template', emberTemplate);
virtualNetwork.shimModule('@glimmer/tracking', glimmerTracking);
virtualNetwork.shimModule('@ember/object', emberObject);
virtualNetwork.shimModule('@ember/object/internals', emberObjectInternals);
virtualNetwork.shimModule('@ember/helper', emberHelper);
virtualNetwork.shimModule('@ember/modifier', emberModifier);
virtualNetwork.shimModule('ember-resources', emberResources);
virtualNetwork.shimModule('ember-concurrency', emberConcurrency);
virtualNetwork.shimModule(
'ember-concurrency/-private/async-arrow-runtime',
emberConcurrencyAsyncArrowRuntime,
);
loader.shimModule('ember-modifier', emberModifier2);
loader.shimModule('flat', flat);
loader.shimModule('lodash', lodash);
loader.shimModule('tracked-built-ins', tracked);
loader.shimModule('date-fns', dateFns);
loader.shimModule('@ember/destroyable', emberDestroyable);
loader.shimModule('marked', marked);
loader.shimModule('ethers', ethers);
loader.shimModule('ember-source/types', { default: class {} });
loader.shimModule('ember-source/types/preview', { default: class {} });
virtualNetwork.shimModule('ember-modifier', emberModifier2);
virtualNetwork.shimModule('flat', flat);
virtualNetwork.shimModule('lodash', lodash);
virtualNetwork.shimModule('tracked-built-ins', tracked);
virtualNetwork.shimModule('date-fns', dateFns);
virtualNetwork.shimModule('@ember/destroyable', emberDestroyable);
virtualNetwork.shimModule('marked', marked);
virtualNetwork.shimModule('ethers', ethers);
virtualNetwork.shimModule('ember-source/types', { default: class {} });
virtualNetwork.shimModule('ember-source/types/preview', {
default: class {},
});
}
16 changes: 9 additions & 7 deletions packages/host/app/services/loader-service.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,19 @@
import Service, { service } from '@ember/service';
import { tracked } from '@glimmer/tracking';

import { baseRealm } from '@cardstack/runtime-common';
import { VirtualNetwork, baseRealm } from '@cardstack/runtime-common';
import { Loader } from '@cardstack/runtime-common/loader';

import config from '@cardstack/host/config/environment';
import { shimExternals } from '@cardstack/host/lib/externals';
import {
type RealmSessionResource,
getRealmSession,
} from '@cardstack/host/resources/realm-session';
import MatrixService from '@cardstack/host/services/matrix-service';
import RealmInfoService from '@cardstack/host/services/realm-info-service';

import { shimExternals } from '../lib/externals';

export default class LoaderService extends Service {
@service declare fastboot: { isFastBoot: boolean };
@service private declare matrixService: MatrixService;
Expand All @@ -24,29 +25,30 @@ export default class LoaderService extends Service {
// which in turn assures the resources will not get torn down.
private realmSessions: Map<string, RealmSessionResource> = new Map();

virtualNetwork = new VirtualNetwork();

reset() {
if (this.loader) {
this.loader = Loader.cloneLoader(this.loader);
shimExternals(this.loader);
} else {
this.loader = this.makeInstance();
}
}

private makeInstance() {
if (this.fastboot.isFastBoot) {
let loader = new Loader();
shimExternals(loader);
let loader = this.virtualNetwork.createLoader();
shimExternals(this.virtualNetwork);
return loader;
}

let loader = new Loader();
let loader = this.virtualNetwork.createLoader();
loader.addURLMapping(
new URL(baseRealm.url),
new URL(config.resolvedBaseRealmURL),
);
loader.prependURLHandlers([(req) => this.fetchWithAuth(req)]);
shimExternals(loader);
shimExternals(this.virtualNetwork);

return loader;
}
Expand Down
6 changes: 4 additions & 2 deletions packages/host/tests/acceptance/basic-test.gts
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,10 @@ module('Acceptance | basic tests', function (hooks) {
hooks.beforeEach(async function () {
window.localStorage.removeItem('recent-files');

let loader = (this.owner.lookup('service:loader-service') as LoaderService)
.loader;
let loaderService = this.owner.lookup(
'service:loader-service',
) as LoaderService;
let loader = loaderService.loader;
let { field, contains, CardDef, Component } = await loader.import<
typeof import('https://cardstack.com/base/card-api')
>(`${baseRealm.url}card-api`);
Expand Down
5 changes: 5 additions & 0 deletions packages/host/tests/acceptance/interact-submode-test.gts
Original file line number Diff line number Diff line change
Expand Up @@ -313,6 +313,8 @@ module('Acceptance | interact submode tests', function (hooks) {
assert.dom('[data-test-search-sheet]').doesNotHaveClass('results'); // Search closed

// The card appears on a new stack
await waitFor('[data-test-operator-mode-stack]');

assert.dom('[data-test-operator-mode-stack]').exists({ count: 1 });
assert
.dom(
Expand Down Expand Up @@ -640,6 +642,7 @@ module('Acceptance | interact submode tests', function (hooks) {
assert.dom('[data-test-search-sheet]').doesNotHaveClass('prompt'); // Search closed

// There are now 2 stacks

assert.dom('[data-test-operator-mode-stack]').exists({ count: 2 });
assert.dom('[data-test-operator-mode-stack="0"]').includesText('Mango'); // Mango goes on the left stack
assert.dom('[data-test-operator-mode-stack="1"]').includesText('Fadhlan');
Expand Down Expand Up @@ -668,6 +671,7 @@ module('Acceptance | interact submode tests', function (hooks) {
assert.dom('[data-test-search-sheet]').doesNotHaveClass('prompt'); // Search closed

// There are now 2 stacks
await waitFor('[data-test-operator-mode-stack="0"]');
assert.dom('[data-test-operator-mode-stack]').exists({ count: 2 });
assert.dom('[data-test-operator-mode-stack="0"]').includesText('Fadhlan');
assert.dom('[data-test-operator-mode-stack="1"]').includesText('Mango'); // Mango gets move onto the right stack
Expand Down Expand Up @@ -1357,6 +1361,7 @@ module('Acceptance | interact submode tests', function (hooks) {
);
},
});

await waitUntil(() =>
document
.querySelector('[data-test-operator-mode-stack="0"] [data-test-person]')
Expand Down
27 changes: 5 additions & 22 deletions packages/host/tests/helpers/index.gts
Original file line number Diff line number Diff line change
Expand Up @@ -471,14 +471,14 @@ async function setupTestRealm({
isAcceptanceTest?: boolean;
permissions?: RealmPermissions;
}) {
let owner = (getContext() as TestContext).owner;

realmURL = realmURL ?? testRealmURL;

for (const [path, mod] of Object.entries(contents)) {
if (path.endsWith('.gts') && typeof mod !== 'string') {
await shimModule(
`${realmURL}${path.replace(/\.gts$/, '')}`,
mod as object,
loader,
);
let moduleURLString = `${realmURL}${path.replace(/\.gts$/, '')}`;
loader.shimModule(moduleURLString, mod as object);
}
}
let api = await loader.import<CardAPI>(`${baseRealm.url}card-api`);
Expand Down Expand Up @@ -506,7 +506,6 @@ async function setupTestRealm({
}
}
let adapter = new TestRealmAdapter(flatFiles, new URL(realmURL));
let owner = (getContext() as TestContext).owner;
if (isAcceptanceTest) {
await visit('/acceptance-test-setup');
} else {
Expand Down Expand Up @@ -567,22 +566,6 @@ export async function saveCard(instance: CardDef, id: string, loader: Loader) {
return doc;
}

export async function shimModule(
moduleURL: string,
module: Record<string, any>,
loader: Loader,
) {
if (loader) {
loader.shimModule(moduleURL, module);
}
await Promise.all(
Object.keys(module).map(async (name) => {
let m = await loader.import<any>(moduleURL);
m[name];
}),
);
}

export function setupCardLogs(
hooks: NestedHooks,
apiThunk: () => Promise<CardAPI>,
Expand Down
Loading

0 comments on commit f6cb3c4

Please sign in to comment.