From d6c96750ed9db4eaed6e1edb836015fb7814af72 Mon Sep 17 00:00:00 2001 From: Mike Christensen Date: Tue, 6 Jun 2023 17:51:20 +0100 Subject: [PATCH] model: add simple mutation registration --- src/Model.test.ts | 28 ++++++++++++++++++++++++++++ src/Model.ts | 29 +++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+) diff --git a/src/Model.test.ts b/src/Model.test.ts index 78475f56..fcfc41c8 100644 --- a/src/Model.test.ts +++ b/src/Model.test.ts @@ -227,5 +227,33 @@ describe('Model', () => { }); }); + it('executes a registered mutation', async ({ streams }) => { + streams.s1.subscribe = vi.fn(); + streams.s2.subscribe = vi.fn(); + const model = new Model('test', { streams, sync: async () => ({ version: 1, data: 'foobar' }) }); + await modelStatePromise(model, ModelState.READY); + + const mutation = vi.fn(); + model.registerMutation('foo', mutation); + expect(mutation).toHaveBeenCalledTimes(0); + await model.mutate<[string, number], void>('foo', 'bar', 123); + expect(mutation).toHaveBeenCalledTimes(1); + expect(mutation).toHaveBeenCalledWith('bar', 123); + }); + + it('fails to register a duplicate mutation', async ({ streams }) => { + streams.s1.subscribe = vi.fn(); + streams.s2.subscribe = vi.fn(); + const model = new Model('test', { streams, sync: async () => ({ version: 1, data: 'foobar' }) }); + await modelStatePromise(model, ModelState.READY); + + const mutation = vi.fn(); + model.registerMutation('foo', mutation); + expect(mutation).toHaveBeenCalledTimes(0); + expect(() => model.registerMutation('foo', mutation)).toThrowError( + `mutation with name 'foo' already registered on model 'test'`, + ); + }); + // TODO disposes of the model on stream failed }); diff --git a/src/Model.ts b/src/Model.ts index 99e2e629..eb37cca2 100644 --- a/src/Model.ts +++ b/src/Model.ts @@ -32,8 +32,15 @@ export enum ModelState { } export type SyncFunc = () => Promise>; + export type UpdateFunc = (state: T, event: Types.Message) => Promise; +export type MutationResult = { + result: R; + expected: Partial; +}; +export type MutationFunc = (...args: T) => Promise>; + export type Streams = { [name: string]: Stream; }; @@ -65,9 +72,12 @@ class Model extends EventEmitter> { private currentData: Versioned; private sync: SyncFunc; + private streams: Streams; private updateFuncs: UpdateFuncs> = {}; + private mutations: Record = {}; + private subscriptions = new Subject(); private subscriptionMap: Map, Subscription> = new Map(); @@ -124,6 +134,25 @@ class Model extends EventEmitter> { this.updateFuncs[stream][event].push(update); } + public registerMutation(name: string, mutation: MutationFunc) { + if (this.mutations[name]) { + throw new Error(`mutation with name '${name}' already registered on model '${this.name}'`); + } + this.mutations[name] = mutation; + } + + public async mutate(name: string, ...args: TArgs): Promise> { + const mutation = this.mutations[name]; + if (!mutation) { + throw new Error(`mutation with name '${name}' not registered on model '${this.name}'`); + } + const result = await mutation(...args); + + // TODO: process the expected result optimistically + + return result; + } + public subscribe(callback: StandardCallback) { const subscription = this.subscriptions.subscribe({ next: (message) => callback(null, message),