@xtrable-ltd/nanoesis 0.1.5 → 0.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (24) hide show
  1. package/dist/adapter-azure-blob.js +1 -1
  2. package/dist/{chunk-S23S4ZAK.js → chunk-Y7PUOM5I.js} +86 -49
  3. package/dist/editor-api.js +1 -1
  4. package/dist/index.d.ts +15 -0
  5. package/dist/index.js +1 -1
  6. package/editor/assets/{TemplatesPane-CEKbBX1Y.js → TemplatesPane-8SToGitk.js} +126 -126
  7. package/editor/assets/{cssMode-q1Nw_pOI.js → cssMode-BgMFxIL5.js} +1 -1
  8. package/editor/assets/{freemarker2-D4uWo9kP.js → freemarker2-C1n0dZBT.js} +1 -1
  9. package/editor/assets/{handlebars-DIXKS_PM.js → handlebars-DHtQKz6I.js} +1 -1
  10. package/editor/assets/{html-WMfbPtFq.js → html-DCKECHJy.js} +1 -1
  11. package/editor/assets/{htmlMode-D1MWdaop.js → htmlMode-DIxPoVyG.js} +1 -1
  12. package/editor/assets/{index-erqFXslE.js → index-BZ3Bij0z.js} +60 -60
  13. package/editor/assets/{javascript-C0daj-se.js → javascript-BQa7DljQ.js} +1 -1
  14. package/editor/assets/{jsonMode-F4j8hmn2.js → jsonMode-CUqvw4kw.js} +1 -1
  15. package/editor/assets/{liquid-BmXUpyt6.js → liquid-BdmTog1w.js} +1 -1
  16. package/editor/assets/{mdx-wzY123NO.js → mdx-CMmi05CR.js} +1 -1
  17. package/editor/assets/{python-WOKQ0Svl.js → python-BxYWYFBZ.js} +1 -1
  18. package/editor/assets/{razor-B9Yg1Ydt.js → razor-NbjX7n9N.js} +1 -1
  19. package/editor/assets/{tsMode-966q_lbW.js → tsMode-DiCcyKwn.js} +1 -1
  20. package/editor/assets/{typescript-dxbYO44F.js → typescript-CJcxRbel.js} +1 -1
  21. package/editor/assets/{xml-BHa7-pyl.js → xml-CdXggTD3.js} +1 -1
  22. package/editor/assets/{yaml-CcgAVYu7.js → yaml-CUKQ9Cpf.js} +1 -1
  23. package/editor/index.html +1 -1
  24. package/package.json +1 -1
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  contentTypeFor
3
- } from "./chunk-S23S4ZAK.js";
3
+ } from "./chunk-Y7PUOM5I.js";
4
4
 
5
5
  // ../../adapters/azure-blob/src/container.ts
