@durable-streams/client 0.1.2 → 0.1.3

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
@@ -1,6 +1,12 @@
1
1
  # @durable-streams/client
2
2
 
3
- TypeScript client for the Electric Durable Streams protocol.
3
+ TypeScript client for the Durable Streams protocol.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @durable-streams/client
9
+ ```
4
10
 
5
11
  ## Overview
6
12
 
package/dist/index.cjs CHANGED
@@ -466,6 +466,7 @@ async function* parseSSEStream(stream$1, signal) {
466
466
  const { done, value } = await reader.read();
467
467
  if (done) break;
468
468
  buffer += decoder.decode(value, { stream: true });
469
+ buffer = buffer.replace(/\r\n/g, `\n`).replace(/\r/g, `\n`);
469
470
  const lines = buffer.split(`\n`);
470
471
  buffer = lines.pop() ?? ``;
471
472
  for (const line of lines) if (line === ``) {
@@ -1180,6 +1181,78 @@ async function resolveParams(params) {
1180
1181
  else resolved[key] = value;
1181
1182
  return resolved;
1182
1183
  }
1184
+ const warnedOrigins = new Set();
1185
+ /**
1186
+ * Safely read NODE_ENV without triggering "process is not defined" errors.
1187
+ * Works in both browser and Node.js environments.
1188
+ */
1189
+ function getNodeEnvSafely() {
1190
+ if (typeof process === `undefined`) return void 0;
1191
+ return process.env?.NODE_ENV;
1192
+ }
1193
+ /**
1194
+ * Check if we're in a browser environment.
1195
+ */
1196
+ function isBrowserEnvironment() {
1197
+ return typeof globalThis.window !== `undefined`;
1198
+ }
1199
+ /**
1200
+ * Get window.location.href safely, returning undefined if not available.
1201
+ */
1202
+ function getWindowLocationHref() {
1203
+ if (typeof globalThis.window !== `undefined` && typeof globalThis.window.location !== `undefined`) return globalThis.window.location.href;
1204
+ return void 0;
1205
+ }
1206
+ /**
1207
+ * Resolve a URL string, handling relative URLs in browser environments.
1208
+ * Returns undefined if the URL cannot be parsed.
1209
+ */
1210
+ function resolveUrlMaybe(urlString) {
1211
+ try {
1212
+ return new URL(urlString);
1213
+ } catch {
1214
+ const base = getWindowLocationHref();
1215
+ if (base) try {
1216
+ return new URL(urlString, base);
1217
+ } catch {
1218
+ return void 0;
1219
+ }
1220
+ return void 0;
1221
+ }
1222
+ }
1223
+ /**
1224
+ * Warn if using HTTP (not HTTPS) URL in a browser environment.
1225
+ * HTTP typically limits browsers to ~6 concurrent connections per origin under HTTP/1.1,
1226
+ * which can cause slow streams and app freezes with multiple active streams.
1227
+ *
1228
+ * Features:
1229
+ * - Warns only once per origin to prevent log spam
1230
+ * - Handles relative URLs by resolving against window.location.href
1231
+ * - Safe to call in Node.js environments (no-op)
1232
+ * - Skips warning during tests (NODE_ENV=test)
1233
+ */
1234
+ function warnIfUsingHttpInBrowser(url, warnOnHttp) {
1235
+ if (warnOnHttp === false) return;
1236
+ const nodeEnv = getNodeEnvSafely();
1237
+ if (nodeEnv === `test`) return;
1238
+ if (!isBrowserEnvironment() || typeof console === `undefined` || typeof console.warn !== `function`) return;
1239
+ const urlStr = url instanceof URL ? url.toString() : url;
1240
+ const parsedUrl = resolveUrlMaybe(urlStr);
1241
+ if (!parsedUrl) return;
1242
+ if (parsedUrl.protocol === `http:`) {
1243
+ if (!warnedOrigins.has(parsedUrl.origin)) {
1244
+ warnedOrigins.add(parsedUrl.origin);
1245
+ console.warn("[DurableStream] Using HTTP (not HTTPS) typically limits browsers to ~6 concurrent connections per origin under HTTP/1.1. This can cause slow streams and app freezes with multiple active streams. Use HTTPS for HTTP/2 support. See https://electric-sql.com/r/electric-http2 for more information.");
1246
+ }
1247
+ }
1248
+ }
1249
+ /**
1250
+ * Reset the HTTP warning state. Only exported for testing purposes.
1251
+ * @internal
1252
+ */
1253
+ function _resetHttpWarningForTesting() {
1254
+ warnedOrigins.clear();
1255
+ }
1183
1256
 
1184
1257
  //#endregion
1185
1258
  //#region src/stream-api.ts
@@ -1787,7 +1860,8 @@ var DurableStream = class DurableStream {
1787
1860
  offset: options?.offset,
1788
1861
  live: options?.live,
1789
1862
  json: options?.json,
1790
- onError: options?.onError ?? this.#onError
1863
+ onError: options?.onError ?? this.#onError,
1864
+ warnOnHttp: options?.warnOnHttp ?? this.#options.warnOnHttp
1791
1865
  });
1792
1866
  }
1793
1867
  /**
@@ -1848,6 +1922,7 @@ function toReadableStream(source) {
1848
1922
  function validateOptions(options) {
1849
1923
  if (!options.url) throw new MissingStreamUrlError();
1850
1924
  if (options.signal && !(options.signal instanceof AbortSignal)) throw new InvalidSignalError();
1925
+ warnIfUsingHttpInBrowser(options.url, options.warnOnHttp);
1851
1926
  }
1852
1927
 
1853
1928
  //#endregion
@@ -1869,7 +1944,9 @@ exports.STREAM_OFFSET_HEADER = STREAM_OFFSET_HEADER
1869
1944
  exports.STREAM_SEQ_HEADER = STREAM_SEQ_HEADER
1870
1945
  exports.STREAM_TTL_HEADER = STREAM_TTL_HEADER
1871
1946
  exports.STREAM_UP_TO_DATE_HEADER = STREAM_UP_TO_DATE_HEADER
1947
+ exports._resetHttpWarningForTesting = _resetHttpWarningForTesting
1872
1948
  exports.asAsyncIterableReadableStream = asAsyncIterableReadableStream
1873
1949
  exports.createFetchWithBackoff = createFetchWithBackoff
1874
1950
  exports.createFetchWithConsumedBody = createFetchWithConsumedBody
1875
- exports.stream = stream
1951
+ exports.stream = stream
1952
+ exports.warnIfUsingHttpInBrowser = warnIfUsingHttpInBrowser
package/dist/index.d.cts CHANGED
@@ -249,6 +249,14 @@ interface StreamOptions {
249
249
  * fall back to long-polling mode.
250
250
  */
251
251
  sseResilience?: SSEResilienceOptions;
252
+ /**
253
+ * Whether to warn when using HTTP (not HTTPS) URLs in browser environments.
254
+ * HTTP limits browsers to 6 concurrent connections (HTTP/1.1), which can
255
+ * cause slow streams and app freezes with multiple active streams.
256
+ *
257
+ * @default true
258
+ */
259
+ warnOnHttp?: boolean;
252
260
  }
253
261
  /**
254
262
  * Options for SSE connection resilience.
@@ -374,6 +382,14 @@ interface StreamHandleOptions {
374
382
  * @default true
375
383
  */
376
384
  batching?: boolean;
385
+ /**
386
+ * Whether to warn when using HTTP (not HTTPS) URLs in browser environments.
387
+ * HTTP limits browsers to 6 concurrent connections (HTTP/1.1), which can
388
+ * cause slow streams and app freezes with multiple active streams.
389
+ *
390
+ * @default true
391
+ */
392
+ warnOnHttp?: boolean;
377
393
  }
378
394
  /**
379
395
  * Options for creating a new stream.
@@ -944,6 +960,26 @@ declare class DurableStream {
944
960
  stream<TJson = unknown>(options?: Omit<StreamOptions, `url`>): Promise<StreamResponse<TJson>>;
945
961
  }
946
962
 
963
+ //#endregion
964
+ //#region src/utils.d.ts
965
+ /**
966
+ * Warn if using HTTP (not HTTPS) URL in a browser environment.
967
+ * HTTP typically limits browsers to ~6 concurrent connections per origin under HTTP/1.1,
968
+ * which can cause slow streams and app freezes with multiple active streams.
969
+ *
970
+ * Features:
971
+ * - Warns only once per origin to prevent log spam
972
+ * - Handles relative URLs by resolving against window.location.href
973
+ * - Safe to call in Node.js environments (no-op)
974
+ * - Skips warning during tests (NODE_ENV=test)
975
+ */
976
+ declare function warnIfUsingHttpInBrowser(url: string | URL, warnOnHttp?: boolean): void;
977
+ /**
978
+ * Reset the HTTP warning state. Only exported for testing purposes.
979
+ * @internal
980
+ */
981
+ declare function _resetHttpWarningForTesting(): void;
982
+
947
983
  //#endregion
948
984
  //#region src/error.d.ts
949
985
  /**
@@ -1069,4 +1105,4 @@ declare const SSE_COMPATIBLE_CONTENT_TYPES: ReadonlyArray<string>;
1069
1105
  declare const DURABLE_STREAM_PROTOCOL_QUERY_PARAMS: Array<string>;
1070
1106
 
1071
1107
  //#endregion
1072
- export { AppendOptions, BackoffDefaults, BackoffOptions, ByteChunk, CURSOR_QUERY_PARAM, CreateOptions, DURABLE_STREAM_PROTOCOL_QUERY_PARAMS, DurableStream, DurableStreamError, DurableStreamErrorCode, DurableStreamOptions, FetchBackoffAbortError, FetchError, HeadResult, HeadersRecord, InvalidSignalError, JsonBatch, JsonBatchMeta, LIVE_QUERY_PARAM, LegacyLiveMode, LiveMode, MaybePromise, MissingStreamUrlError, OFFSET_QUERY_PARAM, Offset, ParamsRecord, ReadOptions, ReadableStreamAsyncIterable, RetryOpts, SSEResilienceOptions, SSE_COMPATIBLE_CONTENT_TYPES, STREAM_CURSOR_HEADER, STREAM_EXPIRES_AT_HEADER, STREAM_OFFSET_HEADER, STREAM_SEQ_HEADER, STREAM_TTL_HEADER, STREAM_UP_TO_DATE_HEADER, StreamErrorHandler, StreamHandleOptions, StreamOptions, StreamResponse, TextChunk, asAsyncIterableReadableStream, createFetchWithBackoff, createFetchWithConsumedBody, stream };
1108
+ export { AppendOptions, BackoffDefaults, BackoffOptions, ByteChunk, CURSOR_QUERY_PARAM, CreateOptions, DURABLE_STREAM_PROTOCOL_QUERY_PARAMS, DurableStream, DurableStreamError, DurableStreamErrorCode, DurableStreamOptions, FetchBackoffAbortError, FetchError, HeadResult, HeadersRecord, InvalidSignalError, JsonBatch, JsonBatchMeta, LIVE_QUERY_PARAM, LegacyLiveMode, LiveMode, MaybePromise, MissingStreamUrlError, OFFSET_QUERY_PARAM, Offset, ParamsRecord, ReadOptions, ReadableStreamAsyncIterable, RetryOpts, SSEResilienceOptions, SSE_COMPATIBLE_CONTENT_TYPES, STREAM_CURSOR_HEADER, STREAM_EXPIRES_AT_HEADER, STREAM_OFFSET_HEADER, STREAM_SEQ_HEADER, STREAM_TTL_HEADER, STREAM_UP_TO_DATE_HEADER, StreamErrorHandler, StreamHandleOptions, StreamOptions, StreamResponse, TextChunk, _resetHttpWarningForTesting, asAsyncIterableReadableStream, createFetchWithBackoff, createFetchWithConsumedBody, stream, warnIfUsingHttpInBrowser };
package/dist/index.d.ts CHANGED
@@ -249,6 +249,14 @@ interface StreamOptions {
249
249
  * fall back to long-polling mode.
250
250
  */
251
251
  sseResilience?: SSEResilienceOptions;
252
+ /**
253
+ * Whether to warn when using HTTP (not HTTPS) URLs in browser environments.
254
+ * HTTP limits browsers to 6 concurrent connections (HTTP/1.1), which can
255
+ * cause slow streams and app freezes with multiple active streams.
256
+ *
257
+ * @default true
258
+ */
259
+ warnOnHttp?: boolean;
252
260
  }
253
261
  /**
254
262
  * Options for SSE connection resilience.
@@ -374,6 +382,14 @@ interface StreamHandleOptions {
374
382
  * @default true
375
383
  */
376
384
  batching?: boolean;
385
+ /**
386
+ * Whether to warn when using HTTP (not HTTPS) URLs in browser environments.
387
+ * HTTP limits browsers to 6 concurrent connections (HTTP/1.1), which can
388
+ * cause slow streams and app freezes with multiple active streams.
389
+ *
390
+ * @default true
391
+ */
392
+ warnOnHttp?: boolean;
377
393
  }
378
394
  /**
379
395
  * Options for creating a new stream.
@@ -944,6 +960,26 @@ declare class DurableStream {
944
960
  stream<TJson = unknown>(options?: Omit<StreamOptions, `url`>): Promise<StreamResponse<TJson>>;
945
961
  }
946
962
 
963
+ //#endregion
964
+ //#region src/utils.d.ts
965
+ /**
966
+ * Warn if using HTTP (not HTTPS) URL in a browser environment.
967
+ * HTTP typically limits browsers to ~6 concurrent connections per origin under HTTP/1.1,
968
+ * which can cause slow streams and app freezes with multiple active streams.
969
+ *
970
+ * Features:
971
+ * - Warns only once per origin to prevent log spam
972
+ * - Handles relative URLs by resolving against window.location.href
973
+ * - Safe to call in Node.js environments (no-op)
974
+ * - Skips warning during tests (NODE_ENV=test)
975
+ */
976
+ declare function warnIfUsingHttpInBrowser(url: string | URL, warnOnHttp?: boolean): void;
977
+ /**
978
+ * Reset the HTTP warning state. Only exported for testing purposes.
979
+ * @internal
980
+ */
981
+ declare function _resetHttpWarningForTesting(): void;
982
+
947
983
  //#endregion
948
984
  //#region src/error.d.ts
949
985
  /**
@@ -1069,4 +1105,4 @@ declare const SSE_COMPATIBLE_CONTENT_TYPES: ReadonlyArray<string>;
1069
1105
  declare const DURABLE_STREAM_PROTOCOL_QUERY_PARAMS: Array<string>;
1070
1106
 
1071
1107
  //#endregion
1072
- export { AppendOptions, BackoffDefaults, BackoffOptions, ByteChunk, CURSOR_QUERY_PARAM, CreateOptions, DURABLE_STREAM_PROTOCOL_QUERY_PARAMS, DurableStream, DurableStreamError, DurableStreamErrorCode, DurableStreamOptions, FetchBackoffAbortError, FetchError, HeadResult, HeadersRecord, InvalidSignalError, JsonBatch, JsonBatchMeta, LIVE_QUERY_PARAM, LegacyLiveMode, LiveMode, MaybePromise, MissingStreamUrlError, OFFSET_QUERY_PARAM, Offset, ParamsRecord, ReadOptions, ReadableStreamAsyncIterable, RetryOpts, SSEResilienceOptions, SSE_COMPATIBLE_CONTENT_TYPES, STREAM_CURSOR_HEADER, STREAM_EXPIRES_AT_HEADER, STREAM_OFFSET_HEADER, STREAM_SEQ_HEADER, STREAM_TTL_HEADER, STREAM_UP_TO_DATE_HEADER, StreamErrorHandler, StreamHandleOptions, StreamOptions, StreamResponse, TextChunk, asAsyncIterableReadableStream, createFetchWithBackoff, createFetchWithConsumedBody, stream };
1108
+ export { AppendOptions, BackoffDefaults, BackoffOptions, ByteChunk, CURSOR_QUERY_PARAM, CreateOptions, DURABLE_STREAM_PROTOCOL_QUERY_PARAMS, DurableStream, DurableStreamError, DurableStreamErrorCode, DurableStreamOptions, FetchBackoffAbortError, FetchError, HeadResult, HeadersRecord, InvalidSignalError, JsonBatch, JsonBatchMeta, LIVE_QUERY_PARAM, LegacyLiveMode, LiveMode, MaybePromise, MissingStreamUrlError, OFFSET_QUERY_PARAM, Offset, ParamsRecord, ReadOptions, ReadableStreamAsyncIterable, RetryOpts, SSEResilienceOptions, SSE_COMPATIBLE_CONTENT_TYPES, STREAM_CURSOR_HEADER, STREAM_EXPIRES_AT_HEADER, STREAM_OFFSET_HEADER, STREAM_SEQ_HEADER, STREAM_TTL_HEADER, STREAM_UP_TO_DATE_HEADER, StreamErrorHandler, StreamHandleOptions, StreamOptions, StreamResponse, TextChunk, _resetHttpWarningForTesting, asAsyncIterableReadableStream, createFetchWithBackoff, createFetchWithConsumedBody, stream, warnIfUsingHttpInBrowser };
package/dist/index.js CHANGED
@@ -442,6 +442,7 @@ async function* parseSSEStream(stream$1, signal) {
442
442
  const { done, value } = await reader.read();
443
443
  if (done) break;
444
444
  buffer += decoder.decode(value, { stream: true });
445
+ buffer = buffer.replace(/\r\n/g, `\n`).replace(/\r/g, `\n`);
445
446
  const lines = buffer.split(`\n`);
446
447
  buffer = lines.pop() ?? ``;
447
448
  for (const line of lines) if (line === ``) {
@@ -1156,6 +1157,78 @@ async function resolveParams(params) {
1156
1157
  else resolved[key] = value;
1157
1158
  return resolved;
1158
1159
  }
1160
+ const warnedOrigins = new Set();
1161
+ /**
1162
+ * Safely read NODE_ENV without triggering "process is not defined" errors.
1163
+ * Works in both browser and Node.js environments.
1164
+ */
1165
+ function getNodeEnvSafely() {
1166
+ if (typeof process === `undefined`) return void 0;
1167
+ return process.env?.NODE_ENV;
1168
+ }
1169
+ /**
1170
+ * Check if we're in a browser environment.
1171
+ */
1172
+ function isBrowserEnvironment() {
1173
+ return typeof globalThis.window !== `undefined`;
1174
+ }
1175
+ /**
1176
+ * Get window.location.href safely, returning undefined if not available.
1177
+ */
1178
+ function getWindowLocationHref() {
1179
+ if (typeof globalThis.window !== `undefined` && typeof globalThis.window.location !== `undefined`) return globalThis.window.location.href;
1180
+ return void 0;
1181
+ }
1182
+ /**
1183
+ * Resolve a URL string, handling relative URLs in browser environments.
1184
+ * Returns undefined if the URL cannot be parsed.
1185
+ */
1186
+ function resolveUrlMaybe(urlString) {
1187
+ try {
1188
+ return new URL(urlString);
1189
+ } catch {
1190
+ const base = getWindowLocationHref();
1191
+ if (base) try {
1192
+ return new URL(urlString, base);
1193
+ } catch {
1194
+ return void 0;
1195
+ }
1196
+ return void 0;
1197
+ }
1198
+ }
1199
+ /**
1200
+ * Warn if using HTTP (not HTTPS) URL in a browser environment.
1201
+ * HTTP typically limits browsers to ~6 concurrent connections per origin under HTTP/1.1,
1202
+ * which can cause slow streams and app freezes with multiple active streams.
1203
+ *
1204
+ * Features:
1205
+ * - Warns only once per origin to prevent log spam
1206
+ * - Handles relative URLs by resolving against window.location.href
1207
+ * - Safe to call in Node.js environments (no-op)
1208
+ * - Skips warning during tests (NODE_ENV=test)
1209
+ */
1210
+ function warnIfUsingHttpInBrowser(url, warnOnHttp) {
1211
+ if (warnOnHttp === false) return;
1212
+ const nodeEnv = getNodeEnvSafely();
1213
+ if (nodeEnv === `test`) return;
1214
+ if (!isBrowserEnvironment() || typeof console === `undefined` || typeof console.warn !== `function`) return;
1215
+ const urlStr = url instanceof URL ? url.toString() : url;
1216
+ const parsedUrl = resolveUrlMaybe(urlStr);
1217
+ if (!parsedUrl) return;
1218
+ if (parsedUrl.protocol === `http:`) {
1219
+ if (!warnedOrigins.has(parsedUrl.origin)) {
1220
+ warnedOrigins.add(parsedUrl.origin);
1221
+ console.warn("[DurableStream] Using HTTP (not HTTPS) typically limits browsers to ~6 concurrent connections per origin under HTTP/1.1. This can cause slow streams and app freezes with multiple active streams. Use HTTPS for HTTP/2 support. See https://electric-sql.com/r/electric-http2 for more information.");
1222
+ }
1223
+ }
1224
+ }
1225
+ /**
1226
+ * Reset the HTTP warning state. Only exported for testing purposes.
1227
+ * @internal
1228
+ */
1229
+ function _resetHttpWarningForTesting() {
1230
+ warnedOrigins.clear();
1231
+ }
1159
1232
 
1160
1233
  //#endregion
1161
1234
  //#region src/stream-api.ts
@@ -1763,7 +1836,8 @@ var DurableStream = class DurableStream {
1763
1836
  offset: options?.offset,
1764
1837
  live: options?.live,
1765
1838
  json: options?.json,
1766
- onError: options?.onError ?? this.#onError
1839
+ onError: options?.onError ?? this.#onError,
1840
+ warnOnHttp: options?.warnOnHttp ?? this.#options.warnOnHttp
1767
1841
  });
1768
1842
  }
1769
1843
  /**
@@ -1824,7 +1898,8 @@ function toReadableStream(source) {
1824
1898
  function validateOptions(options) {
1825
1899
  if (!options.url) throw new MissingStreamUrlError();
1826
1900
  if (options.signal && !(options.signal instanceof AbortSignal)) throw new InvalidSignalError();
1901
+ warnIfUsingHttpInBrowser(options.url, options.warnOnHttp);
1827
1902
  }
1828
1903
 
1829
1904
  //#endregion
1830
- export { BackoffDefaults, CURSOR_QUERY_PARAM, DURABLE_STREAM_PROTOCOL_QUERY_PARAMS, DurableStream, DurableStreamError, FetchBackoffAbortError, FetchError, InvalidSignalError, LIVE_QUERY_PARAM, MissingStreamUrlError, OFFSET_QUERY_PARAM, SSE_COMPATIBLE_CONTENT_TYPES, STREAM_CURSOR_HEADER, STREAM_EXPIRES_AT_HEADER, STREAM_OFFSET_HEADER, STREAM_SEQ_HEADER, STREAM_TTL_HEADER, STREAM_UP_TO_DATE_HEADER, asAsyncIterableReadableStream, createFetchWithBackoff, createFetchWithConsumedBody, stream };
1905
+ export { BackoffDefaults, CURSOR_QUERY_PARAM, DURABLE_STREAM_PROTOCOL_QUERY_PARAMS, DurableStream, DurableStreamError, FetchBackoffAbortError, FetchError, InvalidSignalError, LIVE_QUERY_PARAM, MissingStreamUrlError, OFFSET_QUERY_PARAM, SSE_COMPATIBLE_CONTENT_TYPES, STREAM_CURSOR_HEADER, STREAM_EXPIRES_AT_HEADER, STREAM_OFFSET_HEADER, STREAM_SEQ_HEADER, STREAM_TTL_HEADER, STREAM_UP_TO_DATE_HEADER, _resetHttpWarningForTesting, asAsyncIterableReadableStream, createFetchWithBackoff, createFetchWithConsumedBody, stream, warnIfUsingHttpInBrowser };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@durable-streams/client",
3
3
  "description": "TypeScript client for the Durable Streams protocol",
4
- "version": "0.1.2",
4
+ "version": "0.1.3",
5
5
  "author": "Durable Stream contributors",
6
6
  "license": "Apache-2.0",
7
7
  "repository": {
@@ -47,7 +47,7 @@
47
47
  "devDependencies": {
48
48
  "fast-check": "^4.4.0",
49
49
  "tsdown": "^0.9.0",
50
- "@durable-streams/server": "0.1.2"
50
+ "@durable-streams/server": "0.1.4"
51
51
  },
52
52
  "engines": {
53
53
  "node": ">=18.0.0"
package/src/index.ts CHANGED
@@ -20,6 +20,9 @@ export { stream } from "./stream-api"
20
20
  // DurableStream class for read/write operations
21
21
  export { DurableStream, type DurableStreamOptions } from "./stream"
22
22
 
23
+ // HTTP warning utility
24
+ export { warnIfUsingHttpInBrowser, _resetHttpWarningForTesting } from "./utils"
25
+
23
26
  // ============================================================================
24
27
  // Types
25
28
  // ============================================================================
package/src/sse.ts CHANGED
@@ -50,6 +50,9 @@ export async function* parseSSEStream(
50
50
 
51
51
  buffer += decoder.decode(value, { stream: true })
52
52
 
53
+ // Normalize line endings: CRLF → LF, lone CR → LF (per SSE spec)
54
+ buffer = buffer.replace(/\r\n/g, `\n`).replace(/\r/g, `\n`)
55
+
53
56
  // Process complete lines
54
57
  const lines = buffer.split(`\n`)
55
58
  // Keep the last incomplete line in the buffer
package/src/stream.ts CHANGED
@@ -24,7 +24,12 @@ import {
24
24
  createFetchWithConsumedBody,
25
25
  } from "./fetch"
26
26
  import { stream as streamFn } from "./stream-api"
27
- import { handleErrorResponse, resolveHeaders, resolveParams } from "./utils"
27
+ import {
28
+ handleErrorResponse,
29
+ resolveHeaders,
30
+ resolveParams,
31
+ warnIfUsingHttpInBrowser,
32
+ } from "./utils"
28
33
  import type { BackoffOptions } from "./fetch"
29
34
  import type { queueAsPromised } from "fastq"
30
35
  import type {
@@ -742,6 +747,7 @@ export class DurableStream {
742
747
  live: options?.live,
743
748
  json: options?.json,
744
749
  onError: options?.onError ?? this.#onError,
750
+ warnOnHttp: options?.warnOnHttp ?? this.#options.warnOnHttp,
745
751
  })
746
752
  }
747
753
 
@@ -864,4 +870,5 @@ function validateOptions(options: Partial<DurableStreamOptions>): void {
864
870
  if (options.signal && !(options.signal instanceof AbortSignal)) {
865
871
  throw new InvalidSignalError()
866
872
  }
873
+ warnIfUsingHttpInBrowser(options.url, options.warnOnHttp)
867
874
  }
package/src/types.ts CHANGED
@@ -158,6 +158,15 @@ export interface StreamOptions {
158
158
  * fall back to long-polling mode.
159
159
  */
160
160
  sseResilience?: SSEResilienceOptions
161
+
162
+ /**
163
+ * Whether to warn when using HTTP (not HTTPS) URLs in browser environments.
164
+ * HTTP limits browsers to 6 concurrent connections (HTTP/1.1), which can
165
+ * cause slow streams and app freezes with multiple active streams.
166
+ *
167
+ * @default true
168
+ */
169
+ warnOnHttp?: boolean
161
170
  }
162
171
 
163
172
  /**
@@ -310,6 +319,15 @@ export interface StreamHandleOptions {
310
319
  * @default true
311
320
  */
312
321
  batching?: boolean
322
+
323
+ /**
324
+ * Whether to warn when using HTTP (not HTTPS) URLs in browser environments.
325
+ * HTTP limits browsers to 6 concurrent connections (HTTP/1.1), which can
326
+ * cause slow streams and app freezes with multiple active streams.
327
+ *
328
+ * @default true
329
+ */
330
+ warnOnHttp?: boolean
313
331
  }
314
332
 
315
333
  /**
package/src/utils.ts CHANGED
@@ -102,3 +102,123 @@ export async function resolveValue<T>(
102
102
  }
103
103
  return value
104
104
  }
105
+
106
+ // Module-level Set to track origins we've already warned about (prevents log spam)
107
+ const warnedOrigins = new Set<string>()
108
+
109
+ /**
110
+ * Safely read NODE_ENV without triggering "process is not defined" errors.
111
+ * Works in both browser and Node.js environments.
112
+ */
113
+ function getNodeEnvSafely(): string | undefined {
114
+ if (typeof process === `undefined`) return undefined
115
+ // Use optional chaining for process.env in case it's undefined (e.g., in some bundler environments)
116
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
117
+ return process.env?.NODE_ENV
118
+ }
119
+
120
+ /**
121
+ * Check if we're in a browser environment.
122
+ */
123
+ function isBrowserEnvironment(): boolean {
124
+ return typeof globalThis.window !== `undefined`
125
+ }
126
+
127
+ /**
128
+ * Get window.location.href safely, returning undefined if not available.
129
+ */
130
+ function getWindowLocationHref(): string | undefined {
131
+ if (
132
+ typeof globalThis.window !== `undefined` &&
133
+ typeof globalThis.window.location !== `undefined`
134
+ ) {
135
+ return globalThis.window.location.href
136
+ }
137
+ return undefined
138
+ }
139
+
140
+ /**
141
+ * Resolve a URL string, handling relative URLs in browser environments.
142
+ * Returns undefined if the URL cannot be parsed.
143
+ */
144
+ function resolveUrlMaybe(urlString: string): URL | undefined {
145
+ try {
146
+ // First try parsing as an absolute URL
147
+ return new URL(urlString)
148
+ } catch {
149
+ // If that fails and we're in a browser, try resolving as relative URL
150
+ const base = getWindowLocationHref()
151
+ if (base) {
152
+ try {
153
+ return new URL(urlString, base)
154
+ } catch {
155
+ return undefined
156
+ }
157
+ }
158
+ return undefined
159
+ }
160
+ }
161
+
162
+ /**
163
+ * Warn if using HTTP (not HTTPS) URL in a browser environment.
164
+ * HTTP typically limits browsers to ~6 concurrent connections per origin under HTTP/1.1,
165
+ * which can cause slow streams and app freezes with multiple active streams.
166
+ *
167
+ * Features:
168
+ * - Warns only once per origin to prevent log spam
169
+ * - Handles relative URLs by resolving against window.location.href
170
+ * - Safe to call in Node.js environments (no-op)
171
+ * - Skips warning during tests (NODE_ENV=test)
172
+ */
173
+ export function warnIfUsingHttpInBrowser(
174
+ url: string | URL,
175
+ warnOnHttp?: boolean
176
+ ): void {
177
+ // Skip warning if explicitly disabled
178
+ if (warnOnHttp === false) return
179
+
180
+ // Skip warning during tests
181
+ const nodeEnv = getNodeEnvSafely()
182
+ if (nodeEnv === `test`) {
183
+ return
184
+ }
185
+
186
+ // Only warn in browser environments
187
+ if (
188
+ !isBrowserEnvironment() ||
189
+ typeof console === `undefined` ||
190
+ typeof console.warn !== `function`
191
+ ) {
192
+ return
193
+ }
194
+
195
+ // Parse the URL (handles both absolute and relative URLs)
196
+ const urlStr = url instanceof URL ? url.toString() : url
197
+ const parsedUrl = resolveUrlMaybe(urlStr)
198
+
199
+ if (!parsedUrl) {
200
+ // Could not parse URL - silently skip
201
+ return
202
+ }
203
+
204
+ // Check if URL uses HTTP protocol
205
+ if (parsedUrl.protocol === `http:`) {
206
+ // Only warn once per origin
207
+ if (!warnedOrigins.has(parsedUrl.origin)) {
208
+ warnedOrigins.add(parsedUrl.origin)
209
+ console.warn(
210
+ `[DurableStream] Using HTTP (not HTTPS) typically limits browsers to ~6 concurrent connections per origin under HTTP/1.1. ` +
211
+ `This can cause slow streams and app freezes with multiple active streams. ` +
212
+ `Use HTTPS for HTTP/2 support. See https://electric-sql.com/r/electric-http2 for more information.`
213
+ )
214
+ }
215
+ }
216
+ }
217
+
218
+ /**
219
+ * Reset the HTTP warning state. Only exported for testing purposes.
220
+ * @internal
221
+ */
222
+ export function _resetHttpWarningForTesting(): void {
223
+ warnedOrigins.clear()
224
+ }