Skip to content

Commit

Permalink
feat: built-in data versioning
Browse files Browse the repository at this point in the history
  • Loading branch information
KnorpelSenf committed Oct 15, 2024
1 parent 5a13b43 commit 6a543d0
Show file tree
Hide file tree
Showing 2 changed files with 98 additions and 67 deletions.
73 changes: 6 additions & 67 deletions src/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,10 @@ import {
ReplayEngine,
type ReplayState,
} from "./engine.ts";
import { type ConversationStorage, uniformStorage } from "./storage.ts";
export { type Conversation } from "./conversation.ts";

// TODO: merge some of these
const internalRecursionDetection = Symbol("conversations.recursion");
const internalMutableState = Symbol("conversations.data");
const internalIndex = Symbol("conversations.builders");
Expand All @@ -35,74 +37,11 @@ export interface ApiBaseData {
options?: ApiClientOptions;
}

type MaybePromise<T> = T | Promise<T>;
export type ConversationStorage<C extends Context> =
| ConversationContextStorage<C>
| ConversationKeyStorage<C>;
export interface ConversationContextStorage<C extends Context> {
adapter?: never;

read(ctx: C): MaybePromise<ConversationData | undefined>;
write(ctx: C, state: ConversationData): MaybePromise<void>;
delete(ctx: C): MaybePromise<void>;
}
export interface ConversationKeyStorage<C extends Context> {
getStorageKey(ctx: C): string | undefined;
adapter: {
read(key: string): MaybePromise<ConversationData | undefined>;
write(key: string, state: ConversationData): MaybePromise<void>;
delete(key: string): MaybePromise<void>;
};

read?: never;
write?: never;
delete?: never;
}
export interface ConversationOptions<OC extends Context, C extends Context> {
storage?: ConversationStorage<OC>;
storage?: ConversationStorage<OC, ConversationData>;
plugins?: Middleware<C>[];
onEnter?(id: string): MaybePromise<unknown>;
onExit?(id: string): MaybePromise<unknown>;
}
function defaultStorage<C extends Context>(): ConversationKeyStorage<C> {
const store = new Map<string, ConversationData>();
return {
getStorageKey: (ctx) => ctx.chatId?.toString(),
adapter: {
read: (key) => store.get(key),
write: (key, state) => void store.set(key, state),
delete: (key) => void store.delete(key),
},
};
}
function uniformStorage<C extends Context>(
storage: ConversationStorage<C> = defaultStorage(),
) {
if ("getStorageKey" in storage) {
return (ctx: C) => {
const key = storage.getStorageKey(ctx);
return key === undefined
? {
read: () => undefined,
write: () => undefined,
delete: () => undefined,
}
: {
read: () => storage.adapter.read(key),
write: (state: ConversationData) =>
storage.adapter.write(key, state),
delete: () => storage.adapter.delete(key),
};
};
} else {
return (ctx: C) => {
return {
read: () => storage.read(ctx),
write: (state: ConversationData) => storage.write(ctx, state),
delete: () => storage.delete(ctx),
};
};
}
onEnter?(id: string): unknown | Promise<unknown>;
onExit?(id: string): unknown | Promise<unknown>;
}
export interface ConversationData {
[id: string]: ConversationState[];
Expand Down Expand Up @@ -407,7 +346,7 @@ export function createConversation<OC extends Context, C extends Context>(
maxMillisecondsToWait,
});
const onExit = ctx[internalExitHandler] as
| ((name: string) => MaybePromise<unknown>)
| ((name: string) => unknown | Promise<unknown>)
| undefined;
const onHalt = async () => {
await onExit?.(id);
Expand Down
92 changes: 92 additions & 0 deletions src/storage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import type { Context } from "./deps.deno.ts";

const PLUGIN_DATA_VERSION = 0;
export interface VersionedState<S> {
version: [typeof PLUGIN_DATA_VERSION, string | number];
state: S;
}

export type MaybePromise<T> = T | Promise<T>;
export type ConversationStorage<C extends Context, S> =
| ConversationContextStorage<C, S>
| ConversationKeyStorage<C, S>;
export interface ConversationContextStorage<C extends Context, S> {
version?: string | number;

adapter?: never;

read(ctx: C): MaybePromise<VersionedState<S> | undefined>;
write(ctx: C, state: VersionedState<S>): MaybePromise<void>;
delete(ctx: C): MaybePromise<void>;
}
export interface ConversationKeyStorage<C extends Context, S> {
version?: string | number;

getStorageKey(ctx: C): string | undefined;
adapter: {
read(key: string): MaybePromise<VersionedState<S> | undefined>;
write(key: string, state: VersionedState<S>): MaybePromise<void>;
delete(key: string): MaybePromise<void>;
};

read?: never;
write?: never;
delete?: never;
}

function defaultStorage<C extends Context, S>(): ConversationKeyStorage<C, S> {
const store = new Map<string, VersionedState<S>>();
return {
getStorageKey: (ctx) => ctx.chatId?.toString(),
adapter: {
read: (key) => store.get(key),
write: (key, state) => void store.set(key, state),
delete: (key) => void store.delete(key),
},
};
}
export function uniformStorage<C extends Context, S>(
storage: ConversationStorage<C, S> = defaultStorage(),
) {
const version = storage.version ?? 0;
function addVersion(state: S): VersionedState<S> {
return { version: [PLUGIN_DATA_VERSION, version], state };
}
function migrate(data?: VersionedState<S>): S | undefined {
if (data === undefined) return undefined;
const [pluginVersion, dataVersion] = data.version;
if (dataVersion !== version) return undefined;
if (pluginVersion !== PLUGIN_DATA_VERSION) {
// In the future, we might want to migrate the data from an old
// plugin version to a new one here.
return undefined;
}
return data.state;
}

if ("getStorageKey" in storage) {
return (ctx: C) => {
const key = storage.getStorageKey(ctx);
return key === undefined
? {
read: () => undefined,
write: () => undefined,
delete: () => undefined,
}
: {
read: async () => migrate(await storage.adapter.read(key)),
write: (state: S) =>
storage.adapter.write(key, addVersion(state)),
delete: () => storage.adapter.delete(key),
};
};
} else {
return (ctx: C) => {
return {
read: async () => migrate(await storage.read(ctx)),
write: (state: S) => storage.write(ctx, addVersion(state)),
delete: () => storage.delete(ctx),
};
};
}
}

0 comments on commit 6a543d0

Please sign in to comment.