UNPKG

@vyer-technologies/relay-offline

Version:

Forked from @wora Relay Offline Capabilities

284 lines 11.9 kB
'use strict'; import invariant from 'fbjs/lib/invariant'; import jest from 'jest-mock'; import areEqual from 'fbjs/lib/areEqual'; import { QueryResponseCache, Observable, Network, createOperationDescriptor, getRequest, } from 'relay-runtime'; import { Store, RecordSource, Environment } from '../lib'; import { createPersistedRecordSource, createPersistedStore } from '@vyer-technologies/relay-store/test'; const MAX_SIZE = 10; const MAX_TTL = 5 * 60 * 1000; function mockInstanceMethod(object, key) { object[key] = jest.fn(object[key].bind(object)); } function mockDisposableMethod(object, key) { const fn = object[key].bind(object); object[key] = jest.fn((...args) => { const disposable = fn(...args); const dispose = jest.fn(() => disposable.dispose()); object[key].mock.dispose = dispose; return { dispose }; }); const mockClear = object[key].mockClear.bind(object[key]); object[key].mockClear = () => { mockClear(); object[key].mock.dispose = null; }; } function mockObservableMethod(object, key) { const fn = object[key].bind(object); const subscriptions = []; object[key] = jest.fn((...args) => fn(...args).do({ start: (subscription) => { subscriptions.push(subscription); }, })); object[key].mock.subscriptions = subscriptions; const mockClear = object[key].mockClear.bind(object[key]); object[key].mockClear = () => { mockClear(); object[key].mock.subscriptions = []; }; } export function createMockEnvironment(config) { const store = config?.store ?? new Store(new RecordSource({ storage: createPersistedStore() }), { storage: createPersistedRecordSource() }, { queryCacheExpirationTime: null }); const cache = new QueryResponseCache({ size: MAX_SIZE, ttl: MAX_TTL, }); let pendingRequests = []; let pendingOperations = []; const queuePendingOperation = (query, variables) => { const operationDescriptor = createOperationDescriptor(getRequest(query), variables); pendingOperations = pendingOperations.concat([operationDescriptor]); }; let resolversQueue = []; const queueOperationResolver = (resolver) => { resolversQueue = resolversQueue.concat([resolver]); }; const execute = (request, variables, cacheConfig) => { const { id, text } = request; const cacheID = id ?? text; let cachedPayload = null; if ((cacheConfig?.force == null || cacheConfig?.force === false) && cacheID != null) { cachedPayload = cache.get(cacheID, variables); } if (cachedPayload !== null) { return Observable.from(cachedPayload); } const currentOperation = pendingOperations.find((op) => op.request.node.params === request && areEqual(op.request.variables, variables)); if (currentOperation != null && resolversQueue.length > 0) { const currentResolver = resolversQueue[0]; const result = currentResolver(currentOperation); if (result != null) { resolversQueue = resolversQueue.filter((res) => res !== currentResolver); pendingOperations = pendingOperations.filter((op) => op !== currentOperation); if (result instanceof Error) { return Observable.create((sink) => { sink.error(result); }); } else { return Observable.from(result); } } } return Observable.create((sink) => { const nextRequest = { request, variables, cacheConfig, sink }; pendingRequests = pendingRequests.concat([nextRequest]); return () => { pendingRequests = pendingRequests.filter((pending) => !areEqual(pending, nextRequest)); pendingOperations = pendingOperations.filter((op) => op !== currentOperation); }; }); }; function getConcreteRequest(input) { if (input.kind === 'Request') { return input; } else { const operationDescriptor = input; invariant(pendingOperations.includes(operationDescriptor), 'RelayModernMockEnvironment: Operation "%s" was not found in the list of pending operations', operationDescriptor.request.node.operation.name); return operationDescriptor.request.node; } } function getRequests(input) { let concreteRequest; let operationDescriptor; if (input.kind === 'Request') { concreteRequest = input; } else { operationDescriptor = input; concreteRequest = operationDescriptor.request.node; } const foundRequests = pendingRequests.filter((pending) => { if (!areEqual(pending.request, concreteRequest.params)) { return false; } if (operationDescriptor) { return areEqual(operationDescriptor.request.variables, pending.variables); } else { return true; } }); invariant(foundRequests.length, 'MockEnvironment: Cannot respond to request, it has not been requested yet.'); foundRequests.forEach((foundRequest) => { invariant(foundRequest.sink, 'MockEnvironment: Cannot respond to `%s`, it has not been requested yet.', concreteRequest.params.name); }); return foundRequests; } function ensureValidPayload(payload) { invariant(typeof payload === 'object' && payload !== null && payload.hasOwnProperty('data'), 'MockEnvironment(): Expected payload to be an object with a `data` key.'); return payload; } const cachePayload = (request, variables, payload) => { const { id, text } = getConcreteRequest(request).params; const cacheID = id ?? text; invariant(cacheID != null, 'CacheID should not be null'); cache.set(cacheID, variables, payload); }; const clearCache = () => { cache.clear(); }; const isLoading = (request, variables, cacheConfig) => { return pendingRequests.some((pending) => areEqual(pending.request, getConcreteRequest(request).params) && areEqual(pending.variables, variables) && areEqual(pending.cacheConfig, cacheConfig ?? {})); }; const reject = (request, error) => { const rejectError = typeof error === 'string' ? new Error(error) : error; getRequests(request).forEach((foundRequest) => { const { sink } = foundRequest; invariant(sink !== null, 'Sink should be defined.'); sink.error(rejectError); }); }; const nextValue = (request, payload) => { getRequests(request).forEach((foundRequest) => { const { sink } = foundRequest; invariant(sink !== null, 'Sink should be defined.'); sink.next(ensureValidPayload(payload)); }); }; const complete = (request) => { getRequests(request).forEach((foundRequest) => { const { sink } = foundRequest; invariant(sink !== null, 'Sink should be defined.'); sink.complete(); }); }; const resolve = (request, payload) => { getRequests(request).forEach((foundRequest) => { const { sink } = foundRequest; invariant(sink !== null, 'Sink should be defined.'); sink.next(ensureValidPayload(payload)); sink.complete(); }); }; const getMostRecentOperation = () => { const mostRecentOperation = pendingOperations[pendingOperations.length - 1]; invariant(mostRecentOperation != null, 'RelayModernMockEnvironment: There are no pending operations in the list'); return mostRecentOperation; }; const findOperation = (findFn) => { const pendingOperation = pendingOperations.find(findFn); invariant(pendingOperation != null, 'RelayModernMockEnvironment: Operation was not found in the list of pending operations'); return pendingOperation; }; const environment = new Environment({ configName: 'RelayModernMockEnvironment', network: Network.create(execute, execute), store, ...config, }); const createExecuteProxy = (env, fn) => { return (...argumentsList) => { const [{ operation }] = argumentsList; pendingOperations = pendingOperations.concat([operation]); return fn.apply(env, argumentsList); }; }; let promiseHydrate; const hydrate = () => promiseHydrate; const createHydrateProxy = (env, fn) => { return (...argumentsList) => { promiseHydrate = fn.apply(env, argumentsList); return promiseHydrate; }; }; environment.hydrate = createHydrateProxy(environment, environment.hydrate); environment.execute = createExecuteProxy(environment, environment.execute); environment.executeMutation = createExecuteProxy(environment, environment.executeMutation); if (global?.process?.env?.NODE_ENV === 'test') { mockDisposableMethod(environment, 'applyUpdate'); mockInstanceMethod(environment, 'commitPayload'); mockInstanceMethod(environment, 'getStore'); mockInstanceMethod(environment, 'lookup'); mockInstanceMethod(environment, 'check'); mockInstanceMethod(environment, 'hydrate'); mockDisposableMethod(environment, 'subscribe'); mockDisposableMethod(environment, 'retain'); mockObservableMethod(environment, 'execute'); mockObservableMethod(environment, 'executeWithSource'); mockObservableMethod(environment, 'executeMutation'); mockInstanceMethod(store, 'getSource'); mockInstanceMethod(store, 'lookup'); mockInstanceMethod(store, 'notify'); mockInstanceMethod(store, 'publish'); mockDisposableMethod(store, 'retain'); mockDisposableMethod(store, 'subscribe'); } const mock = { cachePayload, clearCache, isLoading, reject, resolve, nextValue, complete, hydrate, getMostRecentOperation, resolveMostRecentOperation(payload) { const operation = getMostRecentOperation(); const data = typeof payload === 'function' ? payload(operation) : payload; return resolve(operation, data); }, rejectMostRecentOperation(error) { const operation = getMostRecentOperation(); const rejector = typeof error === 'function' ? error(operation) : error; return reject(operation, rejector); }, findOperation, queuePendingOperation, getAllOperations() { return pendingOperations; }, queueOperationResolver, }; environment.mock = mock; environment.mockClear = () => { environment.applyUpdate.mockClear(); environment.commitPayload.mockClear(); environment.getStore.mockClear(); environment.lookup.mockClear(); environment.check.mockClear(); environment.subscribe.mockClear(); environment.retain.mockClear(); environment.hydrate.mockClear(); environment.execute.mockClear(); environment.executeMutation.mockClear(); store.getSource.mockClear(); store.lookup.mockClear(); store.notify.mockClear(); store.publish.mockClear(); store.retain.mockClear(); store.subscribe.mockClear(); cache.clear(); pendingOperations = []; pendingRequests = []; }; return environment; } //# sourceMappingURL=createMockEnvironment.js.map