@reticlehq/electron 2.12.0 → 2.13.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.
Files changed (2) hide show
  1. package/package.json +2 -2
  2. package/preload.cjs +37 -2
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reticlehq/electron",
3
- "version": "2.12.0",
3
+ "version": "2.13.0",
4
4
  "description": "Reticle adapter for Electron: makes main-process IPC observable and the window screenshottable, from the two places the renderer cannot reach.",
5
5
  "type": "module",
6
6
  "exports": {
@@ -8,7 +8,7 @@
8
8
  "./main": "./main.cjs"
9
9
  },
10
10
  "dependencies": {
11
- "@reticlehq/core": "2.12.0"
11
+ "@reticlehq/core": "2.13.0"
12
12
  },
13
13
  "peerDependencies": {
14
14
  "electron": ">=22"
package/preload.cjs CHANGED
@@ -39,14 +39,36 @@ const {
39
39
  * `contextBridge` as a proxy, so the reference the preload holds is not the one the renderer passes
40
40
  * back and `delete(callback)` would never match.
41
41
  *
42
- * Empty until a renderer subscribes, so records before `connect()` are dropped rather than queued —
43
- * the SDK only wants activity from the moment it is watching.
42
+ * Empty until a renderer subscribes. What happens to records made before that is `backlog`, below.
44
43
  */
45
44
  const sinks = new Map();
46
45
  let sinkToken = 0;
47
46
  let seq = 0;
48
47
 
48
+ /**
49
+ * IPC that happened before the SDK was watching, held for the first subscriber.
50
+ *
51
+ * This used to be dropped, on the reasoning that the SDK only wants activity from the moment it is
52
+ * watching. That reasoning is a web reflex and it is wrong here. A desktop app loads its data over
53
+ * IPC on mount, and `connect()` is injected and does asynchronous work, so the app's FIRST calls —
54
+ * the ones that populate the entire screen — routinely land before any sink exists. Dropping them
55
+ * does not produce a gap a reader can see: it produces a network view that is empty and looks clean,
56
+ * which is the failure this project treats as the worst kind. An agent reads it as "the app made no
57
+ * backend calls" and reports so.
58
+ *
59
+ * Bounded, because an app running for hours with no SDK attached must not grow a queue, and replayed
60
+ * to the FIRST subscriber only: a second `connect()` in the same renderer is not entitled to history
61
+ * it was not present for.
62
+ */
63
+ const backlog = [];
64
+ const BACKLOG_MAX = 200;
65
+ let backlogDelivered = false;
66
+
49
67
  function report(record) {
68
+ if (sinks.size === 0) {
69
+ if (!backlogDelivered && backlog.length < BACKLOG_MAX) backlog.push(record);
70
+ return;
71
+ }
50
72
  for (const sink of sinks.values()) {
51
73
  try {
52
74
  sink(record);
@@ -201,6 +223,19 @@ contextBridge.exposeInMainWorld(RETICLE_IPC_GLOBAL, {
201
223
  if (typeof callback !== 'function') return -1;
202
224
  sinkToken += 1;
203
225
  sinks.set(sinkToken, callback);
226
+ // Whatever the app did before anyone was listening, in the order it did it. Delivered once: the
227
+ // records carry their own ids and timestamps, so a duplicate would read as a second call.
228
+ if (!backlogDelivered) {
229
+ backlogDelivered = true;
230
+ const pending = backlog.splice(0, backlog.length);
231
+ for (const record of pending) {
232
+ try {
233
+ callback(record);
234
+ } catch {
235
+ /* a renderer sink is best-effort; a bad one must not lose the rest of the backlog */
236
+ }
237
+ }
238
+ }
204
239
  return sinkToken;
205
240
  },
206
241
  /**