@adonisjs/transmit-client 1.0.0 → 1.1.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/README.md CHANGED
@@ -7,7 +7,11 @@
7
7
 
8
8
  <div align="center">
9
9
 
10
- [![gh-workflow-image]][gh-workflow-url] [![npm-image]][npm-url] ![][typescript-image] [![license-image]][license-url] [![synk-image]][synk-url]
10
+ [![typescript-image]][typescript-url]
11
+ [![gh-workflow-image]][gh-workflow-url]
12
+ [![npm-image]][npm-url]
13
+ [![npm-download-image]][npm-download-url]
14
+ [![license-image]][license-url]
11
15
 
12
16
  </div>
13
17
 
@@ -38,6 +42,8 @@ AdonisJS Transmit Client is a client for the native Server-Sent-Event (SSE) modu
38
42
  - [Creating a subscription](#creating-a-subscription)
39
43
  - [Unsubscribing](#unsubscribing)
40
44
  - [Subscription Request](#subscription-request)
45
+ - [Authenticated event stream](#authenticated-event-stream)
46
+ - [Custom UID Generator](#custom-uid-generator)
41
47
  - [Reconnecting](#reconnecting)
42
48
  - [Events](#events)
43
49
 
@@ -130,6 +136,93 @@ const transmit = new Transmit({
130
136
  })
131
137
  ```
132
138
 
139
+ ### Authenticated event stream
140
+
141
+ The `__transmit/events` stream is opened using `EventSource`, which cannot send custom headers. That means `beforeSubscribe`/`beforeUnsubscribe` only affect the subscribe/unsubscribe HTTP calls. If you rely on header-based auth, protect `__transmit/subscribe` and `__transmit/unsubscribe`, or provide a custom `eventSourceFactory` that can send headers.
142
+
143
+ Example using `@microsoft/fetch-event-source`:
144
+
145
+ ```ts
146
+ import { fetchEventSource } from '@microsoft/fetch-event-source'
147
+
148
+ function createFetchEventSource(
149
+ url: string | URL,
150
+ options: { withCredentials: boolean },
151
+ headers: Record<string, string>
152
+ ) {
153
+ const controller = new AbortController()
154
+ const listeners = new Map<string, Set<(event: MessageEvent) => void>>()
155
+
156
+ const dispatch = (type: string, data?: string) => {
157
+ const event = new MessageEvent(type, { data })
158
+ listeners.get(type)?.forEach((listener) => listener(event))
159
+ }
160
+
161
+ fetchEventSource(url.toString(), {
162
+ headers,
163
+ credentials: options.withCredentials ? 'include' : 'omit',
164
+ signal: controller.signal,
165
+ onopen: () => dispatch('open'),
166
+ onmessage: (message) => dispatch(message.event ?? 'message', message.data),
167
+ onerror: () => {
168
+ dispatch('error')
169
+ },
170
+ })
171
+
172
+ return {
173
+ addEventListener(type: string, listener: (event: MessageEvent) => void) {
174
+ if (!listeners.has(type)) {
175
+ listeners.set(type, new Set())
176
+ }
177
+ listeners.get(type)!.add(listener)
178
+ },
179
+ removeEventListener(type: string, listener: (event: MessageEvent) => void) {
180
+ listeners.get(type)?.delete(listener)
181
+ },
182
+ close() {
183
+ controller.abort()
184
+ },
185
+ } as EventSource
186
+ }
187
+
188
+ const transmit = new Transmit({
189
+ baseUrl: 'http://localhost:3333',
190
+ eventSourceFactory: (url, options) => {
191
+ return createFetchEventSource(url, options, {
192
+ Authorization: `Bearer ${token}`,
193
+ })
194
+ },
195
+ })
196
+ ```
197
+
198
+ Note: this adapter is minimal and only wires `open`, `error`, and `message` (or custom event names). If you rely on other `EventSource` features like `readyState` or `onopen`, you may want to expand it.
199
+
200
+ ### Custom UID Generator
201
+
202
+ By default, Transmit uses `crypto.randomUUID()` to generate unique client identifiers. This method only works in [secure contexts](https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts) (HTTPS). If you need to use Transmit over HTTP (e.g., local network deployments), you can provide a custom `uidGenerator` function.
203
+
204
+ ```ts
205
+ const transmit = new Transmit({
206
+ baseUrl: 'http://localhost:3333',
207
+ uidGenerator: () => {
208
+ return Array.from({ length: 16 }, () =>
209
+ Math.floor(Math.random() * 256).toString(16).padStart(2, '0')
210
+ ).join('')
211
+ },
212
+ })
213
+ ```
214
+
215
+ Or using a library like `uuid`:
216
+
217
+ ```ts
218
+ import { v4 as uuid } from 'uuid'
219
+
220
+ const transmit = new Transmit({
221
+ baseUrl: 'http://localhost:3333',
222
+ uidGenerator: () => uuid(),
223
+ })
224
+ ```
225
+
133
226
  ### Reconnecting
134
227
 
135
228
  The transmit client will automatically reconnect to the server when the connection is lost. You can change the number of retries and hook into the reconnect lifecycle as follows:
@@ -149,9 +242,9 @@ const transmit = new Transmit({
149
242
 
150
243
  # Events
151
244
 
152
- The`Transmit` class uses the [`EventTarget`](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget) class to emits multiple events.
245
+ The `Transmit` class uses the [`EventTarget`](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget) class to emits multiple events.
153
246
 
154
- ```ts
247
+ ```ts
155
248
  transmit.on('connected', () => {
156
249
  console.log('connected')
157
250
  })
@@ -165,17 +258,24 @@ transmit.on('reconnecting', () => {
165
258
  })
166
259
  ```
167
260
 
168
- [gh-workflow-image]: https://img.shields.io/github/actions/workflow/status/adonisjs/transmit-client/test?style=for-the-badge
169
- [gh-workflow-url]: https://github.com/adonisjs/transmit-client/actions/workflows/test.yml "Github action"
261
+ That means you can also remove an event listener previously registered, by passing the event listener function itself.
170
262
 
171
- [typescript-image]: https://img.shields.io/badge/Typescript-294E80.svg?style=for-the-badge&logo=typescript
172
- [typescript-url]: "typescript"
263
+ ```ts
264
+ const onConnected = () => {
265
+ console.log('connected')
266
+ }
173
267
 
174
- [npm-image]: https://img.shields.io/npm/v/@adonisjs/transmit-client.svg?style=for-the-badge&logo=npm
175
- [npm-url]: https://npmjs.org/package/@adonisjs/transmit-client 'npm'
268
+ transmit.on('connected', onConnected)
269
+ transmit.off('connected', onConnected)
270
+ ```
176
271
 
272
+ [gh-workflow-image]: https://img.shields.io/github/actions/workflow/status/adonisjs/transmit-client/checks.yml?branch=develop&style=for-the-badge
273
+ [gh-workflow-url]: https://github.com/adonisjs/transmit-client/actions/workflows/checks.yml
274
+ [npm-image]: https://img.shields.io/npm/v/@adonisjs/transmit-client.svg?style=for-the-badge&logo=npm
275
+ [npm-url]: https://www.npmjs.com/package/@adonisjs/transmit-client
276
+ [npm-download-image]: https://img.shields.io/npm/dm/@adonisjs/transmit-client?style=for-the-badge
277
+ [npm-download-url]: https://www.npmjs.com/package/@adonisjs/transmit-client
278
+ [typescript-image]: https://img.shields.io/badge/Typescript-294E80.svg?style=for-the-badge&logo=typescript
279
+ [typescript-url]: https://www.typescriptlang.org
177
280
  [license-image]: https://img.shields.io/npm/l/@adonisjs/transmit-client?color=blueviolet&style=for-the-badge
178
- [license-url]: LICENSE.md 'license'
179
-
180
- [synk-image]: https://img.shields.io/snyk/vulnerabilities/github/adonisjs/transmit-client?label=Synk%20Vulnerabilities&style=for-the-badge
181
- [synk-url]: https://snyk.io/test/github/adonisjs/transmit-client?targetFile=package.json "synk"
281
+ [license-url]: LICENSE.md
package/build/index.d.ts CHANGED
@@ -1,13 +1,11 @@
1
- interface HttpClientOptions {
2
- baseUrl: string;
3
- uid: string;
4
- }
5
- declare class HttpClient {
6
- #private;
7
- constructor(options: HttpClientOptions);
8
- send(request: Request): Promise<Response>;
9
- createRequest(path: string, body: Record<string, unknown>): Request;
10
- }
1
+ declare const TransmitStatus: {
2
+ readonly Initializing: "initializing";
3
+ readonly Connecting: "connecting";
4
+ readonly Connected: "connected";
5
+ readonly Disconnected: "disconnected";
6
+ readonly Reconnecting: "reconnecting";
7
+ };
8
+ type TransmitStatus = (typeof TransmitStatus)[keyof typeof TransmitStatus];
11
9
 
12
10
  declare const HookEvent: {
13
11
  readonly BeforeSubscribe: "beforeSubscribe";
@@ -32,20 +30,23 @@ declare class Hook {
32
30
  onUnsubscription(channel: string): this;
33
31
  }
34
32
 
35
- declare const TransmitStatus: {
36
- readonly Initializing: "initializing";
37
- readonly Connecting: "connecting";
38
- readonly Connected: "connected";
39
- readonly Disconnected: "disconnected";
40
- readonly Reconnecting: "reconnecting";
41
- };
42
- type TransmitStatus = (typeof TransmitStatus)[keyof typeof TransmitStatus];
33
+ interface HttpClientOptions {
34
+ baseUrl: string;
35
+ uid: string;
36
+ }
37
+ declare class HttpClient {
38
+ #private;
39
+ constructor(options: HttpClientOptions);
40
+ send(request: Request): Promise<Response>;
41
+ createRequest(path: string, body: Record<string, unknown>): Request;
42
+ }
43
43
 
44
44
  interface SubscriptionOptions {
45
45
  channel: string;
46
46
  httpClient: HttpClient;
47
47
  getEventSourceStatus: () => TransmitStatus;
48
48
  hooks?: Hook;
49
+ onDelete?: () => void;
49
50
  }
50
51
  declare class Subscription {
51
52
  #private;
@@ -66,8 +67,8 @@ declare class Subscription {
66
67
  * Run all registered handlers for the subscription.
67
68
  */
68
69
  $runHandler(message: unknown): void;
69
- create(): Promise<unknown>;
70
- forceCreate(): Promise<unknown>;
70
+ create(): Promise<void>;
71
+ forceCreate(): Promise<void>;
71
72
  delete(): Promise<void>;
72
73
  onMessage<T>(handler: (message: T) => void): () => void;
73
74
  onMessageOnce<T>(handler: (message: T) => void): void;
@@ -81,8 +82,8 @@ interface TransmitOptions {
81
82
  }) => EventSource;
82
83
  eventTargetFactory?: () => EventTarget | null;
83
84
  httpClientFactory?: (baseUrl: string, uid: string) => HttpClient;
84
- beforeSubscribe?: (request: RequestInit) => void;
85
- beforeUnsubscribe?: (request: RequestInit) => void;
85
+ beforeSubscribe?: (request: Request) => void;
86
+ beforeUnsubscribe?: (request: Request) => void;
86
87
  maxReconnectAttempts?: number;
87
88
  onReconnectAttempt?: (attempt: number) => void;
88
89
  onReconnectFailed?: () => void;
@@ -99,6 +100,7 @@ declare class Transmit {
99
100
  constructor(options: TransmitOptions);
100
101
  subscription(channel: string): Subscription;
101
102
  on(event: Exclude<TransmitStatus, 'connecting'>, callback: (event: CustomEvent) => void): void;
103
+ off(event: Exclude<TransmitStatus, 'connecting'>, callback: (event: CustomEvent) => void): void;
102
104
  close(): void;
103
105
  }
104
106
 
package/build/index.js CHANGED
@@ -1,21 +1,11 @@
1
- var __accessCheck = (obj, member, msg) => {
2
- if (!member.has(obj))
3
- throw TypeError("Cannot " + msg);
4
- };
5
- var __privateGet = (obj, member, getter) => {
6
- __accessCheck(obj, member, "read from private field");
7
- return getter ? getter.call(obj) : member.get(obj);
8
- };
9
- var __privateAdd = (obj, member, value) => {
10
- if (member.has(obj))
11
- throw TypeError("Cannot add the same private member more than once");
12
- member instanceof WeakSet ? member.add(obj) : member.set(obj, value);
13
- };
14
- var __privateSet = (obj, member, value, setter) => {
15
- __accessCheck(obj, member, "write to private field");
16
- setter ? setter.call(obj, value) : member.set(obj, value);
17
- return value;
1
+ var __typeError = (msg) => {
2
+ throw TypeError(msg);
18
3
  };
4
+ var __accessCheck = (obj, member, msg) => member.has(obj) || __typeError("Cannot " + msg);
5
+ var __privateGet = (obj, member, getter) => (__accessCheck(obj, member, "read from private field"), getter ? getter.call(obj) : member.get(obj));
6
+ var __privateAdd = (obj, member, value) => member.has(obj) ? __typeError("Cannot add the same private member more than once") : member instanceof WeakSet ? member.add(obj) : member.set(obj, value);
7
+ var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "write to private field"), setter ? setter.call(obj, value) : member.set(obj, value), value);
8
+ var __privateMethod = (obj, member, method) => (__accessCheck(obj, member, "access private method"), method);
19
9
  var __privateWrapper = (obj, member, setter, getter) => ({
20
10
  set _(value) {
21
11
  __privateSet(obj, member, value, setter);
@@ -24,10 +14,6 @@ var __privateWrapper = (obj, member, setter, getter) => ({
24
14
  return __privateGet(obj, member, getter);
25
15
  }
26
16
  });
27
- var __privateMethod = (obj, member, method) => {
28
- __accessCheck(obj, member, "access private method");
29
- return method;
30
- };
31
17
 
32
18
  // src/subscription_status.ts
33
19
  var SubscriptionStatus = {
@@ -46,29 +32,37 @@ var TransmitStatus = {
46
32
  };
47
33
 
48
34
  // src/subscription.ts
49
- var _httpClient, _hooks, _channel, _getEventSourceStatus, _handlers, _status;
35
+ var _httpClient, _hooks, _onDelete, _channel, _getEventSourceStatus, _handlers, _createPending, _status;
50
36
  var Subscription = class {
51
37
  constructor(options) {
52
38
  /**
53
39
  * HTTP client instance.
54
40
  */
55
- __privateAdd(this, _httpClient, void 0);
41
+ __privateAdd(this, _httpClient);
56
42
  /**
57
43
  * Hook instance.
58
44
  */
59
- __privateAdd(this, _hooks, void 0);
45
+ __privateAdd(this, _hooks);
46
+ /**
47
+ * Callback to call when the subscription is deleted.
48
+ */
49
+ __privateAdd(this, _onDelete);
60
50
  /**
61
51
  * Channel name.
62
52
  */
63
- __privateAdd(this, _channel, void 0);
53
+ __privateAdd(this, _channel);
64
54
  /**
65
55
  * Event source status getter.
66
56
  */
67
- __privateAdd(this, _getEventSourceStatus, void 0);
57
+ __privateAdd(this, _getEventSourceStatus);
68
58
  /**
69
59
  * Registered message handlers.
70
60
  */
71
61
  __privateAdd(this, _handlers, /* @__PURE__ */ new Set());
62
+ /**
63
+ * Pending create retry promise to avoid stacking timeouts.
64
+ */
65
+ __privateAdd(this, _createPending, null);
72
66
  /**
73
67
  * Current status of the subscription.
74
68
  */
@@ -76,6 +70,7 @@ var Subscription = class {
76
70
  __privateSet(this, _channel, options.channel);
77
71
  __privateSet(this, _httpClient, options.httpClient);
78
72
  __privateSet(this, _hooks, options.hooks);
73
+ __privateSet(this, _onDelete, options.onDelete);
79
74
  __privateSet(this, _getEventSourceStatus, options.getEventSourceStatus);
80
75
  }
81
76
  /**
@@ -101,23 +96,36 @@ var Subscription = class {
101
96
  */
102
97
  $runHandler(message) {
103
98
  for (const handler of __privateGet(this, _handlers)) {
104
- handler(message);
99
+ try {
100
+ handler(message);
101
+ } catch (error) {
102
+ console.error(error);
103
+ }
105
104
  }
106
105
  }
107
106
  async create() {
108
107
  if (this.isCreated) {
109
108
  return;
110
109
  }
110
+ if (__privateGet(this, _getEventSourceStatus).call(this) !== TransmitStatus.Connected && __privateGet(this, _createPending)) {
111
+ return __privateGet(this, _createPending);
112
+ }
111
113
  return this.forceCreate();
112
114
  }
113
115
  async forceCreate() {
114
116
  if (__privateGet(this, _getEventSourceStatus).call(this) !== TransmitStatus.Connected) {
115
- return new Promise((resolve) => {
117
+ if (__privateGet(this, _createPending)) {
118
+ return __privateGet(this, _createPending);
119
+ }
120
+ __privateSet(this, _createPending, new Promise((resolve) => {
116
121
  setTimeout(() => {
122
+ __privateSet(this, _createPending, null);
117
123
  resolve(this.create());
118
124
  }, 100);
119
- });
125
+ }));
126
+ return __privateGet(this, _createPending);
120
127
  }
128
+ __privateSet(this, _createPending, null);
121
129
  const request = __privateGet(this, _httpClient).createRequest("/__transmit/subscribe", {
122
130
  channel: __privateGet(this, _channel)
123
131
  });
@@ -135,6 +143,7 @@ var Subscription = class {
135
143
  }
136
144
  }
137
145
  async delete() {
146
+ var _a;
138
147
  if (this.isDeleted || !this.isCreated) {
139
148
  return;
140
149
  }
@@ -150,6 +159,7 @@ var Subscription = class {
150
159
  }
151
160
  __privateSet(this, _status, SubscriptionStatus.Deleted);
152
161
  __privateGet(this, _hooks)?.onUnsubscription(__privateGet(this, _channel));
162
+ (_a = __privateGet(this, _onDelete)) == null ? void 0 : _a.call(this);
153
163
  } catch (error) {
154
164
  }
155
165
  }
@@ -168,17 +178,19 @@ var Subscription = class {
168
178
  };
169
179
  _httpClient = new WeakMap();
170
180
  _hooks = new WeakMap();
181
+ _onDelete = new WeakMap();
171
182
  _channel = new WeakMap();
172
183
  _getEventSourceStatus = new WeakMap();
173
184
  _handlers = new WeakMap();
185
+ _createPending = new WeakMap();
174
186
  _status = new WeakMap();
175
187
 
176
188
  // src/http_client.ts
177
- var _options, _retrieveXsrfToken, retrieveXsrfToken_fn;
189
+ var _options, _HttpClient_instances, retrieveXsrfToken_fn;
178
190
  var HttpClient = class {
179
191
  constructor(options) {
180
- __privateAdd(this, _retrieveXsrfToken);
181
- __privateAdd(this, _options, void 0);
192
+ __privateAdd(this, _HttpClient_instances);
193
+ __privateAdd(this, _options);
182
194
  __privateSet(this, _options, options);
183
195
  }
184
196
  send(request) {
@@ -189,7 +201,7 @@ var HttpClient = class {
189
201
  method: "POST",
190
202
  headers: {
191
203
  "Content-Type": "application/json",
192
- "X-XSRF-TOKEN": __privateMethod(this, _retrieveXsrfToken, retrieveXsrfToken_fn).call(this) ?? ""
204
+ "X-XSRF-TOKEN": __privateMethod(this, _HttpClient_instances, retrieveXsrfToken_fn).call(this) ?? ""
193
205
  },
194
206
  body: JSON.stringify({ uid: __privateGet(this, _options).uid, ...body }),
195
207
  credentials: "include"
@@ -197,10 +209,9 @@ var HttpClient = class {
197
209
  }
198
210
  };
199
211
  _options = new WeakMap();
200
- _retrieveXsrfToken = new WeakSet();
212
+ _HttpClient_instances = new WeakSet();
201
213
  retrieveXsrfToken_fn = function() {
202
- if (typeof document === "undefined")
203
- return null;
214
+ if (typeof document === "undefined") return null;
204
215
  const match = document.cookie.match(new RegExp("(^|;\\s*)(XSRF-TOKEN)=([^;]*)"));
205
216
  return match ? decodeURIComponent(match[3]) : null;
206
217
  };
@@ -261,21 +272,18 @@ var Hook = class {
261
272
  _handlers2 = new WeakMap();
262
273
 
263
274
  // src/transmit.ts
264
- var _uid, _options2, _subscriptions, _httpClient2, _hooks2, _status2, _eventSource, _eventTarget, _reconnectAttempts, _changeStatus, changeStatus_fn, _connect, connect_fn, _onMessage, onMessage_fn, _onError, onError_fn;
275
+ var _uid, _options2, _subscriptions, _httpClient2, _hooks2, _status2, _eventSource, _eventTarget, _reconnectAttempts, _Transmit_instances, changeStatus_fn, connect_fn, onMessage_fn, onError_fn;
265
276
  var Transmit = class {
266
277
  constructor(options) {
267
- __privateAdd(this, _changeStatus);
268
- __privateAdd(this, _connect);
269
- __privateAdd(this, _onMessage);
270
- __privateAdd(this, _onError);
278
+ __privateAdd(this, _Transmit_instances);
271
279
  /**
272
280
  * Unique identifier for this client.
273
281
  */
274
- __privateAdd(this, _uid, void 0);
282
+ __privateAdd(this, _uid);
275
283
  /**
276
284
  * Options for this client.
277
285
  */
278
- __privateAdd(this, _options2, void 0);
286
+ __privateAdd(this, _options2);
279
287
  /**
280
288
  * Registered subscriptions.
281
289
  */
@@ -283,11 +291,11 @@ var Transmit = class {
283
291
  /**
284
292
  * HTTP client instance.
285
293
  */
286
- __privateAdd(this, _httpClient2, void 0);
294
+ __privateAdd(this, _httpClient2);
287
295
  /**
288
296
  * Hook instance.
289
297
  */
290
- __privateAdd(this, _hooks2, void 0);
298
+ __privateAdd(this, _hooks2);
291
299
  /**
292
300
  * Current status of the client.
293
301
  */
@@ -295,11 +303,11 @@ var Transmit = class {
295
303
  /**
296
304
  * EventSource instance.
297
305
  */
298
- __privateAdd(this, _eventSource, void 0);
306
+ __privateAdd(this, _eventSource);
299
307
  /**
300
308
  * EventTarget instance.
301
309
  */
302
- __privateAdd(this, _eventTarget, void 0);
310
+ __privateAdd(this, _eventTarget);
303
311
  /**
304
312
  * Number of reconnect attempts.
305
313
  */
@@ -345,7 +353,7 @@ var Transmit = class {
345
353
  __privateGet(this, _hooks2).register(HookEvent.OnUnsubscription, options.onUnsubscription);
346
354
  }
347
355
  __privateSet(this, _options2, options);
348
- __privateMethod(this, _connect, connect_fn).call(this);
356
+ __privateMethod(this, _Transmit_instances, connect_fn).call(this);
349
357
  }
350
358
  /**
351
359
  * Returns the unique identifier of the client.
@@ -354,21 +362,25 @@ var Transmit = class {
354
362
  return __privateGet(this, _uid);
355
363
  }
356
364
  subscription(channel) {
365
+ if (__privateGet(this, _subscriptions).has(channel)) {
366
+ return __privateGet(this, _subscriptions).get(channel);
367
+ }
357
368
  const subscription = new Subscription({
358
369
  channel,
359
370
  httpClient: __privateGet(this, _httpClient2),
360
371
  hooks: __privateGet(this, _hooks2),
361
- getEventSourceStatus: () => __privateGet(this, _status2)
372
+ getEventSourceStatus: () => __privateGet(this, _status2),
373
+ onDelete: () => __privateGet(this, _subscriptions).delete(channel)
362
374
  });
363
- if (__privateGet(this, _subscriptions).has(channel)) {
364
- return __privateGet(this, _subscriptions).get(channel);
365
- }
366
375
  __privateGet(this, _subscriptions).set(channel, subscription);
367
376
  return subscription;
368
377
  }
369
378
  on(event, callback) {
370
379
  __privateGet(this, _eventTarget)?.addEventListener(event, callback);
371
380
  }
381
+ off(event, callback) {
382
+ __privateGet(this, _eventTarget)?.removeEventListener(event, callback);
383
+ }
372
384
  close() {
373
385
  __privateGet(this, _eventSource)?.close();
374
386
  }
@@ -382,23 +394,22 @@ _status2 = new WeakMap();
382
394
  _eventSource = new WeakMap();
383
395
  _eventTarget = new WeakMap();
384
396
  _reconnectAttempts = new WeakMap();
385
- _changeStatus = new WeakSet();
397
+ _Transmit_instances = new WeakSet();
386
398
  changeStatus_fn = function(status) {
387
399
  __privateSet(this, _status2, status);
388
400
  __privateGet(this, _eventTarget)?.dispatchEvent(new CustomEvent(status));
389
401
  };
390
- _connect = new WeakSet();
391
402
  connect_fn = function() {
392
- __privateMethod(this, _changeStatus, changeStatus_fn).call(this, TransmitStatus.Connecting);
403
+ __privateMethod(this, _Transmit_instances, changeStatus_fn).call(this, TransmitStatus.Connecting);
393
404
  const url = new URL(`${__privateGet(this, _options2).baseUrl}/__transmit/events`);
394
405
  url.searchParams.append("uid", __privateGet(this, _uid));
395
406
  __privateSet(this, _eventSource, __privateGet(this, _options2).eventSourceFactory(url, {
396
407
  withCredentials: true
397
408
  }));
398
- __privateGet(this, _eventSource).addEventListener("message", __privateMethod(this, _onMessage, onMessage_fn).bind(this));
399
- __privateGet(this, _eventSource).addEventListener("error", __privateMethod(this, _onError, onError_fn).bind(this));
409
+ __privateGet(this, _eventSource).addEventListener("message", __privateMethod(this, _Transmit_instances, onMessage_fn).bind(this));
410
+ __privateGet(this, _eventSource).addEventListener("error", __privateMethod(this, _Transmit_instances, onError_fn).bind(this));
400
411
  __privateGet(this, _eventSource).addEventListener("open", () => {
401
- __privateMethod(this, _changeStatus, changeStatus_fn).call(this, TransmitStatus.Connected);
412
+ __privateMethod(this, _Transmit_instances, changeStatus_fn).call(this, TransmitStatus.Connected);
402
413
  __privateSet(this, _reconnectAttempts, 0);
403
414
  for (const subscription of __privateGet(this, _subscriptions).values()) {
404
415
  if (subscription.isCreated) {
@@ -407,7 +418,6 @@ connect_fn = function() {
407
418
  }
408
419
  });
409
420
  };
410
- _onMessage = new WeakSet();
411
421
  onMessage_fn = function(event) {
412
422
  const data = JSON.parse(event.data);
413
423
  const subscription = __privateGet(this, _subscriptions).get(data.channel);
@@ -417,15 +427,14 @@ onMessage_fn = function(event) {
417
427
  try {
418
428
  subscription.$runHandler(data.payload);
419
429
  } catch (error) {
420
- console.log(error);
430
+ console.error(error);
421
431
  }
422
432
  };
423
- _onError = new WeakSet();
424
433
  onError_fn = function() {
425
434
  if (__privateGet(this, _status2) !== TransmitStatus.Reconnecting) {
426
- __privateMethod(this, _changeStatus, changeStatus_fn).call(this, TransmitStatus.Disconnected);
435
+ __privateMethod(this, _Transmit_instances, changeStatus_fn).call(this, TransmitStatus.Disconnected);
427
436
  }
428
- __privateMethod(this, _changeStatus, changeStatus_fn).call(this, TransmitStatus.Reconnecting);
437
+ __privateMethod(this, _Transmit_instances, changeStatus_fn).call(this, TransmitStatus.Reconnecting);
429
438
  __privateGet(this, _hooks2).onReconnectAttempt(__privateGet(this, _reconnectAttempts) + 1);
430
439
  if (__privateGet(this, _options2).maxReconnectAttempts && __privateGet(this, _reconnectAttempts) >= __privateGet(this, _options2).maxReconnectAttempts) {
431
440
  __privateGet(this, _eventSource).close();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adonisjs/transmit-client",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "A client for the native Server-Sent-Event module of AdonisJS.",
5
5
  "keywords": [
6
6
  "client",
@@ -29,20 +29,21 @@
29
29
  "test": "c8 node --loader ts-node/esm --enable-source-maps bin/test.ts"
30
30
  },
31
31
  "devDependencies": {
32
- "@adonisjs/eslint-config": "^1.1.7",
33
- "@adonisjs/prettier-config": "^1.1.7",
34
- "@adonisjs/tsconfig": "^1.3.0",
35
- "@japa/assert": "^2.0.0-2",
36
- "@japa/runner": "^3.0.0-9",
37
- "@swc/core": "^1.4.11",
38
- "c8": "^9.1.0",
39
- "del-cli": "^5.0.0",
40
- "eslint": "^8.44.0",
41
- "prettier": "^3.0.0",
42
- "release-it": "^17.1.1",
32
+ "@adonisjs/eslint-config": "^3.0.0-next.9",
33
+ "@adonisjs/prettier-config": "^1.4.5",
34
+ "@adonisjs/tsconfig": "^1.4.1",
35
+ "@japa/assert": "^4.2.0",
36
+ "@japa/runner": "^5.3.0",
37
+ "@release-it/conventional-changelog": "^10.0.4",
38
+ "@swc/core": "^1.15.8",
39
+ "c8": "^10.1.3",
40
+ "del-cli": "^7.0.0",
41
+ "eslint": "^9.39.2",
42
+ "prettier": "^3.8.0",
43
+ "release-it": "^19.2.3",
43
44
  "ts-node": "^10.9.2",
44
- "tsup": "^8.0.2",
45
- "typescript": "^5.1.6"
45
+ "tsup": "^8.5.1",
46
+ "typescript": "^5.9.3"
46
47
  },
47
48
  "files": [
48
49
  "src",
@@ -51,13 +52,18 @@
51
52
  "engines": {
52
53
  "node": ">=18.16.0"
53
54
  },
54
- "eslintConfig": {
55
- "extends": "@adonisjs/eslint-config/package"
56
- },
57
55
  "prettier": "@adonisjs/prettier-config",
58
56
  "publishConfig": {
59
- "access": "public",
60
- "tag": "latest"
57
+ "provenance": true,
58
+ "access": "public"
59
+ },
60
+ "homepage": "https://github.com/adonisjs/transmit-client#readme",
61
+ "repository": {
62
+ "type": "git",
63
+ "url": "git+https://github.com/adonisjs/transmit-client.git"
64
+ },
65
+ "bugs": {
66
+ "url": "https://github.com/adonisjs/transmit-client/issues"
61
67
  },
62
68
  "tsup": {
63
69
  "dts": true,
@@ -72,14 +78,26 @@
72
78
  },
73
79
  "release-it": {
74
80
  "git": {
81
+ "requireCleanWorkingDir": true,
82
+ "requireUpstream": true,
75
83
  "commitMessage": "chore(release): ${version}",
76
84
  "tagAnnotation": "v${version}",
85
+ "push": true,
77
86
  "tagName": "v${version}"
78
87
  },
79
88
  "github": {
80
- "release": true,
81
- "releaseName": "v${version}",
82
- "web": true
89
+ "release": true
90
+ },
91
+ "npm": {
92
+ "publish": true,
93
+ "skipChecks": true
94
+ },
95
+ "plugins": {
96
+ "@release-it/conventional-changelog": {
97
+ "preset": {
98
+ "name": "angular"
99
+ }
100
+ }
83
101
  }
84
102
  }
85
103
  }
@@ -8,15 +8,16 @@
8
8
  */
9
9
 
10
10
  import { SubscriptionStatus } from './subscription_status.js'
11
- import { HttpClient } from './http_client.js'
12
- import { Hook } from './hook.js'
13
11
  import { TransmitStatus } from './transmit_status.js'
12
+ import type { Hook } from './hook.js'
13
+ import type { HttpClient } from './http_client.js'
14
14
 
15
15
  interface SubscriptionOptions {
16
16
  channel: string
17
17
  httpClient: HttpClient
18
18
  getEventSourceStatus: () => TransmitStatus
19
19
  hooks?: Hook
20
+ onDelete?: () => void
20
21
  }
21
22
 
22
23
  export class Subscription {
@@ -30,6 +31,11 @@ export class Subscription {
30
31
  */
31
32
  #hooks: Hook | undefined
32
33
 
34
+ /**
35
+ * Callback to call when the subscription is deleted.
36
+ */
37
+ readonly #onDelete: (() => void) | undefined
38
+
33
39
  /**
34
40
  * Channel name.
35
41
  */
@@ -45,6 +51,11 @@ export class Subscription {
45
51
  */
46
52
  #handlers = new Set<(message: any) => void>()
47
53
 
54
+ /**
55
+ * Pending create retry promise to avoid stacking timeouts.
56
+ */
57
+ #createPending: Promise<void> | null = null
58
+
48
59
  /**
49
60
  * Current status of the subscription.
50
61
  */
@@ -75,6 +86,7 @@ export class Subscription {
75
86
  this.#channel = options.channel
76
87
  this.#httpClient = options.httpClient
77
88
  this.#hooks = options.hooks
89
+ this.#onDelete = options.onDelete
78
90
  this.#getEventSourceStatus = options.getEventSourceStatus
79
91
  }
80
92
 
@@ -83,7 +95,12 @@ export class Subscription {
83
95
  */
84
96
  $runHandler(message: unknown) {
85
97
  for (const handler of this.#handlers) {
86
- handler(message)
98
+ try {
99
+ handler(message)
100
+ } catch (error) {
101
+ // TODO: Rescue
102
+ console.error(error)
103
+ }
87
104
  }
88
105
  }
89
106
 
@@ -92,18 +109,31 @@ export class Subscription {
92
109
  return
93
110
  }
94
111
 
112
+ if (this.#getEventSourceStatus() !== TransmitStatus.Connected && this.#createPending) {
113
+ return this.#createPending
114
+ }
115
+
95
116
  return this.forceCreate()
96
117
  }
97
118
 
98
119
  async forceCreate() {
99
120
  if (this.#getEventSourceStatus() !== TransmitStatus.Connected) {
100
- return new Promise((resolve) => {
121
+ if (this.#createPending) {
122
+ return this.#createPending
123
+ }
124
+
125
+ this.#createPending = new Promise((resolve) => {
101
126
  setTimeout(() => {
127
+ this.#createPending = null
102
128
  resolve(this.create())
103
129
  }, 100)
104
130
  })
131
+
132
+ return this.#createPending
105
133
  }
106
134
 
135
+ this.#createPending = null
136
+
107
137
  const request = this.#httpClient.createRequest('/__transmit/subscribe', {
108
138
  channel: this.#channel,
109
139
  })
@@ -149,6 +179,7 @@ export class Subscription {
149
179
 
150
180
  this.#status = SubscriptionStatus.Deleted
151
181
  this.#hooks?.onUnsubscription(this.#channel)
182
+ this.#onDelete?.()
152
183
  } catch (error) {}
153
184
  }
154
185
 
package/src/transmit.ts CHANGED
@@ -19,8 +19,8 @@ interface TransmitOptions {
19
19
  eventSourceFactory?: (url: string | URL, options: { withCredentials: boolean }) => EventSource
20
20
  eventTargetFactory?: () => EventTarget | null
21
21
  httpClientFactory?: (baseUrl: string, uid: string) => HttpClient
22
- beforeSubscribe?: (request: RequestInit) => void
23
- beforeUnsubscribe?: (request: RequestInit) => void
22
+ beforeSubscribe?: (request: Request) => void
23
+ beforeUnsubscribe?: (request: Request) => void
24
24
  maxReconnectAttempts?: number
25
25
  onReconnectAttempt?: (attempt: number) => void
26
26
  onReconnectFailed?: () => void
@@ -181,7 +181,7 @@ export class Transmit {
181
181
  subscription.$runHandler(data.payload)
182
182
  } catch (error) {
183
183
  // TODO: Rescue
184
- console.log(error)
184
+ console.error(error)
185
185
  }
186
186
  }
187
187
 
@@ -209,17 +209,18 @@ export class Transmit {
209
209
  }
210
210
 
211
211
  subscription(channel: string) {
212
+ if (this.#subscriptions.has(channel)) {
213
+ return this.#subscriptions.get(channel)!
214
+ }
215
+
212
216
  const subscription = new Subscription({
213
217
  channel,
214
218
  httpClient: this.#httpClient,
215
219
  hooks: this.#hooks,
216
220
  getEventSourceStatus: () => this.#status,
221
+ onDelete: () => this.#subscriptions.delete(channel),
217
222
  })
218
223
 
219
- if (this.#subscriptions.has(channel)) {
220
- return this.#subscriptions.get(channel)!
221
- }
222
-
223
224
  this.#subscriptions.set(channel, subscription)
224
225
 
225
226
  return subscription
@@ -230,6 +231,11 @@ export class Transmit {
230
231
  this.#eventTarget?.addEventListener(event, callback)
231
232
  }
232
233
 
234
+ off(event: Exclude<TransmitStatus, 'connecting'>, callback: (event: CustomEvent) => void) {
235
+ // @ts-ignore
236
+ this.#eventTarget?.removeEventListener(event, callback)
237
+ }
238
+
233
239
  close() {
234
240
  this.#eventSource?.close()
235
241
  }