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/rng.mjs
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RNG — cryptographic and deterministic random number generation.
|
|
3
|
+
*
|
|
4
|
+
* Provides crypto-quality randomness by default, with a seeded mode using
|
|
5
|
+
* xorshift128+ for deterministic replay in tests and chaos engineering.
|
|
6
|
+
*
|
|
7
|
+
* @module rng
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Random number generator with crypto and seeded modes.
|
|
12
|
+
*/
|
|
13
|
+
export class RNG {
|
|
14
|
+
#getFn;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* @param {Object} [opts={}]
|
|
18
|
+
* @param {function(number): Uint8Array} [opts.getFn] - Custom byte source.
|
|
19
|
+
*/
|
|
20
|
+
constructor({ getFn } = {}) {
|
|
21
|
+
this.#getFn = getFn || ((n) => {
|
|
22
|
+
const buf = new Uint8Array(n);
|
|
23
|
+
crypto.getRandomValues(buf);
|
|
24
|
+
return buf;
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Get `n` random bytes.
|
|
30
|
+
*
|
|
31
|
+
* @param {number} n - Number of bytes to generate.
|
|
32
|
+
* @returns {Uint8Array} Random bytes.
|
|
33
|
+
*/
|
|
34
|
+
get(n) {
|
|
35
|
+
return this.#getFn(n);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Create a deterministic seeded RNG using xorshift128+.
|
|
40
|
+
* The same seed always produces the same sequence of bytes.
|
|
41
|
+
*
|
|
42
|
+
* @param {number} seed - The seed value (integer).
|
|
43
|
+
* @returns {RNG}
|
|
44
|
+
*/
|
|
45
|
+
static seeded(seed) {
|
|
46
|
+
// xorshift128+ state
|
|
47
|
+
let s0 = seed >>> 0 || 1;
|
|
48
|
+
let s1 = (seed * 2654435761) >>> 0 || 1;
|
|
49
|
+
|
|
50
|
+
function next() {
|
|
51
|
+
let x = s0;
|
|
52
|
+
const y = s1;
|
|
53
|
+
s0 = y;
|
|
54
|
+
x ^= (x << 23) >>> 0;
|
|
55
|
+
x ^= x >>> 17;
|
|
56
|
+
x ^= y;
|
|
57
|
+
x ^= y >>> 26;
|
|
58
|
+
s1 = x >>> 0;
|
|
59
|
+
return (s0 + s1) >>> 0;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return new RNG({
|
|
63
|
+
getFn(n) {
|
|
64
|
+
const buf = new Uint8Array(n);
|
|
65
|
+
for (let i = 0; i < n; i += 4) {
|
|
66
|
+
const val = next();
|
|
67
|
+
buf[i] = val & 0xff;
|
|
68
|
+
if (i + 1 < n) buf[i + 1] = (val >>> 8) & 0xff;
|
|
69
|
+
if (i + 2 < n) buf[i + 2] = (val >>> 16) & 0xff;
|
|
70
|
+
if (i + 3 < n) buf[i + 3] = (val >>> 24) & 0xff;
|
|
71
|
+
}
|
|
72
|
+
return buf;
|
|
73
|
+
},
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ServiceRegistry — convention-based svc:// service registry.
|
|
3
|
+
*
|
|
4
|
+
* Manages named services with registration, lookup, and lifecycle callbacks.
|
|
5
|
+
* Services are identified by string names (convention: `svc://name`).
|
|
6
|
+
*
|
|
7
|
+
* @module service-registry
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { AlreadyRegisteredError, NotFoundError } from './errors.mjs';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Registry for named services with lifecycle event callbacks.
|
|
14
|
+
*/
|
|
15
|
+
export class ServiceRegistry {
|
|
16
|
+
#services = new Map();
|
|
17
|
+
#onRegisterCbs = [];
|
|
18
|
+
#onUnregisterCbs = [];
|
|
19
|
+
#onLookupMissCbs = [];
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Register a named service.
|
|
23
|
+
*
|
|
24
|
+
* @param {string} name - Service name.
|
|
25
|
+
* @param {*} listener - The service listener/handler.
|
|
26
|
+
* @param {Object} [opts={}]
|
|
27
|
+
* @param {Object} [opts.metadata] - Arbitrary metadata about the service.
|
|
28
|
+
* @param {string} [opts.owner] - Owner identifier.
|
|
29
|
+
* @throws {AlreadyRegisteredError} If the name is already registered.
|
|
30
|
+
*/
|
|
31
|
+
register(name, listener, { metadata, owner } = {}) {
|
|
32
|
+
if (this.#services.has(name)) {
|
|
33
|
+
throw new AlreadyRegisteredError(name);
|
|
34
|
+
}
|
|
35
|
+
const entry = { name, listener, metadata: metadata || {}, owner: owner || null };
|
|
36
|
+
this.#services.set(name, entry);
|
|
37
|
+
for (const cb of this.#onRegisterCbs) {
|
|
38
|
+
try { cb(entry); } catch (_) {}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Unregister a named service.
|
|
44
|
+
*
|
|
45
|
+
* @param {string} name - Service name.
|
|
46
|
+
* @throws {NotFoundError} If the name is not registered.
|
|
47
|
+
*/
|
|
48
|
+
unregister(name) {
|
|
49
|
+
const entry = this.#services.get(name);
|
|
50
|
+
if (!entry) throw new NotFoundError(name);
|
|
51
|
+
this.#services.delete(name);
|
|
52
|
+
for (const cb of this.#onUnregisterCbs) {
|
|
53
|
+
try { cb(entry); } catch (_) {}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Look up a service by name. If not found locally, calls onLookupMiss hooks
|
|
59
|
+
* which may resolve the service from remote sources.
|
|
60
|
+
*
|
|
61
|
+
* @param {string} name - Service name.
|
|
62
|
+
* @returns {Promise<{ name: string, listener: *, metadata: Object, owner: string|null }>}
|
|
63
|
+
* @throws {NotFoundError} If the service is not found (locally or via hooks).
|
|
64
|
+
*/
|
|
65
|
+
async lookup(name) {
|
|
66
|
+
const entry = this.#services.get(name);
|
|
67
|
+
if (entry) return entry;
|
|
68
|
+
|
|
69
|
+
// Try lookup miss hooks (e.g., distributed service resolution)
|
|
70
|
+
for (const cb of this.#onLookupMissCbs) {
|
|
71
|
+
try {
|
|
72
|
+
const result = await cb(name);
|
|
73
|
+
if (result) return result;
|
|
74
|
+
} catch (_) {}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
throw new NotFoundError(name);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Check whether a service is registered.
|
|
82
|
+
*
|
|
83
|
+
* @param {string} name - Service name.
|
|
84
|
+
* @returns {boolean}
|
|
85
|
+
*/
|
|
86
|
+
has(name) {
|
|
87
|
+
return this.#services.has(name);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* List all registered service names.
|
|
92
|
+
*
|
|
93
|
+
* @returns {string[]}
|
|
94
|
+
*/
|
|
95
|
+
list() {
|
|
96
|
+
return [...this.#services.keys()];
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Register a callback for service registration events.
|
|
101
|
+
*
|
|
102
|
+
* @param {function(Object): void} cb - Callback receiving the service entry.
|
|
103
|
+
* @returns {function(): void} Unsubscribe function.
|
|
104
|
+
*/
|
|
105
|
+
onRegister(cb) {
|
|
106
|
+
this.#onRegisterCbs.push(cb);
|
|
107
|
+
return () => {
|
|
108
|
+
const idx = this.#onRegisterCbs.indexOf(cb);
|
|
109
|
+
if (idx >= 0) this.#onRegisterCbs.splice(idx, 1);
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Register a callback for service unregistration events.
|
|
115
|
+
*
|
|
116
|
+
* @param {function(Object): void} cb - Callback receiving the service entry.
|
|
117
|
+
* @returns {function(): void} Unsubscribe function.
|
|
118
|
+
*/
|
|
119
|
+
onUnregister(cb) {
|
|
120
|
+
this.#onUnregisterCbs.push(cb);
|
|
121
|
+
return () => {
|
|
122
|
+
const idx = this.#onUnregisterCbs.indexOf(cb);
|
|
123
|
+
if (idx >= 0) this.#onUnregisterCbs.splice(idx, 1);
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Register a hook called when lookup() misses locally.
|
|
129
|
+
* Hook receives the service name and may return a service entry
|
|
130
|
+
* or null to let the next hook try.
|
|
131
|
+
*
|
|
132
|
+
* @param {function(string): Promise<Object|null>} cb - Lookup miss handler.
|
|
133
|
+
* @returns {function(): void} Unsubscribe function.
|
|
134
|
+
*/
|
|
135
|
+
onLookupMiss(cb) {
|
|
136
|
+
this.#onLookupMissCbs.push(cb);
|
|
137
|
+
return () => {
|
|
138
|
+
const idx = this.#onLookupMissCbs.indexOf(cb);
|
|
139
|
+
if (idx >= 0) this.#onLookupMissCbs.splice(idx, 1);
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Register a remote service entry (for distributed service awareness).
|
|
145
|
+
*
|
|
146
|
+
* @param {string} name - Service name.
|
|
147
|
+
* @param {string} nodeId - Remote node identifier.
|
|
148
|
+
* @param {Object} [metadata={}] - Metadata about the remote service.
|
|
149
|
+
* @throws {AlreadyRegisteredError} If the name is already registered.
|
|
150
|
+
*/
|
|
151
|
+
registerRemote(name, nodeId, metadata = {}) {
|
|
152
|
+
this.register(name, null, {
|
|
153
|
+
metadata: { ...metadata, remote: true, nodeId },
|
|
154
|
+
owner: nodeId,
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Remove all registered services and callbacks.
|
|
160
|
+
*/
|
|
161
|
+
clear() {
|
|
162
|
+
this.#services.clear();
|
|
163
|
+
this.#onRegisterCbs.length = 0;
|
|
164
|
+
this.#onUnregisterCbs.length = 0;
|
|
165
|
+
this.#onLookupMissCbs.length = 0;
|
|
166
|
+
}
|
|
167
|
+
}
|
package/src/signal.mjs
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Signal — cancellation and shutdown via AbortController.
|
|
3
|
+
*
|
|
4
|
+
* Provides named signals (TERM, INT, HUP) with AbortSignal integration
|
|
5
|
+
* for cooperative cancellation of long-running operations.
|
|
6
|
+
*
|
|
7
|
+
* @module signal
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Signal name constants.
|
|
12
|
+
*/
|
|
13
|
+
export const SIGNAL = Object.freeze({
|
|
14
|
+
TERM: 'TERM',
|
|
15
|
+
INT: 'INT',
|
|
16
|
+
HUP: 'HUP',
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Signal controller providing named signal dispatch and AbortSignal integration.
|
|
21
|
+
*/
|
|
22
|
+
export class SignalController {
|
|
23
|
+
#controllers = new Map();
|
|
24
|
+
#listeners = new Map();
|
|
25
|
+
#fired = new Set();
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Fire a named signal. All registered callbacks and AbortSignals for
|
|
29
|
+
* this signal name are triggered.
|
|
30
|
+
*
|
|
31
|
+
* @param {string} name - Signal name (e.g. SIGNAL.TERM).
|
|
32
|
+
*/
|
|
33
|
+
signal(name) {
|
|
34
|
+
this.#fired.add(name);
|
|
35
|
+
|
|
36
|
+
// Fire listeners
|
|
37
|
+
const cbs = this.#listeners.get(name);
|
|
38
|
+
if (cbs) {
|
|
39
|
+
for (const cb of [...cbs]) {
|
|
40
|
+
try { cb(); } catch (_) {}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Abort the associated controller
|
|
45
|
+
const ctrl = this.#controllers.get(name);
|
|
46
|
+
if (ctrl && !ctrl.signal.aborted) {
|
|
47
|
+
ctrl.abort();
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Register a callback for a named signal.
|
|
53
|
+
*
|
|
54
|
+
* @param {string} name - Signal name.
|
|
55
|
+
* @param {function(): void} cb - Callback.
|
|
56
|
+
* @returns {function(): void} Unsubscribe function.
|
|
57
|
+
*/
|
|
58
|
+
onSignal(name, cb) {
|
|
59
|
+
if (!this.#listeners.has(name)) {
|
|
60
|
+
this.#listeners.set(name, []);
|
|
61
|
+
}
|
|
62
|
+
this.#listeners.get(name).push(cb);
|
|
63
|
+
return () => {
|
|
64
|
+
const arr = this.#listeners.get(name);
|
|
65
|
+
if (!arr) return;
|
|
66
|
+
const idx = arr.indexOf(cb);
|
|
67
|
+
if (idx >= 0) arr.splice(idx, 1);
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Get an AbortSignal that aborts when the named signal fires.
|
|
73
|
+
*
|
|
74
|
+
* @param {string} name - Signal name.
|
|
75
|
+
* @returns {AbortSignal}
|
|
76
|
+
*/
|
|
77
|
+
abortSignal(name) {
|
|
78
|
+
if (!this.#controllers.has(name)) {
|
|
79
|
+
this.#controllers.set(name, new AbortController());
|
|
80
|
+
}
|
|
81
|
+
const ctrl = this.#controllers.get(name);
|
|
82
|
+
// If already fired, abort immediately
|
|
83
|
+
if (this.#fired.has(name) && !ctrl.signal.aborted) {
|
|
84
|
+
ctrl.abort();
|
|
85
|
+
}
|
|
86
|
+
return ctrl.signal;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Check whether a signal has been fired.
|
|
91
|
+
*
|
|
92
|
+
* @param {string} name - Signal name.
|
|
93
|
+
* @returns {boolean}
|
|
94
|
+
*/
|
|
95
|
+
hasFired(name) {
|
|
96
|
+
return this.#fired.has(name);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Reset a signal so it can be fired again.
|
|
101
|
+
*
|
|
102
|
+
* @param {string} name - Signal name.
|
|
103
|
+
*/
|
|
104
|
+
reset(name) {
|
|
105
|
+
this.#fired.delete(name);
|
|
106
|
+
// Replace the AbortController so a fresh signal can be obtained
|
|
107
|
+
this.#controllers.delete(name);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Get a composite AbortSignal that aborts on either TERM or INT.
|
|
112
|
+
* Useful for graceful shutdown scenarios.
|
|
113
|
+
*
|
|
114
|
+
* @returns {AbortSignal}
|
|
115
|
+
*/
|
|
116
|
+
get shutdownSignal() {
|
|
117
|
+
return AbortSignal.any([
|
|
118
|
+
this.abortSignal(SIGNAL.TERM),
|
|
119
|
+
this.abortSignal(SIGNAL.INT),
|
|
120
|
+
]);
|
|
121
|
+
}
|
|
122
|
+
}
|
package/src/stdio.mjs
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stdio — standard I/O streams as ByteStreams.
|
|
3
|
+
*
|
|
4
|
+
* Provides stdin, stdout, stderr as ByteStream-compatible objects with
|
|
5
|
+
* convenience methods for text output.
|
|
6
|
+
*
|
|
7
|
+
* @module stdio
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { devNull } from './byte-stream.mjs';
|
|
11
|
+
|
|
12
|
+
const encoder = new TextEncoder();
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Standard I/O container wrapping ByteStream-compatible streams.
|
|
16
|
+
*/
|
|
17
|
+
export class Stdio {
|
|
18
|
+
#stdin;
|
|
19
|
+
#stdout;
|
|
20
|
+
#stderr;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* @param {Object} [opts={}]
|
|
24
|
+
* @param {Object} [opts.stdin] - Input ByteStream (defaults to devNull).
|
|
25
|
+
* @param {Object} [opts.stdout] - Output ByteStream (defaults to devNull).
|
|
26
|
+
* @param {Object} [opts.stderr] - Error ByteStream (defaults to devNull).
|
|
27
|
+
*/
|
|
28
|
+
constructor({ stdin, stdout, stderr } = {}) {
|
|
29
|
+
this.#stdin = stdin || devNull();
|
|
30
|
+
this.#stdout = stdout || devNull();
|
|
31
|
+
this.#stderr = stderr || devNull();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Standard input stream. */
|
|
35
|
+
get stdin() { return this.#stdin; }
|
|
36
|
+
|
|
37
|
+
/** Standard output stream. */
|
|
38
|
+
get stdout() { return this.#stdout; }
|
|
39
|
+
|
|
40
|
+
/** Standard error stream. */
|
|
41
|
+
get stderr() { return this.#stderr; }
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Write text to stdout (without trailing newline).
|
|
45
|
+
*
|
|
46
|
+
* @param {string} text - Text to write.
|
|
47
|
+
* @returns {Promise<void>}
|
|
48
|
+
*/
|
|
49
|
+
async print(text) {
|
|
50
|
+
await this.#stdout.write(encoder.encode(text));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Write text to stdout with a trailing newline.
|
|
55
|
+
*
|
|
56
|
+
* @param {string} text - Text to write.
|
|
57
|
+
* @returns {Promise<void>}
|
|
58
|
+
*/
|
|
59
|
+
async println(text) {
|
|
60
|
+
await this.#stdout.write(encoder.encode(text + '\n'));
|
|
61
|
+
}
|
|
62
|
+
}
|
package/src/tracer.mjs
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tracer — structured event tracing with ring buffer and async iteration.
|
|
3
|
+
*
|
|
4
|
+
* Events are auto-stamped with ID and timestamp. A ring buffer with
|
|
5
|
+
* evict-half strategy provides smooth degradation when capacity is exceeded.
|
|
6
|
+
* Multiple consumers each get independent AsyncIterables.
|
|
7
|
+
*
|
|
8
|
+
* @module tracer
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { KERNEL_DEFAULTS } from './constants.mjs';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Structured event tracer with ring buffer storage and async iteration.
|
|
15
|
+
*/
|
|
16
|
+
export class Tracer {
|
|
17
|
+
#events = [];
|
|
18
|
+
#counter = 0;
|
|
19
|
+
#capacity;
|
|
20
|
+
#clock;
|
|
21
|
+
#waiters = [];
|
|
22
|
+
#enabled = true;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* @param {Object} [opts={}]
|
|
26
|
+
* @param {number} [opts.capacity=1024] - Maximum events in the ring buffer.
|
|
27
|
+
* @param {Object} [opts.clock] - Clock instance with `nowMonotonic()`. Falls back to performance.now().
|
|
28
|
+
*/
|
|
29
|
+
constructor({ capacity = KERNEL_DEFAULTS.DEFAULT_TRACER_CAPACITY, clock } = {}) {
|
|
30
|
+
this.#capacity = capacity;
|
|
31
|
+
this.#clock = clock || null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Whether emit() records events. Toggled via enable()/disable() (e.g. /sys/kernel/trace). */
|
|
35
|
+
get enabled() { return this.#enabled; }
|
|
36
|
+
|
|
37
|
+
/** Resume recording trace events. */
|
|
38
|
+
enable() { this.#enabled = true; }
|
|
39
|
+
|
|
40
|
+
/** Stop recording trace events. Buffered events remain readable. */
|
|
41
|
+
disable() { this.#enabled = false; }
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Emit a trace event. The event is auto-stamped with `id` and `timestamp`.
|
|
45
|
+
* No-op while the tracer is disabled.
|
|
46
|
+
*
|
|
47
|
+
* @param {Object} event - Event data (must include `type` string).
|
|
48
|
+
*/
|
|
49
|
+
emit(event) {
|
|
50
|
+
if (!this.#enabled) return;
|
|
51
|
+
const stamped = {
|
|
52
|
+
id: ++this.#counter,
|
|
53
|
+
timestamp: this.#clock ? this.#clock.nowMonotonic() : performance.now(),
|
|
54
|
+
...event,
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
this.#events.push(stamped);
|
|
58
|
+
|
|
59
|
+
// Evict-half strategy when at capacity
|
|
60
|
+
if (this.#events.length > this.#capacity) {
|
|
61
|
+
const half = Math.floor(this.#capacity / 2);
|
|
62
|
+
this.#events = this.#events.slice(-half);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Notify waiting consumers
|
|
66
|
+
for (const w of this.#waiters) {
|
|
67
|
+
w.resolve(stamped);
|
|
68
|
+
}
|
|
69
|
+
this.#waiters.length = 0;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Get an AsyncIterable of trace events. Each consumer gets an independent
|
|
74
|
+
* iterator that yields events as they are emitted. Does not replay past events.
|
|
75
|
+
*
|
|
76
|
+
* @returns {AsyncIterable<Object>}
|
|
77
|
+
*/
|
|
78
|
+
events() {
|
|
79
|
+
const self = this;
|
|
80
|
+
let done = false;
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
[Symbol.asyncIterator]() {
|
|
84
|
+
return {
|
|
85
|
+
next() {
|
|
86
|
+
if (done) return Promise.resolve({ value: undefined, done: true });
|
|
87
|
+
return new Promise(resolve => {
|
|
88
|
+
self.#waiters.push({
|
|
89
|
+
resolve: (event) => resolve({ value: event, done: false }),
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
},
|
|
93
|
+
return() {
|
|
94
|
+
done = true;
|
|
95
|
+
return Promise.resolve({ value: undefined, done: true });
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Get a snapshot of all currently buffered events.
|
|
104
|
+
*
|
|
105
|
+
* @returns {Object[]} Array of trace events.
|
|
106
|
+
*/
|
|
107
|
+
snapshot() {
|
|
108
|
+
return [...this.#events];
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Clear all buffered events.
|
|
113
|
+
*/
|
|
114
|
+
clear() {
|
|
115
|
+
this.#events.length = 0;
|
|
116
|
+
}
|
|
117
|
+
}
|