@kosdev-code/kos-ui-sdk 3.0.14 → 3.0.16
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/common/constants/index.d.ts +0 -3
- package/common/constants/index.d.ts.map +1 -1
- package/common/types/global.d.ts +0 -1
- package/core/core/transport/base-message-transport.d.ts.map +1 -1
- package/core/core/transport/index.d.ts +1 -0
- package/core/core/transport/index.d.ts.map +1 -1
- package/core/core/transport/mock/future-simulator.d.ts +17 -0
- package/core/core/transport/mock/future-simulator.d.ts.map +1 -0
- package/core/core/transport/mock/index.d.ts +3 -0
- package/core/core/transport/mock/index.d.ts.map +1 -0
- package/core/core/transport/mock/mock-config.d.ts +4 -0
- package/core/core/transport/mock/mock-config.d.ts.map +1 -0
- package/core/core/transport/mock/mock-fixtures.d.ts +26 -0
- package/core/core/transport/mock/mock-fixtures.d.ts.map +1 -0
- package/core/core/transport/mock/mock-frames.d.ts +63 -0
- package/core/core/transport/mock/mock-frames.d.ts.map +1 -0
- package/core/core/transport/mock/mock-interceptor.d.ts +5 -0
- package/core/core/transport/mock/mock-interceptor.d.ts.map +1 -0
- package/core/core/transport/mock/mock-playback.d.ts +4 -0
- package/core/core/transport/mock/mock-playback.d.ts.map +1 -0
- package/core/core/transport/mock/mock-recorder.d.ts +23 -0
- package/core/core/transport/mock/mock-recorder.d.ts.map +1 -0
- package/core/core/transport/mock/mock-registry.d.ts +142 -0
- package/core/core/transport/mock/mock-registry.d.ts.map +1 -0
- package/core/core/transport/mock/mock-setups.d.ts +10 -0
- package/core/core/transport/mock/mock-setups.d.ts.map +1 -0
- package/core/core/transport/mock/mock-types.d.ts +677 -0
- package/core/core/transport/mock/mock-types.d.ts.map +1 -0
- package/core/core/transport/mock/route-matcher.d.ts +25 -0
- package/core/core/transport/mock/route-matcher.d.ts.map +1 -0
- package/core/core/transport/mock/standalone-mock-socket.d.ts +19 -0
- package/core/core/transport/mock/standalone-mock-socket.d.ts.map +1 -0
- package/core/core/transport/transport-factory.d.ts +2 -1
- package/core/core/transport/transport-factory.d.ts.map +1 -1
- package/core/types/global.d.ts +2 -12
- package/core/util/kos-fetch.d.ts.map +1 -1
- package/index.cjs +1896 -40
- package/index.cjs.map +1 -1
- package/index.js +1896 -40
- package/index.js.map +1 -1
- package/models/types/global.d.ts +0 -1
- package/package.json +2 -2
package/index.js
CHANGED
|
@@ -49,10 +49,6 @@ var KosWsEvents = /* @__PURE__ */ ((KosWsEvents2) => {
|
|
|
49
49
|
KosWsEvents2["RECEIVED"] = "/ws/log/msg/receive";
|
|
50
50
|
return KosWsEvents2;
|
|
51
51
|
})(KosWsEvents || {});
|
|
52
|
-
var KosMockEvents = /* @__PURE__ */ ((KosMockEvents2) => {
|
|
53
|
-
KosMockEvents2["MSG_RECEIVE"] = "/mock/msg/receive";
|
|
54
|
-
return KosMockEvents2;
|
|
55
|
-
})(KosMockEvents || {});
|
|
56
52
|
const EVENT_KOS_MODEL_READY = "/kos/model/ready/";
|
|
57
53
|
log$R.info("Initializing event bus");
|
|
58
54
|
globalThis.kos = globalThis.kos || {};
|
|
@@ -7572,6 +7568,1822 @@ const abortableWhen = async (predicate, signal) => {
|
|
|
7572
7568
|
pending.cancel();
|
|
7573
7569
|
}
|
|
7574
7570
|
};
|
|
7571
|
+
const KOS_HTTP_REQUEST_TYPE = "kos.http.request";
|
|
7572
|
+
const KOS_HTTP_RESPONSE_TYPE = "kos.http.response";
|
|
7573
|
+
const KOS_FUTURE_TYPE = "kos.future";
|
|
7574
|
+
const KOS_BROKER_SUBSCRIBE_TYPE = "kos.broker.subscribe";
|
|
7575
|
+
const KOS_BROKER_UNSUBSCRIBE_TYPE = "kos.broker.unsubscribe";
|
|
7576
|
+
const KOS_MOCKED_HEADER = "kos-mocked";
|
|
7577
|
+
const KOS_MOCK_FIXTURE_HEADER = "kos-mock-fixture";
|
|
7578
|
+
const KOS_MOCK_TRANSFORMED_HEADER = "kos-mock-transformed";
|
|
7579
|
+
const CONTENT_TYPE_HEADER = "content-type";
|
|
7580
|
+
const MESSAGE_SEPARATOR = "\n\n";
|
|
7581
|
+
const toTransportString = (data) => typeof data === "string" ? data : void 0;
|
|
7582
|
+
function peekFrameType(data) {
|
|
7583
|
+
const text = toTransportString(data);
|
|
7584
|
+
if (!text) {
|
|
7585
|
+
return void 0;
|
|
7586
|
+
}
|
|
7587
|
+
try {
|
|
7588
|
+
return processKosMessage(text).headers[HEADER_REQUEST_TYPE];
|
|
7589
|
+
} catch {
|
|
7590
|
+
return void 0;
|
|
7591
|
+
}
|
|
7592
|
+
}
|
|
7593
|
+
function decodeRequestFrame(data) {
|
|
7594
|
+
const text = toTransportString(data);
|
|
7595
|
+
if (!text) {
|
|
7596
|
+
return void 0;
|
|
7597
|
+
}
|
|
7598
|
+
let headers;
|
|
7599
|
+
let body;
|
|
7600
|
+
try {
|
|
7601
|
+
({ headers, body } = processKosMessage(text));
|
|
7602
|
+
} catch {
|
|
7603
|
+
return void 0;
|
|
7604
|
+
}
|
|
7605
|
+
if (headers[HEADER_REQUEST_TYPE] !== KOS_HTTP_REQUEST_TYPE) {
|
|
7606
|
+
return void 0;
|
|
7607
|
+
}
|
|
7608
|
+
const method = headers[HEADER_METHOD];
|
|
7609
|
+
const url = headers[HEADER_URL];
|
|
7610
|
+
const requestId = headers[HEADER_REQUEST_ID];
|
|
7611
|
+
if (!method || !url || !requestId) {
|
|
7612
|
+
return void 0;
|
|
7613
|
+
}
|
|
7614
|
+
return {
|
|
7615
|
+
method,
|
|
7616
|
+
url,
|
|
7617
|
+
requestId,
|
|
7618
|
+
tracker: headers[HEADER_WORK_TRACKER],
|
|
7619
|
+
ordered: headers[HEADER_ORDERED] === "true",
|
|
7620
|
+
headers,
|
|
7621
|
+
rawBody: body || void 0
|
|
7622
|
+
};
|
|
7623
|
+
}
|
|
7624
|
+
function decodeSubscribeTopics(data) {
|
|
7625
|
+
const text = toTransportString(data);
|
|
7626
|
+
if (!text) {
|
|
7627
|
+
return void 0;
|
|
7628
|
+
}
|
|
7629
|
+
let headers;
|
|
7630
|
+
let body;
|
|
7631
|
+
try {
|
|
7632
|
+
({ headers, body } = processKosMessage(text));
|
|
7633
|
+
} catch {
|
|
7634
|
+
return void 0;
|
|
7635
|
+
}
|
|
7636
|
+
const frameType = headers[HEADER_REQUEST_TYPE];
|
|
7637
|
+
if (frameType !== KOS_BROKER_SUBSCRIBE_TYPE && frameType !== KOS_BROKER_UNSUBSCRIBE_TYPE) {
|
|
7638
|
+
return void 0;
|
|
7639
|
+
}
|
|
7640
|
+
let topics = [];
|
|
7641
|
+
try {
|
|
7642
|
+
const parsed = JSON.parse(body);
|
|
7643
|
+
if (Array.isArray(parsed)) {
|
|
7644
|
+
topics = parsed.filter(
|
|
7645
|
+
(topic) => typeof topic === "string"
|
|
7646
|
+
);
|
|
7647
|
+
}
|
|
7648
|
+
} catch {
|
|
7649
|
+
}
|
|
7650
|
+
return {
|
|
7651
|
+
type: frameType === KOS_BROKER_SUBSCRIBE_TYPE ? "subscribe" : "unsubscribe",
|
|
7652
|
+
topics
|
|
7653
|
+
};
|
|
7654
|
+
}
|
|
7655
|
+
const assertHeaderSafe = (name, value) => {
|
|
7656
|
+
if (value.includes("\n")) {
|
|
7657
|
+
throw new Error(`KosMock frame ${name} must not contain newlines`);
|
|
7658
|
+
}
|
|
7659
|
+
};
|
|
7660
|
+
const encodeFrame$1 = (headers, body) => {
|
|
7661
|
+
const lines = headers.map(([name, value]) => {
|
|
7662
|
+
assertHeaderSafe(name, value);
|
|
7663
|
+
return `${name}:${value}`;
|
|
7664
|
+
});
|
|
7665
|
+
return `${lines.join("\n")}${MESSAGE_SEPARATOR}${body}`;
|
|
7666
|
+
};
|
|
7667
|
+
function encodeRawFrame(headers, body) {
|
|
7668
|
+
return encodeFrame$1(Object.entries(headers), body);
|
|
7669
|
+
}
|
|
7670
|
+
function encodeHttpResponseFrame({
|
|
7671
|
+
requestId,
|
|
7672
|
+
status = 200,
|
|
7673
|
+
data,
|
|
7674
|
+
headers = {},
|
|
7675
|
+
raw
|
|
7676
|
+
}) {
|
|
7677
|
+
const body = raw ?? JSON.stringify({ status, data: data ?? {} });
|
|
7678
|
+
return encodeFrame$1(
|
|
7679
|
+
[
|
|
7680
|
+
[HEADER_RESPONSE_ID, requestId],
|
|
7681
|
+
[STATUS, `${status}`],
|
|
7682
|
+
[CONTENT_TYPE_HEADER, "application/json"],
|
|
7683
|
+
...Object.entries(headers),
|
|
7684
|
+
[HEADER_REQUEST_TYPE, KOS_HTTP_RESPONSE_TYPE]
|
|
7685
|
+
],
|
|
7686
|
+
body
|
|
7687
|
+
);
|
|
7688
|
+
}
|
|
7689
|
+
function encodeTopicFrame(topic, body, headers = {}) {
|
|
7690
|
+
return encodeFrame$1(
|
|
7691
|
+
[[HEADER_TOPIC, topic], ...Object.entries(headers)],
|
|
7692
|
+
typeof body === "string" ? body : JSON.stringify(body ?? {})
|
|
7693
|
+
);
|
|
7694
|
+
}
|
|
7695
|
+
function encodeFutureFrame(future) {
|
|
7696
|
+
return encodeFrame$1(
|
|
7697
|
+
[[HEADER_REQUEST_TYPE, KOS_FUTURE_TYPE]],
|
|
7698
|
+
JSON.stringify(future)
|
|
7699
|
+
);
|
|
7700
|
+
}
|
|
7701
|
+
const DEFAULT_DURATION_MS = 5e3;
|
|
7702
|
+
const DEFAULT_INTERVAL_MS = 250;
|
|
7703
|
+
class KosMockFutureHandle {
|
|
7704
|
+
constructor(options, emitFrame) {
|
|
7705
|
+
this.emitFrame = emitFrame;
|
|
7706
|
+
this.id = options.id ?? uuid();
|
|
7707
|
+
this.tracker = options.tracker;
|
|
7708
|
+
this.durationMs = options.durationMs ?? DEFAULT_DURATION_MS;
|
|
7709
|
+
this.intervalMs = options.intervalMs ?? DEFAULT_INTERVAL_MS;
|
|
7710
|
+
this.targetEndState = options.endState ?? "SUCCESS";
|
|
7711
|
+
this.targetReason = options.reason;
|
|
7712
|
+
this.clientData = options.clientData;
|
|
7713
|
+
this.remainingTimeMs = this.durationMs;
|
|
7714
|
+
}
|
|
7715
|
+
emitFrame;
|
|
7716
|
+
id;
|
|
7717
|
+
tracker;
|
|
7718
|
+
progress = 0;
|
|
7719
|
+
done = false;
|
|
7720
|
+
remainingTimeMs;
|
|
7721
|
+
elapsedMs = 0;
|
|
7722
|
+
endStateValue;
|
|
7723
|
+
reasonValue;
|
|
7724
|
+
timer;
|
|
7725
|
+
started = false;
|
|
7726
|
+
durationMs;
|
|
7727
|
+
intervalMs;
|
|
7728
|
+
targetEndState;
|
|
7729
|
+
targetReason;
|
|
7730
|
+
clientData;
|
|
7731
|
+
snapshot() {
|
|
7732
|
+
return {
|
|
7733
|
+
id: this.id,
|
|
7734
|
+
tracker: this.tracker,
|
|
7735
|
+
progress: this.progress,
|
|
7736
|
+
remainingTimeMs: this.remainingTimeMs,
|
|
7737
|
+
endState: this.endStateValue,
|
|
7738
|
+
reason: this.reasonValue,
|
|
7739
|
+
clientData: this.clientData
|
|
7740
|
+
};
|
|
7741
|
+
}
|
|
7742
|
+
start() {
|
|
7743
|
+
if (this.started || this.done) {
|
|
7744
|
+
return;
|
|
7745
|
+
}
|
|
7746
|
+
this.started = true;
|
|
7747
|
+
this.timer = setInterval(() => this.tick(), this.intervalMs);
|
|
7748
|
+
}
|
|
7749
|
+
tick() {
|
|
7750
|
+
this.elapsedMs += this.intervalMs;
|
|
7751
|
+
if (this.elapsedMs >= this.durationMs) {
|
|
7752
|
+
this.complete(this.targetEndState, this.targetReason);
|
|
7753
|
+
} else {
|
|
7754
|
+
this.emitProgress(
|
|
7755
|
+
this.elapsedMs / this.durationMs,
|
|
7756
|
+
this.durationMs - this.elapsedMs
|
|
7757
|
+
);
|
|
7758
|
+
}
|
|
7759
|
+
}
|
|
7760
|
+
emitProgress(progress, remainingTimeMs) {
|
|
7761
|
+
if (this.done) {
|
|
7762
|
+
return;
|
|
7763
|
+
}
|
|
7764
|
+
this.progress = progress;
|
|
7765
|
+
if (remainingTimeMs !== void 0) {
|
|
7766
|
+
this.remainingTimeMs = remainingTimeMs;
|
|
7767
|
+
}
|
|
7768
|
+
this.emitFrame(encodeFutureFrame(this.snapshot()));
|
|
7769
|
+
}
|
|
7770
|
+
complete(endState = "SUCCESS", reason) {
|
|
7771
|
+
if (this.done) {
|
|
7772
|
+
return;
|
|
7773
|
+
}
|
|
7774
|
+
this.stopTimer();
|
|
7775
|
+
this.done = true;
|
|
7776
|
+
this.endStateValue = endState;
|
|
7777
|
+
this.reasonValue = reason;
|
|
7778
|
+
this.remainingTimeMs = 0;
|
|
7779
|
+
if (endState === "SUCCESS") {
|
|
7780
|
+
this.progress = 1;
|
|
7781
|
+
}
|
|
7782
|
+
this.emitFrame(encodeFutureFrame(this.snapshot()));
|
|
7783
|
+
}
|
|
7784
|
+
cancel() {
|
|
7785
|
+
this.complete("CANCEL", this.reasonValue ?? "canceled by mock");
|
|
7786
|
+
}
|
|
7787
|
+
dispose() {
|
|
7788
|
+
this.stopTimer();
|
|
7789
|
+
this.done = true;
|
|
7790
|
+
}
|
|
7791
|
+
stopTimer() {
|
|
7792
|
+
if (this.timer !== void 0) {
|
|
7793
|
+
clearInterval(this.timer);
|
|
7794
|
+
this.timer = void 0;
|
|
7795
|
+
}
|
|
7796
|
+
}
|
|
7797
|
+
}
|
|
7798
|
+
class KosMockFutureSimulator {
|
|
7799
|
+
constructor(deps) {
|
|
7800
|
+
this.deps = deps;
|
|
7801
|
+
}
|
|
7802
|
+
deps;
|
|
7803
|
+
futures = /* @__PURE__ */ new Map();
|
|
7804
|
+
create(options = {}) {
|
|
7805
|
+
const handle = new KosMockFutureHandle(options, this.deps.emitFrame);
|
|
7806
|
+
this.futures.set(handle.id, handle);
|
|
7807
|
+
if (options.registerCancelRoute !== false) {
|
|
7808
|
+
this.deps.ensureCancelRoutes();
|
|
7809
|
+
}
|
|
7810
|
+
if (!options.tracker) {
|
|
7811
|
+
KosLog.warn(
|
|
7812
|
+
`KosMock future ${handle.id} has no tracker — models will not link it to their request; prefer futures.fromRequest`
|
|
7813
|
+
);
|
|
7814
|
+
}
|
|
7815
|
+
if (options.autoStart !== false) {
|
|
7816
|
+
handle.start();
|
|
7817
|
+
}
|
|
7818
|
+
return handle;
|
|
7819
|
+
}
|
|
7820
|
+
fromRequest(req, options = {}) {
|
|
7821
|
+
return this.create({ tracker: req.tracker, ...options });
|
|
7822
|
+
}
|
|
7823
|
+
get(idOrTracker) {
|
|
7824
|
+
const byId = this.futures.get(idOrTracker);
|
|
7825
|
+
if (byId) {
|
|
7826
|
+
return byId;
|
|
7827
|
+
}
|
|
7828
|
+
for (const handle of this.futures.values()) {
|
|
7829
|
+
if (handle.tracker === idOrTracker) {
|
|
7830
|
+
return handle;
|
|
7831
|
+
}
|
|
7832
|
+
}
|
|
7833
|
+
return void 0;
|
|
7834
|
+
}
|
|
7835
|
+
get active() {
|
|
7836
|
+
return [...this.futures.values()].filter((handle) => !handle.done);
|
|
7837
|
+
}
|
|
7838
|
+
clear() {
|
|
7839
|
+
this.futures.forEach((handle) => handle.dispose());
|
|
7840
|
+
this.futures.clear();
|
|
7841
|
+
}
|
|
7842
|
+
}
|
|
7843
|
+
const DEFAULT_FIXTURE_SERVER_URL = "http://localhost:5676";
|
|
7844
|
+
const DB_NAME = "kos-mock-fixtures";
|
|
7845
|
+
const STORE_NAME = "fixtures";
|
|
7846
|
+
const summarize = (name, fixture) => {
|
|
7847
|
+
return {
|
|
7848
|
+
name,
|
|
7849
|
+
capturedAt: fixture.capturedAt,
|
|
7850
|
+
exchanges: fixture.exchanges.length,
|
|
7851
|
+
pushes: fixture.pushes.length
|
|
7852
|
+
};
|
|
7853
|
+
};
|
|
7854
|
+
const openDb = () => new Promise((resolve, reject) => {
|
|
7855
|
+
const request2 = indexedDB.open(DB_NAME, 1);
|
|
7856
|
+
request2.onupgradeneeded = () => {
|
|
7857
|
+
if (!request2.result.objectStoreNames.contains(STORE_NAME)) {
|
|
7858
|
+
request2.result.createObjectStore(STORE_NAME);
|
|
7859
|
+
}
|
|
7860
|
+
};
|
|
7861
|
+
request2.onsuccess = () => resolve(request2.result);
|
|
7862
|
+
request2.onerror = () => reject(request2.error);
|
|
7863
|
+
});
|
|
7864
|
+
const withStore = async (mode, operation) => {
|
|
7865
|
+
const db = await openDb();
|
|
7866
|
+
try {
|
|
7867
|
+
return await new Promise((resolve, reject) => {
|
|
7868
|
+
const request2 = operation(
|
|
7869
|
+
db.transaction(STORE_NAME, mode).objectStore(STORE_NAME)
|
|
7870
|
+
);
|
|
7871
|
+
request2.onsuccess = () => resolve(request2.result);
|
|
7872
|
+
request2.onerror = () => reject(request2.error);
|
|
7873
|
+
});
|
|
7874
|
+
} finally {
|
|
7875
|
+
db.close();
|
|
7876
|
+
}
|
|
7877
|
+
};
|
|
7878
|
+
class BrowserFixtureBackend {
|
|
7879
|
+
get available() {
|
|
7880
|
+
return typeof indexedDB !== "undefined";
|
|
7881
|
+
}
|
|
7882
|
+
async save(name, fixture) {
|
|
7883
|
+
if (!this.available) {
|
|
7884
|
+
throw new Error("IndexedDB is not available in this environment");
|
|
7885
|
+
}
|
|
7886
|
+
await withStore("readwrite", (store) => store.put(fixture, name));
|
|
7887
|
+
}
|
|
7888
|
+
async load(name) {
|
|
7889
|
+
if (!this.available) {
|
|
7890
|
+
return void 0;
|
|
7891
|
+
}
|
|
7892
|
+
return withStore(
|
|
7893
|
+
"readonly",
|
|
7894
|
+
(store) => store.get(name)
|
|
7895
|
+
);
|
|
7896
|
+
}
|
|
7897
|
+
async list() {
|
|
7898
|
+
if (!this.available) {
|
|
7899
|
+
return [];
|
|
7900
|
+
}
|
|
7901
|
+
const [keys, values] = await Promise.all([
|
|
7902
|
+
withStore("readonly", (store) => store.getAllKeys()),
|
|
7903
|
+
withStore("readonly", (store) => store.getAll())
|
|
7904
|
+
]);
|
|
7905
|
+
return keys.map((key, index2) => summarize(String(key), values[index2]));
|
|
7906
|
+
}
|
|
7907
|
+
async delete(name) {
|
|
7908
|
+
if (!this.available) {
|
|
7909
|
+
return false;
|
|
7910
|
+
}
|
|
7911
|
+
const existing = await this.load(name);
|
|
7912
|
+
if (!existing) {
|
|
7913
|
+
return false;
|
|
7914
|
+
}
|
|
7915
|
+
await withStore("readwrite", (store) => store.delete(name));
|
|
7916
|
+
return true;
|
|
7917
|
+
}
|
|
7918
|
+
}
|
|
7919
|
+
class ServerFixtureBackend {
|
|
7920
|
+
constructor(baseUrl) {
|
|
7921
|
+
this.baseUrl = baseUrl;
|
|
7922
|
+
}
|
|
7923
|
+
baseUrl;
|
|
7924
|
+
url(name) {
|
|
7925
|
+
const base = this.baseUrl.replace(/\/$/, "");
|
|
7926
|
+
return name ? `${base}/fixtures/${encodeURIComponent(name)}` : `${base}/fixtures`;
|
|
7927
|
+
}
|
|
7928
|
+
async save(name, fixture) {
|
|
7929
|
+
const response = await fetch(this.url(name), {
|
|
7930
|
+
method: "PUT",
|
|
7931
|
+
headers: { "content-type": "application/json" },
|
|
7932
|
+
body: JSON.stringify(fixture)
|
|
7933
|
+
});
|
|
7934
|
+
if (!response.ok) {
|
|
7935
|
+
throw new Error(`Fixture server rejected save: ${response.status}`);
|
|
7936
|
+
}
|
|
7937
|
+
}
|
|
7938
|
+
async load(name) {
|
|
7939
|
+
const response = await fetch(this.url(name));
|
|
7940
|
+
if (!response.ok) {
|
|
7941
|
+
return void 0;
|
|
7942
|
+
}
|
|
7943
|
+
return await response.json();
|
|
7944
|
+
}
|
|
7945
|
+
async list() {
|
|
7946
|
+
const response = await fetch(this.url());
|
|
7947
|
+
if (!response.ok) {
|
|
7948
|
+
return [];
|
|
7949
|
+
}
|
|
7950
|
+
const payload = await response.json();
|
|
7951
|
+
return payload.fixtures ?? [];
|
|
7952
|
+
}
|
|
7953
|
+
async delete(name) {
|
|
7954
|
+
const response = await fetch(this.url(name), { method: "DELETE" });
|
|
7955
|
+
return response.ok;
|
|
7956
|
+
}
|
|
7957
|
+
}
|
|
7958
|
+
function createFixtureStore(registry, deps = {}) {
|
|
7959
|
+
const browser = deps.browserBackend ?? new BrowserFixtureBackend();
|
|
7960
|
+
const serverFactory = deps.serverBackendFactory ?? ((url) => new ServerFixtureBackend(url));
|
|
7961
|
+
let server;
|
|
7962
|
+
let serverUrl;
|
|
7963
|
+
const resolveName = (fixture, name) => {
|
|
7964
|
+
const resolved = name ?? fixture.name;
|
|
7965
|
+
if (!resolved) {
|
|
7966
|
+
throw new Error(
|
|
7967
|
+
"Fixture has no name — pass one: fixtures.save(fixture, {name})"
|
|
7968
|
+
);
|
|
7969
|
+
}
|
|
7970
|
+
return resolved;
|
|
7971
|
+
};
|
|
7972
|
+
const store = {
|
|
7973
|
+
get serverUrl() {
|
|
7974
|
+
return serverUrl;
|
|
7975
|
+
},
|
|
7976
|
+
useServer(url) {
|
|
7977
|
+
if (url === void 0) {
|
|
7978
|
+
server = void 0;
|
|
7979
|
+
serverUrl = void 0;
|
|
7980
|
+
return;
|
|
7981
|
+
}
|
|
7982
|
+
serverUrl = url === true ? DEFAULT_FIXTURE_SERVER_URL : url;
|
|
7983
|
+
server = serverFactory(serverUrl);
|
|
7984
|
+
},
|
|
7985
|
+
async save(fixture, options = {}) {
|
|
7986
|
+
const name = resolveName(fixture, options.name);
|
|
7987
|
+
const named = { ...fixture, name };
|
|
7988
|
+
const target = options.to === "browser" ? browser : options.to === "server" ? server : server ?? browser;
|
|
7989
|
+
if (!target) {
|
|
7990
|
+
throw new Error(
|
|
7991
|
+
"No fixture server configured — useServer() or save to the browser"
|
|
7992
|
+
);
|
|
7993
|
+
}
|
|
7994
|
+
await target.save(name, named);
|
|
7995
|
+
KosLog.info(`KosMock fixture "${name}" saved`);
|
|
7996
|
+
return name;
|
|
7997
|
+
},
|
|
7998
|
+
saveRecording(name) {
|
|
7999
|
+
return store.save(registry.recorder.toFixture(), { name });
|
|
8000
|
+
},
|
|
8001
|
+
async load(name) {
|
|
8002
|
+
if (server) {
|
|
8003
|
+
const fromServer = await server.load(name).catch(() => void 0);
|
|
8004
|
+
if (fromServer) {
|
|
8005
|
+
return fromServer;
|
|
8006
|
+
}
|
|
8007
|
+
}
|
|
8008
|
+
return browser.load(name).catch(() => void 0);
|
|
8009
|
+
},
|
|
8010
|
+
async list() {
|
|
8011
|
+
const summaries = [];
|
|
8012
|
+
if (server) {
|
|
8013
|
+
try {
|
|
8014
|
+
(await server.list()).forEach(
|
|
8015
|
+
(summary) => summaries.push({ ...summary, source: "server" })
|
|
8016
|
+
);
|
|
8017
|
+
} catch {
|
|
8018
|
+
KosLog.warn(`KosMock fixture server unreachable at ${serverUrl}`);
|
|
8019
|
+
}
|
|
8020
|
+
}
|
|
8021
|
+
try {
|
|
8022
|
+
(await browser.list()).forEach(
|
|
8023
|
+
(summary) => summaries.push({ ...summary, source: "browser" })
|
|
8024
|
+
);
|
|
8025
|
+
} catch {
|
|
8026
|
+
}
|
|
8027
|
+
return summaries;
|
|
8028
|
+
},
|
|
8029
|
+
async delete(name) {
|
|
8030
|
+
let deleted = false;
|
|
8031
|
+
if (server) {
|
|
8032
|
+
deleted = await server.delete(name).catch(() => false) || deleted;
|
|
8033
|
+
}
|
|
8034
|
+
deleted = await browser.delete(name).catch(() => false) || deleted;
|
|
8035
|
+
return deleted;
|
|
8036
|
+
},
|
|
8037
|
+
async import() {
|
|
8038
|
+
if (typeof document === "undefined") {
|
|
8039
|
+
KosLog.warn(`KosMock fixture import requires a browser environment`);
|
|
8040
|
+
return void 0;
|
|
8041
|
+
}
|
|
8042
|
+
return new Promise((resolve) => {
|
|
8043
|
+
const input = document.createElement("input");
|
|
8044
|
+
input.type = "file";
|
|
8045
|
+
input.accept = ".json,application/json";
|
|
8046
|
+
input.onchange = async () => {
|
|
8047
|
+
const file = input.files?.[0];
|
|
8048
|
+
if (!file) {
|
|
8049
|
+
resolve(void 0);
|
|
8050
|
+
return;
|
|
8051
|
+
}
|
|
8052
|
+
try {
|
|
8053
|
+
const fixture = JSON.parse(await file.text());
|
|
8054
|
+
const name = await store.save(fixture, {
|
|
8055
|
+
name: fixture.name ?? file.name.replace(/\.json$/, "")
|
|
8056
|
+
});
|
|
8057
|
+
resolve(name);
|
|
8058
|
+
} catch (error) {
|
|
8059
|
+
KosLog.error(`KosMock fixture import failed`, error?.message);
|
|
8060
|
+
resolve(void 0);
|
|
8061
|
+
}
|
|
8062
|
+
};
|
|
8063
|
+
input.click();
|
|
8064
|
+
});
|
|
8065
|
+
},
|
|
8066
|
+
async play(name, options) {
|
|
8067
|
+
const fixture = await store.load(name);
|
|
8068
|
+
if (!fixture) {
|
|
8069
|
+
throw new Error(
|
|
8070
|
+
`No fixture named "${name}" in the store${serverUrl ? ` or at ${serverUrl}` : ""}`
|
|
8071
|
+
);
|
|
8072
|
+
}
|
|
8073
|
+
return registry.loadFixture(fixture, options);
|
|
8074
|
+
}
|
|
8075
|
+
};
|
|
8076
|
+
return store;
|
|
8077
|
+
}
|
|
8078
|
+
const REGEX_SPECIALS = /[.*+?^${}()|[\]\\]/g;
|
|
8079
|
+
const escapeLiteral = (segment) => segment.replace(REGEX_SPECIALS, "\\$&");
|
|
8080
|
+
const normalizePath = (path) => path.length > 1 && path.endsWith("/") ? path.slice(0, -1) : path;
|
|
8081
|
+
function compileRoutePattern(pattern) {
|
|
8082
|
+
const paramNames = [];
|
|
8083
|
+
const segments = normalizePath(pattern).split("/");
|
|
8084
|
+
const parts = segments.map((segment, index2) => {
|
|
8085
|
+
if (segment.startsWith(":") && segment.length > 1) {
|
|
8086
|
+
paramNames.push(segment.slice(1));
|
|
8087
|
+
return "([^/]+)";
|
|
8088
|
+
}
|
|
8089
|
+
if (segment === "*") {
|
|
8090
|
+
if (index2 === segments.length - 1) {
|
|
8091
|
+
paramNames.push("*");
|
|
8092
|
+
return "(.*)";
|
|
8093
|
+
}
|
|
8094
|
+
return "[^/]+";
|
|
8095
|
+
}
|
|
8096
|
+
return escapeLiteral(segment);
|
|
8097
|
+
});
|
|
8098
|
+
return {
|
|
8099
|
+
pattern,
|
|
8100
|
+
regex: new RegExp(`^${parts.join("/")}$`),
|
|
8101
|
+
paramNames
|
|
8102
|
+
};
|
|
8103
|
+
}
|
|
8104
|
+
function matchRoutePattern(compiled, path) {
|
|
8105
|
+
const result = compiled.regex.exec(normalizePath(path));
|
|
8106
|
+
if (!result) {
|
|
8107
|
+
return void 0;
|
|
8108
|
+
}
|
|
8109
|
+
const params2 = {};
|
|
8110
|
+
compiled.paramNames.forEach((name, index2) => {
|
|
8111
|
+
params2[name] = result[index2 + 1];
|
|
8112
|
+
});
|
|
8113
|
+
return params2;
|
|
8114
|
+
}
|
|
8115
|
+
function splitUrl(url) {
|
|
8116
|
+
const queryIndex = url.indexOf("?");
|
|
8117
|
+
if (queryIndex === -1) {
|
|
8118
|
+
return { path: url, query: {} };
|
|
8119
|
+
}
|
|
8120
|
+
const path = url.slice(0, queryIndex);
|
|
8121
|
+
const query = {};
|
|
8122
|
+
for (const pair of url.slice(queryIndex + 1).split("&")) {
|
|
8123
|
+
if (!pair) {
|
|
8124
|
+
continue;
|
|
8125
|
+
}
|
|
8126
|
+
const [key, value] = pair.split("=");
|
|
8127
|
+
query[decodeURIComponent(key)] = decodeURIComponent(value || "");
|
|
8128
|
+
}
|
|
8129
|
+
return { path, query };
|
|
8130
|
+
}
|
|
8131
|
+
function methodMatches(routeMethod, requestMethod) {
|
|
8132
|
+
return routeMethod === "*" || routeMethod.toUpperCase() === requestMethod.toUpperCase();
|
|
8133
|
+
}
|
|
8134
|
+
const rewriteTrackers = (raw, trackerMap) => {
|
|
8135
|
+
let out = raw;
|
|
8136
|
+
trackerMap.forEach((liveTracker, capturedTracker) => {
|
|
8137
|
+
out = out.split(capturedTracker).join(liveTracker);
|
|
8138
|
+
});
|
|
8139
|
+
return out;
|
|
8140
|
+
};
|
|
8141
|
+
const discriminatorKey = (spec, body) => {
|
|
8142
|
+
try {
|
|
8143
|
+
if (typeof spec === "function") {
|
|
8144
|
+
return String(spec(body));
|
|
8145
|
+
}
|
|
8146
|
+
let current = body;
|
|
8147
|
+
for (const part of spec.split(".")) {
|
|
8148
|
+
if (current === null || typeof current !== "object") {
|
|
8149
|
+
return String(void 0);
|
|
8150
|
+
}
|
|
8151
|
+
current = current[part];
|
|
8152
|
+
}
|
|
8153
|
+
return String(current);
|
|
8154
|
+
} catch {
|
|
8155
|
+
return String(void 0);
|
|
8156
|
+
}
|
|
8157
|
+
};
|
|
8158
|
+
function installFixturePlayback(registry, fixture, options = {}) {
|
|
8159
|
+
const name = options.name ?? fixture.name;
|
|
8160
|
+
const speed = options.speed ?? 1;
|
|
8161
|
+
const consume = options.consume ?? (registry.standalone ? "repeat-last" : "once");
|
|
8162
|
+
const discriminators = {
|
|
8163
|
+
...fixture.discriminators,
|
|
8164
|
+
...options.discriminators
|
|
8165
|
+
};
|
|
8166
|
+
const trackerMap = /* @__PURE__ */ new Map();
|
|
8167
|
+
const timers2 = /* @__PURE__ */ new Set();
|
|
8168
|
+
let stopped = false;
|
|
8169
|
+
const schedule = (delayMs, fire) => {
|
|
8170
|
+
const timer = setTimeout(() => {
|
|
8171
|
+
timers2.delete(timer);
|
|
8172
|
+
fire();
|
|
8173
|
+
}, Math.max(0, delayMs));
|
|
8174
|
+
timers2.add(timer);
|
|
8175
|
+
};
|
|
8176
|
+
const firePush = (push) => {
|
|
8177
|
+
if (push.kind === "topic" && push.topic) {
|
|
8178
|
+
registry.publishFrame(encodeTopicFrame(push.topic, push.body));
|
|
8179
|
+
} else if (push.kind === "future") {
|
|
8180
|
+
registry.publishFrame(
|
|
8181
|
+
`type:${KOS_FUTURE_TYPE}
|
|
8182
|
+
|
|
8183
|
+
${rewriteTrackers(push.body, trackerMap)}`
|
|
8184
|
+
);
|
|
8185
|
+
}
|
|
8186
|
+
};
|
|
8187
|
+
const capturedTrackers = new Set(
|
|
8188
|
+
fixture.exchanges.map((exchange) => exchange.tracker).filter((tracker) => !!tracker)
|
|
8189
|
+
);
|
|
8190
|
+
const anchoredByTracker = /* @__PURE__ */ new Map();
|
|
8191
|
+
const unanchored = [];
|
|
8192
|
+
for (const push of fixture.pushes) {
|
|
8193
|
+
if (push.kind === "future" && push.tracker && capturedTrackers.has(push.tracker)) {
|
|
8194
|
+
const list = anchoredByTracker.get(push.tracker) ?? [];
|
|
8195
|
+
list.push(push);
|
|
8196
|
+
anchoredByTracker.set(push.tracker, list);
|
|
8197
|
+
} else {
|
|
8198
|
+
unanchored.push(push);
|
|
8199
|
+
}
|
|
8200
|
+
}
|
|
8201
|
+
const groups = /* @__PURE__ */ new Map();
|
|
8202
|
+
for (const exchange of fixture.exchanges) {
|
|
8203
|
+
const { path } = splitUrl(exchange.url);
|
|
8204
|
+
const key = `${exchange.method.toUpperCase()} ${path}`;
|
|
8205
|
+
const group = groups.get(key) ?? {
|
|
8206
|
+
method: exchange.method.toUpperCase(),
|
|
8207
|
+
path,
|
|
8208
|
+
queue: []
|
|
8209
|
+
};
|
|
8210
|
+
group.queue.push(exchange);
|
|
8211
|
+
groups.set(key, group);
|
|
8212
|
+
}
|
|
8213
|
+
const anchorsScheduled = /* @__PURE__ */ new Set();
|
|
8214
|
+
const routeHandles = [];
|
|
8215
|
+
groups.forEach((group, groupKey) => {
|
|
8216
|
+
const spec = discriminators[groupKey];
|
|
8217
|
+
const wholeGroup = { queue: group.queue, served: 0 };
|
|
8218
|
+
let buckets;
|
|
8219
|
+
if (spec !== void 0) {
|
|
8220
|
+
buckets = /* @__PURE__ */ new Map();
|
|
8221
|
+
for (const exchange of group.queue) {
|
|
8222
|
+
let parsedBody;
|
|
8223
|
+
try {
|
|
8224
|
+
parsedBody = exchange.requestBody ? JSON.parse(exchange.requestBody) : void 0;
|
|
8225
|
+
} catch {
|
|
8226
|
+
parsedBody = void 0;
|
|
8227
|
+
}
|
|
8228
|
+
const key = discriminatorKey(spec, parsedBody);
|
|
8229
|
+
const bucket = buckets.get(key) ?? { queue: [], served: 0 };
|
|
8230
|
+
bucket.queue.push(exchange);
|
|
8231
|
+
buckets.set(key, bucket);
|
|
8232
|
+
}
|
|
8233
|
+
}
|
|
8234
|
+
routeHandles.push(
|
|
8235
|
+
registry.route(
|
|
8236
|
+
group.method,
|
|
8237
|
+
group.path,
|
|
8238
|
+
(req) => {
|
|
8239
|
+
if (stopped) {
|
|
8240
|
+
return void 0;
|
|
8241
|
+
}
|
|
8242
|
+
let serveQueue;
|
|
8243
|
+
if (buckets && spec !== void 0) {
|
|
8244
|
+
serveQueue = buckets.get(discriminatorKey(spec, req.json()));
|
|
8245
|
+
if (!serveQueue) {
|
|
8246
|
+
return void 0;
|
|
8247
|
+
}
|
|
8248
|
+
} else {
|
|
8249
|
+
serveQueue = wholeGroup;
|
|
8250
|
+
}
|
|
8251
|
+
if (serveQueue.served >= serveQueue.queue.length) {
|
|
8252
|
+
if (consume === "once") {
|
|
8253
|
+
return void 0;
|
|
8254
|
+
}
|
|
8255
|
+
serveQueue.served = serveQueue.queue.length - 1;
|
|
8256
|
+
}
|
|
8257
|
+
const exchange = serveQueue.queue[serveQueue.served];
|
|
8258
|
+
serveQueue.served += 1;
|
|
8259
|
+
if (exchange.tracker && req.tracker) {
|
|
8260
|
+
trackerMap.set(exchange.tracker, req.tracker);
|
|
8261
|
+
}
|
|
8262
|
+
if (exchange.tracker && !anchorsScheduled.has(exchange.tracker)) {
|
|
8263
|
+
anchorsScheduled.add(exchange.tracker);
|
|
8264
|
+
const anchored = anchoredByTracker.get(exchange.tracker) ?? [];
|
|
8265
|
+
anchored.forEach(
|
|
8266
|
+
(push) => schedule((push.tMs - exchange.tMs) / speed, () => firePush(push))
|
|
8267
|
+
);
|
|
8268
|
+
}
|
|
8269
|
+
return {
|
|
8270
|
+
status: exchange.status,
|
|
8271
|
+
raw: rewriteTrackers(exchange.responseBody, trackerMap),
|
|
8272
|
+
delayMs: options.ignoreLatency ? 0 : (exchange.latencyMs ?? 0) / speed
|
|
8273
|
+
};
|
|
8274
|
+
},
|
|
8275
|
+
{
|
|
8276
|
+
fixture: name,
|
|
8277
|
+
description: `fixture replay: ${group.method} ${group.path} (${group.queue.length} captured${spec !== void 0 ? ", discriminated" : ""})`
|
|
8278
|
+
}
|
|
8279
|
+
)
|
|
8280
|
+
);
|
|
8281
|
+
});
|
|
8282
|
+
let pushesStarted = false;
|
|
8283
|
+
const startPushes = () => {
|
|
8284
|
+
if (pushesStarted) {
|
|
8285
|
+
return;
|
|
8286
|
+
}
|
|
8287
|
+
pushesStarted = true;
|
|
8288
|
+
unanchored.forEach(
|
|
8289
|
+
(push) => schedule(push.tMs / speed, () => firePush(push))
|
|
8290
|
+
);
|
|
8291
|
+
};
|
|
8292
|
+
if ((options.pushes ?? "scheduled") === "scheduled") {
|
|
8293
|
+
startPushes();
|
|
8294
|
+
}
|
|
8295
|
+
KosLog.info(
|
|
8296
|
+
`KosMock fixture${name ? ` "${name}"` : ""} loaded: ${groups.size} routes, ${fixture.pushes.length} pushes (${fixture.pushes.length - unanchored.length} anchored), consume: ${consume}`
|
|
8297
|
+
);
|
|
8298
|
+
return {
|
|
8299
|
+
name,
|
|
8300
|
+
startPushes,
|
|
8301
|
+
stop: () => {
|
|
8302
|
+
stopped = true;
|
|
8303
|
+
routeHandles.forEach((handle) => handle.remove());
|
|
8304
|
+
timers2.forEach((timer) => clearTimeout(timer));
|
|
8305
|
+
timers2.clear();
|
|
8306
|
+
}
|
|
8307
|
+
};
|
|
8308
|
+
}
|
|
8309
|
+
class KosMockRecorder {
|
|
8310
|
+
recording = false;
|
|
8311
|
+
name;
|
|
8312
|
+
startedAt = 0;
|
|
8313
|
+
capturedAt;
|
|
8314
|
+
pending = /* @__PURE__ */ new Map();
|
|
8315
|
+
exchanges = [];
|
|
8316
|
+
pushes = [];
|
|
8317
|
+
get isRecording() {
|
|
8318
|
+
return this.recording;
|
|
8319
|
+
}
|
|
8320
|
+
start(options = {}) {
|
|
8321
|
+
this.recording = true;
|
|
8322
|
+
this.name = options.name;
|
|
8323
|
+
this.startedAt = Date.now();
|
|
8324
|
+
this.capturedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
8325
|
+
this.pending.clear();
|
|
8326
|
+
this.exchanges = [];
|
|
8327
|
+
this.pushes = [];
|
|
8328
|
+
KosLog.info(
|
|
8329
|
+
`KosMock recorder started${options.name ? ` (${options.name})` : ""}`
|
|
8330
|
+
);
|
|
8331
|
+
}
|
|
8332
|
+
stop() {
|
|
8333
|
+
const fixture = this.toFixture();
|
|
8334
|
+
this.recording = false;
|
|
8335
|
+
KosLog.info(
|
|
8336
|
+
`KosMock recorder stopped: ${fixture.exchanges.length} exchanges, ${fixture.pushes.length} pushes`
|
|
8337
|
+
);
|
|
8338
|
+
return fixture;
|
|
8339
|
+
}
|
|
8340
|
+
toFixture() {
|
|
8341
|
+
return {
|
|
8342
|
+
version: 1,
|
|
8343
|
+
name: this.name,
|
|
8344
|
+
capturedAt: this.capturedAt,
|
|
8345
|
+
exchanges: [...this.exchanges],
|
|
8346
|
+
pushes: [...this.pushes]
|
|
8347
|
+
};
|
|
8348
|
+
}
|
|
8349
|
+
download(filename) {
|
|
8350
|
+
if (typeof document === "undefined") {
|
|
8351
|
+
KosLog.warn(`KosMock recorder download requires a browser environment`);
|
|
8352
|
+
return;
|
|
8353
|
+
}
|
|
8354
|
+
const fixture = this.toFixture();
|
|
8355
|
+
const blob = new Blob([JSON.stringify(fixture, null, 2)], {
|
|
8356
|
+
type: "application/json"
|
|
8357
|
+
});
|
|
8358
|
+
const url = URL.createObjectURL(blob);
|
|
8359
|
+
const anchor = document.createElement("a");
|
|
8360
|
+
anchor.href = url;
|
|
8361
|
+
anchor.download = filename ?? `${fixture.name ?? "kos-mock-fixture"}.json`;
|
|
8362
|
+
anchor.click();
|
|
8363
|
+
URL.revokeObjectURL(url);
|
|
8364
|
+
}
|
|
8365
|
+
elapsed() {
|
|
8366
|
+
return Date.now() - this.startedAt;
|
|
8367
|
+
}
|
|
8368
|
+
/** Interceptor tap: every outbound frame. Internal. */
|
|
8369
|
+
noteOutbound(data) {
|
|
8370
|
+
if (!this.recording) {
|
|
8371
|
+
return;
|
|
8372
|
+
}
|
|
8373
|
+
const frame = decodeRequestFrame(data);
|
|
8374
|
+
if (!frame) {
|
|
8375
|
+
return;
|
|
8376
|
+
}
|
|
8377
|
+
this.pending.set(frame.requestId, {
|
|
8378
|
+
tMs: this.elapsed(),
|
|
8379
|
+
method: frame.method,
|
|
8380
|
+
url: frame.url,
|
|
8381
|
+
tracker: frame.tracker,
|
|
8382
|
+
requestBody: frame.rawBody
|
|
8383
|
+
});
|
|
8384
|
+
}
|
|
8385
|
+
/** Interceptor tap: every inbound frame (real, mocked, injected). Internal. */
|
|
8386
|
+
noteInbound(frame) {
|
|
8387
|
+
if (!this.recording) {
|
|
8388
|
+
return;
|
|
8389
|
+
}
|
|
8390
|
+
let headers;
|
|
8391
|
+
let body;
|
|
8392
|
+
try {
|
|
8393
|
+
({ headers, body } = processKosMessage(frame));
|
|
8394
|
+
} catch {
|
|
8395
|
+
return;
|
|
8396
|
+
}
|
|
8397
|
+
const responseId = headers["response-id"];
|
|
8398
|
+
if (responseId) {
|
|
8399
|
+
const request2 = this.pending.get(responseId);
|
|
8400
|
+
if (!request2) {
|
|
8401
|
+
return;
|
|
8402
|
+
}
|
|
8403
|
+
this.pending.delete(responseId);
|
|
8404
|
+
this.exchanges.push({
|
|
8405
|
+
...request2,
|
|
8406
|
+
status: parseInt(headers["status"] ?? "200", 10) || 200,
|
|
8407
|
+
responseBody: body ?? "",
|
|
8408
|
+
latencyMs: Math.max(0, this.elapsed() - request2.tMs),
|
|
8409
|
+
...headers[KOS_MOCKED_HEADER] === "true" ? { mocked: true } : {}
|
|
8410
|
+
});
|
|
8411
|
+
return;
|
|
8412
|
+
}
|
|
8413
|
+
const topic = headers["subscription"] || headers["topic"];
|
|
8414
|
+
if (topic) {
|
|
8415
|
+
this.pushes.push({
|
|
8416
|
+
tMs: this.elapsed(),
|
|
8417
|
+
kind: "topic",
|
|
8418
|
+
topic,
|
|
8419
|
+
body: body ?? ""
|
|
8420
|
+
});
|
|
8421
|
+
return;
|
|
8422
|
+
}
|
|
8423
|
+
if (headers["type"] === KOS_FUTURE_TYPE) {
|
|
8424
|
+
let tracker;
|
|
8425
|
+
try {
|
|
8426
|
+
tracker = JSON.parse(body)?.tracker;
|
|
8427
|
+
} catch {
|
|
8428
|
+
}
|
|
8429
|
+
this.pushes.push({
|
|
8430
|
+
tMs: this.elapsed(),
|
|
8431
|
+
kind: "future",
|
|
8432
|
+
tracker,
|
|
8433
|
+
body: body ?? ""
|
|
8434
|
+
});
|
|
8435
|
+
}
|
|
8436
|
+
}
|
|
8437
|
+
}
|
|
8438
|
+
const dynamicImport = (url) => import(
|
|
8439
|
+
/* @vite-ignore */
|
|
8440
|
+
/* webpackIgnore: true */
|
|
8441
|
+
url
|
|
8442
|
+
);
|
|
8443
|
+
function createSetupStore(registry, deps = {}) {
|
|
8444
|
+
const loadModule = deps.loadModule ?? dynamicImport;
|
|
8445
|
+
const activeSetups = /* @__PURE__ */ new Map();
|
|
8446
|
+
const serverBase = () => (registry.fixtures.serverUrl ?? DEFAULT_FIXTURE_SERVER_URL).replace(
|
|
8447
|
+
/\/$/,
|
|
8448
|
+
""
|
|
8449
|
+
);
|
|
8450
|
+
const resolve = (nameOrUrl) => {
|
|
8451
|
+
if (/^https?:\/\//.test(nameOrUrl)) {
|
|
8452
|
+
const stem = nameOrUrl.split("/").pop() ?? nameOrUrl;
|
|
8453
|
+
return { name: stem.replace(/\.mjs$/, ""), url: nameOrUrl };
|
|
8454
|
+
}
|
|
8455
|
+
const name = nameOrUrl.replace(/\.mjs$/, "");
|
|
8456
|
+
return {
|
|
8457
|
+
name,
|
|
8458
|
+
url: `${serverBase()}/setups/${encodeURIComponent(name)}.mjs`
|
|
8459
|
+
};
|
|
8460
|
+
};
|
|
8461
|
+
const runTeardown = (name) => {
|
|
8462
|
+
const entry = activeSetups.get(name);
|
|
8463
|
+
if (!entry) {
|
|
8464
|
+
return false;
|
|
8465
|
+
}
|
|
8466
|
+
activeSetups.delete(name);
|
|
8467
|
+
try {
|
|
8468
|
+
entry.teardown?.();
|
|
8469
|
+
} catch (error) {
|
|
8470
|
+
KosLog.error(`KosMock setup "${name}" teardown threw`, error?.message);
|
|
8471
|
+
}
|
|
8472
|
+
return true;
|
|
8473
|
+
};
|
|
8474
|
+
const store = {
|
|
8475
|
+
async load(nameOrUrl) {
|
|
8476
|
+
const { name, url } = resolve(nameOrUrl);
|
|
8477
|
+
const module = await loadModule(url);
|
|
8478
|
+
const setup = module?.default;
|
|
8479
|
+
if (typeof setup !== "function") {
|
|
8480
|
+
throw new Error(
|
|
8481
|
+
`Setup module ${url} has no default-export function (see KosMockSetupModule)`
|
|
8482
|
+
);
|
|
8483
|
+
}
|
|
8484
|
+
runTeardown(name);
|
|
8485
|
+
const result = await setup(registry);
|
|
8486
|
+
const handle = {
|
|
8487
|
+
name,
|
|
8488
|
+
url,
|
|
8489
|
+
unload: () => {
|
|
8490
|
+
runTeardown(name);
|
|
8491
|
+
}
|
|
8492
|
+
};
|
|
8493
|
+
activeSetups.set(name, {
|
|
8494
|
+
handle,
|
|
8495
|
+
teardown: typeof result === "function" ? result : void 0
|
|
8496
|
+
});
|
|
8497
|
+
KosLog.info(`KosMock setup "${name}" installed from ${url}`);
|
|
8498
|
+
return handle;
|
|
8499
|
+
},
|
|
8500
|
+
async list() {
|
|
8501
|
+
const response = await fetch(`${serverBase()}/setups`);
|
|
8502
|
+
if (!response.ok) {
|
|
8503
|
+
return [];
|
|
8504
|
+
}
|
|
8505
|
+
const payload = await response.json();
|
|
8506
|
+
return payload.setups ?? [];
|
|
8507
|
+
},
|
|
8508
|
+
active() {
|
|
8509
|
+
return [...activeSetups.values()].map((entry) => entry.handle);
|
|
8510
|
+
},
|
|
8511
|
+
unload(name) {
|
|
8512
|
+
return runTeardown(name);
|
|
8513
|
+
},
|
|
8514
|
+
clear() {
|
|
8515
|
+
[...activeSetups.keys()].forEach((name) => runTeardown(name));
|
|
8516
|
+
}
|
|
8517
|
+
};
|
|
8518
|
+
return store;
|
|
8519
|
+
}
|
|
8520
|
+
const asWireValue = (value) => typeof value === "string" ? value : JSON.stringify(value);
|
|
8521
|
+
const configTopic = (beanId) => `/kos/config/${beanId}`;
|
|
8522
|
+
const changeEnvelope = (props, previousValues = {}) => {
|
|
8523
|
+
return {
|
|
8524
|
+
changes: Object.entries(props).map(([attr, value]) => {
|
|
8525
|
+
return {
|
|
8526
|
+
attr,
|
|
8527
|
+
currentValue: asWireValue(value),
|
|
8528
|
+
previousValue: previousValues[attr] === void 0 ? "" : asWireValue(previousValues[attr])
|
|
8529
|
+
};
|
|
8530
|
+
}),
|
|
8531
|
+
scopedChanges: [],
|
|
8532
|
+
errors: []
|
|
8533
|
+
};
|
|
8534
|
+
};
|
|
8535
|
+
function createConfigFacade(registry) {
|
|
8536
|
+
const update = (beanId, props, options = {}) => {
|
|
8537
|
+
registry.publishTopic(
|
|
8538
|
+
configTopic(beanId),
|
|
8539
|
+
changeEnvelope(props, options.previousValues)
|
|
8540
|
+
);
|
|
8541
|
+
};
|
|
8542
|
+
const inject = (beanId, injection) => {
|
|
8543
|
+
const current = { ...injection.props };
|
|
8544
|
+
const schema = injection.schema ?? {};
|
|
8545
|
+
const transformerHandle = registry.transformResponse(
|
|
8546
|
+
"GET",
|
|
8547
|
+
`/api/kos/config/details/${beanId}/:depth`,
|
|
8548
|
+
(body) => {
|
|
8549
|
+
if (!body || typeof body !== "object") {
|
|
8550
|
+
return void 0;
|
|
8551
|
+
}
|
|
8552
|
+
const details = Array.isArray(body.data?.details) ? body.data.details : void 0;
|
|
8553
|
+
if (!details) {
|
|
8554
|
+
return void 0;
|
|
8555
|
+
}
|
|
8556
|
+
const detail = details[0] ?? {};
|
|
8557
|
+
details[0] = {
|
|
8558
|
+
...detail,
|
|
8559
|
+
bean: { ...detail.bean, ...current },
|
|
8560
|
+
schema: {
|
|
8561
|
+
paths: [
|
|
8562
|
+
...detail.schema?.paths ?? [],
|
|
8563
|
+
...Object.keys(schema).filter(
|
|
8564
|
+
(key) => !(detail.schema?.paths ?? []).includes(key)
|
|
8565
|
+
)
|
|
8566
|
+
],
|
|
8567
|
+
schema: { ...detail.schema?.schema, ...schema }
|
|
8568
|
+
}
|
|
8569
|
+
};
|
|
8570
|
+
return body;
|
|
8571
|
+
},
|
|
8572
|
+
{ description: `config.inject ${beanId}` }
|
|
8573
|
+
);
|
|
8574
|
+
const routeHandle = registry.post(
|
|
8575
|
+
`/api/kos/config/${beanId}`,
|
|
8576
|
+
(req) => {
|
|
8577
|
+
const requested = req.json();
|
|
8578
|
+
if (!requested) {
|
|
8579
|
+
return void 0;
|
|
8580
|
+
}
|
|
8581
|
+
const keys = Object.keys(requested);
|
|
8582
|
+
const injectedKeys = keys.filter((key) => key in current);
|
|
8583
|
+
if (injectedKeys.length === 0) {
|
|
8584
|
+
return void 0;
|
|
8585
|
+
}
|
|
8586
|
+
if (injectedKeys.length < keys.length) {
|
|
8587
|
+
KosLog.warn(
|
|
8588
|
+
`KosMock config.inject ${beanId}: mixed write passes through; injected props will be rejected by the backend: ${injectedKeys.join(
|
|
8589
|
+
", "
|
|
8590
|
+
)}`
|
|
8591
|
+
);
|
|
8592
|
+
return void 0;
|
|
8593
|
+
}
|
|
8594
|
+
const previousValues = { ...current };
|
|
8595
|
+
Object.assign(current, requested);
|
|
8596
|
+
update(beanId, requested, { previousValues });
|
|
8597
|
+
return { data: {} };
|
|
8598
|
+
},
|
|
8599
|
+
{ description: `config.inject ${beanId} writes` }
|
|
8600
|
+
);
|
|
8601
|
+
const handle = {
|
|
8602
|
+
id: routeHandle.id,
|
|
8603
|
+
set: (props) => {
|
|
8604
|
+
const previousValues = { ...current };
|
|
8605
|
+
Object.assign(current, props);
|
|
8606
|
+
update(beanId, props, { previousValues });
|
|
8607
|
+
},
|
|
8608
|
+
remove: () => {
|
|
8609
|
+
transformerHandle.remove();
|
|
8610
|
+
routeHandle.remove();
|
|
8611
|
+
},
|
|
8612
|
+
enable: () => void 0,
|
|
8613
|
+
disable: () => {
|
|
8614
|
+
transformerHandle.remove();
|
|
8615
|
+
routeHandle.remove();
|
|
8616
|
+
}
|
|
8617
|
+
};
|
|
8618
|
+
return handle;
|
|
8619
|
+
};
|
|
8620
|
+
return { update, inject };
|
|
8621
|
+
}
|
|
8622
|
+
const PENDING_REQUEST_CAP = 500;
|
|
8623
|
+
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
8624
|
+
const asHandler = (handler) => typeof handler === "function" ? handler : () => handler;
|
|
8625
|
+
class KosMockRegistry {
|
|
8626
|
+
routes = [];
|
|
8627
|
+
settings = {};
|
|
8628
|
+
enabledFlag = false;
|
|
8629
|
+
injectors = /* @__PURE__ */ new Set();
|
|
8630
|
+
subscribedTopicSet = /* @__PURE__ */ new Set();
|
|
8631
|
+
emitters = /* @__PURE__ */ new Map();
|
|
8632
|
+
scenarioTeardowns = /* @__PURE__ */ new Map();
|
|
8633
|
+
cancelRoutesRegistered = false;
|
|
8634
|
+
state = /* @__PURE__ */ new Map();
|
|
8635
|
+
futures = new KosMockFutureSimulator({
|
|
8636
|
+
emitFrame: (frame) => this.publishFrame(frame),
|
|
8637
|
+
ensureCancelRoutes: () => this.ensureFutureCancelRoutes()
|
|
8638
|
+
});
|
|
8639
|
+
recorder = new KosMockRecorder();
|
|
8640
|
+
config = createConfigFacade(this);
|
|
8641
|
+
fixtures = createFixtureStore(this);
|
|
8642
|
+
setups = createSetupStore(this);
|
|
8643
|
+
/**
|
|
8644
|
+
* Internal: when set (a boot fixture is loading), the standalone socket
|
|
8645
|
+
* delays its open until this settles, so replayed routes exist before the
|
|
8646
|
+
* app's boot requests fire. Always resolves (errors are logged upstream).
|
|
8647
|
+
*/
|
|
8648
|
+
bootGate;
|
|
8649
|
+
observers = /* @__PURE__ */ new Set();
|
|
8650
|
+
transformers = [];
|
|
8651
|
+
pendingRequests = /* @__PURE__ */ new Map();
|
|
8652
|
+
transformResponse(method, pattern, transform, options = {}) {
|
|
8653
|
+
const registered = {
|
|
8654
|
+
id: options.id ?? uuid(),
|
|
8655
|
+
method,
|
|
8656
|
+
pattern,
|
|
8657
|
+
compiled: compileRoutePattern(pattern),
|
|
8658
|
+
transform,
|
|
8659
|
+
description: options.description
|
|
8660
|
+
};
|
|
8661
|
+
this.transformers.push(registered);
|
|
8662
|
+
const remove = () => {
|
|
8663
|
+
this.transformers = this.transformers.filter(
|
|
8664
|
+
(candidate) => candidate.id !== registered.id
|
|
8665
|
+
);
|
|
8666
|
+
};
|
|
8667
|
+
return {
|
|
8668
|
+
id: registered.id,
|
|
8669
|
+
remove,
|
|
8670
|
+
enable: () => void 0,
|
|
8671
|
+
disable: remove
|
|
8672
|
+
};
|
|
8673
|
+
}
|
|
8674
|
+
/**
|
|
8675
|
+
* Interceptor seam: patches a matching inbound response frame through the
|
|
8676
|
+
* transformer chain before it reaches the app. Returns the (possibly
|
|
8677
|
+
* modified) frame. Internal.
|
|
8678
|
+
*/
|
|
8679
|
+
transformInboundFrame(frame) {
|
|
8680
|
+
if (this.transformers.length === 0) {
|
|
8681
|
+
return frame;
|
|
8682
|
+
}
|
|
8683
|
+
let headers;
|
|
8684
|
+
let body;
|
|
8685
|
+
try {
|
|
8686
|
+
({ headers, body } = processKosMessage(frame));
|
|
8687
|
+
} catch {
|
|
8688
|
+
return frame;
|
|
8689
|
+
}
|
|
8690
|
+
const responseId = headers["response-id"];
|
|
8691
|
+
if (!responseId) {
|
|
8692
|
+
return frame;
|
|
8693
|
+
}
|
|
8694
|
+
const request2 = this.pendingRequests.get(responseId);
|
|
8695
|
+
this.pendingRequests.delete(responseId);
|
|
8696
|
+
if (!request2) {
|
|
8697
|
+
return frame;
|
|
8698
|
+
}
|
|
8699
|
+
const matching = this.transformers.filter(
|
|
8700
|
+
(transformer) => methodMatches(transformer.method, request2.method) && matchRoutePattern(transformer.compiled, request2.path) !== void 0
|
|
8701
|
+
);
|
|
8702
|
+
if (matching.length === 0) {
|
|
8703
|
+
return frame;
|
|
8704
|
+
}
|
|
8705
|
+
let parsed;
|
|
8706
|
+
try {
|
|
8707
|
+
parsed = body ? JSON.parse(body) : void 0;
|
|
8708
|
+
} catch {
|
|
8709
|
+
return frame;
|
|
8710
|
+
}
|
|
8711
|
+
const info = {
|
|
8712
|
+
request: request2,
|
|
8713
|
+
headers,
|
|
8714
|
+
status: parseInt(headers["status"] ?? "200", 10) || 200,
|
|
8715
|
+
mocked: headers[KOS_MOCKED_HEADER] === "true"
|
|
8716
|
+
};
|
|
8717
|
+
let current = parsed;
|
|
8718
|
+
let changed = false;
|
|
8719
|
+
for (const transformer of matching) {
|
|
8720
|
+
try {
|
|
8721
|
+
const result = transformer.transform(current, info);
|
|
8722
|
+
if (result !== void 0) {
|
|
8723
|
+
current = result;
|
|
8724
|
+
changed = true;
|
|
8725
|
+
}
|
|
8726
|
+
} catch (error) {
|
|
8727
|
+
KosLog.error(
|
|
8728
|
+
`KosMock transformer for ${transformer.method} ${transformer.pattern} threw`,
|
|
8729
|
+
error?.message
|
|
8730
|
+
);
|
|
8731
|
+
}
|
|
8732
|
+
}
|
|
8733
|
+
if (!changed) {
|
|
8734
|
+
return frame;
|
|
8735
|
+
}
|
|
8736
|
+
return encodeRawFrame(
|
|
8737
|
+
{ ...headers, [KOS_MOCK_TRANSFORMED_HEADER]: "true" },
|
|
8738
|
+
JSON.stringify(current)
|
|
8739
|
+
);
|
|
8740
|
+
}
|
|
8741
|
+
playbacks = /* @__PURE__ */ new Set();
|
|
8742
|
+
observe(observer3) {
|
|
8743
|
+
this.observers.add(observer3);
|
|
8744
|
+
return () => this.observers.delete(observer3);
|
|
8745
|
+
}
|
|
8746
|
+
/**
|
|
8747
|
+
* Transport-integration tap: every outbound frame passes through here
|
|
8748
|
+
* (recorder + observers), independent of the enabled flag. Internal.
|
|
8749
|
+
*/
|
|
8750
|
+
noteOutboundFrame(data) {
|
|
8751
|
+
this.recorder.noteOutbound(data);
|
|
8752
|
+
const hasRequestObservers = [...this.observers].some(
|
|
8753
|
+
(observer3) => observer3.onRequest
|
|
8754
|
+
);
|
|
8755
|
+
if (!hasRequestObservers && this.transformers.length === 0) {
|
|
8756
|
+
return;
|
|
8757
|
+
}
|
|
8758
|
+
const frame = decodeRequestFrame(data);
|
|
8759
|
+
if (!frame) {
|
|
8760
|
+
return;
|
|
8761
|
+
}
|
|
8762
|
+
const { path, query } = splitUrl(frame.url);
|
|
8763
|
+
const observation = {
|
|
8764
|
+
method: frame.method,
|
|
8765
|
+
url: frame.url,
|
|
8766
|
+
path,
|
|
8767
|
+
query,
|
|
8768
|
+
requestId: frame.requestId,
|
|
8769
|
+
tracker: frame.tracker,
|
|
8770
|
+
rawBody: frame.rawBody
|
|
8771
|
+
};
|
|
8772
|
+
if (this.transformers.length > 0) {
|
|
8773
|
+
if (this.pendingRequests.size >= PENDING_REQUEST_CAP) {
|
|
8774
|
+
const oldest = this.pendingRequests.keys().next().value;
|
|
8775
|
+
if (oldest !== void 0) {
|
|
8776
|
+
this.pendingRequests.delete(oldest);
|
|
8777
|
+
}
|
|
8778
|
+
}
|
|
8779
|
+
this.pendingRequests.set(frame.requestId, observation);
|
|
8780
|
+
}
|
|
8781
|
+
if (hasRequestObservers) {
|
|
8782
|
+
this.observers.forEach(
|
|
8783
|
+
(observer3) => this.notifyObserver(() => observer3.onRequest?.(observation))
|
|
8784
|
+
);
|
|
8785
|
+
}
|
|
8786
|
+
}
|
|
8787
|
+
/**
|
|
8788
|
+
* Transport-integration tap: every inbound frame (real, mocked, injected)
|
|
8789
|
+
* passes through here. Internal.
|
|
8790
|
+
*/
|
|
8791
|
+
noteInboundFrame(frame) {
|
|
8792
|
+
this.recorder.noteInbound(frame);
|
|
8793
|
+
if (this.observers.size === 0) {
|
|
8794
|
+
return;
|
|
8795
|
+
}
|
|
8796
|
+
let headers;
|
|
8797
|
+
let body;
|
|
8798
|
+
try {
|
|
8799
|
+
({ headers, body } = processKosMessage(frame));
|
|
8800
|
+
} catch {
|
|
8801
|
+
return;
|
|
8802
|
+
}
|
|
8803
|
+
const responseId = headers["response-id"];
|
|
8804
|
+
if (responseId) {
|
|
8805
|
+
const observation = {
|
|
8806
|
+
requestId: responseId,
|
|
8807
|
+
status: parseInt(headers["status"] ?? "200", 10) || 200,
|
|
8808
|
+
mocked: headers[KOS_MOCKED_HEADER] === "true",
|
|
8809
|
+
fixture: headers[KOS_MOCK_FIXTURE_HEADER],
|
|
8810
|
+
body: body ?? "",
|
|
8811
|
+
headers
|
|
8812
|
+
};
|
|
8813
|
+
this.observers.forEach(
|
|
8814
|
+
(observer3) => this.notifyObserver(() => observer3.onResponse?.(observation))
|
|
8815
|
+
);
|
|
8816
|
+
return;
|
|
8817
|
+
}
|
|
8818
|
+
const topic = headers["subscription"] || headers["topic"];
|
|
8819
|
+
if (topic) {
|
|
8820
|
+
const observation = { topic, body: body ?? "", headers };
|
|
8821
|
+
this.observers.forEach(
|
|
8822
|
+
(observer3) => this.notifyObserver(() => observer3.onTopic?.(observation))
|
|
8823
|
+
);
|
|
8824
|
+
return;
|
|
8825
|
+
}
|
|
8826
|
+
if (headers["type"] === KOS_FUTURE_TYPE) {
|
|
8827
|
+
let future;
|
|
8828
|
+
try {
|
|
8829
|
+
future = JSON.parse(body);
|
|
8830
|
+
} catch {
|
|
8831
|
+
return;
|
|
8832
|
+
}
|
|
8833
|
+
this.observers.forEach(
|
|
8834
|
+
(observer3) => this.notifyObserver(() => observer3.onFuture?.(future))
|
|
8835
|
+
);
|
|
8836
|
+
}
|
|
8837
|
+
}
|
|
8838
|
+
/** An observer throwing must never disturb transport flow. */
|
|
8839
|
+
notifyObserver(notify) {
|
|
8840
|
+
try {
|
|
8841
|
+
notify();
|
|
8842
|
+
} catch (error) {
|
|
8843
|
+
KosLog.error(`KosMock observer threw`, error?.message);
|
|
8844
|
+
}
|
|
8845
|
+
}
|
|
8846
|
+
loadFixture(fixture, options) {
|
|
8847
|
+
const inner = installFixturePlayback(this, fixture, options);
|
|
8848
|
+
const handle = {
|
|
8849
|
+
name: inner.name,
|
|
8850
|
+
startPushes: inner.startPushes,
|
|
8851
|
+
stop: () => {
|
|
8852
|
+
this.playbacks.delete(handle);
|
|
8853
|
+
inner.stop();
|
|
8854
|
+
}
|
|
8855
|
+
};
|
|
8856
|
+
this.playbacks.add(handle);
|
|
8857
|
+
return handle;
|
|
8858
|
+
}
|
|
8859
|
+
// -- activation ------------------------------------------------------------
|
|
8860
|
+
get enabled() {
|
|
8861
|
+
return this.enabledFlag;
|
|
8862
|
+
}
|
|
8863
|
+
enable() {
|
|
8864
|
+
if (!this.enabledFlag) {
|
|
8865
|
+
this.enabledFlag = true;
|
|
8866
|
+
KosLog.info(`KosMock enabled`);
|
|
8867
|
+
}
|
|
8868
|
+
}
|
|
8869
|
+
disable() {
|
|
8870
|
+
if (this.enabledFlag) {
|
|
8871
|
+
this.enabledFlag = false;
|
|
8872
|
+
KosLog.info(`KosMock disabled`);
|
|
8873
|
+
}
|
|
8874
|
+
}
|
|
8875
|
+
get standalone() {
|
|
8876
|
+
return !!this.settings.standalone;
|
|
8877
|
+
}
|
|
8878
|
+
/** Internal: whether the standalone socket self-authorizes on open. */
|
|
8879
|
+
get authorizeOnOpen() {
|
|
8880
|
+
return this.settings.authorize !== false;
|
|
8881
|
+
}
|
|
8882
|
+
configure(config2) {
|
|
8883
|
+
this.settings = { ...this.settings, ...config2 };
|
|
8884
|
+
if (config2.standalone) {
|
|
8885
|
+
this.enable();
|
|
8886
|
+
}
|
|
8887
|
+
}
|
|
8888
|
+
// -- routes ----------------------------------------------------------------
|
|
8889
|
+
route(method, pattern, handler, options = {}) {
|
|
8890
|
+
const registered = {
|
|
8891
|
+
id: options.id ?? uuid(),
|
|
8892
|
+
method,
|
|
8893
|
+
pattern,
|
|
8894
|
+
compiled: compileRoutePattern(pattern),
|
|
8895
|
+
handler: asHandler(handler),
|
|
8896
|
+
description: options.description,
|
|
8897
|
+
once: !!options.once,
|
|
8898
|
+
enabled: options.enabled !== false,
|
|
8899
|
+
delayMs: options.delayMs,
|
|
8900
|
+
fixture: options.fixture,
|
|
8901
|
+
when: options.when,
|
|
8902
|
+
hitCount: 0
|
|
8903
|
+
};
|
|
8904
|
+
this.routes.push(registered);
|
|
8905
|
+
this.enable();
|
|
8906
|
+
return {
|
|
8907
|
+
id: registered.id,
|
|
8908
|
+
remove: () => this.removeRoute(registered.id),
|
|
8909
|
+
enable: () => this.setRouteEnabled(registered.id, true),
|
|
8910
|
+
disable: () => this.setRouteEnabled(registered.id, false)
|
|
8911
|
+
};
|
|
8912
|
+
}
|
|
8913
|
+
get(pattern, handler, options) {
|
|
8914
|
+
return this.route("GET", pattern, handler, options);
|
|
8915
|
+
}
|
|
8916
|
+
post(pattern, handler, options) {
|
|
8917
|
+
return this.route("POST", pattern, handler, options);
|
|
8918
|
+
}
|
|
8919
|
+
put(pattern, handler, options) {
|
|
8920
|
+
return this.route("PUT", pattern, handler, options);
|
|
8921
|
+
}
|
|
8922
|
+
del(pattern, handler, options) {
|
|
8923
|
+
return this.route("DELETE", pattern, handler, options);
|
|
8924
|
+
}
|
|
8925
|
+
removeRoute(id) {
|
|
8926
|
+
this.routes = this.routes.filter((route) => route.id !== id);
|
|
8927
|
+
}
|
|
8928
|
+
setRouteEnabled(id, enabled) {
|
|
8929
|
+
const route = this.routes.find((candidate) => candidate.id === id);
|
|
8930
|
+
if (route) {
|
|
8931
|
+
route.enabled = enabled;
|
|
8932
|
+
}
|
|
8933
|
+
}
|
|
8934
|
+
// -- inbound injection -----------------------------------------------------
|
|
8935
|
+
/**
|
|
8936
|
+
* Transport integrations register how inbound frames reach the receive
|
|
8937
|
+
* path. Internal — not part of IKosMockControl.
|
|
8938
|
+
*/
|
|
8939
|
+
registerInjector(inject) {
|
|
8940
|
+
this.injectors.add(inject);
|
|
8941
|
+
return () => this.injectors.delete(inject);
|
|
8942
|
+
}
|
|
8943
|
+
publishTopic(topic, body, headers) {
|
|
8944
|
+
this.publishFrame(encodeTopicFrame(topic, body, headers));
|
|
8945
|
+
}
|
|
8946
|
+
publishFrame(rawFrame) {
|
|
8947
|
+
if (this.injectors.size === 0) {
|
|
8948
|
+
KosLog.warn(
|
|
8949
|
+
`KosMock.publishFrame: no active transport to deliver the frame`
|
|
8950
|
+
);
|
|
8951
|
+
return;
|
|
8952
|
+
}
|
|
8953
|
+
this.injectors.forEach((inject) => inject(rawFrame));
|
|
8954
|
+
}
|
|
8955
|
+
topicEmitter(topic, options) {
|
|
8956
|
+
const id = uuid();
|
|
8957
|
+
let tick = 0;
|
|
8958
|
+
const start = () => {
|
|
8959
|
+
const timer = setInterval(() => {
|
|
8960
|
+
this.publishTopic(topic, options.body(tick));
|
|
8961
|
+
tick += 1;
|
|
8962
|
+
}, options.intervalMs);
|
|
8963
|
+
this.emitters.set(id, timer);
|
|
8964
|
+
};
|
|
8965
|
+
const stop = () => {
|
|
8966
|
+
const timer = this.emitters.get(id);
|
|
8967
|
+
if (timer !== void 0) {
|
|
8968
|
+
clearInterval(timer);
|
|
8969
|
+
this.emitters.delete(id);
|
|
8970
|
+
}
|
|
8971
|
+
};
|
|
8972
|
+
start();
|
|
8973
|
+
return {
|
|
8974
|
+
id,
|
|
8975
|
+
remove: stop,
|
|
8976
|
+
enable: () => {
|
|
8977
|
+
if (!this.emitters.has(id)) {
|
|
8978
|
+
start();
|
|
8979
|
+
}
|
|
8980
|
+
},
|
|
8981
|
+
disable: stop
|
|
8982
|
+
};
|
|
8983
|
+
}
|
|
8984
|
+
// -- scenarios ---------------------------------------------------------------
|
|
8985
|
+
scenario(scenario) {
|
|
8986
|
+
const id = `scenario:${scenario.name}:${uuid()}`;
|
|
8987
|
+
const teardown = scenario.setup(this);
|
|
8988
|
+
if (teardown) {
|
|
8989
|
+
this.scenarioTeardowns.set(id, teardown);
|
|
8990
|
+
}
|
|
8991
|
+
KosLog.info(`KosMock scenario "${scenario.name}" installed`);
|
|
8992
|
+
const remove = () => {
|
|
8993
|
+
const dispose = this.scenarioTeardowns.get(id);
|
|
8994
|
+
this.scenarioTeardowns.delete(id);
|
|
8995
|
+
dispose?.();
|
|
8996
|
+
};
|
|
8997
|
+
return { id, remove, enable: () => void 0, disable: remove };
|
|
8998
|
+
}
|
|
8999
|
+
// -- introspection -----------------------------------------------------------
|
|
9000
|
+
list() {
|
|
9001
|
+
return this.routes.map((route) => {
|
|
9002
|
+
return {
|
|
9003
|
+
id: route.id,
|
|
9004
|
+
method: route.method,
|
|
9005
|
+
pattern: route.pattern,
|
|
9006
|
+
description: route.description,
|
|
9007
|
+
enabled: route.enabled,
|
|
9008
|
+
hitCount: route.hitCount
|
|
9009
|
+
};
|
|
9010
|
+
});
|
|
9011
|
+
}
|
|
9012
|
+
describe() {
|
|
9013
|
+
return {
|
|
9014
|
+
enabled: this.enabled,
|
|
9015
|
+
standalone: this.standalone,
|
|
9016
|
+
routes: this.list(),
|
|
9017
|
+
transformers: this.transformers.map((transformer) => {
|
|
9018
|
+
return {
|
|
9019
|
+
id: transformer.id,
|
|
9020
|
+
method: transformer.method,
|
|
9021
|
+
pattern: transformer.pattern,
|
|
9022
|
+
description: transformer.description
|
|
9023
|
+
};
|
|
9024
|
+
}),
|
|
9025
|
+
setups: this.setups.active().map((setup) => {
|
|
9026
|
+
return { name: setup.name, url: setup.url };
|
|
9027
|
+
}),
|
|
9028
|
+
activeFutures: this.futures.active.map((future) => future.id),
|
|
9029
|
+
subscribedTopics: this.subscribedTopics
|
|
9030
|
+
};
|
|
9031
|
+
}
|
|
9032
|
+
get subscribedTopics() {
|
|
9033
|
+
return [...this.subscribedTopicSet];
|
|
9034
|
+
}
|
|
9035
|
+
clear() {
|
|
9036
|
+
this.setups.clear();
|
|
9037
|
+
[...this.playbacks].forEach((playback) => playback.stop());
|
|
9038
|
+
this.routes = [];
|
|
9039
|
+
this.transformers = [];
|
|
9040
|
+
this.pendingRequests.clear();
|
|
9041
|
+
this.cancelRoutesRegistered = false;
|
|
9042
|
+
this.emitters.forEach((timer) => clearInterval(timer));
|
|
9043
|
+
this.emitters.clear();
|
|
9044
|
+
this.scenarioTeardowns.forEach((teardown) => teardown());
|
|
9045
|
+
this.scenarioTeardowns.clear();
|
|
9046
|
+
this.futures.clear();
|
|
9047
|
+
this.state.clear();
|
|
9048
|
+
this.subscribedTopicSet.clear();
|
|
9049
|
+
}
|
|
9050
|
+
// -- frame handling (internal seam for transport integrations) --------------
|
|
9051
|
+
/**
|
|
9052
|
+
* Inspects an outbound frame. Returns true when the registry takes
|
|
9053
|
+
* ownership (it will deliver a response or forward asynchronously); false
|
|
9054
|
+
* means the caller sends the frame as usual. Internal — not part of
|
|
9055
|
+
* IKosMockControl.
|
|
9056
|
+
*/
|
|
9057
|
+
handleOutboundFrame(data, io) {
|
|
9058
|
+
if (!this.enabledFlag) {
|
|
9059
|
+
return false;
|
|
9060
|
+
}
|
|
9061
|
+
const subscription = decodeSubscribeTopics(data);
|
|
9062
|
+
if (subscription) {
|
|
9063
|
+
subscription.topics.forEach((topic) => {
|
|
9064
|
+
if (subscription.type === "subscribe") {
|
|
9065
|
+
this.subscribedTopicSet.add(topic);
|
|
9066
|
+
} else {
|
|
9067
|
+
this.subscribedTopicSet.delete(topic);
|
|
9068
|
+
}
|
|
9069
|
+
});
|
|
9070
|
+
return false;
|
|
9071
|
+
}
|
|
9072
|
+
const frame = decodeRequestFrame(data);
|
|
9073
|
+
if (!frame) {
|
|
9074
|
+
return false;
|
|
9075
|
+
}
|
|
9076
|
+
const { path, query } = splitUrl(frame.url);
|
|
9077
|
+
const candidates = this.routes.map((route) => {
|
|
9078
|
+
return {
|
|
9079
|
+
route,
|
|
9080
|
+
params: route.enabled && methodMatches(route.method, frame.method) ? matchRoutePattern(route.compiled, path) : void 0
|
|
9081
|
+
};
|
|
9082
|
+
}).filter(
|
|
9083
|
+
(candidate) => candidate.params !== void 0
|
|
9084
|
+
);
|
|
9085
|
+
const policy = this.unmatchedPolicy();
|
|
9086
|
+
if (candidates.length === 0) {
|
|
9087
|
+
if (policy === "respond404") {
|
|
9088
|
+
this.deliverResponse(io, frame.requestId, {
|
|
9089
|
+
status: 404,
|
|
9090
|
+
data: { message: `KosMock: no route for ${frame.method} ${path}` }
|
|
9091
|
+
});
|
|
9092
|
+
return true;
|
|
9093
|
+
}
|
|
9094
|
+
if (policy === "warn") {
|
|
9095
|
+
KosLog.warn(`KosMock: unmatched ${frame.method} ${path}`);
|
|
9096
|
+
}
|
|
9097
|
+
return false;
|
|
9098
|
+
}
|
|
9099
|
+
const request2 = {
|
|
9100
|
+
method: frame.method,
|
|
9101
|
+
url: frame.url,
|
|
9102
|
+
path,
|
|
9103
|
+
query,
|
|
9104
|
+
headers: frame.headers,
|
|
9105
|
+
requestId: frame.requestId,
|
|
9106
|
+
tracker: frame.tracker,
|
|
9107
|
+
ordered: frame.ordered,
|
|
9108
|
+
rawBody: frame.rawBody,
|
|
9109
|
+
json: () => {
|
|
9110
|
+
if (!frame.rawBody) {
|
|
9111
|
+
return void 0;
|
|
9112
|
+
}
|
|
9113
|
+
try {
|
|
9114
|
+
return JSON.parse(frame.rawBody);
|
|
9115
|
+
} catch {
|
|
9116
|
+
return void 0;
|
|
9117
|
+
}
|
|
9118
|
+
}
|
|
9119
|
+
};
|
|
9120
|
+
void this.resolve(request2, candidates, io, policy);
|
|
9121
|
+
return true;
|
|
9122
|
+
}
|
|
9123
|
+
async resolve(request2, candidates, io, policy) {
|
|
9124
|
+
for (const { route, params: params2 } of candidates) {
|
|
9125
|
+
const scopedRequest = { ...request2, params: params2 };
|
|
9126
|
+
if (route.when) {
|
|
9127
|
+
let accepted = false;
|
|
9128
|
+
try {
|
|
9129
|
+
accepted = route.when(scopedRequest);
|
|
9130
|
+
} catch (error) {
|
|
9131
|
+
KosLog.error(
|
|
9132
|
+
`KosMock \`when\` guard for ${route.method} ${route.pattern} threw`,
|
|
9133
|
+
error?.message
|
|
9134
|
+
);
|
|
9135
|
+
}
|
|
9136
|
+
if (!accepted) {
|
|
9137
|
+
continue;
|
|
9138
|
+
}
|
|
9139
|
+
}
|
|
9140
|
+
let response;
|
|
9141
|
+
try {
|
|
9142
|
+
response = await route.handler(scopedRequest, this.context());
|
|
9143
|
+
} catch (error) {
|
|
9144
|
+
KosLog.error(
|
|
9145
|
+
`KosMock handler for ${route.method} ${route.pattern} threw`,
|
|
9146
|
+
error?.message
|
|
9147
|
+
);
|
|
9148
|
+
response = {
|
|
9149
|
+
status: 500,
|
|
9150
|
+
data: { message: `KosMock handler error: ${error?.message}` }
|
|
9151
|
+
};
|
|
9152
|
+
}
|
|
9153
|
+
if (!response) {
|
|
9154
|
+
continue;
|
|
9155
|
+
}
|
|
9156
|
+
const delayMs = response.delayMs ?? route.delayMs ?? this.settings.defaultDelayMs;
|
|
9157
|
+
if (delayMs) {
|
|
9158
|
+
await sleep(delayMs);
|
|
9159
|
+
}
|
|
9160
|
+
route.hitCount += 1;
|
|
9161
|
+
if (route.once) {
|
|
9162
|
+
this.removeRoute(route.id);
|
|
9163
|
+
}
|
|
9164
|
+
this.deliverResponse(io, request2.requestId, response, route);
|
|
9165
|
+
return;
|
|
9166
|
+
}
|
|
9167
|
+
if (policy === "respond404") {
|
|
9168
|
+
this.deliverResponse(io, request2.requestId, {
|
|
9169
|
+
status: 404,
|
|
9170
|
+
data: {
|
|
9171
|
+
message: `KosMock: all routes declined ${request2.method} ${request2.path}`
|
|
9172
|
+
}
|
|
9173
|
+
});
|
|
9174
|
+
return;
|
|
9175
|
+
}
|
|
9176
|
+
if (policy === "warn") {
|
|
9177
|
+
KosLog.warn(
|
|
9178
|
+
`KosMock: all routes declined ${request2.method} ${request2.path}`
|
|
9179
|
+
);
|
|
9180
|
+
}
|
|
9181
|
+
io.forward();
|
|
9182
|
+
}
|
|
9183
|
+
/**
|
|
9184
|
+
* Every mock-origin response is stamped `kos-mocked: true` (plus
|
|
9185
|
+
* `kos-mock-fixture` when the route belongs to a fixture) so mocked traffic
|
|
9186
|
+
* is detectable in devtools and on the fetch response headers.
|
|
9187
|
+
*/
|
|
9188
|
+
deliverResponse(io, requestId, response, route) {
|
|
9189
|
+
io.deliver(
|
|
9190
|
+
encodeHttpResponseFrame({
|
|
9191
|
+
requestId,
|
|
9192
|
+
status: response.status,
|
|
9193
|
+
data: response.data,
|
|
9194
|
+
raw: response.raw,
|
|
9195
|
+
headers: {
|
|
9196
|
+
[KOS_MOCKED_HEADER]: "true",
|
|
9197
|
+
...route?.fixture ? { [KOS_MOCK_FIXTURE_HEADER]: route.fixture } : {},
|
|
9198
|
+
...response.headers
|
|
9199
|
+
}
|
|
9200
|
+
})
|
|
9201
|
+
);
|
|
9202
|
+
}
|
|
9203
|
+
context() {
|
|
9204
|
+
return {
|
|
9205
|
+
state: this.state,
|
|
9206
|
+
futures: this.futures,
|
|
9207
|
+
publishTopic: (topic, body, headers) => this.publishTopic(topic, body, headers)
|
|
9208
|
+
};
|
|
9209
|
+
}
|
|
9210
|
+
unmatchedPolicy() {
|
|
9211
|
+
return this.settings.onUnmatched ?? (this.standalone ? "respond404" : "passthrough");
|
|
9212
|
+
}
|
|
9213
|
+
/**
|
|
9214
|
+
* Shared cancel routes for simulated futures. They decline unknown ids, so
|
|
9215
|
+
* in hybrid mode cancellation of real futures still reaches the device.
|
|
9216
|
+
*/
|
|
9217
|
+
ensureFutureCancelRoutes() {
|
|
9218
|
+
if (this.cancelRoutesRegistered) {
|
|
9219
|
+
return;
|
|
9220
|
+
}
|
|
9221
|
+
this.cancelRoutesRegistered = true;
|
|
9222
|
+
const cancelHandler = (req) => {
|
|
9223
|
+
const future = this.futures.get(req.params.id);
|
|
9224
|
+
if (!future || future.done) {
|
|
9225
|
+
return void 0;
|
|
9226
|
+
}
|
|
9227
|
+
future.cancel();
|
|
9228
|
+
return { data: {} };
|
|
9229
|
+
};
|
|
9230
|
+
this.route("POST", "/api/kos/future/:id/cancel", cancelHandler, {
|
|
9231
|
+
id: "kos-mock-future-cancel",
|
|
9232
|
+
description: "KosMock simulated future cancel"
|
|
9233
|
+
});
|
|
9234
|
+
this.route("DELETE", "/api/kos/future/:id", cancelHandler, {
|
|
9235
|
+
id: "kos-mock-future-delete",
|
|
9236
|
+
description: "KosMock simulated future delete"
|
|
9237
|
+
});
|
|
9238
|
+
}
|
|
9239
|
+
}
|
|
9240
|
+
const resolveSingleton = () => {
|
|
9241
|
+
if (typeof window !== "undefined") {
|
|
9242
|
+
const existing = window.KosMock;
|
|
9243
|
+
if (existing instanceof KosMockRegistry) {
|
|
9244
|
+
return existing;
|
|
9245
|
+
}
|
|
9246
|
+
const registry = new KosMockRegistry();
|
|
9247
|
+
window.KosMock = registry;
|
|
9248
|
+
return registry;
|
|
9249
|
+
}
|
|
9250
|
+
return new KosMockRegistry();
|
|
9251
|
+
};
|
|
9252
|
+
const kosMockRegistry = resolveSingleton();
|
|
9253
|
+
const KosMock = kosMockRegistry;
|
|
9254
|
+
if (typeof window !== "undefined" && window.location?.search) {
|
|
9255
|
+
const params2 = getQueryParams();
|
|
9256
|
+
if (params2["kosMock"] === "standalone") {
|
|
9257
|
+
KosMock.configure({ standalone: true });
|
|
9258
|
+
} else if (params2["kosMock"] === "on") {
|
|
9259
|
+
KosMock.enable();
|
|
9260
|
+
}
|
|
9261
|
+
if (params2["kosRecord"] !== void 0) {
|
|
9262
|
+
KosMock.recorder.start({ name: params2["kosRecord"] || void 0 });
|
|
9263
|
+
}
|
|
9264
|
+
if (params2["kosFixtureServer"] !== void 0) {
|
|
9265
|
+
KosMock.fixtures.useServer(params2["kosFixtureServer"] || true);
|
|
9266
|
+
}
|
|
9267
|
+
const bootLoads = [];
|
|
9268
|
+
if (params2["kosFixture"]) {
|
|
9269
|
+
bootLoads.push(
|
|
9270
|
+
KosMock.fixtures.play(params2["kosFixture"]).then(() => void 0).catch(
|
|
9271
|
+
(error) => KosLog.error(
|
|
9272
|
+
`KosMock: boot fixture "${params2["kosFixture"]}" failed to load`,
|
|
9273
|
+
error?.message
|
|
9274
|
+
)
|
|
9275
|
+
)
|
|
9276
|
+
);
|
|
9277
|
+
}
|
|
9278
|
+
if (params2["kosSetup"]) {
|
|
9279
|
+
for (const name of params2["kosSetup"].split(",").filter(Boolean)) {
|
|
9280
|
+
bootLoads.push(
|
|
9281
|
+
KosMock.setups.load(name).then(() => void 0).catch(
|
|
9282
|
+
(error) => KosLog.error(
|
|
9283
|
+
`KosMock: boot setup "${name}" failed to load`,
|
|
9284
|
+
error?.message
|
|
9285
|
+
)
|
|
9286
|
+
)
|
|
9287
|
+
);
|
|
9288
|
+
}
|
|
9289
|
+
}
|
|
9290
|
+
if (bootLoads.length > 0) {
|
|
9291
|
+
kosMockRegistry.bootGate = Promise.all(bootLoads).then(() => void 0);
|
|
9292
|
+
}
|
|
9293
|
+
}
|
|
9294
|
+
class MockInterceptorTransport {
|
|
9295
|
+
constructor(inner, registry) {
|
|
9296
|
+
this.inner = inner;
|
|
9297
|
+
this.registry = registry;
|
|
9298
|
+
this.unregisterInjector = registry.registerInjector(
|
|
9299
|
+
(frame) => this.deliver(frame)
|
|
9300
|
+
);
|
|
9301
|
+
inner.onmessage = (ev) => {
|
|
9302
|
+
if (typeof ev.data === "string") {
|
|
9303
|
+
this.deliver(ev.data);
|
|
9304
|
+
} else {
|
|
9305
|
+
this.messageHandler?.(ev);
|
|
9306
|
+
}
|
|
9307
|
+
};
|
|
9308
|
+
inner.onopen = () => {
|
|
9309
|
+
const ev = { currentTarget: this };
|
|
9310
|
+
this.openHandler?.(ev);
|
|
9311
|
+
};
|
|
9312
|
+
inner.onclose = (ev) => {
|
|
9313
|
+
this.unregisterInjector();
|
|
9314
|
+
this.closeHandler?.(ev);
|
|
9315
|
+
};
|
|
9316
|
+
inner.onerror = (ev) => this.errorHandler?.(ev);
|
|
9317
|
+
}
|
|
9318
|
+
inner;
|
|
9319
|
+
registry;
|
|
9320
|
+
messageHandler = null;
|
|
9321
|
+
openHandler = null;
|
|
9322
|
+
closeHandler = null;
|
|
9323
|
+
errorHandler = null;
|
|
9324
|
+
unregisterInjector;
|
|
9325
|
+
/**
|
|
9326
|
+
* Every inbound frame's path to the app: transform → deliver → observe.
|
|
9327
|
+
* Observation runs last so a reacting observer's injections land AFTER the
|
|
9328
|
+
* frame that caused them, as a real backend's consequences would; observers
|
|
9329
|
+
* and the recorder see the transformed frame — what the app saw.
|
|
9330
|
+
*/
|
|
9331
|
+
deliver(frame) {
|
|
9332
|
+
const transformed = this.registry.transformInboundFrame(frame);
|
|
9333
|
+
this.messageHandler?.({ data: transformed });
|
|
9334
|
+
this.registry.noteInboundFrame(transformed);
|
|
9335
|
+
}
|
|
9336
|
+
get onmessage() {
|
|
9337
|
+
return this.messageHandler;
|
|
9338
|
+
}
|
|
9339
|
+
set onmessage(handler) {
|
|
9340
|
+
this.messageHandler = handler;
|
|
9341
|
+
}
|
|
9342
|
+
get onopen() {
|
|
9343
|
+
return this.openHandler;
|
|
9344
|
+
}
|
|
9345
|
+
set onopen(handler) {
|
|
9346
|
+
this.openHandler = handler;
|
|
9347
|
+
}
|
|
9348
|
+
get onclose() {
|
|
9349
|
+
return this.closeHandler;
|
|
9350
|
+
}
|
|
9351
|
+
set onclose(handler) {
|
|
9352
|
+
this.closeHandler = handler;
|
|
9353
|
+
}
|
|
9354
|
+
get onerror() {
|
|
9355
|
+
return this.errorHandler;
|
|
9356
|
+
}
|
|
9357
|
+
set onerror(handler) {
|
|
9358
|
+
this.errorHandler = handler;
|
|
9359
|
+
}
|
|
9360
|
+
send(data) {
|
|
9361
|
+
this.registry.noteOutboundFrame(data);
|
|
9362
|
+
const owned = this.registry.handleOutboundFrame(data, {
|
|
9363
|
+
deliver: (frame) => this.deliver(frame),
|
|
9364
|
+
forward: () => this.inner.send(data)
|
|
9365
|
+
});
|
|
9366
|
+
if (!owned) {
|
|
9367
|
+
this.inner.send(data);
|
|
9368
|
+
}
|
|
9369
|
+
}
|
|
9370
|
+
close() {
|
|
9371
|
+
this.unregisterInjector();
|
|
9372
|
+
return this.inner.close();
|
|
9373
|
+
}
|
|
9374
|
+
addEventListener(type, listener, options) {
|
|
9375
|
+
this.inner.addEventListener(type, listener, options);
|
|
9376
|
+
}
|
|
9377
|
+
removeEventListener(type, listener, options) {
|
|
9378
|
+
this.inner.removeEventListener(type, listener, options);
|
|
9379
|
+
}
|
|
9380
|
+
dispatchEvent(event) {
|
|
9381
|
+
return this.inner.dispatchEvent(event);
|
|
9382
|
+
}
|
|
9383
|
+
}
|
|
9384
|
+
function installMockInterceptor(inner, registry = kosMockRegistry) {
|
|
9385
|
+
return new MockInterceptorTransport(inner, registry);
|
|
9386
|
+
}
|
|
7575
9387
|
class WebSocketBridgeTransport extends WebSocket {
|
|
7576
9388
|
constructor(address) {
|
|
7577
9389
|
super(address);
|
|
@@ -7586,13 +9398,13 @@ var WebSocketEvents = /* @__PURE__ */ ((WebSocketEvents2) => {
|
|
|
7586
9398
|
return WebSocketEvents2;
|
|
7587
9399
|
})(WebSocketEvents || {});
|
|
7588
9400
|
function processKosMessage(data) {
|
|
7589
|
-
const
|
|
9401
|
+
const MESSAGE_SEPARATOR2 = `
|
|
7590
9402
|
|
|
7591
9403
|
`;
|
|
7592
9404
|
const SEPARATOR = `
|
|
7593
9405
|
`;
|
|
7594
9406
|
const HEADER_SEPARATOR = /:(.*)/s;
|
|
7595
|
-
const [headerText, body] = data.split(
|
|
9407
|
+
const [headerText, body] = data.split(MESSAGE_SEPARATOR2).map((line) => line.trim());
|
|
7596
9408
|
const headers = headerText.split(SEPARATOR).reduce((acc, curr) => {
|
|
7597
9409
|
const [header, value] = curr.split(HEADER_SEPARATOR).map((raw) => raw.trim());
|
|
7598
9410
|
if (header && value) {
|
|
@@ -7606,13 +9418,13 @@ function processKosMessage(data) {
|
|
|
7606
9418
|
};
|
|
7607
9419
|
}
|
|
7608
9420
|
function processKosSnifferMessage(data) {
|
|
7609
|
-
const
|
|
9421
|
+
const MESSAGE_SEPARATOR2 = `
|
|
7610
9422
|
|
|
7611
9423
|
`;
|
|
7612
9424
|
const SEPARATOR = `
|
|
7613
9425
|
`;
|
|
7614
9426
|
const HEADER_SEPARATOR = /:(.*)/s;
|
|
7615
|
-
const [, headerText, body] = data.split(
|
|
9427
|
+
const [, headerText, body] = data.split(MESSAGE_SEPARATOR2).map((line) => line.trim());
|
|
7616
9428
|
const headers = headerText.split(SEPARATOR).reduce((acc, curr) => {
|
|
7617
9429
|
const [header, value] = curr.split(HEADER_SEPARATOR).map((raw) => raw.trim());
|
|
7618
9430
|
if (header && value) {
|
|
@@ -7651,8 +9463,8 @@ class BaseTransport {
|
|
|
7651
9463
|
this.onConnectionEstablished = onConnectionEstablished;
|
|
7652
9464
|
this.onConnectionLost = onConnectionLost;
|
|
7653
9465
|
this.alias = alias;
|
|
7654
|
-
this.socket =
|
|
7655
|
-
`${this.protocol}${this.host}:${this.port}/events`
|
|
9466
|
+
this.socket = installMockInterceptor(
|
|
9467
|
+
new Transport2(`${this.protocol}${this.host}:${this.port}/events`)
|
|
7656
9468
|
);
|
|
7657
9469
|
this.socket.onmessage = this.onMessage.bind(this);
|
|
7658
9470
|
this.socket.onopen = this.onOpen.bind(this);
|
|
@@ -7660,15 +9472,6 @@ class BaseTransport {
|
|
|
7660
9472
|
this.socket.onerror = this.onError.bind(this);
|
|
7661
9473
|
this.oldSend = this.socket.send;
|
|
7662
9474
|
this.connectionEstablished = false;
|
|
7663
|
-
subscribe("/mock/msg/receive", (data) => {
|
|
7664
|
-
if (data?.body) {
|
|
7665
|
-
KosLog.wsReceive(data?.body);
|
|
7666
|
-
const msg = {
|
|
7667
|
-
data: data.body
|
|
7668
|
-
};
|
|
7669
|
-
this.socket?.onmessage?.(msg);
|
|
7670
|
-
}
|
|
7671
|
-
});
|
|
7672
9475
|
makeObservable(this, {
|
|
7673
9476
|
connectionEstablished: observable
|
|
7674
9477
|
});
|
|
@@ -7793,23 +9596,7 @@ alias:${this.alias}
|
|
|
7793
9596
|
const { headers, body } = processKosMessage(String(data));
|
|
7794
9597
|
publish(KosWsEvents.SEND, { headers, body });
|
|
7795
9598
|
}
|
|
7796
|
-
|
|
7797
|
-
const { handler, requestId } = window.kosTopicIntercept(data);
|
|
7798
|
-
if (handler) {
|
|
7799
|
-
data = `mocked:true
|
|
7800
|
-
${data}`;
|
|
7801
|
-
const msg = handler(requestId);
|
|
7802
|
-
const returnData = {
|
|
7803
|
-
data: `mocked:true
|
|
7804
|
-
${msg}`
|
|
7805
|
-
};
|
|
7806
|
-
this.socket?.onmessage?.(returnData);
|
|
7807
|
-
} else {
|
|
7808
|
-
this.oldSend.call(this.socket, data);
|
|
7809
|
-
}
|
|
7810
|
-
} else {
|
|
7811
|
-
this.oldSend.call(this.socket, data);
|
|
7812
|
-
}
|
|
9599
|
+
this.oldSend.call(this.socket, data);
|
|
7813
9600
|
}
|
|
7814
9601
|
}
|
|
7815
9602
|
class BridgeTransport {
|
|
@@ -7930,7 +9717,65 @@ class KosTransport extends BaseTransport {
|
|
|
7930
9717
|
this.RESPONSE_ID_HEADER = `response-id`;
|
|
7931
9718
|
}
|
|
7932
9719
|
}
|
|
7933
|
-
|
|
9720
|
+
const LOGGED_IN_FRAME = `topic:/studioServer/auth/LOGGED_IN
|
|
9721
|
+
type:kos.broker.topic
|
|
9722
|
+
|
|
9723
|
+
{}`;
|
|
9724
|
+
class StandaloneMockSocket {
|
|
9725
|
+
onclose = null;
|
|
9726
|
+
onerror = null;
|
|
9727
|
+
onmessage = null;
|
|
9728
|
+
onopen = null;
|
|
9729
|
+
constructor(url) {
|
|
9730
|
+
KosLog.info(`KosMock standalone socket replacing ${url}`);
|
|
9731
|
+
setTimeout(async () => {
|
|
9732
|
+
await kosMockRegistry.bootGate;
|
|
9733
|
+
this.onopen?.({ currentTarget: this });
|
|
9734
|
+
if (kosMockRegistry.authorizeOnOpen) {
|
|
9735
|
+
this.onmessage?.({ data: LOGGED_IN_FRAME });
|
|
9736
|
+
}
|
|
9737
|
+
}, 0);
|
|
9738
|
+
}
|
|
9739
|
+
send(data) {
|
|
9740
|
+
if (peekFrameType(data) === KOS_HTTP_REQUEST_TYPE) {
|
|
9741
|
+
KosLog.warn(
|
|
9742
|
+
`KosMock standalone: unmatched request reached the loopback socket and will hang — add a route or use onUnmatched: "respond404"`,
|
|
9743
|
+
String(data).split("\n", 2).join(" ")
|
|
9744
|
+
);
|
|
9745
|
+
}
|
|
9746
|
+
}
|
|
9747
|
+
close() {
|
|
9748
|
+
}
|
|
9749
|
+
addEventListener() {
|
|
9750
|
+
}
|
|
9751
|
+
removeEventListener() {
|
|
9752
|
+
}
|
|
9753
|
+
dispatchEvent() {
|
|
9754
|
+
return true;
|
|
9755
|
+
}
|
|
9756
|
+
}
|
|
9757
|
+
class MockMessageTransport extends BaseTransport {
|
|
9758
|
+
RESPONSE_ID_HEADER;
|
|
9759
|
+
constructor({
|
|
9760
|
+
host,
|
|
9761
|
+
port,
|
|
9762
|
+
protocol,
|
|
9763
|
+
alias,
|
|
9764
|
+
onConnectionEstablished,
|
|
9765
|
+
onConnectionLost
|
|
9766
|
+
}) {
|
|
9767
|
+
super({
|
|
9768
|
+
host,
|
|
9769
|
+
port,
|
|
9770
|
+
protocol,
|
|
9771
|
+
alias,
|
|
9772
|
+
onConnectionEstablished,
|
|
9773
|
+
onConnectionLost,
|
|
9774
|
+
Transport: StandaloneMockSocket
|
|
9775
|
+
});
|
|
9776
|
+
this.RESPONSE_ID_HEADER = `response-id`;
|
|
9777
|
+
}
|
|
9778
|
+
}
|
|
7934
9779
|
window.kosLogEvents = process.env.KOS_LOG_WS === "true";
|
|
7935
9780
|
class TransportFactory {
|
|
7936
9781
|
static build({
|
|
@@ -7947,7 +9792,18 @@ class TransportFactory {
|
|
|
7947
9792
|
`kosWindowWebsocketOpen: ${globalThis.kosWindowWebsocketOpen ? `true` : `false`}`
|
|
7948
9793
|
);
|
|
7949
9794
|
KosLog.warn(`fos: ${fos ? `true` : `false`}`);
|
|
7950
|
-
if (
|
|
9795
|
+
if (KosMock.standalone && !fos) {
|
|
9796
|
+
KosLog.warn(`Using KosMock standalone transport`);
|
|
9797
|
+
return new MockMessageTransport({
|
|
9798
|
+
host,
|
|
9799
|
+
port,
|
|
9800
|
+
protocol,
|
|
9801
|
+
alias,
|
|
9802
|
+
onConnectionEstablished,
|
|
9803
|
+
onConnectionLost
|
|
9804
|
+
});
|
|
9805
|
+
}
|
|
9806
|
+
if (globalThis.kosWindowWebsocketOpen && !fos) {
|
|
7951
9807
|
KosLog.warn(`Using bridged message transport`);
|
|
7952
9808
|
const transport = new BridgeMessageTransport({
|
|
7953
9809
|
host,
|
|
@@ -27058,7 +28914,7 @@ function withProjectReloading(WrappedComponent) {
|
|
|
27058
28914
|
return reloading ? /* @__PURE__ */ jsx(LoadingMessage, { message: "Wait a moment while your project reloads..." }) : /* @__PURE__ */ jsx(WrappedComponent, { ...props });
|
|
27059
28915
|
};
|
|
27060
28916
|
}
|
|
27061
|
-
registerApiLevels({ "kos-ui-sdk":
|
|
28917
|
+
registerApiLevels({ "kos-ui-sdk": 24 });
|
|
27062
28918
|
export {
|
|
27063
28919
|
AncestorPathVisitor,
|
|
27064
28920
|
AuthState,
|
|
@@ -27169,7 +29025,7 @@ export {
|
|
|
27169
29025
|
KosLogManager,
|
|
27170
29026
|
index$b as KosLogManagerServices,
|
|
27171
29027
|
KosLoggerFactory,
|
|
27172
|
-
|
|
29028
|
+
KosMock,
|
|
27173
29029
|
KosModel,
|
|
27174
29030
|
KosModelCollectionProvider,
|
|
27175
29031
|
KosModelContainer,
|