brep-io-kernel 1.0.26 → 1.0.28

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/src/idbStorage.js CHANGED
@@ -1,19 +1,24 @@
1
1
  /* idbStorage.js
2
- Primary IndexedDB-backed app storage with a localStorage-like API.
2
+ VFS-backed app storage with a localStorage-like API.
3
3
  - Synchronous reads via in-memory cache
4
- - Async persistence to IndexedDB
4
+ - Async persistence to VFS (fs.proxy -> __BREP_VFS_DB__)
5
5
  - Same-tab + optional cross-tab change events
6
+ - Migrates legacy __LS_SHIM_DB__ data into VFS
6
7
  */
7
8
 
8
- const STORAGE_DB_NAME = '__LS_SHIM_DB__';
9
- const STORE_NAME = 'kv';
10
- const DB_VERSION = 1;
9
+ import { fs } from './fs.proxy.js';
10
+
11
+ const SETTINGS_DIR = 'settings';
12
+ const DATA_DIR = '__BREP_DATA__';
13
+ const MODEL_PREFIX = '__BREP_DATA__:';
14
+ const LEGACY_MODEL_PREFIX = '__BREP_MODEL__:';
15
+ const LEGACY_DB_NAME = '__LS_SHIM_DB__';
16
+ const LEGACY_STORE_NAME = 'kv';
11
17
  const BC_NAME = '__BREP_STORAGE_BC__';
12
18
 
13
19
  const hasIndexedDB = typeof indexedDB !== 'undefined' && !!indexedDB.open;
14
20
 
15
21
  function toStringValue(v) {
16
- // Match Web Storage semantics: everything is coerced to string
17
22
  return v === undefined || v === null ? String(v) : String(v);
18
23
  }
19
24
 
@@ -37,119 +42,286 @@ function tryDispatchStorageEvent(storage, { key, oldValue, newValue }) {
37
42
  } catch {}
38
43
  }
39
44
 