6
6
  var InMemoryBlobContainer = class {
@@ -237,6 +237,20 @@ var IndexedStore = class {
237
237
  store;
238
238
  index;
239
239
  loading;
240
+ /**
241
+ * Per-instance mutex (DESIGN §11d): every mutation (`write`/`delete`/`rename`/`reconcile`)
242
+ * runs through {@link serializeMutation}, which chains onto the previous mutation's
243
+ * completion. Without it, two concurrent mutations both read the cached `this.index`,
244
+ * each compute their "next" key set from that stale snapshot, and the second
245
+ * `saveIndex` clobbers the first — the file writes land in blob, but the index keeps
246
+ * stale references or loses fresh ones (the "I deleted it and it's still there" or
247
+ * "I added it and it didn't appear" bugs surfaced dogfooding the marketing site,
248
+ * 2026-05-28). Reads (`list`/`exists`/`readBytes`) are *not* serialised; an
249
+ * in-flight mutation simply means a reader sees the pre-mutation index, eventually
250
+ * consistent and safe. Crashes inside `work()` release the lock so a single failure
251
+ * does not deadlock subsequent operations.
252
+ */
253
+ mutationLock = Promise.resolve();
240
254
  /** The index, loaded once on first need and cached (mutations replace the cached copy). */
241
255
  async loaded() {
242
256
  if (this.index !== void 0) return this.index;
@@ -244,6 +258,21 @@ var IndexedStore = class {
244
258
  this.index = await this.loading;
245
259
  return this.index;
246
260
  }
261
+ serializeMutation(work) {
262
+ const previous = this.mutationLock;
263
+ let release;
264
+ this.mutationLock = new Promise((resolve) => {
265
+ release = resolve;
266
+ });
267
+ return (async () => {
268
+ try {
269
+ await previous;
270
+ return await work();
271
+ } finally {
272
+ release();
273
+ }
274
+ })();
275
+ }
247
276
  async list(dir) {
248
277
  return childrenOf((await this.loaded()).keys, dir);
249
278
  }
@@ -262,31 +291,35 @@ var IndexedStore = class {
262
291
  * Create or overwrite `key`. The index is rewritten only when `key` is new (an
263
292
  * overwrite leaves the key set unchanged, so editing an item is a single `put`).
264
293
  */
265
- async write(key, bytes) {
266
- const target = guarded(normalize(key));
267
- await this.store.put(target, bytes);
268
- const index = await this.loaded();
269
- if (!index.keys.includes(target)) {
270
- this.index = await saveIndex(this.store, index, [...index.keys, target]);
271
- }
294
+ write(key, bytes) {
295
+ return this.serializeMutation(async () => {
296
+ const target = guarded(normalize(key));
297
+ await this.store.put(target, bytes);
298
+ const index = await this.loaded();
299
+ if (!index.keys.includes(target)) {
300
+ this.index = await saveIndex(this.store, index, [...index.keys, target]);
301
+ }
302
+ });
272
303
  }
273
304
  /**
274
305
  * Delete a file, or a whole directory subtree (every key under `key/`). Idempotent: a
275
306
  * path the index does not know is still deleted from the store (clearing an orphan),
276
307
  * and deleting nothing is a no-op.
277
308
  */
278
- async delete(key) {
279
- const target = guarded(normalize(key));
280
- const index = await this.loaded();
281
- const prefix = `${target}/`;
282
- const removed = index.keys.filter((k) => k === target || k.startsWith(prefix));
283
- if (removed.length === 0) {
284
- await this.store.delete(target);
285
- return;
286
- }
287
- await Promise.all(removed.map((k) => this.store.delete(k)));
288
- const remaining = index.keys.filter((k) => k !== target && !k.startsWith(prefix));
289
- this.index = await saveIndex(this.store, index, remaining);
309
+ delete(key) {
310
+ return this.serializeMutation(async () => {
311
+ const target = guarded(normalize(key));
312
+ const index = await this.loaded();
313
+ const prefix = `${target}/`;
314
+ const removed = index.keys.filter((k) => k === target || k.startsWith(prefix));
315
+ if (removed.length === 0) {
316
+ await this.store.delete(target);
317
+ return;
318
+ }
319
+ await Promise.all(removed.map((k) => this.store.delete(k)));
320
+ const remaining = index.keys.filter((k) => k !== target && !k.startsWith(prefix));
321
+ this.index = await saveIndex(this.store, index, remaining);
322
+ });
290
323
  }
291
324
  /**
292
325
  * Move/rename a file, or a whole directory subtree (every key under `from/` is remapped
@@ -295,32 +328,34 @@ var IndexedStore = class {
295
328
  * `/api/rename` enforces. (Mutating the reserved namespace is a programmer error and
296
329
  * still throws.)
297
330
  */
298
- async rename(from, to) {
299
- const source = guarded(normalize(from));
300
- const dest = guarded(normalize(to));
301
- if (source === dest) return { ok: true };
302
- const index = await this.loaded();
303
- const sourcePrefix = `${source}/`;
304
- const affected = index.keys.filter((k) => k === source || k.startsWith(sourcePrefix));
305
- if (affected.length === 0) return { ok: false, reason: "missing" };
306
- const destPrefix = `${dest}/`;
307
- if (index.keys.some((k) => k === dest || k.startsWith(destPrefix))) {
308
- return { ok: false, reason: "exists" };
309
- }
310
- const moves = affected.map((k) => ({
311
- from: k,
312
- to: k === source ? dest : dest + k.slice(source.length)
313
- }));
314
- for (const move of moves) {
315
- const bytes = await this.store.get(move.from);
316
- if (bytes === void 0) continue;
317
- await this.store.put(move.to, bytes);
318
- await this.store.delete(move.from);
319
- }
320
- const movedFrom = new Set(moves.map((move) => move.from));
321
- const next = index.keys.filter((k) => !movedFrom.has(k)).concat(moves.map((m) => m.to));
322
- this.index = await saveIndex(this.store, index, next);
323
- return { ok: true };
331
+ rename(from, to) {
332
+ return this.serializeMutation(async () => {
333
+ const source = guarded(normalize(from));
334
+ const dest = guarded(normalize(to));
335
+ if (source === dest) return { ok: true };
336
+ const index = await this.loaded();
337
+ const sourcePrefix = `${source}/`;
338
+ const affected = index.keys.filter((k) => k === source || k.startsWith(sourcePrefix));
339
+ if (affected.length === 0) return { ok: false, reason: "missing" };
340
+ const destPrefix = `${dest}/`;
341
+ if (index.keys.some((k) => k === dest || k.startsWith(destPrefix))) {
342
+ return { ok: false, reason: "exists" };
343
+ }
344
+ const moves = affected.map((k) => ({
345
+ from: k,
346
+ to: k === source ? dest : dest + k.slice(source.length)
347
+ }));
348
+ for (const move of moves) {
349
+ const bytes = await this.store.get(move.from);
350
+ if (bytes === void 0) continue;
351
+ await this.store.put(move.to, bytes);
352
+ await this.store.delete(move.from);
353
+ }
354
+ const movedFrom = new Set(moves.map((move) => move.from));
355
+ const next = index.keys.filter((k) => !movedFrom.has(k)).concat(moves.map((m) => m.to));
356
+ this.index = await saveIndex(this.store, index, next);
357
+ return { ok: true };
358
+ });
324
359
  }
325
360
  /**
326
361
  * Rebuild this store's index from the *actual* keys the underlying store holds (DESIGN
@@ -330,10 +365,12 @@ var IndexedStore = class {
330
365
  * {@link reconcileIndex} on a fresh store, this also refreshes the cached in-memory
331
366
  * index, so this live instance sees the recovered files immediately.
332
367
  */
333
- async reconcile(actualKeys) {
334
- const result = await reconcileIndex(this.store, actualKeys);
335
- this.index = result.index;
336
- return result;
368
+ reconcile(actualKeys) {
369
+ return this.serializeMutation(async () => {
370
+ const result = await reconcileIndex(this.store, actualKeys);
371
+ this.index = result.index;
372
+ return result;
373
+ });
337
374
  }
338
375
  };
339
376
  function guarded(key) {
@@ -8,7 +8,7 @@ import {
8
8
  loadTemplate,
9
9
  renderReferenceMarkdown,
10
10
  validateSite
11
- } from "./chunk-S23S4ZAK.js";
11
+ } from "./chunk-Y7PUOM5I.js";
12
12
 
13
13
  // ../editor-api/src/scaffold.ts
14
14
  var HOME_HTML = `<!doctype html>
package/dist/index.d.ts CHANGED
@@ -281,9 +281,24 @@ declare class IndexedStore implements WorkingStore {
281
281
  private readonly store;
282
282
  private index;
283
283
  private loading;
284
+ /**
285
+ * Per-instance mutex (DESIGN §11d): every mutation (`write`/`delete`/`rename`/`reconcile`)
286
+ * runs through {@link serializeMutation}, which chains onto the previous mutation's
287
+ * completion. Without it, two concurrent mutations both read the cached `this.index`,
288
+ * each compute their "next" key set from that stale snapshot, and the second
289
+ * `saveIndex` clobbers the first — the file writes land in blob, but the index keeps
290
+ * stale references or loses fresh ones (the "I deleted it and it's still there" or
291
+ * "I added it and it didn't appear" bugs surfaced dogfooding the marketing site,
292
+ * 2026-05-28). Reads (`list`/`exists`/`readBytes`) are *not* serialised; an
293
+ * in-flight mutation simply means a reader sees the pre-mutation index, eventually
294
+ * consistent and safe. Crashes inside `work()` release the lock so a single failure
295
+ * does not deadlock subsequent operations.
296
+ */
297
+ private mutationLock;
284
298
  constructor(store: BlobStore);
285
299
  /** The index, loaded once on first need and cached (mutations replace the cached copy). */
286
300
  private loaded;
301
+ private serializeMutation;
287
302
  list(dir: string): Promise<readonly DirEntry[]>;
288
303
  readText(path: string): Promise<string>;
289
304
  readBytes(path: string): Promise<Uint8Array>;
package/dist/index.js CHANGED
@@ -61,7 +61,7 @@ import {
61
61
  validateSite,
62
62
  valueKindOf,
63
63
  wholeValueToken
64
- } from "./chunk-S23S4ZAK.js";
64
+ } from "./chunk-Y7PUOM5I.js";
65
65
  export {
66
66
  ContentParseError,
67
67
  DEFAULT_DIRS,