UNPKG

scrivito

Version:

Scrivito is a professional, yet easy to use SaaS Enterprise Content Management Service, built for digital agencies and medium to large businesses. It is completely maintenance-free, cost-effective, and has unprecedented performance and security.

238 lines (201 loc) 6.4 kB
import { ScrivitoError } from 'scrivito_sdk/common'; type UnsubscribeFunction = () => void; /** a SubscribeFunction receives a subscriber, which will receive values from the stream. * It returns an UnsubscribeFunction, to cancel the subscription. */ type SubscribeFunction<T> = ( subscriber: Subscriber<T> ) => Subscription | UnsubscribeFunction; export interface Subscriber<T> { next(value: T): void; complete(): void; isClosed(): boolean; } /** a Subscription represents an open ('subscribed') stream. * Close the stream by unsubscribing. */ export interface Subscription { unsubscribe(): void; isClosed(): boolean; } /** A Streamable represents a resource that can be streamed. * * The resource is streamed, by subscribing the Streamable. * The result is a Subscription, which can be used to unsubscribe, * thereby closing the stream. * * Note: It is a very light-weight subset of the Observable pattern, * known from RxJs et. al. */ export class Streamable<T> { /** create a Streamable from the given subscribeFunction */ constructor(private readonly subscribeFunction: SubscribeFunction<T>) {} /** subscribe this Streamable, streaming values into the provided function. */ subscribe( nextOrSubscriber: Partial<Subscriber<T>> | ((value: T) => void) ): Subscription { const intermediary = new Intermediary( typeof nextOrSubscriber === 'object' ? nextOrSubscriber : { next: nextOrSubscriber } ); const subscriptionOrUnsubscribe = this.subscribeFunction(intermediary); intermediary.setUnsubscribeCallback( typeof subscriptionOrUnsubscribe === 'object' ? () => subscriptionOrUnsubscribe.unsubscribe() : subscriptionOrUnsubscribe ); return intermediary; } map<S>(fn: (value: T) => S): Streamable<S> { return new Streamable((subscriber) => this.subscribe({ next: (value: T) => subscriber.next(fn(value)), complete: () => subscriber.complete(), }) ); } filter<S extends T>(test: (value: T) => value is S): Streamable<S>; filter(test: (value: T) => boolean): Streamable<T>; filter<S extends T>(test: (value: T) => value is S): Streamable<S> { return new Streamable((subscriber) => this.subscribe({ next: (value: T) => { if (test(value)) { subscriber.next(value); } }, complete: () => subscriber.complete(), }) ); } /** Returns a Promise that resolves with the final (=last) value of the stream, * when the stream completes. * If the stream is empty (i.e. it completes before emitting a value), * the Promise resolves with undefined. */ toPromise(): Promise<T | undefined> { return new Promise((resolve) => { let lastValue: T | undefined; this.subscribe({ next(value) { lastValue = value; }, complete() { resolve(lastValue); }, }); }); } /** Returns a new Streamable, truncated to the first value. */ takeOne(): Streamable<T> { return new Streamable((subscriber) => { let subscription: null | Subscription = null; subscription = this.subscribe({ next: (value) => { if (subscription) subscription.unsubscribe(); subscriber.next(value); subscriber.complete(); }, complete: () => { subscriber.complete(); }, }); if (subscriber.isClosed()) subscription.unsubscribe(); return subscription; }); } /** Returns a Promise to the first value that the stream emits. * The Promise rejects, if the stream completes before any value is emitted. */ waitForFirst(): Promise<T> { return new Promise((resolve, reject) => { let resolved = false; this.takeOne().subscribe({ next(value) { resolved = true; resolve(value); }, complete() { if (!resolved) reject(new EndOfStreamError()); }, }); }); } /** Transforms this stream, so that it completes, when the passed-in stream * emits its first value or completes. */ takeUntil(until: Streamable<unknown>): Streamable<T> { return new Streamable<T>((subscriber) => { let untilSubscription: Subscription | undefined = undefined; let subscription: Subscription | undefined = undefined; subscription = this.subscribe({ next(value) { subscriber.next(value); }, complete() { completeStream(); }, }); // edge-case: stream that completes immediately if (subscription.isClosed()) return () => undefined; untilSubscription = until.subscribe({ next() { completeStream(); }, complete() { completeStream(); }, }); function completeStream() { subscriber.complete(); cleanup(); } function cleanup() { if (subscription) subscription.unsubscribe(); if (untilSubscription) untilSubscription.unsubscribe(); } return cleanup; }); } } export class EndOfStreamError extends ScrivitoError {} /** An Intermediary is a proxy between a Streamable and a Subscriber. * * The Streamable receives the Intermediary as the Subscriber. * The Subscriber receives the Intermediary as the Subscription. * * The purpose of the Intermediary is to normalize the API. * It ensures that unsubscribe and complete are always idempotent, for example. */ class Intermediary<T> implements Subscriber<T>, Subscription { private subscriber?: Partial<Subscriber<T>>; private unsubscribeCallback?: UnsubscribeFunction; constructor(subscriber: Partial<Subscriber<T>>) { this.subscriber = subscriber; } next(value: T) { if (this.subscriber && this.subscriber.next) { this.subscriber.next(value); } } complete() { if (this.subscriber && this.subscriber.complete) { this.subscriber.complete(); } this.subscriber = undefined; } unsubscribe() { if (!this.subscriber) return; this.subscriber = undefined; if (this.unsubscribeCallback) { this.unsubscribeCallback.apply(undefined); } } isClosed() { return !this.subscriber; } setUnsubscribeCallback(callback: UnsubscribeFunction) { this.unsubscribeCallback = callback; } }