blob: 5d6774179941e3345a569b3acef1e2b0290e2450 [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 type { SessionTurnAccessRequest } from '../protocol/index.js';
import type { RuntimeHostAccessAuthority } from './access-authority.js';
import type {
ConnectionContext,
OperationHandler,
OperationResidency,
} from './operation-dispatcher.js';
type TurnAccessRequestAuthority = Pick<
RuntimeHostAccessAuthority,
'approvedTurnAccessRequests' | 'completeTurnAccessRequest' | 'subscribeApprovedTurnAccessRequests'
>;
export class SessionTurnAccessRequestCoordinator {
readonly #authority: TurnAccessRequestAuthority;
readonly #startTurn: OperationHandler<'turn.start'>;
readonly #regenerateTurn: OperationHandler<'turn.regenerate'>;
readonly #acquireResidency: () => OperationResidency;
readonly #requestDrain: () => void;
readonly #whenIdle: (sessionId: string) => Promise<void> | undefined;
readonly #hostEpoch: string;
readonly #tasks = new Map<string, Promise<void>>();
readonly #unsubscribe: () => void;
#draining = false;
constructor(input: {
readonly authority: TurnAccessRequestAuthority;
readonly startTurn: OperationHandler<'turn.start'>;
readonly regenerateTurn: OperationHandler<'turn.regenerate'>;
readonly acquireResidency: () => OperationResidency;
readonly requestDrain: () => void;
readonly whenIdle: (sessionId: string) => Promise<void> | undefined;
readonly hostEpoch: string;
}) {
this.#authority = input.authority;
this.#startTurn = input.startTurn;
this.#regenerateTurn = input.regenerateTurn;
this.#acquireResidency = input.acquireResidency;
this.#requestDrain = input.requestDrain;
this.#whenIdle = input.whenIdle;
this.#hostEpoch = input.hostEpoch;
this.#unsubscribe = input.authority.subscribeApprovedTurnAccessRequests((request) => {
this.#schedule(request);
});
}
recover(): void {
for (const request of this.#authority.approvedTurnAccessRequests()) {
this.#schedule(request);
}
}
beginDrain(): void {
if (this.#draining) return;
this.#draining = true;
this.#unsubscribe();
}
async close(): Promise<void> {
this.beginDrain();
await this.#settled();
}
#schedule(request: SessionTurnAccessRequest): void {
if (this.#draining || this.#tasks.has(request.requestId)) return;
const task = this.#admit(request).finally(() => {
this.#tasks.delete(request.requestId);
});
this.#tasks.set(request.requestId, task);
}
async #admit(request: SessionTurnAccessRequest): Promise<void> {
if (request.state.kind !== 'approved' || request.state.admission !== 'pending') return;
const residency = this.#acquireResidency();
const context: ConnectionContext = {
hostEpoch: this.#hostEpoch,
connectionId: `collaboration:${request.requestId}`,
principal: request.principalId,
turnAdmissionAuthorization: {
kind: 'session_turn_access_request',
requestId: request.requestId,
principalId: request.principalId,
grantId: request.grantId,
approvedAt: Date.parse(request.state.decidedAt),
approvedBy: request.state.decidedBy,
},
acquireResidency: this.#acquireResidency,
};
try {
const admission = await this.#run(request, context);
if (!admission) return;
try {
await this.#authority.completeTurnAccessRequest(request.requestId, admission);
} catch {
this.#requestDrain();
}
} finally {
residency.release();
}
}
async #run(
request: SessionTurnAccessRequest,
context: ConnectionContext,
): Promise<'started' | 'blocked' | 'failed' | undefined> {
for (;;) {
try {
if ('content' in request.intent) {
const outcome = await this.#startTurn(request.intent, context);
if (outcome.ok) return outcome.result.kind;
if (outcome.error.code !== 'session_busy') {
return this.#draining ? undefined : 'failed';
}
} else {
const outcome = await this.#regenerateTurn(request.intent, context);
if (outcome.ok) return 'started';
if (outcome.error.code !== 'session_busy') {
return this.#draining ? undefined : 'failed';
}
}
} catch {
this.#requestDrain();
return undefined;
}
const whenIdle = this.#whenIdle(request.intent.sessionId);
if (whenIdle) {
try {
await whenIdle;
} catch {
this.#requestDrain();
return undefined;
}
}
if (this.#draining) return undefined;
}
}
#settled(): Promise<void> {
return Promise.all(this.#tasks.values()).then(() => undefined);
}
}