wenay-common2 1.0.55 → 1.0.57

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.
@@ -33,7 +33,9 @@ function reactive(root, opts = {}) {
33
33
  const { drain = 'immediate', depth = Infinity, eager = false } = opts;
34
34
  const fire = scheduler(drain);
35
35
  const eng = {
36
- live: 0, pathLive: 0, dirty: new Set(), dirtyPaths: [], scheduled: false, waiters: new Set(), depth,
36
+ live: 0, pathLive: 0, dirty: new Set(), dirtyPaths: [],
37
+ dirtyPathKeys: new Set(), pathKey: createPathKeyer(),
38
+ scheduled: false, waiters: new Set(), depth,
37
39
  schedule() {
38
40
  if (eng.scheduled)
39
41
  return;
@@ -44,6 +46,8 @@ function reactive(root, opts = {}) {
44
46
  eng.dirty.clear();
45
47
  const dirtyPaths = eng.dirtyPaths;
46
48
  eng.dirtyPaths = [];
49
+ eng.dirtyPathKeys = new Set();
50
+ eng.pathKey = createPathKeyer();
47
51
  let err;
48
52
  for (const n of batch) {
49
53
  for (const cb of [...n.subs]) {
@@ -112,7 +116,6 @@ function makeNode(target, parent, path, level, eng) {
112
116
  v = toRaw(v);
113
117
  if (Object.is(node.target[k], v))
114
118
  return true;
115
- const dirtyPath = dirtyPathFor(node, k);
116
119
  node.target[k] = v;
117
120
  if (Array.isArray(proxyTarget) && k == "length")
118
121
  proxyTarget.length = v;
@@ -120,7 +123,7 @@ function makeNode(target, parent, path, level, eng) {
120
123
  if (kid)
121
124
  rebind(kid, v);
122
125
  if (eng.live > 0)
123
- bubble(node, dirtyPath);
126
+ bubble(node, k);
124
127
  return true;
125
128
  },
126
129
  defineProperty(_, k, d) {
@@ -136,7 +139,6 @@ function makeNode(target, parent, path, level, eng) {
136
139
  }
137
140
  const v = node.target[k];
138
141
  if (!Object.is(old, v)) {
139
- const dirtyPath = dirtyPathFor(node, k);
140
142
  const kid = node.kids.get(k);
141
143
  if (kid) {
142
144
  if (isReactiveObj(v))
@@ -148,14 +150,13 @@ function makeNode(target, parent, path, level, eng) {
148
150
  }
149
151
  }
150
152
  if (eng.live > 0)
151
- bubble(node, dirtyPath);
153
+ bubble(node, k);
152
154
  }
153
155
  return true;
154
156
  },
155
157
  deleteProperty(_, k) {
156
158
  if (!(k in node.target))
157
159
  return true;
158
- const dirtyPath = dirtyPathFor(node, k);
159
160
  delete node.target[k];
160
161
  const kid = node.kids.get(k);
161
162
  if (kid) {
@@ -164,7 +165,7 @@ function makeNode(target, parent, path, level, eng) {
164
165
  detachTree(kid);
165
166
  }
166
167
  if (eng.live > 0)
167
- bubble(node, dirtyPath);
168
+ bubble(node, k);
168
169
  return true;
169
170
  },
170
171
  has(_, k) { return k in node.target; },
@@ -191,10 +192,10 @@ function makeNode(target, parent, path, level, eng) {
191
192
  });
192
193
  return node;
193
194
  }
194
- function bubble(from, dirtyPath) {
195
+ function bubble(from, key) {
195
196
  const eng = from.eng;
196
- if (eng.pathLive > 0 && dirtyPath)
197
- addDirtyPath(eng, dirtyPath);
197
+ if (eng.pathLive > 0)
198
+ addDirtyPath(eng, dirtyPathFor(from, key));
198
199
  for (let n = from; n && n.active; n = n.parent)
199
200
  if (n.subs.size || n.pathSubs.size)
200
201
  eng.dirty.add(n);
@@ -224,26 +225,55 @@ function markChanged(node) {
224
225
  function dirtyPathFor(node, key) {
225
226
  return Array.isArray(node.target) ? [...node.path] : [...node.path, key];
226
227
  }
227
- function addDirtyPath(eng, path) {
228
- if (!eng.dirtyPaths.some(p => samePath(p, path)))
229
- eng.dirtyPaths.push([...path]);
228
+ function createPathKeyer() {
229
+ let symIds = null;
230
+ return function pathKey(path) {
231
+ let out = '';
232
+ for (const p of path) {
233
+ if (typeof p == 'symbol') {
234
+ symIds ??= new Map();
235
+ let id = symIds.get(p);
236
+ if (id == null) {
237
+ id = symIds.size;
238
+ symIds.set(p, id);
239
+ }
240
+ out += 'y' + id + '|';
241
+ }
242
+ else {
243
+ const s = String(p);
244
+ out += (typeof p)[0] + s.length + ':' + s + '|';
245
+ }
246
+ }
247
+ return out;
248
+ };
230
249
  }
231
- function samePath(a, b) {
232
- return a.length == b.length && a.every((k, i) => Object.is(k, b[i]));
250
+ function addDirtyPath(eng, path) {
251
+ const k = eng.pathKey(path);
252
+ if (eng.dirtyPathKeys.has(k))
253
+ return;
254
+ eng.dirtyPathKeys.add(k);
255
+ eng.dirtyPaths.push(path);
233
256
  }
234
257
  function startsWithPath(path, prefix) {
235
258
  return prefix.length <= path.length && prefix.every((k, i) => Object.is(k, path[i]));
236
259
  }
237
260
  function pathsForNode(node, dirtyPaths) {
238
261
  const out = [];
262
+ const seen = new Set();
263
+ const pathKey = createPathKeyer();
239
264
  for (const path of dirtyPaths) {
240
265
  let next = null;
241
266
  if (startsWithPath(path, node.path))
242
267
  next = path.slice(node.path.length);
243
268
  else if (startsWithPath(node.path, path))
244
269
  next = [];
245
- if (next && !out.some(p => samePath(p, next)))
246
- out.push(next);
270
+ if (next == null)
271
+ continue;
272
+ const k = pathKey(next);
273
+ if (seen.has(k))
274
+ continue;
275
+ seen.add(k);
276
+ out.push(next);
247
277
  }
248
278
  return out;
249
279
  }
@@ -6,31 +6,7 @@ export type StoreReplayOpts = Pick<ReplayListenOptions<[StorePatch]>, 'history'
6
6
  export declare function storePatchKey(patch: StorePatch): string | null;
7
7
  export declare function exposeStoreReplay<T extends object>(store: Store<T>, opts?: StoreReplayOpts): {
8
8
  api: {
9
- replay: {
10
- line: {
11
- func: import("../..").Listener<[import("../events/replay-listen").ReplayEvent<[StorePatch]>]>;
12
- isRun: () => boolean;
13
- run: () => void;
14
- close: () => void;
15
- eventClose: (cb: () => void) => () => void;
16
- onClose: (cb: () => void) => () => void;
17
- removeEventClose: (cb: () => void) => void;
18
- on: import("../..").ListenOn<[import("../events/replay-listen").ReplayEvent<[StorePatch]>]>;
19
- off: (k: (string | symbol) | import("../..").Listener<[import("../events/replay-listen").ReplayEvent<[StorePatch]>]> | null) => void;
20
- addListen: (cb: import("../..").Listener<[import("../events/replay-listen").ReplayEvent<[StorePatch]>]>, opts?: {
21
- cbClose?: () => void;
22
- key?: string | symbol;
23
- }) => () => void;
24
- removeListen: (k: (string | symbol) | import("../..").Listener<[import("../events/replay-listen").ReplayEvent<[StorePatch]>]> | null) => void;
25
- once: (cb: import("../..").Listener<[import("../events/replay-listen").ReplayEvent<[StorePatch]>]>, opts?: {
26
- key?: string | symbol;
27
- }) => () => void;
28
- count: () => number;
29
- readonly getAllKeys: (string | symbol)[];
30
- };
31
- since: (seq: number) => import("../events/replay-listen").ReplayEvent<[StorePatch]>[] | null;
32
- keyframe: () => import("../events/replay-listen").ReplayEvent<[StorePatch]> | null;
33
- };
9
+ replay: import("../events/replay-wire").ReplayExpose<[StorePatch]>;
34
10
  get(): T;
35
11
  get<M extends import("./store").StoreMask<T>>(mask: M): import("./store").StorePick<T, M>;
36
12
  set(path: import("./store").StorePath, value: any): void;
@@ -43,6 +19,9 @@ export declare function exposeStoreReplay<T extends object>(store: Store<T>, opt
43
19
  replay: {
44
20
  func: import("../..").Listener<[StorePatch]>;
45
21
  head: () => number;
22
+ isStale: () => boolean;
23
+ lastTs: () => number;
24
+ close: () => void;
46
25
  getSince: (seq: number) => import("../events/replay-listen").ReplayEvent<[StorePatch]>[] | undefined;
47
26
  line: {
48
27
  func: import("../..").Listener<[import("../events/replay-listen").ReplayEvent<[StorePatch]>]>;
@@ -83,7 +62,6 @@ export declare function exposeStoreReplay<T extends object>(store: Store<T>, opt
83
62
  getAllKeys: (string | symbol)[];
84
63
  isRun: () => boolean;
85
64
  run: () => void;
86
- close: () => void;
87
65
  eventClose: (cb: () => void) => () => void;
88
66
  onClose: (cb: () => void) => () => void;
89
67
  removeEventClose: (cb: () => void) => void;
@@ -95,6 +73,8 @@ export declare function exposeStoreReplay<T extends object>(store: Store<T>, opt
95
73
  export declare function syncStoreReplay<T extends object>(store: Store<T>, remote: ReplayRemote<[StorePatch]>, opts?: ReplaySubscribeOpts): (() => void) & {
96
74
  ready: Promise<void>;
97
75
  seq: () => number;
76
+ isStale: () => boolean;
77
+ lastTs: () => number;
98
78
  };
99
79
  export declare function storeReplayAt<T extends object>(storage: ReplayStorage<[StorePatch]>, at?: {
100
80
  seq?: number;
@@ -122,6 +122,9 @@ function snapshotValue(value, seen = new WeakMap()) {
122
122
  value = (0, reactive2_1.toRaw)(value);
123
123
  if (!isObj(value))
124
124
  return value;
125
+ const old = seen.get(value);
126
+ if (old)
127
+ return old;
125
128
  if (value instanceof Date)
126
129
  return new Date(value.valueOf());
127
130
  if (value instanceof RegExp)
@@ -138,9 +141,6 @@ function snapshotValue(value, seen = new WeakMap()) {
138
141
  value.forEach(v => out.add(snapshotValue(v, seen)));
139
142
  return out;
140
143
  }
141
- const old = seen.get(value);
142
- if (old)
143
- return old;
144
144
  const out = Array.isArray(value) ? [] : {};
145
145
  seen.set(value, out);
146
146
  for (const k of Reflect.ownKeys(value))
@@ -2,7 +2,6 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.conflateReplay = conflateReplay;
4
4
  const Listen3_1 = require("./Listen3");
5
- const replay_wire_1 = require("./replay-wire");
6
5
  function conflateReplay(replay, opts) {
7
6
  const { pending, highWater, lowWater = 0, pollMs = 25, keyOf, maxKeys = 1024 } = opts;
8
7
  if (!replay.hasKeyframe)
@@ -92,7 +91,11 @@ function conflateReplay(replay, opts) {
92
91
  gate.close();
93
92
  }
94
93
  return {
95
- api: { ...(0, replay_wire_1.exposeReplay)(replay), line: gate },
94
+ api: {
95
+ line: gate,
96
+ since: (seq) => replay.getSince(seq) ?? null,
97
+ keyframe: () => replay.keyframe() ?? null,
98
+ },
96
99
  close,
97
100
  stats: () => ({ conflating, dropped, keyframes, coalesced, flushes }),
98
101
  };
@@ -6,12 +6,19 @@ export type ReplayEvent<Z extends any[] = any[]> = {
6
6
  ts: number;
7
7
  event: Z;
8
8
  };
9
+ export type StaleInfo = {
10
+ stale: boolean;
11
+ lastTs: number;
12
+ age: number;
13
+ };
9
14
  export type ReplayListenOptions<Z extends any[]> = {
10
15
  current?: ListenCurrentProvider<Z>;
11
16
  history?: number;
12
17
  getSince?: (seq: number) => ReplayEvent<Z>[] | undefined;
13
18
  onJournal?: (ev: ReplayEvent<Z>) => void;
14
19
  now?: () => number;
20
+ staleMs?: number;
21
+ onStale?: (info: StaleInfo) => void;
15
22
  };
16
23
  type ReplayOnOptions<Z extends any[]> = {
17
24
  cbClose?: cbClose;
@@ -24,6 +31,9 @@ export type ListenOnReplay<Z extends any[] = any[]> = ((cb: Listener<Z>, opts?:
24
31
  export declare function withReplayListen<T>(base: ListenApi<T>, options?: ReplayListenOptions<NormalizeTuple<T>>): {
25
32
  func: Listener<NormalizeTuple<T>>;
26
33
  head: () => number;
34
+ isStale: () => boolean;
35
+ lastTs: () => number;
36
+ close: () => void;
27
37
  getSince: (seq: number) => ReplayEvent<NormalizeTuple<T>>[] | undefined;
28
38
  line: {
29
39
  func: Listener<[ReplayEvent<NormalizeTuple<T>>]>;
@@ -58,7 +68,6 @@ export declare function withReplayListen<T>(base: ListenApi<T>, options?: Replay
58
68
  getAllKeys: key[];
59
69
  isRun: () => boolean;
60
70
  run: () => void;
61
- close: () => void;
62
71
  eventClose: (cb: () => void) => () => void;
63
72
  onClose: (cb: () => void) => () => void;
64
73
  removeEventClose: (cb: () => void) => void;
@@ -70,6 +79,9 @@ export type ReplayListenUseOptions<T> = ListenOptions<T> & ReplayListenOptions<N
70
79
  export declare function UseReplayListen<T>(options?: ReplayListenUseOptions<T>): readonly [(...a: NormalizeTuple<T>) => void, {
71
80
  func: Listener<NormalizeTuple<T>>;
72
81
  head: () => number;
82
+ isStale: () => boolean;
83
+ lastTs: () => number;
84
+ close: () => void;
73
85
  getSince: (seq: number) => ReplayEvent<NormalizeTuple<T>>[] | undefined;
74
86
  line: {
75
87
  func: Listener<[ReplayEvent<NormalizeTuple<T>>]>;
@@ -104,7 +116,6 @@ export declare function UseReplayListen<T>(options?: ReplayListenUseOptions<T>):
104
116
  getAllKeys: key[];
105
117
  isRun: () => boolean;
106
118
  run: () => void;
107
- close: () => void;
108
119
  eventClose: (cb: () => void) => () => void;
109
120
  onClose: (cb: () => void) => () => void;
110
121
  removeEventClose: (cb: () => void) => void;
@@ -4,7 +4,7 @@ exports.withReplayListen = withReplayListen;
4
4
  exports.UseReplayListen = UseReplayListen;
5
5
  const Listen3_1 = require("./Listen3");
6
6
  function withReplayListen(base, options = {}) {
7
- const { current, history = 0, getSince, onJournal, now = Date.now } = options;
7
+ const { current, history = 0, getSince, onJournal, now = Date.now, staleMs, onStale } = options;
8
8
  let head = 0;
9
9
  const ring = [];
10
10
  let emitting = null;
@@ -30,6 +30,44 @@ function withReplayListen(base, options = {}) {
30
30
  return c();
31
31
  return c ? current?.() : undefined;
32
32
  }
33
+ let lastTs = 0;
34
+ let staleFlag = false;
35
+ let staleTimer = null;
36
+ function stopStaleTimer() {
37
+ if (staleTimer) {
38
+ clearTimeout(staleTimer);
39
+ staleTimer = null;
40
+ }
41
+ }
42
+ function reportStale(stale) {
43
+ staleFlag = stale;
44
+ try {
45
+ onStale({ stale, lastTs, age: now() - lastTs });
46
+ }
47
+ catch (e) {
48
+ setTimeout(function rethrowOnStale() { throw e; }, 0);
49
+ }
50
+ }
51
+ function checkStale() {
52
+ staleTimer = null;
53
+ const age = now() - lastTs;
54
+ if (age >= staleMs)
55
+ reportStale(true);
56
+ else
57
+ armStaleTimer(staleMs - age);
58
+ }
59
+ function armStaleTimer(delay) {
60
+ if (staleTimer || !onStale || staleMs == null)
61
+ return;
62
+ staleTimer = setTimeout(checkStale, delay);
63
+ staleTimer.unref?.();
64
+ }
65
+ function touchStale(ts) {
66
+ lastTs = ts;
67
+ if (staleFlag)
68
+ reportStale(false);
69
+ armStaleTimer(staleMs);
70
+ }
33
71
  const api = {
34
72
  ...base,
35
73
  func: function emitJournaled(...e) {
@@ -37,6 +75,7 @@ function withReplayListen(base, options = {}) {
37
75
  if (history > 0)
38
76
  ring[(ev.seq - 1) % history] = ev;
39
77
  onJournal?.(ev);
78
+ touchStale(ev.ts);
40
79
  line.func(ev);
41
80
  const prev = emitting;
42
81
  emitting = ev;
@@ -48,6 +87,9 @@ function withReplayListen(base, options = {}) {
48
87
  }
49
88
  },
50
89
  head: () => head,
90
+ isStale: () => staleMs != null && head > 0 && now() - lastTs >= staleMs,
91
+ lastTs: () => lastTs,
92
+ close: function closeReplay() { stopStaleTimer(); base.close(); },
51
93
  getSince: journalSince,
52
94
  line,
53
95
  hasKeyframe: current != null,
@@ -124,10 +166,10 @@ function withReplayListen(base, options = {}) {
124
166
  return api;
125
167
  }
126
168
  function UseReplayListen(options = {}) {
127
- const { current, history, getSince, onJournal, now, ...listenOptions } = options;
169
+ const { current, history, getSince, onJournal, now, staleMs, onStale, ...listenOptions } = options;
128
170
  let t;
129
171
  const base = (0, Listen3_1.funcListenCallbackBase)((e) => { t = e; }, { fast: true, ...listenOptions });
130
- const listen = withReplayListen(base, { current, history, getSince, onJournal, now });
172
+ const listen = withReplayListen(base, { current, history, getSince, onJournal, now, staleMs, onStale });
131
173
  base.run();
132
174
  t = listen.func;
133
175
  return [t, listen];
@@ -1,29 +1,17 @@
1
- import { Listener } from './Listen3';
2
- import { ListenReplayApi, ReplayEvent } from './replay-listen';
3
- export declare function exposeReplay<T>(replay: ListenReplayApi<T>): {
4
- line: {
5
- func: Listener<[ReplayEvent<import("./Listen3").NormalizeTuple<T>>]>;
6
- isRun: () => boolean;
7
- run: () => void;
8
- close: () => void;
9
- eventClose: (cb: () => void) => () => void;
10
- onClose: (cb: () => void) => () => void;
11
- removeEventClose: (cb: () => void) => void;
12
- on: import("./Listen3").ListenOn<[ReplayEvent<import("./Listen3").NormalizeTuple<T>>]>;
13
- off: (k: (string | symbol) | Listener<[ReplayEvent<import("./Listen3").NormalizeTuple<T>>]> | null) => void;
14
- addListen: (cb: Listener<[ReplayEvent<import("./Listen3").NormalizeTuple<T>>]>, opts?: {
15
- cbClose?: () => void;
16
- key?: string | symbol;
17
- }) => () => void;
18
- removeListen: (k: (string | symbol) | Listener<[ReplayEvent<import("./Listen3").NormalizeTuple<T>>]> | null) => void;
19
- once: (cb: Listener<[ReplayEvent<import("./Listen3").NormalizeTuple<T>>]>, opts?: {
20
- key?: string | symbol;
21
- }) => () => void;
22
- count: () => number;
23
- readonly getAllKeys: (string | symbol)[];
24
- };
25
- since: (seq: number) => ReplayEvent<import("./Listen3").NormalizeTuple<T>>[] | null;
26
- keyframe: () => ReplayEvent<import("./Listen3").NormalizeTuple<T>> | null;
1
+ import { Listener, NormalizeTuple } from './Listen3';
2
+ import { ListenReplayApi, ReplayEvent, StaleInfo } from './replay-listen';
3
+ import { conflateReplay, ConflateOpts } from './replay-conflate';
4
+ export type ReplayExpose<T> = {
5
+ line: ListenReplayApi<T>['line'];
6
+ since: (seq: number) => ReplayEvent<NormalizeTuple<T>>[] | null;
7
+ keyframe: () => ReplayEvent<NormalizeTuple<T>> | null;
8
+ };
9
+ export declare function exposeReplay<T>(replay: ListenReplayApi<T>): ReplayExpose<T>;
10
+ export declare function exposeReplay<T>(replay: ListenReplayApi<T>, opts: {
11
+ conflate: ConflateOpts<NormalizeTuple<T>>;
12
+ }): ReplayExpose<T> & {
13
+ close: () => void;
14
+ stats: ReturnType<typeof conflateReplay<T>>['stats'];
27
15
  };
28
16
  export type ReplayRemote<Z extends any[] = any[]> = {
29
17
  line: {
@@ -36,8 +24,14 @@ export type ReplaySubscribeOpts = {
36
24
  since?: number;
37
25
  onSeq?: (seq: number) => void;
38
26
  onError?: (e: any) => void;
27
+ staleMs?: number;
28
+ onStale?: (info: StaleInfo) => void;
29
+ skewMs?: number;
30
+ now?: () => number;
39
31
  };
40
32
  export declare function replaySubscribe<Z extends any[]>(remote: ReplayRemote<Z>, cb: Listener<Z>, opts?: ReplaySubscribeOpts): (() => void) & {
41
33
  ready: Promise<void>;
42
34
  seq: () => number;
35
+ isStale: () => boolean;
36
+ lastTs: () => number;
43
37
  };
@@ -2,13 +2,20 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.exposeReplay = exposeReplay;
4
4
  exports.replaySubscribe = replaySubscribe;
5
- function exposeReplay(replay) {
5
+ const replay_conflate_1 = require("./replay-conflate");
6
+ function exposeReplayPlain(replay) {
6
7
  return {
7
8
  line: replay.line,
8
9
  since: (seq) => replay.getSince(seq) ?? null,
9
10
  keyframe: () => replay.keyframe() ?? null,
10
11
  };
11
12
  }
13
+ function exposeReplay(replay, opts) {
14
+ if (!opts?.conflate)
15
+ return exposeReplayPlain(replay);
16
+ const gated = (0, replay_conflate_1.conflateReplay)(replay, opts.conflate);
17
+ return { ...gated.api, close: gated.close, stats: gated.stats };
18
+ }
12
19
  function unsubscribeHandle(handle) {
13
20
  if (typeof handle == 'function') {
14
21
  handle();
@@ -20,19 +27,75 @@ function unsubscribeHandle(handle) {
20
27
  handle.unsubscribe();
21
28
  }
22
29
  function replaySubscribe(remote, cb, opts = {}) {
23
- const { since = -1, onSeq, onError } = opts;
30
+ const { since = -1, onSeq, onError, staleMs, onStale, skewMs = 0, now = Date.now } = opts;
24
31
  let lastDelivered = since;
25
32
  let replaying = true;
26
33
  let closed = false;
27
34
  const queue = [];
35
+ let lastTs = 0;
36
+ let lastArrival = now();
37
+ let staleFlag = false;
38
+ let staleTimer = null;
39
+ function stopStaleTimer() {
40
+ if (staleTimer) {
41
+ clearTimeout(staleTimer);
42
+ staleTimer = null;
43
+ }
44
+ }
45
+ function reportStale(stale) {
46
+ staleFlag = stale;
47
+ if (!onStale)
48
+ return;
49
+ try {
50
+ onStale({ stale, lastTs, age: now() - (lastTs || lastArrival) });
51
+ }
52
+ catch (e) {
53
+ setTimeout(function rethrowOnStale() { throw e; }, 0);
54
+ }
55
+ }
56
+ function checkArrivalGap() {
57
+ staleTimer = null;
58
+ if (closed)
59
+ return;
60
+ const gap = now() - lastArrival;
61
+ if (gap >= staleMs) {
62
+ if (!staleFlag)
63
+ reportStale(true);
64
+ }
65
+ else
66
+ armStaleTimer(staleMs - gap);
67
+ }
68
+ function armStaleTimer(delay) {
69
+ if (staleTimer || !onStale || staleMs == null || closed)
70
+ return;
71
+ staleTimer = setTimeout(checkArrivalGap, delay);
72
+ staleTimer.unref?.();
73
+ }
74
+ function assessStale() {
75
+ if (staleMs == null || closed)
76
+ return;
77
+ const tsStale = lastTs > 0 && now() - lastTs > staleMs + skewMs;
78
+ if (tsStale != staleFlag)
79
+ reportStale(tsStale);
80
+ if (tsStale)
81
+ stopStaleTimer();
82
+ else
83
+ armStaleTimer(staleMs);
84
+ }
85
+ armStaleTimer(staleMs);
28
86
  function deliver(ev) {
29
87
  if (closed || ev.seq <= lastDelivered)
30
88
  return;
31
89
  lastDelivered = ev.seq;
90
+ lastTs = ev.ts;
91
+ lastArrival = now();
32
92
  cb(...ev.event);
33
93
  onSeq?.(ev.seq);
94
+ if (!replaying)
95
+ assessStale();
34
96
  }
35
97
  const handle = remote.line.on(function liveTap(ev) {
98
+ lastArrival = now();
36
99
  if (replaying)
37
100
  queue.push(ev);
38
101
  else
@@ -53,6 +116,8 @@ function replaySubscribe(remote, cb, opts = {}) {
53
116
  return;
54
117
  if (kf) {
55
118
  lastDelivered = kf.seq;
119
+ lastTs = kf.ts;
120
+ lastArrival = now();
56
121
  cb(...kf.event);
57
122
  onSeq?.(kf.seq);
58
123
  }
@@ -68,6 +133,7 @@ function replaySubscribe(remote, cb, opts = {}) {
68
133
  while (queue.length)
69
134
  deliver(queue.shift());
70
135
  replaying = false;
136
+ assessStale();
71
137
  }
72
138
  }
73
139
  const ready = catchUp();
@@ -75,10 +141,13 @@ function replaySubscribe(remote, cb, opts = {}) {
75
141
  if (closed)
76
142
  return;
77
143
  closed = true;
144
+ stopStaleTimer();
78
145
  unsubscribeHandle(handle);
79
146
  }
80
147
  return Object.assign(off, {
81
148
  ready,
82
149
  seq: () => lastDelivered,
150
+ isStale: () => staleFlag || (staleMs != null && now() - lastArrival >= staleMs),
151
+ lastTs: () => lastTs,
83
152
  });
84
153
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wenay-common2",
3
- "version": "1.0.55",
3
+ "version": "1.0.57",
4
4
  "description": "Common library",
5
5
  "strict": true,
6
6
  "main": "lib/index.js",