b7bf8931b6608b01c6cbcebaa576aebf0f829bf1
[WebKit-https.git] / Source / WebInspectorUI / UserInterface / Base / ObjectStore.js
1 /*
2  * Copyright (C) 2018 Apple Inc. All rights reserved.
3  *
4  * Redistribution and use in source and binary forms, with or without
5  * modification, are permitted provided that the following conditions
6  * are met:
7  * 1. Redistributions of source code must retain the above copyright
8  *    notice, this list of conditions and the following disclaimer.
9  * 2. Redistributions in binary form must reproduce the above copyright
10  *    notice, this list of conditions and the following disclaimer in the
11  *    documentation and/or other materials provided with the distribution.
12  *
13  * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
14  * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
15  * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
16  * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
17  * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
18  * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
19  * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
20  * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
21  * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
22  * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
23  * THE POSSIBILITY OF SUCH DAMAGE.
24  */
25
26 WI.ObjectStore = class ObjectStore
27 {
28     constructor(name, options = {})
29     {
30         this._name = name;
31         this._options = options;
32     }
33
34     // Static
35
36     static supported()
37     {
38         return (!window.InspectorTest || WI.ObjectStore.__testObjectStore) && window.indexedDB;
39     }
40
41     static get _databaseName()
42     {
43         let inspectionLevel = InspectorFrontendHost ? InspectorFrontendHost.inspectionLevel() : 1;
44         let levelString = (inspectionLevel > 1) ? "-" + inspectionLevel : "";
45         return "com.apple.WebInspector" + levelString;
46     }
47
48     static _open(callback)
49     {
50         if (WI.ObjectStore._database) {
51             callback(WI.ObjectStore._database);
52             return;
53         }
54
55         const version = 1; // Increment this for every edit to `WI.objectStores`.
56
57         let databaseRequest = indexedDB.open(WI.ObjectStore._databaseName, version);
58         databaseRequest.addEventListener("upgradeneeded", (event) => {
59             let database = databaseRequest.result;
60
61             let objectStores = Object.values(WI.objectStores);
62             if (WI.ObjectStore.__testObjectStore)
63                 objectStores.push(WI.ObjectStore.__testObjectStore);
64
65             let existingNames = new Set;
66             for (let objectStore of objectStores) {
67                 if (!database.objectStoreNames.contains(objectStore._name))
68                     database.createObjectStore(objectStore._name, objectStore._options);
69
70                 existingNames.add(objectStore._name);
71             }
72
73             for (let objectStoreName of database.objectStoreNames) {
74                 if (!existingNames.has(objectStoreName))
75                     database.deleteObjectStore(objectStoreName);
76             }
77         });
78         databaseRequest.addEventListener("success", (successEvent) => {
79             WI.ObjectStore._database = databaseRequest.result;
80             WI.ObjectStore._database.addEventListener("close", (closeEvent) => {
81                 WI.ObjectStore._database = null;
82             });
83
84             callback(WI.ObjectStore._database);
85         });
86     }
87
88     // Public
89
90     associateObject(object, key, value)
91     {
92         if (typeof value === "object")
93             value = this._resolveKeyPath(value, key).value;
94
95         let resolved = this._resolveKeyPath(object, key);
96         resolved.object[resolved.key] = value;
97     }
98
99     async getAll(...args)
100     {
101         if (!WI.ObjectStore.supported())
102             return undefined;
103
104         return this._operation("readonly", (objectStore) => objectStore.getAll(...args));
105     }
106
107     async add(...args)
108     {
109         if (!WI.ObjectStore.supported())
110             return undefined;
111
112         return this._operation("readwrite", (objectStore) => objectStore.add(...args));
113     }
114
115     async addObject(object, ...args)
116     {
117         if (!WI.ObjectStore.supported())
118             return undefined;
119
120         console.assert(typeof object.toJSON === "function", "ObjectStore cannot store an object without JSON serialization", object.constructor.name);
121         let result = await this.add(object.toJSON(), ...args);
122         this.associateObject(object, args[0], result);
123         return result;
124     }
125
126     async delete(...args)
127     {
128         if (!WI.ObjectStore.supported())
129             return undefined;
130
131         return this._operation("readwrite", (objectStore) => objectStore.delete(...args));
132     }
133
134     async deleteObject(object, ...args)
135     {
136         if (!WI.ObjectStore.supported())
137             return undefined;
138
139         return this.delete(this._resolveKeyPath(object).value, ...args);
140     }
141
142     // Private
143
144     _resolveKeyPath(object, keyPath)
145     {
146         keyPath = keyPath || this._options.keyPath || "";
147
148         let parts = keyPath.split(".");
149         let key = parts.splice(-1, 1);
150         while (parts.length) {
151             if (!object.hasOwnProperty(parts[0]))
152                 break;
153             object = object[parts.shift()];
154         }
155
156         if (parts.length)
157             key = parts.join(".") + "." + key;
158
159         return {
160             object,
161             key,
162             value: object[key],
163         };
164     }
165
166     async _operation(mode, func)
167     {
168         // IndexedDB transactions will auto-close if there are no active operations at the end of a
169         // microtask, so we need to do everything using event listeners instead of promises.
170         return new Promise((resolve, reject) => {
171             WI.ObjectStore._open((database) => {
172                 let transaction = database.transaction([this._name], mode);
173                 let objectStore = transaction.objectStore(this._name);
174                 let request = null;
175
176                 try {
177                     request = func(objectStore);
178                 } catch (e) {
179                     reject(e);
180                     return;
181                 }
182
183                 function listener(event) {
184                     transaction.removeEventListener("complete", listener);
185                     transaction.removeEventListener("error", listener);
186                     request.removeEventListener("success", listener);
187                     request.removeEventListener("error", listener);
188
189                     if (request.error) {
190                         reject(request.error);
191                         return;
192                     }
193
194                     resolve(request.result);
195                 }
196                 transaction.addEventListener("complete", listener, {once: true});
197                 transaction.addEventListener("error", listener, {once: true});
198                 request.addEventListener("success", listener, {once: true});
199                 request.addEventListener("error", listener, {once: true});
200             });
201         });
202     }
203 };
204
205 WI.ObjectStore._database = null;
206
207 // Be sure to update the `version` above when making changes.
208 WI.objectStores = {
209     audits: new WI.ObjectStore("audit-manager-tests", {keyPath: "__id", autoIncrement: true}),
210 };