@spooled/sdk 1.0.35 → 1.0.37

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 CHANGED
@@ -321,7 +321,10 @@ All operations automatically enforce tier-based limits:
321
321
  - ✅ Webhook creation
322
322
 
323
323
  When a plan quota or limit is exceeded, you'll receive an `HTTP 429` response with
324
- `code: "QUOTA_EXCEEDED"` and a body describing the `resource`, `current`, `limit`, and `plan`:
324
+ `code: "QUOTA_EXCEEDED"`. The SDK reliably preserves the HTTP status, error code, message,
325
+ request ID, and rate-limit headers. `error.details` is populated only when the server nests
326
+ metadata under a JSON `details` field; quota fields sent at the top level of the server payload
327
+ (such as `resource`, `current`, `limit`, and `plan`) are not currently exposed by the SDK.
325
328
 
326
329
  ```typescript
327
330
  import { RateLimitError } from '@spooled/sdk';
@@ -330,9 +333,10 @@ try {
330
333
  await client.jobs.create({ /* ... */ });
331
334
  } catch (error) {
332
335
  if (error instanceof RateLimitError && error.code === 'QUOTA_EXCEEDED') {
333
- // Plan quota reached (HTTP 429); details carry resource/current/limit/plan
334
336
  console.log(`Plan limit: ${error.message}`);
335
- console.log('Details:', error.details);
337
+ console.log(`Request ID: ${error.requestId ?? 'not provided'}`);
338
+ // Present only if this server response included a nested `details` object.
339
+ if (error.details) console.log('Details:', error.details);
336
340
  }
337
341
  }
338
342
  ```
@@ -406,12 +410,13 @@ const { jobs } = await grpcClient.queue.dequeue({
406
410
  leaseDurationSecs: 300
407
411
  });
408
412
 
409
- // Process and complete
413
+ // Process and complete. Echo leaseId when present so current servers can fence stale executions; legacy servers may omit it and then use worker-ID-only fencing.
410
414
  for (const job of jobs) {
411
415
  await processJob(job);
412
416
  await grpcClient.queue.complete({
413
417
  jobId: job.id,
414
418
  workerId,
419
+ leaseId: job.leaseId ?? undefined,
415
420
  result: { success: true }
416
421
  });
417
422
  }
@@ -441,6 +446,46 @@ import type {
441
446
  } from '@spooled/sdk';
442
447
  ```
443
448
 
449
+ ## Development and releases
450
+
451
+ Before opening a pull request or tagging a release, run the same checks used by the package and publish workflow:
452
+
453
+ ```bash
454
+ npm ci
455
+ npm run lint
456
+ npm run typecheck
457
+ npm run build
458
+ npm test
459
+ ```
460
+
461
+ Production-safe API verification is separate and credentialed: `npm run verify:production`. Integration tests use `npm run test:integration` against a local or isolated test backend.
462
+
463
+ Maintainers release by pushing a matching `vX.Y.Z` tag. The tag workflow verifies all same-artifact version surfaces before publishing through npm Trusted Publishing (OIDC) with provenance.
464
+
465
+ ### Maintainer release checklist (advisory)
466
+
467
+ This checklist records evidence and does not block ordinary commits, experiments, or compatibility research. A mismatch among values that identify the same npm artifact is a release error and cannot be waived for publication. Publishing the SDK is not a service deployment; consumers report new metadata only after upgrading and restarting or redeploying their applications.
468
+
469
+ Before pushing `vX.Y.Z`:
470
+
471
+ - [ ] Set `package.json.version` to `X.Y.Z`.
472
+ - [ ] Regenerate `package-lock.json`; verify both top-level `version` and `packages[""].version` are `X.Y.Z`.
473
+ - [ ] Set `SDK_VERSION` and the default User-Agent in `src/config.ts` to `X.Y.Z`.
474
+ - [ ] Confirm `SpooledWorker` derives its default registration version from `SDK_VERSION`.
475
+ - [ ] Add `## [X.Y.Z] - YYYY-MM-DD` to `CHANGELOG.md` and review version-bearing worker documentation.
476
+ - [ ] Run `npm ci`, lint, typecheck, build, tests, and `npm pack --dry-run`.
477
+ - [ ] Review the packed file list and compiled output; confirm the package contains no local logs, caches, credentials, or unintended generated files.
478
+ - [ ] Record any cross-repository compatibility difference separately; backend, SDK, and application versions are not required to match numerically.
479
+
480
+ Publish and verify:
481
+
482
+ - [ ] Create one immutable `vX.Y.Z` tag on the reviewed commit and never move or recreate a pushed tag. Fix forward with a new patch version after external visibility.
483
+ - [ ] Record the tag commit and publish workflow URL.
484
+ - [ ] Verify `npm view @spooled/sdk@X.Y.Z version gitHead dist.integrity dist.attestations --json` against the tag and commit.
485
+ - [ ] Confirm `latest` points to `X.Y.Z` when intended and inspect the published tarball's `SDK_VERSION`, User-Agent, and worker registration default.
486
+ - [ ] Confirm npm provenance names this repository, `.github/workflows/publish.yml`, `refs/tags/vX.Y.Z`, and the release commit.
487
+ - [ ] Record registry publication separately from consumer rollout, backend deployment, or documentation deployment.
488
+
444
489
  ## License
