API Docs for: 3.17.2
Show:

File: app/js/model-extensions/model-sync-rest.js

  1. /**
  2. An extension which provides a RESTful XHR sync implementation that can be mixed
  3. into a Model or ModelList subclass.
  4.  
  5. @module app
  6. @submodule model-sync-rest
  7. @since 3.6.0
  8. **/
  9.  
  10. var Lang = Y.Lang;
  11.  
  12. /**
  13. An extension which provides a RESTful XHR sync implementation that can be mixed
  14. into a Model or ModelList subclass.
  15.  
  16. This makes it trivial for your Model or ModelList subclasses communicate and
  17. transmit their data via RESTful XHRs. In most cases you'll only need to provide
  18. a value for `root` when sub-classing `Y.Model`.
  19.  
  20. Y.User = Y.Base.create('user', Y.Model, [Y.ModelSync.REST], {
  21. root: '/users'
  22. });
  23.  
  24. Y.Users = Y.Base.create('users', Y.ModelList, [Y.ModelSync.REST], {
  25. // By convention `Y.User`'s `root` will be used for the lists' URL.
  26. model: Y.User
  27. });
  28.  
  29. var users = new Y.Users();
  30.  
  31. // GET users list from: "/users"
  32. users.load(function () {
  33. var firstUser = users.item(0);
  34.  
  35. firstUser.get('id'); // => "1"
  36.  
  37. // PUT updated user data at: "/users/1"
  38. firstUser.set('name', 'Eric').save();
  39. });
  40.  
  41. @class ModelSync.REST
  42. @extensionfor Model
  43. @extensionfor ModelList
  44. @since 3.6.0
  45. **/
  46. function RESTSync() {}
  47.  
  48. /**
  49. A request authenticity token to validate HTTP requests made by this extension
  50. with the server when the request results in changing persistent state. This
  51. allows you to protect your server from Cross-Site Request Forgery attacks.
  52.  
  53. A CSRF token provided by the server can be embedded in the HTML document and
  54. assigned to `YUI.Env.CSRF_TOKEN` like this:
  55.  
  56. <script>
  57. YUI.Env.CSRF_TOKEN = {{session.authenticityToken}};
  58. </script>
  59.  
  60. The above should come after YUI seed file so that `YUI.Env` will be defined.
  61.  
  62. **Note:** This can be overridden on a per-request basis. See `sync()` method.
  63.  
  64. When a value for the CSRF token is provided, either statically or via `options`
  65. passed to the `save()` and `destroy()` methods, the applicable HTTP requests
  66. will have a `X-CSRF-Token` header added with the token value.
  67.  
  68. @property CSRF_TOKEN
  69. @type String
  70. @default YUI.Env.CSRF_TOKEN
  71. @static
  72. @since 3.6.0
  73. **/
  74. RESTSync.CSRF_TOKEN = YUI.Env.CSRF_TOKEN;
  75.  
  76. /**
  77. Static flag to use the HTTP POST method instead of PUT or DELETE.
  78.  
  79. If the server-side HTTP framework isn't RESTful, setting this flag to `true`
  80. will cause all PUT and DELETE requests to instead use the POST HTTP method, and
  81. add a `X-HTTP-Method-Override` HTTP header with the value of the method type
  82. which was overridden.
  83.  
  84. @property EMULATE_HTTP
  85. @type Boolean
  86. @default false
  87. @static
  88. @since 3.6.0
  89. **/
  90. RESTSync.EMULATE_HTTP = false;
  91.  
  92. /**
  93. Default headers used with all XHRs.
  94.  
  95. By default the `Accept` and `Content-Type` headers are set to
  96. "application/json", this signals to the HTTP server to process the request
  97. bodies as JSON and send JSON responses. If you're sending and receiving content
  98. other than JSON, you can override these headers and the `parse()` and
  99. `serialize()` methods.
  100.  
  101. **Note:** These headers will be merged with any request-specific headers, and
  102. the request-specific headers will take precedence.
  103.  
  104. @property HTTP_HEADERS
  105. @type Object
  106. @default
  107. {
  108. "Accept" : "application/json",
  109. "Content-Type": "application/json"
  110. }
  111. @static
  112. @since 3.6.0
  113. **/
  114. RESTSync.HTTP_HEADERS = {
  115. 'Accept' : 'application/json',
  116. 'Content-Type': 'application/json'
  117. };
  118.  
  119. /**
  120. Static mapping of RESTful HTTP methods corresponding to CRUD actions.
  121.  
  122. @property HTTP_METHODS
  123. @type Object
  124. @default
  125. {
  126. "create": "POST",
  127. "read" : "GET",
  128. "update": "PUT",
  129. "delete": "DELETE"
  130. }
  131. @static
  132. @since 3.6.0
  133. **/
  134. RESTSync.HTTP_METHODS = {
  135. 'create': 'POST',
  136. 'read' : 'GET',
  137. 'update': 'PUT',
  138. 'delete': 'DELETE'
  139. };
  140.  
  141. /**
  142. The number of milliseconds before the XHRs will timeout/abort. This defaults to
  143. 30 seconds.
  144.  
  145. **Note:** This can be overridden on a per-request basis. See `sync()` method.
  146.  
  147. @property HTTP_TIMEOUT
  148. @type Number
  149. @default 30000
  150. @static
  151. @since 3.6.0
  152. **/
  153. RESTSync.HTTP_TIMEOUT = 30000;
  154.  
  155. /**
  156. Properties that shouldn't be turned into ad-hoc attributes when passed to a
  157. Model or ModelList constructor.
  158.  
  159. @property _NON_ATTRS_CFG
  160. @type Array
  161. @default ["root", "url"]
  162. @static
  163. @protected
  164. @since 3.6.0
  165. **/
  166. RESTSync._NON_ATTRS_CFG = ['root', 'url'];
  167.  
  168. RESTSync.prototype = {
  169.  
  170. // -- Public Properties ----------------------------------------------------
  171.  
  172. /**
  173. A string which represents the root or collection part of the URL which
  174. relates to a Model or ModelList. Usually this value should be same for all
  175. instances of a specific Model/ModelList subclass.
  176.  
  177. When sub-classing `Y.Model`, usually you'll only need to override this
  178. property, which lets the URLs for the XHRs be generated by convention. If
  179. the `root` string ends with a trailing-slash, XHR URLs will also end with a
  180. "/", and if the `root` does not end with a slash, neither will the XHR URLs.
  181.  
  182. @example
  183. Y.User = Y.Base.create('user', Y.Model, [Y.ModelSync.REST], {
  184. root: '/users'
  185. });
  186.  
  187. var currentUser, newUser;
  188.  
  189. // GET the user data from: "/users/123"
  190. currentUser = new Y.User({id: '123'}).load();
  191.  
  192. // POST the new user data to: "/users"
  193. newUser = new Y.User({name: 'Eric Ferraiuolo'}).save();
  194.  
  195. When sub-classing `Y.ModelList`, usually you'll want to ignore configuring
  196. the `root` and simply rely on the build-in convention of the list's
  197. generated URLs defaulting to the `root` specified by the list's `model`.
  198.  
  199. @property root
  200. @type String
  201. @default ""
  202. @since 3.6.0
  203. **/
  204. root: '',
  205.  
  206. /**
  207. A string which specifies the URL to use when making XHRs, if not value is
  208. provided, the URLs used to make XHRs will be generated by convention.
  209.  
  210. While a `url` can be provided for each Model/ModelList instance, usually
  211. you'll want to either rely on the default convention or provide a tokenized
  212. string on the prototype which can be used for all instances.
  213.  
  214. When sub-classing `Y.Model`, you will probably be able to rely on the
  215. default convention of generating URLs in conjunction with the `root`
  216. property and whether the model is new or not (i.e. has an `id`). If the
  217. `root` property ends with a trailing-slash, the generated URL for the
  218. specific model will also end with a trailing-slash.
  219.  
  220. @example
  221. Y.User = Y.Base.create('user', Y.Model, [Y.ModelSync.REST], {
  222. root: '/users/'
  223. });
  224.  
  225. var currentUser, newUser;
  226.  
  227. // GET the user data from: "/users/123/"
  228. currentUser = new Y.User({id: '123'}).load();
  229.  
  230. // POST the new user data to: "/users/"
  231. newUser = new Y.User({name: 'Eric Ferraiuolo'}).save();
  232.  
  233. If a `url` is specified, it will be processed by `Y.Lang.sub()`, which is
  234. useful when the URLs for a Model/ModelList subclass match a specific pattern
  235. and can use simple replacement tokens; e.g.:
  236.  
  237. @example
  238. Y.User = Y.Base.create('user', Y.Model, [Y.ModelSync.REST], {
  239. root: '/users',
  240. url : '/users/{username}'
  241. });
  242.  
  243. **Note:** String subsitituion of the `url` only use string an number values
  244. provided by this object's attribute and/or the `options` passed to the
  245. `getURL()` method. Do not expect something fancy to happen with Object,
  246. Array, or Boolean values, they will simply be ignored.
  247.  
  248. If your URLs have plural roots or collection URLs, while the specific item
  249. resources are under a singular name, e.g. "/users" (plural) and "/user/123"
  250. (singular), you'll probably want to configure the `root` and `url`
  251. properties like this:
  252.  
  253. @example
  254. Y.User = Y.Base.create('user', Y.Model, [Y.ModelSync.REST], {
  255. root: '/users',
  256. url : '/user/{id}'
  257. });
  258.  
  259. var currentUser, newUser;
  260.  
  261. // GET the user data from: "/user/123"
  262. currentUser = new Y.User({id: '123'}).load();
  263.  
  264. // POST the new user data to: "/users"
  265. newUser = new Y.User({name: 'Eric Ferraiuolo'}).save();
  266.  
  267. When sub-classing `Y.ModelList`, usually you'll be able to rely on the
  268. associated `model` to supply its `root` to be used as the model list's URL.
  269. If this needs to be customized, you can provide a simple string for the
  270. `url` property.
  271.  
  272. @example
  273. Y.Users = Y.Base.create('users', Y.ModelList, [Y.ModelSync.REST], {
  274. // Leverages `Y.User`'s `root`, which is "/users".
  275. model: Y.User
  276. });
  277.  
  278. // Or specified explicitly...
  279.  
  280. Y.Users = Y.Base.create('users', Y.ModelList, [Y.ModelSync.REST], {
  281. model: Y.User,
  282. url : '/users'
  283. });
  284.  
  285. @property url
  286. @type String
  287. @default ""
  288. @since 3.6.0
  289. **/
  290. url: '',
  291.  
  292. // -- Lifecycle Methods ----------------------------------------------------
  293.  
  294. initializer: function (config) {
  295. config || (config = {});
  296.  
  297. // Overrides `root` at the instance level.
  298. if ('root' in config) {
  299. this.root = config.root || '';
  300. }
  301.  
  302. // Overrides `url` at the instance level.
  303. if ('url' in config) {
  304. this.url = config.url || '';
  305. }
  306. },
  307.  
  308. // -- Public Methods -------------------------------------------------------
  309.  
  310. /**
  311. Returns the URL for this model or model list for the given `action` and
  312. `options`, if specified.
  313.  
  314. This method correctly handles the variations of `root` and `url` values and
  315. is called by the `sync()` method to get the URLs used to make the XHRs.
  316.  
  317. You can override this method if you need to provide a specific
  318. implementation for how the URLs of your Model and ModelList subclasses need
  319. to be generated.
  320.  
  321. @method getURL
  322. @param {String} [action] Optional `sync()` action for which to generate the
  323. URL.
  324. @param {Object} [options] Optional options which may be used to help
  325. generate the URL.
  326. @return {String} this model's or model list's URL for the the given
  327. `action` and `options`.
  328. @since 3.6.0
  329. **/
  330. getURL: function (action, options) {
  331. var root = this.root,
  332. url = this.url;
  333.  
  334. // If this is a model list, use its `url` and substitute placeholders,
  335. // but default to the `root` of its `model`. By convention a model's
  336. // `root` is the location to a collection resource.
  337. if (this._isYUIModelList) {
  338. if (!url) {
  339. return this.model.prototype.root;
  340. }
  341.  
  342. return this._substituteURL(url, Y.merge(this.getAttrs(), options));
  343. }
  344.  
  345. // Assume `this` is a model.
  346.  
  347. // When a model is new, i.e. has no `id`, the `root` should be used. By
  348. // convention a model's `root` is the location to a collection resource.
  349. // The model's `url` will be used as a fallback if `root` isn't defined.
  350. if (root && (action === 'create' || this.isNew())) {
  351. return root;
  352. }
  353.  
  354. // When a model's `url` is not provided, we'll generate a URL to use by
  355. // convention. This will combine the model's `id` with its configured
  356. // `root` and add a trailing-slash if the root ends with "/".
  357. if (!url) {
  358. return this._joinURL(this.getAsURL('id') || '');
  359. }
  360.  
  361. // Substitute placeholders in the `url` with URL-encoded values from the
  362. // model's attribute values or the specified `options`.
  363. return this._substituteURL(url, Y.merge(this.getAttrs(), options));
  364. },
  365.  
  366. /**
  367. Called to parse the response object returned from `Y.io()`. This method
  368. receives the full response object and is expected to "prep" a response which
  369. is suitable to pass to the `parse()` method.
  370.  
  371. By default the response body is returned (`responseText`), because it
  372. usually represents the entire entity of this model on the server.
  373.  
  374. If you need to parse data out of the response's headers you should do so by
  375. overriding this method. If you'd like the entire response object from the
  376. XHR to be passed to your `parse()` method, you can simply assign this
  377. property to `false`.
  378.  
  379. @method parseIOResponse
  380. @param {Object} response Response object from `Y.io()`.
  381. @return {Any} The modified response to pass along to the `parse()` method.
  382. @since 3.7.0
  383. **/
  384. parseIOResponse: function (response) {
  385. return response.responseText;
  386. },
  387.  
  388. /**
  389. Serializes `this` model to be used as the HTTP request entity body.
  390.  
  391. By default this model will be serialized to a JSON string via its `toJSON()`
  392. method.
  393.  
  394. You can override this method when the HTTP server expects a different
  395. representation of this model's data that is different from the default JSON
  396. serialization. If you're sending and receive content other than JSON, be
  397. sure change the `Accept` and `Content-Type` `HTTP_HEADERS` as well.
  398.  
  399. **Note:** A model's `toJSON()` method can also be overridden. If you only
  400. need to modify which attributes are serialized to JSON, that's a better
  401. place to start.
  402.  
  403. @method serialize
  404. @param {String} [action] Optional `sync()` action for which to generate the
  405. the serialized representation of this model.
  406. @return {String} serialized HTTP request entity body.
  407. @since 3.6.0
  408. **/
  409. serialize: function (action) {
  410. return Y.JSON.stringify(this);
  411. },
  412.  
  413. /**
  414. Communicates with a RESTful HTTP server by sending and receiving data via
  415. XHRs. This method is called internally by load(), save(), and destroy().
  416.  
  417. The URL used for each XHR will be retrieved by calling the `getURL()` method
  418. and passing it the specified `action` and `options`.
  419.  
  420. This method relies heavily on standard RESTful HTTP conventions
  421.  
  422. @method sync
  423. @param {String} action Sync action to perform. May be one of the following:
  424.  
  425. * `create`: Store a newly-created model for the first time.
  426. * `delete`: Delete an existing model.
  427. * `read` : Load an existing model.
  428. * `update`: Update an existing model.
  429.  
  430. @param {Object} [options] Sync options:
  431. @param {String} [options.csrfToken] The authenticity token used by the
  432. server to verify the validity of this request and protected against CSRF
  433. attacks. This overrides the default value provided by the static
  434. `CSRF_TOKEN` property.
  435. @param {Object} [options.headers] The HTTP headers to mix with the default
  436. headers specified by the static `HTTP_HEADERS` property.
  437. @param {Number} [options.timeout] The number of milliseconds before the
  438. request will timeout and be aborted. This overrides the default provided
  439. by the static `HTTP_TIMEOUT` property.
  440. @param {Function} [callback] Called when the sync operation finishes.
  441. @param {Error|null} callback.err If an error occurred, this parameter will
  442. contain the error. If the sync operation succeeded, _err_ will be
  443. falsy.
  444. @param {Any} [callback.response] The server's response.
  445. **/
  446. sync: function (action, options, callback) {
  447. options || (options = {});
  448.  
  449. var url = this.getURL(action, options),
  450. method = RESTSync.HTTP_METHODS[action],
  451. headers = Y.merge(RESTSync.HTTP_HEADERS, options.headers),
  452. timeout = options.timeout || RESTSync.HTTP_TIMEOUT,
  453. csrfToken = options.csrfToken || RESTSync.CSRF_TOKEN,
  454. entity;
  455.  
  456. // Prepare the content if we are sending data to the server.
  457. if (method === 'POST' || method === 'PUT') {
  458. entity = this.serialize(action);
  459. } else {
  460. // Remove header, no content is being sent.
  461. delete headers['Content-Type'];
  462. }
  463.  
  464. // Setup HTTP emulation for older servers if we need it.
  465. if (RESTSync.EMULATE_HTTP &&
  466. (method === 'PUT' || method === 'DELETE')) {
  467.  
  468. // Pass along original method type in the headers.
  469. headers['X-HTTP-Method-Override'] = method;
  470.  
  471. // Fall-back to using POST method type.
  472. method = 'POST';
  473. }
  474.  
  475. // Add CSRF token to HTTP request headers if one is specified and the
  476. // request will cause side effects on the server.
  477. if (csrfToken &&
  478. (method === 'POST' || method === 'PUT' || method === 'DELETE')) {
  479.  
  480. headers['X-CSRF-Token'] = csrfToken;
  481. }
  482.  
  483. this._sendSyncIORequest({
  484. action : action,
  485. callback: callback,
  486. entity : entity,
  487. headers : headers,
  488. method : method,
  489. timeout : timeout,
  490. url : url
  491. });
  492. },
  493.  
  494. // -- Protected Methods ----------------------------------------------------
  495.  
  496. /**
  497. Joins the `root` URL to the specified `url`, normalizing leading/trailing
  498. "/" characters.
  499.  
  500. @example
  501. model.root = '/foo'
  502. model._joinURL('bar'); // => '/foo/bar'
  503. model._joinURL('/bar'); // => '/foo/bar'
  504.  
  505. model.root = '/foo/'
  506. model._joinURL('bar'); // => '/foo/bar/'
  507. model._joinURL('/bar'); // => '/foo/bar/'
  508.  
  509. @method _joinURL
  510. @param {String} url URL to append to the `root` URL.
  511. @return {String} Joined URL.
  512. @protected
  513. @since 3.6.0
  514. **/
  515. _joinURL: function (url) {
  516. var root = this.root;
  517.  
  518. if (!(root || url)) {
  519. return '';
  520. }
  521.  
  522. if (url.charAt(0) === '/') {
  523. url = url.substring(1);
  524. }
  525.  
  526. // Combines the `root` with the `url` and adds a trailing-slash if the
  527. // `root` has a trailing-slash.
  528. return root && root.charAt(root.length - 1) === '/' ?
  529. root + url + '/' :
  530. root + '/' + url;
  531. },
  532.  
  533.  
  534. /**
  535. Calls both public, overrideable methods: `parseIOResponse()`, then `parse()`
  536. and returns the result.
  537.  
  538. This will call into `parseIOResponse()`, if it's defined as a method,
  539. passing it the full response object from the XHR and using its return value
  540. to pass along to the `parse()`. This enables developers to easily parse data
  541. out of the response headers which should be used by the `parse()` method.
  542.  
  543. @method _parse
  544. @param {Object} response Response object from `Y.io()`.
  545. @return {Object|Object[]} Attribute hash or Array of model attribute hashes.
  546. @protected
  547. @since 3.7.0
  548. **/
  549. _parse: function (response) {
  550. // When `parseIOResponse` is defined as a method, it will be invoked and
  551. // the result will become the new response object that the `parse()`
  552. // will be invoked with.
  553. if (typeof this.parseIOResponse === 'function') {
  554. response = this.parseIOResponse(response);
  555. }
  556.  
  557. return this.parse(response);
  558. },
  559.  
  560. /**
  561. Performs the XHR and returns the resulting `Y.io()` request object.
  562.  
  563. This method is called by `sync()`.
  564.  
  565. @method _sendSyncIORequest
  566. @param {Object} config An object with the following properties:
  567. @param {String} config.action The `sync()` action being performed.
  568. @param {Function} [config.callback] Called when the sync operation
  569. finishes.
  570. @param {String} [config.entity] The HTTP request entity body.
  571. @param {Object} config.headers The HTTP request headers.
  572. @param {String} config.method The HTTP request method.
  573. @param {Number} [config.timeout] Time until the HTTP request is aborted.
  574. @param {String} config.url The URL of the HTTP resource.
  575. @return {Object} The resulting `Y.io()` request object.
  576. @protected
  577. @since 3.6.0
  578. **/
  579. _sendSyncIORequest: function (config) {
  580. return Y.io(config.url, {
  581. 'arguments': {
  582. action : config.action,
  583. callback: config.callback,
  584. url : config.url
  585. },
  586.  
  587. context: this,
  588. data : config.entity,
  589. headers: config.headers,
  590. method : config.method,
  591. timeout: config.timeout,
  592.  
  593. on: {
  594. start : this._onSyncIOStart,
  595. failure: this._onSyncIOFailure,
  596. success: this._onSyncIOSuccess,
  597. end : this._onSyncIOEnd
  598. }
  599. });
  600. },
  601.  
  602. /**
  603. Utility which takes a tokenized `url` string and substitutes its
  604. placeholders using a specified `data` object.
  605.  
  606. This method will property URL-encode any values before substituting them.
  607. Also, only expect it to work with String and Number values.
  608.  
  609. @example
  610. var url = this._substituteURL('/users/{name}', {id: 'Eric F'});
  611. // => "/users/Eric%20F"
  612.  
  613. @method _substituteURL
  614. @param {String} url Tokenized URL string to substitute placeholder values.
  615. @param {Object} data Set of data to fill in the `url`'s placeholders.
  616. @return {String} Substituted URL.
  617. @protected
  618. @since 3.6.0
  619. **/
  620. _substituteURL: function (url, data) {
  621. if (!url) {
  622. return '';
  623. }
  624.  
  625. var values = {};
  626.  
  627. // Creates a hash of the string and number values only to be used to
  628. // replace any placeholders in a tokenized `url`.
  629. Y.Object.each(data, function (v, k) {
  630. if (Lang.isString(v) || Lang.isNumber(v)) {
  631. // URL-encode any string or number values.
  632. values[k] = encodeURIComponent(v);
  633. }
  634. });
  635.  
  636. return Lang.sub(url, values);
  637. },
  638.  
  639. // -- Event Handlers -------------------------------------------------------
  640.  
  641. /**
  642. Called when the `Y.io` request has finished, after "success" or "failure"
  643. has been determined.
  644.  
  645. This is a no-op by default, but provides a hook for overriding.
  646.  
  647. @method _onSyncIOEnd
  648. @param {String} txId The `Y.io` transaction id.
  649. @param {Object} details Extra details carried through from `sync()`:
  650. @param {String} details.action The sync action performed.
  651. @param {Function} [details.callback] The function to call after syncing.
  652. @param {String} details.url The URL of the requested resource.
  653. @protected
  654. @since 3.6.0
  655. **/
  656. _onSyncIOEnd: function (txId, details) {},
  657.  
  658. /**
  659. Called when the `Y.io` request has finished unsuccessfully.
  660.  
  661. By default this calls the `details.callback` function passing it the HTTP
  662. status code and message as an error object along with the response body.
  663.  
  664. @method _onSyncIOFailure
  665. @param {String} txId The `Y.io` transaction id.
  666. @param {Object} res The `Y.io` response object.
  667. @param {Object} details Extra details carried through from `sync()`:
  668. @param {String} details.action The sync action performed.
  669. @param {Function} [details.callback] The function to call after syncing.
  670. @param {String} details.url The URL of the requested resource.
  671. @protected
  672. @since 3.6.0
  673. **/
  674. _onSyncIOFailure: function (txId, res, details) {
  675. var callback = details.callback;
  676.  
  677. if (callback) {
  678. callback({
  679. code: res.status,
  680. msg : res.statusText
  681. }, res);
  682. }
  683. },
  684.  
  685. /**
  686. Called when the `Y.io` request has finished successfully.
  687.  
  688. By default this calls the `details.callback` function passing it the
  689. response body.
  690.  
  691. @method _onSyncIOSuccess
  692. @param {String} txId The `Y.io` transaction id.
  693. @param {Object} res The `Y.io` response object.
  694. @param {Object} details Extra details carried through from `sync()`:
  695. @param {String} details.action The sync action performed.
  696. @param {Function} [details.callback] The function to call after syncing.
  697. @param {String} details.url The URL of the requested resource.
  698. @protected
  699. @since 3.6.0
  700. **/
  701. _onSyncIOSuccess: function (txId, res, details) {
  702. var callback = details.callback;
  703.  
  704. if (callback) {
  705. callback(null, res);
  706. }
  707. },
  708.  
  709. /**
  710. Called when the `Y.io` request is made.
  711.  
  712. This is a no-op by default, but provides a hook for overriding.
  713.  
  714. @method _onSyncIOStart
  715. @param {String} txId The `Y.io` transaction id.
  716. @param {Object} details Extra details carried through from `sync()`:
  717. @param {String} details.action The sync action performed.
  718. @param {Function} [details.callback] The function to call after syncing.
  719. @param {String} details.url The URL of the requested resource.
  720. @protected
  721. @since 3.6.0
  722. **/
  723. _onSyncIOStart: function (txId, details) {}
  724. };
  725.  
  726. // -- Namespace ----------------------------------------------------------------
  727.  
  728. Y.namespace('ModelSync').REST = RESTSync;
  729.