@actyx/sdk
Version:
Actyx SDK
254 lines (229 loc) • 7.44 kB
text/typescript
/*
* Actyx SDK: Functions for writing distributed apps
* deployed on peer-to-peer networks, without any servers.
*
* Copyright (C) 2021 Actyx AG
*/
/*
* Actyx Pond: A TypeScript framework for writing distributed apps
* deployed on peer-to-peer networks, without any servers.
*
* Copyright (C) 2020 Actyx AG
*/
import * as t from 'io-ts'
import { Observable, lastValueFrom, defer, combineLatest, of } from '../../node_modules/rxjs'
import {
map,
first,
shareReplay,
take,
concatMap,
defaultIfEmpty,
} from '../../node_modules/rxjs/operators'
import {
DoPersistEvents,
DoQuery,
DoSubscribe,
DoSubscribeMonotonic,
EventStore,
RequestOffsets,
} from '../internal_common/eventStore'
import log from '../internal_common/log'
import { EventsSortOrder, Timestamp, Where } from '../types'
import { validateOrThrow } from '../util'
import { MultiplexedWebsocket } from './multiplexedWebsocket'
import { SubscriptionSet, SubscriptionSetIO } from './subscription'
import {
AllEventsSortOrder,
AllEventsSortOrders,
Event,
Events,
OffsetMapWithDefault,
PersistedEventsSortOrder,
PersistedEventsSortOrders,
UnstoredEvents,
} from './types'
export const enum RequestTypes {
SourceId = '/ax/events/getSourceId',
Present = '/ax/events/requestPresent',
PersistedEvents = '/ax/events/requestPersistedEvents',
AllEvents = '/ax/events/requestAllEvents',
PersistEvents = '/ax/events/persistEvents',
HighestSeen = '/ax/events/highestSeenOffsets',
Connectivity = '/ax/events/requestConnectivity',
}
const EventKeyIO = t.readonly(
t.type({
lamport: t.number,
psn: t.number,
sourceId: t.string,
}),
)
const EventKeyOrNull = t.union([t.null, EventKeyIO])
const ValueOrLimit = t.union([t.number, t.literal('min'), t.literal('max')])
export type ValueOrLimit = t.TypeOf<typeof ValueOrLimit>
export const PersistedEventsRequest = t.readonly(
t.type({
minEventKey: EventKeyOrNull,
fromPsnsExcluding: OffsetMapWithDefault,
toPsnsIncluding: OffsetMapWithDefault,
subscriptionSet: SubscriptionSetIO,
sortOrder: PersistedEventsSortOrder,
count: ValueOrLimit,
}),
)
export type PersistedEventsRequest = t.TypeOf<typeof PersistedEventsRequest>
export const AllEventsRequest = t.readonly(
t.type({
fromPsnsExcluding: OffsetMapWithDefault,
minEventKey: EventKeyOrNull,
toPsnsIncluding: OffsetMapWithDefault,
subscriptionSet: SubscriptionSetIO,
sortOrder: AllEventsSortOrder,
count: ValueOrLimit,
}),
)
export type AllEventsRequest = t.TypeOf<typeof AllEventsRequest>
export const PersistEventsRequest = t.readonly(t.type({ events: UnstoredEvents }))
export type PersistEventsRequest = t.TypeOf<typeof PersistEventsRequest>
export const getSourceId = (multiplexedWebsocket: MultiplexedWebsocket): Promise<string> =>
lastValueFrom(
multiplexedWebsocket
.request(RequestTypes.SourceId)
.pipe(map(validateOrThrow(t.string)), first()),
)
const toSubscriptionSet = (where: Where<unknown>): SubscriptionSet => {
const wire = where.toV1WireFormat()
return {
type: 'tags',
subscriptions: Array.isArray(wire) ? wire : [wire],
}
}
const toPersistedSortOrder = (o: EventsSortOrder) => {
switch (o) {
case EventsSortOrder.Ascending:
return PersistedEventsSortOrders.EventKey
case EventsSortOrder.Descending:
return PersistedEventsSortOrders.ReverseEventKey
case EventsSortOrder.StreamAscending:
return PersistedEventsSortOrders.Unsorted
}
}
const convertV1toV2 = (e: Event) => {
const tags = [...e.tags]
tags.push('semantics:' + e.semantics)
tags.push('fish_name:' + e.name)
return {
appId: 'com.unknown',
stream: e.sourceId,
tags,
payload: e.payload,
timestamp: e.timestamp,
lamport: e.lamport,
offset: e.psn,
}
}
export class WebsocketEventStore implements EventStore {
private _present: Observable<OffsetMapWithDefault>
private _highestSeen: Observable<OffsetMapWithDefault>
constructor(private readonly multiplexer: MultiplexedWebsocket, readonly sourceId: string) {
this._present = defer(() =>
this.multiplexer
.request(RequestTypes.Present)
.pipe(map(validateOrThrow(OffsetMapWithDefault)), shareReplay(1)),
)
this._highestSeen = defer(() =>
this.multiplexer
.request(RequestTypes.HighestSeen)
.pipe(map(validateOrThrow(OffsetMapWithDefault)), shareReplay(1)),
)
}
offsets: RequestOffsets = () =>
lastValueFrom(combineLatest([this._present, this._highestSeen]).pipe(take(1))).then(
([pres, _hi]) => {
// FIXME: Calculate toReplicate from highestSeen
return { present: pres.psns, toReplicate: {} }
},
)
queryUnchecked = () => {
throw new Error('not implemented for V1')
}
subscribeUnchecked = () => {
throw new Error('not implemented for V1')
}
query: DoQuery = (lowerBound, upperBound, query, sortOrder) => {
if (typeof query === 'string') {
throw new Error('No AQL support in V1')
}
return this.multiplexer
.request(
RequestTypes.PersistedEvents,
PersistedEventsRequest.encode({
fromPsnsExcluding: { psns: lowerBound, default: 'min' },
toPsnsIncluding: { psns: upperBound, default: 'min' },
subscriptionSet: toSubscriptionSet(query),
minEventKey: null,
sortOrder: toPersistedSortOrder(sortOrder),
count: 'max',
}),
)
.pipe(concatMap(validateOrThrow(Events)), map(convertV1toV2))
}
subscribe: DoSubscribe = (lowerBound, query) => {
if (typeof query === 'string') {
throw new Error('No AQL support in V1')
}
return this.multiplexer
.request(
RequestTypes.AllEvents,
AllEventsRequest.encode({
fromPsnsExcluding: { psns: lowerBound, default: 'min' },
toPsnsIncluding: { psns: {}, default: 'max' },
subscriptionSet: toSubscriptionSet(query),
minEventKey: null,
sortOrder: AllEventsSortOrders.Unsorted,
count: 'max',
}),
)
.pipe(concatMap(validateOrThrow(Events)), map(convertV1toV2))
}
subscribeMonotonic: DoSubscribeMonotonic = () => {
throw new Error('ActyxOS v1 does not support subscribeMonotonic')
}
persistEvents: DoPersistEvents = (eventsV2) => {
if (eventsV2.length === 0) {
return of()
}
const events = eventsV2.map((e) => ({
...e,
semantics: '_t_',
name: '_t_',
timestamp: Timestamp.now(),
}))
return this.multiplexer
.request(RequestTypes.PersistEvents, PersistEventsRequest.encode({ events }))
.pipe(
map(validateOrThrow(t.type({ events: Events }))),
map(({ events: persistedEvents }) => {
if (events.length !== persistedEvents.length) {
log.ws.error(
'PutEvents: Sent %d events, but only got %d PSNs back.',
events.length,
persistedEvents.length,
)
return []
}
return events.map((ev, idx) => ({
appId: 'com.unknown',
stream: this.sourceId,
tags: ev.tags,
payload: ev.payload,
timestamp: persistedEvents[idx].timestamp,
lamport: persistedEvents[idx].lamport,
offset: persistedEvents[idx].psn,
}))
}),
defaultIfEmpty([]),
)
}
}