@zhin.js/adapter 1.1.7 → 1.1.8

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/lib/provider.js CHANGED
@@ -16,29 +16,17 @@ const adapterFeature = defineFeatureProvider({
16
16
  },
17
17
  runtime: {
18
18
  async project(slots, context) {
19
- const index = await AdapterIndex.create(slots, context.snapshot);
20
- let previousIndex;
19
+ const index = await AdapterIndex.create(slots, context.snapshot, context.signal);
21
20
  return {
22
21
  value: index,
23
22
  dispose: () => index.stop(),
24
23
  handoff: {
25
- quiescePrevious(previous) {
26
- previousIndex = previousAdapterIndex(previous);
27
- return previousIndex?.close();
28
- },
29
- activateNext: () => index.start(),
24
+ activateNext: (signal) => index.activate(signal),
30
25
  deactivateNext: () => index.stop(),
31
- resumePrevious() {
32
- previousIndex?.open();
33
- },
34
- openNext: () => index.open(),
35
26
  },
36
27
  };
37
28
  },
38
29
  },
39
30
  });
40
- function previousAdapterIndex(snapshot) {
41
- return snapshot.projections.get(adapterFeatureId);
42
- }
43
31
  export { adapterFeature };
44
32
  export default adapterFeature;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhin.js/adapter",
3
- "version": "1.1.7",
3
+ "version": "1.1.8",
4
4
  "description": "Convention-based Adapter and Endpoint Feature for Zhin Plugin Runtime",
5
5
  "type": "module",
6
6
  "main": "./lib/index.js",
@@ -18,15 +18,15 @@
18
18
  ],
19
19
  "dependencies": {
20
20
  "yaml": "^2.9.0",
21
- "@zhin.js/feature-kit": "1.0.8",
21
+ "@zhin.js/feature-kit": "1.0.9",
22
22
  "@zhin.js/im-contract": "1.0.3",
23
23
  "@zhin.js/logger": "1.0.76",
24
- "@zhin.js/plugin-runtime": "1.1.5"
24
+ "@zhin.js/plugin-runtime": "1.1.6"
25
25
  },
26
26
  "devDependencies": {
27
27
  "@types/node": "^26.1.2",
28
28
  "typescript": "^6.0.3",
29
- "@zhin.js/command": "1.0.9"
29
+ "@zhin.js/command": "1.0.12"
30
30
  },
