browsermesh-kernel 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +59 -0
- package/package.json +25 -0
- package/src/byte-stream.mjs +258 -0
- package/src/caps.mjs +102 -0
- package/src/chaos.mjs +151 -0
- package/src/clock.mjs +80 -0
- package/src/constants.mjs +85 -0
- package/src/env.mjs +56 -0
- package/src/errors.mjs +148 -0
- package/src/index.d.ts +826 -0
- package/src/index.mjs +73 -0
- package/src/kernel.mjs +155 -0
- package/src/logger.mjs +182 -0
- package/src/message-port.mjs +93 -0
- package/src/resource-table.mjs +164 -0
- package/src/rng.mjs +76 -0
- package/src/service-registry.mjs +167 -0
- package/src/signal.mjs +122 -0
- package/src/stdio.mjs +62 -0
- package/src/tracer.mjs +117 -0
package/src/index.mjs
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* kernel — Capability-secure browser microkernel.
|
|
3
|
+
*
|
|
4
|
+
* Provides resource handles, ByteStreams, IPC, service mesh, structured
|
|
5
|
+
* tracing, chaos engineering, and tenant isolation — all with zero npm
|
|
6
|
+
* dependencies, pure ES modules.
|
|
7
|
+
*
|
|
8
|
+
* ## Quick start
|
|
9
|
+
*
|
|
10
|
+
* ```js
|
|
11
|
+
* import { Kernel, KERNEL_CAP } from 'browsermesh-kernel';
|
|
12
|
+
*
|
|
13
|
+
* const kernel = new Kernel();
|
|
14
|
+
*
|
|
15
|
+
* // Create a tenant with scoped capabilities
|
|
16
|
+
* const tenant = kernel.createTenant({
|
|
17
|
+
* capabilities: [KERNEL_CAP.CLOCK, KERNEL_CAP.IPC, KERNEL_CAP.STDIO],
|
|
18
|
+
* env: { MODE: 'sandbox' },
|
|
19
|
+
* });
|
|
20
|
+
*
|
|
21
|
+
* // Use kernel subsystems
|
|
22
|
+
* const handle = kernel.resources.allocate('stream', myStream, tenant.id);
|
|
23
|
+
* kernel.tracer.emit({ type: 'custom', tenant: tenant.id });
|
|
24
|
+
*
|
|
25
|
+
* // Clean up
|
|
26
|
+
* kernel.destroyTenant(tenant.id);
|
|
27
|
+
* kernel.close();
|
|
28
|
+
* ```
|
|
29
|
+
*
|
|
30
|
+
* @module kernel
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
// Constants + errors
|
|
34
|
+
export { KERNEL_DEFAULTS, KERNEL_CAP, KERNEL_ERROR } from './constants.mjs';
|
|
35
|
+
export {
|
|
36
|
+
KernelError, HandleNotFoundError, HandleTypeMismatchError,
|
|
37
|
+
TableFullError, StreamClosedError, CapabilityDeniedError,
|
|
38
|
+
AlreadyRegisteredError, NotFoundError,
|
|
39
|
+
} from './errors.mjs';
|
|
40
|
+
|
|
41
|
+
// Resource management
|
|
42
|
+
export { ResourceTable } from './resource-table.mjs';
|
|
43
|
+
|
|
44
|
+
// ByteStream protocol
|
|
45
|
+
export { BYTE_STREAM, isByteStream, asByteStream, createPipe, pipe, devNull, compose } from './byte-stream.mjs';
|
|
46
|
+
|
|
47
|
+
// Clock + RNG
|
|
48
|
+
export { Clock } from './clock.mjs';
|
|
49
|
+
export { RNG } from './rng.mjs';
|
|
50
|
+
|
|
51
|
+
// Capabilities
|
|
52
|
+
export { buildCaps, requireCap, CapsBuilder } from './caps.mjs';
|
|
53
|
+
|
|
54
|
+
// IPC
|
|
55
|
+
export { KernelMessagePort, createChannel } from './message-port.mjs';
|
|
56
|
+
export { ServiceRegistry } from './service-registry.mjs';
|
|
57
|
+
|
|
58
|
+
// Observability
|
|
59
|
+
export { Tracer } from './tracer.mjs';
|
|
60
|
+
export { Logger, LOG_LEVEL } from './logger.mjs';
|
|
61
|
+
|
|
62
|
+
// Chaos engineering
|
|
63
|
+
export { ChaosEngine } from './chaos.mjs';
|
|
64
|
+
|
|
65
|
+
// Environment
|
|
66
|
+
export { Environment } from './env.mjs';
|
|
67
|
+
|
|
68
|
+
// Signals + Stdio
|
|
69
|
+
export { SIGNAL, SignalController } from './signal.mjs';
|
|
70
|
+
export { Stdio } from './stdio.mjs';
|
|
71
|
+
|
|
72
|
+
// Kernel facade
|
|
73
|
+
export { Kernel } from './kernel.mjs';
|
package/src/kernel.mjs
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Kernel — top-level facade composing all kernel subsystems.
|
|
3
|
+
*
|
|
4
|
+
* Creates and wires ResourceTable, Clock, RNG, Tracer, Logger, ChaosEngine,
|
|
5
|
+
* ServiceRegistry, and SignalController. Provides tenant lifecycle management
|
|
6
|
+
* with capability-scoped access.
|
|
7
|
+
*
|
|
8
|
+
* @module kernel
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { ResourceTable } from './resource-table.mjs';
|
|
12
|
+
import { Clock } from './clock.mjs';
|
|
13
|
+
import { RNG } from './rng.mjs';
|
|
14
|
+
import { Tracer } from './tracer.mjs';
|
|
15
|
+
import { Logger } from './logger.mjs';
|
|
16
|
+
import { ChaosEngine } from './chaos.mjs';
|
|
17
|
+
import { ServiceRegistry } from './service-registry.mjs';
|
|
18
|
+
import { SignalController } from './signal.mjs';
|
|
19
|
+
import { Environment } from './env.mjs';
|
|
20
|
+
import { Stdio } from './stdio.mjs';
|
|
21
|
+
import { buildCaps } from './caps.mjs';
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* The Kernel facade. Creates and wires all subsystems.
|
|
25
|
+
*/
|
|
26
|
+
export class Kernel {
|
|
27
|
+
#resources;
|
|
28
|
+
#clock;
|
|
29
|
+
#rng;
|
|
30
|
+
#tracer;
|
|
31
|
+
#logger;
|
|
32
|
+
#chaos;
|
|
33
|
+
#services;
|
|
34
|
+
#signals;
|
|
35
|
+
#tenants = new Map();
|
|
36
|
+
#tenantCounter = 0;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* @param {Object} [opts={}]
|
|
40
|
+
* @param {Object} [opts.clock] - Clock instance (defaults to real clock).
|
|
41
|
+
* @param {Object} [opts.rng] - RNG instance (defaults to crypto RNG).
|
|
42
|
+
* @param {Object} [opts.tracerOpts] - Options for Tracer constructor.
|
|
43
|
+
* @param {Object} [opts.loggerOpts] - Options for Logger constructor.
|
|
44
|
+
* @param {Object} [opts.resourceOpts] - Options for ResourceTable constructor.
|
|
45
|
+
*/
|
|
46
|
+
constructor({ clock, rng, tracerOpts, loggerOpts, resourceOpts } = {}) {
|
|
47
|
+
this.#clock = clock || new Clock();
|
|
48
|
+
this.#rng = rng || new RNG();
|
|
49
|
+
this.#resources = new ResourceTable(resourceOpts);
|
|
50
|
+
this.#tracer = new Tracer({ clock: this.#clock, ...tracerOpts });
|
|
51
|
+
this.#logger = new Logger({ tracer: this.#tracer, ...loggerOpts });
|
|
52
|
+
this.#chaos = new ChaosEngine({ rng: this.#rng, clock: this.#clock });
|
|
53
|
+
this.#services = new ServiceRegistry();
|
|
54
|
+
this.#signals = new SignalController();
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** The kernel's resource table. */
|
|
58
|
+
get resources() { return this.#resources; }
|
|
59
|
+
|
|
60
|
+
/** The kernel clock. */
|
|
61
|
+
get clock() { return this.#clock; }
|
|
62
|
+
|
|
63
|
+
/** The kernel RNG. */
|
|
64
|
+
get rng() { return this.#rng; }
|
|
65
|
+
|
|
66
|
+
/** The kernel tracer. */
|
|
67
|
+
get tracer() { return this.#tracer; }
|
|
68
|
+
|
|
69
|
+
/** The kernel logger (shorthand). */
|
|
70
|
+
get log() { return this.#logger; }
|
|
71
|
+
|
|
72
|
+
/** The chaos engine. */
|
|
73
|
+
get chaos() { return this.#chaos; }
|
|
74
|
+
|
|
75
|
+
/** The service registry. */
|
|
76
|
+
get services() { return this.#services; }
|
|
77
|
+
|
|
78
|
+
/** The signal controller. */
|
|
79
|
+
get signals() { return this.#signals; }
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Create a new tenant with scoped capabilities.
|
|
83
|
+
*
|
|
84
|
+
* @param {Object} [opts={}]
|
|
85
|
+
* @param {string[]} [opts.capabilities=[]] - KERNEL_CAP tags to grant.
|
|
86
|
+
* @param {Record<string,string>} [opts.env={}] - Tenant environment variables.
|
|
87
|
+
* @param {Object} [opts.stdio] - Tenant stdio streams ({stdin, stdout, stderr}).
|
|
88
|
+
* @returns {{ id: string, caps: Readonly<Object>, env: Environment, stdio: Stdio, signals: SignalController }}
|
|
89
|
+
*/
|
|
90
|
+
createTenant({ capabilities = [], env = {}, stdio } = {}) {
|
|
91
|
+
const id = `tenant_${++this.#tenantCounter}`;
|
|
92
|
+
const caps = buildCaps(this, capabilities);
|
|
93
|
+
const tenantEnv = new Environment(env);
|
|
94
|
+
const tenantStdio = new Stdio(stdio || {});
|
|
95
|
+
const tenantSignals = new SignalController();
|
|
96
|
+
|
|
97
|
+
const tenant = { id, caps, env: tenantEnv, stdio: tenantStdio, signals: tenantSignals };
|
|
98
|
+
this.#tenants.set(id, tenant);
|
|
99
|
+
|
|
100
|
+
this.#logger.info('kernel', `Tenant created: ${id}`, { capabilities });
|
|
101
|
+
|
|
102
|
+
return tenant;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Destroy a tenant, dropping all owned resources.
|
|
107
|
+
*
|
|
108
|
+
* @param {string} tenantId - Tenant identifier.
|
|
109
|
+
*/
|
|
110
|
+
destroyTenant(tenantId) {
|
|
111
|
+
const tenant = this.#tenants.get(tenantId);
|
|
112
|
+
if (!tenant) return;
|
|
113
|
+
|
|
114
|
+
// Drop all resources owned by this tenant
|
|
115
|
+
const handles = this.#resources.listByOwner(tenantId);
|
|
116
|
+
for (const h of handles) {
|
|
117
|
+
try { this.#resources.drop(h); } catch (_) {}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
this.#tenants.delete(tenantId);
|
|
121
|
+
this.#logger.info('kernel', `Tenant destroyed: ${tenantId}`);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Get a tenant by ID.
|
|
126
|
+
*
|
|
127
|
+
* @param {string} tenantId - Tenant identifier.
|
|
128
|
+
* @returns {Object|undefined}
|
|
129
|
+
*/
|
|
130
|
+
getTenant(tenantId) {
|
|
131
|
+
return this.#tenants.get(tenantId);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* List all tenant IDs.
|
|
136
|
+
*
|
|
137
|
+
* @returns {string[]}
|
|
138
|
+
*/
|
|
139
|
+
listTenants() {
|
|
140
|
+
return [...this.#tenants.keys()];
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Close the kernel, destroying all tenants and clearing all subsystems.
|
|
145
|
+
*/
|
|
146
|
+
close() {
|
|
147
|
+
for (const id of [...this.#tenants.keys()]) {
|
|
148
|
+
this.destroyTenant(id);
|
|
149
|
+
}
|
|
150
|
+
this.#resources.clear();
|
|
151
|
+
this.#services.clear();
|
|
152
|
+
this.#tracer.clear();
|
|
153
|
+
this.#logger.info('kernel', 'Kernel closed');
|
|
154
|
+
}
|
|
155
|
+
}
|
package/src/logger.mjs
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Logger — structured per-module logging with optional Tracer integration.
|
|
3
|
+
*
|
|
4
|
+
* Provides leveled logging (DEBUG, INFO, WARN, ERROR) with module tagging.
|
|
5
|
+
* Optionally pipes entries to a Tracer for unified event streaming.
|
|
6
|
+
*
|
|
7
|
+
* @module logger
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { KERNEL_DEFAULTS } from './constants.mjs';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Log level constants.
|
|
14
|
+
*/
|
|
15
|
+
export const LOG_LEVEL = Object.freeze({
|
|
16
|
+
DEBUG: 0,
|
|
17
|
+
INFO: 1,
|
|
18
|
+
WARN: 2,
|
|
19
|
+
ERROR: 3,
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Structured logger with per-module tagging and async iteration.
|
|
24
|
+
*/
|
|
25
|
+
export class Logger {
|
|
26
|
+
#entries = [];
|
|
27
|
+
#capacity;
|
|
28
|
+
#tracer;
|
|
29
|
+
#waiters = [];
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* @param {Object} [opts={}]
|
|
33
|
+
* @param {number} [opts.capacity=1024] - Maximum log entries in buffer.
|
|
34
|
+
* @param {Object} [opts.tracer] - Optional Tracer to pipe log entries to.
|
|
35
|
+
*/
|
|
36
|
+
constructor({ capacity = KERNEL_DEFAULTS.DEFAULT_LOGGER_CAPACITY, tracer } = {}) {
|
|
37
|
+
this.#capacity = capacity;
|
|
38
|
+
this.#tracer = tracer || null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Log a debug message.
|
|
43
|
+
* @param {string} module - Module name.
|
|
44
|
+
* @param {string} message - Log message.
|
|
45
|
+
* @param {Object} [data] - Additional data.
|
|
46
|
+
*/
|
|
47
|
+
debug(module, message, data) {
|
|
48
|
+
this.#log(LOG_LEVEL.DEBUG, module, message, data);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Log an info message.
|
|
53
|
+
* @param {string} module - Module name.
|
|
54
|
+
* @param {string} message - Log message.
|
|
55
|
+
* @param {Object} [data] - Additional data.
|
|
56
|
+
*/
|
|
57
|
+
info(module, message, data) {
|
|
58
|
+
this.#log(LOG_LEVEL.INFO, module, message, data);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Log a warning message.
|
|
63
|
+
* @param {string} module - Module name.
|
|
64
|
+
* @param {string} message - Log message.
|
|
65
|
+
* @param {Object} [data] - Additional data.
|
|
66
|
+
*/
|
|
67
|
+
warn(module, message, data) {
|
|
68
|
+
this.#log(LOG_LEVEL.WARN, module, message, data);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Log an error message.
|
|
73
|
+
* @param {string} module - Module name.
|
|
74
|
+
* @param {string} message - Log message.
|
|
75
|
+
* @param {Object} [data] - Additional data.
|
|
76
|
+
*/
|
|
77
|
+
error(module, message, data) {
|
|
78
|
+
this.#log(LOG_LEVEL.ERROR, module, message, data);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Create a scoped logger for a specific module.
|
|
83
|
+
*
|
|
84
|
+
* @param {string} name - Module name.
|
|
85
|
+
* @returns {{ debug, info, warn, error: function(string, Object=): void }}
|
|
86
|
+
*/
|
|
87
|
+
forModule(name) {
|
|
88
|
+
return {
|
|
89
|
+
debug: (message, data) => this.debug(name, message, data),
|
|
90
|
+
info: (message, data) => this.info(name, message, data),
|
|
91
|
+
warn: (message, data) => this.warn(name, message, data),
|
|
92
|
+
error: (message, data) => this.error(name, message, data),
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Get an AsyncIterable of log entries, optionally filtered.
|
|
98
|
+
*
|
|
99
|
+
* @param {Object} [opts={}]
|
|
100
|
+
* @param {string} [opts.module] - Filter by module name.
|
|
101
|
+
* @param {number} [opts.minLevel] - Minimum log level (LOG_LEVEL constant).
|
|
102
|
+
* @returns {AsyncIterable<Object>}
|
|
103
|
+
*/
|
|
104
|
+
entries({ module, minLevel } = {}) {
|
|
105
|
+
const self = this;
|
|
106
|
+
let done = false;
|
|
107
|
+
|
|
108
|
+
return {
|
|
109
|
+
[Symbol.asyncIterator]() {
|
|
110
|
+
return {
|
|
111
|
+
next() {
|
|
112
|
+
if (done) return Promise.resolve({ value: undefined, done: true });
|
|
113
|
+
return new Promise(resolve => {
|
|
114
|
+
self.#waiters.push({
|
|
115
|
+
module,
|
|
116
|
+
minLevel,
|
|
117
|
+
resolve: (entry) => resolve({ value: entry, done: false }),
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
},
|
|
121
|
+
return() {
|
|
122
|
+
done = true;
|
|
123
|
+
return Promise.resolve({ value: undefined, done: true });
|
|
124
|
+
},
|
|
125
|
+
};
|
|
126
|
+
},
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Get a snapshot of all currently buffered entries.
|
|
132
|
+
*
|
|
133
|
+
* @param {Object} [opts={}]
|
|
134
|
+
* @param {string} [opts.module] - Filter by module.
|
|
135
|
+
* @param {number} [opts.minLevel] - Minimum level.
|
|
136
|
+
* @returns {Object[]}
|
|
137
|
+
*/
|
|
138
|
+
snapshot({ module, minLevel } = {}) {
|
|
139
|
+
return this.#entries.filter(e => {
|
|
140
|
+
if (module && e.module !== module) return false;
|
|
141
|
+
if (minLevel != null && e.level < minLevel) return false;
|
|
142
|
+
return true;
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
#log(level, module, message, data) {
|
|
147
|
+
const entry = {
|
|
148
|
+
level,
|
|
149
|
+
module,
|
|
150
|
+
message,
|
|
151
|
+
data: data || null,
|
|
152
|
+
timestamp: Date.now(),
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
this.#entries.push(entry);
|
|
156
|
+
|
|
157
|
+
// Evict-half strategy
|
|
158
|
+
if (this.#entries.length > this.#capacity) {
|
|
159
|
+
const half = Math.floor(this.#capacity / 2);
|
|
160
|
+
this.#entries = this.#entries.slice(-half);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Pipe to tracer if available
|
|
164
|
+
if (this.#tracer) {
|
|
165
|
+
this.#tracer.emit({ type: 'log', ...entry });
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Notify filtered waiters
|
|
169
|
+
const matched = [];
|
|
170
|
+
const remaining = [];
|
|
171
|
+
for (const w of this.#waiters) {
|
|
172
|
+
if (w.module && w.module !== module) { remaining.push(w); continue; }
|
|
173
|
+
if (w.minLevel != null && level < w.minLevel) { remaining.push(w); continue; }
|
|
174
|
+
matched.push(w);
|
|
175
|
+
}
|
|
176
|
+
this.#waiters.length = 0;
|
|
177
|
+
this.#waiters.push(...remaining);
|
|
178
|
+
for (const w of matched) {
|
|
179
|
+
w.resolve(entry);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* KernelMessagePort — structured IPC with FIFO ordering.
|
|
3
|
+
*
|
|
4
|
+
* Provides in-memory message passing between kernel components. Messages are
|
|
5
|
+
* dispatched asynchronously via queueMicrotask for same-realm FIFO delivery.
|
|
6
|
+
*
|
|
7
|
+
* @module message-port
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { StreamClosedError } from './errors.mjs';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* A message endpoint. Part of a channel pair created by {@link createChannel}.
|
|
14
|
+
* Posting to one port delivers to the peer's listeners in FIFO order.
|
|
15
|
+
*/
|
|
16
|
+
export class KernelMessagePort {
|
|
17
|
+
#listeners = [];
|
|
18
|
+
#closed = false;
|
|
19
|
+
#peer = null;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* @param {KernelMessagePort} [peer] - The peer port (set internally by createChannel).
|
|
23
|
+
* @private
|
|
24
|
+
*/
|
|
25
|
+
_setPeer(peer) {
|
|
26
|
+
this.#peer = peer;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Post a structured message. Delivered to the **peer** port's listeners.
|
|
31
|
+
*
|
|
32
|
+
* @param {*} msg - The message payload.
|
|
33
|
+
* @param {Array} [transfers] - Optional transferable objects.
|
|
34
|
+
* @throws {StreamClosedError} If this port has been closed.
|
|
35
|
+
*/
|
|
36
|
+
post(msg, transfers) {
|
|
37
|
+
if (this.#closed) throw new StreamClosedError();
|
|
38
|
+
if (!this.#peer || this.#peer.closed) return;
|
|
39
|
+
this.#peer._deliver(msg, transfers);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Deliver a message to this port's listeners (called by peer).
|
|
44
|
+
* @private
|
|
45
|
+
*/
|
|
46
|
+
_deliver(msg, transfers) {
|
|
47
|
+
const snapshot = [...this.#listeners];
|
|
48
|
+
queueMicrotask(() => {
|
|
49
|
+
for (const cb of snapshot) {
|
|
50
|
+
try { cb(msg, transfers); } catch (_) {}
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Register a callback to receive messages.
|
|
57
|
+
*
|
|
58
|
+
* @param {function(*, Array=): void} cb - Message handler.
|
|
59
|
+
* @returns {function(): void} Unsubscribe function.
|
|
60
|
+
*/
|
|
61
|
+
onMessage(cb) {
|
|
62
|
+
this.#listeners.push(cb);
|
|
63
|
+
return () => {
|
|
64
|
+
const idx = this.#listeners.indexOf(cb);
|
|
65
|
+
if (idx >= 0) this.#listeners.splice(idx, 1);
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Close the port. Subsequent `post` calls throw StreamClosedError.
|
|
71
|
+
*/
|
|
72
|
+
close() {
|
|
73
|
+
this.#closed = true;
|
|
74
|
+
this.#listeners.length = 0;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Whether this port has been closed. */
|
|
78
|
+
get closed() { return this.#closed; }
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Create a connected pair of KernelMessagePorts.
|
|
83
|
+
* Posting to portA delivers to portB's listeners, and vice versa.
|
|
84
|
+
*
|
|
85
|
+
* @returns {[KernelMessagePort, KernelMessagePort]}
|
|
86
|
+
*/
|
|
87
|
+
export function createChannel() {
|
|
88
|
+
const portA = new KernelMessagePort();
|
|
89
|
+
const portB = new KernelMessagePort();
|
|
90
|
+
portA._setPeer(portB);
|
|
91
|
+
portB._setPeer(portA);
|
|
92
|
+
return [portA, portB];
|
|
93
|
+
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ResourceTable — handle-based resource management.
|
|
3
|
+
*
|
|
4
|
+
* Provides a capability-secure resource table where every resource is accessed
|
|
5
|
+
* through an opaque string handle (`res_N`). Supports typed lookups, ownership
|
|
6
|
+
* transfer, and bounded capacity with TOCTOU-safe allocation.
|
|
7
|
+
*
|
|
8
|
+
* @module resource-table
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { KERNEL_DEFAULTS } from './constants.mjs';
|
|
12
|
+
import { HandleNotFoundError, HandleTypeMismatchError, TableFullError } from './errors.mjs';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* A bounded, handle-keyed resource table with ownership tracking.
|
|
16
|
+
*/
|
|
17
|
+
export class ResourceTable {
|
|
18
|
+
#entries = new Map();
|
|
19
|
+
#counter = 0;
|
|
20
|
+
#maxSize;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* @param {Object} [opts={}]
|
|
24
|
+
* @param {number} [opts.maxSize=4096] - Maximum number of entries.
|
|
25
|
+
*/
|
|
26
|
+
constructor({ maxSize = KERNEL_DEFAULTS.MAX_RESOURCE_TABLE_SIZE } = {}) {
|
|
27
|
+
this.#maxSize = maxSize;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Allocate a new handle for a resource.
|
|
32
|
+
*
|
|
33
|
+
* @param {string} type - Resource type tag (e.g. `'stream'`, `'port'`, `'socket'`).
|
|
34
|
+
* @param {*} value - The resource value.
|
|
35
|
+
* @param {string} owner - Owner identifier (e.g. tenant ID).
|
|
36
|
+
* @returns {string} The allocated handle (e.g. `'res_1'`).
|
|
37
|
+
* @throws {TableFullError} If the table is at maximum capacity.
|
|
38
|
+
*/
|
|
39
|
+
allocate(type, value, owner) {
|
|
40
|
+
// TOCTOU-safe: check size immediately before insert
|
|
41
|
+
if (this.#entries.size >= this.#maxSize) {
|
|
42
|
+
throw new TableFullError(this.#maxSize);
|
|
43
|
+
}
|
|
44
|
+
const handle = `res_${++this.#counter}`;
|
|
45
|
+
this.#entries.set(handle, { type, value, owner });
|
|
46
|
+
return handle;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Get a resource entry by handle.
|
|
51
|
+
*
|
|
52
|
+
* @param {string} handle - The resource handle.
|
|
53
|
+
* @returns {{ type: string, value: *, owner: string }} The resource entry.
|
|
54
|
+
* @throws {HandleNotFoundError} If the handle does not exist.
|
|
55
|
+
*/
|
|
56
|
+
get(handle) {
|
|
57
|
+
const entry = this.#entries.get(handle);
|
|
58
|
+
if (!entry) throw new HandleNotFoundError(handle);
|
|
59
|
+
return { type: entry.type, value: entry.value, owner: entry.owner };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Get a resource value by handle, verifying the expected type.
|
|
64
|
+
*
|
|
65
|
+
* @param {string} handle - The resource handle.
|
|
66
|
+
* @param {string} type - The expected resource type.
|
|
67
|
+
* @returns {*} The resource value.
|
|
68
|
+
* @throws {HandleNotFoundError} If the handle does not exist.
|
|
69
|
+
* @throws {HandleTypeMismatchError} If the resource type does not match.
|
|
70
|
+
*/
|
|
71
|
+
getTyped(handle, type) {
|
|
72
|
+
const entry = this.get(handle);
|
|
73
|
+
if (entry.type !== type) {
|
|
74
|
+
throw new HandleTypeMismatchError(handle, type, entry.type);
|
|
75
|
+
}
|
|
76
|
+
return entry.value;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Transfer ownership of a resource to a new owner.
|
|
81
|
+
*
|
|
82
|
+
* @param {string} handle - The resource handle.
|
|
83
|
+
* @param {string} newOwner - The new owner identifier.
|
|
84
|
+
* @throws {HandleNotFoundError} If the handle does not exist.
|
|
85
|
+
*/
|
|
86
|
+
transfer(handle, newOwner) {
|
|
87
|
+
const entry = this.#entries.get(handle);
|
|
88
|
+
if (!entry) throw new HandleNotFoundError(handle);
|
|
89
|
+
entry.owner = newOwner;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Drop (remove) a resource from the table.
|
|
94
|
+
*
|
|
95
|
+
* @param {string} handle - The resource handle to drop.
|
|
96
|
+
* @returns {*} The resource value that was removed.
|
|
97
|
+
* @throws {HandleNotFoundError} If the handle does not exist.
|
|
98
|
+
*/
|
|
99
|
+
drop(handle) {
|
|
100
|
+
const entry = this.#entries.get(handle);
|
|
101
|
+
if (!entry) throw new HandleNotFoundError(handle);
|
|
102
|
+
this.#entries.delete(handle);
|
|
103
|
+
return entry.value;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Check whether a handle exists in the table.
|
|
108
|
+
*
|
|
109
|
+
* @param {string} handle - The resource handle.
|
|
110
|
+
* @returns {boolean}
|
|
111
|
+
*/
|
|
112
|
+
has(handle) {
|
|
113
|
+
return this.#entries.has(handle);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* List all handles owned by a given owner.
|
|
118
|
+
*
|
|
119
|
+
* @param {string} owner - The owner identifier.
|
|
120
|
+
* @returns {string[]} Array of handles owned by the owner.
|
|
121
|
+
*/
|
|
122
|
+
listByOwner(owner) {
|
|
123
|
+
const result = [];
|
|
124
|
+
for (const [handle, entry] of this.#entries) {
|
|
125
|
+
if (entry.owner === owner) result.push(handle);
|
|
126
|
+
}
|
|
127
|
+
return result;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* List all handles of a given type.
|
|
132
|
+
*
|
|
133
|
+
* @param {string} type - The resource type tag.
|
|
134
|
+
* @returns {string[]} Array of handles of the given type.
|
|
135
|
+
*/
|
|
136
|
+
listByType(type) {
|
|
137
|
+
const result = [];
|
|
138
|
+
for (const [handle, entry] of this.#entries) {
|
|
139
|
+
if (entry.type === type) result.push(handle);
|
|
140
|
+
}
|
|
141
|
+
return result;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* List all handles in the table.
|
|
146
|
+
*
|
|
147
|
+
* @returns {string[]} Array of all allocated handles.
|
|
148
|
+
*/
|
|
149
|
+
listAll() {
|
|
150
|
+
return [...this.#entries.keys()];
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** Current number of entries. */
|
|
154
|
+
get size() {
|
|
155
|
+
return this.#entries.size;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Remove all entries from the table.
|
|
160
|
+
*/
|
|
161
|
+
clear() {
|
|
162
|
+
this.#entries.clear();
|
|
163
|
+
}
|
|
164
|
+
}
|