@thatopen/services 0.10.0 → 0.11.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.
@@ -283,4 +283,9 @@ component, once added to a project, is then triggered by an app or an automation
283
283
  four-point plan and waiting for approval to do what was just requested costs the user a whole turn
284
284
  and buys nothing. Stop and ask only when you are about to change files you did not create, when
285
285
  the request can be read two ways that mean different work, or when the next step is destructive.
286
+ - **Never save to the platform on every change.** Drafts belong in `localStorage` /
287
+ IndexedDB; the platform gets an explicit save. Writes are capped at 30 per minute and a
288
+ `429` loses the write. See
289
+ `node_modules/@thatopen/services/docs/rate-limits.md` before you write any save, sync or
290
+ polling code.
286
291
  - The scaffold already works — **extend it, don't replace it.**
@@ -0,0 +1,123 @@
1
+ # Rate limits, and how to save data without hitting them
2
+
3
+ The platform API is rate limited. Every app and cloud component shares the same
4
+ budget, so **how often you write decides whether your app works**. This page has
5
+ the numbers, the failure mode, and the pattern to use instead.
6
+
7
+ > **The one rule:** never send a request on every change the user makes. No
8
+ > autosave-per-keystroke, no write inside a render loop, no polling loop that
9
+ > hammers a list endpoint. Keep work-in-progress in the browser and talk to the
10
+ > server on an explicit save.
11
+
12
+ ---
13
+
14
+ ## The numbers
15
+
16
+ Limits are per **rolling 60-second window**, counted per user (JWT), or per API
17
+ token owner, or per IP — whichever identifies the caller.
18
+
19
+ | Endpoint | Client method | Limit |
20
+ |---|---|---|
21
+ | `POST /api/item` | `createFile`, `createComponent`, `createApp` | **30 / min** |
22
+ | `POST /api/item/:id/version` | `updateFile` / `updateComponent` **with a `file`** | **30 / min** |
23
+ | `PUT /api/item/:id/version/:tag/metadata` | `updateFileVersionMetadata` | 30 / min |
24
+ | `DELETE /api/item/:id/version/:tag/metadata` | `deleteFileVersionMetadata` | 30 / min |
25
+ | `PUT /api/item/:id/version/:tag/archive` \| `/recover` | `archiveVersion`, `recoverVersion` | 30 / min |
26
+ | `POST /api/item/hidden` \| `/hidden/batch` | `createHiddenFile`, `createHiddenFilesBatch` | 30 / min |
27
+ | `POST /api/processor/:id/execute` | `executeComponent` | 20 / min |
28
+ | `GET /api/item/folder/:id/download`, `POST /api/item/batch/download` | `downloadFolder` | 10 / min |
29
+ | `POST /api/item/batch/versions` \| `/batch/version-metadata` \| `/batch/folders` | `listVersionsBatch`, `getFileVersionMetadataBatch`, `getFoldersBatch` | 60 / min |
30
+ | `POST /api/item/hidden/signed-url/batch` | `getHiddenFileSignedUrlsBatch` | 100 / min |
31
+ | `GET /api/item/hidden/:id/download` | `downloadHiddenFile` | 3000 / min |
32
+ | Everything else | — | 100 / min |
33
+
34
+ Two things follow from the table:
35
+
36
+ - **30 writes per minute is one write every two seconds.** An autosave tied to
37
+ user input passes that in a few seconds of typing or dragging.
38
+ - **Reads are cheap, but not free.** A viewer that mints one signed URL per tile
39
+ will exhaust 100/min quickly — use `getHiddenFileSignedUrlsBatch`, which signs
40
+ up to `STORAGE_BATCH_MAX` files per request.
41
+
42
+ ## What a rate-limited response looks like
43
+
44
+ Status `429`, with a `Retry-After` header (seconds) and this body:
45
+
46
+ ```json
47
+ {
48
+ "statusCode": 429,
49
+ "message": "Rate limit exceeded: max 30 requests per 60s for this endpoint. Retry after 12s.",
50
+ "code": "RATE_LIMITED",
51
+ "details": { "limit": 30, "windowSeconds": 60, "retryAfter": 12, "scope": "user" }
52
+ }
53
+ ```
54
+
55
+ The client surfaces it as a `RequestError`:
56
+
57
+ ```ts
58
+ import { RequestError } from '@thatopen/services';
59
+
60
+ try {
61
+ await client.updateFile(fileId, { file: blob, versionTag: tag });
62
+ } catch (err) {
63
+ if (err instanceof RequestError && err.status === 429) {
64
+ // err.code === 'RATE_LIMITED'
65
+ // err.retryAfter — seconds to wait, from Retry-After or details.retryAfter
66
+ showToast(`Saving is paused for ${err.retryAfter}s — your work is kept locally.`);
67
+ return;
68
+ }
69
+ throw err;
70
+ }
71
+ ```
72
+
73
+ **A 429 means the write did not happen.** Nothing was saved. If the user's only
74
+ copy of the change was in that request, it is gone — which is the real reason
75
+ the local-draft pattern below matters.
76
+
77
+ ## The pattern: local drafts, explicit saves
78
+
79
+ Keep every intermediate state in the browser. Write to the platform only when
80
+ the user asks for it.
81
+
82
+ ```ts
83
+ const draftKey = `draft:${fileId}`;
84
+
85
+ function onChange(state: unknown) {
86
+ localStorage.setItem(draftKey, JSON.stringify({ state, at: Date.now() }));
87
+ }
88
+
89
+ async function onSave(state: unknown) {
90
+ const blob = new Blob([JSON.stringify(state)], { type: 'application/json' });
91
+ await client.updateFile(fileId, { file: blob, versionTag: `v${Date.now()}` });
92
+ localStorage.removeItem(draftKey);
93
+ }
94
+ ```
95
+
96
+ On load, if a draft exists for the file, offer to restore it. That gives crash
97
+ recovery — the thing autosave was really for — at zero requests.
98
+
99
+ Rules of thumb:
100
+
101
+ - **Explicit save**, or a timer no faster than **once every 30 seconds**, and
102
+ only when something actually changed.
103
+ - **One request per save**, not one per changed object. Batch the whole document.
104
+ - **Never save while a save is in flight.** Keep a flag per file, and drop or
105
+ queue the second save. Overlapping writes also create versions that are hard
106
+ to reconcile.
107
+ - **Big or binary work-in-progress** belongs in IndexedDB, not `localStorage`
108
+ (about 5 MB per origin).
109
+ - There is **no draft-write method in the client on purpose.** Local storage is
110
+ the draft store; the platform stores versions the user chose to keep.
111
+
112
+ ## Retries
113
+
114
+ The client does not retry by default. When you turn retries on, it backs off
115
+ exponentially, adds jitter, and honours `Retry-After`:
116
+
117
+ ```ts
118
+ const client = new EngineServicesClient(token, apiUrl, { retries: 3 });
119
+ ```
120
+
121
+ Only network failures, `429` and `5xx` are retried. Other `4xx` fail straight
122
+ away, because repeating them cannot help. Never write your own immediate retry
123
+ loop around a 429 — that is what turns a throttled request into an outage.
package/package.json CHANGED
@@ -15,7 +15,7 @@
15
15
  "access": "public"
