@thatopen/services 0.10.0 → 0.12.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.
@@ -1567,6 +1567,238 @@ export type HelloWorld = InstanceType<typeof _HelloWorld>;
1567
1567
  export declare const HelloWorld: typeof _HelloWorld & {
1568
1568
  uuid: "2c4ae432-fc24-43e9-9783-0c960c674e96";
1569
1569
  };
1570
+ export type ProjectNamespaceKey = "origin" | "georeferencing" | "appSettings";
1571
+ export interface ProjectNamespace {
1572
+ label: string;
1573
+ description?: string;
1574
+ }
1575
+ export interface ProjectEntity<T = unknown> {
1576
+ namespaceId: ProjectNamespaceKey;
1577
+ label: string;
1578
+ description?: string;
1579
+ data: T;
1580
+ }
1581
+ /** GIS origin point that fixes the scene's (0,0,0) to a real-world coordinate. */
1582
+ export interface OriginData {
1583
+ lat: number;
1584
+ lon: number;
1585
+ height: number;
1586
+ rotation: number;
1587
+ }
1588
+ export interface BimSite {
1589
+ label: string;
1590
+ description?: string;
1591
+ lat: number;
1592
+ lon: number;
1593
+ height: number;
1594
+ rotation: number;
1595
+ /** Column-major 4x4 matrix (16 values) mapping BIM local space to the site's georeferenced frame. */
1596
+ baseMatrix: number[];
1597
+ }
1598
+ export interface BimCoordinatesData {
1599
+ defaultSiteId?: string;
1600
+ sites: Record<string, BimSite>;
1601
+ }
1602
+ export type AssetCoordinateType = "splat" | "ifc" | "point-cloud" | (string & {});
1603
+ export interface AssetCoordinate {
1604
+ fileId: string;
1605
+ type: AssetCoordinateType;
1606
+ lat: number;
1607
+ lon: number;
1608
+ height: number;
1609
+ rotation: number;
1610
+ }
1611
+ export interface GraphicsSettingsData {
1612
+ stylePreset: string;
1613
+ postproduction: boolean;
1614
+ edges: {
1615
+ enabled: boolean;
1616
+ color: string;
1617
+ opacity: number;
1618
+ };
1619
+ ambientOcclusion: {
1620
+ enabled: boolean;
1621
+ value: number;
1622
+ };
1623
+ tonalShading: {
1624
+ enabled: boolean;
1625
+ value: number;
1626
+ };
1627
+ surfaceColor: {
1628
+ enabled: boolean;
1629
+ };
1630
+ transparentBackground: {
1631
+ enabled: boolean;
1632
+ color: string;
1633
+ };
1634
+ grid: {
1635
+ enabled: boolean;
1636
+ color: string;
1637
+ };
1638
+ gridHeight: {
1639
+ auto: boolean;
1640
+ value: number;
1641
+ };
1642
+ antiAliasing: boolean;
1643
+ nativeResolution: boolean;
1644
+ adaptiveResolution: {
1645
+ enabled: boolean;
1646
+ value: number;
1647
+ };
1648
+ minResolution: number;
1649
+ renderScale: number;
1650
+ }
1651
+ export interface ProjectManagerData {
1652
+ schemaVersion: 1;
1653
+ namespaces: Record<ProjectNamespaceKey, ProjectNamespace>;
1654
+ entities: {
1655
+ origin: ProjectEntity<OriginData>;
1656
+ bimCoordinates: ProjectEntity<BimCoordinatesData>;
1657
+ assetCoordinates: ProjectEntity<AssetCoordinate[]>;
1658
+ graphicsSettings: ProjectEntity<GraphicsSettingsData>;
1659
+ };
1660
+ }
1661
+ export interface SingletonEntity<T> {
1662
+ get(): T | undefined;
1663
+ set(data: T): Promise<void>;
1664
+ update(partial: Partial<T>): Promise<void>;
1665
+ }
1666
+ export interface BimCoordinatesEntity {
1667
+ get(): BimCoordinatesData;
1668
+ getSites(): Record<string, BimSite>;
1669
+ getSite(siteId: string): BimSite | undefined;
1670
+ getDefaultSite(): BimSite | undefined;
1671
+ /** Creates a new site. `ProjectManager` generates the opaque `siteId` — callers
1672
+ * never invent their own (see the class-level JSDoc on `ProjectManager` for why). */
1673
+ createSite(site: BimSite): Promise<string>;
1674
+ /** Updates a site that already exists (from `createSite`). Throws if `siteId`
1675
+ * isn't in `sites` — this is not an upsert; use `createSite` to add one. */
1676
+ setSite(siteId: string, site: BimSite): Promise<void>;
1677
+ setDefaultSite(siteId: string): Promise<void>;
1678
+ removeSite(siteId: string): Promise<void>;
1679
+ }
1680
+ export interface AssetCoordinatesEntity {
1681
+ getAll(): AssetCoordinate[];
1682
+ get(fileId: string): AssetCoordinate | undefined;
1683
+ getByType(type: AssetCoordinateType): AssetCoordinate[];
1684
+ set(fileId: string, data: Omit<AssetCoordinate, "fileId">): Promise<void>;
1685
+ remove(fileId: string): Promise<void>;
1686
+ }
1687
+ /**
1688
+ * ProjectManager is the platform's canonical store for project-level
1689
+ * configuration and data — origin/georeferencing/app-settings today, with
1690
+ * room to grow. It persists a single JSON file (`${ProjectManager.uuid}.json`)
1691
+ * loose directly inside `__project_data` (same folder convention `GISManager`
1692
+ * already used) — NOT in a per-app subfolder like `collider`/`cde`, which are
1693
+ * for secondary, per-builtin data. Sharing that root folder with other
1694
+ * builtins doesn't change its role: it's still the one canonical project
1695
+ * record, meant to stop each builtin from inventing its own ad-hoc
1696
+ * project-persistence pattern.
1697
+ *
1698
+ * Schema shape (see `src/types.ts` for the full rationale):
1699
+ * - `namespaces`/`entities` are maps keyed by a fixed, readable slug — a
1700
+ * closed set this code defines, never created at runtime.
1701
+ * - `bimCoordinates.data.sites` is the one open, runtime-created collection
1702
+ * today, so its keys are opaque generated ids instead of slugs.
1703
+ * - `origin`/`graphicsSettings` are singletons; `sites`/`assetCoordinates`
1704
+ * are collections.
1705
+ * - `entities[id]` is a uniform envelope: `namespaceId`, mandatory `label`,
1706
+ * optional `description` (omitted, not stored empty), free-form `data`.
1707
+ *
1708
+ * Id generation rule: whenever a collection needs an opaque id (today just
1709
+ * `sites`, potentially more collections later), `ProjectManager` generates it
1710
+ * internally (`crypto.randomUUID()`, no prefix) — callers never invent their
1711
+ * own. `createSite` is the current example; follow the same pattern for any
1712
+ * future `create*` method.
1713
+ */
1714
+ declare class _ProjectManager extends OBC.Component {
1715
+ static readonly uuid: "6f3a9c2e-8b1d-4e6a-9c3f-1d7b5a8e2c4f";
1716
+ enabled: boolean;
1717
+ /** True once init(client) has been called — safe to use the entity accessors. */
1718
+ get ready(): boolean;
1719
+ /** Suppress auto-save while bulk-loading data. */
1720
+ private _loadingData;
1721
+ readonly onSaveStart: OBC.Event<void>;
1722
+ readonly onSaveComplete: OBC.Event<boolean>;
1723
+ private _client?;
1724
+ private _fileId?;
1725
+ private _data;
1726
+ private _saveTimer?;
1727
+ private _savePromise?;
1728
+ private _scheduleSave;
1729
+ private _queueSave;
1730
+ private _doSave;
1731
+ /** Cancels the pending debounce and runs the save immediately. Awaits completion. */
1732
+ flush(): Promise<void>;
1733
+ init(client: PlatformClient): Promise<void>;
1734
+ /** Opaque id generator for any collection needing one (currently just
1735
+ * `bimCoordinates.sites`) — see the class-level JSDoc's id-generation rule. */
1736
+ private _generateId;
1737
+ origin: SingletonEntity<OriginData>;
1738
+ graphicsSettings: SingletonEntity<GraphicsSettingsData>;
1739
+ bimCoordinates: BimCoordinatesEntity;
1740
+ assetCoordinates: AssetCoordinatesEntity;
1741
+ getNamespaces(): Record<ProjectNamespaceKey, ProjectNamespace>;
1742
+ getNamespace(key: ProjectNamespaceKey): Record<string, unknown>;
1743
+ }
1744
+ /**
1745
+ * ProjectManager is the platform's canonical store for project-level
1746
+ * configuration and data — origin/georeferencing/app-settings today, with
1747
+ * room to grow. It persists a single JSON file (`${ProjectManager.uuid}.json`)
1748
+ * loose directly inside `__project_data` (same folder convention `GISManager`
1749
+ * already used) — NOT in a per-app subfolder like `collider`/`cde`, which are
1750
+ * for secondary, per-builtin data. Sharing that root folder with other
1751
+ * builtins doesn't change its role: it's still the one canonical project
1752
+ * record, meant to stop each builtin from inventing its own ad-hoc
1753
+ * project-persistence pattern.
1754
+ *
1755
+ * Schema shape (see `src/types.ts` for the full rationale):
1756
+ * - `namespaces`/`entities` are maps keyed by a fixed, readable slug — a
1757
+ * closed set this code defines, never created at runtime.
1758
+ * - `bimCoordinates.data.sites` is the one open, runtime-created collection
1759
+ * today, so its keys are opaque generated ids instead of slugs.
1760
+ * - `origin`/`graphicsSettings` are singletons; `sites`/`assetCoordinates`
1761
+ * are collections.
1762
+ * - `entities[id]` is a uniform envelope: `namespaceId`, mandatory `label`,
1763
+ * optional `description` (omitted, not stored empty), free-form `data`.
1764
+ *
1765
+ * Id generation rule: whenever a collection needs an opaque id (today just
1766
+ * `sites`, potentially more collections later), `ProjectManager` generates it
1767
+ * internally (`crypto.randomUUID()`, no prefix) — callers never invent their
1768
+ * own. `createSite` is the current example; follow the same pattern for any
1769
+ * future `create*` method.
1770
+ */
1771
+ export type ProjectManager = InstanceType<typeof _ProjectManager>;
1772
+ /**
1773
+ * ProjectManager is the platform's canonical store for project-level
1774
+ * configuration and data — origin/georeferencing/app-settings today, with
1775
+ * room to grow. It persists a single JSON file (`${ProjectManager.uuid}.json`)
1776
+ * loose directly inside `__project_data` (same folder convention `GISManager`
1777
+ * already used) — NOT in a per-app subfolder like `collider`/`cde`, which are
1778
+ * for secondary, per-builtin data. Sharing that root folder with other
1779
+ * builtins doesn't change its role: it's still the one canonical project
1780
+ * record, meant to stop each builtin from inventing its own ad-hoc
1781
+ * project-persistence pattern.
1782
+ *
1783
+ * Schema shape (see `src/types.ts` for the full rationale):
1784
+ * - `namespaces`/`entities` are maps keyed by a fixed, readable slug — a
1785
+ * closed set this code defines, never created at runtime.
1786
+ * - `bimCoordinates.data.sites` is the one open, runtime-created collection
1787
+ * today, so its keys are opaque generated ids instead of slugs.
1788
+ * - `origin`/`graphicsSettings` are singletons; `sites`/`assetCoordinates`
1789
+ * are collections.
1790
+ * - `entities[id]` is a uniform envelope: `namespaceId`, mandatory `label`,
1791
+ * optional `description` (omitted, not stored empty), free-form `data`.
1792
+ *
1793
+ * Id generation rule: whenever a collection needs an opaque id (today just
1794
+ * `sites`, potentially more collections later), `ProjectManager` generates it
1795
+ * internally (`crypto.randomUUID()`, no prefix) — callers never invent their
1796
+ * own. `createSite` is the current example; follow the same pattern for any
1797
+ * future `create*` method.
1798
+ */
1799
+ export declare const ProjectManager: typeof _ProjectManager & {
1800
+ uuid: "6f3a9c2e-8b1d-4e6a-9c3f-1d7b5a8e2c4f";
1801
+ };
1570
1802
  /** `main.bcf`'s path relative to `__project_data/` — shared with
1571
1803
  * `requestBcfSave` (`UIManager/src/utils/bcf-storage.ts`) so both sides of
1572
1804
  * the save/load round-trip agree on where the file lives. */
