From 0f023a467f7566171f2e43cd26fb90cf6ae4519b Mon Sep 17 00:00:00 2001 From: Chip Morningstar Date: Thu, 17 Oct 2024 18:14:29 -0700 Subject: [PATCH 1/5] chore: merge with main branch --- packages/extension/src/kernel-worker.ts | 6 +- ...ite-kernel-store.ts => sqlite-kv-store.ts} | 46 +- packages/kernel/src/Kernel.test.ts | 32 +- packages/kernel/src/Kernel.ts | 10 +- packages/kernel/src/index.ts | 2 +- packages/kernel/src/kernel-store.ts | 392 +++++++++++++++++- .../{extension => kernel}/src/kernel-types.ts | 32 +- packages/kernel/test/storage.ts | 34 +- 8 files changed, 483 insertions(+), 71 deletions(-) rename packages/extension/src/{sqlite-kernel-store.ts => sqlite-kv-store.ts} (62%) rename packages/{extension => kernel}/src/kernel-types.ts (64%) diff --git a/packages/extension/src/kernel-worker.ts b/packages/extension/src/kernel-worker.ts index b57e165bb..96a921f97 100644 --- a/packages/extension/src/kernel-worker.ts +++ b/packages/extension/src/kernel-worker.ts @@ -3,7 +3,7 @@ import type { KernelCommand, KernelCommandReply, VatId } from '@ocap/kernel'; import { Kernel } from '@ocap/kernel'; import { MessagePortDuplexStream, receiveMessagePort } from '@ocap/streams'; -import { makeKernelStore } from './sqlite-kernel-store.js'; +import { makeSQLKVStore } from './sqlite-kv-store.js'; import { ExtensionVatWorkerClient } from './VatWorkerClient.js'; main('v0').catch(console.error); @@ -28,10 +28,10 @@ async function main(defaultVatId: VatId): Promise { // Initialize kernel store. - const kernelStore = await makeKernelStore(); + const kvStore = await makeSQLKVStore(); // Create and start kernel. - const kernel = new Kernel(kernelStream, vatWorkerClient, kernelStore); + const kernel = new Kernel(kernelStream, vatWorkerClient, kvStore); await kernel.init({ defaultVatId }); } diff --git a/packages/extension/src/sqlite-kernel-store.ts b/packages/extension/src/sqlite-kv-store.ts similarity index 62% rename from packages/extension/src/sqlite-kernel-store.ts rename to packages/extension/src/sqlite-kv-store.ts index ddfb69f16..a3315c942 100644 --- a/packages/extension/src/sqlite-kernel-store.ts +++ b/packages/extension/src/sqlite-kv-store.ts @@ -1,4 +1,4 @@ -import type { KernelStore } from '@ocap/kernel'; +import type { KVStore } from '@ocap/kernel'; import { makeLogger } from '@ocap/utils'; import type { Database } from '@sqlite.org/sqlite-wasm'; import sqlite3InitModule from '@sqlite.org/sqlite-wasm'; @@ -18,14 +18,14 @@ async function initDB(): Promise { } /** - * Makes a {@link KernelStore} for persistent storage. + * Makes a {@link KVStore} for low-level persistent storage. * * @param label - A logger prefix label. Defaults to '[sqlite]'. - * @returns The kernel store. + * @returns The key/value store to base the kernel store on. */ -export async function makeKernelStore( +export async function makeSQLKVStore( label: string = '[sqlite]', -): Promise { +): Promise { const logger = makeLogger(label); const db = await initDB(); @@ -44,12 +44,13 @@ export async function makeKernelStore( `); /** - * Exercise reading from the database. + * Read a key's value from the database. * * @param key - A key to fetch. + * @param required - True if it is an error for the entry not to be there. * @returns The value at that key. */ - function kvGet(key: string): string { + function kvGet(key: string, required: boolean): string { sqlKVGet.bind([key]); if (sqlKVGet.step()) { const result = sqlKVGet.getString(0); @@ -60,7 +61,11 @@ export async function makeKernelStore( } } sqlKVGet.reset(); - throw Error(`no record matching key '${key}'`); + if (required) { + throw Error(`no record matching key '${key}'`); + } else { + return undefined as unknown as string; + } } const sqlKVSet = db.prepare(` @@ -70,7 +75,7 @@ export async function makeKernelStore( `); /** - * Exercise writing to the database. + * Set the value associated with a key in the database. * * @param key - A key to assign. * @param value - The value to assign to it. @@ -82,8 +87,27 @@ export async function makeKernelStore( sqlKVSet.reset(); } + const sqlKVDelete = db.prepare(` + DELETE FROM kv + WHERE key = ? + `); + + /** + * Delete a key from the database. + * + * @param key - The key to remove. + */ + function kvDelete(key: string): void { + logger.debug(`kernel delete '${key}'`); + sqlKVDelete.bind([key]); + sqlKVDelete.step(); + sqlKVDelete.reset(); + } + return { - kvGet, - kvSet, + get: (key) => kvGet(key, false), + getRequired: (key) => kvGet(key, true), + set: kvSet, + delete: kvDelete, }; } diff --git a/packages/kernel/src/Kernel.test.ts b/packages/kernel/src/Kernel.test.ts index a0d026749..0ae90b0bc 100644 --- a/packages/kernel/src/Kernel.test.ts +++ b/packages/kernel/src/Kernel.test.ts @@ -5,7 +5,7 @@ import type { MessagePortDuplexStream, DuplexStream } from '@ocap/streams'; import type { MockInstance } from 'vitest'; import { describe, it, expect, vi, beforeEach } from 'vitest'; -import type { KernelStore } from './kernel-store.js'; +import type { KVStore } from './kernel-store.js'; import { Kernel } from './Kernel.js'; import type { KernelCommand, @@ -15,7 +15,7 @@ import type { import type { StreamEnvelope, StreamEnvelopeReply } from './stream-envelope.js'; import type { VatId, VatWorkerService } from './types.js'; import { Vat } from './Vat.js'; -import { makeMapKernelStore } from '../test/storage.js'; +import { makeMapKVStore } from '../test/storage.js'; describe('Kernel', () => { let mockStream: DuplexStream; @@ -25,7 +25,7 @@ describe('Kernel', () => { let initMock: MockInstance; let terminateMock: MockInstance; - let mockKernelStore: KernelStore; + let mockKVStore: KVStore; beforeEach(() => { mockStream = { @@ -56,23 +56,23 @@ describe('Kernel', () => { .spyOn(Vat.prototype, 'terminate') .mockImplementation(vi.fn()); - mockKernelStore = makeMapKernelStore(); + mockKVStore = makeMapKVStore(); }); describe('getVatIds()', () => { it('returns an empty array when no vats are added', () => { - const kernel = new Kernel(mockStream, mockWorkerService, mockKernelStore); + const kernel = new Kernel(mockStream, mockWorkerService, mockKVStore); expect(kernel.getVatIds()).toStrictEqual([]); }); it('returns the vat IDs after adding a vat', async () => { - const kernel = new Kernel(mockStream, mockWorkerService, mockKernelStore); + const kernel = new Kernel(mockStream, mockWorkerService, mockKVStore); await kernel.launchVat({ id: 'v0' }); expect(kernel.getVatIds()).toStrictEqual(['v0']); }); it('returns multiple vat IDs after adding multiple vats', async () => { - const kernel = new Kernel(mockStream, mockWorkerService, mockKernelStore); + const kernel = new Kernel(mockStream, mockWorkerService, mockKVStore); await kernel.launchVat({ id: 'v0' }); await kernel.launchVat({ id: 'v1' }); expect(kernel.getVatIds()).toStrictEqual(['v0', 'v1']); @@ -81,7 +81,7 @@ describe('Kernel', () => { describe('launchVat()', () => { it('adds a vat to the kernel without errors when no vat with the same ID exists', async () => { - const kernel = new Kernel(mockStream, mockWorkerService, mockKernelStore); + const kernel = new Kernel(mockStream, mockWorkerService, mockKVStore); await kernel.launchVat({ id: 'v0' }); expect(initMock).toHaveBeenCalledOnce(); expect(launchWorkerMock).toHaveBeenCalled(); @@ -89,7 +89,7 @@ describe('Kernel', () => { }); it('throws an error when launching a vat that already exists in the kernel', async () => { - const kernel = new Kernel(mockStream, mockWorkerService, mockKernelStore); + const kernel = new Kernel(mockStream, mockWorkerService, mockKVStore); await kernel.launchVat({ id: 'v0' }); expect(kernel.getVatIds()).toStrictEqual(['v0']); await expect( @@ -103,7 +103,7 @@ describe('Kernel', () => { describe('deleteVat()', () => { it('deletes a vat from the kernel without errors when the vat exists', async () => { - const kernel = new Kernel(mockStream, mockWorkerService, mockKernelStore); + const kernel = new Kernel(mockStream, mockWorkerService, mockKVStore); await kernel.launchVat({ id: 'v0' }); expect(kernel.getVatIds()).toStrictEqual(['v0']); await kernel.deleteVat('v0'); @@ -113,7 +113,7 @@ describe('Kernel', () => { }); it('throws an error when deleting a vat that does not exist in the kernel', async () => { - const kernel = new Kernel(mockStream, mockWorkerService, mockKernelStore); + const kernel = new Kernel(mockStream, mockWorkerService, mockKVStore); const nonExistentVatId: VatId = 'v9'; await expect(async () => kernel.deleteVat(nonExistentVatId), @@ -122,7 +122,7 @@ describe('Kernel', () => { }); it('throws an error when a vat terminate method throws', async () => { - const kernel = new Kernel(mockStream, mockWorkerService, mockKernelStore); + const kernel = new Kernel(mockStream, mockWorkerService, mockKVStore); await kernel.launchVat({ id: 'v0' }); vi.spyOn(Vat.prototype, 'terminate').mockRejectedValueOnce('Test error'); await expect(async () => kernel.deleteVat('v0')).rejects.toThrow( @@ -133,7 +133,7 @@ describe('Kernel', () => { describe('sendMessage()', () => { it('sends a message to the vat without errors when the vat exists', async () => { - const kernel = new Kernel(mockStream, mockWorkerService, mockKernelStore); + const kernel = new Kernel(mockStream, mockWorkerService, mockKVStore); await kernel.launchVat({ id: 'v0' }); vi.spyOn(Vat.prototype, 'sendMessage').mockResolvedValueOnce('test'); expect( @@ -145,7 +145,7 @@ describe('Kernel', () => { }); it('throws an error when sending a message to the vat that does not exist in the kernel', async () => { - const kernel = new Kernel(mockStream, mockWorkerService, mockKernelStore); + const kernel = new Kernel(mockStream, mockWorkerService, mockKVStore); const nonExistentVatId: VatId = 'v9'; await expect(async () => kernel.sendMessage(nonExistentVatId, {} as VatCommand['payload']), @@ -153,7 +153,7 @@ describe('Kernel', () => { }); it('throws an error when sending a message to the vat throws', async () => { - const kernel = new Kernel(mockStream, mockWorkerService, mockKernelStore); + const kernel = new Kernel(mockStream, mockWorkerService, mockKVStore); await kernel.launchVat({ id: 'v0' }); vi.spyOn(Vat.prototype, 'sendMessage').mockRejectedValueOnce('error'); await expect(async () => @@ -165,7 +165,7 @@ describe('Kernel', () => { describe('constructor()', () => { it('initializes the kernel without errors', () => { expect( - async () => new Kernel(mockStream, mockWorkerService, mockKernelStore), + async () => new Kernel(mockStream, mockWorkerService, mockKVStore), ).not.toThrow(); }); }); diff --git a/packages/kernel/src/Kernel.ts b/packages/kernel/src/Kernel.ts index 6e778b7de..5245294d2 100644 --- a/packages/kernel/src/Kernel.ts +++ b/packages/kernel/src/Kernel.ts @@ -6,7 +6,7 @@ import type { DuplexStream } from '@ocap/streams'; import type { Logger } from '@ocap/utils'; import { makeLogger, stringify } from '@ocap/utils'; -import type { KernelStore } from './kernel-store.js'; +import type { KVStore } from './kernel-store.js'; import { isKernelCommand, KernelCommandMethod, @@ -27,7 +27,7 @@ export class Kernel { readonly #vatWorkerService: VatWorkerService; - readonly #storage: KernelStore; + readonly #storage: KVStore; // Hopefully removed when we get to n+1 vats. readonly #defaultVatKit: PromiseKit; @@ -37,7 +37,7 @@ export class Kernel { constructor( stream: DuplexStream, vatWorkerService: VatWorkerService, - storage: KernelStore, + storage: KVStore, logger?: Logger, ) { this.#stream = stream; @@ -145,11 +145,11 @@ export class Kernel { } kvGet(key: string): string { - return this.#storage.kvGet(key); + return this.#storage.get(key); } kvSet(key: string, value: string): void { - this.#storage.kvSet(key, value); + this.#storage.set(key, value); } /** diff --git a/packages/kernel/src/index.ts b/packages/kernel/src/index.ts index 354ac72ce..e6fc58cb9 100644 --- a/packages/kernel/src/index.ts +++ b/packages/kernel/src/index.ts @@ -1,6 +1,6 @@ export * from './messages/index.js'; export { Kernel } from './Kernel.js'; -export type { KernelStore } from './kernel-store.js'; +export type { KVStore } from './kernel-store.js'; export { Vat } from './Vat.js'; export { Supervisor } from './Supervisor.js'; export type { StreamEnvelope, StreamEnvelopeReply } from './stream-envelope.js'; diff --git a/packages/kernel/src/kernel-store.ts b/packages/kernel/src/kernel-store.ts index af4da6b64..f228cc062 100644 --- a/packages/kernel/src/kernel-store.ts +++ b/packages/kernel/src/kernel-store.ts @@ -1,4 +1,390 @@ -export type KernelStore = { - kvGet: (key: string) => string; - kvSet: (key: string, value: string) => void; +import type { + VatId, + RemoteId, + EndpointId, + KRef, + ERef, + Message, + KernelObject, + KernelPromise, +} from './kernel-types.js'; + +type StoredValue = { + get(): string | undefined; + set(newValue: string): void; + delete(): void; }; + +type StoredMessageQueue = { + enqueue(message: Message): void; + dequeue(): Message | undefined; + delete(): void; +}; + +export type KVStore = { + get(key: string): string; + getRequired(key: string): string; + set(key: string, value: string): void; + delete(key: string): void; +}; + +/** + * Create a new KernelStore object wrapped around a simple string-to-string + * key/value store. The resulting object provides a variety of operations for + * accessing various kernel-relevent persistent data structure abstractions on + * their own terms, without burdening the kernel with the particular details of + * how they are stored. It is our hope that these operations may be later + * reimplemented on top of a more sophisticated storage layer that can realize + * them more directly (and thus, one hopes, more efficiently) without requiring + * the kernel itself to be any the wiser. + * + * @param kv - A key/value store to provide the underlying persistence mechanism. + * @returns A KernelStore object that maps various persistent kernel data + * structures onto `kv`. + */ +// eslint-disable-next-line @typescript-eslint/explicit-function-return-type +export function makeKernelStore(kv: KVStore) { + /** + * Provide a stored value object for which we keep an in-memory cache. We only + * touch persistent storage if the value hasn't ever been read of if it is + * modified; otherwise we can service read requests from memory. + * + * @param key - A key string that identifies the value. + * @param init - A initial setting if the indicated value is not present. + * @returns An object for interacting with the value. + */ + function makeCachedStoredValue(key: string, init: string): StoredValue { + let value: string | undefined; + if (kv.get(key) === undefined && init !== undefined) { + kv.set(key, init); + value = init; + } + return harden({ + get(): string | undefined { + return value; + }, + set(newValue: string): void { + value = newValue; + kv.set(key, value); + }, + delete(): void { + value = undefined; + kv.delete(key); + }, + }); + } + + /** + * Provide a stored value object that is backed soley by persistent storage. + * + * @param key - A key string that identifies the value. + * @param init - A initial setting if the indicated value is not present. + * @returns An object for interacting with the value. + */ + function makeRawStoredValue(key: string, init: string): StoredValue { + if (kv.get(key) === undefined) { + kv.set(key, init); + } + return harden({ + get: () => kv.get(key), + set: (newValue: string) => kv.set(key, newValue), + delete: () => kv.delete(key), + }); + } + + /** + * Increment the value of a persistently stored counter. + * + * Note that the while the value is interpreted as an integer (in order to + * enable it to be incremented), it is stored and returned in the form of a + * string. This is because (a) our persistent storage only stores strings, and + * (b) the sole purpose of one of these counters is simply to provide an + * unending sequence of unique values; we don't actually use them as numbers + * or, indeed, even care at all if this sequence is produced using numbers. + * + * @param value - Reference to the stored value to be incremented. + * @returns The value as it was prior to being incremented. + */ + function incCounter(value: StoredValue): string { + const current = value.get(); + const next = `{Number(current) + 1}`; + value.set(next); + return current as string; + } + + /** + * Create a new persistently stored message queue. + * + * @param queueName - The name for the new queue (must be unique among queues). + * @param cached - Optional flag: set to true if the queue should cache its + * limit indices in memory (only do this if the queue is going to be accessed or + * checked frequently). + * @returns An object for interacting with the queue. + */ + function makeStoredMessageQueue( + queueName: string, + cached: boolean = false, + ): StoredMessageQueue { + const qk = `queue.${queueName}`; + // Note: cached==true ==> caches only the head & tail indices, NOT the messages themselves + const makeValue = cached ? makeCachedStoredValue : makeRawStoredValue; + const head = makeValue(`${qk}.head`, '1'); + const tail = makeValue(`${qk}.tail`, '1'); + return { + enqueue(message: Message): void { + const entryPos = incCounter(head); + kv.set(`${qk}.${entryPos}`, JSON.stringify(message)); + }, + dequeue(): Message | undefined { + const headPos = head.get(); + const tailPos = tail.get(); + if (tailPos !== headPos) { + const entry = kv.get(`{qk}.${tailPos}`); + kv.delete(`${qk}.${tailPos}`); + incCounter(tail); + return JSON.parse(entry) as Message; + } + return undefined; + }, + delete(): void { + const headPos = head.get(); + let tailPos = tail.get(); + while (tailPos !== headPos) { + kv.delete(`${qk}.${tailPos}`); + tailPos = `${Number(tailPos) + 1}`; + } + head.delete(); + tail.delete(); + }, + }; + } + + /** The kernel's run queue. */ + const runQueue = makeStoredMessageQueue('run', true); + + /** + * Append a message to the kernel's run queue. + * + * @param message - The message to enqueue. + */ + function enqueueRun(message: Message): void { + runQueue.enqueue(message); + } + + /** + * Fetch the next message on the kernel's run queue. + * + * @returns The next message on the run queue, or undefined if the queue is + * empty. + */ + function dequeueRun(): Message | undefined { + return runQueue.dequeue(); + } + + /** Counter for allocating VatIDs */ + const nextVatId = makeCachedStoredValue('nextVatId', '1'); + /** + * Obtain an ID for a new vat. + * + * @returns The next VatID use. + */ + function getNextVatId(): VatId { + return `v${incCounter(nextVatId)}`; + } + + /** Counter for allocating RemoteIDs */ + const nextRemoteId = makeCachedStoredValue('nextRemoteId', '1'); + /** + * Obtain an ID for a new remote connection. + * + * @returns The next remote ID use. + */ + function getNextRemoteId(): RemoteId { + return `r${incCounter(nextRemoteId)}`; + } + + /** Counter for allocating kernel object IDs */ + const nextObjectId = makeCachedStoredValue('nextObjectId', '1'); + /** + * Obtain a KRef for the next unallocated kernel object. + * + * @returns The next koid use. + */ + function getNextObjectId(): KRef { + return `ko${incCounter(nextObjectId)}`; + } + + /** + * Create a new kernel object. The new object will be born with reference and + * recognizability counts of 1, on the assumption that the new object + * corresponds to an object that has just been imported from somewhere. + * + * @param owner - The endpoint that is the owner of the new object. + * @returns A tuple of the new object's KRef and an object describing the new + * kernel object itself. + */ + function initKernelObject(owner: EndpointId): [KRef, KernelObject] { + const kobj = { owner, reachableCount: 1, recognizableCount: 1 }; + const koid = getNextObjectId(); + kv.set(koid, JSON.stringify(kobj)); + return [koid, kobj]; + } + + /** + * Fetch the descriptive record for a kernel object. + * + * @param koid - The KRef of the kernel object of interest. + * @returns An object describing the requested kernel object. + */ + function getKernelObject(koid: KRef): KernelObject { + const raw = kv.get(koid); + if (raw === undefined) { + throw Error(`unknown kernel object ${koid}`); + } + return JSON.parse(raw) as KernelObject; + } + + /** + * Expunge a kernel object from the kernel's persistent state. + * + * @param koid - The KRef of the kernel object to delete. + */ + function deleteKernelObject(koid: KRef): void { + kv.delete(koid); + } + + /** Counter for allocating kernel promise IDs */ + const nextPromiseId = makeCachedStoredValue('nextPromiseId', '1'); + /** + * Obtain a KRef for the next unallocated kernel promise. + * + * @returns The next kpid use. + */ + function getNextPromiseId(): KRef { + return `kp${incCounter(nextPromiseId)}`; + } + + /** + * Create a new, unresolved kernel promise. The new promise will be born with + * a reference count of 1 on the assumption that the promise has just been + * imported from somewhere. + * + * @param decider - The endpoint that is the decider for the new promise. + * @returns A tuple of the new promise's KRef and a object describing the + * new promise itself. + */ + function initKernelPromise(decider: EndpointId): [KRef, KernelPromise] { + const kpr: KernelPromise = { + decider, + state: 'unresolved', + referenceCount: 1, + value: undefined, + }; + const kpid = getNextPromiseId(); + makeStoredMessageQueue(`${kpid}.q`); + kv.set(kpid, JSON.stringify(kpr)); + return [kpid, kpr]; + } + + /** + * Fetch the descriptive record for a kernel promise. + * + * @param kpid - The KRef of the kernel promise of interest. + * @returns An object describing the requested kernel promise. + */ + function getKernelPromise(kpid: KRef): KernelPromise { + const raw = kv.get(kpid); + if (raw === undefined) { + throw Error(`unknown kernel promise ${kpid}`); + } + return JSON.parse(raw) as KernelPromise; + } + + /** + * Fetch the messages in a kernel promise's message queue. + * + * @param kpid - The KRef of the kernel promise of interest. + * @returns An array of all the messages in the given promise's message queue. + */ + function getKernelPromiseMessageQueue(kpid: KRef): Message[] { + const result: Message[] = []; + const queue = makeStoredMessageQueue(`${kpid}.q`); + for (;;) { + const message = queue.dequeue(); + if (message) { + result.push(message); + } else { + return result; + } + } + } + + /** + * Expunge a kernel promise from the kernel's persistent state. + * + * @param kpid - The KRef of the kernel promise to delete. + */ + function deleteKernelPromise(kpid: KRef): void { + kv.delete(kpid); + const queue = makeStoredMessageQueue(`${kpid}.q`); + queue.delete(); + } + + /** + * Look up the ERef that and endpoint's c-list maps a KRef to. + * + * @param endpointId - The endpoint in question. + * @param eref - The ERef to look up. + * @returns The KRef corresponding to `eref` in the given endpoints c-list, or undefined + * if there is no such mapping. + */ + function erefToKref(endpointId: EndpointId, eref: ERef): KRef | undefined { + return kv.get(`cle.${endpointId}.${eref}`) as KRef; + } + + /** + * Look up the KRef that and endpoint's c-list maps an ERef to. + * + * @param endpointId - The endpoint in question. + * @param kref - The KRef to look up. + * @returns The given endpoint's ERef corresponding to `kref`, or undefined if + * there is no such mapping. + */ + function krefToEref(endpointId: EndpointId, kref: KRef): ERef | undefined { + return kv.get(`clk.${endpointId}.${kref}`) as ERef; + } + + /** + * Add an entry to a endpoints c-list, creating a new bidirectional mapping + * between an ERef belonging to the endpoint and a KRef belonging to the + * kernel. + * + * @param endpointId - The endpoint whose c-list is to be updated. + * @param kref - The KRef. + * @param eref - The ERef. + */ + function addClistEntry(endpointId: EndpointId, kref: KRef, eref: ERef): void { + kv.set(`clk.${endpointId}.${kref}`, eref); + kv.set(`cle.${endpointId}.${eref}`, kref); + } + + return harden({ + enqueueRun, + dequeueRun, + getNextVatId, + getNextRemoteId, + initKernelObject, + getKernelObject, + deleteKernelObject, + initKernelPromise, + getKernelPromise, + getKernelPromiseMessageQueue, + deleteKernelPromise, + erefToKref, + krefToEref, + addClistEntry, + kv, + }); +} + +export type KernelStore = ReturnType; diff --git a/packages/extension/src/kernel-types.ts b/packages/kernel/src/kernel-types.ts similarity index 64% rename from packages/extension/src/kernel-types.ts rename to packages/kernel/src/kernel-types.ts index aed415d68..e58161f45 100644 --- a/packages/extension/src/kernel-types.ts +++ b/packages/kernel/src/kernel-types.ts @@ -1,28 +1,27 @@ /** * A structured representation of an ocap kernel. */ -type Queue = Type[]; -type VatId = `v${number}`; -type RemoteId = `r${number}`; -type EndpointId = VatId | RemoteId; +export type VatId = `v${string}`; +export type RemoteId = `r${string}`; +export type EndpointId = VatId | RemoteId; type RefTypeTag = 'o' | 'p'; type RefDirectionTag = '+' | '-'; -type InnerKRef = `${RefTypeTag}${number}`; -type InnerERef = `${RefTypeTag}${RefDirectionTag}${number}`; +type InnerKRef = `${RefTypeTag}${string}`; +type InnerERef = `${RefTypeTag}${RefDirectionTag}${string}`; -type KRef = `k${InnerKRef}`; -type VRef = `v${InnerERef}`; -type RRef = `r${InnerERef}`; -type ERef = VRef | RRef; +export type KRef = `k${InnerKRef}`; +export type VRef = `v${InnerERef}`; +export type RRef = `r${InnerERef}`; +export type ERef = VRef | RRef; type CapData = { body: string; slots: string[]; }; -type Message = { +export type Message = { target: ERef | KRef; method: string; params: CapData; @@ -52,7 +51,7 @@ type RemoteState = { }; // Kernel persistent state -type KernelObject = { +export type KernelObject = { owner: EndpointId; reachableCount: number; recognizableCount: number; @@ -60,23 +59,16 @@ type KernelObject = { type PromiseState = 'unresolved' | 'fulfilled' | 'rejected'; -type KernelPromise = { +export type KernelPromise = { decider: EndpointId; state: PromiseState; referenceCount: number; - messageQueue: Queue; value: undefined | CapData; }; -// export temporarily to shut up lint whinges about unusedness export type KernelState = { - runQueue: Queue; - nextVatIdCounter: number; vats: Map; - nextRemoteIdCounter: number; remotes: Map; - nextKernelObjectIdCounter: number; kernelObjects: Map; - nextKernePromiseIdCounter: number; kernelPromises: Map; }; diff --git a/packages/kernel/test/storage.ts b/packages/kernel/test/storage.ts index 46c38e5da..3feb08d25 100644 --- a/packages/kernel/test/storage.ts +++ b/packages/kernel/test/storage.ts @@ -1,20 +1,30 @@ -import type { KernelStore } from '../src/kernel-store.js'; +import type { KVStore } from '../src/kernel-store.js'; /** - * A mock kernel store realized as a Map. + * A mock key/value store realized as a Map. * - * @returns The mock {@link KernelStore}. + * @returns The mock {@link KVStore}. */ -export function makeMapKernelStore(): KernelStore { +export function makeMapKVStore(): KVStore { const map = new Map(); + + /** + * Like `get`, but fail if the key isn't there. + * + * @param key - The key to fetch. + * @returns The value at `key`. + */ + function getRequired(key: string): string { + if (!map.has(key)) { + throw Error(`No value found for key ${key}.`); + } + return map.get(key); + } + return { - kvGet: (key) => { - const value = map.get(key); - if (value === undefined) { - throw new Error(`No value found for key ${key}.`); - } - return value; - }, - kvSet: map.set.bind(map), + get: map.get.bind(map), + getRequired, + set: map.set.bind(map), + delete: map.set.delete(map), }; } From 214d8cae961b8359d74f22fe443b469cbc423efc Mon Sep 17 00:00:00 2001 From: Chip Morningstar Date: Mon, 21 Oct 2024 11:58:26 -0700 Subject: [PATCH 2/5] feat: Implement kernel storage abstraction layer Closes #118 --- packages/extension/src/sqlite-kv-store.ts | 1 + packages/kernel/src/kernel-store.test.ts | 178 ++++++++++++++++++ packages/kernel/src/kernel-store.ts | 208 ++++++++++++++++++---- packages/kernel/src/kernel-types.ts | 3 - packages/kernel/test/storage.ts | 2 +- 5 files changed, 352 insertions(+), 40 deletions(-) create mode 100644 packages/kernel/src/kernel-store.test.ts diff --git a/packages/extension/src/sqlite-kv-store.ts b/packages/extension/src/sqlite-kv-store.ts index a3315c942..d4688ba0d 100644 --- a/packages/extension/src/sqlite-kv-store.ts +++ b/packages/extension/src/sqlite-kv-store.ts @@ -64,6 +64,7 @@ export async function makeSQLKVStore( if (required) { throw Error(`no record matching key '${key}'`); } else { + // Sometimes, we really lean on TypeScript's unsoundness return undefined as unknown as string; } } diff --git a/packages/kernel/src/kernel-store.test.ts b/packages/kernel/src/kernel-store.test.ts new file mode 100644 index 000000000..077f1974f --- /dev/null +++ b/packages/kernel/src/kernel-store.test.ts @@ -0,0 +1,178 @@ +import '@ocap/shims/endoify'; + +import { describe, it, expect, beforeEach } from 'vitest'; + +import { makeKernelStore } from './kernel-store.js'; +import { makeMapKVStore } from '../test/storage.js'; + +describe('kernel store', () => { + let mockKVStore: KVStore; + + beforeEach(() => { + mockKVStore = makeMapKVStore(); + }); + + describe('initialization', () => { + it('has a working KV store', () => { + const ks = makeKernelStore(mockKVStore); + const { kv } = ks; + expect(kv.get('foo')).toBeUndefined(); + kv.set('foo', 'some value'); + expect(kv.get('foo')).toBe('some value'); + kv.delete('foo'); + expect(kv.get('foo')).toBeUndefined(); + expect(() => kv.getRequired('foo')).toThrow( + 'No value found for key foo.', + ); + }); + it('has all the expected parts', () => { + const ks = makeKernelStore(mockKVStore); + expect(Object.keys(ks).sort()).toStrictEqual([ + 'addClistEntry', + 'decRefCt', + 'deleteKernelObject', + 'deleteKernelPromise', + 'dequeueRun', + 'enqueuePromiseMessage', + 'enqueueRun', + 'erefToKref', + 'forgetEref', + 'forgetKref', + 'getKernelObject', + 'getKernelPromise', + 'getKernelPromiseMessageQueue', + 'getNextRemoteId', + 'getNextVatId', + 'getRefCt', + 'incRefCt', + 'initKernelObject', + 'initKernelPromise', + 'krefToEref', + 'kv', + ]); + }); + }); + + describe('kernel entity management', () => { + it('generates IDs', () => { + const ks = makeKernelStore(mockKVStore); + expect(ks.getNextVatId()).toBe('v1'); + expect(ks.getNextVatId()).toBe('v2'); + expect(ks.getNextVatId()).toBe('v3'); + expect(ks.getNextRemoteId()).toBe('r1'); + expect(ks.getNextRemoteId()).toBe('r2'); + expect(ks.getNextRemoteId()).toBe('r3'); + }); + it('manages kernel objects', () => { + const ks = makeKernelStore(mockKVStore); + const ko1 = { + owner: 'v47', + }; + const ko2 = { + owner: 'r23', + }; + expect(ks.initKernelObject('v47')).toStrictEqual(['ko1', ko1]); + expect(ks.getRefCt('ko1')).toBe(1); + expect(ks.incRefCt('ko1')).toBe(2); + ks.incRefCt('ko1'); + expect(ks.getRefCt('ko1')).toBe(3); + expect(ks.decRefCt('ko1')).toBe(2); + ks.decRefCt('ko1'); + ks.decRefCt('ko1'); + expect(ks.getRefCt('ko1')).toBe(0); + expect(ks.initKernelObject('r23')).toStrictEqual(['ko2', ko2]); + expect(ks.getKernelObject('ko1')).toStrictEqual(ko1); + expect(ks.getKernelObject('ko2')).toStrictEqual(ko2); + ks.deleteKernelObject('ko1'); + expect(() => ks.getKernelObject('ko1')).toThrow( + 'unknown kernel object ko1', + ); + expect(() => ks.getKernelObject('ko99')).toThrow( + 'unknown kernel object ko99', + ); + }); + it('manages kernel promises', () => { + const ks = makeKernelStore(mockKVStore); + const kp1 = { + decider: 'v23', + state: 'unresolved', + value: undefined, + }; + const kp2 = { + decider: 'r47', + state: 'unresolved', + value: undefined, + }; + expect(ks.initKernelPromise('v23')).toStrictEqual(['kp1', kp1]); + expect(ks.getRefCt('kp1')).toBe(1); + expect(ks.incRefCt('kp1')).toBe(2); + ks.incRefCt('kp1'); + expect(ks.getRefCt('kp1')).toBe(3); + expect(ks.decRefCt('kp1')).toBe(2); + ks.decRefCt('kp1'); + ks.decRefCt('kp1'); + expect(ks.getRefCt('kp1')).toBe(0); + expect(ks.initKernelPromise('r47')).toStrictEqual(['kp2', kp2]); + // eslint-disable-next-line vitest/prefer-strict-equal + expect(ks.getKernelPromise('kp1')).toEqual(kp1); + // eslint-disable-next-line vitest/prefer-strict-equal + expect(ks.getKernelPromise('kp2')).toEqual(kp2); + ks.enqueuePromiseMessage('kp1', 'first message to kp1'); + ks.enqueuePromiseMessage('kp1', 'second message to kp1'); + expect(ks.getKernelPromiseMessageQueue('kp1')).toStrictEqual([ + 'first message to kp1', + 'second message to kp1', + ]); + expect(ks.getKernelPromiseMessageQueue('kp1')).toStrictEqual([]); + ks.enqueuePromiseMessage('kp1', 'sacrificial message'); + ks.deleteKernelPromise('kp1'); + expect(() => ks.getKernelPromise('kp1')).toThrow( + 'unknown kernel promise kp1', + ); + expect(() => ks.enqueuePromiseMessage('kp1', 'not really')).toThrow( + 'enqueue into deleted queue kp1', + ); + expect(ks.getKernelPromiseMessageQueue('kp1')).toStrictEqual([]); + expect(() => ks.getKernelPromise('kp99')).toThrow( + 'unknown kernel promise kp99', + ); + }); + it('manages the run queue', () => { + const ks = makeKernelStore(mockKVStore); + ks.enqueueRun('first message' as Message); + ks.enqueueRun('second message'); + expect(ks.dequeueRun()).toBe('first message'); + ks.enqueueRun('third message'); + expect(ks.dequeueRun()).toBe('second message'); + expect(ks.dequeueRun()).toBe('third message'); + expect(ks.dequeueRun()).toBeUndefined(); + ks.enqueueRun('fourth message'); + expect(ks.dequeueRun()).toBe('fourth message'); + expect(ks.dequeueRun()).toBeUndefined(); + }); + it('manages clists', () => { + const ks = makeKernelStore(mockKVStore); + ks.addClistEntry('v2', 'ko42', 'vo-63'); + ks.addClistEntry('v2', 'ko51', 'vo-74'); + ks.addClistEntry('v2', 'kp60', 'vp+85'); + ks.addClistEntry('r7', 'ko42', 'ro+11'); + ks.addClistEntry('r7', 'kp61', 'rp-99'); + expect(ks.krefToEref('v2', 'ko42')).toBe('vo-63'); + expect(ks.erefToKref('v2', 'vo-63')).toBe('ko42'); + expect(ks.krefToEref('v2', 'ko51')).toBe('vo-74'); + expect(ks.erefToKref('v2', 'vo-74')).toBe('ko51'); + expect(ks.krefToEref('v2', 'kp60')).toBe('vp+85'); + expect(ks.erefToKref('v2', 'vp+85')).toBe('kp60'); + expect(ks.krefToEref('r7', 'ko42')).toBe('ro+11'); + expect(ks.erefToKref('r7', 'ro+11')).toBe('ko42'); + expect(ks.krefToEref('r7', 'kp61')).toBe('rp-99'); + expect(ks.erefToKref('r7', 'rp-99')).toBe('kp61'); + ks.forgetKref('v2', 'ko42'); + expect(ks.krefToEref('v2', 'ko42')).toBeUndefined(); + expect(ks.erefToKref('v2', 'vo-63')).toBeUndefined(); + ks.forgetEref('r7', 'rp-99'); + expect(ks.krefToEref('r7', 'kp61')).toBeUndefined(); + expect(ks.erefToKref('r7', 'rp-99')).toBeUndefined(); + }); + }); +}); diff --git a/packages/kernel/src/kernel-store.ts b/packages/kernel/src/kernel-store.ts index f228cc062..ee87e4e6e 100644 --- a/packages/kernel/src/kernel-store.ts +++ b/packages/kernel/src/kernel-store.ts @@ -49,13 +49,16 @@ export function makeKernelStore(kv: KVStore) { * touch persistent storage if the value hasn't ever been read of if it is * modified; otherwise we can service read requests from memory. * + * IMPORTANT NOTE: in order for the cache to work, all subsequent accesses to + * the value MUST be made through a common stored value object. + * * @param key - A key string that identifies the value. - * @param init - A initial setting if the indicated value is not present. + * @param init - If provided, an initial setting if the stored entity does not exist. * @returns An object for interacting with the value. */ - function makeCachedStoredValue(key: string, init: string): StoredValue { - let value: string | undefined; - if (kv.get(key) === undefined && init !== undefined) { + function provideCachedStoredValue(key: string, init?: string): StoredValue { + let value: string | undefined = kv.get(key); + if (value === undefined && init !== undefined) { kv.set(key, init); value = init; } @@ -75,14 +78,14 @@ export function makeKernelStore(kv: KVStore) { } /** - * Provide a stored value object that is backed soley by persistent storage. + * Provide a stored value object that is kept in persistent storage without caching. * * @param key - A key string that identifies the value. - * @param init - A initial setting if the indicated value is not present. + * @param init - If provided, an initial setting if the stored entity does not exist. * @returns An object for interacting with the value. */ - function makeRawStoredValue(key: string, init: string): StoredValue { - if (kv.get(key) === undefined) { + function provideRawStoredValue(key: string, init?: string): StoredValue { + if (kv.get(key) === undefined && init !== undefined) { kv.set(key, init); } return harden({ @@ -107,39 +110,64 @@ export function makeKernelStore(kv: KVStore) { */ function incCounter(value: StoredValue): string { const current = value.get(); - const next = `{Number(current) + 1}`; + const next = `${Number(current) + 1}`; value.set(next); return current as string; } /** - * Create a new persistently stored message queue. + * Create a new (empty) persistently stored message queue. + * + * @param queueName - The name for the queue (must be unique among queues). + * @param cached - Optional flag: set to true if the queue should cache its + * @returns An object for interacting with the new queue. + */ + function createStoredMessageQueue( + queueName: string, + cached: boolean = false, + ): StoredMessageQueue { + const qk = `queue.${queueName}`; + kv.set(`${qk}.head`, '1'); + kv.set(`${qk}.tail`, '1'); + return provideStoredMessageQueue(queueName, cached); + } + + /** + * Produce an object to access a persistently stored message queue. * - * @param queueName - The name for the new queue (must be unique among queues). + * @param queueName - The name for the queue (must be unique among queues). * @param cached - Optional flag: set to true if the queue should cache its * limit indices in memory (only do this if the queue is going to be accessed or * checked frequently). * @returns An object for interacting with the queue. */ - function makeStoredMessageQueue( + function provideStoredMessageQueue( queueName: string, cached: boolean = false, ): StoredMessageQueue { const qk = `queue.${queueName}`; - // Note: cached==true ==> caches only the head & tail indices, NOT the messages themselves - const makeValue = cached ? makeCachedStoredValue : makeRawStoredValue; - const head = makeValue(`${qk}.head`, '1'); - const tail = makeValue(`${qk}.tail`, '1'); + // Note: cached=true ==> caches only the head & tail indices, NOT the messages themselves + const provideValue = cached + ? provideCachedStoredValue + : provideRawStoredValue; + const head = provideValue(`${qk}.head`); + const tail = provideValue(`${qk}.tail`); return { enqueue(message: Message): void { + if (head.get() === undefined) { + throw Error(`enqueue into deleted queue ${queueName}`); + } const entryPos = incCounter(head); kv.set(`${qk}.${entryPos}`, JSON.stringify(message)); }, dequeue(): Message | undefined { const headPos = head.get(); + if (headPos === undefined) { + return undefined; + } const tailPos = tail.get(); if (tailPos !== headPos) { - const entry = kv.get(`{qk}.${tailPos}`); + const entry = kv.get(`${qk}.${tailPos}`); kv.delete(`${qk}.${tailPos}`); incCounter(tail); return JSON.parse(entry) as Message; @@ -148,19 +176,21 @@ export function makeKernelStore(kv: KVStore) { }, delete(): void { const headPos = head.get(); - let tailPos = tail.get(); - while (tailPos !== headPos) { - kv.delete(`${qk}.${tailPos}`); - tailPos = `${Number(tailPos) + 1}`; + if (headPos !== undefined) { + let tailPos = tail.get(); + while (tailPos !== headPos) { + kv.delete(`${qk}.${tailPos}`); + tailPos = `${Number(tailPos) + 1}`; + } + head.delete(); + tail.delete(); } - head.delete(); - tail.delete(); }, }; } /** The kernel's run queue. */ - const runQueue = makeStoredMessageQueue('run', true); + const runQueue = createStoredMessageQueue('run', true); /** * Append a message to the kernel's run queue. @@ -182,7 +212,7 @@ export function makeKernelStore(kv: KVStore) { } /** Counter for allocating VatIDs */ - const nextVatId = makeCachedStoredValue('nextVatId', '1'); + const nextVatId = provideCachedStoredValue('nextVatId', '1'); /** * Obtain an ID for a new vat. * @@ -193,7 +223,7 @@ export function makeKernelStore(kv: KVStore) { } /** Counter for allocating RemoteIDs */ - const nextRemoteId = makeCachedStoredValue('nextRemoteId', '1'); + const nextRemoteId = provideCachedStoredValue('nextRemoteId', '1'); /** * Obtain an ID for a new remote connection. * @@ -204,7 +234,7 @@ export function makeKernelStore(kv: KVStore) { } /** Counter for allocating kernel object IDs */ - const nextObjectId = makeCachedStoredValue('nextObjectId', '1'); + const nextObjectId = provideCachedStoredValue('nextObjectId', '1'); /** * Obtain a KRef for the next unallocated kernel object. * @@ -214,6 +244,52 @@ export function makeKernelStore(kv: KVStore) { return `ko${incCounter(nextObjectId)}`; } + /** + * Generate the storage key for a kernel entity's reference count. + * + * @param kref - The KRef of interest. + * @returns the key to store the indicated reference count at. + */ + function refCtKey(kref: KRef): string { + return `${kref}.rc`; + } + + /** + * Get a kernel entity's reference count. + * + * @param kref - The KRef of interest. + * @returns the reference count of the indicated kernel entity. + */ + function getRefCt(kref: KRef): number { + return Number(kv.get(refCtKey(kref))); + } + + /** + * Increment a kernel entity's reference count. + * + * @param kref - The KRef of the entity to increment the ref count of. + * @returns the new reference count after incrementing. + */ + function incRefCt(kref: KRef): number { + const key = refCtKey(kref); + const newCount = Number(kv.get(key)) + 1; + kv.set(key, `${newCount}`); + return newCount; + } + + /** + * Decrement a kernel entity's reference count. + * + * @param kref - The KRef of the entity to decrement the ref count of. + * @returns the new reference count after decrementing. + */ + function decRefCt(kref: KRef): number { + const key = refCtKey(kref); + const newCount = Number(kv.get(key)) - 1; + kv.set(key, `${newCount}`); + return newCount; + } + /** * Create a new kernel object. The new object will be born with reference and * recognizability counts of 1, on the assumption that the new object @@ -224,9 +300,10 @@ export function makeKernelStore(kv: KVStore) { * kernel object itself. */ function initKernelObject(owner: EndpointId): [KRef, KernelObject] { - const kobj = { owner, reachableCount: 1, recognizableCount: 1 }; + const kobj = { owner }; const koid = getNextObjectId(); kv.set(koid, JSON.stringify(kobj)); + kv.set(refCtKey(koid), '1'); return [koid, kobj]; } @@ -251,10 +328,11 @@ export function makeKernelStore(kv: KVStore) { */ function deleteKernelObject(koid: KRef): void { kv.delete(koid); + kv.delete(refCtKey(koid)); } /** Counter for allocating kernel promise IDs */ - const nextPromiseId = makeCachedStoredValue('nextPromiseId', '1'); + const nextPromiseId = provideCachedStoredValue('nextPromiseId', '1'); /** * Obtain a KRef for the next unallocated kernel promise. * @@ -277,15 +355,25 @@ export function makeKernelStore(kv: KVStore) { const kpr: KernelPromise = { decider, state: 'unresolved', - referenceCount: 1, value: undefined, }; const kpid = getNextPromiseId(); - makeStoredMessageQueue(`${kpid}.q`); + createStoredMessageQueue(kpid, false); kv.set(kpid, JSON.stringify(kpr)); + kv.set(refCtKey(kpid), '1'); return [kpid, kpr]; } + /** + * Append a message to a promise's message queue. + * + * @param kpid - The KRef of the promise to enqueue on. + * @param message - The message to enqueue. + */ + function enqueuePromiseMessage(kpid: KRef, message: Message): void { + provideStoredMessageQueue(kpid, false).enqueue(message); + } + /** * Fetch the descriptive record for a kernel promise. * @@ -308,7 +396,7 @@ export function makeKernelStore(kv: KVStore) { */ function getKernelPromiseMessageQueue(kpid: KRef): Message[] { const result: Message[] = []; - const queue = makeStoredMessageQueue(`${kpid}.q`); + const queue = provideStoredMessageQueue(kpid, false); for (;;) { const message = queue.dequeue(); if (message) { @@ -326,8 +414,8 @@ export function makeKernelStore(kv: KVStore) { */ function deleteKernelPromise(kpid: KRef): void { kv.delete(kpid); - const queue = makeStoredMessageQueue(`${kpid}.q`); - queue.delete(); + kv.delete(refCtKey(kpid)); + provideStoredMessageQueue(kpid).delete(); } /** @@ -355,11 +443,11 @@ export function makeKernelStore(kv: KVStore) { } /** - * Add an entry to a endpoints c-list, creating a new bidirectional mapping + * Add an entry to an endpoint's c-list, creating a new bidirectional mapping * between an ERef belonging to the endpoint and a KRef belonging to the * kernel. * - * @param endpointId - The endpoint whose c-list is to be updated. + * @param endpointId - The endpoint whose c-list is to be added to. * @param kref - The KRef. * @param eref - The ERef. */ @@ -368,21 +456,69 @@ export function makeKernelStore(kv: KVStore) { kv.set(`cle.${endpointId}.${eref}`, kref); } + /** + * Remove an entry from an endpoint's c-list. + * + * @param endpointId - The endpoint whose c-list entry is to be removed. + * @param kref - The KRef. + * @param eref - The ERef. + */ + function deleteClistEntry( + endpointId: EndpointId, + kref: KRef, + eref: ERef, + ): void { + kv.delete(`clk.${endpointId}.${kref}`); + kv.delete(`cle.${endpointId}.${eref}`); + } + + /** + * Remove an entry from an endpoint's c-list given an eref. + * + * @param endpointId - The endpoint whose c-list entry is to be removed. + * @param eref - The ERef. + */ + function forgetEref(endpointId: EndpointId, eref: ERef): void { + const kref = erefToKref(endpointId, eref); + if (kref) { + deleteClistEntry(endpointId, kref, eref); + } + } + + /** + * Remove an entry from an endpoint's c-list given a kref. + * + * @param endpointId - The endpoint whose c-list entry is to be removed. + * @param kref - The Kref. + */ + function forgetKref(endpointId: EndpointId, kref: KRef): void { + const eref = krefToEref(endpointId, kref); + if (eref) { + deleteClistEntry(endpointId, kref, eref); + } + } + return harden({ enqueueRun, dequeueRun, getNextVatId, getNextRemoteId, + getRefCt, + incRefCt, + decRefCt, initKernelObject, getKernelObject, deleteKernelObject, initKernelPromise, getKernelPromise, + enqueuePromiseMessage, getKernelPromiseMessageQueue, deleteKernelPromise, erefToKref, krefToEref, addClistEntry, + forgetEref, + forgetKref, kv, }); } diff --git a/packages/kernel/src/kernel-types.ts b/packages/kernel/src/kernel-types.ts index e58161f45..b53aac129 100644 --- a/packages/kernel/src/kernel-types.ts +++ b/packages/kernel/src/kernel-types.ts @@ -53,8 +53,6 @@ type RemoteState = { // Kernel persistent state export type KernelObject = { owner: EndpointId; - reachableCount: number; - recognizableCount: number; }; type PromiseState = 'unresolved' | 'fulfilled' | 'rejected'; @@ -62,7 +60,6 @@ type PromiseState = 'unresolved' | 'fulfilled' | 'rejected'; export type KernelPromise = { decider: EndpointId; state: PromiseState; - referenceCount: number; value: undefined | CapData; }; diff --git a/packages/kernel/test/storage.ts b/packages/kernel/test/storage.ts index 3feb08d25..ee4c582d0 100644 --- a/packages/kernel/test/storage.ts +++ b/packages/kernel/test/storage.ts @@ -25,6 +25,6 @@ export function makeMapKVStore(): KVStore { get: map.get.bind(map), getRequired, set: map.set.bind(map), - delete: map.set.delete(map), + delete: map.delete.bind(map), }; } From 1d9b852ec6b45f900c8fcadd0e9311bd88e2cde2 Mon Sep 17 00:00:00 2001 From: Chip Morningstar Date: Tue, 22 Oct 2024 10:59:58 -0700 Subject: [PATCH 3/5] chore: get rid of TS whinges in kernel-store test script --- packages/kernel/src/kernel-store.test.ts | 28 +++++++++++++++++------- 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/packages/kernel/src/kernel-store.test.ts b/packages/kernel/src/kernel-store.test.ts index 077f1974f..3a26baeb3 100644 --- a/packages/kernel/src/kernel-store.test.ts +++ b/packages/kernel/src/kernel-store.test.ts @@ -3,8 +3,20 @@ import '@ocap/shims/endoify'; import { describe, it, expect, beforeEach } from 'vitest'; import { makeKernelStore } from './kernel-store.js'; +import type { KVStore } from './kernel-store.js'; +import type { Message } from './kernel-types.js'; import { makeMapKVStore } from '../test/storage.js'; +/** + * Stupid hack to allow use of strings as fake Messages without TS complaints + * + * @param str - The string. + * @returns The same string coerced to type Message + */ +function sm(str: string): Message { + return str as unknown as Message; +} + describe('kernel store', () => { let mockKVStore: KVStore; @@ -117,19 +129,19 @@ describe('kernel store', () => { expect(ks.getKernelPromise('kp1')).toEqual(kp1); // eslint-disable-next-line vitest/prefer-strict-equal expect(ks.getKernelPromise('kp2')).toEqual(kp2); - ks.enqueuePromiseMessage('kp1', 'first message to kp1'); - ks.enqueuePromiseMessage('kp1', 'second message to kp1'); + ks.enqueuePromiseMessage('kp1', sm('first message to kp1')); + ks.enqueuePromiseMessage('kp1', sm('second message to kp1')); expect(ks.getKernelPromiseMessageQueue('kp1')).toStrictEqual([ 'first message to kp1', 'second message to kp1', ]); expect(ks.getKernelPromiseMessageQueue('kp1')).toStrictEqual([]); - ks.enqueuePromiseMessage('kp1', 'sacrificial message'); + ks.enqueuePromiseMessage('kp1', sm('sacrificial message')); ks.deleteKernelPromise('kp1'); expect(() => ks.getKernelPromise('kp1')).toThrow( 'unknown kernel promise kp1', ); - expect(() => ks.enqueuePromiseMessage('kp1', 'not really')).toThrow( + expect(() => ks.enqueuePromiseMessage('kp1', sm('not really'))).toThrow( 'enqueue into deleted queue kp1', ); expect(ks.getKernelPromiseMessageQueue('kp1')).toStrictEqual([]); @@ -139,14 +151,14 @@ describe('kernel store', () => { }); it('manages the run queue', () => { const ks = makeKernelStore(mockKVStore); - ks.enqueueRun('first message' as Message); - ks.enqueueRun('second message'); + ks.enqueueRun(sm('first message')); + ks.enqueueRun(sm('second message')); expect(ks.dequeueRun()).toBe('first message'); - ks.enqueueRun('third message'); + ks.enqueueRun(sm('third message')); expect(ks.dequeueRun()).toBe('second message'); expect(ks.dequeueRun()).toBe('third message'); expect(ks.dequeueRun()).toBeUndefined(); - ks.enqueueRun('fourth message'); + ks.enqueueRun(sm('fourth message')); expect(ks.dequeueRun()).toBe('fourth message'); expect(ks.dequeueRun()).toBeUndefined(); }); From 8e84a9247c7589273fe7118c7b70a578a03149b8 Mon Sep 17 00:00:00 2001 From: Chip Morningstar Date: Tue, 22 Oct 2024 19:43:34 -0700 Subject: [PATCH 4/5] chore: clean ups based on review comments; tidying up loose ends that I noticed --- packages/kernel/src/kernel-store.test.ts | 85 +++++----- packages/kernel/src/kernel-store.ts | 193 ++++++++++++++++------- packages/kernel/src/kernel-types.ts | 11 +- 3 files changed, 177 insertions(+), 112 deletions(-) diff --git a/packages/kernel/src/kernel-store.test.ts b/packages/kernel/src/kernel-store.test.ts index 3a26baeb3..226e44e18 100644 --- a/packages/kernel/src/kernel-store.test.ts +++ b/packages/kernel/src/kernel-store.test.ts @@ -8,10 +8,11 @@ import type { Message } from './kernel-types.js'; import { makeMapKVStore } from '../test/storage.js'; /** - * Stupid hack to allow use of strings as fake Messages without TS complaints + * Stupid hack to allow easy use of strings as fake Messages without TS + * complaints. * * @param str - The string. - * @returns The same string coerced to type Message + * @returns The same string coerced to type Message. */ function sm(str: string): Message { return str as unknown as Message; @@ -41,7 +42,7 @@ describe('kernel store', () => { const ks = makeKernelStore(mockKVStore); expect(Object.keys(ks).sort()).toStrictEqual([ 'addClistEntry', - 'decRefCt', + 'decRefCount', 'deleteKernelObject', 'deleteKernelPromise', 'dequeueRun', @@ -50,13 +51,13 @@ describe('kernel store', () => { 'erefToKref', 'forgetEref', 'forgetKref', - 'getKernelObject', 'getKernelPromise', 'getKernelPromiseMessageQueue', 'getNextRemoteId', 'getNextVatId', - 'getRefCt', - 'incRefCt', + 'getOwner', + 'getRefCount', + 'incRefCount', 'initKernelObject', 'initKernelPromise', 'krefToEref', @@ -77,58 +78,48 @@ describe('kernel store', () => { }); it('manages kernel objects', () => { const ks = makeKernelStore(mockKVStore); - const ko1 = { - owner: 'v47', - }; - const ko2 = { - owner: 'r23', - }; - expect(ks.initKernelObject('v47')).toStrictEqual(['ko1', ko1]); - expect(ks.getRefCt('ko1')).toBe(1); - expect(ks.incRefCt('ko1')).toBe(2); - ks.incRefCt('ko1'); - expect(ks.getRefCt('ko1')).toBe(3); - expect(ks.decRefCt('ko1')).toBe(2); - ks.decRefCt('ko1'); - ks.decRefCt('ko1'); - expect(ks.getRefCt('ko1')).toBe(0); - expect(ks.initKernelObject('r23')).toStrictEqual(['ko2', ko2]); - expect(ks.getKernelObject('ko1')).toStrictEqual(ko1); - expect(ks.getKernelObject('ko2')).toStrictEqual(ko2); + const ko1Owner = 'v47'; + const ko2Owner = 'r23'; + expect(ks.initKernelObject(ko1Owner)).toBe('ko1'); + expect(ks.getRefCount('ko1')).toBe(1); + expect(ks.incRefCount('ko1')).toBe(2); + ks.incRefCount('ko1'); + expect(ks.getRefCount('ko1')).toBe(3); + expect(ks.decRefCount('ko1')).toBe(2); + ks.decRefCount('ko1'); + ks.decRefCount('ko1'); + expect(ks.getRefCount('ko1')).toBe(0); + expect(ks.initKernelObject(ko2Owner)).toBe('ko2'); + expect(ks.getOwner('ko1')).toBe(ko1Owner); + expect(ks.getOwner('ko2')).toBe(ko2Owner); ks.deleteKernelObject('ko1'); - expect(() => ks.getKernelObject('ko1')).toThrow( - 'unknown kernel object ko1', - ); - expect(() => ks.getKernelObject('ko99')).toThrow( - 'unknown kernel object ko99', - ); + expect(() => ks.getOwner('ko1')).toThrow('unknown kernel object ko1'); + expect(() => ks.getOwner('ko99')).toThrow('unknown kernel object ko99'); }); it('manages kernel promises', () => { const ks = makeKernelStore(mockKVStore); const kp1 = { decider: 'v23', state: 'unresolved', - value: undefined, + subscribers: [], }; const kp2 = { decider: 'r47', state: 'unresolved', - value: undefined, + subscribers: [], }; expect(ks.initKernelPromise('v23')).toStrictEqual(['kp1', kp1]); - expect(ks.getRefCt('kp1')).toBe(1); - expect(ks.incRefCt('kp1')).toBe(2); - ks.incRefCt('kp1'); - expect(ks.getRefCt('kp1')).toBe(3); - expect(ks.decRefCt('kp1')).toBe(2); - ks.decRefCt('kp1'); - ks.decRefCt('kp1'); - expect(ks.getRefCt('kp1')).toBe(0); + expect(ks.getRefCount('kp1')).toBe(1); + expect(ks.incRefCount('kp1')).toBe(2); + ks.incRefCount('kp1'); + expect(ks.getRefCount('kp1')).toBe(3); + expect(ks.decRefCount('kp1')).toBe(2); + ks.decRefCount('kp1'); + ks.decRefCount('kp1'); + expect(ks.getRefCount('kp1')).toBe(0); expect(ks.initKernelPromise('r47')).toStrictEqual(['kp2', kp2]); - // eslint-disable-next-line vitest/prefer-strict-equal - expect(ks.getKernelPromise('kp1')).toEqual(kp1); - // eslint-disable-next-line vitest/prefer-strict-equal - expect(ks.getKernelPromise('kp2')).toEqual(kp2); + expect(ks.getKernelPromise('kp1')).toStrictEqual(kp1); + expect(ks.getKernelPromise('kp2')).toStrictEqual(kp2); ks.enqueuePromiseMessage('kp1', sm('first message to kp1')); ks.enqueuePromiseMessage('kp1', sm('second message to kp1')); expect(ks.getKernelPromiseMessageQueue('kp1')).toStrictEqual([ @@ -142,9 +133,11 @@ describe('kernel store', () => { 'unknown kernel promise kp1', ); expect(() => ks.enqueuePromiseMessage('kp1', sm('not really'))).toThrow( - 'enqueue into deleted queue kp1', + 'queue kp1 not initialized', + ); + expect(() => ks.getKernelPromiseMessageQueue('kp1')).toThrow( + 'queue kp1 not initialized', ); - expect(ks.getKernelPromiseMessageQueue('kp1')).toStrictEqual([]); expect(() => ks.getKernelPromise('kp99')).toThrow( 'unknown kernel promise kp99', ); diff --git a/packages/kernel/src/kernel-store.ts b/packages/kernel/src/kernel-store.ts index ee87e4e6e..b15c0a7fa 100644 --- a/packages/kernel/src/kernel-store.ts +++ b/packages/kernel/src/kernel-store.ts @@ -1,3 +1,54 @@ +/* + * Organization of keys in the key value store: + * + * Definitions + * NN ::= some decimal integer + * CAPDATA ::= capdata encoded structure value + * JSON(${xx}) ::= JSON encoding of ${xx} + * + * ${koid} ::= ko${NN} // kernel object ID + * ${kpid} ::= kp${NN} // kernel promise ID + * ${kref} ::= ${koid} | ${kpid} // kernel reference + * ${dir} ::= + | - // direction (for remote and vat references) + * ${roid} ::= ro${dir}${NN} // remote object ID + * ${rpid} ::= rp${dir}${NN} // remote promise ID + * ${rref} ::= ${roid} | ${rpid} // remote reference + * ${void} ::= vo${dir}${NN} // vat object ID + * ${vpid} ::= vp${dir}${NN} // vat promise ID + * ${vref} ::= ${void} | ${vpid} // vat reference + * ${eref} ::= ${vref} | ${rref} // external reference + * ${vatid} ::= v${NN} // vat ID + * ${remid} ::= r${NN} // remote ID + * ${endid} ::= ${vatid} | ${remid} // endpoint ID + * ${queueName} ::= run | ${kpid} + * + * Queues + * queue.${queueName}.head = NN // queue head index + * queue.${queueName}.tail = NN // queue tail index + * queue.${queueName}.${NN} = JSON(CAPDATA) // queue entry #NN + * + * Kernel objects + * ${koid}.refCount = NN // reference count + * ${koid}.owner = ${vatid} // owner (where the object is) + * + * Kernel promises + * ${kpid}.refCount = NN // reference count + * ${kpid}.state = unresolved | fulfilled | rejected // current state of settlement + * ${kpid}.subscribers = JSON([${endid}]) // array of who is waiting for settlement + * ${kpid}.decider = ${endid} // who decides on settlement + * ${kpid}.value = JSON(CAPDATA) // value settled to, if settled + * + * C-lists + * cle.${endpointId}.${eref} = ${kref} // ERef->KRef mapping + * clk.${endpointId}.${kref} = ${eref} // KRef->ERef mapping + * + * Kernel bookkeeping + * nextVatId = NN + * nextRemoteId = NN + * nextObjectId = NN + * nextPromiseId = NN + */ + import type { VatId, RemoteId, @@ -5,7 +56,7 @@ import type { KRef, ERef, Message, - KernelObject, + PromiseState, KernelPromise, } from './kernel-types.js'; @@ -152,6 +203,9 @@ export function makeKernelStore(kv: KVStore) { : provideRawStoredValue; const head = provideValue(`${qk}.head`); const tail = provideValue(`${qk}.tail`); + if (head.get() === undefined || tail.get() === undefined) { + throw Error(`queue ${queueName} not initialized`); + } return { enqueue(message: Message): void { if (head.get() === undefined) { @@ -238,7 +292,7 @@ export function makeKernelStore(kv: KVStore) { /** * Obtain a KRef for the next unallocated kernel object. * - * @returns The next koid use. + * @returns The next koId use. */ function getNextObjectId(): KRef { return `ko${incCounter(nextObjectId)}`; @@ -250,8 +304,8 @@ export function makeKernelStore(kv: KVStore) { * @param kref - The KRef of interest. * @returns the key to store the indicated reference count at. */ - function refCtKey(kref: KRef): string { - return `${kref}.rc`; + function refCountKey(kref: KRef): string { + return `${kref}.refCount`; } /** @@ -260,8 +314,8 @@ export function makeKernelStore(kv: KVStore) { * @param kref - The KRef of interest. * @returns the reference count of the indicated kernel entity. */ - function getRefCt(kref: KRef): number { - return Number(kv.get(refCtKey(kref))); + function getRefCount(kref: KRef): number { + return Number(kv.get(refCountKey(kref))); } /** @@ -270,8 +324,8 @@ export function makeKernelStore(kv: KVStore) { * @param kref - The KRef of the entity to increment the ref count of. * @returns the new reference count after incrementing. */ - function incRefCt(kref: KRef): number { - const key = refCtKey(kref); + function incRefCount(kref: KRef): number { + const key = refCountKey(kref); const newCount = Number(kv.get(key)) + 1; kv.set(key, `${newCount}`); return newCount; @@ -283,8 +337,8 @@ export function makeKernelStore(kv: KVStore) { * @param kref - The KRef of the entity to decrement the ref count of. * @returns the new reference count after decrementing. */ - function decRefCt(kref: KRef): number { - const key = refCtKey(kref); + function decRefCount(kref: KRef): number { + const key = refCountKey(kref); const newCount = Number(kv.get(key)) - 1; kv.set(key, `${newCount}`); return newCount; @@ -296,39 +350,37 @@ export function makeKernelStore(kv: KVStore) { * corresponds to an object that has just been imported from somewhere. * * @param owner - The endpoint that is the owner of the new object. - * @returns A tuple of the new object's KRef and an object describing the new - * kernel object itself. + * @returns The new object's KRef. */ - function initKernelObject(owner: EndpointId): [KRef, KernelObject] { - const kobj = { owner }; - const koid = getNextObjectId(); - kv.set(koid, JSON.stringify(kobj)); - kv.set(refCtKey(koid), '1'); - return [koid, kobj]; + function initKernelObject(owner: EndpointId): KRef { + const koId = getNextObjectId(); + kv.set(`${koId}.owner`, owner); + kv.set(refCountKey(koId), '1'); + return koId; } /** - * Fetch the descriptive record for a kernel object. + * Get a kernel object's owner. * - * @param koid - The KRef of the kernel object of interest. - * @returns An object describing the requested kernel object. + * @param koId - The KRef of the kernel object of interest. + * @returns The identity of the vat or remote that owns the object. */ - function getKernelObject(koid: KRef): KernelObject { - const raw = kv.get(koid); - if (raw === undefined) { - throw Error(`unknown kernel object ${koid}`); + function getOwner(koId: KRef): EndpointId { + const owner = kv.get(`${koId}.owner`); + if (owner === undefined) { + throw Error(`unknown kernel object ${koId}`); } - return JSON.parse(raw) as KernelObject; + return owner as EndpointId; } /** * Expunge a kernel object from the kernel's persistent state. * - * @param koid - The KRef of the kernel object to delete. + * @param koId - The KRef of the kernel object to delete. */ - function deleteKernelObject(koid: KRef): void { - kv.delete(koid); - kv.delete(refCtKey(koid)); + function deleteKernelObject(koId: KRef): void { + kv.delete(`${koId}.owner`); + kv.delete(refCountKey(koId)); } /** Counter for allocating kernel promise IDs */ @@ -336,7 +388,7 @@ export function makeKernelStore(kv: KVStore) { /** * Obtain a KRef for the next unallocated kernel promise. * - * @returns The next kpid use. + * @returns The next kpId use. */ function getNextPromiseId(): KRef { return `kp${incCounter(nextPromiseId)}`; @@ -348,55 +400,75 @@ export function makeKernelStore(kv: KVStore) { * imported from somewhere. * * @param decider - The endpoint that is the decider for the new promise. - * @returns A tuple of the new promise's KRef and a object describing the + * @returns A tuple of the new promise's KRef and an object describing the * new promise itself. */ function initKernelPromise(decider: EndpointId): [KRef, KernelPromise] { const kpr: KernelPromise = { decider, state: 'unresolved', - value: undefined, + subscribers: [], }; - const kpid = getNextPromiseId(); - createStoredMessageQueue(kpid, false); - kv.set(kpid, JSON.stringify(kpr)); - kv.set(refCtKey(kpid), '1'); - return [kpid, kpr]; + const kpId = getNextPromiseId(); + createStoredMessageQueue(kpId, false); + kv.set(`${kpId}.decider`, decider); + kv.set(`${kpId}.state`, 'unresolved'); + kv.set(`${kpId}.subscribers`, '[]'); + kv.set(refCountKey(kpId), '1'); + return [kpId, kpr]; } /** * Append a message to a promise's message queue. * - * @param kpid - The KRef of the promise to enqueue on. + * @param kpId - The KRef of the promise to enqueue on. * @param message - The message to enqueue. */ - function enqueuePromiseMessage(kpid: KRef, message: Message): void { - provideStoredMessageQueue(kpid, false).enqueue(message); + function enqueuePromiseMessage(kpId: KRef, message: Message): void { + provideStoredMessageQueue(kpId, false).enqueue(message); } /** * Fetch the descriptive record for a kernel promise. * - * @param kpid - The KRef of the kernel promise of interest. + * @param kpId - The KRef of the kernel promise of interest. * @returns An object describing the requested kernel promise. */ - function getKernelPromise(kpid: KRef): KernelPromise { - const raw = kv.get(kpid); - if (raw === undefined) { - throw Error(`unknown kernel promise ${kpid}`); + function getKernelPromise(kpId: KRef): KernelPromise { + const state = kv.get(`${kpId}.state`) as PromiseState; + if (state === undefined) { + throw Error(`unknown kernel promise ${kpId}`); + } + const result: KernelPromise = { state }; + switch (state as string) { + case 'unresolved': { + const decider = kv.get(`${kpId}.decider`); + if (decider !== '' && decider !== undefined) { + result.decider = decider as EndpointId; + } + result.subscribers = JSON.parse(kv.get(`${kpId}.subscribers`)); + break; + } + case 'fulfilled': + case 'rejected': { + result.value = JSON.parse(kv.get(`${kpId}.value`)); + break; + } + default: + throw Error(`unknown state for ${kpId}: ${state}`); } - return JSON.parse(raw) as KernelPromise; + return result; } /** * Fetch the messages in a kernel promise's message queue. * - * @param kpid - The KRef of the kernel promise of interest. + * @param kpId - The KRef of the kernel promise of interest. * @returns An array of all the messages in the given promise's message queue. */ - function getKernelPromiseMessageQueue(kpid: KRef): Message[] { + function getKernelPromiseMessageQueue(kpId: KRef): Message[] { const result: Message[] = []; - const queue = provideStoredMessageQueue(kpid, false); + const queue = provideStoredMessageQueue(kpId, false); for (;;) { const message = queue.dequeue(); if (message) { @@ -410,12 +482,15 @@ export function makeKernelStore(kv: KVStore) { /** * Expunge a kernel promise from the kernel's persistent state. * - * @param kpid - The KRef of the kernel promise to delete. + * @param kpId - The KRef of the kernel promise to delete. */ - function deleteKernelPromise(kpid: KRef): void { - kv.delete(kpid); - kv.delete(refCtKey(kpid)); - provideStoredMessageQueue(kpid).delete(); + function deleteKernelPromise(kpId: KRef): void { + kv.delete(`${kpId}.state`); + kv.delete(`${kpId}.decider`); + kv.delete(`${kpId}.subscribers`); + kv.delete(`${kpId}.value`); + kv.delete(refCountKey(kpId)); + provideStoredMessageQueue(kpId).delete(); } /** @@ -503,11 +578,11 @@ export function makeKernelStore(kv: KVStore) { dequeueRun, getNextVatId, getNextRemoteId, - getRefCt, - incRefCt, - decRefCt, + getRefCount, + incRefCount, + decRefCount, initKernelObject, - getKernelObject, + getOwner, deleteKernelObject, initKernelPromise, getKernelPromise, diff --git a/packages/kernel/src/kernel-types.ts b/packages/kernel/src/kernel-types.ts index b53aac129..d86cedad4 100644 --- a/packages/kernel/src/kernel-types.ts +++ b/packages/kernel/src/kernel-types.ts @@ -51,21 +51,18 @@ type RemoteState = { }; // Kernel persistent state -export type KernelObject = { - owner: EndpointId; -}; -type PromiseState = 'unresolved' | 'fulfilled' | 'rejected'; +export type PromiseState = 'unresolved' | 'fulfilled' | 'rejected'; export type KernelPromise = { - decider: EndpointId; state: PromiseState; - value: undefined | CapData; + decider?: EndpointId; + subscribers?: EndpointId[]; + value?: CapData; }; export type KernelState = { vats: Map; remotes: Map; - kernelObjects: Map; kernelPromises: Map; }; From ff6f5dc6009681ef9c6125680cd09ecc56412fa7 Mon Sep 17 00:00:00 2001 From: Chip Morningstar Date: Thu, 24 Oct 2024 16:41:34 -0700 Subject: [PATCH 5/5] chore: more post-review cleanup --- packages/kernel/src/Kernel.ts | 8 +++++--- packages/kernel/src/kernel-store.test.ts | 25 ++++++++++++------------ packages/kernel/src/kernel-store.ts | 8 ++++---- packages/kernel/test/storage.ts | 5 +++-- 4 files changed, 25 insertions(+), 21 deletions(-) diff --git a/packages/kernel/src/Kernel.ts b/packages/kernel/src/Kernel.ts index 5245294d2..784bd53ba 100644 --- a/packages/kernel/src/Kernel.ts +++ b/packages/kernel/src/Kernel.ts @@ -94,10 +94,12 @@ export class Kernel { break; case KernelCommandMethod.KVGet: { try { - const result = this.kvGet(params); + const value = this.kvGet(params); + const result = + typeof value === 'string' ? `"${value}"` : `${value}`; await this.#reply({ method, - params: result, + params: `~~~ got ${result} ~~~`, }); } catch (problem) { // TODO: marshal @@ -144,7 +146,7 @@ export class Kernel { } } - kvGet(key: string): string { + kvGet(key: string): string | undefined { return this.#storage.get(key); } diff --git a/packages/kernel/src/kernel-store.test.ts b/packages/kernel/src/kernel-store.test.ts index 226e44e18..04b377754 100644 --- a/packages/kernel/src/kernel-store.test.ts +++ b/packages/kernel/src/kernel-store.test.ts @@ -8,13 +8,14 @@ import type { Message } from './kernel-types.js'; import { makeMapKVStore } from '../test/storage.js'; /** - * Stupid hack to allow easy use of strings as fake Messages without TS - * complaints. + * Mock Message: A stupid TS hack to allow trivial use of plain strings as if they + * were Messages, since, for testing purposes here, all that's necessary to be a + * "message" is to be stringifiable. * - * @param str - The string. + * @param str - A string. * @returns The same string coerced to type Message. */ -function sm(str: string): Message { +function mm(str: string): Message { return str as unknown as Message; } @@ -120,19 +121,19 @@ describe('kernel store', () => { expect(ks.initKernelPromise('r47')).toStrictEqual(['kp2', kp2]); expect(ks.getKernelPromise('kp1')).toStrictEqual(kp1); expect(ks.getKernelPromise('kp2')).toStrictEqual(kp2); - ks.enqueuePromiseMessage('kp1', sm('first message to kp1')); - ks.enqueuePromiseMessage('kp1', sm('second message to kp1')); + ks.enqueuePromiseMessage('kp1', mm('first message to kp1')); + ks.enqueuePromiseMessage('kp1', mm('second message to kp1')); expect(ks.getKernelPromiseMessageQueue('kp1')).toStrictEqual([ 'first message to kp1', 'second message to kp1', ]); expect(ks.getKernelPromiseMessageQueue('kp1')).toStrictEqual([]); - ks.enqueuePromiseMessage('kp1', sm('sacrificial message')); + ks.enqueuePromiseMessage('kp1', mm('sacrificial message')); ks.deleteKernelPromise('kp1'); expect(() => ks.getKernelPromise('kp1')).toThrow( 'unknown kernel promise kp1', ); - expect(() => ks.enqueuePromiseMessage('kp1', sm('not really'))).toThrow( + expect(() => ks.enqueuePromiseMessage('kp1', mm('not really'))).toThrow( 'queue kp1 not initialized', ); expect(() => ks.getKernelPromiseMessageQueue('kp1')).toThrow( @@ -144,14 +145,14 @@ describe('kernel store', () => { }); it('manages the run queue', () => { const ks = makeKernelStore(mockKVStore); - ks.enqueueRun(sm('first message')); - ks.enqueueRun(sm('second message')); + ks.enqueueRun(mm('first message')); + ks.enqueueRun(mm('second message')); expect(ks.dequeueRun()).toBe('first message'); - ks.enqueueRun(sm('third message')); + ks.enqueueRun(mm('third message')); expect(ks.dequeueRun()).toBe('second message'); expect(ks.dequeueRun()).toBe('third message'); expect(ks.dequeueRun()).toBeUndefined(); - ks.enqueueRun(sm('fourth message')); + ks.enqueueRun(mm('fourth message')); expect(ks.dequeueRun()).toBe('fourth message'); expect(ks.dequeueRun()).toBeUndefined(); }); diff --git a/packages/kernel/src/kernel-store.ts b/packages/kernel/src/kernel-store.ts index b15c0a7fa..b0e4efe6e 100644 --- a/packages/kernel/src/kernel-store.ts +++ b/packages/kernel/src/kernel-store.ts @@ -73,7 +73,7 @@ type StoredMessageQueue = { }; export type KVStore = { - get(key: string): string; + get(key: string): string | undefined; getRequired(key: string): string; set(key: string, value: string): void; delete(key: string): void; @@ -221,7 +221,7 @@ export function makeKernelStore(kv: KVStore) { } const tailPos = tail.get(); if (tailPos !== headPos) { - const entry = kv.get(`${qk}.${tailPos}`); + const entry = kv.getRequired(`${qk}.${tailPos}`); kv.delete(`${qk}.${tailPos}`); incCounter(tail); return JSON.parse(entry) as Message; @@ -446,12 +446,12 @@ export function makeKernelStore(kv: KVStore) { if (decider !== '' && decider !== undefined) { result.decider = decider as EndpointId; } - result.subscribers = JSON.parse(kv.get(`${kpId}.subscribers`)); + result.subscribers = JSON.parse(kv.getRequired(`${kpId}.subscribers`)); break; } case 'fulfilled': case 'rejected': { - result.value = JSON.parse(kv.get(`${kpId}.value`)); + result.value = JSON.parse(kv.getRequired(`${kpId}.value`)); break; } default: diff --git a/packages/kernel/test/storage.ts b/packages/kernel/test/storage.ts index ee4c582d0..490605708 100644 --- a/packages/kernel/test/storage.ts +++ b/packages/kernel/test/storage.ts @@ -15,10 +15,11 @@ export function makeMapKVStore(): KVStore { * @returns The value at `key`. */ function getRequired(key: string): string { - if (!map.has(key)) { + const result = map.get(key); + if (result === undefined) { throw Error(`No value found for key ${key}.`); } - return map.get(key); + return result; } return {