graphlin 0.1.3 → 0.2.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.
Files changed (102) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/.codex-plugin/plugin.json +1 -1
  3. package/README.md +12 -3
  4. package/docs/decision-service.md +393 -0
  5. package/docs/extension-authoring.md +553 -0
  6. package/docs/model-api.md +293 -0
  7. package/docs/usage.md +465 -0
  8. package/docs/visualizer-views.md +199 -0
  9. package/node_modules/@vscode/tree-sitter-wasm/LICENSE +21 -0
  10. package/node_modules/@vscode/tree-sitter-wasm/README.md +36 -0
  11. package/node_modules/@vscode/tree-sitter-wasm/SECURITY.md +41 -0
  12. package/node_modules/@vscode/tree-sitter-wasm/cgmanifest.json +16 -0
  13. package/node_modules/@vscode/tree-sitter-wasm/package.json +42 -0
  14. package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-bash.wasm +0 -0
  15. package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-c-sharp.wasm +0 -0
  16. package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-cpp.wasm +0 -0
  17. package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-css.wasm +0 -0
  18. package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-go.wasm +0 -0
  19. package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-ini.wasm +0 -0
  20. package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-java.wasm +0 -0
  21. package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-javascript.wasm +0 -0
  22. package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-php.wasm +0 -0
  23. package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-powershell.wasm +0 -0
  24. package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-python.wasm +0 -0
  25. package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-regex.wasm +0 -0
  26. package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-ruby.wasm +0 -0
  27. package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-rust.wasm +0 -0
  28. package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-tsx.wasm +0 -0
  29. package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-typescript.wasm +0 -0
  30. package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter.js +4075 -0
  31. package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter.wasm +0 -0
  32. package/node_modules/@vscode/tree-sitter-wasm/wasm/web-tree-sitter.d.ts +1027 -0
  33. package/package.json +74 -9
  34. package/plugin.json +4 -2
  35. package/runtime/core/evidence.mjs +43 -9
  36. package/runtime/core/graph.mjs +11 -6
  37. package/runtime/core/privacy.mjs +1 -0
  38. package/runtime/daemon/auth.mjs +7 -3
  39. package/runtime/daemon/diagnostics.mjs +1 -1
  40. package/runtime/daemon/extension-api.mjs +203 -0
  41. package/runtime/daemon/lineage.mjs +70 -0
  42. package/runtime/daemon/manager.mjs +9 -6
  43. package/runtime/daemon/model-api.mjs +728 -0
  44. package/runtime/daemon/model-persistence.mjs +220 -0
  45. package/runtime/daemon/server.mjs +70 -12
  46. package/runtime/daemon/settings.mjs +11 -3
  47. package/runtime/decisions/broker.mjs +349 -0
  48. package/runtime/decisions/contracts.mjs +179 -0
  49. package/runtime/decisions/evaluation.mjs +305 -0
  50. package/runtime/decisions/faults.mjs +32 -0
  51. package/runtime/decisions/index.mjs +818 -0
  52. package/runtime/decisions/profiles.mjs +93 -0
  53. package/runtime/decisions/questions.mjs +268 -0
  54. package/runtime/discovery/index.mjs +2 -0
  55. package/runtime/discovery/inventory.mjs +160 -0
  56. package/runtime/discovery/parser.mjs +40 -0
  57. package/runtime/discovery/structure.mjs +232 -0
  58. package/runtime/extensions/contracts.mjs +59 -0
  59. package/runtime/extensions/frame.mjs +64 -0
  60. package/runtime/extensions/index.mjs +9 -0
  61. package/runtime/extensions/manifest.mjs +95 -0
  62. package/runtime/extensions/packages.mjs +222 -0
  63. package/runtime/extensions/profiles.mjs +36 -0
  64. package/runtime/extensions/projection.mjs +130 -0
  65. package/runtime/extensions/registry.mjs +285 -0
  66. package/runtime/extensions/scene.mjs +105 -0
  67. package/runtime/extensions/sdk.d.ts +205 -0
  68. package/runtime/extensions/sdk.mjs +88 -0
  69. package/runtime/jev/index.mjs +13 -777
  70. package/runtime/jev/provider.mjs +101 -0
  71. package/runtime/jev/questions.mjs +16 -258
  72. package/runtime/jev/wire.mjs +17 -25
  73. package/runtime/model/changes.mjs +42 -0
  74. package/runtime/model/history.mjs +124 -0
  75. package/runtime/model/index.mjs +2 -0
  76. package/runtime/model/project-model.mjs +889 -0
  77. package/runtime/model/records.mjs +239 -0
  78. package/runtime/pipeline.mjs +127 -48
  79. package/runtime/platform.mjs +254 -0
  80. package/runtime/visualizers/blocks.mjs +5 -0
  81. package/runtime/visualizers/c4.mjs +52 -0
  82. package/runtime/visualizers/changes.mjs +24 -0
  83. package/runtime/visualizers/code.mjs +5 -0
  84. package/runtime/visualizers/index.mjs +23 -0
  85. package/runtime/visualizers/structure.mjs +120 -0
  86. package/runtime/visualizers/timeline.mjs +66 -0
  87. package/runtime/web/app.js +225 -63
  88. package/runtime/web/extension-frame.js +128 -0
  89. package/runtime/web/index.html +36 -1
  90. package/runtime/web/model-client.js +162 -0
  91. package/runtime/web/platform.js +337 -0
  92. package/runtime/web/scene.js +111 -0
  93. package/runtime/web/style.css +49 -0
  94. package/schemas/graph.schema.json +4 -1
  95. package/scripts/arguments.mjs +5 -1
  96. package/scripts/build-packages.mjs +6 -2
  97. package/scripts/control.mjs +1 -1
  98. package/scripts/daemon.mjs +2 -1
  99. package/scripts/extensions.mjs +44 -0
  100. package/scripts/graphlin.mjs +23 -3
  101. package/scripts/onboarding.mjs +10 -3
  102. package/scripts/validate-packages.mjs +54 -8
