@rotorsoft/act 1.27.0 → 1.29.0
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 +20 -2
- package/dist/@types/act.d.ts.map +1 -1
- package/dist/@types/adapters/in-memory-store.d.ts.map +1 -1
- package/dist/@types/internal/build-classify.d.ts +4 -6
- package/dist/@types/internal/build-classify.d.ts.map +1 -1
- package/dist/@types/internal/close-cycle.d.ts +19 -0
- package/dist/@types/internal/close-cycle.d.ts.map +1 -1
- package/dist/@types/internal/correlate-cycle.d.ts +31 -20
- package/dist/@types/internal/correlate-cycle.d.ts.map +1 -1
- package/dist/@types/internal/settle.d.ts +0 -10
- package/dist/@types/internal/settle.d.ts.map +1 -1
- package/dist/@types/types/ports.d.ts +8 -0
- package/dist/@types/types/ports.d.ts.map +1 -1
- package/dist/{chunk-66PTM4QB.js → chunk-PX3IBVLW.js} +4 -15
- package/dist/chunk-PX3IBVLW.js.map +1 -0
- package/dist/index.cjs +140 -93
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +138 -80
- package/dist/index.js.map +1 -1
- package/dist/test/index.cjs +3 -18
- package/dist/test/index.cjs.map +1 -1
- package/dist/test/index.js +1 -1
- package/package.json +6 -6
- package/dist/chunk-66PTM4QB.js.map +0 -1
package/dist/index.cjs
CHANGED
|
@@ -1028,19 +1028,7 @@ var InMemoryStore = class {
|
|
|
1028
1028
|
*/
|
|
1029
1029
|
async claim(lagging, leading, by, millis, lane) {
|
|
1030
1030
|
await sleep();
|
|
1031
|
-
const has_work = (s) =>
|
|
1032
|
-
if (s.correlated_at !== void 0) return s.at < s.correlated_at;
|
|
1033
|
-
if (!s.source) return s.at < this._max_non_snap_event_id;
|
|
1034
|
-
if (is_literal_source(s.source)) {
|
|
1035
|
-
const max_id = this._max_event_id_by_stream.get(s.source);
|
|
1036
|
-
return max_id !== void 0 && max_id > s.at;
|
|
1037
|
-
}
|
|
1038
|
-
const re = new RegExp(s.source);
|
|
1039
|
-
for (const [stream_name, max_id] of this._max_event_id_by_stream) {
|
|
1040
|
-
if (max_id > s.at && re.test(stream_name)) return true;
|
|
1041
|
-
}
|
|
1042
|
-
return false;
|
|
1043
|
-
};
|
|
1031
|
+
const has_work = (s) => s.correlated_at !== void 0 && s.at < s.correlated_at;
|
|
1044
1032
|
const available = [...this._streams.values()].filter(
|
|
1045
1033
|
(s) => s.is_available && has_work(s) && (lane === void 0 || s.lane === lane)
|
|
1046
1034
|
);
|
|
@@ -1329,7 +1317,8 @@ var InMemoryStore = class {
|
|
|
1329
1317
|
leased_by: s.leased_by,
|
|
1330
1318
|
leased_until: s.leased_until,
|
|
1331
1319
|
lane: s.lane,
|
|
1332
|
-
deferred_at: s.deferred_at
|
|
1320
|
+
deferred_at: s.deferred_at,
|
|
1321
|
+
correlated_at: s.correlated_at
|
|
1333
1322
|
});
|
|
1334
1323
|
count++;
|
|
1335
1324
|
if (count >= limit) break;
|
|
@@ -2418,12 +2407,10 @@ function classify_registry(registry, states) {
|
|
|
2418
2407
|
const target_lanes = /* @__PURE__ */ new Map();
|
|
2419
2408
|
const reactive_events = /* @__PURE__ */ new Set();
|
|
2420
2409
|
const event_to_lanes = /* @__PURE__ */ new Map();
|
|
2421
|
-
let has_dynamic_resolvers = false;
|
|
2422
2410
|
for (const [name, register] of Object.entries(registry.events)) {
|
|
2423
2411
|
if (register.reactions.size > 0) reactive_events.add(name);
|
|
2424
2412
|
for (const reaction of register.reactions.values()) {
|
|
2425
2413
|
if (typeof reaction.resolver === "function") {
|
|
2426
|
-
has_dynamic_resolvers = true;
|
|
2427
2414
|
event_to_lanes.set(name, ALL_LANES);
|
|
2428
2415
|
} else {
|
|
2429
2416
|
const { target, source, priority = 0, lane } = reaction.resolver;
|
|
@@ -2459,7 +2446,6 @@ function classify_registry(registry, states) {
|
|
|
2459
2446
|
}
|
|
2460
2447
|
return {
|
|
2461
2448
|
static_targets: [...statics.values()],
|
|
2462
|
-
has_dynamic_resolvers,
|
|
2463
2449
|
reactive_events,
|
|
2464
2450
|
event_to_state,
|
|
2465
2451
|
event_to_lanes
|
|
@@ -2943,7 +2929,8 @@ async function run_close_cycle(targets, deps) {
|
|
|
2943
2929
|
stream_info,
|
|
2944
2930
|
deps.reactive_events_size,
|
|
2945
2931
|
skipped,
|
|
2946
|
-
deps.probe_page_size ?? SAFETY_PROBE_PAGE_SIZE
|
|
2932
|
+
deps.probe_page_size ?? SAFETY_PROBE_PAGE_SIZE,
|
|
2933
|
+
deps.catch_up_correlation
|
|
2947
2934
|
);
|
|
2948
2935
|
if (!safe.length) return { truncated: windowed_result, skipped };
|
|
2949
2936
|
const { guarded, guard_events } = await guard_with_tombstones(
|
|
@@ -3062,8 +3049,16 @@ async function scan_stream_heads(streams) {
|
|
|
3062
3049
|
}
|
|
3063
3050
|
return out;
|
|
3064
3051
|
}
|
|
3065
|
-
async function partition_by_safety(stream_info, reactive_events_size, skipped, page_size) {
|
|
3052
|
+
async function partition_by_safety(stream_info, reactive_events_size, skipped, page_size, catch_up_correlation) {
|
|
3066
3053
|
if (reactive_events_size === 0) return [...stream_info.keys()];
|
|
3054
|
+
let needed = -1;
|
|
3055
|
+
for (const info of stream_info.values())
|
|
3056
|
+
needed = Math.max(needed, info.max_id);
|
|
3057
|
+
const checkpoint = await catch_up_correlation(needed);
|
|
3058
|
+
const uncorrelated = /* @__PURE__ */ new Set();
|
|
3059
|
+
for (const [stream, info] of stream_info) {
|
|
3060
|
+
if (checkpoint < info.max_id) uncorrelated.add(stream);
|
|
3061
|
+
}
|
|
3067
3062
|
const pending_set = /* @__PURE__ */ new Set();
|
|
3068
3063
|
const source_regex = /* @__PURE__ */ new Map();
|
|
3069
3064
|
const get_regex = (source) => {
|
|
@@ -3082,6 +3077,8 @@ async function partition_by_safety(stream_info, reactive_events_size, skipped, p
|
|
|
3082
3077
|
(position) => {
|
|
3083
3078
|
last = position.stream;
|
|
3084
3079
|
const source_re = position.source ? get_regex(position.source) : void 0;
|
|
3080
|
+
const has_work = position.correlated_at !== void 0 && position.at < position.correlated_at;
|
|
3081
|
+
if (!has_work) return;
|
|
3085
3082
|
for (const [stream, info] of stream_info) {
|
|
3086
3083
|
if ((!source_re || source_re.test(stream)) && position.at < info.max_id) {
|
|
3087
3084
|
pending_set.add(stream);
|
|
@@ -3095,7 +3092,8 @@ async function partition_by_safety(stream_info, reactive_events_size, skipped, p
|
|
|
3095
3092
|
}
|
|
3096
3093
|
const safe = [];
|
|
3097
3094
|
for (const [stream] of stream_info) {
|
|
3098
|
-
if (pending_set.has(stream)
|
|
3095
|
+
if (pending_set.has(stream) || uncorrelated.has(stream))
|
|
3096
|
+
skipped.push(stream);
|
|
3099
3097
|
else safe.push(stream);
|
|
3100
3098
|
}
|
|
3101
3099
|
return safe;
|
|
@@ -3185,21 +3183,24 @@ async function truncate_and_warm_cache(guarded, seed_states, guard_events, corre
|
|
|
3185
3183
|
|
|
3186
3184
|
// src/internal/correlate-cycle.ts
|
|
3187
3185
|
var DEFAULT_COLD_START_BACK_SCAN = 1e4;
|
|
3186
|
+
var PATTERN_CACHE_SIZE = 32;
|
|
3188
3187
|
var CorrelateCycle = class {
|
|
3189
3188
|
_checkpoint = -1;
|
|
3190
3189
|
_initialized = false;
|
|
3191
3190
|
/** In-flight init, memoized for single-flight and cleared on failure. */
|
|
3192
3191
|
_init_promise;
|
|
3193
3192
|
_timer = void 0;
|
|
3194
|
-
// target →
|
|
3195
|
-
//
|
|
3196
|
-
//
|
|
3197
|
-
//
|
|
3198
|
-
//
|
|
3193
|
+
// target → what it was last subscribed at. Every scan re-subscribes the
|
|
3194
|
+
// targets it resolved (that is how the work mark lands), so this no longer
|
|
3195
|
+
// decides *whether* a target is sent — it decides *what* is sent with it:
|
|
3196
|
+
// a resolution raises priority/lane only when it beats the recorded floor,
|
|
3197
|
+
// and otherwise re-sends the row's own values so the mark changes nothing
|
|
3198
|
+
// else. See {@link Subscription}.
|
|
3199
3199
|
_subscribed;
|
|
3200
|
+
/** Compiled pattern sources, bounded by {@link PATTERN_CACHE_SIZE}. */
|
|
3201
|
+
_patterns = new LruMap(PATTERN_CACHE_SIZE);
|
|
3200
3202
|
_registry;
|
|
3201
3203
|
_static_targets;
|
|
3202
|
-
_has_dynamic_resolvers;
|
|
3203
3204
|
_cd;
|
|
3204
3205
|
_on_init;
|
|
3205
3206
|
/**
|
|
@@ -3229,7 +3230,6 @@ var CorrelateCycle = class {
|
|
|
3229
3230
|
constructor({
|
|
3230
3231
|
registry,
|
|
3231
3232
|
static_targets,
|
|
3232
|
-
has_dynamic_resolvers,
|
|
3233
3233
|
cd,
|
|
3234
3234
|
max_subscribed_streams,
|
|
3235
3235
|
run_scoped,
|
|
@@ -3240,7 +3240,6 @@ var CorrelateCycle = class {
|
|
|
3240
3240
|
this._subscribed = new LruMap(max_subscribed_streams);
|
|
3241
3241
|
this._registry = registry;
|
|
3242
3242
|
this._static_targets = static_targets;
|
|
3243
|
-
this._has_dynamic_resolvers = has_dynamic_resolvers;
|
|
3244
3243
|
this._cd = cd;
|
|
3245
3244
|
this._on_init = on_init;
|
|
3246
3245
|
this._run_scoped = run_scoped;
|
|
@@ -3251,19 +3250,10 @@ var CorrelateCycle = class {
|
|
|
3251
3250
|
get checkpoint() {
|
|
3252
3251
|
return this._checkpoint;
|
|
3253
3252
|
}
|
|
3254
|
-
/**
|
|
3255
|
-
* Whether any reaction uses a dynamic resolver — i.e. whether `correlate`
|
|
3256
|
-
* performs a store scan rather than the static no-op early-return. The
|
|
3257
|
-
* settle loop reads this to avoid recording a fictitious circuit-breaker
|
|
3258
|
-
* success on a static app whose correlate never touches the store (#1329).
|
|
3259
|
-
*/
|
|
3260
|
-
get has_dynamic_resolvers() {
|
|
3261
|
-
return this._has_dynamic_resolvers;
|
|
3262
|
-
}
|
|
3263
3253
|
/**
|
|
3264
3254
|
* Initialize correlation state on first call.
|
|
3265
|
-
* - Reads max(at) from store,
|
|
3266
|
-
* `watermark - back_scan`
|
|
3255
|
+
* - Reads the durable correlate checkpoint (and max(at)) from the store,
|
|
3256
|
+
* flooring a first boot at `watermark - back_scan` so an event
|
|
3267
3257
|
* committed-but-not-correlated before a crash is re-scanned on
|
|
3268
3258
|
* restart instead of skipped (ACT-1207)
|
|
3269
3259
|
* - Subscribes static resolver targets (idempotent upsert)
|
|
@@ -3285,10 +3275,14 @@ var CorrelateCycle = class {
|
|
|
3285
3275
|
const { watermark, correlated_at } = await store2().subscribe([
|
|
3286
3276
|
...this._static_targets
|
|
3287
3277
|
]);
|
|
3288
|
-
this._checkpoint =
|
|
3278
|
+
this._checkpoint = correlated_at >= 0 ? correlated_at : Math.max(-1, watermark - this._cold_start_back_scan);
|
|
3289
3279
|
this._on_init?.();
|
|
3290
|
-
for (const { stream } of this._static_targets) {
|
|
3291
|
-
this._subscribed.set(stream,
|
|
3280
|
+
for (const { stream, priority = 0, lane } of this._static_targets) {
|
|
3281
|
+
this._subscribed.set(stream, {
|
|
3282
|
+
floor: Number.POSITIVE_INFINITY,
|
|
3283
|
+
priority,
|
|
3284
|
+
lane
|
|
3285
|
+
});
|
|
3292
3286
|
}
|
|
3293
3287
|
await this._on_init_async?.();
|
|
3294
3288
|
}
|
|
@@ -3313,14 +3307,34 @@ var CorrelateCycle = class {
|
|
|
3313
3307
|
}
|
|
3314
3308
|
}
|
|
3315
3309
|
/**
|
|
3316
|
-
*
|
|
3317
|
-
*
|
|
3318
|
-
*
|
|
3310
|
+
* Would an event from `stream` be fetched for a target subscribed with
|
|
3311
|
+
* `source`? The subscription's source is the filter `fetch` queries with
|
|
3312
|
+
* — literal names by equality, patterns compiled as a `RegExp` — so an
|
|
3313
|
+
* event outside it is not work for that target and must not mark it.
|
|
3314
|
+
* No source means the target consumes every stream.
|
|
3315
|
+
*/
|
|
3316
|
+
_in_fetch_window(source, stream) {
|
|
3317
|
+
if (source === void 0 || source === stream) return true;
|
|
3318
|
+
if (is_literal_source(source)) return false;
|
|
3319
|
+
let pattern = this._patterns.get(source);
|
|
3320
|
+
if (!pattern) {
|
|
3321
|
+
pattern = new RegExp(source);
|
|
3322
|
+
this._patterns.set(source, pattern);
|
|
3323
|
+
}
|
|
3324
|
+
return pattern.test(stream);
|
|
3325
|
+
}
|
|
3326
|
+
/**
|
|
3327
|
+
* Scan the events past the checkpoint, resolve each to its target
|
|
3328
|
+
* streams, and record what it found through `cd.subscribe` — new dynamic
|
|
3329
|
+
* targets get registered, and every target an event resolved to gets its
|
|
3330
|
+
* **work mark** raised to that event's id (#1487).
|
|
3331
|
+
*
|
|
3332
|
+
* Both resolver kinds are walked. A static target is already subscribed
|
|
3333
|
+
* at init, but marking it is what makes it claimable without probing the
|
|
3334
|
+
* event log, so the scan runs for every app.
|
|
3319
3335
|
*/
|
|
3320
3336
|
async correlate(query = { after: -1, limit: 10 }) {
|
|
3321
3337
|
await this.init();
|
|
3322
|
-
if (!this._has_dynamic_resolvers)
|
|
3323
|
-
return { subscribed: 0, last_id: this._checkpoint };
|
|
3324
3338
|
const after = Math.max(this._checkpoint, query.after || -1);
|
|
3325
3339
|
const correlated = /* @__PURE__ */ new Map();
|
|
3326
3340
|
let last_id = after;
|
|
@@ -3330,52 +3344,61 @@ var CorrelateCycle = class {
|
|
|
3330
3344
|
const register = this._registry.events[event.name];
|
|
3331
3345
|
if (register) {
|
|
3332
3346
|
for (const reaction of register.reactions.values()) {
|
|
3333
|
-
|
|
3334
|
-
|
|
3335
|
-
|
|
3336
|
-
|
|
3337
|
-
|
|
3338
|
-
|
|
3339
|
-
|
|
3340
|
-
|
|
3341
|
-
|
|
3342
|
-
|
|
3343
|
-
|
|
3344
|
-
|
|
3345
|
-
|
|
3346
|
-
|
|
3347
|
-
entry.
|
|
3348
|
-
|
|
3349
|
-
|
|
3350
|
-
event
|
|
3351
|
-
});
|
|
3352
|
-
correlated.set(resolved.target, entry);
|
|
3347
|
+
const resolved = typeof reaction.resolver === "function" ? reaction.resolver(event) : reaction.resolver;
|
|
3348
|
+
if (!resolved) continue;
|
|
3349
|
+
const recorded = this._subscribed.get(resolved.target);
|
|
3350
|
+
const priority = resolved.priority ?? 0;
|
|
3351
|
+
const upgraded = !recorded || priority > recorded.floor;
|
|
3352
|
+
const carried = upgraded ? { priority, lane: resolved.lane } : { priority: recorded.priority, lane: recorded.lane };
|
|
3353
|
+
const entry = correlated.get(resolved.target) || {
|
|
3354
|
+
source: resolved.source,
|
|
3355
|
+
priority: carried.priority,
|
|
3356
|
+
lane: carried.lane,
|
|
3357
|
+
upgraded,
|
|
3358
|
+
correlated_at: void 0
|
|
3359
|
+
};
|
|
3360
|
+
if (carried.priority > entry.priority) {
|
|
3361
|
+
entry.priority = carried.priority;
|
|
3362
|
+
entry.lane = carried.lane;
|
|
3363
|
+
entry.upgraded = upgraded;
|
|
3353
3364
|
}
|
|
3365
|
+
if (this._in_fetch_window(resolved.source, event.stream))
|
|
3366
|
+
entry.correlated_at = event.id;
|
|
3367
|
+
correlated.set(resolved.target, entry);
|
|
3354
3368
|
}
|
|
3355
3369
|
}
|
|
3356
3370
|
},
|
|
3357
3371
|
{ ...query, after }
|
|
3358
3372
|
);
|
|
3359
|
-
|
|
3360
|
-
|
|
3361
|
-
|
|
3373
|
+
const streams = [];
|
|
3374
|
+
for (const [stream, entry] of correlated) {
|
|
3375
|
+
if (entry.upgraded || entry.correlated_at !== void 0)
|
|
3376
|
+
streams.push({
|
|
3362
3377
|
stream,
|
|
3363
|
-
source,
|
|
3364
|
-
priority,
|
|
3365
|
-
lane
|
|
3366
|
-
|
|
3367
|
-
|
|
3378
|
+
source: entry.source,
|
|
3379
|
+
priority: entry.priority,
|
|
3380
|
+
lane: entry.lane,
|
|
3381
|
+
correlated_at: entry.correlated_at
|
|
3382
|
+
});
|
|
3383
|
+
}
|
|
3384
|
+
if (streams.length) {
|
|
3368
3385
|
const { subscribed } = await this._cd.subscribe(streams, last_id);
|
|
3386
|
+
const marked = streams.filter(
|
|
3387
|
+
(entry) => entry.correlated_at !== void 0
|
|
3388
|
+
).length;
|
|
3369
3389
|
this._checkpoint = last_id;
|
|
3370
|
-
|
|
3371
|
-
|
|
3372
|
-
this._subscribed.set(stream,
|
|
3373
|
-
|
|
3390
|
+
for (const { stream, priority, lane } of streams) {
|
|
3391
|
+
if (correlated.get(stream)?.upgraded)
|
|
3392
|
+
this._subscribed.set(stream, {
|
|
3393
|
+
floor: priority,
|
|
3394
|
+
priority,
|
|
3395
|
+
lane
|
|
3396
|
+
});
|
|
3374
3397
|
}
|
|
3375
|
-
return { subscribed, last_id };
|
|
3398
|
+
return { subscribed, last_id, marked };
|
|
3376
3399
|
}
|
|
3377
3400
|
this._checkpoint = last_id;
|
|
3378
|
-
return { subscribed: 0, last_id };
|
|
3401
|
+
return { subscribed: 0, last_id, marked: 0 };
|
|
3379
3402
|
}
|
|
3380
3403
|
/**
|
|
3381
3404
|
* Start a periodic correlation worker. Returns false if one is already
|
|
@@ -4678,7 +4701,7 @@ var SettleLoop = class {
|
|
|
4678
4701
|
...correlate_query,
|
|
4679
4702
|
after: after_before
|
|
4680
4703
|
});
|
|
4681
|
-
|
|
4704
|
+
this._deps.breaker.passed();
|
|
4682
4705
|
const drain = await this._deps.drain(drain_options);
|
|
4683
4706
|
settled_drain = settled_drain ? {
|
|
4684
4707
|
fetched: [...settled_drain.fetched, ...drain.fetched],
|
|
@@ -4724,6 +4747,8 @@ var SettleLoop = class {
|
|
|
4724
4747
|
|
|
4725
4748
|
// src/act.ts
|
|
4726
4749
|
var DEFAULT_MAX_SUBSCRIBED_STREAMS = 1e3;
|
|
4750
|
+
var CLOSE_CATCH_UP_LIMIT = 1e3;
|
|
4751
|
+
var CLOSE_CATCH_UP_PASSES = 20;
|
|
4727
4752
|
var DEFAULT_SETTLE_DEBOUNCE_MS = 10;
|
|
4728
4753
|
function validate_only_lanes(options, lanes) {
|
|
4729
4754
|
if (!options.onlyLanes || options.onlyLanes.length === 0) return;
|
|
@@ -5039,6 +5064,7 @@ var Act = class {
|
|
|
5039
5064
|
const close_actor = { id: "$close", name: "close" };
|
|
5040
5065
|
const result = await run_close_cycle(targets, {
|
|
5041
5066
|
reactive_events_size: this._reactive_events.size,
|
|
5067
|
+
catch_up_correlation: (until) => this._catch_up_correlation(until),
|
|
5042
5068
|
event_to_state: this._event_to_state,
|
|
5043
5069
|
load: this._es.load,
|
|
5044
5070
|
tombstone: this._es.tombstone,
|
|
@@ -5125,7 +5151,6 @@ var Act = class {
|
|
|
5125
5151
|
return new CorrelateCycle({
|
|
5126
5152
|
registry: this.registry,
|
|
5127
5153
|
static_targets: classification.static_targets,
|
|
5128
|
-
has_dynamic_resolvers: classification.has_dynamic_resolvers,
|
|
5129
5154
|
cd: this._cd,
|
|
5130
5155
|
max_subscribed_streams: options.maxSubscribedStreams ?? DEFAULT_MAX_SUBSCRIBED_STREAMS,
|
|
5131
5156
|
on_init: () => {
|
|
@@ -5176,11 +5201,7 @@ var Act = class {
|
|
|
5176
5201
|
correlate: (q) => this.correlate(q),
|
|
5177
5202
|
drain: (o) => this.drain(o),
|
|
5178
5203
|
on_settled: (drain) => this.emit("settled", drain),
|
|
5179
|
-
breaker: this._breaker
|
|
5180
|
-
// Static-reaction apps' correlate is a no-op (no store call), so a
|
|
5181
|
-
// settle pass carries no store-health signal — don't let it record a
|
|
5182
|
-
// breaker success (#1329).
|
|
5183
|
-
correlate_probes_store: this._correlate.has_dynamic_resolvers
|
|
5204
|
+
breaker: this._breaker
|
|
5184
5205
|
},
|
|
5185
5206
|
options.settleDebounceMs ?? DEFAULT_SETTLE_DEBOUNCE_MS
|
|
5186
5207
|
);
|
|
@@ -5553,8 +5574,13 @@ var Act = class {
|
|
|
5553
5574
|
* Drain uses a dual-frontier strategy to balance processing of new streams (lagging)
|
|
5554
5575
|
* vs active streams (leading). The ratio adapts based on event pressure.
|
|
5555
5576
|
*
|
|
5556
|
-
* Call `correlate()` before `drain()
|
|
5557
|
-
*
|
|
5577
|
+
* Call `correlate()` before `drain()`. It is not only how dynamic targets
|
|
5578
|
+
* are discovered: a stream is claimable while `at < correlated_at`, and
|
|
5579
|
+
* `correlate` is the only component that raises that mark (#1487), so a
|
|
5580
|
+
* commit no correlate has seen is not drainable — including for static
|
|
5581
|
+
* targets, which were served by a probe of the event log before. For a
|
|
5582
|
+
* higher-level API that handles debouncing, correlation, and signaling
|
|
5583
|
+
* automatically, use {@link settle}.
|
|
5558
5584
|
*
|
|
5559
5585
|
* @param options - Drain configuration — see {@link DrainOptions} for fields
|
|
5560
5586
|
* (`streamLimit`, `eventLimit`, `leaseMillis`).
|
|
@@ -5683,10 +5709,10 @@ var Act = class {
|
|
|
5683
5709
|
async correlate(query = { after: -1, limit: 10 }) {
|
|
5684
5710
|
if (!this._drain) return { subscribed: 0, last_id: -1 };
|
|
5685
5711
|
return this._scoped(async () => {
|
|
5686
|
-
const
|
|
5687
|
-
if (
|
|
5712
|
+
const { subscribed, last_id, marked } = await this._correlate.correlate(query);
|
|
5713
|
+
if ((subscribed > 0 || marked > 0) && this._reactive_events.size > 0)
|
|
5688
5714
|
this._arm_all();
|
|
5689
|
-
return
|
|
5715
|
+
return { subscribed, last_id };
|
|
5690
5716
|
});
|
|
5691
5717
|
}
|
|
5692
5718
|
/**
|
|
@@ -6104,6 +6130,26 @@ var Act = class {
|
|
|
6104
6130
|
* removed, so a later correlate can re-subscribe it (#1398). Restart
|
|
6105
6131
|
* targets keep their row, so only fully-retired streams are forgotten.
|
|
6106
6132
|
*/
|
|
6133
|
+
/**
|
|
6134
|
+
* Advance correlation until the read cursor reaches `until`, and report
|
|
6135
|
+
* where it landed (#1487). Used by the close cycle's safety probe, which
|
|
6136
|
+
* cannot judge a subscription's pending work over events correlate has
|
|
6137
|
+
* not resolved yet.
|
|
6138
|
+
*
|
|
6139
|
+
* Bounded on both ends: it stops as soon as a pass makes no progress (the
|
|
6140
|
+
* log has no more to give), and after {@link CLOSE_CATCH_UP_PASSES}
|
|
6141
|
+
* windows, so a close behind an enormous backlog degrades to skipping the
|
|
6142
|
+
* stream — the documented retryable outcome — instead of scanning the
|
|
6143
|
+
* whole log inside an operator call.
|
|
6144
|
+
*/
|
|
6145
|
+
async _catch_up_correlation(until) {
|
|
6146
|
+
for (let pass = 0; pass < CLOSE_CATCH_UP_PASSES && this._correlate.checkpoint < until; pass++) {
|
|
6147
|
+
const before = this._correlate.checkpoint;
|
|
6148
|
+
await this.correlate({ limit: CLOSE_CATCH_UP_LIMIT });
|
|
6149
|
+
if (this._correlate.checkpoint <= before) break;
|
|
6150
|
+
}
|
|
6151
|
+
return this._correlate.checkpoint;
|
|
6152
|
+
}
|
|
6107
6153
|
_forget_closed_subscriptions(result) {
|
|
6108
6154
|
const retired = [...result.truncated.entries()].filter(([, r]) => r.committed.name === TOMBSTONE_EVENT).map(([stream]) => stream);
|
|
6109
6155
|
if (retired.length) this._correlate.forget_subscribed(retired);
|
|
@@ -6115,6 +6161,7 @@ var Act = class {
|
|
|
6115
6161
|
const close_actor = { id: "$close", name: "close" };
|
|
6116
6162
|
const result = await run_close_cycle(targets, {
|
|
6117
6163
|
reactive_events_size: this._reactive_events.size,
|
|
6164
|
+
catch_up_correlation: (until) => this._catch_up_correlation(until),
|
|
6118
6165
|
event_to_state: this._event_to_state,
|
|
6119
6166
|
load: this._es.load,
|
|
6120
6167
|
tombstone: this._es.tombstone,
|