cursedbelt 5.101.0 → 5.102.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.
@@ -35,10 +35,10 @@ export function useEngagementBeacon(place, options = {}) {
35
35
  * may and may not carry.
36
36
  */
37
37
  export function useEngagementAction(action, options = {}) {
38
- const { basePath, fetchImpl, sessionPath, assumeSignedIn, place } = options;
38
+ const { basePath, fetchImpl, sessionPath, assumeSignedIn, place, beaconImpl } = options;
39
39
  return useCallback(() => {
40
- void reportAction(action, { basePath, fetchImpl, sessionPath, assumeSignedIn, place });
41
- }, [action, basePath, fetchImpl, sessionPath, assumeSignedIn, place]);
40
+ void reportAction(action, { basePath, fetchImpl, sessionPath, assumeSignedIn, place, beaconImpl });
41
+ }, [action, basePath, fetchImpl, sessionPath, assumeSignedIn, place, beaconImpl]);
42
42
  }
43
43
  /**
44
44
  * Report wherever the browser IS, and keep reporting as it moves.
@@ -1 +1 @@
1
- {"version":3,"file":"beacon.js","sourceRoot":"","sources":["../../../src/react/engagement/beacon.tsx"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAC/C,OAAO,EAGN,YAAY,EACZ,WAAW,EACX,mBAAmB,GACnB,MAAM,mBAAmB,CAAC;AAE3B;;;;;;;GAOG;AACH,MAAM,UAAU,mBAAmB,CAAC,KAAoB,EAAE,UAA8B,EAAE;IACzF,iGAAiG;IACjG,+FAA+F;IAC/F,kFAAkF;IAClF,MAAM,EAAE,QAAQ,EAAE,SAAS,EAAE,WAAW,EAAE,cAAc,EAAE,GAAG,OAAO,CAAC;IACrE,SAAS,CAAC,GAAG,EAAE;QACd,IAAI,CAAC,KAAK;YAAE,OAAO;QACnB,yEAAyE;QACzE,6CAA6C;QAC7C,KAAK,WAAW,CAAC,KAAK,EAAE,EAAE,QAAQ,EAAE,SAAS,EAAE,WAAW,EAAE,cAAc,EAAE,CAAC,CAAC;IAC/E,CAAC,EAAE,CAAC,KAAK,EAAE,QAAQ,EAAE,SAAS,EAAE,WAAW,EAAE,cAAc,CAAC,CAAC,CAAC;AAC/D,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,mBAAmB,CAAC,MAAc,EAAE,UAA+B,EAAE;IACpF,MAAM,EAAE,QAAQ,EAAE,SAAS,EAAE,WAAW,EAAE,cAAc,EAAE,KAAK,EAAE,GAAG,OAAO,CAAC;IAC5E,OAAO,WAAW,CAAC,GAAG,EAAE;QACvB,KAAK,YAAY,CAAC,MAAM,EAAE,EAAE,QAAQ,EAAE,SAAS,EAAE,WAAW,EAAE,cAAc,EAAE,KAAK,EAAE,CAAC,CAAC;IACxF,CAAC,EAAE,CAAC,MAAM,EAAE,QAAQ,EAAE,SAAS,EAAE,WAAW,EAAE,cAAc,EAAE,KAAK,CAAC,CAAC,CAAC;AACvE,CAAC;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,iBAAiB,CAAC,UAA8B,EAAE;IACjE,MAAM,EAAE,QAAQ,EAAE,SAAS,EAAE,WAAW,EAAE,cAAc,EAAE,GAAG,OAAO,CAAC;IACrE,0EAA0E;IAC1E,uEAAuE;IACvE,uEAAuE;IACvE,SAAS,CACR,GAAG,EAAE,CAAC,mBAAmB,CAAC,EAAE,QAAQ,EAAE,SAAS,EAAE,WAAW,EAAE,cAAc,EAAE,CAAC,EAC/E,CAAC,QAAQ,EAAE,SAAS,EAAE,WAAW,EAAE,cAAc,CAAC,CAClD,CAAC;AACH,CAAC;AAED;;;;;;;;GAQG;AACH,gEAAgE;AAChE,MAAM,UAAU,gBAAgB,CAAC,KAA4B;IAC5D,iBAAiB,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,KAAK,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;IACtE,OAAO,IAAI,CAAC;AACb,CAAC"}
1
+ {"version":3,"file":"beacon.js","sourceRoot":"","sources":["../../../src/react/engagement/beacon.tsx"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAC/C,OAAO,EAGN,YAAY,EACZ,WAAW,EACX,mBAAmB,GACnB,MAAM,mBAAmB,CAAC;AAE3B;;;;;;;GAOG;AACH,MAAM,UAAU,mBAAmB,CAAC,KAAoB,EAAE,UAA8B,EAAE;IACzF,iGAAiG;IACjG,+FAA+F;IAC/F,kFAAkF;IAClF,MAAM,EAAE,QAAQ,EAAE,SAAS,EAAE,WAAW,EAAE,cAAc,EAAE,GAAG,OAAO,CAAC;IACrE,SAAS,CAAC,GAAG,EAAE;QACd,IAAI,CAAC,KAAK;YAAE,OAAO;QACnB,yEAAyE;QACzE,6CAA6C;QAC7C,KAAK,WAAW,CAAC,KAAK,EAAE,EAAE,QAAQ,EAAE,SAAS,EAAE,WAAW,EAAE,cAAc,EAAE,CAAC,CAAC;IAC/E,CAAC,EAAE,CAAC,KAAK,EAAE,QAAQ,EAAE,SAAS,EAAE,WAAW,EAAE,cAAc,CAAC,CAAC,CAAC;AAC/D,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,mBAAmB,CAAC,MAAc,EAAE,UAA+B,EAAE;IACpF,MAAM,EAAE,QAAQ,EAAE,SAAS,EAAE,WAAW,EAAE,cAAc,EAAE,KAAK,EAAE,UAAU,EAAE,GAAG,OAAO,CAAC;IACxF,OAAO,WAAW,CAAC,GAAG,EAAE;QACvB,KAAK,YAAY,CAAC,MAAM,EAAE,EAAE,QAAQ,EAAE,SAAS,EAAE,WAAW,EAAE,cAAc,EAAE,KAAK,EAAE,UAAU,EAAE,CAAC,CAAC;IACpG,CAAC,EAAE,CAAC,MAAM,EAAE,QAAQ,EAAE,SAAS,EAAE,WAAW,EAAE,cAAc,EAAE,KAAK,EAAE,UAAU,CAAC,CAAC,CAAC;AACnF,CAAC;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,iBAAiB,CAAC,UAA8B,EAAE;IACjE,MAAM,EAAE,QAAQ,EAAE,SAAS,EAAE,WAAW,EAAE,cAAc,EAAE,GAAG,OAAO,CAAC;IACrE,0EAA0E;IAC1E,uEAAuE;IACvE,uEAAuE;IACvE,SAAS,CACR,GAAG,EAAE,CAAC,mBAAmB,CAAC,EAAE,QAAQ,EAAE,SAAS,EAAE,WAAW,EAAE,cAAc,EAAE,CAAC,EAC/E,CAAC,QAAQ,EAAE,SAAS,EAAE,WAAW,EAAE,cAAc,CAAC,CAClD,CAAC;AACH,CAAC;AAED;;;;;;;;GAQG;AACH,gEAAgE;AAChE,MAAM,UAAU,gBAAgB,CAAC,KAA4B;IAC5D,iBAAiB,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,KAAK,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;IACtE,OAAO,IAAI,CAAC;AACb,CAAC"}
@@ -85,7 +85,7 @@ export interface ReportPlaceOptions {
85
85
  * not sending it in the first place means user text does not travel at all.
86
86
  */
87
87
  export declare function currentLocationPlace(): string;
88
- /** Reset the de-duplication memory. Tests, and a sign-out. */
88
+ /** Reset the de-duplication memory, and drop any queued action unsent. Tests, and a sign-out. */
89
89
  export declare function resetEngagementBeacon(): void;
