@agentuity/core 0.0.104 → 0.0.106

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.
@@ -2,10 +2,18 @@ import { safeStringify } from '../json';
2
2
  import { FetchAdapter, FetchResponse } from './adapter';
3
3
  import { buildUrl, toServiceException } from './_util';
4
4
  import { StructuredError } from '../error';
5
- import {
6
- WritableStream as WebWritableStream,
7
- TransformStream as WebTransformStream,
8
- } from 'stream/web';
5
+
6
+ // Use Web API streams - in Node.js/Bun, import from 'stream/web' which provides proper Web API
7
+ // In browsers, use globalThis directly
8
+ // Check for Node.js/Bun by looking for process.versions.node
9
+ const isNode =
10
+ typeof process !== 'undefined' && process.versions != null && process.versions.node != null;
11
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
12
+ const streamWeb = isNode ? require('stream/web') : globalThis;
13
+ const NativeWritableStream = streamWeb.WritableStream as typeof WritableStream;
14
+ const NativeReadableStream = streamWeb.ReadableStream as typeof ReadableStream;
15
+ const NativeCompressionStream = (streamWeb.CompressionStream ??
16
+ globalThis.CompressionStream) as typeof CompressionStream;
9
17
 
10
18
  /**
11
19
  * Properties for creating a stream
@@ -270,29 +278,32 @@ const encoder = new TextEncoder();
270
278
  const ReadStreamFailedError = StructuredError('ReadStreamFailedError')<{ status: number }>();
271
279
 
272
280
  /**
273
- * A writable stream implementation that extends WritableStream
281
+ * A writable stream implementation using composition (browser-compatible)
282
+ * This approach works across all environments since native WritableStream can't be properly extended
274
283
  */
