@spooled/sdk 1.0.32 → 1.0.33

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.
@@ -107,11 +107,11 @@ declare const DEFAULT_CONFIG: {
107
107
  successThreshold: number;
108
108
  timeout: number;
109
109
  };
110
- readonly userAgent: "@spooled/sdk-nodejs/1.0.31";
110
+ readonly userAgent: "@spooled/sdk-nodejs/1.0.33";
111
111
  readonly autoRefreshToken: true;
112
112
  };
113
113
  /** SDK version */
114
- declare const SDK_VERSION = "1.0.31";
114
+ declare const SDK_VERSION = "1.0.33";
115
115
  /** API version prefix */
116
116
  declare const API_VERSION = "v1";
117
117
  /**
@@ -2315,6 +2315,26 @@ interface JobStatusChangedEvent extends RealtimeEventBase {
2315
2315
  newStatus: JobStatus;
2316
2316
  };
2317
2317
  }
2318
+ /** Queue stats update event (backend `QueueStats`) */
2319
+ interface QueueStatsEvent extends RealtimeEventBase {
2320
+ type: 'queue.stats';
2321
+ data: {
2322
+ queueName: string;
2323
+ pending: number;
2324
+ processing: number;
2325
+ completed: number;
2326
+ failed: number;
2327
+ };
2328
+ }
2329
+ /** Worker heartbeat event (backend `WorkerHeartbeat`) */
2330
+ interface WorkerHeartbeatEvent extends RealtimeEventBase {
2331
+ type: 'worker.heartbeat';
2332
+ data: {
2333
+ workerId: string;
2334
+ status: string;
2335
+ currentJobId?: string;
2336
+ };
2337
+ }
2318
2338
  /** Queue paused event */
2319
2339
  interface QueuePausedEvent extends RealtimeEventBase {
2320
2340
  type: 'queue.paused';
@@ -2363,7 +2383,7 @@ interface HeartbeatEvent extends RealtimeEventBase {
2363
2383
  };
2364
2384
  }
2365
2385
  /** All realtime event types */
2366
- type RealtimeEvent = JobCreatedEvent | JobStartedEvent | JobCompletedEvent | JobFailedEvent | JobProgressEvent | JobStatusChangedEvent | QueuePausedEvent | QueueResumedEvent | WorkerRegisteredEvent | WorkerDeregisteredEvent | ScheduleTriggeredEvent | HeartbeatEvent;
2386
+ type RealtimeEvent = JobCreatedEvent | JobStartedEvent | JobCompletedEvent | JobFailedEvent | JobProgressEvent | JobStatusChangedEvent | QueueStatsEvent | QueuePausedEvent | QueueResumedEvent | WorkerRegisteredEvent | WorkerDeregisteredEvent | WorkerHeartbeatEvent | ScheduleTriggeredEvent | HeartbeatEvent;
2367
2387
  /** Event type names */
2368
2388
  type RealtimeEventType = RealtimeEvent['type'];
2369
2389
  /** Extract event data type from event type name */
@@ -107,11 +107,11 @@ declare const DEFAULT_CONFIG: {
107
107
  successThreshold: number;
108
108
  timeout: number;
109
109
  };
110
- readonly userAgent: "@spooled/sdk-nodejs/1.0.31";
110
+ readonly userAgent: "@spooled/sdk-nodejs/1.0.33";
111
111
  readonly autoRefreshToken: true;
112
112
  };
113
113
  /** SDK version */
114
- declare const SDK_VERSION = "1.0.31";
114
+ declare const SDK_VERSION = "1.0.33";
115
115
  /** API version prefix */
116
116
  declare const API_VERSION = "v1";
117
117
  /**
@@ -2315,6 +2315,26 @@ interface JobStatusChangedEvent extends RealtimeEventBase {
2315
2315
  newStatus: JobStatus;
2316
2316
  };
2317
2317
  }
2318
+ /** Queue stats update event (backend `QueueStats`) */
2319
+ interface QueueStatsEvent extends RealtimeEventBase {
2320
+ type: 'queue.stats';
2321
+ data: {
2322
+ queueName: string;
2323
+ pending: number;
2324
+ processing: number;
2325
+ completed: number;
2326
+ failed: number;
2327
+ };
2328
+ }
2329
+ /** Worker heartbeat event (backend `WorkerHeartbeat`) */
2330
+ interface WorkerHeartbeatEvent extends RealtimeEventBase {
2331
+ type: 'worker.heartbeat';
2332
+ data: {
2333
+ workerId: string;
2334
+ status: string;
2335
+ currentJobId?: string;
2336
+ };
2337
+ }
2318
2338
  /** Queue paused event */
