UNPKG

@atlaskit/node-data-provider

Version:

Node data provider for @atlaskit/editor-core plugins and @atlaskit/renderer

422 lines (412 loc) 16.9 kB
"use strict"; var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault"); Object.defineProperty(exports, "__esModule", { value: true }); exports.NodeDataProvider = void 0; var _regenerator = _interopRequireDefault(require("@babel/runtime/regenerator")); var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty")); var _asyncToGenerator2 = _interopRequireDefault(require("@babel/runtime/helpers/asyncToGenerator")); var _slicedToArray2 = _interopRequireDefault(require("@babel/runtime/helpers/slicedToArray")); var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime/helpers/classCallCheck")); var _createClass2 = _interopRequireDefault(require("@babel/runtime/helpers/createClass")); var _coreUtils = require("@atlaskit/editor-common/core-utils"); var _experiments = require("@atlaskit/tmp-editor-statsig/experiments"); /** * Represents the SSR data for a single provider. * It's a map where each key is a unique node data key and the value is the prefetched data for that node. * * @example * { * 'node-id-1': { value: 'some data' }, * 'node-id-2': { value: 'other data' } * } */ /** * Represents the cached data for a Node Data Provider. * Each key is a unique node data key, and the value is an object containing: * - `source`: Indicates whether the data was fetched from SSR or the network. * - `data`: The actual data, which can be either a resolved value or a Promise. * * @example * { * 'node-id-1': { source: 'ssr', data: { value: 'some data' } }, * 'node-id-2': { source: 'network', data: { value: 'other data' } } * } */ /** * Represents the payload passed to the callback function when data is fetched. * It can either contain an error or the fetched data. */ /** * A Node Data Provider is responsible for fetching and caching data associated with specific ProseMirror nodes. * It supports a cache-first-then-network strategy, with initial data potentially provided via SSR. * * @template Node The specific type of JSONNode this provider supports. * @template Data The type of data this provider fetches and manages. */ var NodeDataProvider = exports.NodeDataProvider = /*#__PURE__*/function () { function NodeDataProvider() { (0, _classCallCheck2.default)(this, NodeDataProvider); this.cacheVersion = 0; this.cache = {}; this.networkRequestsInFlight = {}; } /** * Sets the SSR data for the provider. * This pre-populates the cache with data rendered on the server, preventing redundant network requests on the client. * Calling this method will invalidate the existing cache. * * @example * ``` * const ssrData = window.__SSR_NODE_DATA__ || {}; * nodeDataProvider.setSSRData(ssrData); * ``` * * @param ssrData A map of node data keys to their corresponding data. */ return (0, _createClass2.default)(NodeDataProvider, [{ key: "setSSRData", value: function setSSRData() { var ssrData = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; if ((0, _experiments.editorExperiment)('platform_synced_block', true)) { this.updateCache(ssrData, { strategy: 'replace', source: 'ssr' }); return; } this.cacheVersion++; this.cache = Object.fromEntries(Object.entries(ssrData).map(function (_ref) { var _ref2 = (0, _slicedToArray2.default)(_ref, 2), key = _ref2[0], data = _ref2[1]; return [key, { data: data, source: 'ssr' }]; })); } /** * Clears all cached data. * This increments the internal cache version, invalidating any pending network requests. * * @example * ``` * function useMyNodeDataProvider(contentId: string) { * const nodeDataProvider = new MyNodeDataProvider(); * * // Reset the cache when the contentId changes (e.g., when the user navigates to a different page). * useEffect(() => { * nodeDataProvider.resetCache(); * }, [contentId]); * * return nodeDataProvider; * } * ``` */ }, { key: "resetCache", value: function resetCache() { this.cacheVersion++; this.cache = {}; } /** * Fetches data for a given node using a cache-first-then-network strategy. * * The provided callback may be called multiple times: * 1. Immediately with data from the SSR cache, if available. * 2. Asynchronously with data fetched from the network. * * @example * ``` * const nodeDataProvider = new MyNodeDataProvider(); * * nodeDataProvider.getData(node, (data) => { * console.log('Node data:', data); * }); * ``` * * @param node The node (or its ProseMirror representation) for which to fetch data. * @param callback The callback function to call with the fetched data or an error. */ }, { key: "getData", value: function getData(node, callback) { // Move implementation to a separate async method // to keep this method synchronous and avoid async/await in the public API. void this.getDataAsync(node, callback); } }, { key: "getDataAsync", value: function () { var _getDataAsync = (0, _asyncToGenerator2.default)( /*#__PURE__*/_regenerator.default.mark(function _callee(node, callback) { var jsonNode, dataKey, dataFromCache, cacheVersionBeforeRequest, networkRequestInFlightKey, networkRequestInFlight, data, dataPromise, _data; return _regenerator.default.wrap(function _callee$(_context) { while (1) switch (_context.prev = _context.next) { case 0: jsonNode = 'toJSON' in node ? node.toJSON() : node; if (this.isNodeSupported(jsonNode)) { _context.next = 4; break; } // eslint-disable-next-line no-console console.error("The ".concat(this.constructor.name, " doesn't support Node ").concat(jsonNode.type, ".")); return _context.abrupt("return"); case 4: dataKey = this.nodeDataKey(jsonNode); dataFromCache = this.cache[dataKey]; if (dataFromCache !== undefined) { // If we have the data in the SSR data, we can use it directly callback({ data: dataFromCache.data }); } if (!(0, _coreUtils.isSSR)()) { _context.next = 9; break; } return _context.abrupt("return"); case 9: if (!((dataFromCache === null || dataFromCache === void 0 ? void 0 : dataFromCache.source) !== 'network')) { _context.next = 41; break; } // Store the current cache version before making the request, // so we can check if the cache has changed while we are waiting for the network response. cacheVersionBeforeRequest = this.cacheVersion; // Create a unique key for the in-flight network request // based on the cache version and the data key. networkRequestInFlightKey = "".concat(cacheVersionBeforeRequest, "-").concat(dataKey); // Check if there is already a network request in flight for this data // to avoid duplicate requests. networkRequestInFlight = this.networkRequestsInFlight[networkRequestInFlightKey]; if (!networkRequestInFlight) { _context.next = 25; break; } _context.prev = 14; _context.next = 17; return networkRequestInFlight; case 17: data = _context.sent; callback({ data: data }); _context.next = 24; break; case 21: _context.prev = 21; _context.t0 = _context["catch"](14); callback({ error: _context.t0 instanceof Error ? _context.t0 : new Error(String(_context.t0)) }); case 24: return _context.abrupt("return"); case 25: _context.prev = 25; dataPromise = this.fetchNodesData([jsonNode]).then(function (_ref3) { var _ref4 = (0, _slicedToArray2.default)(_ref3, 1), value = _ref4[0]; return value; }); // Store the promise in the in-flight requests map this.networkRequestsInFlight[networkRequestInFlightKey] = dataPromise; _context.next = 30; return dataPromise; case 30: _data = _context.sent; // We need to call the callback with the data with result even if the cache version has changed, // so all promises that are waiting for the data can resolve. callback({ data: _data }); // If the cache version has changed, we don't want to use the data from the network // because it could be stale data. if (cacheVersionBeforeRequest === this.cacheVersion) { // Replace promise with the resolved data in the cache if ((0, _experiments.editorExperiment)('platform_synced_block', true)) { this.updateCache((0, _defineProperty2.default)({}, dataKey, _data), { strategy: 'merge', source: 'network' }); } else { this.cache[dataKey] = { data: _data, source: 'network' }; } } _context.next = 38; break; case 35: _context.prev = 35; _context.t1 = _context["catch"](25); // If an error occurs, we call the callback with the error callback({ error: _context.t1 instanceof Error ? _context.t1 : new Error(String(_context.t1)) }); case 38: _context.prev = 38; // Ensure we clean up the in-flight request entry delete this.networkRequestsInFlight[networkRequestInFlightKey]; return _context.finish(38); case 41: case "end": return _context.stop(); } }, _callee, this, [[14, 21], [25, 35, 38, 41]]); })); function getDataAsync(_x, _x2) { return _getDataAsync.apply(this, arguments); } return getDataAsync; }() /** * Fetches data for a given node and returns it as a Promise. * This is a convenience wrapper around the `data` method for use with async/await. * * Note: This promise resolves with the *first* available data, which could be from the SSR cache or the network. * It may not provide the most up-to-date data if a network fetch is in progress. * * Note: This method is only for migration purposes. Use {@link getData} in new code instead. * * @private * @deprecated Don't use this method, use {@link getData} method instead. * This method is only for migration purposes. * * @param node The node (or its ProseMirror representation) for which to fetch data. * @returns A promise that resolves with the node's data. */ }, { key: "getDataAsPromise_DO_NOT_USE_OUTSIDE_MIGRATIONS", value: function getDataAsPromise_DO_NOT_USE_OUTSIDE_MIGRATIONS(node) { var _this = this; return new Promise(function (resolve, reject) { try { _this.getData(node, function (payload) { if (payload.error) { reject(payload.error); } else { resolve(payload.data); } }); } catch (error) { reject(error); } }); } /** * Checks the cache for the given node and returns its status. * * Possible return values: * - `false`: No cached data found for the node. * - `'ssr'`: Data is cached from server-side rendering (SSR). * - `'network'`: Data is cached from a network request. * * @param node The node (or its ProseMirror representation) to check in the cache. * @returns The cache status: `false`, `'ssr'`, or `'network'`. */ }, { key: "getCacheStatusForNode", value: function getCacheStatusForNode(node) { var dataFromCache = this.getNodeDataFromCache(node); return dataFromCache ? dataFromCache.source : false; } /** * Retrieves the cached data for a given node, if available. * * @param node The node (or its ProseMirror representation) for which to retrieve cached data. * @returns The cached data object containing `data` and `source`, or `undefined` if no cache entry exists. */ }, { key: "getNodeDataFromCache", value: function getNodeDataFromCache(node) { var jsonNode = 'toJSON' in node ? node.toJSON() : node; if (!this.isNodeSupported(jsonNode)) { // eslint-disable-next-line no-console console.error("The ".concat(this.constructor.name, " doesn't support Node ").concat(jsonNode.type, ".")); return undefined; } var dataKey = this.nodeDataKey(jsonNode); return this.cache[dataKey]; } /** * Returns the keys of the cache. * * @returns An array of the keys of the cache. */ }, { key: "getNodeDataCacheKeys", value: function getNodeDataCacheKeys() { return Object.keys(this.cache); } /** * Updates the cache with new records using merge or replace strategies. * This method should be the only way to modify the cache directly. * This allow subclasses to use it when needed. e.g. abstract fetchNodesData implementation. * * @example * ``` * const newRecords = { * 'node-id-1': { value: 'updated data' }, * 'node-id-3': { value: 'new data' } * }; * nodeDataProvider.updateCache(newRecords, { strategy: 'merge', source: 'network' }); * ``` * * Supports two strategies: * - 'merge' (default): Merges new records into the existing cache. * - 'replace': Replaces the entire cache with the new records, invalidating any in-flight requests. * * @param records A map of node data keys to their corresponding data. * @param options Optional settings for the update operation. * @param options.strategy The strategy to use for updating the cache ('merge' or 'replace'). Defaults to 'merge'. * @param options.source The source of the data being added ('ssr' or 'network'). Defaults to 'network'. */ }, { key: "updateCache", value: function updateCache() { var _options$strategy, _options$source; var records = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; var options = arguments.length > 1 ? arguments[1] : undefined; var strategy = (_options$strategy = options === null || options === void 0 ? void 0 : options.strategy) !== null && _options$strategy !== void 0 ? _options$strategy : 'merge'; var source = (_options$source = options === null || options === void 0 ? void 0 : options.source) !== null && _options$source !== void 0 ? _options$source : 'network'; if (strategy === 'merge') { for (var _i = 0, _Object$entries = Object.entries(records); _i < _Object$entries.length; _i++) { var _Object$entries$_i = (0, _slicedToArray2.default)(_Object$entries[_i], 2), key = _Object$entries$_i[0], data = _Object$entries$_i[1]; this.cache[key] = { data: data, source: source }; } return; } else if (strategy === 'replace') { // Replace the entire cache with the new records // This will increase the cache version to invalidate any in-flight requests this.resetCache(); this.cache = Object.fromEntries(Object.entries(records).map(function (_ref5) { var _ref6 = (0, _slicedToArray2.default)(_ref5, 2), key = _ref6[0], data = _ref6[1]; return [key, { data: data, source: source }]; })); } } /** * Removes one or more entries from the cache. * * @param keys A single data key or array of data keys to remove from the cache. */ }, { key: "removeFromCache", value: function removeFromCache(keys) { var _this2 = this; keys.forEach(function (key) { delete _this2.cache[key]; }); } }]); }();