@buoy-gg/network 7.0.13 → 7.0.14

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 (123) hide show
  1. package/lib/commonjs/index.js +39 -0
  2. package/lib/commonjs/network/components/NetworkEventDetailView.js +30 -3
  3. package/lib/commonjs/network/components/NetworkEventItemCompact.js +4 -0
  4. package/lib/commonjs/network/components/NetworkHeaderLive.js +26 -6
  5. package/lib/commonjs/network/components/NetworkHeaderMenu.js +183 -0
  6. package/lib/commonjs/network/components/NetworkListScreen.js +126 -5
  7. package/lib/commonjs/network/components/NetworkModal.js +136 -29
  8. package/lib/commonjs/network/components/NetworkNumberPrompt.js +178 -0
  9. package/lib/commonjs/network/components/NetworkOverrideEditor.js +797 -0
  10. package/lib/commonjs/network/components/NetworkOverrideHeaderButton.js +102 -0
  11. package/lib/commonjs/network/components/NetworkOverridesButton.js +88 -0
  12. package/lib/commonjs/network/components/NetworkOverridesScreen.js +489 -0
  13. package/lib/commonjs/network/components/response-editor/Explorer.js +973 -0
  14. package/lib/commonjs/network/components/response-editor/deleteNestedDataByPath.js +56 -0
  15. package/lib/commonjs/network/components/response-editor/editorContext.js +33 -0
  16. package/lib/commonjs/network/components/response-editor/updateNestedDataByPath.js +61 -0
  17. package/lib/commonjs/network/hooks/useOverrideRules.js +36 -0
  18. package/lib/commonjs/network/overrides/applyToFetch.js +80 -0
  19. package/lib/commonjs/network/overrides/applyToXhr.js +190 -0
  20. package/lib/commonjs/network/overrides/engine.js +70 -0
  21. package/lib/commonjs/network/overrides/matchRule.js +81 -0
  22. package/lib/commonjs/network/overrides/overrideRulesStore.js +550 -0
  23. package/lib/commonjs/network/overrides/presets.js +70 -0
  24. package/lib/commonjs/network/overrides/resolveOverride.js +158 -0
  25. package/lib/commonjs/network/overrides/types.js +85 -0
  26. package/lib/commonjs/network/sync/networkSyncAdapter.js +230 -1
  27. package/lib/commonjs/network/types/index.js +5 -1
  28. package/lib/commonjs/network/utils/networkEventStore.js +52 -6
  29. package/lib/commonjs/network/utils/networkListener.js +90 -10
  30. package/lib/commonjs/preset.js +11 -0
  31. package/lib/module/index.js +15 -0
  32. package/lib/module/network/components/NetworkEventDetailView.js +31 -4
  33. package/lib/module/network/components/NetworkEventItemCompact.js +5 -1
  34. package/lib/module/network/components/NetworkHeaderLive.js +26 -6
  35. package/lib/module/network/components/NetworkHeaderMenu.js +177 -0
  36. package/lib/module/network/components/NetworkListScreen.js +127 -6
  37. package/lib/module/network/components/NetworkModal.js +138 -31
  38. package/lib/module/network/components/NetworkNumberPrompt.js +174 -0
  39. package/lib/module/network/components/NetworkOverrideEditor.js +794 -0
  40. package/lib/module/network/components/NetworkOverrideHeaderButton.js +99 -0
  41. package/lib/module/network/components/NetworkOverridesButton.js +85 -0
  42. package/lib/module/network/components/NetworkOverridesScreen.js +485 -0
  43. package/lib/module/network/components/response-editor/Explorer.js +972 -0
  44. package/lib/module/network/components/response-editor/deleteNestedDataByPath.js +51 -0
  45. package/lib/module/network/components/response-editor/editorContext.js +29 -0
  46. package/lib/module/network/components/response-editor/updateNestedDataByPath.js +56 -0
  47. package/lib/module/network/hooks/useOverrideRules.js +32 -0
  48. package/lib/module/network/overrides/applyToFetch.js +72 -0
  49. package/lib/module/network/overrides/applyToXhr.js +185 -0
  50. package/lib/module/network/overrides/engine.js +64 -0
  51. package/lib/module/network/overrides/matchRule.js +75 -0
  52. package/lib/module/network/overrides/overrideRulesStore.js +547 -0
  53. package/lib/module/network/overrides/presets.js +65 -0
  54. package/lib/module/network/overrides/resolveOverride.js +152 -0
  55. package/lib/module/network/overrides/types.js +80 -0
  56. package/lib/module/network/sync/networkSyncAdapter.js +230 -1
  57. package/lib/module/network/types/index.js +3 -1
  58. package/lib/module/network/utils/networkEventStore.js +52 -6
  59. package/lib/module/network/utils/networkListener.js +91 -10
  60. package/lib/module/preset.js +13 -1
  61. package/lib/typescript/index.d.ts +8 -0
  62. package/lib/typescript/index.d.ts.map +1 -1
  63. package/lib/typescript/network/components/NetworkEventDetailView.d.ts.map +1 -1
  64. package/lib/typescript/network/components/NetworkEventItemCompact.d.ts +6 -0
  65. package/lib/typescript/network/components/NetworkEventItemCompact.d.ts.map +1 -1
  66. package/lib/typescript/network/components/NetworkHeaderLive.d.ts +8 -1
  67. package/lib/typescript/network/components/NetworkHeaderLive.d.ts.map +1 -1
  68. package/lib/typescript/network/components/NetworkHeaderMenu.d.ts +46 -0
  69. package/lib/typescript/network/components/NetworkHeaderMenu.d.ts.map +1 -0
  70. package/lib/typescript/network/components/NetworkListScreen.d.ts.map +1 -1
  71. package/lib/typescript/network/components/NetworkModal.d.ts.map +1 -1
  72. package/lib/typescript/network/components/NetworkNumberPrompt.d.ts +33 -0
  73. package/lib/typescript/network/components/NetworkNumberPrompt.d.ts.map +1 -0
  74. package/lib/typescript/network/components/NetworkOverrideEditor.d.ts +17 -0
  75. package/lib/typescript/network/components/NetworkOverrideEditor.d.ts.map +1 -0
  76. package/lib/typescript/network/components/NetworkOverrideHeaderButton.d.ts +27 -0
  77. package/lib/typescript/network/components/NetworkOverrideHeaderButton.d.ts.map +1 -0
  78. package/lib/typescript/network/components/NetworkOverridesButton.d.ts +17 -0
  79. package/lib/typescript/network/components/NetworkOverridesButton.d.ts.map +1 -0
  80. package/lib/typescript/network/components/NetworkOverridesScreen.d.ts +29 -0
  81. package/lib/typescript/network/components/NetworkOverridesScreen.d.ts.map +1 -0
  82. package/lib/typescript/network/components/response-editor/Explorer.d.ts +17 -0
  83. package/lib/typescript/network/components/response-editor/Explorer.d.ts.map +1 -0
  84. package/lib/typescript/network/components/response-editor/deleteNestedDataByPath.d.ts +9 -0
  85. package/lib/typescript/network/components/response-editor/deleteNestedDataByPath.d.ts.map +1 -0
  86. package/lib/typescript/network/components/response-editor/editorContext.d.ts +29 -0
  87. package/lib/typescript/network/components/response-editor/editorContext.d.ts.map +1 -0
  88. package/lib/typescript/network/components/response-editor/updateNestedDataByPath.d.ts +10 -0
  89. package/lib/typescript/network/components/response-editor/updateNestedDataByPath.d.ts.map +1 -0
  90. package/lib/typescript/network/hooks/useOverrideRules.d.ts +29 -0
  91. package/lib/typescript/network/hooks/useOverrideRules.d.ts.map +1 -0
  92. package/lib/typescript/network/overrides/__tests__/applyToFetch.test.d.ts +16 -0
  93. package/lib/typescript/network/overrides/__tests__/applyToFetch.test.d.ts.map +1 -0
  94. package/lib/typescript/network/overrides/__tests__/applyToXhr.test.d.ts +19 -0
  95. package/lib/typescript/network/overrides/__tests__/applyToXhr.test.d.ts.map +1 -0
  96. package/lib/typescript/network/overrides/__tests__/resolveOverride.test.d.ts +22 -0
  97. package/lib/typescript/network/overrides/__tests__/resolveOverride.test.d.ts.map +1 -0
  98. package/lib/typescript/network/overrides/applyToFetch.d.ts +50 -0
  99. package/lib/typescript/network/overrides/applyToFetch.d.ts.map +1 -0
  100. package/lib/typescript/network/overrides/applyToXhr.d.ts +43 -0
  101. package/lib/typescript/network/overrides/applyToXhr.d.ts.map +1 -0
  102. package/lib/typescript/network/overrides/engine.d.ts +47 -0
  103. package/lib/typescript/network/overrides/engine.d.ts.map +1 -0
  104. package/lib/typescript/network/overrides/matchRule.d.ts +22 -0
  105. package/lib/typescript/network/overrides/matchRule.d.ts.map +1 -0
  106. package/lib/typescript/network/overrides/overrideRulesStore.d.ts +205 -0
  107. package/lib/typescript/network/overrides/overrideRulesStore.d.ts.map +1 -0
  108. package/lib/typescript/network/overrides/presets.d.ts +38 -0
  109. package/lib/typescript/network/overrides/presets.d.ts.map +1 -0
  110. package/lib/typescript/network/overrides/resolveOverride.d.ts +46 -0
  111. package/lib/typescript/network/overrides/resolveOverride.d.ts.map +1 -0
  112. package/lib/typescript/network/overrides/types.d.ts +169 -0
  113. package/lib/typescript/network/overrides/types.d.ts.map +1 -0
  114. package/lib/typescript/network/sync/networkSyncAdapter.d.ts +73 -0
  115. package/lib/typescript/network/sync/networkSyncAdapter.d.ts.map +1 -1
  116. package/lib/typescript/network/types/index.d.ts +10 -0
  117. package/lib/typescript/network/types/index.d.ts.map +1 -1
  118. package/lib/typescript/network/utils/networkEventStore.d.ts +14 -0
  119. package/lib/typescript/network/utils/networkEventStore.d.ts.map +1 -1
  120. package/lib/typescript/network/utils/networkListener.d.ts +3 -0
  121. package/lib/typescript/network/utils/networkListener.d.ts.map +1 -1
  122. package/lib/typescript/preset.d.ts.map +1 -1
  123. package/package.json +2 -2
