d25d482dc2
Publish CLI Package / publish-npm (push) Waiting to run
Publish Python SDK / publish-pypi (push) Waiting to run
Publish TypeScript SDK / publish-npm (push) Waiting to run
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
295 lines
7.3 KiB
TypeScript
295 lines
7.3 KiB
TypeScript
export const SOCKET_JOIN_RETRY_BASE_DELAY_MS = 1000
|
|
export const SOCKET_JOIN_RETRY_MAX_DELAY_MS = 10000
|
|
|
|
export type SocketJoinCommand =
|
|
| { type: 'cancel-retry' }
|
|
| { type: 'join'; workflowId: string }
|
|
| { type: 'leave' }
|
|
| {
|
|
type: 'schedule-retry'
|
|
workflowId: string
|
|
attempt: number
|
|
delayMs: number
|
|
}
|
|
|
|
interface SocketJoinSuccessResult {
|
|
apply: boolean
|
|
commands: SocketJoinCommand[]
|
|
ignored: boolean
|
|
workflowId: string
|
|
}
|
|
|
|
interface SocketJoinErrorResult {
|
|
apply: boolean
|
|
commands: SocketJoinCommand[]
|
|
ignored: boolean
|
|
retryScheduled: boolean
|
|
workflowId: string | null
|
|
}
|
|
|
|
interface SocketJoinDeleteResult {
|
|
commands: SocketJoinCommand[]
|
|
shouldClearCurrent: boolean
|
|
}
|
|
|
|
/**
|
|
* Coordinates desired workflow room membership with async socket join results.
|
|
*/
|
|
export class SocketJoinController {
|
|
private desiredWorkflowId: string | null = null
|
|
private joinedWorkflowId: string | null = null
|
|
private pendingJoinWorkflowId: string | null = null
|
|
private blockedWorkflowId: string | null = null
|
|
private retryWorkflowId: string | null = null
|
|
private retryAttempt = 0
|
|
private isConnected = false
|
|
|
|
getJoinedWorkflowId(): string | null {
|
|
return this.joinedWorkflowId
|
|
}
|
|
|
|
setConnected(connected: boolean): SocketJoinCommand[] {
|
|
this.isConnected = connected
|
|
if (!connected) {
|
|
this.pendingJoinWorkflowId = null
|
|
this.joinedWorkflowId = null
|
|
return this.clearRetryCommands()
|
|
}
|
|
|
|
return this.flush()
|
|
}
|
|
|
|
requestWorkflow(workflowId: string | null): SocketJoinCommand[] {
|
|
const commands = this.takeRetryResetCommands(workflowId)
|
|
this.desiredWorkflowId = workflowId
|
|
|
|
if (workflowId !== this.blockedWorkflowId) {
|
|
this.blockedWorkflowId = null
|
|
}
|
|
|
|
return [...commands, ...this.flush()]
|
|
}
|
|
|
|
forceRejoinWorkflow(workflowId: string | null): SocketJoinCommand[] {
|
|
const commands = this.requestWorkflow(workflowId)
|
|
const alreadyChangingRooms = commands.some(
|
|
(command) => command.type === 'join' || command.type === 'leave'
|
|
)
|
|
|
|
if (
|
|
alreadyChangingRooms ||
|
|
!this.isConnected ||
|
|
!this.desiredWorkflowId ||
|
|
this.pendingJoinWorkflowId === this.desiredWorkflowId ||
|
|
this.blockedWorkflowId === this.desiredWorkflowId
|
|
) {
|
|
return commands
|
|
}
|
|
|
|
this.joinedWorkflowId = null
|
|
|
|
return [...commands, ...this.flush()]
|
|
}
|
|
|
|
handleWorkflowDeleted(workflowId: string): SocketJoinDeleteResult {
|
|
const commands = this.takeRetryResetCommands(
|
|
this.retryWorkflowId === workflowId ? null : this.retryWorkflowId
|
|
)
|
|
|
|
if (this.desiredWorkflowId === workflowId) {
|
|
this.blockedWorkflowId = workflowId
|
|
}
|
|
|
|
if (this.pendingJoinWorkflowId === workflowId) {
|
|
this.pendingJoinWorkflowId = null
|
|
}
|
|
|
|
const shouldClearCurrent = this.joinedWorkflowId === workflowId
|
|
if (shouldClearCurrent) {
|
|
this.joinedWorkflowId = null
|
|
}
|
|
|
|
return {
|
|
commands: [...commands, ...this.flush()],
|
|
shouldClearCurrent,
|
|
}
|
|
}
|
|
|
|
handleJoinSuccess(workflowId: string): SocketJoinSuccessResult {
|
|
const commands = this.clearRetryCommands(workflowId)
|
|
this.pendingJoinWorkflowId = null
|
|
this.joinedWorkflowId = workflowId
|
|
|
|
const apply = this.desiredWorkflowId === workflowId && this.blockedWorkflowId !== workflowId
|
|
|
|
return {
|
|
apply,
|
|
commands: [...commands, ...this.flush()],
|
|
ignored: !apply,
|
|
workflowId,
|
|
}
|
|
}
|
|
|
|
handleJoinError({
|
|
workflowId,
|
|
retryable,
|
|
}: {
|
|
workflowId?: string | null
|
|
retryable?: boolean
|
|
}): SocketJoinErrorResult {
|
|
const resolvedWorkflowId = workflowId ?? this.pendingJoinWorkflowId
|
|
|
|
if (resolvedWorkflowId && this.pendingJoinWorkflowId === resolvedWorkflowId) {
|
|
this.pendingJoinWorkflowId = null
|
|
if (this.joinedWorkflowId === resolvedWorkflowId) {
|
|
this.joinedWorkflowId = null
|
|
}
|
|
}
|
|
|
|
const isCurrentDesired =
|
|
Boolean(resolvedWorkflowId) &&
|
|
this.desiredWorkflowId === resolvedWorkflowId &&
|
|
this.blockedWorkflowId !== resolvedWorkflowId
|
|
|
|
const baseCommands =
|
|
resolvedWorkflowId !== null
|
|
? this.takeRetryResetCommands(resolvedWorkflowId)
|
|
: this.clearRetryCommands()
|
|
|
|
if (!isCurrentDesired) {
|
|
return {
|
|
apply: false,
|
|
commands: [...baseCommands, ...this.flush()],
|
|
ignored: true,
|
|
retryScheduled: false,
|
|
workflowId: resolvedWorkflowId,
|
|
}
|
|
}
|
|
|
|
if (retryable && resolvedWorkflowId) {
|
|
const commands = this.scheduleRetry(resolvedWorkflowId)
|
|
|
|
return {
|
|
apply: false,
|
|
commands: [...baseCommands, ...commands],
|
|
ignored: false,
|
|
retryScheduled: true,
|
|
workflowId: resolvedWorkflowId,
|
|
}
|
|
}
|
|
|
|
const leaveCommands = this.blockWorkflow(resolvedWorkflowId)
|
|
|
|
return {
|
|
apply: true,
|
|
commands: [...this.clearRetryCommands(), ...leaveCommands, ...this.flush()],
|
|
ignored: false,
|
|
retryScheduled: false,
|
|
workflowId: resolvedWorkflowId,
|
|
}
|
|
}
|
|
|
|
retryJoin(workflowId: string): SocketJoinCommand[] {
|
|
if (
|
|
this.retryWorkflowId !== workflowId ||
|
|
this.desiredWorkflowId !== workflowId ||
|
|
this.blockedWorkflowId === workflowId
|
|
) {
|
|
return []
|
|
}
|
|
|
|
return this.flush()
|
|
}
|
|
|
|
private flush(): SocketJoinCommand[] {
|
|
if (!this.isConnected || this.pendingJoinWorkflowId) {
|
|
return []
|
|
}
|
|
|
|
if (!this.desiredWorkflowId) {
|
|
if (!this.joinedWorkflowId) {
|
|
return []
|
|
}
|
|
|
|
this.joinedWorkflowId = null
|
|
return [{ type: 'leave' }]
|
|
}
|
|
|
|
if (this.blockedWorkflowId === this.desiredWorkflowId) {
|
|
return []
|
|
}
|
|
|
|
if (this.joinedWorkflowId === this.desiredWorkflowId) {
|
|
return []
|
|
}
|
|
|
|
this.pendingJoinWorkflowId = this.desiredWorkflowId
|
|
|
|
return [{ type: 'join', workflowId: this.desiredWorkflowId }]
|
|
}
|
|
|
|
private scheduleRetry(workflowId: string): SocketJoinCommand[] {
|
|
const nextAttempt = this.retryWorkflowId === workflowId ? this.retryAttempt + 1 : 1
|
|
const delayMs = Math.min(
|
|
SOCKET_JOIN_RETRY_BASE_DELAY_MS * 2 ** Math.max(0, nextAttempt - 1),
|
|
SOCKET_JOIN_RETRY_MAX_DELAY_MS
|
|
)
|
|
|
|
this.retryWorkflowId = workflowId
|
|
this.retryAttempt = nextAttempt
|
|
|
|
return [
|
|
{
|
|
type: 'schedule-retry',
|
|
workflowId,
|
|
attempt: nextAttempt,
|
|
delayMs,
|
|
},
|
|
]
|
|
}
|
|
|
|
private takeRetryResetCommands(nextWorkflowId?: string | null): SocketJoinCommand[] {
|
|
const shouldClearRetry =
|
|
this.retryWorkflowId !== null &&
|
|
(nextWorkflowId === undefined || this.retryWorkflowId !== nextWorkflowId)
|
|
|
|
if (!shouldClearRetry) {
|
|
return []
|
|
}
|
|
|
|
this.retryWorkflowId = null
|
|
this.retryAttempt = 0
|
|
|
|
return [{ type: 'cancel-retry' }]
|
|
}
|
|
|
|
private clearRetryCommands(workflowId?: string): SocketJoinCommand[] {
|
|
const shouldClear =
|
|
this.retryWorkflowId !== null &&
|
|
(workflowId === undefined || this.retryWorkflowId === workflowId)
|
|
|
|
if (!shouldClear) {
|
|
return []
|
|
}
|
|
|
|
this.retryWorkflowId = null
|
|
this.retryAttempt = 0
|
|
|
|
return [{ type: 'cancel-retry' }]
|
|
}
|
|
|
|
private blockWorkflow(workflowId: string | null): SocketJoinCommand[] {
|
|
if (workflowId) {
|
|
this.blockedWorkflowId = workflowId
|
|
}
|
|
|
|
if (!this.joinedWorkflowId) {
|
|
return []
|
|
}
|
|
|
|
this.joinedWorkflowId = null
|
|
|
|
return [{ type: 'leave' }]
|
|
}
|
|
}
|