445
490
 
446
491
  Apache-2.0 - see [LICENSE](LICENSE) for details.
@@ -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.35";
110
+ readonly userAgent: "@spooled/sdk-nodejs/1.0.37";
111
111
  readonly autoRefreshToken: true;
112
112
  };
113
113
  /** SDK version */
114
- declare const SDK_VERSION = "1.0.35";
114
+ declare const SDK_VERSION = "1.0.37";
115
115
  /** API version prefix */
116
116
  declare const API_VERSION = "v1";
117
117
  /**
@@ -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.35";
110
+ readonly userAgent: "@spooled/sdk-nodejs/1.0.37";
111
111
  readonly autoRefreshToken: true;
112
112
  };
113
113
  /** SDK version */
114
- declare const SDK_VERSION = "1.0.35";
114
+ declare const SDK_VERSION = "1.0.37";
115
115
  /** API version prefix */
116
116
  declare const API_VERSION = "v1";
117
117
  /**
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.35",
52
+ userAgent: "@spooled/sdk-nodejs/1.0.37",
53
53
  autoRefreshToken: true
54
54
  };
55
- var SDK_VERSION = "1.0.35";
55
+ var SDK_VERSION = "1.0.37";
56
56
  var API_VERSION = "v1";
57
57
  var API_BASE_PATH = `/api/${API_VERSION}`;
58
58
  function normalizeCredential(value) {
@@ -774,15 +774,17 @@ var HttpClient = class {
774
774
  */
775
775
  resolveRetryConfig(method, options) {
776
776
  const base = this.config.retry;
777
- if (base.retryOn) {
778
- return base;
779
- }
780
777
  if (IDEMPOTENT_METHODS.has(method) || this.hasIdempotencyKey(options)) {
781
778
  return base;
782
779
  }
783
780
  return {
784
781
  ...base,
785
- retryOn: (error) => error instanceof RateLimitError
782
+ retryOn: (error, attempt) => {
783
+ if (error instanceof RateLimitError) {
784
+ return base.retryOn ? base.retryOn(error, attempt) : true;
785
+ }
786
+ return false;
787
+ }
786
788
  };
787
789
  }