40
- function promisifyRequest(req) {
41
- return new Promise((resolve, reject) => {
42
- req.onsuccess = () => resolve(req.result);
43
- req.onerror = () => reject(req.error || new Error('IDB request failed'));
44
- });
45
+ function joinPath(...parts) {
46
+ return parts.filter(Boolean).join('/');
45
47
  }
46
48
 
47
- function openDB() {
48
- return new Promise((resolve, reject) => {
49
- const openReq = indexedDB.open(STORAGE_DB_NAME, DB_VERSION);
50
- openReq.onupgradeneeded = () => {
51
- const db = openReq.result;
52
- if (!db.objectStoreNames.contains(STORE_NAME)) {
53
- db.createObjectStore(STORE_NAME, { keyPath: 'key' });
54
- }
55
- };
56
- openReq.onsuccess = () => resolve(openReq.result);
57
- openReq.onerror = () => reject(openReq.error || new Error('Failed to open storage DB'));
58
- });
49
+ function encodeSettingKey(key) {
50
+ const k = String(key ?? '');
51
+ if (!k || k === '.' || k === '..' || k.includes('/') || k.includes('\\')) {
52
+ return encodeURIComponent(k);
53
+ }
54
+ return k;
55
+ }
56
+
57
+ function decodeSettingKey(name) {
58
+ if (/%[0-9A-Fa-f]{2}/.test(name)) {
59
+ try { return decodeURIComponent(name); } catch {}
60
+ }
61
+ return name;
59
62
  }
60
63
 
61
- function unwrapStoredValue(value) {
62
- if (value && typeof value === 'object' && 'value' in value) return value.value;
63
- return value;
64
+ function isModelKey(key) {
65
+ return typeof key === 'string' && key.startsWith(MODEL_PREFIX);
64
66
  }
65
67
 
66
- async function idbGetAll(db) {
67
- const tx = db.transaction([STORE_NAME], 'readonly');
68
- const store = tx.objectStore(STORE_NAME);
69
- if (store.getAll && store.getAllKeys) {
70
- const [items, keys] = await Promise.all([promisifyRequest(store.getAll()), promisifyRequest(store.getAllKeys())]);
71
- const out = new Map();
72
- for (let i = 0; i < keys.length; i++) {
73
- out.set(String(keys[i]), unwrapStoredValue(items[i]));
74
- }
75
- return out;
68
+ function isLegacyModelKey(key) {
69
+ return typeof key === 'string' && key.startsWith(LEGACY_MODEL_PREFIX);
70
+ }
71
+
72
+ function normalizeModelKey(key) {
73
+ if (isModelKey(key)) return key;
74
+ if (isLegacyModelKey(key)) return MODEL_PREFIX + key.slice(LEGACY_MODEL_PREFIX.length);
75
+ return null;
76
+ }
77
+
78
+ function keyToPath(key) {
79
+ if (isModelKey(key)) {
80
+ const name = key.slice(MODEL_PREFIX.length);
81
+ return joinPath(DATA_DIR, name);
76
82
  }
77
- return new Promise((resolve, reject) => {
78
- const out = new Map();
79
- const req = store.openCursor();
80
- req.onsuccess = (e) => {
81
- const cursor = e.target.result;
82
- if (cursor) {
83
- out.set(String(cursor.key), unwrapStoredValue(cursor.value));
84
- cursor.continue();
85
- } else {
86
- resolve(out);
87
- }
88
- };
89
- req.onerror = () => reject(req.error || new Error('Cursor failed'));
90
- });
83
+ return joinPath(SETTINGS_DIR, encodeSettingKey(key));
91
84
  }
92
85
 
93
- async function idbPut(db, key, value) {
94
- const tx = db.transaction([STORE_NAME], 'readwrite');
95
- const store = tx.objectStore(STORE_NAME);
96
- if (store.keyPath) {
97
- store.put({ key, value });
98
- } else {
99
- store.put(value, key);
86
+ async function ensureDir(path) {
87
+ try {
88
+ await fs.promises.mkdir(path, { recursive: true });
89
+ } catch (e) {
90
+ if (e && e.code !== 'EEXIST') throw e;
100
91
  }
101
- return new Promise((resolve, reject) => {
102
- tx.oncomplete = () => resolve();
103
- tx.onerror = () => reject(tx.error || new Error('IDB put failed'));
92
+ }
93
+
94
+ async function exists(path) {
95
+ try {
96
+ await fs.promises.stat(path);
97
+ return true;
98
+ } catch {
99
+ return false;
100
+ }
101
+ }
102
+
103
+ async function readFileSafe(path) {
104
+ try {
105
+ return await fs.promises.readFile(path, 'utf8');
106
+ } catch {
107
+ return null;
108
+ }
109
+ }
110
+
111
+ async function readDirSafe(path) {
112
+ try {
113
+ return await fs.promises.readdir(path);
114
+ } catch {
115
+ return [];
116
+ }
117
+ }
118
+
119
+ async function deleteLegacyDb() {
120
+ if (!hasIndexedDB) return;
121
+ await new Promise((resolve) => {
122
+ try {
123
+ const req = indexedDB.deleteDatabase(LEGACY_DB_NAME);
124
+ req.onsuccess = () => resolve();
125
+ req.onerror = () => resolve();
126
+ req.onblocked = () => resolve();
127
+ } catch {
128
+ resolve();
129
+ }
104
130
  });
105
131
  }
106
132
 
107
- async function idbDelete(db, key) {
108
- const tx = db.transaction([STORE_NAME], 'readwrite');
109
- tx.objectStore(STORE_NAME).delete(key);
133
+ async function openLegacyDb() {
134
+ if (!hasIndexedDB) return { db: null, created: false };
110
135
  return new Promise((resolve, reject) => {
111
- tx.oncomplete = () => resolve();
112
- tx.onerror = () => reject(tx.error || new Error('IDB delete failed'));
136
+ let created = false;
137
+ let req;
138
+ try {
139
+ req = indexedDB.open(LEGACY_DB_NAME);
140
+ } catch (e) {
141
+ resolve({ db: null, created: false });
142
+ return;
143
+ }
144
+ req.onupgradeneeded = () => { created = true; };
145
+ req.onsuccess = () => {
146
+ const db = req.result;
147
+ if (created) {
148
+ try { db.close(); } catch {}
149
+ resolve({ db: null, created: true });
150
+ return;
151
+ }
152
+ resolve({ db, created: false });
153
+ };
154
+ req.onerror = () => reject(req.error || new Error('Failed to open legacy DB'));
113
155
  });
114
156
  }
115
157
 
116
- async function idbClear(db) {
117
- const tx = db.transaction([STORE_NAME], 'readwrite');
118
- tx.objectStore(STORE_NAME).clear();
158
+ async function readAllLegacyEntries(db) {
119
159
  return new Promise((resolve, reject) => {
120
- tx.oncomplete = () => resolve();
121
- tx.onerror = () => reject(tx.error || new Error('IDB clear failed'));
160
+ try {
161
+ if (!db.objectStoreNames.contains(LEGACY_STORE_NAME)) {
162
+ resolve(new Map());
163
+ return;
164
+ }
165
+ const tx = db.transaction([LEGACY_STORE_NAME], 'readonly');
166
+ const store = tx.objectStore(LEGACY_STORE_NAME);
167
+ const out = new Map();
168
+ const req = store.openCursor();
169
+ req.onsuccess = (e) => {
170
+ const cursor = e.target.result;
171
+ if (cursor) {
172
+ const rawKey = cursor.key;
173
+ const rawVal = cursor.value;
174
+ const key = String(rawKey);
175
+ let value = rawVal;
176
+ if (value && typeof value === 'object' && 'value' in value) value = value.value;
177
+ out.set(key, value);
178
+ cursor.continue();
179
+ } else {
180
+ resolve(out);
181
+ }
182
+ };
183
+ req.onerror = () => reject(req.error || new Error('Legacy cursor failed'));
184
+ } catch (e) {
185
+ reject(e);
186
+ }
122
187
  });
123
188
  }
124
189
 
125
- class IdbStorage {
190
+ class VfsStorage {
126
191
  constructor() {
127
192
  this._cache = new Map();
128
193
  this._ready = false;
129
- this._dbPromise = null;
194
+ this._persistChain = Promise.resolve();
130
195
  this._bc = null;
131
- this._idbEnabled = hasIndexedDB;
132
- this._init();
196
+ this._initPromise = this._init();
133
197
  }
134
198
 
135
199
  async _init() {
136
- if (!this._idbEnabled) {
137
- this._ready = true;
138
- return;
200
+ try {
201
+ await fs.ready();
202
+ await ensureDir(SETTINGS_DIR);
203
+ await ensureDir(DATA_DIR);
204
+ await this._migrateLegacyIdb();
205
+ await this._migrateLegacyFs();
206
+ await this._loadFromFs();
207
+ this._setupBroadcast();
208
+ } catch (e) {
209
+ console.warn('[vfs-storage] init failed; using in-memory storage only.', e);
139
210
  }
211
+ this._ready = true;
212
+ }
213
+
214
+ async _loadFromFs() {
215
+ this._cache.clear();
216
+ const settingFiles = await readDirSafe(SETTINGS_DIR);
217
+ for (const name of settingFiles) {
218
+ const path = joinPath(SETTINGS_DIR, name);
219
+ const raw = await readFileSafe(path);
220
+ if (raw === null) continue;
221
+ const key = decodeSettingKey(name);
222
+ this._cache.set(key, toStringValue(raw));
223
+ }
224
+ const dataFiles = await readDirSafe(DATA_DIR);
225
+ for (const name of dataFiles) {
226
+ const path = joinPath(DATA_DIR, name);
227
+ const raw = await readFileSafe(path);
228
+ if (raw === null) continue;
229
+ const key = MODEL_PREFIX + name;
230
+ this._cache.set(key, toStringValue(raw));
231
+ }
232
+ }
140
233
 
234
+ async _writeKeyToFs(key, value) {
235
+ const path = keyToPath(key);
236
+ await ensureDir(isModelKey(key) ? DATA_DIR : SETTINGS_DIR);
237
+ await fs.promises.writeFile(path, toStringValue(value), 'utf8');
238
+ }
239
+
240
+ async _removeKeyFromFs(key) {
241
+ const path = keyToPath(key);
141
242
  try {
142
- this._dbPromise = openDB();
143
- const db = await this._dbPromise;
144
- const idbMap = await idbGetAll(db);
145
- idbMap.forEach((v, k) => this._cache.set(k, toStringValue(v)));
146
- this._setupBroadcast();
147
- } catch (err) {
148
- console.warn('[idb-storage] IndexedDB unavailable; using in-memory storage only.', err);
149
- this._idbEnabled = false;
243
+ await fs.promises.unlink(path);
244
+ } catch (e) {
245
+ if (!e || e.code !== 'ENOENT') throw e;
150
246
  }
247
+ }
151
248
 
152
- this._ready = true;
249
+ async _resetDir(path) {
250
+ try {
251
+ await fs.promises.rm(path, { recursive: true, force: true });
252
+ } catch {}
253
+ await ensureDir(path);
254
+ }
255
+
256
+ async _migrateLegacyIdb() {
257
+ if (!hasIndexedDB) return;
258
+ let db;
259
+ try {
260
+ const opened = await openLegacyDb();
261
+ if (!opened.db) {
262
+ if (opened.created) await deleteLegacyDb();
263
+ return;
264
+ }
265
+ db = opened.db;
266
+ const entries = await readAllLegacyEntries(db);
267
+ for (const [rawKey, rawVal] of entries) {
268
+ const key = String(rawKey);
269
+ const value = toStringValue(rawVal);
270
+ const normalized = normalizeModelKey(key);
271
+ const destKey = normalized || key;
272
+ const destPath = keyToPath(destKey);
273
+ if (await exists(destPath)) continue;
274
+ await this._writeKeyToFs(destKey, value);
275
+ }
276
+ } catch (e) {
277
+ console.warn('[vfs-storage] legacy IndexedDB migration failed:', e);
278
+ return;
279
+ } finally {
280
+ try { db && db.close && db.close(); } catch {}
281
+ }
282
+ await deleteLegacyDb();
283
+ }
284
+
285
+ async _migrateLegacyFs() {
286
+ // Move any legacy model keys mistakenly stored under settings/
287
+ const settingFiles = await readDirSafe(SETTINGS_DIR);
288
+ for (const name of settingFiles) {
289
+ if (!name.startsWith(LEGACY_MODEL_PREFIX)) continue;
290
+ const legacyPath = joinPath(SETTINGS_DIR, name);
291
+ const raw = await readFileSafe(legacyPath);
292
+ const key = MODEL_PREFIX + name.slice(LEGACY_MODEL_PREFIX.length);
293
+ if (raw !== null) {
294
+ const destPath = keyToPath(key);
295
+ if (!await exists(destPath)) {
296
+ await this._writeKeyToFs(key, raw);
297
+ }
298
+ }
299
+ try { await fs.promises.unlink(legacyPath); } catch {}
300
+ }
301
+
302
+ // Move any legacy data directory (__BREP_MODEL__) contents to __BREP_DATA__
303
+ const legacyDir = '__BREP_MODEL__';
304
+ const legacyFiles = await readDirSafe(legacyDir);
305
+ if (!legacyFiles.length) return;
306
+ await ensureDir(DATA_DIR);
307
+ for (const name of legacyFiles) {
308
+ const from = joinPath(legacyDir, name);
309
+ const to = joinPath(DATA_DIR, name);
310
+ if (await exists(to)) {
311
+ try { await fs.promises.unlink(from); } catch {}
312
+ continue;
313
+ }
314
+ try {
315
+ await fs.promises.rename(from, to);
316
+ } catch {
317
+ const raw = await readFileSafe(from);
318
+ if (raw !== null) {
319
+ await fs.promises.writeFile(to, raw, 'utf8');
320
+ }
321
+ try { await fs.promises.unlink(from); } catch {}
322
+ }
323
+ }
324
+ try { await fs.promises.rm(legacyDir, { recursive: true, force: true }); } catch {}
153
325
  }
154
326
 
155
327
  _setupBroadcast() {
@@ -193,17 +365,22 @@ class IdbStorage {
193
365
  return v === undefined ? null : v;
194
366
  }
195
367
 
368
+ _enqueuePersist(op) {
369
+ this._persistChain = this._persistChain
370
+ .then(() => this._initPromise)
371
+ .then(op)
372
+ .catch((e) => {
373
+ console.warn('[vfs-storage] persist failed:', e);
374
+ });
375
+ }
376
+
196
377
  setItem(key, value) {
197
378
  const k = toStringValue(key);
198
379
  const v = toStringValue(value);
199
380
  const oldValue = this._cache.get(k) ?? null;
200
381
  this._cache.set(k, v);
201
382
 
202
- if (this._idbEnabled) {
203
- this._dbPromise?.then((db) => idbPut(db, k, v)).catch((e) => {
204
- console.warn('[idb-storage] setItem persist failed:', e);
205
- });
206
- }
383
+ this._enqueuePersist(() => this._writeKeyToFs(k, v));
207
384
 
208
385
  tryDispatchStorageEvent(this, { key: k, oldValue, newValue: v });
209
386
  try { this._bc?.postMessage({ type: 'set', key: k, newValue: v, oldValue }); } catch {}
@@ -214,11 +391,7 @@ class IdbStorage {
214
391
  const oldValue = this._cache.get(k) ?? null;
215
392
  this._cache.delete(k);
216
393
 
217
- if (this._idbEnabled) {
218
- this._dbPromise?.then((db) => idbDelete(db, k)).catch((e) => {
219
- console.warn('[idb-storage] removeItem persist failed:', e);
220
- });
221
- }
394
+ this._enqueuePersist(() => this._removeKeyFromFs(k));
222
395
 
223
396
  tryDispatchStorageEvent(this, { key: k, oldValue, newValue: null });
224
397
  try { this._bc?.postMessage({ type: 'remove', key: k, oldValue, newValue: null }); } catch {}
@@ -228,11 +401,10 @@ class IdbStorage {
228
401
  if (this._cache.size === 0) return;
229
402
  this._cache.clear();
230
403
 
231
- if (this._idbEnabled) {
232
- this._dbPromise?.then((db) => idbClear(db)).catch((e) => {
233
- console.warn('[idb-storage] clear persist failed:', e);
234
- });
235
- }
404
+ this._enqueuePersist(async () => {
405
+ await this._resetDir(SETTINGS_DIR);
406
+ await this._resetDir(DATA_DIR);
407
+ });
236
408
 
237
409
  tryDispatchStorageEvent(this, { key: null, oldValue: null, newValue: null });
238
410
  try { this._bc?.postMessage({ type: 'clear' }); } catch {}
@@ -243,12 +415,10 @@ class IdbStorage {
243
415
  }
244
416
 
245
417
  ready() {
246
- if (!this._idbEnabled) return Promise.resolve();
247
- if (this._ready) return Promise.resolve();
248
- return this._dbPromise.then(() => undefined).catch(() => undefined);
418
+ return this._initPromise;
249
419
  }
250
420
  }
251
421
 
252
- const localStorage = new IdbStorage();
422
+ const localStorage = new VfsStorage();
253
423
 
254
424
  export { localStorage };
package/src/index.js CHANGED
@@ -10,3 +10,10 @@ export { PartHistory, extractDefaultValues } from './PartHistory.js';
10
10
  export { AssemblyConstraintHistory } from './assemblyConstraints/AssemblyConstraintHistory.js';
11
11
  export { AssemblyConstraintRegistry } from './assemblyConstraints/AssemblyConstraintRegistry.js';
12
12
 
13
+ // License helpers
14
+ export {
15
+ getPackageLicenseInfo,
16
+ getPackageLicenseInfoString,
17
+ getPackageLicenseText,
18
+ getAllLicensesInfoString,
19
+ } from './licenseInfo.js';
@@ -0,0 +1,71 @@
1
+ import { LICENSE_BUNDLE_TEXT } from './generated/licenseBundle.js';
2
+
3
+ const PACKAGE_NAME = 'brep-io-kernel';
4
+ const PACKAGE_LICENSE = 'SEE LICENSE IN LICENSE.md';
5
+
6
+ const LICENSE_TEXT = `Copyright 2025 Autodrop3d LLC
7
+ https://autodrop3d.com
8
+
9
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
10
+ associated documentation files (the "Software"), to deal in the Software without restriction,
11
+ including without limitation the rights to use, copy, modify, merge, publish, distribute,
12
+ sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
13
+ furnished to do so, subject to the following conditions:
14
+
15
+ 1. Any modifications made to the Software must be submitted to Autodrop3d LLC with an irrevocable
16
+ assignment of the copyright via git pull request. This is intended to allow Autodrop3d LLC to
17
+ sell commercial licenses of the Software for use in proprietary products under a
18
+ dual-licensing strategy. Failure to contribute back modifications without a commercial
19
+ license purchased from Autodrop3d LLC voids all permissions granted by this license.
20
+
21
+ 2. If Autodrop3d LLC is sold, merged, transferred, or otherwise succeeded by any entity or
22
+ individual, all rights and obligations described in this license shall transfer automatically
23
+ to that successor entity or individual.
24
+
25
+ 3. If Autodrop3d LLC ceases operations or dissolves, and no successor entity or individual
26
+ continues to publicly host the Software in a manner that allows the public to obtain the source
27
+ code and submit contributions for a period of at least eighteen (18) consecutive months, then
28
+ Clauses 1 and 2 of this license shall be automatically and irrevocably canceled.
29
+
30
+ The above copyright notice and these permission notices shall be included in all copies or
31
+ substantial portions of the Software.
32
+
33
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
34
+ BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
35
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
36
+ DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
37
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.`;
38
+
39
+ export function getPackageLicenseText() {
40
+ return LICENSE_TEXT;
41
+ }
42
+
43
+ export function getPackageLicenseInfo() {
44
+ return {
45
+ name: PACKAGE_NAME,
46
+ license: PACKAGE_LICENSE,
47
+ text: LICENSE_TEXT,
48
+ };
49
+ }
50
+
51
+ export function getPackageLicenseInfoString(options = {}) {
52
+ const { includeTitle = true, includeLicenseId = true } = options;
53
+ const lines = [];
54
+
55
+ if (includeTitle) {
56
+ lines.push(`${PACKAGE_NAME} License`);
57
+ }
58
+ if (includeLicenseId) {
59
+ lines.push(`License: ${PACKAGE_LICENSE}`);
60
+ }
61
+ if (lines.length) {
62
+ lines.push('');
63
+ }
64
+
65
+ lines.push(LICENSE_TEXT);
66
+ return lines.join('\n');
67
+ }
68
+
69
+ export function getAllLicensesInfoString() {
70
+ return LICENSE_BUNDLE_TEXT;
71
+ }
@@ -1,7 +1,7 @@
1
1
  import JSZip from 'jszip';
2
2
  import { localStorage as LS } from '../idbStorage.js';
3
3
 
4
- export const MODEL_STORAGE_PREFIX = '__BREP_MODEL__:';
4
+ export const MODEL_STORAGE_PREFIX = '__BREP_DATA__:';
5
5
 
6
6
  function safeParse(json) {
7
7
  try {