2319
2339
  interface QueuePausedEvent extends RealtimeEventBase {
2320
2340
  type: 'queue.paused';
@@ -2363,7 +2383,7 @@ interface HeartbeatEvent extends RealtimeEventBase {
2363
2383
  };
2364
2384
  }
2365
2385
  /** All realtime event types */
2366
- type RealtimeEvent = JobCreatedEvent | JobStartedEvent | JobCompletedEvent | JobFailedEvent | JobProgressEvent | JobStatusChangedEvent | QueuePausedEvent | QueueResumedEvent | WorkerRegisteredEvent | WorkerDeregisteredEvent | ScheduleTriggeredEvent | HeartbeatEvent;
2386
+ type RealtimeEvent = JobCreatedEvent | JobStartedEvent | JobCompletedEvent | JobFailedEvent | JobProgressEvent | JobStatusChangedEvent | QueueStatsEvent | QueuePausedEvent | QueueResumedEvent | WorkerRegisteredEvent | WorkerDeregisteredEvent | WorkerHeartbeatEvent | ScheduleTriggeredEvent | HeartbeatEvent;
2367
2387
  /** Event type names */
2368
2388
  type RealtimeEventType = RealtimeEvent['type'];
2369
2389
  /** Extract event data type from event type name */
package/dist/index.cjs CHANGED
@@ -49,10 +49,10 @@ var DEFAULT_CONFIG = {
49
49
  successThreshold: 3,
50
50
  timeout: 3e4
51
51
  },
52
- userAgent: "@spooled/sdk-nodejs/1.0.31",
52
+ userAgent: "@spooled/sdk-nodejs/1.0.33",
53
53
  autoRefreshToken: true
54
54
  };
55
- var SDK_VERSION = "1.0.31";
55
+ var SDK_VERSION = "1.0.33";
56
56
  var API_VERSION = "v1";
57
57
  var API_BASE_PATH = `/api/${API_VERSION}`;
58
58
  function normalizeCredential(value) {
@@ -327,22 +327,33 @@ function parseRateLimitHeaders(headers) {
327
327
  }
328
328
  return info;
329
329
  }
