@ultimat3/realtime 1.2.0 → 3.0.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.
- package/CLAUDE.md +641 -0
- package/README.md +336 -19
- package/package.json +6 -3
- package/src/apply-patches.ts +60 -0
- package/src/change-buffer.ts +77 -11
- package/src/channel.ts +202 -20
- package/src/client-contract.ts +81 -0
- package/src/client-frames.ts +175 -0
- package/src/client-heartbeat.ts +77 -0
- package/src/client-mutations.ts +114 -0
- package/src/client-topics.ts +54 -0
- package/src/client.ts +307 -273
- package/src/cursor.ts +7 -1
- package/src/errors.ts +193 -4
- package/src/frame-lanes.ts +58 -0
- package/src/hooks.ts +19 -5
- package/src/identity-map.ts +141 -0
- package/src/index.ts +99 -28
- package/src/json.ts +38 -1
- package/src/live-contract.ts +67 -0
- package/src/live-definition.ts +16 -11
- package/src/live-fanout.ts +150 -0
- package/src/live-query.ts +215 -268
- package/src/live-rows.ts +143 -0
- package/src/local-store.ts +86 -43
- package/src/nats-client.ts +132 -0
- package/src/nats-fake.ts +389 -344
- package/src/nats-jetstream.ts +21 -20
- package/src/nats-kv.ts +7 -7
- package/src/nats-lib-client.ts +210 -0
- package/src/nats-transport.ts +109 -138
- package/src/offline-queue.ts +146 -30
- package/src/pg-entity-row.ts +99 -31
- package/src/pg-replication.ts +84 -27
- package/src/pg-socket.ts +4 -1
- package/src/policy-gate.ts +13 -5
- package/src/presence.ts +76 -6
- package/src/query-hook.ts +56 -0
- package/src/query-window.ts +187 -0
- package/src/rebase.ts +68 -8
- package/src/replicator.ts +84 -11
- package/src/socket.ts +225 -34
- package/src/subscriber-gate.ts +209 -0
- package/src/subscription-book.ts +237 -0
- package/src/sync-auth.ts +124 -0
- package/src/sync-frames.ts +185 -0
- package/src/sync-listen.ts +73 -0
- package/src/sync-node.ts +324 -248
- package/src/sync-protocol.ts +115 -24
- package/src/sync-upgrade.ts +124 -0
- package/src/thundering-herd.ts +21 -0
- package/src/transport-env.ts +3 -3
- package/src/type-pins.ts +72 -0
- package/src/window-lock.ts +21 -0
- package/src/nats-commands.ts +0 -97
- package/src/nats-connection-fixture.ts +0 -105
- package/src/nats-connection.ts +0 -464
- package/src/nats-protocol.ts +0 -222
- package/src/nats-socket.ts +0 -236
- package/src/pg-connection-fixture.ts +0 -215
- package/src/pg-replication-fixture.ts +0 -261
package/src/nats-jetstream.ts
CHANGED
|
@@ -3,8 +3,7 @@
|
|
|
3
3
|
// built from a validated bucket name, because a stream name goes straight into a request subject.
|
|
4
4
|
|
|
5
5
|
import { TransportProtocolError, TransportUnavailableError } from './errors';
|
|
6
|
-
import type {
|
|
7
|
-
import type { NatsHeaders, NatsMessage } from './nats-protocol';
|
|
6
|
+
import type { NatsClient, NatsHeaders, NatsMessage } from './nats-client';
|
|
8
7
|
|
|
9
8
|
const encoder = new TextEncoder();
|
|
10
9
|
const decoder = new TextDecoder();
|
|
@@ -91,22 +90,22 @@ const errorOf = (body: Record<string, unknown>): JsError | undefined => {
|
|
|
91
90
|
|
|
92
91
|
/** One JetStream API call. The API always answers with json, and reports failure inside it. */
|
|
93
92
|
export async function jsRequest(
|
|
94
|
-
|
|
93
|
+
client: NatsClient,
|
|
95
94
|
subject: string,
|
|
96
95
|
body: unknown,
|
|
97
96
|
): Promise<{ readonly data: Record<string, unknown>; readonly error: JsError | undefined }> {
|
|
98
|
-
const reply = await
|
|
97
|
+
const reply = await client.request(subject, encoder.encode(JSON.stringify(body ?? {})));
|
|
99
98
|
const data = asObject(reply, subject);
|
|
100
99
|
return { data, error: errorOf(data) };
|
|
101
100
|
}
|
|
102
101
|
|
|
103
102
|
/** `jsRequest`, but a JetStream error is thrown rather than returned. */
|
|
104
103
|
export async function jsCall(
|
|
105
|
-
|
|
104
|
+
client: NatsClient,
|
|
106
105
|
subject: string,
|
|
107
106
|
body: unknown,
|
|
108
107
|
): Promise<Record<string, unknown>> {
|
|
109
|
-
const { data, error } = await jsRequest(
|
|
108
|
+
const { data, error } = await jsRequest(client, subject, body);
|
|
110
109
|
if (error === undefined) return data;
|
|
111
110
|
throw new TransportUnavailableError({
|
|
112
111
|
transport: 'nats',
|
|
@@ -122,14 +121,14 @@ export const kvSubject = (bucket: string, key: string): string => `$KV.${bucket}
|
|
|
122
121
|
* rather than in an ops runbook is what lets `x dev` and a fresh cluster boot the same way.
|
|
123
122
|
*/
|
|
124
123
|
export async function ensureKvBucket(
|
|
125
|
-
|
|
124
|
+
client: NatsClient,
|
|
126
125
|
bucket: string,
|
|
127
126
|
ttlMs: number,
|
|
128
127
|
): Promise<void> {
|
|
129
128
|
assertBucket(bucket);
|
|
130
|
-
assertServerVersion(
|
|
129
|
+
assertServerVersion(client.version);
|
|
131
130
|
const stream = kvStream(bucket);
|
|
132
|
-
const info = await jsRequest(
|
|
131
|
+
const info = await jsRequest(client, `$JS.API.STREAM.INFO.${stream}`, {});
|
|
133
132
|
if (info.error === undefined) return;
|
|
134
133
|
if (info.error.code !== STATUS_NOT_FOUND) {
|
|
135
134
|
throw new TransportUnavailableError({
|
|
@@ -137,7 +136,7 @@ export async function ensureKvBucket(
|
|
|
137
136
|
reason: `could not read stream ${stream}: ${info.error.description}`,
|
|
138
137
|
});
|
|
139
138
|
}
|
|
140
|
-
await jsCall(
|
|
139
|
+
await jsCall(client, `$JS.API.STREAM.CREATE.${stream}`, {
|
|
141
140
|
name: stream,
|
|
142
141
|
subjects: [`$KV.${bucket}.>`],
|
|
143
142
|
// History of one: presence is a current value, never a log. `discard: new` keeps a full
|
|
@@ -157,26 +156,26 @@ export async function ensureKvBucket(
|
|
|
157
156
|
}
|
|
158
157
|
|
|
159
158
|
const recordOf = (message: NatsMessage, bucket: string): KvRecord | undefined => {
|
|
160
|
-
const subject = message.
|
|
159
|
+
const subject = message.header('Nats-Subject');
|
|
161
160
|
if (subject === undefined) return undefined;
|
|
162
|
-
const stamp = message.
|
|
161
|
+
const stamp = message.header('Nats-Time-Stamp');
|
|
163
162
|
const writtenAt = stamp === undefined ? undefined : Date.parse(stamp);
|
|
164
163
|
return {
|
|
165
164
|
key: subject.slice(`$KV.${bucket}.`.length),
|
|
166
165
|
value: decoder.decode(message.payload),
|
|
167
166
|
writtenAt: writtenAt === undefined || Number.isNaN(writtenAt) ? undefined : writtenAt,
|
|
168
|
-
operation: message.
|
|
167
|
+
operation: message.header('KV-Operation'),
|
|
169
168
|
};
|
|
170
169
|
};
|
|
171
170
|
|
|
172
171
|
/** The current value for one key, or `undefined` when the server has none. */
|
|
173
172
|
export async function kvGet(
|
|
174
|
-
|
|
173
|
+
client: NatsClient,
|
|
175
174
|
bucket: string,
|
|
176
175
|
key: string,
|
|
177
176
|
): Promise<KvRecord | undefined> {
|
|
178
177
|
const subject = `$JS.API.DIRECT.GET.${kvStream(bucket)}.${kvSubject(bucket, key)}`;
|
|
179
|
-
const reply = await
|
|
178
|
+
const reply = await client.request(subject, new Uint8Array(0));
|
|
180
179
|
if (reply.status === STATUS_NOT_FOUND) return undefined;
|
|
181
180
|
return recordOf(reply, bucket);
|
|
182
181
|
}
|
|
@@ -186,19 +185,21 @@ export async function kvGet(
|
|
|
186
185
|
* messages and then an empty `204 EOB`; a prefix nobody has written answers `404` and nothing else.
|
|
187
186
|
*/
|
|
188
187
|
export async function kvLast(
|
|
189
|
-
|
|
188
|
+
client: NatsClient,
|
|
190
189
|
bucket: string,
|
|
191
190
|
filter: string,
|
|
192
191
|
batch = 1_000,
|
|
193
192
|
): Promise<readonly KvRecord[]> {
|
|
194
193
|
const subject = `$JS.API.DIRECT.GET.${kvStream(bucket)}`;
|
|
195
194
|
const body = { multi_last: [kvSubject(bucket, filter)], batch };
|
|
196
|
-
const replies = await
|
|
195
|
+
const replies = await client.requestMany(subject, encoder.encode(JSON.stringify(body)), {
|
|
197
196
|
until: (message) => message.status === STATUS_EOB || message.status === STATUS_NOT_FOUND,
|
|
198
197
|
});
|
|
199
198
|
const records: KvRecord[] = [];
|
|
200
199
|
for (const reply of replies) {
|
|
201
|
-
|
|
200
|
+
// A status on a batch reply is a marker, never a value — the terminator is filtered by `until`,
|
|
201
|
+
// and anything else the server slips in (a `408` heartbeat) carries no message to read.
|
|
202
|
+
if (reply.status !== 0) continue;
|
|
202
203
|
const record = recordOf(reply, bucket);
|
|
203
204
|
if (record) records.push(record);
|
|
204
205
|
}
|
|
@@ -207,14 +208,14 @@ export async function kvLast(
|
|
|
207
208
|
|
|
208
209
|
/** A KV write is a publish that waits for JetStream's ack — a lost put must not read as stored. */
|
|
209
210
|
export async function kvWrite(
|
|
210
|
-
|
|
211
|
+
client: NatsClient,
|
|
211
212
|
bucket: string,
|
|
212
213
|
key: string,
|
|
213
214
|
value: string,
|
|
214
215
|
headers: NatsHeaders,
|
|
215
216
|
): Promise<void> {
|
|
216
217
|
const subject = kvSubject(bucket, key);
|
|
217
|
-
const reply = await
|
|
218
|
+
const reply = await client.request(subject, encoder.encode(value), { headers });
|
|
218
219
|
const body = asObject(reply, subject);
|
|
219
220
|
const error = errorOf(body);
|
|
220
221
|
if (error !== undefined) {
|
package/src/nats-kv.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
import type { Clock } from '@ultimat3/core';
|
|
6
6
|
import type { TransportSet, TransportSetEntry } from './fanout';
|
|
7
|
-
import type {
|
|
7
|
+
import type { NatsClient } from './nats-client';
|
|
8
8
|
import { kvGet, kvLast, kvWrite } from './nats-jetstream';
|
|
9
9
|
|
|
10
10
|
/** Per-message TTL is expressed in whole seconds, and must never expire before the logical one. */
|
|
@@ -51,8 +51,8 @@ const ttlHeader = (ttlMs: number): ReadonlyMap<string, string> =>
|
|
|
51
51
|
new Map([['Nats-TTL', String(Math.ceil(ttlMs / 1_000) + TTL_GRACE_SECONDS)]]);
|
|
52
52
|
|
|
53
53
|
export interface NatsKvSetOptions {
|
|
54
|
-
/** The live
|
|
55
|
-
readonly
|
|
54
|
+
/** The live client. Awaited per call: the transport dials lazily and re-dials after a loss. */
|
|
55
|
+
readonly client: () => Promise<NatsClient>;
|
|
56
56
|
readonly bucket: string;
|
|
57
57
|
/** Only the fallback when a reply carries no server timestamp; the server's clock is the truth. */
|
|
58
58
|
readonly clock: Clock;
|
|
@@ -69,7 +69,7 @@ export class NatsKvSet implements TransportSet {
|
|
|
69
69
|
async put(key: string, member: string, value: string, ttlMs: number): Promise<void> {
|
|
70
70
|
const stored: StoredValue = { v: value, t: ttlMs };
|
|
71
71
|
await kvWrite(
|
|
72
|
-
await this.#options.
|
|
72
|
+
await this.#options.client(),
|
|
73
73
|
this.#options.bucket,
|
|
74
74
|
this.#key(key, member),
|
|
75
75
|
JSON.stringify(stored),
|
|
@@ -80,7 +80,7 @@ export class NatsKvSet implements TransportSet {
|
|
|
80
80
|
/** `false` when the member had already expired: the caller must re-`put`, which is a re-join. */
|
|
81
81
|
async touch(key: string, member: string, ttlMs: number): Promise<boolean> {
|
|
82
82
|
const record = await kvGet(
|
|
83
|
-
await this.#options.
|
|
83
|
+
await this.#options.client(),
|
|
84
84
|
this.#options.bucket,
|
|
85
85
|
this.#key(key, member),
|
|
86
86
|
);
|
|
@@ -98,7 +98,7 @@ export class NatsKvSet implements TransportSet {
|
|
|
98
98
|
*/
|
|
99
99
|
async drop(key: string, member: string): Promise<void> {
|
|
100
100
|
await kvWrite(
|
|
101
|
-
await this.#options.
|
|
101
|
+
await this.#options.client(),
|
|
102
102
|
this.#options.bucket,
|
|
103
103
|
this.#key(key, member),
|
|
104
104
|
'',
|
|
@@ -111,7 +111,7 @@ export class NatsKvSet implements TransportSet {
|
|
|
111
111
|
|
|
112
112
|
async entries(key: string): Promise<readonly TransportSetEntry[]> {
|
|
113
113
|
const records = await kvLast(
|
|
114
|
-
await this.#options.
|
|
114
|
+
await this.#options.client(),
|
|
115
115
|
this.#options.bucket,
|
|
116
116
|
`${encodeToken(key)}.*`,
|
|
117
117
|
);
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
// Single responsibility: the one adapter from the `nats` client to this package's port. It is the
|
|
2
|
+
// only file in the repo that imports `nats` — everything else speaks `NatsClient`, so the wire, the
|
|
3
|
+
// reconnect and the TLS upgrade are the library's and stay replaceable.
|
|
4
|
+
//
|
|
5
|
+
// WHERE FAILURES ARE TRANSLATED, and it is deliberately not all here. This file coded the calls
|
|
6
|
+
// that have no synchronous caller frame to catch them: `request`, `requestMany`, the dial, and the
|
|
7
|
+
// background `#watch` that is the only place a lost connection is announced. `publish` and
|
|
8
|
+
// `subscribe` are synchronous and stay raw — `NatsTransport.#translating` codes them, because
|
|
9
|
+
// `NatsTransportOptions.connect` is a PUBLIC injection seam: translating in this class would cover
|
|
10
|
+
// the one client the repo ships and leave every app-supplied one uncovered, and translating in both
|
|
11
|
+
// places would be two answers to one event. `unsubscribe()` is wrapped nowhere on purpose — it is
|
|
12
|
+
// synchronous, returns `void`, and its throw reaches the caller rather than being swallowed.
|
|
13
|
+
//
|
|
14
|
+
// This header said "every failure leaves here as an `UltimateError`" until 2026-08, which a reader
|
|
15
|
+
// took as a guarantee it never was.
|
|
16
|
+
|
|
17
|
+
import { connect, Events, headers, Match, type Msg, type MsgHdrs, type NatsConnection } from 'nats';
|
|
18
|
+
import { TransportUnavailableError } from './errors';
|
|
19
|
+
import {
|
|
20
|
+
DEFAULT_REQUEST_TIMEOUT_MS,
|
|
21
|
+
type NatsClient,
|
|
22
|
+
type NatsClientOptions,
|
|
23
|
+
type NatsHeaders,
|
|
24
|
+
type NatsMessage,
|
|
25
|
+
type NatsMessageHandler,
|
|
26
|
+
type NatsRequestManyOptions,
|
|
27
|
+
type NatsRequestOptions,
|
|
28
|
+
type NatsSubscription,
|
|
29
|
+
type NatsTarget,
|
|
30
|
+
parseNatsUrl,
|
|
31
|
+
} from './nats-client';
|
|
32
|
+
|
|
33
|
+
/** A message that has already left the library: the port's shape, read lazily off the headers. */
|
|
34
|
+
const messageOf = (message: Msg): NatsMessage => ({
|
|
35
|
+
subject: message.subject,
|
|
36
|
+
payload: message.data,
|
|
37
|
+
status: message.headers?.code ?? 0,
|
|
38
|
+
// `MsgHdrs.get` answers '' for a header the server never sent, and every header this package
|
|
39
|
+
// reads is meaningless when empty — so one absent answer, rather than two.
|
|
40
|
+
header: (name: string): string | undefined => {
|
|
41
|
+
const value = message.headers?.get(name, Match.IgnoreCase);
|
|
42
|
+
return value === undefined || value === '' ? undefined : value;
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
const headersOf = (map: NatsHeaders | undefined): MsgHdrs | undefined => {
|
|
47
|
+
if (map === undefined || map.size === 0) return undefined;
|
|
48
|
+
const built = headers();
|
|
49
|
+
for (const [name, value] of map) built.set(name, value);
|
|
50
|
+
return built;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const unavailable = (target: NatsTarget, reason: string): TransportUnavailableError =>
|
|
54
|
+
new TransportUnavailableError({
|
|
55
|
+
transport: 'nats',
|
|
56
|
+
// The URL is never echoed back — it carries the credentials.
|
|
57
|
+
reason: `${target.host}:${target.port} — ${reason}`,
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
const describe = (error: unknown): string =>
|
|
61
|
+
error instanceof Error ? error.message : String(error);
|
|
62
|
+
|
|
63
|
+
class LibNatsClient implements NatsClient {
|
|
64
|
+
readonly #connection: NatsConnection;
|
|
65
|
+
readonly #target: NatsTarget;
|
|
66
|
+
readonly #timeoutMs: number;
|
|
67
|
+
readonly #report: (error: unknown) => void;
|
|
68
|
+
#connected = true;
|
|
69
|
+
|
|
70
|
+
constructor(connection: NatsConnection, target: NatsTarget, options: NatsClientOptions) {
|
|
71
|
+
this.#connection = connection;
|
|
72
|
+
this.#target = target;
|
|
73
|
+
this.#timeoutMs = options.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
|
|
74
|
+
this.#report = options.onError ?? ((): void => undefined);
|
|
75
|
+
void this.#watch(options);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
get version(): string {
|
|
79
|
+
return this.#connection.info?.version ?? '';
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
get connected(): boolean {
|
|
83
|
+
return this.#connected && !this.#connection.isClosed();
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
publish(subject: string, payload: Uint8Array): void {
|
|
87
|
+
this.#connection.publish(subject, payload);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
subscribe(subject: string, handler: NatsMessageHandler): NatsSubscription {
|
|
91
|
+
const subscription = this.#connection.subscribe(subject, {
|
|
92
|
+
// A callback rather than the async iterator: the iterator queues, and a `sync` node that
|
|
93
|
+
// falls behind on one subject must drop nothing silently into a growing buffer.
|
|
94
|
+
callback: (error, message) => {
|
|
95
|
+
// A subscription's own failure — a permissions violation on the subject is the common one —
|
|
96
|
+
// arrives here and nowhere else. Dropping it is a node delivering nothing, silently.
|
|
97
|
+
if (error !== null) this.#report(unavailable(this.#target, `${subject}: ${error.message}`));
|
|
98
|
+
else handler(messageOf(message));
|
|
99
|
+
},
|
|
100
|
+
});
|
|
101
|
+
return { unsubscribe: () => subscription.unsubscribe() };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async request(
|
|
105
|
+
subject: string,
|
|
106
|
+
payload: Uint8Array,
|
|
107
|
+
options: NatsRequestOptions = {},
|
|
108
|
+
): Promise<NatsMessage> {
|
|
109
|
+
const built = headersOf(options.headers);
|
|
110
|
+
try {
|
|
111
|
+
const reply = await this.#connection.request(subject, payload, {
|
|
112
|
+
timeout: this.#timeoutMs,
|
|
113
|
+
...(built === undefined ? {} : { headers: built }),
|
|
114
|
+
});
|
|
115
|
+
return messageOf(reply);
|
|
116
|
+
} catch (error) {
|
|
117
|
+
throw unavailable(this.#target, `${subject} did not answer: ${describe(error)}`);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* A batch read ends on a message the caller recognises — a `204` end-of-batch or a `404` for a
|
|
123
|
+
* prefix nobody has written. Breaking the loop is what releases the library's inbox subscription,
|
|
124
|
+
* so the terminator is never collected and never awaited past.
|
|
125
|
+
*/
|
|
126
|
+
async requestMany(
|
|
127
|
+
subject: string,
|
|
128
|
+
payload: Uint8Array,
|
|
129
|
+
options: NatsRequestManyOptions,
|
|
130
|
+
): Promise<readonly NatsMessage[]> {
|
|
131
|
+
const collected: NatsMessage[] = [];
|
|
132
|
+
try {
|
|
133
|
+
const replies = await this.#connection.requestMany(subject, payload, {
|
|
134
|
+
maxWait: this.#timeoutMs,
|
|
135
|
+
});
|
|
136
|
+
for await (const reply of replies) {
|
|
137
|
+
const message = messageOf(reply);
|
|
138
|
+
if (options.until(message)) break;
|
|
139
|
+
collected.push(message);
|
|
140
|
+
}
|
|
141
|
+
} catch (error) {
|
|
142
|
+
throw unavailable(this.#target, `${subject} did not answer: ${describe(error)}`);
|
|
143
|
+
}
|
|
144
|
+
return collected;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async close(): Promise<void> {
|
|
148
|
+
this.#connected = false;
|
|
149
|
+
await this.#connection.close();
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* The library's own status stream is the only place a background loss is announced. Nothing
|
|
154
|
+
* awaits it, so it can neither throw nor end the process: a drop reports and flips `connected`,
|
|
155
|
+
* a reconnect flips it back and tells the transport its cluster may be a new one.
|
|
156
|
+
*/
|
|
157
|
+
async #watch(options: NatsClientOptions): Promise<void> {
|
|
158
|
+
const report = options.onError ?? ((): void => undefined);
|
|
159
|
+
try {
|
|
160
|
+
for await (const status of this.#connection.status()) {
|
|
161
|
+
if (status.type === Events.Disconnect) {
|
|
162
|
+
this.#connected = false;
|
|
163
|
+
report(unavailable(this.#target, 'the connection dropped'));
|
|
164
|
+
} else if (status.type === Events.Reconnect) {
|
|
165
|
+
this.#connected = true;
|
|
166
|
+
options.onReconnect?.();
|
|
167
|
+
} else if (status.type === Events.Error) {
|
|
168
|
+
report(unavailable(this.#target, `the server reported ${String(status.data)}`));
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
// The iterator ends when the connection is done: either `close()` or a reconnect budget spent.
|
|
172
|
+
this.#connected = false;
|
|
173
|
+
const failure = await this.#connection.closed();
|
|
174
|
+
if (failure !== undefined) report(unavailable(this.#target, describe(failure)));
|
|
175
|
+
} catch (error) {
|
|
176
|
+
this.#connected = false;
|
|
177
|
+
report(unavailable(this.#target, describe(error)));
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* The production `NatsConnect`. The first dial retries on the same budget as a later loss
|
|
184
|
+
* (`waitOnFirstConnect`), so a `sync` container that raced the bus into readiness recovers on its
|
|
185
|
+
* own — and a budget that runs out rejects here rather than leaving a half-live connection behind.
|
|
186
|
+
*/
|
|
187
|
+
export const openNatsClient = async (options: NatsClientOptions): Promise<NatsClient> => {
|
|
188
|
+
const target = parseNatsUrl(options.url);
|
|
189
|
+
try {
|
|
190
|
+
const connection = await connect({
|
|
191
|
+
servers: [`${target.host}:${target.port}`],
|
|
192
|
+
name: options.name ?? 'ultimate',
|
|
193
|
+
waitOnFirstConnect: true,
|
|
194
|
+
...(options.maxReconnectAttempts === undefined
|
|
195
|
+
? {}
|
|
196
|
+
: { maxReconnectAttempts: options.maxReconnectAttempts }),
|
|
197
|
+
...(options.reconnectDelay === undefined
|
|
198
|
+
? {}
|
|
199
|
+
: { reconnectDelayHandler: options.reconnectDelay }),
|
|
200
|
+
// The scheme is the only thing that can demand TLS before the server's INFO is read.
|
|
201
|
+
...(target.tls ? { tls: {} } : {}),
|
|
202
|
+
...(target.user === undefined ? {} : { user: target.user }),
|
|
203
|
+
...(target.pass === undefined ? {} : { pass: target.pass }),
|
|
204
|
+
...(target.token === undefined ? {} : { token: target.token }),
|
|
205
|
+
});
|
|
206
|
+
return new LibNatsClient(connection, target, options);
|
|
207
|
+
} catch (error) {
|
|
208
|
+
throw unavailable(target, describe(error));
|
|
209
|
+
}
|
|
210
|
+
};
|