package/dist/cli.js CHANGED
@@ -6811,12 +6811,22 @@ function parseErrorBody(body) {
6811
6811
  }
6812
6812
  return {};
6813
6813
  }
6814
+ function retryAfterFromDetails(details) {
6815
+ if (!details || typeof details !== "object") return void 0;
6816
+ const value2 = details.retryAfter;
6817
+ return typeof value2 === "number" && Number.isFinite(value2) && value2 >= 0 ? value2 : void 0;
6818
+ }
6814
6819
  class RequestError extends Error {
6815
6820
  status;
6816
6821
  code;
6817
6822
  details;
6818
6823
  body;
6819
- constructor(status, statusText, body) {
6824
+ /**
6825
+ * Seconds to wait before retrying, taken from the `Retry-After` header or
6826
+ * from `details.retryAfter`. Only present on rate-limited (429) responses.
6827
+ */
6828
+ retryAfter;
6829
+ constructor(status, statusText, body, retryAfter) {
6820
6830
  const parsed = parseErrorBody(body);
6821
6831
  super(parsed.message ?? `${statusText || "Request failed"} (${status})`);
6822
6832
  this.name = "RequestError";
@@ -6824,6 +6834,7 @@ class RequestError extends Error {
6824
6834
  this.code = parsed.code;
6825
6835
  this.details = parsed.details;
6826
6836
  this.body = body;
6837
+ this.retryAfter = retryAfter ?? retryAfterFromDetails(parsed.details);
6827
6838
  }
6828
6839
  }
6829
6840
  const FOLDER_PATH = "item/folder";
@@ -6834,6 +6845,35 @@ const HIDDEN_PATH = "hidden";
6834
6845
  const ITEM_TYPE_FILE = "FILE";
6835
6846
  const ITEM_TYPE_COMPONENT = "TOOL";
6836
6847
  const ITEM_TYPE_APP = "APP";
6848
+ const RETRY_BASE_DELAY_MS = 500;
6849
+ const RETRY_MAX_DELAY_MS = 3e4;
6850
+ const RETRY_JITTER_RATIO = 0.25;
6851
+ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
6852
+ function parseRetryAfter(response) {
6853
+ const raw = response.headers?.get?.("Retry-After");
6854
+ if (!raw) return void 0;
6855
+ const seconds = Number(raw);
6856
+ if (Number.isFinite(seconds)) return Math.max(0, seconds);
6857
+ const timestamp = Date.parse(raw);
6858
+ if (Number.isNaN(timestamp)) return void 0;
6859
+ return Math.max(0, (timestamp - Date.now()) / 1e3);
6860
+ }
6861
+ function isRetryable(error2) {
6862
+ if (!(error2 instanceof RequestError)) return true;
6863
+ return error2.status === 429 || error2.status >= 500;
6864
+ }
6865
+ function retryDelayMs(error2, attempt) {
6866
+ const backoff = Math.min(
6867
+ RETRY_BASE_DELAY_MS * 2 ** attempt,
6868
+ RETRY_MAX_DELAY_MS
6869
+ );
6870
+ const jitter = backoff * RETRY_JITTER_RATIO * Math.random();
6871
+ const serverDelay = error2 instanceof RequestError && error2.retryAfter != null ? error2.retryAfter * 1e3 : void 0;
6872
+ if (serverDelay != null) {
6873
+ return Math.min(serverDelay, RETRY_MAX_DELAY_MS) + jitter;
6874
+ }
6875
+ return backoff + jitter;
6876
+ }
6837
6877
  class EngineServicesClient {
6838
6878
  apiUrl;
6839
6879
  accessToken;
@@ -6906,7 +6946,9 @@ class EngineServicesClient {
6906
6946
  }
6907
6947
  }
6908
6948
  /**
6909
- * Sets the number of automatic retries for failed requests.
6949
+ * Sets the number of automatic retries for failed requests. Retries use
6950
+ * exponential backoff with jitter and honour `Retry-After`; only network
6951
+ * failures, 429s and 5xx are retried.
6910
6952
  * @param retries - Number of retries (0 = no retries).
6911
6953
  */
6912
6954
  setRetries(retries) {
@@ -6966,7 +7008,7 @@ class EngineServicesClient {
6966
7008
  return this.accessToken;
6967
7009
  }
6968
7010
  async #requestApi(method, path2, requestData) {
6969
- const { body, query, contentType, retries, responseType } = requestData || {};
7011
+ const { body, query, contentType, retries, responseType, retryAttempt } = requestData || {};
6970
7012
  const url2 = this.#buildUrl(path2);
6971
7013
  const cleanQuery = this.#cleanData(query);
6972
7014
  const token = await this.resolveAccessToken();
@@ -6992,7 +7034,8 @@ class EngineServicesClient {
6992
7034
  throw new RequestError(
6993
7035
  response.status,
6994
7036
  response.statusText,
6995
- textResponse
7037
+ textResponse,
7038
+ parseRetryAfter(response)
6996
7039
  );
6997
7040
  }
6998
7041
  if (responseType === "blob") {
@@ -7000,16 +7043,17 @@ class EngineServicesClient {
7000
7043
  }
7001
7044
  return response.json().then((data) => data).catch(() => void 0);
7002
7045
  } catch (e) {
7003
- let retriesAmmount = retries != null ? retries : this.retries;
7004
- if (retriesAmmount) {
7005
- retriesAmmount = retriesAmmount - 1;
7046
+ const retriesLeft = retries != null ? retries : this.retries;
7047
+ if (retriesLeft > 0 && isRetryable(e)) {
7048
+ const attempt = retryAttempt ?? 0;
7049
+ await sleep(retryDelayMs(e, attempt));
7006
7050
  return await this.#requestApi(method, path2, {
7007
7051
  ...requestData,
7008
- retries: retriesAmmount
7052
+ retries: retriesLeft - 1,
7053
+ retryAttempt: attempt + 1
7009
7054
  });
7010
- } else {
7011
- throw e;
7012
7055
  }
7056
+ throw e;
7013
7057
  }
7014
7058
  }
7015
7059
  /**
@@ -84,7 +84,13 @@ export type DownloadItemFileParams = {
84
84
  };
85
85
  /** Configuration options for the {@link EngineServicesClient} constructor. */
86
86
  export type EngineServicesClientProps = {
87
- /** Number of automatic retries on request failure. Default: 0. */
87
+ /**
88
+ * Number of automatic retries on request failure. Default: 0.
89
+ *
90
+ * Retries are spaced with exponential backoff plus jitter, and honour the
91
+ * server's `Retry-After` when it sends one. Only network failures, 429s and
92
+ * 5xx are retried — other 4xx fail immediately.
93
+ */
88
94
  retries?: number;
89
95
  /**
90
96
  * If true, sends the token as an `Authorization: Bearer` header instead of
@@ -167,7 +173,9 @@ export declare class EngineServicesClient {
167
173
  */
168
174
  constructor(accessToken: string, apiUrl: string, props?: EngineServicesClientProps);
169
175
  /**
170
- * Sets the number of automatic retries for failed requests.
176
+ * Sets the number of automatic retries for failed requests. Retries use
177
+ * exponential backoff with jitter and honour `Retry-After`; only network
178
+ * failures, 429s and 5xx are retried.
171
179
  * @param retries - Number of retries (0 = no retries).
172
180
  */
173
181
  setRetries(retries: number): void;
@@ -15,11 +15,27 @@
15
15
  * }
16
16
  * }
17
17
  * ```
18
+ *
19
+ * @example Rate limiting
20
+ * ```ts
21
+ * catch (err) {
22
+ * if (err instanceof RequestError && err.status === 429) {
23
+ * console.log(err.code); // "RATE_LIMITED"
24
+ * console.log(err.details); // { limit, windowSeconds, retryAfter, scope }
25
+ * console.log(err.retryAfter); // seconds to wait before trying again
26
+ * }
27
+ * }
28
+ * ```
18
29
  */
19
30
  export declare class RequestError extends Error {
20
31
  readonly status: number;
21
32
  readonly code?: string;
22
33
  readonly details?: unknown;
23
34
  readonly body: string;
24
- constructor(status: number, statusText: string, body: string);
35
+ /**
36
+ * Seconds to wait before retrying, taken from the `Retry-After` header or
37
+ * from `details.retryAfter`. Only present on rate-limited (429) responses.
38
+ */
39
+ readonly retryAfter?: number;
40
+ constructor(status: number, statusText: string, body: string, retryAfter?: number);
25
41
  }