90
90
  /**
91
91
  * Report an arrival at `place`. Returns whether anything was sent.
@@ -100,10 +100,27 @@ export interface ReportActionOptions extends ReportPlaceOptions {
100
100
  * against the manifest exactly as it resolves a view, so the default is right for every app.
101
101
  */
102
102
  place?: string;
103
+ /** Injected in tests; defaults to `navigator.sendBeacon`. The page-leave flush sends through it. */
104
+ beaconImpl?: (url: string, body: Blob) => boolean;
103
105
  }
106
+ /** How long the action queue waits for quiet before it sends (forge task 4034). */
107
+ export declare const ACTION_QUIET_MS = 5000;
108
+ /** The longest an action waits however busy the page is, so a page that crashes loses 30 s at most. */
109
+ export declare const ACTION_MAX_WAIT_MS = 30000;
110
+ /** One POST carries at most this many: the recorder's `MAX_ACTION_BATCH` (cursedbelt-server). */
111
+ export declare const ACTION_BATCH_MAX = 100;
112
+ /**
113
+ * Send what is queued, now: ONE POST per mount. A queue of one posts `{ place, action }` as
114
+ * `reportAction` always has, so a recorder older than the batch reads it; more posts
115
+ * `{ actions }`, and a recorder that refuses the batch (an older one answers 400) gets them one by
116
+ * one instead. `leaving` sends through `sendBeacon`, which outlives the page. Never throws.
117
+ */
118
+ export declare function flushEngagementActions(options?: {
119
+ leaving?: boolean;
120
+ }): Promise<void>;
104
121
  /**
105
122
  * Report that a FEATURE was used — `reportAction("search")` — beside the place it was used on
106
- * (forge task 3470). Returns whether anything was sent.
123
+ * (forge task 3470). Returns whether it was queued to send.
107
124
  *
108
125
  * The owner's question is "what features do they use and ignore"; a page view cannot answer it,
109
126
  * because one page holds several features. So the app names the action at the press, from the
@@ -117,6 +134,12 @@ export interface ReportActionOptions extends ReportPlaceOptions {
117
134
  * · the same silence: session-probed (the probe's answer is shared with `reportPlace`, one per
118
135
  * page load), fire-and-forget, never throws, never logs.
119
136
  *
137
+ * 🔴 BATCHED (forge task 4034): an action is queued, and the queue goes as ONE request after
138
+ * {@link ACTION_QUIET_MS} of quiet, {@link ACTION_MAX_WAIT_MS} after its first action at most, at
139
+ * {@link ACTION_BATCH_MAX}, and when the page is left ({@link flushEngagementActions}). So a
140
+ * feature the owner adds to the record costs no request per click — his words: *"batching some
141
+ * interactions with debounces will lower traffic"*.
142
+ *
120
143
  * NOT de-duplicated, unlike `reportPlace`: three plays are three uses. Only call it from apps
121
144
  * whose recorder was mounted WITH `actions` — elsewhere `/action` is a 404 in the console.
122
145
  */
