@heroku/sdk 0.5.3 → 0.5.4

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.
@@ -90,6 +90,21 @@ export async function* streamLogs(ctx, appIdentity, options = {}) {
90
90
  const reader = response.body.getReader();
91
91
  const decoder = new TextDecoder();
92
92
  let pending = '';
93
+ // Wire the caller's abort signal into the reader. `await reader.read()`
94
+ // parks until the next chunk arrives; a bare `signal.aborted` check
95
+ // between reads never runs while it's parked, so on a quiet tail the
96
+ // read would block indefinitely past an abort. Cancelling the reader
97
+ // settles the pending `read()` promise, letting the loop below observe
98
+ // the abort and terminate — which also closes the underlying socket.
99
+ const onAbort = () => {
100
+ reader.cancel().catch(() => { });
101
+ };
102
+ if (signal) {
103
+ if (signal.aborted)
104
+ reader.cancel().catch(() => { });
105
+ else
106
+ signal.addEventListener('abort', onAbort, { once: true });
107
+ }
93
108
  // Force a recreate after sessionTimeoutMs of silence; ignored
94
109
  // when not tailing.
95
110
  const armTimeout = () => {
@@ -109,6 +124,11 @@ export async function* streamLogs(ctx, appIdentity, options = {}) {
109
124
  const { done, value } = await reader.read();
110
125
  if (done)
111
126
  break;
127
+ // A cancel triggered by the caller's signal also resolves the read
128
+ // as `done`; stop rather than treating it as a remote close and
129
+ // recreating the session.
130
+ if (signal?.aborted)
131
+ break;
112
132
  armTimeout();
113
133
  pending += decoder.decode(value, { stream: true });
114
134
  if (isSse) {
@@ -156,8 +176,26 @@ export async function* streamLogs(ctx, appIdentity, options = {}) {
156
176
  finally {
157
177
  if (timeoutHandle)
158
178
  clearTimeout(timeoutHandle);
179
+ signal?.removeEventListener('abort', onAbort);
180
+ // Tear the reader down. If a consumer stops early — `break`/`return()`
181
+ // out of a `for await`, or an explicit `.return()` — this finally runs
182
+ // while the stream is still open, and `releaseLock()` alone would leak
183
+ // the underlying socket (plus, when tailing, the session-recreate
184
+ // loop). `cancel()` closes the body; fire-and-forget because on an
185
+ // errored/already-closed stream it rejects with the stored error,
186
+ // which we don't act on. It does not release the lock, so we always
187
+ // `releaseLock()` afterward — otherwise the next recreate's
188
+ // `getReader()` throws "ReadableStream is locked". No pending read
189
+ // exists once the loop above has exited, so `releaseLock()` is safe.
190
+ reader.cancel().catch(() => { });
159
191
  reader.releaseLock();
160
192
  }
193
+ // The caller aborted (or their signal fired mid-read): terminate the
194
+ // whole stream, throwing the abort reason so `for await` consumers see
195
+ // an AbortError rather than a silent end.
196
+ if (signal?.aborted) {
197
+ throw signal.reason ?? new DOMException('The operation was aborted.', 'AbortError');
198
+ }
161
199
  if (timedOut) {
162
200
  debug('streamLogs session=%s recreating after timeout', session.id);
163
201
  continue;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@heroku/sdk",
3
- "version": "0.5.3",
3
+ "version": "0.5.4",
4
4
  "description": "Heroku SDK",
5
5
  "author": "Heroku",
6
6
  "type": "module",
@@ -99,7 +99,7 @@
99
99
  "vitest": "^4.1.4"
100
100
  },
101
101
  "dependencies": {
102
- "@heroku/heroku-fetch": "^0.1.1-beta",
102
+ "@heroku/heroku-fetch": "^0.1.3",
103
103
  "@heroku/types": "^4.0.1",
104
104
  "debug": "^4.4.0"
105
105
  }