@hunterzhu/pulse-runtime 0.1.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/context/builder.d.ts +68 -0
- package/dist/context/builder.js +127 -0
- package/dist/context/index.d.ts +2 -0
- package/dist/context/index.js +2 -0
- package/dist/context/merger.d.ts +25 -0
- package/dist/context/merger.js +125 -0
- package/dist/core/actions.d.ts +1 -0
- package/dist/core/actions.js +1 -0
- package/dist/core/errors.d.ts +8 -0
- package/dist/core/errors.js +36 -0
- package/dist/core/events.d.ts +10 -0
- package/dist/core/events.js +24 -0
- package/dist/core/factory.d.ts +35 -0
- package/dist/core/factory.js +27 -0
- package/dist/core/inbox.d.ts +119 -0
- package/dist/core/inbox.js +217 -0
- package/dist/core/mutations.d.ts +80 -0
- package/dist/core/mutations.js +127 -0
- package/dist/core/records.d.ts +1 -0
- package/dist/core/records.js +1 -0
- package/dist/core/types.d.ts +615 -0
- package/dist/core/types.js +109 -0
- package/dist/dependencies/graph.d.ts +25 -0
- package/dist/dependencies/graph.js +92 -0
- package/dist/dependencies/index.d.ts +1 -0
- package/dist/dependencies/index.js +1 -0
- package/dist/dsl/context-proxy.d.ts +20 -0
- package/dist/dsl/context-proxy.js +64 -0
- package/dist/dsl/index.d.ts +4 -0
- package/dist/dsl/index.js +4 -0
- package/dist/dsl/program.d.ts +314 -0
- package/dist/dsl/program.js +756 -0
- package/dist/dsl/session.d.ts +45 -0
- package/dist/dsl/session.js +93 -0
- package/dist/dsl/templates-index.d.ts +1 -0
- package/dist/dsl/templates-index.js +1 -0
- package/dist/dsl/templates.d.ts +85 -0
- package/dist/dsl/templates.js +110 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.js +15 -0
- package/dist/lifecycle/index.d.ts +2 -0
- package/dist/lifecycle/index.js +2 -0
- package/dist/lifecycle/scopes.d.ts +38 -0
- package/dist/lifecycle/scopes.js +50 -0
- package/dist/lifecycle/watchdog.d.ts +16 -0
- package/dist/lifecycle/watchdog.js +66 -0
- package/dist/models/actions.d.ts +10 -0
- package/dist/models/actions.js +68 -0
- package/dist/models/index.d.ts +2 -0
- package/dist/models/index.js +2 -0
- package/dist/models/router.d.ts +187 -0
- package/dist/models/router.js +353 -0
- package/dist/scheduler/clock.d.ts +45 -0
- package/dist/scheduler/clock.js +92 -0
- package/dist/scheduler/decision.d.ts +72 -0
- package/dist/scheduler/decision.js +63 -0
- package/dist/scheduler/index.d.ts +6 -0
- package/dist/scheduler/index.js +6 -0
- package/dist/scheduler/locks.d.ts +18 -0
- package/dist/scheduler/locks.js +106 -0
- package/dist/scheduler/ready-queue.d.ts +32 -0
- package/dist/scheduler/ready-queue.js +40 -0
- package/dist/scheduler/runtime.d.ts +486 -0
- package/dist/scheduler/runtime.js +3445 -0
- package/dist/scheduler/telemetry.d.ts +111 -0
- package/dist/scheduler/telemetry.js +177 -0
- package/dist/scheduler/worker.d.ts +158 -0
- package/dist/scheduler/worker.js +744 -0
- package/dist/storage/artifacts.d.ts +17 -0
- package/dist/storage/artifacts.js +90 -0
- package/dist/storage/findings.d.ts +12 -0
- package/dist/storage/findings.js +70 -0
- package/dist/storage/index.d.ts +8 -0
- package/dist/storage/index.js +8 -0
- package/dist/storage/memory.d.ts +11 -0
- package/dist/storage/memory.js +21 -0
- package/dist/storage/mutation-log.d.ts +41 -0
- package/dist/storage/mutation-log.js +140 -0
- package/dist/storage/outbox.d.ts +30 -0
- package/dist/storage/outbox.js +59 -0
- package/dist/storage/persistence.d.ts +183 -0
- package/dist/storage/persistence.js +999 -0
- package/dist/storage/policy.d.ts +80 -0
- package/dist/storage/policy.js +268 -0
- package/dist/storage/session.d.ts +140 -0
- package/dist/storage/session.js +447 -0
- package/dist/tools/registry.d.ts +125 -0
- package/dist/tools/registry.js +308 -0
- package/dist/transitions/index.d.ts +2 -0
- package/dist/transitions/index.js +1 -0
- package/dist/transitions/validate.d.ts +4 -0
- package/dist/transitions/validate.js +1118 -0
- package/package.json +21 -0
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { performance } from 'node:perf_hooks';
|
|
2
|
+
export class TimerWheel {
|
|
3
|
+
entries = new Map();
|
|
4
|
+
selected = new Map();
|
|
5
|
+
seq = 0;
|
|
6
|
+
schedule(at, callback) { const id = `timer-${++this.seq}`; this.entries.set(id, { id, at, callback, cancelled: false }); return id; }
|
|
7
|
+
cancel(id) { const entry = this.entries.get(id) ?? this.selected.get(id); if (entry)
|
|
8
|
+
entry.cancelled = true; }
|
|
9
|
+
due(now, limit = Number.POSITIVE_INFINITY) {
|
|
10
|
+
const due = [...this.entries.values()].filter((entry) => !entry.cancelled && entry.at <= now).sort((a, b) => a.at - b.at || a.id.localeCompare(b.id)).slice(0, limit);
|
|
11
|
+
return due.map((entry) => {
|
|
12
|
+
this.entries.delete(entry.id);
|
|
13
|
+
this.selected.set(entry.id, entry);
|
|
14
|
+
let completed = false;
|
|
15
|
+
return {
|
|
16
|
+
...entry,
|
|
17
|
+
callback: () => {
|
|
18
|
+
if (completed)
|
|
19
|
+
return;
|
|
20
|
+
if (entry.cancelled) {
|
|
21
|
+
completed = true;
|
|
22
|
+
this.selected.delete(entry.id);
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
try {
|
|
26
|
+
entry.callback();
|
|
27
|
+
completed = true;
|
|
28
|
+
this.selected.delete(entry.id);
|
|
29
|
+
}
|
|
30
|
+
catch (cause) {
|
|
31
|
+
this.selected.delete(entry.id);
|
|
32
|
+
if (!entry.cancelled)
|
|
33
|
+
this.entries.set(entry.id, entry);
|
|
34
|
+
throw cause;
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
nextAt() { return [...this.entries.values()].filter((entry) => !entry.cancelled).sort((a, b) => a.at - b.at)[0]?.at; }
|
|
41
|
+
get size() { return [...this.entries.values()].filter((entry) => !entry.cancelled).length; }
|
|
42
|
+
}
|
|
43
|
+
export class VirtualClock {
|
|
44
|
+
timers = new TimerWheel();
|
|
45
|
+
current = 0;
|
|
46
|
+
now() { return this.current; }
|
|
47
|
+
set(now) { if (now < this.current)
|
|
48
|
+
throw new Error('VirtualClock cannot move backwards'); this.current = now; }
|
|
49
|
+
advance(ms) { if (ms < 0)
|
|
50
|
+
throw new Error('VirtualClock cannot move backwards'); this.current += ms; }
|
|
51
|
+
schedule(delayMs, callback) { return this.timers.schedule(this.current + delayMs, callback); }
|
|
52
|
+
}
|
|
53
|
+
/** Monotonic host clock for production runtimes; timers never fast-forward. */
|
|
54
|
+
export class MonotonicClock {
|
|
55
|
+
timers = new TimerWheel();
|
|
56
|
+
startedAt = performance.now();
|
|
57
|
+
epoch;
|
|
58
|
+
current;
|
|
59
|
+
constructor(startAt = Date.now()) {
|
|
60
|
+
if (!Number.isFinite(startAt))
|
|
61
|
+
throw new Error('INVALID_CLOCK_START');
|
|
62
|
+
this.epoch = startAt;
|
|
63
|
+
this.current = startAt;
|
|
64
|
+
}
|
|
65
|
+
now() {
|
|
66
|
+
this.current = Math.max(this.current, this.epoch + performance.now() - this.startedAt);
|
|
67
|
+
return this.current;
|
|
68
|
+
}
|
|
69
|
+
set(now) {
|
|
70
|
+
if (!Number.isFinite(now) || now < this.current)
|
|
71
|
+
return;
|
|
72
|
+
this.current = now;
|
|
73
|
+
}
|
|
74
|
+
advance(ms) {
|
|
75
|
+
if (!Number.isFinite(ms) || ms < 0)
|
|
76
|
+
throw new Error('MonotonicClock cannot move backwards');
|
|
77
|
+
this.set(this.now() + ms);
|
|
78
|
+
}
|
|
79
|
+
schedule(delayMs, callback) {
|
|
80
|
+
if (!Number.isFinite(delayMs) || delayMs < 0)
|
|
81
|
+
throw new Error('INVALID_TIMER_DELAY');
|
|
82
|
+
return this.timers.schedule(this.now() + delayMs, callback);
|
|
83
|
+
}
|
|
84
|
+
async waitUntil(at) {
|
|
85
|
+
if (!Number.isFinite(at))
|
|
86
|
+
throw new Error('INVALID_TIMER_DEADLINE');
|
|
87
|
+
while (this.now() < at) {
|
|
88
|
+
const remaining = at - this.current;
|
|
89
|
+
await new Promise((resolve) => setTimeout(resolve, Math.max(1, remaining)));
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import type { LaneRecord } from '../core/types.js';
|
|
2
|
+
export interface SchedulerDecisionCandidate {
|
|
3
|
+
laneId: string;
|
|
4
|
+
agentId: string;
|
|
5
|
+
goal?: string;
|
|
6
|
+
effectivePriority: number;
|
|
7
|
+
basePriority: number;
|
|
8
|
+
waitingMs: number;
|
|
9
|
+
inheritedFloor?: number;
|
|
10
|
+
}
|
|
11
|
+
export interface SchedulerDecisionRequest {
|
|
12
|
+
schemaVersion: 1;
|
|
13
|
+
decisionId: string;
|
|
14
|
+
candidateEpoch: number;
|
|
15
|
+
now: number;
|
|
16
|
+
availableSlots: number;
|
|
17
|
+
candidates: readonly SchedulerDecisionCandidate[];
|
|
18
|
+
}
|
|
19
|
+
export interface SchedulerDecision {
|
|
20
|
+
decisionId: string;
|
|
21
|
+
candidateEpoch: number;
|
|
22
|
+
orderedLaneIds: string[];
|
|
23
|
+
modelId: string;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* An advisory scheduler policy. Implementations may be model-, ranker-, or
|
|
27
|
+
* rule-based. They never receive Runtime mutators and their result is only
|
|
28
|
+
* considered after it returns through the Runtime FactInbox.
|
|
29
|
+
*/
|
|
30
|
+
export interface SchedulerDecisionModel {
|
|
31
|
+
readonly id: string;
|
|
32
|
+
decide(request: SchedulerDecisionRequest, signal: AbortSignal): Promise<SchedulerDecision>;
|
|
33
|
+
}
|
|
34
|
+
export interface SchedulerDecisionConfig {
|
|
35
|
+
model?: SchedulerDecisionModel;
|
|
36
|
+
/** Minimum number of ready lanes before the advisory model is consulted. */
|
|
37
|
+
minCandidates?: number;
|
|
38
|
+
/** Maximum number of deterministic top candidates exposed to the model. */
|
|
39
|
+
candidateLimit?: number;
|
|
40
|
+
/** Maximum time an advisory request may run before deterministic fallback. */
|
|
41
|
+
decisionTimeoutMs?: number;
|
|
42
|
+
/** Number of outstanding advisory requests allowed at once. */
|
|
43
|
+
maxOutstandingDecisions?: number;
|
|
44
|
+
/** Maximum distance from deterministic order that a suggestion may move a lane. */
|
|
45
|
+
maxReorderDistance?: number;
|
|
46
|
+
/** Every Nth dispatch is deterministic, even when a model suggestion exists. */
|
|
47
|
+
deterministicReserveEvery?: number;
|
|
48
|
+
/** Goals are local data; expose them only when the host explicitly opts in. */
|
|
49
|
+
includeGoals?: boolean;
|
|
50
|
+
}
|
|
51
|
+
export interface SchedulerDecisionCoordinatorOptions {
|
|
52
|
+
model: SchedulerDecisionModel;
|
|
53
|
+
timeoutMs: number;
|
|
54
|
+
maxOutstanding: number;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Owns only the lifecycle of advisory calls. It deliberately has no Runtime
|
|
58
|
+
* reference: completion is delivered to the caller, which must enqueue a
|
|
59
|
+
* fact before touching Runtime state.
|
|
60
|
+
*/
|
|
61
|
+
export declare class SchedulerDecisionCoordinator {
|
|
62
|
+
private readonly model;
|
|
63
|
+
private readonly timeoutMs;
|
|
64
|
+
private readonly maxOutstanding;
|
|
65
|
+
private readonly controllers;
|
|
66
|
+
private outstanding;
|
|
67
|
+
constructor(options: SchedulerDecisionCoordinatorOptions);
|
|
68
|
+
get pending(): number;
|
|
69
|
+
request(request: SchedulerDecisionRequest, onDecision: (decision: SchedulerDecision) => void, onFailure?: () => void): boolean;
|
|
70
|
+
cancel(): void;
|
|
71
|
+
}
|
|
72
|
+
export declare function schedulerDecisionCandidateFromLane(lane: Readonly<LaneRecord>, effectivePriority: number, now: number, includeGoal: boolean, inheritedFloor?: number | undefined): SchedulerDecisionCandidate;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Owns only the lifecycle of advisory calls. It deliberately has no Runtime
|
|
3
|
+
* reference: completion is delivered to the caller, which must enqueue a
|
|
4
|
+
* fact before touching Runtime state.
|
|
5
|
+
*/
|
|
6
|
+
export class SchedulerDecisionCoordinator {
|
|
7
|
+
model;
|
|
8
|
+
timeoutMs;
|
|
9
|
+
maxOutstanding;
|
|
10
|
+
controllers = new Set();
|
|
11
|
+
outstanding = 0;
|
|
12
|
+
constructor(options) {
|
|
13
|
+
this.model = options.model;
|
|
14
|
+
this.timeoutMs = options.timeoutMs;
|
|
15
|
+
this.maxOutstanding = options.maxOutstanding;
|
|
16
|
+
}
|
|
17
|
+
get pending() { return this.outstanding; }
|
|
18
|
+
request(request, onDecision, onFailure = () => undefined) {
|
|
19
|
+
if (this.outstanding >= this.maxOutstanding)
|
|
20
|
+
return false;
|
|
21
|
+
const controller = new AbortController();
|
|
22
|
+
this.controllers.add(controller);
|
|
23
|
+
this.outstanding++;
|
|
24
|
+
const timer = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
25
|
+
void Promise.resolve()
|
|
26
|
+
.then(() => this.model.decide(structuredClone(request), controller.signal))
|
|
27
|
+
.then((decision) => {
|
|
28
|
+
if (!decision || typeof decision !== 'object')
|
|
29
|
+
return;
|
|
30
|
+
try {
|
|
31
|
+
onDecision(structuredClone(decision));
|
|
32
|
+
}
|
|
33
|
+
catch { /* advisory failures never escape into the Runtime turn */ }
|
|
34
|
+
}, () => {
|
|
35
|
+
try {
|
|
36
|
+
onFailure();
|
|
37
|
+
}
|
|
38
|
+
catch { /* advisory failures never escape into the Runtime turn */ }
|
|
39
|
+
})
|
|
40
|
+
.finally(() => {
|
|
41
|
+
clearTimeout(timer);
|
|
42
|
+
this.controllers.delete(controller);
|
|
43
|
+
this.outstanding--;
|
|
44
|
+
});
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
cancel() {
|
|
48
|
+
for (const controller of this.controllers)
|
|
49
|
+
controller.abort();
|
|
50
|
+
this.controllers.clear();
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
export function schedulerDecisionCandidateFromLane(lane, effectivePriority, now, includeGoal, inheritedFloor = lane.inheritedFloor) {
|
|
54
|
+
return {
|
|
55
|
+
laneId: lane.id,
|
|
56
|
+
agentId: lane.agentId,
|
|
57
|
+
...(includeGoal ? { goal: lane.goal } : {}),
|
|
58
|
+
effectivePriority,
|
|
59
|
+
basePriority: lane.priority,
|
|
60
|
+
waitingMs: Math.max(0, now - lane.readySince),
|
|
61
|
+
...(inheritedFloor === undefined ? {} : { inheritedFloor }),
|
|
62
|
+
};
|
|
63
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export type LockMode = 'shared' | 'exclusive';
|
|
2
|
+
export declare class ResourceLockManager {
|
|
3
|
+
readonly writerPreferenceBound: number;
|
|
4
|
+
private readonly holders;
|
|
5
|
+
private readonly queues;
|
|
6
|
+
private seq;
|
|
7
|
+
constructor(writerPreferenceBound?: number);
|
|
8
|
+
acquire(resource: string, mode: LockMode, requestId?: string): Promise<() => void>;
|
|
9
|
+
tryAcquire(resource: string, mode: LockMode, requestId?: string): (() => void) | undefined;
|
|
10
|
+
wait(resource: string, mode: LockMode, requestId: string, onGrant: (release: () => void) => void): void;
|
|
11
|
+
cancelWait(resource: string, requestId: string): void;
|
|
12
|
+
restoreHeld(resource: string, mode: LockMode, requestId: string): () => void;
|
|
13
|
+
private canGrant;
|
|
14
|
+
private drain;
|
|
15
|
+
release(resource: string, requestId: string): void;
|
|
16
|
+
isHeld(resource: string, mode?: LockMode): boolean;
|
|
17
|
+
queued(resource: string): number;
|
|
18
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
export class ResourceLockManager {
|
|
2
|
+
writerPreferenceBound;
|
|
3
|
+
holders = new Map();
|
|
4
|
+
queues = new Map();
|
|
5
|
+
seq = 0;
|
|
6
|
+
constructor(writerPreferenceBound = Number.POSITIVE_INFINITY) {
|
|
7
|
+
this.writerPreferenceBound = writerPreferenceBound;
|
|
8
|
+
if (writerPreferenceBound !== Number.POSITIVE_INFINITY && (!Number.isInteger(writerPreferenceBound) || writerPreferenceBound < 0))
|
|
9
|
+
throw new Error('INVALID_WRITER_PREFERENCE_BOUND');
|
|
10
|
+
}
|
|
11
|
+
acquire(resource, mode, requestId = `lock-${++this.seq}`) {
|
|
12
|
+
return new Promise((resolve, reject) => {
|
|
13
|
+
const queue = this.queues.get(resource) ?? [];
|
|
14
|
+
const request = { id: requestId, mode, seq: ++this.seq, ...(mode === 'exclusive' ? { aheadSharedIds: queue.filter((queued) => queued.mode === 'shared').map((queued) => queued.id) } : {}), resolve, reject };
|
|
15
|
+
queue.push(request);
|
|
16
|
+
this.queues.set(resource, queue);
|
|
17
|
+
this.drain(resource);
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
tryAcquire(resource, mode, requestId = `lock-${++this.seq}`) {
|
|
21
|
+
const holders = this.holders.get(resource) ?? new Map();
|
|
22
|
+
const queue = this.queues.get(resource) ?? [];
|
|
23
|
+
if (queue.length > 0)
|
|
24
|
+
return undefined;
|
|
25
|
+
if (mode === 'exclusive' && holders.size > 0)
|
|
26
|
+
return undefined;
|
|
27
|
+
if (mode === 'shared' && [...holders.values()].some((heldMode) => heldMode === 'exclusive'))
|
|
28
|
+
return undefined;
|
|
29
|
+
holders.set(requestId, mode);
|
|
30
|
+
this.holders.set(resource, holders);
|
|
31
|
+
return () => this.release(resource, requestId);
|
|
32
|
+
}
|
|
33
|
+
wait(resource, mode, requestId, onGrant) {
|
|
34
|
+
const holders = this.holders.get(resource) ?? new Map();
|
|
35
|
+
if (holders.has(requestId)) {
|
|
36
|
+
onGrant(() => this.release(resource, requestId));
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
const queue = this.queues.get(resource) ?? [];
|
|
40
|
+
if (queue.some((request) => request.id === requestId))
|
|
41
|
+
return;
|
|
42
|
+
const request = { id: requestId, mode, seq: ++this.seq, ...(mode === 'exclusive' ? { aheadSharedIds: queue.filter((queued) => queued.mode === 'shared').map((queued) => queued.id) } : {}), resolve: onGrant, reject: () => undefined };
|
|
43
|
+
queue.push(request);
|
|
44
|
+
this.queues.set(resource, queue);
|
|
45
|
+
this.drain(resource);
|
|
46
|
+
}
|
|
47
|
+
cancelWait(resource, requestId) {
|
|
48
|
+
const queue = this.queues.get(resource);
|
|
49
|
+
if (!queue)
|
|
50
|
+
return;
|
|
51
|
+
const index = queue.findIndex((request) => request.id === requestId);
|
|
52
|
+
if (index >= 0)
|
|
53
|
+
queue.splice(index, 1);
|
|
54
|
+
if (queue.length === 0)
|
|
55
|
+
this.queues.delete(resource);
|
|
56
|
+
}
|
|
57
|
+
restoreHeld(resource, mode, requestId) {
|
|
58
|
+
if (!requestId || this.holders.get(resource)?.has(requestId))
|
|
59
|
+
throw new Error('INVALID_LOCK_RESTORE');
|
|
60
|
+
const holders = this.holders.get(resource) ?? new Map();
|
|
61
|
+
if (mode === 'exclusive' && holders.size > 0)
|
|
62
|
+
throw new Error('RESOURCE_RESTORE_CONFLICT');
|
|
63
|
+
if (mode === 'shared' && [...holders.values()].some((heldMode) => heldMode === 'exclusive'))
|
|
64
|
+
throw new Error('RESOURCE_RESTORE_CONFLICT');
|
|
65
|
+
if ((this.queues.get(resource)?.length ?? 0) > 0)
|
|
66
|
+
throw new Error('RESOURCE_RESTORE_CONFLICT');
|
|
67
|
+
holders.set(requestId, mode);
|
|
68
|
+
this.holders.set(resource, holders);
|
|
69
|
+
return () => this.release(resource, requestId);
|
|
70
|
+
}
|
|
71
|
+
canGrant(resource, request) {
|
|
72
|
+
const holders = this.holders.get(resource) ?? new Map();
|
|
73
|
+
if (request.mode === 'shared' && [...holders.values()].some((mode) => mode === 'exclusive'))
|
|
74
|
+
return false;
|
|
75
|
+
if (request.mode === 'exclusive' && holders.size)
|
|
76
|
+
return false;
|
|
77
|
+
const queue = this.queues.get(resource) ?? [];
|
|
78
|
+
const index = queue.findIndex((queued) => queued.id === request.id);
|
|
79
|
+
if (index < 0)
|
|
80
|
+
return false;
|
|
81
|
+
if (request.mode === 'exclusive')
|
|
82
|
+
return !queue.slice(0, index).some((queued) => queued.mode === 'exclusive');
|
|
83
|
+
const firstWriter = queue.find((queued) => queued.mode === 'exclusive');
|
|
84
|
+
if (firstWriter === undefined)
|
|
85
|
+
return true;
|
|
86
|
+
const sharedOrdinal = firstWriter.aheadSharedIds?.indexOf(request.id) ?? -1;
|
|
87
|
+
return sharedOrdinal >= 0 && sharedOrdinal < this.writerPreferenceBound;
|
|
88
|
+
}
|
|
89
|
+
drain(resource) {
|
|
90
|
+
const queue = this.queues.get(resource) ?? [];
|
|
91
|
+
for (const request of [...queue]) {
|
|
92
|
+
if (!this.canGrant(resource, request))
|
|
93
|
+
continue;
|
|
94
|
+
const holders = this.holders.get(resource) ?? new Map();
|
|
95
|
+
holders.set(request.id, request.mode);
|
|
96
|
+
this.holders.set(resource, holders);
|
|
97
|
+
queue.splice(queue.findIndex((item) => item.id === request.id), 1);
|
|
98
|
+
request.resolve(() => this.release(resource, request.id));
|
|
99
|
+
if (request.mode === 'exclusive')
|
|
100
|
+
break;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
release(resource, requestId) { this.holders.get(resource)?.delete(requestId); this.drain(resource); }
|
|
104
|
+
isHeld(resource, mode) { const values = [...(this.holders.get(resource)?.values() ?? [])]; return mode ? values.includes(mode) : values.length > 0; }
|
|
105
|
+
queued(resource) { return this.queues.get(resource)?.length ?? 0; }
|
|
106
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { LaneRecord } from '../core/types.js';
|
|
2
|
+
export interface ReadyItem {
|
|
3
|
+
laneId: string;
|
|
4
|
+
basePriority: number;
|
|
5
|
+
readySince: number;
|
|
6
|
+
enqueueSeq: number;
|
|
7
|
+
inheritedFloor?: number;
|
|
8
|
+
}
|
|
9
|
+
export declare class ReadyQueue {
|
|
10
|
+
private readonly agingIntervalMs;
|
|
11
|
+
private readonly agingCap;
|
|
12
|
+
private readonly items;
|
|
13
|
+
constructor(agingIntervalMs?: number, agingCap?: number);
|
|
14
|
+
enqueue(item: ReadyItem): void;
|
|
15
|
+
remove(laneId: string): void;
|
|
16
|
+
get size(): number;
|
|
17
|
+
has(laneId: string): boolean;
|
|
18
|
+
score(item: ReadyItem, now: number): number;
|
|
19
|
+
dequeue(now: number): string | undefined;
|
|
20
|
+
dequeueSpecific(laneId: string): string | undefined;
|
|
21
|
+
snapshot(now: number): Array<ReadyItem & {
|
|
22
|
+
effectivePriority: number;
|
|
23
|
+
}>;
|
|
24
|
+
}
|
|
25
|
+
export declare class PriorityInheritance {
|
|
26
|
+
private readonly floors;
|
|
27
|
+
clear(): void;
|
|
28
|
+
raise(targetLaneId: string, consumerId: string, score: number): void;
|
|
29
|
+
release(targetLaneId: string, consumerId: string): void;
|
|
30
|
+
floor(targetLaneId: string): number | undefined;
|
|
31
|
+
}
|
|
32
|
+
export declare function readyItemFromLane(lane: LaneRecord, inheritedFloor?: number | undefined): ReadyItem;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
export class ReadyQueue {
|
|
2
|
+
agingIntervalMs;
|
|
3
|
+
agingCap;
|
|
4
|
+
items = new Map();
|
|
5
|
+
constructor(agingIntervalMs = 1000, agingCap = Number.POSITIVE_INFINITY) {
|
|
6
|
+
this.agingIntervalMs = agingIntervalMs;
|
|
7
|
+
this.agingCap = agingCap;
|
|
8
|
+
}
|
|
9
|
+
enqueue(item) { this.items.set(item.laneId, item); }
|
|
10
|
+
remove(laneId) { this.items.delete(laneId); }
|
|
11
|
+
get size() { return this.items.size; }
|
|
12
|
+
has(laneId) { return this.items.has(laneId); }
|
|
13
|
+
score(item, now) {
|
|
14
|
+
const aging = Math.min(this.agingCap, Math.floor(Math.max(0, now - item.readySince) / this.agingIntervalMs));
|
|
15
|
+
return Math.max(item.basePriority + aging, item.inheritedFloor ?? Number.NEGATIVE_INFINITY);
|
|
16
|
+
}
|
|
17
|
+
dequeue(now) {
|
|
18
|
+
const best = [...this.items.values()].sort((a, b) => this.score(b, now) - this.score(a, now) || a.enqueueSeq - b.enqueueSeq)[0];
|
|
19
|
+
if (!best)
|
|
20
|
+
return undefined;
|
|
21
|
+
this.items.delete(best.laneId);
|
|
22
|
+
return best.laneId;
|
|
23
|
+
}
|
|
24
|
+
dequeueSpecific(laneId) {
|
|
25
|
+
if (!this.items.has(laneId))
|
|
26
|
+
return undefined;
|
|
27
|
+
this.items.delete(laneId);
|
|
28
|
+
return laneId;
|
|
29
|
+
}
|
|
30
|
+
snapshot(now) { return [...this.items.values()].map((item) => ({ ...item, effectivePriority: this.score(item, now) })).sort((a, b) => b.effectivePriority - a.effectivePriority || a.enqueueSeq - b.enqueueSeq); }
|
|
31
|
+
}
|
|
32
|
+
export class PriorityInheritance {
|
|
33
|
+
floors = new Map();
|
|
34
|
+
clear() { this.floors.clear(); }
|
|
35
|
+
raise(targetLaneId, consumerId, score) { if (!this.floors.has(targetLaneId))
|
|
36
|
+
this.floors.set(targetLaneId, new Map()); this.floors.get(targetLaneId).set(consumerId, score); }
|
|
37
|
+
release(targetLaneId, consumerId) { this.floors.get(targetLaneId)?.delete(consumerId); }
|
|
38
|
+
floor(targetLaneId) { const values = [...(this.floors.get(targetLaneId)?.values() ?? [])]; return values.length ? Math.max(...values) : undefined; }
|
|
39
|
+
}
|
|
40
|
+
export function readyItemFromLane(lane, inheritedFloor = lane.inheritedFloor) { return { laneId: lane.id, basePriority: lane.priority, readySince: lane.readySince, enqueueSeq: lane.enqueueSeq, ...(inheritedFloor === undefined ? {} : { inheritedFloor }) }; }
|