@@ -1 +1 @@
1
- {"version":3,"file":"beaconClient.d.ts","sourceRoot":"","sources":["../../../src/react/engagement/beaconClient.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmDG;AAEH,2EAA2E;AAC3E,eAAO,MAAM,oBAAoB,oBAAoB,CAAC;AAEtD;;;;;;;GAOG;AACH,eAAO,MAAM,YAAY,sBAAsB,CAAC;AAEhD,MAAM,WAAW,kBAAkB;IAClC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,iDAAiD;IACjD,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;IACzB,4EAA4E;IAC5E,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;;;;;OAOG;IACH,cAAc,CAAC,EAAE,OAAO,CAAC;CACzB;AAaD;;;;;;GAMG;AACH,wBAAgB,oBAAoB,IAAI,MAAM,CAI7C;AAYD,8DAA8D;AAC9D,wBAAgB,qBAAqB,IAAI,IAAI,CAG5C;AA6CD;;;;;GAKG;AACH,wBAAsB,WAAW,CAChC,KAAK,EAAE,MAAM,EACb,OAAO,GAAE,kBAAuB,GAC9B,OAAO,CAAC,OAAO,CAAC,CAgClB;AAED,MAAM,WAAW,mBAAoB,SAAQ,kBAAkB;IAC9D;;;OAGG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;CACf;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAsB,YAAY,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,GAAE,mBAAwB,GAAG,OAAO,CAAC,OAAO,CAAC,CAsBtG;AAED,8DAA8D;AAC9D,MAAM,MAAM,UAAU,GAAG,MAAM,IAAI,CAAC;AAIpC;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,GAAE,kBAAuB,GAAG,UAAU,CAehF"}
1
+ {"version":3,"file":"beaconClient.d.ts","sourceRoot":"","sources":["../../../src/react/engagement/beaconClient.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmDG;AAEH,2EAA2E;AAC3E,eAAO,MAAM,oBAAoB,oBAAoB,CAAC;AAEtD;;;;;;;GAOG;AACH,eAAO,MAAM,YAAY,sBAAsB,CAAC;AAEhD,MAAM,WAAW,kBAAkB;IAClC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,iDAAiD;IACjD,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;IACzB,4EAA4E;IAC5E,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;;;;;OAOG;IACH,cAAc,CAAC,EAAE,OAAO,CAAC;CACzB;AAaD;;;;;;GAMG;AACH,wBAAgB,oBAAoB,IAAI,MAAM,CAI7C;AAYD,iGAAiG;AACjG,wBAAgB,qBAAqB,IAAI,IAAI,CAQ5C;AA6CD;;;;;GAKG;AACH,wBAAsB,WAAW,CAChC,KAAK,EAAE,MAAM,EACb,OAAO,GAAE,kBAAuB,GAC9B,OAAO,CAAC,OAAO,CAAC,CAgClB;AAED,MAAM,WAAW,mBAAoB,SAAQ,kBAAkB;IAC9D;;;OAGG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,oGAAoG;IACpG,UAAU,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,KAAK,OAAO,CAAC;CAClD;AAED,mFAAmF;AACnF,eAAO,MAAM,eAAe,OAAQ,CAAC;AACrC,uGAAuG;AACvG,eAAO,MAAM,kBAAkB,QAAS,CAAC;AACzC,iGAAiG;AACjG,eAAO,MAAM,gBAAgB,MAAM,CAAC;AA8CpC;;;;;GAKG;AACH,wBAAsB,sBAAsB,CAAC,OAAO,GAAE;IAAE,OAAO,CAAC,EAAE,OAAO,CAAA;CAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAW/F;AAuBD;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,wBAAsB,YAAY,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,GAAE,mBAAwB,GAAG,OAAO,CAAC,OAAO,CAAC,CAwBtG;AAED,8DAA8D;AAC9D,MAAM,MAAM,UAAU,GAAG,MAAM,IAAI,CAAC;AAIpC;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,GAAE,kBAAuB,GAAG,UAAU,CAehF"}
@@ -92,10 +92,17 @@ export function currentLocationPlace() {
92
92
  * dozens.
93
93
  */
94
94
  let sessionAnswer = null;
95
- /** Reset the de-duplication memory. Tests, and a sign-out. */
95
+ /** Reset the de-duplication memory, and drop any queued action unsent. Tests, and a sign-out. */
96
96
  export function resetEngagementBeacon() {
97
97
  lastReported = null;
98
98
  sessionAnswer = null;
99
+ for (const queue of queues.values()) {
100
+ if (queue.quiet)
101
+ clearTimeout(queue.quiet);
102
+ if (queue.due)
103
+ clearTimeout(queue.due);
104
+ }
105
+ queues.clear();
99
106
  }
100
107
  /**
101
108
  * Read an answer to its end, and drop it.
@@ -183,9 +190,85 @@ export async function reportPlace(place, options = {}) {
183
190
  return false;
184
191
  }
185
192
  }
193
+ /** How long the action queue waits for quiet before it sends (forge task 4034). */
194
+ export const ACTION_QUIET_MS = 5_000;
195
+ /** The longest an action waits however busy the page is, so a page that crashes loses 30 s at most. */
196
+ export const ACTION_MAX_WAIT_MS = 30_000;
197
+ /** One POST carries at most this many: the recorder's `MAX_ACTION_BATCH` (cursedbelt-server). */
198
+ export const ACTION_BATCH_MAX = 100;
199
+ const queues = new Map();
200
+ /** The window the page-leave listeners sit on, so a second one is never added (tests swap it). */
201
+ let leaveTarget = null;
202
+ function defaultBeacon() {
203
+ const nav = globalThis.navigator;
204
+ return typeof nav?.sendBeacon === "function" ? nav.sendBeacon.bind(nav) : undefined;
205
+ }
206
+ /**
207
+ * Flush on the way out: `pagehide` (a close, a navigation, the bfcache) and a tab going hidden,
208
+ * which is the last moment a phone gives a page it is about to kill. A timer cannot fire after
209
+ * either, so this is what delivers the actions of a page closed inside its quiet window.
210
+ */
211
+ function listenForLeave() {
212
+ const win = globalThis.window;
213
+ if (!win || typeof win.addEventListener !== "function" || leaveTarget === win)
214
+ return;
215
+ leaveTarget = win;
216
+ const leave = () => {
217
+ void flushEngagementActions({ leaving: true });
218
+ };
219
+ win.addEventListener("pagehide", leave);
220
+ const doc = globalThis.document;
221
+ doc?.addEventListener?.("visibilitychange", () => {
222
+ if (doc.visibilityState === "hidden")
223
+ leave();
224
+ });
225
+ }
226
+ /**
227
+ * Send what is queued, now: ONE POST per mount. A queue of one posts `{ place, action }` as
228
+ * `reportAction` always has, so a recorder older than the batch reads it; more posts
229
+ * `{ actions }`, and a recorder that refuses the batch (an older one answers 400) gets them one by
230
+ * one instead. `leaving` sends through `sendBeacon`, which outlives the page. Never throws.
231
+ */
232
+ export async function flushEngagementActions(options = {}) {
233
+ const sends = [];
234
+ for (const [basePath, queue] of queues) {
235
+ if (queue.quiet)
236
+ clearTimeout(queue.quiet);
237
+ if (queue.due)
238
+ clearTimeout(queue.due);
239
+ queues.delete(basePath);
240
+ for (let i = 0; i < queue.actions.length; i += ACTION_BATCH_MAX) {
241
+ sends.push(sendActions(`${basePath}/action`, queue.actions.slice(i, i + ACTION_BATCH_MAX), queue, options.leaving === true));
242
+ }
243
+ }
244
+ await Promise.all(sends);
245
+ }
246
+ async function sendActions(url, actions, queue, leaving) {
247
+ const [only] = actions;
248
+ const body = JSON.stringify(actions.length === 1 && only ? { place: only.place, action: only.action } : { actions });
249
+ try {
250
+ if (leaving && queue.beaconImpl?.(url, new Blob([body], { type: "application/json" })))
251
+ return;
252
+ const res = await queue.fetchImpl(url, {
253
+ method: "POST",
254
+ headers: { "content-type": "application/json" },
255
+ credentials: "same-origin",
256
+ body,
257
+ keepalive: true,
258
+ });
259
+ await drain(res);
260
+ if (res.status === 400 && actions.length > 1) {
261
+ for (const a of actions)
262
+ await sendActions(url, [a], queue, false);
263
+ }
264
+ }
265
+ catch {
266
+ // See the header: telemetry never becomes the user's problem.
267
+ }
268
+ }
186
269
  /**
187
270
  * Report that a FEATURE was used — `reportAction("search")` — beside the place it was used on
188
- * (forge task 3470). Returns whether anything was sent.
271
+ * (forge task 3470). Returns whether it was queued to send.
189
272
  *
190
273
  * The owner's question is "what features do they use and ignore"; a page view cannot answer it,
191
274
  * because one page holds several features. So the app names the action at the press, from the
@@ -199,6 +282,12 @@ export async function reportPlace(place, options = {}) {
199
282
  * · the same silence: session-probed (the probe's answer is shared with `reportPlace`, one per
200
283
  * page load), fire-and-forget, never throws, never logs.
201
284
  *
285
+ * 🔴 BATCHED (forge task 4034): an action is queued, and the queue goes as ONE request after
286
+ * {@link ACTION_QUIET_MS} of quiet, {@link ACTION_MAX_WAIT_MS} after its first action at most, at
287
+ * {@link ACTION_BATCH_MAX}, and when the page is left ({@link flushEngagementActions}). So a
288
+ * feature the owner adds to the record costs no request per click — his words: *"batching some
289
+ * interactions with debounces will lower traffic"*.
290
+ *
202
291
  * NOT de-duplicated, unlike `reportPlace`: three plays are three uses. Only call it from apps
203
292
  * whose recorder was mounted WITH `actions` — elsewhere `/action` is a 404 in the console.
204
293
  */
@@ -214,20 +303,22 @@ export async function reportAction(action, options = {}) {
214
303
  return false;
215
304
  }
216
305
  const place = options.place ?? (typeof window === "undefined" ? "/" : currentLocationPlace());
217
- try {
218
- const res = await doFetch(`${options.basePath ?? ENGAGEMENT_BASE_PATH}/action`, {
219
- method: "POST",
220
- headers: { "content-type": "application/json" },
221
- credentials: "same-origin",
222
- body: JSON.stringify({ place, action }),
223
- keepalive: true,
224
- });
225
- await drain(res);
306
+ const basePath = options.basePath ?? ENGAGEMENT_BASE_PATH;
307
+ const queue = queues.get(basePath) ?? { actions: [], fetchImpl: doFetch, beaconImpl: undefined, quiet: null, due: null };
308
+ queue.fetchImpl = doFetch;
309
+ queue.beaconImpl = options.beaconImpl ?? defaultBeacon();
310
+ queue.actions.push({ place, action, at: Date.now() });
311
+ queues.set(basePath, queue);
312
+ listenForLeave();
313
+ if (queue.actions.length >= ACTION_BATCH_MAX) {
314
+ void flushEngagementActions();
226
315
  return true;
227
316
  }
228
- catch {
229
- return false;
230
- }
317
+ if (queue.quiet)
318
+ clearTimeout(queue.quiet);
319
+ queue.quiet = setTimeout(() => void flushEngagementActions(), ACTION_QUIET_MS);
320
+ queue.due ??= setTimeout(() => void flushEngagementActions(), ACTION_MAX_WAIT_MS);
321
+ return true;
231
322
  }
232
323
  let running = null;
233
324
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"beaconClient.js","sourceRoot":"","sources":["../../../src/react/engagement/beaconClient.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"beaconClient.js","sourceRoot":"","sources":["../../../src/react/engagement/beaconClient.ts"],"names":[],"mappings":"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"}
@@ -1,3 +1,3 @@
1
- export { ENGAGEMENT_BASE_PATH, type ReportActionOptions, type ReportPlaceOptions, reportAction, reportPlace, resetEngagementBeacon, SESSION_PATH, type StopBeacon, startLocationBeacon, currentLocationPlace, } from './beaconClient.js';
1
+ export { ENGAGEMENT_BASE_PATH, flushEngagementActions, type ReportActionOptions, type ReportPlaceOptions, reportAction, reportPlace, resetEngagementBeacon, SESSION_PATH, type StopBeacon, startLocationBeacon, currentLocationPlace, } from './beaconClient.js';
2
2
  export { EngagementBeacon, useEngagementAction, useEngagementBeacon, useLocationBeacon } from './beacon.js';
3
3
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/react/engagement/index.ts"],"names":[],"mappings":"AAEA,OAAO,EACN,oBAAoB,EACpB,KAAK,mBAAmB,EACxB,KAAK,kBAAkB,EACvB,YAAY,EACZ,WAAW,EACX,qBAAqB,EACrB,YAAY,EACZ,KAAK,UAAU,EACf,mBAAmB,EACnB,oBAAoB,GACpB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAAE,gBAAgB,EAAE,mBAAmB,EAAE,mBAAmB,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/react/engagement/index.ts"],"names":[],"mappings":"AAEA,OAAO,EACN,oBAAoB,EACpB,sBAAsB,EACtB,KAAK,mBAAmB,EACxB,KAAK,kBAAkB,EACvB,YAAY,EACZ,WAAW,EACX,qBAAqB,EACrB,YAAY,EACZ,KAAK,UAAU,EACf,mBAAmB,EACnB,oBAAoB,GACpB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAAE,gBAAgB,EAAE,mBAAmB,EAAE,mBAAmB,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC"}
@@ -1,5 +1,5 @@
1
1
  // `cursedbelt/react/engagement-beacon` — the engagement beacon: `reportPlace`, `reportAction` and
2
2
  // `startLocationBeacon` (plain JS, no React) and the React hooks over them.
3
- export { ENGAGEMENT_BASE_PATH, reportAction, reportPlace, resetEngagementBeacon, SESSION_PATH, startLocationBeacon, currentLocationPlace, } from './beaconClient.js';
3
+ export { ENGAGEMENT_BASE_PATH, flushEngagementActions, reportAction, reportPlace, resetEngagementBeacon, SESSION_PATH, startLocationBeacon, currentLocationPlace, } from './beaconClient.js';
4
4
  export { EngagementBeacon, useEngagementAction, useEngagementBeacon, useLocationBeacon } from './beacon.js';
5
5
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/react/engagement/index.ts"],"names":[],"mappings":"AAAA,kGAAkG;AAClG,4EAA4E;AAC5E,OAAO,EACN,oBAAoB,EAGpB,YAAY,EACZ,WAAW,EACX,qBAAqB,EACrB,YAAY,EAEZ,mBAAmB,EACnB,oBAAoB,GACpB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAAE,gBAAgB,EAAE,mBAAmB,EAAE,mBAAmB,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/react/engagement/index.ts"],"names":[],"mappings":"AAAA,kGAAkG;AAClG,4EAA4E;AAC5E,OAAO,EACN,oBAAoB,EACpB,sBAAsB,EAGtB,YAAY,EACZ,WAAW,EACX,qBAAqB,EACrB,YAAY,EAEZ,mBAAmB,EACnB,oBAAoB,GACpB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAAE,gBAAgB,EAAE,mBAAmB,EAAE,mBAAmB,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC"}
@@ -51,8 +51,12 @@ export declare function designLedger(baselinePath: string): {
51
51
  * a WebKit context made from `appleContext(devices, device)`, judged by the design audit and
52
52
  * the phone rules — a long page read through a screen at a time, the keyboard raised over
53
53
  * `options.keyboard` — and recorded as `<route>@<device slug>`. The ratchet is the same file.
54
+ * A walk that read no text is walked again: an app's `load` can land on a shell of icons, with
55
+ * a phone's pass the first to see it (forge task 4039 — collections, station, family).
54
56
  */
55
- auditApple(page: ApplePage, route: string, device: AppleDevice, options?: AuditAppleOptions): Promise<void>;
57
+ auditApple(page: ApplePage, route: string, device: AppleDevice, options?: AuditAppleOptions & {
58
+ blankWaitMs?: number;
59
+ }): Promise<void>;
56
60
  record(route: string, violations: DesignViolation[]): void;
57
61
  /**
58
62
  * Every finding so far, formatted with its route — the string a suite with no baseline asserts
@@ -1 +1 @@
1
- {"version":3,"file":"designBaseline.d.ts","sourceRoot":"","sources":["../../src/testing/designBaseline.ts"],"names":[],"mappings":"AAoBA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AACrD,OAAO,EAAE,KAAK,SAAS,EAAE,KAAK,iBAAiB,EAA4B,MAAM,gBAAgB,CAAC;AAClG,OAAO,EACL,KAAK,kBAAkB,EAGvB,KAAK,YAAY,EACjB,KAAK,eAAe,EACpB,KAAK,cAAc,EAEpB,MAAM,kBAAkB,CAAC;AAE1B,2EAA2E;AAC3E,MAAM,WAAW,aAAc,SAAQ,cAAc;IACnD,eAAe,CAAC,IAAI,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACxE,YAAY,IAAI;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;CAC1D;AAED,MAAM,MAAM,WAAW,GAAG,OAAO,GAAG,MAAM,CAAC;AA0C3C,iFAAiF;AACjF,eAAO,MAAM,gBAAgB,sBAAuB,CAAC;AAIrD,MAAM,WAAW,mBAAmB;IAClC,IAAI,EAAE,YAAY,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;CACf;AAOD,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,mBAAmB,EAAE,CAYvE;AAED,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,mBAAmB,EAAE,GAAG,MAAM,CAG3E;AAED,MAAM,WAAW,qBAAqB;IACpC,uFAAuF;IACvF,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,iGAAiG;IACjG,IAAI,EAAE,MAAM,EAAE,CAAC;CAChB;AAED,iGAAiG;AACjG,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,mBAAmB,EAAE,EAAE,QAAQ,EAAE,mBAAmB,EAAE,GAAG,qBAAqB,CAc1H;AAED;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,YAAY,EAAE,MAAM;IAkC7C;;;;OAIG;gBAEK,aAAa,SACZ,MAAM,YACJ,kBAAkB,GAAG;QAAE,MAAM,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;QAAC,OAAO,CAAC,EAAE,SAAS,WAAW,EAAE,CAAC;QAAC,WAAW,CAAC,EAAE,MAAM,CAAA;KAAE,GACnH,OAAO,CAAC,IAAI,CAAC;IAehB;;;;;OAKG;qBACoB,SAAS,SAAS,MAAM,UAAU,WAAW,YAAW,iBAAiB,GAAQ,OAAO,CAAC,IAAI,CAAC;kBAMvG,MAAM,cAAc,eAAe,EAAE,GAAG,IAAI;IAG1D;;;OAGG;cACO,MAAM;aAMP,mBAAmB,EAAE;iBAUlB,MAAM,GAAG,SAAS,GAAiC,qBAAqB;EAkCvF"}
1
+ {"version":3,"file":"designBaseline.d.ts","sourceRoot":"","sources":["../../src/testing/designBaseline.ts"],"names":[],"mappings":"AAoBA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AACrD,OAAO,EAAE,KAAK,SAAS,EAAE,KAAK,iBAAiB,EAA4B,MAAM,gBAAgB,CAAC;AAClG,OAAO,EACL,KAAK,kBAAkB,EAGvB,KAAK,YAAY,EACjB,KAAK,eAAe,EACpB,KAAK,cAAc,EAEpB,MAAM,kBAAkB,CAAC;AAE1B,2EAA2E;AAC3E,MAAM,WAAW,aAAc,SAAQ,cAAc;IACnD,eAAe,CAAC,IAAI,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACxE,YAAY,IAAI;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;CAC1D;AAED,MAAM,MAAM,WAAW,GAAG,OAAO,GAAG,MAAM,CAAC;AA2D3C,iFAAiF;AACjF,eAAO,MAAM,gBAAgB,sBAAuB,CAAC;AAIrD,MAAM,WAAW,mBAAmB;IAClC,IAAI,EAAE,YAAY,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;CACf;AAOD,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,mBAAmB,EAAE,CAYvE;AAED,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,mBAAmB,EAAE,GAAG,MAAM,CAG3E;AAED,MAAM,WAAW,qBAAqB;IACpC,uFAAuF;IACvF,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,iGAAiG;IACjG,IAAI,EAAE,MAAM,EAAE,CAAC;CAChB;AAED,iGAAiG;AACjG,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,mBAAmB,EAAE,EAAE,QAAQ,EAAE,mBAAmB,EAAE,GAAG,qBAAqB,CAc1H;AAED;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,YAAY,EAAE,MAAM;IAiC7C;;;;OAIG;gBAEK,aAAa,SACZ,MAAM,YACJ,kBAAkB,GAAG;QAAE,MAAM,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;QAAC,OAAO,CAAC,EAAE,SAAS,WAAW,EAAE,CAAC;QAAC,WAAW,CAAC,EAAE,MAAM,CAAA;KAAE,GACnH,OAAO,CAAC,IAAI,CAAC;IAehB;;;;;;;OAOG;qBACoB,SAAS,SAAS,MAAM,UAAU,WAAW,YAAW,iBAAiB,GAAG;QAAE,WAAW,CAAC,EAAE,MAAM,CAAA;KAAE,GAAQ,OAAO,CAAC,IAAI,CAAC;kBAOlI,MAAM,cAAc,eAAe,EAAE,GAAG,IAAI;IAG1D;;;OAGG;cACO,MAAM;aAMP,mBAAmB,EAAE;iBAUlB,MAAM,GAAG,SAAS,GAAiC,qBAAqB;EAkCvF"}
@@ -20,8 +20,24 @@
20
20
  import { existsSync, readFileSync, writeFileSync } from 'node:fs';
21
21
  import { auditApple, settleInPage } from './appleWalk.js';
22
22
  import { auditDesign, DESIGN_RULES, formatDesignViolations, } from './designAudit.js';
23
- /** How long a width that reads no visible text is given to paint before the audit calls it blank. */
23
+ /** How long a read that finds no visible text is given to paint before the audit calls it blank. */
24
24
  const BLANK_WAIT_MS = 10_000;
25
+ /**
26
+ * Reads the page until it has text or the deadline passes. A page a test is holding can be blank
27
+ * for a moment that is the machine's, not the app's: a pane the CSS dropped before its replacement
28
+ * mounts, or a shell of icons before the screen's chunk lands, and a loaded machine stretches that
29
+ * past `settleInPage`'s calm. Both of the ledger's audits come through here; what stays blank at
30
+ * the deadline is the caller's to refuse.
31
+ */
32
+ async function readUntilPainted(read, blank, waitMs = BLANK_WAIT_MS) {
33
+ const deadline = Date.now() + waitMs;
34
+ let result = await read();
35
+ while (blank(result) && Date.now() < deadline) {
36
+ await new Promise((resolve) => setTimeout(resolve, 100));
37
+ result = await read();
38
+ }
39
+ return result;
40
+ }
25
41
  /** The color-scheme store's key (cursedbelt-core `COLOR_SCHEME_STORAGE_KEY`), said here so the
26
42
  * ledger stays free of a runtime import from the React half. */
27
43
  const SCHEME_KEY = 'cwip.colorScheme';
@@ -106,20 +122,15 @@ export function designLedger(baselinePath) {
106
122
  const original = page.viewportSize();
107
123
  for (const width of options.widths ?? APP_AUDIT_WIDTHS) {
108
124
  await page.setViewportSize({ width, height: original?.height ?? 900 });
109
- // Wait for the page to settle (`settleInPage` says what that means and why).
110
- await page.evaluate(settleInPage, undefined);
111
125
  const label = `${route}@${width}${scheme ? `:${scheme}` : ''}`;
112
- let { violations, census } = await auditDesign(page, { ...options, route, width });
113
126
  // A width change can hide what was on screen (a pane the CSS drops at this width) before the
114
- // code that mounts its replacement runs, and a loaded machine stretches that gap past
115
- // `settleInPage`'s calm: vault's entry read blank at 390 under the ship's three gates at once
116
- // (forge task 3890). So a blank census is settled and read again until the deadline.
117
- const deadline = Date.now() + (options.blankWaitMs ?? BLANK_WAIT_MS);
118
- while (census.textNodes === 0 && Date.now() < deadline) {
119
- await new Promise((resolve) => setTimeout(resolve, 100));
127
+ // code that mounts its replacement runs: vault's entry read blank at 390 under the ship's
128
+ // three gates at once (forge task 3890). So a blank census is settled and read again.
129
+ const { violations, census } = await readUntilPainted(async () => {
130
+ // Wait for the page to settle (`settleInPage` says what that means and why).
120
131
  await page.evaluate(settleInPage, undefined);
121
- ({ violations, census } = await auditDesign(page, { ...options, route, width }));
122
- }
132
+ return auditDesign(page, { ...options, route, width });
133
+ }, (read) => read.census.textNodes === 0, options.blankWaitMs);
123
134
  // A sweep that read no text audited nothing, and an empty finding list from it would
124
135
  // read as a clean page — the false green the touch-target sweep was once caught in.
125
136
  if (census.textNodes === 0)
@@ -158,10 +169,13 @@ export function designLedger(baselinePath) {
158
169
  * a WebKit context made from `appleContext(devices, device)`, judged by the design audit and
159
170
  * the phone rules — a long page read through a screen at a time, the keyboard raised over
160
171
  * `options.keyboard` — and recorded as `<route>@<device slug>`. The ratchet is the same file.
172
+ * A walk that read no text is walked again: an app's `load` can land on a shell of icons, with
173
+ * a phone's pass the first to see it (forge task 4039 — collections, station, family).
161
174
  */
162
175
  async auditApple(page, route, device, options = {}) {
163
176
  const label = `${route}@${device.slug}`;
164
- const { violations, textNodes } = await auditApple(page, route, device, options);
177
+ const { blankWaitMs, ...walk } = options;
178
+ const { violations, textNodes } = await readUntilPainted(() => auditApple(page, route, device, walk), (read) => read.textNodes === 0, blankWaitMs);
165
179
  if (textNodes === 0)
166
180
  throw new Error(`design audit: ${label} rendered no visible text — nothing was audited`);
167
181
  byRoute.set(label, violations);
@@ -1 +1 @@
1
- {"version":3,"file":"designBaseline.js","sourceRoot":"","sources":["../../src/testing/designBaseline.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"designBaseline.js","sourceRoot":"","sources":["../../src/testing/designBaseline.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cursedbelt",
3
- "version": "5.101.0",
3
+ "version": "5.102.0",
4
4
  "license": "ISC",
5
5
  "type": "module",
6
6
  "description": "The React design system of the cursedbelt split — components, styles, theme. cursedbelt-core below it; server tier in cursedbelt-server.",
@@ -8,7 +8,7 @@
8
8
  import { afterEach, beforeEach, describe, expect, test } from "bun:test";
9
9
  import { cleanup, renderHook, waitFor } from "@testing-library/react";
10
10
  import { useEngagementAction, useEngagementBeacon, useLocationBeacon } from "./beacon.js";
11
- import { resetEngagementBeacon } from "./beaconClient.js";
11
+ import { flushEngagementActions, resetEngagementBeacon } from "./beaconClient.js";
12
12
 
13
13
  /** A `fetch` that records every URL and answers every one signed in. */
14
14
  function recordingFetch() {
@@ -57,6 +57,7 @@ describe("useEngagementAction", () => {
57
57
  expect(result.current).toBe(first);
58
58
  expect(calls).toEqual([]);
59
59
  result.current();
60
+ await flushEngagementActions();
60
61
  await waitFor(() => expect(calls).toEqual(["/api/eng/action"]));
61
62
  });
62
63
  });
@@ -42,10 +42,10 @@ export function useEngagementBeacon(place: string | null, options: ReportPlaceOp
42
42
  * may and may not carry.
43
43
  */
44
44
  export function useEngagementAction(action: string, options: ReportActionOptions = {}): () => void {
45
- const { basePath, fetchImpl, sessionPath, assumeSignedIn, place } = options;
45
+ const { basePath, fetchImpl, sessionPath, assumeSignedIn, place, beaconImpl } = options;
46
46
  return useCallback(() => {
47
- void reportAction(action, { basePath, fetchImpl, sessionPath, assumeSignedIn, place });
48
- }, [action, basePath, fetchImpl, sessionPath, assumeSignedIn, place]);
47
+ void reportAction(action, { basePath, fetchImpl, sessionPath, assumeSignedIn, place, beaconImpl });
48
+ }, [action, basePath, fetchImpl, sessionPath, assumeSignedIn, place, beaconImpl]);
49
49
  }
50
50
 
51
51
  /**
@@ -13,7 +13,7 @@
13
13
  * this file runs in the package's plain `bun test`.
14
14
  */
15
15
  import { beforeEach, describe, expect, test } from "bun:test";
16
- import { reportAction, reportPlace, resetEngagementBeacon, SESSION_PATH } from "./beaconClient.js";
16
+ import { ACTION_BATCH_MAX, flushEngagementActions, reportAction, reportPlace, resetEngagementBeacon, SESSION_PATH } from "./beaconClient.js";
17
17
 
18
18
  /** A `fetch` that records every call, and every answer it handed out, and answers from a script. */
19
19
  function recordingFetch(answers: Record<string, { status?: number; body?: unknown }>) {
@@ -134,6 +134,7 @@ describe("every answer is read to its end (forge task 3884)", () => {
134
134
  const { impl, responses } = recordingFetch(answers);
135
135
  await reportPlace("/#/hub", { fetchImpl: impl });
136
136
  await reportAction("search", { fetchImpl: impl, place: "/#/hub" });
137
+ await flushEngagementActions();
137
138
  expect(responses.length).toBeGreaterThan(0);
138
139
  expect(responses.map((r) => r.bodyUsed)).toEqual(responses.map(() => true));
139
140
  });
@@ -171,22 +172,78 @@ describe("reportAction — a feature NAME on a place, with the same silence (tas
171
172
  test("🔴 the body is the place and the action NAME — nothing else can travel", async () => {
172
173
  const { impl, posts } = bodyFetch(true);
173
174
  expect(await reportAction("search", { fetchImpl: impl, place: "library" })).toBe(true);
175
+ await flushEngagementActions();
174
176
  expect(posts).toEqual([{ url: "/api/engagement/action", body: { place: "library", action: "search" } }]);
175
177
  });
176
178
 
177
- test("NOT de-duplicated: three plays are three uses, and one session probe", async () => {
178
- const { impl, calls } = bodyFetch(true);
179
+ test("NOT de-duplicated: three plays are three uses, ONE request and one session probe (task 4034)", async () => {
180
+ const { impl, calls, posts } = bodyFetch(true);
179
181
  for (let i = 0; i < 3; i++) await reportAction("play", { fetchImpl: impl, place: "/" });
180
- expect(calls).toEqual([SESSION_PATH, "/api/engagement/action", "/api/engagement/action", "/api/engagement/action"]);
182
+ expect(calls).toEqual([SESSION_PATH]);
183
+ await flushEngagementActions();
184
+ expect(calls).toEqual([SESSION_PATH, "/api/engagement/action"]);
185
+ const body = posts[0]?.body as { actions: { place: string; action: string; at: number }[] };
186
+ expect(body.actions.map(({ place, action }) => ({ place, action }))).toEqual([0, 1, 2].map(() => ({ place: "/", action: "play" })));
187
+ expect(body.actions.every((a) => typeof a.at === "number" && Object.keys(a).length === 3)).toBe(true);
188
+ });
189
+
190
+ test("the queue goes by itself: after the quiet window, and at the cap without waiting", async () => {
191
+ const { impl, posts } = bodyFetch(true);
192
+ await reportAction("play", { fetchImpl: impl, assumeSignedIn: true, place: "/" });
193
+ expect(posts).toEqual([]);
194
+ await Bun.sleep(5_100);
195
+ expect(posts).toEqual([{ url: "/api/engagement/action", body: { place: "/", action: "play" } }]);
196
+ for (let i = 0; i < ACTION_BATCH_MAX; i++) await reportAction("play", { fetchImpl: impl, assumeSignedIn: true, place: "/" });
197
+ await Bun.sleep(0);
198
+ expect(posts).toHaveLength(2);
199
+ expect((posts[1] as { body: { actions: unknown[] } }).body.actions).toHaveLength(ACTION_BATCH_MAX);
200
+ }, 10_000);
201
+
202
+ test("🔴 a page closed inside the quiet window still delivers its actions, through sendBeacon", async () => {
203
+ const { impl, calls } = bodyFetch(true);
204
+ const beacons: { url: string; body: Blob }[] = [];
205
+ const beaconImpl = (url: string, body: Blob) => beacons.push({ url, body }) > 0;
206
+ const g = globalThis as { window?: EventTarget };
207
+ const before = g.window;
208
+ g.window = new EventTarget();
209
+ try {
210
+ await reportAction("play", { fetchImpl: impl, assumeSignedIn: true, place: "/", beaconImpl });
211
+ await reportAction("search", { fetchImpl: impl, assumeSignedIn: true, place: "library", beaconImpl });
212
+ g.window.dispatchEvent(new Event("pagehide"));
213
+ await Bun.sleep(0);
214
+ expect(calls).toEqual([]);
215
+ expect(beacons.map((b) => [b.url, b.body.type.split(";")[0]])).toEqual([["/api/engagement/action", "application/json"]]);
216
+ const sent = JSON.parse(await (beacons[0] as { body: Blob }).body.text()) as { actions: { action: string }[] };
217
+ expect(sent.actions.map((a) => a.action)).toEqual(["play", "search"]);
218
+ } finally {
219
+ g.window = before;
220
+ }
221
+ });
222
+
223
+ test("a recorder older than the batch (a 400) gets the actions one by one", async () => {
224
+ const posts: unknown[] = [];
225
+ const impl = (async (_url: string, init?: { body?: string }) => {
226
+ const body = JSON.parse(init?.body ?? "null") as { actions?: unknown };
227
+ posts.push(body);
228
+ return new Response("{}", { status: body?.actions ? 400 : 200 });
229
+ }) as unknown as typeof fetch;
230
+ await reportAction("play", { fetchImpl: impl, assumeSignedIn: true, place: "/" });
231
+ await reportAction("search", { fetchImpl: impl, assumeSignedIn: true, place: "/" });
232
+ await flushEngagementActions();
233
+ expect(posts.slice(1)).toEqual([
234
+ { place: "/", action: "play" },
235
+ { place: "/", action: "search" },
236
+ ]);
181
237
  });
182
238
 
183
- test("an empty name sends nothing; a throwing fetch is false, never a rejection", async () => {
239
+ test("an empty name sends nothing; a throwing fetch is never a rejection", async () => {
184
240
  const { impl, calls } = bodyFetch(true);
185
241
  expect(await reportAction("", { fetchImpl: impl })).toBe(false);
186
242
  expect(calls).toEqual([]);
187
243
  const boom = (async () => {
188
244
  throw new Error("offline");
189
245
  }) as unknown as typeof fetch;
190
- expect(await reportAction("play", { fetchImpl: boom, assumeSignedIn: true, place: "/" })).toBe(false);
246
+ expect(await reportAction("play", { fetchImpl: boom, assumeSignedIn: true, place: "/" })).toBe(true);
247
+ await flushEngagementActions();
191
248
  });
192
249
  });
@@ -115,10 +115,15 @@ export function currentLocationPlace(): string {
115
115
  */
116
116
  let sessionAnswer: Promise<boolean> | null = null;
117
117
 
118
- /** Reset the de-duplication memory. Tests, and a sign-out. */
118
+ /** Reset the de-duplication memory, and drop any queued action unsent. Tests, and a sign-out. */
119
119
  export function resetEngagementBeacon(): void {
120
120
  lastReported = null;
121
121
  sessionAnswer = null;
122
+ for (const queue of queues.values()) {
123
+ if (queue.quiet) clearTimeout(queue.quiet);
124
+ if (queue.due) clearTimeout(queue.due);
125
+ }
126
+ queues.clear();
122
127
  }
123
128
 
124
129
  /**
@@ -213,11 +218,104 @@ export interface ReportActionOptions extends ReportPlaceOptions {
213
218
  * against the manifest exactly as it resolves a view, so the default is right for every app.
214
219
  */
215
220
  place?: string;
221
+ /** Injected in tests; defaults to `navigator.sendBeacon`. The page-leave flush sends through it. */
222
+ beaconImpl?: (url: string, body: Blob) => boolean;
223
+ }
224
+
225
+ /** How long the action queue waits for quiet before it sends (forge task 4034). */
226
+ export const ACTION_QUIET_MS = 5_000;
227
+ /** The longest an action waits however busy the page is, so a page that crashes loses 30 s at most. */
228
+ export const ACTION_MAX_WAIT_MS = 30_000;
229
+ /** One POST carries at most this many: the recorder's `MAX_ACTION_BATCH` (cursedbelt-server). */
230
+ export const ACTION_BATCH_MAX = 100;
231
+
232
+ interface QueuedAction {
233
+ place: string;
234
+ action: string;
235
+ at: number;
236
+ }
237
+
238
+ /** One queue per recorder mount; almost always one. */
239
+ interface ActionQueue {
240
+ actions: QueuedAction[];
241
+ fetchImpl: typeof fetch;
242
+ beaconImpl: ((url: string, body: Blob) => boolean) | undefined;
243
+ quiet: ReturnType<typeof setTimeout> | null;
244
+ due: ReturnType<typeof setTimeout> | null;
245
+ }
246
+
247
+ const queues = new Map<string, ActionQueue>();
248
+
249
+ /** The window the page-leave listeners sit on, so a second one is never added (tests swap it). */
250
+ let leaveTarget: EventTarget | null = null;
251
+
252
+ function defaultBeacon(): ((url: string, body: Blob) => boolean) | undefined {
253
+ const nav = (globalThis as { navigator?: { sendBeacon?: (url: string, body: Blob) => boolean } }).navigator;
254
+ return typeof nav?.sendBeacon === "function" ? nav.sendBeacon.bind(nav) : undefined;
255
+ }
256
+
257
+ /**
258
+ * Flush on the way out: `pagehide` (a close, a navigation, the bfcache) and a tab going hidden,
259
+ * which is the last moment a phone gives a page it is about to kill. A timer cannot fire after
260
+ * either, so this is what delivers the actions of a page closed inside its quiet window.
261
+ */
262
+ function listenForLeave(): void {
263
+ const win = (globalThis as { window?: EventTarget }).window;
264
+ if (!win || typeof win.addEventListener !== "function" || leaveTarget === win) return;
265
+ leaveTarget = win;
266
+ const leave = (): void => {
267
+ void flushEngagementActions({ leaving: true });
268
+ };
269
+ win.addEventListener("pagehide", leave);
270
+ const doc = (globalThis as { document?: EventTarget & { visibilityState?: string } }).document;
271
+ doc?.addEventListener?.("visibilitychange", () => {
272
+ if (doc.visibilityState === "hidden") leave();
273
+ });
274
+ }
275
+
276
+ /**
277
+ * Send what is queued, now: ONE POST per mount. A queue of one posts `{ place, action }` as
278
+ * `reportAction` always has, so a recorder older than the batch reads it; more posts
279
+ * `{ actions }`, and a recorder that refuses the batch (an older one answers 400) gets them one by
280
+ * one instead. `leaving` sends through `sendBeacon`, which outlives the page. Never throws.
281
+ */
282
+ export async function flushEngagementActions(options: { leaving?: boolean } = {}): Promise<void> {
283
+ const sends: Promise<void>[] = [];
284
+ for (const [basePath, queue] of queues) {
285
+ if (queue.quiet) clearTimeout(queue.quiet);
286
+ if (queue.due) clearTimeout(queue.due);
287
+ queues.delete(basePath);
288
+ for (let i = 0; i < queue.actions.length; i += ACTION_BATCH_MAX) {
289
+ sends.push(sendActions(`${basePath}/action`, queue.actions.slice(i, i + ACTION_BATCH_MAX), queue, options.leaving === true));
290
+ }
291
+ }
292
+ await Promise.all(sends);
293
+ }
294
+
295
+ async function sendActions(url: string, actions: QueuedAction[], queue: ActionQueue, leaving: boolean): Promise<void> {
296
+ const [only] = actions;
297
+ const body = JSON.stringify(actions.length === 1 && only ? { place: only.place, action: only.action } : { actions });
298
+ try {
299
+ if (leaving && queue.beaconImpl?.(url, new Blob([body], { type: "application/json" }))) return;
300
+ const res = await queue.fetchImpl(url, {
301
+ method: "POST",
302
+ headers: { "content-type": "application/json" },
303
+ credentials: "same-origin",
304
+ body,
305
+ keepalive: true,
306
+ });
307
+ await drain(res);
308
+ if (res.status === 400 && actions.length > 1) {
309
+ for (const a of actions) await sendActions(url, [a], queue, false);
310
+ }
311
+ } catch {
312
+ // See the header: telemetry never becomes the user's problem.
313
+ }
216
314
  }
217
315
 
218
316
  /**
219
317
  * Report that a FEATURE was used — `reportAction("search")` — beside the place it was used on
220
- * (forge task 3470). Returns whether anything was sent.
318
+ * (forge task 3470). Returns whether it was queued to send.
221
319
  *
222
320
  * The owner's question is "what features do they use and ignore"; a page view cannot answer it,
223
321
  * because one page holds several features. So the app names the action at the press, from the
@@ -231,6 +329,12 @@ export interface ReportActionOptions extends ReportPlaceOptions {
231
329
  * · the same silence: session-probed (the probe's answer is shared with `reportPlace`, one per
232
330
  * page load), fire-and-forget, never throws, never logs.
233
331
  *
332
+ * 🔴 BATCHED (forge task 4034): an action is queued, and the queue goes as ONE request after
333
+ * {@link ACTION_QUIET_MS} of quiet, {@link ACTION_MAX_WAIT_MS} after its first action at most, at
334
+ * {@link ACTION_BATCH_MAX}, and when the page is left ({@link flushEngagementActions}). So a
335
+ * feature the owner adds to the record costs no request per click — his words: *"batching some
336
+ * interactions with debounces will lower traffic"*.
337
+ *
234
338
  * NOT de-duplicated, unlike `reportPlace`: three plays are three uses. Only call it from apps
235
339
  * whose recorder was mounted WITH `actions` — elsewhere `/action` is a 404 in the console.
236
340
  */
@@ -243,19 +347,21 @@ export async function reportAction(action: string, options: ReportActionOptions
243
347
  if (!(await sessionAnswer)) return false;
244
348
  }
245
349
  const place = options.place ?? (typeof window === "undefined" ? "/" : currentLocationPlace());
246
- try {
247
- const res = await doFetch(`${options.basePath ?? ENGAGEMENT_BASE_PATH}/action`, {
248
- method: "POST",
249
- headers: { "content-type": "application/json" },
250
- credentials: "same-origin",
251
- body: JSON.stringify({ place, action }),
252
- keepalive: true,
253
- });
254
- await drain(res);
350
+ const basePath = options.basePath ?? ENGAGEMENT_BASE_PATH;
351
+ const queue: ActionQueue = queues.get(basePath) ?? { actions: [], fetchImpl: doFetch, beaconImpl: undefined, quiet: null, due: null };
352
+ queue.fetchImpl = doFetch;
353
+ queue.beaconImpl = options.beaconImpl ?? defaultBeacon();
354
+ queue.actions.push({ place, action, at: Date.now() });
355
+ queues.set(basePath, queue);
356
+ listenForLeave();
357
+ if (queue.actions.length >= ACTION_BATCH_MAX) {
358
+ void flushEngagementActions();
255
359
  return true;
256
- } catch {
257
- return false;
258
360
  }
361
+ if (queue.quiet) clearTimeout(queue.quiet);
362
+ queue.quiet = setTimeout(() => void flushEngagementActions(), ACTION_QUIET_MS);
363
+ queue.due ??= setTimeout(() => void flushEngagementActions(), ACTION_MAX_WAIT_MS);
364
+ return true;
259
365
  }
260
366
 
261
367
  /** Detaches the listeners `startLocationBeacon` installed. */
@@ -2,6 +2,7 @@
2
2
  // `startLocationBeacon` (plain JS, no React) and the React hooks over them.
3
3
  export {
4
4
  ENGAGEMENT_BASE_PATH,
5
+ flushEngagementActions,
5
6
  type ReportActionOptions,
6
7
  type ReportPlaceOptions,
7
8
  reportAction,
@@ -2,6 +2,7 @@ import { describe, expect, test } from 'bun:test';
2
2
  import { mkdtempSync, readFileSync, writeFileSync } from 'node:fs';
3
3
  import { tmpdir } from 'node:os';
4
4
  import { join } from 'node:path';
5
+ import { APPLE_GATE_DEVICES, type AppleDevice } from './appleDevices';
5
6
  import type { DesignViolation } from './designAudit';
6
7
  import { compareDesignBaseline, designLedger, formatDesignBaseline, parseDesignBaseline } from './designBaseline';
7
8
 
@@ -89,6 +90,41 @@ test('detail() prints every recorded finding with its route, and nothing for a c
89
90
  expect(ledger.detail()).toContain('button "Pin"');
90
91
  });
91
92
 
93
+ describe('an Apple walk that reads blank is walked again (forge task 4039)', () => {
94
+ /** A page whose walk reads no text for its first `blankReads` design audits, then one LAY-10. */
95
+ const pageBlankFor = (blankReads: number) => {
96
+ let probes = 0;
97
+ return {
98
+ probes: () => probes,
99
+ viewportSize: () => ({ width: 320, height: 568 }),
100
+ evaluate: async <R, A>(_fn: (arg: A) => R | Promise<R>, arg: A): Promise<R> => {
101
+ if (arg && typeof arg === 'object' && 'targets' in arg) return { width: 320, height: 568, scrollWidth: 320, offscreen: [], fields: [], targets: [] } as R; // measurePhone
102
+ if (!(arg && typeof arg === 'object' && 'rules' in arg)) return undefined as R; // settleInPage, atRestInPage
103
+ probes++;
104
+ const textNodes = probes > blankReads ? 12 : 0;
105
+ return { violations: textNodes ? [v('LAY-10')] : [], census: { textNodes, targets: 0, controlRows: 0, tableCells: 0, contrastChecked: 0, coarse: true, innerWidth: 320 } } as R;
106
+ },
107
+ };
108
+ };
109
+ const phone = APPLE_GATE_DEVICES[0] as AppleDevice;
110
+
111
+ test('🔴 a shell of icons that gets its screen a moment later is audited once it has', async () => {
112
+ const page = pageBlankFor(2);
113
+ const ledger = designLedger(tmp());
114
+ await ledger.auditApple(page, 'parked', phone);
115
+ expect(page.probes()).toBe(3);
116
+ expect(ledger.detail()).toContain(`parked@${phone.slug}`);
117
+ expect(ledger.detail()).toContain('LAY-10');
118
+ });
119
+
120
+ test('🔴 a screen still blank at the deadline is refused — no empty finding list reads as clean', async () => {
121
+ const page = pageBlankFor(Number.POSITIVE_INFINITY);
122
+ const ledger = designLedger(tmp());
123
+ await expect(ledger.auditApple(page, 'parked', phone, { blankWaitMs: 1_000 })).rejects.toThrow(`parked@${phone.slug} rendered no visible text`);
124
+ expect(page.probes()).toBeGreaterThan(1);
125
+ });
126
+ });
127
+
92
128
  describe('a width that reads blank is given its moment to paint (forge task 3890)', () => {
93
129
  /** A page whose probe reads no text for its first `blankReads` audits, then one LAY-13. */
94
130
  const pageBlankFor = (blankReads: number) => {
@@ -38,9 +38,26 @@ export interface ResizablePage extends EvaluatingPage {
38
38
 
39
39
  export type ColorScheme = 'light' | 'dark';
40
40
 
41
- /** How long a width that reads no visible text is given to paint before the audit calls it blank. */
41
+ /** How long a read that finds no visible text is given to paint before the audit calls it blank. */
42
42
  const BLANK_WAIT_MS = 10_000;
43
43
 
44
+ /**
45
+ * Reads the page until it has text or the deadline passes. A page a test is holding can be blank
46
+ * for a moment that is the machine's, not the app's: a pane the CSS dropped before its replacement
47
+ * mounts, or a shell of icons before the screen's chunk lands, and a loaded machine stretches that
48
+ * past `settleInPage`'s calm. Both of the ledger's audits come through here; what stays blank at
49
+ * the deadline is the caller's to refuse.
50
+ */
51
+ async function readUntilPainted<R>(read: () => Promise<R>, blank: (result: R) => boolean, waitMs = BLANK_WAIT_MS): Promise<R> {
52
+ const deadline = Date.now() + waitMs;
53
+ let result = await read();
54
+ while (blank(result) && Date.now() < deadline) {
55
+ await new Promise((resolve) => setTimeout(resolve, 100));
56
+ result = await read();
57
+ }
58
+ return result;
59
+ }
60
+
44
61
  /** The color-scheme store's key (cursedbelt-core `COLOR_SCHEME_STORAGE_KEY`), said here so the
45
62
  * ledger stays free of a runtime import from the React half. */
46
63
  const SCHEME_KEY = 'cwip.colorScheme';
@@ -154,20 +171,19 @@ export function designLedger(baselinePath: string) {
154
171
  const original = page.viewportSize();
155
172
  for (const width of options.widths ?? APP_AUDIT_WIDTHS) {
156
173
  await page.setViewportSize({ width, height: original?.height ?? 900 });
157
- // Wait for the page to settle (`settleInPage` says what that means and why).
158
- await page.evaluate(settleInPage, undefined);
159
174
  const label = `${route}@${width}${scheme ? `:${scheme}` : ''}`;
160
- let { violations, census } = await auditDesign(page, { ...options, route, width });
161
175
  // A width change can hide what was on screen (a pane the CSS drops at this width) before the
162
- // code that mounts its replacement runs, and a loaded machine stretches that gap past
163
- // `settleInPage`'s calm: vault's entry read blank at 390 under the ship's three gates at once
164
- // (forge task 3890). So a blank census is settled and read again until the deadline.
165
- const deadline = Date.now() + (options.blankWaitMs ?? BLANK_WAIT_MS);
166
- while (census.textNodes === 0 && Date.now() < deadline) {
167
- await new Promise((resolve) => setTimeout(resolve, 100));
168
- await page.evaluate(settleInPage, undefined);
169
- ({ violations, census } = await auditDesign(page, { ...options, route, width }));
170
- }
176
+ // code that mounts its replacement runs: vault's entry read blank at 390 under the ship's
177
+ // three gates at once (forge task 3890). So a blank census is settled and read again.
178
+ const { violations, census } = await readUntilPainted(
179
+ async () => {
180
+ // Wait for the page to settle (`settleInPage` says what that means and why).
181
+ await page.evaluate(settleInPage, undefined);
182
+ return auditDesign(page, { ...options, route, width });
183
+ },
184
+ (read) => read.census.textNodes === 0,
185
+ options.blankWaitMs,
186
+ );
171
187
  // A sweep that read no text audited nothing, and an empty finding list from it would
172
188
  // read as a clean page — the false green the touch-target sweep was once caught in.
173
189
  if (census.textNodes === 0) throw new Error(`design audit: ${label} rendered no visible text — nothing was audited`);
@@ -205,10 +221,13 @@ export function designLedger(baselinePath: string) {
205
221
  * a WebKit context made from `appleContext(devices, device)`, judged by the design audit and
206
222
  * the phone rules — a long page read through a screen at a time, the keyboard raised over
207
223
  * `options.keyboard` — and recorded as `<route>@<device slug>`. The ratchet is the same file.
224
+ * A walk that read no text is walked again: an app's `load` can land on a shell of icons, with
225
+ * a phone's pass the first to see it (forge task 4039 — collections, station, family).
208
226
  */
209
- async auditApple(page: ApplePage, route: string, device: AppleDevice, options: AuditAppleOptions = {}): Promise<void> {
227
+ async auditApple(page: ApplePage, route: string, device: AppleDevice, options: AuditAppleOptions & { blankWaitMs?: number } = {}): Promise<void> {
210
228
  const label = `${route}@${device.slug}`;
211
- const { violations, textNodes } = await auditApple(page, route, device, options);
229
+ const { blankWaitMs, ...walk } = options;
230
+ const { violations, textNodes } = await readUntilPainted(() => auditApple(page, route, device, walk), (read) => read.textNodes === 0, blankWaitMs);
212
231
  if (textNodes === 0) throw new Error(`design audit: ${label} rendered no visible text — nothing was audited`);
213
232
  byRoute.set(label, violations);
214
233
  },