@@ -0,0 +1,550 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.overrideRulesStore = void 0;
7
+ var _sharedUi = require("@buoy-gg/shared-ui");
8
+ var _networkListener = require("../utils/networkListener");
9
+ var _engine = require("./engine");
10
+ var _resolveOverride = require("./resolveOverride");
11
+ var _types = require("./types");
12
+ /**
13
+ * The override rule list — persisted, and the source of truth for the engine.
14
+ *
15
+ * Shape and persistence follow `networkSavedStore` / `ignoredPatternsStore`:
16
+ * lazy hydrate on first use, a `dirty` flag so a slow read can't clobber a
17
+ * mutation the user just made, and commit = set + emit + persist.
18
+ *
19
+ * Two things here are specific to overrides and worth reading:
20
+ *
21
+ * - `getRules()` is on the HOT PATH. It is called inside the app's own
22
+ * `XMLHttpRequest.send()`, for every request, so it does no work: the master
23
+ * switch collapses to returning a frozen empty array.
24
+ *
25
+ * - `syncKeepAlive()` keeps the interceptor installed while any rule is
26
+ * active. Without it a rule would stop firing the moment the Network tool
27
+ * closed, because the swizzle is reference-counted off capture subscribers.
28
+ */
29
+
30
+ /**
31
+ * A rule as authored. `id`/`createdAt` and the runtime counters (`hits`,
32
+ * `seen`) are the store's business, never the caller's.
33
+ */
34
+
35
+ /**
36
+ * Handlers the desktop dashboard installs to forward mutations to the device.
37
+ *
38
+ * Rules only DO anything on the device — a rule is applied inside the app's own
39
+ * `fetch`/`XMLHttpRequest`, which the dashboard doesn't have. So in remote mode
40
+ * this store becomes a MIRROR: it renders the device's rules and forwards every
41
+ * edit, writing nothing to this machine's storage. Same seam
42
+ * `networkSavedStore` uses for pins.
43
+ */
44
+
45
+ /**
46
+ * How long a local optimistic write suppresses incoming mirrors.
47
+ *
48
+ * An edit is applied locally AND forwarded; the device echoes it back on its
49
+ * next snapshot. Without this window the in-flight snapshot — the one already
50
+ * on the wire when the user tapped — would land first and visibly revert the
51
+ * edit for a frame.
52
+ */
53
+ const REMOTE_ECHO_GRACE_MS = 800;
54
+
55
+ /**
56
+ * Launches-with-rules-on before overrides pause themselves.
57
+ *
58
+ * Persisting rules is the point ("force a 500, restart, watch the boot path"),
59
+ * but a body the app can't render turns that into a brick: the rule reinstates
60
+ * itself on every launch and the controls to remove it live inside an app that
61
+ * no longer draws. Verified in testing — the only exit was deleting the file
62
+ * off the simulator.
63
+ *
64
+ * Three is chosen so the intended workflow survives (force, relaunch, look,
65
+ * relaunch, look) while a wedge cannot outlive the second relaunch. The streak
66
+ * resets whenever a rule is created or edited, so an actively-used rule set
67
+ * never pauses.
68
+ */
69
+ const MAX_UNTOUCHED_LAUNCHES = 3;
70
+ const EMPTY_RULES = Object.freeze([]);
71
+
72
+ /** Hit counts change per request; the UI shouldn't re-render per request. */
73
+ const HIT_EMIT_COALESCE_MS = 250;
74
+ const VALID_KINDS = ["respond", "fail", "delay"];
75
+
76
+ /**
77
+ * Tolerate junk and older shapes rather than throwing the list away.
78
+ *
79
+ * Storage is forever: a shape that throws on parse would take the whole feature
80
+ * down on every launch until the user cleared app data.
81
+ */
82
+ function parseRules(raw) {
83
+ if (!Array.isArray(raw)) return [];
84
+ const parsed = [];
85
+ for (const entry of raw) {
86
+ if (!entry || typeof entry !== "object") continue;
87
+ const rule = entry;
88
+ if (typeof rule.id !== "string" || typeof rule.urlPattern !== "string") {
89
+ continue;
90
+ }
91
+ if (!rule.urlPattern) continue;
92
+ const kind = VALID_KINDS.includes(rule.kind) ? rule.kind : "respond";
93
+ parsed.push({
94
+ id: rule.id,
95
+ enabled: rule.enabled !== false,
96
+ name: typeof rule.name === "string" ? rule.name : undefined,
97
+ urlPattern: rule.urlPattern,
98
+ methods: Array.isArray(rule.methods) ? rule.methods.filter(m => typeof m === "string") : undefined,
99
+ kind,
100
+ status: typeof rule.status === "number" ? Math.min(_types.STATUS_MAX, Math.max(_types.STATUS_MIN, rule.status)) : undefined,
101
+ statusText: typeof rule.statusText === "string" ? rule.statusText : undefined,
102
+ headers: rule.headers && typeof rule.headers === "object" ? rule.headers : undefined,
103
+ body: typeof rule.body === "string" ? rule.body : undefined,
104
+ failKind: rule.failKind === "timeout" ? "timeout" : undefined,
105
+ delayMs: typeof rule.delayMs === "number" ? rule.delayMs : undefined,
106
+ times: typeof rule.times === "number" ? rule.times : null,
107
+ alternate: rule.alternate === true,
108
+ // Runtime-only: a rule that fired 3 times last launch has fired 0 times
109
+ // this one, and a persisted count would make `times` mean "ever" instead
110
+ // of "this session".
111
+ hits: 0,
112
+ seen: 0,
113
+ createdAt: typeof rule.createdAt === "number" ? rule.createdAt : 0
114
+ });
115
+ }
116
+ return parsed.slice(0, _types.MAX_RULES);
117
+ }
118
+ class OverrideRulesStore {
119
+ rules = [];
120
+ enabled = true;
121
+ listeners = new Set();
122
+ loaded = false;
123
+ loadPromise = null;
124
+ /** Set once a mutation happens so a slow initial load can't clobber it. */
125
+ dirty = false;
126
+ idCounter = 0;
127
+ state = {
128
+ enabled: true,
129
+ rules: [],
130
+ autoPaused: false
131
+ };
132
+ /** Consecutive launches that began with rules armed and nobody touching them. */
133
+ launchStreak = 0;
134
+ autoPaused = false;
135
+ /** Held while any rule is active — see `syncKeepAlive`. */
136
+ keepAlive = null;
137
+ hitEmitTimer = null;
138
+ /** Set on the desktop dashboard — see `disableEngine`. */
139
+ engineDisabled = false;
140
+ /** Set on the desktop dashboard — see `OverrideRulesRemote`. */
141
+ remote = null;
142
+ remoteEchoUntil = 0;
143
+
144
+ // ===========================================================================
145
+ // REMOTE MIRROR (desktop dashboard)
146
+ // ===========================================================================
147
+
148
+ /**
149
+ * Turn this store into a mirror of a connected device's rules. Edits are
150
+ * applied locally for immediate feedback AND forwarded to the device, which
151
+ * is the source of truth; nothing is persisted to this machine.
152
+ */
153
+ enableRemoteMode(remote) {
154
+ this.remote = remote;
155
+ // A dashboard must never run rules against its own traffic.
156
+ this.disableEngine();
157
+ }
158
+ disableRemoteMode() {
159
+ this.remote = null;
160
+ }
161
+
162
+ /**
163
+ * Feed the device's rules in (mirror mode only). `force` bypasses the echo
164
+ * guard — used when tearing the mirror down, where there is no later snapshot
165
+ * to converge on.
166
+ */
167
+ replaceRules(state, force = false) {
168
+ if (!this.remote) return;
169
+ // A local edit is still in flight — the snapshot on the wire predates it
170
+ // and would flicker the change away. Drop this one; the next carries the
171
+ // device's real answer.
172
+ if (!force && Date.now() < this.remoteEchoUntil) return;
173
+ this.enabled = state.enabled;
174
+ this.rules = state.rules;
175
+ this.rebuild();
176
+ this.emit();
177
+ }
178
+
179
+ /** Mark a local optimistic write so the next in-flight mirror is ignored. */
180
+ markLocalEdit() {
181
+ if (this.remote) this.remoteEchoUntil = Date.now() + REMOTE_ECHO_GRACE_MS;
182
+ }
183
+
184
+ /**
185
+ * Never intercept traffic in this runtime.
186
+ *
187
+ * The desktop dashboard renders the real tool (`NetworkToolView`), so this
188
+ * module is imported there too — but the dashboard is a VIEWER. Its job is to
189
+ * drive rules on the connected device, never to swizzle its own `fetch` and
190
+ * override its own requests. The same seam `networkEventStore.disableCapture()`
191
+ * uses, for the same reason.
192
+ */
193
+ disableEngine() {
194
+ this.engineDisabled = true;
195
+ if (this.keepAlive) {
196
+ this.keepAlive();
197
+ this.keepAlive = null;
198
+ if ((0, _networkListener.getNetworkListenerCount)() === 0) (0, _networkListener.stopNetworkListener)();
199
+ }
200
+ }
201
+ getState() {
202
+ return this.state;
203
+ }
204
+
205
+ /**
206
+ * The hot path — called inside `send()` for every request the app makes.
207
+ * Returns a frozen empty array when the master switch is off so the caller
208
+ * needs no second check.
209
+ */
210
+ getRules() {
211
+ if (this.engineDisabled || !this.enabled) return EMPTY_RULES;
212
+ return this.rules;
213
+ }
214
+
215
+ /**
216
+ * The rule's real body, fetching it from the device when this surface only
217
+ * has the stripped copy (see `OverrideRule.bodyOmitted`).
218
+ *
219
+ * On the device itself every body is already here, so this resolves
220
+ * immediately and the editor never notices the difference.
221
+ */
222
+ async fetchRuleBody(id) {
223
+ const rule = this.rules.find(candidate => candidate.id === id);
224
+ if (!rule) return null;
225
+ if (!rule.bodyOmitted) return rule.body ?? null;
226
+ if (!this.remote?.getRuleBody) return null;
227
+ try {
228
+ return await this.remote.getRuleBody(id);
229
+ } catch {
230
+ // The editor keeps the empty body and the developer can retype it —
231
+ // better than an error state over a payload they can still replace.
232
+ return null;
233
+ }
234
+ }
235
+
236
+ /** Rules that would actually fire — drives the "overrides active" banner. */
237
+ activeCount() {
238
+ if (!this.enabled) return 0;
239
+ return this.rules.filter(rule => rule.enabled && !(0, _resolveOverride.isSpent)(rule)).length;
240
+ }
241
+ subscribe(listener) {
242
+ this.listeners.add(listener);
243
+ void this.ensureLoaded();
244
+ return () => {
245
+ this.listeners.delete(listener);
246
+ };
247
+ }
248
+
249
+ // ===========================================================================
250
+ // MUTATIONS
251
+ // ===========================================================================
252
+
253
+ setEnabled(enabled) {
254
+ if (this.enabled === enabled) return;
255
+ this.remote?.setEnabled(enabled);
256
+ this.markLocalEdit();
257
+ this.markTouched();
258
+ this.enabled = enabled;
259
+ this.commit(this.rules);
260
+ }
261
+ addRule(draft) {
262
+ if (this.rules.length >= _types.MAX_RULES) return null;
263
+ const rule = {
264
+ ...draft,
265
+ id: `ovr_${Date.now().toString(36)}_${++this.idCounter}`,
266
+ hits: 0,
267
+ seen: 0,
268
+ createdAt: Date.now()
269
+ };
270
+ // Newest first: a rule you just made should win over an older, broader one,
271
+ // because first-match wins and you almost certainly meant the new one.
272
+ this.commit([rule, ...this.rules]);
273
+ return rule;
274
+ }
275
+
276
+ /**
277
+ * Create or replace a rule in one call.
278
+ *
279
+ * THE ONLY WRITE PATH THE UI SHOULD USE. `addRule`/`updateRule` are the
280
+ * primitives underneath and deliberately do NOT forward to the device — if
281
+ * the UI called them directly, an edit made on the dashboard would apply to
282
+ * the mirror and never reach the phone, which looks exactly like it worked.
283
+ *
284
+ * Returns the rule, or null when the list is full.
285
+ */
286
+ upsertRule(draft) {
287
+ this.remote?.upsertRule(draft);
288
+ this.markLocalEdit();
289
+ this.markTouched();
290
+ const {
291
+ id,
292
+ ...rest
293
+ } = draft;
294
+ if (id && this.rules.some(rule => rule.id === id)) {
295
+ this.updateRule(id, rest);
296
+ return this.rules.find(rule => rule.id === id) ?? null;
297
+ }
298
+ return this.addRule(rest);
299
+ }
300
+ updateRule(id, patch) {
301
+ const index = this.rules.findIndex(rule => rule.id === id);
302
+ if (index === -1) return;
303
+ const next = [...this.rules];
304
+ // Editing a rule resets its budget — otherwise a spent "fail once" rule
305
+ // stays silently dead after you change it and looks broken.
306
+ next[index] = {
307
+ ...next[index],
308
+ ...patch,
309
+ hits: 0,
310
+ seen: 0
311
+ };
312
+ this.commit(next);
313
+ }
314
+ setRuleEnabled(id, enabled) {
315
+ const index = this.rules.findIndex(rule => rule.id === id);
316
+ if (index === -1 || this.rules[index].enabled === enabled) return;
317
+ this.remote?.setRuleEnabled(id, enabled);
318
+ this.markLocalEdit();
319
+ this.markTouched();
320
+ const next = [...this.rules];
321
+ // Re-enabling a spent rule has to clear its budget too, or the toggle
322
+ // appears to do nothing.
323
+ next[index] = {
324
+ ...next[index],
325
+ enabled,
326
+ hits: enabled ? 0 : next[index].hits,
327
+ seen: enabled ? 0 : next[index].seen
328
+ };
329
+ this.commit(next);
330
+ }
331
+ removeRule(id) {
332
+ const next = this.rules.filter(rule => rule.id !== id);
333
+ if (next.length === this.rules.length) return;
334
+ this.remote?.removeRule(id);
335
+ this.markLocalEdit();
336
+ this.markTouched();
337
+ this.commit(next);
338
+ }
339
+ clearRules() {
340
+ if (this.rules.length === 0) return;
341
+ this.remote?.clearRules();
342
+ this.markLocalEdit();
343
+ this.markTouched();
344
+ this.commit([]);
345
+ }
346
+
347
+ /**
348
+ * One rule matched one request.
349
+ *
350
+ * Replaces the rule object rather than mutating it so memoized rows actually
351
+ * re-render, and coalesces the notification — this runs at request rate.
352
+ */
353
+ /**
354
+ * One rule matched one request, applied or not.
355
+ *
356
+ * Deliberately silent — this fires on requests an alternating rule lets
357
+ * through, where nothing visible changed. `recordHit` owns the notification.
358
+ */
359
+ recordMatch(ruleId) {
360
+ const index = this.rules.findIndex(rule => rule.id === ruleId);
361
+ if (index === -1) return;
362
+ const next = [...this.rules];
363
+ next[index] = {
364
+ ...next[index],
365
+ seen: next[index].seen + 1
366
+ };
367
+ this.rules = next;
368
+ this.rebuild();
369
+ }
370
+ recordHit(ruleId) {
371
+ const index = this.rules.findIndex(rule => rule.id === ruleId);
372
+ if (index === -1) return;
373
+ const rule = this.rules[index];
374
+ const hits = rule.hits + 1;
375
+ const spent = typeof rule.times === "number" && hits >= rule.times;
376
+ const next = [...this.rules];
377
+ next[index] = {
378
+ ...rule,
379
+ hits,
380
+ enabled: spent ? false : rule.enabled
381
+ };
382
+ if (spent) {
383
+ // Auto-expiry is a real state change (and releases the keep-alive), so it
384
+ // commits immediately instead of waiting on the coalesce window.
385
+ this.commit(next);
386
+ return;
387
+ }
388
+ this.rules = next;
389
+ this.rebuild();
390
+ this.scheduleHitEmit();
391
+ }
392
+
393
+ // ===========================================================================
394
+ // LIFECYCLE
395
+ // ===========================================================================
396
+
397
+ /**
398
+ * Hydrate from storage. Safe to call repeatedly; only the first call reads.
399
+ *
400
+ * Called at package import (see `preset.tsx`) rather than on first subscribe,
401
+ * because a rule has to fire from app boot — with the Network tool closed and
402
+ * nothing capturing.
403
+ */
404
+ ensureLoaded() {
405
+ // Mirror mode reads the device's rules, never this machine's storage.
406
+ if (this.remote) return Promise.resolve();
407
+ if (this.loaded) return Promise.resolve();
408
+ if (this.loadPromise) return this.loadPromise;
409
+ this.loadPromise = (async () => {
410
+ try {
411
+ const stored = await _sharedUi.persistentStorage.getItem(_sharedUi.devToolsStorageKeys.network.overrides());
412
+ // A rule may have been created while this read was in flight — never
413
+ // overwrite user intent with older disk state.
414
+ if (stored && !this.dirty) {
415
+ const parsed = JSON.parse(stored);
416
+ this.enabled = parsed?.enabled !== false;
417
+ this.rules = parseRules(parsed?.rules);
418
+ this.launchStreak = typeof parsed?.launchStreak === "number" ? parsed.launchStreak : 0;
419
+ this.applyLaunchGuard();
420
+ this.rebuild();
421
+ }
422
+ } catch {
423
+ // Corrupt or unreadable — start empty rather than taking the tool down.
424
+ } finally {
425
+ this.loaded = true;
426
+ this.syncKeepAlive();
427
+ this.emit();
428
+ }
429
+ })();
430
+ return this.loadPromise;
431
+ }
432
+
433
+ /**
434
+ * Count this launch, and pause overrides if they've been armed and untouched
435
+ * for too many in a row. See MAX_UNTOUCHED_LAUNCHES for why.
436
+ */
437
+ applyLaunchGuard() {
438
+ if (!this.enabled || this.activeCount() === 0) {
439
+ this.launchStreak = 0;
440
+ return;
441
+ }
442
+ this.launchStreak += 1;
443
+ if (this.launchStreak >= MAX_UNTOUCHED_LAUNCHES) {
444
+ this.enabled = false;
445
+ this.autoPaused = true;
446
+ this.launchStreak = 0;
447
+ }
448
+ this.persist();
449
+ }
450
+
451
+ /**
452
+ * Keep the interceptor installed for as long as a rule could fire.
453
+ *
454
+ * The swizzle is reference-counted off `networkListener`'s listener count
455
+ * (the event store adds one while capturing). Holding a no-op listener reuses
456
+ * that existing lifecycle instead of inventing a second one.
457
+ */
458
+ syncKeepAlive() {
459
+ const needed = !this.engineDisabled && this.activeCount() > 0;
460
+ if (needed && !this.keepAlive) {
461
+ (0, _networkListener.startNetworkListener)();
462
+ this.keepAlive = (0, _networkListener.addNetworkListener)(() => {});
463
+ return;
464
+ }
465
+ if (!needed && this.keepAlive) {
466
+ this.keepAlive();
467
+ this.keepAlive = null;
468
+ // Only tear the swizzle down if nothing else wants it — the Network tool
469
+ // may well still be open and capturing.
470
+ if ((0, _networkListener.getNetworkListenerCount)() === 0) (0, _networkListener.stopNetworkListener)();
471
+ }
472
+ }
473
+ scheduleHitEmit() {
474
+ if (this.hitEmitTimer !== null) return;
475
+ this.hitEmitTimer = setTimeout(() => {
476
+ this.hitEmitTimer = null;
477
+ this.emit();
478
+ }, HIT_EMIT_COALESCE_MS);
479
+ }
480
+ commit(next) {
481
+ this.dirty = true;
482
+ this.rules = next.slice(0, _types.MAX_RULES);
483
+ this.rebuild();
484
+ this.syncKeepAlive();
485
+ this.emit();
486
+ this.persist();
487
+ }
488
+
489
+ /** Rebuild the derived view once per mutation so renders get stable refs. */
490
+ rebuild() {
491
+ this.state = {
492
+ enabled: this.enabled,
493
+ rules: this.rules,
494
+ autoPaused: this.autoPaused
495
+ };
496
+ }
497
+
498
+ /**
499
+ * The developer is actively working with the rules, so the launch guard has
500
+ * nothing to protect them from — reset it and clear any auto-pause notice.
501
+ */
502
+ markTouched() {
503
+ this.launchStreak = 0;
504
+ this.autoPaused = false;
505
+ }
506
+
507
+ /**
508
+ * The developer reached the override UI, which is proof the app still runs.
509
+ *
510
+ * The launch guard exists to escape a WEDGE — a body the app can't render,
511
+ * where the controls are unreachable because nothing draws. If this call
512
+ * happens, that isn't the situation, so the streak has nothing to protect
513
+ * against. Without it the guard nags anyone who legitimately keeps a rule
514
+ * armed for days, which is a real workflow ("force /checkout to 500 every
515
+ * morning") and not the failure mode it was built for.
516
+ */
517
+ noteHealthyLaunch() {
518
+ if (this.launchStreak === 0 && !this.autoPaused) return;
519
+ this.markTouched();
520
+ this.persist();
521
+ this.rebuild();
522
+ this.emit();
523
+ }
524
+ persist() {
525
+ // The dashboard must not write the device's rules into its own storage —
526
+ // they'd outlive the connection and shadow the next device.
527
+ if (this.remote) return;
528
+ _sharedUi.persistentStorage.setItem(_sharedUi.devToolsStorageKeys.network.overrides(), JSON.stringify({
529
+ enabled: this.enabled,
530
+ rules: this.rules,
531
+ launchStreak: this.launchStreak
532
+ })).catch(() => {
533
+ // Silently fail — rules remain in memory for this session.
534
+ });
535
+ }
536
+ emit() {
537
+ this.listeners.forEach(listener => listener());
538
+ }
539
+ }
540
+
541
+ /** Singleton — one rule list per JS runtime. */
542
+ const overrideRulesStore = exports.overrideRulesStore = new OverrideRulesStore();
543
+
544
+ // Register with the engine so `networkListener` can reach the rules without
545
+ // importing this module (which imports the listener — see engine.ts).
546
+ (0, _engine.setOverrideHooks)({
547
+ getRules: () => overrideRulesStore.getRules(),
548
+ recordHit: ruleId => overrideRulesStore.recordHit(ruleId),
549
+ recordMatch: ruleId => overrideRulesStore.recordMatch(ruleId)
550
+ });
@@ -0,0 +1,70 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.draftFromEvent = draftFromEvent;
7
+ exports.patternForUrl = patternForUrl;
8
+ /**
9
+ * Turning a captured request into a rule draft.
10
+ *
11
+ * Chrome's flow assumes a keyboard, a file tree and a second monitor. On a
12
+ * phone the equivalent has to start from the request you're already looking at,
13
+ * which is what this file is for — the editor never opens on a blank field.
14
+ *
15
+ * There used to be a list of one-tap presets here (Force 500, Empty list,
16
+ * Offline…) rendered as chips. They're gone: the editor's outcome grid says the
17
+ * same things in the same words, so keeping both meant two lists to maintain
18
+ * and two places to look for the same choice.
19
+ */
20
+
21
+ /**
22
+ * Turn a captured URL into a pattern that will match the request AGAIN.
23
+ *
24
+ * The query string is dropped and replaced with `*`, which is not a
25
+ * simplification — it's the difference between a rule that works and one that
26
+ * silently never fires. Plenty of clients cache-bust
27
+ * (`?timestamp=1785783081234`), carry a request id, or page; pinning the exact
28
+ * URL means the next call to the same endpoint doesn't match. Found exactly
29
+ * that way: an "Offline" preset on a Pokémon request sat at 0 hits because the
30
+ * app appends a timestamp to every call.
31
+ *
32
+ * The path stays exact, so this still means "this endpoint" and not "this
33
+ * host" — and the editor shows the pattern, so narrowing it back down is one
34
+ * edit away.
35
+ */
36
+ function patternForUrl(url) {
37
+ const query = url.indexOf("?");
38
+ return query === -1 ? url : `${url.slice(0, query)}*`;
39
+ }
40
+
41
+ /** Render a captured body as editable text. */
42
+ function bodyToText(data) {
43
+ if (data == null) return "";
44
+ if (typeof data === "string") return data;
45
+ try {
46
+ return JSON.stringify(data, null, 2);
47
+ } catch {
48
+ return "";
49
+ }
50
+ }
51
+
52
+ /**
53
+ * Prefill the rule editor from a captured request.
54
+ *
55
+ * Seeds the REAL response as the body so the common edit — change two fields of
56
+ * a payload you just saw — starts from something true rather than from nothing.
57
+ */
58
+ function draftFromEvent(event) {
59
+ return {
60
+ enabled: true,
61
+ name: undefined,
62
+ urlPattern: patternForUrl(event.url),
63
+ methods: [String(event.method || "GET").toUpperCase()],
64
+ kind: "respond",
65
+ status: event.status && event.status >= 200 ? event.status : 200,
66
+ statusText: event.statusText,
67
+ body: bodyToText(event.responseData),
68
+ times: null
69
+ };
70
+ }