@ait-co/devtools 0.2.0 → 0.2.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.
@@ -25685,6 +25685,24 @@ function useT() {
25685
25685
  return t;
25686
25686
  }
25687
25687
  //#endregion
25688
+ //#region src/mock/throttle-registry.ts
25689
+ /**
25690
+ * throttle 레지스트리 (devtools#834).
25691
+ *
25692
+ * 메서드별 마지막 *허용* 호출 시각만 담는 leaf 모듈이다. `state.ts`가 `reset()`에서
25693
+ * 이걸 비워야 하는데, 로직 본체(`throttle.ts`)는 거꾸로 `state.ts`를 읽는다 —
25694
+ * 그래서 **의존이 없는 이 파일로 레지스트리를 분리해** 순환 import를 피한다.
25695
+ */
25696
+ /** 메서드별 마지막 *허용* 호출 시각(ms). */
25697
+ const lastAllowedAt = /* @__PURE__ */ new Map();
25698
+ /**
25699
+ * throttle 레지스트리를 비운다. `aitState.reset()`이 호출한다 — 상태를 되돌렸는데
25700
+ * 직전 세션의 호출 시각이 남아 있으면 첫 호출이 이유 없이 거부된다.
25701
+ */
25702
+ function resetThrottleRegistry() {
25703
+ lastAllowedAt.clear();
25704
+ }
25705
+ //#endregion
25688
25706
  //#region src/mock/state.ts
25689
25707
  /** SDK 호출 로그 ring buffer 상한 */
25690
25708
  const SDK_CALL_LOG_MAX = 200;
@@ -25914,6 +25932,7 @@ var AitStateManager = class {
25914
25932
  ...structuredClone(DEFAULT_STATE),
25915
25933
  deviceId
25916
25934
  };
25935
+ resetThrottleRegistry();
25917
25936
  this._notify();
25918
25937
  }
