@ultimat3/realtime 27.12.0 → 27.13.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ultimat3/realtime",
3
- "version": "27.12.0",
3
+ "version": "27.13.1",
4
4
  "description": "Three-tier realtime: channels, live queries, local-first sync — one protocol, one mutator shape",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -40,10 +40,10 @@
40
40
  "test": "bun test"
41
41
  },
42
42
  "dependencies": {
43
- "@ultimat3/core": "27.12.0",
44
- "@ultimat3/entity": "27.12.0",
45
- "@ultimat3/policy": "27.12.0",
46
- "@ultimat3/query": "27.12.0",
43
+ "@ultimat3/core": "27.13.1",
44
+ "@ultimat3/entity": "27.13.1",
45
+ "@ultimat3/policy": "27.13.1",
46
+ "@ultimat3/query": "27.13.1",
47
47
  "nats": "2.29.3"
48
48
  }
49
49
  }
@@ -43,6 +43,12 @@ export interface NatsClient {
43
43
  readonly connected: boolean;
44
44
  publish(subject: string, payload: Uint8Array): void;
45
45
  subscribe(subject: string, handler: NatsMessageHandler): NatsSubscription;
46
+ /**
47
+ * Answers once the server has processed every line sent before it — a `SUB` included. `subscribe`
48
+ * only WRITES that line: a message another connection publishes before the server reads it is
49
+ * not delivered, so "subscribed" means flushed.
50
+ */
51
+ flush(): Promise<void>;
46
52
  request(subject: string, payload: Uint8Array, options?: NatsRequestOptions): Promise<NatsMessage>;
47
53
  requestMany(
48
54
  subject: string,
package/src/nats-fake.ts CHANGED
@@ -167,6 +167,11 @@ class FakeNatsClient implements NatsClient {
167
167
  };
168
168
  }
169
169
 
170
+ /** The broker delivers synchronously, so a subscription is held the moment it is added. */
171
+ async flush(): Promise<void> {
172
+ this.#assertOpen();
173
+ }
174
+
170
175
  async request(
171
176
  subject: string,
172
177
  payload: Uint8Array,
@@ -14,7 +14,7 @@
14
14
  // This header said "every failure leaves here as an `UltimateError`" until 2026-08, which a reader
15
15
  // took as a guarantee it never was.
16
16
 
17
- import { finiteOption, renderThrowable } from '@ultimat3/core';
17
+ import { finiteOption, isUltimateError, renderThrowable } from '@ultimat3/core';
18
18
  import { connect, Events, headers, Match, type Msg, type MsgHdrs, type NatsConnection } from 'nats';
19
19
  import { TransportUnavailableError } from './errors';
20
20
  import {
@@ -113,6 +113,25 @@ class LibNatsClient implements NatsClient {
113
113
  return { unsubscribe: () => subscription.unsubscribe() };
114
114
  }
115
115
 
116
+ async flush(): Promise<void> {
117
+ // Bounded like a request: the library's flush waits on a PONG forever, and a server that went
118
+ // silent (`nats-black-hole.live.test.ts`) never sends one.
119
+ let timer: ReturnType<typeof setTimeout> | undefined;
120
+ const late = new Promise<never>((_, reject) => {
121
+ timer = setTimeout(
122
+ () => reject(unavailable(this.#target, `flush unanswered after ${this.#timeoutMs}ms`)),
123
+ this.#timeoutMs,
124
+ );
125
+ });
126
+ try {
127
+ await Promise.race([this.#connection.flush(), late]);
128
+ } catch (error) {
129
+ throw isUltimateError(error) ? error : unavailable(this.#target, `flush: ${describe(error)}`);
130
+ } finally {
131
+ clearTimeout(timer);
132
+ }
133
+ }
134
+
116
135
  async request(
117
136
  subject: string,
118
137
  payload: Uint8Array,
@@ -204,6 +204,11 @@ export class NatsTransport implements Transport {
204
204
  },
205
205
  (call) => this.#translating(`subscribe to ${subject}`, call),
206
206
  );
207
+ // Resolved only once the server HOLDS it: a publish from another process in the gap between
208
+ // writing the SUB and the server reading it was lost (`nats-black-hole.live.test.ts`, CI run
209
+ // 39145087176). Mid-reconnect there is no server to ask: the library re-sends the SUB on the
210
+ // way back, and `onReconnect` reports that gap. A flush that fails is that same case.
211
+ if (client.connected) await client.flush().catch(() => undefined);
207
212
  return { subject, unsubscribe: release };
208
213
  }
209
214