arcane-os 0.34.2 → 0.35.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,22 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.35.0
4
+
5
+ - Add `DBOPFS.createCompressedPNG({tableNames, additionalTables, signal})` to
6
+ return a PNG Blob without downloading or writing storage. Omitted table names
7
+ select all saved tables; an empty list selects none. Caller-supplied tables
8
+ replace matching saved tables in the export only.
9
+ - Preserve `downloadCompressedPNG(name)` and accept the same optional export
10
+ options. Cancellation stops further preparation and prevents a later download;
11
+ browser file reads and canvas encoding already underway finish normally.
12
+ - Add optional async `restoreFromPNG(file, {selectTables})` selection over the
13
+ complete decoded table/file map before any writes. The callback owns selection
14
+ and projection; existing one-argument restore remains supported.
15
+ - Report rejected restore writes after all selected table batches settle through
16
+ `DBOPFS_RESTORE_WRITE_FAILED`, retaining original errors and table/file details.
17
+ Successful writes remain saved; restore is not an atomic transaction. Release
18
+ decoded image resources on both success and extraction failure.
19
+
3
20
  ## 0.34.2
4
21
 
5
22
  - Remove temporary `persist:false` Chat cards on completion, failure,
package/README.md CHANGED
@@ -19,7 +19,7 @@ version-locked SDK runtime, while an integrated Arcane checkout uses its live
19
19
  `arcane/` runtime. Both profiles share the theme, packaging, event, cancellation,
20
20
  and browser run contracts while retaining their selected application layout.
21
21
 
22
- This checkout defines the `0.34.2` SDK contract. Applications pin one exact npm
22
+ This checkout defines the `0.35.0` SDK contract. Applications pin one exact npm
23
23
  version and lockfile; registry state is deliberately not baked into application
24
24
  artifacts.
25
25
 
@@ -568,7 +568,7 @@
568
568
  "availability": "Browser / native WebView",
569
569
  "protocol": "OPFS, DBOPFSWorker, Compression Streams, and per-realm globalThis.arcaneEvents authority",
570
570
  "normalization": "App scope and recognized file parsing are normalized; nonblank unreadable JSONL rows remain complete raw strings, empty-table removal reports removed, absent, or not-empty after a create:false directory lookup and without scanning or recursively clearing a directory, same-named files are preserved, aliases and cached handles are invalidated only when the directory is gone, and unexpected DOM/storage errors are preserved.",
571
- "surface": "default `DBOPFS`; installs `window.dbopfs`, emits `dbopfs-ready`; table/file/backup APIs including removeEmptyTable(tableName)."
571
+ "surface": "default `DBOPFS`; installs `window.dbopfs`, emits `dbopfs-ready`; table/file/backup APIs including removeEmptyTable(tableName). createCompressedPNG({tableNames,additionalTables,signal}) returns a PNG Blob without download or record writes; omitted names select all, [] none, and supplied tables replace matching saved tables in the export. downloadCompressedPNG(name,options) reuses the same preparation and initiates one download; cancellation prevents it. restoreFromPNG(file,{selectTables}) closes the decoded image after pixel extraction, awaits the optional caller selection/projection before any writes, and rejects DBOPFS_RESTORE_WRITE_FAILED AggregateError after all selected table batches settle when records fail, preserving original errors and failures {tableName,fileName,reason}; successful writes remain. No-options restore and setMany retain their contracts."
572
572
  },
