@blocks-network/sdk 0.1.61 → 0.1.63
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 +50 -43
- package/dist/index.d.ts +4 -4
- package/dist/index.js +1 -1
- package/dist/runtime/agent-instance.d.ts +1 -1
- package/dist/runtime/agent-instance.js +1 -1
- package/dist/runtime/agent-registry.d.ts +16 -16
- package/dist/runtime/agent-registry.js +1 -1
- package/dist/runtime/auth-provider.d.ts +30 -0
- package/dist/runtime/auth-provider.js +1 -1
- package/dist/runtime/channel-manager.d.ts +9 -9
- package/dist/runtime/channel-manager.js +1 -1
- package/dist/runtime/consumer-auth.d.ts +18 -0
- package/dist/runtime/consumer-auth.js +1 -1
- package/dist/runtime/file-upload.js +1 -1
- package/dist/runtime/logger.d.ts +14 -12
- package/dist/runtime/logger.js +1 -1
- package/dist/runtime/pubnub-client.d.ts +35 -12
- package/dist/runtime/pubnub-client.js +1 -1
- package/dist/runtime/rpc-client.js +1 -1
- package/dist/runtime/task-client.js +1 -1
- package/dist/runtime/task-session.js +1 -1
- package/dist/runtime/transport-categories.d.ts +60 -0
- package/dist/runtime/transport-categories.js +1 -0
- package/dist/stream/index.d.ts +1 -1
- package/dist/stream/stream-client.d.ts +17 -22
- package/dist/stream/stream-client.js +1 -1
- package/dist/stream/types.d.ts +43 -4
- package/package.json +1 -1
|
@@ -49,6 +49,16 @@ export interface ConsumerAuthOptions {
|
|
|
49
49
|
baseUrl?: string;
|
|
50
50
|
onAuthError?: (error: Error) => void;
|
|
51
51
|
}
|
|
52
|
+
/**
|
|
53
|
+
* Thrown by `TaskClient.sendMessage()` and `TaskClient.connect()` when the
|
|
54
|
+
* underlying `ConsumerAuth` is in a known-broken refresh state (proactive
|
|
55
|
+
* refresh permanently failed after 3 retries). Carries the original failure
|
|
56
|
+
* as `.cause`. Cleared by a subsequent successful reactive refresh.
|
|
57
|
+
*/
|
|
58
|
+
export declare class AuthRefreshFailedError extends Error {
|
|
59
|
+
readonly cause: Error;
|
|
60
|
+
constructor(cause: Error);
|
|
61
|
+
}
|
|
52
62
|
export declare class ConsumerAuth implements AuthProvider {
|
|
53
63
|
private readonly _apiKey?;
|
|
54
64
|
private readonly _tokenEndpoint?;
|
|
@@ -64,6 +74,7 @@ export declare class ConsumerAuth implements AuthProvider {
|
|
|
64
74
|
private _refreshPromise;
|
|
65
75
|
private _initialized;
|
|
66
76
|
private _initPromise;
|
|
77
|
+
private _lastAuthError;
|
|
67
78
|
constructor(options: ConsumerAuthOptions);
|
|
68
79
|
/**
|
|
69
80
|
* Acquire the initial token. Must be called before any other method.
|
|
@@ -71,6 +82,13 @@ export declare class ConsumerAuth implements AuthProvider {
|
|
|
71
82
|
init(): Promise<void>;
|
|
72
83
|
getAuthHeader(): string | null;
|
|
73
84
|
getUserId(): string | null;
|
|
85
|
+
/**
|
|
86
|
+
* Returns the last permanent refresh failure, or null. Set when proactive
|
|
87
|
+
* refresh exhausts its 3 retries; cleared on a successful reactive refresh.
|
|
88
|
+
* Callers (TaskClient.sendMessage / connect) use this to fail fast before
|
|
89
|
+
* making any authenticated request.
|
|
90
|
+
*/
|
|
91
|
+
getLastAuthError(): AuthRefreshFailedError | null;
|
|
74
92
|
/**
|
|
75
93
|
* Reactive refresh triggered by a 401 response. Returns true if a new
|
|
76
94
|
* token was acquired. Concurrent callers share a single in-flight
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{log as e}from"./logger.js";import{CURRENT_PROTOCOL_VERSION as t,PROTOCOL_VERSION_HEADER as
|
|
1
|
+
import{log as e}from"./logger.js";import{CURRENT_PROTOCOL_VERSION as t,PROTOCOL_VERSION_HEADER as r}from"./protocol-version.js";import{captureAffinity as i,injectAffinity as s}from"./write-affinity.js";const n=(t,r,i)=>e("[ConsumerAuth]",t,r,i),o="API_KEY_INVALID";export class AuthRefreshFailedError extends Error{constructor(e){super(`Consumer auth refresh permanently failed: ${e.message}`),this.name="AuthRefreshFailedError",this.cause=e}}export class ConsumerAuth{constructor(e){this._token=null,this._refreshToken=null,this._userId=null,this._expiresIn=0,this._destroyed=!1,this._refreshTimer=null,this._refreshPromise=null,this._initialized=!1,this._initPromise=null,this._lastAuthError=null,this._apiKey=e.apiKey,this._tokenEndpoint=e.tokenEndpoint,this._tokenProvider=e.tokenProvider,this._baseUrl=(e.baseUrl??"").replace(/\/+$/,""),this._onAuthError=e.onAuthError}async init(){if(this._apiKey)await this._initApiKey();else if(this._tokenEndpoint)await this._initTokenEndpoint();else{if(!this._tokenProvider)throw new Error("ConsumerAuth requires one of: apiKey, tokenEndpoint, or tokenProvider");await this._initTokenProvider()}this._scheduleRefresh(),this._initialized=!0}getAuthHeader(){return this._token?`Bearer ${this._token}`:null}getUserId(){return this._userId}getLastAuthError(){return this._lastAuthError}async onAuthFailure(){return!this._destroyed&&this._doRefreshDedup()}destroy(){this._destroyed=!0,this._refreshTimer&&(clearTimeout(this._refreshTimer),this._refreshTimer=null)}async ensureReady(){this._initialized||(this._initPromise||(this._initPromise=this.init().then(()=>{this._initialized=!0},e=>{throw this._initPromise=null,e})),await this._initPromise)}async _initApiKey(){const e=`${this._baseUrl}/api/v1/auth/agent/consumer-token`,n={"Content-Type":"application/json",[r]:t};s(n);const o=await fetch(e,{method:"POST",headers:n,body:JSON.stringify({apiKey:this._apiKey})});if(i(o.headers),!o.ok){const e=await o.text().catch(()=>"");throw new Error(`consumer-token failed: HTTP ${o.status}${e?` ${e}`:""}`)}const h=await o.json();this._refreshToken=h.refreshToken,this._applyTokenResult({token:h.accessToken,expiresIn:h.expiresIn,userId:h.userId})}async _refreshApiKey(){const e=`${this._baseUrl}/api/v1/auth/agent/refresh`,t={Authorization:`Bearer ${this._apiKey}`,"Content-Type":"application/json"};s(t);const r=await fetch(e,{method:"POST",headers:t,body:JSON.stringify({refreshToken:this._refreshToken})});if(i(r.headers),!r.ok){const e=await r.json().catch(()=>({}));if("REFRESH_TOKEN_INVALID"===e.code)return void await this._initApiKey();if(e.code===o)throw new Error(`API key invalid or revoked: ${e.error??o}`);throw new Error(`token refresh failed: ${e.error??`HTTP ${r.status}`}`)}const n=await r.json();this._token=n.accessToken,this._refreshToken=n.refreshToken,"number"==typeof n.expiresIn&&(this._expiresIn=n.expiresIn),this._lastAuthError=null}async _initTokenEndpoint(){const e=await this._fetchTokenEndpoint();this._applyTokenResult(e)}async _fetchTokenEndpoint(){const e=this._tokenEndpoint,t="string"==typeof e,r=t?e:e.url;if("string"!=typeof r||!r)throw new Error("TokenEndpointConfig.url is required and must be a non-empty string");const i=t?void 0:e.credentials,s=t?{}:e.headers??{},n=t?{}:e.body??{},o={method:"POST",headers:{"Content-Type":"application/json",...s},body:JSON.stringify(n)};void 0!==i&&(o.credentials=i);const h=await fetch(r,o);if(!h.ok){const e=await h.text().catch(()=>"");throw new Error(`token endpoint failed: HTTP ${h.status}${e?` ${e}`:""}`)}return h.json()}async _initTokenProvider(){const e=await this._tokenProvider();this._applyTokenResult(e)}_applyTokenResult(e){this._token=e.token,this._expiresIn=e.expiresIn,void 0!==e.userId&&(this._userId=e.userId),this._lastAuthError=null}async _refreshOnce(){if(this._apiKey)await this._refreshApiKey();else if(this._tokenEndpoint){const e=await this._fetchTokenEndpoint();this._applyTokenResult(e)}else if(this._tokenProvider){const e=await this._tokenProvider();this._applyTokenResult(e)}}_doRefreshDedup(){return this._refreshPromise||(this._refreshPromise=this._refreshOnce().then(()=>(this._scheduleRefresh(),!0)).catch(e=>{const t=e instanceof Error?e:new Error(String(e));return n("warn","reactive refresh failed",{event:"consumer_auth_reactive_refresh_failed",error:t.message}),!1}).finally(()=>{this._refreshPromise=null})),this._refreshPromise}_scheduleRefresh(){if(this._destroyed||this._expiresIn<=0)return;this._refreshTimer&&clearTimeout(this._refreshTimer);const e=1e3*this._expiresIn*.8;this._refreshTimer=setTimeout(()=>{this._refreshTimer=null,this._proactiveRefreshWithRetry(0)},e)}async _proactiveRefreshWithRetry(e){if(!this._destroyed)try{await this._refreshOnce(),this._scheduleRefresh()}catch(t){if(e<2){const t=Math.min(5e3*Math.pow(2,e),3e4);this._destroyed||(this._refreshTimer=setTimeout(()=>{this._refreshTimer=null,this._proactiveRefreshWithRetry(e+1)},t))}else{const e=t instanceof Error?t:new Error(String(t));this._lastAuthError=new AuthRefreshFailedError(e),n("warn","proactive refresh permanently failed",{event:"consumer_auth_proactive_refresh_failed",error:e.message}),this._onAuthError&&this._onAuthError(e)}}}}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{CURRENT_PROTOCOL_VERSION as t,PROTOCOL_VERSION_HEADER as o}from"./protocol-version.js";import{captureAffinity as
|
|
1
|
+
import{CURRENT_PROTOCOL_VERSION as t,PROTOCOL_VERSION_HEADER as o}from"./protocol-version.js";import{preflightAuthOrThrow as a}from"./auth-provider.js";import{captureAffinity as e,injectAffinity as r}from"./write-affinity.js";async function i(i,n,s){await a(i.authProvider);const u=`${i.baseUrl.replace(/\/+$/,"")}${n}`,d=a=>{const e={"Content-Type":"application/json",[o]:t},n=i.authProvider?.getAuthHeader();return n&&(e.Authorization=n),a&&r(e),{method:"POST",credentials:"omit",headers:e,body:JSON.stringify(s)}};if(i.agentAuth)return i.agentAuth.authenticatedFetch(u,d(!1));const c=await fetch(u,d(!0));if(e(c.headers),401===c.status&&i.authProvider){if(await i.authProvider.onAuthFailure()){const t=await fetch(u,d(!0));return e(t.headers),t}}return c}export async function requestUpload(t,o){const a=await i(t,"/api/v1/files/request-upload",o);if(!a.ok){const t=await a.text().catch(()=>"");throw new Error(`request-upload failed: HTTP ${a.status} ${t}`)}return a.json()}export async function uploadToStorage(t,o,a,e,r){const i=new FormData;for(const t of o)i.append(t.key,t.value);const n=a instanceof Blob?a:new Blob([a],{type:r});i.append("file",n,e);const s=await fetch(t,{method:"POST",body:i});if(!s.ok){const t=await s.text().catch(()=>"");throw new Error(`S3 upload failed: HTTP ${s.status} ${t}`)}}export async function confirmUpload(t,o,a){const e=await i(t,"/api/v1/files/confirm-upload",{uploadId:o});if(!e.ok){const t=await e.text().catch(()=>"");throw new Error(`confirm-upload failed: HTTP ${e.status} ${t}`)}return e.json()}export async function uploadFile(t,o,a){const e=await requestUpload(t,o);if(await uploadToStorage(e.uploadUrl,e.formFields,a,o.fileName,o.mimeType),"provider-output"===o.role){return await confirmUpload(t,e.uploadId,!0)}const r=e;return await confirmUpload(t,e.uploadId),{uploadSessionId:r.uploadSessionId,uploadId:e.uploadId}}
|
package/dist/runtime/logger.d.ts
CHANGED
|
@@ -3,19 +3,19 @@ export declare const _LOG_LEVEL_ORDER: Record<string, number>;
|
|
|
3
3
|
/** Resolve the effective LOG_LEVEL from env (default: "info"). */
|
|
4
4
|
export declare function _resolveLogLevel(): number;
|
|
5
5
|
/**
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
6
|
+
* Named subsystems that can be activated via BLOCKS_DEBUG_INTERNAL.
|
|
7
|
+
* diagnostics — Blocks-authored transport-connectivity diagnostics
|
|
8
|
+
* (per-client status + alive-snapshot timer).
|
|
9
|
+
* forward_transport — route the underlying realtime transport's own log
|
|
10
|
+
* output through the Blocks logger under the
|
|
11
|
+
* [Transport] tag.
|
|
10
12
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* Defaults to OFF. Without the opt-in, the diagnostics block is a no-op:
|
|
15
|
-
* no listener attached, no timer armed, no per-status emission, no
|
|
16
|
-
* `pubnub_diagnostics_armed` boot line.
|
|
13
|
+
* Usage: BLOCKS_DEBUG_INTERNAL=diagnostics,forward_transport
|
|
14
|
+
* Neither subsystem is implied by LOG_LEVEL=debug.
|
|
17
15
|
*/
|
|
18
|
-
export
|
|
16
|
+
export type DebugSubsystem = 'diagnostics' | 'forward_transport';
|
|
17
|
+
export declare function isDebugSubsystemEnabled(subsystem: DebugSubsystem): boolean;
|
|
18
|
+
export type LogLevel = 'debug' | 'info' | 'warn' | 'error';
|
|
19
19
|
/**
|
|
20
20
|
* Route a tagged, LOG_LEVEL-filtered log entry to console.error / warn / log.
|
|
21
21
|
*
|
|
@@ -24,4 +24,6 @@ export declare function isInternalDebugEnabled(): boolean;
|
|
|
24
24
|
* filename + line number. Entries below the configured LOG_LEVEL threshold
|
|
25
25
|
* are dropped silently.
|
|
26
26
|
*/
|
|
27
|
-
export declare function log(tag: string, level:
|
|
27
|
+
export declare function log(tag: string, level: LogLevel, message: string, meta?: Record<string, unknown>): void;
|
|
28
|
+
/** Returns a partial-applied log() bound to a fixed source tag. */
|
|
29
|
+
export declare function createLogger(source: string): (level: LogLevel, message: string, meta?: Record<string, unknown>) => void;
|
package/dist/runtime/logger.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{getEnv as e}from"../env.js";export const _LOG_LEVEL_ORDER={error:0,warn:1,info:2,debug:3};export function _resolveLogLevel(){const
|
|
1
|
+
import{getEnv as e}from"../env.js";export const _LOG_LEVEL_ORDER={error:0,warn:1,info:2,debug:3};export function _resolveLogLevel(){const n=(e("LOG_LEVEL")??"info").toLowerCase();return _LOG_LEVEL_ORDER[n]??_LOG_LEVEL_ORDER.info}const n=["diagnostics","forward_transport"];let o=!1;export function isDebugSubsystemEnabled(t){const r=e("BLOCKS_DEBUG_INTERNAL")??"";return r.length>0&&function(e){if(o)return;const t=e.split(",").map(e=>e.trim()).filter(e=>e.length>0).filter(e=>!n.includes(e));0!==t.length&&(o=!0,log("[Blocks]","warn",`BLOCKS_DEBUG_INTERNAL contains unrecognized token(s): ${t.map(e=>JSON.stringify(e)).join(", ")}. Supported values: ${n.join(", ")}. Legacy truthy/falsy values (1, 0, true, false) are not recognized.`,{event:"debug_internal_unknown_token",tokens:t}))}(r),r.split(",").some(e=>e.trim()===t)}export function log(e,n,o,t){const r=_resolveLogLevel();if((_LOG_LEVEL_ORDER[n]??_LOG_LEVEL_ORDER.info)>r)return;const s={level:n,message:o,ts:Date.now(),...t};"error"===n?console.error(e,s):"warn"===n?console.warn(e,s):console.log(e,s)}export function createLogger(e){return(n,o,t)=>log(`[${e}]`,n,o,t)}
|
|
@@ -13,7 +13,7 @@ export interface PubNubClientConfig {
|
|
|
13
13
|
*/
|
|
14
14
|
announceSuccessfulHeartbeats?: boolean;
|
|
15
15
|
/**
|
|
16
|
-
*
|
|
16
|
+
* Silent-park fix. When true, the client uses an
|
|
17
17
|
* ExponentialRetryPolicy with maximumRetry expanded to ~30 days
|
|
18
18
|
* (43_200 attempts at the 60s cap). PubNub's Event Engine otherwise
|
|
19
19
|
* exhausts the default 6-attempt budget after ~3.5 min and parks in
|
|
@@ -29,16 +29,39 @@ export interface PubNubClientConfig {
|
|
|
29
29
|
* §Cross-SDK retry-budget defaults for the contract.
|
|
30
30
|
*/
|
|
31
31
|
subscribeRetryUnbounded?: boolean;
|
|
32
|
-
/**
|
|
33
|
-
* BLOCKS-129 Phase 2B: surfaces the SDK's transport-layer retry
|
|
34
|
-
* activity during a network outage. PubNub's middleware logs
|
|
35
|
-
* `'HTTP request retry #N in Nms.'` at warn level whenever the
|
|
36
|
-
* transport schedules a retry; with subscribeRetryUnbounded on these
|
|
37
|
-
* fire continuously through an outage but never bubble up to the
|
|
38
|
-
* Event Engine listener. Wiring this callback turns those warnings
|
|
39
|
-
* into a visible signal so a human watching the log can tell the
|
|
40
|
-
* difference between "agent retrying" and "agent dead".
|
|
41
|
-
*/
|
|
42
|
-
onRetry?: (message: string) => void;
|
|
43
32
|
}
|
|
33
|
+
/** Exported for tests. Resolves eagerly so the value can be compared with `toBe`. */
|
|
34
|
+
export declare const _PUBNUB_LOG_LEVEL_TRACE: number;
|
|
35
|
+
/** Exported for tests. */
|
|
36
|
+
export declare const _PUBNUB_LOG_LEVEL_NONE: number;
|
|
37
|
+
interface PubNubLogMessage {
|
|
38
|
+
location?: string;
|
|
39
|
+
messageType?: string;
|
|
40
|
+
message?: unknown;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Forwarder logger that routes every underlying-transport log entry
|
|
44
|
+
* through the Blocks log() helper at the matching level (trace/debug ->
|
|
45
|
+
* debug, info -> info, warn -> warn, error -> error). Used when
|
|
46
|
+
* BLOCKS_DEBUG_INTERNAL=forward_transport is enabled. The Blocks logger
|
|
47
|
+
* then filters via LOG_LEVEL.
|
|
48
|
+
*/
|
|
49
|
+
declare const buildPubNubForwarder: () => {
|
|
50
|
+
trace: (entry: PubNubLogMessage) => void;
|
|
51
|
+
debug: (entry: PubNubLogMessage) => void;
|
|
52
|
+
info: (entry: PubNubLogMessage) => void;
|
|
53
|
+
warn: (entry: PubNubLogMessage) => void;
|
|
54
|
+
error: (entry: PubNubLogMessage) => void;
|
|
55
|
+
};
|
|
56
|
+
/**
|
|
57
|
+
* Returns the { logLevel, loggers } pair to spread into every
|
|
58
|
+
* realtime-client construction site. Forwarding the underlying transport's
|
|
59
|
+
* own log output requires an explicit BLOCKS_DEBUG_INTERNAL=forward_transport;
|
|
60
|
+
* LOG_LEVEL=debug alone does not enable it.
|
|
61
|
+
*/
|
|
62
|
+
export declare function buildPubNubLogConfig(): {
|
|
63
|
+
logLevel: number;
|
|
64
|
+
loggers: ReturnType<typeof buildPubNubForwarder>[];
|
|
65
|
+
};
|
|
44
66
|
export declare const createPubNubClient: (config: PubNubClientConfig) => PubNub;
|
|
67
|
+
export {};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import e from"pubnub";const n=()=>{const n=e.LogLevel;return"number"==typeof n?.
|
|
1
|
+
import e from"pubnub";import{createLogger as r,isDebugSubsystemEnabled as o}from"./logger.js";const n=(r,o)=>{const n=e.LogLevel;return"number"==typeof n?.[r]?n[r]:o};export const _PUBNUB_LOG_LEVEL_TRACE=n("Trace",0);export const _PUBNUB_LOG_LEVEL_NONE=n("None",5);const t=r("Transport"),u=e=>{if("string"==typeof e)return e;try{return JSON.stringify(e)}catch{return String(e)}};export function buildPubNubLogConfig(){return o("forward_transport")?{logLevel:_PUBNUB_LOG_LEVEL_TRACE,loggers:[{trace:e=>t("debug",u(e.message),{location:e.location}),debug:e=>t("debug",u(e.message),{location:e.location}),info:e=>t("info",u(e.message),{location:e.location}),warn:e=>t("warn",u(e.message),{location:e.location}),error:e=>t("error",u(e.message),{location:e.location})}]}:{logLevel:_PUBNUB_LOG_LEVEL_NONE,loggers:[{trace:()=>{},debug:()=>{},info:()=>{},warn:()=>{},error:()=>{}}]}}export const createPubNubClient=r=>{if(!r.subscribeKey||!r.publishKey)throw new Error("PUBNUB keys not configured");const o=r.subscribeRetryUnbounded??!0?(()=>{const r=e.ExponentialRetryPolicy;if("function"!=typeof r)return null;const o=r({minimumDelay:2,maximumDelay:60,maximumRetry:6});try{o.maximumRetry=43200,o.validate=()=>{}}catch{return null}return 43200!==o.maximumRetry?null:o})():null,{logLevel:n,loggers:t}=buildPubNubLogConfig();return new e({publishKey:r.publishKey,subscribeKey:r.subscribeKey,userId:r.userId??"blocks-agent",enableEventEngine:!0,...void 0!==r.presenceTimeout?{presenceTimeout:r.presenceTimeout}:{},...!0===r.announceSuccessfulHeartbeats?{announceSuccessfulHeartbeats:!0}:{},...null!==o?{retryConfiguration:o}:{},logLevel:n,loggers:t})};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{CURRENT_PROTOCOL_VERSION as
|
|
1
|
+
import{preflightAuthOrThrow as r}from"./auth-provider.js";import{CURRENT_PROTOCOL_VERSION as t,PROTOCOL_VERSION_HEADER as e}from"./protocol-version.js";import{captureAffinity as o,injectAffinity as n}from"./write-affinity.js";export async function withRetry(r,t=3,e=500){let o;for(let n=0;n<t;n++)try{return await r()}catch(r){o=r;const i=String(r);if(!(i.includes("ENOTFOUND")||i.includes("ETIMEDOUT")||i.includes("ECONNRESET")||i.includes("NetworkIssues"))||n===t-1)throw r;const a=e*Math.pow(2,n)+100*Math.random();await new Promise(r=>setTimeout(r,a))}throw o}export function rpcEndpoint(r,t){return t?`${t.replace(/\/+$/,"")}/api/v1/rpc`:`https://ps.pndsn.com/v1/blocks/sub-key/${r}/rpc`}export class RpcError extends Error{constructor(r,t,e){super(`[RPC] ${r}`),this.name="RpcError",this.rpcMessage=r,this.code=t,this.data=e}}export class BillingModeMismatchError extends RpcError{constructor(r,t,e,o,n){super(r,o,n),this.name="BillingModeMismatchError",this.expected=t,this.got=e}}export async function callRpc(i,a,s){const c=rpcEndpoint(i.subscribeKey,i.baseUrl),d=`rpc-${Date.now()}-${Math.random().toString(36).slice(2,8)}`,u=r=>{const o={"Content-Type":"application/json",[e]:t},a=i.authProvider?.getAuthHeader();return a&&(o.Authorization=a),r&&n(o),o},h={jsonrpc:"2.0",id:d,method:a,params:s},p=async()=>{let r;if(i.agentAuth?r=await i.agentAuth.authenticatedFetch(c,{method:"POST",headers:u(!1),body:JSON.stringify(h)}):(r=await fetch(c,{method:"POST",headers:u(!0),body:JSON.stringify(h)}),o(r.headers)),!r.ok)throw new RpcError(`HTTP ${r.status}`,r.status);const t=await r.json();if(t.error){const r=t.error.data?.message??t.error.message??"Unknown RPC error",e=function(r,t,e){if(!e||"object"!=typeof e)return null;const o=e;if("BillingModeMismatch"!==o.code)return null;const n=o.details;if(!n||"object"!=typeof n)return null;const i=n;return"free"!==i.expected&&"paid"!==i.expected||"free"!==i.got&&"paid"!==i.got?null:new BillingModeMismatchError(r,i.expected,i.got,t,e)}(r,t.error.code,t.error.data);if(e)throw e;throw new RpcError(r,t.error.code,t.error.data)}return t.result};return await r(i.authProvider),i.authProvider?.ensureReady&&await i.authProvider.ensureReady(),withRetry(async()=>{try{return await p()}catch(r){if(r instanceof RpcError&&401===r.code&&i.authProvider&&!i.agentAuth){if(await i.authProvider.onAuthFailure())return p()}throw r}})}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import e from"pubnub";import{callRpc as t}from"./rpc-client.js";import{taskChannel as n}from"./channel-manager.js";import{TaskSession as s}from"./task-session.js";import{ConsumerAuth as i}from"./consumer-auth.js";import{shouldInlineArtifact as r,buildArtifactRef as o}from"./artifacts.js";import{uploadFile as a}from"./file-upload.js";import{normalizeFileInput as c}from"./file-input.js";import{fetchCdmConfig as u}from"./cdm-config.js";import{captureAffinity as d,injectAffinity as l}from"./write-affinity.js";import{getEnv as b}from"../env.js";import{log as h}from"./logger.js";import{StreamRef as p}from"./stream-ref.js";import{invertDirection as f}from"../stream/index.js";import{asPubNubFetcher as m}from"./pubnub-types.js";import{CURRENT_PROTOCOL_VERSION as g,PROTOCOL_VERSION_HEADER as k}from"./protocol-version.js";import{getAgent as y}from"./agent-registry.js";const w=(e,t,n)=>h("[TaskClient]",e,t,n),T=new Set(["completed","failed","canceled"]);export class AnonTaskAccessDeniedError extends Error{constructor(e="anon-task-read-token denied: HTTP 403 Forbidden"){super(e),this.name="AnonTaskAccessDeniedError"}}function I(e,t,n,s){const i=new Map,r=[],o=[];let a,c="0",u="0";for(const d of e){d.timetoken&&d.timetoken>c&&(c=d.timetoken);const e=d.message;if(e&&"object"==typeof e&&e.type){if(o.push(e),"terminal"===e.type){const t=String(d.timetoken??"0");if(t>=u){const n=e.state;"completed"!==n&&"failed"!==n&&"canceled"!==n||(a=n,u=t)}}if("progress"===e.type&&"stream_started"===e.streamEvent&&e.streams){const r=e.declaredStream,o=e.streams;for(const[e,a]of Object.entries(o)){if(!a||"object"!=typeof a||i.has(e))continue;const o=a.direction,c=f(o),u=a.format;if(!u||"bytes"!==u&&"events"!==u)continue;const d=a.affinity;if("dedicated"!==d&&"shared"!==d){w("warn",`history-preload: dropping stream "${e}" for task "${t}" — invalid or missing affinity`,{event:"history_preload_invalid_affinity",streamId:e,taskId:t,receivedAffinity:a.affinity});continue}const l={taskId:t,streamId:e,agentName:n,channel:a.channel,token:a.token,agentDirection:o,localDirection:c,format:u,affinity:d,metadata:a.metadata,declaredStream:r};i.set(e,new p(l,s))}}if("artifact"===e.type&&e.artifactRef){const t=e.artifactRef;t&&"object"==typeof t&&t.kind&&r.push(t)}}}return{streams:i,artifacts:r,events:o,highWaterMark:c,terminalState:a}}async function S(e,t){const n=[];let s;for(;;){const i={channels:[t],count:100};s&&(i.start=s);const r=await e.fetchMessages(i),o=r.channels?.[t]??[];if(0===o.length)break;if(n.push(...o),o.length<100)break;s=o[0].timetoken}return n.sort((e,t)=>e.timetoken<t.timetoken?-1:e.timetoken>t.timetoken?1:0),n}export class TaskClient{constructor(e){if(this._ownsPubNub=!1,"free"!==e.billingMode&&"paid"!==e.billingMode)throw new Error("TaskClient requires a billingMode option ('free' or 'paid')");this._billingMode=e.billingMode,this._anonFingerprint=e.anonFingerprint??null,this.config={subscribeKey:e.subscribeKey,authProvider:e.authProvider,baseUrl:e.baseUrl,agentAuth:e.agentAuth},this._pubnub=e.pubnub,this._createPubNub=e.createPubNub,this._createSessionPubNub=e.createSessionPubNub,this.defaultOwnerId=e.defaultOwnerId,this._subscribeKey=e.subscribeKey,this._publishKey=e.publishKey??"",this._ownsPubNub=!e.pubnub&&!!e.createPubNub}static async create(t){const n=t??{},s=n.billingMode;if("free"!==s&&"paid"!==s)throw new Error("TaskClient.create() requires a billingMode option ('free' or 'paid')");const r=!!(n.apiKey||n.tokenEndpoint||n.tokenProvider),o=!!n.anonFingerprint;if([n.apiKey,n.tokenEndpoint,n.tokenProvider,n.anonFingerprint].filter(Boolean).length>1)throw new Error("Only one token provider mode may be specified");if(o&&"free"!==s)throw new Error("TaskClient.create() with anonFingerprint requires billingMode: 'free'");const a=n.cdmUrl??b("BLOCKS_CDM_URL"),c=await u(a),d="paid"===s?c.network:c.playground,l=n.subscribeKey??b("BLOCKS_SUBSCRIBE_KEY")??d.subscribeKey,h=n.publishKey??b("BLOCKS_PUBLISH_KEY")??d.publishKey,p=n.baseUrl??b("BLOCKS_BACKEND_URL")??c.api.baseUrl;if(!l)throw new Error("TaskClient.create() could not resolve subscribeKey from options, env, or CDM");if(!p)throw new Error("TaskClient.create() could not resolve baseUrl. Set baseUrl option, BLOCKS_BACKEND_URL env var, or ensure CDM config has api.baseUrl.");const f=()=>{const t=`blocks-task-${Date.now().toString(36)}-${Math.random().toString(36).slice(2,6)}`;return new e({subscribeKey:l,publishKey:h||void 0,userId:t,enableEventEngine:!0})};if(r){const e=new i({apiKey:n.apiKey,tokenEndpoint:n.tokenEndpoint,tokenProvider:n.tokenProvider,baseUrl:p,onAuthError:n.onAuthError});await e.init();const t=new TaskClient({billingMode:s,subscribeKey:l,publishKey:h,baseUrl:p,createSessionPubNub:f,defaultOwnerId:e.getUserId()??void 0});return t.config.authProvider=e,t._consumerAuth=e,t}return new TaskClient({billingMode:s,subscribeKey:l,publishKey:h,baseUrl:p,createSessionPubNub:f,anonFingerprint:n.anonFingerprint})}getUserId(){return this._consumerAuth?.getUserId()??null}async getAgentCard(e){const t=await y(e,{baseUrl:this.config.baseUrl});return t?.card??null}updateKeys(e,t){this.config.subscribeKey=e,this._subscribeKey=e,this._publishKey=t??""}getPubNub(){if(!this._pubnub&&this._createPubNub&&(this._pubnub=this._createPubNub()),!this._pubnub)throw new Error("TaskClient requires a pubnub instance for subscribe. Pass pubnub or createPubNub in TaskClientOptions.");return this._pubnub}destroy(){this._pubnub&&this._ownsPubNub&&(this._pubnub.destroy(),this._pubnub=void 0),this._consumerAuth&&this._consumerAuth.destroy()}[Symbol.dispose](){this.destroy()}createPerSessionPubNub(t,n,s){let i;const r=n??this._subscribeKey,o=s??this._publishKey;if(this._createSessionPubNub&&r===this._subscribeKey)i=this._createSessionPubNub();else{const t=`blocks-task-${Date.now().toString(36)}-${Math.random().toString(36).slice(2,6)}`;i=new e({subscribeKey:r,publishKey:o||void 0,userId:t,enableEventEngine:!0})}return t&&i.setToken(t),i}async fetchConsumerReadToken(e,t="consumer"){if(!this.config.baseUrl)throw new Error("connect() requires a backend baseUrl. Set baseUrl in TaskClientOptions.");const n=`${this.config.baseUrl.replace(/\/+$/,"")}/api/v1/auth/task-read-token`,s=async()=>{const s={"Content-Type":"application/json",[k]:g},i=this.config.authProvider?.getAuthHeader();return i&&(s.Authorization=i),l(s),fetch(n,{method:"POST",headers:s,body:JSON.stringify({taskId:e,role:t})})};let i=await s();if(d(i.headers),401===i.status&&this.config.authProvider){await this.config.authProvider.onAuthFailure()&&(i=await s(),d(i.headers))}if(!i.ok){const e=await i.text().catch(()=>"");throw new Error(`task-read-token failed: HTTP ${i.status}${e?` ${e}`:""}`)}return i.json()}async fetchAnonConsumerReadToken(e){if(!this.config.baseUrl)throw new Error("connect() requires a backend baseUrl. Set baseUrl in TaskClientOptions.");if(!this._anonFingerprint)throw new Error("fetchAnonConsumerReadToken called without anonFingerprint");const t=`${this.config.baseUrl.replace(/\/+$/,"")}/api/v1/auth/anon-task-read-token`,n={"Content-Type":"application/json",[k]:g};l(n);const s=await fetch(t,{method:"POST",headers:n,body:JSON.stringify({taskId:e,fingerprint:this._anonFingerprint})});if(d(s.headers),403===s.status){const e=await s.text().catch(()=>"");throw new AnonTaskAccessDeniedError("anon-task-read-token denied: HTTP 403"+(e?` ${e}`:""))}if(!s.ok){const e=await s.text().catch(()=>"");throw new Error(`anon-task-read-token failed: HTTP ${s.status}${e?` ${e}`:""}`)}return s.json()}async sendMessage(e){if(this._anonFingerprint)throw new Error("anon-mode TaskClient does not support sendMessage()");this.config.authProvider?.ensureReady&&await this.config.authProvider.ensureReady();const i=e.ownerId||this.defaultOwnerId||"",u=e.taskKind,d=e.duration;if("pipe"===u){if(null==d||!Number.isInteger(d)||d<1||d>43200)throw new Error("Pipe tasks require a duration between 1 and 43200 minutes")}else if(null!=d)throw new Error("Request tasks must not include a duration. Duration is only valid for pipe tasks.");let l;const b=[];for(const t of e.requestParts)if(t.file){if(!t.partId)throw new Error("partId is required for file-bearing request parts");const n=c(t.file),s=t.fileName??"unnamed",i=t.contentType??"application/octet-stream";if(r(n.size)){const e=await n.getBytes(),r=o({data:e,mimeType:i,fileName:s}),{file:a,fileName:c,text:u,...d}=t;b.push({...d,artifactRef:r})}else{if(!this.config.baseUrl)throw new Error("File upload requires a backend baseUrl. Set baseUrl in TaskClientOptions.");const r={baseUrl:this.config.baseUrl,authProvider:this.config.authProvider,agentAuth:this.config.agentAuth},o={role:"consumer-input",agentName:e.agentName,fileName:s,fileSize:n.size,mimeType:i,partId:t.partId,uploadSessionId:l},c=await a(r,o,n.uploadBody);c.uploadSessionId&&(l=c.uploadSessionId);const{file:u,fileName:d,...h}=t;b.push({partId:h.partId,...h.contentType?{contentType:h.contentType}:{}})}}else{const{file:e,fileName:n,...s}=t;b.push(s)}const h={agentName:e.agentName,billingMode:this._billingMode,requestParts:b};l&&(h.uploadSessionId=l),e.idempotencyKey&&(h.idempotencyKey=e.idempotencyKey),h.ownerId=i;const p={};u&&(p.taskKind=u),null!=d&&(p.duration=d),e.consumerPublicKey&&(p.consumerPublicKey=e.consumerPublicKey),Object.keys(p).length>0&&(h.extensions={blocks:p}),e.pushNotificationConfig&&(h.pushNotificationConfig=e.pushNotificationConfig),e.retryPolicy&&(h.retryPolicy=e.retryPolicy);const f=await t(this.config,"SendMessage",h),g=f.extensions?.blocks,k=g?.readToken??null,y=g?.streamChannels?.status??void 0,w=g?.subscribeKey??this._subscribeKey,C=g?.publishKey??this._publishKey;if(!0===f.idempotent&&!!f.state&&T.has(f.state))return new s({taskId:f.taskId,ownerId:i,orgId:f.orgId??i,readToken:k,statusChannel:y,agentName:e.agentName,pubnub:null,ownsSubscribeClient:!1,sdkOptions:{subscribeKey:w,publishKey:C},rpcConfig:this.config,idempotent:f.idempotent,queued:f.queued,pushConfigId:f.pushConfigId,preClosed:!0,state:f.state});const P=this.createPerSessionPubNub(k,w,C),K={subscribeKey:w,publishKey:C},_=y??n(f.taskId,f.orgId??i);try{const t=await P.time(),n=String(t.timetoken),r=m(P);let o,a=new Map,c=[],u="0";if(r?.fetchMessages){const t=I(await S(r,_),f.taskId,e.agentName,K);a=t.streams,c=t.artifacts,u=t.highWaterMark,o=t.terminalState}if(o&&T.has(o))return new s({taskId:f.taskId,ownerId:i,orgId:f.orgId??i,readToken:k,statusChannel:_,agentName:e.agentName,pubnub:P,ownsSubscribeClient:!0,sdkOptions:K,rpcConfig:this.config,idempotent:f.idempotent,queued:f.queued,pushConfigId:f.pushConfigId,autoDrain:e.autoDrain,drainWindowMs:e.drainWindowMs,state:o,skipSubscription:!0,preloadedStreams:a,preloadedArtifacts:c});const d="0"!==u?u:n,l=[];let b,h=!1;const p={message:e=>{if(e.channel!==_)return;const t=e.message;if(!t||"object"!=typeof t||!t.type)return;const n=String(e.timetoken??"0");h&&b?b(t,n):l.push({message:t,timetoken:n})}};P.addListener(p),P.subscribe({channels:[_],timetoken:d});const g=new s({taskId:f.taskId,ownerId:i,orgId:f.orgId??i,readToken:k,statusChannel:_,agentName:e.agentName,pubnub:P,ownsSubscribeClient:!0,sdkOptions:K,rpcConfig:this.config,idempotent:f.idempotent,queued:f.queued,pushConfigId:f.pushConfigId,autoDrain:e.autoDrain,drainWindowMs:e.drainWindowMs,preloadedStreams:a,preloadedArtifacts:c,externalSubscription:{listener:p,channel:_,onReady:e=>{b=e}}});if(b){const e=b;for(const t of l)t.timetoken>d&&e(t.message,t.timetoken)}return l.length=0,h=!0,g}catch{return new s({taskId:f.taskId,ownerId:i,orgId:f.orgId??i,readToken:k,statusChannel:_,agentName:e.agentName,pubnub:P,ownsSubscribeClient:!0,sdkOptions:K,rpcConfig:this.config,idempotent:f.idempotent,queued:f.queued,pushConfigId:f.pushConfigId,autoDrain:e.autoDrain,drainWindowMs:e.drainWindowMs})}}async connect(e){const{taskId:t}=e;if(this._anonFingerprint){const n=await this.getTask(t);if(!n)throw new Error(`Task not found: ${t}`);const s=n.agentName??"",i=n.state??"",r=await this.fetchAnonConsumerReadToken(t);return this._openSessionFromToken({task:n,agentName:s,taskState:i,tokenResult:r,autoDrain:e.autoDrain,drainWindowMs:e.drainWindowMs})}if(!this.config.authProvider?.getAuthHeader())throw new Error("connect() requires an authenticated TaskClient. Use apiKey, tokenEndpoint, or tokenProvider. AgentAuth is not supported for consumer task connections.");const n=await this.getTask(t);if(!n)throw new Error(`Task not found: ${t}`);const s=n.agentName??"",i=n.state??"",r=await this.fetchConsumerReadToken(t,e.role);return this._openSessionFromToken({task:n,agentName:s,taskState:i,tokenResult:r,autoDrain:e.autoDrain,drainWindowMs:e.drainWindowMs})}async _openSessionFromToken(e){const{task:t,agentName:n,taskState:i,tokenResult:r,autoDrain:o,drainWindowMs:a}=e,c=t.taskId,{pamToken:u,channel:d}=r,l={subscribeKey:this._subscribeKey,publishKey:this._publishKey},b=this.createPerSessionPubNub(u);try{const e=await b.time(),r=String(e.timetoken),h=m(b);let p,f=new Map,g=[],k=[],y="0";if(h?.fetchMessages){const e=I(await S(h,d),c,n,l);f=e.streams,g=e.artifacts,k=e.events,y=e.highWaterMark,p=e.terminalState}const w=p&&T.has(p)?p:i;if(T.has(w))return new s({taskId:c,ownerId:t.owner??"",readToken:u,statusChannel:d,agentName:n,pubnub:b,ownsSubscribeClient:!0,sdkOptions:l,rpcConfig:this.config,autoDrain:o,drainWindowMs:a,state:w,skipSubscription:!0,preloadedStreams:f,preloadedArtifacts:g,preloadedEvents:k});const C="0"!==y?y:r,P=[];let K,_=!1;const N={message:e=>{if(e.channel!==d)return;const t=e.message;if(!t||"object"!=typeof t||!t.type)return;const n=String(e.timetoken??"0");_&&K?K(t,n):P.push({message:t,timetoken:n})}};b.addListener(N),b.subscribe({channels:[d],timetoken:C});const E=new s({taskId:c,ownerId:t.owner??"",readToken:u,statusChannel:d,agentName:n,pubnub:b,ownsSubscribeClient:!0,sdkOptions:l,rpcConfig:this.config,autoDrain:o,drainWindowMs:a,state:i,preloadedStreams:f,preloadedArtifacts:g,preloadedEvents:k,externalSubscription:{listener:N,channel:d,onReady:e=>{K=e}}});if(K){const e=K;for(const t of P)t.timetoken>C&&(E._appendHistoryEvent(t.message,t.timetoken),e(t.message,t.timetoken))}return P.length=0,_=!0,E}catch(e){throw b.destroy(),e}}async getTask(e){return(await t(this.config,"GetTask",{taskId:e})).task}async listTasks(e){return t(this.config,"ListTasks",{...e})}async cancelTask(e){await t(this.config,"CancelTask",{taskId:e})}async pauseTask(e){await t(this.config,"PauseTask",{taskId:e})}async resumeTask(e){await t(this.config,"ResumeTask",{taskId:e})}async retryTask(e){await t(this.config,"RetryTask",{taskId:e})}async terminateTask(e){await t(this.config,"TerminateTask",{taskId:e})}subscribeToTask(e,t,s){return function(e,t,s,i){const r=n(t,s),o={message:e=>{if(e.channel!==r)return;const t=e.message;if(t&&"object"==typeof t&&t.type){if(i.onEvent)try{i.onEvent(t)}catch(e){C(e,"onEvent",t,i.onError)}switch(t.type){case"progress":if(i.onProgress)try{i.onProgress(t)}catch(e){C(e,"onProgress",t,i.onError)}break;case"artifact":if(i.onArtifact)try{i.onArtifact(t)}catch(e){C(e,"onArtifact",t,i.onError)}break;case"terminal":if(i.onTerminal)try{i.onTerminal(t)}catch(e){C(e,"onTerminal",t,i.onError)}break;case"system":if(i.onSystem)try{i.onSystem(t)}catch(e){C(e,"onSystem",t,i.onError)}}}}};return e.addListener(o),e.subscribe({channels:[r],timetoken:1e3}),{unsubscribe(){e.removeListener(o),e.unsubscribe({channels:[r]})}}}(this.getPubNub(),e,t,s)}}function C(e,t,n,s){const i=e instanceof Error?e:new Error(String(e));if(s)try{s(i,{entryPoint:"subscribeToTask",callbackType:t,event:n})}catch{}else w("warn",`subscribeToTask callback error in ${t}`,{event:"subscribe_callback_error",callbackType:t,error:i.message})}
|
|
1
|
+
import e from"pubnub";import{buildPubNubLogConfig as t}from"./pubnub-client.js";import{callRpc as n}from"./rpc-client.js";import{taskChannel as s}from"./channel-manager.js";import{TaskSession as i}from"./task-session.js";import{preflightAuthOrThrow as r}from"./auth-provider.js";import{ConsumerAuth as o}from"./consumer-auth.js";import{shouldInlineArtifact as a,buildArtifactRef as c}from"./artifacts.js";import{uploadFile as u}from"./file-upload.js";import{normalizeFileInput as d}from"./file-input.js";import{fetchCdmConfig as l}from"./cdm-config.js";import{captureAffinity as b,injectAffinity as h}from"./write-affinity.js";import{getEnv as p}from"../env.js";import{log as f}from"./logger.js";import{StreamRef as m}from"./stream-ref.js";import{invertDirection as g}from"../stream/index.js";import{asPubNubFetcher as k}from"./pubnub-types.js";import{CURRENT_PROTOCOL_VERSION as y,PROTOCOL_VERSION_HEADER as w}from"./protocol-version.js";import{getAgent as T}from"./agent-registry.js";const I=(e,t,n)=>f("[TaskClient]",e,t,n),S=new Set(["completed","failed","canceled"]);export class AnonTaskAccessDeniedError extends Error{constructor(e="anon-task-read-token denied: HTTP 403 Forbidden"){super(e),this.name="AnonTaskAccessDeniedError"}}function C(e,t,n,s){const i=new Map,r=[],o=[];let a,c="0",u="0";for(const d of e){d.timetoken&&d.timetoken>c&&(c=d.timetoken);const e=d.message;if(e&&"object"==typeof e&&e.type){if(o.push(e),"terminal"===e.type){const t=String(d.timetoken??"0");if(t>=u){const n=e.state;"completed"!==n&&"failed"!==n&&"canceled"!==n||(a=n,u=t)}}if("progress"===e.type&&"stream_started"===e.streamEvent&&e.streams){const r=e.declaredStream,o=e.streams;for(const[e,a]of Object.entries(o)){if(!a||"object"!=typeof a||i.has(e))continue;const o=a.direction,c=g(o),u=a.format;if(!u||"bytes"!==u&&"events"!==u)continue;const d=a.affinity;if("dedicated"!==d&&"shared"!==d){I("warn",`history-preload: dropping stream "${e}" for task "${t}" — invalid or missing affinity`,{event:"history_preload_invalid_affinity",streamId:e,taskId:t,receivedAffinity:a.affinity});continue}const l={taskId:t,streamId:e,agentName:n,channel:a.channel,token:a.token,agentDirection:o,localDirection:c,format:u,affinity:d,metadata:a.metadata,declaredStream:r};i.set(e,new m(l,s))}}if("artifact"===e.type&&e.artifactRef){const t=e.artifactRef;t&&"object"==typeof t&&t.kind&&r.push(t)}}}return{streams:i,artifacts:r,events:o,highWaterMark:c,terminalState:a}}async function P(e,t){const n=[];let s;for(;;){const i={channels:[t],count:100};s&&(i.start=s);const r=await e.fetchMessages(i),o=r.channels?.[t]??[];if(0===o.length)break;if(n.push(...o),o.length<100)break;s=o[0].timetoken}return n.sort((e,t)=>e.timetoken<t.timetoken?-1:e.timetoken>t.timetoken?1:0),n}export class TaskClient{constructor(e){if(this._ownsPubNub=!1,"free"!==e.billingMode&&"paid"!==e.billingMode)throw new Error("TaskClient requires a billingMode option ('free' or 'paid')");this._billingMode=e.billingMode,this._anonFingerprint=e.anonFingerprint??null,this.config={subscribeKey:e.subscribeKey,authProvider:e.authProvider,baseUrl:e.baseUrl,agentAuth:e.agentAuth},this._pubnub=e.pubnub,this._createPubNub=e.createPubNub,this._createSessionPubNub=e.createSessionPubNub,this.defaultOwnerId=e.defaultOwnerId,this._subscribeKey=e.subscribeKey,this._publishKey=e.publishKey??"",this._ownsPubNub=!e.pubnub&&!!e.createPubNub}static async create(n){const s=n??{},i=s.billingMode;if("free"!==i&&"paid"!==i)throw new Error("TaskClient.create() requires a billingMode option ('free' or 'paid')");const r=!!(s.apiKey||s.tokenEndpoint||s.tokenProvider),a=!!s.anonFingerprint;if([s.apiKey,s.tokenEndpoint,s.tokenProvider,s.anonFingerprint].filter(Boolean).length>1)throw new Error("Only one token provider mode may be specified");if(a&&"free"!==i)throw new Error("TaskClient.create() with anonFingerprint requires billingMode: 'free'");const c=s.cdmUrl??p("BLOCKS_CDM_URL"),u=await l(c),d="paid"===i?u.network:u.playground,b=s.subscribeKey??p("BLOCKS_SUBSCRIBE_KEY")??d.subscribeKey,h=s.publishKey??p("BLOCKS_PUBLISH_KEY")??d.publishKey,f=s.baseUrl??p("BLOCKS_BACKEND_URL")??u.api.baseUrl;if(!b)throw new Error("TaskClient.create() could not resolve subscribeKey from options, env, or CDM");if(!f)throw new Error("TaskClient.create() could not resolve baseUrl. Set baseUrl option, BLOCKS_BACKEND_URL env var, or ensure CDM config has api.baseUrl.");const m=()=>{const n=`blocks-task-${Date.now().toString(36)}-${Math.random().toString(36).slice(2,6)}`;return new e({subscribeKey:b,publishKey:h||void 0,userId:n,enableEventEngine:!0,...t()})};if(r){const e=new o({apiKey:s.apiKey,tokenEndpoint:s.tokenEndpoint,tokenProvider:s.tokenProvider,baseUrl:f,onAuthError:s.onAuthError});await e.init();const t=new TaskClient({billingMode:i,subscribeKey:b,publishKey:h,baseUrl:f,createSessionPubNub:m,defaultOwnerId:e.getUserId()??void 0});return t.config.authProvider=e,t._consumerAuth=e,t}return new TaskClient({billingMode:i,subscribeKey:b,publishKey:h,baseUrl:f,createSessionPubNub:m,anonFingerprint:s.anonFingerprint})}getUserId(){return this._consumerAuth?.getUserId()??null}async getAgentCard(e){const t=await T(e,{baseUrl:this.config.baseUrl});return t?.card??null}updateKeys(e,t){this.config.subscribeKey=e,this._subscribeKey=e,this._publishKey=t??""}getPubNub(){if(!this._pubnub&&this._createPubNub&&(this._pubnub=this._createPubNub()),!this._pubnub)throw new Error("TaskClient requires a pubnub instance for subscribe. Pass pubnub or createPubNub in TaskClientOptions.");return this._pubnub}destroy(){this._pubnub&&this._ownsPubNub&&(this._pubnub.destroy(),this._pubnub=void 0),this._consumerAuth&&this._consumerAuth.destroy()}[Symbol.dispose](){this.destroy()}createPerSessionPubNub(n,s,i){let r;const o=s??this._subscribeKey,a=i??this._publishKey;if(this._createSessionPubNub&&o===this._subscribeKey)r=this._createSessionPubNub();else{const n=`blocks-task-${Date.now().toString(36)}-${Math.random().toString(36).slice(2,6)}`;r=new e({subscribeKey:o,publishKey:a||void 0,userId:n,enableEventEngine:!0,...t()})}return n&&r.setToken(n),r}async fetchConsumerReadToken(e,t="consumer"){if(!this.config.baseUrl)throw new Error("connect() requires a backend baseUrl. Set baseUrl in TaskClientOptions.");const n=`${this.config.baseUrl.replace(/\/+$/,"")}/api/v1/auth/task-read-token`,s=async()=>{const s={"Content-Type":"application/json",[w]:y},i=this.config.authProvider?.getAuthHeader();return i&&(s.Authorization=i),h(s),fetch(n,{method:"POST",headers:s,body:JSON.stringify({taskId:e,role:t})})};let i=await s();if(b(i.headers),401===i.status&&this.config.authProvider){await this.config.authProvider.onAuthFailure()&&(i=await s(),b(i.headers))}if(!i.ok){const e=await i.text().catch(()=>"");throw new Error(`task-read-token failed: HTTP ${i.status}${e?` ${e}`:""}`)}return i.json()}async fetchAnonConsumerReadToken(e){if(!this.config.baseUrl)throw new Error("connect() requires a backend baseUrl. Set baseUrl in TaskClientOptions.");if(!this._anonFingerprint)throw new Error("fetchAnonConsumerReadToken called without anonFingerprint");const t=`${this.config.baseUrl.replace(/\/+$/,"")}/api/v1/auth/anon-task-read-token`,n={"Content-Type":"application/json",[w]:y};h(n);const s=await fetch(t,{method:"POST",headers:n,body:JSON.stringify({taskId:e,fingerprint:this._anonFingerprint})});if(b(s.headers),403===s.status){const e=await s.text().catch(()=>"");throw new AnonTaskAccessDeniedError("anon-task-read-token denied: HTTP 403"+(e?` ${e}`:""))}if(!s.ok){const e=await s.text().catch(()=>"");throw new Error(`anon-task-read-token failed: HTTP ${s.status}${e?` ${e}`:""}`)}return s.json()}async sendMessage(e){if(this._anonFingerprint)throw new Error("anon-mode TaskClient does not support sendMessage()");await r(this.config.authProvider),this.config.authProvider?.ensureReady&&await this.config.authProvider.ensureReady();const t=e.ownerId||this.defaultOwnerId||"",o=e.taskKind,l=e.duration;if("pipe"===o){if(null==l||!Number.isInteger(l)||l<1||l>43200)throw new Error("Pipe tasks require an integer duration between 1 and 43200 minutes")}else if(null!=l)throw new Error("Request tasks must not include a duration. Duration is only valid for pipe tasks.");let b;const h=[];for(const t of e.requestParts)if(t.file){if(!t.partId)throw new Error("partId is required for file-bearing request parts");const n=d(t.file),s=t.fileName??"unnamed",i=t.contentType??"application/octet-stream";if(a(n.size)){const e=await n.getBytes(),r=c({data:e,mimeType:i,fileName:s}),{file:o,fileName:a,text:u,...d}=t;h.push({...d,artifactRef:r})}else{if(!this.config.baseUrl)throw new Error("File upload requires a backend baseUrl. Set baseUrl in TaskClientOptions.");const r={baseUrl:this.config.baseUrl,authProvider:this.config.authProvider,agentAuth:this.config.agentAuth},o={role:"consumer-input",agentName:e.agentName,fileName:s,fileSize:n.size,mimeType:i,partId:t.partId,uploadSessionId:b},a=await u(r,o,n.uploadBody);a.uploadSessionId&&(b=a.uploadSessionId);const{file:c,fileName:d,...l}=t;h.push({partId:l.partId,...l.contentType?{contentType:l.contentType}:{}})}}else{const{file:e,fileName:n,...s}=t;h.push(s)}const p={agentName:e.agentName,billingMode:this._billingMode,requestParts:h};b&&(p.uploadSessionId=b),e.idempotencyKey&&(p.idempotencyKey=e.idempotencyKey),p.ownerId=t;const f={};o&&(f.taskKind=o),null!=l&&(f.duration=l),e.consumerPublicKey&&(f.consumerPublicKey=e.consumerPublicKey),Object.keys(f).length>0&&(p.extensions={blocks:f}),e.pushNotificationConfig&&(p.pushNotificationConfig=e.pushNotificationConfig),e.retryPolicy&&(p.retryPolicy=e.retryPolicy);const m=await n(this.config,"SendMessage",p),g=m.extensions?.blocks,y=g?.readToken??null,w=g?.streamChannels?.status??void 0,T=g?.subscribeKey??this._subscribeKey,I=g?.publishKey??this._publishKey;if(!0===m.idempotent&&!!m.state&&S.has(m.state))return new i({taskId:m.taskId,ownerId:t,orgId:m.orgId??t,readToken:y,statusChannel:w,agentName:e.agentName,pubnub:null,ownsSubscribeClient:!1,sdkOptions:{subscribeKey:T,publishKey:I},rpcConfig:this.config,idempotent:m.idempotent,queued:m.queued,pushConfigId:m.pushConfigId,preClosed:!0,state:m.state});const K=this.createPerSessionPubNub(y,T,I),_={subscribeKey:T,publishKey:I},N=w??s(m.taskId,m.orgId??t);try{const n=await K.time(),s=String(n.timetoken),r=k(K);let o,a=new Map,c=[],u="0";if(r?.fetchMessages){const t=C(await P(r,N),m.taskId,e.agentName,_);a=t.streams,c=t.artifacts,u=t.highWaterMark,o=t.terminalState}if(o&&S.has(o))return new i({taskId:m.taskId,ownerId:t,orgId:m.orgId??t,readToken:y,statusChannel:N,agentName:e.agentName,pubnub:K,ownsSubscribeClient:!0,sdkOptions:_,rpcConfig:this.config,idempotent:m.idempotent,queued:m.queued,pushConfigId:m.pushConfigId,autoDrain:e.autoDrain,drainWindowMs:e.drainWindowMs,state:o,skipSubscription:!0,preloadedStreams:a,preloadedArtifacts:c});const d="0"!==u?u:s,l=[];let b,h=!1;const p={message:e=>{if(e.channel!==N)return;const t=e.message;if(!t||"object"!=typeof t||!t.type)return;const n=String(e.timetoken??"0");h&&b?b(t,n):l.push({message:t,timetoken:n})}};K.addListener(p),K.subscribe({channels:[N],timetoken:d});const f=new i({taskId:m.taskId,ownerId:t,orgId:m.orgId??t,readToken:y,statusChannel:N,agentName:e.agentName,pubnub:K,ownsSubscribeClient:!0,sdkOptions:_,rpcConfig:this.config,idempotent:m.idempotent,queued:m.queued,pushConfigId:m.pushConfigId,autoDrain:e.autoDrain,drainWindowMs:e.drainWindowMs,preloadedStreams:a,preloadedArtifacts:c,externalSubscription:{listener:p,channel:N,onReady:e=>{b=e}}});if(b){const e=b;for(const t of l)t.timetoken>d&&e(t.message,t.timetoken)}return l.length=0,h=!0,f}catch{return new i({taskId:m.taskId,ownerId:t,orgId:m.orgId??t,readToken:y,statusChannel:N,agentName:e.agentName,pubnub:K,ownsSubscribeClient:!0,sdkOptions:_,rpcConfig:this.config,idempotent:m.idempotent,queued:m.queued,pushConfigId:m.pushConfigId,autoDrain:e.autoDrain,drainWindowMs:e.drainWindowMs})}}async connect(e){const{taskId:t}=e;if(this._anonFingerprint){const n=await this.getTask(t);if(!n)throw new Error(`Task not found: ${t}`);const s=n.agentName??"",i=n.state??"",r=await this.fetchAnonConsumerReadToken(t);return this._openSessionFromToken({task:n,agentName:s,taskState:i,tokenResult:r,autoDrain:e.autoDrain,drainWindowMs:e.drainWindowMs})}if(await r(this.config.authProvider),!this.config.authProvider?.getAuthHeader())throw new Error("connect() requires an authenticated TaskClient. Use apiKey, tokenEndpoint, or tokenProvider. AgentAuth is not supported for consumer task connections.");const n=await this.getTask(t);if(!n)throw new Error(`Task not found: ${t}`);const s=n.agentName??"",i=n.state??"",o=await this.fetchConsumerReadToken(t,e.role);return this._openSessionFromToken({task:n,agentName:s,taskState:i,tokenResult:o,autoDrain:e.autoDrain,drainWindowMs:e.drainWindowMs})}async _openSessionFromToken(e){const{task:t,agentName:n,taskState:s,tokenResult:r,autoDrain:o,drainWindowMs:a}=e,c=t.taskId,{pamToken:u,channel:d}=r,l={subscribeKey:this._subscribeKey,publishKey:this._publishKey},b=this.createPerSessionPubNub(u);try{const e=await b.time(),r=String(e.timetoken),h=k(b);let p,f=new Map,m=[],g=[],y="0";if(h?.fetchMessages){const e=C(await P(h,d),c,n,l);f=e.streams,m=e.artifacts,g=e.events,y=e.highWaterMark,p=e.terminalState}const w=p&&S.has(p)?p:s;if(S.has(w))return new i({taskId:c,ownerId:t.owner??"",readToken:u,statusChannel:d,agentName:n,pubnub:b,ownsSubscribeClient:!0,sdkOptions:l,rpcConfig:this.config,autoDrain:o,drainWindowMs:a,state:w,skipSubscription:!0,preloadedStreams:f,preloadedArtifacts:m,preloadedEvents:g});const T="0"!==y?y:r,I=[];let K,_=!1;const N={message:e=>{if(e.channel!==d)return;const t=e.message;if(!t||"object"!=typeof t||!t.type)return;const n=String(e.timetoken??"0");_&&K?K(t,n):I.push({message:t,timetoken:n})}};b.addListener(N),b.subscribe({channels:[d],timetoken:T});const v=new i({taskId:c,ownerId:t.owner??"",readToken:u,statusChannel:d,agentName:n,pubnub:b,ownsSubscribeClient:!0,sdkOptions:l,rpcConfig:this.config,autoDrain:o,drainWindowMs:a,state:s,preloadedStreams:f,preloadedArtifacts:m,preloadedEvents:g,externalSubscription:{listener:N,channel:d,onReady:e=>{K=e}}});if(K){const e=K;for(const t of I)t.timetoken>T&&(v._appendHistoryEvent(t.message,t.timetoken),e(t.message,t.timetoken))}return I.length=0,_=!0,v}catch(e){throw b.destroy(),e}}async getTask(e){return(await n(this.config,"GetTask",{taskId:e})).task}async listTasks(e){return n(this.config,"ListTasks",{...e})}async cancelTask(e){await n(this.config,"CancelTask",{taskId:e})}async pauseTask(e){await n(this.config,"PauseTask",{taskId:e})}async resumeTask(e){await n(this.config,"ResumeTask",{taskId:e})}async retryTask(e){await n(this.config,"RetryTask",{taskId:e})}async terminateTask(e){await n(this.config,"TerminateTask",{taskId:e})}subscribeToTask(e,t,n){return function(e,t,n,i){const r=s(t,n),o={message:e=>{if(e.channel!==r)return;const t=e.message;if(t&&"object"==typeof t&&t.type){if(i.onEvent)try{i.onEvent(t)}catch(e){K(e,"onEvent",t,i.onError)}switch(t.type){case"progress":if(i.onProgress)try{i.onProgress(t)}catch(e){K(e,"onProgress",t,i.onError)}break;case"artifact":if(i.onArtifact)try{i.onArtifact(t)}catch(e){K(e,"onArtifact",t,i.onError)}break;case"terminal":if(i.onTerminal)try{i.onTerminal(t)}catch(e){K(e,"onTerminal",t,i.onError)}break;case"system":if(i.onSystem)try{i.onSystem(t)}catch(e){K(e,"onSystem",t,i.onError)}}}}};return e.addListener(o),e.subscribe({channels:[r],timetoken:1e3}),{unsubscribe(){e.removeListener(o),e.unsubscribe({channels:[r]})}}}(this.getPubNub(),e,t,n)}}function K(e,t,n,s){const i=e instanceof Error?e:new Error(String(e));if(s)try{s(i,{entryPoint:"subscribeToTask",callbackType:t,event:n})}catch{}else I("warn",`subscribeToTask callback error in ${t}`,{event:"subscribe_callback_error",callbackType:t,error:i.message})}
|
|
@@ -1 +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 i}from"./channel-manager.js";import{callRpc as r}from"./rpc-client.js";import{downloadArtifact as a}from"./artifacts.js";import{log as n}from"./logger.js";const o=(t,e,s)=>n("[TaskSession]",t,e,s);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.historyEvents=[],this.historyTimetokens=new Set,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??i(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,i]of t.preloadedStreams){const t=new s(i.descriptor,this.sdkOptions,{onOpen:t=>this.trackStreamClient(t),sessionState:()=>this.state});this.streams.set(e,t)}t.preloadedArtifacts&&this.artifacts.push(...t.preloadedArtifacts),t.preloadedEvents&&this.historyEvents.push(...t.preloadedEvents),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 i=null!=t.timetoken?String(t.timetoken):void 0;this.handleEvent(s,i)}},t.addListener(this.listener),t.subscribe({channels:[e],timetoken:1e3})}routeCallbackError(t,e,s){const i=t instanceof Error?t:new Error(String(t));if(this.errorCallbacks.length>0)for(const t of this.errorCallbacks)try{t(i,{entryPoint:"taskSession",callbackType:e,event:s})}catch{}else o("warn",`callback error in ${e}`,{event:"task_session_callback_error",callbackType:e,error:i.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 i=t.streams;if(!i||"object"!=typeof i)return;const r=t.declaredStream;for(const[t,a]of Object.entries(i)){if(!a||"object"!=typeof a)continue;if(this.streams.has(t))continue;const i=a.direction,n=e(i),c=a.format;if(!c||"bytes"!==c&&"events"!==c)continue;const l=a.affinity;if("dedicated"!==l&&"shared"!==l){o("warn",`live stream_started: dropping stream "${t}" for task "${this.taskId}" — invalid or missing affinity`,{event:"task_session_live_invalid_affinity",streamId:t,taskId:this.taskId,receivedAffinity:a.affinity});continue}const h={taskId:this.taskId,streamId:t,agentName:this.agentName,channel:a.channel,token:a.token,agentDirection:i,localDirection:n,format:c,affinity:l,metadata:a.metadata,declaredStream:r},m=new s(h,this.sdkOptions,{onOpen:t=>this.trackStreamClient(t),sessionState:()=>this.state});this.streams.set(t,m);for(const t of this.streamCallbacks)try{t(m)}catch(t){this.routeCallbackError(t,"onStream",m)}this.resolveWaiters(m)}}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 i=!1;if(void 0!==s.streamId)i=t.descriptor.declaredStream===s.streamId||t.descriptor.streamId===s.streamId;else if(s.predicate)try{i=s.predicate(t)}catch(e){this.routeCallbackError(e,"streamPredicate",t)}else i=!0;i?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){this.artifactCallbacks.push(t);for(const e of[...this.artifacts]){const s={type:"artifact",taskId:this.taskId,artifactRef:e};try{t(s)}catch(t){this.routeCallbackError(t,"onArtifact",s)}}return()=>{this.artifactCallbacks=this.artifactCallbacks.filter(e=>e!==t)}}onTerminal(t){if(this.terminalCallbacks.push(t),this.state&&TERMINAL_STATES.has(this.state)){const e={type:"terminal",taskId:this.taskId,state:this.state};try{t(e)}catch(t){this.routeCallbackError(t,"onTerminal",e)}}return()=>{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]}listEvents(){return[...this.historyEvents]}_appendHistoryEvent(t,e){if(e){if(this.historyTimetokens.has(e))return;this.historyTimetokens.add(e)}this.historyEvents.push(t)}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)}`,i=new t({subscribeKey:this.subscribeKey,publishKey:this.publishKey||void 0,userId:s});this.readToken&&i.setToken(this.readToken);try{return await a(e,i)}finally{i.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 i=s.descriptor.localDirection;if("inbound"===i||"bidirectional"===i)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 i={resolve:e,reject:s};if(this.terminalWaiters.push(i),void 0!==t){const e=setTimeout(()=>{const e=this.terminalWaiters.indexOf(i);-1!==e&&this.terminalWaiters.splice(e,1),s(new Error(`waitForTerminal timed out after ${t}ms`))},t),r=i.resolve,a=i.reject;i.resolve=t=>{clearTimeout(e),r(t)},i.reject=t=>{clearTimeout(e),a(t)}}})}async saveArtifacts(t){const{mkdirSync:e,writeFileSync:s}=await import("node:fs"),{join:i,resolve:r,basename:a}=await import("node:path"),n=r(t);e(n,{recursive:!0});const o=[],c=this.listArtifacts();for(let t=0;t<c.length;t++){const e=await this.downloadArtifact(c[t]),l=e.fileName??`artifact-${t}`,h=r(i(n,a(l)||`artifact-${t}`));if(!h.startsWith(n+"/")&&h!==n)throw new Error(`Artifact filename "${l}" 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 r(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 r(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;
|
|
1
|
+
import t from"pubnub";import{buildPubNubLogConfig as e}from"./pubnub-client.js";import{invertDirection as s}from"../stream/index.js";import{StreamRef as i}from"./stream-ref.js";import{taskChannel as r}from"./channel-manager.js";import{callRpc as a}from"./rpc-client.js";import{downloadArtifact as n}from"./artifacts.js";import{log as o}from"./logger.js";const c=(t,e,s)=>o("[TaskSession]",t,e,s);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.historyEvents=[],this.historyTimetokens=new Set,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,consumerUserId:t.ownerId},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,s]of t.preloadedStreams){const t=new i(s.descriptor,this.sdkOptions,{onOpen:t=>this.trackStreamClient(t),sessionState:()=>this.state});this.streams.set(e,t)}t.preloadedArtifacts&&this.artifacts.push(...t.preloadedArtifacts),t.preloadedEvents&&this.historyEvents.push(...t.preloadedEvents),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 i=null!=t.timetoken?String(t.timetoken):void 0;this.handleEvent(s,i)}},t.addListener(this.listener),t.subscribe({channels:[e],timetoken:1e3})}routeCallbackError(t,e,s){const i=t instanceof Error?t:new Error(String(t));if(this.errorCallbacks.length>0)for(const t of this.errorCallbacks)try{t(i,{entryPoint:"taskSession",callbackType:e,event:s})}catch{}else c("warn",`callback error in ${e}`,{event:"task_session_callback_error",callbackType:e,error:i.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 e=t.streams;if(!e||"object"!=typeof e)return;const r=t.declaredStream;for(const[t,a]of Object.entries(e)){if(!a||"object"!=typeof a)continue;if(this.streams.has(t))continue;const e=a.direction,n=s(e),o=a.format;if(!o||"bytes"!==o&&"events"!==o)continue;const l=a.affinity;if("dedicated"!==l&&"shared"!==l){c("warn",`live stream_started: dropping stream "${t}" for task "${this.taskId}" — invalid or missing affinity`,{event:"task_session_live_invalid_affinity",streamId:t,taskId:this.taskId,receivedAffinity:a.affinity});continue}const h={taskId:this.taskId,streamId:t,agentName:this.agentName,channel:a.channel,token:a.token,agentDirection:e,localDirection:n,format:o,affinity:l,metadata:a.metadata,declaredStream:r},m=new i(h,this.sdkOptions,{onOpen:t=>this.trackStreamClient(t),sessionState:()=>this.state});this.streams.set(t,m);for(const t of this.streamCallbacks)try{t(m)}catch(t){this.routeCallbackError(t,"onStream",m)}this.resolveWaiters(m)}}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 i=!1;if(void 0!==s.streamId)i=t.descriptor.declaredStream===s.streamId||t.descriptor.streamId===s.streamId;else if(s.predicate)try{i=s.predicate(t)}catch(e){this.routeCallbackError(e,"streamPredicate",t)}else i=!0;i?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){this.artifactCallbacks.push(t);for(const e of[...this.artifacts]){const s={type:"artifact",taskId:this.taskId,artifactRef:e};try{t(s)}catch(t){this.routeCallbackError(t,"onArtifact",s)}}return()=>{this.artifactCallbacks=this.artifactCallbacks.filter(e=>e!==t)}}onTerminal(t){if(this.terminalCallbacks.push(t),this.state&&TERMINAL_STATES.has(this.state)){const e={type:"terminal",taskId:this.taskId,state:this.state};try{t(e)}catch(t){this.routeCallbackError(t,"onTerminal",e)}}return()=>{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]}listEvents(){return[...this.historyEvents]}_appendHistoryEvent(t,e){if(e){if(this.historyTimetokens.has(e))return;this.historyTimetokens.add(e)}this.historyEvents.push(t)}async downloadArtifact(s){if(this.pubnub)return n(s,this.pubnub);const i=`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:i,...e()});this.readToken&&r.setToken(this.readToken);try{return await n(s,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 i=s.descriptor.localDirection;if("inbound"===i||"bidirectional"===i)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 i={resolve:e,reject:s};if(this.terminalWaiters.push(i),void 0!==t){const e=setTimeout(()=>{const e=this.terminalWaiters.indexOf(i);-1!==e&&this.terminalWaiters.splice(e,1),s(new Error(`waitForTerminal timed out after ${t}ms`))},t),r=i.resolve,a=i.reject;i.resolve=t=>{clearTimeout(e),r(t)},i.reject=t=>{clearTimeout(e),a(t)}}})}async saveArtifacts(t){const{mkdirSync:e,writeFileSync:s}=await import("node:fs"),{join:i,resolve:r,basename:a}=await import("node:path"),n=r(t);e(n,{recursive:!0});const o=[],c=this.listArtifacts();for(let t=0;t<c.length;t++){const e=await this.downloadArtifact(c[t]),l=e.fileName??`artifact-${t}`,h=r(i(n,a(l)||`artifact-${t}`));if(!h.startsWith(n+"/")&&h!==n)throw new Error(`Artifact filename "${l}" 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 a(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 a(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,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Neutral category labels surfaced through user-facing logs and the public
|
|
3
|
+
* `StreamError.category` field. The SDK maps the underlying realtime
|
|
4
|
+
* transport's internal category strings into this small enum so the
|
|
5
|
+
* user-visible surface stays implementation-agnostic.
|
|
6
|
+
*
|
|
7
|
+
* "other" is the catch-all for transport categories the SDK does not
|
|
8
|
+
* specifically classify. Devs investigating an unrecognised category
|
|
9
|
+
* should opt into BLOCKS_DEBUG_INTERNAL=forward_transport to see the
|
|
10
|
+
* underlying entries directly.
|
|
11
|
+
*/
|
|
12
|
+
export type TransportCategory = 'connected' | 'reconnected' | 'network_down' | 'network_issues' | 'timeout' | 'malformed_response' | 'access_denied' | 'bad_request' | 'other';
|
|
13
|
+
/** Status payload shape consumed by the mapper. Mirrors the fields the
|
|
14
|
+
* PubNub JS SDK populates on `Status` / `StatusEvent`. */
|
|
15
|
+
export interface TransportStatusPayload {
|
|
16
|
+
category?: unknown;
|
|
17
|
+
error?: unknown;
|
|
18
|
+
statusCode?: unknown;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Map the underlying transport's status payload to a neutral category.
|
|
22
|
+
*
|
|
23
|
+
* Resolution order:
|
|
24
|
+
* 1. `statusCode === 403` -> `access_denied` (REST-grade override; wins
|
|
25
|
+
* even over an unknown wrapper category, because PAM revocation is
|
|
26
|
+
* the load-bearing case for `accessDeniedHandler`).
|
|
27
|
+
* 2. `statusCode === 400` -> `bad_request` (same rationale).
|
|
28
|
+
* 3. If the outer category is one of the Event Engine wrappers
|
|
29
|
+
* (`PNConnectionErrorCategory` / `PNDisconnectedUnexpectedlyCategory`),
|
|
30
|
+
* look up the nested `error` string in CATEGORY_MAP.
|
|
31
|
+
* 4. Look up the outer category in CATEGORY_MAP.
|
|
32
|
+
* 5. Fallback: `'other'`.
|
|
33
|
+
*
|
|
34
|
+
* Accepts either the full payload `{ category, error, statusCode }` or
|
|
35
|
+
* a bare string (the legacy form). String form is preserved so test
|
|
36
|
+
* fixtures and pure-classifier callers don't have to wrap.
|
|
37
|
+
*/
|
|
38
|
+
export declare function mapTransportCategory(input: TransportStatusPayload | string | null | undefined): TransportCategory;
|
|
39
|
+
/**
|
|
40
|
+
* Convenience predicate for the access-denied path. True when the
|
|
41
|
+
* payload resolves to `access_denied` via `mapTransportCategory` (which
|
|
42
|
+
* already honors statusCode 403, the wrapper unwrap, and the leaf
|
|
43
|
+
* category).
|
|
44
|
+
*/
|
|
45
|
+
export declare function isAccessDeniedStatus(input: TransportStatusPayload): boolean;
|
|
46
|
+
/** Categories that warrant a "connectivity degraded" warn-level log line. */
|
|
47
|
+
export declare const DEGRADED_TRANSPORT_CATEGORIES: ReadonlySet<TransportCategory>;
|
|
48
|
+
/** Categories that warrant a "connectivity restored" info-level log line. */
|
|
49
|
+
export declare const RESTORED_TRANSPORT_CATEGORIES: ReadonlySet<TransportCategory>;
|
|
50
|
+
/** Categories that force-terminate a stream subscription. */
|
|
51
|
+
export declare const FATAL_TRANSPORT_CATEGORIES: ReadonlySet<TransportCategory>;
|
|
52
|
+
/**
|
|
53
|
+
* Neutral operation labels surfaced through user-facing logs. The SDK
|
|
54
|
+
* maps the underlying realtime transport's internal operation strings
|
|
55
|
+
* (e.g. `PNSubscribeOperation`) into this small enum so the user-visible
|
|
56
|
+
* surface stays implementation-agnostic. "other" is the catch-all for
|
|
57
|
+
* operations the SDK does not specifically classify.
|
|
58
|
+
*/
|
|
59
|
+
export type TransportOperation = 'subscribe' | 'heartbeat' | 'publish' | 'history' | 'presence' | 'other';
|
|
60
|
+
export declare function mapTransportOperation(raw: string | undefined): TransportOperation;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
const e={PNConnectedCategory:"connected",PNReconnectedCategory:"reconnected",PNNetworkDownCategory:"network_down",PNNetworkIssuesCategory:"network_issues",PNTimeoutCategory:"timeout",PNMalformedResponseCategory:"malformed_response",PNAccessDeniedCategory:"access_denied",PNBadRequestCategory:"bad_request"},t=new Set(["PNConnectionErrorCategory","PNDisconnectedUnexpectedlyCategory"]);export function mapTransportCategory(r){if(null==r)return"other";if("string"==typeof r)return e[r]??"other";const{category:o,error:n,statusCode:s}=r;if(403===s)return"access_denied";if(400===s)return"bad_request";const a="string"==typeof o?o:"";if(a&&t.has(a)){return e["string"==typeof n?n:""]??"other"}return e[a]??"other"}export function isAccessDeniedStatus(e){return"access_denied"===mapTransportCategory(e)}export const DEGRADED_TRANSPORT_CATEGORIES=new Set(["network_down","network_issues","timeout","malformed_response"]);export const RESTORED_TRANSPORT_CATEGORIES=new Set(["reconnected"]);export const FATAL_TRANSPORT_CATEGORIES=new Set(["access_denied","bad_request"]);const r={PNSubscribeOperation:"subscribe",PNHeartbeatOperation:"heartbeat",PNPublishOperation:"publish",PNSignalOperation:"publish",PNHistoryOperation:"history",PNFetchMessagesOperation:"history",PNMessageCountsOperation:"history",PNHereNowOperation:"presence",PNWhereNowOperation:"presence",PNGetStateOperation:"presence",PNSetStateOperation:"presence"};export function mapTransportOperation(e){return void 0===e?"other":r[e]??"other"}
|
package/dist/stream/index.d.ts
CHANGED
|
@@ -8,4 +8,4 @@ export { StreamClient } from './stream-client.js';
|
|
|
8
8
|
export type { StreamError } from './stream-client.js';
|
|
9
9
|
export { type StreamDescriptor, type StreamAffinity, invertDirection } from './descriptor.js';
|
|
10
10
|
export { validateStreamId } from './validate.js';
|
|
11
|
-
export type { StreamFormat, StreamDirection, StreamClientOptions, StreamClientFromDescriptorOptions, InboundMessage, StreamBundleConfig, } from './types.js';
|
|
11
|
+
export type { StreamFormat, StreamDirection, StreamClientOptions, StreamClientFromDescriptorOptions, InboundMessage, InboundBytesMessage, InboundEventsMessage, InboundRawMessage, StreamBundleConfig, } from './types.js';
|
|
@@ -7,15 +7,15 @@
|
|
|
7
7
|
*
|
|
8
8
|
* StreamClient is the public API. StreamBundle is the internal engine.
|
|
9
9
|
*/
|
|
10
|
+
import { type TransportCategory, type TransportStatusPayload } from '../runtime/transport-categories.js';
|
|
10
11
|
import type { StreamDescriptor } from './descriptor.js';
|
|
11
12
|
import type { StreamClientOptions, StreamClientFromDescriptorOptions, InboundMessage } from './types.js';
|
|
12
13
|
/** Reset the UUID counter (for testing). */
|
|
13
14
|
export declare function _resetUuidCounter(): void;
|
|
14
|
-
export
|
|
15
|
+
export { FATAL_TRANSPORT_CATEGORIES } from '../runtime/transport-categories.js';
|
|
15
16
|
/**
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
* listener:
|
|
17
|
+
* Detect a transport status payload that represents an error. The
|
|
18
|
+
* pinned realtime SDK exposes two shapes on the subscribe listener:
|
|
19
19
|
*
|
|
20
20
|
* - `Status` — has a required `error: boolean` and `statusCode: number`.
|
|
21
21
|
* - `StatusEvent` — has an optional polymorphic
|
|
@@ -25,34 +25,29 @@ export declare const FATAL_STREAM_ERROR_CATEGORIES: ReadonlySet<string>;
|
|
|
25
25
|
* Priority:
|
|
26
26
|
* 1. Truthy `status.error` (covers `true`, non-empty string, or
|
|
27
27
|
* category-name string surfaced by `StatusEvent`).
|
|
28
|
-
* 2. Numeric `statusCode >= 400` (REST-style errors
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
28
|
+
* 2. Numeric `statusCode >= 400` (REST-style errors).
|
|
29
|
+
* 3. Mapped-category membership in FATAL_TRANSPORT_CATEGORIES as a
|
|
30
|
+
* last resort when the payload is sparse (defensive; keeps
|
|
31
|
+
* access-denied detection working even if neither of the above
|
|
32
|
+
* fields is populated by a future SDK minor release).
|
|
33
33
|
*
|
|
34
34
|
* Exported so classifier behavior can be unit-tested directly without
|
|
35
35
|
* instantiating a full `StreamClient`.
|
|
36
36
|
*/
|
|
37
|
-
export declare function isStreamStatusError(status:
|
|
38
|
-
|
|
39
|
-
|
|
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;
|
|
37
|
+
export declare function isStreamStatusError(status: TransportStatusPayload | null | undefined): boolean;
|
|
38
|
+
/** True if this neutral category is in the fatal allowlist. */
|
|
39
|
+
export declare function isFatalTransportCategory(category: TransportCategory | undefined | null): boolean;
|
|
44
40
|
/**
|
|
45
41
|
* Typed error payload fired to `StreamClient.onError(...)` subscribers.
|
|
46
42
|
*
|
|
47
|
-
* Fires for every
|
|
43
|
+
* Fires for every transport status event classified as an error by
|
|
48
44
|
* `isStreamStatusError`. Consumers branch on `fatal` for
|
|
49
|
-
* must-terminate conditions
|
|
50
|
-
* `category` for finer-grained UX.
|
|
45
|
+
* must-terminate conditions and on `category` for finer-grained UX.
|
|
51
46
|
*/
|
|
52
47
|
export interface StreamError {
|
|
53
|
-
/**
|
|
54
|
-
category:
|
|
55
|
-
/** Raw
|
|
48
|
+
/** Neutral transport category (e.g., `'access_denied'`, `'timeout'`). */
|
|
49
|
+
category: TransportCategory;
|
|
50
|
+
/** Raw error data from the transport (`errorData` or `error` payload if present). */
|
|
56
51
|
error: any;
|
|
57
52
|
/** The stream channel the error applies to. */
|
|
58
53
|
channel: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import e from"pubnub";import{StreamBundle as t}from"./stream-bundle.js";import{validateStreamId as r}from"./validate.js";import{getEnv as i}from"../env.js";import{log as n}from"../runtime/logger.js";import{base64ToBytes as s,concatBytes as o,utf8Decode as a,utf8Encode as d}from"./bytes.js";const u=(e,t,r)=>n("[StreamClient]",e,t,r);let h=0;export function _resetUuidCounter(){h=0}function l(e,t,r,n){if(void 0!==e)return e;const s=i(t);return void 0!==s&&""!==s?n(s):r}function c(e){return Number(e)}function f(e){const t=e.toLowerCase();return"true"===t||"on"===t||"1"===t}export const FATAL_STREAM_ERROR_CATEGORIES=new Set(["PNAccessDeniedCategory","PNBadRequestCategory"]);export function isStreamStatusError(e){if(!e)return!1;if(e.error)return!0;const t=e.statusCode;if("number"==typeof t&&t>=400)return!0;const r="string"==typeof e.category?e.category:"";return""!==r&&FATAL_STREAM_ERROR_CATEGORIES.has(r)}export function isFatalStreamCategory(e){return"string"==typeof e&&FATAL_STREAM_ERROR_CATEGORIES.has(e)}export class StreamClient{constructor(i){this._isActive=!0,this.endCallbacks=[],this.inboundDoneCallbacks=[],this.inboundDoneFired=!1,this.errorCallbacks=[],this.inboundQueue=[],this.inboundResolve=null,this.inboundDone=!1,this.messageListener=null,this.multipartBuffers=new Map,this.reorderBuffer=new Map,this.reorderTimer=null,this.endSeq=null;const n=i.agentName;if(!n)throw new Error("agentName is required (pass in options)");if(r(i.streamId),this.format=i.format??"bytes","bytes"!==this.format&&"events"!==this.format)throw new Error(`Invalid stream format: "${this.format}". Must be "bytes" or "events".`);this.direction=i.direction??"outbound";const s=i.affinity;null==s?this._affinity="dedicated":"dedicated"===s||"shared"===s?this._affinity=s:(u("warn","invalid affinity in stream options — falling back to dedicated",{event:"stream_client_invalid_affinity_fallback",streamId:i.streamId,receivedAffinity:s,fallbackAffinity:"dedicated"}),this._affinity="dedicated"),this.nextExpectedSeq="events"===this.format?1:0,this.reorderTimeoutMs=i.reorderTimeoutMs??750,h++,this._uuid=`${n}-stream-${String(h).padStart(4,"0")}`,this._channel=i.channel??`stream.${n}.${i.streamId}`;const o=l(i.maxMessageSize,"STREAM_MAX_MESSAGE_SIZE",16384,c),a=l(i.bundleSizeBytes,"STREAM_BUNDLE_SIZE",4096,c),d=l(i.maxLatencyMs,"STREAM_MAX_LATENCY_MS",250,c),m=l(i.gating,"STREAM_GATING",!0,f);this.pubnub=new e({subscribeKey:i.subscribeKey,publishKey:i.publishKey,userId:this._uuid,enableEventEngine:!0}),this.pubnub.setToken(i.token),"bidirectional"===this.direction&&this.pubnub.setFilterExpression(`meta.sender != '${this._uuid}'`);if("outbound"===this.direction||"bidirectional"===this.direction){const e={maxMessageSize:o,bundleSizeBytes:a,maxLatencyMs:d,uuid:this._uuid};this.bundle=new t(this.pubnub,this._channel,i.streamId,this.format,e,m)}else this.bundle=null;("inbound"===this.direction||"bidirectional"===this.direction)&&this.setupInbound()}get isActive(){return this._isActive}get channel(){return this._channel}get uuid(){return this._uuid}write(e){if(!this._isActive)throw new Error("Cannot write to an ended stream");if(!this.bundle)throw new Error("Cannot write to an inbound-only stream");this.bundle.write(e)}async end(){if(this._isActive){if(this._isActive=!1,this.bundle)try{await this.bundle.end()}catch(e){u("warn","bundle.end() failed during end() — continuing teardown",{event:"stream_client_bundle_end_failed",error:e instanceof Error?e.message:String(e)})}if("bidirectional"!==this.direction&&this.bundle&&"shared"!==this._affinity)try{await this.bundle.publishEndMarker()}catch(e){u("warn","publishEndMarker() failed during end() — continuing teardown",{event:"stream_client_publish_end_marker_failed",error:e instanceof Error?e.message:String(e)})}this.inboundDone=!0,this.inboundResolve&&(this.inboundResolve({value:void 0,done:!0}),this.inboundResolve=null),this.fireInboundDone(),this.multipartBuffers.clear(),this.reorderBuffer.clear(),this.reorderTimer&&(clearTimeout(this.reorderTimer),this.reorderTimer=null),this.messageListener&&(this.pubnub.removeListener(this.messageListener),this.messageListener=null),this.pubnub.unsubscribeAll(),this.pubnub.destroy();for(const e of this.endCallbacks)e();this.endCallbacks=[]}}onEnd(e){this.endCallbacks.push(e)}onInboundDone(e){this.inboundDoneFired?e():this.inboundDoneCallbacks.push(e)}fireInboundDone(){if(!this.inboundDoneFired){this.inboundDoneFired=!0;for(const e of this.inboundDoneCallbacks)try{e()}catch{}this.inboundDoneCallbacks=[]}}onError(e){this.errorCallbacks.push(e)}fireError(e){for(const t of[...this.errorCallbacks])try{t(e)}catch(e){u("error","onError callback raised",{event:"stream_client_on_error_callback_raised",error:e instanceof Error?e.message:String(e)})}}get inbound(){if("outbound"===this.direction)throw new Error("Cannot read from an outbound-only stream");const e=this;return{[Symbol.asyncIterator]:()=>({next:()=>e.inboundQueue.length>0?Promise.resolve({value:e.inboundQueue.shift(),done:!1}):e.inboundDone?Promise.resolve({value:void 0,done:!0}):new Promise(t=>{e.inboundResolve=t})})}}setupInbound(){this.messageListener={message:e=>{e.channel===this._channel&&this.handleInboundMessage(e.message)},status:e=>{this.handleStatusEvent(e)}},this.pubnub.addListener(this.messageListener),this.pubnub.subscribe({channels:[this._channel],timetoken:1e3})}handleStatusEvent(e){try{if(!isStreamStatusError(e))return;const t="string"==typeof e?.category?e.category:"",r=isFatalStreamCategory(t),i=e?.errorData??e?.error??null,n={category:t,error:i,channel:this._channel,timestamp:Date.now(),fatal:r};u("warn",`status error: category=${t} fatal=${r} channel=${this._channel}`,{event:"stream_client_status_error",category:t,fatal:r,channel:this._channel,rawError:i}),this.fireError(n),r&&this._isActive&&this.end().catch(e=>{u("error","forced end() after fatal error raised",{event:"stream_client_forced_end_after_fatal_raised",error:e instanceof Error?e.message:String(e)})})}catch(e){u("error","status handler raised",{event:"stream_client_status_handler_raised",error:e instanceof Error?e.message:String(e)})}}handleInboundMessage(e){if(!e||"object"!=typeof e)return;const t=e;if("stream_end"===t.type)return this.reorderTimeoutMs<=0?(this.inboundDone=!0,this.inboundResolve&&(this.inboundResolve({value:void 0,done:!0}),this.inboundResolve=null),void this.fireInboundDone()):"number"==typeof t.seq&&Number.isInteger(t.seq)?(this.endSeq=t.seq,void(this.nextExpectedSeq>=this.endSeq?this.checkEndReached():this.startReorderTimer())):void u("warn","stream_end missing numeric seq field — ignoring in reorder mode",{event:"stream_client_stream_end_missing_seq"});if(t.multipart&&"object"==typeof t.multipart){const e=t.multipart;return void this.handleMultipartPart(t,e)}const r=this.normalizeMessage(t);r&&this.enqueueReordered(r)}handleMultipartPart(e,t){if(!function(e){return!("string"!=typeof e.id||""===e.id||"number"!=typeof e.part||!Number.isInteger(e.part)||e.part<1||"number"!=typeof e.total||!Number.isInteger(e.total)||e.total<2||e.part>e.total)}(t))return;if("string"!=typeof e.data)return;this.evictStaleGroups();const r=e.seq,i=e.type,n=e.streamId,d=e.data;let u=this.multipartBuffers.get(t.id);if(u){if(u.total!==t.total||u.seq!==r||u.type!==i||u.streamId!==n)return void this.dropMultipartGroup(t.id);const e=u.parts.get(t.part);if(void 0!==e){if(e===d)return;return void this.dropMultipartGroup(t.id)}}else u={total:t.total,parts:new Map,seq:r,ts:e.ts,type:i,streamId:n,createdAt:Date.now()},this.multipartBuffers.set(t.id,u);if(u.parts.set(t.part,d),u.parts.size===u.total){this.dropMultipartGroup(t.id);for(let e=1;e<=u.total;e++)if(!u.parts.has(e))return;try{const e=[];for(let t=1;t<=u.total;t++)e.push(s(u.parts.get(t)));const t=o(e),r=JSON.parse(a(t)),i=this.normalizeMessage(r);i&&this.enqueueReordered(i)}catch{}}}dropMultipartGroup(e){this.multipartBuffers.delete(e)}evictStaleGroups(){const e=Date.now();for(const[t,r]of this.multipartBuffers)e-r.createdAt>3e4&&this.multipartBuffers.delete(t);if(this.multipartBuffers.size>64){const e=[...this.multipartBuffers.entries()].sort((e,t)=>e[1].createdAt-t[1].createdAt),t=e.length-64;for(let r=0;r<t;r++)this.multipartBuffers.delete(e[r][0])}}normalizeMessage(e){const t=e.type;if("stream_data"===t){if("number"!=typeof e.seq||!Number.isInteger(e.seq))return null;const t=e.chunks;if(!Array.isArray(t)||0===t.length||!t.every(e=>"string"==typeof e))return null;const r=e.encoding;return void 0!==r&&"utf8"!==r&&"base64"!==r?null:{data:t,seq:e.seq,ts:e.ts,format:"bytes",encoding:r??"utf8"}}if("stream_events"===t){if("number"!=typeof e.seq||!Number.isInteger(e.seq))return null;const t=e.events;return Array.isArray(t)&&0!==t.length?{data:t,seq:e.seq,ts:e.ts,format:"events",encoding:"utf8"}:null}return"number"==typeof e.seq&&Number.isInteger(e.seq)?{data:e,seq:e.seq,ts:e.ts??Date.now(),format:"raw",encoding:"utf8"}:null}enqueueInbound(e){if(this.inboundResolve){const t=this.inboundResolve;this.inboundResolve=null,t({value:e,done:!1})}else this.inboundQueue.push(e)}enqueueReordered(e){if(this.reorderTimeoutMs<=0)return void this.enqueueInbound(e);const t=e.seq;null!==this.endSeq&&t>=this.endSeq||t<this.nextExpectedSeq||this.reorderBuffer.has(t)||(t===this.nextExpectedSeq?(this.enqueueInbound(e),this.nextExpectedSeq++,this.flushConsecutive(),this.cancelAndRestartTimerIfNeeded(),this.checkEndReached()):(this.reorderBuffer.set(t,e),this.startReorderTimer()))}flushConsecutive(){for(;this.reorderBuffer.has(this.nextExpectedSeq);){const e=this.reorderBuffer.get(this.nextExpectedSeq);this.reorderBuffer.delete(this.nextExpectedSeq),this.enqueueInbound(e),this.nextExpectedSeq++}}startReorderTimer(){this.reorderTimer||(this.reorderTimer=setTimeout(()=>{if(this.reorderTimer=null,this.reorderBuffer.size>0){let e=1/0;for(const t of this.reorderBuffer.keys())t<e&&(e=t);this.nextExpectedSeq=e,this.flushConsecutive(),this.cancelAndRestartTimerIfNeeded(),this.checkEndReached()}else null!==this.endSeq&&this.nextExpectedSeq<this.endSeq&&(this.nextExpectedSeq=this.endSeq,this.checkEndReached())},this.reorderTimeoutMs))}cancelAndRestartTimerIfNeeded(){this.reorderTimer&&(clearTimeout(this.reorderTimer),this.reorderTimer=null),this.reorderBuffer.size>0&&this.startReorderTimer()}checkEndReached(){null!==this.endSeq&&this.nextExpectedSeq>=this.endSeq&&(this.reorderBuffer.size>0&&(u("warn",`reorder buffer not empty at stream end (${this.reorderBuffer.size} unexpected messages with seq >= ${this.endSeq})`,{event:"stream_client_reorder_buffer_not_empty_at_end",unexpectedCount:this.reorderBuffer.size,endSeq:this.endSeq}),this.reorderBuffer.clear()),this.reorderTimer&&(clearTimeout(this.reorderTimer),this.reorderTimer=null),this.inboundDone=!0,this.inboundResolve&&(this.inboundResolve({value:void 0,done:!0}),this.inboundResolve=null),this.fireInboundDone())}async*bytes(){for await(const e of this.inbound){const t=Array.isArray(e.data)?e.data:[e.data];for(const r of t)yield"base64"===e.encoding?s(r):d(r)}}async*events(){for await(const e of this.inbound){const t=Array.isArray(e.data)?e.data:[e.data];for(const e of t)yield e}}async readable(){const{Readable:e}=await import("node:stream"),t=this.bytes()[Symbol.asyncIterator]();return new e({async read(){try{const{value:e,done:r}=await t.next();r?this.push(null):this.push(e)}catch{this.push(null)}}})}static fromDescriptor(e,t){const r=e.localDirection,i="outbound"===r||"bidirectional"===r,n=void 0!==t.gating?t.gating:!i;return new StreamClient({subscribeKey:t.subscribeKey,publishKey:t.publishKey,token:e.token,agentName:e.agentName,streamId:e.streamId,channel:e.channel,direction:e.localDirection,format:e.format,affinity:e.affinity,maxMessageSize:t.maxMessageSize,bundleSizeBytes:t.bundleSizeBytes,maxLatencyMs:t.maxLatencyMs,gating:n,reorderTimeoutMs:t.reorderTimeoutMs})}}
|
|
1
|
+
import e from"pubnub";import{buildPubNubLogConfig as t}from"../runtime/pubnub-client.js";import{StreamBundle as r}from"./stream-bundle.js";import{validateStreamId as i}from"./validate.js";import{getEnv as n}from"../env.js";import{log as s}from"../runtime/logger.js";import{mapTransportCategory as o,FATAL_TRANSPORT_CATEGORIES as a}from"../runtime/transport-categories.js";import{base64ToBytes as d,concatBytes as u,utf8Decode as h,utf8Encode as l}from"./bytes.js";const c=(e,t,r)=>s("[StreamClient]",e,t,r);let f=0;export function _resetUuidCounter(){f=0}function m(e,t,r,i){if(void 0!==e)return e;const s=n(t);return void 0!==s&&""!==s?i(s):r}function b(e){return Number(e)}function p(e){const t=e.toLowerCase();return"true"===t||"on"===t||"1"===t}export{FATAL_TRANSPORT_CATEGORIES}from"../runtime/transport-categories.js";export function isStreamStatusError(e){if(!e)return!1;if(e.error)return!0;const t=e.statusCode;return"number"==typeof t&&t>=400||!("string"!=typeof e.category||!e.category)&&a.has(o(e))}export function isFatalTransportCategory(e){return null!=e&&a.has(e)}export class StreamClient{constructor(n){this._isActive=!0,this.endCallbacks=[],this.inboundDoneCallbacks=[],this.inboundDoneFired=!1,this.errorCallbacks=[],this.inboundQueue=[],this.inboundResolve=null,this.inboundDone=!1,this.messageListener=null,this.multipartBuffers=new Map,this.reorderBuffer=new Map,this.reorderTimer=null,this.endSeq=null;const s=n.agentName;if(!s)throw new Error("agentName is required (pass in options)");if(i(n.streamId),this.format=n.format??"bytes","bytes"!==this.format&&"events"!==this.format)throw new Error(`Invalid stream format: "${this.format}". Must be "bytes" or "events".`);this.direction=n.direction??"outbound";const o=n.affinity;null==o?this._affinity="dedicated":"dedicated"===o||"shared"===o?this._affinity=o:(c("warn","invalid affinity in stream options — falling back to dedicated",{event:"stream_client_invalid_affinity_fallback",streamId:n.streamId,receivedAffinity:o,fallbackAffinity:"dedicated"}),this._affinity="dedicated"),this.nextExpectedSeq="events"===this.format?1:0,this.reorderTimeoutMs=n.reorderTimeoutMs??750,f++,this._uuid=`${s}-stream-${String(f).padStart(4,"0")}`,this._channel=n.channel??`stream.${s}.${n.streamId}`;const a=m(n.maxMessageSize,"STREAM_MAX_MESSAGE_SIZE",16384,b),d=m(n.bundleSizeBytes,"STREAM_BUNDLE_SIZE",4096,b),u=m(n.maxLatencyMs,"STREAM_MAX_LATENCY_MS",250,b),h=m(n.gating,"STREAM_GATING",!0,p);this.pubnub=new e({subscribeKey:n.subscribeKey,publishKey:n.publishKey,userId:this._uuid,enableEventEngine:!0,...t()}),this.pubnub.setToken(n.token),"bidirectional"===this.direction&&this.pubnub.setFilterExpression(`meta.sender != '${this._uuid}'`);if("outbound"===this.direction||"bidirectional"===this.direction){const e={maxMessageSize:a,bundleSizeBytes:d,maxLatencyMs:u,uuid:this._uuid};this.bundle=new r(this.pubnub,this._channel,n.streamId,this.format,e,h)}else this.bundle=null;("inbound"===this.direction||"bidirectional"===this.direction)&&this.setupInbound()}get isActive(){return this._isActive}get channel(){return this._channel}get uuid(){return this._uuid}write(e){if(!this._isActive)throw new Error("Cannot write to an ended stream");if(!this.bundle)throw new Error("Cannot write to an inbound-only stream");this.bundle.write(e)}async end(){if(this._isActive){if(this._isActive=!1,this.bundle)try{await this.bundle.end()}catch(e){c("warn","bundle.end() failed during end() — continuing teardown",{event:"stream_client_bundle_end_failed",error:e instanceof Error?e.message:String(e)})}if("bidirectional"!==this.direction&&this.bundle&&"shared"!==this._affinity)try{await this.bundle.publishEndMarker()}catch(e){c("warn","publishEndMarker() failed during end() — continuing teardown",{event:"stream_client_publish_end_marker_failed",error:e instanceof Error?e.message:String(e)})}this.inboundDone=!0,this.inboundResolve&&(this.inboundResolve({value:void 0,done:!0}),this.inboundResolve=null),this.fireInboundDone(),this.multipartBuffers.clear(),this.reorderBuffer.clear(),this.reorderTimer&&(clearTimeout(this.reorderTimer),this.reorderTimer=null),this.messageListener&&(this.pubnub.removeListener(this.messageListener),this.messageListener=null),this.pubnub.unsubscribeAll(),this.pubnub.destroy();for(const e of this.endCallbacks)e();this.endCallbacks=[]}}onEnd(e){this.endCallbacks.push(e)}onInboundDone(e){this.inboundDoneFired?e():this.inboundDoneCallbacks.push(e)}fireInboundDone(){if(!this.inboundDoneFired){this.inboundDoneFired=!0;for(const e of this.inboundDoneCallbacks)try{e()}catch{}this.inboundDoneCallbacks=[]}}onError(e){this.errorCallbacks.push(e)}fireError(e){for(const t of[...this.errorCallbacks])try{t(e)}catch(e){c("error","onError callback raised",{event:"stream_client_on_error_callback_raised",error:e instanceof Error?e.message:String(e)})}}get inbound(){if("outbound"===this.direction)throw new Error("Cannot read from an outbound-only stream");const e=this;return{[Symbol.asyncIterator]:()=>({next:()=>e.inboundQueue.length>0?Promise.resolve({value:e.inboundQueue.shift(),done:!1}):e.inboundDone?Promise.resolve({value:void 0,done:!0}):new Promise(t=>{e.inboundResolve=t})})}}setupInbound(){this.messageListener={message:e=>{e.channel===this._channel&&this.handleInboundMessage(e.message)},status:e=>{this.handleStatusEvent(e)}},this.pubnub.addListener(this.messageListener),this.pubnub.subscribe({channels:[this._channel],timetoken:1e3})}handleStatusEvent(e){try{if(!isStreamStatusError(e))return;const t=o(e),r=isFatalTransportCategory(t),i=e?.errorData??e?.error??null,n={category:t,error:i,channel:this._channel,timestamp:Date.now(),fatal:r};c("warn",`stream status error: category=${t} fatal=${r} channel=${this._channel}`,{event:"stream_client_status_error",category:t,fatal:r,channel:this._channel,rawError:i}),this.fireError(n),r&&this._isActive&&this.end().catch(e=>{c("error","forced end() after fatal error raised",{event:"stream_client_forced_end_after_fatal_raised",error:e instanceof Error?e.message:String(e)})})}catch(e){c("error","stream status handler raised",{event:"stream_client_status_handler_raised",error:e instanceof Error?e.message:String(e)})}}handleInboundMessage(e){if(!e||"object"!=typeof e)return;const t=e;if("stream_end"===t.type)return this.reorderTimeoutMs<=0?(this.inboundDone=!0,this.inboundResolve&&(this.inboundResolve({value:void 0,done:!0}),this.inboundResolve=null),void this.fireInboundDone()):"number"==typeof t.seq&&Number.isInteger(t.seq)?(this.endSeq=t.seq,void(this.nextExpectedSeq>=this.endSeq?this.checkEndReached():this.startReorderTimer())):void c("warn","stream_end missing numeric seq field — ignoring in reorder mode",{event:"stream_client_stream_end_missing_seq"});if(t.multipart&&"object"==typeof t.multipart){const e=t.multipart;return void this.handleMultipartPart(t,e)}const r=this.normalizeMessage(t);r&&this.enqueueReordered(r)}handleMultipartPart(e,t){if(!function(e){return!("string"!=typeof e.id||""===e.id||"number"!=typeof e.part||!Number.isInteger(e.part)||e.part<1||"number"!=typeof e.total||!Number.isInteger(e.total)||e.total<2||e.part>e.total)}(t))return;if("string"!=typeof e.data)return;this.evictStaleGroups();const r=e.seq,i=e.type,n=e.streamId,s=e.data;let o=this.multipartBuffers.get(t.id);if(o){if(o.total!==t.total||o.seq!==r||o.type!==i||o.streamId!==n)return void this.dropMultipartGroup(t.id);const e=o.parts.get(t.part);if(void 0!==e){if(e===s)return;return void this.dropMultipartGroup(t.id)}}else o={total:t.total,parts:new Map,seq:r,ts:e.ts,type:i,streamId:n,createdAt:Date.now()},this.multipartBuffers.set(t.id,o);if(o.parts.set(t.part,s),o.parts.size===o.total){this.dropMultipartGroup(t.id);for(let e=1;e<=o.total;e++)if(!o.parts.has(e))return;try{const e=[];for(let t=1;t<=o.total;t++)e.push(d(o.parts.get(t)));const t=u(e),r=JSON.parse(h(t)),i=this.normalizeMessage(r);i&&this.enqueueReordered(i)}catch{}}}dropMultipartGroup(e){this.multipartBuffers.delete(e)}evictStaleGroups(){const e=Date.now();for(const[t,r]of this.multipartBuffers)e-r.createdAt>3e4&&this.multipartBuffers.delete(t);if(this.multipartBuffers.size>64){const e=[...this.multipartBuffers.entries()].sort((e,t)=>e[1].createdAt-t[1].createdAt),t=e.length-64;for(let r=0;r<t;r++)this.multipartBuffers.delete(e[r][0])}}normalizeMessage(e){const t=e.type;if("stream_data"===t){if("number"!=typeof e.seq||!Number.isInteger(e.seq))return null;const t=e.chunks;if(!Array.isArray(t)||0===t.length||!t.every(e=>"string"==typeof e))return null;const r=e.encoding;return void 0!==r&&"utf8"!==r&&"base64"!==r?null:{data:t,seq:e.seq,ts:e.ts,format:"bytes",encoding:r??"utf8"}}if("stream_events"===t){if("number"!=typeof e.seq||!Number.isInteger(e.seq))return null;const t=e.events;return Array.isArray(t)&&0!==t.length?{data:t,seq:e.seq,ts:e.ts,format:"events",encoding:"utf8"}:null}return"number"==typeof e.seq&&Number.isInteger(e.seq)?{data:e,seq:e.seq,ts:e.ts??Date.now(),format:"raw",encoding:"utf8"}:null}enqueueInbound(e){if(this.inboundResolve){const t=this.inboundResolve;this.inboundResolve=null,t({value:e,done:!1})}else this.inboundQueue.push(e)}enqueueReordered(e){if(this.reorderTimeoutMs<=0)return void this.enqueueInbound(e);const t=e.seq;null!==this.endSeq&&t>=this.endSeq||t<this.nextExpectedSeq||this.reorderBuffer.has(t)||(t===this.nextExpectedSeq?(this.enqueueInbound(e),this.nextExpectedSeq++,this.flushConsecutive(),this.cancelAndRestartTimerIfNeeded(),this.checkEndReached()):(this.reorderBuffer.set(t,e),this.startReorderTimer()))}flushConsecutive(){for(;this.reorderBuffer.has(this.nextExpectedSeq);){const e=this.reorderBuffer.get(this.nextExpectedSeq);this.reorderBuffer.delete(this.nextExpectedSeq),this.enqueueInbound(e),this.nextExpectedSeq++}}startReorderTimer(){this.reorderTimer||(this.reorderTimer=setTimeout(()=>{if(this.reorderTimer=null,this.reorderBuffer.size>0){let e=1/0;for(const t of this.reorderBuffer.keys())t<e&&(e=t);this.nextExpectedSeq=e,this.flushConsecutive(),this.cancelAndRestartTimerIfNeeded(),this.checkEndReached()}else null!==this.endSeq&&this.nextExpectedSeq<this.endSeq&&(this.nextExpectedSeq=this.endSeq,this.checkEndReached())},this.reorderTimeoutMs))}cancelAndRestartTimerIfNeeded(){this.reorderTimer&&(clearTimeout(this.reorderTimer),this.reorderTimer=null),this.reorderBuffer.size>0&&this.startReorderTimer()}checkEndReached(){null!==this.endSeq&&this.nextExpectedSeq>=this.endSeq&&(this.reorderBuffer.size>0&&(c("warn",`reorder buffer not empty at stream end (${this.reorderBuffer.size} unexpected messages with seq >= ${this.endSeq})`,{event:"stream_client_reorder_buffer_not_empty_at_end",unexpectedCount:this.reorderBuffer.size,endSeq:this.endSeq}),this.reorderBuffer.clear()),this.reorderTimer&&(clearTimeout(this.reorderTimer),this.reorderTimer=null),this.inboundDone=!0,this.inboundResolve&&(this.inboundResolve({value:void 0,done:!0}),this.inboundResolve=null),this.fireInboundDone())}async*bytes(){for await(const e of this.inbound){const t=Array.isArray(e.data)?e.data:[e.data];for(const r of t)yield"base64"===e.encoding?d(r):l(r)}}async*events(){for await(const e of this.inbound){const t=Array.isArray(e.data)?e.data:[e.data];for(const e of t)yield e}}async readable(){const{Readable:e}=await import("node:stream"),t=this.bytes()[Symbol.asyncIterator]();return new e({async read(){try{const{value:e,done:r}=await t.next();r?this.push(null):this.push(e)}catch{this.push(null)}}})}static fromDescriptor(e,t){const r=e.localDirection,i="outbound"===r||"bidirectional"===r,n=void 0!==t.gating?t.gating:!i;return new StreamClient({subscribeKey:t.subscribeKey,publishKey:t.publishKey,token:e.token,agentName:t.consumerUserId||e.agentName,streamId:e.streamId,channel:e.channel,direction:e.localDirection,format:e.format,affinity:e.affinity,maxMessageSize:t.maxMessageSize,bundleSizeBytes:t.bundleSizeBytes,maxLatencyMs:t.maxLatencyMs,gating:n,reorderTimeoutMs:t.reorderTimeoutMs})}}
|