788
790
  /**
@@ -1964,8 +1966,9 @@ var WebSocketRealtimeClient = class {
1964
1966
  };
1965
1967
  this.ws.onclose = (event) => {
1966
1968
  this.options.debug(`WebSocket closed: ${event.code} ${event.reason}`);
1969
+ const wasConnecting = this.state === "connecting";
1967
1970
  this.handleDisconnect();
1968
- if (this.state === "connecting") {
1971
+ if (wasConnecting) {
1969
1972
  reject(new Error(`WebSocket connection failed: ${event.reason || "Unknown error"}`));
1970
1973
  }
1971
1974
  };
@@ -2127,6 +2130,11 @@ var WebSocketRealtimeClient = class {
2127
2130
  await this.connect();
2128
2131
  } catch (error) {
2129
2132
  this.options.debug("Reconnect failed", error);
2133
+ if (this.options.autoReconnect && this.reconnectAttempts < this.options.maxReconnectAttempts) {
2134
+ this.scheduleReconnect();
2135
+ } else {
2136
+ this.setState("disconnected");
2137
+ }
2130
2138
  }
2131
2139
  }, delay);
2132
2140
  }
@@ -2241,7 +2249,7 @@ var SseRealtimeClient = class {
2241
2249
  }
2242
2250
  let token;
2243
2251
  try {
2244
- token = await this.options.tokenProvider();
2252
+ token = await this.options.tokenProvider(this.reconnectAttempts > 0);
2245
2253
  } catch (error) {
2246
2254
  this.setState("disconnected");
2247
2255
  throw new Error(`Failed to acquire realtime token: ${error}`);
@@ -2411,6 +2419,11 @@ var SseRealtimeClient = class {
2411
2419
  await this.connect(this.filter || void 0);
2412
2420
  } catch (error) {
2413
2421
  this.options.debug("SSE reconnect failed", error);
2422
+ if (this.options.autoReconnect && this.reconnectAttempts < this.options.maxReconnectAttempts) {
2423
+ this.scheduleReconnect();
2424
+ } else {
2425
+ this.setState("disconnected");
2426
+ }
2414
2427
  }
2415
2428
  }, delay);
2416
2429
  }
@@ -2682,6 +2695,77 @@ function createJobStream(call, options) {
2682
2695
  }
2683
2696
 
2684
2697
  // src/grpc/client.ts
2698
+ function encodeStruct(value) {
2699
+ if (value == null) {
2700
+ return void 0;
2701
+ }
2702
+ return {
2703
+ fields: Object.fromEntries(
2704
+ Object.entries(value).map(([key, entry]) => [key, encodeValue(entry)])
2705
+ )
2706
+ };
2707
+ }
2708
+ function encodeValue(value) {
2709
+ if (value == null) {
2710
+ return { nullValue: 0 };
2711
+ }
2712
+ if (Array.isArray(value)) {
2713
+ return { listValue: { values: value.map(encodeValue) } };
2714
+ }
2715
+ switch (typeof value) {
2716
+ case "number":
2717
+ return { numberValue: value };
2718
+ case "string":
2719
+ return { stringValue: value };
2720
+ case "boolean":
2721
+ return { boolValue: value };
2722
+ case "object":
2723
+ return { structValue: encodeStruct(value) };
2724
+ default:
2725
+ return { stringValue: String(value) };
2726
+ }
2727
+ }
2728
+ function decodeStruct(value) {
2729
+ if (!value || typeof value !== "object" || !("fields" in value)) {
2730
+ return value;
2731
+ }
2732
+ const fields = value.fields ?? {};
2733
+ return Object.fromEntries(Object.entries(fields).map(([key, entry]) => [key, decodeValue(entry)]));
2734
+ }
2735
+ function decodeValue(value) {
2736
+ if ("nullValue" in value) {
2737
+ return null;
2738
+ }
2739
+ if ("numberValue" in value) {
2740
+ return value.numberValue;
2741
+ }
2742
+ if ("stringValue" in value) {
2743
+ return value.stringValue;
2744
+ }
2745
+ if ("boolValue" in value) {
2746
+ return value.boolValue;
2747
+ }
2748
+ if ("structValue" in value) {
2749
+ return decodeStruct(value.structValue);
2750
+ }
2751
+ if ("listValue" in value) {
2752
+ return (value.listValue.values ?? []).map(decodeValue);
2753
+ }
2754
+ return void 0;
2755
+ }
2756
+ function encodeEnqueueRequest(params) {
2757
+ return { ...params, payload: encodeStruct(params.payload) };
2758
+ }
2759
+ function encodeCompleteRequest(params) {
2760
+ return { ...params, result: encodeStruct(params.result) };
2761
+ }
2762
+ function decodeJob(job) {
2763
+ return {
2764
+ ...job,
2765
+ payload: decodeStruct(job.payload),
2766
+ result: decodeStruct(job.result)
2767
+ };
2768
+ }
2685
2769
  function promisify(client, method, request, metadata) {
2686
2770
  return new Promise((resolve, reject) => {
2687
2771
  method.call(client, request, metadata, (error, response) => {
@@ -2709,19 +2793,20 @@ var GrpcQueueOperations = class {
2709
2793
  * Enqueue a new job
2710
2794
  */
2711
2795
  async enqueue(params) {
2712
- return promisify(this.client, this.client.Enqueue, params, this.metadata);
2796
+ return promisify(this.client, this.client.Enqueue, encodeEnqueueRequest(params), this.metadata);
2713
2797
  }
2714
2798
  /**
2715
2799
  * Dequeue a job (for workers)
2716
2800
  */
2717
2801
  async dequeue(params) {
2718
- return promisify(this.client, this.client.Dequeue, params, this.metadata);
2802
+ const response = await promisify(this.client, this.client.Dequeue, params, this.metadata);
2803
+ return { ...response, jobs: response.jobs.map(decodeJob) };
2719
2804
  }
2720
2805
  /**
2721
2806
  * Complete a job successfully
2722
2807
  */
2723
2808
  async complete(params) {
2724
- return promisify(this.client, this.client.Complete, params, this.metadata);
2809
+ return promisify(this.client, this.client.Complete, encodeCompleteRequest(params), this.metadata);
2725
2810
  }
2726
2811
  /**
2727
2812
  * Fail a job
@@ -2739,7 +2824,8 @@ var GrpcQueueOperations = class {
2739
2824
  * Get a job by ID
2740
2825
  */
2741
2826
  async getJob(jobId) {
2742
- return promisify(this.client, this.client.GetJob, { jobId }, this.metadata);
2827
+ const response = await promisify(this.client, this.client.GetJob, { jobId }, this.metadata);
2828
+ return { ...response, job: response.job ? decodeJob(response.job) : response.job };
2743
2829
  }
2744
2830
  /**
2745
2831
  * Get queue statistics