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 60% rename from packages/extension/src/sqlite-kernel-store.ts rename to packages/extension/src/sqlite-kv-store.ts index ddfb69f16..d4688ba0d 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,12 @@ export async function makeKernelStore( } } sqlKVGet.reset(); - throw Error(`no record matching key '${key}'`); + if (required) { + throw Error(`no record matching key '${key}'`); + } else { + // Sometimes, we really lean on TypeScript's unsoundness + return undefined as unknown as string; + } } const sqlKVSet = db.prepare(` @@ -70,7 +76,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 +88,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..784bd53ba 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; @@ -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,12 +146,12 @@ export class Kernel { } } - kvGet(key: string): string { - return this.#storage.kvGet(key); + kvGet(key: string): string | undefined { + 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.test.ts b/packages/kernel/src/kernel-store.test.ts new file mode 100644 index 000000000..04b377754 --- /dev/null +++ b/packages/kernel/src/kernel-store.test.ts @@ -0,0 +1,184 @@ +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'; + +/** + * 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 - A string. + * @returns The same string coerced to type Message. + */ +function mm(str: string): Message { + return str as unknown as Message; +} + +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', + 'decRefCount', + 'deleteKernelObject', + 'deleteKernelPromise', + 'dequeueRun', + 'enqueuePromiseMessage', + 'enqueueRun', + 'erefToKref', + 'forgetEref', + 'forgetKref', + 'getKernelPromise', + 'getKernelPromiseMessageQueue', + 'getNextRemoteId', + 'getNextVatId', + 'getOwner', + 'getRefCount', + 'incRefCount', + '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 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.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', + subscribers: [], + }; + const kp2 = { + decider: 'r47', + state: 'unresolved', + subscribers: [], + }; + expect(ks.initKernelPromise('v23')).toStrictEqual(['kp1', kp1]); + 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]); + expect(ks.getKernelPromise('kp1')).toStrictEqual(kp1); + expect(ks.getKernelPromise('kp2')).toStrictEqual(kp2); + 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', mm('sacrificial message')); + ks.deleteKernelPromise('kp1'); + expect(() => ks.getKernelPromise('kp1')).toThrow( + 'unknown kernel promise kp1', + ); + expect(() => ks.enqueuePromiseMessage('kp1', mm('not really'))).toThrow( + 'queue kp1 not initialized', + ); + expect(() => ks.getKernelPromiseMessageQueue('kp1')).toThrow( + 'queue kp1 not initialized', + ); + expect(() => ks.getKernelPromise('kp99')).toThrow( + 'unknown kernel promise kp99', + ); + }); + it('manages the run queue', () => { + const ks = makeKernelStore(mockKVStore); + ks.enqueueRun(mm('first message')); + ks.enqueueRun(mm('second message')); + expect(ks.dequeueRun()).toBe('first message'); + ks.enqueueRun(mm('third message')); + expect(ks.dequeueRun()).toBe('second message'); + expect(ks.dequeueRun()).toBe('third message'); + expect(ks.dequeueRun()).toBeUndefined(); + ks.enqueueRun(mm('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 af4da6b64..b0e4efe6e 100644 --- a/packages/kernel/src/kernel-store.ts +++ b/packages/kernel/src/kernel-store.ts @@ -1,4 +1,601 @@ -export type KernelStore = { - kvGet: (key: string) => string; - kvSet: (key: string, value: string) => void; +/* + * 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, + EndpointId, + KRef, + ERef, + Message, + PromiseState, + 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 | undefined; + 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. + * + * 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 - If provided, an initial setting if the stored entity does not exist. + * @returns An object for interacting with the value. + */ + 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; + } + 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 kept in persistent storage without caching. + * + * @param key - A key string that identifies the value. + * @param init - If provided, an initial setting if the stored entity does not exist. + * @returns An object for interacting with the value. + */ + function provideRawStoredValue(key: string, init?: string): StoredValue { + if (kv.get(key) === undefined && init !== 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 (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 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 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 provideValue = cached + ? provideCachedStoredValue + : 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) { + 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.getRequired(`${qk}.${tailPos}`); + kv.delete(`${qk}.${tailPos}`); + incCounter(tail); + return JSON.parse(entry) as Message; + } + return undefined; + }, + delete(): void { + const headPos = head.get(); + if (headPos !== undefined) { + 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 = createStoredMessageQueue('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 = provideCachedStoredValue('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 = provideCachedStoredValue('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 = provideCachedStoredValue('nextObjectId', '1'); + /** + * Obtain a KRef for the next unallocated kernel object. + * + * @returns The next koId use. + */ + function getNextObjectId(): KRef { + 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 refCountKey(kref: KRef): string { + return `${kref}.refCount`; + } + + /** + * Get a kernel entity's reference count. + * + * @param kref - The KRef of interest. + * @returns the reference count of the indicated kernel entity. + */ + function getRefCount(kref: KRef): number { + return Number(kv.get(refCountKey(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 incRefCount(kref: KRef): number { + const key = refCountKey(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 decRefCount(kref: KRef): number { + const key = refCountKey(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 + * corresponds to an object that has just been imported from somewhere. + * + * @param owner - The endpoint that is the owner of the new object. + * @returns The new object's KRef. + */ + function initKernelObject(owner: EndpointId): KRef { + const koId = getNextObjectId(); + kv.set(`${koId}.owner`, owner); + kv.set(refCountKey(koId), '1'); + return koId; + } + + /** + * Get a kernel object's owner. + * + * @param koId - The KRef of the kernel object of interest. + * @returns The identity of the vat or remote that owns the object. + */ + function getOwner(koId: KRef): EndpointId { + const owner = kv.get(`${koId}.owner`); + if (owner === undefined) { + throw Error(`unknown kernel object ${koId}`); + } + return owner as EndpointId; + } + + /** + * 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}.owner`); + kv.delete(refCountKey(koId)); + } + + /** Counter for allocating kernel promise IDs */ + const nextPromiseId = provideCachedStoredValue('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 an object describing the + * new promise itself. + */ + function initKernelPromise(decider: EndpointId): [KRef, KernelPromise] { + const kpr: KernelPromise = { + decider, + state: 'unresolved', + subscribers: [], + }; + 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 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. + * + * @param kpId - The KRef of the kernel promise of interest. + * @returns An object describing the requested kernel promise. + */ + 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.getRequired(`${kpId}.subscribers`)); + break; + } + case 'fulfilled': + case 'rejected': { + result.value = JSON.parse(kv.getRequired(`${kpId}.value`)); + break; + } + default: + throw Error(`unknown state for ${kpId}: ${state}`); + } + return result; + } + + /** + * 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 = provideStoredMessageQueue(kpId, false); + 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}.state`); + kv.delete(`${kpId}.decider`); + kv.delete(`${kpId}.subscribers`); + kv.delete(`${kpId}.value`); + kv.delete(refCountKey(kpId)); + provideStoredMessageQueue(kpId).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 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 added to. + * @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); + } + + /** + * 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, + getRefCount, + incRefCount, + decRefCount, + initKernelObject, + getOwner, + deleteKernelObject, + initKernelPromise, + getKernelPromise, + enqueuePromiseMessage, + getKernelPromiseMessageQueue, + deleteKernelPromise, + erefToKref, + krefToEref, + addClistEntry, + forgetEref, + forgetKref, + kv, + }); +} + +export type KernelStore = ReturnType; diff --git a/packages/extension/src/kernel-types.ts b/packages/kernel/src/kernel-types.ts similarity index 50% rename from packages/extension/src/kernel-types.ts rename to packages/kernel/src/kernel-types.ts index aed415d68..d86cedad4 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,31 +51,18 @@ type RemoteState = { }; // Kernel persistent state -type KernelObject = { - owner: EndpointId; - reachableCount: number; - recognizableCount: number; -}; -type PromiseState = 'unresolved' | 'fulfilled' | 'rejected'; +export type PromiseState = 'unresolved' | 'fulfilled' | 'rejected'; -type KernelPromise = { - decider: EndpointId; +export type KernelPromise = { state: PromiseState; - referenceCount: number; - messageQueue: Queue; - value: undefined | CapData; + decider?: EndpointId; + subscribers?: EndpointId[]; + value?: 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..490605708 100644 --- a/packages/kernel/test/storage.ts +++ b/packages/kernel/test/storage.ts @@ -1,20 +1,31 @@ -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 { + const result = map.get(key); + if (result === undefined) { + throw Error(`No value found for key ${key}.`); + } + return result; + } + 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.delete.bind(map), }; }