@babylonjs-toolkit/next 9.30.0 → 9.34.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (191) hide show
  1. package/lib/contentdownloadoutcome.d.ts +2 -0
  2. package/lib/contentdownloadoutcome.d.ts.map +1 -0
  3. package/lib/contentdownloadoutcome.js +1 -0
  4. package/lib/contentgraphicsoutcome.d.ts +2 -0
  5. package/lib/contentgraphicsoutcome.d.ts.map +1 -0
  6. package/lib/contentgraphicsoutcome.js +1 -0
  7. package/lib/contentgraphicsstatus.d.ts +2 -0
  8. package/lib/contentgraphicsstatus.d.ts.map +1 -0
  9. package/lib/contentgraphicsstatus.js +1 -0
  10. package/lib/contentinstaller.d.ts +245 -0
  11. package/lib/contentinstaller.d.ts.map +1 -0
  12. package/lib/contentinstaller.js +1929 -0
  13. package/lib/contentinstalleravailability.d.ts +2 -0
  14. package/lib/contentinstalleravailability.d.ts.map +1 -0
  15. package/lib/contentinstalleravailability.js +1 -0
  16. package/lib/contentinstallererrorkind.d.ts +2 -0
  17. package/lib/contentinstallererrorkind.d.ts.map +1 -0
  18. package/lib/contentinstallererrorkind.js +1 -0
  19. package/lib/contentinstallphase.d.ts +2 -0
  20. package/lib/contentinstallphase.d.ts.map +1 -0
  21. package/lib/contentinstallphase.js +1 -0
  22. package/lib/contentinstallpolicy.d.ts +2 -0
  23. package/lib/contentinstallpolicy.d.ts.map +1 -0
  24. package/lib/contentinstallpolicy.js +1 -0
  25. package/lib/contentupdatepolicy.d.ts +2 -0
  26. package/lib/contentupdatepolicy.d.ts.map +1 -0
  27. package/lib/contentupdatepolicy.js +1 -0
  28. package/lib/iafterspawned.d.ts +4 -0
  29. package/lib/iafterspawned.d.ts.map +1 -0
  30. package/lib/iafterspawned.js +1 -0
  31. package/lib/iaftertick.d.ts +4 -0
  32. package/lib/iaftertick.d.ts.map +1 -0
  33. package/lib/iaftertick.js +1 -0
  34. package/lib/iafterupdate.d.ts +4 -0
  35. package/lib/iafterupdate.d.ts.map +1 -0
  36. package/lib/iafterupdate.js +1 -0
  37. package/lib/ibeforetick.d.ts +4 -0
  38. package/lib/ibeforetick.d.ts.map +1 -0
  39. package/lib/ibeforetick.js +1 -0
  40. package/lib/ibeforeupdate.d.ts +4 -0
  41. package/lib/ibeforeupdate.d.ts.map +1 -0
  42. package/lib/ibeforeupdate.js +1 -0
  43. package/lib/icontentavailabilityblock.d.ts +6 -0
  44. package/lib/icontentavailabilityblock.d.ts.map +1 -0
  45. package/lib/icontentavailabilityblock.js +1 -0
  46. package/lib/icontentchannelmessage.d.ts +24 -0
  47. package/lib/icontentchannelmessage.d.ts.map +1 -0
  48. package/lib/icontentchannelmessage.js +1 -0
  49. package/lib/icontentdownloadjob.d.ts +7 -0
  50. package/lib/icontentdownloadjob.d.ts.map +1 -0
  51. package/lib/icontentdownloadjob.js +1 -0
  52. package/lib/icontentfileref.d.ts +9 -0
  53. package/lib/icontentfileref.d.ts.map +1 -0
  54. package/lib/icontentfileref.js +1 -0
  55. package/lib/icontentforwardedrequest.d.ts +9 -0
  56. package/lib/icontentforwardedrequest.d.ts.map +1 -0
  57. package/lib/icontentforwardedrequest.js +1 -0
  58. package/lib/icontentgraphicssession.d.ts +8 -0
  59. package/lib/icontentgraphicssession.d.ts.map +1 -0
  60. package/lib/icontentgraphicssession.js +1 -0
  61. package/lib/icontentgroupref.d.ts +6 -0
  62. package/lib/icontentgroupref.d.ts.map +1 -0
  63. package/lib/icontentgroupref.js +1 -0
  64. package/lib/icontentgroupstatus.d.ts +14 -0
  65. package/lib/icontentgroupstatus.d.ts.map +1 -0
  66. package/lib/icontentgroupstatus.js +1 -0
  67. package/lib/icontentinflightdownload.d.ts +8 -0
  68. package/lib/icontentinflightdownload.d.ts.map +1 -0
  69. package/lib/icontentinflightdownload.js +1 -0
  70. package/lib/icontentinstallcall.d.ts +6 -0
  71. package/lib/icontentinstallcall.d.ts.map +1 -0
  72. package/lib/icontentinstallcall.js +1 -0
  73. package/lib/icontentinstallererror.d.ts +7 -0
  74. package/lib/icontentinstallererror.d.ts.map +1 -0
  75. package/lib/icontentinstallererror.js +1 -0
  76. package/lib/icontentinstalleroptions.d.ts +20 -0
  77. package/lib/icontentinstalleroptions.d.ts.map +1 -0
  78. package/lib/icontentinstalleroptions.js +1 -0
  79. package/lib/icontentinstallerstate.d.ts +30 -0
  80. package/lib/icontentinstallerstate.d.ts.map +1 -0
  81. package/lib/icontentinstallerstate.js +1 -0
  82. package/lib/icontentinstalloptions.d.ts +5 -0
  83. package/lib/icontentinstalloptions.d.ts.map +1 -0
  84. package/lib/icontentinstalloptions.js +1 -0
  85. package/lib/icontentinstallprogress.d.ts +21 -0
  86. package/lib/icontentinstallprogress.d.ts.map +1 -0
  87. package/lib/icontentinstallprogress.js +1 -0
  88. package/lib/icontentmanifest.d.ts +10 -0
  89. package/lib/icontentmanifest.d.ts.map +1 -0
  90. package/lib/icontentmanifest.js +1 -0
  91. package/lib/icontentmanifestfile.d.ts +8 -0
  92. package/lib/icontentmanifestfile.d.ts.map +1 -0
  93. package/lib/icontentmanifestfile.js +1 -0
  94. package/lib/icontentmanifestgroup.d.ts +5 -0
  95. package/lib/icontentmanifestgroup.d.ts.map +1 -0
  96. package/lib/icontentmanifestgroup.js +1 -0
  97. package/lib/icontentpreparegraphicsoptions.d.ts +5 -0
  98. package/lib/icontentpreparegraphicsoptions.d.ts.map +1 -0
  99. package/lib/icontentpreparegraphicsoptions.js +1 -0
  100. package/lib/icontentprogresssession.d.ts +10 -0
  101. package/lib/icontentprogresssession.d.ts.map +1 -0
  102. package/lib/icontentprogresssession.js +1 -0
  103. package/lib/icontentretrywait.d.ts +6 -0
  104. package/lib/icontentretrywait.d.ts.map +1 -0
  105. package/lib/icontentretrywait.js +1 -0
  106. package/lib/icontentrootoptions.d.ts +6 -0
  107. package/lib/icontentrootoptions.d.ts.map +1 -0
  108. package/lib/icontentrootoptions.js +1 -0
  109. package/lib/icontentrootruntime.d.ts +8 -0
  110. package/lib/icontentrootruntime.d.ts.map +1 -0
  111. package/lib/icontentrootruntime.js +1 -0
  112. package/lib/icontentrootstate.d.ts +29 -0
  113. package/lib/icontentrootstate.d.ts.map +1 -0
  114. package/lib/icontentrootstate.js +1 -0
  115. package/lib/icontentstorageestimate.d.ts +8 -0
  116. package/lib/icontentstorageestimate.d.ts.map +1 -0
  117. package/lib/icontentstorageestimate.js +1 -0
  118. package/lib/icontentstoreresult.d.ts +8 -0
  119. package/lib/icontentstoreresult.d.ts.map +1 -0
  120. package/lib/icontentstoreresult.js +1 -0
  121. package/lib/icontentstorewaiter.d.ts +6 -0
  122. package/lib/icontentstorewaiter.d.ts.map +1 -0
  123. package/lib/icontentstorewaiter.js +1 -0
  124. package/lib/icontentupdateinfo.d.ts +8 -0
  125. package/lib/icontentupdateinfo.d.ts.map +1 -0
  126. package/lib/icontentupdateinfo.js +1 -0
  127. package/lib/idespawned.d.ts +5 -0
  128. package/lib/idespawned.d.ts.map +1 -0
  129. package/lib/idespawned.js +1 -0
  130. package/lib/iinputauthoritylost.d.ts +4 -0
  131. package/lib/iinputauthoritylost.d.ts.map +1 -0
  132. package/lib/iinputauthoritylost.js +1 -0
  133. package/lib/index.d.ts +141 -29
  134. package/lib/index.d.ts.map +1 -1
  135. package/lib/index.js +141 -29
  136. package/lib/iplayerjoined.d.ts +5 -0
  137. package/lib/iplayerjoined.d.ts.map +1 -0
  138. package/lib/iplayerjoined.js +1 -0
  139. package/lib/iplayerleft.d.ts +5 -0
  140. package/lib/iplayerleft.d.ts.map +1 -0
  141. package/lib/iplayerleft.js +1 -0
  142. package/lib/ispawned.d.ts +4 -0
  143. package/lib/ispawned.d.ts.map +1 -0
  144. package/lib/ispawned.js +1 -0
  145. package/lib/istateauthoritychanged.d.ts +4 -0
  146. package/lib/istateauthoritychanged.d.ts.map +1 -0
  147. package/lib/istateauthoritychanged.js +1 -0
  148. package/lib/loopbackdelivery.d.ts +6 -0
  149. package/lib/loopbackdelivery.d.ts.map +1 -0
  150. package/lib/loopbackdelivery.js +1 -0
  151. package/lib/loopbackhub.d.ts +206 -0
  152. package/lib/loopbackhub.d.ts.map +1 -0
  153. package/lib/loopbackhub.js +986 -0
  154. package/lib/loopbackhuboptions.d.ts +8 -0
  155. package/lib/loopbackhuboptions.d.ts.map +1 -0
  156. package/lib/loopbackhuboptions.js +1 -0
  157. package/lib/network_reconnect_grace_seconds.d.ts +2 -0
  158. package/lib/network_reconnect_grace_seconds.d.ts.map +1 -0
  159. package/lib/network_reconnect_grace_seconds.js +1 -0
  160. package/lib/networkstats.d.ts +51 -0
  161. package/lib/networkstats.d.ts.map +1 -0
  162. package/lib/networkstats.js +204 -0
  163. package/lib/networkstatssample.d.ts +7 -0
  164. package/lib/networkstatssample.d.ts.map +1 -0
  165. package/lib/networkstatssample.js +1 -0
  166. package/lib/networkstatssliderrow.d.ts +9 -0
  167. package/lib/networkstatssliderrow.d.ts.map +1 -0
  168. package/lib/networkstatssliderrow.js +1 -0
  169. package/lib/project.d.ts +2907 -133
  170. package/lib/project.d.ts.map +1 -1
  171. package/lib/project.js +14960 -3764
  172. package/lib/scenemanager.d.ts +4434 -51
  173. package/lib/scenemanager.d.ts.map +1 -1
  174. package/lib/scenemanager.js +18725 -1147
  175. package/lib/shadercodecachememorystorage.d.ts +10 -0
  176. package/lib/shadercodecachememorystorage.d.ts.map +1 -0
  177. package/lib/shadercodecachememorystorage.js +32 -0
  178. package/lib/workers/bt-content-worker.js +1180 -0
  179. package/lib/zigzagdecode.d.ts +2 -0
  180. package/lib/zigzagdecode.d.ts.map +1 -0
  181. package/lib/zigzagdecode.js +3 -0
  182. package/lib/zigzagencode.d.ts +2 -0
  183. package/lib/zigzagencode.d.ts.map +1 -0
  184. package/lib/zigzagencode.js +3 -0
  185. package/package.json +18 -13
  186. package/lib/entitycontroller.d.ts +0 -11
  187. package/lib/entitycontroller.d.ts.map +0 -1
  188. package/lib/entitycontroller.js +0 -41
  189. package/lib/sgshadowdepth.d.ts +0 -19
  190. package/lib/sgshadowdepth.d.ts.map +0 -1
  191. package/lib/sgshadowdepth.js +0 -214
