@blocks-network/sdk 0.1.45

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 (74) hide show
  1. package/README.md +632 -0
  2. package/dist/cli/run.d.ts +39 -0
  3. package/dist/cli/run.js +2 -0
  4. package/dist/config-loader.d.ts +10 -0
  5. package/dist/config-loader.js +1 -0
  6. package/dist/defaults.d.ts +7 -0
  7. package/dist/defaults.js +1 -0
  8. package/dist/env.d.ts +5 -0
  9. package/dist/env.js +1 -0
  10. package/dist/index.d.ts +28 -0
  11. package/dist/index.js +1 -0
  12. package/dist/runtime/agent-auth.d.ts +94 -0
  13. package/dist/runtime/agent-auth.js +1 -0
  14. package/dist/runtime/agent-instance.d.ts +233 -0
  15. package/dist/runtime/agent-instance.js +1 -0
  16. package/dist/runtime/agent-registry.d.ts +270 -0
  17. package/dist/runtime/agent-registry.js +1 -0
  18. package/dist/runtime/artifacts.d.ts +65 -0
  19. package/dist/runtime/artifacts.js +1 -0
  20. package/dist/runtime/auth-provider.d.ts +39 -0
  21. package/dist/runtime/auth-provider.js +1 -0
  22. package/dist/runtime/cdm-config.d.ts +16 -0
  23. package/dist/runtime/cdm-config.js +1 -0
  24. package/dist/runtime/channel-manager.d.ts +125 -0
  25. package/dist/runtime/channel-manager.js +1 -0
  26. package/dist/runtime/consumer-auth.d.ts +112 -0
  27. package/dist/runtime/consumer-auth.js +1 -0
  28. package/dist/runtime/credential-cache.d.ts +40 -0
  29. package/dist/runtime/credential-cache.js +1 -0
  30. package/dist/runtime/file-input.d.ts +37 -0
  31. package/dist/runtime/file-input.js +1 -0
  32. package/dist/runtime/file-upload.d.ts +112 -0
  33. package/dist/runtime/file-upload.js +1 -0
  34. package/dist/runtime/part-helpers.d.ts +52 -0
  35. package/dist/runtime/part-helpers.js +1 -0
  36. package/dist/runtime/protocol-version.d.ts +18 -0
  37. package/dist/runtime/protocol-version.js +1 -0
  38. package/dist/runtime/pubnub-client.d.ts +8 -0
  39. package/dist/runtime/pubnub-client.js +1 -0
  40. package/dist/runtime/pubnub-types.d.ts +119 -0
  41. package/dist/runtime/pubnub-types.js +1 -0
  42. package/dist/runtime/rpc-client.d.ts +45 -0
  43. package/dist/runtime/rpc-client.js +1 -0
  44. package/dist/runtime/stream-context.d.ts +69 -0
  45. package/dist/runtime/stream-context.js +1 -0
  46. package/dist/runtime/stream-ref.d.ts +74 -0
  47. package/dist/runtime/stream-ref.js +1 -0
  48. package/dist/runtime/stream-registry.d.ts +133 -0
  49. package/dist/runtime/stream-registry.js +1 -0
  50. package/dist/runtime/stream-setup-helper.d.ts +87 -0
  51. package/dist/runtime/stream-setup-helper.js +1 -0
  52. package/dist/runtime/task-client.d.ts +259 -0
  53. package/dist/runtime/task-client.js +1 -0
  54. package/dist/runtime/task-session.d.ts +224 -0
  55. package/dist/runtime/task-session.js +1 -0
  56. package/dist/runtime/write-affinity.d.ts +22 -0
  57. package/dist/runtime/write-affinity.js +1 -0
  58. package/dist/runtime/write-affinity.test.d.ts +1 -0
  59. package/dist/runtime/write-affinity.test.js +1 -0
  60. package/dist/stream/bytes.d.ts +14 -0
  61. package/dist/stream/bytes.js +1 -0
  62. package/dist/stream/descriptor.d.ts +47 -0
  63. package/dist/stream/descriptor.js +1 -0
  64. package/dist/stream/index.d.ts +11 -0
  65. package/dist/stream/index.js +1 -0
  66. package/dist/stream/stream-bundle.d.ts +82 -0
  67. package/dist/stream/stream-bundle.js +1 -0
  68. package/dist/stream/stream-client.d.ts +192 -0
  69. package/dist/stream/stream-client.js +1 -0
  70. package/dist/stream/types.d.ts +75 -0
  71. package/dist/stream/types.js +1 -0
  72. package/dist/stream/validate.d.ts +15 -0
  73. package/dist/stream/validate.js +1 -0
  74. package/package.json +74 -0