275
- class StreamImpl extends WebWritableStream implements Stream {
284
+ class StreamImpl implements Stream {
276
285
  public readonly id: string;
277
286
  public readonly url: string;
278
- #activeWriter: WritableStreamDefaultWriter<Uint8Array> | null = null;
287
+ readonly #writable: WritableStream<Uint8Array>;
279
288
  #compressed: boolean;
280
289
  #adapter: FetchAdapter;
281
- #sink: UnderlyingSink;
290
+ #sink: UnderlyingSinkState;
291
+ #closed = false;
282
292
 
283
293
  constructor(
284
294
  id: string,
285
295
  url: string,
286
296
  compressed: boolean,
287
- underlyingSink: UnderlyingSink,
297
+ sink: UnderlyingSinkState,
298
+ writable: WritableStream<Uint8Array>,
288
299
  adapter: FetchAdapter
289
300
  ) {
290
- super(underlyingSink);
291
301
  this.id = id;
292
302
  this.url = url;
293
303
  this.#compressed = compressed;
294
304
  this.#adapter = adapter;
295
- this.#sink = underlyingSink;
305
+ this.#sink = sink;
306
+ this.#writable = writable;
296
307
  }
297
308
 
298
309
  get bytesWritten(): number {
@@ -303,6 +314,11 @@ class StreamImpl extends WebWritableStream implements Stream {
303
314
  return this.#compressed;
304
315
  }
305
316
 
317
+ // WritableStream interface properties
318
+ get locked(): boolean {
319
+ return this.#writable.locked;
320
+ }
321
+
306
322
  /**
307
323
  * Write data to the stream
308
324
  */
@@ -320,29 +336,21 @@ class StreamImpl extends WebWritableStream implements Stream {
320
336
  binaryChunk = encoder.encode(String(chunk));
321
337
  }
322
338
 
323
- if (!this.#activeWriter) {
324
- this.#activeWriter = super.getWriter();
325
- }
326
- await this.#activeWriter.write(binaryChunk);
339
+ // Delegate to the underlying sink's write method
340
+ await this.#sink.write(binaryChunk);
327
341
  }
328
342
 
329
343
  /**
330
- * Override close to handle already closed streams gracefully
331
- * This method safely closes the stream, or silently returns if already closed
344
+ * Close the stream gracefully, handling already closed streams without error
332
345
  */
333
346
  async close(): Promise<void> {
334
- try {
335
- // If we have an active writer from write() calls, use that
336
- if (this.#activeWriter) {
337
- const writer = this.#activeWriter;
338
- this.#activeWriter = null;
339
- await writer.close();
340
- return;
341
- }
347
+ if (this.#closed) {
348
+ return;
349
+ }
350
+ this.#closed = true;
342
351
 
343
- // Otherwise, get a writer and close it
344
- const writer = super.getWriter();
345
- await writer.close();
352
+ try {
353
+ await this.#sink.close();
346
354
  } catch (error) {
347
355
  // If we get a TypeError about the stream being closed, locked, or errored,
348
356
  // that means pipeTo() or another operation already closed it or it's in use
@@ -353,18 +361,32 @@ class StreamImpl extends WebWritableStream implements Stream {
353
361
  error.message.includes('Cannot close'))
354
362
  ) {
355
363
  // Silently return - this is the desired behavior
356
- return Promise.resolve();
364
+ return;
357
365
  }
358
366
  // If the stream is locked, try to close the underlying writer
359
367
  if (error instanceof TypeError && error.message.includes('locked')) {
360
368
  // Best-effort closure for locked streams
361
- return Promise.resolve();
369
+ return;
362
370
  }
363
371
  // Re-throw any other errors
364
372
  throw error;
365
373
  }
366
374
  }
367
375
 
376
+ /**
377
+ * Abort the stream with an optional reason
378
+ */
379
+ abort(reason?: unknown): Promise<void> {
380
+ return this.#writable.abort(reason);
381
+ }
382
+
383
+ /**
384
+ * Get a writer for the underlying stream
385
+ */
386
+ getWriter(): WritableStreamDefaultWriter<Uint8Array> {
387
+ return this.#writable.getWriter();
388
+ }
389
+
368
390
  /**
369
391
  * Get a ReadableStream that streams from the internal URL
370
392
  *
@@ -377,7 +399,8 @@ class StreamImpl extends WebWritableStream implements Stream {
377
399
  const url = this.url;
378
400
  const adapter = this.#adapter;
379
401
  let ac: AbortController | null = null;
380
- return new ReadableStream({
402
+ // Use native ReadableStream to avoid polyfill interference
403
+ return new NativeReadableStream({
381
404
  async start(controller) {
382
405
  try {
383
406
  ac = new AbortController();
@@ -442,17 +465,31 @@ const StreamWriterInitializationError = StructuredError(
442
465
 
443
466
  const StreamAPIError = StructuredError('StreamAPIError')<{ status: number }>();
444
467
 
445
- // Create a WritableStream that writes to the backend stream
446
- // Create the underlying sink that will handle the actual streaming
447
- class UnderlyingSink {
468
+ /**
469
+ * Check if compression is available in the current environment.
470
+ * CompressionStream is available in:
471
+ * - Node.js 18+ (via stream/web)
472
+ * - Chrome 80+
473
+ * - Safari 16.4+
474
+ * - Firefox 113+
475
+ */
476
+ function isCompressionAvailable(): boolean {
477
+ return typeof NativeCompressionStream !== 'undefined' && NativeCompressionStream !== null;
478
+ }
479
+
480
+ // State object that handles the actual streaming to the backend
481
+ // This is used by StreamImpl to manage write operations
482
+ class UnderlyingSinkState {
448
483
  adapter: FetchAdapter;
449
484
  abortController: AbortController | null = null;
450
485
  writer: WritableStreamDefaultWriter<Uint8Array> | null = null;
486
+ writable: WritableStream<Uint8Array> | null = null;
451
487
  putRequestPromise: Promise<FetchResponse<unknown>> | null = null;
452
488
  total = 0;
453
489
  closed = false;
454
490
  url: string;
455
491
  props?: CreateStreamProps;
492
+ compressionEnabled = false;
456
493
 
457
494
  constructor(url: string, adapter: FetchAdapter, props?: CreateStreamProps) {
458
495
  this.url = url;
@@ -460,94 +497,83 @@ class UnderlyingSink {
460
497
  this.props = props;
461
498
  }
462
499
 
463
- async start() {
500
+ async start(): Promise<WritableStream<Uint8Array>> {
464
501
  // Create AbortController for the fetch request
465
502
  this.abortController = new AbortController();
466
503
 
467
- // Create a ReadableStream to pipe data to the PUT request
468
- // eslint-disable-next-line prefer-const
469
- let { readable, writable } = new WebTransformStream<Uint8Array, Uint8Array>();
470
-
471
- // If compression is enabled, add gzip transform
472
- if (this.props?.compress) {
473
- const { Readable, Writable } = await import('node:stream');
474
-
475
- // Create a new transform for the compressed output
476
- const { readable: compressedReadable, writable: compressedWritable } =
477
- new WebTransformStream<Uint8Array, Uint8Array>();
478
-
479
- // Set up compression pipeline
480
- const { createGzip } = await import('node:zlib');
481
- const gzipStream = createGzip();
482
- const nodeWritable = Writable.toWeb(gzipStream) as WritableStream<Uint8Array>;
483
-
484
- // Pipe gzip output to the compressed readable
485
- const gzipReader = Readable.toWeb(gzipStream) as ReadableStream<Uint8Array>;
486
- gzipReader.pipeTo(compressedWritable).catch((error) => {
487
- this.abortController?.abort(error);
488
- this.writer?.abort(error).catch(() => {});
489
- });
490
-
491
- // Chain: writable -> gzip -> compressedReadable
492
- readable.pipeTo(nodeWritable).catch((error) => {
493
- this.abortController?.abort(error);
494
- this.writer?.abort(error).catch(() => {});
495
- });
496
- readable = compressedReadable;
497
- }
504
+ // Create a pass-through WritableStream that writes to a ReadableStream
505
+ // Use native streams captured at module load to avoid polyfill interference
506
+ let readableController: ReadableStreamDefaultController<Uint8Array>;
507
+ const readable = new NativeReadableStream<Uint8Array>({
508
+ start: (controller) => {
509
+ readableController = controller;
510
+ },
511
+ });
512
+
513
+ // Create a WritableStream that pushes chunks to the ReadableStream
514
+ this.writable = new NativeWritableStream<Uint8Array>({
515
+ write: (chunk) => {
516
+ readableController.enqueue(chunk);
517
+ this.total += chunk.length;
518
+ },
519
+ close: () => {
520
+ readableController.close();
521
+ },
522
+ abort: (reason) => {
523
+ readableController.error(reason);
524
+ },
525
+ });
498
526
 
499
- this.writer = writable.getWriter();
527
+ // If compression is enabled and available, pipe through gzip
528
+ // Gracefully skip compression if CompressionStream is not available
529
+ let bodyStream: ReadableStream<Uint8Array> = readable;
530
+ this.compressionEnabled = !!(this.props?.compress && isCompressionAvailable());
531
+ if (this.compressionEnabled) {
532
+ const compressionStream = new NativeCompressionStream('gzip');
533
+ bodyStream = readable.pipeThrough(compressionStream);
534
+ }
500
535
 
501
536
  // Start the PUT request with the readable stream as body
502
537
  const headers: Record<string, string> = {
503
538
  'Content-Type': this.props?.contentType || 'application/octet-stream',
504
539
  };
505
540
 
506
- if (this.props?.compress) {
541
+ if (this.compressionEnabled) {
507
542
  headers['Content-Encoding'] = 'gzip';
508
543
  }
509
544
 
510
545
  this.putRequestPromise = this.adapter.invoke(this.url, {
511
546
  method: 'PUT',
512
547
  headers,
513
- body: readable,
548
+ body: bodyStream,
514
549
  signal: this.abortController.signal,
515
550
  duplex: 'half',
516
551
  });
552
+
553
+ // Acquire writer immediately to prevent race conditions in concurrent write() calls
554
+ this.writer = this.writable.getWriter();
555
+
556
+ return this.writable;
517
557
  }
518
558
 
519
- async write(chunk: string | Uint8Array | ArrayBuffer | Buffer | object) {
559
+ async write(chunk: Uint8Array) {
520
560
  if (!this.writer) {
521
561
  throw new StreamWriterInitializationError();
522
562
  }
523
- // Convert input to Uint8Array if needed
524
- let binaryChunk: Uint8Array;
525
- if (chunk instanceof Uint8Array) {
526
- binaryChunk = chunk;
527
- } else if (typeof chunk === 'string') {
528
- binaryChunk = new TextEncoder().encode(chunk);
529
- } else if (chunk instanceof ArrayBuffer) {
530
- binaryChunk = new Uint8Array(chunk);
531
- } else if (typeof chunk === 'object' && chunk !== null) {
532
- // Convert objects to JSON string, then to bytes
533
- binaryChunk = new TextEncoder().encode(safeStringify(chunk));
534
- } else {
535
- // Handle primitive types (number, boolean, etc.)
536
- binaryChunk = new TextEncoder().encode(String(chunk));
537
- }
538
- // Write the chunk to the transform stream, which pipes to the PUT request
539
- await this.writer.write(binaryChunk);
540
- this.total += binaryChunk.length;
563
+ await this.writer.write(chunk);
541
564
  }
565
+
542
566
  async close() {
543
567
  if (this.closed) {
544
568
  return;
545
569
  }
546
570
  this.closed = true;
571
+
547
572
  if (this.writer) {
548
573
  await this.writer.close();
549
574
  this.writer = null;
550
575
  }
576
+
551
577
  // Wait for the PUT request to complete
552
578
  if (this.putRequestPromise) {
553
579
  try {
@@ -641,13 +667,16 @@ export class StreamStorageService implements StreamStorage {
641
667
  });
642
668
  if (res.ok) {
643
669
  const streamUrl = buildUrl(this.#baseUrl, res.data.id);
644
- const underlyingSink = new UnderlyingSink(streamUrl, this.#adapter, props);
670
+ const sink = new UnderlyingSinkState(streamUrl, this.#adapter, props);
671
+ // Initialize the sink (start the PUT request) and get the writable stream
672
+ const writable = await sink.start();
645
673
 
646
674
  const stream = new StreamImpl(
647
675
  res.data.id,
648
676
  streamUrl,
649
- props?.compress ?? false,
650
- underlyingSink,
677
+ sink.compressionEnabled,
678
+ sink,
679
+ writable,
651
680
  this.#adapter
652
681
  );
653
682