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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 23 additions & 19 deletions docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,30 +49,34 @@ const kernel = await Kernel.make(platformServices, kernelDatabase, {
});
```

#### Configuring Relay Addresses for Workers
#### Configuring Remote Comms for Workers

When creating kernel workers with relay configuration, use the utilities from `@metamask/kernel-browser-runtime`:
When creating kernel workers with relay and other remote comms options, use the utilities from `@metamask/kernel-browser-runtime`:

```typescript
import {
createWorkerUrlWithRelays,
getRelaysFromCurrentLocation,
createCommsQueryString,
getCommsParamsFromCurrentLocation,
} from '@metamask/kernel-browser-runtime';

// Define relay addresses (libp2p multiaddrs)
const relays = [
'/ip4/127.0.0.1/tcp/9001/ws/p2p/12D3KooWJBDqsyHQF2MWiCdU4kdqx4zTsSTLRdShg7Ui6CRWB4uc',
];

// Create a worker with relay configuration
const worker = new Worker(
createWorkerUrlWithRelays('kernel-worker.js', relays),
{ type: 'module' },
);

// Inside the worker, retrieve relay configuration
const relays = getRelaysFromCurrentLocation();
await kernel.initRemoteComms(relays);
// Define relay addresses and other RemoteCommsOptions (allowedWsHosts, maxQueue, directListenAddresses, etc.)
const commsParams = {
relays: [
'/ip4/127.0.0.1/tcp/9001/ws/p2p/12D3KooWJBDqsyHQF2MWiCdU4kdqx4zTsSTLRdShg7Ui6CRWB4uc',
],
allowedWsHosts: ['localhost'],
};

// Build worker URL with query string (createCommsQueryString returns URLSearchParams)
const workerUrlParams = createCommsQueryString(commsParams);
workerUrlParams.set('reset-storage', 'false'); // append other params as needed
const workerUrl = new URL('kernel-worker.js', import.meta.url);
workerUrl.search = workerUrlParams.toString();
const worker = new Worker(workerUrl, { type: 'module' });

