@zlink-systems/stream-connector 0.14.0 → 0.15.0

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.
@@ -9,7 +9,8 @@ function lz4PickleUncompressed(payload) {
9
9
  pickled.set(payload, 1);
10
10
  return pickled;
11
11
  }
12
- function lz4UnpicklePayload(payload, maxDecompressedSize = defaultMaxDecompressedPayloadSize) {
12
+ function lz4UnpicklePayload(payload, maxSize) {
13
+ const maxDecompressedSize = maxSize ?? defaultMaxDecompressedPayloadSize;
13
14
  if (payload.length === 0) {
14
15
  return new Uint8Array();
15
16
  }
@@ -152,7 +153,8 @@ function decodeStreamWireFrame(frame) {
152
153
  payload: frame.slice(6 + headerLength)
153
154
  };
154
155
  }
155
- function encodeStreamWireHeader(header, flags = defaultHeaderFlags) {
156
+ function encodeStreamWireHeader(header, flagOverrides) {
157
+ const flags = flagOverrides ?? defaultHeaderFlags;
156
158
  const reply = isReplyKind(header.kind);
157
159
  const packetName = reply ? "" : header.name;
158
160
  if (!reply) validateStreamWirePacketName(packetName);
@@ -213,7 +215,8 @@ function encodeStreamWireHeader(header, flags = defaultHeaderFlags) {
213
215
  }
214
216
  return buffer;
215
217
  }
216
- function decodeStreamWireHeader(header, flags = defaultHeaderFlags, includeFlow = true) {
218
+ function decodeStreamWireHeader(header, flagOverrides, includeFlow = true) {
219
+ const flags = flagOverrides ?? defaultHeaderFlags;
217
220
  let offset = 0;
218
221
  if (header.length < 5) {
219
222
  throw new Error("Stream header is incomplete.");
@@ -351,8 +354,8 @@ function decodeStreamWireMetadata(metadata) {
351
354
  function lz4PickleUncompressed2(payload) {
352
355
  return lz4PickleUncompressed(payload);
353
356
  }
354
- function lz4UnpicklePayload2(payload, maxDecompressedSize = defaultMaxDecompressedPayloadSize) {
355
- return lz4UnpicklePayload(payload, maxDecompressedSize);
357
+ function lz4UnpicklePayload2(payload, maxDecompressedSize) {
358
+ return lz4UnpicklePayload(payload, maxDecompressedSize ?? defaultMaxDecompressedPayloadSize);
356
359
  }
357
360
  function utf8Encode(value) {
358
361
  return new TextEncoder().encode(value);
@@ -1464,10 +1467,18 @@ var ZlinkStreamPendingRequests = class {
1464
1467
 
1465
1468
  // packages/stream-connector/src/Runtime/ZlinkStreamReceivedMessages.ts
1466
1469
  var ZlinkStreamReceivedMessages = class {
1467
- constructor(events) {
1470
+ /**
1471
+ * @param deliverOnArrival `Immediate` runs registered handlers on the receive
1472
+ * path; `Manual` leaves them queued until {@link pump} runs them on the
1473
+ * caller's thread (spec stream-connector 32 §7). Wait surfaces observe the
1474
+ * queue in both modes, so they never depend on this flag.
1475
+ */
1476
+ constructor(events, deliverOnArrival) {
1468
1477
  this.events = events;
1478
+ this.deliverOnArrival = deliverOnArrival;
1469
1479
  }
1470
1480
  handlers = /* @__PURE__ */ new Map();
1481
+ observers = /* @__PURE__ */ new Map();
1471
1482
  // A handler can be registered after messages for another name arrive, so the
1472
1483
  // queue is not a simple FIFO. Tombstones let us remove a deliverable entry
1473
1484
  // without shifting every later message on the hot receive path.
@@ -1483,7 +1494,7 @@ var ZlinkStreamReceivedMessages = class {
1483
1494
  this.handlers.set(name, set);
1484
1495
  }
1485
1496
  set.add(handler);
1486
- if (this.hasQueuedMessage(name)) {
1497
+ if (this.deliverOnArrival && this.hasQueuedMessage(name)) {
1487
1498
  queueMicrotask(() => this.scheduleDrain());
1488
1499
  }
1489
1500
  return subscription(() => {
@@ -1493,10 +1504,66 @@ var ZlinkStreamReceivedMessages = class {
1493
1504
  }
1494
1505
  });
1495
1506
  }
1507
+ /**
1508
+ * Registers a wait surface over the receive queue. Spec stream-connector 32
1509
+ * §7: these are not registered callbacks — they observe and consume the
1510
+ * packets the queue has not delivered yet, in both dispatch modes, so
1511
+ * `Manual` needs no dispatch pump to complete a wait. The queue is scanned in
1512
+ * a microtask so a message that arrived before the wait started is still
1513
+ * observed, and so the caller has its subscription in hand by then.
1514
+ */
1515
+ observe(name, observer) {
1516
+ validateName(name);
1517
+ let set = this.observers.get(name);
1518
+ if (set === void 0) {
1519
+ set = /* @__PURE__ */ new Set();
1520
+ this.observers.set(name, set);
1521
+ }
1522
+ set.add(observer);
1523
+ queueMicrotask(() => {
1524
+ if (this.observers.get(name)?.has(observer) === true) {
1525
+ this.offerQueued(name, observer);
1526
+ }
1527
+ });
1528
+ return subscription(() => {
1529
+ set.delete(observer);
1530
+ if (set.size === 0 && this.observers.get(name) === set) {
1531
+ this.observers.delete(name);
1532
+ }
1533
+ });
1534
+ }
1496
1535
  enqueue(message, signal) {
1536
+ for (const observer of [...this.observers.get(message.name) ?? []]) {
1537
+ if (observer(message)) {
1538
+ return;
1539
+ }
1540
+ }
1497
1541
  this.queue.push({ message, signal });
1498
1542
  this.queuedCount += 1;
1543
+ if (this.deliverOnArrival) {
1544
+ this.scheduleDrain();
1545
+ }
1546
+ }
1547
+ /**
1548
+ * Runs the registered handlers the receive path left queued. `Manual` calls
1549
+ * this from `dispatch`; `Immediate` has already drained on arrival.
1550
+ */
1551
+ async pump() {
1499
1552
  this.scheduleDrain();
1553
+ await this.drainTask;
1554
+ }
1555
+ offerQueued(name, observer) {
1556
+ for (let index = this.queueHead; index < this.queue.length; index += 1) {
1557
+ const queued = this.queue[index];
1558
+ if (queued === void 0 || queued.message.name !== name) {
1559
+ continue;
1560
+ }
1561
+ if (!observer(queued.message)) {
1562
+ continue;
1563
+ }
1564
+ this.removeAt(index);
1565
+ return;
1566
+ }
1500
1567
  }
1501
1568
  scheduleDrain() {
1502
1569
  if (this.drainTask !== void 0) {
@@ -1504,7 +1571,7 @@ var ZlinkStreamReceivedMessages = class {
1504
1571
  }
1505
1572
  this.drainTask = this.drain().finally(() => {
1506
1573
  this.drainTask = void 0;
1507
- if (this.findDeliverableIndex() >= 0) {
1574
+ if (this.deliverOnArrival && this.findDeliverableIndex() >= 0) {
1508
1575
  this.scheduleDrain();
1509
1576
  }
1510
1577
  });
@@ -1513,10 +1580,7 @@ var ZlinkStreamReceivedMessages = class {
1513
1580
  for (let index = this.findDeliverableIndex(); index >= 0; index = this.findDeliverableIndex()) {
1514
1581
  const queued = this.queue[index];
1515
1582
  if (queued === void 0) continue;
1516
- this.queue[index] = void 0;
1517
- this.queuedCount -= 1;
1518
- this.advanceHead();
1519
- this.compactQueue();
1583
+ this.removeAt(index);
1520
1584
  const { message, signal } = queued;
1521
1585
  const handlers = [...this.handlers.get(message.name)];
1522
1586
  for (const handler of handlers) {
@@ -1532,6 +1596,12 @@ var ZlinkStreamReceivedMessages = class {
1532
1596
  }
1533
1597
  }
1534
1598
  }
1599
+ removeAt(index) {
1600
+ this.queue[index] = void 0;
1601
+ this.queuedCount -= 1;
1602
+ this.advanceHead();
1603
+ this.compactQueue();
1604
+ }
1535
1605
  findDeliverableIndex() {
1536
1606
  for (let index = this.queueHead; index < this.queue.length; index += 1) {
1537
1607
  const queued = this.queue[index];
@@ -1785,15 +1855,19 @@ function decodeRemoteError(protocol, header, payload) {
1785
1855
 
1786
1856
  // packages/stream-connector/src/Runtime/ZlinkStreamConnectorLifecycle.ts
1787
1857
  var ZlinkStreamConnectorLifecycle = class {
1788
- constructor(options, pendingRequests, frameSender, receiveDispatcher, events, metrics) {
1858
+ constructor(options, pendingRequests, frameSender, receiveDispatcher, receivedMessages, events, metrics) {
1789
1859
  this.options = options;
1790
1860
  this.pendingRequests = pendingRequests;
1791
1861
  this.frameSender = frameSender;
1792
1862
  this.receiveDispatcher = receiveDispatcher;
1863
+ this.receivedMessages = receivedMessages;
1793
1864
  this.events = events;
1794
1865
  this.metrics = metrics;
1795
1866
  }
1796
1867
  receiveLoopAbort;
1868
+ receiveLoopSleeping = false;
1869
+ receiveLoopWake;
1870
+ receiveLoopSettled = [];
1797
1871
  currentConnection;
1798
1872
  connectionGeneration = 0;
1799
1873
  currentState = "created" /* Created */;
@@ -1910,16 +1984,21 @@ var ZlinkStreamConnectorLifecycle = class {
1910
1984
  if (errors.length === 1) throw errors[0];
1911
1985
  if (errors.length > 1) throw new AggregateError(errors, "Stream connector close failed.");
1912
1986
  }
1987
+ /**
1988
+ * Spec stream-connector 32 §7: `dispatch` runs the callbacks the receive loop
1989
+ * queued, it does not drive the transport. Receiving is the receive loop's
1990
+ * job in both dispatch modes, which is what lets a `Manual` consumer complete
1991
+ * a `waitFor` without pumping, and what keeps this call from blocking on an
1992
+ * idle connection. In `Manual` it first lets the loop settle whatever has
1993
+ * already arrived, so a packet the transport is holding is delivered by this
1994
+ * pump rather than the next one.
1995
+ */
1913
1996
  async dispatch(signal) {
1914
- const connection = this.currentConnection;
1915
- const generation = this.connectionGeneration;
1916
- try {
1917
- await this.dispatchAvailable(connection, generation, signal);
1918
- } catch (cause) {
1919
- const error = toStreamError(cause, "frameDecodeFailed" /* FrameDecodeFailed */, "Stream dispatch failed.");
1920
- await this.disconnectForTransportFailure(error, connection, generation);
1921
- throw new ZlinkStreamException(error);
1997
+ throwIfAborted(signal);
1998
+ if (this.options.dispatchMode !== "immediate" /* Immediate */) {
1999
+ await this.settleReceiveLoop();
1922
2000
  }
2001
+ await this.receivedMessages.pump();
1923
2002
  }
1924
2003
  connectionForSend() {
1925
2004
  if (this.currentConnection === void 0 || this.currentState !== "connected" /* Connected */) {
@@ -1988,8 +2067,12 @@ var ZlinkStreamConnectorLifecycle = class {
1988
2067
  this.heartbeatTimer = void 0;
1989
2068
  }
1990
2069
  }
2070
+ // Spec stream-connector 32 §7: the receive loop runs in both dispatch modes.
2071
+ // `Manual` only changes what the loop does with a frame — it queues the
2072
+ // registered callbacks instead of running them — never whether frames are
2073
+ // read off the transport.
1991
2074
  startReceiveLoop() {
1992
- if (this.options.dispatchMode !== "immediate" /* Immediate */ || this.currentConnection?.read === void 0) {
2075
+ if (this.currentConnection?.read === void 0) {
1993
2076
  return;
1994
2077
  }
1995
2078
  this.stopReceiveLoop();
@@ -2002,19 +2085,66 @@ var ZlinkStreamConnectorLifecycle = class {
2002
2085
  stopReceiveLoop() {
2003
2086
  this.receiveLoopAbort?.abort();
2004
2087
  this.receiveLoopAbort = void 0;
2088
+ this.receiveLoopSleeping = false;
2089
+ this.releaseReceiveLoopSettled();
2005
2090
  }
2006
2091
  async runReceiveLoop(connection, generation, signal) {
2007
2092
  try {
2008
2093
  while (this.shouldContinueReceiveLoop(connection, generation, signal)) {
2009
2094
  const dispatched = await this.dispatchAvailable(connection, generation, signal);
2010
2095
  if (!dispatched && this.shouldContinueReceiveLoop(connection, generation, signal)) {
2011
- await delay(1, signal);
2096
+ await this.sleepUntilWork(signal);
2012
2097
  }
2013
2098
  }
2014
2099
  } catch (cause) {
2015
2100
  if (signal.aborted) return;
2016
2101
  const error = toStreamError(cause, "frameDecodeFailed" /* FrameDecodeFailed */, "Receive loop failed.");
2017
2102
  await this.disconnectForTransportFailure(error, connection, generation);
2103
+ } finally {
2104
+ this.receiveLoopSleeping = false;
2105
+ this.releaseReceiveLoopSettled();
2106
+ }
2107
+ }
2108
+ // A transport whose read resolves only when a frame arrives parks the loop
2109
+ // inside that read; one that reports "nothing available" instead parks it
2110
+ // here. Both are the loop waiting for new data, and `dispatch` treats them
2111
+ // the same way.
2112
+ async sleepUntilWork(signal) {
2113
+ this.receiveLoopSleeping = true;
2114
+ this.releaseReceiveLoopSettled();
2115
+ try {
2116
+ await new Promise((resolve) => {
2117
+ const finish = () => {
2118
+ clearTimeout(timer);
2119
+ signal.removeEventListener("abort", onAbort);
2120
+ this.receiveLoopWake = void 0;
2121
+ resolve();
2122
+ };
2123
+ const onAbort = () => finish();
2124
+ const timer = setTimeout(finish, 1);
2125
+ signal.addEventListener("abort", onAbort, { once: true });
2126
+ this.receiveLoopWake = finish;
2127
+ });
2128
+ } finally {
2129
+ this.receiveLoopSleeping = false;
2130
+ }
2131
+ }
2132
+ // Returns once the loop has consumed everything the transport already had.
2133
+ // A loop that is mid-batch, or parked inside a read that has not produced a
2134
+ // frame, is already caught up, so only a sleeping loop is woken and awaited.
2135
+ async settleReceiveLoop() {
2136
+ if (this.receiveLoopAbort === void 0 || !this.receiveLoopSleeping) {
2137
+ return;
2138
+ }
2139
+ const settled = new Promise((resolve) => {
2140
+ this.receiveLoopSettled.push(resolve);
2141
+ });
2142
+ this.receiveLoopWake?.();
2143
+ await settled;
2144
+ }
2145
+ releaseReceiveLoopSettled() {
2146
+ for (const resolve of this.receiveLoopSettled.splice(0)) {
2147
+ resolve();
2018
2148
  }
2019
2149
  }
2020
2150
  shouldContinueReceiveLoop(connection, generation, signal) {
@@ -2417,7 +2547,10 @@ var DefaultZlinkStreamConnector = class {
2417
2547
  const metrics = new ZlinkStreamRuntimeMetrics(this.options);
2418
2548
  const protocol = new ZlinkStreamFrameProtocol(this.options);
2419
2549
  this.frameSender = new ZlinkStreamFrameSender(protocol, flowContext, metrics);
2420
- this.receivedMessages = new ZlinkStreamReceivedMessages(this.events);
2550
+ this.receivedMessages = new ZlinkStreamReceivedMessages(
2551
+ this.events,
2552
+ this.options.dispatchMode === "immediate" /* Immediate */
2553
+ );
2421
2554
  this.receiveDispatcher = new ZlinkStreamReceiveDispatcher(
2422
2555
  protocol,
2423
2556
  this.pendingRequests,
@@ -2433,6 +2566,7 @@ var DefaultZlinkStreamConnector = class {
2433
2566
  this.pendingRequests,
2434
2567
  this.frameSender,
2435
2568
  this.receiveDispatcher,
2569
+ this.receivedMessages,
2436
2570
  this.events,
2437
2571
  metrics
2438
2572
  );
@@ -2553,7 +2687,10 @@ var DefaultZlinkStreamConnector = class {
2553
2687
  finish(connectorError("requestTimeout" /* RequestTimeout */, "Wait for stream message timed out."));
2554
2688
  }, timeoutMs);
2555
2689
  signal?.addEventListener("abort", onAbort, { once: true });
2556
- disposable = this.receivedMessages.on(name, (message) => {
2690
+ disposable = this.receivedMessages.observe(name, (message) => {
2691
+ if (done) {
2692
+ return false;
2693
+ }
2557
2694
  try {
2558
2695
  const decoded = {
2559
2696
  name: message.name,
@@ -2562,12 +2699,14 @@ var DefaultZlinkStreamConnector = class {
2562
2699
  flowId: message.flowId,
2563
2700
  flowOrigin: message.flowOrigin
2564
2701
  };
2565
- if (predicate(decoded)) {
2566
- finish(void 0, decoded);
2702
+ if (!predicate(decoded)) {
2703
+ return false;
2567
2704
  }
2705
+ finish(void 0, decoded);
2568
2706
  } catch (cause) {
2569
2707
  finish(cause);
2570
2708
  }
2709
+ return true;
2571
2710
  });
2572
2711
  });
2573
2712
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zlink-systems/stream-connector",
3
- "version": "0.14.0",
3
+ "version": "0.15.0",
4
4
  "license": "SEE LICENSE IN LICENSE",
5
5
  "repository": {
6
6
  "type": "git",
@@ -21,6 +21,6 @@
21
21
  }
22
22
  },
23
23
  "dependencies": {
24
- "@zlink-systems/stream-wire": "0.14.0"
24
+ "@zlink-systems/stream-wire": "0.15.0"
25
25
  }
26
26
  }