@plattar/plattar-ar-adapter 2.7.2 → 2.7.3

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.
@@ -12,6 +12,16 @@ class ConfiguratorState {
12
12
  _mappedVariationIDValues;
13
13
  // This maps Variation SKU against a Variation ID - populated by decodeScene() function
14
14
  _mappedVariationSKUValues;
15
+ // Cache for encodeSceneGraphID() - keyed by serialised scene graph JSON so state
16
+ // changes automatically bust the cache, and concurrent in-flight calls share one fetch
17
+ _cachedSceneGraphID = null;
18
+ _cachedSceneGraphJSON = null;
19
+ // Memoised result of the sceneGraph getter - cleared whenever state is mutated
20
+ _cachedSceneGraph = null;
21
+ // Static page-lifetime cache for Scene API responses, keyed by sceneID.
22
+ // Shared across all controller instances so that multiple <plattar-embed>
23
+ // elements with the same scene-id (or a recreated controller) reuse one fetch.
24
+ static _sceneAPICache = new Map();
15
25
  constructor(state = null) {
16
26
  this._mappedVariationIDValues = new Map();
17
27
  this._mappedVariationSKUValues = new Map();
@@ -108,6 +118,7 @@ class ConfiguratorState {
108
118
  newData[meta.scene_product_index] = SceneModelID;
109
119
  newData[meta.product_variation_index] = null;
110
120
  newData[meta.meta_index] = metaData;
121
+ this._cachedSceneGraph = null;
111
122
  }
112
123
  }
113
124
  /**
@@ -135,6 +146,7 @@ class ConfiguratorState {
135
146
  newData[meta.product_index] = productID;
136
147
  newData[meta.product_variation_index] = productVariationID;
137
148
  newData[meta.meta_index] = metaData;
149
+ this._cachedSceneGraph = null;
138
150
  }
139
151
  }
140
152
  /**
@@ -163,6 +175,7 @@ class ConfiguratorState {
163
175
  newData[meta.scene_product_index] = sceneProductID;
164
176
  newData[meta.product_variation_index] = productVariationID;
165
177
  newData[meta.meta_index] = metaData;
178
+ this._cachedSceneGraph = null;
166
179
  }
167
180
  }
168
181
  /**
@@ -347,24 +360,43 @@ class ConfiguratorState {
347
360
  static decode(state) {
348
361
  return new ConfiguratorState(state);
349
362
  }
363
+ /**
364
+ * Returns a cached Promise<Scene> for the given sceneID, firing at most one
365
+ * API request per sceneID per page lifetime. Both decodeState() and
366
+ * decodeScene() share this cache so they never race each other.
367
+ */
368
+ static _fetchScene(sceneID) {
369
+ const cached = ConfiguratorState._sceneAPICache.get(sceneID);
370
+ if (cached) {
371
+ return cached;
372
+ }
373
+ const fscene = new plattar_api_1.Scene(sceneID);
374
+ fscene.include(plattar_api_1.Project);
375
+ fscene.include(plattar_api_1.SceneModel);
376
+ fscene.include(plattar_api_1.Product);
377
+ fscene.include(plattar_api_1.SceneProduct.include(plattar_api_1.Product.include(plattar_api_1.ProductVariation)));
378
+ const promise = fscene.get();
379
+ ConfiguratorState._sceneAPICache.set(sceneID, promise);
380
+ // Bust the cache on failure so the next call retries
381
+ promise.catch(() => {
382
+ ConfiguratorState._sceneAPICache.delete(sceneID);
383
+ });
384
+ return promise;
385
+ }
350
386
  /**
351
387
  * Decodes a previously generated state
352
388
  * @param sceneID
353
389
  * @param state
390
+ * @param existingState - (optional) reuse this already-decoded instance instead of
391
+ * parsing a fresh one, so mutations applied to it before this call are preserved
354
392
  * @returns
355
393
  */
356
- static async decodeState(sceneID = null, state = null) {
394
+ static async decodeState(sceneID = null, state = null, existingState = null) {
357
395
  if (!sceneID || !state) {
358
396
  throw new Error("ConfiguratorState.decodeState(sceneID, state) - sceneID and state must be defined");
359
397
  }
360
- const configState = new ConfiguratorState(state);
361
- const fscene = new plattar_api_1.Scene(sceneID);
362
- fscene.include(plattar_api_1.Project);
363
- fscene.include(plattar_api_1.Product);
364
- fscene.include(plattar_api_1.SceneProduct);
365
- fscene.include(plattar_api_1.SceneModel);
366
- fscene.include(plattar_api_1.SceneProduct.include(plattar_api_1.Product.include(plattar_api_1.ProductVariation)));
367
- const scene = await fscene.get();
398
+ const configState = existingState ?? new ConfiguratorState(state);
399
+ const scene = await ConfiguratorState._fetchScene(sceneID);
368
400
  return {
369
401
  scene: scene,
370
402
  state: configState
@@ -381,13 +413,7 @@ class ConfiguratorState {
381
413
  throw new Error("ConfiguratorState.decodeScene(sceneID) - sceneID must be defined");
382
414
  }
383
415
  const configState = new ConfiguratorState();
384
- const fscene = new plattar_api_1.Scene(sceneID);
385
- fscene.include(plattar_api_1.Project);
386
- fscene.include(plattar_api_1.SceneProduct);
387
- fscene.include(plattar_api_1.SceneModel);
388
- fscene.include(plattar_api_1.Product);
389
- fscene.include(plattar_api_1.SceneProduct.include(plattar_api_1.Product.include(plattar_api_1.ProductVariation)));
390
- const scene = await fscene.get();
416
+ const scene = await ConfiguratorState._fetchScene(sceneID);
391
417
  const sceneProducts = scene.relationships.filter(plattar_api_1.SceneProduct);
392
418
  const sceneModels = scene.relationships.filter(plattar_api_1.SceneModel);
393
419
  const products = scene.relationships.filter(plattar_api_1.Product);
@@ -445,42 +471,54 @@ class ConfiguratorState {
445
471
  encode() {
446
472
  return btoa(JSON.stringify(this._state));
447
473
  }
448
- async encodeSceneGraphID() {
474
+ encodeSceneGraphID() {
449
475
  const graph = this.sceneGraph;
476
+ const graphJSON = JSON.stringify(graph);
477
+ // Return cached promise if the scene graph state hasn't changed.
478
+ // Caching the Promise (not the resolved value) means concurrent in-flight
479
+ // calls also share a single fetch rather than firing multiple requests.
480
+ if (this._cachedSceneGraphID !== null && this._cachedSceneGraphJSON === graphJSON) {
481
+ return this._cachedSceneGraphID;
482
+ }
450
483
  // some scene-graphs are very large in size, we store it remotely
451
484
  // this storage will expire in 10 minutes so this is a non-permanent version
452
485
  // and is designed for quick ar
453
486
  const url = plattar_api_1.Server.location().type === 'staging' ? 'https://c.plattar.space/v3/redir/store' : 'https://c.plattar.com/v3/redir/store';
454
- // finally send our scene-graph to the backend to generate the AR file and return
455
- try {
456
- const response = await fetch(url, {
457
- method: "POST",
458
- headers: {
459
- "Content-Type": "application/json"
460
- },
461
- body: JSON.stringify({
462
- data: {
463
- attributes: {
464
- data: graph
465
- }
487
+ this._cachedSceneGraphJSON = graphJSON;
488
+ this._cachedSceneGraphID = fetch(url, {
489
+ method: "POST",
490
+ headers: {
491
+ "Content-Type": "application/json"
492
+ },
493
+ body: JSON.stringify({
494
+ data: {
495
+ attributes: {
496
+ data: graph
466
497
  }
467
- })
468
- });
498
+ }
499
+ })
500
+ }).then(async (response) => {
469
501
  if (!response.ok) {
470
502
  throw new Error(`ConfiguratorState.encodeSceneGraphID() - network response was not ok ${response.status}`);
471
503
  }
472
504
  const data = await response.json();
473
505
  return data.data.id;
474
- }
475
- catch (error) {
506
+ }).catch((error) => {
507
+ // Bust the cache on failure so the next call retries
508
+ this._cachedSceneGraphID = null;
509
+ this._cachedSceneGraphJSON = null;
476
510
  throw new Error(`ConfiguratorState.encodeSceneGraphID() - there was a request error to ${url}, error was ${error.message}`);
477
- }
511
+ });
512
+ return this._cachedSceneGraphID;
478
513
  }
479
514
  /**
480
515
  * Compiles and returns the Dynamic Scene Graph (Updated for 2025 for DynamicAR)
481
516
  * NOTE: Eventually this structure should replace ConfiguratorState
482
517
  */
483
518
  get sceneGraph() {
519
+ if (this._cachedSceneGraph !== null) {
520
+ return this._cachedSceneGraph;
521
+ }
484
522
  const objects = this.array();
485
523
  // in here we need to generate the schema input to be sent to the backend service
486
524
  const schema = {
@@ -507,7 +545,8 @@ class ConfiguratorState {
507
545
  schema.inputs.push(data);
508
546
  }
509
547
  });
510
- return schema;
548
+ this._cachedSceneGraph = schema;
549
+ return this._cachedSceneGraph;
511
550
  }
512
551
  }
513
552
  exports.ConfiguratorState = ConfiguratorState;
@@ -2,6 +2,7 @@
2
2
  * Static Utility Functions
3
3
  */
4
4
  export declare class Util {
5
+ private static readonly _cache;
5
6
  static isValidServerLocation(server: string | null | undefined): boolean;
6
7
  static canAugment(): boolean;
7
8
  static canQuicklook(): boolean;
package/dist/util/util.js CHANGED
@@ -5,6 +5,7 @@ exports.Util = void 0;
5
5
  * Static Utility Functions
6
6
  */
7
7
  class Util {
8
+ static _cache = new Map();
8
9
  static isValidServerLocation(server) {
9
10
  if (!server) {
10
11
  return false;
@@ -30,67 +31,118 @@ class Util {
30
31
  return false;
31
32
  }
32
33
  static canAugment() {
33
- return Util.canQuicklook() || Util.canSceneViewer();
34
+ if (Util._cache.has('canAugment'))
35
+ return Util._cache.get('canAugment');
36
+ const result = Util.canQuicklook() || Util.canSceneViewer();
37
+ Util._cache.set('canAugment', result);
38
+ return result;
34
39
  }
35
40
  static canQuicklook() {
41
+ if (Util._cache.has('canQuicklook'))
42
+ return Util._cache.get('canQuicklook');
43
+ let result = false;
36
44
  if (Util.isIOS()) {
37
45
  const isWKWebView = Boolean((window && window).webkit && window.webkit.messageHandlers);
38
46
  if (isWKWebView) {
39
- return Boolean(/CriOS\/|EdgiOS\/|FxiOS\/|GSA\/|DuckDuckGo\//.test(navigator.userAgent));
47
+ result = Boolean(/CriOS\/|EdgiOS\/|FxiOS\/|GSA\/|DuckDuckGo\//.test(navigator.userAgent));
48
+ }
49
+ else {
50
+ const tempAnchor = document.createElement("a");
51
+ result = Boolean(tempAnchor.relList && tempAnchor.relList.supports && tempAnchor.relList.supports("ar"));
40
52
  }
41
- const tempAnchor = document.createElement("a");
42
- return tempAnchor.relList && tempAnchor.relList.supports && tempAnchor.relList.supports("ar");
43
53
  }
44
- return false;
54
+ Util._cache.set('canQuicklook', result);
55
+ return result;
45
56
  }
46
57
  static canSceneViewer() {
47
- return Util.isAndroid() && !Util.isFirefox() && !Util.isOculus();
58
+ if (Util._cache.has('canSceneViewer'))
59
+ return Util._cache.get('canSceneViewer');
60
+ const result = Util.isAndroid() && !Util.isFirefox() && !Util.isOculus();
61
+ Util._cache.set('canSceneViewer', result);
62
+ return result;
48
63
  }
49
64
  static canRealityViewer() {
50
- return Util.isIOS() && Util.getIOSVersion()[0] >= 13;
65
+ if (Util._cache.has('canRealityViewer'))
66
+ return Util._cache.get('canRealityViewer');
67
+ const result = Util.isIOS() && Util.getIOSVersion()[0] >= 13;
68
+ Util._cache.set('canRealityViewer', result);
69
+ return result;
51
70
  }
52
71
  static isSafariOnIOS() {
53
- return Util.isIOS() && Util.isSafari();
72
+ if (Util._cache.has('isSafariOnIOS'))
73
+ return Util._cache.get('isSafariOnIOS');
74
+ const result = Util.isIOS() && Util.isSafari();
75
+ Util._cache.set('isSafariOnIOS', result);
76
+ return result;
54
77
  }
55
78
  static isChromeOnIOS() {
56
- return Util.isIOS() && /CriOS\//.test(navigator.userAgent);
79
+ if (Util._cache.has('isChromeOnIOS'))
80
+ return Util._cache.get('isChromeOnIOS');
81
+ const result = Util.isIOS() && /CriOS\//.test(navigator.userAgent);
82
+ Util._cache.set('isChromeOnIOS', result);
83
+ return result;
57
84
  }
58
85
  static isIOS() {
59
- return (/iPad|iPhone|iPod/.test(navigator.userAgent) && !self.MSStream) || (navigator.platform === "MacIntel" && navigator.maxTouchPoints > 1);
86
+ if (Util._cache.has('isIOS'))
87
+ return Util._cache.get('isIOS');
88
+ const result = (/iPad|iPhone|iPod/.test(navigator.userAgent) && !self.MSStream) || (navigator.platform === "MacIntel" && navigator.maxTouchPoints > 1);
89
+ Util._cache.set('isIOS', result);
90
+ return result;
60
91
  }
61
92
  static isAndroid() {
62
- return /android/i.test(navigator.userAgent);
93
+ if (Util._cache.has('isAndroid'))
94
+ return Util._cache.get('isAndroid');
95
+ const result = /android/i.test(navigator.userAgent);
96
+ Util._cache.set('isAndroid', result);
97
+ return result;
63
98
  }
64
99
  static isFirefox() {
65
- return /firefox/i.test(navigator.userAgent);
100
+ if (Util._cache.has('isFirefox'))
101
+ return Util._cache.get('isFirefox');
102
+ const result = /firefox/i.test(navigator.userAgent);
103
+ Util._cache.set('isFirefox', result);
104
+ return result;
66
105
  }
67
106
  static isOculus() {
68
- return /OculusBrowser/.test(navigator.userAgent);
107
+ if (Util._cache.has('isOculus'))
108
+ return Util._cache.get('isOculus');
109
+ const result = /OculusBrowser/.test(navigator.userAgent);
110
+ Util._cache.set('isOculus', result);
111
+ return result;
69
112
  }
70
113
  static isSafari() {
71
- return Util.isIOS() && /Safari\//.test(navigator.userAgent);
114
+ if (Util._cache.has('isSafari'))
115
+ return Util._cache.get('isSafari');
116
+ const result = Util.isIOS() && /Safari\//.test(navigator.userAgent);
117
+ Util._cache.set('isSafari', result);
118
+ return result;
72
119
  }
73
120
  static getIOSVersion() {
121
+ if (Util._cache.has('getIOSVersion'))
122
+ return Util._cache.get('getIOSVersion');
123
+ let result = [-1, -1, -1];
74
124
  if (/iP(hone|od|ad)/.test(navigator.platform)) {
75
125
  const v = (navigator.appVersion).match(/OS (\d+)_(\d+)_?(\d+)?/);
76
126
  if (v !== null) {
77
- return [parseInt(v[1], 10), parseInt(v[2], 10), parseInt(v[3], 10)];
127
+ result = [parseInt(v[1], 10), parseInt(v[2], 10), parseInt(v[3], 10)];
78
128
  }
79
129
  }
80
- if (/Mac/.test(navigator.platform)) {
130
+ else if (/Mac/.test(navigator.platform)) {
81
131
  const v = (navigator.appVersion).match(/Version\/(\d+)\.(\d+)\.?(\d+)?/);
82
132
  if (v !== null) {
83
- return [parseInt(v[1], 10), parseInt(v[2], 10), parseInt(v[3], 10)];
133
+ result = [parseInt(v[1], 10), parseInt(v[2], 10), parseInt(v[3], 10)];
84
134
  }
85
135
  }
86
- return [-1, -1, -1];
136
+ Util._cache.set('getIOSVersion', result);
137
+ return result;
87
138
  }
88
139
  static getChromeVersion() {
140
+ if (Util._cache.has('getChromeVersion'))
141
+ return Util._cache.get('getChromeVersion');
89
142
  const raw = navigator.userAgent.match(/Chrom(e|ium)\/([0-9]+)\./);
90
- if (raw !== null) {
91
- return parseInt(raw[2], 10);
92
- }
93
- return 1;
143
+ const result = raw !== null ? parseInt(raw[2], 10) : 1;
144
+ Util._cache.set('getChromeVersion', result);
145
+ return result;
94
146
  }
95
147
  }
96
148
  exports.Util = Util;
package/dist/version.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- declare const _default: "2.7.2";
1
+ declare const _default: "2.7.3";
2
2
  export default _default;
package/dist/version.js CHANGED
@@ -1,3 +1,3 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.default = "2.7.2";
3
+ exports.default = "2.7.3";
package/package.json CHANGED
@@ -1,16 +1,21 @@
1
1
  {
2
2
  "name": "@plattar/plattar-ar-adapter",
3
- "version": "2.7.2",
3
+ "version": "2.7.3",
4
4
  "description": "Plattar AR Adapter for interfacing with Google & Apple WebAR",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.js",
7
7
  "types": "dist/index.d.ts",
8
8
  "scripts": {
9
- "clean": "rm -rf build dist node_modules",
9
+ "clean": "node -e \"['build','dist','node_modules'].forEach(p=>require('fs').rmSync(p,{recursive:true,force:true}))\"",
10
10
  "build": "npm run clean && npm install && npm run build-ts && npm run build-es2019 && npm run build-es2015",
11
11
  "build-ts": "tsc --noEmitOnError",
12
- "build-es2019": "rm -rf build/es2019 && mkdir -p build/es2019 && browserify --standalone PlattarARAdapter dist/index.js -o build/es2019/plattar-ar-adapter.js && uglifyjs build/es2019/plattar-ar-adapter.js --output build/es2019/plattar-ar-adapter.min.js",
13
- "build-es2015": "rm -rf build/es2015 && mkdir -p build/es2015 && babel build/es2019/plattar-ar-adapter.js --presets=@babel/env > build/es2015/plattar-ar-adapter.js && uglifyjs build/es2015/plattar-ar-adapter.js --output build/es2015/plattar-ar-adapter.min.js",
12
+ "build-es2019": "rimraf build/es2019 && mkdirp build/es2019 && browserify --standalone PlattarARAdapter dist/index.js -o build/es2019/plattar-ar-adapter.js && uglifyjs build/es2019/plattar-ar-adapter.js --output build/es2019/plattar-ar-adapter.min.js",
13
+ "build-es2015": "rimraf build/es2015 && mkdirp build/es2015 && babel build/es2019/plattar-ar-adapter.js --presets=@babel/env > build/es2015/plattar-ar-adapter.js && uglifyjs build/es2015/plattar-ar-adapter.js --output build/es2015/plattar-ar-adapter.min.js",
14
+ "serve": "http-server build/es2019 -p 3000 --cors -c-1",
15
+ "watch:rebuild": "npm run build-ts && npm run build-es2019",
16
+ "watch:files": "chokidar src/**/*.ts -c \"npm run watch:rebuild\"",
17
+ "prewatch": "npm run watch:rebuild",
18
+ "watch": "concurrently \"npm run serve\" \"npm run watch:files\"",
14
19
  "clean:build": "npm run clean && npm run build"
15
20
  },
16
21
  "repository": {
@@ -47,6 +52,11 @@
47
52
  "@babel/core": "^7.29.0",
48
53
  "@babel/preset-env": "^7.29.5",
49
54
  "browserify": "^17.0.1",
55
+ "chokidar-cli": "^3.0.0",
56
+ "concurrently": "^9.2.1",
57
+ "http-server": "^14.1.1",
58
+ "mkdirp": "^3.0.1",
59
+ "rimraf": "^6.1.3",
50
60
  "typescript": "^6.0.3",
51
61
  "uglify-js": "^3.19.3"
52
62
  },