feat(core): session warping (#25768)

This commit is contained in:
James Long
2026-05-04 21:28:38 -04:00
committed by GitHub
parent 84afd2bef8
commit 22a4a9df8b
25 changed files with 4032 additions and 1095 deletions

View File

@@ -3,6 +3,7 @@ import { sqliteTable, text, integer } from "drizzle-orm/sqlite-core"
export const EventSequenceTable = sqliteTable("event_sequence", {
aggregate_id: text().notNull().primaryKey(),
seq: integer().notNull(),
owner_id: text(),
})
export const EventTable = sqliteTable("event", {

View File

@@ -59,8 +59,11 @@ export interface Interface {
data: Event<Def>["data"],
options?: { publish?: boolean },
) => Effect.Effect<void>
readonly replay: (event: SerializedEvent, options?: { publish: boolean }) => Effect.Effect<void>
readonly replayAll: (events: SerializedEvent[], options?: { publish: boolean }) => Effect.Effect<string | undefined>
readonly replay: (event: SerializedEvent, options?: { publish: boolean; ownerID?: string }) => Effect.Effect<void>
readonly replayAll: (
events: SerializedEvent[],
options?: { publish: boolean; ownerID?: string },
) => Effect.Effect<string | undefined>
readonly remove: (aggregateID: string) => Effect.Effect<void>
}
@@ -76,7 +79,7 @@ export const layer = Layer.effect(Service)(
const row = Database.use((db) =>
db
.select({ seq: EventSequenceTable.seq })
.select({ seq: EventSequenceTable.seq, ownerID: EventSequenceTable.owner_id })
.from(EventSequenceTable)
.where(eq(EventSequenceTable.aggregate_id, event.aggregateID))
.get(),
@@ -85,6 +88,10 @@ export const layer = Layer.effect(Service)(
const latest = row?.seq ?? -1
if (event.seq <= latest) return
if (row?.ownerID && row.ownerID !== options?.ownerID) {
return
}
const expected = latest + 1
if (event.seq !== expected) {
throw new Error(
@@ -99,7 +106,7 @@ export const layer = Layer.effect(Service)(
workspace: yield* InstanceState.workspaceID,
}
: undefined
process(def, event, { publish, context })
process(def, event, { publish, context, ownerID: options?.ownerID })
})
const replayAll: Interface["replayAll"] = Effect.fn("SyncEvent.replayAll")(function* (events, options) {
@@ -263,7 +270,7 @@ export function project<Def extends Definition>(
function process<Def extends Definition>(
def: Def,
event: Event<Def>,
options: { publish: boolean; context?: PublishContext },
options: { publish: boolean; context?: PublishContext; ownerID?: string },
) {
if (projectors == null) {
throw new Error("No projectors available. Call `SyncEvent.init` to install projectors")
@@ -274,8 +281,6 @@ function process<Def extends Definition>(
throw new Error(`Projector not found for event: ${def.type}`)
}
// idempotent: need to ignore any events already logged
Database.transaction((tx) => {
projector(tx, event.data, event)
@@ -284,6 +289,7 @@ function process<Def extends Definition>(
.values({
aggregate_id: event.aggregateID,
seq: event.seq,
owner_id: options?.ownerID,
})
.onConflictDoUpdate({
target: EventSequenceTable.aggregate_id,
@@ -332,11 +338,11 @@ function process<Def extends Definition>(
})
}
export function replay(event: SerializedEvent, options?: { publish: boolean }) {
export function replay(event: SerializedEvent, options?: { publish: boolean; ownerID?: string }) {
return runtime.runSync((sync) => sync.replay(event, options))
}
export function replayAll(events: SerializedEvent[], options?: { publish: boolean }) {
export function replayAll(events: SerializedEvent[], options?: { publish: boolean; ownerID?: string }) {
return runtime.runSync((sync) => sync.replayAll(events, options))
}
@@ -348,6 +354,16 @@ export function remove(aggregateID: string) {
return runtime.runSync((sync) => sync.remove(aggregateID))
}
export function claim(aggregateID: string, ownerID: string) {
Database.use((db) =>
db
.update(EventSequenceTable)
.set({ owner_id: ownerID })
.where(eq(EventSequenceTable.aggregate_id, aggregateID))
.run(),
)
}
export function payloads() {
return registry
.entries()