@avasapp/agent-bridge 0.3.0 → 0.4.1

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.
@@ -29,6 +29,22 @@ __export(network_exports, {
29
29
  });
30
30
  module.exports = __toCommonJS(network_exports);
31
31
 
32
+ // src/shared/mock-signal.ts
33
+ var KEY = /* @__PURE__ */ Symbol.for("@avasapp/agent-bridge/mock-signal");
34
+ var listeners = () => {
35
+ const g = globalThis;
36
+ return g[KEY] ?? (g[KEY] = /* @__PURE__ */ new Set());
37
+ };
38
+ var signalRequestStarted = (requestId) => {
39
+ for (const l of [...listeners()]) l.started?.(requestId);
40
+ };
41
+ var signalMockAnswered = (mockId, requestId) => {
42
+ for (const l of [...listeners()]) l.answered?.(mockId, requestId);
43
+ };
44
+ var signalMocksRemoved = (ids) => {
45
+ for (const l of [...listeners()]) l.removed?.(ids);
46
+ };
47
+
32
48
  // src/network/api.ts
33
49
  var escape = (s) => s.replace(/[.*+?^${}()|[\]\\/]/g, "\\$&");
34
50
  var ROUTE = /^(?:([A-Za-z]+)\s+)?(\/\S*)$/;