573
573
  {
574
574
  "file": "runtime/arcane/modules/DBOPFSDocumentLibrary.js",
@@ -1986,6 +1986,39 @@ default `DBOPFS`; installs `window.dbopfs`, emits `dbopfs-ready`; table/file/bac
1986
1986
 
1987
1987
  Exact exports: `DBOPFS_EVENT_TYPES`, `DBOPFS_REASONS`, `default`.
1988
1988
 
1989
+ `createCompressedPNG({tableNames, additionalTables, signal})` returns an
1990
+ `image/png` Blob without downloading or writing records. Omitted `tableNames`
1991
+ selects all discovered saved tables; `[]` selects none. Named absent tables
1992
+ are encoded as empty tables without creating OPFS directories. The existing
1993
+ `memories`/`memory` alias is retained. `additionalTables` is an application-owned
1994
+ `{tableName:{fileName:value}}` map. Each supplied table replaces the matching
1995
+ saved table in this export only, rather than inheriting records the caller did
1996
+ not select. Values retain the existing JSON serialization contract. Applications
1997
+ own recipient policy, profile projections, and any product format marker.
1998
+
1999
+ `downloadCompressedPNG(name, options)` uses that same method and its options,
2000
+ then initiates one timestamp-named download. Existing `downloadCompressedPNG(name)`
2001
+ calls retain full-database behavior. An optional `AbortSignal` stops further
2002
+ record preparation and prevents a later download. File reads and browser canvas
2003
+ encoding already in progress cannot be interrupted; their result is discarded
2004
+ on cancellation. No object URL or download is created by `createCompressedPNG`.
2005
+
2006
+ `restoreFromPNG(file, {selectTables})` preserves the existing deflate/RGB backup format and
2007
+ restores through `setMany()` without changing its settled-result API or per-key
2008
+ write ordering. The optional callback receives the complete decoded table/file
2009
+ map exactly once and may return a map or promise of a map. Only its returned
2010
+ tables and records are written, allowing application-owned selection and
2011
+ projection of older full backups without a second decoder or temporary storage
2012
+ writes. Callback failure rejects before any writes. With no callback, all
2013
+ decoded tables are restored as before.
2014
+
2015
+ Restore resolves only when every selected record write succeeds. It
2016
+ continues the table batches after individual rejected writes, then rejects an
2017
+ `AggregateError` with code `DBOPFS_RESTORE_WRITE_FAILED`, original reasons in
2018
+ `errors`, and `failures` entries `{tableName, fileName, reason}`. Successful
2019
+ writes remain saved; restore is not an all-or-nothing transaction. The decoded
2020
+ image is closed after pixel extraction, including canvas failures.
2021
+
1989
2022
  ### Availability and normalization
1990
2023
 
1991
2024
  **Browser / native WebView.** App scope and recognized JSON/JSONL file parsing
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "arcane-os",
3
- "version": "0.34.2",
3
+ "version": "0.35.0",
4
4
  "description": "Arcane OS JavaScript SDK, project-local CLI, browser runtime, and repository-portable application packager.",
5
5
  "type": "module",
6
6
  "main": "./src/index.mjs",
@@ -86,7 +86,7 @@
86
86
  "test": "npm run test:unit && npm run test:functional && npm run test:integration && npm run test:regression",
87
87
  "test:release": "node ./bin/arcane-test.mjs test/npm-release.test.mjs",
88
88
  "test:unit": "node ./bin/arcane-test.mjs test/app-descriptor.test.mjs test/app-schema.test.mjs test/app-selection.test.mjs test/contracts.test.mjs test/doctor.test.mjs test/mail-credentials.test.mjs test/mail-outbox.test.mjs test/mail-public-api.test.mjs test/mail-send.test.mjs test/mail-transport.test.mjs test/targets.test.mjs test/workspace-operation-lock.test.mjs",
89
- "test:functional": "node ./bin/arcane-test.mjs test/ai-request-model.test.mjs test/ai-request-retry.test.mjs test/ai-stream-cancellation.test.mjs test/browser-speech-providers.test.mjs test/browser-wasm-gpu-notice.test.mjs test/browser-wasm-download-resume.test.mjs test/chat-stream-speech.test.mjs test/chat-transcript-scroll.test.mjs test/chat-transient-retention.test.mjs test/cli.test.mjs test/dbopfs-document-library.test.mjs test/dbopfs.test.mjs test/dev-server.test.mjs test/dev-pwa.test.mjs test/dom-event-instrumentation.test.mjs test/event-manager.test.mjs test/events.test.mjs test/import-map.test.mjs test/mail-cli.test.mjs test/mail-runtime.test.mjs test/mail-server.test.mjs test/modal.test.mjs test/packaging.test.mjs test/pwa-packaging.test.mjs test/pwa-client.test.mjs test/pwa-install.test.mjs test/pwa-worker.test.mjs test/persistent-ai-chat-session.test.mjs test/reference-completeness.test.mjs test/runtime-api-behavior.test.mjs test/runtime.test.mjs test/scaffold.test.mjs test/speech-playback.test.mjs test/site.test.mjs test/twin-cloud.test.mjs test/update-check.test.mjs test/user-explicit-updates.test.mjs",
89
+ "test:functional": "node ./bin/arcane-test.mjs test/ai-request-model.test.mjs test/ai-request-retry.test.mjs test/ai-stream-cancellation.test.mjs test/browser-speech-providers.test.mjs test/browser-wasm-gpu-notice.test.mjs test/browser-wasm-download-resume.test.mjs test/chat-stream-speech.test.mjs test/chat-transcript-scroll.test.mjs test/chat-transient-retention.test.mjs test/cli.test.mjs test/dbopfs-document-library.test.mjs test/dbopfs-png.test.mjs test/dbopfs.test.mjs test/dev-server.test.mjs test/dev-pwa.test.mjs test/dom-event-instrumentation.test.mjs test/event-manager.test.mjs test/events.test.mjs test/import-map.test.mjs test/mail-cli.test.mjs test/mail-runtime.test.mjs test/mail-server.test.mjs test/modal.test.mjs test/packaging.test.mjs test/pwa-packaging.test.mjs test/pwa-client.test.mjs test/pwa-install.test.mjs test/pwa-worker.test.mjs test/persistent-ai-chat-session.test.mjs test/reference-completeness.test.mjs test/runtime-api-behavior.test.mjs test/runtime.test.mjs test/scaffold.test.mjs test/speech-playback.test.mjs test/site.test.mjs test/twin-cloud.test.mjs test/update-check.test.mjs test/user-explicit-updates.test.mjs",
90
90
  "test:integration": "node ./bin/arcane-test.mjs test/installed-package-runtime.test.mjs test/root-app-layout.test.mjs test/integrated-shared.test.mjs test/integrated-workspace.test.mjs test/mail-browser.test.mjs test/native-plan.test.mjs test/native-provider-loader.test.mjs test/npm-release.test.mjs test/release-bundle.test.mjs test/release-capability-smoke.test.mjs test/shared-payload-batch.test.mjs test/tarball.test.mjs test/browser-wasm-cpu.test.mjs test/wllama-webgpu-runtime.test.mjs",
91
91
  "test:regression": "node ./bin/arcane-test.mjs test/channel-workflows.test.mjs test/html-import-registration.test.mjs test/logging-regression.test.mjs test/markdown-speech.test.mjs test/prepared-speech.test.mjs test/native-provider-generation.test.mjs test/speech-queue-regression.test.mjs test/testing.test.mjs test/test-sets.test.mjs",
92
92
  "check": "node tools/check-source.mjs && npm test",
@@ -1049,165 +1049,194 @@ class DBOPFS {
1049
1049
  }
1050
1050
 
1051
1051
  /**
1052
- * Downloads the entire database as a compressed PNG backup.
1052
+ * Creates a PNG backup without downloading or writing database records.
1053
+ * JSON is deflated and framed with the existing little-endian payload
1054
+ * length, then encoded in RGB channels with opaque alpha for restoration.
1053
1055
  *
1054
- * The database is streamed as JSON, compressed using gzip via
1055
- * CompressionStream, and encoded into RGBA pixels inside a PNG.
1056
- *
1057
- * Each pixel stores 4 bytes of payload data.
1058
- * The first 4 bytes store the gzip byte length header.
1059
- *
1060
- * Output example:
1061
- *
1062
- * DBOPFS-backup-2026-03-10-13-45-12.png
1063
- *
1064
- * @param {string} name
1065
- * Base name used for the generated backup filename.
1066
- *
1067
- * @returns {Promise<void>}
1056
+ * @param {Object} options
1057
+ * @param {string[]} [options.tableNames] Saved tables; omitted selects all.
1058
+ * @param {Object<string,Object<string,*>>} [options.additionalTables]
1059
+ * Caller-owned tables. Each replaces the matching saved table in this
1060
+ * export only. Values must be JSON-serializable; no storage write occurs.
1061
+ * @param {AbortSignal} [options.signal]
1062
+ * Stops further preparation and prevents a subsequent download. Browser
1063
+ * file reads and canvas encoding already in progress cannot be interrupted.
1064
+ * @returns {Promise<Blob>} An image/png Blob in the existing backup format.
1068
1065
  */
1069
- async downloadCompressedPNG(name='DBOPFS-backup'){
1070
- if(!('CompressionStream' in window)){
1071
- throw new Error('CompressionStream not supported.')
1066
+ async createCompressedPNG({tableNames, additionalTables = {}, signal} = {}) {
1067
+ signal?.throwIfAborted();
1068
+ if (!('CompressionStream' in window)) {
1069
+ throw new Error('CompressionStream not supported.');
1072
1070
  }
1073
1071
 
1074
- const encoder=new TextEncoder()
1075
- await this.getTableNames(true)
1076
-
1077
- const tableNames=Object.keys(this.#tableHandles)
1078
-
1079
- const jsonStream=new ReadableStream({
1080
- start:async controller=>{
1081
- controller.enqueue(encoder.encode('{'))
1072
+ await this.readyPromise;
1073
+ signal?.throwIfAborted();
1074
+ await this.getTableNames(true);
1075
+ signal?.throwIfAborted();
1082
1076
 
1083
- for(let t=0;t<tableNames.length;t++){
1084
- const tableName=tableNames[t]
1085
- const table=await this.getTableHandle(tableName)
1086
-
1087
- if(t>0){
1088
- controller.enqueue(encoder.encode(','))
1089
- }
1090
-
1091
- controller.enqueue(
1092
- encoder.encode(`${JSON.stringify(tableName)}:{`)
1093
- )
1094
-
1095
- let first=true
1077
+ function logicalTableName(name) {
1078
+ return tableNameForDirectory(directoryNameForTable(name));
1079
+ }
1096
1080
 
1097
- for await(const [fileName]of table.entries()){
1098
- const value=await this.get(tableName,fileName)
1081
+ const selectedNames = new Set(
1082
+ (tableNames === undefined ? Object.keys(this.#tableHandles) : tableNames)
1083
+ .map(logicalTableName)
1084
+ );
1085
+ const suppliedTables = new Map();
1086
+ for (const [name, records] of Object.entries(additionalTables)) {
1087
+ const tableName = logicalTableName(name);
1088
+ selectedNames.add(tableName);
1089
+ suppliedTables.set(tableName, records);
1090
+ }
1099
1091
 
1100
- if(!first){
1101
- controller.enqueue(encoder.encode(','))
1102
- }
1092
+ const database = this;
1093
+ const encoder = new TextEncoder();
1103
1094
 
1104
- first=false
1095
+ async function* tableRecords(tableName) {
1096
+ if (suppliedTables.has(tableName)) {
1097
+ yield* Object.entries(suppliedTables.get(tableName));
1098
+ return;
1099
+ }
1105
1100
 
1106
- const json=`${JSON.stringify(fileName)}:${JSON.stringify(value)}`
1101
+ const table = database.#tableHandles[tableName];
1102
+ if (!table) return;
1103
+ for await (const [fileName] of table.entries()) {
1104
+ signal?.throwIfAborted();
1105
+ const value = await database.get(tableName, fileName);
1106
+ signal?.throwIfAborted();
1107
+ yield [fileName, value];
1108
+ }
1109
+ }
1107
1110
 
1108
- controller.enqueue(encoder.encode(json))
1111
+ async function* encodeDatabase() {
1112
+ yield encoder.encode('{');
1113
+ let firstTable = true;
1114
+ for (const tableName of selectedNames) {
1115
+ signal?.throwIfAborted();
1116
+ if (!firstTable) yield encoder.encode(',');
1117
+ firstTable = false;
1118
+ yield encoder.encode(`${JSON.stringify(tableName)}:{`);
1119
+
1120
+ let firstRecord = true;
1121
+ for await (const [fileName, value] of tableRecords(tableName)) {
1122
+ signal?.throwIfAborted();
1123
+ if (!firstRecord) yield encoder.encode(',');
1124
+ firstRecord = false;
1125
+ const serialized = JSON.stringify(value);
1126
+ if (serialized === undefined) {
1127
+ throw new TypeError(`PNG record ${tableName}/${fileName} is not JSON-serializable.`);
1109
1128
  }
1110
-
1111
- controller.enqueue(encoder.encode('}'))
1129
+ yield encoder.encode(`${JSON.stringify(fileName)}:${serialized}`);
1112
1130
  }
1113
-
1114
- controller.enqueue(encoder.encode('}'))
1115
- controller.close()
1116
- }
1117
- })
1118
-
1119
- const gzipStream=jsonStream.pipeThrough(
1120
- new CompressionStream('deflate')
1121
- )
1122
-
1123
- const reader=gzipStream.getReader()
1124
-
1125
- const chunks=[]
1126
- let totalLength=0
1127
-
1128
- while(true){
1129
- const {done,value}=await reader.read()
1130
-
1131
- if(done){
1132
- break
1131
+ yield encoder.encode('}');
1133
1132
  }
1134
-
1135
- chunks.push(value)
1136
- totalLength+=value.length
1133
+ yield encoder.encode('}');
1137
1134
  }
1138
1135
 
1139
- const gzipBytes=new Uint8Array(totalLength)
1140
-
1141
- let offset=0
1142
- for(const chunk of chunks){
1143
- gzipBytes.set(chunk,offset)
1144
- offset+=chunk.length
1136
+ const encoded = encodeDatabase();
1137
+ const jsonStream = new ReadableStream(
1138
+ {
1139
+ async pull(controller) {
1140
+ signal?.throwIfAborted();
1141
+ const {done, value} = await encoded.next();
1142
+ if (done) controller.close();
1143
+ else controller.enqueue(value);
1144
+ },
1145
+ async cancel() {
1146
+ await encoded.return();
1147
+ }
1148
+ }
1149
+ );
1150
+ const compressedStream = jsonStream.pipeThrough(
1151
+ new CompressionStream('deflate'),
1152
+ {signal}
1153
+ );
1154
+ const reader = compressedStream.getReader();
1155
+ const chunks = [];
1156
+ let totalLength = 0;
1157
+ try {
1158
+ while (true) {
1159
+ const {done, value} = await reader.read();
1160
+ signal?.throwIfAborted();
1161
+ if (done) break;
1162
+ chunks.push(value);
1163
+ totalLength += value.length;
1164
+ }
1165
+ } finally {
1166
+ reader.releaseLock();
1145
1167
  }
1146
1168
 
1147
- const payload=new Uint8Array(totalLength+4)
1148
-
1149
- const view=new DataView(payload.buffer)
1150
- view.setUint32(0,totalLength,true)
1151
-
1152
- payload.set(gzipBytes,4)
1153
-
1154
- const size=Math.ceil(Math.sqrt(payload.length/3))
1155
-
1156
- const totalPixels=size*size
1157
-
1158
- if(totalPixels*3 < payload.length){
1159
- throw new Error('PNG canvas too small for payload.')
1169
+ // Length and channel offsets belong only to the existing PNG format.
1170
+ const payload = new Uint8Array(totalLength + 4);
1171
+ const view = new DataView(payload.buffer);
1172
+ view.setUint32(0, totalLength, true);
1173
+ let offset = 4;
1174
+ for (const chunk of chunks) {
1175
+ payload.set(chunk, offset);
1176
+ offset += chunk.length;
1160
1177
  }
1161
1178
 
1162
- const canvas=document.createElement('canvas')
1163
- canvas.width=size
1164
- canvas.height=size
1165
-
1166
- const ctx=canvas.getContext('2d')
1167
-
1168
- if(!ctx){
1169
- throw new Error('Canvas 2D context unavailable.')
1179
+ const size = Math.ceil(Math.sqrt(payload.length / 3));
1180
+ const canvas = document.createElement('canvas');
1181
+ canvas.width = size;
1182
+ canvas.height = size;
1183
+ const ctx = canvas.getContext('2d');
1184
+ if (!ctx) {
1185
+ throw new Error('Canvas 2D context unavailable.');
1170
1186
  }
1171
1187
 
1172
- const imgData=ctx.createImageData(size,size)
1173
- imgData.data.fill(0);
1174
-
1175
- let p=0;
1176
-
1177
- for(let i=0;i<imgData.data.length;i+=4){
1178
-
1179
- imgData.data[i] = payload[p] ?? 0;
1180
- imgData.data[i+1] = payload[p+1] ?? 0;
1181
- imgData.data[i+2] = payload[p+2] ?? 0;
1182
- imgData.data[i+3] = 255;
1183
-
1184
- p+=3;
1188
+ const imgData = ctx.createImageData(size, size);
1189
+ let position = 0;
1190
+ for (let index = 0; index < imgData.data.length; index += 4) {
1191
+ imgData.data[index] = payload[position] ?? 0;
1192
+ imgData.data[index + 1] = payload[position + 1] ?? 0;
1193
+ imgData.data[index + 2] = payload[position + 2] ?? 0;
1194
+ imgData.data[index + 3] = 255;
1195
+ position += 3;
1185
1196
  }
1197
+ ctx.putImageData(imgData, 0, 0);
1186
1198
 
1187
- ctx.putImageData(imgData,0,0)
1188
-
1189
- const blob=await new Promise(resolve=>canvas.toBlob(resolve,'image/png'))
1190
-
1191
- if(!blob){
1192
- throw new Error('PNG export failed.')
1199
+ const blob = await new Promise(
1200
+ function encodeBackupPNG(resolve) {
1201
+ canvas.toBlob(resolve, 'image/png');
1202
+ }
1203
+ );
1204
+ signal?.throwIfAborted();
1205
+ if (!blob) {
1206
+ throw new Error('PNG export failed.');
1193
1207
  }
1208
+ return blob;
1209
+ }
1194
1210
 
1195
- const stamp=new Date()
1211
+ /**
1212
+ * Downloads one PNG backup using the same options as createCompressedPNG.
1213
+ * Existing one-argument callers still export the whole database.
1214
+ * @param {string} name Base filename; an ISO timestamp is appended.
1215
+ * @param {Object} options PNG table selection and optional signal.
1216
+ * @returns {Promise<void>}
1217
+ */
1218
+ async downloadCompressedPNG(name = 'DBOPFS-backup', options = {}) {
1219
+ const blob = await this.createCompressedPNG(options);
1220
+ options.signal?.throwIfAborted();
1221
+ const stamp = new Date()
1196
1222
  .toISOString()
1197
- .slice(0,19)
1198
- .replace(/[:T]/g,'-')
1199
-
1200
- const url=URL.createObjectURL(blob)
1201
-
1202
- const a=document.createElement('a')
1203
- a.href=url
1204
- a.download=`${name}-${stamp}.png`
1205
-
1206
- document.body.appendChild(a)
1207
- a.click()
1208
- a.remove()
1209
-
1210
- URL.revokeObjectURL(url)
1223
+ .slice(0, 19)
1224
+ .replace(/[:T]/g, '-');
1225
+
1226
+ const url = URL.createObjectURL(blob);
1227
+
1228
+ const a = document.createElement('a');
1229
+ a.href = url;
1230
+ a.download = `${name}-${stamp}.png`;
1231
+
1232
+ try {
1233
+ document.body.appendChild(a);
1234
+ options.signal?.throwIfAborted();
1235
+ a.click();
1236
+ } finally {
1237
+ a.remove();
1238
+ URL.revokeObjectURL(url);
1239
+ }
1211
1240
  }
1212
1241
 
1213
1242
  /**
@@ -1229,30 +1258,39 @@ class DBOPFS {
1229
1258
  *
1230
1259
  * @param {File|Blob} file
1231
1260
  * PNG backup file generated by DBOPFS.
1261
+ * @param {Object} options
1262
+ * @param {function(Object): Object|Promise<Object>} [options.selectTables]
1263
+ * Receives the complete decoded table/file map once, before writes. Only
1264
+ * its returned map is restored; the application owns selection/projection.
1232
1265
  *
1233
1266
  * @returns {Promise<void>}
1234
- * Resolves once the database restore process is complete.
1267
+ * Resolves after every write succeeds. Rejected writes are collected after
1268
+ * all table batches settle and reported in an AggregateError with the
1269
+ * original reasons and table/file associations; successful writes remain.
1235
1270
  */
1236
- async restoreFromPNG(file){
1271
+ async restoreFromPNG(file, {selectTables} = {}) {
1237
1272
  const img=await createImageBitmap(
1238
1273
  file,
1239
1274
  {premultiplyAlpha:'none'}
1240
1275
  )
1241
1276
 
1242
- const canvas=document.createElement('canvas')
1243
- canvas.width=img.width
1244
- canvas.height=img.height
1277
+ let data;
1278
+ try {
1279
+ const canvas = document.createElement('canvas');
1280
+ canvas.width = img.width;
1281
+ canvas.height = img.height;
1245
1282
 
1246
- const ctx=canvas.getContext('2d')
1283
+ const ctx = canvas.getContext('2d');
1284
+ if (!ctx) {
1285
+ throw new Error('Canvas 2D context unavailable.');
1286
+ }
1247
1287
 
1248
- if(!ctx){
1249
- throw new Error('Canvas 2D context unavailable.')
1288
+ ctx.drawImage(img, 0, 0);
1289
+ data = ctx.getImageData(0, 0, img.width, img.height).data;
1290
+ } finally {
1291
+ img.close();
1250
1292
  }
1251
1293
 
1252
- ctx.drawImage(img,0,0)
1253
-
1254
- const data=ctx.getImageData(0,0,img.width,img.height).data
1255
-
1256
1294
  if(data.length < 4){
1257
1295
  throw new Error('Invalid PNG backup.')
1258
1296
  }
@@ -1283,12 +1321,38 @@ class DBOPFS {
1283
1321
 
1284
1322
  const json=await new Response(stream).text()
1285
1323
 
1286
- const db=JSON.parse(json)
1324
+ const decodedTables = JSON.parse(json);
1325
+ const db = selectTables === undefined
1326
+ ? decodedTables
1327
+ : await selectTables(decodedTables);
1287
1328
 
1288
1329
  const tables=Object.keys(db)
1289
1330
 
1290
- for(const table of tables){
1291
- await this.setMany(table,db[table])
1331
+ const failures = [];
1332
+ for (const tableName of tables) {
1333
+ const fileNames = Object.keys(db[tableName]);
1334
+ const results = await this.setMany(tableName, db[tableName]);
1335
+ for (const [index, result] of results.entries()) {
1336
+ if (result.status === 'rejected') {
1337
+ failures.push(
1338
+ {tableName, fileName: fileNames[index], reason: result.reason}
1339
+ );
1340
+ }
1341
+ }
1342
+ }
1343
+
1344
+ if (failures.length > 0) {
1345
+ const error = new AggregateError(
1346
+ failures.map(
1347
+ function restoreWriteReason(failure) {
1348
+ return failure.reason;
1349
+ }
1350
+ ),
1351
+ 'PNG restore could not save every record.'
1352
+ );
1353
+ error.code = 'DBOPFS_RESTORE_WRITE_FAILED';
1354
+ error.failures = failures;
1355
+ throw error;
1292
1356
  }
1293
1357
  }
1294
1358
  }