diff --git a/.gitignore b/.gitignore index 5254378..7bcb98a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,9 @@ -SCHEMA.md - dist +**/*.tsbuildinfo + node_modules + .env db.sqlite + +.DS_Store diff --git a/examples/02-client.ts b/examples/02-client.ts index e4c0201..0bf9b76 100644 --- a/examples/02-client.ts +++ b/examples/02-client.ts @@ -1,12 +1,12 @@ import { NodeHttpClient, NodeRuntime } from '@effect/platform-node' import { Cause, Config, Effect, Layer, References, Stream } from 'effect' import { DevTools } from 'effect/unstable/devtools' -import { MatrixApi, MatrixAuth, MatrixClient, MatrixConfig, Store } from 'mxfx' +import { MatrixApi, MatrixAuth, MatrixClient, MatrixConfig } from 'mxfx' import { endpoints } from 'mxfx/api' import type { RoomId } from 'mxfx/branded' type SyncFrame = typeof endpoints.getSyncV3ResponseSchema.Type -const lastPredicate = (frame: Stream.Stream) => +const pingPredicate = (frame: Stream.Stream) => frame.pipe( Stream.flatMap(sync => Stream.fromIterable(Object.entries(sync.rooms?.join ?? {}))), Stream.flatMap(([roomId, room]) => @@ -22,7 +22,7 @@ const lastPredicate = (frame: Stream.Stream) => return ( typeof content.body === 'string' && - content.body.trim().startsWith('!last') && + content.body.trim().startsWith('!ping') && (content.msgtype === 'm.text' || content.msgtype === 'm.notice' || content.msgtype === 'm.emote') ) }), @@ -32,20 +32,15 @@ const lastPredicate = (frame: Stream.Stream) => const program = Effect.gen(function* () { const api = yield* MatrixApi.MatrixApi const client = yield* MatrixClient.MatrixClient - const store = yield* Store.Store const { userId, deviceId, isGuest } = yield* endpoints.getAccountWhoami().pipe(Effect.andThen(api.execute)) yield* Effect.logDebug({ userId, deviceId, isGuest }) - yield* client.onEvent({ predicate: lastPredicate }, event => + yield* client.onEvent({ predicate: pingPredicate }, event => Effect.gen(function* () { - const timeline = yield* store.getRoomTimeline(event.roomId) - const lastMessages = timeline.filter(e => e.type === 'm.room.message' && e.sender !== userId).slice(-5) - yield* Effect.log(lastMessages) - yield* endpoints .putRoomsSendV3({ - content: { msgtype: 'm.text', body: lastMessages.map(x => `${x.content['body']}`).join('\n\n') }, + content: { msgtype: 'm.text', body: 'pong' }, eventType: 'm.room.message', roomId: event.roomId, }) diff --git a/packages/mxfx/package.json b/packages/mxfx/package.json index 85a88c4..79ae709 100644 --- a/packages/mxfx/package.json +++ b/packages/mxfx/package.json @@ -18,6 +18,7 @@ "./config": "./src/config/index.ts", "./vault": "./src/vault/index.ts", "./branded": "./src/branded/index.ts", + "./kv": "./src/kv/index.ts", ".": "./src/index.ts" }, "scripts": { diff --git a/packages/mxfx/src/client/client.ts b/packages/mxfx/src/client/client.ts index e75e063..de2b6a2 100644 --- a/packages/mxfx/src/client/client.ts +++ b/packages/mxfx/src/client/client.ts @@ -2,19 +2,17 @@ import { Effect, Layer, Context, Option, PubSub, Duration, Stream } from 'effect import { endpoints, MatrixApi } from '../api/index.ts' import type { RoomId } from '../branded/index.ts' -import { Store } from '../store/index.ts' -import { Reducers } from './sync/reducer.ts' +import { Kv } from '../kv/index.ts' const make = Effect.gen(function* () { const matrixApi = yield* MatrixApi - const reducers = yield* Reducers - const store = yield* Store + const kv = yield* Kv const syncHub = yield* PubSub.unbounded() const syncStream = Stream.fromPubSub(syncHub) - const syncOnce = store.getNextBatch().pipe( + const syncOnce = kv.getString('syncToken').pipe( Effect.flatMap(nextBatch => endpoints .getSyncV3({ @@ -41,7 +39,7 @@ const make = Effect.gen(function* () { .pipe( Effect.andThen(matrixApi.execute), Effect.tap(syncResponse => PubSub.publish(syncHub, syncResponse)), - Effect.tap(syncResponse => Effect.forEach(reducers, reducer => reducer.reduce(syncResponse))), + Effect.andThen(syncResponse => kv.set('syncToken', syncResponse.nextBatch)), ), ), ) @@ -72,4 +70,4 @@ export class MatrixClient extends Context.Service< } >()('mxfx/client') {} -export const layerMatrixClient = Layer.effect(MatrixClient, make).pipe(Layer.provideMerge(Reducers.layer)) +export const layerMatrixClient = Layer.effect(MatrixClient, make) diff --git a/packages/mxfx/src/client/sync/next-batch-reducer.ts b/packages/mxfx/src/client/sync/next-batch-reducer.ts deleted file mode 100644 index a4e8b8c..0000000 --- a/packages/mxfx/src/client/sync/next-batch-reducer.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { Effect } from 'effect' - -import { Store } from '../../store/index.ts' -import type { ReducerShape } from './reducer.ts' - -export const nextBatchReducer: ReducerShape = { - reduce: sync => - Effect.gen(function* () { - const store = yield* Store - yield* store.setNextBatch(sync.nextBatch) - }), -} diff --git a/packages/mxfx/src/client/sync/reducer.ts b/packages/mxfx/src/client/sync/reducer.ts deleted file mode 100644 index 49daeb1..0000000 --- a/packages/mxfx/src/client/sync/reducer.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { Context, Data, Layer, type Effect } from 'effect' - -import type { getSyncV3ResponseSchema } from '../../api/endpoints/index.ts' -import { nextBatchReducer } from './next-batch-reducer.ts' -import { roomStateReducer } from './room-state-reducer.ts' -import { roomTimelineReducer } from './room-timeline-reducer.ts' - -export class ReduceError extends Data.TaggedError('mxfx/ReduceError')<{ - message: string - cause?: unknown -}> {} - -export type ReducerShape = { - reduce: (sync: typeof getSyncV3ResponseSchema.Type) => Effect.Effect -} - -export class Reducers extends Context.Service>()('mxfx/client/reducers') { - static layer = Layer.succeed(Reducers, [roomStateReducer, nextBatchReducer, roomTimelineReducer]) -} diff --git a/packages/mxfx/src/client/sync/room-state-reducer.ts b/packages/mxfx/src/client/sync/room-state-reducer.ts deleted file mode 100644 index c70d760..0000000 --- a/packages/mxfx/src/client/sync/room-state-reducer.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { Effect } from 'effect' - -import { Store } from '../../store/index.ts' -import type { ReducerShape } from './reducer.ts' - -// TODO extract to helpers or somn like it -export const objectEntries = >(obj: O) => - Object.entries(obj) as { - [K in keyof O]-?: K extends string ? [K, O[K]] : never - }[keyof O][] - -// TODO: this room state reducer only supports useStateAfter and joined rooms -export const roomStateReducer: ReducerShape = { - reduce: sync => - Effect.gen(function* () { - if (!sync.rooms?.join) return Effect.void - const store = yield* Store - - for (const [roomId, room] of objectEntries(sync.rooms.join)) { - if (!room.stateAfter?.events) continue - - yield* store.applyState(room.stateAfter.events.map(event => ({ ...event, roomId }))) - } - }), -} diff --git a/packages/mxfx/src/client/sync/room-timeline-reducer.ts b/packages/mxfx/src/client/sync/room-timeline-reducer.ts deleted file mode 100644 index 28dff75..0000000 --- a/packages/mxfx/src/client/sync/room-timeline-reducer.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { Effect } from 'effect' - -import { Store } from '../../store/index.ts' -import type { ReducerShape } from './reducer.ts' - -// TODO extract to helpers or somn like it -export const objectEntries = >(obj: O) => - Object.entries(obj) as { - [K in keyof O]-?: K extends string ? [K, O[K]] : never - }[keyof O][] - -// TODO: this room state reducer only supports -export const roomTimelineReducer: ReducerShape = { - reduce: sync => - Effect.gen(function* () { - if (!sync.rooms?.join) return Effect.void - const store = yield* Store - - for (const [roomId, room] of objectEntries(sync.rooms.join)) { - if (!room.timeline?.events) continue - - yield* store.appendTimelineEvents(room.timeline.events.map(event => ({ ...event, roomId }))) - } - }), -} diff --git a/packages/mxfx/src/index.ts b/packages/mxfx/src/index.ts index ae5f8d6..8e7e59e 100644 --- a/packages/mxfx/src/index.ts +++ b/packages/mxfx/src/index.ts @@ -3,5 +3,5 @@ export * as MatrixApi from './api/index.ts' export * as MatrixClient from './client/index.ts' export * as MatrixConfig from './config/index.ts' export * as Vault from './vault/index.ts' -export * as Store from './store/index.ts' +export * as Kv from './kv/index.ts' export * from './branded/index.ts' diff --git a/packages/mxfx/src/kv/in-memory-kv.ts b/packages/mxfx/src/kv/in-memory-kv.ts new file mode 100644 index 0000000..e88b3df --- /dev/null +++ b/packages/mxfx/src/kv/in-memory-kv.ts @@ -0,0 +1,16 @@ +import { Effect, Schema } from 'effect' + +import { KvGetError, type KvShape } from './kv.ts' + +export const makeInMemoryKv = (): KvShape => { + const kv = new Map() + + return { + set: (key: string, value: string) => Effect.sync(() => kv.set(key, value)), + getString: (key: string) => + Effect.try(() => kv.get(key)).pipe( + Effect.andThen(Schema.decodeUnknownEffect(Schema.OptionFromOptional(Schema.String))), + Effect.mapError(e => new KvGetError({ cause: e })), + ), + } +} diff --git a/packages/mxfx/src/kv/index.ts b/packages/mxfx/src/kv/index.ts new file mode 100644 index 0000000..58480c5 --- /dev/null +++ b/packages/mxfx/src/kv/index.ts @@ -0,0 +1,2 @@ +export * from './kv.ts' +export * from './in-memory-kv.ts' diff --git a/packages/mxfx/src/kv/kv.ts b/packages/mxfx/src/kv/kv.ts new file mode 100644 index 0000000..f90c0b8 --- /dev/null +++ b/packages/mxfx/src/kv/kv.ts @@ -0,0 +1,15 @@ +import { Context, Data, Effect, Option } from 'effect' + +import { makeInMemoryKv } from './in-memory-kv.ts' + +export class KvGetError extends Data.TaggedError('mxfx/kv/get-error')<{ + cause?: unknown +}> {} + +export type KvShape = { + set: (key: string, value: string) => Effect.Effect + getString: (key: string) => Effect.Effect, KvGetError> +} + +export const Kv = Context.Reference('mxfx/kv', { defaultValue: makeInMemoryKv }) +export type Kv = typeof Kv diff --git a/packages/mxfx/src/store/in-memory-store.ts b/packages/mxfx/src/store/in-memory-store.ts deleted file mode 100644 index 41d7f1f..0000000 --- a/packages/mxfx/src/store/in-memory-store.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { Effect, Option } from 'effect' - -import type { RoomId } from '../branded/index.ts' -import type { roomEventWithoutRoomId, stateEventWithoutRoomId } from '../schema/index.ts' -import type { StoreShape } from './store.ts' - -export const makeInMemoryStore = (): StoreShape => { - const storage: { - nextBatch: string | undefined - timelines: Map> - state: Map> - } = { nextBatch: undefined, timelines: new Map(), state: new Map() } - - return { - setNextBatch: nextBatch => - Effect.sync(() => { - storage.nextBatch = nextBatch - }).pipe(Effect.andThen(Effect.log('Writing', nextBatch))), - - getNextBatch: () => Effect.sync(() => Option.fromUndefinedOr(storage.nextBatch)).pipe(Effect.tap(x => Effect.log(x))), - - appendTimelineEvents: events => - Effect.sync(() => { - events.forEach(event => { - const roomTimeline: Array = storage.timelines.get(event.roomId) ?? [] - roomTimeline.push(event) - storage.timelines.set(event.roomId, roomTimeline) - }) - }), - getRoomTimeline: (roomId: RoomId) => - Effect.sync(() => { - const roomTimeline: ReadonlyArray = [...(storage.timelines.get(roomId) ?? [])] - return roomTimeline - }), - - applyState: events => - Effect.sync(() => { - events.forEach(event => { - const roomState: Map = storage.state.get(event.roomId) ?? new Map() - const key = `${event.type}-${event.stateKey}` - - roomState.set(key, event) - storage.state.set(event.roomId, roomState) - }) - }), - getRoomState: (roomId: RoomId) => - Effect.sync(() => { - const roomState: ReadonlyMap = storage.state.get(roomId) ?? new Map() - return Array.from(roomState.values()) - }), - } -} diff --git a/packages/mxfx/src/store/index.ts b/packages/mxfx/src/store/index.ts deleted file mode 100644 index f803107..0000000 --- a/packages/mxfx/src/store/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './store.ts' -export * from './in-memory-store.ts' diff --git a/packages/mxfx/src/store/store.ts b/packages/mxfx/src/store/store.ts deleted file mode 100644 index 794f3cf..0000000 --- a/packages/mxfx/src/store/store.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { Context, Effect, Option } from 'effect' - -import type { RoomId } from '../branded/index.ts' -import { roomEventWithoutRoomId, stateEventWithoutRoomId } from '../schema/index.ts' -import { makeInMemoryStore } from './in-memory-store.ts' - -export type StoreShape = { - setNextBatch: (nextBatch: string) => Effect.Effect - getNextBatch: () => Effect.Effect> - - appendTimelineEvents: (events: ReadonlyArray) => Effect.Effect - getRoomTimeline: (roomId: RoomId) => Effect.Effect> - - applyState: (events: ReadonlyArray) => Effect.Effect - getRoomState: (roomId: RoomId) => Effect.Effect> -} - -export const Store = Context.Reference('mxfx/store', { defaultValue: makeInMemoryStore }) -export type Store = typeof Store