@jxsuite/protocol 2.3.1 → 2.5.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/README.md +1 -1
- package/package.json +2 -2
- package/src/index.ts +4 -3
- package/src/routes.ts +65 -9
- package/src/types.ts +45 -6
package/README.md
CHANGED
|
@@ -17,7 +17,7 @@ A backend is free to change the transport and path prefix; the **sub-path, metho
|
|
|
17
17
|
|
|
18
18
|
## Core vs optional
|
|
19
19
|
|
|
20
|
-
Optional routes back optional `StudioPlatform` members. Omitting one is not an error. Studio degrades exactly as each entry's `degradation` describes (the starter picker empties, the Projects catalogue hides, the Publish panel explains git-push publishing, and so on). `coreRouteNames()` / `optionalRouteNames()` split the table programmatically; a conformance test for a new backend can iterate `STUDIO_ROUTES` and assert every core route is served.
|
|
20
|
+
Optional routes back optional `StudioPlatform` members, or, like `filesRead`, back no member and let an adapter serve one it already has more cheaply (their `degradation` names the route they stand in for). Omitting one is not an error. Studio degrades exactly as each entry's `degradation` describes (the starter picker empties, the Projects catalogue hides, the Publish panel explains git-push publishing, and so on). `coreRouteNames()` / `optionalRouteNames()` split the table programmatically; a conformance test for a new backend can iterate `STUDIO_ROUTES` and assert every core route is served.
|
|
21
21
|
|
|
22
22
|
Highlights an implementer should know:
|
|
23
23
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jxsuite/protocol",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.5.0",
|
|
4
4
|
"description": "The Studio Backend Protocol — wire types and the canonical route table every Jx Studio backend implements",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -27,6 +27,6 @@
|
|
|
27
27
|
"test:coverage": "bun test --isolate --coverage"
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
|
-
"@jxsuite/schema": "^2.
|
|
30
|
+
"@jxsuite/schema": "^2.3.0"
|
|
31
31
|
}
|
|
32
32
|
}
|
package/src/index.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The Studio Backend Protocol package: wire types plus the canonical route
|
|
3
|
-
*
|
|
4
|
-
* guide.
|
|
2
|
+
* The Studio Backend Protocol package: wire types plus the canonical route table every Jx Studio
|
|
3
|
+
* backend implements. See README.md for the implementer guide.
|
|
5
4
|
*
|
|
6
5
|
* @license MIT
|
|
7
6
|
*/
|
|
@@ -10,3 +9,5 @@ export * from "./problem";
|
|
|
10
9
|
export * from "./problems";
|
|
11
10
|
export * from "./routes";
|
|
12
11
|
export type * from "./types";
|
|
12
|
+
// The one runtime value types.ts carries: the batch-read limit a backend and a client both size to.
|
|
13
|
+
export { READ_FILES_MAX_PATHS } from "./types";
|
package/src/routes.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Studio Backend Protocol — the canonical route table. The dev server
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* back
|
|
7
|
-
*
|
|
2
|
+
* Studio Backend Protocol — the canonical route table. The dev server (@jxsuite/server) is the
|
|
3
|
+
* reference implementation; every other backend (desktop RPC bridge, cloud platforms) serves the
|
|
4
|
+
* same shapes, either at these literal paths or through an equivalent transport. Optional routes
|
|
5
|
+
* mostly back optional StudioPlatform members — Studio degrades without them, as described by each
|
|
6
|
+
* entry's `degradation`. A few back no member at all: an adapter uses them to serve members it
|
|
7
|
+
* already has more cheaply (`filesRead`), and their `degradation` is the route they stand in for.
|
|
8
8
|
*
|
|
9
9
|
* @license MIT
|
|
10
10
|
*/
|
|
@@ -28,7 +28,10 @@ export type StudioRouteMethod = "GET" | "POST" | "PUT" | "DELETE";
|
|
|
28
28
|
export interface StudioRoute {
|
|
29
29
|
path: string;
|
|
30
30
|
method: StudioRouteMethod;
|
|
31
|
-
/**
|
|
31
|
+
/**
|
|
32
|
+
* True when a backend may omit the route: its PAL member is optional, or it has no member and an
|
|
33
|
+
* adapter falls back to another route.
|
|
34
|
+
*/
|
|
32
35
|
optional: boolean;
|
|
33
36
|
/** One-line contract summary. */
|
|
34
37
|
summary: string;
|
|
@@ -83,7 +86,7 @@ export const STUDIO_ROUTES = {
|
|
|
83
86
|
"GET",
|
|
84
87
|
"/__studio/locate-directory",
|
|
85
88
|
`Resolve the absolute path of a showDirectoryPicker() folder by the id in its ${LOCATION_ID_FILE}`,
|
|
86
|
-
"
|
|
89
|
+
"Browse… reports that the folder could not be placed; the Location is typed by hand.",
|
|
87
90
|
),
|
|
88
91
|
createProject: route(
|
|
89
92
|
"POST",
|
|
@@ -111,6 +114,31 @@ export const STUDIO_ROUTES = {
|
|
|
111
114
|
"files project-wide",
|
|
112
115
|
),
|
|
113
116
|
fileRead: route("GET", "/__studio/file", "Read a file's text content"),
|
|
117
|
+
/**
|
|
118
|
+
* Many reads in ONE round trip, for a backend whose every request is a network hop.
|
|
119
|
+
*
|
|
120
|
+
* The first optional route with no `StudioPlatform` member behind it: the PAL keeps `readFile`,
|
|
121
|
+
* and an adapter for such a backend coalesces the reads issued in one tick into this call itself
|
|
122
|
+
* (specs/desktop.md §3.1, §10.1). Callers never see it, so a backend without it costs latency and
|
|
123
|
+
* nothing else — which is why the dev server, whose reads are loopback, does not serve it.
|
|
124
|
+
*
|
|
125
|
+
* The answer is partial by design, never a 400: past `READ_FILES_MAX_PATHS` paths or the
|
|
126
|
+
* backend's byte budget, the rest come back in `omitted` for the client to ask again, and the
|
|
127
|
+
* first path is always answered so that asking again makes progress. Each answered path carries
|
|
128
|
+
* its own outcome, because one missing file is not a failed batch. It is a READ: a read-only
|
|
129
|
+
* session may call it, and whatever a single `fileRead` does on the backend's side (a cloud
|
|
130
|
+
* session fetches and stores the file it serves) this does per path too.
|
|
131
|
+
*/
|
|
132
|
+
filesRead: route(
|
|
133
|
+
"POST",
|
|
134
|
+
"/__studio/files/read",
|
|
135
|
+
"Read many files' text content {paths} → ReadFilesResult {files: [path, {content} | " +
|
|
136
|
+
"{error: {status, code?, message}}][], omitted}. At most READ_FILES_MAX_PATHS (200) paths " +
|
|
137
|
+
"and a backend byte budget per call: the excess is listed in `omitted` (never a 400) and " +
|
|
138
|
+
"the first path is always answered. Per-path errors: 404 missing/deleted, 415 binary, 413 " +
|
|
139
|
+
"too large, 502 upstream. Allowed for read-only sessions; keeps fileRead's server-side effects",
|
|
140
|
+
"Adapters fall back to one fileRead per path.",
|
|
141
|
+
),
|
|
114
142
|
fileWrite: route("PUT", "/__studio/file", "Write a file's text content"),
|
|
115
143
|
fileDelete: route("DELETE", "/__studio/file", "Delete a file"),
|
|
116
144
|
fileUpload: route(
|
|
@@ -119,6 +147,31 @@ export const STUDIO_ROUTES = {
|
|
|
119
147
|
"Upload binary content to a path → UploadResult {path, size?}. `path` is the answer, not an " +
|
|
120
148
|
"echo: a store that de-duplicates, suffixes, or normalizes names reports what it really wrote",
|
|
121
149
|
),
|
|
150
|
+
/**
|
|
151
|
+
* A project file's raw BYTES, as an answer rather than as a URL.
|
|
152
|
+
*
|
|
153
|
+
* `fileRead` cannot serve this and never could: it answers `readFile(abs, "utf8")`, and Bun
|
|
154
|
+
* replaces every byte sequence that is not valid UTF-8 with U+FFFD — so a JPEG round-trips
|
|
155
|
+
* through it as mojibake, silently and losslessly-looking. A caller that wants to DECODE a
|
|
156
|
+
* project image (Studio's image editor) needs the bytes intact.
|
|
157
|
+
*
|
|
158
|
+
* Distinct from {@link documentRaw}, which serves files at a URL a `<img src>` or a `fetch` in
|
|
159
|
+
* the canvas realm can name. That is the right shape for rendering and the wrong one for reading:
|
|
160
|
+
* on the desktop the canvas origin is a cross-origin loopback, and `server.md` §4.2 bans CORS
|
|
161
|
+
* outright, so nothing in the shell can read those bytes back. This route is addressed by the
|
|
162
|
+
* PAL, not by a document, which is what lets every backend answer it the same way.
|
|
163
|
+
*
|
|
164
|
+
* `Cache-Control: no-store`, deliberately unlike `fileRead`'s `max-age=5`: the caller is an
|
|
165
|
+
* editor about to overwrite the file, and five seconds of staleness is an edit applied to the
|
|
166
|
+
* wrong bytes.
|
|
167
|
+
*/
|
|
168
|
+
fileBytes: route(
|
|
169
|
+
"GET",
|
|
170
|
+
"/__studio/file/bytes",
|
|
171
|
+
"A project file's raw bytes, undecoded — the read side of `fileUpload`, for a caller that must " +
|
|
172
|
+
"not go through UTF-8",
|
|
173
|
+
"Studio can only read a project file as text, so any surface that must decode one stays hidden.",
|
|
174
|
+
),
|
|
122
175
|
/**
|
|
123
176
|
* A project file as its own BYTES, at its own URL.
|
|
124
177
|
*
|
|
@@ -450,7 +503,10 @@ export function coreRouteNames(): StudioRouteName[] {
|
|
|
450
503
|
);
|
|
451
504
|
}
|
|
452
505
|
|
|
453
|
-
/**
|
|
506
|
+
/**
|
|
507
|
+
* Names of the optional routes (each backs an optional StudioPlatform member, or is an adapter's
|
|
508
|
+
* optimisation with a fallback).
|
|
509
|
+
*/
|
|
454
510
|
export function optionalRouteNames(): StudioRouteName[] {
|
|
455
511
|
return (Object.keys(STUDIO_ROUTES) as StudioRouteName[]).filter(
|
|
456
512
|
(name) => STUDIO_ROUTES[name].optional,
|
package/src/types.ts
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Studio Backend Protocol — wire types. Every shape a Studio backend serves
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* platforms) share one contract. Environment-agnostic: no DOM, no node —
|
|
2
|
+
* Studio Backend Protocol — wire types. Every shape a Studio backend serves (or a `StudioPlatform`
|
|
3
|
+
* adapter consumes) lives here so client adapters and server implementations (the dev server, the
|
|
4
|
+
* desktop RPC bridge, cloud platforms) share one contract. Environment-agnostic: no DOM, no node —
|
|
6
5
|
* importable in browsers, Bun, and Cloudflare Workers alike.
|
|
7
6
|
*
|
|
8
7
|
* @license MIT
|
|
@@ -87,6 +86,45 @@ export interface UploadResult {
|
|
|
87
86
|
size?: number;
|
|
88
87
|
}
|
|
89
88
|
|
|
89
|
+
/**
|
|
90
|
+
* The most paths one `filesRead` request may name. A backend answers at most this many and lists
|
|
91
|
+
* the rest under {@link ReadFilesResult.omitted}, so a client that sends more is not refused — it is
|
|
92
|
+
* answered in part, and asks again.
|
|
93
|
+
*
|
|
94
|
+
* The one runtime value in this file: a client sizes its batches to it, so it has to exist at
|
|
95
|
+
* runtime rather than only in a summary string.
|
|
96
|
+
*/
|
|
97
|
+
export const READ_FILES_MAX_PATHS = 200;
|
|
98
|
+
|
|
99
|
+
/** Body of `POST /__studio/files/read` (`filesRead`): the project-relative paths to read. */
|
|
100
|
+
export interface ReadFilesRequest {
|
|
101
|
+
paths: string[];
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* One path's answer inside a {@link ReadFilesResult}: its text, or why it has none.
|
|
106
|
+
*
|
|
107
|
+
* Per path rather than per request, because one unreadable file in a batch of two hundred is not a
|
|
108
|
+
* failed batch. `status` borrows the HTTP code the single-file read would have answered (404
|
|
109
|
+
* missing or deleted, 415 binary, 413 too large, 502 upstream), so a client can treat both routes'
|
|
110
|
+
* failures alike.
|
|
111
|
+
*/
|
|
112
|
+
export type ReadFilesOutcome =
|
|
113
|
+
| { content: string }
|
|
114
|
+
| { error: { status: number; code?: string; message: string } };
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Answer of `POST /__studio/files/read`. `files` pairs each answered path with its outcome;
|
|
118
|
+
* `omitted` lists the requested paths this response did not get to (a path or byte budget ran out).
|
|
119
|
+
* Omission is never an error and never a 400: the client asks for those paths again. A backend
|
|
120
|
+
* always answers the FIRST requested path, which is what guarantees that asking again makes
|
|
121
|
+
* progress.
|
|
122
|
+
*/
|
|
123
|
+
export interface ReadFilesResult {
|
|
124
|
+
files: [path: string, outcome: ReadFilesOutcome][];
|
|
125
|
+
omitted: string[];
|
|
126
|
+
}
|
|
127
|
+
|
|
90
128
|
/**
|
|
91
129
|
* What a backend will accept as an upload, when it says.
|
|
92
130
|
*
|
|
@@ -772,9 +810,10 @@ export interface AiModelsResponse {
|
|
|
772
810
|
* lapsed", and those are different sentences on screen — the second one explains a thing that
|
|
773
811
|
* used to work. `cf_upstream_error` is the third case and deliberately arrives WITH `configured:
|
|
774
812
|
* true`: Cloudflare being briefly unreachable is not a reason to send someone round an OAuth flow
|
|
775
|
-
* that fixes nothing.
|
|
813
|
+
* that fixes nothing. `cf_account_required` is a live grant with no account chosen (ai.md §2.1):
|
|
814
|
+
* re-authorizing lands back in the same state, so it calls for the account picker instead.
|
|
776
815
|
*/
|
|
777
|
-
code?: "cf_not_connected" | "cf_reconnect_required" | "cf_upstream_error";
|
|
816
|
+
code?: "cf_not_connected" | "cf_reconnect_required" | "cf_account_required" | "cf_upstream_error";
|
|
778
817
|
}
|
|
779
818
|
|
|
780
819
|
// ─── Cloudflare publish surface ──────────────────────────────────────────────
|