feat: add URL-driven workspace state and streamed Codex research
This commit is contained in:
@@ -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" },
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user