@atlaskit/node-data-provider
Version:
Node data provider for @atlaskit/editor-core plugins and @atlaskit/renderer
419 lines (406 loc) • 16.3 kB
JavaScript
import _defineProperty from "@babel/runtime/helpers/defineProperty";
import _asyncToGenerator from "@babel/runtime/helpers/asyncToGenerator";
import _slicedToArray from "@babel/runtime/helpers/slicedToArray";
import _classCallCheck from "@babel/runtime/helpers/classCallCheck";
import _createClass from "@babel/runtime/helpers/createClass";
import _regeneratorRuntime from "@babel/runtime/regenerator";
import { isSSR } from '@atlaskit/editor-common/core-utils';
import { editorExperiment } from '@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.
*/
export var NodeDataProvider = /*#__PURE__*/function () {
function NodeDataProvider() {
_classCallCheck(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 _createClass(NodeDataProvider, [{
key: "setSSRData",
value: function setSSRData() {
var ssrData = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
if (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 = _slicedToArray(_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 = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime.mark(function _callee(node, callback) {
var jsonNode, dataKey, dataFromCache, cacheVersionBeforeRequest, networkRequestInFlightKey, networkRequestInFlight, data, dataPromise, _data;
return _regeneratorRuntime.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 (!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 = _slicedToArray(_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 (editorExperiment('platform_synced_block', true)) {
this.updateCache(_defineProperty({}, 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 = _slicedToArray(_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 = _slicedToArray(_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];
});
}
}]);
}();