wenay-common2 1.0.74 → 1.0.76

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.
@@ -2,6 +2,55 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.serveReplayChannel = serveReplayChannel;
4
4
  exports.channelReplayRemote = channelReplayRemote;
5
+ const BASE64 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
6
+ const REPLAY_BYTES = '__wenayReplayBytes';
7
+ function bytesToBase64(bytes) {
8
+ let out = '';
9
+ for (let i = 0; i < bytes.length; i += 3) {
10
+ const a = bytes[i];
11
+ const b = bytes[i + 1];
12
+ const c = bytes[i + 2];
13
+ out += BASE64[a >> 2];
14
+ out += BASE64[((a & 3) << 4) | ((b ?? 0) >> 4)];
15
+ out += b == null ? '=' : BASE64[((b & 15) << 2) | ((c ?? 0) >> 6)];
16
+ out += c == null ? '=' : BASE64[c & 63];
17
+ }
18
+ return out;
19
+ }
20
+ function base64ToBytes(text) {
21
+ const clean = text.replace(/=+$/, '');
22
+ const out = new Uint8Array(Math.floor(clean.length * 3 / 4));
23
+ let bits = 0;
24
+ let nBits = 0;
25
+ let at = 0;
26
+ for (const char of clean) {
27
+ const n = BASE64.indexOf(char);
28
+ if (n < 0)
29
+ throw new Error('replay channel: invalid binary payload');
30
+ bits = (bits << 6) | n;
31
+ nBits += 6;
32
+ if (nBits < 8)
33
+ continue;
34
+ nBits -= 8;
35
+ out[at++] = (bits >> nBits) & 255;
36
+ }
37
+ return out;
38
+ }
39
+ function stringifyMessage(value) {
40
+ return JSON.stringify(value, function encodeReplayBytes(_key, item) {
41
+ if (item instanceof Uint8Array)
42
+ return { [REPLAY_BYTES]: bytesToBase64(item) };
43
+ return item;
44
+ });
45
+ }
46
+ function parseMessage(raw) {
47
+ return JSON.parse(raw, function decodeReplayBytes(_key, item) {
48
+ if (item != null && typeof item == 'object' && Object.keys(item).length == 1 && typeof item[REPLAY_BYTES] == 'string') {
49
+ return base64ToBytes(item[REPLAY_BYTES]);
50
+ }
51
+ return item;
52
+ });
53
+ }
5
54
  function unsubscribeHandle(handle) {
6
55
  if (typeof handle == 'function') {
7
56
  handle();
@@ -22,11 +71,11 @@ function serveReplayChannel(source, channel) {
22
71
  : msg.m == 'frame' ? (source.frame ? await source.frame(msg.a[0], msg.a[1]) : null)
23
72
  : undefined;
24
73
  if (!closed)
25
- channel.send(JSON.stringify({ t: 'res', id: msg.id, ok: true, v: v ?? null }));
74
+ channel.send(stringifyMessage({ t: 'res', id: msg.id, ok: true, v: v ?? null }));
26
75
  }
27
76
  catch (e) {
28
77
  if (!closed)
29
- channel.send(JSON.stringify({ t: 'res', id: msg.id, ok: false, e: String(e) }));
78
+ channel.send(stringifyMessage({ t: 'res', id: msg.id, ok: false, e: String(e) }));
30
79
  }
31
80
  }
32
81
  const offMsg = channel.onMessage(function onReplayChannelMessage(raw) {
@@ -34,7 +83,7 @@ function serveReplayChannel(source, channel) {
34
83
  return;
35
84
  let msg;
36
85
  try {
37
- msg = JSON.parse(raw);
86
+ msg = parseMessage(raw);
38
87
  }
39
88
  catch {
40
89
  return;
@@ -42,7 +91,7 @@ function serveReplayChannel(source, channel) {
42
91
  if (msg?.t == 'sub' && !lineOff) {
43
92
  lineOff = source.line.on(function forwardEnvelope(ev) {
44
93
  if (!closed)
45
- channel.send(JSON.stringify({ t: 'ev', ev }));
94
+ channel.send(stringifyMessage({ t: 'ev', ev }));
46
95
  });
47
96
  return;
48
97
  }
@@ -70,7 +119,7 @@ function channelReplayRemote(channel) {
70
119
  channel.onMessage(function onRemoteChannelMessage(raw) {
71
120
  let msg;
72
121
  try {
73
- msg = JSON.parse(raw);
122
+ msg = parseMessage(raw);
74
123
  }
75
124
  catch {
76
125
  return;
@@ -108,7 +157,7 @@ function channelReplayRemote(channel) {
108
157
  return new Promise((resolve, reject) => {
109
158
  const id = nextId++;
110
159
  pending.set(id, { resolve, reject });
111
- channel.send(JSON.stringify({ t: 'req', id, m, a }));
160
+ channel.send(stringifyMessage({ t: 'req', id, m, a }));
112
161
  });
113
162
  }
114
163
  return {
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.replayRouteSubscribe = replayRouteSubscribe;
4
+ const transport_lifecycle_1 = require("./transport-lifecycle");
4
5
  function unsubscribeHandle(handle) {
5
6
  if (typeof handle == 'function') {
6
7
  handle();
@@ -50,7 +51,8 @@ function replayRouteSubscribe(remote, cb, opts = {}) {
50
51
  let replaying = true;
51
52
  let lineError;
52
53
  const queue = [];
53
- const liveLine = policy == 'frame' && nextRemote.frameLine ? nextRemote.frameLine : nextRemote.line;
54
+ const frameLineState = (0, transport_lifecycle_1.getRpcMemberState)(nextRemote, 'frameLine');
55
+ const liveLine = policy == 'frame' && frameLineState != false && nextRemote.frameLine ? nextRemote.frameLine : nextRemote.line;
54
56
  const handle = liveLine.on(function liveTap(ev) {
55
57
  if (slotClosed)
56
58
  return;
@@ -76,7 +78,8 @@ function replayRouteSubscribe(remote, cb, opts = {}) {
76
78
  async function catchUp() {
77
79
  try {
78
80
  let done = false;
79
- if (since >= 0 && nextRemote.frame) {
81
+ const frameState = (0, transport_lifecycle_1.getRpcMemberState)(nextRemote, 'frame');
82
+ if (since >= 0 && frameState != false && nextRemote.frame) {
80
83
  const envs = await nextRemote.frame(since, hint);
81
84
  if (slotClosed)
82
85
  return;
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.exposeReplay = exposeReplay;
4
4
  exports.replaySubscribe = replaySubscribe;
5
5
  const replay_conflate_1 = require("./replay-conflate");
6
+ const transport_lifecycle_1 = require("./transport-lifecycle");
6
7
  function exposeReplayPlain(replay) {
7
8
  return {
8
9
  line: replay.line,
@@ -29,10 +30,21 @@ function unsubscribeHandle(handle) {
29
30
  }
30
31
  function replaySubscribe(remote, cb, opts = {}) {
31
32
  const { since = -1, onSeq, onError, staleMs, onStale, skewMs = 0, now = Date.now, policy = 'queue', hint } = opts;
33
+ const lifecycle = (0, transport_lifecycle_1.getRpcTransportLifecycle)(remote);
32
34
  let lastDelivered = since;
33
35
  let replaying = true;
34
36
  let closed = false;
37
+ let recoveryGeneration = 0;
35
38
  const queue = [];
39
+ let readySettled = false;
40
+ let resolveReady = function resolveReadyLater() { };
41
+ const ready = new Promise(function waitUntilReady(resolve) { resolveReady = resolve; });
42
+ function settleReady() {
43
+ if (readySettled)
44
+ return;
45
+ readySettled = true;
46
+ resolveReady();
47
+ }
36
48
  let lastTs = 0;
37
49
  let lastArrival = now();
38
50
  let staleFlag = false;
@@ -95,17 +107,68 @@ function replaySubscribe(remote, cb, opts = {}) {
95
107
  if (!replaying)
96
108
  assessStale();
97
109
  }
98
- const liveLine = policy == 'frame' && remote.frameLine ? remote.frameLine : remote.line;
99
- const handle = liveLine.on(function liveTap(ev) {
110
+ function deliverSorted(events, allowReset) {
111
+ const sorted = [...events].sort((a, b) => a.seq - b.seq);
112
+ if (allowReset && sorted.length && sorted[0].seq <= lastDelivered) {
113
+ lastDelivered = sorted[0].seq - 1;
114
+ }
115
+ for (const event of sorted)
116
+ deliver(event);
117
+ }
118
+ function drainLiveQueue() {
119
+ while (queue.length) {
120
+ const batch = queue.splice(0).sort((a, b) => a.seq - b.seq);
121
+ for (const event of batch)
122
+ deliver(event);
123
+ }
124
+ }
125
+ const frameLineState = (0, transport_lifecycle_1.getRpcMemberState)(remote, 'frameLine');
126
+ const liveLine = policy == 'frame' && frameLineState != false && remote.frameLine ? remote.frameLine : remote.line;
127
+ let handle = null;
128
+ let offConnect = function noConnectListener() { };
129
+ let offDisconnect = function noDisconnectListener() { };
130
+ let offClose = function noCloseListener() { };
131
+ function closeSubscription() {
132
+ if (closed)
133
+ return false;
134
+ closed = true;
135
+ recoveryGeneration++;
136
+ queue.length = 0;
137
+ stopStaleTimer();
138
+ offConnect();
139
+ offDisconnect();
140
+ offClose();
141
+ unsubscribeHandle(handle);
142
+ settleReady();
143
+ return true;
144
+ }
145
+ function errorText(error) {
146
+ if (typeof error?.message == 'string')
147
+ return error.message;
148
+ if (typeof error?.error?.message == 'string')
149
+ return error.error.message;
150
+ return String(error);
151
+ }
152
+ function failRecovery(error, point, phase) {
153
+ const wrapped = new Error('replaySubscribe: ' + phase + ' from seq ' + point + ' failed: ' + errorText(error));
154
+ wrapped.cause = error;
155
+ if (!closeSubscription())
156
+ return;
157
+ if (onError) {
158
+ try {
159
+ onError(wrapped);
160
+ }
161
+ catch (caught) {
162
+ setTimeout(function rethrowOnError() { throw caught; }, 0);
163
+ }
164
+ }
165
+ else {
166
+ setTimeout(function rethrowRecoveryError() { throw wrapped; }, 0);
167
+ }
168
+ }
169
+ handle = liveLine.on(function liveTap(ev) {
100
170
  if (ev == null || typeof ev.seq != 'number') {
101
- if (closed)
102
- return;
103
- const err = new Error('replaySubscribe: line ended by server (' + String(ev) + ')');
104
- off();
105
- if (onError)
106
- onError(err);
107
- else
108
- setTimeout(function rethrowLineEnd() { throw err; }, 0);
171
+ failRecovery(new Error('line ended by server (' + String(ev) + ')'), lastDelivered, 'live line');
109
172
  return;
110
173
  }
111
174
  lastArrival = now();
@@ -114,64 +177,91 @@ function replaySubscribe(remote, cb, opts = {}) {
114
177
  else
115
178
  deliver(ev);
116
179
  });
117
- async function catchUp() {
180
+ if (typeof handle?.then == 'function') {
181
+ handle.then(function logicalLineEnded() {
182
+ if (!closed)
183
+ failRecovery(new Error('logical RPC line ended'), lastDelivered, 'live line');
184
+ }, function logicalLineFailed(error) {
185
+ if (!closed)
186
+ failRecovery(error, lastDelivered, 'live line');
187
+ });
188
+ }
189
+ function isCurrent(generation) {
190
+ return !closed && generation == recoveryGeneration;
191
+ }
192
+ async function catchUp(generation, point, initial) {
118
193
  try {
119
194
  let done = false;
120
- if (since >= 0 && remote.frame) {
121
- const envs = await remote.frame(since, hint);
122
- if (closed)
195
+ const frameState = (0, transport_lifecycle_1.getRpcMemberState)(remote, 'frame');
196
+ if (point >= 0 && frameState != false && remote.frame) {
197
+ const envelopes = await remote.frame(point, hint);
198
+ if (!isCurrent(generation))
123
199
  return;
124
- if (envs) {
125
- if (envs.length) {
126
- lastDelivered = envs[0].seq - 1;
127
- for (const ev of envs)
128
- deliver(ev);
129
- }
200
+ if (envelopes) {
201
+ deliverSorted(envelopes, initial);
130
202
  done = true;
131
203
  }
132
204
  }
133
205
  if (!done) {
134
- const tail = since >= 0 ? await remote.since(since) : null;
135
- if (closed)
206
+ const tail = point >= 0 ? await remote.since(point) : null;
207
+ if (!isCurrent(generation))
136
208
  return;
137
209
  if (tail) {
138
- for (const ev of tail)
139
- deliver(ev);
210
+ deliverSorted(tail, false);
140
211
  }
141
212
  else {
142
- const kf = await remote.keyframe();
143
- if (closed)
213
+ const keyframe = await remote.keyframe();
214
+ if (!isCurrent(generation))
144
215
  return;
145
- if (kf) {
146
- lastDelivered = kf.seq;
147
- lastTs = kf.ts;
148
- lastArrival = now();
149
- cb(...kf.event);
150
- onSeq?.(kf.seq);
216
+ if (keyframe) {
217
+ if (initial && keyframe.seq <= lastDelivered)
218
+ lastDelivered = keyframe.seq - 1;
219
+ deliver(keyframe);
220
+ }
221
+ else if (point >= 0) {
222
+ throw new Error('journal evicted or unavailable; no keyframe can cover the gap');
151
223
  }
152
224
  }
153
225
  }
154
- }
155
- catch (e) {
156
- if (onError)
157
- onError(e);
158
- else
159
- setTimeout(function rethrowCatchUp() { throw e; }, 0);
160
- }
161
- finally {
162
- while (queue.length)
163
- deliver(queue.shift());
226
+ if (!isCurrent(generation))
227
+ return;
228
+ drainLiveQueue();
164
229
  replaying = false;
165
230
  assessStale();
231
+ settleReady();
232
+ }
233
+ catch (error) {
234
+ if (!isCurrent(generation))
235
+ return;
236
+ failRecovery(error, point, initial ? 'initial catch-up' : 'reconnect catch-up');
166
237
  }
167
238
  }
168
- const ready = catchUp();
169
- function off() {
239
+ function startCatchUp(initial) {
170
240
  if (closed)
171
241
  return;
172
- closed = true;
173
- stopStaleTimer();
174
- unsubscribeHandle(handle);
242
+ replaying = true;
243
+ const generation = ++recoveryGeneration;
244
+ void catchUp(generation, lastDelivered, initial);
245
+ }
246
+ if (lifecycle) {
247
+ offDisconnect = lifecycle.onDisconnect(function replayTransportDisconnected() {
248
+ if (closed)
249
+ return;
250
+ replaying = true;
251
+ recoveryGeneration++;
252
+ queue.length = 0;
253
+ });
254
+ offConnect = lifecycle.onConnect(function replayTransportConnected() {
255
+ startCatchUp(!readySettled);
256
+ });
257
+ offClose = lifecycle.onClose(function replayTransportClosed() {
258
+ closeSubscription();
259
+ });
260
+ }
261
+ if (!lifecycle || lifecycle.connected())
262
+ startCatchUp(true);
263
+ function off() {
264
+ closeSubscription();
175
265
  }
176
266
  return Object.assign(off, {
177
267
  ready,
@@ -0,0 +1,23 @@
1
+ export declare const RPC_TRANSPORT_LIFECYCLE: unique symbol;
2
+ export declare const RPC_TRANSPORT_CONTROL: unique symbol;
3
+ export declare const RPC_MEMBER_LOOKUP: unique symbol;
4
+ export type RpcMemberLookup = (member: string) => boolean | undefined;
5
+ export declare function getRpcMemberState(remote: any, member: string): boolean | undefined;
6
+ export type TransportLifecycleApi = {
7
+ connected: () => boolean;
8
+ closed: () => boolean;
9
+ generation: () => number;
10
+ onConnect: (cb: (generation: number) => void) => () => void;
11
+ onDisconnect: (cb: (reason: string, generation: number) => void) => () => void;
12
+ onClose: (cb: (reason: string) => void) => () => void;
13
+ };
14
+ export declare function getRpcTransportLifecycle(remote: any): TransportLifecycleApi | undefined;
15
+ export type TransportLifecycleControl = {
16
+ connect: () => void;
17
+ disconnect: (reason: string) => void;
18
+ close: (reason: string) => void;
19
+ };
20
+ export declare function createTransportLifecycle(initialConnected?: boolean): {
21
+ api: TransportLifecycleApi;
22
+ control: TransportLifecycleControl;
23
+ };
@@ -0,0 +1,94 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.RPC_MEMBER_LOOKUP = exports.RPC_TRANSPORT_CONTROL = exports.RPC_TRANSPORT_LIFECYCLE = void 0;
4
+ exports.getRpcMemberState = getRpcMemberState;
5
+ exports.getRpcTransportLifecycle = getRpcTransportLifecycle;
6
+ exports.createTransportLifecycle = createTransportLifecycle;
7
+ exports.RPC_TRANSPORT_LIFECYCLE = Symbol.for('wenay-common2.rpc.transportLifecycle');
8
+ exports.RPC_TRANSPORT_CONTROL = Symbol.for('wenay-common2.rpc.transportControl');
9
+ exports.RPC_MEMBER_LOOKUP = Symbol.for('wenay-common2.rpc.memberLookup');
10
+ function getRpcMemberState(remote, member) {
11
+ let lookup;
12
+ try {
13
+ const candidate = remote?.[exports.RPC_MEMBER_LOOKUP];
14
+ if (typeof candidate != 'function')
15
+ return undefined;
16
+ if (Object.getOwnPropertyDescriptor(candidate, exports.RPC_MEMBER_LOOKUP)?.value != true)
17
+ return undefined;
18
+ lookup = candidate;
19
+ }
20
+ catch {
21
+ return undefined;
22
+ }
23
+ return lookup(member);
24
+ }
25
+ function getRpcTransportLifecycle(remote) {
26
+ try {
27
+ const candidate = remote?.[exports.RPC_TRANSPORT_LIFECYCLE];
28
+ if (candidate == null || (typeof candidate != 'object' && typeof candidate != 'function'))
29
+ return undefined;
30
+ if (Object.getOwnPropertyDescriptor(candidate, exports.RPC_TRANSPORT_LIFECYCLE)?.value != true)
31
+ return undefined;
32
+ return candidate;
33
+ }
34
+ catch {
35
+ return undefined;
36
+ }
37
+ }
38
+ function createTransportLifecycle(initialConnected = true) {
39
+ let online = initialConnected;
40
+ let terminal = false;
41
+ let generation = initialConnected ? 1 : 0;
42
+ const connectCbs = new Set();
43
+ const disconnectCbs = new Set();
44
+ const closeCbs = new Set();
45
+ function onConnect(cb) {
46
+ connectCbs.add(cb);
47
+ return function offConnect() { connectCbs.delete(cb); };
48
+ }
49
+ function onDisconnect(cb) {
50
+ disconnectCbs.add(cb);
51
+ return function offDisconnect() { disconnectCbs.delete(cb); };
52
+ }
53
+ function onClose(cb) {
54
+ closeCbs.add(cb);
55
+ return function offClose() { closeCbs.delete(cb); };
56
+ }
57
+ function connect() {
58
+ if (terminal || online)
59
+ return;
60
+ online = true;
61
+ generation++;
62
+ for (const cb of [...connectCbs])
63
+ cb(generation);
64
+ }
65
+ function disconnect(reason) {
66
+ if (terminal || !online)
67
+ return;
68
+ online = false;
69
+ for (const cb of [...disconnectCbs])
70
+ cb(reason, generation);
71
+ }
72
+ function close(reason) {
73
+ if (terminal)
74
+ return;
75
+ terminal = true;
76
+ online = false;
77
+ for (const cb of [...closeCbs])
78
+ cb(reason);
79
+ connectCbs.clear();
80
+ disconnectCbs.clear();
81
+ closeCbs.clear();
82
+ }
83
+ const api = {
84
+ connected: () => online,
85
+ closed: () => terminal,
86
+ generation: () => generation,
87
+ onConnect,
88
+ onDisconnect,
89
+ onClose,
90
+ };
91
+ Object.defineProperty(api, exports.RPC_TRANSPORT_LIFECYCLE, { value: true });
92
+ const control = { connect, disconnect, close };
93
+ return { api, control };
94
+ }