16
16
  },
17
17
  "private": false,
18
- "version": "0.10.0",
18
+ "version": "0.11.0",
19
19
  "main": "dist/index.cjs.js",
20
20
  "module": "dist/index.es.js",
21
21
  "types": "dist/index.d.ts",
@@ -19,6 +19,7 @@ Do this before answering any question or writing any code. These are compact des
19
19
  | Platform built-ins | `node_modules/@thatopen/services/docs/builtin/paths.json` |
20
20
  | Platform client API | `node_modules/@thatopen/services/docs/client/paths.json` |
21
21
  | CLI commands | `node_modules/@thatopen/services/docs/cli/paths.json` |
22
+ | Rate limits + how to save data | `node_modules/@thatopen/services/docs/rate-limits.md` |
22
23
  | Engine components (`OBC`, `OBF`) | `https://raw.githubusercontent.com/ThatOpen/engine_components/refs/heads/main/examples/paths.json` |
23
24
  | Fragments (`FRAGS`) | `https://raw.githubusercontent.com/ThatOpen/engine_fragment/refs/heads/main/examples/paths.json` |
24
25
  | UI components (`BUI`) | `https://raw.githubusercontent.com/ThatOpen/engine_ui-components/refs/heads/main/examples/paths.json` — **skip** entries whose path contains `packages/obc` or `bim-grid` |
@@ -38,4 +39,13 @@ Once you have these, you know everything available on the platform. Only then fe
38
39
 
39
40
  ## Hard rules (always apply)
40
41
 
41
- 1. **All UI must be built with Lit**, using the web components from `@thatopen/ui` (`BUI`) — `bim-button`, `bim-panel`, `bim-panel-section`, `bim-toolbar`, `bim-dropdown`, `bim-input`, and the rest of `packages/core`. Always consult the design system before writing any UI: `https://raw.githubusercontent.com/ThatOpen/engine_ui-components/refs/heads/main/DESIGN.md`.
42
+ 1. **Never write to the platform on every change.** No autosave per keystroke,
43
+ per drag, or inside a render loop. Keep work in progress in `localStorage` /
44
+ IndexedDB and call the platform on an **explicit user save** (or a timer no
45
+ faster than once every 30 seconds). Writes are capped at **30 per minute**;
46
+ crossing that returns `429` and the write is **lost**, not queued.
47
+ If the user asks for autosave, build it against local storage and say so.
48
+ Read `node_modules/@thatopen/services/docs/rate-limits.md` before writing any
49
+ save, sync, or polling code — it has the per-endpoint limits and the pattern.
50
+
51
+ 2. **All UI must be built with Lit**, using the web components from `@thatopen/ui` (`BUI`) — `bim-button`, `bim-panel`, `bim-panel-section`, `bim-toolbar`, `bim-dropdown`, `bim-input`, and the rest of `packages/core`. Always consult the design system before writing any UI: `https://raw.githubusercontent.com/ThatOpen/engine_ui-components/refs/heads/main/DESIGN.md`.