/** Client-side Workspace state model shared by Remote transport and UI projection. */
import { notifySubscribers } from '@deepseek-ai/dsh-client-store';
/**
 * Owns the Client Workspace projection, mutation echoes, and stream/unary race resolution.
 */
export class ClientWorkspaceModel {
    remote;
    items = [];
    archivedSessionIds = [];
    state = 'loading';
    phase = 'pending';
    error = null;
    /** Latest local reorder request; only its unary echo may install order. */
    orderRequestGeneration = 0;
    /** Increments on stream orders so a later remote commit outranks an older unary echo. */
    orderFrameGeneration = 0;
    /** Last complete order accepted from a baseline, increment, or current unary echo. */
    committedOrder = [];
    /** Host Workspace ids are never reused, so delayed data cannot resurrect a removed row. */
    removedIds = new Set();
    listeners = new Set();
    snapshotCache;
    snapshotDirty = false;
    notificationPending = false;
    notificationScheduled = false;
    notificationGeneration = 0;
    /** @param remote - generated Workspace Remote namespace. */
    constructor(remote) {
        this.remote = remote;
        this.snapshotCache = this.buildSnapshot();
    }
    /**
     * Create or resolve a Workspace and merge the unary result immediately.
     * @param input - existing absolute path to adopt.
     * @returns generated Remote result.
     */
    async create(input) {
        let result;
        try {
            result = await this.remote.create(input);
        }
        catch (error) {
            result = failureResult(error);
        }
        if (result.ok)
            this.upsert(result.value.workspace);
        return result;
    }
    /**
     * Rename a Workspace and merge the unary result immediately.
     * @param workspaceId - target Workspace.
     * @param title - new display title.
     * @returns generated Remote result.
     */
    async rename(workspaceId, title) {
        const result = await this.remote.rename({ workspaceId, title });
        if (result.ok)
            this.upsert(result.value.workspace);
        return result;
    }
    /**
     * Delete a Workspace and remove it from the local projection immediately.
     * @param workspaceId - target Workspace.
     * @returns generated Remote result.
     */
    async delete(workspaceId) {
        const result = await this.remote.delete({ workspaceId });
        if (result.ok)
            this.remove(workspaceId, true);
        return result;
    }
    /**
     * Optimistically move a Workspace and reconcile the returned complete order.
     * @param workspaceId - Workspace to move.
     * @param beforeWorkspaceId - anchor Workspace; omitted appends.
     * @returns generated Remote result.
     */
    async insertBefore(workspaceId, beforeWorkspaceId) {
        const requestGeneration = ++this.orderRequestGeneration;
        const frameGeneration = this.orderFrameGeneration;
        const localOrder = this.items.map(workspace => workspace.workspaceId);
        this.installOrder(insertIdBefore(localOrder, workspaceId, beforeWorkspaceId));
        let result;
        try {
            result = await this.remote.insertBefore({
                workspaceId,
                ...beforeWorkspaceId === undefined ? {} : { beforeWorkspaceId },
            });
        }
        catch (error) {
            if (requestGeneration === this.orderRequestGeneration
                && frameGeneration === this.orderFrameGeneration) {
                this.installOrder(this.committedOrder);
            }
            throw error;
        }
        if (requestGeneration === this.orderRequestGeneration
            && frameGeneration === this.orderFrameGeneration) {
            this.installOrder(result.ok ? result.value.workspaceIds : this.committedOrder, result.ok);
        }
        return result;
    }
    /**
     * Move a Session within its Workspace and merge the returned row.
     * @param workspaceId - owning Workspace.
     * @param sessionId - accounted Session to move.
     * @param beforeSessionId - accounted anchor; omitted appends.
     * @returns generated Remote result.
     */
    async insertSessionBefore(workspaceId, sessionId, beforeSessionId) {
        const result = await this.remote.insertSessionBefore({
            workspaceId,
            sessionId,
            ...beforeSessionId === undefined ? {} : { beforeSessionId },
        });
        if (result.ok)
            this.upsert(result.value.workspace);
        return result;
    }
    /**
     * Archive one Session and install the returned complete archive set.
     * @param sessionId - Session to archive.
     * @returns generated Remote result.
     */
    async archiveSession(sessionId) {
        const result = await this.remote.archiveSession({ sessionId });
        if (result.ok)
            this.installArchived(result.value.archivedSessionIds);
        return result;
    }
    /**
     * Replace the projection from one complete stream-generation baseline.
     * @param baseline - complete Workspace and archive projection.
     */
    replaceBaseline(baseline) {
        this.orderFrameGeneration++;
        this.installViews(baseline.items);
        this.installArchived(baseline.archivedSessionIds);
        this.state = 'idle';
        this.phase = 'ready';
        this.error = null;
        this.invalidate();
    }
    /** Merge one decoded Workspace upsert from the current follow generation. */
    upsertView(workspace) {
        this.upsert(workspace);
    }
    /** Apply one decoded Workspace removal from the current follow generation. */
    removeView(workspaceId) {
        this.remove(workspaceId);
    }
    /** Replace Host-confirmed order from the current follow generation. */
    replaceOrder(workspaceIds) {
        this.orderFrameGeneration++;
        this.installOrder(workspaceIds, true);
    }
    /**
     * Replace the archived Session set from the current follow generation.
     * @param archivedSessionIds - complete Host-confirmed archive set.
     */
    replaceArchived(archivedSessionIds) {
        this.installArchived(archivedSessionIds);
    }
    /** Keep the last complete projection visible while a lost carrier reconnects. */
    handleCarrierFailure() {
        this.state = 'loading';
        this.error = null;
        this.invalidate();
    }
    /**
     * Publish a non-retryable stream or protocol failure.
     * @param error - terminal stream failure.
     */
    handleStreamFailure(error) {
        this.state = 'error';
        this.error = failureOf(error);
        this.invalidate();
    }
    /**
     * Subscribe to Workspace state invalidation.
     * @param listener - invalidation callback.
     * @returns unsubscribe function.
     */
    subscribe(listener) {
        this.listeners.add(listener);
        return () => { this.listeners.delete(listener); };
    }
    /**
     * Read the cached state, rebuilding it first when necessary.
     * @returns the current stable Workspace list snapshot.
     */
    getSnapshot() {
        this.refreshSnapshot();
        return this.snapshotCache;
    }
    buildSnapshot() {
        return {
            items: this.items,
            archivedSessionIds: this.archivedSessionIds,
            state: this.state,
            phase: this.phase,
            error: this.error,
        };
    }
    installArchived(archivedSessionIds) {
        if (archivedSessionIds.length === this.archivedSessionIds.length
            && archivedSessionIds.every((id, index) => id === this.archivedSessionIds[index]))
            return;
        this.archivedSessionIds = [...archivedSessionIds];
        this.invalidate();
    }
    installOrder(workspaceIds, committed = false) {
        if (committed)
            this.committedOrder = [...workspaceIds];
        const rank = new Map(workspaceIds.map((id, index) => [id, index]));
        const items = [...this.items].sort((left, right) => (rank.get(left.workspaceId) ?? Number.MAX_SAFE_INTEGER)
            - (rank.get(right.workspaceId) ?? Number.MAX_SAFE_INTEGER));
        if (items.every((item, index) => item === this.items[index]))
            return;
        this.items = items;
        this.invalidate();
    }
    upsert(view) {
        if (this.removedIds.has(view.workspaceId))
            return;
        const index = this.items.findIndex(item => item.workspaceId === view.workspaceId);
        const installed = this.items[index];
        // Unary responses and stream increments race on separate requests. Keep
        // the newest Host projection regardless of their arrival order.
        if (installed !== undefined && Date.parse(view.updatedAt) < Date.parse(installed.updatedAt))
            return;
        if (!this.committedOrder.includes(view.workspaceId)) {
            this.committedOrder = [view.workspaceId, ...this.committedOrder];
        }
        this.items = index === -1
            ? [view, ...this.items]
            : this.items.map((item, position) => position === index ? view : item);
        this.invalidate();
    }
    remove(workspaceId, immediate = false) {
        this.removedIds.add(workspaceId);
        this.committedOrder = this.committedOrder.filter(id => id !== workspaceId);
        const items = this.items.filter(item => item.workspaceId !== workspaceId);
        if (items.length === this.items.length) {
            // A successful unary echo still publishes an earlier increment's
            // pending removal before the user operation resolves.
            if (immediate)
                this.invalidate(true);
            return;
        }
        this.items = items;
        this.invalidate(immediate);
    }
    installViews(views) {
        const installed = new Map();
        for (const view of views) {
            if (!this.removedIds.has(view.workspaceId))
                installed.set(view.workspaceId, view);
        }
        this.items = [...installed.values()];
        this.committedOrder = views.map(view => view.workspaceId);
    }
    invalidate(immediate = false) {
        this.snapshotDirty = true;
        this.notificationPending = true;
        if (immediate) {
            this.notificationGeneration++;
            this.notificationScheduled = false;
            this.flush();
            return;
        }
        if (this.notificationScheduled)
            return;
        this.notificationScheduled = true;
        const generation = ++this.notificationGeneration;
        queueMicrotask(() => {
            if (generation !== this.notificationGeneration)
                return;
            this.notificationScheduled = false;
            this.flush();
        });
    }
    flush() {
        if (!this.notificationPending || this.listeners.size === 0)
            return;
        this.notificationPending = false;
        this.refreshSnapshot();
        notifySubscribers(this.listeners, '[workspace-controller]');
    }
    refreshSnapshot() {
        if (!this.snapshotDirty)
            return;
        this.snapshotDirty = false;
        this.snapshotCache = this.buildSnapshot();
    }
}
function insertIdBefore(ids, id, beforeId) {
    if (!ids.includes(id) || (beforeId !== undefined && !ids.includes(beforeId)) || beforeId === id) {
        return [...ids];
    }
    const without = ids.filter(candidate => candidate !== id);
    const at = beforeId === undefined ? without.length : without.indexOf(beforeId);
    return [...without.slice(0, at), id, ...without.slice(at)];
}
function failureResult(error) {
    return { ok: false, error: failureOf(error) };
}
function failureOf(error) {
    return {
        code: 'internal',
        message: error instanceof Error ? error.message : String(error),
        details: {},
    };
}
//# sourceMappingURL=model.js.map