@fieldnotes/core 0.44.0 → 0.46.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/dist/index.cjs CHANGED
@@ -29,9 +29,12 @@ __export(index_exports, {
29
29
  HandTool: () => HandTool,
30
30
  HistoryStack: () => HistoryStack,
31
31
  ImageTool: () => ImageTool,
32
+ IndexedDBAdapter: () => IndexedDBAdapter,
32
33
  LaserTool: () => LaserTool,
33
34
  LayerManager: () => LayerManager,
35
+ LocalStorageAdapter: () => LocalStorageAdapter,
34
36
  MeasureTool: () => MeasureTool,
37
+ MemoryAdapter: () => MemoryAdapter,
35
38
  NoteTool: () => NoteTool,
36
39
  PencilTool: () => PencilTool,
37
40
  SelectTool: () => SelectTool,
@@ -368,6 +371,22 @@ function migrateElement(obj) {
368
371
  }
369
372
  }
370
373
 
374
+ // src/core/storage/local-storage-adapter.ts
375
+ var LocalStorageAdapter = class {
376
+ async load(key) {
377
+ if (typeof localStorage === "undefined") return null;
378
+ return localStorage.getItem(key);
379
+ }
380
+ async save(key, value) {
381
+ if (typeof localStorage === "undefined") return;
382
+ localStorage.setItem(key, value);
383
+ }
384
+ async clear(key) {
385
+ if (typeof localStorage === "undefined") return;
386
+ localStorage.removeItem(key);
387
+ }
388
+ };
389
+
371
390
  // src/core/auto-save.ts
372
391
  var DEFAULT_KEY = "fieldnotes-autosave";
373
392
  var DEFAULT_DEBOUNCE_MS = 1e3;
@@ -378,14 +397,18 @@ var AutoSave = class {
378
397
  this.key = options.key ?? DEFAULT_KEY;
379
398
  this.debounceMs = options.debounceMs ?? DEFAULT_DEBOUNCE_MS;
380
399
  this.layerManager = options.layerManager;
400
+ this.adapter = options.adapter ?? new LocalStorageAdapter();
381
401
  this.onError = options.onError;
382
402
  }
383
403
  key;
384
404
  debounceMs;
385
405
  layerManager;
406
+ adapter;
386
407
  timerId = null;
387
408
  unsubscribers = [];
388
409
  onError;
410
+ saving = false;
411
+ pendingSave = false;
389
412
  start() {
390
413
  const schedule = () => this.scheduleSave();
391
414
  this.unsubscribers = [
@@ -403,9 +426,8 @@ var AutoSave = class {
403
426
  this.unsubscribers.forEach((fn) => fn());
404
427
  this.unsubscribers = [];
405
428
  }
406
- load() {
407
- if (typeof localStorage === "undefined") return null;
408
- const json = localStorage.getItem(this.key);
429
+ async load() {
430
+ const json = await this.adapter.load(this.key);
409
431
  if (!json) return null;
410
432
  try {
411
433
  return parseState(json);
@@ -413,13 +435,12 @@ var AutoSave = class {
413
435
  return null;
414
436
  }
415
437
  }
416
- clear() {
417
- if (typeof localStorage === "undefined") return;
418
- localStorage.removeItem(this.key);
438
+ async clear() {
439
+ await this.adapter.clear(this.key);
419
440
  }
420
441
  scheduleSave() {
421
442
  this.cancelPending();
422
- this.timerId = setTimeout(() => this.save(), this.debounceMs);
443
+ this.timerId = setTimeout(() => void this.save(), this.debounceMs);
423
444
  }
424
445
  cancelPending() {
425
446
  if (this.timerId !== null) {
@@ -427,19 +448,108 @@ var AutoSave = class {
427
448
  this.timerId = null;
428
449
  }
429
450
  }
430
- save() {
431
- if (typeof localStorage === "undefined") return;
432
- const layers = this.layerManager?.snapshot() ?? [];
433
- const state = exportState(this.store.snapshot(), this.camera, layers);
451
+ async save() {
452
+ if (this.saving) {
453
+ this.pendingSave = true;
454
+ return;
455
+ }
456
+ this.saving = true;
434
457
  try {
435
- localStorage.setItem(this.key, JSON.stringify(state));
458
+ const layers = this.layerManager?.snapshot() ?? [];
459
+ const state = exportState(this.store.snapshot(), this.camera, layers);
460
+ await this.adapter.save(this.key, JSON.stringify(state));
436
461
  } catch (e) {
437
- console.warn("Auto-save failed: storage quota exceeded. State too large for localStorage.");
438
462
  this.onError?.(e instanceof Error ? e : new Error(String(e)));
463
+ } finally {
464
+ this.saving = false;
465
+ if (this.pendingSave) {
466
+ this.pendingSave = false;
467
+ void this.save();
468
+ }
439
469
  }
440
470
  }
441
471
  };
442
472
 
473
+ // src/core/storage/memory-adapter.ts
474
+ var MemoryAdapter = class {
475
+ store = /* @__PURE__ */ new Map();
476
+ async load(key) {
477
+ return this.store.get(key) ?? null;
478
+ }
479
+ async save(key, value) {
480
+ this.store.set(key, value);
481
+ }
482
+ async clear(key) {
483
+ this.store.delete(key);
484
+ }
485
+ };
486
+
487
+ // src/core/storage/indexeddb-adapter.ts
488
+ var DEFAULT_DB = "fieldnotes";
489
+ var DEFAULT_STORE = "state";
490
+ var IndexedDBAdapter = class {
491
+ dbName;
492
+ storeName;
493
+ idb;
494
+ dbPromise = null;
495
+ constructor(options = {}) {
496
+ this.dbName = options.dbName ?? DEFAULT_DB;
497
+ this.storeName = options.storeName ?? DEFAULT_STORE;
498
+ this.idb = options.indexedDB ?? (typeof indexedDB !== "undefined" ? indexedDB : null);
499
+ }
500
+ open() {
501
+ const idb = this.idb;
502
+ if (!idb) return Promise.reject(new Error("IndexedDB unavailable"));
503
+ if (!this.dbPromise) {
504
+ const storeName = this.storeName;
505
+ this.dbPromise = new Promise((resolve, reject) => {
506
+ const req = idb.open(this.dbName, 1);
507
+ req.onupgradeneeded = () => {
508
+ const db = req.result;
509
+ if (!db.objectStoreNames.contains(storeName)) db.createObjectStore(storeName);
510
+ };
511
+ req.onsuccess = () => resolve(req.result);
512
+ req.onerror = () => reject(req.error ?? new Error("IndexedDB open failed"));
513
+ });
514
+ }
515
+ return this.dbPromise;
516
+ }
517
+ async load(key) {
518
+ if (!this.idb) return null;
519
+ const db = await this.open();
520
+ return new Promise((resolve, reject) => {
521
+ const req = db.transaction(this.storeName, "readonly").objectStore(this.storeName).get(key);
522
+ req.onsuccess = () => {
523
+ const v = req.result;
524
+ resolve(typeof v === "string" ? v : null);
525
+ };
526
+ req.onerror = () => reject(req.error ?? new Error("IndexedDB read failed"));
527
+ });
528
+ }
529
+ async save(key, value) {
530
+ if (!this.idb) return;
531
+ const db = await this.open();
532
+ return new Promise((resolve, reject) => {
533
+ const tx = db.transaction(this.storeName, "readwrite");
534
+ tx.objectStore(this.storeName).put(value, key);
535
+ tx.oncomplete = () => resolve();
536
+ tx.onerror = () => reject(tx.error ?? new Error("IndexedDB write failed"));
537
+ tx.onabort = () => reject(tx.error ?? new Error("IndexedDB write aborted"));
538
+ });
539
+ }
540
+ async clear(key) {
541
+ if (!this.idb) return;
542
+ const db = await this.open();
543
+ return new Promise((resolve, reject) => {
544
+ const tx = db.transaction(this.storeName, "readwrite");
545
+ tx.objectStore(this.storeName).delete(key);
546
+ tx.oncomplete = () => resolve();
547
+ tx.onerror = () => reject(tx.error ?? new Error("IndexedDB delete failed"));
548
+ tx.onabort = () => reject(tx.error ?? new Error("IndexedDB delete aborted"));
549
+ });
550
+ }
551
+ };
552
+
443
553
  // src/canvas/camera.ts
444
554
  var DEFAULT_MIN_ZOOM = 0.1;
445
555
  var DEFAULT_MAX_ZOOM = 10;
@@ -2122,6 +2232,7 @@ var Background = class {
2122
2232
  };
2123
2233
 
2124
2234
  // src/core/event-bus.ts
2235
+ var EMPTY_META = Object.freeze({});
2125
2236
  var EventBus = class {
2126
2237
  listeners = /* @__PURE__ */ new Map();
2127
2238
  on(event, listener) {
@@ -2137,10 +2248,10 @@ var EventBus = class {
2137
2248
  off(event, listener) {
2138
2249
  this.listeners.get(event)?.delete(listener);
2139
2250
  }
2140
- emit(event, data) {
2251
+ emit(event, data, meta = EMPTY_META) {
2141
2252
  this.listeners.get(event)?.forEach((listener) => {
2142
2253
  try {
2143
- listener(data);
2254
+ listener(data, meta);
2144
2255
  } catch (err) {
2145
2256
  console.error(`[fieldnotes] listener error for "${String(event)}"`, err);
2146
2257
  }
@@ -2460,15 +2571,15 @@ var ElementStore = class {
2460
2571
  const angle = element.rotation ?? 0;
2461
2572
  return angle === 0 ? bounds : rotatedAABB(bounds, angle);
2462
2573
  }
2463
- add(element) {
2574
+ add(element, meta) {
2464
2575
  this.sortedCache = null;
2465
2576
  this._versions.set(element.id, 0);
2466
2577
  this.elements.set(element.id, element);
2467
2578
  const bounds = this.indexBounds(element);
2468
2579
  if (bounds) this.spatialIndex.insert(element.id, bounds);
2469
- this.bus.emit("add", element);
2580
+ this.bus.emit("add", element, meta);
2470
2581
  }
2471
- update(id, partial) {
2582
+ update(id, partial, meta) {
2472
2583
  const existing = this.elements.get(id);
2473
2584
  if (!existing) return;
2474
2585
  this.sortedCache = null;
@@ -2495,28 +2606,28 @@ var ElementStore = class {
2495
2606
  if (newBounds) {
2496
2607
  this.spatialIndex.update(id, newBounds);
2497
2608
  }
2498
- this.bus.emit("update", { previous: existing, current: updated });
2609
+ this.bus.emit("update", { previous: existing, current: updated }, meta);
2499
2610
  }
2500
- remove(id) {
2611
+ remove(id, meta) {
2501
2612
  const element = this.elements.get(id);
2502
2613
  if (!element) return;
2503
2614
  this.sortedCache = null;
2504
2615
  this._versions.delete(id);
2505
2616
  this.elements.delete(id);
2506
2617
  this.spatialIndex.remove(id);
2507
- this.bus.emit("remove", element);
2618
+ this.bus.emit("remove", element, meta);
2508
2619
  }
2509
- clear() {
2620
+ clear(meta) {
2510
2621
  this.sortedCache = null;
2511
2622
  this._versions.clear();
2512
2623
  this.elements.clear();
2513
2624
  this.spatialIndex.clear();
2514
- this.bus.emit("clear", null);
2625
+ this.bus.emit("clear", null, meta);
2515
2626
  }
2516
2627
  snapshot() {
2517
2628
  return this.getAll().map((el) => ({ ...el }));
2518
2629
  }
2519
- loadSnapshot(elements) {
2630
+ loadSnapshot(elements, meta) {
2520
2631
  this.sortedCache = null;
2521
2632
  this._versions.clear();
2522
2633
  this.elements.clear();
@@ -2533,9 +2644,9 @@ var ElementStore = class {
2533
2644
  el.cachedControlPoint = getArrowControlPoint(el.from, el.to, el.bend);
2534
2645
  }
2535
2646
  }
2536
- this.bus.emit("clear", null);
2647
+ this.bus.emit("clear", null, meta);
2537
2648
  for (const el of elements) {
2538
- this.bus.emit("add", el);
2649
+ this.bus.emit("add", el, meta);
2539
2650
  }
2540
2651
  }
2541
2652
  bringToFront(id) {
@@ -4810,15 +4921,18 @@ var UpdateLayerCommand = class {
4810
4921
  };
4811
4922
 
4812
4923
  // src/history/history-recorder.ts
4924
+ function isExternalChange(meta) {
4925
+ return meta.origin !== void 0 && meta.origin !== "local";
4926
+ }
4813
4927
  var HistoryRecorder = class {
4814
4928
  constructor(store, stack, layerManager) {
4815
4929
  this.store = store;
4816
4930
  this.stack = stack;
4817
4931
  this.layerManager = layerManager;
4818
4932
  this.unsubscribers = [
4819
- store.on("add", (el) => this.onAdd(el)),
4820
- store.on("remove", (el) => this.onRemove(el)),
4821
- store.on("update", ({ previous, current }) => this.onUpdate(previous, current))
4933
+ store.on("add", (el, meta) => this.onAdd(el, meta)),
4934
+ store.on("remove", (el, meta) => this.onRemove(el, meta)),
4935
+ store.on("update", ({ previous, current }, meta) => this.onUpdate(previous, current, meta))
4822
4936
  ];
4823
4937
  if (layerManager) {
4824
4938
  this.unsubscribers.push(
@@ -4874,18 +4988,21 @@ var HistoryRecorder = class {
4874
4988
  this.stack.push(command);
4875
4989
  }
4876
4990
  }
4877
- onAdd(element) {
4991
+ onAdd(element, meta) {
4992
+ if (isExternalChange(meta)) return;
4878
4993
  if (!this.recording) return;
4879
4994
  this.record(new AddElementCommand(element));
4880
4995
  }
4881
- onRemove(element) {
4996
+ onRemove(element, meta) {
4997
+ if (isExternalChange(meta)) return;
4882
4998
  if (!this.recording) return;
4883
4999
  if (this.transaction && this.updateSnapshots.has(element.id)) {
4884
5000
  this.updateSnapshots.delete(element.id);
4885
5001
  }
4886
5002
  this.record(new RemoveElementCommand(element));
4887
5003
  }
4888
- onUpdate(previous, current) {
5004
+ onUpdate(previous, current, meta) {
5005
+ if (isExternalChange(meta)) return;
4889
5006
  if (!this.recording) return;
4890
5007
  if (this.transaction) {
4891
5008
  if (!this.updateSnapshots.has(current.id)) {
@@ -10057,7 +10174,7 @@ var LaserTool = class {
10057
10174
  };
10058
10175
 
10059
10176
  // src/index.ts
10060
- var VERSION = "0.44.0";
10177
+ var VERSION = "0.46.0";
10061
10178
  // Annotate the CommonJS export names for ESM import in node:
10062
10179
  0 && (module.exports = {
10063
10180
  ArrowTool,
@@ -10069,9 +10186,12 @@ var VERSION = "0.44.0";
10069
10186
  HandTool,
10070
10187
  HistoryStack,
10071
10188
  ImageTool,
10189
+ IndexedDBAdapter,
10072
10190
  LaserTool,
10073
10191
  LayerManager,
10192
+ LocalStorageAdapter,
10074
10193
  MeasureTool,
10194
+ MemoryAdapter,
10075
10195
  NoteTool,
10076
10196
  PencilTool,
10077
10197
  SelectTool,