@entrinsik/vite-plugin-informer 2.10.0 → 2.12.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 +7 -3
- package/bin/init.js +13 -2
- package/bin/workspace.js +15 -7
- package/index.d.ts +14 -0
- package/package.json +1 -1
- package/src/agent-dev.js +8 -11
- package/src/dev-bag.js +241 -0
- package/src/dev-channel-handlers.js +335 -0
- package/src/dev-channel-shim.js +276 -39
- package/src/dev-channels.js +138 -28
- package/src/dev-dependencies.js +140 -4
- package/src/dev-platform.js +81 -5
- package/src/dev-streams.js +176 -27
- package/src/env.js +17 -1
- package/src/index.js +87 -9
- package/src/server-routes.js +79 -211
- package/src/streams-client.js +121 -8
package/src/dev-channel-shim.js
CHANGED
|
@@ -1,25 +1,32 @@
|
|
|
1
|
-
import { CHANNEL_NAME, CHANNEL_NAME_MAX_LENGTH, DEV_CHANNEL_EVENT } from './dev-channels.js';
|
|
1
|
+
import { CHANNEL_NAME, CHANNEL_NAME_MAX_LENGTH, EVENT_NAME, EVENT_NAME_MAX_LENGTH, RESERVED_EVENTS, USER_CHANNEL_PREFIX, DEV_CHANNEL_EVENT, DEV_CHANNEL_API } from './dev-channels.js';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* The dev `__INFORMER__.channel(name)` mock: the App Channels client
|
|
5
|
-
* (App API v2 §1.9)
|
|
6
|
-
*
|
|
7
|
-
* codes (`join_refused` | `disconnected` | `not_supported`; `rate_limited`
|
|
8
|
-
* never happens locally), phase-1 `send()` rejection and idempotent
|
|
9
|
-
* `close()` as the origin-mode shim in html-utils.js generateChannelShim.
|
|
4
|
+
* The dev `__INFORMER__.channel(name, opts)` mock: the App Channels client
|
|
5
|
+
* surface (App API v2 §1.9) as the origin-mode shim in html-utils.js
|
|
6
|
+
* generateChannelShim implements it —
|
|
10
7
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
8
|
+
* var ch = __INFORMER__.channel('rooms/east', { since: 12 }); // sync; throws join_refused on a bad name
|
|
9
|
+
* ch.on('created', function (payload, frame) {}); // first handler subscribes
|
|
10
|
+
* ch.on('connected', function (info) {}); // { replayed: n } after each (re)subscribe
|
|
11
|
+
* ch.on('error', function (err) {}); // err.code: join_refused | rate_limited | budget_exhausted | handler_failed | disconnected | replay_gap
|
|
12
|
+
* ch.send(event, payload).then(result); // rejects: send_refused | rate_limited | budget_exhausted | handler_failed | disconnected
|
|
13
|
+
* ch.close();
|
|
14
|
+
*
|
|
15
|
+
* Transport: production subscribes each channel over a nes socket; dev POSTs
|
|
16
|
+
* subscribe / unsubscribe / send to DEV_CHANNEL_API, where the matching
|
|
17
|
+
* channels/ handler runs (dev-channel-handlers.js). Frames ride Vite's own dev
|
|
18
|
+
* websocket as the custom event DEV_CHANNEL_EVENT and reach every page; a
|
|
19
|
+
* channel dispatches only once its subscribe was admitted, matching frames by
|
|
20
|
+
* exact name or, for a `rooms/*` wildcard, by the frame's wildcard parents.
|
|
21
|
+
* Every frame carries a per-channel `seq`; after a dropped socket the shim
|
|
22
|
+
* re-subscribes on `vite:ws:connect` (or the next `on()`), replays from the
|
|
23
|
+
* last seq it saw via GET /replay, drops what it already delivered, and
|
|
24
|
+
* reports `replay_gap` when the buffer no longer reaches back that far.
|
|
19
25
|
*
|
|
20
26
|
* Returned as source (an `installDevChannels(hot, informer)` function
|
|
21
27
|
* declaration) so it can be evaluated in a test `vm` against a fake hot
|
|
22
|
-
* context, the same way the server spec drives the
|
|
28
|
+
* context and a fake fetch, the same way the server spec drives the
|
|
29
|
+
* production shim.
|
|
23
30
|
*/
|
|
24
31
|
export function generateDevChannelShim() {
|
|
25
32
|
return `
|
|
@@ -27,50 +34,153 @@ export function generateDevChannelShim() {
|
|
|
27
34
|
function installDevChannels(hot, informer, unavailableReason) {
|
|
28
35
|
var CHANNEL_NAME = /${CHANNEL_NAME.source}/;
|
|
29
36
|
var CHANNEL_NAME_MAX_LENGTH = ${CHANNEL_NAME_MAX_LENGTH};
|
|
37
|
+
var EVENT_NAME = /${EVENT_NAME.source}/;
|
|
38
|
+
var EVENT_NAME_MAX_LENGTH = ${EVENT_NAME_MAX_LENGTH};
|
|
39
|
+
var RESERVED_EVENTS = ${JSON.stringify(RESERVED_EVENTS)};
|
|
40
|
+
var USER_PREFIX = '${USER_CHANNEL_PREFIX}';
|
|
30
41
|
var FRAME_EVENT = '${DEV_CHANNEL_EVENT}';
|
|
31
|
-
var
|
|
42
|
+
var API = '${DEV_CHANNEL_API}';
|
|
43
|
+
var clientId = newClientId(); // identifies this page to the dev server
|
|
44
|
+
var open = []; // channels holding at least one handler
|
|
45
|
+
|
|
46
|
+
function newClientId() {
|
|
47
|
+
var c = typeof crypto !== 'undefined' ? crypto : null;
|
|
48
|
+
if (c && typeof c.randomUUID === 'function') return c.randomUUID();
|
|
49
|
+
return Math.random().toString(36).slice(2) + Date.now().toString(36);
|
|
50
|
+
}
|
|
32
51
|
|
|
33
|
-
function channelError(code, message) {
|
|
52
|
+
function channelError(code, message, channel) {
|
|
34
53
|
var e = new Error(message);
|
|
35
54
|
e.code = code;
|
|
55
|
+
if (channel) e.channel = channel;
|
|
36
56
|
return e;
|
|
37
57
|
}
|
|
38
58
|
|
|
59
|
+
function isWildcard(name) {
|
|
60
|
+
return name.indexOf(USER_PREFIX) !== 0 && name.slice(-2) === '/*';
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// The wildcard names a frame on \`channel\` also belongs to: a/b/c → a/b/*, a/*.
|
|
64
|
+
function wildcardParents(channel) {
|
|
65
|
+
if (channel.indexOf(USER_PREFIX) === 0) return [];
|
|
66
|
+
var segments = channel.split('/');
|
|
67
|
+
var parents = [];
|
|
68
|
+
for (var i = segments.length - 1; i >= 1; i--) parents.push(segments.slice(0, i).join('/') + '/*');
|
|
69
|
+
return parents;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function covers(ch, frameChannel) {
|
|
73
|
+
if (ch.name === frameChannel) return true;
|
|
74
|
+
return isWildcard(ch.name) && wildcardParents(frameChannel).indexOf(ch.name) !== -1;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// A refused subscribe maps the server's status the way nes errors do.
|
|
78
|
+
// A handler that threw is its own code: 'disconnected' would make a
|
|
79
|
+
// typo in a join handler read exactly like a dropped network.
|
|
80
|
+
function mapStatus(status) {
|
|
81
|
+
if (status === 403) return 'join_refused';
|
|
82
|
+
if (status === 429) return 'rate_limited';
|
|
83
|
+
if (status === 402) return 'budget_exhausted';
|
|
84
|
+
if (status >= 400 && status < 500) return 'join_refused';
|
|
85
|
+
if (status >= 500) return 'handler_failed';
|
|
86
|
+
return 'disconnected';
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function call(method, path, body) {
|
|
90
|
+
var opts = { method: method, credentials: 'same-origin', headers: {} };
|
|
91
|
+
if (body !== undefined) {
|
|
92
|
+
opts.headers['Content-Type'] = 'application/json';
|
|
93
|
+
opts.body = JSON.stringify(body);
|
|
94
|
+
}
|
|
95
|
+
return fetch(API + path, opts).then(function (res) {
|
|
96
|
+
return res.text().then(function (text) {
|
|
97
|
+
var data = null;
|
|
98
|
+
try { data = text ? JSON.parse(text) : null; } catch (e) { data = null; }
|
|
99
|
+
return { ok: res.ok, status: res.status, body: data };
|
|
100
|
+
});
|
|
101
|
+
}, function (e) {
|
|
102
|
+
throw channelError('disconnected', 'The dev server could not be reached: ' + ((e && e.message) || e));
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function refusal(res, channel, fallback) {
|
|
107
|
+
return channelError(mapStatus(res.status), (res.body && res.body.error) || (fallback + ' (' + res.status + ')'), channel);
|
|
108
|
+
}
|
|
109
|
+
|
|
39
110
|
function deliver(frame) {
|
|
40
111
|
if (!frame || typeof frame.channel !== 'string') return;
|
|
41
112
|
open.slice().forEach(function (ch) {
|
|
42
|
-
if (ch
|
|
113
|
+
if (covers(ch, frame.channel)) ch._deliver(frame);
|
|
43
114
|
});
|
|
44
115
|
}
|
|
45
116
|
|
|
117
|
+
// A dropped socket: every open channel reports it and re-subscribes on
|
|
118
|
+
// reconnect, replaying from the last seq it saw.
|
|
46
119
|
function dropAll() {
|
|
47
120
|
open.slice().forEach(function (ch) {
|
|
48
|
-
ch.
|
|
121
|
+
ch._admitted = false;
|
|
122
|
+
ch._resume = true;
|
|
123
|
+
ch._emitError(channelError('disconnected', 'The dev server connection was lost', ch.name));
|
|
49
124
|
});
|
|
50
125
|
}
|
|
51
126
|
|
|
127
|
+
function subscribeAll() {
|
|
128
|
+
open.slice().forEach(function (ch) { ch._subscribe(); });
|
|
129
|
+
}
|
|
130
|
+
|
|
52
131
|
if (hot) {
|
|
53
132
|
hot.on(FRAME_EVENT, deliver);
|
|
54
133
|
hot.on('vite:ws:disconnect', dropAll);
|
|
134
|
+
hot.on('vite:ws:connect', subscribeAll);
|
|
55
135
|
}
|
|
56
136
|
|
|
57
|
-
function Channel(name) {
|
|
137
|
+
function Channel(name, opts) {
|
|
58
138
|
this.name = name;
|
|
59
|
-
|
|
139
|
+
// null-prototype: an event name admits 'constructor' and 'toString',
|
|
140
|
+
// which resolve through Object.prototype on a plain object
|
|
141
|
+
this._handlers = Object.create(null);
|
|
60
142
|
this._closed = false;
|
|
61
143
|
this._unavailable = false;
|
|
144
|
+
this._admitted = false;
|
|
145
|
+
this._subscribing = null; // in-flight subscribe (never rejects)
|
|
146
|
+
this._replaying = false; // live frames queue behind an in-flight replay
|
|
147
|
+
this._pending = [];
|
|
148
|
+
this._lastSeq = Object.create(null); // concrete channel → highest seq seen
|
|
149
|
+
this._replayedThrough = Object.create(null); // → how far a replay caught it up
|
|
150
|
+
this._since = (opts && typeof opts.since === 'number') ? opts.since : null;
|
|
151
|
+
this._resume = this._since !== null; // replay on the next subscribe
|
|
62
152
|
}
|
|
63
153
|
|
|
64
|
-
Channel.prototype.
|
|
65
|
-
|
|
66
|
-
var list = this._handlers[frame.event];
|
|
154
|
+
Channel.prototype._dispatch = function (event, payload, frame) {
|
|
155
|
+
var list = this._handlers[event];
|
|
67
156
|
if (!list) return;
|
|
68
157
|
list.slice().forEach(function (fn) {
|
|
69
|
-
try { fn(
|
|
158
|
+
try { fn(payload, frame); }
|
|
70
159
|
catch (e) { console.error('[Informer] channel handler failed:', e); }
|
|
71
160
|
});
|
|
72
161
|
};
|
|
73
162
|
|
|
163
|
+
// Only a frame the replay already handed over is a duplicate. Seq is
|
|
164
|
+
// allocated atomically but published separately, so on a real server two
|
|
165
|
+
// concurrent broadcasts can arrive out of seq order; deduping against
|
|
166
|
+
// the running max would drop the lower one for good.
|
|
167
|
+
// Returns whether it was dispatched.
|
|
168
|
+
Channel.prototype._accept = function (frame) {
|
|
169
|
+
if (typeof frame.seq === 'number') {
|
|
170
|
+
if (frame.seq <= (this._replayedThrough[frame.channel] || 0)) return false;
|
|
171
|
+
if (frame.seq > (this._lastSeq[frame.channel] || 0)) this._lastSeq[frame.channel] = frame.seq;
|
|
172
|
+
}
|
|
173
|
+
this._dispatch(frame.event, frame.payload, frame);
|
|
174
|
+
return true;
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
// A live frame in: only once admitted, and behind any in-flight replay.
|
|
178
|
+
Channel.prototype._deliver = function (frame) {
|
|
179
|
+
if (this._closed || !this._admitted) return false;
|
|
180
|
+
if (this._replaying) { this._pending.push(frame); return false; }
|
|
181
|
+
return this._accept(frame);
|
|
182
|
+
};
|
|
183
|
+
|
|
74
184
|
Channel.prototype._emitError = function (err) {
|
|
75
185
|
if (this._closed) return;
|
|
76
186
|
var list = this._handlers.error;
|
|
@@ -84,6 +194,89 @@ export function generateDevChannelShim() {
|
|
|
84
194
|
});
|
|
85
195
|
};
|
|
86
196
|
|
|
197
|
+
Channel.prototype._subscribe = function () {
|
|
198
|
+
var self = this;
|
|
199
|
+
if (!hot || this._closed || this._admitted || this._subscribing) return;
|
|
200
|
+
this._subscribing = call('POST', '/subscribe', { clientId: clientId, channel: this.name }).then(function (res) {
|
|
201
|
+
if (!res.ok) throw refusal(res, self.name, 'Subscribe refused');
|
|
202
|
+
if (self._closed) return 0;
|
|
203
|
+
self._admitted = true;
|
|
204
|
+
return self._replay();
|
|
205
|
+
}).then(function (replayed) {
|
|
206
|
+
self._subscribing = null;
|
|
207
|
+
if (self._closed || !self._admitted) return;
|
|
208
|
+
self._dispatch('connected', { replayed: replayed || 0 });
|
|
209
|
+
}, function (err) {
|
|
210
|
+
self._subscribing = null;
|
|
211
|
+
self._emitError(err);
|
|
212
|
+
});
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
// Catch up from the dev server's buffer: the concrete channels this
|
|
216
|
+
// handle has seen frames from (or itself, from \`since\` on a first
|
|
217
|
+
// subscribe). Live frames arriving meanwhile queue behind the replay so
|
|
218
|
+
// the seq dedupe sees the buffered frames first. Resolves the count delivered.
|
|
219
|
+
Channel.prototype._replay = function () {
|
|
220
|
+
var self = this;
|
|
221
|
+
var since = this._since;
|
|
222
|
+
this._since = null;
|
|
223
|
+
var targets = [];
|
|
224
|
+
if (this._resume) {
|
|
225
|
+
if (isWildcard(this.name)) targets = Object.keys(this._lastSeq);
|
|
226
|
+
else if (since !== null || this._lastSeq[this.name] !== undefined) targets = [this.name];
|
|
227
|
+
}
|
|
228
|
+
this._resume = false;
|
|
229
|
+
if (!targets.length) return Promise.resolve(0);
|
|
230
|
+
var delivered = 0;
|
|
231
|
+
this._replaying = true;
|
|
232
|
+
return targets.reduce(function (chain, concrete) {
|
|
233
|
+
return chain.then(function () {
|
|
234
|
+
var last = self._lastSeq[concrete] || 0;
|
|
235
|
+
if (since !== null && since > last) last = since;
|
|
236
|
+
return call('GET', '/replay?channel=' + encodeURIComponent(concrete) + '&since=' + last).then(function (res) {
|
|
237
|
+
if (!res.ok) throw refusal(res, concrete, 'Replay refused');
|
|
238
|
+
var data = res.body || {};
|
|
239
|
+
var current = typeof data.current === 'number' ? data.current : 0;
|
|
240
|
+
var oldest = typeof data.oldest === 'number' ? data.oldest : null;
|
|
241
|
+
if (current < last) {
|
|
242
|
+
// the counter restarted (an idle channel's key expired,
|
|
243
|
+
// or redis was flushed): nothing seen applies
|
|
244
|
+
delete self._lastSeq[concrete];
|
|
245
|
+
delete self._replayedThrough[concrete];
|
|
246
|
+
last = 0;
|
|
247
|
+
} else if (current > last && (oldest === null || oldest > last + 1)) {
|
|
248
|
+
self._emitError(channelError('replay_gap', 'Frames on "' + concrete + '" after seq ' + last + ' are no longer buffered', concrete));
|
|
249
|
+
}
|
|
250
|
+
// the mark queued live frames dedupe against: what the
|
|
251
|
+
// page already held, raised as the replay hands frames
|
|
252
|
+
// over. Set before the loop, so a frame at or below
|
|
253
|
+
// that seq is dropped rather than dispatched twice.
|
|
254
|
+
self._replayedThrough[concrete] = last;
|
|
255
|
+
(data.frames || []).forEach(function (frame) {
|
|
256
|
+
if (self._closed || !frame || frame.channel !== concrete) return;
|
|
257
|
+
if (self._accept(frame)) delivered++;
|
|
258
|
+
if (typeof frame.seq === 'number' && frame.seq > self._replayedThrough[concrete]) self._replayedThrough[concrete] = frame.seq;
|
|
259
|
+
});
|
|
260
|
+
}).catch(function (err) {
|
|
261
|
+
// a read refused for one channel (over the inbound rate,
|
|
262
|
+
// say) is reported on that channel and must not throw
|
|
263
|
+
// away the others, as on the server
|
|
264
|
+
self._emitError(err && err.code ? err : channelError('disconnected', String((err && err.message) || err), concrete));
|
|
265
|
+
});
|
|
266
|
+
});
|
|
267
|
+
}, Promise.resolve()).then(function () {
|
|
268
|
+
self._replaying = false;
|
|
269
|
+
var queued = self._pending;
|
|
270
|
+
self._pending = [];
|
|
271
|
+
queued.forEach(function (frame) { self._deliver(frame); });
|
|
272
|
+
return delivered;
|
|
273
|
+
}, function (err) {
|
|
274
|
+
self._replaying = false;
|
|
275
|
+
self._pending = [];
|
|
276
|
+
throw err;
|
|
277
|
+
});
|
|
278
|
+
};
|
|
279
|
+
|
|
87
280
|
Channel.prototype.on = function (event, fn) {
|
|
88
281
|
if (this._closed) throw channelError('disconnected', 'The channel is closed');
|
|
89
282
|
if (typeof event !== 'string' || !event) throw new TypeError('channel.on: event name required');
|
|
@@ -91,14 +284,29 @@ export function generateDevChannelShim() {
|
|
|
91
284
|
var list = this._handlers[event] || (this._handlers[event] = []);
|
|
92
285
|
list.push(fn);
|
|
93
286
|
if (open.indexOf(this) === -1) open.push(this);
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
287
|
+
var self = this;
|
|
288
|
+
if (!hot) {
|
|
289
|
+
if (!this._unavailable) {
|
|
290
|
+
// Not served by the Vite dev server: no frames can ever arrive.
|
|
291
|
+
// Reported once, asynchronously, like a failed connection.
|
|
292
|
+
this._unavailable = true;
|
|
293
|
+
Promise.resolve().then(function () {
|
|
294
|
+
self._emitError(channelError('disconnected', unavailableReason || 'The Vite dev websocket is not available', self.name));
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
} else if (this._admitted) {
|
|
298
|
+
// A late listener still learns the channel is up.
|
|
299
|
+
if (event === 'connected') {
|
|
300
|
+
Promise.resolve().then(function () {
|
|
301
|
+
if (self._closed || list.indexOf(fn) === -1) return;
|
|
302
|
+
try { fn({ replayed: 0 }); }
|
|
303
|
+
catch (e) { console.error('[Informer] channel handler failed:', e); }
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
} else {
|
|
307
|
+
// A use: after a dropped socket, bring every open channel back
|
|
308
|
+
// before this one registers its own handlers.
|
|
309
|
+
subscribeAll();
|
|
102
310
|
}
|
|
103
311
|
return function () {
|
|
104
312
|
var i = list.indexOf(fn);
|
|
@@ -106,8 +314,28 @@ export function generateDevChannelShim() {
|
|
|
106
314
|
};
|
|
107
315
|
};
|
|
108
316
|
|
|
109
|
-
Channel.prototype.send = function () {
|
|
110
|
-
|
|
317
|
+
Channel.prototype.send = function (event, payload) {
|
|
318
|
+
var self = this;
|
|
319
|
+
if (this._closed) return Promise.reject(channelError('disconnected', 'The channel is closed', this.name));
|
|
320
|
+
if (typeof event !== 'string' || !event || event.length > EVENT_NAME_MAX_LENGTH || !EVENT_NAME.test(event) || RESERVED_EVENTS.indexOf(event) !== -1) {
|
|
321
|
+
return Promise.reject(channelError('send_refused', 'Invalid event name: ' + event, this.name));
|
|
322
|
+
}
|
|
323
|
+
if (isWildcard(this.name)) {
|
|
324
|
+
return Promise.reject(channelError('send_refused', 'Cannot send on a wildcard channel; send to a concrete channel instead', this.name));
|
|
325
|
+
}
|
|
326
|
+
if (!hot) return Promise.reject(channelError('disconnected', unavailableReason || 'The Vite dev websocket is not available', this.name));
|
|
327
|
+
// a send right after the first on(): let the subscribe land first
|
|
328
|
+
return (this._subscribing || Promise.resolve()).then(function () {
|
|
329
|
+
return call('POST', '/send', { clientId: clientId, channel: self.name, event: event, payload: payload === undefined ? null : payload });
|
|
330
|
+
}).then(function (res) {
|
|
331
|
+
if (res.ok) return res.body ? res.body.result : null;
|
|
332
|
+
var message = (res.body && res.body.error) || ('Send refused (' + res.status + ')');
|
|
333
|
+
if (res.status === 429) throw channelError('rate_limited', message, self.name);
|
|
334
|
+
if (res.status === 402) throw channelError('budget_exhausted', message, self.name);
|
|
335
|
+
if (res.status >= 400 && res.status < 500) throw channelError('send_refused', message, self.name);
|
|
336
|
+
if (res.status >= 500) throw channelError('handler_failed', message, self.name);
|
|
337
|
+
throw channelError('disconnected', message, self.name);
|
|
338
|
+
});
|
|
111
339
|
};
|
|
112
340
|
|
|
113
341
|
Channel.prototype.close = function () {
|
|
@@ -115,14 +343,23 @@ export function generateDevChannelShim() {
|
|
|
115
343
|
this._closed = true;
|
|
116
344
|
var i = open.indexOf(this);
|
|
117
345
|
if (i !== -1) open.splice(i, 1);
|
|
118
|
-
this._handlers =
|
|
346
|
+
this._handlers = Object.create(null);
|
|
347
|
+
var pending = this._subscribing;
|
|
348
|
+
var subscribed = this._admitted || pending;
|
|
349
|
+
this._admitted = false;
|
|
350
|
+
if (!hot || !subscribed) return;
|
|
351
|
+
var self = this;
|
|
352
|
+
// after an in-flight subscribe lands, so the server forgets what it just recorded
|
|
353
|
+
(pending || Promise.resolve()).then(function () {
|
|
354
|
+
return call('POST', '/unsubscribe', { clientId: clientId, channel: self.name });
|
|
355
|
+
}).catch(function () {});
|
|
119
356
|
};
|
|
120
357
|
|
|
121
|
-
informer.channel = function (name) {
|
|
358
|
+
informer.channel = function (name, opts) {
|
|
122
359
|
if (typeof name !== 'string' || name.length > CHANNEL_NAME_MAX_LENGTH || !CHANNEL_NAME.test(name)) {
|
|
123
360
|
throw channelError('join_refused', 'Invalid channel name: ' + name);
|
|
124
361
|
}
|
|
125
|
-
return new Channel(name);
|
|
362
|
+
return new Channel(name, opts);
|
|
126
363
|
};
|
|
127
364
|
}`;
|
|
128
365
|
}
|