Replace Worker list revisions with ordered subscriptions
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -304,11 +304,7 @@ resource_key?: string | null,
|
||||
*/
|
||||
availability: SubscriptionWorkerAvailability,
|
||||
/**
|
||||
* Producer-owned monotonic revision for this Worker subject.
|
||||
*/
|
||||
subject_revision: number,
|
||||
/**
|
||||
* Latest revisioned foreground state observed from the Worker. This remains
|
||||
* Latest foreground state observed from the Worker. This remains
|
||||
* absent until an authoritative Worker snapshot/event has been applied.
|
||||
*/
|
||||
worker_state?: WorkerStateSnapshot | null,
|
||||
@@ -336,9 +332,9 @@ export type SubscriptionRequest = { "method": "subscribe_events", "params": { re
|
||||
|
||||
export type SubscriptionWorkerProtocolMethod = { subscription_id: SubscriptionId, method: Method, };
|
||||
|
||||
export type SubscriptionResponse = { "result": "subscribed", "payload": { request_id: SubscriptionRequestId, subscription_id: SubscriptionId, selector: EventSubscriptionSelector, snapshot_revision: number, snapshot: SubscriptionSnapshot, } } | { "result": "unsubscribed", "payload": { request_id: SubscriptionRequestId, subscription_id: SubscriptionId, } } | { "result": "subscription_rejected", "payload": { request_id: SubscriptionRequestId, subscription_id?: SubscriptionId | null, code: SubscriptionRejectionCode, message: string, } };
|
||||
export type SubscriptionResponse = { "result": "subscribed", "payload": { request_id: SubscriptionRequestId, subscription_id: SubscriptionId, selector: EventSubscriptionSelector, snapshot: SubscriptionSnapshot, } } | { "result": "unsubscribed", "payload": { request_id: SubscriptionRequestId, subscription_id: SubscriptionId, } } | { "result": "subscription_rejected", "payload": { request_id: SubscriptionRequestId, subscription_id?: SubscriptionId | null, code: SubscriptionRejectionCode, message: string, } };
|
||||
|
||||
export type SubscriptionEvent = { "event": "event", "data": { subscription_id: SubscriptionId, subject_revision: number, payload: SubscriptionEventPayload, } } | { "event": "subscription_closed", "data": { subscription_id: SubscriptionId, code: SubscriptionTerminationCode, message: string, } };
|
||||
export type SubscriptionEvent = { "event": "event", "data": { subscription_id: SubscriptionId, payload: SubscriptionEventPayload, } } | { "event": "subscription_closed", "data": { subscription_id: SubscriptionId, code: SubscriptionTerminationCode, message: string, } };
|
||||
|
||||
export type SubscriptionFramePayload = { "frame": "request", "message": SubscriptionRequest } | { "frame": "response", "message": SubscriptionResponse } | { "frame": "event", "message": SubscriptionEvent } | { "frame": "worker_protocol", "message": SubscriptionWorkerProtocolMethod };
|
||||
|
||||
|
||||
@@ -83,7 +83,7 @@ test('invalid inbound frame closes before dispatch and reconnects for a fresh sn
|
||||
|
||||
first.message(
|
||||
JSON.stringify({
|
||||
protocol_version: 1,
|
||||
protocol_version: 2,
|
||||
frame: 'event',
|
||||
message: {
|
||||
event: 'subscription_closed',
|
||||
@@ -141,7 +141,7 @@ test('valid subscription closure remains isolated and resubscribes only its sele
|
||||
: { topic: 'workspace_workdirs', data: { workdirs: [] } };
|
||||
socket.message(
|
||||
JSON.stringify({
|
||||
protocol_version: 1,
|
||||
protocol_version: 2,
|
||||
frame: 'response',
|
||||
message: {
|
||||
result: 'subscribed',
|
||||
@@ -149,7 +149,6 @@ test('valid subscription closure remains isolated and resubscribes only its sele
|
||||
request_id: request.message.params.request_id,
|
||||
subscription_id: `subscription-${index + 1}`,
|
||||
selector,
|
||||
snapshot_revision: 1,
|
||||
snapshot,
|
||||
},
|
||||
},
|
||||
@@ -160,7 +159,7 @@ test('valid subscription closure remains isolated and resubscribes only its sele
|
||||
const sentBeforeClosure = socket.sent.length;
|
||||
socket.message(
|
||||
JSON.stringify({
|
||||
protocol_version: 1,
|
||||
protocol_version: 2,
|
||||
frame: 'event',
|
||||
message: {
|
||||
event: 'subscription_closed',
|
||||
|
||||
@@ -138,7 +138,7 @@ export class WorkspaceMultiplexer {
|
||||
subscription.requestId = requestId;
|
||||
this.#requests.set(requestId, subscription.clientId);
|
||||
this.#send({
|
||||
protocol_version: 1,
|
||||
protocol_version: 2,
|
||||
frame: 'request',
|
||||
message: {
|
||||
method: 'subscribe_events',
|
||||
@@ -213,7 +213,7 @@ export class WorkspaceMultiplexer {
|
||||
const subscription = this.#subscriptions.get(clientId);
|
||||
if (!subscription?.subscriptionId) throw new Error('Worker protocol subscription is not open');
|
||||
this.#send({
|
||||
protocol_version: 1,
|
||||
protocol_version: 2,
|
||||
frame: 'worker_protocol',
|
||||
message: { subscription_id: subscription.subscriptionId, method },
|
||||
});
|
||||
@@ -225,7 +225,7 @@ export class WorkspaceMultiplexer {
|
||||
this.#subscriptions.delete(clientId);
|
||||
if (subscription.subscriptionId && this.#socket?.readyState === WebSocket.OPEN) {
|
||||
this.#send({
|
||||
protocol_version: 1,
|
||||
protocol_version: 2,
|
||||
frame: 'request',
|
||||
message: {
|
||||
method: 'unsubscribe_events',
|
||||
|
||||
@@ -16,7 +16,7 @@ function assert(condition: unknown, message: string): asserts condition {
|
||||
|
||||
function subscribedFrame(overrides: Record<string, unknown> = {}): Record<string, unknown> {
|
||||
return {
|
||||
protocol_version: 1,
|
||||
protocol_version: 2,
|
||||
frame: 'response',
|
||||
message: {
|
||||
result: 'subscribed',
|
||||
@@ -24,7 +24,6 @@ function subscribedFrame(overrides: Record<string, unknown> = {}): Record<string
|
||||
request_id: 'request-1',
|
||||
subscription_id: 'subscription-1',
|
||||
selector: { topic: 'workspace_workers' },
|
||||
snapshot_revision: 1,
|
||||
snapshot: { topic: 'workers', data: { workers: [] } },
|
||||
...overrides,
|
||||
},
|
||||
@@ -86,7 +85,6 @@ Deno.test('generated subscription validator accepts an external Workdir Worker',
|
||||
runtime_id: 'runtime-1',
|
||||
resource_key: 'worker-resource-1',
|
||||
availability: 'observed',
|
||||
subject_revision: 1,
|
||||
state: 'idle',
|
||||
has_running_internal_workers: false,
|
||||
workspace_id: 'workspace-1',
|
||||
@@ -123,8 +121,8 @@ Deno.test('generated subscription validator rejects malformed and newer shapes',
|
||||
'unknown nested field',
|
||||
subscribedFrame({ snapshot: { topic: 'workers', data: { workers: [], future: true } } }),
|
||||
],
|
||||
['wrong field type', subscribedFrame({ snapshot_revision: '1' })],
|
||||
['unsafe integer', subscribedFrame({ snapshot_revision: Number.MAX_SAFE_INTEGER + 1 })],
|
||||
['wrong field type', { ...subscribedFrame(), protocol_version: '2' }],
|
||||
['unsafe integer', { ...subscribedFrame(), protocol_version: Number.MAX_SAFE_INTEGER + 1 }],
|
||||
[
|
||||
'missing serialized Worker defaults',
|
||||
subscribedFrame({
|
||||
@@ -132,23 +130,22 @@ Deno.test('generated subscription validator rejects malformed and newer shapes',
|
||||
topic: 'workers',
|
||||
data: {
|
||||
workers: [
|
||||
{ worker_id: 'worker-1', subject_revision: 1, state: 'idle' },
|
||||
{ worker_id: 'worker-1', state: 'idle' },
|
||||
],
|
||||
},
|
||||
},
|
||||
}),
|
||||
],
|
||||
['unsupported version', { ...subscribedFrame(), protocol_version: 2 }],
|
||||
['unsupported version', { ...subscribedFrame(), protocol_version: 1 }],
|
||||
[
|
||||
'newer Worker event',
|
||||
{
|
||||
protocol_version: 1,
|
||||
protocol_version: 2,
|
||||
frame: 'event',
|
||||
message: {
|
||||
event: 'event',
|
||||
data: {
|
||||
subscription_id: 'subscription-1',
|
||||
subject_revision: 2,
|
||||
payload: {
|
||||
event: 'worker_protocol',
|
||||
data: { worker_id: 'worker-1', event: { event: 'future_event' } },
|
||||
@@ -160,13 +157,12 @@ Deno.test('generated subscription validator rejects malformed and newer shapes',
|
||||
[
|
||||
'out-of-range Rust u32',
|
||||
{
|
||||
protocol_version: 1,
|
||||
protocol_version: 2,
|
||||
frame: 'event',
|
||||
message: {
|
||||
event: 'event',
|
||||
data: {
|
||||
subscription_id: 'subscription-1',
|
||||
subject_revision: 2,
|
||||
payload: {
|
||||
event: 'worker_protocol',
|
||||
data: {
|
||||
@@ -207,7 +203,7 @@ Deno.test('generated subscription validator enforces Rust semantic bounds and se
|
||||
[
|
||||
'oversized rejection message',
|
||||
{
|
||||
protocol_version: 1,
|
||||
protocol_version: 2,
|
||||
frame: 'response',
|
||||
message: {
|
||||
result: 'subscription_rejected',
|
||||
@@ -242,7 +238,6 @@ Deno.test('generated subscription validator enforces Rust semantic bounds and se
|
||||
worker_id: 'worker-1',
|
||||
runtime_id: 'runtime-1',
|
||||
availability: 'observed',
|
||||
subject_revision: 1,
|
||||
state: 'idle',
|
||||
has_running_internal_workers: false,
|
||||
},
|
||||
@@ -250,7 +245,6 @@ Deno.test('generated subscription validator enforces Rust semantic bounds and se
|
||||
worker_id: 'worker-1',
|
||||
runtime_id: 'runtime-1',
|
||||
availability: 'observed',
|
||||
subject_revision: 2,
|
||||
state: 'running',
|
||||
has_running_internal_workers: false,
|
||||
},
|
||||
@@ -262,7 +256,7 @@ Deno.test('generated subscription validator enforces Rust semantic bounds and se
|
||||
[
|
||||
'server-to-Browser direction mismatch',
|
||||
{
|
||||
protocol_version: 1,
|
||||
protocol_version: 2,
|
||||
frame: 'request',
|
||||
message: {
|
||||
method: 'unsubscribe_events',
|
||||
@@ -281,13 +275,12 @@ Deno.test('generated subscription validator enforces Rust semantic bounds and se
|
||||
Deno.test('aggregate limits include keys in unconstrained Worker JSON values', () => {
|
||||
const oversizedKey = 'k'.repeat(MAX_SUBSCRIPTION_STRING_BYTES + 1);
|
||||
const result = decode({
|
||||
protocol_version: 1,
|
||||
protocol_version: 2,
|
||||
frame: 'event',
|
||||
message: {
|
||||
event: 'event',
|
||||
data: {
|
||||
subscription_id: 'subscription-1',
|
||||
subject_revision: 2,
|
||||
payload: {
|
||||
event: 'worker_protocol',
|
||||
data: {
|
||||
@@ -306,13 +299,12 @@ Deno.test('aggregate limits include keys in unconstrained Worker JSON values', (
|
||||
|
||||
Deno.test('stateful selector validation fences a routed event before projection', () => {
|
||||
const frame: SubscriptionFrame = {
|
||||
protocol_version: 1,
|
||||
protocol_version: 2,
|
||||
frame: 'event',
|
||||
message: {
|
||||
event: 'event',
|
||||
data: {
|
||||
subscription_id: 'subscription-1',
|
||||
subject_revision: 2,
|
||||
payload: {
|
||||
event: 'worker_protocol',
|
||||
data: { worker_id: 'worker-2', event: { event: 'thinking_start' } },
|
||||
|
||||
@@ -43,7 +43,7 @@ function snapshot(
|
||||
availability: SubscriptionWorker["availability"],
|
||||
): SubscriptionFrame {
|
||||
return {
|
||||
protocol_version: 1,
|
||||
protocol_version: 2,
|
||||
frame: "response",
|
||||
message: {
|
||||
result: "subscribed",
|
||||
@@ -51,7 +51,6 @@ function snapshot(
|
||||
request_id: "request-1",
|
||||
subscription_id: "subscription-1",
|
||||
selector: { topic: "workspace_workers" },
|
||||
snapshot_revision: 1,
|
||||
snapshot: {
|
||||
topic: "workers",
|
||||
data: {
|
||||
@@ -60,7 +59,6 @@ function snapshot(
|
||||
runtime_id: "runtime-a",
|
||||
resource_key: "W-1",
|
||||
availability,
|
||||
subject_revision: 1,
|
||||
state,
|
||||
// An absent foreground snapshot must not prevent stopping an active catalog Worker.
|
||||
worker_state: null,
|
||||
|
||||
@@ -6,55 +6,55 @@ import type {
|
||||
|
||||
export type WorkspaceWorkersProjection = {
|
||||
workers: Map<string, SubscriptionWorker>;
|
||||
revisions: Map<string, number>;
|
||||
subscriptionId: string | null;
|
||||
};
|
||||
|
||||
export function createWorkspaceWorkersProjection(): WorkspaceWorkersProjection {
|
||||
return { workers: new Map(), revisions: new Map() };
|
||||
return { workers: new Map(), subscriptionId: null };
|
||||
}
|
||||
|
||||
export function applyWorkspaceWorkersFrame(
|
||||
projection: WorkspaceWorkersProjection,
|
||||
frame: SubscriptionFrame,
|
||||
): void {
|
||||
if (frame.protocol_version !== 1) throw new Error('unsupported Worker subscription protocol');
|
||||
if (frame.protocol_version !== 2) throw new Error('unsupported Worker subscription protocol');
|
||||
if (frame.frame === 'response' && frame.message.result === 'subscribed') {
|
||||
if (frame.message.payload.selector.topic !== 'workspace_workers') return;
|
||||
const snapshot = frame.message.payload.snapshot;
|
||||
if (snapshot.topic !== 'workers') throw new Error('workspace_workers returned a non-Worker snapshot');
|
||||
projection.workers.clear();
|
||||
projection.revisions.clear();
|
||||
|
||||
// Build the replacement before mutating the live projection so one malformed
|
||||
// Worker cannot partially clear the previous subscription lifetime.
|
||||
const workers = new Map<string, SubscriptionWorker>();
|
||||
for (const worker of snapshot.data.workers) {
|
||||
const key = workerKey(worker.runtime_id, worker.worker_id);
|
||||
projection.workers.set(key, worker);
|
||||
projection.revisions.set(key, worker.subject_revision);
|
||||
workers.set(workerKey(worker.runtime_id, worker.worker_id), worker);
|
||||
}
|
||||
projection.workers = workers;
|
||||
projection.subscriptionId = frame.message.payload.subscription_id;
|
||||
return;
|
||||
}
|
||||
if (frame.frame !== 'event' || frame.message.event !== 'event') return;
|
||||
applyPayload(projection, frame.message.data.subject_revision, frame.message.data.payload);
|
||||
const subscriptionId = frame.message.data.subscription_id;
|
||||
if (!projection.subscriptionId) {
|
||||
throw new Error('workspace_workers event arrived before its snapshot');
|
||||
}
|
||||
if (subscriptionId !== projection.subscriptionId) return;
|
||||
applyPayload(projection, frame.message.data.payload);
|
||||
}
|
||||
|
||||
function applyPayload(
|
||||
projection: WorkspaceWorkersProjection,
|
||||
subjectRevision: number,
|
||||
payload: SubscriptionEventPayload,
|
||||
): void {
|
||||
if (payload.event === 'worker_upserted') {
|
||||
const worker = payload.data.worker;
|
||||
const key = workerKey(worker.runtime_id, worker.worker_id);
|
||||
if (subjectRevision <= (projection.revisions.get(key) ?? 0)) return;
|
||||
projection.revisions.set(key, subjectRevision);
|
||||
projection.workers.set(key, worker);
|
||||
projection.workers.set(workerKey(worker.runtime_id, worker.worker_id), worker);
|
||||
} else if (payload.event === 'worker_removed') {
|
||||
const key = workerKey(payload.data.runtime_id, payload.data.worker_id);
|
||||
if (subjectRevision <= (projection.revisions.get(key) ?? 0)) return;
|
||||
projection.revisions.set(key, subjectRevision);
|
||||
projection.workers.delete(key);
|
||||
projection.workers.delete(workerKey(payload.data.runtime_id, payload.data.worker_id));
|
||||
}
|
||||
}
|
||||
|
||||
function workerKey(runtimeId: string | null | undefined, workerId: string): string {
|
||||
if (!runtimeId) throw new Error('Workspace Worker projection is missing runtime_id');
|
||||
return `${runtimeId}:${workerId}`;
|
||||
return JSON.stringify([runtimeId, workerId]);
|
||||
}
|
||||
|
||||
@@ -1,15 +1,29 @@
|
||||
import type { SubscriptionFrame, SubscriptionWorker } from '$lib/generated/protocol';
|
||||
import type {
|
||||
SubscriptionEventPayload,
|
||||
SubscriptionFrame,
|
||||
SubscriptionWorker,
|
||||
} from '$lib/generated/protocol';
|
||||
|
||||
import { liveWorkerState } from './worker-state';
|
||||
import {
|
||||
applyWorkspaceWorkersFrame,
|
||||
createWorkspaceWorkersProjection,
|
||||
} from './worker-subscription-model';
|
||||
|
||||
function assertEquals(actual: unknown, expected: unknown): void {
|
||||
if (JSON.stringify(actual) !== JSON.stringify(expected)) {
|
||||
throw new Error(`expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`);
|
||||
}
|
||||
}
|
||||
import { liveWorkerState } from './worker-state';
|
||||
import {
|
||||
applyWorkspaceWorkersFrame,
|
||||
createWorkspaceWorkersProjection,
|
||||
} from './worker-subscription-model';
|
||||
|
||||
function assertThrows(fn: () => void, message: string): void {
|
||||
try {
|
||||
fn();
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
throw new Error(message);
|
||||
}
|
||||
|
||||
declare const Deno: {
|
||||
test(name: string, fn: () => void | Promise<void>): void;
|
||||
@@ -18,7 +32,6 @@ declare const Deno: {
|
||||
function worker(
|
||||
runtimeId: string,
|
||||
workerId: string,
|
||||
revision: number,
|
||||
hasRunningInternalWorkers = false,
|
||||
availability: 'observed' | 'unavailable' = 'observed',
|
||||
): SubscriptionWorker {
|
||||
@@ -26,7 +39,6 @@ function worker(
|
||||
worker_id: workerId,
|
||||
runtime_id: runtimeId,
|
||||
availability,
|
||||
subject_revision: revision,
|
||||
state: 'idle',
|
||||
has_running_internal_workers: hasRunningInternalWorkers,
|
||||
workspace_id: 'workspace-test',
|
||||
@@ -36,15 +48,52 @@ function worker(
|
||||
};
|
||||
}
|
||||
|
||||
function key(runtimeId: string, workerId: string): string {
|
||||
return JSON.stringify([runtimeId, workerId]);
|
||||
}
|
||||
|
||||
function snapshot(
|
||||
subscriptionId: string,
|
||||
workers: SubscriptionWorker[],
|
||||
): SubscriptionFrame {
|
||||
return {
|
||||
protocol_version: 2,
|
||||
frame: 'response',
|
||||
message: {
|
||||
result: 'subscribed',
|
||||
payload: {
|
||||
request_id: `request-${subscriptionId}`,
|
||||
subscription_id: subscriptionId,
|
||||
selector: { topic: 'workspace_workers' },
|
||||
snapshot: { topic: 'workers', data: { workers } },
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function event(
|
||||
subscriptionId: string,
|
||||
payload: SubscriptionEventPayload,
|
||||
): SubscriptionFrame {
|
||||
return {
|
||||
protocol_version: 2,
|
||||
frame: 'event',
|
||||
message: {
|
||||
event: 'event',
|
||||
data: { subscription_id: subscriptionId, payload },
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
Deno.test('Worker list state uses the authoritative live snapshot separately from lifecycle', () => {
|
||||
const active = worker('runtime-a', 'worker-1', 1);
|
||||
const active = worker('runtime-a', 'worker-1');
|
||||
active.worker_state = {
|
||||
last_command_id: 1,
|
||||
state: { kind: 'busy', state: { kind: 'run', state: 'paused' } },
|
||||
};
|
||||
assertEquals(liveWorkerState(active), 'paused');
|
||||
|
||||
const unavailable = worker('runtime-a', 'worker-2', 1, false, 'unavailable');
|
||||
const unavailable = worker('runtime-a', 'worker-2', false, 'unavailable');
|
||||
unavailable.worker_state = {
|
||||
last_command_id: 2,
|
||||
state: { kind: 'busy', state: { kind: 'run', state: 'running' } },
|
||||
@@ -67,127 +116,118 @@ Deno.test('Worker list state uses the authoritative live snapshot separately fro
|
||||
|
||||
Deno.test('workspace Worker snapshot keeps equal local ids from different Runtimes', () => {
|
||||
const projection = createWorkspaceWorkersProjection();
|
||||
const frame: SubscriptionFrame = {
|
||||
protocol_version: 1,
|
||||
frame: 'response',
|
||||
message: {
|
||||
result: 'subscribed',
|
||||
payload: {
|
||||
request_id: 'request-1',
|
||||
subscription_id: 'subscription-1',
|
||||
selector: { topic: 'workspace_workers' },
|
||||
snapshot_revision: 1,
|
||||
snapshot: {
|
||||
topic: 'workers',
|
||||
data: { workers: [worker('runtime-a', '1', 1), worker('runtime-b', '1', 1)] },
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
applyWorkspaceWorkersFrame(projection, frame);
|
||||
assertEquals([...projection.workers.keys()].sort(), ['runtime-a:1', 'runtime-b:1']);
|
||||
applyWorkspaceWorkersFrame(
|
||||
projection,
|
||||
snapshot('subscription-1', [worker('runtime-a', '1'), worker('runtime-b', '1')]),
|
||||
);
|
||||
assertEquals([...projection.workers.keys()].sort(), [key('runtime-a', '1'), key('runtime-b', '1')]);
|
||||
});
|
||||
|
||||
Deno.test('workspace Worker snapshot and live update preserve canonical Job metadata', () => {
|
||||
Deno.test('workspace Worker snapshot and ordered live update preserve canonical Job metadata', () => {
|
||||
const projection = createWorkspaceWorkersProjection();
|
||||
const snapshotWorker = worker('runtime-a', 'job-worker', 1);
|
||||
const snapshotWorker = worker('runtime-a', 'job-worker');
|
||||
snapshotWorker.job = {
|
||||
job_id: 'check:T-1:r1',
|
||||
attempt_id: 'check:T-1:r1:attempt:1',
|
||||
purpose: 'ticket_item_check',
|
||||
};
|
||||
applyWorkspaceWorkersFrame(projection, {
|
||||
protocol_version: 1,
|
||||
frame: 'response',
|
||||
message: {
|
||||
result: 'subscribed',
|
||||
payload: {
|
||||
request_id: 'request-1',
|
||||
subscription_id: 'subscription-1',
|
||||
selector: { topic: 'workspace_workers' },
|
||||
snapshot_revision: 1,
|
||||
snapshot: { topic: 'workers', data: { workers: [snapshotWorker] } },
|
||||
},
|
||||
},
|
||||
});
|
||||
assertEquals(projection.workers.get('runtime-a:job-worker')?.job, snapshotWorker.job);
|
||||
applyWorkspaceWorkersFrame(projection, snapshot('subscription-1', [snapshotWorker]));
|
||||
assertEquals(projection.workers.get(key('runtime-a', 'job-worker'))?.job, snapshotWorker.job);
|
||||
|
||||
const updated = { ...snapshotWorker, subject_revision: 2 };
|
||||
const updated = { ...snapshotWorker };
|
||||
updated.job = { ...snapshotWorker.job, attempt_id: 'check:T-1:r1:attempt:2' };
|
||||
applyWorkspaceWorkersFrame(projection, {
|
||||
protocol_version: 1,
|
||||
frame: 'event',
|
||||
message: {
|
||||
event: 'event',
|
||||
data: {
|
||||
subscription_id: 'subscription-1',
|
||||
subject_revision: 2,
|
||||
payload: { event: 'worker_upserted', data: { worker: updated } },
|
||||
},
|
||||
},
|
||||
});
|
||||
assertEquals(projection.workers.get('runtime-a:job-worker')?.job, updated.job);
|
||||
applyWorkspaceWorkersFrame(
|
||||
projection,
|
||||
event('subscription-1', { event: 'worker_upserted', data: { worker: updated } }),
|
||||
);
|
||||
assertEquals(projection.workers.get(key('runtime-a', 'job-worker'))?.job, updated.job);
|
||||
});
|
||||
|
||||
Deno.test('workspace Worker reducer ignores stale events and removes composite subject', () => {
|
||||
Deno.test('fresh subscription snapshot replaces old state and fences delayed old events', () => {
|
||||
const projection = createWorkspaceWorkersProjection();
|
||||
projection.workers.set('runtime-a:1', worker('runtime-a', '1', 3));
|
||||
projection.revisions.set('runtime-a:1', 3);
|
||||
applyWorkspaceWorkersFrame(
|
||||
projection,
|
||||
snapshot('subscription-old', [worker('runtime-a', 'old'), worker('runtime-b', 'same')]),
|
||||
);
|
||||
applyWorkspaceWorkersFrame(
|
||||
projection,
|
||||
event('subscription-old', {
|
||||
event: 'worker_upserted',
|
||||
data: { worker: worker('runtime-a', 'before-reconnect') },
|
||||
}),
|
||||
);
|
||||
|
||||
applyWorkspaceWorkersFrame(projection, {
|
||||
protocol_version: 1,
|
||||
frame: 'event',
|
||||
message: {
|
||||
event: 'event',
|
||||
data: {
|
||||
subscription_id: 'subscription-1',
|
||||
subject_revision: 2,
|
||||
payload: { event: 'worker_upserted', data: { worker: worker('runtime-a', '1', 2) } },
|
||||
},
|
||||
},
|
||||
});
|
||||
assertEquals(projection.revisions.get('runtime-a:1'), 3);
|
||||
applyWorkspaceWorkersFrame(
|
||||
projection,
|
||||
snapshot('subscription-new', [worker('runtime-a', 'current')]),
|
||||
);
|
||||
applyWorkspaceWorkersFrame(
|
||||
projection,
|
||||
event('subscription-old', {
|
||||
event: 'worker_upserted',
|
||||
data: { worker: worker('runtime-a', 'late-old-event') },
|
||||
}),
|
||||
);
|
||||
|
||||
applyWorkspaceWorkersFrame(projection, {
|
||||
protocol_version: 1,
|
||||
frame: 'event',
|
||||
message: {
|
||||
event: 'event',
|
||||
data: {
|
||||
subscription_id: 'subscription-1',
|
||||
subject_revision: 4,
|
||||
payload: {
|
||||
event: 'worker_removed',
|
||||
data: { worker_id: '1', runtime_id: 'runtime-a' },
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
assertEquals(projection.workers.size, 0);
|
||||
assertEquals(projection.revisions.get('runtime-a:1'), 4);
|
||||
assertEquals(projection.subscriptionId, 'subscription-new');
|
||||
assertEquals([...projection.workers.keys()], [key('runtime-a', 'current')]);
|
||||
});
|
||||
|
||||
Deno.test('workspace Worker reducer applies ordered updates and composite removal', () => {
|
||||
const projection = createWorkspaceWorkersProjection();
|
||||
applyWorkspaceWorkersFrame(
|
||||
projection,
|
||||
snapshot('subscription-1', [worker('runtime-a', '1'), worker('runtime-b', '1')]),
|
||||
);
|
||||
|
||||
const updated = worker('runtime-a', '1');
|
||||
updated.state = 'running';
|
||||
applyWorkspaceWorkersFrame(
|
||||
projection,
|
||||
event('subscription-1', { event: 'worker_upserted', data: { worker: updated } }),
|
||||
);
|
||||
applyWorkspaceWorkersFrame(
|
||||
projection,
|
||||
event('subscription-1', {
|
||||
event: 'worker_removed',
|
||||
data: { worker_id: '1', runtime_id: 'runtime-a' },
|
||||
}),
|
||||
);
|
||||
|
||||
assertEquals([...projection.workers.keys()], [key('runtime-b', '1')]);
|
||||
});
|
||||
|
||||
Deno.test('workspace Worker event before a snapshot is rejected', () => {
|
||||
const projection = createWorkspaceWorkersProjection();
|
||||
assertThrows(
|
||||
() =>
|
||||
applyWorkspaceWorkersFrame(
|
||||
projection,
|
||||
event('subscription-1', {
|
||||
event: 'worker_upserted',
|
||||
data: { worker: worker('runtime-a', '1') },
|
||||
}),
|
||||
),
|
||||
'event-before-snapshot must be rejected',
|
||||
);
|
||||
});
|
||||
|
||||
Deno.test('fatal child stop replaces the running-child sidebar projection', () => {
|
||||
const projection = createWorkspaceWorkersProjection();
|
||||
projection.workers.set('runtime-a:1', worker('runtime-a', '1', 1, true));
|
||||
projection.revisions.set('runtime-a:1', 1);
|
||||
applyWorkspaceWorkersFrame(
|
||||
projection,
|
||||
snapshot('subscription-1', [worker('runtime-a', '1', true)]),
|
||||
);
|
||||
applyWorkspaceWorkersFrame(
|
||||
projection,
|
||||
event('subscription-1', {
|
||||
event: 'worker_upserted',
|
||||
data: { worker: worker('runtime-a', '1', false) },
|
||||
}),
|
||||
);
|
||||
|
||||
applyWorkspaceWorkersFrame(projection, {
|
||||
protocol_version: 1,
|
||||
frame: 'event',
|
||||
message: {
|
||||
event: 'event',
|
||||
data: {
|
||||
subscription_id: 'subscription-1',
|
||||
subject_revision: 2,
|
||||
payload: {
|
||||
event: 'worker_upserted',
|
||||
data: { worker: worker('runtime-a', '1', 2, false) },
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
assertEquals(projection.workers.get('runtime-a:1')?.has_running_internal_workers, false);
|
||||
assertEquals(projection.revisions.get('runtime-a:1'), 2);
|
||||
assertEquals(
|
||||
projection.workers.get(key('runtime-a', '1'))?.has_running_internal_workers,
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user