330
+ var MAX_RAW_ERROR_BODY_CHARS = 500;
330
331
  async function parseErrorBody(response) {
331
332
  const contentType = response.headers.get("Content-Type") || "";
332
- if (!contentType.includes("application/json")) {
333
+ let raw;
334
+ try {
335
+ raw = await response.text();
336
+ } catch {
333
337
  return null;
334
338
  }
335
- try {
336
- const body = await response.json();
337
- if (typeof body === "object" && body !== null) {
338
- const message = (body.message ?? body.error) || response.statusText;
339
- return {
340
- code: body.code || "UNKNOWN_ERROR",
341
- message,
342
- details: body.details
343
- };
339
+ if (contentType.includes("application/json")) {
340
+ try {
341
+ const body = JSON.parse(raw);
342
+ if (typeof body === "object" && body !== null) {
343
+ const message = (body.message ?? body.error) || response.statusText;
344
+ return {
345
+ code: body.code || "UNKNOWN_ERROR",
346
+ message,
347
+ details: body.details
348
+ };
349
+ }
350
+ } catch {
344
351
  }
345
- } catch {
352
+ }
353
+ const trimmed = raw.trim();
354
+ if (trimmed) {
355
+ const message = trimmed.length > MAX_RAW_ERROR_BODY_CHARS ? `${trimmed.slice(0, MAX_RAW_ERROR_BODY_CHARS)}\u2026` : trimmed;
356
+ return { code: "UNKNOWN_ERROR", message };
346
357
  }
347
358
  return null;
348
359
  }
@@ -1848,6 +1859,24 @@ var WebhookIngestionResource = class {
1848
1859
  }
1849
1860
  };
1850
1861
 
1862
+ // src/realtime/event-map.ts
1863
+ var BACKEND_EVENT_TYPE_MAP = {
1864
+ JobStatusChange: "job.status_changed",
1865
+ JobCreated: "job.created",
1866
+ JobCompleted: "job.completed",
1867
+ JobFailed: "job.failed",
1868
+ QueueStats: "queue.stats",
1869
+ WorkerHeartbeat: "worker.heartbeat",
1870
+ WorkerRegistered: "worker.registered",
1871
+ WorkerDeregistered: "worker.deregistered",
1872
+ SystemHealth: "system.health",
1873
+ Ping: "ping",
1874
+ Error: "error"
1875
+ };
1876
+ function mapEventType(type) {
1877
+ return BACKEND_EVENT_TYPE_MAP[type] ?? type;
1878
+ }
1879
+
1851
1880
  // src/realtime/websocket.ts
1852
1881
  var WebSocketRealtimeClient = class {
1853
1882
  options;
@@ -1863,7 +1892,6 @@ var WebSocketRealtimeClient = class {
1863
1892
  */
1864
1893
  authFailure = false;
1865
1894
  subscriptions = /* @__PURE__ */ new Map();
1866
- pendingCommands = /* @__PURE__ */ new Map();
1867
1895
  // Event handlers
1868
1896
  eventHandlers = /* @__PURE__ */ new Map();
1869
1897
  allEventsHandlers = /* @__PURE__ */ new Set();
@@ -1968,10 +1996,13 @@ var WebSocketRealtimeClient = class {
1968
1996
  }
1969
1997
  this.setState("disconnected");
1970
1998
  this.subscriptions.clear();
1971
- this.clearPendingCommands();
1972
1999
  }
1973
2000
  /**
1974
- * Subscribe to events matching a filter
2001
+ * Subscribe to events matching a filter.
2002
+ *
2003
+ * The server applies filtering itself and sends no acknowledgement, so this
2004
+ * resolves as soon as the command has been written to the socket. The filter
2005
+ * is tracked locally so it is replayed automatically on reconnect.
1975
2006
  */
1976
2007
  async subscribe(filter) {
1977
2008
  const filterId = this.filterToId(filter);
@@ -1980,11 +2011,13 @@ var WebSocketRealtimeClient = class {
1980
2011
  }
1981
2012
  this.subscriptions.set(filterId, filter);
1982
2013
  if (this.state === "connected") {
1983
- await this.sendCommand({ type: "subscribe", filter });
2014
+ this.sendCommand("Subscribe", filter);
1984
2015
  }
1985
2016
  }
1986
2017
  /**
1987
- * Unsubscribe from events matching a filter
2018
+ * Unsubscribe from events matching a filter.
2019
+ *
2020
+ * Fire-and-forget, like {@link subscribe} — the server sends no ack.
1988
2021
  */
1989
2022
  async unsubscribe(filter) {
1990
2023
  const filterId = this.filterToId(filter);
@@ -1993,7 +2026,7 @@ var WebSocketRealtimeClient = class {
1993
2026
  }
1994
2027
  this.subscriptions.delete(filterId);
1995
2028
  if (this.state === "connected") {
1996
- await this.sendCommand({ type: "unsubscribe", filter });
2029
+ this.sendCommand("Unsubscribe", filter);
1997
2030
  }
1998
2031
  }
1999
2032
  /**
@@ -2046,13 +2079,11 @@ var WebSocketRealtimeClient = class {
2046
2079
  handleMessage(data) {
2047
2080
  try {
2048
2081
  const message = convertResponse(JSON.parse(data));
2049
- if (message.type === "subscribed" || message.type === "unsubscribed" || message.type === "error") {
2050
- this.handleCommandResponse(message);
2051
- return;
2052
- }
2082
+ const eventType = mapEventType(String(message.type));
2083
+ message.type = eventType;
2053
2084
  const event = message;
2054
- this.options.debug(`Received event: ${event.type}`, event);
2055
- const handlers = this.eventHandlers.get(event.type);
2085
+ this.options.debug(`Received event: ${eventType}`, event);
2086
+ const handlers = this.eventHandlers.get(eventType);
2056
2087
  if (handlers) {
2057
2088
  const eventData = event.data;
2058
2089
  handlers.forEach((handler) => {
@@ -2074,25 +2105,8 @@ var WebSocketRealtimeClient = class {
2074
2105
  this.options.debug("Failed to parse message", { data, error });
2075
2106
  }
2076
2107
  }
2077
- handleCommandResponse(response) {
2078
- if (!response.requestId) {
2079
- return;
2080
- }
2081
- const pending = this.pendingCommands.get(response.requestId);
2082
- if (!pending) {
2083
- return;
2084
- }
2085
- clearTimeout(pending.timeout);
2086
- this.pendingCommands.delete(response.requestId);
2087
- if (response.type === "error") {
2088
- pending.reject(new Error(response.error || "Unknown error"));
2089
- } else {
2090
- pending.resolve();
2091
- }
2092
- }
2093
2108
  handleDisconnect() {
2094
2109
  this.ws = null;
2095
- this.clearPendingCommands();
2096
2110
  if (this.options.autoReconnect && this.reconnectAttempts < this.options.maxReconnectAttempts) {
2097
2111
  this.scheduleReconnect();
2098
2112
  } else {
@@ -2116,43 +2130,37 @@ var WebSocketRealtimeClient = class {
2116
2130
  }
2117
2131
  }, delay);
2118
2132
  }
2119
- async resubscribeAll() {
2133
+ resubscribeAll() {
2120
2134
  for (const filter of this.subscriptions.values()) {
2121
2135
  try {
2122
- await this.sendCommand({ type: "subscribe", filter });
2136
+ this.sendCommand("Subscribe", filter);
2123
2137
  } catch (error) {
2124
2138
  this.options.debug("Failed to resubscribe", { filter, error });
2125
2139
  }
2126
2140
  }
2127
2141
  }
2128
- async sendCommand(command) {
2142
+ /**
2143
+ * Send a subscribe/unsubscribe command to the server.
2144
+ *
2145
+ * The backend `ClientCommand` shape is `{ cmd, queue, job_id }` and it sends
2146
+ * no acknowledgement, so this is fire-and-forget — it writes to the socket
2147
+ * and returns. The `SubscriptionFilter`'s `workerId`/`scheduleId` have no
2148
+ * server-side equivalent and are ignored.
2149
+ */
2150
+ sendCommand(cmd, filter) {
2129
2151
  if (!this.ws || this.state !== "connected") {
2130
2152
  throw new Error("WebSocket not connected");
2131
2153
  }
2132
- const requestId = this.generateRequestId();
2133
- const fullCommand = { ...command, requestId };
2134
- return new Promise((resolve, reject) => {
2135
- const timeout = setTimeout(() => {
2136
- this.pendingCommands.delete(requestId);
2137
- reject(new Error("Command timeout"));
2138
- }, 1e4);
2139
- this.pendingCommands.set(requestId, { resolve, reject, timeout });
2140
- this.ws.send(JSON.stringify(fullCommand));
2141
- });
2142
- }
2143
- clearPendingCommands() {
2144
- for (const pending of this.pendingCommands.values()) {
2145
- clearTimeout(pending.timeout);
2146
- pending.reject(new Error("Connection closed"));
2147
- }
2148
- this.pendingCommands.clear();
2154
+ const command = {
2155
+ cmd,
2156
+ queue: filter.queueName,
2157
+ job_id: filter.jobId
2158
+ };
2159
+ this.ws.send(JSON.stringify(command));
2149
2160
  }
2150
2161
  filterToId(filter) {
2151
2162
  return JSON.stringify(filter);
2152
2163
  }
2153
- generateRequestId() {
2154
- return `req_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`;
2155
- }
2156
2164
  };
2157
2165
  function redactToken(url) {
2158
2166
  return url.replace(/([?&](?:token|api_key)=)[^&]*/gi, "$1***");
@@ -2351,9 +2359,12 @@ var SseRealtimeClient = class {
2351
2359
  }
2352
2360
  handleMessage(data) {
2353
2361
  try {
2354
- const event = convertResponse(JSON.parse(data));
2355
- this.options.debug(`Received SSE event: ${event.type}`, event);
2356
- const handlers = this.eventHandlers.get(event.type);
2362
+ const parsed = convertResponse(JSON.parse(data));
2363
+ const eventType = mapEventType(String(parsed.type));
2364
+ parsed.type = eventType;
2365
+ const event = parsed;
2366
+ this.options.debug(`Received SSE event: ${eventType}`, event);
2367
+ const handlers = this.eventHandlers.get(eventType);
2357
2368
  if (handlers) {
2358
2369
  const eventData = event.data;
2359
2370
  handlers.forEach((handler) => {