Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,9 @@ module.exports = {
'./blueprints/*/index.js',
'./config/**/*.js',
'./tests/dummy/config/**/*.js',
'./bin/**/*.js',
'./lib/**/*.js',
'./node-tests/**/*.js',
],
parserOptions: {
sourceType: 'script',
Expand Down
6 changes: 6 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,12 @@ jobs:
- name: Coverage gate self-tests
run: node --test scripts/check-coverage.test.mjs

# The engine release builder, compatibility check and their CLIs. The
# end-to-end mount test in node-tests/ skips itself: it needs two console
# builds (see docs/runtime-engines.md).
- name: Node tests (runtime engine tooling)
run: pnpm run test:node

- name: Test with coverage (full suite)
run: pnpm run coverage

Expand Down
1 change: 1 addition & 0 deletions .npmignore
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
/ember-cli-build.js
/testem.js
/tests/
/node-tests/
/yarn-error.log
/yarn.lock
.gitkeep
Expand Down
9 changes: 5 additions & 4 deletions addon/services/universe/extension-manager.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { assert, debug, warn } from '@ember/debug';
import { next } from '@ember/runloop';
import loadInstalledExtensions from '@fleetbase/ember-core/utils/load-installed-extensions';
import mapEngines from '@fleetbase/ember-core/utils/map-engines';
import { getRuntimeEngines, getRuntimeExtensionLoader } from '@fleetbase/ember-core/utils/runtime-engines';
import config from 'ember-get-config';
import { getExtensionLoader } from '@fleetbase/console/extensions';
import { isArray } from '@ember/array';
Expand Down Expand Up @@ -736,8 +737,8 @@ export default class ExtensionManagerService extends Service.extend(Evented) {
// Get admin-configured extensions from config
const additionalCoreExtensions = config.APP?.extensions ?? [];

// Load installed extensions (includes core, admin-configured, and user-installed)
const extensions = await loadInstalledExtensions(additionalCoreExtensions);
// Load installed extensions (includes core, admin-configured, user-installed and runtime engines)
const extensions = await loadInstalledExtensions(additionalCoreExtensions, getRuntimeEngines());

application.extensions = extensions;
application.engines = mapEngines(extensions);
Expand Down Expand Up @@ -790,8 +791,8 @@ export default class ExtensionManagerService extends Service.extend(Evented) {
const extensionName = extension.name || extension;
const extStartTime = performance.now();

// Lookup the loader function from the build-time generated map
const loader = getExtensionLoader(extensionName);
// Lookup the loader function from the build-time generated map, then the runtime engines
const loader = getExtensionLoader(extensionName) ?? getRuntimeExtensionLoader(extensionName);

if (!loader) {
warn(`[ExtensionManager] No loader registered for ${extensionName}. Ensure addon/extension.js exists and prebuild generated the mapping.`, false, {
Expand Down
27 changes: 21 additions & 6 deletions addon/utils/load-extensions.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,16 @@ import config from 'ember-get-config';
*/
const CACHE_KEY = 'fleetbase_extensions_list';
const CACHE_VERSION_KEY = 'fleetbase_extensions_version';
const CACHE_RUNTIME_SIGNATURE_KEY = 'fleetbase_extensions_runtime_signature';
const CACHE_TTL = 1000 * 60 * 60; // 1 hour

/**
* Get cached extensions from localStorage
*
* @param {String} runtimeSignature Signature of the runtime engine list the cache must match
* @returns {Array|null} Cached extensions or null
*/
function getCachedExtensions() {
function getCachedExtensions(runtimeSignature) {
try {
const cached = localStorage.getItem(CACHE_KEY);
const cachedVersion = localStorage.getItem(CACHE_VERSION_KEY);
Expand All @@ -28,6 +30,13 @@ function getCachedExtensions() {
return null;
}

// A runtime engine was installed, removed or updated since the list was cached
const cachedSignature = localStorage.getItem(CACHE_RUNTIME_SIGNATURE_KEY) ?? '';
if (cachedSignature !== runtimeSignature) {
debug(`[loadExtensions] Runtime engines changed (cached: "${cachedSignature}", current: "${runtimeSignature}")`);
return null;
}

const cacheData = JSON.parse(cached);
const cacheAge = Date.now() - cacheData.timestamp;

Expand All @@ -49,15 +58,17 @@ function getCachedExtensions() {
* Save extensions to localStorage cache
*
* @param {Array} extensions Extensions array
* @param {String} runtimeSignature Signature of the runtime engine list at the time
*/
function setCachedExtensions(extensions) {
function setCachedExtensions(extensions, runtimeSignature) {
try {
const cacheData = {
extensions,
timestamp: Date.now(),
};
localStorage.setItem(CACHE_KEY, JSON.stringify(cacheData));
localStorage.setItem(CACHE_VERSION_KEY, config.APP.version);
localStorage.setItem(CACHE_RUNTIME_SIGNATURE_KEY, runtimeSignature);
debug('[loadExtensions] Extensions list cached to localStorage');
} catch (e) {
debug(`[loadExtensions] Failed to cache extensions: ${e.message}`);
Expand All @@ -73,6 +84,7 @@ export function clearExtensionsCache() {
try {
localStorage.removeItem(CACHE_KEY);
localStorage.removeItem(CACHE_VERSION_KEY);
localStorage.removeItem(CACHE_RUNTIME_SIGNATURE_KEY);
debug('[loadExtensions] Cache cleared');
} catch (e) {
debug(`[loadExtensions] Failed to clear cache: ${e.message}`);
Expand All @@ -86,16 +98,19 @@ export function clearExtensionsCache() {
* 1. Check localStorage cache first (instant, no HTTP request)
* 2. If cache hit and valid, use it immediately
* 3. If cache miss, fetch from server and cache the result
* 4. Cache is valid for 1 hour
* 4. Cache is valid for 1 hour, for one app version and one runtime engine list
*
* @export
* @param {Object} [options]
* @param {String} [options.runtimeSignature] Signature of the runtime engine list (see utils/runtime-engines);
* a cache written under another signature is discarded
* @returns {Promise<Array>} Extensions array
*/
export default async function loadExtensions() {
export default async function loadExtensions({ runtimeSignature = '' } = {}) {
const isProduction = config?.environment === 'production';
if (isProduction) {
// Try cache first only in production
const cachedExtensions = getCachedExtensions();
const cachedExtensions = getCachedExtensions(runtimeSignature);
if (cachedExtensions) {
return Promise.resolve(cachedExtensions);
}
Expand All @@ -114,7 +129,7 @@ export default async function loadExtensions() {
debug(`[loadExtensions] Fetched from server in ${(endTime - startTime).toFixed(2)}ms`);

// Cache the result
setCachedExtensions(extensions);
setCachedExtensions(extensions, runtimeSignature);
resolve(extensions);
})
.catch(reject);
Expand Down
23 changes: 20 additions & 3 deletions addon/utils/load-installed-extensions.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,22 @@
import loadExtensions from './load-extensions';
import fleetbaseApiFetch from './fleetbase-api-fetch';
import isAuthenticated from './is-authenticated';
import { runtimeSignature, toExtensionEntry } from './runtime-engines';

export default async function loadInstalledExtensions(additionalCoreEngines = []) {
/**
* The extensions this console may set up: build-time `extensions.json`
* entries that are core, admin-configured or installed, followed by the
* runtime engines (prebuilt releases mounted at boot, see utils/runtime-engines).
*
* A runtime engine counts as installed: the runtime manifest only lists
* releases installed on this instance. When the build already has an engine,
* its build-time entry wins and the runtime one is dropped.
*
* @param {Array<String>} additionalCoreEngines Engines to treat as core (admin-configured)
* @param {Array<Object>} runtimeEngines Prepared runtime engines (getRuntimeEngines())
* @return {Promise<Array<Object>>}
*/
export default async function loadInstalledExtensions(additionalCoreEngines = [], runtimeEngines = []) {
const CORE_ENGINES = [
'@fleetbase/fleetops-engine',
'@fleetbase/storefront-engine',
Expand All @@ -17,7 +31,7 @@ export default async function loadInstalledExtensions(additionalCoreEngines = []
'@fleetbase/valhalla-engine',
...additionalCoreEngines,
];
const INDEXED_ENGINES = await loadExtensions();
const INDEXED_ENGINES = await loadExtensions({ runtimeSignature: runtimeSignature(runtimeEngines) });
// const INSTALLED_ENGINES = await fleetbaseApiFetch('get', 'engines', {}, { namespace: '~registry/v1', fallbackResponse: [] });

let INSTALLED_ENGINES = [];
Expand All @@ -29,7 +43,10 @@ export default async function loadInstalledExtensions(additionalCoreEngines = []
return CORE_ENGINES.includes(engineName) || INSTALLED_ENGINES.find((pkg) => pkg.name === engineName);
};

return INDEXED_ENGINES.filter((pkg) => {
const indexed = INDEXED_ENGINES.filter((pkg) => {
return isInstalledEngine(pkg.name);
});
const runtime = runtimeEngines.filter((engine) => !INDEXED_ENGINES.some((pkg) => pkg.name === engine.name)).map(toExtensionEntry);

return [...indexed, ...runtime];
}
Loading
Loading