brep-io-kernel 1.0.34 → 1.0.35

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.
@@ -3,6 +3,19 @@ const GH = {
3
3
  apiVersion: '2022-11-28',
4
4
  };
5
5
 
6
+ const _WRITE_CHAINS = new Map();
7
+
8
+ function _enqueueGithubWrite(key, op) {
9
+ const prev = _WRITE_CHAINS.get(key) || Promise.resolve();
10
+ const next = prev.then(op);
11
+ let safe;
12
+ safe = next.catch(() => {}).finally(() => {
13
+ if (_WRITE_CHAINS.get(key) === safe) _WRITE_CHAINS.delete(key);
14
+ });
15
+ _WRITE_CHAINS.set(key, safe);
16
+ return next;
17
+ }
18
+
6
19
  const STORAGE_ROOT = 'brep-storage';
7
20
  const SETTINGS_DIR = 'settings';
8
21
  const DATA_DIR = '__BREP_DATA__';
@@ -227,30 +240,51 @@ export async function readGithubFileBase64({ token, repoFull, branch, path }) {
227
240
  return null;
228
241
  }
229
242
 
230
- export async function writeGithubFileBase64({ token, repoFull, branch, path, base64, message }) {
243
+ export async function writeGithubFileBase64({ token, repoFull, branch, path, base64, message, retryOn409 = 2 }) {
231
244
  const t = String(token || '').trim();
232
245
  if (!t) throw new Error('Missing GitHub token');
233
246
  const { owner, repo } = parseRepo(repoFull);
234
- let sha = null;
235
- try {
236
- const meta = await readGithubFileMeta({ token: t, repoFull, branch, path });
237
- sha = meta?.sha || null;
238
- } catch (e) {
239
- if (!e || e.status !== 404) throw e;
240
- }
241
- const body = {
242
- message: message || `BREP storage update: ${path}`,
243
- content: String(base64 || '').replace(/\s+/g, ''),
244
- };
245
- if (branch) body.branch = branch;
246
- if (sha) body.sha = sha;
247
247
  const url = `${GH.apiBase}/repos/${owner}/${repo}/contents/${encodePath(path)}`;
248
- const res = await ghFetch(url, t, {
249
- method: 'PUT',
250
- headers: { 'Content-Type': 'application/json' },
251
- body: JSON.stringify(body),
248
+ const content = String(base64 || '').replace(/\s+/g, '');
249
+ const maxRetry = Math.max(0, Number.isFinite(retryOn409) ? retryOn409 : 0);
250
+ const writeKey = `${repoFull}@${branch || ''}:${path}`;
251
+
252
+ return _enqueueGithubWrite(writeKey, async () => {
253
+ let sha = null;
254
+ const refreshSha = async () => {
255
+ try {
256
+ const meta = await readGithubFileMeta({ token: t, repoFull, branch, path });
257
+ sha = meta?.sha || null;
258
+ } catch (e) {
259
+ if (!e || e.status !== 404) throw e;
260
+ sha = null;
261
+ }
262
+ };
263
+
264
+ for (let attempt = 0; attempt <= maxRetry; attempt++) {
265
+ await refreshSha();
266
+ const body = {
267
+ message: message || `BREP storage update: ${path}`,
268
+ content,
269
+ };
270
+ if (branch) body.branch = branch;
271
+ if (sha) body.sha = sha;
272
+ try {
273
+ const res = await ghFetch(url, t, {
274
+ method: 'PUT',
275
+ headers: { 'Content-Type': 'application/json' },
276
+ body: JSON.stringify(body),
277
+ });
278
+ return res;
279
+ } catch (e) {
280
+ if (e && e.status === 409 && attempt < maxRetry) {
281
+ continue;
282
+ }
283
+ throw e;
284
+ }
285
+ }
286
+ return null;
252
287
  });
253
- return res;
254
288
  }
255
289
 
256
290
  export async function deleteGithubFile({ token, repoFull, branch, path, message }) {
@@ -285,7 +319,10 @@ async function readGithubFileMeta({ token, repoFull, branch, path }) {
285
319
  const { owner, repo } = parseRepo(repoFull);
286
320
  const url = new URL(`${GH.apiBase}/repos/${owner}/${repo}/contents/${encodePath(path)}`);
287
321
  if (branch) url.searchParams.set('ref', branch);
288
- return await ghFetch(url.toString(), t);
322
+ url.searchParams.set('_ts', Date.now().toString());
323
+ return await ghFetch(url.toString(), t, {
324
+ cache: 'no-store',
325
+ });
289
326
  }
290
327
 
291
328
  export class GithubStorage {
@@ -400,38 +437,58 @@ export class GithubStorage {
400
437
  const { owner, repo } = this._repo;
401
438
  const url = new URL(`${GH.apiBase}/repos/${owner}/${repo}/contents/${encodePath(path)}`);
402
439
  url.searchParams.set('ref', this._branch || 'main');
403
- return await ghFetch(url.toString(), this._token);
440
+ url.searchParams.set('_ts', Date.now().toString());
441
+ return await ghFetch(url.toString(), this._token, {
442
+ cache: 'no-store',
443
+ });
404
444
  }
405
445
 
406
446
  async _writeKeyToRepo(key, value) {
407
447
  if (!this._token || !this._repo) return;
408
448
  const { owner, repo } = this._repo;
409
449
  const path = keyToPath(key, this._rootDir);
410
- let sha = this._shaByKey.get(key) || null;
411
- if (!sha) {
412
- try {
413
- const meta = await this._getFileMeta(path);
414
- sha = meta?.sha || null;
415
- if (sha) this._shaByKey.set(key, sha);
416
- } catch (e) {
417
- if (!e || e.status !== 404) throw e;
418
- }
419
- }
420
- const body = {
421
- message: `BREP storage update: ${key}`,
422
- content: encodeBase64(value),
423
- branch: this._branch || 'main',
424
- };
425
- if (sha) body.sha = sha;
426
450
  const url = `${GH.apiBase}/repos/${owner}/${repo}/contents/${encodePath(path)}`;
427
- const res = await ghFetch(url, this._token, {
428
- method: 'PUT',
429
- headers: { 'Content-Type': 'application/json' },
430
- body: JSON.stringify(body),
451
+ const content = encodeBase64(value);
452
+ const writeKey = `${this._repoFull || ''}@${this._branch || ''}:${path}`;
453
+ return _enqueueGithubWrite(writeKey, async () => {
454
+ let sha = null;
455
+ const refreshSha = async () => {
456
+ try {
457
+ const meta = await this._getFileMeta(path);
458
+ sha = meta?.sha || null;
459
+ if (sha) this._shaByKey.set(key, sha);
460
+ } catch (e) {
461
+ if (!e || e.status !== 404) throw e;
462
+ sha = null;
463
+ }
464
+ };
465
+
466
+ for (let attempt = 0; attempt <= 1; attempt++) {
467
+ await refreshSha();
468
+ const body = {
469
+ message: `BREP storage update: ${key}`,
470
+ content,
471
+ branch: this._branch || 'main',
472
+ };
473
+ if (sha) body.sha = sha;
474
+ try {
475
+ const res = await ghFetch(url, this._token, {
476
+ method: 'PUT',
477
+ headers: { 'Content-Type': 'application/json' },
478
+ body: JSON.stringify(body),
479
+ });
480
+ if (res && res.content && res.content.sha) {
481
+ this._shaByKey.set(key, res.content.sha);
482
+ }
483
+ return;
484
+ } catch (e) {
485
+ if (e && e.status === 409 && attempt < 1) {
486
+ continue;
487
+ }
488
+ throw e;
489
+ }
490
+ }
431
491
  });
432
- if (res && res.content && res.content.sha) {
433
- this._shaByKey.set(key, res.content.sha);
434
- }
435
492
  }
436
493
 
437
494
  async _removeKeyFromRepo(key) {
@@ -96,7 +96,7 @@ async function listGithubComponentRecords() {
96
96
  return out;
97
97
  }
98
98
 
99
- async function getGithubComponentRecord(name) {
99
+ async function getGithubComponentRecord(name, options = {}) {
100
100
  const cfg = getGithubStorageConfig();
101
101
  if (!cfg?.token || !cfg?.repoFull) return null;
102
102
  const { dataPath, metaPath } = getGithubModelPaths(name);
@@ -110,7 +110,11 @@ async function getGithubComponentRecord(name) {
110
110
  branch: cfg.branch,
111
111
  path: dataPath,
112
112
  });
113
- } catch { /* ignore */ }
113
+ } catch (err) {
114
+ if (err && err.status === 404) return null;
115
+ if (options?.throwOnError) throw err;
116
+ return null;
117
+ }
114
118
  if (!data3mf) return null;
115
119
  try {
116
120
  const metaB64 = await readGithubFileBase64({
@@ -147,6 +151,7 @@ async function setGithubComponentRecord(name, dataObj) {
147
151
  path: dataPath,
148
152
  base64: data3mf,
149
153
  message: `BREP model update: ${name}`,
154
+ retryOn409: 3,
150
155
  });
151
156
  const meta = {
152
157
  savedAt: dataObj?.savedAt || new Date().toISOString(),
@@ -160,6 +165,7 @@ async function setGithubComponentRecord(name, dataObj) {
160
165
  path: metaPath,
161
166
  base64: metaB64,
162
167
  message: `BREP model meta: ${name}`,
168
+ retryOn409: 3,
163
169
  });
164
170
  }
165
171
 
@@ -233,9 +239,9 @@ export async function listComponentRecords() {
233
239
  return items;
234
240
  }
235
241
 
236
- export async function getComponentRecord(name) {
242
+ export async function getComponentRecord(name, options = {}) {
237
243
  if (LS?.isGithub?.()) {
238
- return await getGithubComponentRecord(name);
244
+ return await getGithubComponentRecord(name, options);
239
245
  }
240
246
  if (!name) return null;
241
247
  const key = MODEL_STORAGE_PREFIX + encodeURIComponent(String(name));