@ait-co/devtools 0.2.0 → 0.2.2
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.
- package/README.en.md +14 -1
- package/README.md +3 -0
- package/dist/mock/index.d.ts +17 -2
- package/dist/mock/index.d.ts.map +1 -1
- package/dist/mock/index.js +91 -2
- package/dist/mock/index.js.map +1 -1
- package/dist/panel/index.js +90 -2
- package/dist/panel/index.js.map +1 -1
- package/dist/{relay-url-store-DLjlvMSA.cjs → relay-url-store-CkVSQZMq.cjs} +4 -2
- package/dist/relay-url-store-CkVSQZMq.cjs.map +1 -0
- package/dist/{relay-url-store-CPZAn-T5.js → relay-url-store-dkII-DHD.js} +4 -2
- package/dist/relay-url-store-dkII-DHD.js.map +1 -0
- package/dist/unplugin/index.cjs +1 -1
- package/dist/unplugin/index.cjs.map +1 -1
- package/dist/unplugin/index.d.cts +4 -2
- package/dist/unplugin/index.d.cts.map +1 -1
- package/dist/unplugin/index.d.ts +4 -2
- package/dist/unplugin/index.d.ts.map +1 -1
- package/dist/unplugin/index.js +1 -1
- package/dist/unplugin/index.js.map +1 -1
- package/package.json +1 -1
- package/dist/relay-url-store-CPZAn-T5.js.map +0 -1
- package/dist/relay-url-store-DLjlvMSA.cjs.map +0 -1
package/dist/panel/index.js
CHANGED
|
@@ -25685,6 +25685,34 @@ 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
|
+
* `state.ts`의 `AitStateManager`와 같은 이유로 `globalThis` 싱글턴이어야 한다 (#836):
|
|
25697
|
+
* `tsdown.config.ts`가 mock/panel/unplugin entry를 각각 self-contained로 빌드하므로,
|
|
25698
|
+
* 소비자가 두 entry를 동시에 import하면 이 모듈이 entry당 하나씩 복제된다. Map을
|
|
25699
|
+
* 모듈 지역 변수로 두면 `aitState.reset()`이 비우는 Map(싱글턴을 먼저 생성한 번들의 것)과
|
|
25700
|
+
* `throttleErrorFor`가 읽는 Map(mock 번들의 것)이 갈려, 리셋 직후 첫 호출이 낡은
|
|
25701
|
+
* 타임스탬프 때문에 거부된다 — 이 파일이 막으려던 바로 그 증상이다.
|
|
25702
|
+
*/
|
|
25703
|
+
const SINGLETON_KEY$1 = "__aitDevtoolsThrottleRegistry__";
|
|
25704
|
+
const globalRef$1 = globalThis;
|
|
25705
|
+
if (!globalRef$1[SINGLETON_KEY$1]) globalRef$1[SINGLETON_KEY$1] = /* @__PURE__ */ new Map();
|
|
25706
|
+
/** 메서드별 마지막 *허용* 호출 시각(ms). 페이지 안의 모든 entry가 공유한다. */
|
|
25707
|
+
const lastAllowedAt = globalRef$1[SINGLETON_KEY$1];
|
|
25708
|
+
/**
|
|
25709
|
+
* throttle 레지스트리를 비운다. `aitState.reset()`이 호출한다 — 상태를 되돌렸는데
|
|
25710
|
+
* 직전 세션의 호출 시각이 남아 있으면 첫 호출이 이유 없이 거부된다.
|
|
25711
|
+
*/
|
|
25712
|
+
function resetThrottleRegistry() {
|
|
25713
|
+
lastAllowedAt.clear();
|
|
25714
|
+
}
|
|
25715
|
+
//#endregion
|
|
25688
25716
|
//#region src/mock/state.ts
|
|
25689
25717
|
/** SDK 호출 로그 ring buffer 상한 */
|
|
25690
25718
|
const SDK_CALL_LOG_MAX = 200;
|
|
@@ -25914,6 +25942,7 @@ var AitStateManager = class {
|
|
|
25914
25942
|
...structuredClone(DEFAULT_STATE),
|
|
25915
25943
|
deviceId
|
|
25916
25944
|
};
|
|
25945
|
+
resetThrottleRegistry();
|
|
25917
25946
|
this._notify();
|
|
25918
25947
|
}
|
|
25919
25948
|
_notify() {
|
|
@@ -26331,7 +26360,9 @@ function waitForPromptResponse(type) {
|
|
|
26331
26360
|
* - 3.x 라인: 같은 실패가 "맨 Error"(위 envelope 필드 없음)로 평탄화된다 —
|
|
26332
26361
|
* sdk-example#284 "패턴 ① 오류 envelope 평탄화" 표.
|
|
26333
26362
|
*
|
|
26334
|
-
* `src/test-runner/bridge-stub.ts
|
|
26363
|
+
* `@ait-co/debugger`의 `packages/debugger/src/test-runner/bridge-stub.ts`(분리 전
|
|
26364
|
+
* 이 repo의 `src/test-runner/bridge-stub.ts`)에 있는
|
|
26365
|
+
* `NativeBridgeErrorShape`/`makeNativeError`가
|
|
26335
26366
|
* 같은 shape을 이미 CI 러너 쪽에서 재현하고 있다(devtools#740) — 이 모듈은 그
|
|
26336
26367
|
* shape을 mock 소비자 표면(`src/mock/**`)에 대해 재사용 가능한 형태로 옮긴 것이다.
|
|
26337
26368
|
* 두 모듈은 레이어가 다르다: test-runner 쪽은 env3 blocking-call 인터셉터(빌드에
|
|
@@ -26709,6 +26740,50 @@ const _fetchAlbumItems = async (options) => {
|
|
|
26709
26740
|
const _fetchAlbumItemsWithPermission = withPermission(_fetchAlbumItems, "photos");
|
|
26710
26741
|
Object.assign(_fetchAlbumItemsWithPermission, { isSupported: () => true });
|
|
26711
26742
|
//#endregion
|
|
26743
|
+
//#region src/mock/throttle.ts
|
|
26744
|
+
/**
|
|
26745
|
+
* THROTTLED 시뮬레이션 (devtools#834 — #770 §3 분리).
|
|
26746
|
+
*
|
|
26747
|
+
* 실기기 네이티브 브리지는 같은 메서드를 짧은 간격으로 연타하면
|
|
26748
|
+
* `APP_BRIDGE_THROTTLED`로 거부한다. 이 모듈은 그 per-method rate limit을
|
|
26749
|
+
* env1(mock)에서 **opt-in으로** 재현한다 — 다이얼(`failureModes.throttled`)이
|
|
26750
|
+
* 미설정이면 아무 일도 하지 않으므로 zero behavior change다.
|
|
26751
|
+
*
|
|
26752
|
+
* 설계 메모 두 가지:
|
|
26753
|
+
*
|
|
26754
|
+
* 1. **거부된 호출은 창을 갱신하지 않는다.** 마지막 *허용된* 호출 시각만 기록하므로,
|
|
26755
|
+
* `intervalMs` 안에서 계속 연타해도 창이 밀려나지 않고 최초 허용 시점 기준으로
|
|
26756
|
+
* 풀린다. 네이티브 rate limit의 통상 동작이자, 연타 중 영원히 막히는(창이 계속
|
|
26757
|
+
* 갱신되는) 반-패턴을 피한다.
|
|
26758
|
+
* 2. **`observe()`에 걸지 않는다.** `observe()`는 `fn`을 호출하기 *전에* 감싸므로
|
|
26759
|
+
* 거기서 throw하면 원래 Promise를 반환하는 API가 **동기 throw**로 바뀐다.
|
|
26760
|
+
* `threwSync`는 env1↔env3 동치 diff의 관측 축이라 그 차이가 곧 가짜 불일치가 된다.
|
|
26761
|
+
* 그래서 각 mock 구현 본문 안(= 그 API 자신의 sync/async 계약 안)에 수동 삽입한다.
|
|
26762
|
+
*/
|
|
26763
|
+
/**
|
|
26764
|
+
* `method`가 지금 throttle에 걸리면 네이티브 실패 에러를 돌려주고, 아니면
|
|
26765
|
+
* 호출 시각을 기록한 뒤 `undefined`를 돌려준다.
|
|
26766
|
+
*
|
|
26767
|
+
* 반환된 에러를 **호출부가 자신의 계약대로** 흘려보낸다 — async mock은 `throw`
|
|
26768
|
+
* (= reject), 콜백형 mock은 `onError(err)`. 그래서 이 함수 자체는 던지지 않는다.
|
|
26769
|
+
*/
|
|
26770
|
+
function throttleErrorFor(method) {
|
|
26771
|
+
const dial = aitState.state.failureModes.throttled;
|
|
26772
|
+
if (!dial?.methods?.includes(method)) return void 0;
|
|
26773
|
+
const now = Date.now();
|
|
26774
|
+
const prev = lastAllowedAt.get(method);
|
|
26775
|
+
if (prev !== void 0 && now - prev < dial.intervalMs) return buildNativeError("APP_BRIDGE_THROTTLED");
|
|
26776
|
+
lastAllowedAt.set(method, now);
|
|
26777
|
+
}
|
|
26778
|
+
/**
|
|
26779
|
+
* `throttleErrorFor`의 throw 버전 — async mock 본문에서 한 줄로 쓴다.
|
|
26780
|
+
* async 함수 안에서 던지므로 호출자에게는 rejection으로 도달한다.
|
|
26781
|
+
*/
|
|
26782
|
+
function checkThrottle(method) {
|
|
26783
|
+
const err = throttleErrorFor(method);
|
|
26784
|
+
if (err) throw err;
|
|
26785
|
+
}
|
|
26786
|
+
//#endregion
|
|
26712
26787
|
//#region src/mock/device/clipboard.ts
|
|
26713
26788
|
/**
|
|
26714
26789
|
* Clipboard mock
|
|
@@ -26716,6 +26791,7 @@ Object.assign(_fetchAlbumItemsWithPermission, { isSupported: () => true });
|
|
|
26716
26791
|
*/
|
|
26717
26792
|
const _getClipboardText = async () => {
|
|
26718
26793
|
checkPermission("clipboard", "getClipboardText");
|
|
26794
|
+
checkThrottle("getClipboardText");
|
|
26719
26795
|
if (aitState.state.deviceModes.clipboard === "mock") return aitState.state.mockData.clipboardText;
|
|
26720
26796
|
try {
|
|
26721
26797
|
return await navigator.clipboard.readText();
|
|
@@ -26726,6 +26802,7 @@ const _getClipboardText = async () => {
|
|
|
26726
26802
|
withPermission(_getClipboardText, "clipboard");
|
|
26727
26803
|
const _setClipboardText = async (text) => {
|
|
26728
26804
|
checkPermission("clipboard", "setClipboardText");
|
|
26805
|
+
checkThrottle("setClipboardText");
|
|
26729
26806
|
if (aitState.state.deviceModes.clipboard === "mock") {
|
|
26730
26807
|
aitState.patch("mockData", { clipboardText: text });
|
|
26731
26808
|
return { text };
|
|
@@ -26877,6 +26954,7 @@ async function getCurrentLocationPrompt() {
|
|
|
26877
26954
|
}
|
|
26878
26955
|
const _getCurrentLocation = async (_options) => {
|
|
26879
26956
|
checkPermission("geolocation", "getCurrentLocation");
|
|
26957
|
+
checkThrottle("getCurrentLocation");
|
|
26880
26958
|
const mode = aitState.state.deviceModes.location;
|
|
26881
26959
|
if (mode === "web") return getCurrentLocationWeb();
|
|
26882
26960
|
if (mode === "prompt") return getCurrentLocationPrompt();
|
|
@@ -27344,6 +27422,11 @@ const GoogleAdMob = createMockProxy("GoogleAdMob", {
|
|
|
27344
27422
|
args.onError(buildNativeError(failureCode));
|
|
27345
27423
|
return;
|
|
27346
27424
|
}
|
|
27425
|
+
const throttleError = throttleErrorFor("loadAppsInTossAdMob");
|
|
27426
|
+
if (throttleError) {
|
|
27427
|
+
args.onError(throttleError);
|
|
27428
|
+
return;
|
|
27429
|
+
}
|
|
27347
27430
|
if (aitState.state.ads.forceNoFill) {
|
|
27348
27431
|
args.onError(/* @__PURE__ */ new Error("No fill"));
|
|
27349
27432
|
return;
|
|
@@ -27472,6 +27555,11 @@ const loadFullScreenAd = observe("loadFullScreenAd", "faithful", (args) => {
|
|
|
27472
27555
|
args.onError(buildNativeError(failureCode));
|
|
27473
27556
|
return;
|
|
27474
27557
|
}
|
|
27558
|
+
const throttleError = throttleErrorFor("loadFullScreenAd");
|
|
27559
|
+
if (throttleError) {
|
|
27560
|
+
args.onError(throttleError);
|
|
27561
|
+
return;
|
|
27562
|
+
}
|
|
27475
27563
|
if (aitState.state.ads.forceNoFill) {
|
|
27476
27564
|
args.onError(/* @__PURE__ */ new Error("No fill"));
|
|
27477
27565
|
return;
|
|
@@ -30193,7 +30281,7 @@ function Panel() {
|
|
|
30193
30281
|
color: "#666",
|
|
30194
30282
|
fontWeight: 400
|
|
30195
30283
|
},
|
|
30196
|
-
children: ["v", "0.2.
|
|
30284
|
+
children: ["v", "0.2.2"]
|
|
30197
30285
|
}),
|
|
30198
30286
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", {
|
|
30199
30287
|
type: "button",
|