25919
25938
  _notify() {
@@ -26331,7 +26350,9 @@ function waitForPromptResponse(type) {
26331
26350
  * - 3.x 라인: 같은 실패가 "맨 Error"(위 envelope 필드 없음)로 평탄화된다 —
26332
26351
  * sdk-example#284 "패턴 ① 오류 envelope 평탄화" 표.
26333
26352
  *
26334
- * `src/test-runner/bridge-stub.ts`의 `NativeBridgeErrorShape`/`makeNativeError`가
26353
+ * `@ait-co/debugger`의 `packages/debugger/src/test-runner/bridge-stub.ts`(분리
26354
+ * 이 repo의 `src/test-runner/bridge-stub.ts`)에 있는
26355
+ * `NativeBridgeErrorShape`/`makeNativeError`가
26335
26356
  * 같은 shape을 이미 CI 러너 쪽에서 재현하고 있다(devtools#740) — 이 모듈은 그
26336
26357
  * shape을 mock 소비자 표면(`src/mock/**`)에 대해 재사용 가능한 형태로 옮긴 것이다.
26337
26358
  * 두 모듈은 레이어가 다르다: test-runner 쪽은 env3 blocking-call 인터셉터(빌드에
@@ -26709,6 +26730,50 @@ const _fetchAlbumItems = async (options) => {
26709
26730
  const _fetchAlbumItemsWithPermission = withPermission(_fetchAlbumItems, "photos");
26710
26731
  Object.assign(_fetchAlbumItemsWithPermission, { isSupported: () => true });
26711
26732
  //#endregion
26733
+ //#region src/mock/throttle.ts
26734
+ /**
26735
+ * THROTTLED 시뮬레이션 (devtools#834 — #770 §3 분리).
26736
+ *
26737
+ * 실기기 네이티브 브리지는 같은 메서드를 짧은 간격으로 연타하면
26738
+ * `APP_BRIDGE_THROTTLED`로 거부한다. 이 모듈은 그 per-method rate limit을
26739
+ * env1(mock)에서 **opt-in으로** 재현한다 — 다이얼(`failureModes.throttled`)이
26740
+ * 미설정이면 아무 일도 하지 않으므로 zero behavior change다.
26741
+ *
26742
+ * 설계 메모 두 가지:
26743
+ *
26744
+ * 1. **거부된 호출은 창을 갱신하지 않는다.** 마지막 *허용된* 호출 시각만 기록하므로,
26745
+ * `intervalMs` 안에서 계속 연타해도 창이 밀려나지 않고 최초 허용 시점 기준으로
26746
+ * 풀린다. 네이티브 rate limit의 통상 동작이자, 연타 중 영원히 막히는(창이 계속
26747
+ * 갱신되는) 반-패턴을 피한다.
26748
+ * 2. **`observe()`에 걸지 않는다.** `observe()`는 `fn`을 호출하기 *전에* 감싸므로
26749
+ * 거기서 throw하면 원래 Promise를 반환하는 API가 **동기 throw**로 바뀐다.
26750
+ * `threwSync`는 env1↔env3 동치 diff의 관측 축이라 그 차이가 곧 가짜 불일치가 된다.
26751
+ * 그래서 각 mock 구현 본문 안(= 그 API 자신의 sync/async 계약 안)에 수동 삽입한다.
26752
+ */
26753
+ /**
26754
+ * `method`가 지금 throttle에 걸리면 네이티브 실패 에러를 돌려주고, 아니면
26755
+ * 호출 시각을 기록한 뒤 `undefined`를 돌려준다.
26756
+ *
26757
+ * 반환된 에러를 **호출부가 자신의 계약대로** 흘려보낸다 — async mock은 `throw`
26758
+ * (= reject), 콜백형 mock은 `onError(err)`. 그래서 이 함수 자체는 던지지 않는다.
26759
+ */
26760
+ function throttleErrorFor(method) {
26761
+ const dial = aitState.state.failureModes.throttled;
26762
+ if (!dial?.methods.includes(method)) return void 0;
26763
+ const now = Date.now();
26764
+ const prev = lastAllowedAt.get(method);
26765
+ if (prev !== void 0 && now - prev < dial.intervalMs) return buildNativeError("APP_BRIDGE_THROTTLED");
26766
+ lastAllowedAt.set(method, now);
26767
+ }
26768
+ /**
26769
+ * `throttleErrorFor`의 throw 버전 — async mock 본문에서 한 줄로 쓴다.
26770
+ * async 함수 안에서 던지므로 호출자에게는 rejection으로 도달한다.
26771
+ */
26772
+ function checkThrottle(method) {
26773
+ const err = throttleErrorFor(method);
26774
+ if (err) throw err;
26775
+ }
26776
+ //#endregion
26712
26777
  //#region src/mock/device/clipboard.ts
26713
26778
  /**
26714
26779
  * Clipboard mock
@@ -26716,6 +26781,7 @@ Object.assign(_fetchAlbumItemsWithPermission, { isSupported: () => true });
26716
26781
  */
26717
26782
  const _getClipboardText = async () => {
26718
26783
  checkPermission("clipboard", "getClipboardText");
26784
+ checkThrottle("getClipboardText");
26719
26785
  if (aitState.state.deviceModes.clipboard === "mock") return aitState.state.mockData.clipboardText;
26720
26786
  try {
26721
26787
  return await navigator.clipboard.readText();
@@ -26726,6 +26792,7 @@ const _getClipboardText = async () => {
26726
26792
  withPermission(_getClipboardText, "clipboard");
26727
26793
  const _setClipboardText = async (text) => {
26728
26794
  checkPermission("clipboard", "setClipboardText");
26795
+ checkThrottle("setClipboardText");
26729
26796
  if (aitState.state.deviceModes.clipboard === "mock") {
26730
26797
  aitState.patch("mockData", { clipboardText: text });
26731
26798
  return { text };
@@ -26877,6 +26944,7 @@ async function getCurrentLocationPrompt() {
26877
26944
  }
26878
26945
  const _getCurrentLocation = async (_options) => {
26879
26946
  checkPermission("geolocation", "getCurrentLocation");
26947
+ checkThrottle("getCurrentLocation");
26880
26948
  const mode = aitState.state.deviceModes.location;
26881
26949
  if (mode === "web") return getCurrentLocationWeb();
26882
26950
  if (mode === "prompt") return getCurrentLocationPrompt();
@@ -27344,6 +27412,11 @@ const GoogleAdMob = createMockProxy("GoogleAdMob", {
27344
27412
  args.onError(buildNativeError(failureCode));
27345
27413
  return;
27346
27414
  }
27415
+ const throttleError = throttleErrorFor("loadAppsInTossAdMob");
27416
+ if (throttleError) {
27417
+ args.onError(throttleError);
27418
+ return;
27419
+ }
27347
27420
  if (aitState.state.ads.forceNoFill) {
27348
27421
  args.onError(/* @__PURE__ */ new Error("No fill"));
27349
27422
  return;
@@ -27472,6 +27545,11 @@ const loadFullScreenAd = observe("loadFullScreenAd", "faithful", (args) => {
27472
27545
  args.onError(buildNativeError(failureCode));
27473
27546
  return;
27474
27547
  }
27548
+ const throttleError = throttleErrorFor("loadFullScreenAd");
27549
+ if (throttleError) {
27550
+ args.onError(throttleError);
27551
+ return;
27552
+ }
27475
27553
  if (aitState.state.ads.forceNoFill) {
27476
27554
  args.onError(/* @__PURE__ */ new Error("No fill"));
27477
27555
  return;
@@ -30193,7 +30271,7 @@ function Panel() {
30193
30271
  color: "#666",
30194
30272
  fontWeight: 400
30195
30273
  },
30196
- children: ["v", "0.2.0"]
30274
+ children: ["v", "0.2.1"]
30197
30275
  }),
30198
30276
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", {
30199
30277
  type: "button",