@durable-streams/client 0.1.2 → 0.1.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.
- package/README.md +175 -5
- package/dist/index.cjs +510 -5
- package/dist/index.d.cts +254 -1
- package/dist/index.d.ts +254 -1
- package/dist/index.js +500 -5
- package/package.json +2 -2
- package/src/constants.ts +31 -0
- package/src/idempotent-producer.ts +642 -0
- package/src/index.ts +24 -0
- package/src/sse.ts +3 -0
- package/src/stream.ts +13 -1
- package/src/types.ts +111 -0
- package/src/utils.ts +120 -0
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 {
|
|
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 {
|
|
@@ -158,6 +163,11 @@ export class DurableStream {
|
|
|
158
163
|
this.#options = { ...opts, url: urlStr }
|
|
159
164
|
this.#onError = opts.onError
|
|
160
165
|
|
|
166
|
+
// Set contentType from options if provided (for IdempotentProducer and other use cases)
|
|
167
|
+
if (opts.contentType) {
|
|
168
|
+
this.contentType = opts.contentType
|
|
169
|
+
}
|
|
170
|
+
|
|
161
171
|
// Batching is enabled by default
|
|
162
172
|
this.#batchingEnabled = opts.batching !== false
|
|
163
173
|
|
|
@@ -742,6 +752,7 @@ export class DurableStream {
|
|
|
742
752
|
live: options?.live,
|
|
743
753
|
json: options?.json,
|
|
744
754
|
onError: options?.onError ?? this.#onError,
|
|
755
|
+
warnOnHttp: options?.warnOnHttp ?? this.#options.warnOnHttp,
|
|
745
756
|
})
|
|
746
757
|
}
|
|
747
758
|
|
|
@@ -864,4 +875,5 @@ function validateOptions(options: Partial<DurableStreamOptions>): void {
|
|
|
864
875
|
if (options.signal && !(options.signal instanceof AbortSignal)) {
|
|
865
876
|
throw new InvalidSignalError()
|
|
866
877
|
}
|
|
878
|
+
warnIfUsingHttpInBrowser(options.url, options.warnOnHttp)
|
|
867
879
|
}
|
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
|
/**
|
|
@@ -363,6 +381,26 @@ export interface AppendOptions {
|
|
|
363
381
|
* AbortSignal for this operation.
|
|
364
382
|
*/
|
|
365
383
|
signal?: AbortSignal
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* Producer ID for idempotent writes.
|
|
387
|
+
* Client-supplied stable identifier (e.g., "order-service-1").
|
|
388
|
+
* Must be provided together with producerEpoch and producerSeq.
|
|
389
|
+
*/
|
|
390
|
+
producerId?: string
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* Producer epoch for idempotent writes.
|
|
394
|
+
* Client-declared, server-validated monotonically increasing.
|
|
395
|
+
* Increment on producer restart.
|
|
396
|
+
*/
|
|
397
|
+
producerEpoch?: number
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* Producer sequence for idempotent writes.
|
|
401
|
+
* Monotonically increasing per epoch, per-batch.
|
|
402
|
+
*/
|
|
403
|
+
producerSeq?: number
|
|
366
404
|
}
|
|
367
405
|
|
|
368
406
|
/**
|
|
@@ -735,3 +773,76 @@ export interface StreamResponse<TJson = unknown> {
|
|
|
735
773
|
*/
|
|
736
774
|
readonly closed: Promise<void>
|
|
737
775
|
}
|
|
776
|
+
|
|
777
|
+
// ============================================================================
|
|
778
|
+
// Idempotent Producer Types
|
|
779
|
+
// ============================================================================
|
|
780
|
+
|
|
781
|
+
/**
|
|
782
|
+
* Options for creating an IdempotentProducer.
|
|
783
|
+
*/
|
|
784
|
+
export interface IdempotentProducerOptions {
|
|
785
|
+
/**
|
|
786
|
+
* Starting epoch (default: 0).
|
|
787
|
+
* Increment this on producer restart.
|
|
788
|
+
*/
|
|
789
|
+
epoch?: number
|
|
790
|
+
|
|
791
|
+
/**
|
|
792
|
+
* On 403 Forbidden (stale epoch), automatically retry with epoch+1.
|
|
793
|
+
* Useful for serverless/ephemeral producers.
|
|
794
|
+
* @default false
|
|
795
|
+
*/
|
|
796
|
+
autoClaim?: boolean
|
|
797
|
+
|
|
798
|
+
/**
|
|
799
|
+
* Maximum bytes before sending a batch.
|
|
800
|
+
* @default 1048576 (1MB)
|
|
801
|
+
*/
|
|
802
|
+
maxBatchBytes?: number
|
|
803
|
+
|
|
804
|
+
/**
|
|
805
|
+
* Maximum time to wait for more messages before sending batch (ms).
|
|
806
|
+
* @default 5
|
|
807
|
+
*/
|
|
808
|
+
lingerMs?: number
|
|
809
|
+
|
|
810
|
+
/**
|
|
811
|
+
* Maximum number of concurrent batches in flight.
|
|
812
|
+
* Higher values improve throughput at the cost of more memory.
|
|
813
|
+
* @default 5
|
|
814
|
+
*/
|
|
815
|
+
maxInFlight?: number
|
|
816
|
+
|
|
817
|
+
/**
|
|
818
|
+
* Custom fetch implementation.
|
|
819
|
+
*/
|
|
820
|
+
fetch?: typeof globalThis.fetch
|
|
821
|
+
|
|
822
|
+
/**
|
|
823
|
+
* AbortSignal for the producer lifecycle.
|
|
824
|
+
*/
|
|
825
|
+
signal?: AbortSignal
|
|
826
|
+
|
|
827
|
+
/**
|
|
828
|
+
* Callback for batch errors in fire-and-forget mode.
|
|
829
|
+
* Since append() returns immediately, errors are reported via this callback.
|
|
830
|
+
* @param error - The error that occurred
|
|
831
|
+
*/
|
|
832
|
+
onError?: (error: Error) => void
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
/**
|
|
836
|
+
* Result of an append operation from IdempotentProducer.
|
|
837
|
+
*/
|
|
838
|
+
export interface IdempotentAppendResult {
|
|
839
|
+
/**
|
|
840
|
+
* The offset after this message was appended.
|
|
841
|
+
*/
|
|
842
|
+
offset: Offset
|
|
843
|
+
|
|
844
|
+
/**
|
|
845
|
+
* Whether this was a duplicate (idempotent success).
|
|
846
|
+
*/
|
|
847
|
+
duplicate: boolean
|
|
848
|
+
}
|
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
|
+
}
|