UNPKG

@actyx/sdk

Version:
503 lines (433 loc) 14 kB
/* * Actyx SDK: Functions for writing distributed apps * deployed on peer-to-peer networks, without any servers. * * Copyright (C) 2021 Actyx AG */ import { contramap, Ord } from 'fp-ts/lib/Ord' import { Ord as OrdString } from 'fp-ts/lib/string' import { Ord as OrdNumber } from 'fp-ts/lib/number' import { Ordering } from 'fp-ts/lib/Ordering' import { OffsetMap } from './offsetMap' import { Tags } from './tags' /** * An Actyx source id. * @public */ export type NodeId = string const mkNodeId = (text: string): NodeId => text as NodeId const randomBase58: (digits: number) => string = (digits: number) => { const base58 = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'.split('') let result = '' let char while (result.length < digits) { char = base58[(Math.random() * 57) >> 0] result += char } return result } /** * `SourceId` associated functions. * @public */ export const NodeId = { /** * Creates a NodeId from a string */ of: mkNodeId, /** * Creates a random SourceId with the given number of digits */ random: (digits?: number) => mkNodeId(randomBase58(digits || 11)), streamNo: (nodeId: NodeId, num: number) => nodeId + '-' + num, } /** * An Actyx stream id. * @public */ export type StreamId = string const mkStreamId = (text: string): StreamId => text as StreamId /** * `SourceId` associated functions. * @public */ export const StreamId = { /** * Creates a StreamId from a string */ of: mkStreamId, /** * Creates a random StreamId off a random NodeId. */ random: () => NodeId.streamNo(mkNodeId(randomBase58(11)), Math.floor(Math.random() * 100)), } /** * An Actyx app id. * @public */ export type AppId = string const mkAppId = (text: string): AppId => text as AppId /** * `AppId` associated functions. * @public */ export const AppId = { /** * Creates a AppId from a string */ of: mkAppId, } /** * Lamport timestamp, cf. https://en.wikipedia.org/wiki/Lamport_timestamp * @public */ export type Lamport = number const mkLamport = (value: number): Lamport => value as Lamport /** @public */ export const Lamport = { of: mkLamport, zero: mkLamport(0), } /** Offset within an Actyx event stream. * @public */ export type Offset = number const mkOffset = (n: number): Offset => n as Offset /** Functions related to Offsets. * @public */ export const Offset = { of: mkOffset, zero: mkOffset(0), /** * A value that is below any valid Offset */ min: mkOffset(-1), /** * A value that is above any valid Offset */ max: mkOffset(Number.MAX_SAFE_INTEGER), } /** Timestamp (UNIX epoch), MICROseconds resolution. * @public */ export type Timestamp = number const mkTimestamp = (time: number): Timestamp => time as Timestamp const formatTimestamp = (timestamp: Timestamp): string => new Date(timestamp / 1000).toISOString() const secondsPerDay = 24 * 60 * 60 /** Helper functions for making sense of and converting Timestamps. * @public */ export const Timestamp = { of: mkTimestamp, zero: mkTimestamp(0), maxSafe: mkTimestamp(Number.MAX_SAFE_INTEGER), now: (now?: number) => mkTimestamp((now || Date.now()) * 1e3), format: formatTimestamp, toSeconds: (value: Timestamp) => value / 1e6, toMilliseconds: (value: Timestamp): Milliseconds => Milliseconds.of(value / 1e3), toDate: (value: Timestamp): Date => new Date(value / 1e3), fromDate: (date: Date): Timestamp => mkTimestamp(date.valueOf() * 1e3), fromDays: (value: number) => Timestamp.fromSeconds(secondsPerDay * value), fromSeconds: (value: number) => mkTimestamp(value * 1e6), fromMilliseconds: (value: number) => mkTimestamp(value * 1e3), min: (...values: Timestamp[]) => mkTimestamp(Math.min(...values)), max: (values: Timestamp[]) => mkTimestamp(Math.max(...values)), } /** Some number of milliseconds. * @public */ export type Milliseconds = number const mkMilliseconds = (time: number): Milliseconds => time as Milliseconds /** Helper functions for making sense of and converting Milliseconds. * @public */ export const Milliseconds = { of: mkMilliseconds, fromDate: (date: Date): Milliseconds => mkMilliseconds(date.valueOf()), zero: mkMilliseconds(0), now: (now?: number): Milliseconds => mkMilliseconds(now || Date.now()), toSeconds: (value: Milliseconds): number => value / 1e3, toTimestamp: (value: Milliseconds): Timestamp => Timestamp.of(value * 1e3), fromSeconds: (value: number) => mkMilliseconds(value * 1e3), fromMinutes: (value: number) => mkMilliseconds(value * 1e3 * 60), // Converts millis or micros to millis // Note: This is a stopgap until we fixed once and for all this mess. fromAny: (value: number): Milliseconds => { const digits = Math.floor(Math.abs(value)).toString().length return Milliseconds.of(digits <= 13 ? value : value / 1e3) }, } /** * Triple that Actyx events are sorted and identified by. * * @public */ export type EventKey = { // This is not using t.typeof, so that public API has no io-ts dep lamport: Lamport offset: Offset stream: StreamId } const zeroKey: EventKey = { lamport: Lamport.zero, // Cannot use empty source id, store rejects. stream: NodeId.of('!'), offset: Offset.of(0), } const keysEqual = (a: EventKey, b: EventKey): boolean => a.lamport === b.lamport && a.stream === b.stream const keysCompare = (a: EventKey, b: EventKey): Ordering => { const lamportOrder = OrdNumber.compare(a.lamport, b.lamport) if (lamportOrder !== 0) { return lamportOrder } return OrdString.compare(a.stream, b.stream) } /** * Order for event keys * * Order is [lamport, streamId, psn]. Event keys are considered equal when `lamport`, * `streamId` and `psn` are equal. */ const ordEventKey: Ord<EventKey> = { equals: keysEqual, compare: keysCompare, } const formatEventKey = (key: EventKey): string => `${key.lamport}/${key.stream}` /** Functions related to EventKey. * @public */ export const EventKey = { zero: zeroKey, ord: ordEventKey, format: formatEventKey, } /** Generic Metadata attached to every event. * @public */ export type Metadata = { // Was this event written by the very node we are running on? isLocalEvent: boolean // Tags belonging to the event. tags: string[] // Time since Unix Epoch **in Microseconds**! timestampMicros: Timestamp // Convert the Timestamp to a standard JS Date object. timestampAsDate: () => Date // Lamport timestamp of the event. Cf. https://en.wikipedia.org/wiki/Lamport_timestamp lamport: Lamport // A unique identifier for the event. // Every event has exactly one eventId which is unique to it, guaranteed to not collide with any other event. // Events are *sorted* based on the eventId by Actyx: For a given event, all later events also have a higher eventId according to simple string-comparison. eventId: string // App id of the event appId: AppId // Stream this event belongs to stream: StreamId // Offset of this event inside its stream offset: Offset } /** Max length of a lamport timestamp as string. @internal */ export const maxLamportLength = String(Number.MAX_SAFE_INTEGER).length /** Anthing that has metadata. @internal */ export type HasMetadata = { timestamp: Timestamp lamport: Lamport stream: StreamId tags: string[] offset: Offset appId: AppId } /** Make a function that makes metadata from an Event as received over the wire. @internal */ export const toMetadata = (sourceId: string) => (ev: HasMetadata): Metadata => ({ isLocalEvent: ev.stream === sourceId, tags: ev.tags, timestampMicros: ev.timestamp, timestampAsDate: Timestamp.toDate.bind(null, ev.timestamp), lamport: ev.lamport, appId: ev.appId, eventId: String(ev.lamport).padStart(maxLamportLength, '0') + '/' + ev.stream, stream: ev.stream, offset: ev.offset, }) /** * Cancel an ongoing aggregation (the provided callback will stop being called). * @public */ export type CancelSubscription = () => void /** * Allows you to register actions for when event emission has completed. * @public */ export type PendingEmission = { // Add another callback; if emission has already completed, the callback will be executed straight-away. subscribe: (whenEmitted: (meta: Metadata[]) => void) => void // Convert to a Promise which resolves once emission has completed. toPromise: () => Promise<Metadata[]> } /** An event with tags attached. * @public */ export type TaggedEvent = { tags: string[] event: unknown } /** A typed event with tags attached. * @public */ export interface TaggedTypedEvent<E = unknown> extends TaggedEvent { readonly tags: string[] readonly event: E withTags<E1>(tags: Tags<E1> & (E extends E1 ? unknown : never)): TaggedTypedEvent<E> } /** An event with its metadata. * @public */ export type ActyxEvent<E = unknown> = { meta: Metadata payload: E } /** Things related to ActyxEvent. * @public */ export const ActyxEvent = { // TODO: Maybe improve this by just comparing the lamport -> stream combo ord: contramap((e: ActyxEvent) => e.meta.eventId)(OrdString), } /** * A raw Actyx event to be emitted by the TestEventStore, as if it really arrived from the outside. * @public */ export type TestEvent = { offset: number stream: string timestamp: Timestamp lamport: Lamport tags: string[] payload: unknown } /** * A chunk of events, with lower and upper bound. * A call to `queryKnownRange` with the included bounds is guaranteed to return exactly the contained set of events. * A call to `subscribe` with the included `lowerBound`, however, may find new events from sources not included in the bounds. * * @public */ export type EventChunk = { /** The event data. Sorting depends on the request which produced this chunk. */ events: ActyxEvent[] /** The lower bound of the event chunk, independent of its sorting in memory. */ lowerBound: OffsetMap /** The upper bound of the event chunk, independent of its sorting in memory. */ upperBound: OffsetMap } /** Options used when creating a new `Actyx` instance. * @public */ export type ActyxOpts = { /** Host of the Actxy service. This defaults to localhost and should stay localhost in almost all cases. */ actyxHost?: string /** API port of the Actyx service. Defaults to 4454. */ actyxPort?: number /** Hook, when the connection to the store is closed */ onConnectionLost?: () => void /** * Hook, when the connection to the store has been established. */ onConnectionEstablished?: () => void } /** * Test tool. * @beta */ export type TimeInjector = (tags: string[], events: unknown) => Timestamp /** Options used when creating a new TEST `Actyx` instance. * @public */ export type ActyxTestOpts = { /** Local node id to use * @public */ nodeId?: NodeId /** Install the given time source for test purposes * @beta */ timeInjector?: TimeInjector } /** Manifest describing an Actyx application. Used for authorizing API access. * @public */ export type AppManifest = { /** * Structured application id. * For testing and development purposes, you can always pass 'com.example.somestring' * For production, you will buy a license from Actyx for your specific app id like com.my-company.my-app. */ appId: string /** Arbitrary string describing the app. */ displayName: string /** Arbitrary version string */ version: string /** Manifest signature, if it’s not an example app. */ signature?: string } /** * Sort order for persisted events. * @public */ export enum EventsSortOrder { /** Strictly ascending, meaning events are strictly ordered by eventId. */ Ascending = 'asc', /** Strictly descending, meaning events are strictly ordered by eventId, reverse. */ Descending = 'desc', /** Ascending per stream, meaning between different streams there is no specific order guaranteed. */ StreamAscending = 'stream-asc', } /** AQL message describing the offsets at the current state of the response stream. * @beta */ export type AqlOffsetsMsg = { type: 'offsets' offsets: OffsetMap } /** AQL message conveying a raw event or a record created by mapping an event via SELECT. * @beta */ export type AqlEventMessage = { /** A simply-mapped event */ type: 'event' /** Payload data generated by the AQL query via SELECT statements. */ payload: unknown /** Metadata of the event that yielded this record. */ meta: Metadata } /** AQL diagnostic output describing an error that occured during query evaluation. * @beta */ export type AqlDiagnosticMessage = { /** A problem occured */ type: 'diagnostic' severity: string message: string } /** Future versions of AQL will know additional response message types. * @beta */ export type AqlFutureCompat = { /** Consult AQL documentation to find out about future available types. */ type: Exclude<'event' | 'offsets' | 'diagnostic', string> payload: unknown /** Consult AQL documentation to find out about future metadata contents. */ meta: Record<string, unknown> } /** Response message returned by running AQL query. * @beta */ export type AqlResponse = AqlEventMessage | AqlOffsetsMsg | AqlDiagnosticMessage | AqlFutureCompat /** * Status of another node as observed by the local node * @public */ export enum NodeStatus { /** * Replicates all streams within at most two gossip cycles */ LowLatency = 'LowLatency', /** * Replicates all streams within at most five gossip cycles */ HighLatency = 'HighLatency', /** * Replicates at least half of all streams within five gossip cycles */ PartiallyWorking = 'PartiallyWorking', /** * Replicates less than half of all streams within five gossip cycles * * This state either means that the node is disconnected from our part of * the network or that it has been shut down or decommissioned. Old streams * will stay in the swarm in this state. */ NotWorking = 'NotWorking', }