@@ -0,0 +1,224 @@
1
+ /**
2
+ * TaskSession - Consumer-side task session with eager subscription.
3
+ *
4
+ * Replaces SendMessageResult. Owns one task's channel subscription,
5
+ * parsed task events, discovered streams, and cleanup. Auto-closes
6
+ * on terminal event.
7
+ */
8
+ import PubNub from 'pubnub';
9
+ import { StreamClient, type StreamClientFromDescriptorOptions } from '../stream/index.js';
10
+ import { StreamRef } from './stream-ref.js';
11
+ import { type RpcClientConfig } from './rpc-client.js';
12
+ import { type ArtifactRef, type DownloadedArtifact } from './artifacts.js';
13
+ export type Unsubscribe = () => void;
14
+ export declare const TERMINAL_STATES: Set<string>;
15
+ export interface TaskEvent {
16
+ type: string;
17
+ taskId: string;
18
+ [key: string]: unknown;
19
+ }
20
+ export interface ProgressEvent {
21
+ type: 'progress';
22
+ taskId: string;
23
+ message?: string;
24
+ progress?: number;
25
+ streamEvent?: string;
26
+ [key: string]: unknown;
27
+ }
28
+ export interface ArtifactEvent {
29
+ type: 'artifact';
30
+ taskId: string;
31
+ artifactRef: ArtifactRef;
32
+ outputId?: string;
33
+ [key: string]: unknown;
34
+ }
35
+ export interface TerminalEvent {
36
+ type: 'terminal';
37
+ taskId: string;
38
+ state: 'completed' | 'failed' | 'canceled';
39
+ reason?: string;
40
+ error?: string;
41
+ [key: string]: unknown;
42
+ }
43
+ export interface CallbackErrorContext {
44
+ entryPoint: 'taskSession' | 'subscribeToTask';
45
+ callbackType: 'onProgress' | 'onArtifact' | 'onTerminal' | 'onSystem' | 'onEvent' | 'onStream' | 'streamPredicate';
46
+ event: TaskEvent | StreamRef;
47
+ }
48
+ export declare class TaskSession {
49
+ readonly taskId: string;
50
+ readonly ownerId: string;
51
+ readonly orgId: string;
52
+ readonly readToken: string | null;
53
+ readonly statusChannel: string;
54
+ private readonly pubnub;
55
+ private readonly ownsSubscribeClient;
56
+ private readonly sdkOptions;
57
+ private readonly agentName;
58
+ private readonly rpcConfig;
59
+ private readonly subscribeKey;
60
+ private readonly publishKey;
61
+ private progressCallbacks;
62
+ private artifactCallbacks;
63
+ private terminalCallbacks;
64
+ private eventCallbacks;
65
+ private streamCallbacks;
66
+ private errorCallbacks;
67
+ private streams;
68
+ private artifacts;
69
+ private streamWaiters;
70
+ private terminalWaiters;
71
+ private closed;
72
+ private listener;
73
+ private readonly autoDrain;
74
+ private terminalReceived;
75
+ private drainTimer;
76
+ private readonly drainWindowMs;
77
+ private openStreamClients;
78
+ private readonly seenTimetokens;
79
+ private static readonly SEEN_TIMETOKENS_MAX;
80
+ /** RPC response metadata from sendMessage(). */
81
+ readonly idempotent?: boolean;
82
+ readonly queued?: boolean;
83
+ readonly pushConfigId?: string;
84
+ /**
85
+ * Current session state. Set from history at construction time (for
86
+ * pre-closed idempotent hits and connect() sessions) and kept current
87
+ * on live `terminal` events. Treat as a live observable rather than an
88
+ * immutable snapshot: `StreamRef.open()` consults this value via its
89
+ * `sessionState` hook to short-circuit on terminal sessions.
90
+ */
91
+ state?: string;
92
+ /** Whether this is a skipSubscription session (terminal connect). */
93
+ private readonly skipSubscriptionMode;
94
+ constructor(opts: {
95
+ taskId: string;
96
+ ownerId: string;
97
+ orgId?: string;
98
+ readToken: string | null;
99
+ statusChannel?: string;
100
+ agentName: string;
101
+ pubnub: PubNub | null;
102
+ ownsSubscribeClient?: boolean;
103
+ sdkOptions: StreamClientFromDescriptorOptions;
104
+ rpcConfig?: RpcClientConfig;
105
+ idempotent?: boolean;
106
+ queued?: boolean;
107
+ pushConfigId?: string;
108
+ autoDrain?: boolean;
109
+ /**
110
+ * Duration in milliseconds the session waits for already-open streams
111
+ * to finish draining naturally after a terminal event. Defaults to
112
+ * 30000 ms (30 seconds). Ignored when `autoDrain` is false.
113
+ *
114
+ * Only applies to streams that were opened while the task was still
115
+ * active. Unopened streams on a terminal session throw
116
+ * `StreamUnavailableError` per the merged t7c baseline.
117
+ */
118
+ drainWindowMs?: number;
119
+ /**
120
+ * When true, the session starts already closed. Used for terminal
121
+ * idempotent hits where the task is already in a terminal state and
122
+ * no PubNub subscription is needed.
123
+ */
124
+ preClosed?: boolean;
125
+ /** The terminal state of the task (e.g., "completed", "failed", "canceled"). */
126
+ state?: string;
127
+ /** Skip PubNub subscription but keep client alive for downloadArtifact().
128
+ * Used by connect() for terminal tasks. */
129
+ skipSubscription?: boolean;
130
+ /** Pre-populated stream refs from history (connect()). */
131
+ preloadedStreams?: Map<string, StreamRef>;
132
+ /** Pre-populated artifact refs from history (connect()). */
133
+ preloadedArtifacts?: ArtifactRef[];
134
+ /** External subscription managed by connect() for active tasks. */
135
+ externalSubscription?: {
136
+ listener: unknown;
137
+ channel: string;
138
+ onReady: (dispatch: (event: TaskEvent, timetoken?: string) => void) => void;
139
+ };
140
+ });
141
+ private setupSubscription;
142
+ private routeCallbackError;
143
+ private handleEvent;
144
+ /** Register a stream client for auto-drain tracking. */
145
+ private trackStreamClient;
146
+ private accumulateArtifact;
147
+ private handleStreamStarted;
148
+ private startAutoDrain;
149
+ /**
150
+ * Find a stream by runtime stream ID or declared stream key (Fix 12).
151
+ * Checks the map key first (runtime ID), then falls back to scanning
152
+ * descriptors for a matching declaredStream.
153
+ */
154
+ private findStreamByIdOrDeclared;
155
+ private resolveWaiters;
156
+ onProgress(cb: (event: ProgressEvent) => void): Unsubscribe;
157
+ onArtifact(cb: (event: ArtifactEvent) => void): Unsubscribe;
158
+ onTerminal(cb: (event: TerminalEvent) => void): Unsubscribe;
159
+ onEvent(cb: (event: TaskEvent) => void): Unsubscribe;
160
+ /**
161
+ * Register an error handler for callback exceptions.
162
+ * If registered, callback errors are routed here instead of console.warn.
163
+ * Returns an unsubscribe function.
164
+ */
165
+ onError(cb: (error: Error, context: CallbackErrorContext) => void): Unsubscribe;
166
+ /** Return all artifact refs seen so far (from history and live events). */
167
+ listArtifacts(): ArtifactRef[];
168
+ /**
169
+ * Download an artifact from an ArtifactRef.
170
+ * Delegates to the standalone downloadArtifact() helper.
171
+ * If the session has no active PubNub client (e.g., pre-closed or skipSubscription
172
+ * with destroyed client), lazily creates a temporary client for the download.
173
+ */
174
+ downloadArtifact(ref: ArtifactRef): Promise<DownloadedArtifact>;
175
+ onStream(cb: (stream: StreamRef) => void): Unsubscribe;
176
+ listStreams(): StreamRef[];
177
+ /**
178
+ * Open every readable stream known to this session synchronously.
179
+ *
180
+ * Returns an array of `StreamClient`s in insertion order (matching
181
+ * `listStreams()`). Outbound-only streams are skipped; streams that
182
+ * fail to open (already ended, terminal session, etc.) are skipped
183
+ * silently. Calling twice returns the same client objects for
184
+ * already-opened streams via `StreamRef.open()` idempotence.
185
+ *
186
+ * This is an active-session convenience. Under the merged t7c
187
+ * baseline, `StreamRef.open()` throws `StreamUnavailableError` for
188
+ * never-opened streams on a terminal session — call this method
189
+ * while the task is still running if the goal is to observe every
190
+ * stream.
191
+ */
192
+ openAllStreams(options?: {
193
+ reorderTimeoutMs?: number;
194
+ }): StreamClient[];
195
+ waitForStream(streamId?: string): Promise<StreamRef>;
196
+ waitForStreamWhere(predicate: (stream: StreamRef) => boolean): Promise<StreamRef>;
197
+ /**
198
+ * Wait for the task to reach a terminal state. Returns the terminal event.
199
+ *
200
+ * Resolves immediately if the session is already in a terminal state
201
+ * (pre-closed idempotent hit or terminal connect()).
202
+ *
203
+ * @param timeoutMs Optional timeout in milliseconds. Rejects with Error on timeout.
204
+ */
205
+ waitForTerminal(timeoutMs?: number): Promise<TerminalEvent>;
206
+ /**
207
+ * Download all accumulated artifacts and save them to a directory.
208
+ * Creates the directory if it does not exist.
209
+ * Returns the list of file paths written.
210
+ */
211
+ saveArtifacts(dir: string): Promise<string[]>;
212
+ cancel(): Promise<void>;
213
+ terminate(): Promise<void>;
214
+ close(): void;
215
+ /**
216
+ * Async close that awaits all StreamClient.end() calls before
217
+ * performing sync cleanup. Preferred over close() when streams
218
+ * may have buffered data to flush.
219
+ */
220
+ asyncClose(): Promise<void>;
221
+ [Symbol.asyncDispose](): Promise<void>;
222
+ /** Whether the session has been closed. */
223
+ get isClosed(): boolean;
224
+ }
@@ -0,0 +1 @@
1
+ import t from"pubnub";import{invertDirection as e}from"../stream/index.js";import{StreamRef as s}from"./stream-ref.js";import{taskChannel as r}from"./channel-manager.js";import{callRpc as i}from"./rpc-client.js";import{downloadArtifact as a}from"./artifacts.js";export const TERMINAL_STATES=new Set(["completed","failed","canceled"]);export class TaskSession{constructor(t){if(this.progressCallbacks=[],this.artifactCallbacks=[],this.terminalCallbacks=[],this.eventCallbacks=[],this.streamCallbacks=[],this.errorCallbacks=[],this.streams=new Map,this.artifacts=[],this.streamWaiters=[],this.terminalWaiters=[],this.closed=!1,this.listener=null,this.terminalReceived=!1,this.drainTimer=null,this.openStreamClients=new Set,this.seenTimetokens=new Set,this.taskId=t.taskId,this.ownerId=t.ownerId,this.orgId=t.orgId??t.ownerId,this.readToken=t.readToken,this.agentName=t.agentName,this.pubnub=t.pubnub,this.ownsSubscribeClient=t.ownsSubscribeClient??!1,this.sdkOptions=t.sdkOptions,this.rpcConfig=t.rpcConfig??null,this.statusChannel=t.statusChannel??r(t.taskId,this.orgId),this.idempotent=t.idempotent,this.queued=t.queued,this.pushConfigId=t.pushConfigId,this.autoDrain=t.autoDrain??!0,this.drainWindowMs=t.drainWindowMs??3e4,this.state=t.state,this.skipSubscriptionMode=t.skipSubscription??!1,this.subscribeKey=t.sdkOptions.subscribeKey,this.publishKey=t.sdkOptions.publishKey??"",t.preloadedStreams)for(const[e,r]of t.preloadedStreams){const t=new s(r.descriptor,this.sdkOptions,{onOpen:t=>this.trackStreamClient(t),sessionState:()=>this.state});this.streams.set(e,t)}t.preloadedArtifacts&&this.artifacts.push(...t.preloadedArtifacts),t.preClosed?this.closed=!0:t.skipSubscription||(t.externalSubscription?(this.listener=t.externalSubscription.listener,t.externalSubscription.onReady(this.handleEvent.bind(this))):this.setupSubscription())}setupSubscription(){const t=this.pubnub,e=this.statusChannel;this.listener={message:t=>{if(t.channel!==e)return;const s=t.message;if(!s||"object"!=typeof s||!s.type)return;const r=null!=t.timetoken?String(t.timetoken):void 0;this.handleEvent(s,r)}},t.addListener(this.listener),t.subscribe({channels:[e],timetoken:1e3})}routeCallbackError(t,e,s){const r=t instanceof Error?t:new Error(String(t));if(this.errorCallbacks.length>0)for(const t of this.errorCallbacks)try{t(r,{entryPoint:"taskSession",callbackType:e,event:s})}catch{}else console.warn(`[TaskSession] callback error in ${e}:`,r.message)}handleEvent(t,e){if(!this.closed){if(e){if(this.seenTimetokens.has(e))return;if(this.seenTimetokens.add(e),this.seenTimetokens.size>TaskSession.SEEN_TIMETOKENS_MAX){const t=this.seenTimetokens.values().next().value;void 0!==t&&this.seenTimetokens.delete(t)}}for(const e of this.eventCallbacks)try{e(t)}catch(e){this.routeCallbackError(e,"onEvent",t)}switch(t.type){case"progress":for(const e of this.progressCallbacks)try{e(t)}catch(e){this.routeCallbackError(e,"onProgress",t)}"stream_started"===t.streamEvent&&t.streams&&this.handleStreamStarted(t);break;case"artifact":this.accumulateArtifact(t);for(const e of this.artifactCallbacks)try{e(t)}catch(e){this.routeCallbackError(e,"onArtifact",t)}break;case"terminal":this.state=t.state;for(const e of this.terminalCallbacks)try{e(t)}catch(e){this.routeCallbackError(e,"onTerminal",t)}for(const e of this.terminalWaiters)e.resolve(t);this.terminalWaiters=[],this.autoDrain?this.startAutoDrain():this.close()}}}trackStreamClient(t){this.openStreamClients.add(t),t.onInboundDone(()=>{this.openStreamClients.delete(t),t.isActive&&t.end().catch(()=>{}),this.terminalReceived&&0===this.openStreamClients.size&&(this.drainTimer&&(clearTimeout(this.drainTimer),this.drainTimer=null),this.close())})}accumulateArtifact(t){const e=t.artifactRef;e&&"object"==typeof e&&e.kind&&this.artifacts.push(e)}handleStreamStarted(t){const r=t.streams;if(!r||"object"!=typeof r)return;const i=t.declaredStream;for(const[t,a]of Object.entries(r)){if(!a||"object"!=typeof a)continue;if(this.streams.has(t))continue;const r=a.direction,n=e(r),o=a.format;if(!o||"bytes"!==o&&"events"!==o)continue;const l=a.affinity;if("dedicated"!==l&&"shared"!==l){console.warn(`[TaskSession] live stream_started: dropping stream "${t}" for task "${this.taskId}" — invalid or missing affinity (got ${JSON.stringify(a.affinity)})`);continue}const c={taskId:this.taskId,streamId:t,agentName:this.agentName,channel:a.channel,token:a.token,agentDirection:r,localDirection:n,format:o,affinity:l,metadata:a.metadata,declaredStream:i},h=new s(c,this.sdkOptions,{onOpen:t=>this.trackStreamClient(t),sessionState:()=>this.state});this.streams.set(t,h);for(const t of this.streamCallbacks)try{t(h)}catch(t){this.routeCallbackError(t,"onStream",h)}this.resolveWaiters(h)}}startAutoDrain(){this.closed||(this.terminalReceived=!0,0!==this.openStreamClients.size?this.drainTimer=setTimeout(()=>{this.drainTimer=null;for(const t of this.openStreamClients)t.isActive&&t.end().catch(()=>{});this.close()},this.drainWindowMs):this.close())}findStreamByIdOrDeclared(t){const e=this.streams.get(t);if(e)return e;for(const e of this.streams.values())if(e.descriptor.declaredStream===t)return e}resolveWaiters(t){const e=[];for(const s of this.streamWaiters){let r=!1;if(void 0!==s.streamId)r=t.descriptor.declaredStream===s.streamId||t.descriptor.streamId===s.streamId;else if(s.predicate)try{r=s.predicate(t)}catch(e){this.routeCallbackError(e,"streamPredicate",t)}else r=!0;r?s.resolve(t):e.push(s)}this.streamWaiters=e}onProgress(t){return this.progressCallbacks.push(t),()=>{this.progressCallbacks=this.progressCallbacks.filter(e=>e!==t)}}onArtifact(t){return this.artifactCallbacks.push(t),()=>{this.artifactCallbacks=this.artifactCallbacks.filter(e=>e!==t)}}onTerminal(t){return this.terminalCallbacks.push(t),()=>{this.terminalCallbacks=this.terminalCallbacks.filter(e=>e!==t)}}onEvent(t){return this.eventCallbacks.push(t),()=>{this.eventCallbacks=this.eventCallbacks.filter(e=>e!==t)}}onError(t){return this.errorCallbacks.push(t),()=>{this.errorCallbacks=this.errorCallbacks.filter(e=>e!==t)}}listArtifacts(){return[...this.artifacts]}async downloadArtifact(e){if(this.pubnub)return a(e,this.pubnub);const s=`blocks-dl-${Date.now().toString(36)}-${Math.random().toString(36).slice(2,6)}`,r=new t({subscribeKey:this.subscribeKey,publishKey:this.publishKey||void 0,userId:s});this.readToken&&r.setToken(this.readToken);try{return await a(e,r)}finally{r.destroy()}}onStream(t){this.streamCallbacks.push(t);for(const e of this.streams.values())try{t(e)}catch(t){this.routeCallbackError(t,"onStream",e)}return()=>{this.streamCallbacks=this.streamCallbacks.filter(e=>e!==t)}}listStreams(){return[...this.streams.values()]}openAllStreams(t){const e=[];for(const s of this.streams.values()){const r=s.descriptor.localDirection;if("inbound"===r||"bidirectional"===r)try{e.push(s.open(t))}catch{}}return e}waitForStream(t){if(this.closed)return Promise.reject(new Error("TaskSession is closed"));if(this.skipSubscriptionMode){if(void 0!==t){const e=this.findStreamByIdOrDeclared(t);if(e)return Promise.resolve(e)}else{if(1===this.streams.size)return Promise.resolve(this.streams.values().next().value);if(this.streams.size>1)return Promise.reject(new Error(`Multiple streams exist (${this.streams.size}). Use waitForStream(streamId) or waitForStreamWhere(predicate) to select one.`))}return Promise.reject(new Error("No matching stream found. This is a terminal task session with no live subscription -- no future stream announcements will arrive."))}if(void 0!==t){const e=this.findStreamByIdOrDeclared(t);if(e)return Promise.resolve(e)}else{if(1===this.streams.size)return Promise.resolve(this.streams.values().next().value);if(this.streams.size>1)return Promise.reject(new Error(`Multiple streams exist (${this.streams.size}). Use waitForStream(streamId) or waitForStreamWhere(predicate) to select one.`))}return new Promise((e,s)=>{this.streamWaiters.push({resolve:e,reject:s,streamId:t})})}waitForStreamWhere(t){if(this.closed)return Promise.reject(new Error("TaskSession is closed"));for(const e of this.streams.values())try{if(t(e))return Promise.resolve(e)}catch(t){this.routeCallbackError(t,"streamPredicate",e)}return this.skipSubscriptionMode?Promise.reject(new Error("No matching stream found. This is a terminal task session with no live subscription -- no future stream announcements will arrive.")):new Promise((e,s)=>{this.streamWaiters.push({resolve:e,reject:s,predicate:t})})}async waitForTerminal(t){if(this.state&&TERMINAL_STATES.has(this.state))return{type:"terminal",taskId:this.taskId,state:this.state};if(this.closed)throw new Error("TaskSession closed");return new Promise((e,s)=>{const r={resolve:e,reject:s};if(this.terminalWaiters.push(r),void 0!==t){const e=setTimeout(()=>{const e=this.terminalWaiters.indexOf(r);-1!==e&&this.terminalWaiters.splice(e,1),s(new Error(`waitForTerminal timed out after ${t}ms`))},t),i=r.resolve,a=r.reject;r.resolve=t=>{clearTimeout(e),i(t)},r.reject=t=>{clearTimeout(e),a(t)}}})}async saveArtifacts(t){const{mkdirSync:e,writeFileSync:s}=await import("node:fs"),{join:r,resolve:i,basename:a}=await import("node:path"),n=i(t);e(n,{recursive:!0});const o=[],l=this.listArtifacts();for(let t=0;t<l.length;t++){const e=await this.downloadArtifact(l[t]),c=e.fileName??`artifact-${t}`,h=i(r(n,a(c)||`artifact-${t}`));if(!h.startsWith(n+"/")&&h!==n)throw new Error(`Artifact filename "${c}" resolves outside target directory`);s(h,e.data),o.push(h)}return o}async cancel(){if(!this.rpcConfig)throw new Error("TaskSession was not created with RPC config; use TaskClient.cancelTask() directly");await i(this.rpcConfig,"CancelTask",{taskId:this.taskId})}async terminate(){if(!this.rpcConfig)throw new Error("TaskSession was not created with RPC config; use TaskClient.terminateTask() directly");await i(this.rpcConfig,"TerminateTask",{taskId:this.taskId})}close(){if(this.closed)return;this.closed=!0,this.drainTimer&&(clearTimeout(this.drainTimer),this.drainTimer=null);for(const t of this.openStreamClients)t.isActive&&t.end().catch(()=>{});this.openStreamClients.clear();const t=new Error("TaskSession closed");for(const e of this.streamWaiters)e.reject(t);this.streamWaiters=[];for(const e of this.terminalWaiters)e.reject(t);this.terminalWaiters=[],this.pubnub&&(this.listener&&(this.pubnub.removeListener(this.listener),this.listener=null),this.pubnub.unsubscribe({channels:[this.statusChannel]}),this.ownsSubscribeClient&&this.pubnub.destroy()),this.progressCallbacks=[],this.artifactCallbacks=[],this.terminalCallbacks=[],this.eventCallbacks=[],this.streamCallbacks=[],this.errorCallbacks=[]}async asyncClose(){if(this.closed)return;const t=[...this.openStreamClients].filter(t=>t.isActive).map(t=>t.end().catch(()=>{}));await Promise.all(t),this.openStreamClients.clear(),this.close()}async[Symbol.asyncDispose](){await this.asyncClose()}get isClosed(){return this.closed}}TaskSession.SEEN_TIMETOKENS_MAX=200;
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Write-affinity header tracking for non-browser clients.
3
+ *
4
+ * The backend returns X-Write-Affinity (Unix timestamp) on successful
5
+ * mutations. Echoing it on subsequent requests forces reads from
6
+ * primary during the affinity window, avoiding stale replica reads
7
+ * after a write.
8
+ *
9
+ * Module-level state — affinity is per-process, not per-user.
10
+ *
11
+ * Concurrency: JS is single-threaded, so function bodies run atomically and
12
+ * no lock is needed. However, async response ordering means `capture(newer)`
13
+ * followed by `capture(older)` could otherwise shorten the affinity window.
14
+ * Capture is therefore monotonic — an older expiry never overwrites a newer
15
+ * one — so out-of-order response completions are safe.
16
+ */
17
+ /** Capture the affinity header from a fetch Response. Monotonic — newer wins. */
18
+ export declare function captureAffinity(headers: Headers | undefined): void;
19
+ /** Inject, or strip, the affinity header on an outgoing request. */
20
+ export declare function injectAffinity(headers: Record<string, string>): void;
21
+ /** Reset stored affinity (for tests). */
22
+ export declare function resetAffinity(): void;
@@ -0,0 +1 @@
1
+ const t="x-write-affinity";let n=null,e=0;export function captureAffinity(i){if(!i||"function"!=typeof i.get)return;const f=i.get(t);if(!f)return;const o=Number(f);Number.isFinite(o)&&o>e&&(n=f,e=o)}export function injectAffinity(i){n&&e>Date.now()/1e3?i[t]=n:(n&&(n=null,e=0),delete i[t])}export function resetAffinity(){n=null,e=0}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ import{describe as e,it as t,expect as i,beforeEach as n}from"vitest";import{captureAffinity as a,injectAffinity as r,resetAffinity as o}from"./write-affinity.js";e("write-affinity",()=>{n(()=>{o()}),t("injects nothing when no affinity has been captured",()=>{const e={};r(e),i(e).toEqual({})}),t("captures and injects a valid affinity header",()=>{const e=String(Math.floor(Date.now()/1e3)+10);a(new Headers({"x-write-affinity":e}));const t={};r(t),i(t["x-write-affinity"]).toBe(e)}),t("does not inject an expired affinity header",()=>{const e=String(Math.floor(Date.now()/1e3)-10);a(new Headers({"x-write-affinity":e}));const t={};r(t),i(t).toEqual({})}),t("ignores missing header in response",()=>{a(new Headers);const e={};r(e),i(e).toEqual({})}),t("overwrites older affinity with newer one",()=>{const e=String(Math.floor(Date.now()/1e3)+5),t=String(Math.floor(Date.now()/1e3)+15);a(new Headers({"x-write-affinity":e})),a(new Headers({"x-write-affinity":t}));const n={};r(n),i(n["x-write-affinity"]).toBe(t)}),t("capture is monotonic — older arriving after newer is ignored",()=>{const e=String(Math.floor(Date.now()/1e3)+15),t=String(Math.floor(Date.now()/1e3)+5);a(new Headers({"x-write-affinity":e})),a(new Headers({"x-write-affinity":t}));const n={};r(n),i(n["x-write-affinity"]).toBe(e)}),t("strips stale header from a reused headers object when state has expired",()=>{const e=String(Math.floor(Date.now()/1e3)+10),t={};a(new Headers({"x-write-affinity":e})),r(t),i(t["x-write-affinity"]).toBe(e),o(),r(t),i(t["x-write-affinity"]).toBeUndefined()}),t("strips pre-populated header when no state has been captured",()=>{const e={"x-write-affinity":"1234567890"};r(e),i(e["x-write-affinity"]).toBeUndefined()}),t.each(["Infinity","-Infinity","NaN","not-a-number"])("malformed header value %s does not clobber existing valid state",e=>{const t=String(Math.floor(Date.now()/1e3)+10);a(new Headers({"x-write-affinity":t})),a(new Headers({"x-write-affinity":e}));const n={};r(n),i(n["x-write-affinity"]).toBe(t)})});
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Browser-safe byte utilities for the Stream SDK.
3
+ *
4
+ * Replaces Node.js Buffer usage with Uint8Array, TextEncoder, and
5
+ * TextDecoder, which work identically in browsers and Node.js.
6
+ */
7
+ export declare function utf8ByteLength(str: string): number;
8
+ export declare function utf8Encode(str: string): Uint8Array;
9
+ export declare function utf8Decode(bytes: Uint8Array): string;
10
+ export declare function base64ToBytes(b64: string): Uint8Array;
11
+ /** Convert bytes to base64. Intended for bounded stream chunks and
12
+ * multipart parts (typically <16KB), not arbitrary large blobs. */
13
+ export declare function bytesToBase64(bytes: Uint8Array): string;
14
+ export declare function concatBytes(parts: Uint8Array[]): Uint8Array;
@@ -0,0 +1 @@
1
+ const e=new TextEncoder,t=new TextDecoder;export function utf8ByteLength(t){return e.encode(t).length}export function utf8Encode(t){return e.encode(t)}export function utf8Decode(e){return t.decode(e)}export function base64ToBytes(e){const t=atob(e),n=new Uint8Array(t.length);for(let e=0;e<t.length;e++)n[e]=t.charCodeAt(e);return n}export function bytesToBase64(e){let t="";for(const n of e)t+=String.fromCharCode(n);return btoa(t)}export function concatBytes(e){const t=e.reduce((e,t)=>e+t.length,0),n=new Uint8Array(t);let o=0;for(const t of e)n.set(t,o),o+=t.length;return n}
@@ -0,0 +1,47 @@
1
+ /**
2
+ * StreamDescriptor type and invertDirection helper.
3
+ *
4
+ * StreamDescriptor is the plain data shape that bridges the Agent SDK
5
+ * (control plane) and the Stream SDK (data plane). It carries all the
6
+ * information needed to open a stream connection.
7
+ *
8
+ * invertDirection computes localDirection from agentDirection as
9
+ * delivered in stream_started events.
10
+ */
11
+ import type { StreamDirection, StreamFormat } from './types.js';
12
+ /** Stream affinity as declared on the agent card. */
13
+ export type StreamAffinity = 'dedicated' | 'shared';
14
+ /**
15
+ * Plain data shape carrying all information needed to open a stream connection.
16
+ *
17
+ * - agentDirection: what arrived on the wire in stream_started
18
+ * - localDirection: what the local client actually does (computed via invertDirection)
19
+ * - affinity: 'dedicated' (per-task channel) or 'shared' (cross-task
20
+ * broadcast). Required: the stream_started wire event carries it, and
21
+ * producer-side construction has it in scope from the card declaration.
22
+ * Consumer-side cleanup rules consult this field to decide whether to
23
+ * publish a stream_end marker on a shared channel (they must not).
24
+ */
25
+ export interface StreamDescriptor {
26
+ taskId: string;
27
+ streamId: string;
28
+ agentName: string;
29
+ channel: string;
30
+ token: string;
31
+ agentDirection: StreamDirection;
32
+ localDirection: StreamDirection;
33
+ format: StreamFormat;
34
+ affinity: StreamAffinity;
35
+ metadata?: Record<string, unknown>;
36
+ /** The declared stream key from the agent card's streams block. */
37
+ declaredStream?: string;
38
+ }
39
+ /**
40
+ * Compute the local direction from the agent-facing direction delivered
41
+ * in stream_started.
42
+ *
43
+ * - agent outbound -> consumer inbound (consumer reads)
44
+ * - agent inbound -> consumer outbound (consumer writes)
45
+ * - agent bidirectional -> consumer bidirectional
46
+ */
47
+ export declare function invertDirection(agentDirection: StreamDirection): StreamDirection;
@@ -0,0 +1 @@
1
+ export function invertDirection(n){switch(n){case"outbound":return"inbound";case"inbound":return"outbound";case"bidirectional":return"bidirectional";default:throw new Error(`Unknown direction: ${n}`)}}
@@ -0,0 +1,11 @@
1
+ /**
2
+ * @blocks-network/sdk/stream - Public API surface.
3
+ *
4
+ * Exports the StreamClient class, StreamDescriptor type, invertDirection
5
+ * helper, validateStreamId validator, and all shared types.
6
+ */
7
+ export { StreamClient } from './stream-client.js';
8
+ export type { StreamError } from './stream-client.js';
9
+ export { type StreamDescriptor, type StreamAffinity, invertDirection } from './descriptor.js';
10
+ export { validateStreamId } from './validate.js';
11
+ export type { StreamFormat, StreamDirection, StreamClientOptions, StreamClientFromDescriptorOptions, InboundMessage, StreamBundleConfig, } from './types.js';
@@ -0,0 +1 @@
1
+ export{StreamClient}from"./stream-client.js";export{invertDirection}from"./descriptor.js";export{validateStreamId}from"./validate.js";
@@ -0,0 +1,82 @@
1
+ /**
2
+ * StreamBundle - Internal transport engine for the Stream SDK.
3
+ *
4
+ * Accumulates write() calls into buffered bundles and publishes them to
5
+ * PubNub stream channels. Handles both wire formats (stream_data and
6
+ * stream_events), multipart splitting for oversized payloads, binary
7
+ * encoding, presence gating, and meta.sender on every publish.
8
+ *
9
+ * This class is internal to the Stream SDK package. The public API is
10
+ * StreamClient, which owns a StreamBundle instance.
11
+ *
12
+ * Wire formats:
13
+ * stream_data: { type, streamId, seq, ts, encoding, chunks }
14
+ * stream_events: { type, streamId, seq, ts, encoding: "utf8", events }
15
+ *
16
+ * Sequence numbering:
17
+ * stream_data starts at seq 0
18
+ * stream_events starts at seq 1
19
+ */
20
+ import type PubNub from 'pubnub';
21
+ import type { StreamFormat, StreamBundleConfig } from './types.js';
22
+ export declare class StreamBundle {
23
+ private readonly pubnub;
24
+ private readonly channel;
25
+ private readonly streamId;
26
+ private readonly format;
27
+ private readonly maxMessageSize;
28
+ private readonly bundleSizeBytes;
29
+ private readonly maxLatencyMs;
30
+ private readonly uuid;
31
+ private readonly gated;
32
+ private occupancy;
33
+ private presenceListener;
34
+ private buffer;
35
+ private bufferBytes;
36
+ private currentBatchHasBinary;
37
+ private eventBuffer;
38
+ private eventBufferSize;
39
+ private seq;
40
+ private closed;
41
+ private flushTimer;
42
+ private inflightFlushes;
43
+ /** Callback invoked when the stream ends. */
44
+ onEnd?: (() => Promise<void> | void);
45
+ constructor(pubnub: PubNub, channel: string, streamId: string, format: StreamFormat, config: StreamBundleConfig, gated: boolean);
46
+ get isActive(): boolean;
47
+ get consumerCount(): number;
48
+ /**
49
+ * Buffered write. Appends data to the internal buffer.
50
+ * Flushing happens asynchronously on size or time threshold.
51
+ */
52
+ write(data: string | Uint8Array | unknown): void;
53
+ /**
54
+ * Publish a stream_end marker on the data channel.
55
+ * Uses the next seq value after the final data flush.
56
+ * Swallows publish errors silently (terminal fallback is the safety net).
57
+ */
58
+ publishEndMarker(): Promise<void>;
59
+ /**
60
+ * Flush remaining data, clean up presence, invoke onEnd callback.
61
+ */
62
+ end(): Promise<void>;
63
+ private setupPresenceGating;
64
+ private teardownPresenceGating;
65
+ private writeBytes;
66
+ private flush;
67
+ private writeEvent;
68
+ private flushEvents;
69
+ private trackFlush;
70
+ private publishMessage;
71
+ /**
72
+ * Split an oversized message into multiple base64-encoded parts.
73
+ * Splitting is byte-safe: converts to Uint8Array, splits on byte
74
+ * boundaries, then base64-encodes each part.
75
+ *
76
+ * Publishes run with bounded concurrency (see
77
+ * `DEFAULT_MULTIPART_CONCURRENCY`). Part order on the wire is
78
+ * irrelevant — the consumer reassembles by `multipart.part` index.
79
+ */
80
+ private publishMultipart;
81
+ private clearTimer;
82
+ }
@@ -0,0 +1 @@
1
+ import{utf8ByteLength as t,utf8Encode as e,bytesToBase64 as s}from"./bytes.js";import{CURRENT_PROTOCOL_VERSION as i}from"../runtime/protocol-version.js";export class StreamBundle{constructor(t,e,s,i,n,h){if(this.occupancy=0,this.presenceListener=null,this.buffer=[],this.bufferBytes=0,this.currentBatchHasBinary=!1,this.eventBuffer=[],this.eventBufferSize=0,this.closed=!1,this.flushTimer=null,this.inflightFlushes=[],this.pubnub=t,this.channel=e,this.streamId=s,this.format=i,n.maxMessageSize<=512)throw new Error(`maxMessageSize (${n.maxMessageSize}) must be greater than ENVELOPE_RESERVE (512)`);this.maxMessageSize=n.maxMessageSize,this.bundleSizeBytes=n.bundleSizeBytes,this.maxLatencyMs=n.maxLatencyMs,this.uuid=n.uuid,this.gated=h,this.seq="events"===i?1:0}get isActive(){return!this.closed}get consumerCount(){return this.gated?this.occupancy:0}write(t){if(this.closed)throw new Error("Cannot write to a closed stream");"events"===this.format?this.writeEvent(t):this.writeBytes(t)}async publishEndMarker(){const t={type:"stream_end",streamId:this.streamId,seq:this.seq++,ts:Date.now(),protocolVersion:i};await this.publishMessage(t).catch(()=>{})}async end(){this.closed||(this.closed=!0,this.clearTimer(),this.inflightFlushes.length>0&&(await Promise.all(this.inflightFlushes),this.inflightFlushes=[]),"events"===this.format?await this.flushEvents():await this.flush(),this.teardownPresenceGating(),await(this.onEnd?.()))}setupPresenceGating(){const t=this.channel+"-pnpres";this.presenceListener={message:e=>{e.channel===t&&"number"==typeof e.message?.occupancy&&(this.occupancy=e.message.occupancy)}},this.pubnub.addListener(this.presenceListener),this.pubnub.subscribe({channels:[t]}),this.pubnub.hereNow({channels:[this.channel]}).then(t=>{const e=t.channels?.[this.channel];e&&"number"==typeof e.occupancy&&(this.occupancy=e.occupancy)}).catch(()=>{})}teardownPresenceGating(){this.presenceListener&&(this.pubnub.removeListener(this.presenceListener),this.presenceListener=null),this.gated&&this.pubnub.unsubscribe({channels:[this.channel+"-pnpres"]})}writeBytes(e){let i;"string"==typeof e?i=e:e instanceof Uint8Array?(i=s(e),this.currentBatchHasBinary=!0):i=String(e);const n=t(i);this.buffer.push(i),this.bufferBytes+=n,this.bufferBytes>=this.bundleSizeBytes?this.trackFlush(this.flush()):null===this.flushTimer&&(this.flushTimer=setTimeout(()=>{this.flushTimer=null,this.trackFlush(this.flush())},this.maxLatencyMs))}async flush(){if(0===this.buffer.length)return;this.clearTimer();const e=this.buffer,s=this.currentBatchHasBinary;this.buffer=[],this.bufferBytes=0,this.currentBatchHasBinary=!1;const n={type:"stream_data",streamId:this.streamId,seq:this.seq++,ts:Date.now(),encoding:s?"base64":"utf8",chunks:e,protocolVersion:i},h=JSON.stringify(n);t(h)>this.maxMessageSize?await this.publishMultipart(n):await this.publishMessage(n)}writeEvent(e){if("string"==typeof e)throw new Error('write() does not accept raw strings in format: "events". Pass an object (e.g., { text: "..." }) or use format: "bytes".');e instanceof Uint8Array?(this.eventBuffer.push({$binary:s(e)}),this.eventBufferSize+=Math.ceil(4*e.length/3)+20):(this.eventBuffer.push(e),this.eventBufferSize+=t(JSON.stringify(e))),this.eventBufferSize>=this.bundleSizeBytes?this.trackFlush(this.flushEvents()):null===this.flushTimer&&(this.flushTimer=setTimeout(()=>{this.flushTimer=null,this.trackFlush(this.flushEvents())},this.maxLatencyMs))}async flushEvents(){if(0===this.eventBuffer.length)return;this.clearTimer();const e=this.eventBuffer;this.eventBuffer=[],this.eventBufferSize=0;const s={type:"stream_events",streamId:this.streamId,seq:this.seq++,ts:Date.now(),encoding:"utf8",events:e,protocolVersion:i},n=JSON.stringify(s);t(n)>this.maxMessageSize?await this.publishMultipart(s):await this.publishMessage(s)}trackFlush(t){this.inflightFlushes.push(t),t.then(()=>{const e=this.inflightFlushes.indexOf(t);e>=0&&this.inflightFlushes.splice(e,1)})}async publishMessage(t){let e=null;for(let s=1;s<=3;s++)try{return void await this.pubnub.publish({channel:this.channel,message:t,meta:{sender:this.uuid,protocolVersion:i},storeInHistory:!1,sendByPost:!0})}catch(t){if(e=t,s<3){const t=100*Math.pow(2,s-1);await new Promise(e=>setTimeout(e,t))}}console.error("[StreamBundle] Failed to publish stream message after 3 attempts:",e)}async publishMultipart(t){const n=JSON.stringify(t),h=e(n),r=Math.floor(3*(this.maxMessageSize-512)/4),a=Math.ceil(h.length/r),u=`mp-${Date.now()}-${t.seq}`,l=[];for(let e=0;e<a;e++){const n=h.subarray(e*r,(e+1)*r),c={type:t.type,streamId:this.streamId,seq:t.seq,ts:t.ts,multipart:{id:u,part:e+1,total:a},data:s(n),protocolVersion:i};l.push(()=>this.publishMessage(c))}await async function(t,e){const s=new Array(t.length);let i=0;const n=Math.min(e,t.length),h=Array.from({length:n},async()=>{for(;;){const e=i++;if(e>=t.length)return;s[e]=await t[e]()}});return await Promise.all(h),s}(l,4)}clearTimer(){null!==this.flushTimer&&(clearTimeout(this.flushTimer),this.flushTimer=null)}}
@@ -0,0 +1,192 @@
1
+ /**
2
+ * StreamClient - The developer-facing API for the Stream SDK.
3
+ *
4
+ * Handles PubNub client creation, token setup, UUID generation, channel
5
+ * computation, direction routing, self-publish filtering, presence gating,
6
+ * and inbound message consumption with multipart reassembly.
7
+ *
8
+ * StreamClient is the public API. StreamBundle is the internal engine.
9
+ */
10
+ import type { StreamDescriptor } from './descriptor.js';
11
+ import type { StreamClientOptions, StreamClientFromDescriptorOptions, InboundMessage } from './types.js';
12
+ /** Reset the UUID counter (for testing). */
13
+ export declare function _resetUuidCounter(): void;
14
+ export declare const FATAL_STREAM_ERROR_CATEGORIES: ReadonlySet<string>;
15
+ /**
16
+ * Compatibility helper that detects a PubNub status error across the two
17
+ * shapes the pinned `pubnub@10.2.x` JS SDK exposes on the subscribe
18
+ * listener:
19
+ *
20
+ * - `Status` — has a required `error: boolean` and `statusCode: number`.
21
+ * - `StatusEvent` — has an optional polymorphic
22
+ * `error?: string | StatusCategory | boolean` and no numeric status
23
+ * code.
24
+ *
25
+ * Priority:
26
+ * 1. Truthy `status.error` (covers `true`, non-empty string, or
27
+ * category-name string surfaced by `StatusEvent`).
28
+ * 2. Numeric `statusCode >= 400` (REST-style errors surfaced via
29
+ * `Status`).
30
+ * 3. Fatal-category membership as a last resort when the payload is
31
+ * sparse (defensive; keeps PAM detection working even if neither of
32
+ * the above fields is populated by a future SDK minor release).
33
+ *
34
+ * Exported so classifier behavior can be unit-tested directly without
35
+ * instantiating a full `StreamClient`.
36
+ */
37
+ export declare function isStreamStatusError(status: {
38
+ error?: unknown;
39
+ statusCode?: unknown;
40
+ category?: unknown;
41
+ } | null | undefined): boolean;
42
+ /** True if this category is in the fatal allowlist. */
43
+ export declare function isFatalStreamCategory(category: string | undefined | null): boolean;
44
+ /**
45
+ * Typed error payload fired to `StreamClient.onError(...)` subscribers.
46
+ *
47
+ * Fires for every PubNub status event classified as an error by
48
+ * `isStreamStatusError`. Consumers branch on `fatal` for
49
+ * must-terminate conditions (PAM revocation, bad grant) and on
50
+ * `category` for finer-grained UX.
51
+ */
52
+ export interface StreamError {
53
+ /** PubNub status category (e.g., `PNAccessDeniedCategory`). */
54
+ category: string;
55
+ /** Raw PubNub error data (`errorData` or `error` payload if present). */
56
+ error: any;
57
+ /** The stream channel the error applies to. */
58
+ channel: string;
59
+ /** Unix ms timestamp when the error was observed. */
60
+ timestamp: number;
61
+ /** Whether the error triggered forced stream termination. */
62
+ fatal: boolean;
63
+ }
64
+ export declare class StreamClient {
65
+ private readonly pubnub;
66
+ private readonly bundle;
67
+ private readonly _channel;
68
+ private readonly _uuid;
69
+ private readonly direction;
70
+ private readonly format;
71
+ private readonly _affinity;
72
+ private _isActive;
73
+ private endCallbacks;
74
+ private inboundDoneCallbacks;
75
+ private inboundDoneFired;
76
+ private errorCallbacks;
77
+ private inboundQueue;
78
+ private inboundResolve;
79
+ private inboundDone;
80
+ private messageListener;
81
+ private multipartBuffers;
82
+ private nextExpectedSeq;
83
+ private reorderBuffer;
84
+ private reorderTimer;
85
+ private endSeq;
86
+ private readonly reorderTimeoutMs;
87
+ constructor(options: StreamClientOptions);
88
+ get isActive(): boolean;
89
+ get channel(): string;
90
+ get uuid(): string;
91
+ /**
92
+ * Write data to the stream.
93
+ * Throws if the stream is ended or direction is inbound-only.
94
+ */
95
+ write(data: string | Uint8Array | unknown): void;
96
+ /**
97
+ * Flush remaining data, unsubscribe, and destroy the PubNub client.
98
+ */
99
+ end(): Promise<void>;
100
+ /**
101
+ * Register a callback to be invoked when end() completes.
102
+ */
103
+ onEnd(callback: () => void): void;
104
+ /**
105
+ * Register a callback to fire when the inbound iterator completes for any
106
+ * reason: stream_end marker, explicit end(), or error. Internal only --
107
+ * used by TaskSession for auto-drain tracking.
108
+ *
109
+ * If the inbound side has already completed, the callback fires immediately.
110
+ */
111
+ onInboundDone(cb: () => void): void;
112
+ private fireInboundDone;
113
+ /**
114
+ * Register a callback to fire when the stream encounters a PubNub
115
+ * status error — PAM revocation, network failure, grant mismatch, etc.
116
+ *
117
+ * The callback receives a {@link StreamError} with `fatal: true` when
118
+ * the error caused forced stream termination (and `onInboundDone` will
119
+ * fire shortly after so consumer iterators exit cleanly). Non-fatal
120
+ * errors fire with `fatal: false` and leave the stream running so
121
+ * PubNub's retry machinery can recover.
122
+ *
123
+ * Consumer exceptions from the callback are swallowed and logged; they
124
+ * do not interrupt stream teardown.
125
+ */
126
+ onError(callback: (err: StreamError) => void): void;
127
+ private fireError;
128
+ /**
129
+ * Async iterable of inbound messages. Throws if direction is outbound-only.
130
+ * Handles multipart reassembly transparently.
131
+ */
132
+ get inbound(): AsyncIterable<InboundMessage>;
133
+ private setupInbound;
134
+ /**
135
+ * Dispatch a PubNub status event to `onError` subscribers, then
136
+ * force-terminate the stream if the category is in the fatal
137
+ * allowlist. Non-error / benign status events are ignored.
138
+ *
139
+ * All work is wrapped in try/catch so a listener exception can't
140
+ * destabilize PubNub's internal event loop.
141
+ */
142
+ private handleStatusEvent;
143
+ private handleInboundMessage;
144
+ private handleMultipartPart;
145
+ /** Remove a multipart group from the buffer. */
146
+ private dropMultipartGroup;
147
+ /** Evict groups older than TTL or when buffer exceeds capacity. */
148
+ private evictStaleGroups;
149
+ private normalizeMessage;
150
+ private enqueueInbound;
151
+ private enqueueReordered;
152
+ private flushConsecutive;
153
+ private startReorderTimer;
154
+ /**
155
+ * Called after flushing consecutive messages. If the gap that started
156
+ * the current timer has been resolved but a new gap exists, cancel
157
+ * the old timer and start a fresh one for the new gap. If no gaps
158
+ * remain, just cancel.
159
+ */
160
+ private cancelAndRestartTimerIfNeeded;
161
+ private checkEndReached;
162
+ /**
163
+ * Decoded byte iterator. Each yield is a Uint8Array of decoded data.
164
+ * Handles base64 and utf-8 encoding transparently. Browser-safe: uses
165
+ * TextEncoder / atob via ./bytes.js helpers, no Buffer dependency.
166
+ */
167
+ bytes(): AsyncIterable<Uint8Array>;
168
+ /**
169
+ * Flattened event iterator. Each yield is a single event object
170
+ * unwrapped from batched event arrays.
171
+ */
172
+ events<T = unknown>(): AsyncIterable<T>;
173
+ /**
174
+ * Node Readable adapter for pipe() integration.
175
+ * Creates ONE persistent bytes() iterator and pumps from it across
176
+ * read() calls. This ensures backpressure/resume continues the same
177
+ * stream rather than restarting.
178
+ *
179
+ * Uses dynamic import of node:stream to avoid breaking browser bundles.
180
+ */
181
+ readable(): Promise<import('node:stream').Readable>;
182
+ /**
183
+ * Create a StreamClient from a StreamDescriptor.
184
+ *
185
+ * This is the primary public entry point for descriptor-based construction.
186
+ * Used internally by StreamRef.open() and directly by advanced callers.
187
+ *
188
+ * Consumer gating policy: when localDirection includes writing and gating
189
+ * is not explicitly set, defaults to gating: false.
190
+ */
191
+ static fromDescriptor(descriptor: StreamDescriptor, options: StreamClientFromDescriptorOptions): StreamClient;
192
+ }