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
26WI.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
205WI.ObjectStore._database = null;
206
207// Be sure to update the `version` above when making changes.
208WI.objectStores = {
209 audits: new WI.ObjectStore("audit-manager-tests", {keyPath: "__id", autoIncrement: true}),
210};