@@ -0,0 +1,1929 @@
1
+ import { Observable } from '@babylonjs/core/Misc/observable';
2
+ export class ContentInstaller {
3
+ static Initialize(options) {
4
+ if (ContentInstaller.initializePromise != null)
5
+ return ContentInstaller.initializePromise;
6
+ ContentInstaller.initializePromise = ContentInstaller.runInitialize(options);
7
+ return ContentInstaller.initializePromise;
8
+ }
9
+ static GetState() {
10
+ return { ...ContentInstaller.state };
11
+ }
12
+ static GetGroupStatus(groupIds) {
13
+ return ContentInstaller.resolveGroups(groupIds, true).map((groupRef) => ContentInstaller.groupStatus(groupRef));
14
+ }
15
+ static GetGroupIds() {
16
+ const groupIds = [];
17
+ ContentInstaller.roots.forEach((root) => {
18
+ if (root.state.isAppShell || root.activeManifest == null)
19
+ return;
20
+ for (const groupId of Object.keys(root.activeManifest.groups)) {
21
+ if (groupIds.indexOf(groupId) < 0)
22
+ groupIds.push(groupId);
23
+ }
24
+ });
25
+ return groupIds;
26
+ }
27
+ static async EstimateInstall(groupIds) {
28
+ const missingFiles = ContentInstaller.missingFiles(ContentInstaller.groupFiles(ContentInstaller.resolveGroups(groupIds, true)));
29
+ return ContentInstaller.estimateFor(missingFiles);
30
+ }
31
+ static Install(groupIds, options) {
32
+ if (ContentInstaller.state.availability !== "available")
33
+ return Promise.resolve(false);
34
+ const background = (options != null && options.background === true);
35
+ const prepareGraphics = (options != null && options.prepareGraphics === true);
36
+ const requestedIds = ContentInstaller.normalizeGroupIds(groupIds);
37
+ if (!ContentInstaller.state.isLeader)
38
+ return ContentInstaller.forwardToLeader("install-request", requestedIds, background, prepareGraphics, false);
39
+ return ContentInstaller.installThenPrepareAsLeader(requestedIds, background, prepareGraphics, null);
40
+ }
41
+ static PrepareGraphicsAsync(groupIds, options) {
42
+ if (ContentInstaller.state.availability !== "available")
43
+ return Promise.resolve(false);
44
+ const background = (options != null && options.background === true);
45
+ const force = (options != null && options.force === true);
46
+ const requestedIds = ContentInstaller.normalizeGroupIds(groupIds);
47
+ if (!ContentInstaller.state.isLeader)
48
+ return ContentInstaller.forwardToLeader("prepare-graphics-request", requestedIds, background, false, force);
49
+ return ContentInstaller.prepareGraphicsAsLeader(requestedIds, background, force, null);
50
+ }
51
+ static async MarkGraphicsStale() {
52
+ if (!ContentInstaller.state.isLeader)
53
+ return;
54
+ for (const root of Array.from(ContentInstaller.roots.values())) {
55
+ const preparedIds = Object.keys(root.state.graphics).filter((groupId) => root.state.graphics[groupId].outcome === "prepared");
56
+ if (preparedIds.length === 0)
57
+ continue;
58
+ for (const groupId of preparedIds) {
59
+ const record = { ...root.state.graphics[groupId], gpuCacheLost: true };
60
+ root.state.graphics[groupId] = record;
61
+ ContentInstaller.postToChannel({ type: "graphics-recorded", rootKey: root.state.rootKey, groupId: groupId, graphicsRecord: record });
62
+ }
63
+ await ContentInstaller.writeRootState(root);
64
+ }
65
+ ContentInstaller.updateTotalsState();
66
+ }
67
+ static applyGpuCacheReport() {
68
+ if (!ContentInstaller.state.isLeader)
69
+ return;
70
+ try {
71
+ if (localStorage.getItem(ContentInstaller.GPU_CACHE_LOST_STORAGE_KEY) == null)
72
+ return;
73
+ localStorage.removeItem(ContentInstaller.GPU_CACHE_LOST_STORAGE_KEY);
74
+ }
75
+ catch {
76
+ return;
77
+ }
78
+ void ContentInstaller.MarkGraphicsStale();
79
+ }
80
+ static listenForGpuCacheReports() {
81
+ if (ContentInstaller.listensForGpuCacheReports || typeof window === "undefined")
82
+ return;
83
+ ContentInstaller.listensForGpuCacheReports = true;
84
+ window.addEventListener(ContentInstaller.GPU_CACHE_LOST_EVENT, ContentInstaller.applyGpuCacheReport);
85
+ }
86
+ static SetGraphicsPreparer(preparer) {
87
+ ContentInstaller.graphicsPreparer = (preparer != null) ? preparer : null;
88
+ ContentInstaller.graphicsUnavailable = false;
89
+ ContentInstaller.updateTotalsState();
90
+ }
91
+ static Pause() {
92
+ if (ContentInstaller.state.availability !== "available")
93
+ return;
94
+ if (!ContentInstaller.state.isLeader) {
95
+ ContentInstaller.postToChannel({ type: "pause" });
96
+ return;
97
+ }
98
+ if (ContentInstaller.state.paused)
99
+ return;
100
+ ContentInstaller.setState({ paused: true });
101
+ ContentInstaller.abortInFlight("pause");
102
+ }
103
+ static Resume() {
104
+ if (ContentInstaller.state.availability !== "available")
105
+ return;
106
+ if (!ContentInstaller.state.isLeader) {
107
+ ContentInstaller.postToChannel({ type: "resume" });
108
+ return;
109
+ }
110
+ ContentInstaller.setState({ paused: false });
111
+ ContentInstaller.pumpQueue();
112
+ }
113
+ static Cancel() {
114
+ if (ContentInstaller.state.availability !== "available")
115
+ return;
116
+ if (!ContentInstaller.state.isLeader) {
117
+ ContentInstaller.postToChannel({ type: "cancel" });
118
+ return;
119
+ }
120
+ ContentInstaller.queue = [];
121
+ ContentInstaller.clearRetryWaits();
122
+ ContentInstaller.abortInFlight("cancel");
123
+ ContentInstaller.cancelCount += 1;
124
+ if (ContentInstaller.graphicsRun != null)
125
+ ContentInstaller.graphicsCancelled = true;
126
+ ContentInstaller.wakeGraphicsWaiters();
127
+ const cancelledCalls = ContentInstaller.installCalls;
128
+ ContentInstaller.installCalls = [];
129
+ ContentInstaller.failedHashes.clear();
130
+ ContentInstaller.endSession();
131
+ ContentInstaller.setState({ paused: false });
132
+ void ContentInstaller.completeCalls(cancelledCalls, true);
133
+ }
134
+ static async Remove(groupIds) {
135
+ if (ContentInstaller.state.availability !== "available")
136
+ return;
137
+ const requestedIds = ContentInstaller.normalizeGroupIds(groupIds);
138
+ if (!ContentInstaller.state.isLeader) {
139
+ await ContentInstaller.forwardToLeader("remove-request", requestedIds, false, false, false);
140
+ return;
141
+ }
142
+ await ContentInstaller.removeAsLeader(requestedIds);
143
+ }
144
+ static async CheckForUpdates() {
145
+ if (ContentInstaller.state.availability !== "available")
146
+ return [];
147
+ for (const root of Array.from(ContentInstaller.roots.values())) {
148
+ const manifest = await ContentInstaller.fetchManifest(root.state.rootKey);
149
+ if (manifest == null)
150
+ continue;
151
+ await ContentInstaller.takeServerManifest(root, manifest);
152
+ }
153
+ if (ContentInstaller.state.isLeader)
154
+ await ContentInstaller.persistRoots(false);
155
+ ContentInstaller.updateTotalsState();
156
+ ContentInstaller.runUpdatePolicies();
157
+ return ContentInstaller.rootsWithPending().map((root) => ContentInstaller.updateInfo(root));
158
+ }
159
+ static DownloadUpdate() {
160
+ if (ContentInstaller.state.availability !== "available")
161
+ return Promise.resolve(false);
162
+ if (!ContentInstaller.state.isLeader)
163
+ return Promise.resolve(ContentInstaller.state.updateReady);
164
+ const contentRoots = ContentInstaller.rootsWithPending().filter((root) => !root.state.isAppShell);
165
+ return ContentInstaller.downloadUpdateFor(contentRoots);
166
+ }
167
+ static AcceptUpdate() {
168
+ if (ContentInstaller.state.availability !== "available")
169
+ return;
170
+ for (const root of ContentInstaller.rootsWithPending()) {
171
+ if (root.state.isAppShell || !ContentInstaller.isPendingReady(root))
172
+ continue;
173
+ root.state.applyOnNextLoad = true;
174
+ }
175
+ if (ContentInstaller.state.isLeader)
176
+ void ContentInstaller.persistRoots(false);
177
+ ContentInstaller.updateTotalsState();
178
+ }
179
+ static ApplyShellUpdate() {
180
+ ContentInstaller.roots.forEach((root) => {
181
+ if (root.state.isAppShell && root.pendingManifest != null)
182
+ root.state.applyOnNextLoad = true;
183
+ });
184
+ const reload = () => { location.reload(); };
185
+ const writes = ContentInstaller.state.isLeader ? ContentInstaller.persistRoots(false) : Promise.resolve();
186
+ void writes.then(() => {
187
+ const waitingWorker = (ContentInstaller.registration != null) ? ContentInstaller.registration.waiting : null;
188
+ if (waitingWorker == null) {
189
+ reload();
190
+ return;
191
+ }
192
+ navigator.serviceWorker.addEventListener("controllerchange", reload, { once: true });
193
+ waitingWorker.postMessage({ type: "skip-waiting" });
194
+ });
195
+ }
196
+ static async PrepareSceneLoad(sceneUrl) {
197
+ if (ContentInstaller.state.availability !== "available")
198
+ return false;
199
+ await ContentInstaller.refreshStoredHashes();
200
+ ContentInstaller.roots.forEach((root) => { ContentInstaller.recomputeInstalledGroups(root, false); });
201
+ let activated = false;
202
+ ContentInstaller.roots.forEach((root) => {
203
+ if (root.pendingManifest == null || !ContentInstaller.isPendingReady(root))
204
+ return;
205
+ const accepted = root.state.applyOnNextLoad || ContentInstaller.effectiveUpdatePolicy(root) === "automatic";
206
+ if (!accepted)
207
+ return;
208
+ ContentInstaller.activatePending(root);
209
+ activated = true;
210
+ });
211
+ if (ContentInstaller.state.isLeader)
212
+ await ContentInstaller.persistRoots(activated);
213
+ ContentInstaller.pinRoots();
214
+ const sceneGroup = ContentInstaller.findSceneGroup(sceneUrl);
215
+ if (sceneGroup != null)
216
+ ContentInstaller.prioritiseGroup(sceneGroup);
217
+ ContentInstaller.SetBusy(true);
218
+ ContentInstaller.updateTotalsState();
219
+ return ContentInstaller.IsSceneInstalled(sceneUrl);
220
+ }
221
+ static SetBusy(busy) {
222
+ ContentInstaller.busy = busy;
223
+ ContentInstaller.pumpQueue();
224
+ ContentInstaller.wakeGraphicsWaiters();
225
+ }
226
+ static IsSceneInstalled(sceneUrl) {
227
+ const sceneGroup = ContentInstaller.findSceneGroup(sceneUrl);
228
+ if (sceneGroup == null)
229
+ return false;
230
+ return sceneGroup.root.state.installedGroups.indexOf(sceneGroup.groupId) >= 0;
231
+ }
232
+ static async RequestPersistentStorage() {
233
+ let persisted = false;
234
+ try {
235
+ if (typeof navigator !== "undefined" && navigator.storage != null && typeof navigator.storage.persist === "function") {
236
+ persisted = await navigator.storage.persist();
237
+ }
238
+ }
239
+ catch {
240
+ persisted = false;
241
+ }
242
+ ContentInstaller.setState({ persisted: persisted });
243
+ return persisted;
244
+ }
245
+ static GroupIdFromSceneFile(fileName) {
246
+ const withoutQuery = ContentInstaller.stripQueryAndHash(fileName);
247
+ const lastSegment = withoutQuery.substring(withoutQuery.lastIndexOf("/") + 1).toLowerCase();
248
+ for (const suffix of ContentInstaller.SCENE_FILE_SUFFIXES) {
249
+ if (lastSegment.endsWith(suffix))
250
+ return lastSegment.substring(0, lastSegment.length - suffix.length);
251
+ }
252
+ return lastSegment;
253
+ }
254
+ static async runInitialize(options) {
255
+ const settings = ContentInstaller.normalizeOptions(options);
256
+ ContentInstaller.options = settings;
257
+ if (settings.graphicsPreparer != null)
258
+ ContentInstaller.graphicsPreparer = settings.graphicsPreparer;
259
+ ContentInstaller.tabStartTime = Date.now();
260
+ ContentInstaller.setState({ installPolicy: settings.installPolicy, updatePolicy: settings.updatePolicy });
261
+ const blocked = ContentInstaller.checkAvailability(settings);
262
+ if (blocked != null)
263
+ return ContentInstaller.stopWith(blocked.availability, blocked.reason);
264
+ const registered = await ContentInstaller.registerWorker(settings);
265
+ if (!registered)
266
+ return ContentInstaller.stopWith("unavailable", "the content worker could not be registered");
267
+ const controlled = await ContentInstaller.waitForController();
268
+ if (!controlled)
269
+ return ContentInstaller.stopWith("unavailable", "page not controlled (hard reload)");
270
+ ContentInstaller.openChannel();
271
+ await ContentInstaller.electLeader();
272
+ await ContentInstaller.loadRoots(settings.roots);
273
+ ContentInstaller.setState({ availability: "available", reason: null });
274
+ ContentInstaller.watchConnectivity();
275
+ if (ContentInstaller.state.isLeader)
276
+ ContentInstaller.startLeaderWork();
277
+ return ContentInstaller.GetState();
278
+ }
279
+ static normalizeOptions(options) {
280
+ return {
281
+ roots: (options.roots != null) ? options.roots : [],
282
+ workerUrl: options.workerUrl,
283
+ workerScope: options.workerScope,
284
+ installPolicy: (options.installPolicy != null) ? options.installPolicy : "as-you-play",
285
+ updatePolicy: (options.updatePolicy != null) ? options.updatePolicy : "prompt",
286
+ enableOnLocalHost: options.enableOnLocalHost === true,
287
+ graphicsPreparer: (options.graphicsPreparer != null) ? options.graphicsPreparer : null,
288
+ };
289
+ }
290
+ static checkAvailability(options) {
291
+ if (typeof navigator === "undefined" || !("serviceWorker" in navigator))
292
+ return { availability: "unavailable", reason: "this browser has no service workers" };
293
+ if (typeof caches === "undefined")
294
+ return { availability: "unavailable", reason: "this browser has no Cache Storage" };
295
+ if (typeof isSecureContext !== "undefined" && isSecureContext === false)
296
+ return { availability: "unavailable", reason: "the page is not a secure context" };
297
+ if (!options.enableOnLocalHost && ContentInstaller.isLocalHost(location.hostname))
298
+ return { availability: "disabled", reason: "local host (enableOnLocalHost is off)" };
299
+ if (options.installPolicy === "off")
300
+ return { availability: "disabled", reason: "the install policy is off" };
301
+ return null;
302
+ }
303
+ static async stopWith(availability, reason) {
304
+ if (availability === "disabled")
305
+ await ContentInstaller.unregisterWorkers();
306
+ ContentInstaller.setState({ availability: availability, reason: reason });
307
+ return ContentInstaller.GetState();
308
+ }
309
+ static async unregisterWorkers() {
310
+ if (typeof navigator === "undefined" || navigator.serviceWorker == null)
311
+ return;
312
+ try {
313
+ const registrations = await navigator.serviceWorker.getRegistrations();
314
+ for (const registration of registrations) {
315
+ const worker = registration.active || registration.waiting || registration.installing;
316
+ if (worker == null || !ContentInstaller.stripQueryAndHash(worker.scriptURL).endsWith(ContentInstaller.WORKER_SCRIPT_NAME))
317
+ continue;
318
+ await registration.unregister();
319
+ }
320
+ }
321
+ catch (error) {
322
+ console.warn("[ContentInstaller] Could not unregister the content worker: " + String(error));
323
+ }
324
+ }
325
+ static async registerWorker(options) {
326
+ const workerUrl = ContentInstaller.absoluteUrl(options.workerUrl);
327
+ const scope = (options.workerScope != null) ? options.workerScope : ContentInstaller.folderOf(workerUrl);
328
+ try {
329
+ const registration = await navigator.serviceWorker.register(workerUrl, { scope: scope });
330
+ ContentInstaller.registration = registration;
331
+ ContentInstaller.watchWorkerUpdates(registration);
332
+ return true;
333
+ }
334
+ catch (error) {
335
+ console.warn("[ContentInstaller] The content worker could not be registered: " + String(error));
336
+ return false;
337
+ }
338
+ }
339
+ static watchWorkerUpdates(registration) {
340
+ if (registration.waiting != null && navigator.serviceWorker.controller != null)
341
+ ContentInstaller.markWorkerUpdateWaiting();
342
+ if (typeof registration.addEventListener !== "function")
343
+ return;
344
+ registration.addEventListener("updatefound", () => {
345
+ const installing = registration.installing;
346
+ if (installing == null)
347
+ return;
348
+ installing.addEventListener("statechange", () => {
349
+ if (installing.state === "installed" && navigator.serviceWorker.controller != null)
350
+ ContentInstaller.markWorkerUpdateWaiting();
351
+ });
352
+ });
353
+ }
354
+ static markWorkerUpdateWaiting() {
355
+ ContentInstaller.workerUpdateWaiting = true;
356
+ ContentInstaller.setState({ shellUpdateReady: true });
357
+ }
358
+ static waitForController() {
359
+ const container = navigator.serviceWorker;
360
+ if (container.controller != null)
361
+ return Promise.resolve(true);
362
+ return new Promise((resolve) => {
363
+ let finished = false;
364
+ const finish = () => {
365
+ if (finished)
366
+ return;
367
+ finished = true;
368
+ clearTimeout(timer);
369
+ container.removeEventListener("controllerchange", finish);
370
+ resolve(container.controller != null);
371
+ };
372
+ const timer = setTimeout(finish, ContentInstaller.CONTROLLER_WAIT_MS);
373
+ container.addEventListener("controllerchange", finish);
374
+ });
375
+ }
376
+ static openChannel() {
377
+ if (typeof BroadcastChannel === "undefined")
378
+ return;
379
+ ContentInstaller.channel = new BroadcastChannel(ContentInstaller.BROADCAST_CHANNEL_NAME);
380
+ ContentInstaller.channel.onmessage = (event) => { ContentInstaller.handleChannelMessage(event.data); };
381
+ }
382
+ static async electLeader() {
383
+ const locks = (typeof navigator !== "undefined") ? navigator.locks : undefined;
384
+ if (locks == null || typeof locks.request !== "function") {
385
+ ContentInstaller.setState({ isLeader: true });
386
+ return;
387
+ }
388
+ const won = await new Promise((resolve) => {
389
+ locks.request(ContentInstaller.LOCK_NAME, { ifAvailable: true }, (lock) => {
390
+ resolve(lock != null);
391
+ return (lock != null) ? ContentInstaller.holdLeadership() : undefined;
392
+ }).catch(() => { resolve(true); });
393
+ });
394
+ if (won) {
395
+ ContentInstaller.setState({ isLeader: true });
396
+ return;
397
+ }
398
+ void locks.request(ContentInstaller.LOCK_NAME, () => {
399
+ void ContentInstaller.onLeadershipGranted();
400
+ return ContentInstaller.holdLeadership();
401
+ });
402
+ }
403
+ static holdLeadership() {
404
+ return new Promise(() => undefined);
405
+ }
406
+ static async onLeadershipGranted() {
407
+ ContentInstaller.setState({ isLeader: true });
408
+ if (ContentInstaller.state.availability !== "available")
409
+ return;
410
+ for (const root of Array.from(ContentInstaller.roots.values())) {
411
+ const persisted = await ContentInstaller.readRootState(root.state.rootKey);
412
+ if (persisted == null)
413
+ continue;
414
+ root.state.requestedGroups = ContentInstaller.union(root.state.requestedGroups, persisted.requestedGroups);
415
+ root.state.graphics = Object.assign({}, root.state.graphics, persisted.graphics || {});
416
+ }
417
+ await ContentInstaller.refreshStoredHashes();
418
+ ContentInstaller.roots.forEach((root) => { ContentInstaller.recomputeInstalledGroups(root, false); });
419
+ await ContentInstaller.persistRoots(false);
420
+ ContentInstaller.updateTotalsState();
421
+ ContentInstaller.replayForwardedRequests();
422
+ ContentInstaller.startLeaderWork();
423
+ }
424
+ static replayForwardedRequests() {
425
+ const forwarded = Array.from(ContentInstaller.forwardedRequests.values());
426
+ ContentInstaller.forwardedRequests.clear();
427
+ for (const request of forwarded) {
428
+ if (request.kind === "remove-request") {
429
+ void ContentInstaller.removeAsLeader(request.groupIds).then(() => { request.resolve(true); });
430
+ continue;
431
+ }
432
+ if (request.kind === "prepare-graphics-request") {
433
+ void ContentInstaller.prepareGraphicsAsLeader(request.groupIds, request.background, request.force, null).then(request.resolve);
434
+ continue;
435
+ }
436
+ void ContentInstaller.installThenPrepareAsLeader(request.groupIds, request.background, request.prepareGraphics, null).then(request.resolve);
437
+ }
438
+ }
439
+ static startLeaderWork() {
440
+ ContentInstaller.listenForGpuCacheReports();
441
+ ContentInstaller.applyGpuCacheReport();
442
+ ContentInstaller.resumeRequestedGroups();
443
+ if (ContentInstaller.state.installPolicy === "everything")
444
+ void ContentInstaller.installAsLeader(null, false, null, true);
445
+ ContentInstaller.runUpdatePolicies();
446
+ }
447
+ static resumeRequestedGroups() {
448
+ const groupIds = [];
449
+ ContentInstaller.roots.forEach((root) => {
450
+ for (const groupId of root.state.requestedGroups) {
451
+ if (root.state.installedGroups.indexOf(groupId) < 0 && groupIds.indexOf(groupId) < 0)
452
+ groupIds.push(groupId);
453
+ }
454
+ });
455
+ if (groupIds.length > 0)
456
+ void ContentInstaller.installAsLeader(groupIds, false, null, true);
457
+ }
458
+ static watchConnectivity() {
459
+ if (typeof window === "undefined" || typeof window.addEventListener !== "function")
460
+ return;
461
+ window.addEventListener("online", () => { void ContentInstaller.CheckForUpdates(); });
462
+ }
463
+ static async loadRoots(rootOptions) {
464
+ for (const options of rootOptions) {
465
+ const root = await ContentInstaller.loadRoot(options);
466
+ if (root != null)
467
+ ContentInstaller.roots.set(root.state.rootKey, root);
468
+ }
469
+ await ContentInstaller.refreshStoredHashes();
470
+ let activated = false;
471
+ ContentInstaller.roots.forEach((root) => {
472
+ ContentInstaller.recomputeInstalledGroups(root, true);
473
+ if (ContentInstaller.applyStartupUpdate(root))
474
+ activated = true;
475
+ });
476
+ ContentInstaller.rebuildGroupIndex();
477
+ if (ContentInstaller.state.isLeader)
478
+ await ContentInstaller.persistRoots(activated);
479
+ ContentInstaller.pinRoots();
480
+ ContentInstaller.updateTotalsState();
481
+ }
482
+ static async loadRoot(options) {
483
+ const rootKey = ContentInstaller.absoluteUrl(options.manifestUrl);
484
+ const manifest = await ContentInstaller.fetchManifest(rootKey);
485
+ if (manifest == null) {
486
+ ContentInstaller.reportError("manifest", "[ContentInstaller] No content manifest at " + rootKey + ": content loads from the network.", []);
487
+ return null;
488
+ }
489
+ const baseUrl = ContentInstaller.ensureTrailingSlash((options.baseUrl != null) ? ContentInstaller.absoluteUrl(options.baseUrl) : ContentInstaller.folderOf(rootKey));
490
+ const persisted = await ContentInstaller.readRootState(rootKey);
491
+ const root = { state: (persisted != null) ? persisted : ContentInstaller.createRootState(rootKey), activeManifest: null, pendingManifest: null };
492
+ root.state.baseUrl = baseUrl;
493
+ root.state.isAppShell = options.isAppShell === true;
494
+ root.state.installPolicy = ContentInstaller.state.installPolicy;
495
+ root.state.networkOnly = (root.state.networkOnly != null) ? root.state.networkOnly : null;
496
+ root.state.graphics = (root.state.graphics != null) ? root.state.graphics : {};
497
+ await ContentInstaller.restoreManifests(root, manifest);
498
+ if (ContentInstaller.state.isLeader)
499
+ await ContentInstaller.storeManifest(rootKey, manifest);
500
+ return root;
501
+ }
502
+ static async restoreManifests(root, serverManifest) {
503
+ const activeManifest = await ContentInstaller.findActiveManifest(root, serverManifest);
504
+ if (activeManifest == null) {
505
+ ContentInstaller.makeActive(root, serverManifest);
506
+ return;
507
+ }
508
+ root.activeManifest = activeManifest;
509
+ await ContentInstaller.takeServerManifest(root, serverManifest);
510
+ }
511
+ static async findActiveManifest(root, serverManifest) {
512
+ const activeVersion = root.state.activeVersion;
513
+ if (activeVersion == null)
514
+ return null;
515
+ if (activeVersion === serverManifest.version)
516
+ return serverManifest;
517
+ return ContentInstaller.readStoredManifest(root.state.rootKey, activeVersion);
518
+ }
519
+ static async takeServerManifest(root, serverManifest) {
520
+ const networkOnly = root.state.networkOnly;
521
+ if (networkOnly != null && networkOnly.serverVersion !== serverManifest.version)
522
+ root.state.networkOnly = null;
523
+ if (serverManifest.version === root.state.activeVersion) {
524
+ root.pendingManifest = null;
525
+ root.state.pendingVersion = null;
526
+ root.state.applyOnNextLoad = false;
527
+ return;
528
+ }
529
+ if (serverManifest.version !== root.state.pendingVersion) {
530
+ root.state.pendingVersion = serverManifest.version;
531
+ root.state.applyOnNextLoad = false;
532
+ }
533
+ root.pendingManifest = serverManifest;
534
+ if (ContentInstaller.state.isLeader)
535
+ await ContentInstaller.storeManifest(root.state.rootKey, serverManifest);
536
+ const nothingInstalled = root.state.installedGroups.length === 0;
537
+ if (nothingInstalled && ContentInstaller.progress == null)
538
+ ContentInstaller.activatePending(root);
539
+ }
540
+ static applyStartupUpdate(root) {
541
+ if (root.pendingManifest == null)
542
+ return false;
543
+ const nothingInstalled = root.state.installedGroups.length === 0;
544
+ const accepted = root.state.applyOnNextLoad || ContentInstaller.effectiveUpdatePolicy(root) === "automatic";
545
+ if (!nothingInstalled && !(accepted && ContentInstaller.isPendingReady(root)))
546
+ return false;
547
+ ContentInstaller.activatePending(root);
548
+ return true;
549
+ }
550
+ static makeActive(root, manifest) {
551
+ root.activeManifest = manifest;
552
+ root.state.activeVersion = manifest.version;
553
+ root.pendingManifest = null;
554
+ root.state.pendingVersion = null;
555
+ root.state.applyOnNextLoad = false;
556
+ const networkOnly = root.state.networkOnly;
557
+ if (networkOnly != null && networkOnly.reason === "update-pending" && networkOnly.serverVersion === manifest.version)
558
+ root.state.networkOnly = null;
559
+ }
560
+ static activatePending(root) {
561
+ const previousVersion = root.state.activeVersion;
562
+ ContentInstaller.makeActive(root, root.pendingManifest);
563
+ ContentInstaller.retargetQueuedJobs(root, previousVersion);
564
+ ContentInstaller.rebuildGroupIndex();
565
+ ContentInstaller.recomputeInstalledGroups(root, false);
566
+ }
567
+ static retargetQueuedJobs(root, previousVersion) {
568
+ const manifest = root.activeManifest;
569
+ const kept = [];
570
+ for (const job of ContentInstaller.queue) {
571
+ if (job.rootKey !== root.state.rootKey || job.version !== previousVersion) {
572
+ kept.push(job);
573
+ continue;
574
+ }
575
+ const entry = manifest.files[job.path];
576
+ if (entry != null && entry.hash === job.hash) {
577
+ job.version = manifest.version;
578
+ kept.push(job);
579
+ continue;
580
+ }
581
+ ContentInstaller.failedHashes.add(job.hash);
582
+ }
583
+ ContentInstaller.queue = kept;
584
+ ContentInstaller.settleFinishedCalls();
585
+ }
586
+ static async fetchManifest(url) {
587
+ try {
588
+ const response = await fetch(url, { cache: "no-store" });
589
+ if (!response.ok)
590
+ return null;
591
+ const manifest = await response.json();
592
+ return ContentInstaller.isValidManifest(manifest) ? manifest : null;
593
+ }
594
+ catch {
595
+ return null;
596
+ }
597
+ }
598
+ static isValidManifest(manifest) {
599
+ if (manifest == null || typeof manifest !== "object")
600
+ return false;
601
+ if (manifest.format !== ContentInstaller.SUPPORTED_MANIFEST_FORMAT || typeof manifest.version !== "string")
602
+ return false;
603
+ return manifest.files != null && typeof manifest.files === "object" && manifest.groups != null && typeof manifest.groups === "object";
604
+ }
605
+ static effectiveUpdatePolicy(root) {
606
+ return root.state.isAppShell ? "automatic" : ContentInstaller.state.updatePolicy;
607
+ }
608
+ static storeKey(path) {
609
+ return new URL(ContentInstaller.STORE_KEY_PREFIX + path, location.origin).href;
610
+ }
611
+ static fileKey(hash) {
612
+ return ContentInstaller.storeKey("files/" + hash);
613
+ }
614
+ static rootStateKey(rootKey) {
615
+ return ContentInstaller.storeKey("roots/" + encodeURIComponent(rootKey));
616
+ }
617
+ static manifestKey(rootKey, version) {
618
+ return ContentInstaller.storeKey("manifests/" + encodeURIComponent(rootKey) + "/" + version);
619
+ }
620
+ static async readStateJson(key) {
621
+ try {
622
+ const cache = await caches.open(ContentInstaller.CONTENT_STATE_CACHE);
623
+ const response = await cache.match(key);
624
+ return (response != null) ? await response.json() : null;
625
+ }
626
+ catch {
627
+ return null;
628
+ }
629
+ }
630
+ static async writeStateJson(key, value) {
631
+ try {
632
+ const cache = await caches.open(ContentInstaller.CONTENT_STATE_CACHE);
633
+ await cache.put(key, new Response(JSON.stringify(value), { headers: { "Content-Type": "application/json" } }));
634
+ }
635
+ catch (error) {
636
+ console.warn("[ContentInstaller] Could not write installer state: " + String(error));
637
+ }
638
+ }
639
+ static readRootState(rootKey) {
640
+ return ContentInstaller.readStateJson(ContentInstaller.rootStateKey(rootKey));
641
+ }
642
+ static writeRootState(root) {
643
+ root.state.updatedAt = ContentInstaller.Clock();
644
+ return ContentInstaller.writeStateJson(ContentInstaller.rootStateKey(root.state.rootKey), root.state);
645
+ }
646
+ static async readStoredManifest(rootKey, version) {
647
+ const manifest = await ContentInstaller.readStateJson(ContentInstaller.manifestKey(rootKey, version));
648
+ return ContentInstaller.isValidManifest(manifest) ? manifest : null;
649
+ }
650
+ static storeManifest(rootKey, manifest) {
651
+ return ContentInstaller.writeStateJson(ContentInstaller.manifestKey(rootKey, manifest.version), manifest);
652
+ }
653
+ static async persistRoots(collectGarbage) {
654
+ for (const root of Array.from(ContentInstaller.roots.values()))
655
+ await ContentInstaller.writeRootState(root);
656
+ const indexKey = ContentInstaller.storeKey("roots-index");
657
+ const existing = await ContentInstaller.readStateJson(indexKey);
658
+ await ContentInstaller.writeStateJson(indexKey, ContentInstaller.union(Array.isArray(existing) ? existing : [], Array.from(ContentInstaller.roots.keys())));
659
+ ContentInstaller.postToWorker({ type: "roots-changed" });
660
+ if (collectGarbage)
661
+ ContentInstaller.postToWorker({ type: "collect-garbage" });
662
+ }
663
+ static async refreshStoredHashes() {
664
+ try {
665
+ const cache = await caches.open(ContentInstaller.CONTENT_FILES_CACHE);
666
+ const requests = await cache.keys();
667
+ const prefix = ContentInstaller.fileKey("");
668
+ const hashes = new Set();
669
+ for (const request of requests) {
670
+ if (request.url.startsWith(prefix))
671
+ hashes.add(request.url.substring(prefix.length));
672
+ }
673
+ ContentInstaller.storedHashes = hashes;
674
+ }
675
+ catch (error) {
676
+ console.warn("[ContentInstaller] Could not read the content store: " + String(error));
677
+ }
678
+ }
679
+ static async isHashStored(hash) {
680
+ try {
681
+ const cache = await caches.open(ContentInstaller.CONTENT_FILES_CACHE);
682
+ return (await cache.match(ContentInstaller.fileKey(hash))) != null;
683
+ }
684
+ catch {
685
+ return false;
686
+ }
687
+ }
688
+ static rebuildGroupIndex() {
689
+ const index = new Map();
690
+ ContentInstaller.roots.forEach((root) => {
691
+ if (root.activeManifest == null)
692
+ return;
693
+ for (const [groupId, group] of Object.entries(root.activeManifest.groups)) {
694
+ for (const path of group.files) {
695
+ const entry = root.activeManifest.files[path];
696
+ if (entry == null)
697
+ continue;
698
+ const groupRefs = index.get(entry.hash) || [];
699
+ groupRefs.push({ root: root, groupId: groupId });
700
+ index.set(entry.hash, groupRefs);
701
+ }
702
+ }
703
+ });
704
+ ContentInstaller.groupsByHash = index;
705
+ }
706
+ static isGroupStored(manifest, groupId) {
707
+ const group = manifest.groups[groupId];
708
+ if (group == null)
709
+ return false;
710
+ return group.files.every((path) => {
711
+ const entry = manifest.files[path];
712
+ return entry == null || ContentInstaller.storedHashes.has(entry.hash);
713
+ });
714
+ }
715
+ static recomputeInstalledGroups(root, reportEviction) {
716
+ const manifest = root.activeManifest;
717
+ if (manifest == null)
718
+ return;
719
+ const previous = root.state.installedGroups || [];
720
+ const installed = Object.keys(manifest.groups).filter((groupId) => ContentInstaller.isGroupStored(manifest, groupId));
721
+ root.state.installedGroups = installed;
722
+ if (!reportEviction)
723
+ return;
724
+ const evicted = previous.filter((groupId) => manifest.groups[groupId] != null && installed.indexOf(groupId) < 0);
725
+ if (evicted.length === 0)
726
+ return;
727
+ root.state.requestedGroups = (root.state.requestedGroups || []).filter((groupId) => evicted.indexOf(groupId) < 0);
728
+ const missingPaths = ContentInstaller.missingFiles(ContentInstaller.groupFiles(evicted.map((groupId) => ({ root: root, groupId: groupId }))))
729
+ .map((file) => file.path);
730
+ ContentInstaller.reportError("evicted", "[ContentInstaller] The browser removed stored files of " + evicted.join(", ") + ": they load from the network until installed again.", missingPaths);
731
+ }
732
+ static onFileStored(hash) {
733
+ ContentInstaller.storedHashes.add(hash);
734
+ ContentInstaller.settleStoreWaiters(hash, ContentInstaller.storeResult("stored", null, null, null));
735
+ ContentInstaller.removeQueuedJob(hash);
736
+ const changedRoots = new Set();
737
+ for (const groupRef of ContentInstaller.groupsByHash.get(hash) || []) {
738
+ const installedGroups = groupRef.root.state.installedGroups;
739
+ if (installedGroups.indexOf(groupRef.groupId) >= 0)
740
+ continue;
741
+ if (!ContentInstaller.isGroupStored(groupRef.root.activeManifest, groupRef.groupId))
742
+ continue;
743
+ installedGroups.push(groupRef.groupId);
744
+ changedRoots.add(groupRef.root);
745
+ }
746
+ if (changedRoots.size > 0) {
747
+ if (ContentInstaller.state.isLeader)
748
+ changedRoots.forEach((root) => { void ContentInstaller.writeRootState(root); });
749
+ ContentInstaller.updateTotalsState();
750
+ }
751
+ ContentInstaller.settleFinishedCalls();
752
+ ContentInstaller.emitProgress(false);
753
+ ContentInstaller.finishSessionIfDrained();
754
+ }
755
+ static normalizeGroupIds(groupIds) {
756
+ return (groupIds != null && groupIds.length > 0) ? groupIds.slice() : null;
757
+ }
758
+ static resolveGroups(groupIds, includeShell) {
759
+ const groupRefs = [];
760
+ const everyGroup = (groupIds == null || groupIds.length === 0);
761
+ if (everyGroup) {
762
+ ContentInstaller.roots.forEach((root) => {
763
+ if (root.activeManifest == null || (root.state.isAppShell && !includeShell))
764
+ return;
765
+ for (const groupId of Object.keys(root.activeManifest.groups))
766
+ groupRefs.push({ root: root, groupId: groupId });
767
+ });
768
+ return groupRefs;
769
+ }
770
+ for (const groupId of groupIds) {
771
+ ContentInstaller.roots.forEach((root) => {
772
+ if (root.activeManifest != null && root.activeManifest.groups[groupId] != null)
773
+ groupRefs.push({ root: root, groupId: groupId });
774
+ });
775
+ }
776
+ return groupRefs;
777
+ }
778
+ static groupFiles(groupRefs) {
779
+ const files = [];
780
+ for (const groupRef of groupRefs)
781
+ ContentInstaller.appendGroupFiles(files, groupRef.root.state.rootKey, groupRef.root.activeManifest, groupRef.groupId);
782
+ return files;
783
+ }
784
+ static appendGroupFiles(files, rootKey, manifest, groupId) {
785
+ const group = manifest.groups[groupId];
786
+ if (group == null)
787
+ return;
788
+ for (const path of group.files) {
789
+ const entry = manifest.files[path];
790
+ if (entry != null)
791
+ files.push({ rootKey: rootKey, version: manifest.version, path: path, entry: entry, groupId: groupId });
792
+ }
793
+ }
794
+ static missingFiles(files) {
795
+ const seen = new Set();
796
+ return files.filter((file) => {
797
+ const hash = file.entry.hash;
798
+ if (seen.has(hash) || ContentInstaller.storedHashes.has(hash))
799
+ return false;
800
+ seen.add(hash);
801
+ return true;
802
+ });
803
+ }
804
+ static groupStatus(groupRef) {
805
+ const files = ContentInstaller.groupFiles([groupRef]);
806
+ const storedFiles = files.filter((file) => ContentInstaller.storedHashes.has(file.entry.hash));
807
+ const graphics = ContentInstaller.graphicsStatusOf(groupRef);
808
+ return {
809
+ rootKey: groupRef.root.state.rootKey,
810
+ groupId: groupRef.groupId,
811
+ installed: groupRef.root.state.installedGroups.indexOf(groupRef.groupId) >= 0,
812
+ totalBytes: ContentInstaller.sumSizes(files),
813
+ storedBytes: ContentInstaller.sumSizes(storedFiles),
814
+ fileCount: files.length,
815
+ storedFileCount: storedFiles.length,
816
+ graphics: graphics,
817
+ graphicsPrepared: graphics === "prepared",
818
+ graphicsStale: graphics === "stale",
819
+ };
820
+ }
821
+ static sumSizes(files) {
822
+ const seen = new Set();
823
+ let total = 0;
824
+ for (const file of files) {
825
+ if (seen.has(file.entry.hash))
826
+ continue;
827
+ seen.add(file.entry.hash);
828
+ total += file.entry.size;
829
+ }
830
+ return total;
831
+ }
832
+ static findSceneGroup(sceneUrl) {
833
+ const absolute = ContentInstaller.stripQueryAndHash(ContentInstaller.absoluteUrl(sceneUrl));
834
+ let bestRoot = null;
835
+ for (const root of Array.from(ContentInstaller.roots.values())) {
836
+ if (root.activeManifest == null || !absolute.startsWith(root.state.baseUrl))
837
+ continue;
838
+ if (bestRoot == null || root.state.baseUrl.length > bestRoot.state.baseUrl.length)
839
+ bestRoot = root;
840
+ }
841
+ if (bestRoot == null)
842
+ return null;
843
+ const manifest = bestRoot.activeManifest;
844
+ const relativePath = decodeURIComponent(absolute.substring(bestRoot.state.baseUrl.length));
845
+ for (const [groupId, group] of Object.entries(manifest.groups)) {
846
+ if (group.scene === relativePath)
847
+ return { root: bestRoot, groupId: groupId };
848
+ }
849
+ const groupId = ContentInstaller.GroupIdFromSceneFile(absolute);
850
+ return (manifest.groups[groupId] != null) ? { root: bestRoot, groupId: groupId } : null;
851
+ }
852
+ static async installAsLeader(groupIds, background, requestId, skipPendingRoots = false) {
853
+ const resolvedRefs = ContentInstaller.resolveGroups(groupIds, true);
854
+ const groupRefs = resolvedRefs.filter((groupRef) => ContentInstaller.installsFromActiveVersion(groupRef.root, skipPendingRoots));
855
+ const everyGroupSkipped = resolvedRefs.length > 0 && groupRefs.length === 0;
856
+ if ((groupIds != null && resolvedRefs.length === 0) || everyGroupSkipped)
857
+ return ContentInstaller.answerRequest(requestId, false);
858
+ if (!background)
859
+ await ContentInstaller.markRequested(groupRefs);
860
+ const files = ContentInstaller.groupFiles(groupRefs);
861
+ const missing = ContentInstaller.missingFiles(files);
862
+ if (missing.length === 0) {
863
+ ContentInstaller.roots.forEach((root) => { ContentInstaller.recomputeInstalledGroups(root, false); });
864
+ ContentInstaller.updateTotalsState();
865
+ return ContentInstaller.answerRequest(requestId, true);
866
+ }
867
+ if (!background)
868
+ await ContentInstaller.RequestPersistentStorage();
869
+ const estimate = await ContentInstaller.estimateFor(missing);
870
+ if (!estimate.fits) {
871
+ ContentInstaller.reportQuotaShortfall(estimate);
872
+ return ContentInstaller.answerRequest(requestId, false);
873
+ }
874
+ return ContentInstaller.submitCall(files, missing, background, requestId);
875
+ }
876
+ static async markRequested(groupRefs) {
877
+ const changedRoots = new Set();
878
+ for (const groupRef of groupRefs) {
879
+ if (groupRef.root.state.requestedGroups.indexOf(groupRef.groupId) >= 0)
880
+ continue;
881
+ groupRef.root.state.requestedGroups.push(groupRef.groupId);
882
+ changedRoots.add(groupRef.root);
883
+ }
884
+ for (const root of Array.from(changedRoots))
885
+ await ContentInstaller.writeRootState(root);
886
+ }
887
+ static answerRequest(requestId, success) {
888
+ if (requestId != null)
889
+ ContentInstaller.postToChannel({ type: "install-finished", requestId: requestId, success: success });
890
+ return success;
891
+ }
892
+ static submitCall(files, missing, background, requestId) {
893
+ return new Promise((resolve) => {
894
+ const hashes = new Set(files.map((file) => file.entry.hash));
895
+ ContentInstaller.installCalls.push({ hashes: hashes, requestId: requestId, resolve: resolve });
896
+ ContentInstaller.addToSession(files);
897
+ const jobs = missing.map((file) => ContentInstaller.createJob(file, background));
898
+ for (const job of jobs)
899
+ ContentInstaller.failedHashes.delete(job.hash);
900
+ ContentInstaller.enqueue(jobs, background);
901
+ ContentInstaller.setState({ installing: true });
902
+ ContentInstaller.pumpQueue();
903
+ ContentInstaller.settleFinishedCalls();
904
+ ContentInstaller.finishSessionIfDrained();
905
+ });
906
+ }
907
+ static createJob(file, background) {
908
+ return { rootKey: file.rootKey, version: file.version, path: file.path, entry: file.entry, groupId: file.groupId, hash: file.entry.hash, background: background, attempt: 0 };
909
+ }
910
+ static enqueue(jobs, background) {
911
+ const additions = [];
912
+ for (const job of jobs) {
913
+ const running = ContentInstaller.inFlight.get(job.hash);
914
+ if (running != null) {
915
+ if (!background)
916
+ running.job.background = false;
917
+ continue;
918
+ }
919
+ const retrying = ContentInstaller.retryWaits.get(job.hash);
920
+ if (retrying != null) {
921
+ if (!background)
922
+ retrying.job.background = false;
923
+ continue;
924
+ }
925
+ const queuedIndex = ContentInstaller.queue.findIndex((queued) => queued.hash === job.hash);
926
+ if (queuedIndex < 0) {
927
+ additions.push(job);
928
+ continue;
929
+ }
930
+ const queuedJob = ContentInstaller.queue[queuedIndex];
931
+ if (!queuedJob.background)
932
+ continue;
933
+ ContentInstaller.queue.splice(queuedIndex, 1);
934
+ queuedJob.background = background;
935
+ additions.push(queuedJob);
936
+ }
937
+ ContentInstaller.queue.splice(ContentInstaller.firstBackgroundIndex(), 0, ...additions);
938
+ }
939
+ static firstBackgroundIndex() {
940
+ const index = ContentInstaller.queue.findIndex((job) => job.background);
941
+ return (index < 0) ? ContentInstaller.queue.length : index;
942
+ }
943
+ static requeueFront(job) {
944
+ const index = job.background ? ContentInstaller.firstBackgroundIndex() : 0;
945
+ ContentInstaller.queue.splice(index, 0, job);
946
+ }
947
+ static prioritiseGroup(groupRef) {
948
+ const groupHashes = new Set(ContentInstaller.groupFiles([groupRef]).map((file) => file.entry.hash));
949
+ const front = [];
950
+ const rest = [];
951
+ for (const job of ContentInstaller.queue) {
952
+ if (!groupHashes.has(job.hash)) {
953
+ rest.push(job);
954
+ continue;
955
+ }
956
+ job.background = false;
957
+ front.push(job);
958
+ }
959
+ ContentInstaller.queue = front.concat(rest);
960
+ }
961
+ static removeQueuedJob(hash) {
962
+ const index = ContentInstaller.queue.findIndex((job) => job.hash === hash);
963
+ if (index >= 0)
964
+ ContentInstaller.queue.splice(index, 1);
965
+ const retry = ContentInstaller.retryWaits.get(hash);
966
+ if (retry == null)
967
+ return;
968
+ clearTimeout(retry.timer);
969
+ ContentInstaller.retryWaits.delete(hash);
970
+ }
971
+ static pumpQueue() {
972
+ if (!ContentInstaller.state.isLeader || ContentInstaller.state.paused)
973
+ return;
974
+ const limit = ContentInstaller.busy ? ContentInstaller.BUSY_CONCURRENCY : ContentInstaller.IDLE_CONCURRENCY;
975
+ while (ContentInstaller.inFlight.size < limit) {
976
+ const index = ContentInstaller.queue.findIndex((job) => !(ContentInstaller.busy && job.background));
977
+ if (index < 0)
978
+ return;
979
+ const job = ContentInstaller.queue.splice(index, 1)[0];
980
+ ContentInstaller.startDownload(job);
981
+ }
982
+ }
983
+ static startDownload(job) {
984
+ const download = { job: job, controller: new AbortController(), decodedBytesRead: 0, abortReason: null };
985
+ ContentInstaller.inFlight.set(job.hash, download);
986
+ void ContentInstaller.downloadFile(download).then((result) => { ContentInstaller.finishDownload(download, result); });
987
+ }
988
+ static async downloadFile(download) {
989
+ const job = download.job;
990
+ const verdict = ContentInstaller.awaitStoreResult(job.hash);
991
+ try {
992
+ const response = await fetch(ContentInstaller.downloadUrl(job), { signal: download.controller.signal });
993
+ await ContentInstaller.readBody(response, download);
994
+ }
995
+ catch {
996
+ }
997
+ if (download.controller.signal.aborted)
998
+ return ContentInstaller.storeResult("aborted", null, null, null);
999
+ return verdict;
1000
+ }
1001
+ static downloadUrl(job) {
1002
+ const root = ContentInstaller.roots.get(job.rootKey);
1003
+ const baseUrl = (root != null) ? root.state.baseUrl : ContentInstaller.folderOf(job.rootKey);
1004
+ const encodedPath = job.path.split("/").map((segment) => encodeURIComponent(segment)).join("/");
1005
+ return baseUrl + encodedPath + "?" + ContentInstaller.INSTALL_MARKER_PARAMETER + "=" + encodeURIComponent(job.version) + "&" + ContentInstaller.ATTEMPT_PARAMETER + "=" + job.attempt;
1006
+ }
1007
+ static async readBody(response, download) {
1008
+ if (response.body == null)
1009
+ return;
1010
+ const reader = response.body.getReader();
1011
+ while (true) {
1012
+ const chunk = await reader.read();
1013
+ if (chunk.done)
1014
+ return;
1015
+ download.decodedBytesRead += chunk.value.byteLength;
1016
+ ContentInstaller.recordPartial(download);
1017
+ ContentInstaller.emitProgress(false);
1018
+ }
1019
+ }
1020
+ static awaitStoreResult(hash) {
1021
+ return new Promise((resolve) => {
1022
+ const timer = setTimeout(() => { void ContentInstaller.confirmFromStore(hash); }, ContentInstaller.STORE_CONFIRM_TIMEOUT_MS);
1023
+ const waiters = ContentInstaller.storeWaiters.get(hash) || [];
1024
+ waiters.push({ resolve: resolve, timer: timer });
1025
+ ContentInstaller.storeWaiters.set(hash, waiters);
1026
+ });
1027
+ }
1028
+ static async confirmFromStore(hash) {
1029
+ const stored = await ContentInstaller.isHashStored(hash);
1030
+ const result = stored ? ContentInstaller.storeResult("stored", null, null, null)
1031
+ : ContentInstaller.storeResult("failed", "network", null, "no answer from the content worker");
1032
+ ContentInstaller.settleStoreWaiters(hash, result);
1033
+ }
1034
+ static settleStoreWaiters(hash, result) {
1035
+ const waiters = ContentInstaller.storeWaiters.get(hash);
1036
+ if (waiters == null)
1037
+ return;
1038
+ ContentInstaller.storeWaiters.delete(hash);
1039
+ for (const waiter of waiters) {
1040
+ clearTimeout(waiter.timer);
1041
+ waiter.resolve(result);
1042
+ }
1043
+ }
1044
+ static storeResult(outcome, reason, status, detail) {
1045
+ return { outcome: outcome, reason: reason, status: status, detail: detail };
1046
+ }
1047
+ static finishDownload(download, result) {
1048
+ const job = download.job;
1049
+ if (ContentInstaller.inFlight.get(job.hash) === download)
1050
+ ContentInstaller.inFlight.delete(job.hash);
1051
+ if (result.outcome === "stored")
1052
+ ContentInstaller.onFileStored(job.hash);
1053
+ else if (result.outcome === "failed")
1054
+ ContentInstaller.handleDownloadFailure(job, result);
1055
+ else if (download.abortReason === "pause")
1056
+ ContentInstaller.requeueFront(job);
1057
+ ContentInstaller.settleFinishedCalls();
1058
+ ContentInstaller.emitProgress(false);
1059
+ ContentInstaller.pumpQueue();
1060
+ ContentInstaller.finishSessionIfDrained();
1061
+ }
1062
+ static handleDownloadFailure(job, result) {
1063
+ if (result.reason === "cors") {
1064
+ ContentInstaller.stopForCors(job);
1065
+ return;
1066
+ }
1067
+ if (result.detail != null && result.detail.indexOf("QuotaExceeded") >= 0) {
1068
+ ContentInstaller.requeueFront(job);
1069
+ ContentInstaller.reportError("quota", "[ContentInstaller] Storage is full while storing " + job.path + ": installing is paused.", [job.path]);
1070
+ ContentInstaller.Pause();
1071
+ return;
1072
+ }
1073
+ if (job.attempt + 1 < ContentInstaller.MAX_ATTEMPTS) {
1074
+ ContentInstaller.scheduleRetry(job);
1075
+ return;
1076
+ }
1077
+ ContentInstaller.failedHashes.add(job.hash);
1078
+ if (result.reason === "mismatch") {
1079
+ void ContentInstaller.reportInstallMismatch(job);
1080
+ return;
1081
+ }
1082
+ ContentInstaller.reportError("network", "[ContentInstaller] " + job.path + " could not be downloaded (" + ContentInstaller.describeFailure(result) + "): not stored.", [job.path]);
1083
+ }
1084
+ static async reportInstallMismatch(job) {
1085
+ const reported = await ContentInstaller.recheckAfterMismatch(job.rootKey);
1086
+ if (reported)
1087
+ return;
1088
+ ContentInstaller.reportError("mismatch", "[ContentInstaller] The server's " + job.path + " does not match its manifest: not stored.", [job.path]);
1089
+ }
1090
+ static describeFailure(result) {
1091
+ if (result.status != null)
1092
+ return "HTTP " + result.status;
1093
+ if (result.detail != null)
1094
+ return result.detail;
1095
+ return (result.reason != null) ? result.reason : "network";
1096
+ }
1097
+ static scheduleRetry(job) {
1098
+ const delay = ContentInstaller.RETRY_DELAYS_MS[job.attempt];
1099
+ job.attempt += 1;
1100
+ const timer = setTimeout(() => {
1101
+ ContentInstaller.retryWaits.delete(job.hash);
1102
+ ContentInstaller.requeueFront(job);
1103
+ ContentInstaller.pumpQueue();
1104
+ }, delay);
1105
+ ContentInstaller.retryWaits.set(job.hash, { job: job, timer: timer });
1106
+ }
1107
+ static clearRetryWaits() {
1108
+ ContentInstaller.retryWaits.forEach((retry) => { clearTimeout(retry.timer); });
1109
+ ContentInstaller.retryWaits.clear();
1110
+ }
1111
+ static stopForCors(job) {
1112
+ const root = ContentInstaller.roots.get(job.rootKey);
1113
+ const assetOrigin = new URL((root != null) ? root.state.baseUrl : job.rootKey).origin;
1114
+ ContentInstaller.reportError("cors", "[ContentInstaller] " + assetOrigin + " sends no CORS headers for " + location.origin + ": nothing is stored.", []);
1115
+ ContentInstaller.failedHashes.add(job.hash);
1116
+ for (const queued of ContentInstaller.queue)
1117
+ ContentInstaller.failedHashes.add(queued.hash);
1118
+ ContentInstaller.retryWaits.forEach((retry) => { ContentInstaller.failedHashes.add(retry.job.hash); });
1119
+ ContentInstaller.inFlight.forEach((running) => { ContentInstaller.failedHashes.add(running.job.hash); });
1120
+ ContentInstaller.queue = [];
1121
+ ContentInstaller.clearRetryWaits();
1122
+ ContentInstaller.abortInFlight("cancel");
1123
+ ContentInstaller.settleFinishedCalls();
1124
+ }
1125
+ static abortInFlight(reason) {
1126
+ ContentInstaller.inFlight.forEach((download) => {
1127
+ download.abortReason = reason;
1128
+ download.controller.abort();
1129
+ ContentInstaller.settleStoreWaiters(download.job.hash, ContentInstaller.storeResult("aborted", null, null, null));
1130
+ });
1131
+ }
1132
+ static settleFinishedCalls() {
1133
+ const finished = [];
1134
+ const waiting = [];
1135
+ for (const call of ContentInstaller.installCalls) {
1136
+ const settled = Array.from(call.hashes).every((hash) => ContentInstaller.storedHashes.has(hash) || ContentInstaller.failedHashes.has(hash));
1137
+ if (settled)
1138
+ finished.push(call);
1139
+ else
1140
+ waiting.push(call);
1141
+ }
1142
+ if (finished.length === 0)
1143
+ return;
1144
+ ContentInstaller.installCalls = waiting;
1145
+ void ContentInstaller.completeCalls(finished, false);
1146
+ }
1147
+ static async completeCalls(calls, cancelled) {
1148
+ if (calls.length === 0)
1149
+ return;
1150
+ ContentInstaller.roots.forEach((root) => { ContentInstaller.recomputeInstalledGroups(root, false); });
1151
+ if (ContentInstaller.state.isLeader) {
1152
+ for (const root of Array.from(ContentInstaller.roots.values()))
1153
+ await ContentInstaller.writeRootState(root);
1154
+ }
1155
+ ContentInstaller.updateTotalsState();
1156
+ for (const call of calls) {
1157
+ const success = !cancelled && Array.from(call.hashes).every((hash) => ContentInstaller.storedHashes.has(hash));
1158
+ call.resolve(success);
1159
+ if (call.requestId != null)
1160
+ ContentInstaller.postToChannel({ type: "install-finished", requestId: call.requestId, success: success });
1161
+ }
1162
+ }
1163
+ static addToSession(files) {
1164
+ if (ContentInstaller.progress == null) {
1165
+ ContentInstaller.progress = { sizeByHash: new Map(), partialByHash: new Map(), hashesByGroup: new Map(), lastEmitTime: -Infinity, lastSpeedTime: null, lastSpeedBytes: 0, bytesPerSecond: 0 };
1166
+ }
1167
+ const session = ContentInstaller.progress;
1168
+ for (const file of files) {
1169
+ session.sizeByHash.set(file.entry.hash, file.entry.size);
1170
+ const groupHashes = session.hashesByGroup.get(file.groupId) || new Set();
1171
+ groupHashes.add(file.entry.hash);
1172
+ session.hashesByGroup.set(file.groupId, groupHashes);
1173
+ }
1174
+ }
1175
+ static recordPartial(download) {
1176
+ const session = ContentInstaller.progress;
1177
+ if (session == null)
1178
+ return;
1179
+ const entry = download.job.entry;
1180
+ const share = (entry.contentSize > 0) ? Math.min(1, download.decodedBytesRead / entry.contentSize) : 0;
1181
+ const estimate = share * entry.size;
1182
+ const previous = session.partialByHash.get(download.job.hash) || 0;
1183
+ if (estimate > previous)
1184
+ session.partialByHash.set(download.job.hash, estimate);
1185
+ }
1186
+ static completedBytesOf(session, hash) {
1187
+ if (ContentInstaller.storedHashes.has(hash))
1188
+ return session.sizeByHash.get(hash) || 0;
1189
+ return session.partialByHash.get(hash) || 0;
1190
+ }
1191
+ static buildProgress(session) {
1192
+ let totalBytes = 0;
1193
+ let completedBytes = 0;
1194
+ let completedFileCount = 0;
1195
+ session.sizeByHash.forEach((size, hash) => {
1196
+ totalBytes += size;
1197
+ completedBytes += ContentInstaller.completedBytesOf(session, hash);
1198
+ if (ContentInstaller.storedHashes.has(hash))
1199
+ completedFileCount += 1;
1200
+ });
1201
+ const groupProgress = {};
1202
+ session.hashesByGroup.forEach((hashes, groupId) => {
1203
+ let groupTotal = 0;
1204
+ let groupCompleted = 0;
1205
+ hashes.forEach((hash) => {
1206
+ groupTotal += session.sizeByHash.get(hash) || 0;
1207
+ groupCompleted += ContentInstaller.completedBytesOf(session, hash);
1208
+ });
1209
+ groupProgress[groupId] = { totalBytes: groupTotal, completedBytes: Math.min(groupTotal, groupCompleted) };
1210
+ });
1211
+ ContentInstaller.sampleSpeed(session, completedBytes);
1212
+ const remainingBytes = Math.max(0, totalBytes - completedBytes);
1213
+ const firstRunning = ContentInstaller.inFlight.values().next().value;
1214
+ return {
1215
+ totalBytes: totalBytes,
1216
+ completedBytes: Math.min(totalBytes, completedBytes),
1217
+ fraction: (totalBytes > 0) ? Math.min(1, completedBytes / totalBytes) : 1,
1218
+ fileCount: session.sizeByHash.size,
1219
+ completedFileCount: completedFileCount,
1220
+ bytesPerSecond: session.bytesPerSecond,
1221
+ secondsRemaining: (session.bytesPerSecond > 0) ? remainingBytes / session.bytesPerSecond : null,
1222
+ currentGroupId: (firstRunning != null) ? firstRunning.job.groupId : null,
1223
+ groupProgress: groupProgress,
1224
+ phase: "download",
1225
+ };
1226
+ }
1227
+ static sampleSpeed(session, completedBytes) {
1228
+ const now = ContentInstaller.Clock();
1229
+ if (session.lastSpeedTime == null) {
1230
+ session.lastSpeedTime = now;
1231
+ session.lastSpeedBytes = completedBytes;
1232
+ return;
1233
+ }
1234
+ const elapsedSeconds = (now - session.lastSpeedTime) / ContentInstaller.MILLISECONDS_PER_SECOND;
1235
+ if (elapsedSeconds <= 0)
1236
+ return;
1237
+ const sampleRate = (completedBytes - session.lastSpeedBytes) / elapsedSeconds;
1238
+ const smoothing = 1 - Math.exp(-elapsedSeconds / ContentInstaller.SPEED_TIME_CONSTANT_SECONDS);
1239
+ session.bytesPerSecond = Math.max(0, session.bytesPerSecond + (sampleRate - session.bytesPerSecond) * smoothing);
1240
+ session.lastSpeedTime = now;
1241
+ session.lastSpeedBytes = completedBytes;
1242
+ }
1243
+ static emitProgress(force) {
1244
+ const session = ContentInstaller.progress;
1245
+ if (session == null)
1246
+ return;
1247
+ const now = ContentInstaller.Clock();
1248
+ if (!force && now - session.lastEmitTime < ContentInstaller.PROGRESS_INTERVAL_MS)
1249
+ return;
1250
+ session.lastEmitTime = now;
1251
+ const report = ContentInstaller.buildProgress(session);
1252
+ ContentInstaller.OnProgressObservable.notifyObservers(report);
1253
+ if (ContentInstaller.state.isLeader)
1254
+ ContentInstaller.postToChannel({ type: "progress", progress: report });
1255
+ }
1256
+ static finishSessionIfDrained() {
1257
+ if (ContentInstaller.progress == null)
1258
+ return;
1259
+ if (ContentInstaller.queue.length > 0 || ContentInstaller.inFlight.size > 0 || ContentInstaller.retryWaits.size > 0)
1260
+ return;
1261
+ ContentInstaller.endSession();
1262
+ }
1263
+ static endSession() {
1264
+ if (ContentInstaller.progress != null)
1265
+ ContentInstaller.emitProgress(true);
1266
+ ContentInstaller.progress = null;
1267
+ ContentInstaller.setState({ installing: false });
1268
+ }
1269
+ static async installThenPrepareAsLeader(groupIds, background, prepareGraphics, requestId) {
1270
+ if (!prepareGraphics)
1271
+ return ContentInstaller.installAsLeader(groupIds, background, requestId);
1272
+ const cancelsBefore = ContentInstaller.cancelCount;
1273
+ const downloaded = await ContentInstaller.installAsLeader(groupIds, background, null);
1274
+ if (ContentInstaller.cancelCount !== cancelsBefore)
1275
+ return ContentInstaller.answerRequest(requestId, downloaded);
1276
+ const installedIds = ContentInstaller.sceneGroupRefs(groupIds)
1277
+ .filter((groupRef) => ContentInstaller.isGroupInstalled(groupRef))
1278
+ .map((groupRef) => groupRef.groupId);
1279
+ if (installedIds.length > 0)
1280
+ await ContentInstaller.startGraphicsStep(installedIds, background, false);
1281
+ return ContentInstaller.answerRequest(requestId, downloaded);
1282
+ }
1283
+ static async prepareGraphicsAsLeader(groupIds, background, force, requestId) {
1284
+ const prepared = await ContentInstaller.startGraphicsStep(groupIds, background, force);
1285
+ return ContentInstaller.answerRequest(requestId, prepared);
1286
+ }
1287
+ static startGraphicsStep(groupIds, background, force) {
1288
+ if (ContentInstaller.graphicsRun != null)
1289
+ return ContentInstaller.graphicsRun;
1290
+ const preparer = ContentInstaller.graphicsPreparer;
1291
+ if (preparer == null || ContentInstaller.graphicsUnavailable)
1292
+ return Promise.resolve(false);
1293
+ const candidates = (groupIds == null)
1294
+ ? ContentInstaller.sceneGroupRefs(null).filter((groupRef) => ContentInstaller.isGroupInstalled(groupRef))
1295
+ : ContentInstaller.sceneGroupRefs(groupIds);
1296
+ if (groupIds != null && candidates.length === 0)
1297
+ return Promise.resolve(false);
1298
+ const groupRefs = force ? candidates : candidates.filter((groupRef) => ContentInstaller.graphicsStatusOf(groupRef) !== "prepared");
1299
+ if (groupRefs.length === 0)
1300
+ return Promise.resolve(true);
1301
+ const run = ContentInstaller.runGraphicsStep(preparer, groupRefs, background).finally(() => { ContentInstaller.graphicsRun = null; });
1302
+ ContentInstaller.graphicsRun = run;
1303
+ return run;
1304
+ }
1305
+ static async runGraphicsStep(preparer, groupRefs, background) {
1306
+ ContentInstaller.graphicsCancelled = false;
1307
+ ContentInstaller.graphicsSession = { total: groupRefs.length, completed: 0, groupFraction: 0, currentGroupId: null, lastEmitTime: -Infinity };
1308
+ ContentInstaller.setState({ graphicsPreparing: true });
1309
+ let allPrepared = true;
1310
+ try {
1311
+ for (const groupRef of groupRefs) {
1312
+ await ContentInstaller.waitForGraphicsTurn(background);
1313
+ if (ContentInstaller.graphicsCancelled) {
1314
+ allPrepared = false;
1315
+ break;
1316
+ }
1317
+ const outcome = await ContentInstaller.prepareGroupGraphics(preparer, groupRef);
1318
+ if (outcome !== "prepared")
1319
+ allPrepared = false;
1320
+ if (outcome === "cancelled" || outcome === "unavailable")
1321
+ break;
1322
+ }
1323
+ }
1324
+ finally {
1325
+ ContentInstaller.finishGraphicsStep(preparer);
1326
+ }
1327
+ return allPrepared;
1328
+ }
1329
+ static async prepareGroupGraphics(preparer, groupRef) {
1330
+ const session = ContentInstaller.graphicsSession;
1331
+ session.currentGroupId = groupRef.groupId;
1332
+ session.groupFraction = 0;
1333
+ ContentInstaller.emitGraphicsProgress(true);
1334
+ const onProgress = (fraction) => {
1335
+ session.groupFraction = Math.max(session.groupFraction, Math.min(1, fraction));
1336
+ ContentInstaller.emitGraphicsProgress(false);
1337
+ };
1338
+ let failure = null;
1339
+ try {
1340
+ await preparer.prepareSceneAsync(ContentInstaller.groupSceneUrl(groupRef), () => ContentInstaller.graphicsCancelled, onProgress);
1341
+ }
1342
+ catch (error) {
1343
+ if (ContentInstaller.isGraphicsUnavailableError(error)) {
1344
+ ContentInstaller.markGraphicsUnavailable(error);
1345
+ return "unavailable";
1346
+ }
1347
+ failure = (error != null && typeof error.message === "string") ? error.message : String(error);
1348
+ }
1349
+ if (ContentInstaller.graphicsCancelled)
1350
+ return "cancelled";
1351
+ if (failure != null)
1352
+ console.warn("[ContentInstaller] The graphics of " + groupRef.groupId + " could not be prepared (" + failure + "): it loads as before.");
1353
+ await ContentInstaller.recordGraphics(preparer, groupRef, (failure == null) ? "prepared" : "failed", failure);
1354
+ session.completed += 1;
1355
+ session.groupFraction = 0;
1356
+ ContentInstaller.emitGraphicsProgress(false);
1357
+ return (failure == null) ? "prepared" : "failed";
1358
+ }
1359
+ static async waitForGraphicsTurn(background) {
1360
+ const isHeld = () => !ContentInstaller.graphicsCancelled && (ContentInstaller.state.paused || ContentInstaller.state.installing || (background && ContentInstaller.busy));
1361
+ while (isHeld())
1362
+ await new Promise((resolve) => { ContentInstaller.graphicsWaiters.push(resolve); });
1363
+ }
1364
+ static wakeGraphicsWaiters() {
1365
+ if (ContentInstaller.graphicsWaiters.length === 0)
1366
+ return;
1367
+ const waiters = ContentInstaller.graphicsWaiters;
1368
+ ContentInstaller.graphicsWaiters = [];
1369
+ for (const wake of waiters)
1370
+ wake();
1371
+ }
1372
+ static finishGraphicsStep(preparer) {
1373
+ const session = ContentInstaller.graphicsSession;
1374
+ if (session != null) {
1375
+ session.currentGroupId = null;
1376
+ session.groupFraction = 0;
1377
+ ContentInstaller.emitGraphicsProgress(true);
1378
+ }
1379
+ ContentInstaller.graphicsSession = null;
1380
+ if (typeof preparer.release === "function") {
1381
+ try {
1382
+ preparer.release();
1383
+ }
1384
+ catch (error) {
1385
+ console.warn("[ContentInstaller] The graphics preparer could not be released: " + String(error));
1386
+ }
1387
+ }
1388
+ ContentInstaller.setState({ graphicsPreparing: false });
1389
+ ContentInstaller.updateTotalsState();
1390
+ }
1391
+ static async recordGraphics(preparer, groupRef, outcome, error) {
1392
+ const root = groupRef.root;
1393
+ const record = {
1394
+ manifestVersion: root.state.activeVersion, runtimeVersion: preparer.runtimeVersion, babylonVersion: preparer.babylonVersion,
1395
+ engineKey: preparer.engineKey, outcome: outcome, error: error,
1396
+ };
1397
+ root.state.graphics[groupRef.groupId] = record;
1398
+ if (ContentInstaller.state.isLeader)
1399
+ await ContentInstaller.writeRootState(root);
1400
+ ContentInstaller.postToChannel({ type: "graphics-recorded", rootKey: root.state.rootKey, groupId: groupRef.groupId, graphicsRecord: record });
1401
+ ContentInstaller.updateTotalsState();
1402
+ }
1403
+ static takeGraphicsRecord(message) {
1404
+ const root = (message.rootKey != null) ? ContentInstaller.roots.get(message.rootKey) : undefined;
1405
+ if (root == null || message.groupId == null || message.graphicsRecord == null)
1406
+ return;
1407
+ root.state.graphics[message.groupId] = message.graphicsRecord;
1408
+ }
1409
+ static emitGraphicsProgress(force) {
1410
+ const session = ContentInstaller.graphicsSession;
1411
+ if (session == null)
1412
+ return;
1413
+ const now = ContentInstaller.Clock();
1414
+ if (!force && now - session.lastEmitTime < ContentInstaller.PROGRESS_INTERVAL_MS)
1415
+ return;
1416
+ session.lastEmitTime = now;
1417
+ const finished = Math.min(session.total, session.completed + session.groupFraction);
1418
+ const report = {
1419
+ totalBytes: 0, completedBytes: 0, fraction: (session.total > 0) ? finished / session.total : 1, fileCount: 0, completedFileCount: 0,
1420
+ bytesPerSecond: 0, secondsRemaining: null, currentGroupId: session.currentGroupId, groupProgress: {},
1421
+ phase: "graphics", graphicsCurrent: Math.min(session.total, session.completed + 1), graphicsCompleted: session.completed, graphicsTotal: session.total,
1422
+ };
1423
+ ContentInstaller.OnProgressObservable.notifyObservers(report);
1424
+ if (ContentInstaller.state.isLeader)
1425
+ ContentInstaller.postToChannel({ type: "progress", progress: report });
1426
+ }
1427
+ static updateGraphicsState() {
1428
+ if (!ContentInstaller.state.isLeader)
1429
+ return;
1430
+ const sceneGroups = ContentInstaller.sceneGroupRefs(null).filter((groupRef) => ContentInstaller.isGroupInstalled(groupRef));
1431
+ const preparedCount = sceneGroups.filter((groupRef) => ContentInstaller.graphicsStatusOf(groupRef) === "prepared").length;
1432
+ ContentInstaller.setState({
1433
+ graphicsAvailable: ContentInstaller.graphicsPreparer != null && !ContentInstaller.graphicsUnavailable,
1434
+ graphicsNeeded: preparedCount < sceneGroups.length,
1435
+ graphicsPreparedCount: preparedCount,
1436
+ graphicsGroupCount: sceneGroups.length,
1437
+ });
1438
+ }
1439
+ static graphicsStatusOf(groupRef) {
1440
+ if (!ContentInstaller.hasSceneFile(groupRef))
1441
+ return "none";
1442
+ const record = groupRef.root.state.graphics[groupRef.groupId];
1443
+ if (record == null)
1444
+ return "none";
1445
+ if (!ContentInstaller.isGraphicsRecordCurrent(groupRef.root, record))
1446
+ return "stale";
1447
+ return (record.outcome === "prepared") ? "prepared" : "failed";
1448
+ }
1449
+ static isGraphicsRecordCurrent(root, record) {
1450
+ if (record.manifestVersion !== root.state.activeVersion || record.gpuCacheLost === true)
1451
+ return false;
1452
+ const preparer = ContentInstaller.graphicsPreparer;
1453
+ if (preparer == null)
1454
+ return true;
1455
+ return record.runtimeVersion === preparer.runtimeVersion && record.babylonVersion === preparer.babylonVersion && record.engineKey === preparer.engineKey;
1456
+ }
1457
+ static sceneGroupRefs(groupIds) {
1458
+ return ContentInstaller.resolveGroups(groupIds, false)
1459
+ .filter((groupRef) => !groupRef.root.state.isAppShell && ContentInstaller.hasSceneFile(groupRef));
1460
+ }
1461
+ static hasSceneFile(groupRef) {
1462
+ const group = (groupRef.root.activeManifest != null) ? groupRef.root.activeManifest.groups[groupRef.groupId] : undefined;
1463
+ if (group == null || typeof group.scene !== "string")
1464
+ return false;
1465
+ const scene = group.scene.toLowerCase();
1466
+ return ContentInstaller.SCENE_FILE_SUFFIXES.some((suffix) => scene.endsWith(suffix));
1467
+ }
1468
+ static isGroupInstalled(groupRef) {
1469
+ return groupRef.root.state.installedGroups.indexOf(groupRef.groupId) >= 0;
1470
+ }
1471
+ static groupSceneUrl(groupRef) {
1472
+ const scenePath = groupRef.root.activeManifest.groups[groupRef.groupId].scene;
1473
+ return groupRef.root.state.baseUrl + scenePath.split("/").map((segment) => encodeURIComponent(segment)).join("/");
1474
+ }
1475
+ static isGraphicsUnavailableError(error) {
1476
+ return error != null && typeof error === "object" && error.graphicsUnavailable === true;
1477
+ }
1478
+ static markGraphicsUnavailable(error) {
1479
+ if (!ContentInstaller.graphicsUnavailable)
1480
+ console.warn("[ContentInstaller] Prepare graphics is unavailable here (" + String(error.message || error) + "): scenes load as before.");
1481
+ ContentInstaller.graphicsUnavailable = true;
1482
+ ContentInstaller.updateTotalsState();
1483
+ }
1484
+ static async estimateFor(missingFiles) {
1485
+ const neededBytes = missingFiles.reduce((total, file) => total + file.entry.contentSize, 0);
1486
+ let quotaBytes = null;
1487
+ let usageBytes = null;
1488
+ try {
1489
+ if (typeof navigator !== "undefined" && navigator.storage != null && typeof navigator.storage.estimate === "function") {
1490
+ const estimate = await navigator.storage.estimate();
1491
+ quotaBytes = (estimate.quota != null) ? estimate.quota : null;
1492
+ usageBytes = (estimate.usage != null) ? estimate.usage : null;
1493
+ }
1494
+ }
1495
+ catch {
1496
+ quotaBytes = null;
1497
+ usageBytes = null;
1498
+ }
1499
+ const freeBytes = (quotaBytes != null && usageBytes != null) ? quotaBytes - usageBytes : null;
1500
+ const fits = freeBytes == null || freeBytes >= neededBytes * ContentInstaller.QUOTA_SAFETY_FACTOR;
1501
+ return { neededBytes: neededBytes, freeBytes: freeBytes, quotaBytes: quotaBytes, usageBytes: usageBytes, fits: fits };
1502
+ }
1503
+ static reportQuotaShortfall(estimate) {
1504
+ const neededMegabytes = (estimate.neededBytes / ContentInstaller.BYTES_PER_MEGABYTE).toFixed(1);
1505
+ const freeMegabytes = ((estimate.freeBytes || 0) / ContentInstaller.BYTES_PER_MEGABYTE).toFixed(1);
1506
+ ContentInstaller.reportError("quota", "[ContentInstaller] Installing needs " + neededMegabytes + " MB but only " + freeMegabytes + " MB is free: nothing was downloaded.", []);
1507
+ }
1508
+ static rootsWithPending() {
1509
+ return Array.from(ContentInstaller.roots.values()).filter((root) => root.pendingManifest != null);
1510
+ }
1511
+ static updateFiles(root) {
1512
+ const files = [];
1513
+ for (const groupId of root.state.installedGroups)
1514
+ ContentInstaller.appendGroupFiles(files, root.state.rootKey, root.pendingManifest, groupId);
1515
+ return ContentInstaller.missingFiles(files);
1516
+ }
1517
+ static isPendingReady(root) {
1518
+ return root.pendingManifest != null && ContentInstaller.updateFiles(root).length === 0;
1519
+ }
1520
+ static updateInfo(root) {
1521
+ const delta = ContentInstaller.updateFiles(root);
1522
+ return {
1523
+ rootKey: root.state.rootKey,
1524
+ fromVersion: root.state.activeVersion,
1525
+ toVersion: root.state.pendingVersion,
1526
+ downloadBytes: ContentInstaller.sumSizes(delta),
1527
+ fileCount: delta.length,
1528
+ };
1529
+ }
1530
+ static downloadUpdateFor(pendingRoots) {
1531
+ if (ContentInstaller.updateDownload != null)
1532
+ return ContentInstaller.updateDownload;
1533
+ const run = async () => {
1534
+ const delta = [];
1535
+ for (const root of pendingRoots)
1536
+ delta.push(...ContentInstaller.updateFiles(root));
1537
+ const missing = ContentInstaller.missingFiles(delta);
1538
+ if (missing.length > 0) {
1539
+ const estimate = await ContentInstaller.estimateFor(missing);
1540
+ if (!estimate.fits) {
1541
+ ContentInstaller.reportQuotaShortfall(estimate);
1542
+ return false;
1543
+ }
1544
+ await ContentInstaller.submitCall(missing, missing, true, null);
1545
+ }
1546
+ ContentInstaller.updateTotalsState();
1547
+ return pendingRoots.every((root) => ContentInstaller.isPendingReady(root));
1548
+ };
1549
+ ContentInstaller.updateDownload = run().finally(() => { ContentInstaller.updateDownload = null; });
1550
+ return ContentInstaller.updateDownload;
1551
+ }
1552
+ static runUpdatePolicies() {
1553
+ if (!ContentInstaller.state.isLeader)
1554
+ return;
1555
+ const automaticRoots = ContentInstaller.rootsWithPending()
1556
+ .filter((root) => ContentInstaller.effectiveUpdatePolicy(root) === "automatic" && !root.state.applyOnNextLoad);
1557
+ if (automaticRoots.length === 0)
1558
+ return;
1559
+ void ContentInstaller.downloadUpdateFor(automaticRoots).then(() => {
1560
+ for (const root of automaticRoots) {
1561
+ if (ContentInstaller.isPendingReady(root))
1562
+ root.state.applyOnNextLoad = true;
1563
+ }
1564
+ void ContentInstaller.persistRoots(false);
1565
+ ContentInstaller.updateTotalsState();
1566
+ });
1567
+ }
1568
+ static recheckAfterMismatch(rootKey) {
1569
+ const started = ContentInstaller.mismatchRecheckedRoots.get(rootKey);
1570
+ if (started != null)
1571
+ return started;
1572
+ const root = ContentInstaller.roots.get(rootKey);
1573
+ if (ContentInstaller.state.availability !== "available" || root == null)
1574
+ return Promise.resolve(false);
1575
+ const recheck = ContentInstaller.runMismatchRecheck(root);
1576
+ ContentInstaller.mismatchRecheckedRoots.set(rootKey, recheck);
1577
+ return recheck;
1578
+ }
1579
+ static async runMismatchRecheck(root) {
1580
+ const loadedFromNetworkOnly = root.state.networkOnly != null;
1581
+ const serverManifest = await ContentInstaller.fetchManifest(root.state.rootKey);
1582
+ if (serverManifest == null)
1583
+ return false;
1584
+ const activeVersion = root.state.activeVersion;
1585
+ const reason = (serverManifest.version !== activeVersion) ? "update-pending" : "stale-server";
1586
+ root.state.networkOnly = { reason: reason, serverVersion: serverManifest.version };
1587
+ if (reason === "update-pending")
1588
+ await ContentInstaller.takeServerManifest(root, serverManifest);
1589
+ if (ContentInstaller.state.isLeader)
1590
+ await ContentInstaller.persistRoots(false);
1591
+ ContentInstaller.updateTotalsState();
1592
+ ContentInstaller.runUpdatePolicies();
1593
+ if (loadedFromNetworkOnly)
1594
+ return true;
1595
+ ContentInstaller.setState({ contentChangedOnServer: true });
1596
+ ContentInstaller.publishError("server-mismatch", ContentInstaller.serverMismatchMessage(root, reason, activeVersion, serverManifest.version), []);
1597
+ return true;
1598
+ }
1599
+ static serverMismatchMessage(root, reason, activeVersion, serverVersion) {
1600
+ const baseUrl = root.state.baseUrl;
1601
+ if (reason === "update-pending") {
1602
+ return "[ContentInstaller] Game content changed on the server at " + baseUrl + " (version " + ContentInstaller.shortVersion(activeVersion) + " → " + ContentInstaller.shortVersion(serverVersion) + "); offering a reload.";
1603
+ }
1604
+ return "[ContentInstaller] The server's files at " + baseUrl + " do not match its content manifest (version " + ContentInstaller.shortVersion(serverVersion) + "); loading them from the network until a new manifest is published.";
1605
+ }
1606
+ static shortVersion(version) {
1607
+ return (version != null) ? version.substring(0, ContentInstaller.VERSION_DISPLAY_LENGTH) : "none";
1608
+ }
1609
+ static installsFromActiveVersion(root, skipPendingRoots) {
1610
+ if (root.state.networkOnly != null)
1611
+ return false;
1612
+ return !(skipPendingRoots && root.pendingManifest != null);
1613
+ }
1614
+ static createInitialState() {
1615
+ return {
1616
+ availability: "unknown", reason: null, installPolicy: "as-you-play", updatePolicy: "prompt", isLeader: false, installing: false, paused: false,
1617
+ persisted: null, totalBytes: 0, installedBytes: 0, everythingInstalled: false, updateAvailable: null, updateReady: false, applyOnNextLoad: false,
1618
+ shellUpdateReady: false, lastError: null, contentChangedOnServer: false, graphicsAvailable: false, graphicsPreparing: false, graphicsNeeded: false,
1619
+ graphicsPreparedCount: 0, graphicsGroupCount: 0,
1620
+ };
1621
+ }
1622
+ static createRootState(rootKey) {
1623
+ return {
1624
+ rootKey: rootKey, baseUrl: ContentInstaller.folderOf(rootKey), isAppShell: false, installPolicy: ContentInstaller.state.installPolicy, activeVersion: null,
1625
+ pendingVersion: null, installedGroups: [], requestedGroups: [], applyOnNextLoad: false, updatedAt: ContentInstaller.Clock(), networkOnly: null,
1626
+ graphics: {},
1627
+ };
1628
+ }
1629
+ static updateTotalsState() {
1630
+ const allFiles = ContentInstaller.groupFiles(ContentInstaller.resolveGroups(null, true));
1631
+ const installedRefs = [];
1632
+ let everythingInstalled = ContentInstaller.roots.size > 0;
1633
+ ContentInstaller.roots.forEach((root) => {
1634
+ if (root.activeManifest == null)
1635
+ return;
1636
+ for (const groupId of Object.keys(root.activeManifest.groups)) {
1637
+ if (root.state.installedGroups.indexOf(groupId) >= 0)
1638
+ installedRefs.push({ root: root, groupId: groupId });
1639
+ else
1640
+ everythingInstalled = false;
1641
+ }
1642
+ });
1643
+ const contentPending = ContentInstaller.rootsWithPending().filter((root) => !root.state.isAppShell);
1644
+ const shellReady = ContentInstaller.rootsWithPending().some((root) => root.state.isAppShell && ContentInstaller.isPendingReady(root));
1645
+ ContentInstaller.setState({
1646
+ totalBytes: ContentInstaller.sumSizes(allFiles),
1647
+ installedBytes: ContentInstaller.sumSizes(ContentInstaller.groupFiles(installedRefs)),
1648
+ everythingInstalled: everythingInstalled,
1649
+ updateAvailable: (contentPending.length > 0) ? ContentInstaller.updateInfo(contentPending[0]) : null,
1650
+ updateReady: contentPending.length > 0 && contentPending.every((root) => ContentInstaller.isPendingReady(root)),
1651
+ applyOnNextLoad: contentPending.some((root) => root.state.applyOnNextLoad),
1652
+ shellUpdateReady: ContentInstaller.workerUpdateWaiting || shellReady,
1653
+ });
1654
+ ContentInstaller.updateGraphicsState();
1655
+ }
1656
+ static setState(changes) {
1657
+ const current = ContentInstaller.state;
1658
+ const keys = Object.keys(changes);
1659
+ const changed = keys.some((key) => !ContentInstaller.valuesEqual(current[key], changes[key]));
1660
+ if (!changed)
1661
+ return;
1662
+ ContentInstaller.state = { ...current, ...changes };
1663
+ ContentInstaller.OnStateChangedObservable.notifyObservers(ContentInstaller.GetState());
1664
+ if (ContentInstaller.state.isLeader)
1665
+ ContentInstaller.postToChannel({ type: "state", state: ContentInstaller.state });
1666
+ ContentInstaller.wakeGraphicsWaiters();
1667
+ }
1668
+ static valuesEqual(first, second) {
1669
+ if (first === second)
1670
+ return true;
1671
+ if (first == null || second == null || typeof first !== "object" || typeof second !== "object")
1672
+ return false;
1673
+ const firstRecord = first;
1674
+ const secondRecord = second;
1675
+ const firstKeys = Object.keys(firstRecord);
1676
+ if (firstKeys.length !== Object.keys(secondRecord).length)
1677
+ return false;
1678
+ return firstKeys.every((key) => firstRecord[key] === secondRecord[key]);
1679
+ }
1680
+ static reportError(kind, message, paths) {
1681
+ const key = kind + "\n" + paths.slice().sort().join("\n");
1682
+ if (ContentInstaller.reportedErrors.has(key))
1683
+ return;
1684
+ ContentInstaller.reportedErrors.add(key);
1685
+ ContentInstaller.publishError(kind, message, paths);
1686
+ }
1687
+ static publishError(kind, message, paths) {
1688
+ console.warn(message);
1689
+ const error = { kind: kind, message: message, paths: paths.slice() };
1690
+ ContentInstaller.setState({ lastError: error });
1691
+ ContentInstaller.OnErrorObservable.notifyObservers(error);
1692
+ }
1693
+ static handleChannelMessage(message) {
1694
+ if (message == null || typeof message.type !== "string")
1695
+ return;
1696
+ const isLeader = ContentInstaller.state.isLeader;
1697
+ switch (message.type) {
1698
+ case "file-stored":
1699
+ if (message.hash != null)
1700
+ ContentInstaller.onFileStored(message.hash);
1701
+ return;
1702
+ case "file-failed":
1703
+ if (message.hash != null)
1704
+ ContentInstaller.settleStoreWaiters(message.hash, ContentInstaller.storeResult("failed", message.reason || "network", (message.status != null) ? message.status : null, message.message || null));
1705
+ if (message.reason === "mismatch" && typeof message.rootKey === "string")
1706
+ void ContentInstaller.recheckAfterMismatch(message.rootKey);
1707
+ return;
1708
+ case "state":
1709
+ if (!isLeader && message.state != null)
1710
+ ContentInstaller.mirrorLeaderState(message.state);
1711
+ return;
1712
+ case "progress":
1713
+ if (!isLeader && message.progress != null)
1714
+ ContentInstaller.OnProgressObservable.notifyObservers(message.progress);
1715
+ return;
1716
+ case "install-finished":
1717
+ ContentInstaller.answerForwardedRequest(message.requestId, message.success === true);
1718
+ return;
1719
+ case "graphics-recorded":
1720
+ if (!isLeader)
1721
+ ContentInstaller.takeGraphicsRecord(message);
1722
+ return;
1723
+ default:
1724
+ if (isLeader)
1725
+ ContentInstaller.handleFollowerRequest(message);
1726
+ }
1727
+ }
1728
+ static handleFollowerRequest(message) {
1729
+ const groupIds = (message.groupIds != null) ? message.groupIds : null;
1730
+ const requestId = (message.requestId != null) ? message.requestId : null;
1731
+ if (message.type === "install-request")
1732
+ void ContentInstaller.installThenPrepareAsLeader(groupIds, message.background === true, message.prepareGraphics === true, requestId);
1733
+ else if (message.type === "prepare-graphics-request")
1734
+ void ContentInstaller.prepareGraphicsAsLeader(groupIds, message.background === true, message.force === true, requestId);
1735
+ else if (message.type === "remove-request")
1736
+ void ContentInstaller.removeAsLeader(groupIds).then(() => { ContentInstaller.answerRequest(requestId, true); });
1737
+ else if (message.type === "pause")
1738
+ ContentInstaller.Pause();
1739
+ else if (message.type === "resume")
1740
+ ContentInstaller.Resume();
1741
+ else if (message.type === "cancel")
1742
+ ContentInstaller.Cancel();
1743
+ }
1744
+ static mirrorLeaderState(leaderState) {
1745
+ const own = ContentInstaller.state;
1746
+ const contentChangedOnServer = own.contentChangedOnServer || leaderState.contentChangedOnServer === true;
1747
+ ContentInstaller.setState({ ...leaderState, availability: own.availability, reason: own.reason, isLeader: false, persisted: own.persisted, contentChangedOnServer: contentChangedOnServer });
1748
+ }
1749
+ static forwardToLeader(kind, groupIds, background, prepareGraphics, force) {
1750
+ const requestId = Math.random().toString(36).slice(2) + "-" + ContentInstaller.tabStartTime;
1751
+ return new Promise((resolve) => {
1752
+ ContentInstaller.forwardedRequests.set(requestId, { kind: kind, groupIds: groupIds, background: background, prepareGraphics: prepareGraphics, force: force, resolve: resolve });
1753
+ ContentInstaller.postToChannel({ type: kind, requestId: requestId, groupIds: groupIds, background: background, prepareGraphics: prepareGraphics, force: force });
1754
+ });
1755
+ }
1756
+ static answerForwardedRequest(requestId, success) {
1757
+ if (requestId == null)
1758
+ return;
1759
+ const request = ContentInstaller.forwardedRequests.get(requestId);
1760
+ if (request == null)
1761
+ return;
1762
+ ContentInstaller.forwardedRequests.delete(requestId);
1763
+ ContentInstaller.updateTotalsState();
1764
+ request.resolve(success);
1765
+ }
1766
+ static postToChannel(message) {
1767
+ if (ContentInstaller.channel == null)
1768
+ return;
1769
+ try {
1770
+ ContentInstaller.channel.postMessage(message);
1771
+ }
1772
+ catch (error) {
1773
+ console.warn("[ContentInstaller] Could not post to the other tabs: " + String(error));
1774
+ }
1775
+ }
1776
+ static postToWorker(message) {
1777
+ const controller = (typeof navigator !== "undefined" && navigator.serviceWorker != null) ? navigator.serviceWorker.controller : null;
1778
+ if (controller == null)
1779
+ return;
1780
+ try {
1781
+ controller.postMessage(message);
1782
+ }
1783
+ catch (error) {
1784
+ console.warn("[ContentInstaller] Could not message the content worker: " + String(error));
1785
+ }
1786
+ }
1787
+ static pinRoots() {
1788
+ ContentInstaller.roots.forEach((root) => {
1789
+ if (root.state.activeVersion != null)
1790
+ ContentInstaller.postToWorker({ type: "pin", rootKey: root.state.rootKey, version: root.state.activeVersion });
1791
+ });
1792
+ }
1793
+ static async removeAsLeader(groupIds) {
1794
+ const removed = ContentInstaller.resolveGroups(groupIds, false);
1795
+ if (removed.length === 0)
1796
+ return;
1797
+ const isRemoved = (root, groupId) => removed.some((groupRef) => groupRef.root === root && groupRef.groupId === groupId);
1798
+ const keptRefs = [];
1799
+ ContentInstaller.roots.forEach((root) => {
1800
+ for (const groupId of root.state.installedGroups) {
1801
+ if (!isRemoved(root, groupId))
1802
+ keptRefs.push({ root: root, groupId: groupId });
1803
+ }
1804
+ });
1805
+ const keptHashes = new Set(ContentInstaller.groupFiles(keptRefs).map((file) => file.entry.hash));
1806
+ const deletedHashes = new Set(ContentInstaller.groupFiles(removed).map((file) => file.entry.hash).filter((hash) => !keptHashes.has(hash)));
1807
+ for (const groupRef of removed) {
1808
+ groupRef.root.state.installedGroups = groupRef.root.state.installedGroups.filter((groupId) => groupId !== groupRef.groupId);
1809
+ groupRef.root.state.requestedGroups = groupRef.root.state.requestedGroups.filter((groupId) => groupId !== groupRef.groupId);
1810
+ }
1811
+ await ContentInstaller.deleteHashes(deletedHashes);
1812
+ await ContentInstaller.persistRoots(true);
1813
+ ContentInstaller.updateTotalsState();
1814
+ }
1815
+ static async deleteHashes(hashes) {
1816
+ ContentInstaller.queue = ContentInstaller.queue.filter((job) => {
1817
+ if (!hashes.has(job.hash))
1818
+ return true;
1819
+ ContentInstaller.failedHashes.add(job.hash);
1820
+ return false;
1821
+ });
1822
+ try {
1823
+ const cache = await caches.open(ContentInstaller.CONTENT_FILES_CACHE);
1824
+ for (const hash of Array.from(hashes)) {
1825
+ await cache.delete(ContentInstaller.fileKey(hash));
1826
+ ContentInstaller.storedHashes.delete(hash);
1827
+ }
1828
+ }
1829
+ catch (error) {
1830
+ console.warn("[ContentInstaller] Could not remove stored files: " + String(error));
1831
+ }
1832
+ ContentInstaller.settleFinishedCalls();
1833
+ ContentInstaller.finishSessionIfDrained();
1834
+ }
1835
+ static absoluteUrl(url) {
1836
+ const baseUrl = (typeof document !== "undefined" && document.baseURI) ? document.baseURI : location.href;
1837
+ return new URL(url, baseUrl).href;
1838
+ }
1839
+ static stripQueryAndHash(url) {
1840
+ const queryIndex = url.indexOf("?");
1841
+ const hashIndex = url.indexOf("#");
1842
+ const cutPoints = [queryIndex, hashIndex].filter((index) => index >= 0);
1843
+ return (cutPoints.length > 0) ? url.substring(0, Math.min(...cutPoints)) : url;
1844
+ }
1845
+ static folderOf(url) {
1846
+ const withoutQuery = ContentInstaller.stripQueryAndHash(url);
1847
+ return withoutQuery.substring(0, withoutQuery.lastIndexOf("/") + 1);
1848
+ }
1849
+ static ensureTrailingSlash(url) {
1850
+ return url.endsWith("/") ? url : url + "/";
1851
+ }
1852
+ static isLocalHost(hostname) {
1853
+ const host = hostname.toLowerCase().replace(/^\[/, "").replace(/\]$/, "");
1854
+ if (ContentInstaller.LOCAL_HOST_NAMES.indexOf(host) >= 0)
1855
+ return true;
1856
+ if (host.endsWith(ContentInstaller.LOCAL_DOMAIN_SUFFIX))
1857
+ return true;
1858
+ return ContentInstaller.PRIVATE_IPV4_PATTERN.test(host);
1859
+ }
1860
+ static union(first, second) {
1861
+ const result = first.slice();
1862
+ for (const value of second || []) {
1863
+ if (result.indexOf(value) < 0)
1864
+ result.push(value);
1865
+ }
1866
+ return result;
1867
+ }
1868
+ }
1869
+ ContentInstaller.OnStateChangedObservable = new Observable();
1870
+ ContentInstaller.OnProgressObservable = new Observable();
1871
+ ContentInstaller.OnErrorObservable = new Observable();
1872
+ ContentInstaller.Clock = () => ((typeof performance !== "undefined") ? performance.now() : Date.now());
1873
+ ContentInstaller.CONTENT_FILES_CACHE = "bt-content-files-v1";
1874
+ ContentInstaller.CONTENT_STATE_CACHE = "bt-content-state-v1";
1875
+ ContentInstaller.STORE_KEY_PREFIX = "/__bt-content__/";
1876
+ ContentInstaller.INSTALL_MARKER_PARAMETER = "bt-content-install";
1877
+ ContentInstaller.ATTEMPT_PARAMETER = "bt-content-attempt";
1878
+ ContentInstaller.BROADCAST_CHANNEL_NAME = "bt-content-installer";
1879
+ ContentInstaller.IDLE_CONCURRENCY = 4;
1880
+ ContentInstaller.BUSY_CONCURRENCY = 1;
1881
+ ContentInstaller.MAX_ATTEMPTS = 3;
1882
+ ContentInstaller.RETRY_DELAYS_MS = [1000, 4000, 16000];
1883
+ ContentInstaller.PROGRESS_INTERVAL_MS = 250;
1884
+ ContentInstaller.SPEED_TIME_CONSTANT_SECONDS = 3;
1885
+ ContentInstaller.CONTROLLER_WAIT_MS = 3000;
1886
+ ContentInstaller.QUOTA_SAFETY_FACTOR = 1.1;
1887
+ ContentInstaller.STORE_CONFIRM_TIMEOUT_MS = 30000;
1888
+ ContentInstaller.LOCK_NAME = "bt-content-installer";
1889
+ ContentInstaller.WORKER_SCRIPT_NAME = "bt-content-worker.js";
1890
+ ContentInstaller.SUPPORTED_MANIFEST_FORMAT = 1;
1891
+ ContentInstaller.SCENE_FILE_SUFFIXES = [".gz.gltf", ".gz.glb", ".gltf", ".glb"];
1892
+ ContentInstaller.LOCAL_HOST_NAMES = ["localhost", "127.0.0.1", "::1"];
1893
+ ContentInstaller.LOCAL_DOMAIN_SUFFIX = ".local";
1894
+ ContentInstaller.PRIVATE_IPV4_PATTERN = /^(?:10\.\d{1,3}\.\d{1,3}\.\d{1,3}|172\.(?:1[6-9]|2\d|3[01])\.\d{1,3}\.\d{1,3}|192\.168\.\d{1,3}\.\d{1,3})$/;
1895
+ ContentInstaller.BYTES_PER_MEGABYTE = 1024 * 1024;
1896
+ ContentInstaller.MILLISECONDS_PER_SECOND = 1000;
1897
+ ContentInstaller.VERSION_DISPLAY_LENGTH = 12;
1898
+ ContentInstaller.options = null;
1899
+ ContentInstaller.initializePromise = null;
1900
+ ContentInstaller.state = ContentInstaller.createInitialState();
1901
+ ContentInstaller.roots = new Map();
1902
+ ContentInstaller.storedHashes = new Set();
1903
+ ContentInstaller.groupsByHash = new Map();
1904
+ ContentInstaller.queue = [];
1905
+ ContentInstaller.inFlight = new Map();
1906
+ ContentInstaller.retryWaits = new Map();
1907
+ ContentInstaller.failedHashes = new Set();
1908
+ ContentInstaller.storeWaiters = new Map();
1909
+ ContentInstaller.installCalls = [];
1910
+ ContentInstaller.progress = null;
1911
+ ContentInstaller.busy = false;
1912
+ ContentInstaller.workerUpdateWaiting = false;
1913
+ ContentInstaller.updateDownload = null;
1914
+ ContentInstaller.channel = null;
1915
+ ContentInstaller.forwardedRequests = new Map();
1916
+ ContentInstaller.reportedErrors = new Set();
1917
+ ContentInstaller.mismatchRecheckedRoots = new Map();
1918
+ ContentInstaller.registration = null;
1919
+ ContentInstaller.tabStartTime = 0;
1920
+ ContentInstaller.graphicsPreparer = null;
1921
+ ContentInstaller.graphicsUnavailable = false;
1922
+ ContentInstaller.graphicsRun = null;
1923
+ ContentInstaller.graphicsCancelled = false;
1924
+ ContentInstaller.GPU_CACHE_LOST_STORAGE_KEY = "bt-gpu-cache-lost";
1925
+ ContentInstaller.GPU_CACHE_LOST_EVENT = "bt-gpu-cache-lost";
1926
+ ContentInstaller.listensForGpuCacheReports = false;
1927
+ ContentInstaller.cancelCount = 0;
1928
+ ContentInstaller.graphicsSession = null;
1929
+ ContentInstaller.graphicsWaiters = [];