Bug fixes
This commit is contained in:
@@ -2,7 +2,8 @@ import z from "zod"
|
||||
import sessionProjectors from "../session/projectors"
|
||||
import { SyncEvent } from "@/sync"
|
||||
import { Session } from "@/session"
|
||||
import { SessionID } from "@/session/schema"
|
||||
import { SessionTable } from "@/session/session.sql"
|
||||
import { Database, eq } from "@/storage/db"
|
||||
|
||||
let initialized = false
|
||||
|
||||
@@ -16,10 +17,14 @@ export function initProjectors() {
|
||||
projectors: sessionProjectors,
|
||||
convertEvent: (type, data) => {
|
||||
if (type === "session.updated") {
|
||||
const sessionID = (data as z.infer<typeof Session.Event.Updated.schema>).sessionID
|
||||
const id = (data as z.infer<typeof Session.Event.Updated.schema>).sessionID
|
||||
const row = Database.use((db) => db.select().from(SessionTable).where(eq(SessionTable.id, id)).get())
|
||||
|
||||
if (!row) return data
|
||||
|
||||
return {
|
||||
sessionID: SessionID.zod,
|
||||
info: Session.get(sessionID),
|
||||
sessionID: id,
|
||||
info: Session.fromRow(row),
|
||||
}
|
||||
}
|
||||
return data
|
||||
|
||||
@@ -16,6 +16,7 @@ import { SessionTable } from "./session.sql"
|
||||
import { ProjectTable } from "../project/project.sql"
|
||||
import { Storage } from "@/storage/storage"
|
||||
import { Log } from "../util/log"
|
||||
import { updateSchema } from "../util/update-schema"
|
||||
import { MessageV2 } from "./message-v2"
|
||||
import { Instance } from "../project/instance"
|
||||
import { SessionPrompt } from "./prompt"
|
||||
@@ -198,9 +199,9 @@ export namespace Session {
|
||||
aggregate: "sessionID",
|
||||
schema: z.object({
|
||||
sessionID: SessionID.zod,
|
||||
info: Info.partial().extend({
|
||||
share: Info.shape.share.unwrap().partial().optional(),
|
||||
time: Info.shape.time.partial().optional(),
|
||||
info: updateSchema(Info).extend({
|
||||
share: updateSchema(Info.shape.share.unwrap()).optional(),
|
||||
time: updateSchema(Info.shape.time).optional(),
|
||||
}),
|
||||
}),
|
||||
busSchema: z.object({
|
||||
@@ -378,7 +379,7 @@ export namespace Session {
|
||||
const { ShareNext } = await import("@/share/share-next")
|
||||
await ShareNext.remove(id)
|
||||
|
||||
SyncEvent.run(Event.Updated, { sessionID: id, info: { share: { url: undefined } } })
|
||||
SyncEvent.run(Event.Updated, { sessionID: id, info: { share: { url: null } } })
|
||||
})
|
||||
|
||||
export const setTitle = fn(
|
||||
@@ -437,7 +438,7 @@ export namespace Session {
|
||||
sessionID,
|
||||
info: {
|
||||
time: { updated: Date.now() },
|
||||
revert: undefined,
|
||||
revert: null,
|
||||
},
|
||||
})
|
||||
})
|
||||
@@ -615,6 +616,9 @@ export namespace Session {
|
||||
await unshare(sessionID).catch(() => {})
|
||||
|
||||
SyncEvent.run(Event.Deleted, { sessionID, info: session })
|
||||
|
||||
// Eagerly remove event sourcing data to free up space
|
||||
SyncEvent.remove(sessionID)
|
||||
} catch (e) {
|
||||
log.error(e)
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import { MessageV2 } from "./message-v2"
|
||||
import { SessionTable, MessageTable, PartTable } from "./session.sql"
|
||||
import { ProjectTable } from "../project/project.sql"
|
||||
|
||||
export type DeepPartial<T> = T extends object ? { [K in keyof T]?: DeepPartial<T[K]> } : T
|
||||
export type DeepPartial<T> = T extends object ? { [K in keyof T]?: DeepPartial<T[K]> | null } : T
|
||||
|
||||
function grab<T extends object, K1 extends keyof T, X>(
|
||||
obj: T,
|
||||
@@ -18,7 +18,12 @@ function grab<T extends object, K1 extends keyof T, X>(
|
||||
if (val && typeof val === "object" && cb) {
|
||||
return cb(val)
|
||||
}
|
||||
return (val === undefined ? null : val) as X | undefined
|
||||
if (val === undefined) {
|
||||
throw new Error(
|
||||
"Session update failure: pass `null` to clear a field instead of `undefined`: " + JSON.stringify(obj),
|
||||
)
|
||||
}
|
||||
return val as X | undefined
|
||||
}
|
||||
|
||||
export function toPartialRow(info: DeepPartial<Session.Info>) {
|
||||
|
||||
@@ -35,14 +35,11 @@ export namespace SyncEvent {
|
||||
let projectors: Map<Definition, ProjectorFunc> | undefined
|
||||
const versions = new Map<string, number>()
|
||||
let frozen = false
|
||||
let convertEvent: ((type: string, event: Event["data"]) => Record<string, unknown>) | undefined
|
||||
let convertEvent: (type: string, event: Event["data"]) => Promise<Record<string, unknown>> | Record<string, unknown>
|
||||
|
||||
const Bus = new EventEmitter<{ event: [{ def: Definition; event: Event }] }>()
|
||||
|
||||
export function init(input: {
|
||||
projectors: Array<[Definition, ProjectorFunc]>
|
||||
convertEvent?: Exclude<typeof convertEvent, undefined>
|
||||
}) {
|
||||
export function init(input: { projectors: Array<[Definition, ProjectorFunc]>; convertEvent?: typeof convertEvent }) {
|
||||
projectors = new Map(input.projectors)
|
||||
|
||||
// Install all the latest event defs to the bus. We only ever emit
|
||||
@@ -58,7 +55,7 @@ export namespace SyncEvent {
|
||||
// Freeze the system so it clearly errors if events are defined
|
||||
// after `init` which would cause bugs
|
||||
frozen = true
|
||||
convertEvent = input.convertEvent
|
||||
convertEvent = input.convertEvent || ((_, data) => data)
|
||||
}
|
||||
|
||||
export function versionedType<A extends string>(type: A): A
|
||||
@@ -99,7 +96,7 @@ export namespace SyncEvent {
|
||||
return [def, func as ProjectorFunc]
|
||||
}
|
||||
|
||||
function process<Def extends Definition>(def: Def, input: Event<Def>) {
|
||||
function process<Def extends Definition>(def: Def, event: Event<Def>, options: { publish: boolean }) {
|
||||
if (projectors == null) {
|
||||
throw new Error("No projectors available. Call `SyncEvent.init` to install projectors")
|
||||
}
|
||||
@@ -112,29 +109,47 @@ export namespace SyncEvent {
|
||||
// idempotent: need to ignore any events already logged
|
||||
|
||||
Database.transaction((tx) => {
|
||||
projector(tx, input.data)
|
||||
projector(tx, event.data)
|
||||
|
||||
if (Flag.OPENCODE_EXPERIMENTAL_WORKSPACES) {
|
||||
tx.insert(EventSequenceTable)
|
||||
.values({
|
||||
aggregate_id: input.aggregateID,
|
||||
seq: input.seq,
|
||||
aggregate_id: event.aggregateID,
|
||||
seq: event.seq,
|
||||
})
|
||||
.onConflictDoUpdate({
|
||||
target: EventSequenceTable.aggregate_id,
|
||||
set: { seq: input.seq },
|
||||
set: { seq: event.seq },
|
||||
})
|
||||
.run()
|
||||
tx.insert(EventTable)
|
||||
.values({
|
||||
id: input.id,
|
||||
seq: input.seq,
|
||||
aggregate_id: input.aggregateID,
|
||||
name: def.type,
|
||||
data: input.data as Record<string, unknown>,
|
||||
id: event.id,
|
||||
seq: event.seq,
|
||||
aggregate_id: event.aggregateID,
|
||||
name: versionedType(def.type, def.version),
|
||||
data: event.data as Record<string, unknown>,
|
||||
})
|
||||
.run()
|
||||
}
|
||||
|
||||
Database.effect(() => {
|
||||
Bus.emit("event", {
|
||||
def,
|
||||
event,
|
||||
})
|
||||
|
||||
if (options?.publish) {
|
||||
const result = convertEvent(def.type, event.data)
|
||||
if (result instanceof Promise) {
|
||||
result.then((data) => {
|
||||
ProjectBus.publish({ type: def.type, properties: def.schema }, data)
|
||||
})
|
||||
} else {
|
||||
ProjectBus.publish({ type: def.type, properties: def.schema }, result)
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -144,7 +159,7 @@ export namespace SyncEvent {
|
||||
// and it validets all the sequence ids
|
||||
// * when loading events from db, apply zod validation to ensure shape
|
||||
|
||||
export function replay(event: SerializedEvent) {
|
||||
export function replay(event: SerializedEvent, options?: { republish: boolean }) {
|
||||
const def = registry.get(event.type)
|
||||
if (!def) {
|
||||
throw new Error(`Unknown event type: ${event.type}`)
|
||||
@@ -158,12 +173,17 @@ export namespace SyncEvent {
|
||||
.get(),
|
||||
)
|
||||
|
||||
const expected = row ? row.seq + 1 : 0
|
||||
const latest = row?.seq ?? -1
|
||||
if (event.seq <= latest) {
|
||||
return
|
||||
}
|
||||
|
||||
const expected = latest + 1
|
||||
if (event.seq !== expected) {
|
||||
throw new Error(`Sequence mismatch for aggregate "${event.aggregateID}": expected ${expected}, got ${event.seq}`)
|
||||
}
|
||||
|
||||
process(def, event)
|
||||
process(def, event, { publish: !!options?.republish })
|
||||
}
|
||||
|
||||
export function run<Def extends Definition>(def: Def, data: Event<Def>["data"]) {
|
||||
@@ -192,22 +212,7 @@ export namespace SyncEvent {
|
||||
const seq = row?.seq != null ? row.seq + 1 : 0
|
||||
|
||||
const event = { id, seq, aggregateID: agg, data }
|
||||
process(def, event)
|
||||
|
||||
Database.effect(() => {
|
||||
Bus.emit("event", {
|
||||
def,
|
||||
event,
|
||||
})
|
||||
|
||||
ProjectBus.publish(
|
||||
{
|
||||
type: def.type,
|
||||
properties: def.schema,
|
||||
},
|
||||
convertEvent ? convertEvent(def.type, event.data) : event.data,
|
||||
)
|
||||
})
|
||||
process(def, event, { publish: true })
|
||||
},
|
||||
{
|
||||
behavior: "immediate",
|
||||
@@ -215,6 +220,13 @@ export namespace SyncEvent {
|
||||
)
|
||||
}
|
||||
|
||||
export function remove(aggregateID: string) {
|
||||
Database.transaction((tx) => {
|
||||
tx.delete(EventSequenceTable).where(eq(EventSequenceTable.aggregate_id, aggregateID)).run()
|
||||
tx.delete(EventTable).where(eq(EventTable.aggregate_id, aggregateID)).run()
|
||||
})
|
||||
}
|
||||
|
||||
export function subscribeAll(handler: (event: { def: Definition; event: Event }) => void) {
|
||||
Bus.on("event", handler)
|
||||
return () => Bus.off("event", handler)
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import z from "zod"
|
||||
|
||||
export function updateSchema<T extends z.ZodRawShape>(schema: z.ZodObject<T>) {
|
||||
const next = {} as {
|
||||
[K in keyof T]: z.ZodOptional<z.ZodNullable<T[K]>>
|
||||
}
|
||||
|
||||
for (const [k, v] of Object.entries(schema.required().shape) as [keyof T & string, z.ZodTypeAny][]) {
|
||||
next[k] = v.nullable() as unknown as (typeof next)[typeof k]
|
||||
}
|
||||
|
||||
return z.object(next)
|
||||
}
|
||||
Reference in New Issue
Block a user