UNPKG

ember-source

Version:

A JavaScript framework for creating ambitious web applications

557 lines (513 loc) 16.1 kB
import { getOwner } from '../-internals/owner/index.js'; import { _backburner, next } from '../runloop/index.js'; import EmberObject from '../object/index.js'; import { dasherize } from '../-internals/string/index.js'; import Namespace from '../application/namespace.js'; import { A } from '../array/index.js'; import { createCache, consumeTag, tagFor, untrack, getValue } from '../../@glimmer/validator/index.js'; import './index.js'; import { g as get } from '../../shared-chunks/cache-DORQczuy.js'; import '../-internals/meta/lib/meta.js'; import '../../shared-chunks/mandatory-setter-BiXq-dpN.js'; import '@embroider/macros'; import '../../@glimmer/destroyable/index.js'; import '../../@glimmer/manager/index.js'; import '../../shared-chunks/env-mInZ1DuF.js'; import { assert } from './lib/assert.js'; /** @module @ember/debug/data-adapter */ // Represents the base contract for iterables as understood in the GLimmer VM // historically. This is *not* the public API for it, because there *is* no // public API for it. Recent versions of Glimmer simply use `Symbol.iterator`, // but some older consumers still use this basic shape. function iterate(arr, fn) { if (Symbol.iterator in arr) { for (let item of arr) { fn(item); } } else { // SAFETY: this cast required to work this way to interop between TS 4.8 // and 4.9. When we drop support for 4.8, it will narrow correctly via the // use of the `in` operator above. (Preferably we will solve this by just // switching to require `Symbol.iterator` instead.) assert('', typeof arr.forEach === 'function'); arr.forEach(fn); } } class RecordsWatcher { recordCaches = new Map(); added = []; updated = []; removed = []; getCacheForItem(record) { let recordCache = this.recordCaches.get(record); if (!recordCache) { let hasBeenAdded = false; recordCache = createCache(() => { if (!hasBeenAdded) { this.added.push(this.wrapRecord(record)); hasBeenAdded = true; } else { this.updated.push(this.wrapRecord(record)); } }); this.recordCaches.set(record, recordCache); } return recordCache; } constructor(records, recordsAdded, recordsUpdated, recordsRemoved, wrapRecord, release) { this.wrapRecord = wrapRecord; this.release = release; this.recordArrayCache = createCache(() => { let seen = new Set(); // Track `[]` for legacy support consumeTag(tagFor(records, '[]')); iterate(records, record => { getValue(this.getCacheForItem(record)); seen.add(record); }); // Untrack this operation because these records are being removed, they // should not be polled again in the future untrack(() => { this.recordCaches.forEach((_cache, record) => { if (!seen.has(record)) { this.removed.push(wrapRecord(record)); this.recordCaches.delete(record); } }); }); if (this.added.length > 0) { recordsAdded(this.added); this.added = []; } if (this.updated.length > 0) { recordsUpdated(this.updated); this.updated = []; } if (this.removed.length > 0) { recordsRemoved(this.removed); this.removed = []; } }); } revalidate() { getValue(this.recordArrayCache); } } class TypeWatcher { constructor(records, onChange, release) { this.release = release; let hasBeenAccessed = false; this.cache = createCache(() => { // Empty iteration, we're doing this just // to track changes to the records array iterate(records, () => {}); // Also track `[]` for legacy support consumeTag(tagFor(records, '[]')); if (hasBeenAccessed === true) { next(onChange); } else { hasBeenAccessed = true; } }); this.release = release; } revalidate() { getValue(this.cache); } } /** The `DataAdapter` helps a data persistence library interface with tools that debug Ember such as the [Ember Inspector](https://github.com/emberjs/ember-inspector) for Chrome and Firefox. This class will be extended by a persistence library which will override some of the methods with library-specific code. The methods likely to be overridden are: * `getFilters` * `detect` * `columnsForType` * `getRecords` * `getRecordColumnValues` * `getRecordKeywords` * `getRecordFilterValues` * `getRecordColor` The adapter will need to be registered in the application's container as `dataAdapter:main`. Example: ```javascript Application.initializer({ name: "data-adapter", initialize: function(application) { application.register('data-adapter:main', DS.DataAdapter); } }); ``` @class DataAdapter @extends EmberObject @public */ class DataAdapter extends EmberObject { releaseMethods = A(); recordsWatchers = new Map(); typeWatchers = new Map(); flushWatchers = null; // TODO: Revisit this constructor(owner) { super(owner); this.containerDebugAdapter = getOwner(this).lookup('container-debug-adapter:main'); } /** The container-debug-adapter which is used to list all models. @property containerDebugAdapter @default undefined @since 1.5.0 @public **/ /** The number of attributes to send as columns. (Enough to make the record identifiable). @private @property attributeLimit @default 3 @since 1.3.0 */ attributeLimit = 3; /** Ember Data > v1.0.0-beta.18 requires string model names to be passed around instead of the actual factories. This is a stamp for the Ember Inspector to differentiate between the versions to be able to support older versions too. @public @property acceptsModelName */ acceptsModelName = true; /** Map from records arrays to RecordsWatcher instances @private @property recordsWatchers @since 3.26.0 */ /** Map from records arrays to TypeWatcher instances @private @property typeWatchers @since 3.26.0 */ /** Callback that is currently scheduled on backburner end to flush and check all active watchers. @private @property flushWatchers @since 3.26.0 */ /** Stores all methods that clear observers. These methods will be called on destruction. @private @property releaseMethods @since 1.3.0 */ /** Specifies how records can be filtered. Records returned will need to have a `filterValues` property with a key for every name in the returned array. @public @method getFilters @return {Array} List of objects defining filters. The object should have a `name` and `desc` property. */ getFilters() { return A(); } /** Fetch the model types and observe them for changes. @public @method watchModelTypes @param {Function} typesAdded Callback to call to add types. Takes an array of objects containing wrapped types (returned from `wrapModelType`). @param {Function} typesUpdated Callback to call when a type has changed. Takes an array of objects containing wrapped types. @return {Function} Method to call to remove all observers */ watchModelTypes(typesAdded, typesUpdated) { let modelTypes = this.getModelTypes(); let releaseMethods = A(); let typesToSend; typesToSend = modelTypes.map(type => { let klass = type.klass; let wrapped = this.wrapModelType(klass, type.name); releaseMethods.push(this.observeModelType(type.name, typesUpdated)); return wrapped; }); typesAdded(typesToSend); let release = () => { releaseMethods.forEach(fn => fn()); this.releaseMethods.removeObject(release); }; this.releaseMethods.pushObject(release); return release; } _nameToClass(type) { if (typeof type === 'string') { let owner = getOwner(this); let Factory = owner.factoryFor(`model:${type}`); type = Factory && Factory.class; } return type; } /** Fetch the records of a given type and observe them for changes. @public @method watchRecords @param {String} modelName The model name. @param {Function} recordsAdded Callback to call to add records. Takes an array of objects containing wrapped records. The object should have the following properties: columnValues: {Object} The key and value of a table cell. object: {Object} The actual record object. @param {Function} recordsUpdated Callback to call when a record has changed. Takes an array of objects containing wrapped records. @param {Function} recordsRemoved Callback to call when a record has removed. Takes an array of objects containing wrapped records. @return {Function} Method to call to remove all observers. */ watchRecords(modelName, recordsAdded, recordsUpdated, recordsRemoved) { let klass = this._nameToClass(modelName); let records = this.getRecords(klass, modelName); let { recordsWatchers } = this; let recordsWatcher = recordsWatchers.get(records); if (!recordsWatcher) { recordsWatcher = new RecordsWatcher(records, recordsAdded, recordsUpdated, recordsRemoved, record => this.wrapRecord(record), () => { recordsWatchers.delete(records); this.updateFlushWatchers(); }); recordsWatchers.set(records, recordsWatcher); this.updateFlushWatchers(); recordsWatcher.revalidate(); } return recordsWatcher.release; } updateFlushWatchers() { if (this.flushWatchers === null) { if (this.typeWatchers.size > 0 || this.recordsWatchers.size > 0) { this.flushWatchers = () => { this.typeWatchers.forEach(watcher => watcher.revalidate()); this.recordsWatchers.forEach(watcher => watcher.revalidate()); }; _backburner.on('end', this.flushWatchers); } } else if (this.typeWatchers.size === 0 && this.recordsWatchers.size === 0) { _backburner.off('end', this.flushWatchers); this.flushWatchers = null; } } /** Clear all observers before destruction @private @method willDestroy */ willDestroy() { this._super(...arguments); this.typeWatchers.forEach(watcher => watcher.release()); this.recordsWatchers.forEach(watcher => watcher.release()); this.releaseMethods.forEach(fn => fn()); if (this.flushWatchers) { _backburner.off('end', this.flushWatchers); } } /** Detect whether a class is a model. Test that against the model class of your persistence library. @public @method detect @return boolean Whether the class is a model class or not. */ detect(_klass) { return false; } /** Get the columns for a given model type. @public @method columnsForType @return {Array} An array of columns of the following format: name: {String} The name of the column. desc: {String} Humanized description (what would show in a table column name). */ columnsForType(_klass) { return A(); } /** Adds observers to a model type class. @private @method observeModelType @param {String} modelName The model type name. @param {Function} typesUpdated Called when a type is modified. @return {Function} The function to call to remove observers. */ observeModelType(modelName, typesUpdated) { let klass = this._nameToClass(modelName); let records = this.getRecords(klass, modelName); let onChange = () => { typesUpdated([this.wrapModelType(klass, modelName)]); }; let { typeWatchers } = this; let typeWatcher = typeWatchers.get(records); if (!typeWatcher) { typeWatcher = new TypeWatcher(records, onChange, () => { typeWatchers.delete(records); this.updateFlushWatchers(); }); typeWatchers.set(records, typeWatcher); this.updateFlushWatchers(); typeWatcher.revalidate(); } return typeWatcher.release; } /** Wraps a given model type and observes changes to it. @private @method wrapModelType @param {Class} klass A model class. @param {String} modelName Name of the class. @return {Object} The wrapped type has the following format: name: {String} The name of the type. count: {Integer} The number of records available. columns: {Columns} An array of columns to describe the record. object: {Class} The actual Model type class. */ wrapModelType(klass, name) { let records = this.getRecords(klass, name); return { name, count: get(records, 'length'), columns: this.columnsForType(klass), object: klass }; } /** Fetches all models defined in the application. @private @method getModelTypes @return {Array} Array of model types. */ getModelTypes() { let containerDebugAdapter = this.containerDebugAdapter; let stringTypes = containerDebugAdapter.canCatalogEntriesByType('model') ? containerDebugAdapter.catalogEntriesByType('model') : this._getObjectsOnNamespaces(); // New adapters return strings instead of classes. let klassTypes = stringTypes.map(name => { return { klass: this._nameToClass(name), name }; }); return klassTypes.filter(type => this.detect(type.klass)); } /** Loops over all namespaces and all objects attached to them. @private @method _getObjectsOnNamespaces @return {Array} Array of model type strings. */ _getObjectsOnNamespaces() { let namespaces = Namespace.NAMESPACES; let types = []; namespaces.forEach(namespace => { for (let key in namespace) { if (!Object.prototype.hasOwnProperty.call(namespace, key)) { continue; } // Even though we will filter again in `getModelTypes`, // we should not call `lookupFactory` on non-models if (!this.detect(namespace[key])) { continue; } let name = dasherize(key); types.push(name); } }); return types; } /** Fetches all loaded records for a given type. @public @method getRecords @return {Array} An array of records. This array will be observed for changes, so it should update when new records are added/removed. */ getRecords(_klass, _name) { return A(); } /** Wraps a record and observers changes to it. @private @method wrapRecord @param {Object} record The record instance. @return {Object} The wrapped record. Format: columnValues: {Array} searchKeywords: {Array} */ wrapRecord(record) { return { object: record, columnValues: this.getRecordColumnValues(record), searchKeywords: this.getRecordKeywords(record), filterValues: this.getRecordFilterValues(record), color: this.getRecordColor(record) }; } /** Gets the values for each column. @public @method getRecordColumnValues @return {Object} Keys should match column names defined by the model type. */ getRecordColumnValues(_record) { return {}; } /** Returns keywords to match when searching records. @public @method getRecordKeywords @return {Array} Relevant keywords for search. */ getRecordKeywords(_record) { return A(); } /** Returns the values of filters defined by `getFilters`. @public @method getRecordFilterValues @param {Object} record The record instance. @return {Object} The filter values. */ getRecordFilterValues(_record) { return {}; } /** Each record can have a color that represents its state. @public @method getRecordColor @param {Object} record The record instance @return {String} The records color. Possible options: black, red, blue, green. */ getRecordColor(_record) { return null; } } export { DataAdapter as default };