feat: add URL-driven workspace state and streamed Codex research

This commit is contained in:
2026-09-28 20:08:28 +09:00
parent 85d23a328b
commit 8085ad2f90
207 changed files with 15287 additions and 16026 deletions
+60 -72
View File
@@ -1,6 +1,6 @@
import { and, asc, eq } from 'drizzle-orm'
import { type AppDatabase, getDatabase } from '../storage/database.server'
import { connections, deckColumns, decks } from '../storage/schema'
import { and, asc, eq } from "drizzle-orm";
import { type AppDatabase, getDatabase } from "../storage/database.server";
import { connections, deckColumns, decks } from "../storage/schema";
import {
type DeckPersistenceFailure,
deleteDeckInputSchema,
@@ -9,33 +9,27 @@ import {
type SavedDeckDefinition,
savedDeckDefinitionSchema,
savedDeckSchema,
} from './persistence-model'
} from "./persistence-model";
export class DeckPersistenceError extends Error {
constructor(
readonly code: DeckPersistenceFailure['error']['code'],
readonly code: DeckPersistenceFailure["error"]["code"],
message: string,
) {
super(message)
super(message);
}
}
const conflict = () =>
new DeckPersistenceError(
'conflict',
'This deck changed on another device. Reload the latest deck.',
)
"conflict",
"This deck changed on another device. Reload the latest deck.",
);
const missing = () =>
new DeckPersistenceError(
'not-found',
'This deck was deleted or could not be found.',
)
new DeckPersistenceError("not-found", "This deck was deleted or could not be found.");
export function loadDeck(
id: string,
database = getDatabase(),
): SavedDeck | null {
const record = database.select().from(decks).where(eq(decks.id, id)).get()
if (!record) return null
export function loadDeck(id: string, database = getDatabase()): SavedDeck | null {
const record = database.select().from(decks).where(eq(decks.id, id)).get();
if (!record) return null;
const columns = database
.select({
id: deckColumns.id,
@@ -46,8 +40,8 @@ export function loadDeck(
.from(deckColumns)
.where(eq(deckColumns.deckId, id))
.orderBy(asc(deckColumns.position))
.all()
return savedDeckSchema.parse({ ...record, columns })
.all();
return savedDeckSchema.parse({ ...record, columns });
}
export function listDecks(database = getDatabase()): SavedDeck[] {
@@ -58,11 +52,11 @@ export function listDecks(database = getDatabase()): SavedDeck[] {
.orderBy(asc(decks.createdAt), asc(decks.id))
.all()
.map(({ id }) => {
const deck = loadDeck(id, database)
if (!deck) throw missing()
return deck
const deck = loadDeck(id, database);
if (!deck) throw missing();
return deck;
}),
)
);
}
function validateConnections(deck: SavedDeckDefinition, database: AppDatabase) {
@@ -71,22 +65,22 @@ function validateConnections(deck: SavedDeckDefinition, database: AppDatabase) {
.select({ platform: connections.platform, status: connections.status })
.from(connections)
.where(eq(connections.id, column.connectionId))
.get()
.get();
if (
!connection ||
connection.platform !== column.source.platform ||
connection.status !== 'connected'
connection.status !== "connected"
) {
throw new DeckPersistenceError(
'connection-unavailable',
'The account for this column is unavailable. Check your connections.',
)
"connection-unavailable",
"The account for this column is unavailable. Check your connections.",
);
}
}
}
function writeColumns(deck: SavedDeckDefinition, database: AppDatabase) {
database.delete(deckColumns).where(eq(deckColumns.deckId, deck.id)).run()
database.delete(deckColumns).where(eq(deckColumns.deckId, deck.id)).run();
if (deck.columns.length)
database
.insert(deckColumns)
@@ -97,29 +91,26 @@ function writeColumns(deck: SavedDeckDefinition, database: AppDatabase) {
position,
})),
)
.run()
.run();
}
export function createDeck(
input: unknown,
database = getDatabase(),
): SavedDeck {
const deck = savedDeckDefinitionSchema.parse(input)
export function createDeck(input: unknown, database = getDatabase()): SavedDeck {
const deck = savedDeckDefinitionSchema.parse(input);
return database.transaction(
() => {
const existing = loadDeck(deck.id, database)
const existing = loadDeck(deck.id, database);
if (existing) {
const original = savedDeckDefinitionSchema.parse({
id: existing.id,
title: existing.title,
columns: existing.columns,
})
if (JSON.stringify(original) !== JSON.stringify(deck)) throw conflict()
return existing
});
if (JSON.stringify(original) !== JSON.stringify(deck)) throw conflict();
return existing;
}
validateConnections(deck, database)
const now = Date.now()
const result = { ...deck, revision: 1, createdAt: now, updatedAt: now }
validateConnections(deck, database);
const now = Date.now();
const result = { ...deck, revision: 1, createdAt: now, updatedAt: now };
database
.insert(decks)
.values({
@@ -129,56 +120,53 @@ export function createDeck(
createdAt: now,
updatedAt: now,
})
.run()
writeColumns(deck, database)
return result
.run();
writeColumns(deck, database);
return result;
},
{ behavior: 'immediate' },
)
{ behavior: "immediate" },
);
}
export function replaceDeck(
input: unknown,
database = getDatabase(),
): SavedDeck {
const { deck, expectedRevision } = replaceDeckInputSchema.parse(input)
export function replaceDeck(input: unknown, database = getDatabase()): SavedDeck {
const { deck, expectedRevision } = replaceDeckInputSchema.parse(input);
return database.transaction(
() => {
const existing = loadDeck(deck.id, database)
if (!existing) throw missing()
if (existing.revision !== expectedRevision) throw conflict()
validateConnections(deck, database)
const updatedAt = Date.now()
const existing = loadDeck(deck.id, database);
if (!existing) throw missing();
if (existing.revision !== expectedRevision) throw conflict();
validateConnections(deck, database);
const updatedAt = Date.now();
const result = database
.update(decks)
.set({ title: deck.title, revision: expectedRevision + 1, updatedAt })
.where(and(eq(decks.id, deck.id), eq(decks.revision, expectedRevision)))
.run()
if (result.changes !== 1) throw conflict()
writeColumns(deck, database)
.run();
if (result.changes !== 1) throw conflict();
writeColumns(deck, database);
return {
...deck,
revision: expectedRevision + 1,
createdAt: existing.createdAt,
updatedAt,
}
};
},
{ behavior: 'immediate' },
)
{ behavior: "immediate" },
);
}
export function deleteDeck(input: unknown, database = getDatabase()): string {
const { id, expectedRevision } = deleteDeckInputSchema.parse(input)
const { id, expectedRevision } = deleteDeckInputSchema.parse(input);
return database.transaction(
() => {
if (!loadDeck(id, database)) throw missing()
if (!loadDeck(id, database)) throw missing();
const result = database
.delete(decks)
.where(and(eq(decks.id, id), eq(decks.revision, expectedRevision)))
.run()
if (result.changes !== 1) throw conflict()
return id
.run();
if (result.changes !== 1) throw conflict();
return id;
},
{ behavior: 'immediate' },
)
{ behavior: "immediate" },
);
}