blob: 4caebf29e1269aea1125fa0605cd8e440f1350bd [file]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { exactQueueReorder } from '@maka/core/message-queue-order';
interface QueueEntryIdentity {
readonly entryId: string;
readonly messageId: string;
}
interface QueueCollections<Entry extends QueueEntryIdentity> {
steering: Entry[];
followup: Entry[];
readonly inFlight: ReadonlyMap<string, Entry>;
}
export type QueueRevisionCheck =
| { readonly kind: 'current'; readonly revision: number }
| { readonly kind: 'stale'; readonly expected: number; readonly actual: number };
/** The Host owns this fence; clients may only propose a mutation against the revision they read. */
export function checkQueueRevision(actual: number, expected: number): QueueRevisionCheck {
return actual === expected
? { kind: 'current', revision: actual }
: { kind: 'stale', expected, actual };
}
export interface QueuedEntryLocation<Entry extends QueueEntryIdentity> {
readonly lane: 'steering' | 'followup';
readonly index: number;
readonly entry: Entry;
}
export type QueuedEntrySelection<Entry extends QueueEntryIdentity> =
| { readonly kind: 'found'; readonly location: QueuedEntryLocation<Entry> }
| { readonly kind: 'in_flight' }
| { readonly kind: 'wrong_lane'; readonly lane: QueuedEntryLocation<Entry>['lane'] }
| { readonly kind: 'missing' };
export function locateQueuedEntry<Entry extends QueueEntryIdentity>(
state: QueueCollections<Entry>,
entryId: string,
): QueuedEntryLocation<Entry> | undefined {
for (const lane of ['steering', 'followup'] as const) {
const index = state[lane].findIndex((entry) => entry.entryId === entryId);
const entry = index < 0 ? undefined : state[lane][index];
if (entry) return { lane, index, entry };
}
return undefined;
}
export function hasInFlightEntry<Entry extends QueueEntryIdentity>(
state: QueueCollections<Entry>,
entryId: string,
): boolean {
return [...state.inFlight.values()].some((entry) => entry.entryId === entryId);
}
export function selectQueuedEntry<Entry extends QueueEntryIdentity>(
state: QueueCollections<Entry>,
entryId: string,
requiredLane?: QueuedEntryLocation<Entry>['lane'],
): QueuedEntrySelection<Entry> {
const location = locateQueuedEntry(state, entryId);
if (location && (!requiredLane || location.lane === requiredLane)) {
return { kind: 'found', location };
}
if (location) return { kind: 'wrong_lane', lane: location.lane };
return hasInFlightEntry(state, entryId) ? { kind: 'in_flight' } : { kind: 'missing' };
}
export function removeQueuedEntry<Entry extends QueueEntryIdentity>(
state: QueueCollections<Entry>,
location: QueuedEntryLocation<Entry>,
): Entry {
const [removed] = state[location.lane].splice(location.index, 1);
if (removed !== location.entry) throw new Error('Queued entry location changed before commit');
return removed;
}
export function commitFollowupPromotion<Entry extends QueueEntryIdentity>(
state: QueueCollections<Entry>,
location: QueuedEntryLocation<Entry>,
promoted: Entry,
): void {
if (location.lane !== 'followup') throw new Error('Only follow-up entries can be promoted');
removeQueuedEntry(state, location);
state.steering.push(promoted);
}
export function planQueueReorder<Entry extends QueueEntryIdentity>(
state: QueueCollections<Entry>,
entryIds: readonly string[],
):
| {
readonly lane: 'steering' | 'followup';
readonly entries: readonly Entry[];
readonly changed: boolean;
}
| undefined {
for (const lane of ['steering', 'followup'] as const) {
const reorder = exactQueueReorder(state[lane], entryIds);
if (reorder) return { lane, entries: reorder.entries, changed: reorder.changed };
}
return undefined;
}
export function commitQueueReorder<Entry extends QueueEntryIdentity>(
state: QueueCollections<Entry>,
lane: 'steering' | 'followup',
entries: readonly Entry[],
): void {
state[lane] = [...entries];
}