// Inside the worker, retrieve all comms options and init
const options = getCommsParamsFromCurrentLocation();
await kernel.initRemoteComms(options);
```

### Node.js Environment
Expand Down Expand Up @@ -249,7 +253,7 @@ The `initRemoteComms` method enables peer-to-peer communication between kernels
const relays = [
'/ip4/127.0.0.1/tcp/9001/ws/p2p/12D3KooWJBDqsyHQF2MWiCdU4kdqx4zTsSTLRdShg7Ui6CRWB4uc',
];
await kernel.initRemoteComms(relays);
await kernel.initRemoteComms({ relays });

//... launch subcluster

Expand Down
15 changes: 6 additions & 9 deletions packages/extension/src/offscreen.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import {
makeIframeVatWorker,
PlatformServicesServer,
createRelayQueryString,
createCommsQueryString,
setupConsoleForwarding,
isConsoleForwardMessage,
} from '@metamask/kernel-browser-runtime';
Expand Down Expand Up @@ -64,14 +64,11 @@ async function main(): Promise<void> {
async function makeKernelWorker(): Promise<
DuplexStream<JsonRpcMessage, JsonRpcMessage>
> {
// Assign local relay address generated from `yarn ocap relay`
const relayQueryString = createRelayQueryString([
'/ip4/127.0.0.1/tcp/9001/ws/p2p/12D3KooWJBDqsyHQF2MWiCdU4kdqx4zTsSTLRdShg7Ui6CRWB4uc',
// '/dns4/troll.fudco.com/tcp/9001/ws/p2p/12D3KooWJBDqsyHQF2MWiCdU4kdqx4zTsSTLRdShg7Ui6CRWB4uc',
// '/dns4/troll.fudco.com/tcp/9003/ws/p2p/12D3KooWL9PaFePyNg2hFLpaWPFEPVYGzTvrWAFU9Lk2KoiKqJqR',
]);

const workerUrlParams = new URLSearchParams(relayQueryString);
const workerUrlParams = createCommsQueryString({
relays: [
'/ip4/127.0.0.1/tcp/9001/ws/p2p/12D3KooWJBDqsyHQF2MWiCdU4kdqx4zTsSTLRdShg7Ui6CRWB4uc',
],
});
workerUrlParams.set('reset-storage', process.env.RESET_STORAGE ?? 'false');

const workerUrl = new URL('kernel-worker.js', import.meta.url);
Expand Down
6 changes: 3 additions & 3 deletions packages/kernel-browser-runtime/src/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,16 @@ describe('index', () => {
'PlatformServicesClient',
'PlatformServicesServer',
'connectToKernel',
'createRelayQueryString',
'createCommsQueryString',
'getCapTPMessage',
'getRelaysFromCurrentLocation',
'getCommsParamsFromCurrentLocation',
'handleConsoleForwardMessage',
'isCapTPNotification',
'isConsoleForwardMessage',
'makeBackgroundCapTP',
'makeCapTPNotification',
'makeIframeVatWorker',
'parseRelayQueryString',
'parseCommsQueryString',
'receiveInternalConnections',
'rpcHandlers',
'rpcMethodSpecs',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import { setupConsoleForwarding } from '../utils/console-forwarding.ts';
import { makeKernelCapTP } from './captp/index.ts';
import { makeLoggingMiddleware } from './middleware/logging.ts';
import { makePanelMessageMiddleware } from './middleware/panel-message.ts';
import { getRelaysFromCurrentLocation } from '../utils/relay-query-string.ts';
import { getCommsParamsFromCurrentLocation } from '../utils/comms-query-string.ts';

const logger = new Logger('kernel-worker');
const DB_FILENAME = 'store.db';
Expand Down Expand Up @@ -107,6 +107,6 @@ async function main(): Promise<void> {
logger.error('Message stream error:', error);
});

const relays = getRelaysFromCurrentLocation();
await kernel.initRemoteComms({ relays });
const commsOptions = getCommsParamsFromCurrentLocation();
await kernel.initRemoteComms(commsOptions);
}
216 changes: 216 additions & 0 deletions packages/kernel-browser-runtime/src/utils/comms-query-string.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';

import {
createCommsQueryString,
parseCommsQueryString,
getCommsParamsFromCurrentLocation,
} from './comms-query-string.ts';

// Mock the logger module
vi.mock('@metamask/logger', () => ({
Logger: vi.fn().mockImplementation(function () {
return {
error: vi.fn(),
warn: vi.fn(),
};
}),
}));

describe('comms-query-string', () => {
describe('createCommsQueryString', () => {
it('returns URLSearchParams with relays only', () => {
const relays = ['/ip4/127.0.0.1/tcp/9001/ws'];
const result = createCommsQueryString({ relays });
expect(result.get('relays')).toBe(JSON.stringify(relays));
});

it('returns URLSearchParams with allowedWsHosts only', () => {
const allowedWsHosts = ['localhost', 'relay.example.com'];
const result = createCommsQueryString({ allowedWsHosts });
expect(result.get('allowedWsHosts')).toBe(JSON.stringify(allowedWsHosts));
});

it('returns URLSearchParams with both params', () => {
const relays = ['/ip4/127.0.0.1/tcp/9001/ws'];
const allowedWsHosts = ['localhost'];
const result = createCommsQueryString({ relays, allowedWsHosts });
expect(result.has('relays')).toBe(true);
expect(result.has('allowedWsHosts')).toBe(true);
});

it('returns empty URLSearchParams for empty arrays', () => {
expect(
createCommsQueryString({ relays: [], allowedWsHosts: [] }).toString(),
).toBe('');
expect(createCommsQueryString({}).toString()).toBe('');
});

it('returns URLSearchParams with number options and directListenAddresses', () => {
const result = createCommsQueryString({
relays: ['/ip4/127.0.0.1/tcp/9001/ws'],
maxRetryAttempts: 3,
maxQueue: 100,
directListenAddresses: ['/ip4/0.0.0.0/udp/0/quic-v1'],
});
expect(result.get('relays')).toBe(
JSON.stringify(['/ip4/127.0.0.1/tcp/9001/ws']),
);
expect(result.get('maxRetryAttempts')).toBe('3');
expect(result.get('maxQueue')).toBe('100');
expect(result.get('directListenAddresses')).toBe(
JSON.stringify(['/ip4/0.0.0.0/udp/0/quic-v1']),
);
});

it('round-trips full options via createCommsQueryString and parseCommsQueryString', () => {
const options = {
relays: ['/dns4/relay.example.com/tcp/443/wss/p2p/QmRelay'],
allowedWsHosts: ['relay.example.com'],
maxRetryAttempts: 5,
maxQueue: 200,
};
const params = createCommsQueryString(options);
expect(parseCommsQueryString(`?${params.toString()}`)).toStrictEqual(
options,
);
});

it('throws on invalid array param types', () => {
expect(() =>
createCommsQueryString({
relays: 'not-an-array' as unknown as string[],
}),
).toThrow(TypeError);
expect(() =>
createCommsQueryString({
relays: [1, 2] as unknown as string[],
}),
).toThrow(TypeError);
});

it('throws on invalid number param types', () => {
expect(() => createCommsQueryString({ maxRetryAttempts: -1 })).toThrow(
TypeError,
);
expect(() => createCommsQueryString({ maxQueue: 1.5 })).toThrow(
TypeError,
);
expect(() =>
createCommsQueryString({
maxRetryAttempts: 'five' as unknown as number,
}),
).toThrow(TypeError);
});
});

describe('parseCommsQueryString', () => {
it('returns both relays and allowedWsHosts', () => {
const queryString = `?relays=${encodeURIComponent(JSON.stringify(['/ip4/127.0.0.1/tcp/9001/ws']))}&allowedWsHosts=${encodeURIComponent(JSON.stringify(['localhost']))}`;
expect(parseCommsQueryString(queryString)).toStrictEqual({
relays: ['/ip4/127.0.0.1/tcp/9001/ws'],
allowedWsHosts: ['localhost'],
});
});

it('returns empty object when no comms params present', () => {
expect(parseCommsQueryString('?foo=bar')).toStrictEqual({});
});

it('parses directListenAddresses and number options', () => {
const queryString = `?directListenAddresses=${encodeURIComponent(JSON.stringify(['/ip4/0.0.0.0/udp/0/quic-v1']))}&maxRetryAttempts=5&maxQueue=100`;
expect(parseCommsQueryString(queryString)).toStrictEqual({
directListenAddresses: ['/ip4/0.0.0.0/udp/0/quic-v1'],
maxRetryAttempts: 5,
maxQueue: 100,
});
});

it('parses query string without leading ?', () => {
const queryString = `relays=${encodeURIComponent(JSON.stringify(['/ip4/127.0.0.1/tcp/9001/ws']))}`;
expect(parseCommsQueryString(queryString)).toStrictEqual({
relays: ['/ip4/127.0.0.1/tcp/9001/ws'],
});
});

it('throws on array params with non-string-array JSON values', () => {
expect(() =>
parseCommsQueryString(
`?relays=${encodeURIComponent(JSON.stringify({ not: 'an array' }))}`,
),
).toThrow(TypeError);
expect(() =>
parseCommsQueryString(
`?relays=${encodeURIComponent(JSON.stringify([1, 2]))}`,
),
).toThrow(TypeError);
});

it('throws on array params with invalid JSON', () => {
expect(() => parseCommsQueryString('?relays=not-json')).toThrow(
TypeError,
);
});

it('throws on invalid number values', () => {
expect(() => parseCommsQueryString('?maxRetryAttempts=-1')).toThrow(
TypeError,
);
expect(() => parseCommsQueryString('?maxRetryAttempts=1.5')).toThrow(
TypeError,
);
expect(parseCommsQueryString('?maxRetryAttempts=10')).toStrictEqual({
maxRetryAttempts: 10,
});
});
});

describe('getCommsParamsFromCurrentLocation', () => {
const originalLocation = globalThis.location;

beforeEach(() => {
Object.defineProperty(globalThis, 'location', {
value: { search: '' },
writable: true,
configurable: true,
});
});

afterEach(() => {
if (originalLocation) {
Object.defineProperty(globalThis, 'location', {
value: originalLocation,
writable: true,
configurable: true,
});
} else {
// @ts-expect-error - deleting global property
delete globalThis.location;
}
});

it('returns relays and allowedWsHosts from location', () => {
const relays = ['/ip4/127.0.0.1/tcp/9001/ws'];
const allowedWsHosts = ['localhost'];
globalThis.location.search = `?relays=${encodeURIComponent(JSON.stringify(relays))}&allowedWsHosts=${encodeURIComponent(JSON.stringify(allowedWsHosts))}`;
expect(getCommsParamsFromCurrentLocation()).toStrictEqual({
relays,
allowedWsHosts,
});
});

it('returns empty object when location is undefined', () => {
// @ts-expect-error - testing undefined location
delete globalThis.location;
expect(getCommsParamsFromCurrentLocation()).toStrictEqual({});
});

it('returns all parsed options including numbers and directListenAddresses', () => {
globalThis.location.search = `?relays=${encodeURIComponent(JSON.stringify(['/ip4/127.0.0.1/tcp/9001/ws']))}&maxQueue=50&stalePeerTimeoutMs=3600000`;
expect(getCommsParamsFromCurrentLocation()).toStrictEqual({
relays: ['/ip4/127.0.0.1/tcp/9001/ws'],
maxQueue: 50,
stalePeerTimeoutMs: 3600000,
});
});
});
});
Loading
Loading