@@ -0,0 +1,305 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { DecisionFault } from './faults.mjs';
3
+ import { isRecord, validateQuestions } from './contracts.mjs';
4
+
5
+ export const DEFAULT_CACHE_LIMITS = Object.freeze({
6
+ maxEntries: 512, maxBytes: 8 * 1024 * 1024, ttlMs: 60_000,
7
+ });
8
+ const id = value => typeof value === 'string' && /^[A-Za-z0-9_.:-]{1,160}$/.test(value)
9
+ && !['__proto__', 'constructor', 'prototype'].includes(value);
10
+ const version = value => id(value) || (Number.isSafeInteger(value) && value >= 0);
11
+ const sourceFields = new Set([
12
+ 'code', 'text', 'snippet', 'snippets', 'sourcecode', 'sourcetext',
13
+ 'rawsource', 'rawhook', 'transcript', 'prompt', 'body', 'content',
14
+ 'apikey', 'credentials', 'password', 'secret', 'token', 'absolutepath',
15
+ ]);
16
+ function freeze(value) {
17
+ if (value && typeof value === 'object') {
18
+ Object.values(value).forEach(freeze);
19
+ Object.freeze(value);
20
+ }
21
+ return value;
22
+ }
23
+ const hash = value => createHash('sha256').update(value).digest('hex');
24
+ const canonical = value => JSON.stringify(value, (_key, child) =>
25
+ isRecord(child) ? Object.fromEntries(Object.keys(child).sort().map(key => [key, child[key]])) : child);
26
+
27
+ // This is an accidental-source guard, not an authorization boundary. Only the
28
+ // trusted broker may call evaluate; it owns grants, consent and current versions.
29
+ function metadataSnapshot(value, maximumBytes) {
30
+ const ancestors = new Set();
31
+ let visited = 0;
32
+ let bytes = 0;
33
+ function visit(item, depth) {
34
+ if (++visited > 10_000 || depth > 16) throw new DecisionFault('invalid_state');
35
+ if (item === null || typeof item === 'boolean') { bytes += 5; return item; }
36
+ if (typeof item === 'number' && Number.isFinite(item)) { bytes += 24; return item; }
37
+ if (typeof item === 'string') {
38
+ bytes += Buffer.byteLength(item);
39
+ if (bytes > maximumBytes || item.length > 8192) throw new DecisionFault('request_too_large', 'abstained');
40
+ return item;
41
+ }
42
+ if (typeof item !== 'object' || ancestors.has(item)
43
+ || (!Array.isArray(item) && Object.getPrototypeOf(item) !== Object.prototype
44
+ && Object.getPrototypeOf(item) !== null)) throw new DecisionFault('invalid_state');
45
+ ancestors.add(item);
46
+ let result;
47
+ if (Array.isArray(item)) {
48
+ result = Array.from(item, child => visit(child, depth + 1));
49
+ } else {
50
+ result = Object.fromEntries(Object.entries(item).map(([key, child]) => {
51
+ if (!id(key)) throw new DecisionFault('invalid_state');
52
+ if (sourceFields.has(key.replaceAll('_', '').replaceAll('-', '').toLowerCase())) {
53
+ throw new DecisionFault('source_state_requires_intake');
54
+ }
55
+ if (key === 'source' && !id(child)) throw new DecisionFault('source_state_requires_intake');
56
+ bytes += Buffer.byteLength(key);
57
+ return [key, visit(child, depth + 1)];
58
+ }));
59
+ }
60
+ ancestors.delete(item);
61
+ if (bytes > maximumBytes) throw new DecisionFault('request_too_large', 'abstained');
62
+ return result;
63
+ }
64
+ if (!isRecord(value)) throw new DecisionFault('invalid_state');
65
+ return visit(value, 0);
66
+ }
67
+
68
+ export function buildEvaluation(input, limits) {
69
+ if (!Array.isArray(input.questions) || !input.questions.length) throw new DecisionFault('invalid_question_type');
70
+ if (input.questions.length > limits.maxQuestionsPerStage) throw new DecisionFault('question_budget', 'abstained');
71
+ const seen = new Set();
72
+ const questions = Object.fromEntries(input.questions.map(descriptor => {
73
+ if (!isRecord(descriptor) || !id(descriptor.id) || seen.has(descriptor.id)
74
+ || !['boolean', 'choice', 'score'].includes(descriptor.kind)
75
+ || Object.keys(descriptor).some(key =>
76
+ !['id', 'kind', 'question', 'focus', 'options', 'requiredMetrics'].includes(key))) {
77
+ throw new DecisionFault('invalid_question_type');
78
+ }
79
+ seen.add(descriptor.id);
80
+ let criteria;
81
+ if (descriptor.kind === 'boolean') {
82
+ if (descriptor.options !== undefined) throw new DecisionFault('invalid_question_type');
83
+ criteria = { true: 'The proposition is supported.', false: 'The proposition is not supported.' };
84
+ } else if (descriptor.kind === 'choice') {
85
+ if (!Array.isArray(descriptor.options) || descriptor.options.some(option => !isRecord(option)
86
+ || !id(option.id) || Object.keys(option).some(key => !['id', 'label'].includes(key)))
87
+ || new Set(descriptor.options.map(option => option.id)).size !== descriptor.options.length) {
88
+ throw new DecisionFault('invalid_question_type');
89
+ }
90
+ criteria = Object.fromEntries(descriptor.options.map(option => [option.id, option.label]));
91
+ } else {
92
+ criteria = descriptor.options;
93
+ }
94
+ return [descriptor.id, {
95
+ type: descriptor.kind,
96
+ instructions: {
97
+ question: descriptor.question,
98
+ ...(descriptor.focus === undefined ? {} : { focus: descriptor.focus }),
99
+ },
100
+ criteria,
101
+ requiredMetrics: descriptor.requiredMetrics ?? [],
102
+ }];
103
+ }));
104
+ validateQuestions(questions);
105
+ let profile = null;
106
+ if (input.profile !== undefined) {
107
+ if (!isRecord(input.profile) || !id(input.profile.id) || !id(input.profile.version)
108
+ || Object.keys(input.profile).some(key => !['id', 'version'].includes(key))) throw new DecisionFault('invalid_profile');
109
+ profile = { id: input.profile.id, version: input.profile.version };
110
+ }
111
+ let cacheContext = null;
112
+ if (input.cacheContext !== undefined) {
113
+ const keys = ['projectId', 'worktreeId', 'lineage', 'policyVersion', 'evidenceVersion', 'taskScope'];
114
+ if (!isRecord(input.cacheContext) || Object.keys(input.cacheContext).length !== keys.length
115
+ || keys.some(key => !Object.hasOwn(input.cacheContext, key)
116
+ || (key === 'taskScope' && input.cacheContext[key] === null ? false : !version(input.cacheContext[key])))) {
117
+ throw new DecisionFault('invalid_cache_context');
118
+ }
119
+ cacheContext = { ...input.cacheContext };
120
+ }
121
+ const request = { state: metadataSnapshot(input.state, limits.maxRequestBytes), questions };
122
+ if (Buffer.byteLength(JSON.stringify(request)) > limits.maxRequestBytes) {
123
+ throw new DecisionFault('request_too_large', 'abstained');
124
+ }
125
+ // Clone before freezing: never freeze a caller's descriptors or arrays.
126
+ return freeze(structuredClone({ request, profile, cacheContext }));
127
+ }
128
+
129
+ export function evaluationAnswers(answers) {
130
+ return Object.entries(answers).map(([id, answer]) => ({
131
+ id, kind: answer.type,
132
+ value: answer.type === 'boolean' ? answer.value : answer.type === 'choice' ? answer.choice : answer.score,
133
+ probability: answer.type === 'boolean' ? answer.probability : null,
134
+ probabilities: answer.type === 'boolean' ? null : answer.probabilities,
135
+ confidence: answer.type === 'boolean' ? null : answer.confidence,
136
+ }));
137
+ }
138
+
139
+ function normalizeCache(options = {}) {
140
+ if (!isRecord(options) || Object.keys(options).some(key => !Object.hasOwn(DEFAULT_CACHE_LIMITS, key))) {
141
+ throw new DecisionFault('invalid_cache_limits');
142
+ }
143
+ const value = { ...DEFAULT_CACHE_LIMITS, ...options };
144
+ for (const [key, maximum] of [['maxEntries', 4096], ['maxBytes', 32 * 1024 * 1024], ['ttlMs', 3_600_000]]) {
145
+ if (!Number.isSafeInteger(value[key]) || value[key] < 0 || value[key] > maximum) {
146
+ throw new DecisionFault('invalid_cache_limits');
147
+ }
148
+ }
149
+ return value;
150
+ }
151
+
152
+ export function createEvaluationAPI({ provider, clock, limits, cache: cacheOptions, submit, failure }) {
153
+ const bounds = normalizeCache(cacheOptions);
154
+ const cache = new Map();
155
+ const shared = new Map();
156
+ let bytes = 0;
157
+ let epoch = 0;
158
+ let closed = false;
159
+ let cacheHits = 0;
160
+ let sharedHits = 0;
161
+ let cacheEvictions = 0;
162
+ let subscribers = 0;
163
+ const remove = key => {
164
+ const entry = cache.get(key);
165
+ if (entry) { bytes -= entry.bytes; cache.delete(key); cacheEvictions++; }
166
+ };
167
+ const expire = () => {
168
+ for (const [key, entry] of cache) if (entry.expiresAt <= clock.now()) remove(key);
169
+ };
170
+ function remember(key, result, generation) {
171
+ if (closed || epoch !== generation || result.status !== 'accepted' || !bounds.maxEntries
172
+ || !bounds.maxBytes || !bounds.ttlMs) return;
173
+ expire();
174
+ const size = Buffer.byteLength(JSON.stringify(result)) + Buffer.byteLength(key);
175
+ if (size > bounds.maxBytes) return;
176
+ remove(key);
177
+ while (cache.size >= bounds.maxEntries || bytes + size > bounds.maxBytes) remove(cache.keys().next().value);
178
+ cache.set(key, { result, bytes: size, expiresAt: clock.now() + bounds.ttlMs });
179
+ bytes += size;
180
+ }
181
+ function decorate(result, status, key, startedAt) {
182
+ return freeze({
183
+ ...result,
184
+ provenance: result.provenance ? { ...result.provenance, cacheKey: key } : null,
185
+ diagnostics: {
186
+ ...result.diagnostics,
187
+ ...(status === 'hit' ? {
188
+ calls: 0, questionCounts: {}, stageDurationMs: {},
189
+ usage: { input_tokens: 0, output_tokens: 0 }, usageIncomplete: false,
190
+ trace: { ...result.diagnostics.trace, requests: [] },
191
+ } : {}),
192
+ durationMs: Math.max(0, clock.now() - startedAt),
193
+ cache: { status, key },
194
+ },
195
+ });
196
+ }
197
+ function subscribe(entry, { signal, deadlineAt, startedAt }, cacheStatus, key) {
198
+ entry.owners++;
199
+ subscribers++;
200
+ const waiter = { signal, deadlineAt };
201
+ entry.waiters.add(waiter);
202
+ return new Promise(resolve => {
203
+ let settled = false;
204
+ let timer;
205
+ const finish = result => {
206
+ if (settled) return;
207
+ if (result.status === 'accepted' && clock.now() >= deadlineAt) {
208
+ result = failure('deadline_exceeded', 'timeout', startedAt);
209
+ }
210
+ settled = true;
211
+ clock.clearTimeout(timer);
212
+ signal?.removeEventListener('abort', cancel);
213
+ entry.owners--;
214
+ entry.waiters.delete(waiter);
215
+ subscribers--;
216
+ if (!entry.owners && !entry.finished) {
217
+ if (shared.get(key) === entry) shared.delete(key);
218
+ entry.controller.abort();
219
+ }
220
+ resolve(decorate(result, cacheStatus, key, startedAt));
221
+ };
222
+ const cancel = () => finish(failure('cancelled', 'abstained', startedAt));
223
+ signal?.addEventListener('abort', cancel, { once: true });
224
+ timer = clock.setTimeout(() => finish(failure('deadline_exceeded', 'timeout', startedAt)), deadlineAt - clock.now());
225
+ entry.promise.then(finish);
226
+ if (signal?.aborted) cancel();
227
+ });
228
+ }
229
+ function evaluate(input = {}) {
230
+ const startedAt = clock.now();
231
+ if (closed) return Promise.resolve(failure('service_closed', 'abstained', startedAt));
232
+ let evaluation;
233
+ let deadlineAt;
234
+ try {
235
+ if (!isRecord(input)) throw new DecisionFault('invalid_input');
236
+ if (input.signal !== undefined && (!input.signal || typeof input.signal.aborted !== 'boolean'
237
+ || typeof input.signal.addEventListener !== 'function' || typeof input.signal.removeEventListener !== 'function')) {
238
+ throw new DecisionFault('invalid_signal');
239
+ }
240
+ if (input.signal?.aborted) throw new DecisionFault('cancelled', 'abstained');
241
+ deadlineAt = Math.min(input.deadlineAt ?? startedAt + limits.eventDeadlineMs, startedAt + limits.eventDeadlineMs);
242
+ if (!Number.isFinite(deadlineAt)) throw new DecisionFault('invalid_deadline');
243
+ if (deadlineAt <= startedAt) throw new DecisionFault('deadline_exceeded', 'timeout');
244
+ if (provider.unavailableCode) throw new DecisionFault(provider.unavailableCode, 'unavailable');
245
+ evaluation = buildEvaluation(input, limits);
246
+ if (clock.now() >= deadlineAt) throw new DecisionFault('deadline_exceeded', 'timeout');
247
+ } catch (error) {
248
+ return Promise.resolve(failure(error instanceof DecisionFault ? error.code : 'invalid_input',
249
+ error instanceof DecisionFault ? error.status : 'invalid', startedAt));
250
+ }
251
+ const cacheEnabled = bounds.maxEntries > 0 && bounds.maxBytes > 0 && bounds.ttlMs > 0 && evaluation.cacheContext;
252
+ if (!cacheEnabled) return submit({ ...input, evaluation }, 'evaluate')
253
+ .then(result => decorate(result, 'disabled', null, startedAt));
254
+ const key = hash(canonical({
255
+ contractVersion: provider.contractVersion, provider: provider.id, version: provider.version,
256
+ model: provider.model ?? null, mode: provider.mode, capabilities: provider.capabilities, evaluation,
257
+ }));
258
+ expire();
259
+ const hit = cache.get(key);
260
+ if (hit) {
261
+ cacheHits++;
262
+ cache.delete(key); cache.set(key, hit);
263
+ return Promise.resolve(decorate(hit.result, 'hit', key, startedAt));
264
+ }
265
+ if (subscribers >= limits.maxQueue + limits.concurrency) {
266
+ return Promise.resolve(failure('queue_full', 'overloaded', startedAt));
267
+ }
268
+ let entry = shared.get(key);
269
+ let cacheStatus = 'shared';
270
+ if (!entry) {
271
+ cacheStatus = 'miss';
272
+ const generation = epoch;
273
+ entry = { controller: new AbortController(), owners: 0, finished: false, waiters: new Set() };
274
+ shared.set(key, entry);
275
+ // Each subscriber owns its own shorter deadline. The shared workflow
276
+ // remains bounded by the original service deadline and active owners.
277
+ entry.promise = submit({
278
+ evaluation, signal: entry.controller.signal, deadlineAt: startedAt + limits.eventDeadlineMs,
279
+ }, 'evaluate').then(result => {
280
+ entry.finished = true;
281
+ if (shared.get(key) === entry) shared.delete(key);
282
+ if ([...entry.waiters].some(waiter => !waiter.signal?.aborted && waiter.deadlineAt > clock.now())) {
283
+ remember(key, result, generation);
284
+ }
285
+ return result;
286
+ });
287
+ } else sharedHits++;
288
+ return subscribe(entry, { signal: input.signal, deadlineAt, startedAt }, cacheStatus, key);
289
+ }
290
+ function invalidate() {
291
+ epoch++;
292
+ cache.clear(); bytes = 0;
293
+ for (const entry of shared.values()) entry.controller.abort(new DecisionFault('stale_evidence', 'abstained'));
294
+ shared.clear();
295
+ }
296
+ return {
297
+ evaluate, invalidate,
298
+ close() { closed = true; invalidate(); },
299
+ stats() {
300
+ expire();
301
+ return { cacheHits, sharedHits, cacheEvictions, cacheEntries: cache.size, cacheBytes: bytes,
302
+ sharedEvaluations: shared.size, evaluationSubscribers: subscribers };
303
+ },
304
+ };
305
+ }
@@ -0,0 +1,32 @@
1
+ export class DecisionFault extends Error {
2
+ constructor(code, status = 'invalid', { retryAfterMs } = {}) {
3
+ super(code);
4
+ this.name = 'DecisionFault';
5
+ this.code = code;
6
+ this.status = status;
7
+ if (Number.isFinite(retryAfterMs)) this.retryAfterMs = retryAfterMs;
8
+ }
9
+ }
10
+
11
+ export function abortFault(signal) {
12
+ return signal.reason instanceof DecisionFault
13
+ ? signal.reason : new DecisionFault('cancelled', 'abstained');
14
+ }
15
+
16
+ // Providers may ignore cancellation; the service must still settle on time.
17
+ export async function withAbort(operation, signal) {
18
+ if (signal.aborted) throw abortFault(signal);
19
+ let onAbort;
20
+ const aborted = new Promise((_, reject) => {
21
+ onAbort = () => reject(abortFault(signal));
22
+ signal.addEventListener('abort', onAbort, { once: true });
23
+ });
24
+ try {
25
+ return await Promise.race([Promise.resolve().then(() => {
26
+ if (signal.aborted) throw abortFault(signal);
27
+ return operation();
28
+ }), aborted]);
29
+ } finally {
30
+ signal.removeEventListener('abort', onAbort);
31
+ }
32
+ }