31
31
  "zhin": {
32
32
  "protocol": 1,
@@ -1,12 +1,15 @@
1
1
  import {
2
2
  DisposeStack,
3
+ GenerationCompensationError,
4
+ createGenerationAdmissionGate,
5
+ generationAdmissionSource,
3
6
  type CapabilityId,
4
7
  type CapabilitySlot,
8
+ type GenerationAdmissionGate,
5
9
  type PluginId,
6
10
  type RuntimeSnapshot,
7
11
  } from '@zhin.js/plugin-runtime';
8
12
  import { createCapabilityContext } from '@zhin.js/feature-kit';
9
- import { formatCompact, getLogger } from '@zhin.js/logger';
10
13
  import type {
11
14
  AdapterCapability,
12
15
  AdapterDefinition,
@@ -20,8 +23,6 @@ import {
20
23
  } from './endpoint-management.js';
21
24
  import { assertDeclaredEndpointOperations } from './endpoint-control.js';
22
25
 
23
- const logger = getLogger('Adapter');
24
-
25
26
  export interface AdapterDescriptor {
26
27
  readonly id: CapabilityId;
27
28
  readonly owner: PluginId;
@@ -39,57 +40,46 @@ export interface AdapterEndpointSummary extends AdapterDescriptor {
39
40
  }
40
41
 
41
42
  export type AdapterEndpointPhase =
42
- 'pending' | 'starting' | 'online' | 'failed' | 'unconfigured';
43
+ 'pending' | 'starting' | 'online';
43
44
 
44
45
  interface AdapterRecord extends AdapterDescriptor {
45
46
  readonly endpoint: EndpointInstance;
46
47
  readonly segments?: AdapterSegmentPolicy;
47
- readonly unconfigured: boolean;
48
48
  started: boolean;
49
49
  open: boolean;
50
50
  stopped: boolean;
51
- /** Start rejected or was given up on — distinguishes 'failed' from 'unconfigured'. */
52
- failed: boolean;
53
- /** start() was invoked at least once (may still be in flight). */
51
+ stopping?: Promise<void>;
54
52
  startAttempted: boolean;
55
53
  }
56
54
 
57
55
  export class AdapterIndex {
58
56
  readonly $projection = 'zhin.adapter-index/1' as const;
59
57
  readonly #records = new Map<CapabilityId, AdapterRecord>();
58
+ readonly [generationAdmissionSource]: readonly GenerationAdmissionGate[];
60
59
  readonly #order: readonly AdapterRecord[];
61
- /** True after `open()` until `close()` / `stop()` — late starts may open themselves. */
62
- #admissionOpen = false;
63
- readonly #startTimeoutMs: number;
64
- /** Final give-up budget for deferred starts (never-settling start promises). */
65
- readonly #deferredGiveUpMs: number;
66
60
 
67
61
  private constructor(
68
62
  records: readonly AdapterRecord[],
69
- startTimeoutMs: number,
70
- deferredGiveUpMs: number,
63
+ admission: GenerationAdmissionGate,
71
64
  ) {
72
65
  this.#order = Object.freeze([...records]);
73
- this.#startTimeoutMs = startTimeoutMs;
74
- this.#deferredGiveUpMs = deferredGiveUpMs;
66
+ this[generationAdmissionSource] = Object.freeze([admission]);
75
67
  for (const record of records) this.#records.set(record.id, record);
76
68
  }
77
69
 
78
70
  static async create(
79
71
  slots: readonly Readonly<CapabilitySlot<AdapterDefinition>>[],
80
72
  snapshot: RuntimeSnapshot,
81
- options: {
82
- readonly startTimeoutMs?: number;
83
- readonly deferredGiveUpMs?: number;
84
- } = {},
73
+ signal: AbortSignal,
85
74
  ): Promise<AdapterIndex> {
86
75
  const records: AdapterRecord[] = [];
87
- const unconfigured: string[] = [];
76
+ const admission = createGenerationAdmissionGate();
88
77
  try {
89
78
  for (const slot of [...slots].sort((left, right) => left.id.localeCompare(right.id))) {
79
+ signal.throwIfAborted();
90
80
  for (const expansion of expandEndpointConfigs(slot, snapshot)) {
91
- const endpoint = await createEndpointSoft(slot, snapshot, expansion);
92
- if (endpoint.unconfigured) unconfigured.push(expansion.endpointId);
81
+ const endpoint = await createEndpoint(slot, snapshot, admission, signal, expansion);
82
+ signal.throwIfAborted();
93
83
  records.push({
94
84
  id: expansion.id,
95
85
  owner: slot.owner,
@@ -98,30 +88,16 @@ export class AdapterIndex {
98
88
  name: expansion.endpointId,
99
89
  source: slot.source,
100
90
  capabilities: slot.definition.capabilities,
101
- endpoint: endpoint.instance,
91
+ endpoint,
102
92
  ...(slot.definition.segments ? { segments: slot.definition.segments } : {}),
103
- unconfigured: endpoint.unconfigured,
104
93
  started: false,
105
94
  open: false,
106
- failed: false,
107
95
  startAttempted: false,
108
- // Unconfigured stubs skip start/open so kitchen-sink Roots stay quiet.
109
- stopped: endpoint.unconfigured,
96
+ stopped: false,
110
97
  });
111
98
  }
112
99
  }
113
- if (unconfigured.length > 0) {
114
- logger.info(formatCompact({
115
- op: 'adapters_unconfigured',
116
- count: unconfigured.length,
117
- names: unconfigured.join(','),
118
- }));
119
- }
120
- return new AdapterIndex(
121
- records,
122
- options.startTimeoutMs ?? 3_000,
123
- options.deferredGiveUpMs ?? 60_000,
124
- );
100
+ return new AdapterIndex(records, admission);
125
101
  } catch (error) {
126
102
  await stopRecords(records, error);
127
103
  throw error;
@@ -129,8 +105,8 @@ export class AdapterIndex {
129
105
  }
130
106
 
131
107
  list(): readonly AdapterDescriptor[] {
132
- return this.#order.map(({ endpoint: _endpoint, unconfigured: _unconfigured,
133
- started: _started, open: _open, stopped: _stopped, failed: _failed,
108
+ return this.#order.map(({ endpoint: _endpoint,
109
+ started: _started, open: _open, stopped: _stopped, stopping: _stopping,
134
110
  startAttempted: _startAttempted, segments: _segments,
135
111
  ...descriptor }) => Object.freeze(descriptor));
136
112
  }
@@ -188,103 +164,59 @@ export class AdapterIndex {
188
164
  return this.#records.get(id)?.segments;
189
165
  }
190
166
 
191
- async start(): Promise<void> {
192
- // Soft-start in parallel with a short wait so kitchen-sink Roots do not
193
- // stall generation. Configured platforms that need longer (QQ auth, Slack
194
- // socket, GitHub verify) stay in-flight instead of being stop()'d mid-connect.
195
- const startTimeoutMs = this.#startTimeoutMs;
196
- await Promise.all(this.#order.map(async (record) => {
197
- if (record.started || record.stopped) return;
198
- record.startAttempted = true;
199
- const startPromise = (async () => record.endpoint.start?.())();
200
- try {
201
- await withTimeout(
202
- startPromise,
203
- startTimeoutMs,
204
- `Adapter start timed out after ${startTimeoutMs}ms`,
205
- );
206
- if (record.stopped) return;
207
- record.started = true;
208
- } catch (error) {
209
- const message = error instanceof Error ? error.message : String(error);
210
- if (message.includes('timed out after')) {
211
- logger.info(formatCompact({
212
- op: 'adapter_start_deferred',
213
- id: record.id,
214
- name: record.name,
215
- waitMs: startTimeoutMs,
216
- }));
217
- // Final backstop: a deferred start promise that never settles must
218
- // not keep the Endpoint in limbo forever.
219
- const giveUp = setTimeout(() => {
220
- if (record.stopped || record.started) return;
221
- record.stopped = true;
222
- record.failed = true;
223
- // Swallow a late rejection so it does not become unhandled.
224
- void startPromise.catch(() => undefined);
225
- logger.warn(formatCompact({
226
- op: 'adapter_start_give_up',
227
- id: record.id,
228
- name: record.name,
229
- waitMs: this.#deferredGiveUpMs,
230
- }));
231
- }, this.#deferredGiveUpMs);
232
- giveUp.unref?.();
233
- void startPromise.then(
234
- () => {
235
- clearTimeout(giveUp);
236
- if (record.stopped || record.started) return;
237
- record.started = true;
238
- if (this.#admissionOpen && !record.open) {
239
- try {
240
- record.endpoint.open?.();
241
- record.open = true;
242
- } catch (openError) {
243
- logger.warn(formatCompact({
244
- op: 'adapter_open_after_deferred_fail',
245
- id: record.id,
246
- name: record.name,
247
- error: openError instanceof Error ? openError.message : String(openError),
248
- }));
249
- }
250
- }
251
- },
252
- (startError) => {
253
- clearTimeout(giveUp);
254
- if (record.stopped) return;
255
- record.stopped = true;
256
- record.failed = true;
257
- logger.warn(formatCompact({
258
- op: 'adapter_start_soft_fail',
259
- id: record.id,
260
- name: record.name,
261
- error: startError instanceof Error ? startError.message : String(startError),
262
- stack: startError instanceof Error ? startError.stack : undefined,
263
- }));
264
- },
265
- );
266
- return;
167
+ async start(signal: AbortSignal = new AbortController().signal): Promise<void> {
168
+ try {
169
+ // Sequential readiness gives the candidate one owned in-flight start at a
170
+ // time. A sibling failure can therefore never leave an un-awaited start
171
+ // promise mutating resources after rollback has returned.
172
+ for (const record of this.#order) {
173
+ if (record.started || record.stopped) continue;
174
+ record.startAttempted = true;
175
+ signal.throwIfAborted();
176
+ let stopOnAbort!: () => void;
177
+ const aborted = new Promise<never>((_resolve, reject) => {
178
+ stopOnAbort = () => {
179
+ void stopRecord(record).then(
180
+ () => reject(signal.reason ?? new Error('Adapter Endpoint start aborted')),
181
+ (cleanupError) => reject(new GenerationCompensationError(
182
+ [signal.reason, cleanupError],
183
+ 'Adapter Endpoint cancellation cleanup failed',
184
+ { cause: cleanupError },
185
+ )),
186
+ );
187
+ };
188
+ });
189
+ signal.addEventListener('abort', stopOnAbort, { once: true });
190
+ try {
191
+ await Promise.race([
192
+ Promise.resolve(record.endpoint.start?.(signal)),
193
+ aborted,
194
+ ]);
195
+ } finally {
196
+ signal.removeEventListener('abort', stopOnAbort);
267
197
  }
268
- record.stopped = true;
269
- record.failed = true;
270
- void startPromise.catch(() => undefined);
271
- // Startup connect failures are logged once here (with stack); Endpoint
272
- // implementations must NOT re-log them at error level.
273
- logger.warn(formatCompact({
274
- op: 'adapter_start_soft_fail',
275
- id: record.id,
276
- name: record.name,
277
- error: message,
278
- stack: error instanceof Error ? error.stack : undefined,
279
- }));
280
- // No endpoint.stop() here: adapter Endpoints self-stop in their start()
281
- // catch by convention (verified across icqq/qq/slack/… endpoints).
198
+ signal.throwIfAborted();
199
+ if (record.stopped) throw new Error(`Adapter Endpoint stopped during start: ${record.id}`);
200
+ record.started = true;
282
201
  }
283
- }));
202
+ } catch (error) {
203
+ await stopRecords(this.#order, error);
204
+ throw error;
205
+ }
206
+ }
207
+
208
+ /** Required readiness boundary: no generation can publish a partial Endpoint set. */
209
+ async activate(signal: AbortSignal): Promise<void> {
210
+ try {
211
+ await this.start(signal);
212
+ this.open();
213
+ } catch (error) {
214
+ await stopRecords(this.#order, error);
215
+ throw error;
216
+ }
284
217
  }
285
218
 
286
219
  open(): void {
287
- this.#admissionOpen = true;
288
220
  const errors: unknown[] = [];
289
221
  for (const record of this.#order) {
290
222
  if (!record.started || record.open || record.stopped) continue;
@@ -299,7 +231,6 @@ export class AdapterIndex {
299
231
  }
300
232
 
301
233
  async close(): Promise<void> {
302
- this.#admissionOpen = false;
303
234
  const stack = new DisposeStack();
304
235
  for (const record of this.#order) {
305
236
  if (!record.open || record.stopped) continue;
@@ -370,10 +301,8 @@ function endpointLiveName(endpoint: EndpointInstance): string | undefined {
370
301
  }
371
302
 
372
303
  function endpointPhase(record: AdapterRecord): AdapterEndpointPhase {
373
- if (record.unconfigured) return 'unconfigured';
374
- if (record.failed) return 'failed';
375
304
  if (record.open && !record.stopped) return 'online';
376
- if (record.startAttempted) return 'starting';
305
+ if (record.startAttempted && !record.started) return 'starting';
377
306
  return 'pending';
378
307
  }
379
308
 
@@ -383,17 +312,6 @@ function assertEndpoint(value: unknown, id: CapabilityId): asserts value is Endp
383
312
  }
384
313
  }
385
314
 
386
- /**
387
- * Adapter `resolveXxxConfig` helpers report missing config/credentials as
388
- * TypeError("… requires …") by convention; only those are expected failures.
389
- */
390
- function isUnconfiguredError(error: unknown): boolean {
391
- return (
392
- error instanceof TypeError
393
- && /requires|not configured|missing|未配置|缺少/i.test(error.message)
394
- );
395
- }
396
-
397
315
  /** 单个实例配置展开的 endpoint 描述(多账号适配器经 `endpoints` 数组声明)。 */
398
316
  interface EndpointExpansion {
399
317
  readonly id: CapabilityId;
@@ -414,137 +332,64 @@ function expandEndpointConfigs(
414
332
  | { endpoints?: unknown }
415
333
  | undefined;
416
334
  const raw = config?.endpoints;
417
- const entries = Array.isArray(raw)
418
- ? raw.filter((entry): entry is Record<string, unknown> & { id: string } =>
419
- !!entry && typeof entry === 'object'
420
- && typeof (entry as { id?: unknown }).id === 'string'
421
- && (entry as { id: string }).id.length > 0)
422
- : [];
335
+ if (raw !== undefined && !Array.isArray(raw)) {
336
+ throw new TypeError(`Adapter ${slot.id} endpoints must be an array`);
337
+ }
338
+ const entries = (raw ?? []) as readonly unknown[];
423
339
  if (entries.length === 0) {
424
- if (Array.isArray(raw) && raw.length > 0) {
425
- logger.warn(formatCompact({
426
- op: 'adapter_endpoints_entries_dropped',
427
- id: slot.id,
428
- reason: 'every endpoints entry is missing a non-empty string id',
429
- }));
430
- }
431
340
  return Object.freeze([{ id: slot.id, endpointId: slot.localName }]);
432
341
  }
433
- // `~` record id 的分隔符、\0 是 CapabilityId 的分隔符,混入会破坏解析
434
- const valid = entries.filter((entry) => {
435
- if (/[~\0]/u.test(entry.id)) {
436
- logger.warn(formatCompact({
437
- op: 'adapter_endpoint_id_invalid',
438
- id: slot.id,
439
- endpointId: entry.id,
440
- }));
441
- return false;
342
+ const normalized = entries.map((entry, index) => {
343
+ if (!entry || typeof entry !== 'object'
344
+ || typeof (entry as { id?: unknown }).id !== 'string'
345
+ || (entry as { id: string }).id.length === 0) {
346
+ throw new TypeError(`Adapter ${slot.id} endpoints[${index}].id must be a non-empty string`);
442
347
  }
443
- return true;
348
+ return entry as Record<string, unknown> & { id: string };
444
349
  });
445
- // id 会让 #records 覆盖与 #order/resolve 三者不一致;保留首个并告警
350
+ // `~` 是 record id 的分隔符、\0 CapabilityId 的分隔符,混入会破坏解析。
351
+ for (const entry of normalized) {
352
+ if (/[~\0]/u.test(entry.id)) {
353
+ throw new TypeError(`Adapter ${slot.id} endpoint id contains a reserved delimiter: ${entry.id}`);
354
+ }
355
+ }
446
356
  const seen = new Set<string>();
447
- const deduped = valid.filter((entry) => {
357
+ for (const entry of normalized) {
448
358
  if (seen.has(entry.id)) {
449
- logger.warn(formatCompact({
450
- op: 'adapter_endpoint_id_duplicate',
451
- id: slot.id,
452
- endpointId: entry.id,
453
- }));
454
- return false;
359
+ throw new TypeError(`Adapter ${slot.id} endpoint id is duplicated: ${entry.id}`);
455
360
  }
456
361
  seen.add(entry.id);
457
- return true;
458
- });
459
- if (deduped.length === 0) {
460
- return Object.freeze([{ id: slot.id, endpointId: slot.localName }]);
461
362
  }
462
363
  const { endpoints: _drop, ...base } = (config ?? {}) as Record<string, unknown>;
463
- return Object.freeze(deduped.map((entry) => Object.freeze({
364
+ return Object.freeze(normalized.map((entry) => Object.freeze({
464
365
  id: `${slot.id}~${entry.id}` as CapabilityId,
465
366
  endpointId: entry.id,
466
367
  config: Object.freeze({ ...base, ...entry, id: entry.id }),
467
368
  })));
468
369
  }
469
370
 
470
- async function createEndpointSoft(
371
+ async function createEndpoint(
471
372
  slot: Readonly<CapabilitySlot<AdapterDefinition>>,
472
373
  snapshot: RuntimeSnapshot,
374
+ admission: GenerationAdmissionGate,
375
+ signal: AbortSignal,
473
376
  expansion?: EndpointExpansion,
474
- ): Promise<{ readonly instance: EndpointInstance; readonly unconfigured: boolean }> {
475
- let endpoint: unknown;
476
- try {
477
- endpoint = await slot.definition.create(
478
- Object.freeze({
479
- ...createCapabilityContext(snapshot, slot.owner),
480
- ...(expansion?.config ? { config: expansion.config } : {}),
481
- id: expansion?.id ?? slot.id,
482
- name: slot.localName,
483
- }),
484
- );
485
- } catch (error) {
486
- // Missing config / credentials: degrade to an inert stub so the rest of
487
- // the generation still boots. Anything else (network failures, bugs in
488
- // create()) is unexpected — keep the stub but surface a warning instead
489
- // of silently swallowing it at debug level.
490
- const message = error instanceof Error ? error.message : String(error);
491
- const log = isUnconfiguredError(error) ? logger.debug.bind(logger) : logger.warn.bind(logger);
492
- log(formatCompact({
493
- op: 'adapter_create_soft_fail',
377
+ ): Promise<EndpointInstance> {
378
+ const endpoint = await slot.definition.create(
379
+ Object.freeze({
380
+ ...createCapabilityContext(snapshot, slot.owner, admission, signal),
381
+ ...(expansion?.config ? { config: expansion.config } : {}),
494
382
  id: expansion?.id ?? slot.id,
495
- name: expansion?.endpointId ?? slot.localName,
496
- error: message,
497
- }));
498
- return {
499
- instance: createUnconfiguredEndpoint(message),
500
- unconfigured: true,
501
- };
502
- }
503
- // Programming errors (create() did not return an Endpoint) must surface:
504
- // they propagate to AdapterIndex.create's catch, which disposes the records
505
- // created so far instead of hiding the bug behind an unconfigured stub.
383
+ name: slot.localName,
384
+ }),
385
+ );
506
386
  assertEndpoint(endpoint, expansion?.id ?? slot.id);
507
387
  assertDeclaredEndpointOperations(
508
388
  endpoint,
509
389
  slot.definition.operations,
510
390
  String(expansion?.id ?? slot.id),
511
391
  );
512
- return { instance: endpoint, unconfigured: false };
513
- }
514
-
515
- function createUnconfiguredEndpoint(reason: string): EndpointInstance {
516
- return Object.freeze({
517
- start() {
518
- throw new Error(`Adapter unconfigured: ${reason}`);
519
- },
520
- open() {},
521
- close() {},
522
- stop() {},
523
- send() {
524
- throw new Error(`Adapter unconfigured: ${reason}`);
525
- },
526
- });
527
- }
528
-
529
- function withTimeout<T>(
530
- promise: Promise<T> | T | undefined,
531
- ms: number,
532
- message: string,
533
- ): Promise<T | undefined> {
534
- if (promise === undefined) return Promise.resolve(undefined);
535
- return new Promise<T | undefined>((resolve, reject) => {
536
- const timer = setTimeout(() => reject(new Error(message)), ms);
537
- Promise.resolve(promise).then(
538
- (value) => {
539
- clearTimeout(timer);
540
- resolve(value);
541
- },
542
- (error) => {
543
- clearTimeout(timer);
544
- reject(error);
545
- },
546
- );
547
- });
392
+ return endpoint;
548
393
  }
549
394
 
550
395
  async function stopRecords(
@@ -554,17 +399,13 @@ async function stopRecords(
554
399
  const stack = new DisposeStack();
555
400
  for (const record of records) {
556
401
  if (record.stopped) continue;
557
- stack.add(async () => {
558
- record.stopped = true;
559
- record.open = false;
560
- await record.endpoint.stop?.();
561
- });
402
+ stack.add(() => stopRecord(record));
562
403
  }
563
404
  try {
564
405
  await stack.dispose();
565
406
  } catch (stopError) {
566
407
  if (primaryError !== undefined) {
567
- throw new AggregateError(
408
+ throw new GenerationCompensationError(
568
409
  [primaryError, stopError],
569
410
  'Adapter prepare and Endpoint cleanup both failed',
570
411
  { cause: stopError },
@@ -573,3 +414,17 @@ async function stopRecords(
573
414
  throw stopError;
574
415
  }
575
416
  }
417
+
418
+ function stopRecord(record: AdapterRecord): Promise<void> {
419
+ if (record.stopped) return Promise.resolve();
420
+ if (record.stopping) return record.stopping;
421
+ const stopping = Promise.resolve(record.endpoint.stop?.()).then(() => {
422
+ record.stopped = true;
423
+ record.open = false;
424
+ });
425
+ record.stopping = stopping;
426
+ void stopping.catch(() => undefined).finally(() => {
427
+ if (!record.stopped && record.stopping === stopping) record.stopping = undefined;
428
+ });
429
+ return stopping;
430
+ }
package/src/definition.ts CHANGED
@@ -32,9 +32,9 @@ export interface EndpointInstance<TResult = unknown> {
32
32
  readonly management?: EndpointManagement;
33
33
  /** Optional platform-neutral control surface for existing messages. */
34
34
  readonly control?: EndpointControl;
35
- /** Allocates transport resources but must not admit inbound events yet. */
36
- start?(): void | Promise<void>;
37
- /** Opens admission after the candidate generation has committed. */
35
+ /** Required readiness; must observe abort and settle before rollback returns. */
36
+ start?(signal: AbortSignal): void | Promise<void>;
37
+ /** Opens Endpoint-local flow behind the candidate generation admission gate. */
38
38
  open?(): void;
39
39
  /** Stops new inbound events while preserving in-flight work. */
40
40
  close?(): void | Promise<void>;