From 802d6d1a662f2d8d7050e0884e9f2f38e5f96734 Mon Sep 17 00:00:00 2001 From: Dimitris Marlagkoutsos Date: Fri, 22 Nov 2024 15:21:09 +0000 Subject: [PATCH 1/8] fix: Vat restart functionality --- packages/kernel/src/Kernel.ts | 218 ++++++++++-------- packages/kernel/src/types.ts | 11 +- packages/kernel/src/vat-state-service.test.ts | 83 +++++++ packages/kernel/src/vat-state-service.ts | 25 ++ 4 files changed, 240 insertions(+), 97 deletions(-) create mode 100644 packages/kernel/src/vat-state-service.test.ts create mode 100644 packages/kernel/src/vat-state-service.ts diff --git a/packages/kernel/src/Kernel.ts b/packages/kernel/src/Kernel.ts index eb16c6e4a..cc5b8b097 100644 --- a/packages/kernel/src/Kernel.ts +++ b/packages/kernel/src/Kernel.ts @@ -30,6 +30,7 @@ import type { ClusterConfig, VatConfig, } from './types.js'; +import { VatStateService } from './vat-state-service.js'; import { Vat } from './Vat.js'; export class Kernel { @@ -43,6 +44,8 @@ export class Kernel { readonly #logger: Logger; + readonly #vatStateService: VatStateService; + constructor( stream: DuplexStream, vatWorkerService: VatWorkerService, @@ -54,6 +57,7 @@ export class Kernel { this.#vatWorkerService = vatWorkerService; this.#storage = makeKernelStore(rawStorage); this.#logger = logger ?? makeLogger('[ocap kernel]'); + this.#vatStateService = new VatStateService(); } async init(): Promise { @@ -68,80 +72,6 @@ export class Kernel { }); } - async #receiveMessages(): Promise { - for await (const message of this.#stream) { - if (!isKernelCommand(message)) { - this.#logger.error('Received unexpected message', message); - continue; - } - - const { method, params } = message; - - let vat: Vat; - - switch (method) { - case KernelCommandMethod.ping: - await this.#reply({ method, params: 'pong' }); - break; - case KernelCommandMethod.evaluate: - if (!this.#vats.size) { - throw new Error('No vats available to call'); - } - vat = this.#vats.values().next().value as Vat; - await this.#reply({ - method, - params: await this.evaluate(vat.vatId, params), - }); - break; - case KernelCommandMethod.capTpCall: - if (!this.#vats.size) { - throw new Error('No vats available to call'); - } - vat = this.#vats.values().next().value as Vat; - await this.#reply({ - method, - params: stringify(await vat.callCapTp(params)), - }); - break; - case KernelCommandMethod.kvSet: - this.kvSet(params.key, params.value); - await this.#reply({ - method, - params: `~~~ set "${params.key}" to "${params.value}" ~~~`, - }); - break; - case KernelCommandMethod.kvGet: { - try { - const value = this.kvGet(params); - const result = - typeof value === 'string' ? `"${value}"` : `${value}`; - await this.#reply({ - method, - params: `~~~ got ${result} ~~~`, - }); - } catch (problem) { - // TODO: marshal - await this.#reply({ - method, - params: String(toError(problem)), - }); - } - break; - } - default: - console.error( - 'kernel worker received unexpected command', - // @ts-expect-error Runtime does not respect "never". - { method: method.valueOf(), params }, - ); - } - } - } - - async #reply(message: KernelCommandReply): Promise { - await this.#stream.write(message); - } - /** * Evaluate a string in the default iframe. * @@ -192,17 +122,7 @@ export class Kernel { if (this.#vats.has(vatId)) { throw new VatAlreadyExistsError(vatId); } - const multiplexer = await this.#vatWorkerService.launch(vatId, vatConfig); - multiplexer.start().catch((error) => this.#logger.error(error)); - const commandStream = multiplexer.createChannel< - VatCommandReply, - VatCommand - >('command', isVatCommandReply); - const capTpStream = multiplexer.createChannel('capTp'); - const vat = new Vat({ vatId, vatConfig, commandStream, capTpStream }); - this.#vats.set(vat.vatId, vat); - await vat.init(); - return vat; + return this.#initVat(vatId, vatConfig); } /** @@ -223,14 +143,18 @@ export class Kernel { /** * Restarts a vat. * - * @param id - The ID of the vat. + * @param vatId - The ID of the vat. + * @returns A promise that resolves the restarted vat. */ - async restartVat(id: VatId): Promise { - await this.terminateVat(id); - // XXX TODO the following line has been hacked up to enable a successful - // build, but is entirely wrong. Restart expressed this way loses the original vat - // ID and configuration. - await this.launchVat({ sourceSpec: 'not-really-there.js' }); + async restartVat(vatId: VatId): Promise { + const state = this.#vatStateService.getVatState(vatId); + if (!state) { + throw new Error(`No state found for vat ${vatId}`); + } + + await this.terminateVat(vatId); + const vat = await this.#initVat(vatId, state.config); + return vat; } /** @@ -274,6 +198,116 @@ export class Kernel { return vat.sendMessage(command); } + // -------------------------------------------------------------------------- + // Private methods + // -------------------------------------------------------------------------- + + /** + * Receives messages from the stream. + */ + async #receiveMessages(): Promise { + for await (const message of this.#stream) { + if (!isKernelCommand(message)) { + this.#logger.error('Received unexpected message', message); + continue; + } + + const { method, params } = message; + + let vat: Vat; + + switch (method) { + case KernelCommandMethod.ping: + await this.#reply({ method, params: 'pong' }); + break; + case KernelCommandMethod.evaluate: + if (!this.#vats.size) { + throw new Error('No vats available to call'); + } + vat = this.#vats.values().next().value as Vat; + await this.#reply({ + method, + params: await this.evaluate(vat.vatId, params), + }); + break; + case KernelCommandMethod.capTpCall: + if (!this.#vats.size) { + throw new Error('No vats available to call'); + } + vat = this.#vats.values().next().value as Vat; + await this.#reply({ + method, + params: stringify(await vat.callCapTp(params)), + }); + break; + case KernelCommandMethod.kvSet: + this.kvSet(params.key, params.value); + await this.#reply({ + method, + params: `~~~ set "${params.key}" to "${params.value}" ~~~`, + }); + break; + case KernelCommandMethod.kvGet: { + try { + const value = this.kvGet(params); + const result = + typeof value === 'string' ? `"${value}"` : `${value}`; + await this.#reply({ + method, + params: `~~~ got ${result} ~~~`, + }); + } catch (problem) { + // TODO: marshal + await this.#reply({ + method, + params: String(toError(problem)), + }); + } + break; + } + default: + console.error( + 'kernel worker received unexpected command', + // @ts-expect-error Runtime does not respect "never". + { method: method.valueOf(), params }, + ); + } + } + } + + /** + * Replies to a message. + * + * @param message - The message to reply to. + */ + async #reply(message: KernelCommandReply): Promise { + await this.#stream.write(message); + } + + /** + * Initializes a vat. + * + * @param vatId - The ID of the vat. + * @param vatConfig - The configuration of the vat. + * @returns A promise that resolves the vat. + */ + async #initVat(vatId: VatId, vatConfig: VatConfig): Promise { + const multiplexer = await this.#vatWorkerService.launch(vatId, vatConfig); + multiplexer.start().catch((error) => this.#logger.error(error)); + const commandStream = multiplexer.createChannel< + VatCommandReply, + VatCommand + >('command', isVatCommandReply); + const capTpStream = multiplexer.createChannel('capTp'); + const vat = new Vat({ vatId, vatConfig, commandStream, capTpStream }); + this.#vats.set(vat.vatId, vat); + this.#vatStateService.saveVatState(vatId, { + config: vatConfig, + }); + await vat.init(); + return vat; + } + /** * Gets a vat. * diff --git a/packages/kernel/src/types.ts b/packages/kernel/src/types.ts index 100303415..5ef5ab470 100644 --- a/packages/kernel/src/types.ts +++ b/packages/kernel/src/types.ts @@ -47,7 +47,7 @@ type EndpointState = { kRefToERef: Map; }; -type VatState = { +type VatKernelState = { messagePort: typeof MessagePort; state: EndpointState; source: string; @@ -72,7 +72,7 @@ export type KernelPromise = { }; export type KernelState = { - vats: Map; + vats: Map; remotes: Map; kernelPromises: Map; }; @@ -176,9 +176,10 @@ export const VatConfigStruct = define('VatConfig', (value) => { return false; } - const { sourceSpec, bundleSpec, bundleName, creationOptions, parameters } = - value as Record; - const specOnly = { sourceSpec, bundleSpec, bundleName }; + const { creationOptions, parameters, ...specOnly } = value as Record< + string, + unknown + >; return ( is(specOnly, UserCodeSpecStruct) && diff --git a/packages/kernel/src/vat-state-service.test.ts b/packages/kernel/src/vat-state-service.test.ts new file mode 100644 index 000000000..5561ae282 --- /dev/null +++ b/packages/kernel/src/vat-state-service.test.ts @@ -0,0 +1,83 @@ +import { describe, it, expect, beforeEach } from 'vitest'; + +import type { VatId, VatConfig } from './types'; +import { VatStateService } from './vat-state-service'; +import type { VatState } from './vat-state-service'; + +describe('VatStateService', () => { + let vatStateService: VatStateService; + const mockVatId: VatId = 'v1'; + const mockVatConfig: VatConfig = { sourceSpec: 'test-vat.js' }; + const mockVatState: VatState = { config: mockVatConfig }; + + beforeEach(() => { + vatStateService = new VatStateService(); + }); + + describe('saveVatState', () => { + it('should save vat state', () => { + vatStateService.saveVatState(mockVatId, mockVatState); + const savedState = vatStateService.getVatState(mockVatId); + expect(savedState).toStrictEqual(mockVatState); + }); + + it('should overwrite existing vat state', () => { + const initialState: VatState = { + config: { sourceSpec: 'initial.js' }, + }; + vatStateService.saveVatState(mockVatId, initialState); + + const updatedState: VatState = { + config: { sourceSpec: 'updated.js' }, + }; + vatStateService.saveVatState(mockVatId, updatedState); + + const savedState = vatStateService.getVatState(mockVatId); + expect(savedState).toStrictEqual(updatedState); + expect(savedState).not.toStrictEqual(initialState); + }); + }); + + describe('getVatState', () => { + it('should return undefined for non-existent vat', () => { + const state = vatStateService.getVatState('v999'); + expect(state).toBeUndefined(); + }); + + it('should return correct state for existing vat', () => { + vatStateService.saveVatState(mockVatId, mockVatState); + const state = vatStateService.getVatState(mockVatId); + expect(state).toStrictEqual(mockVatState); + }); + }); + + describe('deleteVatState', () => { + it('should delete existing vat state', () => { + vatStateService.saveVatState(mockVatId, mockVatState); + vatStateService.deleteVatState(mockVatId); + const state = vatStateService.getVatState(mockVatId); + expect(state).toBeUndefined(); + }); + + it('should not throw when deleting non-existent vat state', () => { + expect(() => vatStateService.deleteVatState('v999')).not.toThrow(); + }); + }); + + describe('multiple vats', () => { + it('should handle multiple vat states independently', () => { + const vat1State: VatState = { config: { sourceSpec: 'vat1.js' } }; + const vat2State: VatState = { config: { sourceSpec: 'vat2.js' } }; + + vatStateService.saveVatState('v1', vat1State); + vatStateService.saveVatState('v2', vat2State); + + expect(vatStateService.getVatState('v1')).toStrictEqual(vat1State); + expect(vatStateService.getVatState('v2')).toStrictEqual(vat2State); + + vatStateService.deleteVatState('v1'); + expect(vatStateService.getVatState('v1')).toBeUndefined(); + expect(vatStateService.getVatState('v2')).toStrictEqual(vat2State); + }); + }); +}); diff --git a/packages/kernel/src/vat-state-service.ts b/packages/kernel/src/vat-state-service.ts new file mode 100644 index 000000000..ced4d6520 --- /dev/null +++ b/packages/kernel/src/vat-state-service.ts @@ -0,0 +1,25 @@ +import type { VatId, VatConfig } from './types.js'; + +export type VatState = { + config: VatConfig; +}; + +export class VatStateService { + readonly #states: Map; + + constructor() { + this.#states = new Map(); + } + + saveVatState(vatId: VatId, state: VatState): void { + this.#states.set(vatId, state); + } + + getVatState(vatId: VatId): VatState | undefined { + return this.#states.get(vatId); + } + + deleteVatState(vatId: VatId): void { + this.#states.delete(vatId); + } +} From 4c33ea9fd3e3e2af838c81b8392d666e8be7195f Mon Sep 17 00:00:00 2001 From: Dimitris Marlagkoutsos Date: Fri, 22 Nov 2024 15:26:28 +0000 Subject: [PATCH 2/8] update kernel tests --- packages/kernel/src/Kernel.test.ts | 44 ++++++++++++++++++++++++++++-- 1 file changed, 42 insertions(+), 2 deletions(-) diff --git a/packages/kernel/src/Kernel.test.ts b/packages/kernel/src/Kernel.test.ts index 21e7eaf99..a21aee37e 100644 --- a/packages/kernel/src/Kernel.test.ts +++ b/packages/kernel/src/Kernel.test.ts @@ -148,17 +148,57 @@ describe('Kernel', () => { }); describe('restartVat()', () => { - // Disabling this test for now, as vat restart is not currently a thing - it.todo('restarts a vat', async () => { + it('restarts a vat while preserving ID and config', async () => { const kernel = new Kernel(mockStream, mockWorkerService, mockKVStore); await kernel.launchVat(mockVatConfig); expect(kernel.getVatIds()).toStrictEqual(['v1']); await kernel.restartVat('v1'); expect(terminateMock).toHaveBeenCalledOnce(); expect(terminateWorkerMock).toHaveBeenCalledOnce(); + expect(launchWorkerMock).toHaveBeenCalledTimes(2); + expect(launchWorkerMock).toHaveBeenLastCalledWith('v1', mockVatConfig); expect(kernel.getVatIds()).toStrictEqual(['v1']); expect(initMock).toHaveBeenCalledTimes(2); }); + + it('throws error when restarting non-existent vat', async () => { + const kernel = new Kernel(mockStream, mockWorkerService, mockKVStore); + await expect(kernel.restartVat('v999')).rejects.toThrow( + 'No state found for vat v999', + ); + expect(terminateMock).not.toHaveBeenCalled(); + expect(launchWorkerMock).not.toHaveBeenCalled(); + }); + + it('preserves vat state across multiple restarts', async () => { + const kernel = new Kernel(mockStream, mockWorkerService, mockKVStore); + await kernel.launchVat(mockVatConfig); + await kernel.restartVat('v1'); + await kernel.restartVat('v1'); + expect(terminateMock).toHaveBeenCalledTimes(2); + expect(launchWorkerMock).toHaveBeenCalledTimes(3); + expect(launchWorkerMock).toHaveBeenLastCalledWith('v1', mockVatConfig); + expect(kernel.getVatIds()).toStrictEqual(['v1']); + }); + + it('handles restart failure during termination', async () => { + const kernel = new Kernel(mockStream, mockWorkerService, mockKVStore); + await kernel.launchVat(mockVatConfig); + terminateMock.mockRejectedValueOnce(new Error('Termination failed')); + await expect(kernel.restartVat('v1')).rejects.toThrow( + 'Termination failed', + ); + expect(launchWorkerMock).toHaveBeenCalledTimes(1); + }); + + it('handles restart failure during launch', async () => { + const kernel = new Kernel(mockStream, mockWorkerService, mockKVStore); + await kernel.launchVat(mockVatConfig); + launchWorkerMock.mockRejectedValueOnce(new Error('Launch failed')); + await expect(kernel.restartVat('v1')).rejects.toThrow('Launch failed'); + expect(terminateMock).toHaveBeenCalledOnce(); + expect(kernel.getVatIds()).toStrictEqual([]); + }); }); describe('sendMessage()', () => { From 7efa88148081a9b79ab6a62a38fc06c8eee8076d Mon Sep 17 00:00:00 2001 From: Dimitris Marlagkoutsos Date: Fri, 22 Nov 2024 15:29:16 +0000 Subject: [PATCH 3/8] fix new vat name style --- packages/extension/src/panel/styles.css | 2 +- packages/extension/src/popup.html | 6 +----- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/packages/extension/src/panel/styles.css b/packages/extension/src/panel/styles.css index 527105de3..7ff89ce0a 100644 --- a/packages/extension/src/panel/styles.css +++ b/packages/extension/src/panel/styles.css @@ -111,7 +111,7 @@ select { } #new-vat-name { - width: 80px; + width: 95px; } #status-display { diff --git a/packages/extension/src/popup.html b/packages/extension/src/popup.html index d2b72c970..48f8d6fe2 100644 --- a/packages/extension/src/popup.html +++ b/packages/extension/src/popup.html @@ -15,11 +15,7 @@

Kernel Status

- +
From b78f91daf91be4472a7d6fed3336e88dfdaadd5c Mon Sep 17 00:00:00 2001 From: Dimitris Marlagkoutsos Date: Fri, 22 Nov 2024 16:02:14 +0000 Subject: [PATCH 4/8] refactor vat state service --- packages/kernel/src/Kernel.test.ts | 29 +++-- packages/kernel/src/Kernel.ts | 111 ++++++++++++++++++ packages/kernel/src/vat-state-service.test.ts | 96 +++++++-------- packages/kernel/src/vat-state-service.ts | 55 ++++++++- 4 files changed, 226 insertions(+), 65 deletions(-) diff --git a/packages/kernel/src/Kernel.test.ts b/packages/kernel/src/Kernel.test.ts index a21aee37e..6c797ae31 100644 --- a/packages/kernel/src/Kernel.test.ts +++ b/packages/kernel/src/Kernel.test.ts @@ -148,7 +148,19 @@ describe('Kernel', () => { }); describe('restartVat()', () => { - it('restarts a vat while preserving ID and config', async () => { + it('preserves vat state across multiple restarts', async () => { + const kernel = new Kernel(mockStream, mockWorkerService, mockKVStore); + await kernel.launchVat(mockVatConfig); + await kernel.restartVat('v1'); + expect(kernel.getVatIds()).toStrictEqual(['v1']); + await kernel.restartVat('v1'); + expect(kernel.getVatIds()).toStrictEqual(['v1']); + expect(terminateMock).toHaveBeenCalledTimes(2); + expect(launchWorkerMock).toHaveBeenCalledTimes(3); // initial + 2 restarts + expect(launchWorkerMock).toHaveBeenLastCalledWith('v1', mockVatConfig); + }); + + it('restarts a vat', async () => { const kernel = new Kernel(mockStream, mockWorkerService, mockKVStore); await kernel.launchVat(mockVatConfig); expect(kernel.getVatIds()).toStrictEqual(['v1']); @@ -163,24 +175,11 @@ describe('Kernel', () => { it('throws error when restarting non-existent vat', async () => { const kernel = new Kernel(mockStream, mockWorkerService, mockKVStore); - await expect(kernel.restartVat('v999')).rejects.toThrow( - 'No state found for vat v999', - ); + await expect(kernel.restartVat('v999')).rejects.toThrow(VatNotFoundError); expect(terminateMock).not.toHaveBeenCalled(); expect(launchWorkerMock).not.toHaveBeenCalled(); }); - it('preserves vat state across multiple restarts', async () => { - const kernel = new Kernel(mockStream, mockWorkerService, mockKVStore); - await kernel.launchVat(mockVatConfig); - await kernel.restartVat('v1'); - await kernel.restartVat('v1'); - expect(terminateMock).toHaveBeenCalledTimes(2); - expect(launchWorkerMock).toHaveBeenCalledTimes(3); - expect(launchWorkerMock).toHaveBeenLastCalledWith('v1', mockVatConfig); - expect(kernel.getVatIds()).toStrictEqual(['v1']); - }); - it('handles restart failure during termination', async () => { const kernel = new Kernel(mockStream, mockWorkerService, mockKVStore); await kernel.launchVat(mockVatConfig); diff --git a/packages/kernel/src/Kernel.ts b/packages/kernel/src/Kernel.ts index cc5b8b097..5cf71b249 100644 --- a/packages/kernel/src/Kernel.ts +++ b/packages/kernel/src/Kernel.ts @@ -291,6 +291,7 @@ export class Kernel { * @param vatConfig - The configuration of the vat. * @returns A promise that resolves the vat. */ +<<<<<<< HEAD async #initVat(vatId: VatId, vatConfig: VatConfig): Promise { const multiplexer = await this.#vatWorkerService.launch(vatId, vatConfig); multiplexer.start().catch((error) => this.#logger.error(error)); @@ -304,11 +305,121 @@ export class Kernel { this.#vatStateService.saveVatState(vatId, { config: vatConfig, }); +======= + async launchVat(vatConfig: VatConfig): Promise { + const vatId = this.#storage.getNextVatId(); + if (this.#vats.has(vatId)) { + throw new VatAlreadyExistsError(vatId); + } + + const stream = await this.#vatWorkerService.launch(vatId, vatConfig); + const vat = new Vat({ + vatId, + vatConfig, + multiplexer: new StreamMultiplexer(stream), + }); + + this.#vatStateService.set(vatId, { config: vatConfig }); + this.#vats.set(vat.vatId, vat); +>>>>>>> b905815 (refactor vat state service) await vat.init(); return vat; } /** +<<<<<<< HEAD +======= + * Launches a sub-cluster of vats. + * + * @param config - Configuration object for sub-cluster. + * @returns A record of the vats launched. + */ + async launchSubcluster(config: ClusterConfig): Promise> { + const vats: Record = {}; + for (const [vatName, vatConfig] of Object.entries(config.vats)) { + const vat = await this.launchVat(vatConfig); + vats[vatName] = vat; + } + return vats; + } + + /** + * Restarts a vat. + * + * @param vatId - The ID of the vat. + */ + async restartVat(vatId: VatId): Promise { + const state = this.#vatStateService.get(vatId); + if (!state) { + throw new VatNotFoundError(vatId); + } + + await this.terminateVat(vatId, false); + + try { + const stream = await this.#vatWorkerService.launch(vatId, state.config); + const vat = new Vat({ + vatId, + vatConfig: state.config, + multiplexer: new StreamMultiplexer(stream), + }); + + this.#vats.set(vatId, vat); + await vat.init(); + } catch (error) { + this.#vatStateService.delete(vatId); + throw error; + } + } + + /** + * Terminate a vat. + * + * @param vatId - The ID of the vat. + * @param deleteState - Whether to delete the vat state (defaults to true). + */ + async terminateVat(vatId: VatId, deleteState = true): Promise { + const vat = this.#getVat(vatId); + await vat.terminate(); + await this.#vatWorkerService.terminate(vatId).catch(console.error); + this.#vats.delete(vatId); + if (deleteState) { + this.#vatStateService.delete(vatId); + } + } + + /** + * Terminate all vats. + */ + async terminateAllVats(): Promise { + await Promise.all( + this.getVatIds().map(async (vatId) => { + const vat = this.#getVat(vatId); + await vat.terminate(); + this.#vats.delete(vatId); + this.#vatStateService.delete(vatId); + }), + ); + await this.#vatWorkerService.terminateAll(); + } + + /** + * Send a message to a vat. + * + * @param id - The id of the vat to send the message to. + * @param command - The command to send. + * @returns A promise that resolves the response to the message. + */ + async sendMessage( + id: VatId, + command: VatCommand['payload'], + ): Promise { + const vat = this.#getVat(id); + return vat.sendMessage(command); + } + + /** +>>>>>>> b905815 (refactor vat state service) * Gets a vat. * * @param vatId - The ID of the vat. diff --git a/packages/kernel/src/vat-state-service.test.ts b/packages/kernel/src/vat-state-service.test.ts index 5561ae282..4f92458bf 100644 --- a/packages/kernel/src/vat-state-service.test.ts +++ b/packages/kernel/src/vat-state-service.test.ts @@ -2,82 +2,86 @@ import { describe, it, expect, beforeEach } from 'vitest'; import type { VatId, VatConfig } from './types'; import { VatStateService } from './vat-state-service'; -import type { VatState } from './vat-state-service'; describe('VatStateService', () => { let vatStateService: VatStateService; const mockVatId: VatId = 'v1'; const mockVatConfig: VatConfig = { sourceSpec: 'test-vat.js' }; - const mockVatState: VatState = { config: mockVatConfig }; + const mockVatState = { config: mockVatConfig }; beforeEach(() => { vatStateService = new VatStateService(); }); - describe('saveVatState', () => { - it('should save vat state', () => { - vatStateService.saveVatState(mockVatId, mockVatState); - const savedState = vatStateService.getVatState(mockVatId); - expect(savedState).toStrictEqual(mockVatState); + describe('set', () => { + it('should store valid vat state', () => { + vatStateService.set(mockVatId, mockVatState); + expect(vatStateService.get(mockVatId)).toStrictEqual(mockVatState); }); - it('should overwrite existing vat state', () => { - const initialState: VatState = { - config: { sourceSpec: 'initial.js' }, - }; - vatStateService.saveVatState(mockVatId, initialState); - - const updatedState: VatState = { - config: { sourceSpec: 'updated.js' }, - }; - vatStateService.saveVatState(mockVatId, updatedState); - - const savedState = vatStateService.getVatState(mockVatId); - expect(savedState).toStrictEqual(updatedState); - expect(savedState).not.toStrictEqual(initialState); + it('should overwrite existing state', () => { + const newState = { config: { sourceSpec: 'new.js' } }; + vatStateService.set(mockVatId, mockVatState); + vatStateService.set(mockVatId, newState); + expect(vatStateService.get(mockVatId)).toStrictEqual(newState); }); }); - describe('getVatState', () => { + describe('get', () => { it('should return undefined for non-existent vat', () => { - const state = vatStateService.getVatState('v999'); - expect(state).toBeUndefined(); + expect(vatStateService.get('v999')).toBeUndefined(); }); it('should return correct state for existing vat', () => { - vatStateService.saveVatState(mockVatId, mockVatState); - const state = vatStateService.getVatState(mockVatId); - expect(state).toStrictEqual(mockVatState); + vatStateService.set(mockVatId, mockVatState); + expect(vatStateService.get(mockVatId)).toStrictEqual(mockVatState); }); }); - describe('deleteVatState', () => { - it('should delete existing vat state', () => { - vatStateService.saveVatState(mockVatId, mockVatState); - vatStateService.deleteVatState(mockVatId); - const state = vatStateService.getVatState(mockVatId); - expect(state).toBeUndefined(); + describe('delete', () => { + it('should return true when deleting existing state', () => { + vatStateService.set(mockVatId, mockVatState); + expect(vatStateService.delete(mockVatId)).toBe(true); + expect(vatStateService.get(mockVatId)).toBeUndefined(); }); - it('should not throw when deleting non-existent vat state', () => { - expect(() => vatStateService.deleteVatState('v999')).not.toThrow(); + it('should return false when deleting non-existent state', () => { + expect(vatStateService.delete('v999')).toBe(false); }); }); - describe('multiple vats', () => { - it('should handle multiple vat states independently', () => { - const vat1State: VatState = { config: { sourceSpec: 'vat1.js' } }; - const vat2State: VatState = { config: { sourceSpec: 'vat2.js' } }; + describe('has', () => { + it('should return true for existing vat', () => { + vatStateService.set(mockVatId, mockVatState); + expect(vatStateService.has(mockVatId)).toBe(true); + }); - vatStateService.saveVatState('v1', vat1State); - vatStateService.saveVatState('v2', vat2State); + it('should return false for non-existent vat', () => { + expect(vatStateService.has('v999')).toBe(false); + }); + }); - expect(vatStateService.getVatState('v1')).toStrictEqual(vat1State); - expect(vatStateService.getVatState('v2')).toStrictEqual(vat2State); + describe('vatIds', () => { + it('should return all vat IDs', () => { + vatStateService.set('v1', mockVatState); + vatStateService.set('v2', mockVatState); + expect(vatStateService.vatIds).toStrictEqual(['v1', 'v2']); + }); + + it('should return empty array when no states exist', () => { + expect(vatStateService.vatIds).toStrictEqual([]); + }); + }); - vatStateService.deleteVatState('v1'); - expect(vatStateService.getVatState('v1')).toBeUndefined(); - expect(vatStateService.getVatState('v2')).toStrictEqual(vat2State); + describe('size', () => { + it('should return correct number of stored states', () => { + expect(vatStateService.size).toBe(0); + vatStateService.set('v1', mockVatState); + expect(vatStateService.size).toBe(1); + vatStateService.set('v2', mockVatState); + expect(vatStateService.size).toBe(2); + vatStateService.delete('v1'); + expect(vatStateService.size).toBe(1); }); }); }); diff --git a/packages/kernel/src/vat-state-service.ts b/packages/kernel/src/vat-state-service.ts index ced4d6520..7d7a020c0 100644 --- a/packages/kernel/src/vat-state-service.ts +++ b/packages/kernel/src/vat-state-service.ts @@ -11,15 +11,62 @@ export class VatStateService { this.#states = new Map(); } - saveVatState(vatId: VatId, state: VatState): void { + /** + * Set the state for a vat. + * + * @param vatId - The ID of the vat. + * @param state - The state to set. + * @throws {Error} If state is invalid. + */ + set(vatId: VatId, state: VatState): void { this.#states.set(vatId, state); } - getVatState(vatId: VatId): VatState | undefined { + /** + * Get the state of a vat. + * + * @param vatId - The ID of the vat. + * @returns The vat state, or undefined if not found. + */ + get(vatId: VatId): VatState | undefined { return this.#states.get(vatId); } - deleteVatState(vatId: VatId): void { - this.#states.delete(vatId); + /** + * Delete the state of a vat. + * + * @param vatId - The ID of the vat. + * @returns true if state was deleted, false if it didn't exist. + */ + delete(vatId: VatId): boolean { + return this.#states.delete(vatId); + } + + /** + * Check if a vat has state stored. + * + * @param vatId - The ID of the vat. + * @returns true if state exists for the vat. + */ + has(vatId: VatId): boolean { + return this.#states.has(vatId); + } + + /** + * Get all vat IDs with stored state. + * + * @returns Array of vat IDs. + */ + get vatIds(): VatId[] { + return Array.from(this.#states.keys()); + } + + /** + * Get number of vats with stored state. + * + * @returns Number of vats. + */ + get size(): number { + return this.#states.size; } } From f71b3da136e1f8c429ad2366d887a2488b59bb80 Mon Sep 17 00:00:00 2001 From: Dimitris Marlagkoutsos Date: Fri, 22 Nov 2024 17:32:50 +0000 Subject: [PATCH 5/8] fix kernel conflicts --- packages/kernel/src/Kernel.ts | 115 +--------------------------------- 1 file changed, 2 insertions(+), 113 deletions(-) diff --git a/packages/kernel/src/Kernel.ts b/packages/kernel/src/Kernel.ts index 5cf71b249..6307239bd 100644 --- a/packages/kernel/src/Kernel.ts +++ b/packages/kernel/src/Kernel.ts @@ -147,7 +147,7 @@ export class Kernel { * @returns A promise that resolves the restarted vat. */ async restartVat(vatId: VatId): Promise { - const state = this.#vatStateService.getVatState(vatId); + const state = this.#vatStateService.get(vatId); if (!state) { throw new Error(`No state found for vat ${vatId}`); } @@ -291,7 +291,6 @@ export class Kernel { * @param vatConfig - The configuration of the vat. * @returns A promise that resolves the vat. */ -<<<<<<< HEAD async #initVat(vatId: VatId, vatConfig: VatConfig): Promise { const multiplexer = await this.#vatWorkerService.launch(vatId, vatConfig); multiplexer.start().catch((error) => this.#logger.error(error)); @@ -302,124 +301,14 @@ export class Kernel { const capTpStream = multiplexer.createChannel('capTp'); const vat = new Vat({ vatId, vatConfig, commandStream, capTpStream }); this.#vats.set(vat.vatId, vat); - this.#vatStateService.saveVatState(vatId, { + this.#vatStateService.set(vatId, { config: vatConfig, }); -======= - async launchVat(vatConfig: VatConfig): Promise { - const vatId = this.#storage.getNextVatId(); - if (this.#vats.has(vatId)) { - throw new VatAlreadyExistsError(vatId); - } - - const stream = await this.#vatWorkerService.launch(vatId, vatConfig); - const vat = new Vat({ - vatId, - vatConfig, - multiplexer: new StreamMultiplexer(stream), - }); - - this.#vatStateService.set(vatId, { config: vatConfig }); - this.#vats.set(vat.vatId, vat); ->>>>>>> b905815 (refactor vat state service) await vat.init(); return vat; } /** -<<<<<<< HEAD -======= - * Launches a sub-cluster of vats. - * - * @param config - Configuration object for sub-cluster. - * @returns A record of the vats launched. - */ - async launchSubcluster(config: ClusterConfig): Promise> { - const vats: Record = {}; - for (const [vatName, vatConfig] of Object.entries(config.vats)) { - const vat = await this.launchVat(vatConfig); - vats[vatName] = vat; - } - return vats; - } - - /** - * Restarts a vat. - * - * @param vatId - The ID of the vat. - */ - async restartVat(vatId: VatId): Promise { - const state = this.#vatStateService.get(vatId); - if (!state) { - throw new VatNotFoundError(vatId); - } - - await this.terminateVat(vatId, false); - - try { - const stream = await this.#vatWorkerService.launch(vatId, state.config); - const vat = new Vat({ - vatId, - vatConfig: state.config, - multiplexer: new StreamMultiplexer(stream), - }); - - this.#vats.set(vatId, vat); - await vat.init(); - } catch (error) { - this.#vatStateService.delete(vatId); - throw error; - } - } - - /** - * Terminate a vat. - * - * @param vatId - The ID of the vat. - * @param deleteState - Whether to delete the vat state (defaults to true). - */ - async terminateVat(vatId: VatId, deleteState = true): Promise { - const vat = this.#getVat(vatId); - await vat.terminate(); - await this.#vatWorkerService.terminate(vatId).catch(console.error); - this.#vats.delete(vatId); - if (deleteState) { - this.#vatStateService.delete(vatId); - } - } - - /** - * Terminate all vats. - */ - async terminateAllVats(): Promise { - await Promise.all( - this.getVatIds().map(async (vatId) => { - const vat = this.#getVat(vatId); - await vat.terminate(); - this.#vats.delete(vatId); - this.#vatStateService.delete(vatId); - }), - ); - await this.#vatWorkerService.terminateAll(); - } - - /** - * Send a message to a vat. - * - * @param id - The id of the vat to send the message to. - * @param command - The command to send. - * @returns A promise that resolves the response to the message. - */ - async sendMessage( - id: VatId, - command: VatCommand['payload'], - ): Promise { - const vat = this.#getVat(id); - return vat.sendMessage(command); - } - - /** ->>>>>>> b905815 (refactor vat state service) * Gets a vat. * * @param vatId - The ID of the vat. From f5e14732f851f988d5017255aa7317b23109964d Mon Sep 17 00:00:00 2001 From: Dimitris Marlagkoutsos Date: Fri, 22 Nov 2024 17:34:03 +0000 Subject: [PATCH 6/8] fix test --- packages/kernel/src/Kernel.ts | 2 +- vitest.config.ts | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/kernel/src/Kernel.ts b/packages/kernel/src/Kernel.ts index 6307239bd..4d11dfdc5 100644 --- a/packages/kernel/src/Kernel.ts +++ b/packages/kernel/src/Kernel.ts @@ -149,7 +149,7 @@ export class Kernel { async restartVat(vatId: VatId): Promise { const state = this.#vatStateService.get(vatId); if (!state) { - throw new Error(`No state found for vat ${vatId}`); + throw new VatNotFoundError(vatId); } await this.terminateVat(vatId); diff --git a/vitest.config.ts b/vitest.config.ts index a46382c72..8ba396523 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -47,10 +47,10 @@ export default defineConfig({ lines: 57.33, }, 'packages/kernel/**': { - statements: 77.4, - functions: 86.9, - branches: 64.22, - lines: 77.62, + statements: 78.74, + functions: 89.01, + branches: 64.86, + lines: 78.96, }, 'packages/shims/**': { statements: 0, From 01cfdab1ab1f143cd33a28ce879faf6aba85c85d Mon Sep 17 00:00:00 2001 From: Dimitris Marlagkoutsos Date: Fri, 22 Nov 2024 17:35:23 +0000 Subject: [PATCH 7/8] reset popup styles --- packages/extension/src/panel/styles.css | 2 +- packages/extension/src/popup.html | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/extension/src/panel/styles.css b/packages/extension/src/panel/styles.css index 7ff89ce0a..527105de3 100644 --- a/packages/extension/src/panel/styles.css +++ b/packages/extension/src/panel/styles.css @@ -111,7 +111,7 @@ select { } #new-vat-name { - width: 95px; + width: 80px; } #status-display { diff --git a/packages/extension/src/popup.html b/packages/extension/src/popup.html index 48f8d6fe2..d2b72c970 100644 --- a/packages/extension/src/popup.html +++ b/packages/extension/src/popup.html @@ -15,7 +15,11 @@

Kernel Status

- +
From 13f776b856de6293a0229e785ad1beffb2e5d08f Mon Sep 17 00:00:00 2001 From: Dimitris Marlagkoutsos Date: Sat, 23 Nov 2024 22:44:02 +0000 Subject: [PATCH 8/8] rename type --- packages/kernel/src/types.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/kernel/src/types.ts b/packages/kernel/src/types.ts index 5ef5ab470..dadb0826f 100644 --- a/packages/kernel/src/types.ts +++ b/packages/kernel/src/types.ts @@ -47,7 +47,7 @@ type EndpointState = { kRefToERef: Map; }; -type VatKernelState = { +type KernelVatState = { messagePort: typeof MessagePort; state: EndpointState; source: string; @@ -72,7 +72,7 @@ export type KernelPromise = { }; export type KernelState = { - vats: Map; + vats: Map; remotes: Map; kernelPromises: Map; };