arcane-os 0.1.0-dev.5

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 (248) hide show
  1. package/CHANGELOG.md +154 -0
  2. package/COMMERCIAL-LICENSE.md +13 -0
  3. package/LICENSE +661 -0
  4. package/NOTICE +70 -0
  5. package/README.md +448 -0
  6. package/bin/arcane-test.mjs +741 -0
  7. package/bin/arcane.mjs +5 -0
  8. package/docs/architecture.md +234 -0
  9. package/docs/compatibility.md +36 -0
  10. package/docs/event-manager.md +166 -0
  11. package/docs/platform-targets.md +108 -0
  12. package/docs/publishing.md +203 -0
  13. package/docs/reference/README.md +117 -0
  14. package/docs/reference/arcane-ollama.md +288 -0
  15. package/docs/reference/availability-and-normalization.md +123 -0
  16. package/docs/reference/behavioral-testing.md +86 -0
  17. package/docs/reference/cli.md +569 -0
  18. package/docs/reference/core/README.md +62 -0
  19. package/docs/reference/core/arcane-ai-contracts.md +873 -0
  20. package/docs/reference/core/arcane-api.md +601 -0
  21. package/docs/reference/core/arcane-entities.md +65 -0
  22. package/docs/reference/core/arcane-events.md +134 -0
  23. package/docs/reference/core/ollama-module.md +181 -0
  24. package/docs/reference/core/reference/arcane-api/ai-and-ollama.md +1909 -0
  25. package/docs/reference/core/reference/arcane-api/applications-terminal-capabilities.md +1057 -0
  26. package/docs/reference/core/reference/arcane-api/core-and-events.md +320 -0
  27. package/docs/reference/core/reference/arcane-api/filesystem-storage-preferences-appearance.md +610 -0
  28. package/docs/reference/core/reference/arcane-api/namespaces.md +1157 -0
  29. package/docs/reference/core/reference/arcane-api/platform-installation-users-system.md +1423 -0
  30. package/docs/reference/core/reference/arcane-api/session-provisioning-diagnostics-development.md +315 -0
  31. package/docs/reference/event-manager.md +957 -0
  32. package/docs/reference/inventory/package-api.json +2632 -0
  33. package/docs/reference/inventory/runtime-components.json +934 -0
  34. package/docs/reference/inventory/runtime-entities.json +26 -0
  35. package/docs/reference/inventory/runtime-modules.json +1249 -0
  36. package/docs/reference/protocols.md +242 -0
  37. package/docs/reference/runtime-components.md +1098 -0
  38. package/docs/reference/runtime-entities.md +303 -0
  39. package/docs/reference/runtime-modules.md +2010 -0
  40. package/docs/reference/sdk-api.md +4901 -0
  41. package/docs/roadmap.md +79 -0
  42. package/docs/work-amplification.md +124 -0
  43. package/node_modules/event-pubsub/CHANGELOG.md +55 -0
  44. package/node_modules/event-pubsub/MIGRATION.md +70 -0
  45. package/node_modules/event-pubsub/README.md +363 -0
  46. package/node_modules/event-pubsub/SECURITY.md +37 -0
  47. package/node_modules/event-pubsub/index.js +141 -0
  48. package/node_modules/event-pubsub/licence +21 -0
  49. package/node_modules/event-pubsub/package.json +59 -0
  50. package/node_modules/strong-type/README.md +408 -0
  51. package/node_modules/strong-type/assets/strong-type-header.png +0 -0
  52. package/node_modules/strong-type/index.js +1151 -0
  53. package/node_modules/strong-type/licence +21 -0
  54. package/node_modules/strong-type/node.js +125 -0
  55. package/node_modules/strong-type/package.json +61 -0
  56. package/package.json +95 -0
  57. package/runtime/ARCANE_RUNTIME_RELEASE.json +791 -0
  58. package/runtime/arcane/components/app-bar.html +468 -0
  59. package/runtime/arcane/components/assistant-panel.html +715 -0
  60. package/runtime/arcane/components/calculator.html +8 -0
  61. package/runtime/arcane/components/chart.html +655 -0
  62. package/runtime/arcane/components/chat.html +1225 -0
  63. package/runtime/arcane/components/conversation-view.html +13 -0
  64. package/runtime/arcane/components/dashboard-config.html +341 -0
  65. package/runtime/arcane/components/data-maintenance.html +112 -0
  66. package/runtime/arcane/components/data-view.html +92 -0
  67. package/runtime/arcane/components/directory-picker.html +197 -0
  68. package/runtime/arcane/components/document-inspector.html +252 -0
  69. package/runtime/arcane/components/file-drop.html +264 -0
  70. package/runtime/arcane/components/file-inspector.html +293 -0
  71. package/runtime/arcane/components/file-manager.html +1715 -0
  72. package/runtime/arcane/components/header.html +142 -0
  73. package/runtime/arcane/components/integration-settings.html +14 -0
  74. package/runtime/arcane/components/local-ai-status.html +360 -0
  75. package/runtime/arcane/components/markdown-document.html +1048 -0
  76. package/runtime/arcane/components/markdown-editor.html +360 -0
  77. package/runtime/arcane/components/media-embed.html +8 -0
  78. package/runtime/arcane/components/modal.html +402 -0
  79. package/runtime/arcane/components/output-panel.html +259 -0
  80. package/runtime/arcane/components/preferences-form.html +135 -0
  81. package/runtime/arcane/components/record-timeline.html +105 -0
  82. package/runtime/arcane/components/relationship-board.html +116 -0
  83. package/runtime/arcane/components/screen-capture.html +8 -0
  84. package/runtime/arcane/components/source-code-viewer.html +441 -0
  85. package/runtime/arcane/components/source-explanation.html +124 -0
  86. package/runtime/arcane/components/speech.html +365 -0
  87. package/runtime/arcane/components/summary-strip.html +177 -0
  88. package/runtime/arcane/components/table.html +77 -0
  89. package/runtime/arcane/components/task-progress.html +282 -0
  90. package/runtime/arcane/components/terminal-workspace.html +65 -0
  91. package/runtime/arcane/components/theme-editor.html +41 -0
  92. package/runtime/arcane/components/theme-switcher.html +46 -0
  93. package/runtime/arcane/components/unified-inbox.html +20 -0
  94. package/runtime/arcane/components/voice-transcription.html +476 -0
  95. package/runtime/arcane/components/weather-widget.html +8 -0
  96. package/runtime/arcane/components/web-navigator.html +239 -0
  97. package/runtime/arcane/css/communications.css +1 -0
  98. package/runtime/arcane/css/dashboard-config.css +45 -0
  99. package/runtime/arcane/css/document-site.css +981 -0
  100. package/runtime/arcane/css/layout.css +438 -0
  101. package/runtime/arcane/css/primitives.css +321 -0
  102. package/runtime/arcane/css/theme.css +112 -0
  103. package/runtime/arcane/css/utility-workspace.css +1 -0
  104. package/runtime/arcane/entities/ApiModelRecord.js +20 -0
  105. package/runtime/arcane/entities/Calculation.js +13 -0
  106. package/runtime/arcane/entities/Chat.js +581 -0
  107. package/runtime/arcane/entities/CommunicationMessage.js +29 -0
  108. package/runtime/arcane/entities/CommunicationThread.js +21 -0
  109. package/runtime/arcane/entities/Document.js +10 -0
  110. package/runtime/arcane/entities/File.js +143 -0
  111. package/runtime/arcane/entities/Image.js +135 -0
  112. package/runtime/arcane/entities/IntentEnvelope.js +834 -0
  113. package/runtime/arcane/entities/Preference.js +83 -0
  114. package/runtime/arcane/entities/TWiNPolicyDecision.js +1092 -0
  115. package/runtime/arcane/entities/TerminalSession.js +46 -0
  116. package/runtime/arcane/entities/Theme.js +107 -0
  117. package/runtime/arcane/entities/User.js +1046 -0
  118. package/runtime/arcane/entities/Weather.js +23 -0
  119. package/runtime/arcane/img/arcane-os-everywhere.png +0 -0
  120. package/runtime/arcane/img/arrow-left.png +0 -0
  121. package/runtime/arcane/img/arrow-right.png +0 -0
  122. package/runtime/arcane/img/doc.svg +5 -0
  123. package/runtime/arcane/img/folder.svg +4 -0
  124. package/runtime/arcane/img/image.svg +5 -0
  125. package/runtime/arcane/img/refresh.png +0 -0
  126. package/runtime/arcane/img/send.svg +9 -0
  127. package/runtime/arcane/img/trash.svg +5 -0
  128. package/runtime/arcane/img/upload.svg +5 -0
  129. package/runtime/arcane/modules/AI.js +2048 -0
  130. package/runtime/arcane/modules/AIPreferenceRuntime.js +32 -0
  131. package/runtime/arcane/modules/AIPreferenceTuple.js +92 -0
  132. package/runtime/arcane/modules/AIResponseLength.js +42 -0
  133. package/runtime/arcane/modules/AIResponseURLPolicy.js +626 -0
  134. package/runtime/arcane/modules/AnsiText.js +53 -0
  135. package/runtime/arcane/modules/ApiModelDatabase.js +25 -0
  136. package/runtime/arcane/modules/AppDataScope.js +245 -0
  137. package/runtime/arcane/modules/AppearancePreferences.js +28 -0
  138. package/runtime/arcane/modules/ArcaneCommunicationBridge.js +22 -0
  139. package/runtime/arcane/modules/ArcaneNavigationPolicy.js +135 -0
  140. package/runtime/arcane/modules/ArcaneNetworkPolicy.js +255 -0
  141. package/runtime/arcane/modules/AsyncBoundary.js +161 -0
  142. package/runtime/arcane/modules/BrowserTestSuite.js +326 -0
  143. package/runtime/arcane/modules/CalculatorEngine.js +20 -0
  144. package/runtime/arcane/modules/CaseEvidenceIndexer.js +134 -0
  145. package/runtime/arcane/modules/ChartLibrary.js +35 -0
  146. package/runtime/arcane/modules/ChatRecords.js +13 -0
  147. package/runtime/arcane/modules/CommunicationAppController.js +43 -0
  148. package/runtime/arcane/modules/CommunicationHub.js +16 -0
  149. package/runtime/arcane/modules/CommunicationPreferences.js +13 -0
  150. package/runtime/arcane/modules/CommunicationProviderRegistry.js +16 -0
  151. package/runtime/arcane/modules/ComponentContracts.js +586 -0
  152. package/runtime/arcane/modules/ConfiguredAIChatSession.js +288 -0
  153. package/runtime/arcane/modules/ConversationActionItems.js +488 -0
  154. package/runtime/arcane/modules/ConversationClosingReport.js +274 -0
  155. package/runtime/arcane/modules/ConversationTimebox.js +527 -0
  156. package/runtime/arcane/modules/CoreLocalModelCatalog.js +255 -0
  157. package/runtime/arcane/modules/DBLS.js +171 -0
  158. package/runtime/arcane/modules/DBOPFS.js +1154 -0
  159. package/runtime/arcane/modules/DBOPFSWorker.js +116 -0
  160. package/runtime/arcane/modules/DataMaintenance.js +91 -0
  161. package/runtime/arcane/modules/DevelopmentWorkspace.js +74 -0
  162. package/runtime/arcane/modules/DirectoryPicker.js +109 -0
  163. package/runtime/arcane/modules/DocumentNavigation.js +223 -0
  164. package/runtime/arcane/modules/Errors.js +1025 -0
  165. package/runtime/arcane/modules/GifEncoder.js +29 -0
  166. package/runtime/arcane/modules/HTMLImport.js +114 -0
  167. package/runtime/arcane/modules/InMemoryCommunicationProvider.js +14 -0
  168. package/runtime/arcane/modules/IsolatedModelQuestionRunner.js +275 -0
  169. package/runtime/arcane/modules/LocalAIReadiness.js +870 -0
  170. package/runtime/arcane/modules/LocalAIReadinessController.js +156 -0
  171. package/runtime/arcane/modules/MD.js +111 -0
  172. package/runtime/arcane/modules/Mail.js +352 -0
  173. package/runtime/arcane/modules/MailTransport.mjs +180 -0
  174. package/runtime/arcane/modules/Marked.min.js +71 -0
  175. package/runtime/arcane/modules/MemoryRecords.js +44 -0
  176. package/runtime/arcane/modules/MessageAdvisory.js +38 -0
  177. package/runtime/arcane/modules/ModelDefinition.js +189 -0
  178. package/runtime/arcane/modules/Ollama.js +74 -0
  179. package/runtime/arcane/modules/OllamaModelIdentifier.js +21 -0
  180. package/runtime/arcane/modules/OllamaSettings.js +24 -0
  181. package/runtime/arcane/modules/OpenMeteoWeatherProvider.js +14 -0
  182. package/runtime/arcane/modules/PreferenceStore.js +109 -0
  183. package/runtime/arcane/modules/QRCode.min.js +1 -0
  184. package/runtime/arcane/modules/Questionnaire.js +61 -0
  185. package/runtime/arcane/modules/RecordLinkIndex.js +24 -0
  186. package/runtime/arcane/modules/RecordPassageIndex.js +222 -0
  187. package/runtime/arcane/modules/RecordReviewStore.js +94 -0
  188. package/runtime/arcane/modules/RevocableProjectionLedger.js +1623 -0
  189. package/runtime/arcane/modules/RiskSignalAnalyzer.js +37 -0
  190. package/runtime/arcane/modules/ScamRiskPolicy.js +62 -0
  191. package/runtime/arcane/modules/ScopedOPFSCache.js +183 -0
  192. package/runtime/arcane/modules/ScreenCapture.js +20 -0
  193. package/runtime/arcane/modules/SpeechPlayback.js +581 -0
  194. package/runtime/arcane/modules/StaticDocumentCatalog.js +1248 -0
  195. package/runtime/arcane/modules/SystemAppearance.js +20 -0
  196. package/runtime/arcane/modules/SystemPlatformPresentation.js +51 -0
  197. package/runtime/arcane/modules/SystemToolRegistry.js +28 -0
  198. package/runtime/arcane/modules/TerminalClient.js +52 -0
  199. package/runtime/arcane/modules/TerminalCommandRegistry.js +50 -0
  200. package/runtime/arcane/modules/ThemeBootstrap.js +26 -0
  201. package/runtime/arcane/modules/ThemeManager.js +131 -0
  202. package/runtime/arcane/modules/TimeGuard.js +149 -0
  203. package/runtime/arcane/modules/ToolCallRouter.js +83 -0
  204. package/runtime/arcane/modules/WaitForComponent.js +102 -0
  205. package/runtime/arcane/modules/YouTubeMedia.js +16 -0
  206. package/runtime/arcane/modules/uPlot.LICENSE.txt +21 -0
  207. package/runtime/arcane/modules/uPlot.iife.min.js +2 -0
  208. package/runtime/arcane/modules/uPlot.min.css +1 -0
  209. package/runtime/arcane/security/arcane-network-policy.json +6 -0
  210. package/runtime/strong-type/index.js +352 -0
  211. package/runtime/strong-type/licence +21 -0
  212. package/runtime/strong-type/package.json +45 -0
  213. package/schemas/arcane-app-bundle.schema.json +125 -0
  214. package/schemas/arcane-app.schema.json +329 -0
  215. package/schemas/arcane-lock.schema.json +86 -0
  216. package/schemas/arcane-package.schema.json +224 -0
  217. package/schemas/cli-event.schema.json +122 -0
  218. package/schemas/event-stack.schema.json +152 -0
  219. package/schemas/native-build-plan.schema.json +176 -0
  220. package/schemas/target-adapter.schema.json +117 -0
  221. package/src/app-descriptor.mjs +500 -0
  222. package/src/cli/main.mjs +561 -0
  223. package/src/constants.mjs +31 -0
  224. package/src/dev-server.mjs +718 -0
  225. package/src/doctor.mjs +315 -0
  226. package/src/dom-event-instrumentation.mjs +594 -0
  227. package/src/errors.mjs +75 -0
  228. package/src/event-manager.mjs +1342 -0
  229. package/src/event-queue.mjs +138 -0
  230. package/src/events.mjs +219 -0
  231. package/src/index.mjs +177 -0
  232. package/src/integrated-provider-loader.mjs +432 -0
  233. package/src/native-plan.mjs +698 -0
  234. package/src/native-provider-loader.mjs +1126 -0
  235. package/src/packager/core.mjs +2691 -0
  236. package/src/process.mjs +353 -0
  237. package/src/release-bundle.mjs +2523 -0
  238. package/src/repository.mjs +90 -0
  239. package/src/runtime.mjs +452 -0
  240. package/src/scaffold.mjs +380 -0
  241. package/src/targets/index.mjs +436 -0
  242. package/src/templates/assets/app-icon.png +0 -0
  243. package/src/templates/workspace-template.mjs +388 -0
  244. package/src/testing-loader.mjs +9 -0
  245. package/src/testing.mjs +427 -0
  246. package/src/toolchain.mjs +1335 -0
  247. package/src/update-check.mjs +307 -0
  248. package/src/workspace.mjs +449 -0
