1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4
5var { PromiseUtils } = ChromeUtils.import("resource://gre/modules/PromiseUtils.jsm");
6
7/*
8 * Asynchronous tools for handling calendar operations
9 */
10
11// NOTE: This module should not be loaded directly, it is available when
12// including calUtils.jsm under the cal.async namespace.
13
14const EXPORTED_SYMBOLS = ["calasync"]; /* exported calasync */
15
16var cIOL = Ci.calIOperationListener;
17var cIC = Ci.calICalendar;
18
19var promisifyProxyHandler = {
20  promiseOperation(target, name, args) {
21    let deferred = PromiseUtils.defer();
22    let listener = calasync.promiseOperationListener(deferred);
23    args.push(listener);
24    target[name](...args);
25    return deferred.promise;
26  },
27  get(target, name) {
28    switch (name) {
29      // calICalendar methods
30      case "adoptItem":
31      case "addItem":
32      case "modifyItem":
33      case "deleteItem":
34      case "getItem":
35      case "getItems":
36        return (...args) => this.promiseOperation(target, name, args);
37      // calIOfflineStorage methods
38      case "addOfflineItem":
39      case "modifyOfflineItem":
40      case "deleteOfflineItem":
41      case "getOfflineItemFlag":
42      case "resetItemOfflineFlag": {
43        let offline = target.QueryInterface(Ci.calIOfflineStorage);
44        return (...args) => this.promiseOperation(offline, name, args);
45      }
46
47      // Special getAllItems shortcut
48      case "getAllItems":
49        return () =>
50          this.promiseOperation(target, "getItems", [cIC.ITEM_FILTER_ALL_ITEMS, 0, null, null]);
51      case "proxyTarget":
52        return target;
53      default:
54        return target[name];
55    }
56  },
57};
58
59var calasync = {
60  /**
61   * Creates a proxy to the given calendar where the CRUD operations are replaced
62   * with versions that return a promise and don't take a listener.
63   *
64   * Before:
65   *   calendar.addItem(item, {
66   *     onGetResult: function() {},
67   *     onOperationComplete: function (c,status,t,c,detail) {
68   *       if (Components.isSuccessCode(status)) {
69   *         handleSuccess(detail);
70   *       } else {
71   *         handleFailure(status);
72   *       }
73   *     }
74   *   });
75   *
76   * After:
77   *   let pcal = promisifyCalendar(calendar);
78   *   pcal.addItem(item).then(handleSuccess, handleFailure);
79   *
80   * Bonus methods in addition:
81   *   pcal.getAllItems()  // alias for getItems without any filters
82   *
83   * IMPORTANT: Don't pass this around thinking its like an xpcom calICalendar,
84   * otherwise code might indefinitely wait for the listener to return or there
85   * will be complaints that an argument is missing.
86   */
87  promisifyCalendar(aCalendar) {
88    return new Proxy(aCalendar, promisifyProxyHandler);
89  },
90  /**
91   * Create an operation listener (calIOperationListener) that resolves when
92   * the operation succeeds. Note this listener will collect the items, so it
93   * might not be a good idea in a situation where a lot of items will be
94   * retrieved.
95   *
96   * Standalone Usage:
97   *   function promiseAddItem(aItem) {
98   *     let deferred = PromiseUtils.defer();
99   *     let listener = cal.async.promiseOperationListener(deferred);
100   *     aItem.calendar.addItem(aItem, listener);
101   *     return deferred.promise;
102   *   }
103   *
104   * See also promisifyCalendar, where the above can be replaced with:
105   *   function promiseAddItem(aItem) {
106   *     let calendar = cal.async.promisifyCalendar(aItem.calendar);
107   *     return calendar.addItem(aItem);
108   *   }
109   */
110  promiseOperationListener(deferred) {
111    return {
112      QueryInterface: ChromeUtils.generateQI(["calIOperationListener"]),
113      items: [],
114      itemStatus: Cr.NS_OK,
115      onGetResult(aCalendar, aStatus, aItemType, aDetail, aItems) {
116        this.itemStatus = aStatus;
117        if (Components.isSuccessCode(aStatus)) {
118          this.items = this.items.concat(aItems);
119        } else {
120          this.itemSuccess = aStatus;
121        }
122      },
123
124      onOperationComplete(aCalendar, aStatus, aOpType, aId, aDetail) {
125        if (!Components.isSuccessCode(aStatus)) {
126          // This function has failed, reject with the status
127          deferred.reject(aStatus);
128        } else if (!Components.isSuccessCode(this.itemStatus)) {
129          // onGetResult has failed, reject with its status
130          deferred.reject(this.itemStatus);
131        } else if (aOpType == cIOL.GET) {
132          // Success of a GET operation: resolve with array of
133          // resulting items.
134          deferred.resolve(this.items);
135        } else {
136          /* ADD,MODIFY,DELETE: resolve with 1 item */
137          // Success of an ADD MODIFY or DELETE operation, resolve
138          // with the one item that was processed.
139          deferred.resolve(aDetail);
140        }
141      },
142    };
143  },
144};
145