@rotorsoft/act 1.13.0 → 1.14.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.
- package/dist/.tsbuildinfo +1 -1
- package/dist/@types/act.d.ts +37 -0
- package/dist/@types/act.d.ts.map +1 -1
- package/dist/@types/internal/autoclose-cycle.d.ts +9 -0
- package/dist/@types/internal/autoclose-cycle.d.ts.map +1 -1
- package/dist/@types/internal/circuit-breaker.d.ts +90 -0
- package/dist/@types/internal/circuit-breaker.d.ts.map +1 -0
- package/dist/@types/internal/drain-cycle.d.ts +9 -0
- package/dist/@types/internal/drain-cycle.d.ts.map +1 -1
- package/dist/@types/internal/index.d.ts +1 -0
- package/dist/@types/internal/index.d.ts.map +1 -1
- package/dist/@types/internal/settle.d.ts +10 -2
- package/dist/@types/internal/settle.d.ts.map +1 -1
- package/dist/@types/types/errors.d.ts +31 -0
- package/dist/@types/types/errors.d.ts.map +1 -1
- package/dist/{chunk-NGVWJXEF.js → chunk-4TJN4G7L.js} +13 -2
- package/dist/chunk-4TJN4G7L.js.map +1 -0
- package/dist/{chunk-QCSXT5MB.js → chunk-QKVY5QAQ.js} +2 -2
- package/dist/{chunk-5WRI5ZAA.js → chunk-YLJ4XMA6.js} +6 -2
- package/dist/{dist-INXH4GUE.js → dist-EOG547W7.js} +2 -2
- package/dist/index.cjs +169 -13
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +159 -13
- package/dist/index.js.map +1 -1
- package/dist/{magic-string.es-ZGO77SPS.js → magic-string.es-5F2IVYTG.js} +2 -2
- package/dist/test/index.cjs +9 -4
- package/dist/test/index.cjs.map +1 -1
- package/dist/test/index.js +5 -5
- package/dist/test/index.js.map +1 -1
- package/dist/types/index.cjs +13 -1
- package/dist/types/index.cjs.map +1 -1
- package/dist/types/index.js +4 -2
- package/package.json +2 -2
- package/dist/chunk-NGVWJXEF.js.map +0 -1
- /package/dist/{chunk-QCSXT5MB.js.map → chunk-QKVY5QAQ.js.map} +0 -0
- /package/dist/{chunk-5WRI5ZAA.js.map → chunk-YLJ4XMA6.js.map} +0 -0
- /package/dist/{dist-INXH4GUE.js.map → dist-EOG547W7.js.map} +0 -0
- /package/dist/{magic-string.es-ZGO77SPS.js.map → magic-string.es-5F2IVYTG.js.map} +0 -0
package/dist/index.cjs
CHANGED
|
@@ -57,6 +57,7 @@ __export(index_exports, {
|
|
|
57
57
|
REDACTED: () => REDACTED,
|
|
58
58
|
SHREDDED: () => SHREDDED,
|
|
59
59
|
SNAP_EVENT: () => SNAP_EVENT,
|
|
60
|
+
StoreError: () => StoreError,
|
|
60
61
|
StreamClosedError: () => StreamClosedError,
|
|
61
62
|
TOMBSTONE_EVENT: () => TOMBSTONE_EVENT,
|
|
62
63
|
TargetSchema: () => TargetSchema,
|
|
@@ -307,7 +308,8 @@ var Errors = {
|
|
|
307
308
|
InvariantError: "ERR_INVARIANT",
|
|
308
309
|
ConcurrencyError: "ERR_CONCURRENCY",
|
|
309
310
|
StreamClosedError: "ERR_STREAM_CLOSED",
|
|
310
|
-
NonRetryableError: "ERR_NON_RETRYABLE"
|
|
311
|
+
NonRetryableError: "ERR_NON_RETRYABLE",
|
|
312
|
+
StoreError: "ERR_STORE"
|
|
311
313
|
};
|
|
312
314
|
var ValidationError = class extends Error {
|
|
313
315
|
/** The type of target being validated (e.g., "action", "event") */
|
|
@@ -376,6 +378,15 @@ var StreamClosedError = class extends Error {
|
|
|
376
378
|
this.stream = stream;
|
|
377
379
|
}
|
|
378
380
|
};
|
|
381
|
+
var StoreError = class extends Error {
|
|
382
|
+
/** The store operation that failed (e.g. `"claim"`, `"ack"`, `"commit"`). */
|
|
383
|
+
operation;
|
|
384
|
+
constructor(operation, options) {
|
|
385
|
+
super(`Store operation "${operation}" failed`, options);
|
|
386
|
+
this.name = Errors.StoreError;
|
|
387
|
+
this.operation = operation;
|
|
388
|
+
}
|
|
389
|
+
};
|
|
379
390
|
var NonRetryableError = class extends Error {
|
|
380
391
|
/** The original failure, if any. Mirrors the standard `Error.cause` shape. */
|
|
381
392
|
cause;
|
|
@@ -1933,8 +1944,17 @@ async function scan_stream_heads(streams) {
|
|
|
1933
1944
|
async function partition_by_safety(stream_info, reactive_events_size, skipped) {
|
|
1934
1945
|
if (reactive_events_size === 0) return [...stream_info.keys()];
|
|
1935
1946
|
const pending_set = /* @__PURE__ */ new Set();
|
|
1947
|
+
const source_regex = /* @__PURE__ */ new Map();
|
|
1948
|
+
const get_regex = (source) => {
|
|
1949
|
+
let re = source_regex.get(source);
|
|
1950
|
+
if (!re) {
|
|
1951
|
+
re = new RegExp(source);
|
|
1952
|
+
source_regex.set(source, re);
|
|
1953
|
+
}
|
|
1954
|
+
return re;
|
|
1955
|
+
};
|
|
1936
1956
|
await store2().query_streams((position) => {
|
|
1937
|
-
const source_re = position.source ?
|
|
1957
|
+
const source_re = position.source ? get_regex(position.source) : void 0;
|
|
1938
1958
|
for (const [stream, info] of stream_info) {
|
|
1939
1959
|
if ((!source_re || source_re.test(stream)) && position.at < info.max_id) {
|
|
1940
1960
|
pending_set.add(stream);
|
|
@@ -2136,10 +2156,7 @@ var AutocloseController = class {
|
|
|
2136
2156
|
start() {
|
|
2137
2157
|
if (this._timer) return false;
|
|
2138
2158
|
this._timer = setInterval(() => {
|
|
2139
|
-
this.run_once().catch((
|
|
2140
|
-
this.deps.logger.error(
|
|
2141
|
-
`Autoclose cycle errored: ${err instanceof Error ? err.message : String(err)}`
|
|
2142
|
-
);
|
|
2159
|
+
this.run_once().catch(() => {
|
|
2143
2160
|
});
|
|
2144
2161
|
}, this.deps.config.autocloseCycleMs);
|
|
2145
2162
|
this._timer.unref();
|
|
@@ -2172,6 +2189,7 @@ var AutocloseController = class {
|
|
|
2172
2189
|
*/
|
|
2173
2190
|
async run_once() {
|
|
2174
2191
|
if (this._running) return null;
|
|
2192
|
+
if (this.deps.breaker.state(Date.now()) === "open") return null;
|
|
2175
2193
|
this._running = true;
|
|
2176
2194
|
try {
|
|
2177
2195
|
const correlation = close_correlation(
|
|
@@ -2189,10 +2207,14 @@ var AutocloseController = class {
|
|
|
2189
2207
|
config: this.deps.config,
|
|
2190
2208
|
correlation
|
|
2191
2209
|
});
|
|
2210
|
+
this.deps.breaker.passed();
|
|
2192
2211
|
if (result.close_result.truncated.size > 0) {
|
|
2193
2212
|
this.deps.on_closed(result.close_result);
|
|
2194
2213
|
}
|
|
2195
2214
|
return result;
|
|
2215
|
+
} catch (error) {
|
|
2216
|
+
this.deps.breaker.failed(Date.now(), error);
|
|
2217
|
+
throw error;
|
|
2196
2218
|
} finally {
|
|
2197
2219
|
this._running = false;
|
|
2198
2220
|
}
|
|
@@ -2322,6 +2344,93 @@ function classify_registry(registry, states) {
|
|
|
2322
2344
|
};
|
|
2323
2345
|
}
|
|
2324
2346
|
|
|
2347
|
+
// src/internal/circuit-breaker.ts
|
|
2348
|
+
var import_zod7 = require("zod");
|
|
2349
|
+
var DEFAULT_CIRCUIT_FAILURE_THRESHOLD = 5;
|
|
2350
|
+
var DEFAULT_CIRCUIT_COOLDOWN_MS = 3e4;
|
|
2351
|
+
var CircuitBreakerOptionsSchema = import_zod7.z.object({
|
|
2352
|
+
failureThreshold: import_zod7.z.number().int().min(1).default(DEFAULT_CIRCUIT_FAILURE_THRESHOLD),
|
|
2353
|
+
cooldownMs: import_zod7.z.number().int().min(100).max(36e5).default(DEFAULT_CIRCUIT_COOLDOWN_MS)
|
|
2354
|
+
});
|
|
2355
|
+
var resolveCircuitBreakerConfig = (options) => CircuitBreakerOptionsSchema.parse(options ?? {});
|
|
2356
|
+
var CircuitBreaker = class {
|
|
2357
|
+
_failures = 0;
|
|
2358
|
+
_opened_at;
|
|
2359
|
+
_wake;
|
|
2360
|
+
_threshold;
|
|
2361
|
+
_cooldown_ms;
|
|
2362
|
+
_hooks;
|
|
2363
|
+
constructor(config2, hooks = {}) {
|
|
2364
|
+
this._threshold = config2.failureThreshold;
|
|
2365
|
+
this._cooldown_ms = config2.cooldownMs;
|
|
2366
|
+
this._hooks = hooks;
|
|
2367
|
+
}
|
|
2368
|
+
/** Current state given the wall-clock `now`. */
|
|
2369
|
+
state(now) {
|
|
2370
|
+
if (this._opened_at === void 0) return "closed";
|
|
2371
|
+
return now - this._opened_at >= this._cooldown_ms ? "half-open" : "open";
|
|
2372
|
+
}
|
|
2373
|
+
/** A store op passed — reset to closed and cancel any pending retry. */
|
|
2374
|
+
passed() {
|
|
2375
|
+
this._failures = 0;
|
|
2376
|
+
this._opened_at = void 0;
|
|
2377
|
+
this._clear_wake();
|
|
2378
|
+
}
|
|
2379
|
+
/**
|
|
2380
|
+
* A store op failed. Opens the breaker when the consecutive-failure
|
|
2381
|
+
* threshold is reached, or immediately re-opens (restarting the cooldown)
|
|
2382
|
+
* if a half-open trial just failed. Returns the resulting state.
|
|
2383
|
+
*
|
|
2384
|
+
* Callers gate on the state directly — `state(now) === "open"` means skip.
|
|
2385
|
+
* On opening, schedules the `on_retry` wake so recovery is automatic;
|
|
2386
|
+
* always surfaces the failure via `on_error`.
|
|
2387
|
+
*/
|
|
2388
|
+
failed(now, error) {
|
|
2389
|
+
const circuit = this._advance(now);
|
|
2390
|
+
if (circuit === "open") this._schedule_wake();
|
|
2391
|
+
this._hooks.on_error?.(error, circuit);
|
|
2392
|
+
return circuit;
|
|
2393
|
+
}
|
|
2394
|
+
/** Cancel the pending retry timer. Idempotent; call on shutdown. */
|
|
2395
|
+
stop() {
|
|
2396
|
+
this._clear_wake();
|
|
2397
|
+
}
|
|
2398
|
+
/**
|
|
2399
|
+
* Schedule the `on_retry` wake `cooldownMs` out so the breaker re-trials
|
|
2400
|
+
* the store on its own. No-op when no `on_retry` hook is wired (e.g. unit
|
|
2401
|
+
* tests of the pure state machine). The timer is `unref`'d so it never
|
|
2402
|
+
* keeps the process alive.
|
|
2403
|
+
*/
|
|
2404
|
+
_schedule_wake() {
|
|
2405
|
+
if (!this._hooks.on_retry) return;
|
|
2406
|
+
this._clear_wake();
|
|
2407
|
+
this._wake = setTimeout(() => {
|
|
2408
|
+
this._wake = void 0;
|
|
2409
|
+
this._hooks.on_retry?.();
|
|
2410
|
+
}, this._cooldown_ms);
|
|
2411
|
+
this._wake.unref();
|
|
2412
|
+
}
|
|
2413
|
+
_clear_wake() {
|
|
2414
|
+
if (this._wake) {
|
|
2415
|
+
clearTimeout(this._wake);
|
|
2416
|
+
this._wake = void 0;
|
|
2417
|
+
}
|
|
2418
|
+
}
|
|
2419
|
+
/** Pure state transition for a failure — no side effects. */
|
|
2420
|
+
_advance(now) {
|
|
2421
|
+
if (this._opened_at !== void 0) {
|
|
2422
|
+
this._opened_at = now;
|
|
2423
|
+
return "open";
|
|
2424
|
+
}
|
|
2425
|
+
this._failures += 1;
|
|
2426
|
+
if (this._failures >= this._threshold) {
|
|
2427
|
+
this._opened_at = now;
|
|
2428
|
+
return "open";
|
|
2429
|
+
}
|
|
2430
|
+
return "closed";
|
|
2431
|
+
}
|
|
2432
|
+
};
|
|
2433
|
+
|
|
2325
2434
|
// src/internal/correlate-cycle.ts
|
|
2326
2435
|
var CorrelateCycle = class {
|
|
2327
2436
|
_checkpoint = -1;
|
|
@@ -3174,6 +3283,8 @@ var DrainController = class {
|
|
|
3174
3283
|
async drain(options = {}) {
|
|
3175
3284
|
if (!this._armed) return EMPTY_DRAIN;
|
|
3176
3285
|
if (this._locked) return EMPTY_DRAIN;
|
|
3286
|
+
if (this._deps.breaker.state(Date.now()) === "open")
|
|
3287
|
+
return EMPTY_DRAIN;
|
|
3177
3288
|
const d = this._deps.defaults ?? {};
|
|
3178
3289
|
const streamLimit = d.streamLimit ?? options.streamLimit ?? 10;
|
|
3179
3290
|
const eventLimit = d.eventLimit ?? options.eventLimit ?? 10;
|
|
@@ -3195,6 +3306,7 @@ var DrainController = class {
|
|
|
3195
3306
|
this._backoff.size > 0 ? this.is_deferred : void 0,
|
|
3196
3307
|
this._deps.lane
|
|
3197
3308
|
);
|
|
3309
|
+
this._deps.breaker.passed();
|
|
3198
3310
|
if (!cycle) {
|
|
3199
3311
|
this._armed = false;
|
|
3200
3312
|
return EMPTY_DRAIN;
|
|
@@ -3216,7 +3328,7 @@ var DrainController = class {
|
|
|
3216
3328
|
if (!acked.length && !blocked.length && !has_errors) this._armed = false;
|
|
3217
3329
|
return { fetched, leased, acked, blocked };
|
|
3218
3330
|
} catch (error) {
|
|
3219
|
-
this._deps.
|
|
3331
|
+
this._deps.breaker.failed(Date.now(), error);
|
|
3220
3332
|
return EMPTY_DRAIN;
|
|
3221
3333
|
} finally {
|
|
3222
3334
|
this._locked = false;
|
|
@@ -3225,7 +3337,7 @@ var DrainController = class {
|
|
|
3225
3337
|
};
|
|
3226
3338
|
|
|
3227
3339
|
// src/internal/merge.ts
|
|
3228
|
-
var
|
|
3340
|
+
var import_zod8 = require("zod");
|
|
3229
3341
|
function base_type_name(zodType) {
|
|
3230
3342
|
let t = zodType;
|
|
3231
3343
|
while (typeof t.unwrap === "function") {
|
|
@@ -3234,7 +3346,7 @@ function base_type_name(zodType) {
|
|
|
3234
3346
|
return t.constructor.name;
|
|
3235
3347
|
}
|
|
3236
3348
|
function merge_schemas(existing, incoming, state_name) {
|
|
3237
|
-
if (existing instanceof
|
|
3349
|
+
if (existing instanceof import_zod8.ZodObject && incoming instanceof import_zod8.ZodObject) {
|
|
3238
3350
|
const existing_shape = existing.shape;
|
|
3239
3351
|
const incoming_shape = incoming.shape;
|
|
3240
3352
|
for (const key of Object.keys(incoming_shape)) {
|
|
@@ -3504,12 +3616,15 @@ var SettleLoop = class {
|
|
|
3504
3616
|
...correlate_query,
|
|
3505
3617
|
after: this._deps.checkpoint()
|
|
3506
3618
|
});
|
|
3619
|
+
this._deps.breaker.passed();
|
|
3507
3620
|
last_drain = await this._deps.drain(drain_options);
|
|
3508
3621
|
const made_progress = subscribed > 0 || last_drain.acked.length > 0 || last_drain.blocked.length > 0;
|
|
3509
3622
|
if (!made_progress) break;
|
|
3510
3623
|
}
|
|
3511
3624
|
if (last_drain) this._deps.on_settled(last_drain);
|
|
3512
|
-
})().catch((err) =>
|
|
3625
|
+
})().catch((err) => {
|
|
3626
|
+
this._deps.breaker.failed(Date.now(), err);
|
|
3627
|
+
}).finally(() => {
|
|
3513
3628
|
this._running = false;
|
|
3514
3629
|
});
|
|
3515
3630
|
}, debounceMs);
|
|
@@ -3528,6 +3643,8 @@ var DEFAULT_MAX_SUBSCRIBED_STREAMS = 1e3;
|
|
|
3528
3643
|
var DEFAULT_SETTLE_DEBOUNCE_MS = 10;
|
|
3529
3644
|
var Act = class {
|
|
3530
3645
|
_emitter = new import_node_events.default();
|
|
3646
|
+
/** ACT-984: orchestrator-owned circuit breaker shared by all drain lanes. */
|
|
3647
|
+
_breaker;
|
|
3531
3648
|
/** #803: gate the `Store.notify` subscription side. */
|
|
3532
3649
|
_listen;
|
|
3533
3650
|
/** #803: gate the local reaction pipeline (drain controllers, settle, correlate). */
|
|
@@ -3581,6 +3698,22 @@ var Act = class {
|
|
|
3581
3698
|
emit(event, args) {
|
|
3582
3699
|
return this._emitter.emit(event, args);
|
|
3583
3700
|
}
|
|
3701
|
+
/**
|
|
3702
|
+
* The single store-failure handler: log it, then emit the `error`
|
|
3703
|
+
* lifecycle event. Wired into the circuit breaker's `on_error`, so it
|
|
3704
|
+
* runs on every `failed()` — drain / settle / autoclose just call
|
|
3705
|
+
* `breaker.failed(now, error)` and never log or emit themselves.
|
|
3706
|
+
*
|
|
3707
|
+
* The emit is guarded against Node's `EventEmitter` contract that an
|
|
3708
|
+
* unhandled `"error"` emission is rethrown (which would crash the process
|
|
3709
|
+
* from inside the drain catch); logging is unconditional so failures are
|
|
3710
|
+
* never silent.
|
|
3711
|
+
*/
|
|
3712
|
+
_emit_error(error, circuit) {
|
|
3713
|
+
this._logger.error(error);
|
|
3714
|
+
if (this._emitter.listenerCount("error") > 0)
|
|
3715
|
+
this.emit("error", { error, circuit });
|
|
3716
|
+
}
|
|
3584
3717
|
/**
|
|
3585
3718
|
* Register a listener for a lifecycle event. The listener receives the
|
|
3586
3719
|
* event-specific payload.
|
|
@@ -3709,6 +3842,25 @@ var Act = class {
|
|
|
3709
3842
|
this._listen = options.listen !== false;
|
|
3710
3843
|
this._drain = options.drain !== false;
|
|
3711
3844
|
this._autoclose_config = resolveAutocloseConfig(options);
|
|
3845
|
+
this._breaker = new CircuitBreaker(
|
|
3846
|
+
resolveCircuitBreakerConfig(options.circuitBreaker),
|
|
3847
|
+
{
|
|
3848
|
+
on_error: (error, circuit) => this._emit_error(error, circuit),
|
|
3849
|
+
// Re-probe the store when the cooldown elapses, so recovery is
|
|
3850
|
+
// automatic even on the default lane (which has no periodic poller).
|
|
3851
|
+
// The breaker is shared by every store-polling loop (drain, the
|
|
3852
|
+
// settle correlate, autoclose), so any of them can trip it — but a
|
|
3853
|
+
// bare `drain()` only touches the store when the controller is armed,
|
|
3854
|
+
// so a trip from a correlate failure could go unprobed. `settle()`
|
|
3855
|
+
// always runs a correlate (a store query) before draining, so it
|
|
3856
|
+
// re-probes regardless of which loop opened the breaker: one success
|
|
3857
|
+
// closes it and every loop resumes on its own cadence. A failed probe
|
|
3858
|
+
// re-opens the breaker and reschedules the wake.
|
|
3859
|
+
on_retry: () => {
|
|
3860
|
+
this.settle({ debounceMs: 0 });
|
|
3861
|
+
}
|
|
3862
|
+
}
|
|
3863
|
+
);
|
|
3712
3864
|
this._event_to_state = event_to_state;
|
|
3713
3865
|
this._event_to_lanes = event_to_lanes;
|
|
3714
3866
|
const all_lanes = ["default", ...lanes.map((l) => l.name)];
|
|
@@ -3727,6 +3879,7 @@ var Act = class {
|
|
|
3727
3879
|
handle_batch: this._handle_batch,
|
|
3728
3880
|
on_acked: (acked) => this.emit("acked", acked),
|
|
3729
3881
|
on_blocked: (blocked) => this.emit("blocked", blocked),
|
|
3882
|
+
breaker: this._breaker,
|
|
3730
3883
|
// Pass lane only when a true per-lane controller is active.
|
|
3731
3884
|
// The all-lanes (single default) case keeps lane=undefined so
|
|
3732
3885
|
// adapter SQL collapses to the pre-1103 shape.
|
|
@@ -3763,12 +3916,12 @@ var Act = class {
|
|
|
3763
3916
|
);
|
|
3764
3917
|
this._settle = new SettleLoop(
|
|
3765
3918
|
{
|
|
3766
|
-
logger: this._logger,
|
|
3767
3919
|
init: () => this._correlate.init(),
|
|
3768
3920
|
checkpoint: () => this._correlate.checkpoint,
|
|
3769
3921
|
correlate: (q) => this.correlate(q),
|
|
3770
3922
|
drain: (o) => this.drain(o),
|
|
3771
|
-
on_settled: (drain) => this.emit("settled", drain)
|
|
3923
|
+
on_settled: (drain) => this.emit("settled", drain),
|
|
3924
|
+
breaker: this._breaker
|
|
3772
3925
|
},
|
|
3773
3926
|
options.settleDebounceMs ?? DEFAULT_SETTLE_DEBOUNCE_MS
|
|
3774
3927
|
);
|
|
@@ -3790,7 +3943,8 @@ var Act = class {
|
|
|
3790
3943
|
config: this._autoclose_config,
|
|
3791
3944
|
correlator: this._correlator,
|
|
3792
3945
|
close_actor: { id: "$autoclose", name: "autoclose" },
|
|
3793
|
-
on_closed: (result) => this.emit("closed", result)
|
|
3946
|
+
on_closed: (result) => this.emit("closed", result),
|
|
3947
|
+
breaker: this._breaker
|
|
3794
3948
|
}) : void 0;
|
|
3795
3949
|
this._notify_disposer = this._wire_notify(options.scoped?.store ?? store2());
|
|
3796
3950
|
dispose(() => this.shutdown());
|
|
@@ -3813,6 +3967,7 @@ var Act = class {
|
|
|
3813
3967
|
this._emitter.removeAllListeners();
|
|
3814
3968
|
this.stop_correlations();
|
|
3815
3969
|
this.stop_settling();
|
|
3970
|
+
this._breaker.stop();
|
|
3816
3971
|
for (const c of this._drain_controllers.values()) c.stop();
|
|
3817
3972
|
const disposer = await this._notify_disposer;
|
|
3818
3973
|
if (disposer) await disposer();
|
|
@@ -5330,6 +5485,7 @@ function write_line(writer, line) {
|
|
|
5330
5485
|
REDACTED,
|
|
5331
5486
|
SHREDDED,
|
|
5332
5487
|
SNAP_EVENT,
|
|
5488
|
+
StoreError,
|
|
5333
5489
|
StreamClosedError,
|
|
5334
5490
|
TOMBSTONE_EVENT,
|
|
5335
5491
|
TargetSchema,
|