@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.
- package/README.md +632 -0
- package/dist/cli/run.d.ts +39 -0
- package/dist/cli/run.js +2 -0
- package/dist/config-loader.d.ts +10 -0
- package/dist/config-loader.js +1 -0
- package/dist/defaults.d.ts +7 -0
- package/dist/defaults.js +1 -0
- package/dist/env.d.ts +5 -0
- package/dist/env.js +1 -0
- package/dist/index.d.ts +28 -0
- package/dist/index.js +1 -0
- package/dist/runtime/agent-auth.d.ts +94 -0
- package/dist/runtime/agent-auth.js +1 -0
- package/dist/runtime/agent-instance.d.ts +233 -0
- package/dist/runtime/agent-instance.js +1 -0
- package/dist/runtime/agent-registry.d.ts +270 -0
- package/dist/runtime/agent-registry.js +1 -0
- package/dist/runtime/artifacts.d.ts +65 -0
- package/dist/runtime/artifacts.js +1 -0
- package/dist/runtime/auth-provider.d.ts +39 -0
- package/dist/runtime/auth-provider.js +1 -0
- package/dist/runtime/cdm-config.d.ts +16 -0
- package/dist/runtime/cdm-config.js +1 -0
- package/dist/runtime/channel-manager.d.ts +125 -0
- package/dist/runtime/channel-manager.js +1 -0
- package/dist/runtime/consumer-auth.d.ts +112 -0
- package/dist/runtime/consumer-auth.js +1 -0
- package/dist/runtime/credential-cache.d.ts +40 -0
- package/dist/runtime/credential-cache.js +1 -0
- package/dist/runtime/file-input.d.ts +37 -0
- package/dist/runtime/file-input.js +1 -0
- package/dist/runtime/file-upload.d.ts +112 -0
- package/dist/runtime/file-upload.js +1 -0
- package/dist/runtime/part-helpers.d.ts +52 -0
- package/dist/runtime/part-helpers.js +1 -0
- package/dist/runtime/protocol-version.d.ts +18 -0
- package/dist/runtime/protocol-version.js +1 -0
- package/dist/runtime/pubnub-client.d.ts +8 -0
- package/dist/runtime/pubnub-client.js +1 -0
- package/dist/runtime/pubnub-types.d.ts +119 -0
- package/dist/runtime/pubnub-types.js +1 -0
- package/dist/runtime/rpc-client.d.ts +45 -0
- package/dist/runtime/rpc-client.js +1 -0
- package/dist/runtime/stream-context.d.ts +69 -0
- package/dist/runtime/stream-context.js +1 -0
- package/dist/runtime/stream-ref.d.ts +74 -0
- package/dist/runtime/stream-ref.js +1 -0
- package/dist/runtime/stream-registry.d.ts +133 -0
- package/dist/runtime/stream-registry.js +1 -0
- package/dist/runtime/stream-setup-helper.d.ts +87 -0
- package/dist/runtime/stream-setup-helper.js +1 -0
- package/dist/runtime/task-client.d.ts +259 -0
- package/dist/runtime/task-client.js +1 -0
- package/dist/runtime/task-session.d.ts +224 -0
- package/dist/runtime/task-session.js +1 -0
- package/dist/runtime/write-affinity.d.ts +22 -0
- package/dist/runtime/write-affinity.js +1 -0
- package/dist/runtime/write-affinity.test.d.ts +1 -0
- package/dist/runtime/write-affinity.test.js +1 -0
- package/dist/stream/bytes.d.ts +14 -0
- package/dist/stream/bytes.js +1 -0
- package/dist/stream/descriptor.d.ts +47 -0
- package/dist/stream/descriptor.js +1 -0
- package/dist/stream/index.d.ts +11 -0
- package/dist/stream/index.js +1 -0
- package/dist/stream/stream-bundle.d.ts +82 -0
- package/dist/stream/stream-bundle.js +1 -0
- package/dist/stream/stream-client.d.ts +192 -0
- package/dist/stream/stream-client.js +1 -0
- package/dist/stream/types.d.ts +75 -0
- package/dist/stream/types.js +1 -0
- package/dist/stream/validate.d.ts +15 -0
- package/dist/stream/validate.js +1 -0
- 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
|
+
}
|