@@ -96,14 +112,18 @@ function apiRoutes(baseUrl, routes, options = {}) {
96
112
 
97
113
  // src/network/body.ts
98
114
  var MAX_BODY = 2048;
99
- function formatBody(text) {
115
+ var MAX_FULL_BODY = 1e6;
116
+ var FULL_BUDGET = 4e6;
117
+ function compactBody(text) {
100
118
  if (text === void 0 || text === "") return void 0;
101
- let out = text;
102
119
  try {
103
- out = JSON.stringify(JSON.parse(text));
120
+ return JSON.stringify(JSON.parse(text));
104
121
  } catch {
122
+ return text;
105
123
  }
106
- return out.length > MAX_BODY ? `${out.slice(0, MAX_BODY)}\u2026 (+${out.length - MAX_BODY} chars)` : out;
124
+ }
125
+ function truncateBody(text) {
126
+ return text.length > MAX_BODY ? `${text.slice(0, MAX_BODY)}\u2026 (+${text.length - MAX_BODY} chars)` : text;
107
127
  }
108
128
  function bodyText(body) {
109
129
  if (body === void 0 || body === null) return void 0;
@@ -129,10 +149,10 @@ function errorMessage(error) {
129
149
 
130
150
  // src/network/state.ts
131
151
  var LOG_SIZE = 100;
132
- var KEY = /* @__PURE__ */ Symbol.for("@avasapp/agent-bridge/network");
152
+ var KEY2 = /* @__PURE__ */ Symbol.for("@avasapp/agent-bridge/network");
133
153
  function networkState() {
134
154
  const g = globalThis;
135
- g[KEY] ?? (g[KEY] = {
155
+ g[KEY2] ?? (g[KEY2] = {
136
156
  installed: false,
137
157
  log: [],
138
158
  nextLogId: 1,
@@ -146,7 +166,7 @@ function networkState() {
146
166
  strictAgent: null,
147
167
  blocked: /* @__PURE__ */ new Map()
148
168
  });
149
- return g[KEY];
169
+ return g[KEY2];
150
170
  }
151
171
  var URL_PARTS = /^(?:[a-z][a-z0-9+.-]*:)?(?:\/\/([^/?#]*))?([^?#]*)/i;
152
172
  var hostOf = (url) => URL_PARTS.exec(url)?.[1] ?? "";
@@ -170,29 +190,96 @@ function deriveDevHost() {
170
190
  return null;
171
191
  }
172
192
  }
193
+ var FULL = /* @__PURE__ */ new WeakMap();
194
+ var fullOf = (entry) => {
195
+ let full = FULL.get(entry);
196
+ if (!full) FULL.set(entry, full = {});
197
+ return full;
198
+ };
199
+ function enforceBudget() {
200
+ const { log } = networkState();
201
+ const size = (f) => (f?.requestBody?.length ?? 0) + (f?.responseBody?.length ?? 0);
202
+ let total = log.reduce((sum, e) => sum + size(FULL.get(e)), 0);
203
+ for (const entry of log) {
204
+ if (total <= FULL_BUDGET) return;
205
+ const full = FULL.get(entry);
206
+ if (!full || !size(full)) continue;
207
+ total -= size(full);
208
+ delete full.requestBody;
209
+ if (full.responseBody !== void 0) {
210
+ delete full.responseBody;
211
+ full.response = "evicted";
212
+ }
213
+ }
214
+ }
215
+ function keepRequestBody(entry, text) {
216
+ const body = compactBody(text);
217
+ if (body === void 0) return;
218
+ entry.requestBody = truncateBody(body);
219
+ if (text.length <= MAX_FULL_BODY) {
220
+ fullOf(entry).requestBody = text;
221
+ enforceBudget();
222
+ }
223
+ }
224
+ function setResponseBody(entry, text, contentType) {
225
+ const full = fullOf(entry);
226
+ if (contentType !== void 0) full.contentType = contentType;
227
+ if (text === void 0) {
228
+ full.response = "none";
229
+ return;
230
+ }
231
+ const body = compactBody(text);
232
+ if (body !== void 0) entry.responseBody = truncateBody(body);
233
+ if (text.length > MAX_FULL_BODY) {
234
+ full.response = "cut";
235
+ return;
236
+ }
237
+ full.responseBody = text;
238
+ full.response = "kept";
239
+ enforceBudget();
240
+ }
241
+ function markResponse(entry, response) {
242
+ fullOf(entry).response = response;
243
+ }
173
244
  function startEntry(state, fields) {
174
245
  const entry = {
175
246
  id: state.nextLogId++,
176
- ...fields,
247
+ method: fields.method,
248
+ url: fields.url,
249
+ startedAt: Date.now(),
177
250
  ms: 0,
178
- pending: true,
179
- started: Date.now()
251
+ pending: true
180
252
  };
181
- if (entry.requestBody === void 0) delete entry.requestBody;
253
+ keepRequestBody(entry, fields.requestBody);
182
254
  state.log.push(entry);
255
+ signalRequestStarted(entry.id);
183
256
  if (state.log.length > LOG_SIZE)
184
257
  state.log.splice(0, state.log.length - LOG_SIZE);
185
258
  return entry;
186
259
  }
187
260
  function finishEntry(entry, fields) {
188
- entry.ms = Date.now() - entry.started;
261
+ entry.ms = Date.now() - entry.startedAt;
189
262
  delete entry.pending;
190
- for (const [key, value] of Object.entries(fields))
263
+ const { responseBody, contentType, ...rest } = fields;
264
+ for (const [key, value] of Object.entries(rest))
191
265
  if (value !== void 0) entry[key] = value;
266
+ if (responseBody !== void 0) setResponseBody(entry, responseBody, contentType);
267
+ }
268
+ function publicEntry(entry, full = false) {
269
+ const out = entry.pending ? { ...entry, ms: Date.now() - entry.startedAt } : { ...entry };
270
+ if (full) {
271
+ const { requestBody, responseBody } = FULL.get(entry) ?? {};
272
+ if (requestBody !== void 0) out.requestBody = requestBody;
273
+ if (responseBody !== void 0) out.responseBody = responseBody;
274
+ const cut = entry.responseBody !== void 0 && responseBody === void 0;
275
+ const cutRequest = entry.requestBody !== void 0 && requestBody === void 0;
276
+ if (cut || cutRequest) out.truncated = true;
277
+ }
278
+ return out;
192
279
  }
193
- function publicEntry(entry) {
194
- const { started, ...rest } = entry;
195
- return rest.pending && started !== void 0 ? { ...rest, ms: Date.now() - started } : rest;
280
+ function fullResponse(entry) {
281
+ const full = FULL.get(entry);
282
+ return { state: full?.response ?? "none", body: full?.responseBody, contentType: full?.contentType };
196
283
  }
197
284
  function urlTest(url) {
198
285
  if (typeof url === "string") return (u) => u.includes(url);
@@ -213,6 +300,8 @@ function compileMatch(match) {
213
300
  function addMock(state, source, match, response, options = {}) {
214
301
  if (!response || typeof response !== "object" && typeof response !== "function")
215
302
  throw new Error("A mock needs a response: { status?, json?, body?, headers? }, { offline: true } or a handler");
303
+ if (options.priority !== void 0 && !Number.isFinite(options.priority))
304
+ throw new Error("priority must be a finite number");
216
305
  const seq = state.nextSeq++;
217
306
  const id = options.id ?? `${source}-${seq}`;
218
307
  removeMocks(state, (m) => m.id === id);
@@ -223,6 +312,7 @@ function addMock(state, source, match, response, options = {}) {
223
312
  response,
224
313
  times: options.times,
225
314
  delayMs: options.delayMs ?? 0,
315
+ priority: options.priority ?? 0,
226
316
  hits: 0,
227
317
  seq,
228
318
  test: compileMatch(match)
@@ -235,7 +325,21 @@ function removeMocks(state, which) {
235
325
  state.mocks = state.mocks.filter((m) => !which(m));
236
326
  return before - state.mocks.length;
237
327
  }
238
- var mockOrder = (a, b) => Number(b.source === "agent") - Number(a.source === "agent") || b.seq - a.seq;
328
+ var mockOrder = (a, b) => Number(b.source === "agent") - Number(a.source === "agent") || b.priority - a.priority || b.seq - a.seq;
329
+ var substringMatch = (match) => {
330
+ const spec = typeof match === "string" ? { url: match } : match;
331
+ return typeof spec.url === "string" ? { url: spec.url, method: spec.method?.toUpperCase() } : null;
332
+ };
333
+ function shadowedMocks(state, added) {
334
+ const next = substringMatch(added.match);
335
+ if (!next || typeof added.response === "function") return [];
336
+ return state.mocks.filter((old) => {
337
+ if (old === added || old.source !== "agent" || mockOrder(added, old) >= 0)
338
+ return false;
339
+ const prev = substringMatch(old.match);
340
+ return !!prev && prev.url.includes(next.url) && (!next.method || next.method === prev.method);
341
+ });
342
+ }
239
343
  function matchingMocks(state, method, url) {
240
344
  return state.mocks.filter((m) => m.test(method, url)).sort(mockOrder);
241
345
  }
@@ -266,6 +370,7 @@ function mockRequest(method, url, headers, body) {
266
370
  }
267
371
  };
268
372
  }
373
+ var contentTypeOf = (headers) => Object.entries(headers).find(([k]) => k.toLowerCase() === "content-type")?.[1];
269
374
  function responseParts(response) {
270
375
  const r = response;
271
376
  const status = r.status ?? 200;
@@ -378,7 +483,7 @@ function patchFetch(state) {
378
483
  const entry = startEntry(state, {
379
484
  method,
380
485
  url,
381
- requestBody: formatBody(requestBody)
486
+ requestBody
382
487
  });
383
488
  const real = () => {
384
489
  let promise;
@@ -391,14 +496,11 @@ function patchFetch(state) {
391
496
  return promise.then(
392
497
  (res) => {
393
498
  finishEntry(entry, { status: res.status });
394
- const type = res.headers?.get?.("content-type");
499
+ const type = res.headers?.get?.("content-type") ?? void 0;
395
500
  if (isTextual(type) && typeof res.clone === "function") {
396
- res.clone().text().then((text) => {
397
- const body = formatBody(text);
398
- if (body !== void 0) entry.responseBody = body;
399
- }).catch(() => {
400
- });
401
- }
501
+ markResponse(entry, "pending");
502
+ res.clone().text().then((text) => setResponseBody(entry, text, type)).catch(() => markResponse(entry, "none"));
503
+ } else markResponse(entry, "none");
402
504
  return res;
403
505
  },
404
506
  (error) => {
@@ -415,8 +517,8 @@ function patchFetch(state) {
415
517
  finishEntry(entry, { error: `${OFFLINE_MESSAGE} (strict network)` });
416
518
  return Promise.reject(new TypeError(OFFLINE_MESSAGE));
417
519
  }
418
- const { status, text } = responseParts(blocked);
419
- finishEntry(entry, { status, responseBody: formatBody(text) });
520
+ const { status, text, headers } = responseParts(blocked);
521
+ finishEntry(entry, { status, responseBody: text, contentType: contentTypeOf(headers) });
420
522
  return Promise.resolve(mockedResponse(blocked));
421
523
  };
422
524
  const candidates = matchingMocks(state, method, url);
@@ -441,6 +543,7 @@ function patchFetch(state) {
441
543
  return answered.then(async (hit) => {
442
544
  if (!hit) return passOn();
443
545
  entry.mocked = true;
546
+ if (hit.mock.source === "agent") signalMockAnswered(hit.mock.id, entry.id);
444
547
  await sleep(hit.mock.delayMs);
445
548
  if (init?.signal?.aborted) {
446
549
  finishEntry(entry, { error: "aborted" });
@@ -450,8 +553,8 @@ function patchFetch(state) {
450
553
  finishEntry(entry, { error: `${OFFLINE_MESSAGE} (mocked offline)` });
451
554
  throw new TypeError(OFFLINE_MESSAGE);
452
555
  }
453
- const { status, text } = responseParts(hit.response);
454
- finishEntry(entry, { status, responseBody: formatBody(text) });
556
+ const { status, text, headers } = responseParts(hit.response);
557
+ finishEntry(entry, { status, responseBody: text, contentType: contentTypeOf(headers) });
455
558
  return mockedResponse(hit.response);
456
559
  });
457
560
  };
@@ -478,11 +581,14 @@ function track(xhr, entry) {
478
581
  xhr.addEventListener("timeout", () => error = "timed out");
479
582
  xhr.addEventListener("abort", () => error = "aborted");
480
583
  xhr.addEventListener("loadend", () => {
584
+ const text = error ? void 0 : responseTextOf(xhr);
481
585
  finishEntry(entry, {
482
586
  status: error ? void 0 : xhr.status,
483
587
  error,
484
- responseBody: error ? void 0 : formatBody(responseTextOf(xhr))
588
+ responseBody: text,
589
+ contentType: xhr.getResponseHeader?.("content-type") ?? void 0
485
590
  });
591
+ if (!error && text === void 0) markResponse(entry, "none");
486
592
  });
487
593
  }
488
594
  var fakeRequestId = 0;
@@ -569,7 +675,7 @@ function patchXhr(state) {
569
675
  const entry = startEntry(state, {
570
676
  method,
571
677
  url,
572
- requestBody: formatBody(text)
678
+ requestBody: text
573
679
  });
574
680
  const passOn = () => {
575
681
  const blocked = blockedResponse(state, method, url);
@@ -577,11 +683,11 @@ function patchXhr(state) {
577
683
  track(this, entry);
578
684
  return send.call(this, body);
579
685
  }
580
- const { status, text: out } = responseParts(blocked);
686
+ const { status, text: out, headers } = responseParts(blocked);
581
687
  entry.blocked = true;
582
688
  finishEntry(
583
689
  entry,
584
- isOffline(blocked) ? { error: `${OFFLINE_MESSAGE} (strict network)` } : { status, responseBody: formatBody(out) }
690
+ isOffline(blocked) ? { error: `${OFFLINE_MESSAGE} (strict network)` } : { status, responseBody: out, contentType: contentTypeOf(headers) }
585
691
  );
586
692
  void Promise.resolve().then(() => {
587
693
  if (!meta.aborted) respond(this, blocked, url);
@@ -594,16 +700,17 @@ function patchXhr(state) {
594
700
  async (hit) => {
595
701
  if (!hit) return passOn();
596
702
  entry.mocked = true;
703
+ if (hit.mock.source === "agent") signalMockAnswered(hit.mock.id, entry.id);
597
704
  await sleep(hit.mock.delayMs);
598
705
  if (meta.aborted) {
599
706
  finishEntry(entry, { error: "aborted" });
600
707
  return;
601
708
  }
602
709
  const offline = isOffline(hit.response);
603
- const { status, text: out } = responseParts(hit.response);
710
+ const { status, text: out, headers } = responseParts(hit.response);
604
711
  finishEntry(
605
712
  entry,
606
- offline ? { error: `${OFFLINE_MESSAGE} (mocked offline)` } : { status, responseBody: formatBody(out) }
713
+ offline ? { error: `${OFFLINE_MESSAGE} (mocked offline)` } : { status, responseBody: out, contentType: contentTypeOf(headers) }
607
714
  );
608
715
  respond(this, hit.response, url);
609
716
  },
@@ -652,48 +759,141 @@ function strictNetwork(options = {}) {
652
759
  state.strictApp = state.strictApp.filter((r) => r !== rule);
653
760
  };
654
761
  }
655
- var describeMock = (m) => ({
762
+ var escapeRegex = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
763
+ var isPlain = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
764
+ function deepMerge(base, patch) {
765
+ if (!isPlain(base) || !isPlain(patch)) return patch;
766
+ const out = { ...base };
767
+ for (const [key, value] of Object.entries(patch)) out[key] = deepMerge(base[key], value);
768
+ return out;
769
+ }
770
+ var describeMatch = (match) => {
771
+ if (match instanceof RegExp) return String(match);
772
+ if (typeof match === "object" && match.url instanceof RegExp)
773
+ return { ...match, url: String(match.url) };
774
+ return match;
775
+ };
776
+ function describeResponse(response, full) {
777
+ if (typeof response === "function") return { response: "[handler]" };
778
+ if (full || "offline" in response) return { response };
779
+ const out = { ...response };
780
+ let truncated = false;
781
+ if (response.json !== void 0) {
782
+ const text = JSON.stringify(response.json) ?? "";
783
+ const cut = truncateBody(text);
784
+ if (cut !== text) {
785
+ out.json = cut;
786
+ truncated = true;
787
+ }
788
+ }
789
+ if (typeof response.body === "string") {
790
+ const cut = truncateBody(response.body);
791
+ if (cut !== response.body) {
792
+ out.body = cut;
793
+ truncated = true;
794
+ }
795
+ }
796
+ return truncated ? { response: out, truncated } : { response };
797
+ }
798
+ var describeMock = (m, full) => ({
656
799
  id: m.id,
657
800
  source: m.source,
658
- match: m.match instanceof RegExp ? String(m.match) : m.match,
659
- response: typeof m.response === "function" ? "[handler]" : m.response,
801
+ match: describeMatch(m.match),
802
+ ...describeResponse(m.response, full),
660
803
  times: m.times,
661
804
  hits: m.hits,
662
- delayMs: m.delayMs || void 0
805
+ delayMs: m.delayMs || void 0,
806
+ priority: m.priority || void 0
663
807
  });
664
808
  function networkTools(options = {}) {
665
809
  installNetwork();
666
810
  const state = networkState();
667
811
  if (options.skip) state.skip = options.skip;
668
812
  const isAgent = (m) => m.source === "agent";
813
+ const logged = (id) => {
814
+ const entry = state.log.find((e) => e.id === id);
815
+ if (!entry) throw new Error(`No logged request with id ${id}; it may have left the log`);
816
+ return entry;
817
+ };
818
+ const addAgentMock = (match, response, { times, delayMs, priority }) => {
819
+ const added = addMock(state, "agent", match, response, { times, delayMs, priority });
820
+ const shadows = shadowedMocks(state, added).map((m) => m.id);
821
+ return shadows.length ? { id: added.id, shadows } : { id: added.id };
822
+ };
669
823
  return {
670
824
  "net.log": {
671
- description: "Recent requests, newest first: method, url, status, ms, bodies (~2 KB), error, mocked. Filter by url substring or method; clear: true empties the log after reading.",
825
+ description: 'Recent requests, newest first: method, url, startedAt (epoch ms), status, ms, bodies (~2 KB, cut with "\u2026 (+N chars)"), error, mocked. Filter by url substring, method, since (epoch ms: startedAt >= since) or sinceId (id > sinceId); full: true returns whole bodies (truncated: true when one is too large or gone); clear: true empties the log after reading. net.entry gets one request whole.',
672
826
  run: (filter = {}) => {
673
827
  const method = filter.method?.toUpperCase();
828
+ if (filter.since !== void 0 && !(filter.since >= 1e11))
829
+ throw new Error("since is an epoch time in ms (e.g. Date.now()); use sinceId for log ids");
674
830
  const entries = state.log.filter(
675
- (e) => (!filter.url || e.url.includes(filter.url)) && (!method || e.method === method)
676
- ).reverse().slice(0, filter.limit ?? 20).map(publicEntry);
831
+ (e) => (!filter.url || e.url.includes(filter.url)) && (!method || e.method === method) && (filter.since === void 0 || e.startedAt >= filter.since) && (filter.sinceId === void 0 || e.id > filter.sinceId)
832
+ ).reverse().slice(0, filter.limit ?? 20).map((e) => publicEntry(e, filter.full));
677
833
  if (filter.clear) state.log.length = 0;
678
834
  return entries;
679
835
  }
680
836
  },
837
+ "net.entry": {
838
+ description: "One logged request by id, with its request and response bodies whole (not cut at ~2 KB). Ids come from net.log.",
839
+ run: (id) => publicEntry(logged(id), true)
840
+ },
681
841
  "net.mock": {
682
- description: 'Answer matching requests with a canned response until unmocked. match: "/path" or { url: string | { regex }, method? }. response: { status?, json?, body?, headers? } or { offline: true }. options: { times?, delayMs? }.',
842
+ description: 'Answer matching requests with a canned response until unmocked. match: "/path" or { url: string | { regex }, method? }. response: { status?, json?, body?, headers? } or { offline: true }. options: { times?, delayMs?, priority? }. The newest mock is tried first, so a later broad mock shadows an earlier specific one; give the specific one a higher priority (default 0, ties newest first; negative makes a fallback); agent mocks still come before the app mocks. The result lists `shadows` when the new mock will be tried before an agent mock it also matches.',
683
843
  run: (match, response, mockOptions = {}) => {
684
844
  if (typeof response !== "object" || response === null)
685
845
  throw new Error("response must be an object, e.g. { status: 500 }");
686
- const { times, delayMs } = mockOptions;
687
- return { id: addMock(state, "agent", match, response, { times, delayMs }).id };
846
+ return addAgentMock(match, response, mockOptions);
847
+ }
848
+ },
849
+ "net.mockFromLog": {
850
+ description: "Mock the same method and URL as a logged request (id from net.log) with its status and whole response body, optionally changed by patch: JSON objects merge deeply, other values replace. [id, patch?, options?] options as net.mock. Returns { id, match, response }.",
851
+ run: (id, patch, mockOptions = {}) => {
852
+ const entry = logged(id);
853
+ if (entry.status === void 0)
854
+ throw new Error(`Request ${id} has no response to copy (${entry.error ?? "still pending"})`);
855
+ const { state: kept, body, contentType } = fullResponse(entry);
856
+ if (kept !== "kept" || body === void 0)
857
+ throw new Error(
858
+ `Request ${id}'s response body can't be copied: ${{
859
+ pending: "it is still being read, try again",
860
+ none: "it was not captured (binary or unreadable)",
861
+ cut: "it is too large",
862
+ evicted: "it was dropped to save memory",
863
+ kept: "it is missing"
864
+ }[kept]}`
865
+ );
866
+ const response = { status: entry.status };
867
+ if (patch !== void 0) {
868
+ let json;
869
+ try {
870
+ json = JSON.parse(body);
871
+ } catch {
872
+ throw new Error(`Request ${id}'s response is not JSON, so it can't take a patch`);
873
+ }
874
+ response.json = deepMerge(json, patch);
875
+ } else {
876
+ response.body = body;
877
+ if (contentType) response.headers = { "content-type": contentType };
878
+ }
879
+ const match = { url: { regex: `^${escapeRegex(entry.url)}$` }, method: entry.method };
880
+ return { ...addAgentMock(match, response, mockOptions), match: { url: entry.url, method: entry.method }, response };
688
881
  }
689
882
  },
690
883
  "net.mocks": {
691
- description: "Active mocks, agent and app, in the order they are tried.",
692
- run: () => [...state.mocks].sort(mockOrder).map(describeMock)
884
+ description: 'Active mocks, agent and app, in the order they are tried. Response json and body over ~2 KB are cut like net.log (json shown as its cut JSON text, "\u2026 (+N chars)"), with truncated: true; { full: true } returns them whole.',
885
+ run: (options2 = {}) => [...state.mocks].sort(mockOrder).map((m) => describeMock(m, options2.full))
693
886
  },
694
887
  "net.unmock": {
695
- description: "Remove one agent mock by id, or every agent mock when no id. Returns how many.",
696
- run: (id) => removeMocks(state, (m) => isAgent(m) && (id === void 0 || m.id === id))
888
+ description: "Remove one agent mock by id, or every agent mock when no id. Returns how many. With the query adapter, queries that fetched while the mock answered are reset, so its fake data can't outlive it.",
889
+ run: (id) => {
890
+ const removed = state.mocks.filter(
891
+ (m) => isAgent(m) && (id === void 0 || m.id === id)
892
+ );
893
+ const count = removeMocks(state, (m) => removed.includes(m));
894
+ signalMocksRemoved(id === void 0 ? void 0 : [id]);
895
+ return count;
896
+ }
697
897
  },
698
898
  "net.strict": {
699
899
  description: "Strict mode: requests no mock answers fail with a 501 (or like offline) instead of reaching a server. [true | false | { allow?: [substring | { regex }], status?, offline? }] sets it for the agent, [null] follows the app again; no args reads it. Returns { strict, source, allow, blocked }.",
@@ -723,10 +923,13 @@ function networkTools(options = {}) {
723
923
  }
724
924
  },
725
925
  "net.restore": {
726
- description: "Undo the agent's network changes: remove agent mocks and its net.strict setting, keep the app's. Returns how many mocks were removed.",
926
+ pending: () => state.strictAgent !== null || state.mocks.some(isAgent),
927
+ description: "Undo the agent's network changes: remove agent mocks and its net.strict setting, keep the app's. Returns how many mocks were removed. With the query adapter, queries that fetched while an agent mock answered are reset (query.restore reports how many).",
727
928
  run: () => {
728
929
  state.strictAgent = null;
729
- return removeMocks(state, isAgent);
930
+ const count = removeMocks(state, isAgent);
931
+ signalMocksRemoved();
932
+ return count;
730
933
  }
731
934
  }
732
935
  };
@@ -1,4 +1,4 @@
1
- import { T as Tools } from '../types-6kKQsLC1.cjs';
1
+ import { T as Tools } from '../types-BmpykP1P.cjs';
2
2
 
3
3
  /** Which requests a mock answers. A bare string is a URL substring. */
4
4
  type MockMatch = string | {
@@ -38,6 +38,8 @@ type MockOptions = {
38
38
  times?: number;
39
39
  /** Wait before answering. */
40
40
  delayMs?: number;
41
+ /** Higher is tried first among mocks from the same source (agent mocks still come before the app's). Default 0; ties go to the newest. Negative makes a fallback. */
42
+ priority?: number;
41
43
  /** Registering again with the same id replaces the mock (handy with Fast Refresh). */
42
44
  id?: string;
43
45
  };
@@ -50,11 +52,15 @@ type LogEntry = {
50
52
  id: number;
51
53
  method: string;
52
54
  url: string;
55
+ /** When the request started, epoch ms (the app's clock). */
56
+ startedAt: number;
53
57
  status?: number;
54
58
  ms: number;
55
59
  pending?: boolean;
56
60
  requestBody?: string;
57
61
  responseBody?: string;
62
+ /** From net.entry: a body shown is only a preview because the whole one was too large or is gone. */
63
+ truncated?: boolean;
58
64
  error?: string;
59
65
  mocked?: boolean;
60
66
  /** Failed by strict mode: no mock answered it. */
@@ -94,7 +100,8 @@ type MockApiOptions = Omit<MockOptions, 'id'> & {
94
100
  declare function installNetwork(): void;
95
101
  /**
96
102
  * Answers matching requests from the app's own code, e.g. a fake backend.
97
- * Agent mocks (from `net.mock`) take precedence; `net.restore` keeps these.
103
+ * Agent mocks (from `net.mock`) are tried first, whatever their priority;
104
+ * `net.restore` keeps these.
98
105
  */
99
106
  declare function mock(match: MockMatch, response: MockResponse | MockHandler, options?: MockOptions): {
100
107
  id: string;
@@ -22,6 +22,7 @@ var noop_exports = {};
22
22
  __export(noop_exports, {
23
23
  cdpTransport: () => cdpTransport,
24
24
  createGate: () => createGate,
25
+ onRestore: () => onRestore,
25
26
  settle: () => settle,
26
27
  startAgentBridge: () => startAgentBridge,
27
28
  useAgentBridge: () => useAgentBridge
@@ -35,6 +36,8 @@ var useAgentBridge = () => {
35
36
  };
36
37
  var cdpTransport = inert;
37
38
  var settle = () => Promise.resolve({ commits: 0, ms: 0 });
39
+ var onRestore = () => {
40
+ };
38
41
  var createGate = (name) => ({
39
42
  name,
40
43
  closed: false,
@@ -0,0 +1,38 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/noop/react-native-webview.ts
21
+ var react_native_webview_exports = {};
22
+ __export(react_native_webview_exports, {
23
+ useWebViewTools: () => useWebViewTools,
24
+ webviewTools: () => webviewTools
25
+ });
26
+ module.exports = __toCommonJS(react_native_webview_exports);
27
+ var wrap = (onMessage) => onMessage;
28
+ function useWebViewTools(_ref, options) {
29
+ const props = {};
30
+ const { injectedJavaScriptBeforeContentLoaded, onLoadStart, onLoadEnd, onNavigationStateChange } = options;
31
+ if (injectedJavaScriptBeforeContentLoaded !== void 0)
32
+ props.injectedJavaScriptBeforeContentLoaded = injectedJavaScriptBeforeContentLoaded;
33
+ if (onLoadStart) props.onLoadStart = onLoadStart;
34
+ if (onLoadEnd) props.onLoadEnd = onLoadEnd;
35
+ if (onNavigationStateChange) props.onNavigationStateChange = onNavigationStateChange;
36
+ return { props, wrap };
37
+ }
38
+ var webviewTools = () => ({});
@@ -221,7 +221,7 @@ function createRealtimeTap(options = {}) {
221
221
  }
222
222
  },
223
223
  [`${ns}.emit`]: {
224
- description: "Deliver a fake message to every listener on a channel: [channel, message]. Runs the app's own handlers. Returns { id, delivered }.",
224
+ description: "Deliver a fake message to every listener on a channel: [channel, message]. Runs the app's own handlers. Returns { id, delivered }." + (options.emitUsage ? ` ${options.emitUsage}` : ""),
225
225
  run: (channel, ...values) => {
226
226
  const entries = channels.get(channel);
227
227
  if (!entries?.size)
@@ -257,6 +257,7 @@ function createRealtimeTap(options = {}) {
257
257
  run: (channel) => setMute(channel, null)
258
258
  },
259
259
  [`${ns}.restore`]: {
260
+ pending: () => mutes.size > 0 || faked !== null,
260
261
  description: "Undo the agent: unmute every channel and go back to the real connection state.",
261
262
  run: () => {
262
263
  const unmuted = [...mutes.keys()];
@@ -1,4 +1,4 @@
1
- import { T as Tools } from '../types-6kKQsLC1.cjs';
1
+ import { T as Tools } from '../types-BmpykP1P.cjs';
2
2
 
3
3
  /** How a message shows up in `realtime.log`. */
4
4
  type MessageSummary = {
@@ -34,6 +34,8 @@ type RealtimeTapOptions<M> = {
34
34
  channel: string;
35
35
  id: string;
36
36
  }) => M;
37
+ /** What to pass to `realtime.emit` for this library, appended to its description. */
38
+ emitUsage?: string;
37
39
  /** Extra fields for `realtime.channels`, e.g. the library's channel state. */
38
40
  channelInfo?: (channel: string) => Record<string, unknown>;
39
41
  /** Adds `realtime.connection`, to fake connection states. */