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.
- package/CHANGELOG.md +154 -0
- package/COMMERCIAL-LICENSE.md +13 -0
- package/LICENSE +661 -0
- package/NOTICE +70 -0
- package/README.md +448 -0
- package/bin/arcane-test.mjs +741 -0
- package/bin/arcane.mjs +5 -0
- package/docs/architecture.md +234 -0
- package/docs/compatibility.md +36 -0
- package/docs/event-manager.md +166 -0
- package/docs/platform-targets.md +108 -0
- package/docs/publishing.md +203 -0
- package/docs/reference/README.md +117 -0
- package/docs/reference/arcane-ollama.md +288 -0
- package/docs/reference/availability-and-normalization.md +123 -0
- package/docs/reference/behavioral-testing.md +86 -0
- package/docs/reference/cli.md +569 -0
- package/docs/reference/core/README.md +62 -0
- package/docs/reference/core/arcane-ai-contracts.md +873 -0
- package/docs/reference/core/arcane-api.md +601 -0
- package/docs/reference/core/arcane-entities.md +65 -0
- package/docs/reference/core/arcane-events.md +134 -0
- package/docs/reference/core/ollama-module.md +181 -0
- package/docs/reference/core/reference/arcane-api/ai-and-ollama.md +1909 -0
- package/docs/reference/core/reference/arcane-api/applications-terminal-capabilities.md +1057 -0
- package/docs/reference/core/reference/arcane-api/core-and-events.md +320 -0
- package/docs/reference/core/reference/arcane-api/filesystem-storage-preferences-appearance.md +610 -0
- package/docs/reference/core/reference/arcane-api/namespaces.md +1157 -0
- package/docs/reference/core/reference/arcane-api/platform-installation-users-system.md +1423 -0
- package/docs/reference/core/reference/arcane-api/session-provisioning-diagnostics-development.md +315 -0
- package/docs/reference/event-manager.md +957 -0
- package/docs/reference/inventory/package-api.json +2632 -0
- package/docs/reference/inventory/runtime-components.json +934 -0
- package/docs/reference/inventory/runtime-entities.json +26 -0
- package/docs/reference/inventory/runtime-modules.json +1249 -0
- package/docs/reference/protocols.md +242 -0
- package/docs/reference/runtime-components.md +1098 -0
- package/docs/reference/runtime-entities.md +303 -0
- package/docs/reference/runtime-modules.md +2010 -0
- package/docs/reference/sdk-api.md +4901 -0
- package/docs/roadmap.md +79 -0
- package/docs/work-amplification.md +124 -0
- package/node_modules/event-pubsub/CHANGELOG.md +55 -0
- package/node_modules/event-pubsub/MIGRATION.md +70 -0
- package/node_modules/event-pubsub/README.md +363 -0
- package/node_modules/event-pubsub/SECURITY.md +37 -0
- package/node_modules/event-pubsub/index.js +141 -0
- package/node_modules/event-pubsub/licence +21 -0
- package/node_modules/event-pubsub/package.json +59 -0
- package/node_modules/strong-type/README.md +408 -0
- package/node_modules/strong-type/assets/strong-type-header.png +0 -0
- package/node_modules/strong-type/index.js +1151 -0
- package/node_modules/strong-type/licence +21 -0
- package/node_modules/strong-type/node.js +125 -0
- package/node_modules/strong-type/package.json +61 -0
- package/package.json +95 -0
- package/runtime/ARCANE_RUNTIME_RELEASE.json +791 -0
- package/runtime/arcane/components/app-bar.html +468 -0
- package/runtime/arcane/components/assistant-panel.html +715 -0
- package/runtime/arcane/components/calculator.html +8 -0
- package/runtime/arcane/components/chart.html +655 -0
- package/runtime/arcane/components/chat.html +1225 -0
- package/runtime/arcane/components/conversation-view.html +13 -0
- package/runtime/arcane/components/dashboard-config.html +341 -0
- package/runtime/arcane/components/data-maintenance.html +112 -0
- package/runtime/arcane/components/data-view.html +92 -0
- package/runtime/arcane/components/directory-picker.html +197 -0
- package/runtime/arcane/components/document-inspector.html +252 -0
- package/runtime/arcane/components/file-drop.html +264 -0
- package/runtime/arcane/components/file-inspector.html +293 -0
- package/runtime/arcane/components/file-manager.html +1715 -0
- package/runtime/arcane/components/header.html +142 -0
- package/runtime/arcane/components/integration-settings.html +14 -0
- package/runtime/arcane/components/local-ai-status.html +360 -0
- package/runtime/arcane/components/markdown-document.html +1048 -0
- package/runtime/arcane/components/markdown-editor.html +360 -0
- package/runtime/arcane/components/media-embed.html +8 -0
- package/runtime/arcane/components/modal.html +402 -0
- package/runtime/arcane/components/output-panel.html +259 -0
- package/runtime/arcane/components/preferences-form.html +135 -0
- package/runtime/arcane/components/record-timeline.html +105 -0
- package/runtime/arcane/components/relationship-board.html +116 -0
- package/runtime/arcane/components/screen-capture.html +8 -0
- package/runtime/arcane/components/source-code-viewer.html +441 -0
- package/runtime/arcane/components/source-explanation.html +124 -0
- package/runtime/arcane/components/speech.html +365 -0
- package/runtime/arcane/components/summary-strip.html +177 -0
- package/runtime/arcane/components/table.html +77 -0
- package/runtime/arcane/components/task-progress.html +282 -0
- package/runtime/arcane/components/terminal-workspace.html +65 -0
- package/runtime/arcane/components/theme-editor.html +41 -0
- package/runtime/arcane/components/theme-switcher.html +46 -0
- package/runtime/arcane/components/unified-inbox.html +20 -0
- package/runtime/arcane/components/voice-transcription.html +476 -0
- package/runtime/arcane/components/weather-widget.html +8 -0
- package/runtime/arcane/components/web-navigator.html +239 -0
- package/runtime/arcane/css/communications.css +1 -0
- package/runtime/arcane/css/dashboard-config.css +45 -0
- package/runtime/arcane/css/document-site.css +981 -0
- package/runtime/arcane/css/layout.css +438 -0
- package/runtime/arcane/css/primitives.css +321 -0
- package/runtime/arcane/css/theme.css +112 -0
- package/runtime/arcane/css/utility-workspace.css +1 -0
- package/runtime/arcane/entities/ApiModelRecord.js +20 -0
- package/runtime/arcane/entities/Calculation.js +13 -0
- package/runtime/arcane/entities/Chat.js +581 -0
- package/runtime/arcane/entities/CommunicationMessage.js +29 -0
- package/runtime/arcane/entities/CommunicationThread.js +21 -0
- package/runtime/arcane/entities/Document.js +10 -0
- package/runtime/arcane/entities/File.js +143 -0
- package/runtime/arcane/entities/Image.js +135 -0
- package/runtime/arcane/entities/IntentEnvelope.js +834 -0
- package/runtime/arcane/entities/Preference.js +83 -0
- package/runtime/arcane/entities/TWiNPolicyDecision.js +1092 -0
- package/runtime/arcane/entities/TerminalSession.js +46 -0
- package/runtime/arcane/entities/Theme.js +107 -0
- package/runtime/arcane/entities/User.js +1046 -0
- package/runtime/arcane/entities/Weather.js +23 -0
- package/runtime/arcane/img/arcane-os-everywhere.png +0 -0
- package/runtime/arcane/img/arrow-left.png +0 -0
- package/runtime/arcane/img/arrow-right.png +0 -0
- package/runtime/arcane/img/doc.svg +5 -0
- package/runtime/arcane/img/folder.svg +4 -0
- package/runtime/arcane/img/image.svg +5 -0
- package/runtime/arcane/img/refresh.png +0 -0
- package/runtime/arcane/img/send.svg +9 -0
- package/runtime/arcane/img/trash.svg +5 -0
- package/runtime/arcane/img/upload.svg +5 -0
- package/runtime/arcane/modules/AI.js +2048 -0
- package/runtime/arcane/modules/AIPreferenceRuntime.js +32 -0
- package/runtime/arcane/modules/AIPreferenceTuple.js +92 -0
- package/runtime/arcane/modules/AIResponseLength.js +42 -0
- package/runtime/arcane/modules/AIResponseURLPolicy.js +626 -0
- package/runtime/arcane/modules/AnsiText.js +53 -0
- package/runtime/arcane/modules/ApiModelDatabase.js +25 -0
- package/runtime/arcane/modules/AppDataScope.js +245 -0
- package/runtime/arcane/modules/AppearancePreferences.js +28 -0
- package/runtime/arcane/modules/ArcaneCommunicationBridge.js +22 -0
- package/runtime/arcane/modules/ArcaneNavigationPolicy.js +135 -0
- package/runtime/arcane/modules/ArcaneNetworkPolicy.js +255 -0
- package/runtime/arcane/modules/AsyncBoundary.js +161 -0
- package/runtime/arcane/modules/BrowserTestSuite.js +326 -0
- package/runtime/arcane/modules/CalculatorEngine.js +20 -0
- package/runtime/arcane/modules/CaseEvidenceIndexer.js +134 -0
- package/runtime/arcane/modules/ChartLibrary.js +35 -0
- package/runtime/arcane/modules/ChatRecords.js +13 -0
- package/runtime/arcane/modules/CommunicationAppController.js +43 -0
- package/runtime/arcane/modules/CommunicationHub.js +16 -0
- package/runtime/arcane/modules/CommunicationPreferences.js +13 -0
- package/runtime/arcane/modules/CommunicationProviderRegistry.js +16 -0
- package/runtime/arcane/modules/ComponentContracts.js +586 -0
- package/runtime/arcane/modules/ConfiguredAIChatSession.js +288 -0
- package/runtime/arcane/modules/ConversationActionItems.js +488 -0
- package/runtime/arcane/modules/ConversationClosingReport.js +274 -0
- package/runtime/arcane/modules/ConversationTimebox.js +527 -0
- package/runtime/arcane/modules/CoreLocalModelCatalog.js +255 -0
- package/runtime/arcane/modules/DBLS.js +171 -0
- package/runtime/arcane/modules/DBOPFS.js +1154 -0
- package/runtime/arcane/modules/DBOPFSWorker.js +116 -0
- package/runtime/arcane/modules/DataMaintenance.js +91 -0
- package/runtime/arcane/modules/DevelopmentWorkspace.js +74 -0
- package/runtime/arcane/modules/DirectoryPicker.js +109 -0
- package/runtime/arcane/modules/DocumentNavigation.js +223 -0
- package/runtime/arcane/modules/Errors.js +1025 -0
- package/runtime/arcane/modules/GifEncoder.js +29 -0
- package/runtime/arcane/modules/HTMLImport.js +114 -0
- package/runtime/arcane/modules/InMemoryCommunicationProvider.js +14 -0
- package/runtime/arcane/modules/IsolatedModelQuestionRunner.js +275 -0
- package/runtime/arcane/modules/LocalAIReadiness.js +870 -0
- package/runtime/arcane/modules/LocalAIReadinessController.js +156 -0
- package/runtime/arcane/modules/MD.js +111 -0
- package/runtime/arcane/modules/Mail.js +352 -0
- package/runtime/arcane/modules/MailTransport.mjs +180 -0
- package/runtime/arcane/modules/Marked.min.js +71 -0
- package/runtime/arcane/modules/MemoryRecords.js +44 -0
- package/runtime/arcane/modules/MessageAdvisory.js +38 -0
- package/runtime/arcane/modules/ModelDefinition.js +189 -0
- package/runtime/arcane/modules/Ollama.js +74 -0
- package/runtime/arcane/modules/OllamaModelIdentifier.js +21 -0
- package/runtime/arcane/modules/OllamaSettings.js +24 -0
- package/runtime/arcane/modules/OpenMeteoWeatherProvider.js +14 -0
- package/runtime/arcane/modules/PreferenceStore.js +109 -0
- package/runtime/arcane/modules/QRCode.min.js +1 -0
- package/runtime/arcane/modules/Questionnaire.js +61 -0
- package/runtime/arcane/modules/RecordLinkIndex.js +24 -0
- package/runtime/arcane/modules/RecordPassageIndex.js +222 -0
- package/runtime/arcane/modules/RecordReviewStore.js +94 -0
- package/runtime/arcane/modules/RevocableProjectionLedger.js +1623 -0
- package/runtime/arcane/modules/RiskSignalAnalyzer.js +37 -0
- package/runtime/arcane/modules/ScamRiskPolicy.js +62 -0
- package/runtime/arcane/modules/ScopedOPFSCache.js +183 -0
- package/runtime/arcane/modules/ScreenCapture.js +20 -0
- package/runtime/arcane/modules/SpeechPlayback.js +581 -0
- package/runtime/arcane/modules/StaticDocumentCatalog.js +1248 -0
- package/runtime/arcane/modules/SystemAppearance.js +20 -0
- package/runtime/arcane/modules/SystemPlatformPresentation.js +51 -0
- package/runtime/arcane/modules/SystemToolRegistry.js +28 -0
- package/runtime/arcane/modules/TerminalClient.js +52 -0
- package/runtime/arcane/modules/TerminalCommandRegistry.js +50 -0
- package/runtime/arcane/modules/ThemeBootstrap.js +26 -0
- package/runtime/arcane/modules/ThemeManager.js +131 -0
- package/runtime/arcane/modules/TimeGuard.js +149 -0
- package/runtime/arcane/modules/ToolCallRouter.js +83 -0
- package/runtime/arcane/modules/WaitForComponent.js +102 -0
- package/runtime/arcane/modules/YouTubeMedia.js +16 -0
- package/runtime/arcane/modules/uPlot.LICENSE.txt +21 -0
- package/runtime/arcane/modules/uPlot.iife.min.js +2 -0
- package/runtime/arcane/modules/uPlot.min.css +1 -0
- package/runtime/arcane/security/arcane-network-policy.json +6 -0
- package/runtime/strong-type/index.js +352 -0
- package/runtime/strong-type/licence +21 -0
- package/runtime/strong-type/package.json +45 -0
- package/schemas/arcane-app-bundle.schema.json +125 -0
- package/schemas/arcane-app.schema.json +329 -0
- package/schemas/arcane-lock.schema.json +86 -0
- package/schemas/arcane-package.schema.json +224 -0
- package/schemas/cli-event.schema.json +122 -0
- package/schemas/event-stack.schema.json +152 -0
- package/schemas/native-build-plan.schema.json +176 -0
- package/schemas/target-adapter.schema.json +117 -0
- package/src/app-descriptor.mjs +500 -0
- package/src/cli/main.mjs +561 -0
- package/src/constants.mjs +31 -0
- package/src/dev-server.mjs +718 -0
- package/src/doctor.mjs +315 -0
- package/src/dom-event-instrumentation.mjs +594 -0
- package/src/errors.mjs +75 -0
- package/src/event-manager.mjs +1342 -0
- package/src/event-queue.mjs +138 -0
- package/src/events.mjs +219 -0
- package/src/index.mjs +177 -0
- package/src/integrated-provider-loader.mjs +432 -0
- package/src/native-plan.mjs +698 -0
- package/src/native-provider-loader.mjs +1126 -0
- package/src/packager/core.mjs +2691 -0
- package/src/process.mjs +353 -0
- package/src/release-bundle.mjs +2523 -0
- package/src/repository.mjs +90 -0
- package/src/runtime.mjs +452 -0
- package/src/scaffold.mjs +380 -0
- package/src/targets/index.mjs +436 -0
- package/src/templates/assets/app-icon.png +0 -0
- package/src/templates/workspace-template.mjs +388 -0
- package/src/testing-loader.mjs +9 -0
- package/src/testing.mjs +427 -0
- package/src/toolchain.mjs +1335 -0
- package/src/update-check.mjs +307 -0
- 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;
|