UNPKG

graphile-build

Version:

Build a GraphQL schema from plugins

267 lines (260 loc) 9.81 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.LiveSource = exports.LiveProvider = exports.LiveMonitor = exports.LiveCoordinator = void 0; exports.makeAsyncIteratorFromMonitor = makeAsyncIteratorFromMonitor; var _callbackToAsyncIterator = _interopRequireDefault(require("./callbackToAsyncIterator")); var _lodash = require("lodash"); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } /* eslint-disable flowtype/no-weak-types */ const DEBOUNCE_DURATION = 25; const MONITOR_THROTTLE_DURATION = Math.max(DEBOUNCE_DURATION + 1, parseInt(process.env.LIVE_THROTTLE || "", 10) || 500); /* * Sources are long-lived (i.e. in "watch" mode you just re-use the same one * over and over) because there is no release for them */ class LiveSource { subscribeCollection(_callback, _collectionIdentifier, _predicate) { return null; } subscribeRecord(_callback, _collectionIdentifier, _recordIdentifier) { return null; } } /* * Providers enable a namespace, perform validation, and track the sources used * by that namespace within one single schema build. The should not directly use * any long-lived features as they do not have an explicit "release"/"close" * command when a new schema is built. */ exports.LiveSource = LiveSource; class LiveProvider { constructor(namespace) { this.namespace = namespace; this.sources = []; } registerSource(source) { this.sources.push(source); } collectionIdentifierIsValid(_collectionIdentifier) { return false; } recordIdentifierIsValid(_collectionIdentifier, _recordIdentifier) { return false; } } /* * During a single execution of GraphQL (specifically a subscription request), * the LiveMonitor tracks the resources viewed and subscribes to updates in them. */ exports.LiveProvider = LiveProvider; class LiveMonitor { constructor(providers, extraRootValue) { this.extraRootValue = extraRootValue; this.released = false; this.providers = providers; this.subscriptionReleasersByCounter = {}; this.changeCallback = null; this.changeCounter = 0; this.liveConditionsByCounter = {}; this.handleChange = function () { /* This function is throttled to ~25ms (see constructor); it's purpose is * to bundle up all the changes that occur in a small window into the same * handle change flow, so _reallyHandleChange doesn't get called twice in * quick succession. _reallyHandleChange is then further throttled with a * larger window, BUT it triggers on both leading and trailing edge, * whereas this only triggers on the trailing edge. */ if (this._reallyHandleChange) { this._reallyHandleChange(); } }; this._reallyHandleChange = function () { // This function is throttled to MONITOR_THROTTLE_DURATION (see constructor) if (this.changeCallback) { // Convince Flow this won't suddenly become null const cb = this.changeCallback; const counter = this.changeCounter++; /* * In live queries we need to know when the current result set has * finished being calculated so that we know we've received all the * liveRecord / liveCollection calls and can release the out of date * ones. To achieve this, we use a custom `subscribe` function which * calls `rootValue.release()` once the result set has been calculated. */ this.subscriptionReleasersByCounter[String(counter)] = []; this.liveConditionsByCounter[String(counter)] = []; const changeRootValue = { ...this.extraRootValue, counter, liveCollection: this.liveCollection.bind(this, counter), liveRecord: this.liveRecord.bind(this, counter), liveConditions: this.liveConditionsByCounter[String(counter)], release: () => { // Despite it's name, this means that the execution has complete, which means we're actually releasing everything *before* this. this.resetBefore(counter); } }; cb(changeRootValue); } else { // eslint-disable-next-line no-console console.warn("Change occurred, but no-one was listening"); } }; this.onChange = function (callback) { if (this.released) { throw new Error("Monitors cannot be reused."); } if (this.changeCallback) { throw new Error("Already monitoring for changes"); } // Throttle to every 250ms this.changeCallback = callback; if (this.handleChange) { setImmediate(this.handleChange); } return () => { if (this.changeCallback === callback) { this.changeCallback = null; } this.release(); }; }; this.handleChange = (0, _lodash.throttle)(this.handleChange.bind(this), DEBOUNCE_DURATION, { leading: false, trailing: true }); if (!this._reallyHandleChange) { throw new Error("This is just to make flow happy"); } this._reallyHandleChange = (0, _lodash.throttle)(this._reallyHandleChange.bind(this), MONITOR_THROTTLE_DURATION - DEBOUNCE_DURATION, { leading: true, trailing: true }); this.onChange = this.onChange.bind(this); } resetBefore(currentCounter) { // Clear out of date subscriptionReleasers { const oldCounters = Object.keys(this.subscriptionReleasersByCounter).filter(n => parseInt(n, 10) < currentCounter); for (const oldCounter of oldCounters) { for (const releaser of this.subscriptionReleasersByCounter[oldCounter]) { releaser(); } delete this.subscriptionReleasersByCounter[oldCounter]; } } // Clear out of date liveConditions { const oldCounters = Object.keys(this.liveConditionsByCounter).filter(n => parseInt(n, 10) < currentCounter); for (const oldCounter of oldCounters) { delete this.liveConditionsByCounter[oldCounter]; } } } release() { if (this.handleChange) { // $FlowFixMe: throttled function this.handleChange.cancel(); } this.handleChange = null; if (this._reallyHandleChange) { // $FlowFixMe: throttled function this._reallyHandleChange.cancel(); } this._reallyHandleChange = null; this.resetBefore(Infinity); this.providers = {}; this.released = true; } liveCollection(counter, namespace, collectionIdentifier, predicate = () => true) { const handleChange = this.handleChange; if (this.released || !handleChange) { return; } const provider = this.providers[namespace]; if (!provider || provider.sources.length === 0) return; if (!provider.collectionIdentifierIsValid(collectionIdentifier)) { throw new Error(`Invalid collection identifier passed to LiveMonitor[${namespace}]: ${collectionIdentifier}`); } for (const source of provider.sources) { const releaser = source.subscribeCollection(handleChange, collectionIdentifier, predicate); if (releaser) { this.subscriptionReleasersByCounter[String(counter)].push(releaser); } } } liveRecord(counter, namespace, collectionIdentifier, recordIdentifier) { const handleChange = this.handleChange; if (this.released || !handleChange) { return; } // TODO: if (recordIdentifier == null) {return} const provider = this.providers[namespace]; if (!provider || provider.sources.length === 0) return; if (!provider.collectionIdentifierIsValid(collectionIdentifier)) { throw new Error(`Invalid collection identifier passed to LiveMonitor[${namespace}]: ${collectionIdentifier}`); } if (!provider.recordIdentifierIsValid(collectionIdentifier, recordIdentifier)) { throw new Error(`Invalid record identifier passed to LiveMonitor[${namespace}]: ${collectionIdentifier}`); } for (const source of provider.sources) { const releaser = source.subscribeRecord(handleChange, collectionIdentifier, recordIdentifier); if (releaser) { this.subscriptionReleasersByCounter[String(counter)].push(releaser); } } } } /* * There is one coordinator for each build of the GraphQL schema, it tracks the providers * and gives a handy `subscribe` method that can be used for live queries (assuming * that the `resolve` is provided the same as in a Query). */ exports.LiveMonitor = LiveMonitor; class LiveCoordinator { constructor() { this.providers = {}; this.subscribe = this.subscribe.bind(this); } registerProvider(provider) { const { namespace } = provider; if (this.providers[namespace]) { throw new Error(`Namespace ${namespace} already registered with Live`); } this.providers[namespace] = provider; } registerSource(namespace, source) { if (!this.providers[namespace]) { // eslint-disable-next-line no-console console.warn(`LiveProvider '${namespace}' is not registered, skipping live source.`); return; } this.providers[namespace].registerSource(source); } getMonitor(extraRootValue) { return new LiveMonitor(this.providers, extraRootValue); } // Tell Flow that we're okay with overwriting this subscribe(_parent, _args, _context, _info) { const monitor = this.getMonitor({ liveAbort: e => { if (iterator) iterator.throw(e); } }); const iterator = makeAsyncIteratorFromMonitor(monitor); return iterator; } } exports.LiveCoordinator = LiveCoordinator; function makeAsyncIteratorFromMonitor(monitor) { return (0, _callbackToAsyncIterator.default)(monitor.onChange, { onClose: release => { if (release) release(); } }); } //# sourceMappingURL=Live.js.map