@roll-agent/client-node 0.4.0 → 0.5.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.
@@ -27,11 +27,11 @@ type RuntimeEventRecoveryPhase = (typeof RECOVERY_PHASES)[keyof typeof RECOVERY_
27
27
  export type RuntimeEventRecoveryMode = (typeof RECOVERY_MODES)[keyof typeof RECOVERY_MODES];
28
28
  export type RuntimeEventRecoverySnapshotReason = (typeof RUNTIME_EVENT_RECOVERY_SNAPSHOT_REASONS)[keyof typeof RUNTIME_EVENT_RECOVERY_SNAPSHOT_REASONS];
29
29
  export type RuntimeDurableEventEnvelope = Extract<RuntimeEventEnvelope, {
30
- readonly protocolVersion: "1.3";
30
+ readonly protocolVersion: "1.3" | "1.4";
31
31
  readonly durability: "durable";
32
32
  }>;
33
33
  export type RuntimeEphemeralEventEnvelope = Extract<RuntimeEventEnvelope, {
34
- readonly protocolVersion: "1.3";
34
+ readonly protocolVersion: "1.3" | "1.4";
35
35
  readonly durability: "ephemeral";
36
36
  }>;
37
37
  export type RuntimeEventRecoverySnapshot = RuntimeMethodResultForVersion<RuntimeProtocolVersion, typeof RUNTIME_METHODS.threadSnapshot>;
@@ -66,13 +66,14 @@ export interface RuntimeEventRecoveryManagerOptions {
66
66
  }
67
67
  export type RuntimeEventRecoveryStartResult = {
68
68
  readonly mode: "resumed" | "snapshot-resumed";
69
- readonly protocolVersion: "1.3";
69
+ readonly protocolVersion: DurableRecoveryProtocolVersion;
70
70
  readonly checkpoint: RuntimeEventRecoveryCheckpoint;
71
71
  } | {
72
72
  readonly mode: "snapshot-only";
73
- readonly protocolVersion: Exclude<RuntimeProtocolVersion, "1.3">;
73
+ readonly protocolVersion: Exclude<RuntimeProtocolVersion, DurableRecoveryProtocolVersion>;
74
74
  readonly checkpoint: null;
75
75
  };
76
+ export type DurableRecoveryProtocolVersion = Extract<RuntimeProtocolVersion, "1.3" | "1.4">;
76
77
  /**
77
78
  * Internal transport bridge supplied by RollNodeClient.
78
79
  *
@@ -1 +1 @@
1
- import{createHash as e}from"node:crypto";import{RUNTIME_ERROR_CODES_V13 as t,RUNTIME_METHODS as r,RUNTIME_V13_DEFAULT_REPLAY_BUFFER_BYTES as n,RUNTIME_V13_MAX_DURABLE_EVENT_RECORDS as i,compareRuntimeEventCursors as o,runtimeEventCursorDistance as s}from"@roll-agent/protocol";export const DEFAULT_RUNTIME_EVENT_RECOVERY_MAX_BUFFERED_EVENTS=i;export const DEFAULT_RUNTIME_EVENT_RECOVERY_MAX_BUFFERED_BYTES=n;const a={buffering:"buffering",draining:"draining",live:"live",failed:"failed",stopped:"stopped"},u={resumed:"resumed",snapshotResumed:"snapshot-resumed",snapshotOnly:"snapshot-only"};export const RUNTIME_EVENT_RECOVERY_SNAPSHOT_REASONS={initial:"initial",runtimeRestarted:"runtime-restarted",cursorExpired:"cursor-expired",cursorGap:"cursor-gap",streamGap:"stream-gap",bufferOverflow:"buffer-overflow",cursorConflict:"cursor-conflict",protocolUnsupported:"protocol-unsupported"};export class RuntimeEventRecoveryError extends Error{constructor(e,t){super(e,t),this.name="RuntimeEventRecoveryError"}}class RuntimeEventStreamIntegrityError extends RuntimeEventRecoveryError{reason;constructor(e,t,r){super(t,r),this.name="RuntimeEventStreamIntegrityError",this.reason=e}}function c(e,t,r){const n=e??t;if(!Number.isSafeInteger(n)||n<=0)throw new Error(`${r} must be a positive safe integer`);return n}function E(e){return d(e).fingerprint}function d(t){const r=JSON.stringify({...t,sequence:void 0});return{fingerprint:e("sha256").update(r).digest("hex"),bytes:Buffer.byteLength(r,"utf8")}}function h(e){return e instanceof Error?e:new Error(String(e))}function f(e){if("object"==typeof e&&null!==e&&"data"in e&&"object"==typeof e.data&&null!==e.data&&"rollCode"in e.data&&"string"==typeof e.data.rollCode)return e.data.rollCode}function v(e){const r=f(e);return r===t.eventCursorExpired?RUNTIME_EVENT_RECOVERY_SNAPSHOT_REASONS.cursorExpired:r===t.eventCursorGap?RUNTIME_EVENT_RECOVERY_SNAPSHOT_REASONS.cursorGap:void 0}function l(e){if(!("eventCursor"in e))throw new RuntimeEventRecoveryError("Runtime Protocol 1.3 snapshot omitted eventCursor");return e.eventCursor}function p(e){if(!("recoveryProjection"in e)||!0!==e.recoveryProjection)throw new RuntimeEventRecoveryError("Runtime Protocol 1.3 recovery Snapshot omitted the bounded recovery projection marker")}function R(e,t){try{return o(e,t)}catch(e){throw new RuntimeEventStreamIntegrityError(RUNTIME_EVENT_RECOVERY_SNAPSHOT_REASONS.cursorConflict,"Runtime Event cursors are not comparable",{cause:e})}}function m(e,t){try{return s(e,t)}catch(e){throw new RuntimeEventStreamIntegrityError(RUNTIME_EVENT_RECOVERY_SNAPSHOT_REASONS.cursorConflict,"Runtime Event cursor distance is not comparable",{cause:e})}}function y(e){return"1.3"===e.protocolVersion&&"durable"===e.durability}function I(e){return"1.3"===e.protocolVersion&&"ephemeral"===e.durability}export class RuntimeEventRecoveryManager{bridge;maxBufferedEvents;maxBufferedBytes;states=new Map;checkpoints=new Map;closed=!1;constructor(e,t={}){this.bridge=e,this.maxBufferedEvents=c(t.maxBufferedEvents,DEFAULT_RUNTIME_EVENT_RECOVERY_MAX_BUFFERED_EVENTS,"maxBufferedEvents"),this.maxBufferedBytes=c(t.maxBufferedBytes,DEFAULT_RUNTIME_EVENT_RECOVERY_MAX_BUFFERED_BYTES,"maxBufferedBytes")}isClosed(){return this.closed}getCheckpoint(e){return this.checkpoints.get(e)}async resumeThread(e){if(this.closed)throw new RuntimeEventRecoveryError("Runtime Event recovery manager is closed");if(this.states.has(e.threadId))throw new RuntimeEventRecoveryError(`Runtime Event recovery already owns Thread ${JSON.stringify(e.threadId)}`);if(void 0!==e.checkpoint&&e.checkpoint.threadId!==e.threadId)throw new RuntimeEventRecoveryError("Runtime Event checkpoint belongs to another Thread");const t=this.bridge.getInitializationResult();if("1.3"!==t.protocolVersion){const r=await this.bridge.requestSnapshot(e.threadId);return await e.applySnapshot(r,{reason:RUNTIME_EVENT_RECOVERY_SNAPSHOT_REASONS.protocolUnsupported,protocolVersion:t.protocolVersion,recoveryProjection:!1}),{mode:u.snapshotOnly,protocolVersion:t.protocolVersion,checkpoint:null}}const r={threadId:e.threadId,runtimeInstanceId:t.runtimeInstanceId,cursor:e.checkpoint?.runtimeInstanceId===t.runtimeInstanceId?e.checkpoint.cursor:null},n={options:e,runtimeInstanceId:t.runtimeInstanceId,buffer:[],bufferedByEventId:new Map,bufferedByCursor:new Map,seenByEventId:new Map,seenByCursor:new Map,phase:a.buffering,checkpoint:r,bufferBytes:0,integrityError:void 0,liveDrain:void 0};this.states.set(e.threadId,n),this.checkpoints.set(e.threadId,r);try{let r;if(void 0===e.checkpoint)await this.snapshotAndResume(n,RUNTIME_EVENT_RECOVERY_SNAPSHOT_REASONS.initial),r=u.snapshotResumed;else if(e.checkpoint.runtimeInstanceId!==t.runtimeInstanceId)await this.snapshotAndResume(n,RUNTIME_EVENT_RECOVERY_SNAPSHOT_REASONS.runtimeRestarted),r=u.snapshotResumed;else try{await this.resumeFrom(n,e.checkpoint.cursor),r=u.resumed}catch(e){const t=v(e)??(e instanceof RuntimeEventStreamIntegrityError?e.reason:void 0);if(void 0===t)throw e;await this.snapshotAndResume(n,t),r=u.snapshotResumed}return this.assertActive(n),n.phase=a.live,this.scheduleLiveDrain(n),{mode:r,protocolVersion:"1.3",checkpoint:n.checkpoint}}catch(t){throw this.failState(n,h(t)),this.states.delete(e.threadId),t}}stopThread(e){const t=this.states.get(e);return void 0!==t&&(t.phase=a.stopped,this.states.delete(e),this.clearBuffer(t),!0)}close(){if(!this.closed){this.closed=!0;for(const e of this.states.values())e.phase=a.stopped,this.clearBuffer(e);this.states.clear()}}acceptEvent(e){const t=this.states.get(e.threadId);if(void 0===t)return!1;if(t.phase===a.stopped||t.phase===a.failed)return!0;if(e.runtimeInstanceId!==t.runtimeInstanceId)return this.markIntegrityFailure(t,new RuntimeEventStreamIntegrityError(RUNTIME_EVENT_RECOVERY_SNAPSHOT_REASONS.cursorConflict,"Runtime Event belongs to another Runtime instance")),!0;if(I(e)){if(t.phase===a.live)try{t.options.onEphemeralEvent?.(e)}catch{}return!0}return y(e)?(this.bufferDurableEvent(t,e),t.phase===a.live&&this.scheduleLiveDrain(t),!0):(this.markIntegrityFailure(t,new RuntimeEventStreamIntegrityError(RUNTIME_EVENT_RECOVERY_SNAPSHOT_REASONS.cursorConflict,"Claimed Thread received an event outside Runtime Protocol 1.3")),!0)}acceptClientExit(e){for(const t of this.states.values())this.failState(t,e)}async snapshotAndResume(e,t){let r=t;for(let t=0;t<2;t+=1){this.assertActive(e),e.phase=a.buffering;const n=await this.bridge.requestSnapshot(e.options.threadId);this.assertActive(e),p(n),await e.options.applySnapshot(n,{reason:r,protocolVersion:"1.3",recoveryProjection:!0}),this.assertActive(e);const i=l(n);this.clearBuffer(e),e.seenByEventId.clear(),e.seenByCursor.clear(),e.checkpoint={threadId:e.options.threadId,runtimeInstanceId:e.runtimeInstanceId,cursor:i},this.checkpoints.set(e.options.threadId,e.checkpoint);try{return void await this.resumeFrom(e,i)}catch(e){const n=v(e)??(e instanceof RuntimeEventStreamIntegrityError?e.reason:void 0);if(void 0===n||1===t)throw e;r=n}}throw new RuntimeEventRecoveryError("Runtime Event recovery could not converge on a Snapshot")}async resumeFrom(e,t){let r;this.assertActive(e),e.phase=a.buffering,e.integrityError=void 0;const n=await this.bridge.requestResume({threadId:e.options.threadId,afterCursor:t},e=>{r=e});this.assertActive(e);const i=r??n;e.phase=a.draining,this.throwIntegrityError(e);const o=this.takeBuffer(e),s=this.validateBarrier(o,t,i);for(await this.deliverBatch(e,s,t);e.buffer.length>0;){this.throwIntegrityError(e);const t=this.prepareBatch(e,this.takeBuffer(e));await this.deliverBatch(e,t,e.checkpoint.cursor)}this.throwIntegrityError(e)}validateBarrier(e,t,r){const n=this.prepareBatch(void 0,e).filter(e=>null===t||R(e.cursor,t)>0),i=null===r.throughCursor?[]:n.filter(e=>R(e.cursor,r.throughCursor)<=0);if(i.length!==r.replayedCount)throw new RuntimeEventStreamIntegrityError(RUNTIME_EVENT_RECOVERY_SNAPSHOT_REASONS.streamGap,`Runtime Event replay announced ${String(r.replayedCount)} events but delivered ${String(i.length)}`);if(0===r.replayedCount){if(0!==R(t,r.throughCursor))throw new RuntimeEventStreamIntegrityError(RUNTIME_EVENT_RECOVERY_SNAPSHOT_REASONS.streamGap,"Runtime Event replay returned an invalid empty barrier")}else{const e=i.at(-1);if(void 0===e||null===r.throughCursor||0!==R(e.cursor,r.throughCursor))throw new RuntimeEventStreamIntegrityError(RUNTIME_EVENT_RECOVERY_SNAPSHOT_REASONS.streamGap,"Runtime Event replay did not reach its throughCursor barrier");if(m(t,r.throughCursor)!==BigInt(r.replayedCount))throw new RuntimeEventStreamIntegrityError(RUNTIME_EVENT_RECOVERY_SNAPSHOT_REASONS.streamGap,"Runtime Event replay barrier contains a cursor gap")}return n}prepareBatch(e,t){return t.sort((e,t)=>R(e.cursor,t.cursor)),void 0===e?t:t.filter(t=>{const r=e.seenByEventId.get(t.eventId);if(void 0!==r){if(r.cursor!==t.cursor||r.fingerprint!==E(t))throw new RuntimeEventStreamIntegrityError(RUNTIME_EVENT_RECOVERY_SNAPSHOT_REASONS.cursorConflict,"Runtime Event reused an eventId with conflicting content");return!1}const n=e.seenByCursor.get(t.cursor);if(void 0!==n&&n!==t.eventId)throw new RuntimeEventStreamIntegrityError(RUNTIME_EVENT_RECOVERY_SNAPSHOT_REASONS.cursorConflict,"Runtime Event cursor was reused by another eventId");return!0})}async deliverBatch(e,t,r){let n=r;for(const r of t){this.assertActive(e);const t=m(n,r.cursor);if(t<=0n)continue;if(1n!==t)throw new RuntimeEventStreamIntegrityError(RUNTIME_EVENT_RECOVERY_SNAPSHOT_REASONS.streamGap,"Runtime Event stream contains a cursor gap");const i={threadId:e.options.threadId,runtimeInstanceId:e.runtimeInstanceId,cursor:r.cursor};await e.options.onDurableEvent(r,i),this.assertActive(e),e.checkpoint=i,this.checkpoints.set(e.options.threadId,i),this.rememberDelivered(e,r),n=r.cursor}}bufferDurableEvent(e,t){const{fingerprint:r,bytes:n}=d(t),i=e.seenByEventId.get(t.eventId);if(void 0!==i)return void(i.cursor===t.cursor&&i.fingerprint===r||this.markIntegrityFailure(e,new RuntimeEventStreamIntegrityError(RUNTIME_EVENT_RECOVERY_SNAPSHOT_REASONS.cursorConflict,"Runtime Event reused a delivered eventId with conflicting content")));const o=e.bufferedByEventId.get(t.eventId);if(void 0!==o)return void(o.cursor===t.cursor&&o.fingerprint===r||this.markIntegrityFailure(e,new RuntimeEventStreamIntegrityError(RUNTIME_EVENT_RECOVERY_SNAPSHOT_REASONS.cursorConflict,"Runtime Event reused a buffered eventId with conflicting content")));const s=e.bufferedByCursor.get(t.cursor)??e.seenByCursor.get(t.cursor);if(void 0===s||s===t.eventId){if(e.buffer.length+1>this.maxBufferedEvents||e.bufferBytes+n>this.maxBufferedBytes)return this.clearBuffer(e),void this.markIntegrityFailure(e,new RuntimeEventStreamIntegrityError(RUNTIME_EVENT_RECOVERY_SNAPSHOT_REASONS.bufferOverflow,"Runtime Event recovery buffer exceeded its bounded capacity"));e.buffer.push(t),e.bufferBytes+=n,e.bufferedByEventId.set(t.eventId,{cursor:t.cursor,fingerprint:r}),e.bufferedByCursor.set(t.cursor,t.eventId)}else this.markIntegrityFailure(e,new RuntimeEventStreamIntegrityError(RUNTIME_EVENT_RECOVERY_SNAPSHOT_REASONS.cursorConflict,"Runtime Event cursor was reused by another eventId"))}takeBuffer(e){const t=e.buffer.splice(0);return e.bufferBytes=0,e.bufferedByEventId.clear(),e.bufferedByCursor.clear(),t}clearBuffer(e){e.buffer.splice(0),e.bufferBytes=0,e.bufferedByEventId.clear(),e.bufferedByCursor.clear(),e.integrityError=void 0}rememberDelivered(e,t){for(e.seenByEventId.set(t.eventId,{cursor:t.cursor,fingerprint:E(t)}),e.seenByCursor.set(t.cursor,t.eventId);e.seenByEventId.size>this.maxBufferedEvents;){const t=e.seenByEventId.keys().next().value;if(void 0===t)break;const r=e.seenByEventId.get(t);e.seenByEventId.delete(t),void 0!==r&&e.seenByCursor.delete(r.cursor)}}markIntegrityFailure(e,t){e.integrityError??=t,e.phase===a.live&&this.scheduleLiveDrain(e)}throwIntegrityError(e){if(void 0!==e.integrityError)throw e.integrityError}scheduleLiveDrain(e){void 0!==e.liveDrain||e.phase!==a.live||0===e.buffer.length&&void 0===e.integrityError||(e.liveDrain=this.drainLive(e).finally(()=>{e.liveDrain=void 0,e.phase===a.live&&this.scheduleLiveDrain(e)}))}async drainLive(e){try{for(this.throwIntegrityError(e);e.buffer.length>0;){const t=this.prepareBatch(e,this.takeBuffer(e));await this.deliverBatch(e,t,e.checkpoint.cursor),this.throwIntegrityError(e)}}catch(t){if(t instanceof RuntimeEventStreamIntegrityError)try{return await this.snapshotAndResume(e,t.reason),this.assertActive(e),void(e.phase=a.live)}catch(t){return void this.failState(e,h(t))}this.failState(e,h(t))}}assertActive(e){if(this.closed||this.states.get(e.options.threadId)!==e||e.phase===a.stopped||e.phase===a.failed)throw new RuntimeEventRecoveryError("Runtime Event recovery was stopped")}failState(e,t){if(e.phase!==a.failed&&e.phase!==a.stopped){e.phase=a.failed,this.clearBuffer(e);try{e.options.onError?.(t,{threadId:e.options.threadId,phase:a.failed})}catch{}}}}
1
+ import{createHash as e}from"node:crypto";import{RUNTIME_ERROR_CODES_V13 as t,RUNTIME_METHODS as r,RUNTIME_V13_DEFAULT_REPLAY_BUFFER_BYTES as n,RUNTIME_V13_MAX_DURABLE_EVENT_RECORDS as i,compareRuntimeEventCursors as o,runtimeEventCursorDistance as s}from"@roll-agent/protocol";export const DEFAULT_RUNTIME_EVENT_RECOVERY_MAX_BUFFERED_EVENTS=i;export const DEFAULT_RUNTIME_EVENT_RECOVERY_MAX_BUFFERED_BYTES=n;const a={buffering:"buffering",draining:"draining",live:"live",failed:"failed",stopped:"stopped"},u={resumed:"resumed",snapshotResumed:"snapshot-resumed",snapshotOnly:"snapshot-only"};export const RUNTIME_EVENT_RECOVERY_SNAPSHOT_REASONS={initial:"initial",runtimeRestarted:"runtime-restarted",cursorExpired:"cursor-expired",cursorGap:"cursor-gap",streamGap:"stream-gap",bufferOverflow:"buffer-overflow",cursorConflict:"cursor-conflict",protocolUnsupported:"protocol-unsupported"};function c(e){return"1.3"===e||"1.4"===e}export class RuntimeEventRecoveryError extends Error{constructor(e,t){super(e,t),this.name="RuntimeEventRecoveryError"}}class RuntimeEventStreamIntegrityError extends RuntimeEventRecoveryError{reason;constructor(e,t,r){super(t,r),this.name="RuntimeEventStreamIntegrityError",this.reason=e}}function E(e,t,r){const n=e??t;if(!Number.isSafeInteger(n)||n<=0)throw new Error(`${r} must be a positive safe integer`);return n}function d(e){return h(e).fingerprint}function h(t){const r=JSON.stringify({...t,sequence:void 0});return{fingerprint:e("sha256").update(r).digest("hex"),bytes:Buffer.byteLength(r,"utf8")}}function f(e){return e instanceof Error?e:new Error(String(e))}function v(e){if("object"==typeof e&&null!==e&&"data"in e&&"object"==typeof e.data&&null!==e.data&&"rollCode"in e.data&&"string"==typeof e.data.rollCode)return e.data.rollCode}function l(e){const r=v(e);return r===t.eventCursorExpired?RUNTIME_EVENT_RECOVERY_SNAPSHOT_REASONS.cursorExpired:r===t.eventCursorGap?RUNTIME_EVENT_RECOVERY_SNAPSHOT_REASONS.cursorGap:void 0}function p(e){if(!("eventCursor"in e))throw new RuntimeEventRecoveryError("Runtime Protocol 1.3 snapshot omitted eventCursor");return e.eventCursor}function R(e){if(!("recoveryProjection"in e)||!0!==e.recoveryProjection)throw new RuntimeEventRecoveryError("Runtime Protocol 1.3 recovery Snapshot omitted the bounded recovery projection marker")}function m(e,t){try{return o(e,t)}catch(e){throw new RuntimeEventStreamIntegrityError(RUNTIME_EVENT_RECOVERY_SNAPSHOT_REASONS.cursorConflict,"Runtime Event cursors are not comparable",{cause:e})}}function y(e,t){try{return s(e,t)}catch(e){throw new RuntimeEventStreamIntegrityError(RUNTIME_EVENT_RECOVERY_SNAPSHOT_REASONS.cursorConflict,"Runtime Event cursor distance is not comparable",{cause:e})}}function I(e){return("1.3"===e.protocolVersion||"1.4"===e.protocolVersion)&&"durable"===e.durability}function S(e){return("1.3"===e.protocolVersion||"1.4"===e.protocolVersion)&&"ephemeral"===e.durability}export class RuntimeEventRecoveryManager{bridge;maxBufferedEvents;maxBufferedBytes;states=new Map;checkpoints=new Map;closed=!1;constructor(e,t={}){this.bridge=e,this.maxBufferedEvents=E(t.maxBufferedEvents,DEFAULT_RUNTIME_EVENT_RECOVERY_MAX_BUFFERED_EVENTS,"maxBufferedEvents"),this.maxBufferedBytes=E(t.maxBufferedBytes,DEFAULT_RUNTIME_EVENT_RECOVERY_MAX_BUFFERED_BYTES,"maxBufferedBytes")}isClosed(){return this.closed}getCheckpoint(e){return this.checkpoints.get(e)}async resumeThread(e){if(this.closed)throw new RuntimeEventRecoveryError("Runtime Event recovery manager is closed");if(this.states.has(e.threadId))throw new RuntimeEventRecoveryError(`Runtime Event recovery already owns Thread ${JSON.stringify(e.threadId)}`);if(void 0!==e.checkpoint&&e.checkpoint.threadId!==e.threadId)throw new RuntimeEventRecoveryError("Runtime Event checkpoint belongs to another Thread");const t=this.bridge.getInitializationResult();if(!c(t.protocolVersion)){const r=await this.bridge.requestSnapshot(e.threadId);return await e.applySnapshot(r,{reason:RUNTIME_EVENT_RECOVERY_SNAPSHOT_REASONS.protocolUnsupported,protocolVersion:t.protocolVersion,recoveryProjection:"recoveryProjection"in r&&!0===r.recoveryProjection}),{mode:u.snapshotOnly,protocolVersion:t.protocolVersion,checkpoint:null}}const r={threadId:e.threadId,runtimeInstanceId:t.runtimeInstanceId,cursor:e.checkpoint?.runtimeInstanceId===t.runtimeInstanceId?e.checkpoint.cursor:null},n={options:e,runtimeInstanceId:t.runtimeInstanceId,buffer:[],bufferedByEventId:new Map,bufferedByCursor:new Map,seenByEventId:new Map,seenByCursor:new Map,phase:a.buffering,checkpoint:r,bufferBytes:0,integrityError:void 0,liveDrain:void 0};this.states.set(e.threadId,n),this.checkpoints.set(e.threadId,r);try{let r;if(void 0===e.checkpoint)await this.snapshotAndResume(n,RUNTIME_EVENT_RECOVERY_SNAPSHOT_REASONS.initial),r=u.snapshotResumed;else if(e.checkpoint.runtimeInstanceId!==t.runtimeInstanceId)await this.snapshotAndResume(n,RUNTIME_EVENT_RECOVERY_SNAPSHOT_REASONS.runtimeRestarted),r=u.snapshotResumed;else try{await this.resumeFrom(n,e.checkpoint.cursor),r=u.resumed}catch(e){const t=l(e)??(e instanceof RuntimeEventStreamIntegrityError?e.reason:void 0);if(void 0===t)throw e;await this.snapshotAndResume(n,t),r=u.snapshotResumed}return this.assertActive(n),n.phase=a.live,this.scheduleLiveDrain(n),{mode:r,protocolVersion:t.protocolVersion,checkpoint:n.checkpoint}}catch(t){throw this.failState(n,f(t)),this.states.delete(e.threadId),t}}stopThread(e){const t=this.states.get(e);return void 0!==t&&(t.phase=a.stopped,this.states.delete(e),this.clearBuffer(t),!0)}close(){if(!this.closed){this.closed=!0;for(const e of this.states.values())e.phase=a.stopped,this.clearBuffer(e);this.states.clear()}}acceptEvent(e){const t=this.states.get(e.threadId);if(void 0===t)return!1;if(t.phase===a.stopped||t.phase===a.failed)return!0;if(e.runtimeInstanceId!==t.runtimeInstanceId)return this.markIntegrityFailure(t,new RuntimeEventStreamIntegrityError(RUNTIME_EVENT_RECOVERY_SNAPSHOT_REASONS.cursorConflict,"Runtime Event belongs to another Runtime instance")),!0;if(S(e)){if(t.phase===a.live)try{t.options.onEphemeralEvent?.(e)}catch{}return!0}return I(e)?(this.bufferDurableEvent(t,e),t.phase===a.live&&this.scheduleLiveDrain(t),!0):(this.markIntegrityFailure(t,new RuntimeEventStreamIntegrityError(RUNTIME_EVENT_RECOVERY_SNAPSHOT_REASONS.cursorConflict,"Claimed Thread received an event outside Runtime Protocol 1.3")),!0)}acceptClientExit(e){for(const t of this.states.values())this.failState(t,e)}async snapshotAndResume(e,t){let r=t;for(let t=0;t<2;t+=1){this.assertActive(e),e.phase=a.buffering;const n=await this.bridge.requestSnapshot(e.options.threadId);this.assertActive(e),R(n),await e.options.applySnapshot(n,{reason:r,protocolVersion:this.bridge.getInitializationResult().protocolVersion,recoveryProjection:!0}),this.assertActive(e);const i=p(n);this.clearBuffer(e),e.seenByEventId.clear(),e.seenByCursor.clear(),e.checkpoint={threadId:e.options.threadId,runtimeInstanceId:e.runtimeInstanceId,cursor:i},this.checkpoints.set(e.options.threadId,e.checkpoint);try{return void await this.resumeFrom(e,i)}catch(e){const n=l(e)??(e instanceof RuntimeEventStreamIntegrityError?e.reason:void 0);if(void 0===n||1===t)throw e;r=n}}throw new RuntimeEventRecoveryError("Runtime Event recovery could not converge on a Snapshot")}async resumeFrom(e,t){let r;this.assertActive(e),e.phase=a.buffering,e.integrityError=void 0;const n=await this.bridge.requestResume({threadId:e.options.threadId,afterCursor:t},e=>{r=e});this.assertActive(e);const i=r??n;e.phase=a.draining,this.throwIntegrityError(e);const o=this.takeBuffer(e),s=this.validateBarrier(o,t,i);for(await this.deliverBatch(e,s,t);e.buffer.length>0;){this.throwIntegrityError(e);const t=this.prepareBatch(e,this.takeBuffer(e));await this.deliverBatch(e,t,e.checkpoint.cursor)}this.throwIntegrityError(e)}validateBarrier(e,t,r){const n=this.prepareBatch(void 0,e).filter(e=>null===t||m(e.cursor,t)>0),i=null===r.throughCursor?[]:n.filter(e=>m(e.cursor,r.throughCursor)<=0);if(i.length!==r.replayedCount)throw new RuntimeEventStreamIntegrityError(RUNTIME_EVENT_RECOVERY_SNAPSHOT_REASONS.streamGap,`Runtime Event replay announced ${String(r.replayedCount)} events but delivered ${String(i.length)}`);if(0===r.replayedCount){if(0!==m(t,r.throughCursor))throw new RuntimeEventStreamIntegrityError(RUNTIME_EVENT_RECOVERY_SNAPSHOT_REASONS.streamGap,"Runtime Event replay returned an invalid empty barrier")}else{const e=i.at(-1);if(void 0===e||null===r.throughCursor||0!==m(e.cursor,r.throughCursor))throw new RuntimeEventStreamIntegrityError(RUNTIME_EVENT_RECOVERY_SNAPSHOT_REASONS.streamGap,"Runtime Event replay did not reach its throughCursor barrier");if(y(t,r.throughCursor)!==BigInt(r.replayedCount))throw new RuntimeEventStreamIntegrityError(RUNTIME_EVENT_RECOVERY_SNAPSHOT_REASONS.streamGap,"Runtime Event replay barrier contains a cursor gap")}return n}prepareBatch(e,t){return t.sort((e,t)=>m(e.cursor,t.cursor)),void 0===e?t:t.filter(t=>{const r=e.seenByEventId.get(t.eventId);if(void 0!==r){if(r.cursor!==t.cursor||r.fingerprint!==d(t))throw new RuntimeEventStreamIntegrityError(RUNTIME_EVENT_RECOVERY_SNAPSHOT_REASONS.cursorConflict,"Runtime Event reused an eventId with conflicting content");return!1}const n=e.seenByCursor.get(t.cursor);if(void 0!==n&&n!==t.eventId)throw new RuntimeEventStreamIntegrityError(RUNTIME_EVENT_RECOVERY_SNAPSHOT_REASONS.cursorConflict,"Runtime Event cursor was reused by another eventId");return!0})}async deliverBatch(e,t,r){let n=r;for(const r of t){this.assertActive(e);const t=y(n,r.cursor);if(t<=0n)continue;if(1n!==t)throw new RuntimeEventStreamIntegrityError(RUNTIME_EVENT_RECOVERY_SNAPSHOT_REASONS.streamGap,"Runtime Event stream contains a cursor gap");const i={threadId:e.options.threadId,runtimeInstanceId:e.runtimeInstanceId,cursor:r.cursor};await e.options.onDurableEvent(r,i),this.assertActive(e),e.checkpoint=i,this.checkpoints.set(e.options.threadId,i),this.rememberDelivered(e,r),n=r.cursor}}bufferDurableEvent(e,t){const{fingerprint:r,bytes:n}=h(t),i=e.seenByEventId.get(t.eventId);if(void 0!==i)return void(i.cursor===t.cursor&&i.fingerprint===r||this.markIntegrityFailure(e,new RuntimeEventStreamIntegrityError(RUNTIME_EVENT_RECOVERY_SNAPSHOT_REASONS.cursorConflict,"Runtime Event reused a delivered eventId with conflicting content")));const o=e.bufferedByEventId.get(t.eventId);if(void 0!==o)return void(o.cursor===t.cursor&&o.fingerprint===r||this.markIntegrityFailure(e,new RuntimeEventStreamIntegrityError(RUNTIME_EVENT_RECOVERY_SNAPSHOT_REASONS.cursorConflict,"Runtime Event reused a buffered eventId with conflicting content")));const s=e.bufferedByCursor.get(t.cursor)??e.seenByCursor.get(t.cursor);if(void 0===s||s===t.eventId){if(e.buffer.length+1>this.maxBufferedEvents||e.bufferBytes+n>this.maxBufferedBytes)return this.clearBuffer(e),void this.markIntegrityFailure(e,new RuntimeEventStreamIntegrityError(RUNTIME_EVENT_RECOVERY_SNAPSHOT_REASONS.bufferOverflow,"Runtime Event recovery buffer exceeded its bounded capacity"));e.buffer.push(t),e.bufferBytes+=n,e.bufferedByEventId.set(t.eventId,{cursor:t.cursor,fingerprint:r}),e.bufferedByCursor.set(t.cursor,t.eventId)}else this.markIntegrityFailure(e,new RuntimeEventStreamIntegrityError(RUNTIME_EVENT_RECOVERY_SNAPSHOT_REASONS.cursorConflict,"Runtime Event cursor was reused by another eventId"))}takeBuffer(e){const t=e.buffer.splice(0);return e.bufferBytes=0,e.bufferedByEventId.clear(),e.bufferedByCursor.clear(),t}clearBuffer(e){e.buffer.splice(0),e.bufferBytes=0,e.bufferedByEventId.clear(),e.bufferedByCursor.clear(),e.integrityError=void 0}rememberDelivered(e,t){for(e.seenByEventId.set(t.eventId,{cursor:t.cursor,fingerprint:d(t)}),e.seenByCursor.set(t.cursor,t.eventId);e.seenByEventId.size>this.maxBufferedEvents;){const t=e.seenByEventId.keys().next().value;if(void 0===t)break;const r=e.seenByEventId.get(t);e.seenByEventId.delete(t),void 0!==r&&e.seenByCursor.delete(r.cursor)}}markIntegrityFailure(e,t){e.integrityError??=t,e.phase===a.live&&this.scheduleLiveDrain(e)}throwIntegrityError(e){if(void 0!==e.integrityError)throw e.integrityError}scheduleLiveDrain(e){void 0!==e.liveDrain||e.phase!==a.live||0===e.buffer.length&&void 0===e.integrityError||(e.liveDrain=this.drainLive(e).finally(()=>{e.liveDrain=void 0,e.phase===a.live&&this.scheduleLiveDrain(e)}))}async drainLive(e){try{for(this.throwIntegrityError(e);e.buffer.length>0;){const t=this.prepareBatch(e,this.takeBuffer(e));await this.deliverBatch(e,t,e.checkpoint.cursor),this.throwIntegrityError(e)}}catch(t){if(t instanceof RuntimeEventStreamIntegrityError)try{return await this.snapshotAndResume(e,t.reason),this.assertActive(e),void(e.phase=a.live)}catch(t){return void this.failState(e,f(t))}this.failState(e,f(t))}}assertActive(e){if(this.closed||this.states.get(e.options.threadId)!==e||e.phase===a.stopped||e.phase===a.failed)throw new RuntimeEventRecoveryError("Runtime Event recovery was stopped")}failState(e,t){if(e.phase!==a.failed&&e.phase!==a.stopped){e.phase=a.failed,this.clearBuffer(e);try{e.options.onError?.(t,{threadId:e.options.threadId,phase:a.failed})}catch{}}}}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@roll-agent/client-node",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "type": "module",
5
5
  "repository": {
6
6
  "type": "git",