blob: 26e20766dd76ab5dafb7c073e8b29c964d1d7bf8 [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 {
requireCount,
requireEntityId,
requireExactRecord,
requireId,
requireUtf8String,
} from './codec.js';
import { invalidProtocolFrame } from './errors.js';
import { MESSAGE_QUEUE_MAX_ENTRIES } from './message-queue-limits.js';
import { defineOperation } from './operation-spec.js';
import { TURN_MESSAGE_TEXT_MAX_BYTES } from './turn.js';
interface QueueEntryCommand {
readonly originHostEpoch: string;
readonly sessionId: string;
readonly entryId: string;
}
export interface QueueEntryRetractInput extends QueueEntryCommand {
readonly retractId: string;
}
export interface QueueEntryPromoteInput extends QueueEntryCommand {
readonly promoteId: string;
}
export interface QueueEntryUpdateInput extends QueueEntryCommand {
readonly updateId: string;
readonly expectedQueueRevision: number;
readonly text: string;
}
export interface QueueEntriesReorderInput {
readonly originHostEpoch: string;
readonly sessionId: string;
readonly reorderId: string;
readonly expectedQueueRevision: number;
readonly entryIds: readonly string[];
}
export interface QueueMutationResult {
readonly queueRevision: number;
}
const QUEUE_MUTATION_ERRORS = [
'host_not_ready',
'host_draining',
'operation_unavailable',
'not_found',
'session_archived',
'session_busy',
'operation_conflict',
'outcome_unknown',
'internal_failure',
] as const;
export const QUEUE_MUTATION_OPERATION_SPECS = {
'queue.entry.retract': defineOperation({
mode: 'command',
availability: 'ready',
errors: QUEUE_MUTATION_ERRORS,
decodeInput: decodeQueueEntryRetractInput,
decodeOutput: decodeQueueMutationResult,
}),
'queue.entry.promote': defineOperation({
mode: 'command',
availability: 'ready',
errors: QUEUE_MUTATION_ERRORS,
decodeInput: decodeQueueEntryPromoteInput,
decodeOutput: decodeQueueMutationResult,
}),
'queue.entry.update': defineOperation({
mode: 'command',
availability: 'ready',
errors: QUEUE_MUTATION_ERRORS,
decodeInput: decodeQueueEntryUpdateInput,
decodeOutput: decodeQueueMutationResult,
}),
'queue.entries.reorder': defineOperation({
mode: 'command',
availability: 'ready',
errors: QUEUE_MUTATION_ERRORS,
decodeInput: decodeQueueEntriesReorderInput,
decodeOutput: decodeQueueMutationResult,
}),
} as const;
export function decodeQueueEntryRetractInput(value: unknown): QueueEntryRetractInput {
const command = decodeEntryCommand(value, 'queue.entry.retract input', 'retractId');
const { operationId, ...identity } = command;
return { ...identity, retractId: requireEntityId(operationId, 'retractId') };
}
export function decodeQueueEntryPromoteInput(value: unknown): QueueEntryPromoteInput {
const command = decodeEntryCommand(value, 'queue.entry.promote input', 'promoteId');
const { operationId, ...identity } = command;
return { ...identity, promoteId: requireEntityId(operationId, 'promoteId') };
}
export function decodeQueueEntryUpdateInput(value: unknown): QueueEntryUpdateInput {
const record = requireExactRecord(value, 'queue.entry.update input', [
'originHostEpoch',
'sessionId',
'entryId',
'updateId',
'expectedQueueRevision',
'text',
]);
const text = requireUtf8String(record.text, 'Message text', TURN_MESSAGE_TEXT_MAX_BYTES);
if (text.trim().length === 0) throw invalidProtocolFrame('Invalid Message text');
return {
...decodeEntryIdentity(record),
updateId: requireEntityId(record.updateId, 'updateId'),
expectedQueueRevision: requireCount(record.expectedQueueRevision, 'expectedQueueRevision'),
text,
};
}
export function decodeQueueEntriesReorderInput(value: unknown): QueueEntriesReorderInput {
const record = requireExactRecord(value, 'queue.entries.reorder input', [
'originHostEpoch',
'sessionId',
'reorderId',
'expectedQueueRevision',
'entryIds',
]);
if (!Array.isArray(record.entryIds) || record.entryIds.length > MESSAGE_QUEUE_MAX_ENTRIES) {
throw invalidProtocolFrame('Invalid reorder entry identities');
}
const entryIds = record.entryIds.map((entryId) => requireEntityId(entryId, 'entryId'));
if (new Set(entryIds).size !== entryIds.length) {
throw invalidProtocolFrame('Invalid reorder entry identities');
}
return {
originHostEpoch: requireId(record.originHostEpoch, 'originHostEpoch'),
sessionId: requireEntityId(record.sessionId, 'sessionId'),
reorderId: requireEntityId(record.reorderId, 'reorderId'),
expectedQueueRevision: requireCount(record.expectedQueueRevision, 'expectedQueueRevision'),
entryIds,
};
}
export function decodeQueueMutationResult(value: unknown): QueueMutationResult {
const record = requireExactRecord(value, 'queue mutation result', ['queueRevision']);
return { queueRevision: requireCount(record.queueRevision, 'queueRevision') };
}
function decodeEntryCommand(
value: unknown,
label: string,
operationIdKey: 'retractId' | 'promoteId',
): QueueEntryCommand & { readonly operationId: unknown } {
const record = requireExactRecord(value, label, [
'originHostEpoch',
'sessionId',
'entryId',
operationIdKey,
]);
return { ...decodeEntryIdentity(record), operationId: record[operationIdKey] };
}
function decodeEntryIdentity(record: Record<string, unknown>): QueueEntryCommand {
return {
originHostEpoch: requireId(record.originHostEpoch, 'originHostEpoch'),
sessionId: requireEntityId(record.sessionId, 'sessionId'),
entryId: requireEntityId(record.entryId, 'entryId'),
};
}