@blocks-network/sdk 0.1.60 → 0.1.61
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 +19 -21
- package/dist/runtime/agent-instance.d.ts +0 -4
- package/dist/runtime/agent-instance.js +1 -1
- package/dist/runtime/consumer-auth.js +1 -1
- package/dist/runtime/logger.d.ts +27 -0
- package/dist/runtime/logger.js +1 -0
- package/dist/runtime/stream-context.js +1 -1
- package/dist/runtime/task-client.js +1 -1
- package/dist/runtime/task-session.d.ts +1 -1
- package/dist/runtime/task-session.js +1 -1
- package/dist/stream/stream-bundle.js +1 -1
- package/dist/stream/stream-client.js +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @blocks-network/sdk
|
|
2
2
|
|
|
3
|
-
Blocks Network SDK for Node.js
|
|
3
|
+
Blocks Network SDK for Node.js
|
|
4
4
|
|
|
5
5
|
## Installation
|
|
6
6
|
|
|
@@ -198,9 +198,9 @@ import type { TokenEndpointConfig } from '@blocks-network/sdk';
|
|
|
198
198
|
|
|
199
199
|
const tokenEndpoint: TokenEndpointConfig = {
|
|
200
200
|
url: '/api/blocks-token',
|
|
201
|
-
credentials: 'include',
|
|
202
|
-
headers: { 'X-CSRF-Token': readCsrfMeta() },
|
|
203
|
-
body: { sessionId: getCurrentSessionId() },
|
|
201
|
+
credentials: 'include', // send session cookies
|
|
202
|
+
headers: { 'X-CSRF-Token': readCsrfMeta() }, // merged with Content-Type
|
|
203
|
+
body: { sessionId: getCurrentSessionId() }, // replaces the default {}
|
|
204
204
|
};
|
|
205
205
|
|
|
206
206
|
const client = await TaskClient.create({
|
|
@@ -228,12 +228,12 @@ the client.
|
|
|
228
228
|
Blocks API key, authenticates the browser caller however you
|
|
229
229
|
choose (session cookie / OAuth / etc.), and forwards to the
|
|
230
230
|
Blocks backend's `POST /api/v1/auth/agent/consumer-token`.
|
|
231
|
-
2. **Dashboard embedder
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
231
|
+
2. **Dashboard embedder.** The Blocks backend's own
|
|
232
|
+
`POST /api/v1/auth/agent/consumer-token` endpoint, called directly
|
|
233
|
+
from a signed-in dashboard with the user's session cookie plus
|
|
234
|
+
`X-Active-Org` and `X-CSRF-Token` headers. No proxy, no API key in
|
|
235
|
+
the browser. Custom apps usually use the customer-owned backend
|
|
236
|
+
proxy shape.
|
|
237
237
|
|
|
238
238
|
Both shapes speak the same Mode 2 contract and are consumed
|
|
239
239
|
uniformly by this SDK.
|
|
@@ -437,9 +437,7 @@ import type { StreamError } from '@blocks-network/sdk/stream';
|
|
|
437
437
|
|
|
438
438
|
const stream = ref.open();
|
|
439
439
|
stream.onError((err: StreamError) => {
|
|
440
|
-
console.warn(
|
|
441
|
-
`[stream] ${err.category} fatal=${err.fatal} channel=${err.channel}`,
|
|
442
|
-
);
|
|
440
|
+
console.warn(`[stream] ${err.category} fatal=${err.fatal} channel=${err.channel}`);
|
|
443
441
|
});
|
|
444
442
|
|
|
445
443
|
for await (const chunk of stream.bytes()) {
|
|
@@ -484,10 +482,10 @@ session.onStream((ref) => {
|
|
|
484
482
|
});
|
|
485
483
|
|
|
486
484
|
// Option 2 — open every readable stream in one call, then branch
|
|
487
|
-
await session.waitForStream();
|
|
488
|
-
const streams = session.openAllStreams();
|
|
485
|
+
await session.waitForStream(); // ensure at least one is announced
|
|
486
|
+
const streams = session.openAllStreams(); // returns StreamClient[] in insertion order
|
|
489
487
|
for (const s of streams) {
|
|
490
|
-
void consume(s
|
|
488
|
+
void consume(s /* whichever ref you care about */);
|
|
491
489
|
}
|
|
492
490
|
```
|
|
493
491
|
|
|
@@ -507,13 +505,13 @@ loops. Tune the window per session:
|
|
|
507
505
|
const session = await client.sendMessage({
|
|
508
506
|
agentName: 'llm_streamer',
|
|
509
507
|
requestParts: [textPart('stream please')],
|
|
510
|
-
drainWindowMs: 5_000,
|
|
508
|
+
drainWindowMs: 5_000, // 5 seconds
|
|
511
509
|
});
|
|
512
510
|
|
|
513
511
|
// Wider window for long-tail consumers, or on connect()
|
|
514
512
|
const resumed = await client.connect({
|
|
515
513
|
taskId: 'task-abc',
|
|
516
|
-
drainWindowMs: 60_000,
|
|
514
|
+
drainWindowMs: 60_000, // 60 seconds
|
|
517
515
|
});
|
|
518
516
|
```
|
|
519
517
|
|
|
@@ -586,11 +584,11 @@ Import from the package root — no special browser entrypoint needed:
|
|
|
586
584
|
|
|
587
585
|
import { TaskClient, TaskSession, StreamRef } from '@blocks-network/sdk';
|
|
588
586
|
|
|
589
|
-
The Node SDK package.json declares `engines.node >=
|
|
590
|
-
LTS) alongside the `browser` exports field. Node
|
|
587
|
+
The Node SDK package.json declares `engines.node >= 22.0.0` (Node 22
|
|
588
|
+
Maintenance LTS) alongside the `browser` exports field. Node 22+ has native
|
|
591
589
|
`FormData`, `fetch`, `Blob`, `Uint8Array`, `TextEncoder`, and
|
|
592
590
|
`TextDecoder`, which the SDK uses directly — no polyfill required on
|
|
593
|
-
either platform.
|
|
591
|
+
either platform. CI tests against Node 22, 24, and 26.
|
|
594
592
|
|
|
595
593
|
**Consumer APIs** (TaskClient, TaskSession, StreamRef, StreamClient)
|
|
596
594
|
are browser-safe — no Node.js `Buffer` polyfill needed:
|
|
@@ -189,10 +189,6 @@ export interface ControlMessage {
|
|
|
189
189
|
caller?: string;
|
|
190
190
|
protocolVersion?: string;
|
|
191
191
|
}
|
|
192
|
-
/** Numeric precedence for LOG_LEVEL threshold filtering. */
|
|
193
|
-
export declare const _LOG_LEVEL_ORDER: Record<string, number>;
|
|
194
|
-
/** Resolve the effective LOG_LEVEL from env (default: "info"). */
|
|
195
|
-
export declare function _resolveLogLevel(): number;
|
|
196
192
|
/**
|
|
197
193
|
* True when the diag entry is "parked" — past the staleness threshold
|
|
198
194
|
* AND not currently in the connected state. A long-silent
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{StreamClient as e}from"../stream/index.js";import{buildArtifactRef as t,shouldInlineArtifact as a,decodeInlineArtifact as n}from"./artifacts.js";import{createPubNubClient as s}from"./pubnub-client.js";import{createChannelManager as r}from"./channel-manager.js";import{toPayload as o}from"./pubnub-types.js";import{connectAgent as i,getAgent as d}from"./agent-registry.js";import{parseStreamSetupResponse as c,parseStreamSetupError as l}from"./stream-setup-helper.js";import{DEFAULTS as u}from"../defaults.js";import{getEnv as m}from"../env.js";import{uploadFile as f}from"./file-upload.js";import{StaticAuthProvider as p}from"./auth-provider.js";import{StreamRegistry as g}from"./stream-registry.js";import{CredentialCache as k}from"./credential-cache.js";import{createStreamObject as y,createExternalStreamObject as h,runOnActivate as b}from"./stream-context.js";import{TaskClient as I}from"./task-client.js";import{ConsumerAuth as w}from"./consumer-auth.js";import{fetchCdmConfig as v}from"./cdm-config.js";import{AgentAuth as S,AgentAuthFatalError as T}from"./agent-auth.js";import{CURRENT_PROTOCOL_VERSION as C,SUPPORTED_PROTOCOL_VERSIONS as E,isProtocolVersionSupported as A}from"./protocol-version.js";const $=async(e,t,a,n,s,i=C)=>{const d=r(n);try{await e.publish({channel:d.taskChannel(t,a),message:o({...s,protocolVersion:i}),storeInHistory:!0,sendByPost:!0,meta:o({agentName:n,taskId:t,protocolVersion:i})})}catch{}};export const _LOG_LEVEL_ORDER={error:0,warn:1,info:2,debug:3};export function _resolveLogLevel(){const e=(m("LOG_LEVEL")??"info").toLowerCase();return _LOG_LEVEL_ORDER[e]??_LOG_LEVEL_ORDER.info}export function _isDiagEntryStale(e){const{lastStatusAt:t,lastCategory:a,now:n,thresholdMs:s}=e;return null!==t&&("PNConnectedCategory"!==a&&n-t>s)}const _=(e,t,a)=>{const n=_resolveLogLevel();if((_LOG_LEVEL_ORDER[e]??_LOG_LEVEL_ORDER.info)>n)return;const s={level:e,message:t,ts:Date.now(),...a};"error"===e?console.error("[AgentInstance]",s):"warn"===e?console.warn("[AgentInstance]",s):console.log("[AgentInstance]",s)};export function resolveMaxRunningTimeSec(e,t){return void 0!==e&&void 0!==t&&e!==t&&console.info(`[AgentInstance] opts.maxRunningTimeSec (${e}) overrides card.runtime.maxRunningTimeSec (${t}).`),e??t}export function computeStreamDurationMinutes(e,t,a){return"number"==typeof e?e:t?60:Math.ceil((a??3600)/60)}const K=async(e,n,s,r,o,i,d,c,l)=>{const u={type:"artifact",taskId:n};if(c&&(u.outputId=c),a(o.length))await $(e,n,s,r,{...u,artifactRef:t({data:o,mimeType:i,fileName:d})});else{if(!l)throw new Error("Artifact size exceeds the inline threshold but no baseUrl is configured. Set baseUrl in AgentInstanceOptions to enable the pre-signed URL upload flow.");await f(l,{role:"provider-output",taskId:n,fileName:d??`${n}-artifact`,fileSize:o.length,mimeType:i,outputId:c},o)}};async function x(e,t){const a=`setup.${t.orgId}.${t.taskId}`,n={type:"stream_setup",taskId:t.taskId,orgId:t.orgId,agentName:t.agentName,streamId:t.streamId,channel:t.channel,direction:t.direction,format:t.format,taskKind:t.taskKind,durationMinutes:t.durationMinutes,affinity:t.affinity,declaredStream:t.declaredStream??"_default",protocolVersion:C};t.phase&&(n.phase=t.phase),t.metadata&&(n.metadata=t.metadata);try{throw await e.publish({channel:a,message:o(n),storeInHistory:!1,sendByPost:!0}),new Error("stream_setup publish should have been aborted by Function")}catch(e){const t=l(e);if(t)throw new Error(`Stream setup failed: [${t.code}] ${t.message}`);const a=c(e);if(a)return{token:a.token,tokenTtlMinutes:a.tokenTtlMinutes};throw e}}export const startAgentInstance=async t=>{const a=t.agentName;if(!a)throw new Error("agentName is required: provide opts.agentName");if(!/^[a-zA-Z0-9_]+$/.test(a))throw new Error("agentName must contain only alphanumeric characters and underscores (no hyphens)");const o=r(a),c=t.instanceId??`AG-${a}-${crypto.randomUUID()}`,l=t.token;let f,R=null,M="playground",L=M;if(t.pubnub){const e=t.pubnub._configuration?.keySet,a={publishKey:e?.publishKey??"",subscribeKey:e?.subscribeKey??""};f={playground:a,network:a}}else R=await v(t.cdmUrl),f={playground:R.playground,network:R.network},console.log("[AgentInstance] CDM config loaded — environment switching enabled (playground + network)");const N=m("BLOCKS_API_KEY");if(!N)throw new Error("BLOCKS_API_KEY is required. Run 'blocks login --write-env' to set up credentials.");let P;const j=t.baseUrl??R?.api.baseUrl;let U,D;if(N&&j&&(P=new S(N,j),console.log("[AgentInstance] AgentAuth created — will authenticate via connect")),R){const e=t.baseUrl??R.api.baseUrl,n=await d(a,{baseUrl:e,apiKey:N});if(!n)throw new Error(`[AgentInstance] Agent "${a}" not found in registry. Register the agent (e.g. via \`blocks publish\`) before starting an instance.`);if(!n.billingMode)throw new Error(`[AgentInstance] Agent "${a}" registry entry is missing billingMode. Re-register the agent so the registry persists an explicit billingMode.`);U=n.billingMode,D=n.listing,M="paid"===U?"network":"playground",console.log(`[AgentInstance] Registry billingMode: ${U} — using ${M} environment`),L=M}const O=U??"free",q=[];let V=null;const B=(e,t)=>{const a={label:e,pn:t,listener:void 0,lastStatusAt:null,lastConnectedAt:null,lastMessageAt:null,lastCategory:null,lastOperation:null,lastStatusCode:null};a.listener=(e=>({status:t=>{const a=t,n=String(a.category??""),s=String(a.operation??""),r="number"==typeof a.statusCode?a.statusCode:null,o=Date.now(),i=e.lastCategory;e.lastStatusAt=o,e.lastCategory=n,e.lastOperation=s,e.lastStatusCode=r,"PNConnectedCategory"===n&&(e.lastConnectedAt=o),_("debug","pubnub status event",{event:"pubnub_status",client:e.label,category:n,operation:s,statusCode:r,error:Boolean(a.error),affectedChannels:Array.isArray(a.affectedChannels)?a.affectedChannels.length:void 0,instanceId:c}),n&&n!==i&&_("info",`pubnub status transition [${e.label}]: ${i??"(none)"} -> ${n}`,{event:"pubnub_status_transition",client:e.label,from:i,to:n,operation:s,statusCode:r,instanceId:c})},message:()=>{e.lastMessageAt=Date.now()}}))(a),q.push(a),t.addListener(a.listener)},F=e=>{const t=q.findIndex(t=>t.pn===e);if(t<0)return;const a=q[t];try{e.removeListener(a.listener)}catch{}q.splice(t,1)};let G,z=!1;if(t.pubnub)G=t.pubnub,z=!1,l&&G.setToken(l);else{const e=f[L];G=s({publishKey:e.publishKey,subscribeKey:e.subscribeKey,userId:c,presenceTimeout:20,announceSuccessfulHeartbeats:_resolveLogLevel()>=_LOG_LEVEL_ORDER.debug,subscribeRetryUnbounded:!0,onRetry:e=>{_("warn","pubnub transport retrying",{event:"pubnub_transport_retry",message:e,instanceId:c})}}),z=!0,l&&G.setToken(l)}let H=f[L].subscribeKey,Y=f[L].publishKey;const W=j?new w({apiKey:N,baseUrl:j}):void 0,Z=new I({billingMode:O,subscribeKey:H,publishKey:Y,authProvider:W,baseUrl:j,createPubNub:()=>{const e=s({publishKey:Y,subscribeKey:H,userId:`${c}-taskclient`,subscribeRetryUnbounded:!1});return B("consumer-task",e),e}}),J=t.concurrency??u.concurrency,Q=t.expectedInstances??u.expectedInstances,X=t.maxPendingBacklog,ee=resolveMaxRunningTimeSec(t.maxRunningTimeSec,t.card?.runtime?.maxRunningTimeSec),te=Date.now();let ae=0;const ne=new g,se=new k,re=new Map,oe=new Map,ie=new Map,de=new Map,ce=new Map,le=new Set,ue=new Set,me=new Map,fe=new Set,pe=new Map;let ge,ke;t.logChannel;const ye=async()=>{if("function"==typeof G.setState&&ke)try{await G.setState({channels:[ke],state:{instanceId:c,activeTasks:ae,activeStreams:ne.activeStreamCount,concurrency:J,startedAt:te,preferredProtocolVersion:C,protocolVersions:[...E]}})}catch{}},he=(e,t)=>{const a=re.get(e);a&&(a.delete(t),0===a.size&&re.delete(e))},be=async e=>{(e=>{for(const[t,a]of re)a.has(e)&&(a.delete(e),0===a.size&&re.delete(t))})(e);const t=ne.releaseAllForTask(e);for(const e of t)if(e.streamClient)try{await e.streamClient.end()}catch{}},Ie=async(e,t)=>{he(e,t);const a=ne.get(e);if(0===ne.release(e,t)&&a?.streamClient){console.log("[AgentInstance]",{level:"info",message:"last-ref teardown",event:"stream_registry_last_ref_teardown",streamId:e,affinity:a.affinity,releasingTaskId:t,ts:Date.now()});try{await a.streamClient.end()}catch{}}ye().catch(()=>{})},we=async(e,t)=>{const a=ne.forceRemove(e);if((e=>{re.delete(e)})(e),a){if(a.streamClient)try{await a.streamClient.end()}catch{}for(const e of a.taskIds){const a=se.get(e);if(a){try{const n=f[a.environment??M]??f[M],r=s({publishKey:n.publishKey,subscribeKey:n.subscribeKey,userId:c,subscribeRetryUnbounded:!1});r.setToken(a.writeToken),await $(r,e,a.orgId,a.agentName,{type:"terminal",taskId:e,state:"failed",error:t}),r.destroy()}catch{}se.remove(e)}}await ye()}},ve=async(e,t)=>{const a=se.get(e);if(!a)throw new Error(`No cached credentials for task ${e}`);const n=f[a.environment??M]??f[M],r=s({publishKey:n.publishKey,subscribeKey:n.subscribeKey,userId:c,subscribeRetryUnbounded:!1});r.setToken(a.writeToken);try{await $(r,e,a.orgId,a.agentName,{...t,type:"terminal",taskId:e})}finally{r.destroy()}await be(e),se.remove(e),await ye()},Se=async(s,r,i,d,c)=>{const u=a,m="pipe"===s.taskKind,f=m?"pipe":"request",g=computeStreamDurationMinutes(s.duration,m,ee);if(m&&"number"==typeof s.durationExpiresAtMs){const e=Math.max(0,s.durationExpiresAtMs-Date.now());if(e>0){const t=setTimeout(()=>{le.add(s.taskId),c.abort()},e);me.set(s.taskId,t)}}await $(r,s.taskId,d,u,{type:"progress",taskId:s.taskId,progress:0,state:"running"}),pe.set(s.taskId,0);let k=0,I=null,w=null;const v=()=>{if(null===I)return;const e=I;I=null,k=Date.now(),$(r,s.taskId,d,u,{type:"progress",taskId:s.taskId,message:e}).catch(()=>{})},S={taskId:s.taskId,requestParts:s.requestParts??[],reportStatus:e=>{de.set(s.taskId,e);const t=Date.now();if(t-k>=1e3)I=null,k=t,$(r,s.taskId,d,u,{type:"progress",taskId:s.taskId,message:e}).catch(()=>{});else if(I=e,!w){w=setTimeout(()=>{w=null,v()},1e3-(t-k))}},get isCancelled(){return c.signal.aborted},get isExpired(){return le.has(s.taskId)},hasStream:!!s.hasStream,cancelSignal:c.signal,taskClient:Z,consumerPublicKey:s.consumerPublicKey,downloadInputArtifact:async e=>{const t=e.artifactRef;if(!t)throw new Error(`Request part has no artifactRef (partId: ${e.partId??"none"})`);if("inline"===t.kind)return Buffer.from(n(t));if(!t.channel||!t.fileId||!t.fileName)throw new Error("File artifactRef missing channel, fileId, or fileName");if(!r.downloadFile)throw new Error("PubNub client does not support downloadFile");const a=await r.downloadFile({channel:t.channel,id:t.fileId,name:t.fileName}),s=a?.data;if(Buffer.isBuffer(s))return s;if(s instanceof Uint8Array)return Buffer.from(s);if(s&&"function"==typeof s.arrayBuffer){const e=await s.arrayBuffer();return Buffer.from(e)}throw new Error("Unexpected downloadFile response format")},publishArtifact:async(e,t)=>{const a="string"==typeof e?Buffer.from(e,"utf-8"):e,n=t?.mimeType??"application/octet-stream",o=j?{baseUrl:j,authProvider:l?new p(l):void 0,agentAuth:P}:void 0;await K(r,s.taskId,d,u,a,n,t?.fileName,t?.outputId,o)},createStream:async a=>{if(void 0!==a&&("object"!=typeof a||null===a||Array.isArray(a)))throw new TypeError("createStream expects an options object. The positional streamId argument was removed; move any declared-stream key into CreateStreamOptions.declaredStream. See SDK_CONTRACT §8.2.1.");if(!s.hasStream)throw new Error("Streaming was not negotiated for this task. Ensure the agent card is registered with streaming capability.");const n=t.card.streams;if(!n||0===Object.keys(n).length)throw new Error("Agent card has no streams block. Streaming requires declared streams in the card.");const i=Object.keys(n);let c;if(a?.declaredStream)c=a.declaredStream;else{if(1!==i.length)throw new Error(`Card declares multiple streams (${i.join(", ")}). Specify declaredStream in CreateStreamOptions.`);c=i[0]}const l=n[c];if(!l)throw new Error(`Stream "${c}" is not declared in the agent card. Available streams: ${i.join(", ")}`);const p=a?.direction??l.direction,k=a?.format??l.format;if(a?.direction&&a.direction!==l.direction)throw new Error(`Direction "${a.direction}" conflicts with card declaration "${l.direction}" for stream "${c}".`);if(a?.format&&a.format!==l.format)throw new Error(`Format "${a.format}" conflicts with card declaration "${l.format}" for stream "${c}".`);const I=a?.external??!1,w=a?.metadata,v=a?.onActivate,S=l.affinity??"dedicated";if("shared"===S&&I)throw new Error(`Shared-affinity external streams are not supported. Declared stream '${c}' has affinity: 'shared' and createStream was called with external: true. Shared affinity requires a single SDK-managed writer with per-task ref-counting; external streams delegate the writer entirely. Use affinity: 'dedicated' with external: true, or affinity: 'shared' without external.`);if(!m){if("shared"===S)throw new Error(`Shared-affinity streams are not supported on request tasks. Declared stream '${c}' has affinity: 'shared'. Request tasks are single-shot; cross-task broadcast is inherently a pipe-task concept. Use affinity: 'dedicated' or remove 'request' from the agent card's taskKinds.`);if("outbound"!==p)throw new Error("Request tasks only support outbound streams");if(I)throw new Error("Request tasks cannot use external streams")}let T;if("shared"===S)T=c;else{const e=(pe.get(s.taskId)??0)+1;pe.set(s.taskId,e),T=`${s.taskId}-${e}`}const C=o.streamChannel(T);if("shared"===S){const e=re.get(T)?.get(s.taskId);if(e)return e}const{entry:E,isNew:A,isNewForTask:$}=ne.acquire(T,s.taskId,{direction:p,format:k,external:I,affinity:S});se.addStream(s.taskId,T);const _=e=>{if("shared"!==S)return;let t=re.get(T);t||(t=new Map,re.set(T,t)),t.set(s.taskId,e)};if(!$){E.setupPromise&&await E.setupPromise;const e=re.get(T)?.get(s.taskId);if(e)return e;if(E.streamClient){const e=y(T,E.streamClient,s.taskId,{releaseStream:Ie});return _(e),e}throw new Error(`Stream "${T}" same-task reacquire reached an impossible state: setup completed but no client installed. This indicates a logic error in the shared-stream handle cache.`)}if(!A)try{if(E.setupPromise&&await E.setupPromise,"shared"===S&&await(async()=>{await x(r,{taskId:s.taskId,orgId:d,agentName:u,streamId:T,channel:C,direction:p,format:k,taskKind:f,durationMinutes:g,affinity:S,phase:"activate",metadata:w,declaredStream:c})})(),E.streamClient){const e=y(T,E.streamClient,s.taskId,{releaseStream:Ie});return _(e),e}if(I)return h(T,C,"",async()=>{throw new Error("External stream already activated")});throw new Error(`Stream "${T}" exists but has no client`)}catch(e){he(T,s.taskId);try{await ne.release(T,s.taskId)}catch{}throw e}let K,R;E.setupPromise=new Promise((e,t)=>{K=e,R=t}),E.setupPromise.catch(()=>{});const M=I?"token_request":"embedded";let L;try{L=await x(r,{taskId:s.taskId,orgId:d,agentName:u,streamId:T,channel:C,direction:p,format:k,taskKind:f,durationMinutes:g,affinity:S,phase:M,metadata:w,declaredStream:c})}catch(e){R(e),he(T,s.taskId);try{await ne.release(T,s.taskId)}catch{}throw e}if(I){K(),E.setupPromise=null;const e=L.token??"";return h(T,C,e,async e=>{await x(r,{taskId:s.taskId,orgId:d,agentName:u,streamId:T,channel:C,direction:p,format:k,taskKind:f,durationMinutes:g,affinity:S,phase:"activate",metadata:e?.metadata??w,declaredStream:c})})}const N=L.token;if(!N){const e=new Error("No T7a token received from stream setup handshake");R(e),he(T,s.taskId);try{await ne.release(T,s.taskId)}catch{}throw e}const P=new e({subscribeKey:H,publishKey:Y,token:N,agentName:u,streamId:T,channel:C,format:k,direction:p,affinity:S,gating:m,bundleSizeBytes:a?.bundleSizeBytes,maxLatencyMs:a?.maxLatencyMs});E.streamClient=P;const j=y(T,P,s.taskId,{releaseStream:Ie});if(_(j),K(),E.setupPromise=null,"outbound"===p||"bidirectional"===p){const e=a?.subscribeGraceMs??1e3;e>0&&await new Promise(t=>setTimeout(t,e))}return v&&(E.activated=!0,E.activatePromise=b(T,j,v,we)),j}};let T;try{T=t.handler?await t.handler(s,S):void 0}finally{pe.delete(s.taskId),w&&(clearTimeout(w),w=null),v()}const C=j?{baseUrl:j,authProvider:l?new p(l):void 0,agentAuth:P}:void 0;if(T?.artifacts&&T.artifacts.length>0)for(const e of T.artifacts){const t="string"==typeof e.data?Buffer.from(e.data,"utf-8"):e.data;await K(r,s.taskId,d,u,t,e.mimeType,e.fileName,e.outputId,C)}const E=c.signal.aborted;if(m){const e=le.has(s.taskId),t=ue.has(s.taskId);if(E||e||t){await be(s.taskId);const a=E&&!e?"canceled":"completed";await $(r,s.taskId,d,u,{type:"terminal",taskId:s.taskId,state:a,...e?{completionReason:"duration_expired"}:{},...t?{reason:"terminated"}:{}}),se.remove(s.taskId),_("info",`Task ${s.taskId} ${a}`,{taskId:s.taskId,agentName:u,owner:i})}else _("info",`Task ${s.taskId} handler returned (pipe, no auto-terminal)`,{taskId:s.taskId,agentName:u,owner:i})}else{const e=ue.has(s.taskId);await be(s.taskId);const t=E?"canceled":"completed";await $(r,s.taskId,d,u,{type:"terminal",taskId:s.taskId,state:t,...e?{reason:"terminated"}:{}}),se.remove(s.taskId),_("info",`Task ${s.taskId} ${t}`,{taskId:s.taskId,agentName:u,owner:i})}},Te=(()=>{let e=!1;return(t,a,n)=>{if(_("debug","pamDeniedHandler invoked",{event:"pam_handler_invoked",category:t,operation:a,statusCode:n,alreadyHandled:e,instanceId:c}),"PNAccessDeniedCategory"===t&&!e){e=!0,_("error","PAM access denied — destroying control client (agent will go silent)",{event:"pam_denied_destroy",category:t,operation:a,statusCode:n,instanceId:c,controlChannel:ke}),console.error(`[AgentInstance] PAM token expired or revoked — agent ${c} is no longer receiving tasks. Re-register the agent to resume.`);try{G.destroy()}catch{}}}})(),Ce={status:e=>{const t=e,a=String(t.category??""),n=String(t.operation??""),s="number"==typeof t.statusCode?t.statusCode:null;"PNConnectedCategory"===a&&console.log(`[AgentInstance] PubNub connected to ${ke}`),Te(a,n,s)},message:e=>{const n=e.message;if(!n)return;if("SwitchEnvironment"===n.type){const e=n,t=e.environment;return void(t&&t!==L&&t in f&&((e,t)=>{if(_("info",`SwitchEnvironment requested: ${L} -> ${e}`,{pamToken:t?"present":"absent"}),!t)return void _("error","SwitchEnvironment rejected: pamToken is required but was absent. The backend must include a pamToken in SwitchEnvironment messages.");const a=G,n=z;try{a.removeListener(Ce),ke&&a.unsubscribe({channels:[ke]})}catch{}F(a);const r=f[e];if(G=s({publishKey:r.publishKey,subscribeKey:r.subscribeKey,userId:c,presenceTimeout:20,announceSuccessfulHeartbeats:_resolveLogLevel()>=_LOG_LEVEL_ORDER.debug,subscribeRetryUnbounded:!0,onRetry:e=>{_("warn","pubnub transport retrying",{event:"pubnub_transport_retry",message:e,instanceId:c})}}),z=!0,ge=void 0,G.setToken(t),0!==Q&&"function"==typeof G.setFilterExpression&&G.setFilterExpression(`meta.instance == '${c}' || meta.broadcast == "true"`),G.addListener(Ce),B("control",G),ke&&G.subscribe({channels:[ke]}),n)try{a.destroy()}catch{}L=e,H=r.subscribeKey,Y=r.publishKey,Z.updateKeys(r.subscribeKey,r.publishKey),_("info",`Switched to ${e} environment`),ye().catch(()=>{})})(t,e.pamToken))}(async(e,n)=>{if("StartTask"===e.type){const r=L,o=((e,t)=>{if("string"==typeof e&&e.length>0)return e;const a=t?.sub;return"string"==typeof a&&a.length>0?a:"anonymous"})(e.ownerId,e.callerClaims),i=e.orgId??o;if(fe.has(e.taskId))return void _("warn",`Ignoring duplicate StartTask for in-flight task ${e.taskId}`,{taskId:e.taskId});const d=Boolean(n?.broadcast);if(e.protocolVersion&&!A(e.protocolVersion))return d?void _("debug",`Ignoring broadcast StartTask ${e.taskId} with unsupported protocolVersion ${e.protocolVersion}`,{taskId:e.taskId}):(await $(G,e.taskId,i,a,{type:"terminal",taskId:e.taskId,state:"failed",error:"unsupported_protocol_version"},e.protocolVersion),void _("warn",`Task ${e.taskId} rejected: unsupported protocolVersion ${e.protocolVersion}`,{taskId:e.taskId,instanceId:c}));if("pipe"===(e.taskKind||"request")){const t=e.duration,n=e.durationExpiresAtMs;if(null==t||!Number.isInteger(t)||t<1||t>43200||"number"!=typeof n||n<=0)return _("error",`Task ${e.taskId} rejected: pipe StartTask with invalid duration (duration=${t}, durationExpiresAtMs=${n})`,{taskId:e.taskId,instanceId:c}),void await $(G,e.taskId,i,a,{type:"terminal",taskId:e.taskId,state:"failed",error:"invalid_start_task"},e.protocolVersion)}const l=ae+ne.activeStreamCount;if(J>0&&l>=J)return d?void _("debug",`Task ${e.taskId} skipped (broadcast, at capacity ${l}/${J})`,{taskId:e.taskId,instanceId:c}):(await $(G,e.taskId,i,a,{type:"terminal",taskId:e.taskId,state:"failed",error:"agent_at_capacity"}),void _("warn",`Task ${e.taskId} rejected: agent at capacity`,{taskId:e.taskId,instanceId:c,activeTasks:ae}));oe.set(e.taskId,o),ie.set(e.taskId,i),fe.add(e.taskId),ae++,e.controlToken&&(ge=e.controlToken);const u=new AbortController;ce.set(e.taskId,u);let m=null;try{const n=f[r];if(!n.publishKey||!n.subscribeKey)throw new Error("Per-task PubNub client requires valid pub/sub keys. When using an injected PubNub client, ensure it was created with publishKey and subscribeKey.");m=s({publishKey:n.publishKey,subscribeKey:n.subscribeKey,userId:c,subscribeRetryUnbounded:!1}),e.writeToken&&m.setToken(e.writeToken),B(`task-sub:${e.taskId}`,m),se.set(e.taskId,{ownerId:o,orgId:i,writeToken:e.writeToken??"",agentName:a,environment:r}),await ye();const{writeToken:d,controlToken:l,...p}=e;t.onStartTask?await t.onStartTask(e,m):await Se(p,m,o,i,u)}catch(n){const s=n?.message??"Agent instance error",r=m??G;try{await be(e.taskId)}catch{}await $(r,e.taskId,i,a,{type:"terminal",taskId:e.taskId,state:"failed",error:s}),t.onError&&await t.onError(e.taskId,n),_("error",s,{taskId:e.taskId}),se.remove(e.taskId)}finally{const t=me.get(e.taskId);if(t&&(clearTimeout(t),me.delete(e.taskId)),fe.delete(e.taskId),oe.delete(e.taskId),ie.delete(e.taskId),de.delete(e.taskId),ce.delete(e.taskId),le.delete(e.taskId),ue.delete(e.taskId),m){F(m);try{m.destroy()}catch{}}ae--,0===ae&&ge&&G.setToken(ge),await ye()}}else if("CancelTask"===e.type){const t=ce.get(e.taskId);t?(t.abort(),_("info",`Task ${e.taskId} cancel requested (cooperative)`,{taskId:e.taskId,instanceId:c})):se.get(e.taskId)&&await ve(e.taskId,{state:"canceled"})}else if("ExpireTask"===e.type)ce.has(e.taskId)?(le.add(e.taskId),ce.get(e.taskId)?.abort(),_("info",`Task ${e.taskId} expired`,{taskId:e.taskId,instanceId:c})):se.get(e.taskId)&&await ve(e.taskId,{state:"completed",completionReason:"duration_expired"});else if("TerminateTask"===e.type)ce.has(e.taskId)?(ue.add(e.taskId),ce.get(e.taskId)?.abort()):se.get(e.taskId)&&await ve(e.taskId,{state:"canceled",reason:"terminated"});else if("PauseTask"===e.type){if(fe.has(e.taskId)){const t=ie.get(e.taskId)??"anonymous";await $(G,e.taskId,t,a,{type:"system",taskId:e.taskId,status:"paused"})}}else if("ResumeTask"===e.type){if(fe.has(e.taskId)){const t=ie.get(e.taskId)??"anonymous";await $(G,e.taskId,t,a,{type:"system",taskId:e.taskId,status:"resumed"})}}else"RetryTask"===e.type&&_("info","RetryTask received (no-op)",{taskId:e.taskId})})(n,e.userMetadata&&"object"==typeof e.userMetadata?e.userMetadata:void 0).catch(e=>{console.error("[AgentInstance] Unhandled error in message handler:",e)})}};G.addListener(Ce),B("control",G),null===V&&(V=setInterval(()=>{const e=Date.now();let t=!1;const a=q.map(a=>{const n=null!==a.lastStatusAt?e-a.lastStatusAt:null,s=_isDiagEntryStale({lastStatusAt:a.lastStatusAt,lastCategory:a.lastCategory,now:e,thresholdMs:6e4});s&&(t=!0);const r="function"==typeof a.pn.getSubscribedChannels?a.pn.getSubscribedChannels().length:null;return{client:a.label,lastCategory:a.lastCategory,lastOperation:a.lastOperation,lastStatusCode:a.lastStatusCode,msSinceStatus:n,msSinceConnected:null!==a.lastConnectedAt?e-a.lastConnectedAt:null,msSinceMessage:null!==a.lastMessageAt?e-a.lastMessageAt:null,subscribedChannels:r,stale:s}}),n={event:"pubnub_alive_snapshot",instanceId:c,clients:a};t?_("info","pubnub alive snapshot — STALE clients present",n):_("debug","pubnub alive snapshot",n)},1e4),"function"==typeof V.unref&&V.unref()),_("info","pubnub diagnostics armed",{event:"pubnub_diagnostics_armed",snapshotIntervalMs:1e4,staleThresholdMs:6e4,instanceId:c}),i(a,{instanceId:c,billingMode:O,description:t.description,scaling:{expectedInstances:Q,concurrency:J,maxPendingBacklog:X,maxRunningTimeSec:ee},card:t.card,cardRef:t.cardRef,cardSummary:t.cardSummary,listing:D??t.listing,actor:`agent-instance:${c}`,baseUrl:t.baseUrl??R?.api.baseUrl,agentAuth:P}).then(e=>{if(console.log(`[AgentInstance] Registered agent: ${a} (instance: ${c})`),!e.controlChannel)throw new Error("Connect response missing controlChannel — server may be outdated");ke=e.controlChannel,e.pamToken&&G.setToken(e.pamToken),0!==Q&&"function"==typeof G.setFilterExpression&&G.setFilterExpression(`meta.instance == '${c}' || meta.broadcast == "true"`),G.subscribe({channels:[ke]}),ye().catch(()=>{})}).catch(e=>{const t=e instanceof Error?e.message:String(e);console.error(`[AgentInstance] Failed to register agent: ${a} — ${t}`),e instanceof T&&process.exit(1)}),_("info",`Agent instance ${c} started (agent name: ${a})`,{agentName:a,event:"agent_instance_started",instanceId:c});return{stop:()=>{try{null!==V&&(clearInterval(V),V=null);for(const e of q)try{e.pn.removeListener(e.listener)}catch{}q.length=0,W?.destroy(),Z.destroy(),G.removeListener(Ce),ke&&G.unsubscribe({channels:[ke]}),z&&G.destroy();for(const e of ne.streamIds()){const t=ne.get(e);if(t?.streamClient)try{t.streamClient.end()}catch{}}ne.clear(),re.clear(),se.clear()}catch{}},agentName:a,instanceId:c,get controlChannel(){return ke},get pubnub(){return G},get subscribeKey(){return f[L].subscribeKey},taskClient:Z,publishTerminal:ve,failStream:we,cdmConfig:R}};export const artifactUtils={buildArtifactRef:t,shouldInlineArtifact:a};
|
|
1
|
+
import{StreamClient as e}from"../stream/index.js";import{buildArtifactRef as t,shouldInlineArtifact as a,decodeInlineArtifact as n}from"./artifacts.js";import{createPubNubClient as s}from"./pubnub-client.js";import{createChannelManager as r}from"./channel-manager.js";import{toPayload as o}from"./pubnub-types.js";import{connectAgent as i,getAgent as d}from"./agent-registry.js";import{parseStreamSetupResponse as c,parseStreamSetupError as l}from"./stream-setup-helper.js";import{DEFAULTS as u}from"../defaults.js";import{getEnv as m}from"../env.js";import{log as f,isInternalDebugEnabled as p,_resolveLogLevel as k,_LOG_LEVEL_ORDER as g}from"./logger.js";import{uploadFile as y}from"./file-upload.js";import{StaticAuthProvider as h}from"./auth-provider.js";import{StreamRegistry as b}from"./stream-registry.js";import{CredentialCache as I}from"./credential-cache.js";import{createStreamObject as w,createExternalStreamObject as v,runOnActivate as S}from"./stream-context.js";import{TaskClient as T}from"./task-client.js";import{ConsumerAuth as C}from"./consumer-auth.js";import{fetchCdmConfig as $}from"./cdm-config.js";import{AgentAuth as K,AgentAuthFatalError as _}from"./agent-auth.js";import{CURRENT_PROTOCOL_VERSION as A,SUPPORTED_PROTOCOL_VERSIONS as E,isProtocolVersionSupported as x}from"./protocol-version.js";const M=async(e,t,a,n,s,i=A)=>{const d=r(n);try{await e.publish({channel:d.taskChannel(t,a),message:o({...s,protocolVersion:i}),storeInHistory:!0,sendByPost:!0,meta:o({agentName:n,taskId:t,protocolVersion:i})})}catch{}};export function _isDiagEntryStale(e){const{lastStatusAt:t,lastCategory:a,now:n,thresholdMs:s}=e;return null!==t&&("PNConnectedCategory"!==a&&n-t>s)}const N=(e,t,a)=>f("[AgentInstance]",e,t,a);export function resolveMaxRunningTimeSec(e,t){return void 0!==e&&void 0!==t&&e!==t&&f("[AgentInstance]","info",`opts.maxRunningTimeSec (${e}) overrides card.runtime.maxRunningTimeSec (${t})`,{event:"max_running_time_override",optsValue:e,cardValue:t}),e??t}export function computeStreamDurationMinutes(e,t,a){return"number"==typeof e?e:t?60:Math.ceil((a??3600)/60)}const R=async(e,n,s,r,o,i,d,c,l)=>{const u={type:"artifact",taskId:n};if(c&&(u.outputId=c),a(o.length))await M(e,n,s,r,{...u,artifactRef:t({data:o,mimeType:i,fileName:d})});else{if(!l)throw new Error("Artifact size exceeds the inline threshold but no baseUrl is configured. Set baseUrl in AgentInstanceOptions to enable the pre-signed URL upload flow.");await y(l,{role:"provider-output",taskId:n,fileName:d??`${n}-artifact`,fileSize:o.length,mimeType:i,outputId:c},o)}};async function P(e,t){const a=`setup.${t.orgId}.${t.taskId}`,n={type:"stream_setup",taskId:t.taskId,orgId:t.orgId,agentName:t.agentName,streamId:t.streamId,channel:t.channel,direction:t.direction,format:t.format,taskKind:t.taskKind,durationMinutes:t.durationMinutes,affinity:t.affinity,declaredStream:t.declaredStream??"_default",protocolVersion:A};t.phase&&(n.phase=t.phase),t.metadata&&(n.metadata=t.metadata);try{throw await e.publish({channel:a,message:o(n),storeInHistory:!1,sendByPost:!0}),new Error("stream_setup publish should have been aborted by Function")}catch(e){const t=l(e);if(t)throw new Error(`Stream setup failed: [${t.code}] ${t.message}`);const a=c(e);if(a)return{token:a.token,tokenTtlMinutes:a.tokenTtlMinutes};throw e}}export const startAgentInstance=async t=>{const a=t.agentName;if(!a)throw new Error("agentName is required: provide opts.agentName");if(!/^[a-zA-Z0-9_]+$/.test(a))throw new Error("agentName must contain only alphanumeric characters and underscores (no hyphens)");const o=r(a),c=t.instanceId??`AG-${a}-${crypto.randomUUID()}`,l=t.token;let f,y=null,j="playground",U=j;if(t.pubnub){const e=t.pubnub._configuration?.keySet,a={publishKey:e?.publishKey??"",subscribeKey:e?.subscribeKey??""};f={playground:a,network:a}}else y=await $(t.cdmUrl),f={playground:y.playground,network:y.network},N("info","CDM config loaded — environment switching enabled",{event:"cdm_config_loaded"});const q=m("BLOCKS_API_KEY");if(!q)throw new Error("BLOCKS_API_KEY is required. Run 'blocks login --write-env' to set up credentials.");let D;const B=t.baseUrl??y?.api.baseUrl;let V,F;if(q&&B&&(D=new K(q,B),N("info","AgentAuth created — will authenticate via connect",{event:"agent_auth_created"})),y){const e=t.baseUrl??y.api.baseUrl,n=await d(a,{baseUrl:e,apiKey:q});if(!n)throw new Error(`[AgentInstance] Agent "${a}" not found in registry. Register the agent (e.g. via \`blocks publish\`) before starting an instance.`);if(!n.billingMode)throw new Error(`[AgentInstance] Agent "${a}" registry entry is missing billingMode. Re-register the agent so the registry persists an explicit billingMode.`);V=n.billingMode,F=n.listing,j="paid"===V?"network":"playground",N("info",`registry billingMode: ${V} — using ${j} environment`,{event:"registry_billing_mode_resolved",billingMode:V,environment:j}),U=j}const L=V??"free",O=[];let z=null;const H=p(),G=(e,t)=>{if(!H)return;const a={label:e,pn:t,listener:void 0,lastStatusAt:null,lastConnectedAt:null,lastMessageAt:null,lastCategory:null,lastOperation:null,lastStatusCode:null};a.listener=(e=>({status:t=>{const a=t,n=String(a.category??""),s=String(a.operation??""),r="number"==typeof a.statusCode?a.statusCode:null,o=Date.now(),i=e.lastCategory;e.lastStatusAt=o,e.lastCategory=n,e.lastOperation=s,e.lastStatusCode=r,"PNConnectedCategory"===n&&(e.lastConnectedAt=o),N("debug","pubnub status event",{event:"pubnub_status",client:e.label,category:n,operation:s,statusCode:r,error:Boolean(a.error),affectedChannels:Array.isArray(a.affectedChannels)?a.affectedChannels.length:void 0,instanceId:c}),n&&n!==i&&N("info",`pubnub status transition [${e.label}]: ${i??"(none)"} -> ${n}`,{event:"pubnub_status_transition",client:e.label,from:i,to:n,operation:s,statusCode:r,instanceId:c})},message:()=>{e.lastMessageAt=Date.now()}}))(a),O.push(a),t.addListener(a.listener)},Y=e=>{if(!H)return;const t=O.findIndex(t=>t.pn===e);if(t<0)return;const a=O[t];try{e.removeListener(a.listener)}catch{}O.splice(t,1)};let W,Z=!1;if(t.pubnub)W=t.pubnub,Z=!1,l&&W.setToken(l);else{const e=f[U];W=s({publishKey:e.publishKey,subscribeKey:e.subscribeKey,userId:c,presenceTimeout:20,announceSuccessfulHeartbeats:k()>=g.debug,subscribeRetryUnbounded:!0,onRetry:e=>{N("warn","pubnub transport retrying",{event:"pubnub_transport_retry",message:e,instanceId:c})}}),Z=!0,l&&W.setToken(l)}let J=f[U].subscribeKey,Q=f[U].publishKey;const X=B?new C({apiKey:q,baseUrl:B}):void 0,ee=new T({billingMode:L,subscribeKey:J,publishKey:Q,authProvider:X,baseUrl:B,createPubNub:()=>{const e=s({publishKey:Q,subscribeKey:J,userId:`${c}-taskclient`,subscribeRetryUnbounded:!1});return G("consumer-task",e),e}}),te=t.concurrency??u.concurrency,ae=t.expectedInstances??u.expectedInstances,ne=t.maxPendingBacklog,se=resolveMaxRunningTimeSec(t.maxRunningTimeSec,t.card?.runtime?.maxRunningTimeSec),re=Date.now();let oe=0;const ie=new b,de=new I,ce=new Map,le=new Map,ue=new Map,me=new Map,fe=new Map,pe=new Set,ke=new Set,ge=new Map,ye=new Set,he=new Map;let be,Ie;t.logChannel;const we=async()=>{if("function"==typeof W.setState&&Ie)try{await W.setState({channels:[Ie],state:{instanceId:c,activeTasks:oe,activeStreams:ie.activeStreamCount,concurrency:te,startedAt:re,preferredProtocolVersion:A,protocolVersions:[...E]}})}catch{}},ve=(e,t)=>{const a=ce.get(e);a&&(a.delete(t),0===a.size&&ce.delete(e))},Se=async e=>{(e=>{for(const[t,a]of ce)a.has(e)&&(a.delete(e),0===a.size&&ce.delete(t))})(e);const t=ie.releaseAllForTask(e);for(const e of t)if(e.streamClient)try{await e.streamClient.end()}catch{}},Te=async(e,t)=>{ve(e,t);const a=ie.get(e);if(0===ie.release(e,t)&&a?.streamClient){N("info","last-ref teardown",{event:"stream_registry_last_ref_teardown",streamId:e,affinity:a.affinity,releasingTaskId:t});try{await a.streamClient.end()}catch{}}we().catch(()=>{})},Ce=async(e,t)=>{const a=ie.forceRemove(e);if((e=>{ce.delete(e)})(e),a){if(a.streamClient)try{await a.streamClient.end()}catch{}for(const e of a.taskIds){const a=de.get(e);if(a){try{const n=f[a.environment??j]??f[j],r=s({publishKey:n.publishKey,subscribeKey:n.subscribeKey,userId:c,subscribeRetryUnbounded:!1});r.setToken(a.writeToken),await M(r,e,a.orgId,a.agentName,{type:"terminal",taskId:e,state:"failed",error:t}),r.destroy()}catch{}de.remove(e)}}await we()}},$e=async(e,t)=>{const a=de.get(e);if(!a)throw new Error(`No cached credentials for task ${e}`);const n=f[a.environment??j]??f[j],r=s({publishKey:n.publishKey,subscribeKey:n.subscribeKey,userId:c,subscribeRetryUnbounded:!1});r.setToken(a.writeToken);try{await M(r,e,a.orgId,a.agentName,{...t,type:"terminal",taskId:e})}finally{r.destroy()}await Se(e),de.remove(e),await we()},Ke=async(s,r,i,d,c)=>{const u=a,m="pipe"===s.taskKind,f=m?"pipe":"request",p=computeStreamDurationMinutes(s.duration,m,se);if(m&&"number"==typeof s.durationExpiresAtMs){const e=Math.max(0,s.durationExpiresAtMs-Date.now());if(e>0){const t=setTimeout(()=>{pe.add(s.taskId),c.abort()},e);ge.set(s.taskId,t)}}await M(r,s.taskId,d,u,{type:"progress",taskId:s.taskId,progress:0,state:"running"}),he.set(s.taskId,0);let k=0,g=null,y=null;const b=()=>{if(null===g)return;const e=g;g=null,k=Date.now(),M(r,s.taskId,d,u,{type:"progress",taskId:s.taskId,message:e}).catch(()=>{})},I={taskId:s.taskId,requestParts:s.requestParts??[],reportStatus:e=>{me.set(s.taskId,e);const t=Date.now();if(t-k>=1e3)g=null,k=t,M(r,s.taskId,d,u,{type:"progress",taskId:s.taskId,message:e}).catch(()=>{});else if(g=e,!y){y=setTimeout(()=>{y=null,b()},1e3-(t-k))}},get isCancelled(){return c.signal.aborted},get isExpired(){return pe.has(s.taskId)},hasStream:!!s.hasStream,cancelSignal:c.signal,taskClient:ee,consumerPublicKey:s.consumerPublicKey,downloadInputArtifact:async e=>{const t=e.artifactRef;if(!t)throw new Error(`Request part has no artifactRef (partId: ${e.partId??"none"})`);if("inline"===t.kind)return Buffer.from(n(t));if(!t.channel||!t.fileId||!t.fileName)throw new Error("File artifactRef missing channel, fileId, or fileName");if(!r.downloadFile)throw new Error("PubNub client does not support downloadFile");const a=await r.downloadFile({channel:t.channel,id:t.fileId,name:t.fileName}),s=a?.data;if(Buffer.isBuffer(s))return s;if(s instanceof Uint8Array)return Buffer.from(s);if(s&&"function"==typeof s.arrayBuffer){const e=await s.arrayBuffer();return Buffer.from(e)}throw new Error("Unexpected downloadFile response format")},publishArtifact:async(e,t)=>{const a="string"==typeof e?Buffer.from(e,"utf-8"):e,n=t?.mimeType??"application/octet-stream",o=B?{baseUrl:B,authProvider:l?new h(l):void 0,agentAuth:D}:void 0;await R(r,s.taskId,d,u,a,n,t?.fileName,t?.outputId,o)},createStream:async a=>{if(void 0!==a&&("object"!=typeof a||null===a||Array.isArray(a)))throw new TypeError("createStream expects an options object. The positional streamId argument was removed; move any declared-stream key into CreateStreamOptions.declaredStream. See SDK_CONTRACT §8.2.1.");if(!s.hasStream)throw new Error("Streaming was not negotiated for this task. Ensure the agent card is registered with streaming capability.");const n=t.card.streams;if(!n||0===Object.keys(n).length)throw new Error("Agent card has no streams block. Streaming requires declared streams in the card.");const i=Object.keys(n);let c;if(a?.declaredStream)c=a.declaredStream;else{if(1!==i.length)throw new Error(`Card declares multiple streams (${i.join(", ")}). Specify declaredStream in CreateStreamOptions.`);c=i[0]}const l=n[c];if(!l)throw new Error(`Stream "${c}" is not declared in the agent card. Available streams: ${i.join(", ")}`);const k=a?.direction??l.direction,g=a?.format??l.format;if(a?.direction&&a.direction!==l.direction)throw new Error(`Direction "${a.direction}" conflicts with card declaration "${l.direction}" for stream "${c}".`);if(a?.format&&a.format!==l.format)throw new Error(`Format "${a.format}" conflicts with card declaration "${l.format}" for stream "${c}".`);const y=a?.external??!1,h=a?.metadata,b=a?.onActivate,I=l.affinity??"dedicated";if("shared"===I&&y)throw new Error(`Shared-affinity external streams are not supported. Declared stream '${c}' has affinity: 'shared' and createStream was called with external: true. Shared affinity requires a single SDK-managed writer with per-task ref-counting; external streams delegate the writer entirely. Use affinity: 'dedicated' with external: true, or affinity: 'shared' without external.`);if(!m){if("shared"===I)throw new Error(`Shared-affinity streams are not supported on request tasks. Declared stream '${c}' has affinity: 'shared'. Request tasks are single-shot; cross-task broadcast is inherently a pipe-task concept. Use affinity: 'dedicated' or remove 'request' from the agent card's taskKinds.`);if("outbound"!==k)throw new Error("Request tasks only support outbound streams");if(y)throw new Error("Request tasks cannot use external streams")}let T;if("shared"===I)T=c;else{const e=(he.get(s.taskId)??0)+1;he.set(s.taskId,e),T=`${s.taskId}-${e}`}const C=o.streamChannel(T);if("shared"===I){const e=ce.get(T)?.get(s.taskId);if(e)return e}const{entry:$,isNew:K,isNewForTask:_}=ie.acquire(T,s.taskId,{direction:k,format:g,external:y,affinity:I});de.addStream(s.taskId,T);const A=e=>{if("shared"!==I)return;let t=ce.get(T);t||(t=new Map,ce.set(T,t)),t.set(s.taskId,e)};if(!_){$.setupPromise&&await $.setupPromise;const e=ce.get(T)?.get(s.taskId);if(e)return e;if($.streamClient){const e=w(T,$.streamClient,s.taskId,{releaseStream:Te});return A(e),e}throw new Error(`Stream "${T}" same-task reacquire reached an impossible state: setup completed but no client installed. This indicates a logic error in the shared-stream handle cache.`)}if(!K)try{if($.setupPromise&&await $.setupPromise,"shared"===I&&await(async()=>{await P(r,{taskId:s.taskId,orgId:d,agentName:u,streamId:T,channel:C,direction:k,format:g,taskKind:f,durationMinutes:p,affinity:I,phase:"activate",metadata:h,declaredStream:c})})(),$.streamClient){const e=w(T,$.streamClient,s.taskId,{releaseStream:Te});return A(e),e}if(y)return v(T,C,"",async()=>{throw new Error("External stream already activated")});throw new Error(`Stream "${T}" exists but has no client`)}catch(e){ve(T,s.taskId);try{await ie.release(T,s.taskId)}catch{}throw e}let E,x;$.setupPromise=new Promise((e,t)=>{E=e,x=t}),$.setupPromise.catch(()=>{});const M=y?"token_request":"embedded";let N;try{N=await P(r,{taskId:s.taskId,orgId:d,agentName:u,streamId:T,channel:C,direction:k,format:g,taskKind:f,durationMinutes:p,affinity:I,phase:M,metadata:h,declaredStream:c})}catch(e){x(e),ve(T,s.taskId);try{await ie.release(T,s.taskId)}catch{}throw e}if(y){E(),$.setupPromise=null;const e=N.token??"";return v(T,C,e,async e=>{await P(r,{taskId:s.taskId,orgId:d,agentName:u,streamId:T,channel:C,direction:k,format:g,taskKind:f,durationMinutes:p,affinity:I,phase:"activate",metadata:e?.metadata??h,declaredStream:c})})}const R=N.token;if(!R){const e=new Error("No T7a token received from stream setup handshake");x(e),ve(T,s.taskId);try{await ie.release(T,s.taskId)}catch{}throw e}const j=new e({subscribeKey:J,publishKey:Q,token:R,agentName:u,streamId:T,channel:C,format:g,direction:k,affinity:I,gating:m,bundleSizeBytes:a?.bundleSizeBytes,maxLatencyMs:a?.maxLatencyMs});$.streamClient=j;const U=w(T,j,s.taskId,{releaseStream:Te});if(A(U),E(),$.setupPromise=null,"outbound"===k||"bidirectional"===k){const e=a?.subscribeGraceMs??1e3;e>0&&await new Promise(t=>setTimeout(t,e))}return b&&($.activated=!0,$.activatePromise=S(T,U,b,Ce)),U}};let T;try{T=t.handler?await t.handler(s,I):void 0}finally{he.delete(s.taskId),y&&(clearTimeout(y),y=null),b()}const C=B?{baseUrl:B,authProvider:l?new h(l):void 0,agentAuth:D}:void 0;if(T?.artifacts&&T.artifacts.length>0)for(const e of T.artifacts){const t="string"==typeof e.data?Buffer.from(e.data,"utf-8"):e.data;await R(r,s.taskId,d,u,t,e.mimeType,e.fileName,e.outputId,C)}const $=c.signal.aborted;if(m){const e=pe.has(s.taskId),t=ke.has(s.taskId);if($||e||t){await Se(s.taskId);const a=$&&!e?"canceled":"completed";await M(r,s.taskId,d,u,{type:"terminal",taskId:s.taskId,state:a,...e?{completionReason:"duration_expired"}:{},...t?{reason:"terminated"}:{}}),de.remove(s.taskId),N("info",`Task ${s.taskId} ${a}`,{taskId:s.taskId,agentName:u,owner:i})}else N("info",`Task ${s.taskId} handler returned (pipe, no auto-terminal)`,{taskId:s.taskId,agentName:u,owner:i})}else{const e=ke.has(s.taskId);await Se(s.taskId);const t=$?"canceled":"completed";await M(r,s.taskId,d,u,{type:"terminal",taskId:s.taskId,state:t,...e?{reason:"terminated"}:{}}),de.remove(s.taskId),N("info",`Task ${s.taskId} ${t}`,{taskId:s.taskId,agentName:u,owner:i})}},_e=(()=>{let e=!1;return(t,a,n)=>{if(N("debug","pamDeniedHandler invoked",{event:"pam_handler_invoked",category:t,operation:a,statusCode:n,alreadyHandled:e,instanceId:c}),"PNAccessDeniedCategory"===t&&!e){e=!0,N("error","PAM access denied — destroying control client (agent will go silent)",{event:"pam_denied_destroy",category:t,operation:a,statusCode:n,instanceId:c,controlChannel:Ie}),N("error","PAM token expired or revoked — agent is no longer receiving tasks. Re-register the agent to resume.",{event:"pam_denied_user_message",instanceId:c});try{W.destroy()}catch{}}}})(),Ae={status:e=>{const t=e,a=String(t.category??""),n=String(t.operation??""),s="number"==typeof t.statusCode?t.statusCode:null;"PNConnectedCategory"===a&&N("debug","pubnub control client connected",{event:"control_connected",controlChannel:Ie}),_e(a,n,s)},message:e=>{const n=e.message;if(!n)return;if("SwitchEnvironment"===n.type){const e=n,t=e.environment;return void(t&&t!==U&&t in f&&((e,t)=>{if(N("info",`SwitchEnvironment requested: ${U} -> ${e}`,{pamToken:t?"present":"absent"}),!t)return void N("error","SwitchEnvironment rejected: pamToken is required but was absent. The backend must include a pamToken in SwitchEnvironment messages.");const a=W,n=Z;try{a.removeListener(Ae),Ie&&a.unsubscribe({channels:[Ie]})}catch{}Y(a);const r=f[e];if(W=s({publishKey:r.publishKey,subscribeKey:r.subscribeKey,userId:c,presenceTimeout:20,announceSuccessfulHeartbeats:k()>=g.debug,subscribeRetryUnbounded:!0,onRetry:e=>{N("warn","pubnub transport retrying",{event:"pubnub_transport_retry",message:e,instanceId:c})}}),Z=!0,be=void 0,W.setToken(t),0!==ae&&"function"==typeof W.setFilterExpression&&W.setFilterExpression(`meta.instance == '${c}' || meta.broadcast == "true"`),W.addListener(Ae),G("control",W),Ie&&W.subscribe({channels:[Ie]}),n)try{a.destroy()}catch{}U=e,J=r.subscribeKey,Q=r.publishKey,ee.updateKeys(r.subscribeKey,r.publishKey),N("info",`Switched to ${e} environment`),we().catch(()=>{})})(t,e.pamToken))}(async(e,n)=>{if("StartTask"===e.type){const r=U,o=((e,t)=>{if("string"==typeof e&&e.length>0)return e;const a=t?.sub;return"string"==typeof a&&a.length>0?a:"anonymous"})(e.ownerId,e.callerClaims),i=e.orgId??o;if(ye.has(e.taskId))return void N("warn",`Ignoring duplicate StartTask for in-flight task ${e.taskId}`,{taskId:e.taskId});const d=Boolean(n?.broadcast);if(e.protocolVersion&&!x(e.protocolVersion))return d?void N("debug",`Ignoring broadcast StartTask ${e.taskId} with unsupported protocolVersion ${e.protocolVersion}`,{taskId:e.taskId}):(await M(W,e.taskId,i,a,{type:"terminal",taskId:e.taskId,state:"failed",error:"unsupported_protocol_version"},e.protocolVersion),void N("warn",`Task ${e.taskId} rejected: unsupported protocolVersion ${e.protocolVersion}`,{taskId:e.taskId,instanceId:c}));if("pipe"===(e.taskKind||"request")){const t=e.duration,n=e.durationExpiresAtMs;if(null==t||!Number.isInteger(t)||t<1||t>43200||"number"!=typeof n||n<=0)return N("error",`Task ${e.taskId} rejected: pipe StartTask with invalid duration (duration=${t}, durationExpiresAtMs=${n})`,{taskId:e.taskId,instanceId:c}),void await M(W,e.taskId,i,a,{type:"terminal",taskId:e.taskId,state:"failed",error:"invalid_start_task"},e.protocolVersion)}const l=oe+ie.activeStreamCount;if(te>0&&l>=te)return d?void N("debug",`Task ${e.taskId} skipped (broadcast, at capacity ${l}/${te})`,{taskId:e.taskId,instanceId:c}):(await M(W,e.taskId,i,a,{type:"terminal",taskId:e.taskId,state:"failed",error:"agent_at_capacity"}),void N("warn",`Task ${e.taskId} rejected: agent at capacity`,{taskId:e.taskId,instanceId:c,activeTasks:oe}));le.set(e.taskId,o),ue.set(e.taskId,i),ye.add(e.taskId),oe++,e.controlToken&&(be=e.controlToken);const u=new AbortController;fe.set(e.taskId,u);let m=null;try{const n=f[r];if(!n.publishKey||!n.subscribeKey)throw new Error("Per-task PubNub client requires valid pub/sub keys. When using an injected PubNub client, ensure it was created with publishKey and subscribeKey.");m=s({publishKey:n.publishKey,subscribeKey:n.subscribeKey,userId:c,subscribeRetryUnbounded:!1}),e.writeToken&&m.setToken(e.writeToken),G(`task-sub:${e.taskId}`,m),de.set(e.taskId,{ownerId:o,orgId:i,writeToken:e.writeToken??"",agentName:a,environment:r}),await we();const{writeToken:d,controlToken:l,...p}=e;t.onStartTask?await t.onStartTask(e,m):await Ke(p,m,o,i,u)}catch(n){const s=n?.message??"Agent instance error",r=m??W;try{await Se(e.taskId)}catch{}await M(r,e.taskId,i,a,{type:"terminal",taskId:e.taskId,state:"failed",error:s}),t.onError&&await t.onError(e.taskId,n),N("error",s,{taskId:e.taskId}),de.remove(e.taskId)}finally{const t=ge.get(e.taskId);if(t&&(clearTimeout(t),ge.delete(e.taskId)),ye.delete(e.taskId),le.delete(e.taskId),ue.delete(e.taskId),me.delete(e.taskId),fe.delete(e.taskId),pe.delete(e.taskId),ke.delete(e.taskId),m){Y(m);try{m.destroy()}catch{}}oe--,0===oe&&be&&W.setToken(be),await we()}}else if("CancelTask"===e.type){const t=fe.get(e.taskId);t?(t.abort(),N("info",`Task ${e.taskId} cancel requested (cooperative)`,{taskId:e.taskId,instanceId:c})):de.get(e.taskId)&&await $e(e.taskId,{state:"canceled"})}else if("ExpireTask"===e.type)fe.has(e.taskId)?(pe.add(e.taskId),fe.get(e.taskId)?.abort(),N("info",`Task ${e.taskId} expired`,{taskId:e.taskId,instanceId:c})):de.get(e.taskId)&&await $e(e.taskId,{state:"completed",completionReason:"duration_expired"});else if("TerminateTask"===e.type)fe.has(e.taskId)?(ke.add(e.taskId),fe.get(e.taskId)?.abort()):de.get(e.taskId)&&await $e(e.taskId,{state:"canceled",reason:"terminated"});else if("PauseTask"===e.type){if(ye.has(e.taskId)){const t=ue.get(e.taskId)??"anonymous";await M(W,e.taskId,t,a,{type:"system",taskId:e.taskId,status:"paused"})}}else if("ResumeTask"===e.type){if(ye.has(e.taskId)){const t=ue.get(e.taskId)??"anonymous";await M(W,e.taskId,t,a,{type:"system",taskId:e.taskId,status:"resumed"})}}else"RetryTask"===e.type&&N("info","RetryTask received (no-op)",{taskId:e.taskId})})(n,e.userMetadata&&"object"==typeof e.userMetadata?e.userMetadata:void 0).catch(e=>{N("error","unhandled error in message handler",{event:"message_handler_error",error:e instanceof Error?e.message:String(e),stack:e instanceof Error?e.stack:void 0})})}};W.addListener(Ae),G("control",W),H&&null===z&&(z=setInterval(()=>{const e=Date.now();let t=!1;const a=O.map(a=>{const n=null!==a.lastStatusAt?e-a.lastStatusAt:null,s=_isDiagEntryStale({lastStatusAt:a.lastStatusAt,lastCategory:a.lastCategory,now:e,thresholdMs:6e4});s&&(t=!0);const r="function"==typeof a.pn.getSubscribedChannels?a.pn.getSubscribedChannels().length:null;return{client:a.label,lastCategory:a.lastCategory,lastOperation:a.lastOperation,lastStatusCode:a.lastStatusCode,msSinceStatus:n,msSinceConnected:null!==a.lastConnectedAt?e-a.lastConnectedAt:null,msSinceMessage:null!==a.lastMessageAt?e-a.lastMessageAt:null,subscribedChannels:r,stale:s}}),n={event:"pubnub_alive_snapshot",instanceId:c,clients:a};t?N("info","pubnub alive snapshot — STALE clients present",n):N("debug","pubnub alive snapshot",n)},1e4),"function"==typeof z.unref&&z.unref()),H&&N("info","pubnub diagnostics armed",{event:"pubnub_diagnostics_armed",snapshotIntervalMs:1e4,staleThresholdMs:6e4,instanceId:c}),i(a,{instanceId:c,billingMode:L,description:t.description,scaling:{expectedInstances:ae,concurrency:te,maxPendingBacklog:ne,maxRunningTimeSec:se},card:t.card,cardRef:t.cardRef,cardSummary:t.cardSummary,listing:F??t.listing,actor:`agent-instance:${c}`,baseUrl:t.baseUrl??y?.api.baseUrl,agentAuth:D}).then(e=>{if(N("info",`registered agent: ${a} (instance: ${c})`,{event:"agent_registered",agentName:a,instanceId:c}),!e.controlChannel)throw new Error("Connect response missing controlChannel — server may be outdated");Ie=e.controlChannel,e.pamToken&&W.setToken(e.pamToken),0!==ae&&"function"==typeof W.setFilterExpression&&W.setFilterExpression(`meta.instance == '${c}' || meta.broadcast == "true"`),W.subscribe({channels:[Ie]}),we().catch(()=>{})}).catch(e=>{const t=e instanceof Error?e.message:String(e);N("error",`failed to register agent: ${a} — ${t}`,{event:"agent_registration_failed",agentName:a,instanceId:c,error:t}),e instanceof _&&process.exit(1)}),N("info",`Agent instance ${c} started (agent name: ${a})`,{agentName:a,event:"agent_instance_started",instanceId:c});return{stop:()=>{try{null!==z&&(clearInterval(z),z=null);for(const e of O)try{e.pn.removeListener(e.listener)}catch{}O.length=0,X?.destroy(),ee.destroy(),W.removeListener(Ae),Ie&&W.unsubscribe({channels:[Ie]}),Z&&W.destroy();for(const e of ie.streamIds()){const t=ie.get(e);if(t?.streamClient)try{t.streamClient.end()}catch{}}ie.clear(),ce.clear(),de.clear()}catch{}},agentName:a,instanceId:c,get controlChannel(){return Ie},get pubnub(){return W},get subscribeKey(){return f[U].subscribeKey},taskClient:ee,publishTerminal:$e,failStream:Ce,cdmConfig:y}};export const artifactUtils={buildArtifactRef:t,shouldInlineArtifact:a};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{CURRENT_PROTOCOL_VERSION as
|
|
1
|
+
import{log as e}from"./logger.js";import{CURRENT_PROTOCOL_VERSION as t,PROTOCOL_VERSION_HEADER as i}from"./protocol-version.js";import{captureAffinity as r,injectAffinity as s}from"./write-affinity.js";const n="API_KEY_INVALID";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._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}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",[i]:t};s(n);const o=await fetch(e,{method:"POST",headers:n,body:JSON.stringify({apiKey:this._apiKey})});if(r(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._token=h.accessToken,this._refreshToken=h.refreshToken,this._expiresIn=h.expiresIn,this._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 i=await fetch(e,{method:"POST",headers:t,body:JSON.stringify({refreshToken:this._refreshToken})});if(r(i.headers),!i.ok){const e=await i.json().catch(()=>({}));if("REFRESH_TOKEN_INVALID"===e.code)return void await this._initApiKey();if(e.code===n)throw new Error(`API key invalid or revoked: ${e.error??n}`);throw new Error(`token refresh failed: ${e.error??`HTTP ${i.status}`}`)}const o=await i.json();this._token=o.accessToken,this._refreshToken=o.refreshToken,"number"==typeof o.expiresIn&&(this._expiresIn=o.expiresIn)}async _initTokenEndpoint(){const e=await this._fetchTokenEndpoint();this._applyTokenResult(e)}async _fetchTokenEndpoint(){const e=this._tokenEndpoint,t="string"==typeof e,i=t?e:e.url;if("string"!=typeof i||!i)throw new Error("TokenEndpointConfig.url is required and must be a non-empty string");const r=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!==r&&(o.credentials=r);const h=await fetch(i,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)}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(()=>!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(t){var i,r,s;if(!this._destroyed)try{await this._refreshOnce(),this._scheduleRefresh()}catch(n){if(t<2){const e=Math.min(5e3*Math.pow(2,t),3e4);this._destroyed||(this._refreshTimer=setTimeout(()=>{this._refreshTimer=null,this._proactiveRefreshWithRetry(t+1)},e))}else{const t=n instanceof Error?n:new Error(String(n));this._onAuthError?this._onAuthError(t):(i="warn",r="proactive refresh permanently failed",s={event:"consumer_auth_proactive_refresh_failed",error:t.message},e("[ConsumerAuth]",i,r,s))}}}}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/** Numeric precedence for LOG_LEVEL threshold filtering. */
|
|
2
|
+
export declare const _LOG_LEVEL_ORDER: Record<string, number>;
|
|
3
|
+
/** Resolve the effective LOG_LEVEL from env (default: "info"). */
|
|
4
|
+
export declare function _resolveLogLevel(): number;
|
|
5
|
+
/**
|
|
6
|
+
* True iff the caller has opted into SDK-internal diagnostics output
|
|
7
|
+
* (currently: the BLOCKS-129 PubNub connectivity-diagnostics surface —
|
|
8
|
+
* `pubnub_diagnostics_armed`, status transitions, alive snapshots).
|
|
9
|
+
* Two equivalent opt-ins:
|
|
10
|
+
*
|
|
11
|
+
* BLOCKS_DEBUG_INTERNAL=1 (explicit subsystem toggle)
|
|
12
|
+
* LOG_LEVEL=debug (general SDK debug — implies internal too)
|
|
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.
|
|
17
|
+
*/
|
|
18
|
+
export declare function isInternalDebugEnabled(): boolean;
|
|
19
|
+
/**
|
|
20
|
+
* Route a tagged, LOG_LEVEL-filtered log entry to console.error / warn / log.
|
|
21
|
+
*
|
|
22
|
+
* Tag is required (e.g. `'[AgentInstance]'`, `'[StreamClient]'`) so call
|
|
23
|
+
* sites identify which subsystem emitted the line without relying on
|
|
24
|
+
* filename + line number. Entries below the configured LOG_LEVEL threshold
|
|
25
|
+
* are dropped silently.
|
|
26
|
+
*/
|
|
27
|
+
export declare function log(tag: string, level: 'debug' | 'info' | 'warn' | 'error', message: string, meta?: Record<string, unknown>): void;
|
|
@@ -0,0 +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 o=(e("LOG_LEVEL")??"info").toLowerCase();return _LOG_LEVEL_ORDER[o]??_LOG_LEVEL_ORDER.info}export function isInternalDebugEnabled(){const o=e("BLOCKS_DEBUG_INTERNAL");if(void 0!==o){const e=o.trim().toLowerCase();if(e&&"0"!==e&&"false"!==e)return!0}return _resolveLogLevel()>=_LOG_LEVEL_ORDER.debug}export function log(e,o,r,n){const L=_resolveLogLevel();if((_LOG_LEVEL_ORDER[o]??_LOG_LEVEL_ORDER.info)>L)return;const t={level:o,message:r,ts:Date.now(),...n};"error"===o?console.error(e,t):"warn"===o?console.warn(e,t):console.log(e,t)}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export function createStreamObject(r,e,t,n){return{get streamId(){return r},get channel(){return e.channel},get isActive(){return e.isActive},get external(){return!1},get uuid(){return e.uuid},write(r){e.write(r)},async end(){n&&t?await n.releaseStream(r,t):await e.end()},get inbound(){return e.inbound},bytes:()=>e.bytes(),events:()=>e.events(),readable:()=>e.readable(),onEnd(r){e.onEnd(r)},onError(r){e.onError(r)}}}export function createExternalStreamObject(r,e,t,n){let a=!1;const o=[];return{get streamId(){return r},get channel(){return e},get isActive(){return!a},get external(){return!0},get uuid(){throw new Error("Cannot read uuid on an external stream")},write(){throw new Error("Cannot write to an external stream -- use Stream SDK directly")},async end(){a=!0;for(const r of o)r();o.length=0},get inbound(){throw new Error("Cannot read from an external stream")},bytes(){throw new Error("Cannot read from an external stream")},events(){throw new Error("Cannot read from an external stream")},readable(){throw new Error("Cannot read from an external stream")},onEnd(r){o.push(r)},onError(r){throw new Error("Cannot subscribe to errors on an external stream")},get token(){return t},activate:n}}export function runOnActivate(r,
|
|
1
|
+
import{log as r}from"./logger.js";const e=(e,t,n)=>r("[StreamContext]",e,t,n);export function createStreamObject(r,e,t,n){return{get streamId(){return r},get channel(){return e.channel},get isActive(){return e.isActive},get external(){return!1},get uuid(){return e.uuid},write(r){e.write(r)},async end(){n&&t?await n.releaseStream(r,t):await e.end()},get inbound(){return e.inbound},bytes:()=>e.bytes(),events:()=>e.events(),readable:()=>e.readable(),onEnd(r){e.onEnd(r)},onError(r){e.onError(r)}}}export function createExternalStreamObject(r,e,t,n){let a=!1;const o=[];return{get streamId(){return r},get channel(){return e},get isActive(){return!a},get external(){return!0},get uuid(){throw new Error("Cannot read uuid on an external stream")},write(){throw new Error("Cannot write to an external stream -- use Stream SDK directly")},async end(){a=!0;for(const r of o)r();o.length=0},get inbound(){throw new Error("Cannot read from an external stream")},bytes(){throw new Error("Cannot read from an external stream")},events(){throw new Error("Cannot read from an external stream")},readable(){throw new Error("Cannot read from an external stream")},onEnd(r){o.push(r)},onError(r){throw new Error("Cannot subscribe to errors on an external stream")},get token(){return t},activate:n}}export function runOnActivate(r,t,n,a){return Promise.resolve().then(()=>n(t)).catch(async t=>{const n=t instanceof Error?t.message:String(t);e("error",`onActivate error for stream "${r}"`,{event:"stream_context_on_activate_error",streamId:r,error:n});try{await a(r,"stream_crashed")}catch(t){e("error",`failStream error for "${r}"`,{event:"stream_context_fail_stream_error",streamId:r,error:t instanceof Error?t.message:String(t)})}})}
|
|
@@ -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{StreamRef as h}from"./stream-ref.js";import{invertDirection as p}from"../stream/index.js";import{asPubNubFetcher as f}from"./pubnub-types.js";import{CURRENT_PROTOCOL_VERSION as m,PROTOCOL_VERSION_HEADER as g}from"./protocol-version.js";import{getAgent as k}from"./agent-registry.js";const y=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 w(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=p(o),u=a.format;if(!u||"bytes"!==u&&"events"!==u)continue;const d=a.affinity;if("dedicated"!==d&&"shared"!==d){console.warn(`[TaskClient] history-preload: dropping stream "${e}" for task "${t}" — invalid or missing affinity (got ${JSON.stringify(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 h(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 T(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 k(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",[g]:m},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",[g]:m};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 m=await t(this.config,"SendMessage",h),g=m.extensions?.blocks,k=g?.readToken??null,S=g?.streamChannels?.status??void 0,I=g?.subscribeKey??this._subscribeKey,C=g?.publishKey??this._publishKey;if(!0===m.idempotent&&!!m.state&&y.has(m.state))return new s({taskId:m.taskId,ownerId:i,orgId:m.orgId??i,readToken:k,statusChannel:S,agentName:e.agentName,pubnub:null,ownsSubscribeClient:!1,sdkOptions:{subscribeKey:I,publishKey:C},rpcConfig:this.config,idempotent:m.idempotent,queued:m.queued,pushConfigId:m.pushConfigId,preClosed:!0,state:m.state});const P=this.createPerSessionPubNub(k,I,C),K={subscribeKey:I,publishKey:C},N=S??n(m.taskId,m.orgId??i);try{const t=await P.time(),n=String(t.timetoken),r=f(P);let o,a=new Map,c=[],u="0";if(r?.fetchMessages){const t=w(await T(r,N),m.taskId,e.agentName,K);a=t.streams,c=t.artifacts,u=t.highWaterMark,o=t.terminalState}if(o&&y.has(o))return new s({taskId:m.taskId,ownerId:i,orgId:m.orgId??i,readToken:k,statusChannel:N,agentName:e.agentName,pubnub:P,ownsSubscribeClient:!0,sdkOptions:K,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:n,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})}};P.addListener(p),P.subscribe({channels:[N],timetoken:d});const g=new s({taskId:m.taskId,ownerId:i,orgId:m.orgId??i,readToken:k,statusChannel:N,agentName:e.agentName,pubnub:P,ownsSubscribeClient:!0,sdkOptions:K,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,g}catch{return new s({taskId:m.taskId,ownerId:i,orgId:m.orgId??i,readToken:k,statusChannel:N,agentName:e.agentName,pubnub:P,ownsSubscribeClient:!0,sdkOptions:K,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(!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=f(b);let p,m=new Map,g=[],k=[],S="0";if(h?.fetchMessages){const e=w(await T(h,d),c,n,l);m=e.streams,g=e.artifacts,k=e.events,S=e.highWaterMark,p=e.terminalState}const I=p&&y.has(p)?p:i;if(y.has(I))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:I,skipSubscription:!0,preloadedStreams:m,preloadedArtifacts:g,preloadedEvents:k});const C="0"!==S?S:r,P=[];let K,N=!1;const E={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");N&&K?K(t,n):P.push({message:t,timetoken:n})}};b.addListener(E),b.subscribe({channels:[d],timetoken:C});const _=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:m,preloadedArtifacts:g,preloadedEvents:k,externalSubscription:{listener:E,channel:d,onReady:e=>{K=e}}});if(K){const e=K;for(const t of P)t.timetoken>C&&(_._appendHistoryEvent(t.message,t.timetoken),e(t.message,t.timetoken))}return P.length=0,N=!0,_}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){S(e,"onEvent",t,i.onError)}switch(t.type){case"progress":if(i.onProgress)try{i.onProgress(t)}catch(e){S(e,"onProgress",t,i.onError)}break;case"artifact":if(i.onArtifact)try{i.onArtifact(t)}catch(e){S(e,"onArtifact",t,i.onError)}break;case"terminal":if(i.onTerminal)try{i.onTerminal(t)}catch(e){S(e,"onTerminal",t,i.onError)}break;case"system":if(i.onSystem)try{i.onSystem(t)}catch(e){S(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 S(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 console.warn(`[subscribeToTask] callback error in ${t}:`,i.message)}
|
|
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})}
|
|
@@ -163,7 +163,7 @@ export declare class TaskSession {
|
|
|
163
163
|
onEvent(cb: (event: TaskEvent) => void): Unsubscribe;
|
|
164
164
|
/**
|
|
165
165
|
* Register an error handler for callback exceptions.
|
|
166
|
-
* If registered, callback errors are routed here instead of
|
|
166
|
+
* If registered, callback errors are routed here instead of the SDK logger.
|
|
167
167
|
* Returns an unsubscribe function.
|
|
168
168
|
*/
|
|
169
169
|
onError(cb: (error: Error, context: CallbackErrorContext) => void): Unsubscribe;
|
|
@@ -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";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 console.warn(`[TaskSession] callback error in ${e}:`,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),o=a.format;if(!o||"bytes"!==o&&"events"!==o)continue;const c=a.affinity;if("dedicated"!==c&&"shared"!==c){console.warn(`[TaskSession] live stream_started: dropping stream "${t}" for task "${this.taskId}" — invalid or missing affinity (got ${JSON.stringify(a.affinity)})`);continue}const l={taskId:this.taskId,streamId:t,agentName:this.agentName,channel:a.channel,token:a.token,agentDirection:i,localDirection:n,format:o,affinity:c,metadata:a.metadata,declaredStream:r},h=new s(l,this.sdkOptions,{onOpen:t=>this.trackStreamClient(t),sessionState:()=>this.state});this.streams.set(t,h);for(const t of this.streamCallbacks)try{t(h)}catch(t){this.routeCallbackError(t,"onStream",h)}this.resolveWaiters(h)}}startAutoDrain(){this.closed||(this.terminalReceived=!0,0!==this.openStreamClients.size?this.drainTimer=setTimeout(()=>{this.drainTimer=null;for(const t of this.openStreamClients)t.isActive&&t.end().catch(()=>{});this.close()},this.drainWindowMs):this.close())}findStreamByIdOrDeclared(t){const e=this.streams.get(t);if(e)return e;for(const e of this.streams.values())if(e.descriptor.declaredStream===t)return e}resolveWaiters(t){const e=[];for(const s of this.streamWaiters){let 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{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 +1 @@
|
|
|
1
|
-
import{utf8ByteLength as t,utf8Encode as e,bytesToBase64 as s}from"./bytes.js";import{CURRENT_PROTOCOL_VERSION as i}from"../runtime/protocol-version.js";export class StreamBundle{constructor(t,e,s,i,n,
|
|
1
|
+
import{utf8ByteLength as t,utf8Encode as e,bytesToBase64 as s}from"./bytes.js";import{CURRENT_PROTOCOL_VERSION as i}from"../runtime/protocol-version.js";import{log as n}from"../runtime/logger.js";export class StreamBundle{constructor(t,e,s,i,n,r){if(this.occupancy=0,this.presenceListener=null,this.buffer=[],this.bufferBytes=0,this.currentBatchHasBinary=!1,this.eventBuffer=[],this.eventBufferSize=0,this.closed=!1,this.flushTimer=null,this.inflightFlushes=[],this.pubnub=t,this.channel=e,this.streamId=s,this.format=i,n.maxMessageSize<=512)throw new Error(`maxMessageSize (${n.maxMessageSize}) must be greater than ENVELOPE_RESERVE (512)`);this.maxMessageSize=n.maxMessageSize,this.bundleSizeBytes=n.bundleSizeBytes,this.maxLatencyMs=n.maxLatencyMs,this.uuid=n.uuid,this.gated=r,this.seq="events"===i?1:0}get isActive(){return!this.closed}get consumerCount(){return this.gated?this.occupancy:0}write(t){if(this.closed)throw new Error("Cannot write to a closed stream");"events"===this.format?this.writeEvent(t):this.writeBytes(t)}async publishEndMarker(){const t={type:"stream_end",streamId:this.streamId,seq:this.seq++,ts:Date.now(),protocolVersion:i};await this.publishMessage(t).catch(()=>{})}async end(){this.closed||(this.closed=!0,this.clearTimer(),this.inflightFlushes.length>0&&(await Promise.all(this.inflightFlushes),this.inflightFlushes=[]),"events"===this.format?await this.flushEvents():await this.flush(),this.teardownPresenceGating(),await(this.onEnd?.()))}setupPresenceGating(){const t=this.channel+"-pnpres";this.presenceListener={message:e=>{e.channel===t&&"number"==typeof e.message?.occupancy&&(this.occupancy=e.message.occupancy)}},this.pubnub.addListener(this.presenceListener),this.pubnub.subscribe({channels:[t]}),this.pubnub.hereNow({channels:[this.channel]}).then(t=>{const e=t.channels?.[this.channel];e&&"number"==typeof e.occupancy&&(this.occupancy=e.occupancy)}).catch(()=>{})}teardownPresenceGating(){this.presenceListener&&(this.pubnub.removeListener(this.presenceListener),this.presenceListener=null),this.gated&&this.pubnub.unsubscribe({channels:[this.channel+"-pnpres"]})}writeBytes(e){let i;"string"==typeof e?i=e:e instanceof Uint8Array?(i=s(e),this.currentBatchHasBinary=!0):i=String(e);const n=t(i);this.buffer.push(i),this.bufferBytes+=n,this.bufferBytes>=this.bundleSizeBytes?this.trackFlush(this.flush()):null===this.flushTimer&&(this.flushTimer=setTimeout(()=>{this.flushTimer=null,this.trackFlush(this.flush())},this.maxLatencyMs))}async flush(){if(0===this.buffer.length)return;this.clearTimer();const e=this.buffer,s=this.currentBatchHasBinary;this.buffer=[],this.bufferBytes=0,this.currentBatchHasBinary=!1;const n={type:"stream_data",streamId:this.streamId,seq:this.seq++,ts:Date.now(),encoding:s?"base64":"utf8",chunks:e,protocolVersion:i},r=JSON.stringify(n);t(r)>this.maxMessageSize?await this.publishMultipart(n):await this.publishMessage(n)}writeEvent(e){if("string"==typeof e)throw new Error('write() does not accept raw strings in format: "events". Pass an object (e.g., { text: "..." }) or use format: "bytes".');e instanceof Uint8Array?(this.eventBuffer.push({$binary:s(e)}),this.eventBufferSize+=Math.ceil(4*e.length/3)+20):(this.eventBuffer.push(e),this.eventBufferSize+=t(JSON.stringify(e))),this.eventBufferSize>=this.bundleSizeBytes?this.trackFlush(this.flushEvents()):null===this.flushTimer&&(this.flushTimer=setTimeout(()=>{this.flushTimer=null,this.trackFlush(this.flushEvents())},this.maxLatencyMs))}async flushEvents(){if(0===this.eventBuffer.length)return;this.clearTimer();const e=this.eventBuffer;this.eventBuffer=[],this.eventBufferSize=0;const s={type:"stream_events",streamId:this.streamId,seq:this.seq++,ts:Date.now(),encoding:"utf8",events:e,protocolVersion:i},n=JSON.stringify(s);t(n)>this.maxMessageSize?await this.publishMultipart(s):await this.publishMessage(s)}trackFlush(t){this.inflightFlushes.push(t),t.then(()=>{const e=this.inflightFlushes.indexOf(t);e>=0&&this.inflightFlushes.splice(e,1)})}async publishMessage(t){let e=null;for(let s=1;s<=3;s++)try{return void await this.pubnub.publish({channel:this.channel,message:t,meta:{sender:this.uuid,protocolVersion:i},storeInHistory:!1,sendByPost:!0})}catch(t){if(e=t,s<3){const t=100*Math.pow(2,s-1);await new Promise(e=>setTimeout(e,t))}}((t,e,s)=>{n("[StreamBundle]",t,e,s)})("error","failed to publish stream message after 3 attempts",{event:"stream_bundle_publish_failed_after_retries",maxAttempts:3,error:e instanceof Error?e.message:String(e)})}async publishMultipart(t){const n=JSON.stringify(t),r=e(n),h=Math.floor(3*(this.maxMessageSize-512)/4),a=Math.ceil(r.length/h),u=`mp-${Date.now()}-${t.seq}`,l=[];for(let e=0;e<a;e++){const n=r.subarray(e*h,(e+1)*h),c={type:t.type,streamId:this.streamId,seq:t.seq,ts:t.ts,multipart:{id:u,part:e+1,total:a},data:s(n),protocolVersion:i};l.push(()=>this.publishMessage(c))}await async function(t,e){const s=new Array(t.length);let i=0;const n=Math.min(e,t.length),r=Array.from({length:n},async()=>{for(;;){const e=i++;if(e>=t.length)return;s[e]=await t[e]()}});return await Promise.all(r),s}(l,4)}clearTimer(){null!==this.flushTimer&&(clearTimeout(this.flushTimer),this.flushTimer=null)}}
|
|
@@ -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{base64ToBytes as n,concatBytes as s,utf8Decode as o,utf8Encode as a}from"./bytes.js";let u=0;export function _resetUuidCounter(){u=0}function d(e,t,r,n){if(void 0!==e)return e;const s=i(t);return void 0!==s&&""!==s?n(s):r}function h(e){return Number(e)}function l(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:(console.warn("[StreamClient] 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,u++,this._uuid=`${n}-stream-${String(u).padStart(4,"0")}`,this._channel=i.channel??`stream.${n}.${i.streamId}`;const o=d(i.maxMessageSize,"STREAM_MAX_MESSAGE_SIZE",16384,h),a=d(i.bundleSizeBytes,"STREAM_BUNDLE_SIZE",4096,h),c=d(i.maxLatencyMs,"STREAM_MAX_LATENCY_MS",250,h),f=d(i.gating,"STREAM_GATING",!0,l);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:c,uuid:this._uuid};this.bundle=new t(this.pubnub,this._channel,i.streamId,this.format,e,f)}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){console.warn("[StreamClient] bundle.end() failed during end() (continuing teardown):",e)}if("bidirectional"!==this.direction&&this.bundle&&"shared"!==this._affinity)try{await this.bundle.publishEndMarker()}catch(e){console.warn("[StreamClient] publishEndMarker() failed during end() (continuing teardown):",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){console.error("[StreamClient] onError callback raised:",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};console.warn(`[StreamClient] status error: category=${t} fatal=${r} channel=${this._channel}`,i),this.fireError(n),r&&this._isActive&&this.end().catch(e=>{console.error("[StreamClient] forced end() after fatal error raised:",e)})}catch(e){console.error("[StreamClient] status handler raised:",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 console.warn("[StreamClient] stream_end missing numeric seq field; ignoring in reorder mode");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,a=e.streamId,u=e.data;let d=this.multipartBuffers.get(t.id);if(d){if(d.total!==t.total||d.seq!==r||d.type!==i||d.streamId!==a)return void this.dropMultipartGroup(t.id);const e=d.parts.get(t.part);if(void 0!==e){if(e===u)return;return void this.dropMultipartGroup(t.id)}}else d={total:t.total,parts:new Map,seq:r,ts:e.ts,type:i,streamId:a,createdAt:Date.now()},this.multipartBuffers.set(t.id,d);if(d.parts.set(t.part,u),d.parts.size===d.total){this.dropMultipartGroup(t.id);for(let e=1;e<=d.total;e++)if(!d.parts.has(e))return;try{const e=[];for(let t=1;t<=d.total;t++)e.push(n(d.parts.get(t)));const t=s(e),r=JSON.parse(o(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&&(console.warn(`[StreamClient] reorder buffer not empty at stream end (${this.reorderBuffer.size} unexpected messages with seq >= ${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?n(r):a(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{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})}}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@blocks-network/sdk",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.61",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Blocks Network SDK",
|
|
6
6
|
"exports": {
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"LICENSE"
|
|
32
32
|
],
|
|
33
33
|
"engines": {
|
|
34
|
-
"node": ">=
|
|
34
|
+
"node": ">=22.0.0"
|
|
35
35
|
},
|
|
36
36
|
"scripts": {
|
|
37
37
|
"build": "tsc",
|