@thatopen/services 0.9.1 → 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.
- package/dist/cli.js +209 -26
- package/dist/core/client.d.ts +78 -2
- package/dist/core/examples/batch-reads.d.ts +1 -0
- package/dist/core/request-error.d.ts +17 -1
- package/dist/index.cjs.js +2 -2
- package/dist/index.d.ts +1 -0
- package/dist/index.es.js +647 -501
- package/dist/types/batch.d.ts +36 -0
- package/docs/ai-quickstart.md +5 -0
- package/docs/client/paths.json +4 -0
- package/docs/rate-limits.md +123 -0
- package/package.json +1 -1
- package/resources/AGENTS.md +11 -1
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { Metadata } from './files';
|
|
2
|
+
import { ItemFolder, ItemVersion } from './items';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Maximum number of ids the API accepts in one batch request. The client
|
|
6
|
+
* methods split larger inputs into chunks of this size automatically.
|
|
7
|
+
*/
|
|
8
|
+
export declare const STORAGE_BATCH_MAX = 100;
|
|
9
|
+
export type BatchError = {
|
|
10
|
+
status: number;
|
|
11
|
+
message: string;
|
|
12
|
+
};
|
|
13
|
+
export type HiddenFileSignedUrlBatchEntry = {
|
|
14
|
+
hiddenFileId: string;
|
|
15
|
+
url?: string;
|
|
16
|
+
expiresAt?: string;
|
|
17
|
+
error?: BatchError;
|
|
18
|
+
};
|
|
19
|
+
export type ItemVersionsBatchEntry = {
|
|
20
|
+
itemId: string;
|
|
21
|
+
versions?: ItemVersion[];
|
|
22
|
+
error?: BatchError;
|
|
23
|
+
};
|
|
24
|
+
export type VersionMetadataBatchRequest = {
|
|
25
|
+
itemId: string;
|
|
26
|
+
versionTag: string;
|
|
27
|
+
};
|
|
28
|
+
export type VersionMetadataBatchEntry = VersionMetadataBatchRequest & {
|
|
29
|
+
metadata?: Metadata;
|
|
30
|
+
error?: BatchError;
|
|
31
|
+
};
|
|
32
|
+
export type ItemFoldersBatchEntry = {
|
|
33
|
+
folderId: string;
|
|
34
|
+
folder?: ItemFolder;
|
|
35
|
+
error?: BatchError;
|
|
36
|
+
};
|
package/docs/ai-quickstart.md
CHANGED
|
@@ -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.**
|
package/docs/client/paths.json
CHANGED
|
@@ -3,6 +3,10 @@
|
|
|
3
3
|
"path": "src/core/examples/apps.ts",
|
|
4
4
|
"description": "App lifecycle — list, get with versions, create, download bundle, and archive."
|
|
5
5
|
},
|
|
6
|
+
{
|
|
7
|
+
"path": "src/core/examples/batch-reads.ts",
|
|
8
|
+
"description": "Batch reads — sign many hidden files, list versions and metadata for many files, and resolve many folders in one request each."
|
|
9
|
+
},
|
|
6
10
|
{
|
|
7
11
|
"path": "src/core/examples/components.ts",
|
|
8
12
|
"description": "Cloud component lifecycle — list, get with versions, create, update, download bundle, archive, and recover."
|
|
@@ -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
package/resources/AGENTS.md
CHANGED
|
@@ -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. **
|
|
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`.
|