@@ -0,0 +1,1154 @@
1
+ import Is from "../../node_modules/strong-type/index.js";
2
+ import {openApplicationDataDirectory} from './AppDataScope.js';
3
+ const is=new Is(false);
4
+
5
+ const DEFAULT_TABLE_DIRECTORIES=Object.freeze({
6
+ users:'users',
7
+ scores:'scores',
8
+ chats:'chats',
9
+ notes:'notes',
10
+ documents:'documents',
11
+ songs:'songs',
12
+ images:'images',
13
+ journal_entries:'journal_entries',
14
+ streams_of_consciousness:'streams_of_consciousness',
15
+ reports:'reports',
16
+ errors:'errors',
17
+ memories:'memory'
18
+ });
19
+
20
+ function parseFileValue(fileName='',textContent=''){
21
+ let value=textContent;
22
+ const extension=fileName.slice(fileName.lastIndexOf('.')+1).toLowerCase();
23
+
24
+ switch(extension){
25
+ case 'json':
26
+ try{
27
+ value=JSON.parse(textContent.trim());
28
+ }catch{}
29
+ break;
30
+ case 'jsonl':
31
+ case 'ndjson':
32
+ value=[];
33
+ for(const row of textContent.split('\n')){
34
+ try{
35
+ value.push(JSON.parse(row.trim()));
36
+ }catch{}
37
+ }
38
+ break;
39
+ }
40
+
41
+ return value;
42
+ }
43
+
44
+ if(navigator.storage?.persist){
45
+ await navigator.storage.persist().catch(()=>false);
46
+ }
47
+
48
+ /**
49
+ * @typedef {Object<string,any>} DBOPFSTableCache
50
+ * Represents cached values of a table in memory.
51
+ * Key = filename
52
+ * Value = parsed file content
53
+ */
54
+
55
+ /**
56
+ * @typedef {Object<string,DBOPFSTableCache>} DBOPFSTables
57
+ * Represents the in-memory cache of all tables.
58
+ */
59
+
60
+ /**
61
+ * @typedef {Object<string,FileSystemDirectoryHandle>} DBOPFSTableHandles
62
+ * Handles to directories inside OPFS.
63
+ */
64
+
65
+ /**
66
+ * @typedef {Object<string,Promise>} DBOPFSWriteLocks
67
+ * Promise based write locks to serialize writes to the same file.
68
+ */
69
+
70
+ /**
71
+ * @typedef {Object} DBOPFSTableUpdate
72
+ * @property {string} tableName
73
+ * @property {string} fileName
74
+ * @property {*} value
75
+ */
76
+
77
+
78
+ /**
79
+ * DBOPFS
80
+ *
81
+ * A lightweight database abstraction on top of the
82
+ * **Origin Private File System (OPFS)**.
83
+ *
84
+ * Tables are directories and records are files.
85
+ *
86
+ * This module automatically attaches a singleton to:
87
+ *
88
+ * window.dbopfs
89
+ *
90
+ * Example:
91
+ *
92
+ * await dbopfs.set('users','alex',{email:'alex@example.com'})
93
+ * const user=await dbopfs.get('users','alex')
94
+ */
95
+ class DBOPFS {
96
+
97
+ /** @type {FileSystemDirectoryHandle|Object} */
98
+ #db={}
99
+
100
+ /** @type {DBOPFSTableHandles} */
101
+ #tableHandles={}
102
+
103
+ /** @type {DBOPFSTables} */
104
+ #tables={}
105
+
106
+ /** @type {DBOPFSWriteLocks} */
107
+ #writeLocks={}
108
+
109
+ /** @type {ServiceWorker|Object} */
110
+ #serviceWorker={}
111
+
112
+ /** @type {Worker|Object} */
113
+ #writeWorker={}
114
+
115
+ /** @type {string} */
116
+ #applicationId=''
117
+
118
+ /** @type {string} */
119
+ #storagePath=''
120
+
121
+ /**
122
+ * Handles messages received from the service worker.
123
+ * Placeholder for future synchronization logic.
124
+ * @private
125
+ */
126
+ #handleServiceWorkerMessage(){};
127
+
128
+ /**
129
+ * Writes file data through a dedicated OPFS worker.
130
+ * Used when FileSystemFileHandle.createWritable is unavailable.
131
+ *
132
+ * @private
133
+ * @param {string} directoryName
134
+ * @param {string} fileName
135
+ * @param {*} fileData
136
+ * @param {boolean} append
137
+ * @returns {Promise<boolean>}
138
+ */
139
+ async #writeFileWithWorker(directoryName='',fileName='',fileData='',append=false){
140
+ const fileDataBuffer=await new Blob([fileData]).arrayBuffer();
141
+
142
+ await this.#requestFileWorker(
143
+ {
144
+ operation:'write',
145
+ applicationId:this.#applicationId,
146
+ directoryName,
147
+ fileName,
148
+ fileData:fileDataBuffer,
149
+ append
150
+ },
151
+ [fileDataBuffer]
152
+ )
153
+
154
+ return true
155
+ }
156
+
157
+ /**
158
+ * Reads file data through a dedicated OPFS worker.
159
+ * Used when FileSystemFileHandle.getFile is unavailable.
160
+ *
161
+ * @private
162
+ * @param {string} directoryName
163
+ * @param {string} fileName
164
+ * @returns {Promise<File>}
165
+ */
166
+ async #readFileWithWorker(directoryName='',fileName=''){
167
+ const response=await this.#requestFileWorker(
168
+ {
169
+ operation:'read',
170
+ applicationId:this.#applicationId,
171
+ directoryName,
172
+ fileName
173
+ }
174
+ )
175
+
176
+ return new File(
177
+ [response.fileData],
178
+ fileName
179
+ )
180
+ }
181
+
182
+ /**
183
+ * Sends a file operation to the shared OPFS worker.
184
+ *
185
+ * @private
186
+ * @param {Object} data
187
+ * @param {Transferable[]} transfer
188
+ * @returns {Promise<Object>}
189
+ */
190
+ async #requestFileWorker(data={},transfer=[]){
191
+
192
+ if(typeof this.#writeWorker?.postMessage!=='function'){
193
+ this.#writeWorker=new Worker(
194
+ new URL('./DBOPFSWorker.js',import.meta.url)
195
+ );
196
+ }
197
+
198
+ const channel=new MessageChannel();
199
+ const worker=this.#writeWorker;
200
+ const db=this;
201
+
202
+ return new Promise(
203
+ function fileWorkerPromise(resolve,reject){
204
+ function cleanup(){
205
+ channel.port1.onmessage=null;
206
+ channel.port1.close();
207
+ worker.removeEventListener('error',workerErrorHandler);
208
+ }
209
+
210
+ function workerErrorHandler(event){
211
+ cleanup();
212
+ worker.terminate();
213
+
214
+ if(db.#writeWorker===worker){
215
+ db.#writeWorker={};
216
+ }
217
+
218
+ reject(event.error||new Error(event.message||'OPFS worker failed'));
219
+ }
220
+
221
+ channel.port1.onmessage=function fileWorkerMessage(event){
222
+ cleanup();
223
+
224
+ if(event.data?.error){
225
+ const error=new Error(event.data.error.message);
226
+ error.name=event.data.error.name;
227
+ reject(error);
228
+ return;
229
+ }
230
+
231
+ resolve(event.data);
232
+ };
233
+
234
+ channel.port1.start();
235
+ worker.addEventListener('error',workerErrorHandler);
236
+
237
+ try{
238
+ worker.postMessage(
239
+ data,
240
+ transfer.concat(channel.port2)
241
+ );
242
+ }catch(error){
243
+ cleanup();
244
+ channel.port2.close();
245
+ reject(error);
246
+ }
247
+ }
248
+ );
249
+ }
250
+
251
+ /**
252
+ * Generates a unique key for a file lock.
253
+ * @private
254
+ * @param {string} tableName
255
+ * @param {string} fileName
256
+ * @returns {string}
257
+ */
258
+ #getLockKey(tableName,fileName){
259
+ return `${tableName}:${fileName}`
260
+ }
261
+
262
+ /** @type {boolean} */
263
+ ready=false;
264
+
265
+ /** @type {Promise<void>} */
266
+ readyPromise=Promise.resolve();
267
+
268
+ constructor(options={}){
269
+ if(window.dbopfs){
270
+ return window.dbopfs;
271
+ }
272
+
273
+ this.readyPromise=this.#init(options);
274
+ }
275
+
276
+ /**
277
+ * Initializes OPFS database and default tables.
278
+ * Dispatches `dbopfs-ready` event when complete.
279
+ *
280
+ * @returns {Promise<void>}
281
+ */
282
+ async #init(options={}){
283
+ const scope=await openApplicationDataDirectory({
284
+ storage:options.storage||navigator.storage,
285
+ applicationId:options.applicationId||null,
286
+ documentObject:options.documentObject||globalThis.document,
287
+ arcane:options.arcane||globalThis.Arcane,
288
+ create:true
289
+ });
290
+ this.#applicationId=scope.applicationId;
291
+ this.#storagePath=scope.path;
292
+ this.#db=scope.directory;
293
+ await this.#createDefaultTables();
294
+
295
+ this.ready=true;
296
+
297
+ window.dispatchEvent(
298
+ new CustomEvent(
299
+ 'dbopfs-ready',
300
+ {
301
+ detail:{
302
+ dbopfs:this,
303
+ applicationId:this.#applicationId,
304
+ storagePath:this.#storagePath
305
+ }
306
+ }
307
+ )
308
+ );
309
+ }
310
+
311
+ async #createDefaultTables(){
312
+ for(const [alias,directoryName]of Object.entries(DEFAULT_TABLE_DIRECTORIES)){
313
+ this.#tableHandles[alias]=await this.#db.getDirectoryHandle(
314
+ directoryName,
315
+ {create:true}
316
+ );
317
+ }
318
+ }
319
+
320
+ /**
321
+ * Canonical application identity owning this database.
322
+ *
323
+ * @returns {string}
324
+ */
325
+ get applicationId(){
326
+ return this.#applicationId;
327
+ }
328
+
329
+ /**
330
+ * Application-relative OPFS directory used by this database.
331
+ *
332
+ * @returns {string}
333
+ */
334
+ get storagePath(){
335
+ return this.#storagePath;
336
+ }
337
+
338
+ /**
339
+ * Returns in-memory table cache.
340
+ *
341
+ * @returns {DBOPFSTables}
342
+ */
343
+ get tables(){
344
+ return this.#tables
345
+ }
346
+
347
+ /**
348
+ * Convenience setter to write to tables.
349
+ *
350
+ * Example:
351
+ *
352
+ * dbopfs.tables={
353
+ * tableName:'users',
354
+ * fileName:'alex',
355
+ * value:{email:'alex@example.com'}
356
+ * }
357
+ *
358
+ * @param {DBOPFSTableUpdate} update
359
+ */
360
+ set tables(update={tableName:'',fileName:'',value:''}){
361
+ this.set(update.tableName,update.fileName,update.value)
362
+ }
363
+
364
+ /**
365
+ * Gets the directory handle for a table.
366
+ * Creates the table if it does not exist.
367
+ *
368
+ * @param {string} tableName
369
+ * @returns {Promise<FileSystemDirectoryHandle>}
370
+ */
371
+ async getTableHandle(tableName=''){
372
+ if(!this.ready){
373
+ await this.readyPromise;
374
+ }
375
+
376
+ if(!this.#tableHandles[tableName]){
377
+ const existingHandle=Object.values(this.#tableHandles).find(
378
+ handle=>handle.name===tableName
379
+ )
380
+
381
+ if(existingHandle){
382
+ return existingHandle
383
+ }
384
+
385
+ this.#tableHandles[tableName]=await this.#db.getDirectoryHandle(tableName,{create:true});
386
+ }
387
+
388
+ return this.#tableHandles[tableName];
389
+ }
390
+
391
+ /**
392
+ * Writes a value to OPFS.
393
+ *
394
+ * @param {string} tableName
395
+ * @param {string} fileName
396
+ * @param {*} value
397
+ * @returns {Promise<*>}
398
+ */
399
+ async set(tableName='',fileName='',value={}, append=false){
400
+ const lockKey=this.#getLockKey(tableName,fileName)
401
+
402
+ const previousWrite=this.#writeLocks[lockKey]||Promise.resolve()
403
+ const currentWrite=previousWrite.catch(()=>{}).then(
404
+ async function setWriteLocked(){
405
+ if(!this.#tables[tableName]){
406
+ this.#tables[tableName]={}
407
+ }
408
+
409
+ let dataToWrite=value
410
+
411
+ if(typeof value!=='string'){
412
+ dataToWrite=JSON.stringify(dataToWrite)
413
+ }
414
+
415
+ try{
416
+ await this.writeFile(
417
+ tableName,
418
+ fileName,
419
+ dataToWrite,
420
+ append
421
+ );
422
+
423
+ if(append){
424
+ delete this.#tables[tableName][fileName];
425
+ }else{
426
+ this.#tables[tableName][fileName]=parseFileValue(
427
+ fileName,
428
+ String(dataToWrite)
429
+ );
430
+ }
431
+ }catch(error){
432
+ console.error(`Error writing file '${fileName}' to table '${tableName}':`,error)
433
+ throw error
434
+ }
435
+
436
+ return append?true:this.#tables[tableName][fileName]
437
+ }.bind(this)
438
+ )
439
+
440
+ this.#writeLocks[lockKey]=currentWrite
441
+
442
+ function clearWriteLock(){
443
+ if(this.#writeLocks[lockKey]===currentWrite){
444
+ delete this.#writeLocks[lockKey]
445
+ }
446
+ }
447
+
448
+ currentWrite.then(
449
+ clearWriteLock.bind(this),
450
+ clearWriteLock.bind(this)
451
+ )
452
+
453
+ return currentWrite
454
+ }
455
+
456
+ /**
457
+ * Writes raw file data to OPFS.
458
+ *
459
+ * @param {string} tableName
460
+ * @param {string} fileName
461
+ * @param {*} fileData
462
+ * @param {boolean} append
463
+ * @returns {Promise<boolean>}
464
+ */
465
+ async writeFile(tableName='',fileName='',fileData='',append=false){
466
+ const table=await this.getTableHandle(tableName)
467
+ const handle=await table.getFileHandle(
468
+ fileName,
469
+ {create:true}
470
+ );
471
+
472
+ if(typeof handle.createWritable!=='function'){
473
+ return this.#writeFileWithWorker(
474
+ table.name,
475
+ fileName,
476
+ fileData,
477
+ append
478
+ );
479
+ }
480
+
481
+ const writable=await handle.createWritable(
482
+ {keepExistingData:append}
483
+ );
484
+
485
+ if(append){
486
+ const file=await handle.getFile();
487
+ await writable.seek(file.size);
488
+ }
489
+
490
+ const blob = new Blob(
491
+ [
492
+ fileData
493
+ ]
494
+ );
495
+
496
+ await writable.write(blob);
497
+ await writable.close();
498
+
499
+ return true
500
+ }
501
+
502
+ /**
503
+ * Reads a raw file from OPFS.
504
+ * Falls back to a synchronous access handle in a worker on browsers
505
+ * without FileSystemFileHandle.getFile.
506
+ *
507
+ * @param {string} tableName
508
+ * @param {string} fileName
509
+ * @returns {Promise<File>}
510
+ */
511
+ async readFile(tableName='',fileName=''){
512
+ const table=await this.getTableHandle(tableName)
513
+ const handle=await table.getFileHandle(fileName,{create:false})
514
+
515
+ if(typeof handle.getFile==='function'){
516
+ return handle.getFile()
517
+ }
518
+
519
+ return this.#readFileWithWorker(
520
+ table.name,
521
+ fileName
522
+ )
523
+ }
524
+
525
+ /**
526
+ * Returns metadata exposed by the browser File API.
527
+ * Creation time is not available through the OPFS file handle.
528
+ *
529
+ * @param {string} tableName
530
+ * @param {string} fileName
531
+ * @returns {Promise<Object>}
532
+ */
533
+ async getFileMetadata(tableName='',fileName=''){
534
+ const table=await this.getTableHandle(tableName)
535
+ const handle=await table.getFileHandle(fileName,{create:false})
536
+
537
+ if(typeof handle.getFile!=='function'){
538
+ return {
539
+ lastModified:null,
540
+ size:null,
541
+ type:''
542
+ }
543
+ }
544
+
545
+ const file=await handle.getFile()
546
+
547
+ return {
548
+ lastModified:file.lastModified||null,
549
+ size:file.size,
550
+ type:file.type||''
551
+ }
552
+ }
553
+
554
+ /**
555
+ * Writes multiple files into a table.
556
+ *
557
+ * @param {string} tableName
558
+ * @param {Object<string,*>} items
559
+ * @returns {Promise<PromiseSettledResult[]>}
560
+ */
561
+ async setMany(tableName,items){
562
+ const entries=Object.entries(items)
563
+
564
+ const setPromises=entries.map(
565
+ function setManyPromises([fileName,value]){
566
+ return this.set(tableName,fileName,value)
567
+ }.bind(this)
568
+ );
569
+
570
+ const results=await Promise.allSettled(setPromises);
571
+
572
+ results.forEach(
573
+ function setManyResultsItterator(result,index){
574
+ const fileName=entries[index][0]
575
+
576
+ if(result.status!=='fulfilled'){
577
+ console.error(`Failed to set file '${fileName}':`,result.reason)
578
+ }
579
+ }
580
+ );
581
+
582
+ return results
583
+ }
584
+
585
+ /**
586
+ * Reads a file from OPFS.
587
+ *
588
+ * @param {string} tableName
589
+ * @param {string} fileName
590
+ * @param {boolean} force
591
+ * @returns {Promise<*>}
592
+ */
593
+ async get(tableName='',fileName='',force=false){
594
+ if(force||!this.#tables[tableName]?.[fileName]){
595
+ try{
596
+ const file=await this.readFile(tableName,fileName)
597
+ const textContent=await file.text()
598
+
599
+ if(!this.#tables[tableName]){
600
+ this.#tables[tableName]={}
601
+ }
602
+
603
+ this.#tables[tableName][fileName]=parseFileValue(
604
+ fileName,
605
+ textContent
606
+ );
607
+ }catch(error){
608
+ if(error.name==='NotFoundError'){
609
+ return null
610
+ }
611
+
612
+ throw error
613
+ }
614
+ }
615
+
616
+ return this.#tables[tableName][fileName];
617
+ }
618
+
619
+ /**
620
+ * Reads multiple files.
621
+ *
622
+ * @param {string} tableName
623
+ * @param {string[]} items
624
+ * @returns {Promise<PromiseSettledResult[]>}
625
+ */
626
+ async getMany(tableName,items){
627
+ const getPromises=items.map(
628
+ function getManyPromises(fileName){
629
+ return this.get(tableName,fileName)
630
+ }.bind(this)
631
+ )
632
+
633
+ return Promise.allSettled(getPromises);
634
+ }
635
+
636
+ /**
637
+ * Reads all files from a table or the entire DB.
638
+ *
639
+ * @param {string} tableName
640
+ * @param {boolean} force
641
+ * Force each record to be read from OPFS instead of the in-memory cache.
642
+ * @returns {Promise<Object>}
643
+ */
644
+ async getAll(tableName='',force=false){
645
+ if(!is.boolean(force)){
646
+ throw new TypeError('DBOPFS.getAll force must be boolean');
647
+ }
648
+ const items={}
649
+
650
+ if(tableName){
651
+ const table=await this.getTableHandle(tableName)
652
+
653
+ const readPromises=[]
654
+
655
+ for await(const [name]of table.entries()){
656
+ readPromises.push(
657
+ this.get(tableName,name,force).then(
658
+ function assignValue(value){
659
+ items[name]=value
660
+ }
661
+ )
662
+ )
663
+ }
664
+
665
+ await Promise.all(readPromises)
666
+
667
+ return items
668
+ }
669
+
670
+ await this.getTableNames(true)
671
+
672
+ const tableNames=Object.keys(this.#tableHandles)
673
+
674
+ const tablePromises=tableNames.map(
675
+ async function loadTable(tableName){
676
+ const table=await this.getTableHandle(tableName);
677
+ const tableItems={};
678
+
679
+ const readPromises=[];
680
+
681
+ for await(const [name]of table.entries()){
682
+ readPromises.push(
683
+ this.get(tableName,name,force).then(
684
+ function assignValue(value){
685
+ tableItems[name]=value;
686
+ }
687
+ )
688
+ );
689
+ }
690
+
691
+ await Promise.all(readPromises);
692
+
693
+ items[tableName]=tableItems;
694
+ }.bind(this)
695
+ )
696
+
697
+ await Promise.all(tablePromises)
698
+
699
+ return items
700
+ }
701
+
702
+ /**
703
+ * Deletes a file.
704
+ *
705
+ * @param {string} tableName
706
+ * @param {string} fileName
707
+ * @returns {Promise<boolean>}
708
+ */
709
+ async delete(tableName='',fileName=''){
710
+ const table=await this.getTableHandle(tableName)
711
+
712
+ try{
713
+ await table.removeEntry(fileName)
714
+ }catch(error){
715
+ if(error.name!=='NotFoundError'){
716
+ console.error(error)
717
+ throw error
718
+ }
719
+ }
720
+
721
+ if(this.#tables[tableName]){
722
+ delete this.#tables[tableName][fileName]
723
+ }
724
+
725
+ return true
726
+ }
727
+
728
+ /**
729
+ * Deletes multiple files.
730
+ *
731
+ * @param {string} tableName
732
+ * @param {string[]} fileNames
733
+ * @returns {Promise<PromiseSettledResult[]>}
734
+ */
735
+ async deleteMany(tableName,fileNames){
736
+ const deletionPromises=fileNames.map(
737
+ function deleteManyPromises(fileName){
738
+ return this.delete(tableName,fileName)
739
+ }.bind(this)
740
+ );
741
+
742
+ return Promise.allSettled(deletionPromises);
743
+ }
744
+
745
+ /**
746
+ * Deletes a full table.
747
+ *
748
+ * @param {string} tableName
749
+ * @returns {Promise<boolean>}
750
+ */
751
+ async deleteTable(tableName){
752
+ try{
753
+ await this.#db.removeEntry(tableName,{recursive:true})
754
+
755
+ delete this.#tables[tableName]
756
+ delete this.#tableHandles[tableName]
757
+ }catch(error){
758
+ console.error(error)
759
+ }
760
+
761
+ return true
762
+ }
763
+
764
+ /**
765
+ * Clears only the current application's OPFS database.
766
+ *
767
+ * @returns {Promise<DBOPFS>}
768
+ */
769
+ async clearAllStorage(){
770
+ if(!this.ready){
771
+ await this.readyPromise;
772
+ }
773
+
774
+ for await(const [name]of this.#db.entries()){
775
+ await this.#db.removeEntry(name,{recursive:true})
776
+ }
777
+
778
+ this.#tables={}
779
+ this.#tableHandles={}
780
+ this.#writeLocks={}
781
+ await this.#createDefaultTables()
782
+
783
+ return this
784
+ }
785
+
786
+ /**
787
+ * Clears a table.
788
+ *
789
+ * @param {string} tableName
790
+ * @returns {Promise<void>}
791
+ */
792
+ async clear(tableName){
793
+ const table=await this.getTableHandle(tableName)
794
+
795
+ for await(const [name]of table.entries()){
796
+ await this.delete(tableName,name)
797
+ }
798
+ }
799
+
800
+ /**
801
+ * Returns all keys in a table.
802
+ *
803
+ * @param {string} tableName
804
+ * @returns {Promise<string[]>}
805
+ */
806
+ async getAllKeys(tableName){
807
+ const keys=[]
808
+ const table=await this.getTableHandle(tableName)
809
+
810
+ for await(const [name]of table.entries()){
811
+ keys.push(name)
812
+ }
813
+
814
+ return keys
815
+ }
816
+
817
+ /**
818
+ * Returns registered table names, or discovers physical OPFS directories.
819
+ * Discovered directories are registered so later reads and exports include
820
+ * data created outside the current page load.
821
+ *
822
+ * @param {boolean} discover
823
+ * @returns {Promise<string[]>}
824
+ */
825
+ async getTableNames(discover=false){
826
+ if(!discover){
827
+ return Object.keys(this.#tableHandles)
828
+ }
829
+
830
+ const tableNames=[]
831
+
832
+ for await(const [name,handle]of this.#db.entries()){
833
+ if(handle.kind!=='directory'){
834
+ continue
835
+ }
836
+
837
+ const registered=Object.values(this.#tableHandles).some(
838
+ tableHandle=>tableHandle.name===name
839
+ )
840
+
841
+ if(!registered){
842
+ this.#tableHandles[name]=handle
843
+ }
844
+
845
+ tableNames.push(name)
846
+ }
847
+
848
+ return tableNames
849
+ }
850
+
851
+ /**
852
+ * Filters files by key substring.
853
+ *
854
+ * @param {string} tableName
855
+ * @param {string} subString
856
+ * @returns {Promise<Object>}
857
+ */
858
+ async filterKeyIncludes(tableName,subString=''){
859
+ const items={}
860
+ const table=await this.getTableHandle(tableName)
861
+
862
+ for await(const [name]of table.entries()){
863
+ if(name.includes(subString)){
864
+ items[name]=await this.get(tableName,name)
865
+ }
866
+ }
867
+
868
+ return items
869
+ }
870
+
871
+ /**
872
+ * Checks if a key exists.
873
+ *
874
+ * @param {string} tableName
875
+ * @param {string} key
876
+ * @returns {Promise<boolean>}
877
+ */
878
+ async hasKey(tableName,key){
879
+ try{
880
+ const table=await this.getTableHandle(tableName)
881
+ await table.getFileHandle(key)
882
+ return true
883
+ }catch(err){
884
+ return false
885
+ }
886
+ }
887
+
888
+ /**
889
+ * Counts items in a table.
890
+ *
891
+ * @param {string} tableName
892
+ * @returns {Promise<number>}
893
+ */
894
+ async count(tableName){
895
+ let count=0
896
+ const table=await this.getTableHandle(tableName)
897
+
898
+ for await(const _ of table.entries()){
899
+ count++
900
+ }
901
+
902
+ return count
903
+ }
904
+
905
+ /**
906
+ * Downloads the entire database as a compressed PNG backup.
907
+ *
908
+ * The database is streamed as JSON, compressed using gzip via
909
+ * CompressionStream, and encoded into RGBA pixels inside a PNG.
910
+ *
911
+ * Each pixel stores 4 bytes of payload data.
912
+ * The first 4 bytes store the gzip byte length header.
913
+ *
914
+ * Output example:
915
+ *
916
+ * DBOPFS-backup-2026-03-10-13-45-12.png
917
+ *
918
+ * @param {string} name
919
+ * Base name used for the generated backup filename.
920
+ *
921
+ * @returns {Promise<void>}
922
+ */
923
+ async downloadCompressedPNG(name='DBOPFS-backup'){
924
+ if(!('CompressionStream' in window)){
925
+ throw new Error('CompressionStream not supported.')
926
+ }
927
+
928
+ const encoder=new TextEncoder()
929
+ await this.getTableNames(true)
930
+
931
+ const tableNames=Object.keys(this.#tableHandles)
932
+
933
+ const jsonStream=new ReadableStream({
934
+ start:async controller=>{
935
+ controller.enqueue(encoder.encode('{'))
936
+
937
+ for(let t=0;t<tableNames.length;t++){
938
+ const tableName=tableNames[t]
939
+ const table=await this.getTableHandle(tableName)
940
+
941
+ if(t>0){
942
+ controller.enqueue(encoder.encode(','))
943
+ }
944
+
945
+ controller.enqueue(
946
+ encoder.encode(`${JSON.stringify(tableName)}:{`)
947
+ )
948
+
949
+ let first=true
950
+
951
+ for await(const [fileName]of table.entries()){
952
+ const value=await this.get(tableName,fileName)
953
+
954
+ if(!first){
955
+ controller.enqueue(encoder.encode(','))
956
+ }
957
+
958
+ first=false
959
+
960
+ const json=`${JSON.stringify(fileName)}:${JSON.stringify(value)}`
961
+
962
+ controller.enqueue(encoder.encode(json))
963
+ }
964
+
965
+ controller.enqueue(encoder.encode('}'))
966
+ }
967
+
968
+ controller.enqueue(encoder.encode('}'))
969
+ controller.close()
970
+ }
971
+ })
972
+
973
+ const gzipStream=jsonStream.pipeThrough(
974
+ new CompressionStream('deflate')
975
+ )
976
+
977
+ const reader=gzipStream.getReader()
978
+
979
+ const chunks=[]
980
+ let totalLength=0
981
+
982
+ while(true){
983
+ const {done,value}=await reader.read()
984
+
985
+ if(done){
986
+ break
987
+ }
988
+
989
+ chunks.push(value)
990
+ totalLength+=value.length
991
+ }
992
+
993
+ const gzipBytes=new Uint8Array(totalLength)
994
+
995
+ let offset=0
996
+ for(const chunk of chunks){
997
+ gzipBytes.set(chunk,offset)
998
+ offset+=chunk.length
999
+ }
1000
+
1001
+ const payload=new Uint8Array(totalLength+4)
1002
+
1003
+ const view=new DataView(payload.buffer)
1004
+ view.setUint32(0,totalLength,true)
1005
+
1006
+ payload.set(gzipBytes,4)
1007
+
1008
+ const size=Math.ceil(Math.sqrt(payload.length/3))
1009
+
1010
+ const totalPixels=size*size
1011
+
1012
+ if(totalPixels*3 < payload.length){
1013
+ throw new Error('PNG canvas too small for payload.')
1014
+ }
1015
+
1016
+ const canvas=document.createElement('canvas')
1017
+ canvas.width=size
1018
+ canvas.height=size
1019
+
1020
+ const ctx=canvas.getContext('2d')
1021
+
1022
+ if(!ctx){
1023
+ throw new Error('Canvas 2D context unavailable.')
1024
+ }
1025
+
1026
+ const imgData=ctx.createImageData(size,size)
1027
+ imgData.data.fill(0);
1028
+
1029
+ let p=0;
1030
+
1031
+ for(let i=0;i<imgData.data.length;i+=4){
1032
+
1033
+ imgData.data[i] = payload[p] ?? 0;
1034
+ imgData.data[i+1] = payload[p+1] ?? 0;
1035
+ imgData.data[i+2] = payload[p+2] ?? 0;
1036
+ imgData.data[i+3] = 255;
1037
+
1038
+ p+=3;
1039
+ }
1040
+
1041
+ ctx.putImageData(imgData,0,0)
1042
+
1043
+ const blob=await new Promise(resolve=>canvas.toBlob(resolve,'image/png'))
1044
+
1045
+ if(!blob){
1046
+ throw new Error('PNG export failed.')
1047
+ }
1048
+
1049
+ const stamp=new Date()
1050
+ .toISOString()
1051
+ .slice(0,19)
1052
+ .replace(/[:T]/g,'-')
1053
+
1054
+ const url=URL.createObjectURL(blob)
1055
+
1056
+ const a=document.createElement('a')
1057
+ a.href=url
1058
+ a.download=`${name}-${stamp}.png`
1059
+
1060
+ document.body.appendChild(a)
1061
+ a.click()
1062
+ a.remove()
1063
+
1064
+ URL.revokeObjectURL(url)
1065
+ }
1066
+
1067
+ /**
1068
+ * Restores a database backup from a PNG image created by
1069
+ * `downloadCompressedPNG`.
1070
+ *
1071
+ * The PNG is decoded into RGBA pixel data,the compressed payload
1072
+ * is extracted using the stored byte length header, then
1073
+ * decompressed using DecompressionStream.
1074
+ *
1075
+ * The resulting JSON database structure is parsed and written
1076
+ * back into OPFS using `setMany`.
1077
+ *
1078
+ * Restore process:
1079
+ *
1080
+ * PNG → RGBA bytes → deflate payload → JSON → DBOPFS tables
1081
+ *
1082
+ * Any existing records with matching keys will be overwritten.
1083
+ *
1084
+ * @param {File|Blob} file
1085
+ * PNG backup file generated by DBOPFS.
1086
+ *
1087
+ * @returns {Promise<void>}
1088
+ * Resolves once the database restore process is complete.
1089
+ */
1090
+ async restoreFromPNG(file){
1091
+ const img=await createImageBitmap(
1092
+ file,
1093
+ {premultiplyAlpha:'none'}
1094
+ )
1095
+
1096
+ const canvas=document.createElement('canvas')
1097
+ canvas.width=img.width
1098
+ canvas.height=img.height
1099
+
1100
+ const ctx=canvas.getContext('2d')
1101
+
1102
+ if(!ctx){
1103
+ throw new Error('Canvas 2D context unavailable.')
1104
+ }
1105
+
1106
+ ctx.drawImage(img,0,0)
1107
+
1108
+ const data=ctx.getImageData(0,0,img.width,img.height).data
1109
+
1110
+ if(data.length < 4){
1111
+ throw new Error('Invalid PNG backup.')
1112
+ }
1113
+
1114
+ const payload=new Uint8Array((data.length/4)*3);
1115
+
1116
+ let p=0;
1117
+
1118
+ for(let i=0;i<data.length;i+=4){
1119
+ payload[p++] = data[i];
1120
+ payload[p++] = data[i+1];
1121
+ payload[p++] = data[i+2];
1122
+ }
1123
+
1124
+ const view=new DataView(payload.buffer)
1125
+
1126
+ const length=view.getUint32(0,true);
1127
+
1128
+ if(length <= 0 || length > payload.length-4){
1129
+ throw new Error('Invalid PNG backup payload length.')
1130
+ }
1131
+
1132
+ const gzipBytes=payload.slice(4,4+length)
1133
+
1134
+ const stream=new Blob([gzipBytes])
1135
+ .stream()
1136
+ .pipeThrough(new DecompressionStream('deflate'))
1137
+
1138
+ const json=await new Response(stream).text()
1139
+
1140
+ const db=JSON.parse(json)
1141
+
1142
+ const tables=Object.keys(db)
1143
+
1144
+ for(const table of tables){
1145
+ await this.setMany(table,db[table])
1146
+ }
1147
+ }
1148
+ }
1149
+
1150
+ if(typeof window.dbopfs?.get!=="function"){
1151
+ window.dbopfs=new DBOPFS();
1152
+ }
1153
+
1154
+ export default DBOPFS;