@statewalker/webrun-files-rpc 0.1.2 → 0.2.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 +6 -0
- package/dist/cjs/index.cjs +64 -2
- package/dist/client.d.ts +6 -0
- package/dist/client.d.ts.map +1 -1
- package/dist/esm/index.js +64 -2
- package/package.json +4 -4
- package/src/client.ts +31 -2
package/README.md
CHANGED
|
@@ -119,6 +119,12 @@ Every argument and result is a plain value: strings, numbers, booleans, plain ob
|
|
|
119
119
|
| `stats`, `exists` | as the `FilesApi` |
|
|
120
120
|
| `mkdir(path, {requestId?})`, `remove(path, {requestId?})`, `move(source, target, {requestId?})`, `copy(source, target, {requestId?})` | as the `FilesApi`; with a `requestId`, once (a call delivered again gets the first answer) |
|
|
121
121
|
|
|
122
|
+
`move` is as atomic as the served backend's `move` (node, mem, sqlite: atomic; s3 or a cross-mount
|
|
123
|
+
composite: copy then delete); the protocol adds no guarantee of its own. A `MoveIncompleteError`
|
|
124
|
+
thrown by the served backend is rebuilt by the client as a `MoveIncompleteError` with the client's
|
|
125
|
+
paths. It is recognised by its message (`move /a -> /b: the target was replaced, but the source
|
|
126
|
+
could not be removed`), since a structured clone keeps an error's message but only standard names.
|
|
127
|
+
|
|
122
128
|
- **Stream ids:** `streamId` may be chosen by the caller: 16 to 128 characters of `[A-Za-z0-9_-]`,
|
|
123
129
|
random, such as `crypto.randomUUID()` without dashes. Then an open delivered twice opens one
|
|
124
130
|
stream; the client always does this.
|
package/dist/cjs/index.cjs
CHANGED
|
@@ -1,4 +1,46 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
// ../webrun-files/dist/esm/index.js
|
|
3
|
+
/**
|
|
4
|
+
* Thrown by a `move` done as copy-then-delete when the copy succeeded but the source
|
|
5
|
+
* could not be removed, even on a second attempt. The move is half done: the target
|
|
6
|
+
* already holds the moved content (an existing target was replaced), and the source
|
|
7
|
+
* still exists. A `false` would wrongly say that nothing happened, so this is thrown
|
|
8
|
+
* instead.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* try {
|
|
12
|
+
* await files.move("/repo/index.lock", "/repo/index");
|
|
13
|
+
* } catch (error) {
|
|
14
|
+
* if (error instanceof MoveIncompleteError) await files.remove(error.source);
|
|
15
|
+
* else throw error;
|
|
16
|
+
* }
|
|
17
|
+
*/
|
|
18
|
+
var MoveIncompleteError = class extends Error {
|
|
19
|
+
source;
|
|
20
|
+
target;
|
|
21
|
+
name = "MoveIncompleteError";
|
|
22
|
+
constructor(source, target, options) {
|
|
23
|
+
super(`move ${source} -> ${target}: the target was replaced, but the source could not be removed`, options);
|
|
24
|
+
this.source = source;
|
|
25
|
+
this.target = target;
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* Path manipulation utilities for virtual filesystem paths.
|
|
30
|
+
* All paths use forward slashes and start with "/".
|
|
31
|
+
*/
|
|
32
|
+
/**
|
|
33
|
+
* Normalizes a file path to a consistent format.
|
|
34
|
+
* - Adds leading slash if missing
|
|
35
|
+
* - Removes trailing slash
|
|
36
|
+
* - Removes `.` segments
|
|
37
|
+
* - Collapses multiple slashes
|
|
38
|
+
*/
|
|
39
|
+
function normalizePath(filePath) {
|
|
40
|
+
const segments = filePath.split("/").filter((s) => !!s && s !== ".");
|
|
41
|
+
if (segments.length === 0) return "/";
|
|
42
|
+
return `/${segments.join("/")}`;
|
|
43
|
+
}
|
|
2
44
|
// src/client.ts
|
|
3
45
|
const toBytes$1 = (v) => v instanceof Uint8Array ? v : ArrayBuffer.isView(v) ? new Uint8Array(v.buffer, v.byteOffset, v.byteLength) : new Uint8Array(v);
|
|
4
46
|
const newStreamId$1 = () => crypto.randomUUID().replace(/-/g, "");
|
|
@@ -155,8 +197,18 @@ var FilesRpcClient = class {
|
|
|
155
197
|
remove(path) {
|
|
156
198
|
return this.#rpc.remove(path, { requestId: newStreamId$1() });
|
|
157
199
|
}
|
|
158
|
-
|
|
159
|
-
|
|
200
|
+
/**
|
|
201
|
+
* One `move` call: the server's backend does the move, so it is exactly as atomic as
|
|
202
|
+
* that backend's `move` (node, mem, sqlite: atomic; s3 or a cross-mount composite:
|
|
203
|
+
* copy then delete). The client adds no guarantee, and a call that fails in transit
|
|
204
|
+
* leaves it unknown whether the move happened.
|
|
205
|
+
*/
|
|
206
|
+
async move(source, target) {
|
|
207
|
+
try {
|
|
208
|
+
return await this.#rpc.move(source, target, { requestId: newStreamId$1() });
|
|
209
|
+
} catch (error) {
|
|
210
|
+
throw moveIncomplete(error, source, target);
|
|
211
|
+
}
|
|
160
212
|
}
|
|
161
213
|
copy(source, target) {
|
|
162
214
|
return this.#rpc.copy(source, target, { requestId: newStreamId$1() });
|
|
@@ -166,6 +218,16 @@ var FilesRpcClient = class {
|
|
|
166
218
|
function newFilesRpcClient(options) {
|
|
167
219
|
return new FilesRpcClient(options);
|
|
168
220
|
}
|
|
221
|
+
/**
|
|
222
|
+
* A server-side `MoveIncompleteError` arrives as a plain error: over http with its name and
|
|
223
|
+
* message, through a structured clone (rpc transports) with its message only. Recognised by
|
|
224
|
+
* either, it is rebuilt with the client's paths, the original kept as its cause.
|
|
225
|
+
*/
|
|
226
|
+
function moveIncomplete(error, source, target) {
|
|
227
|
+
const { name, message } = error ?? {};
|
|
228
|
+
return name === "MoveIncompleteError" || typeof message === "string" && MOVE_INCOMPLETE_MESSAGE.test(message) ? new MoveIncompleteError(normalizePath(source), normalizePath(target), { cause: error }) : error;
|
|
229
|
+
}
|
|
230
|
+
const MOVE_INCOMPLETE_MESSAGE = /^move .* -> .*: the target was replaced, but the source could not be removed$/s;
|
|
169
231
|
// src/errors.ts
|
|
170
232
|
/**
|
|
171
233
|
* An error of the protocol itself. The code also leads the message (`UNKNOWN_STREAM: …`), since
|
package/dist/client.d.ts
CHANGED
|
@@ -37,6 +37,12 @@ export declare class FilesRpcClient implements FilesApi {
|
|
|
37
37
|
stats(path: string): Promise<FileStats | undefined>;
|
|
38
38
|
exists(path: string): Promise<boolean>;
|
|
39
39
|
remove(path: string): Promise<boolean>;
|
|
40
|
+
/**
|
|
41
|
+
* One `move` call: the server's backend does the move, so it is exactly as atomic as
|
|
42
|
+
* that backend's `move` (node, mem, sqlite: atomic; s3 or a cross-mount composite:
|
|
43
|
+
* copy then delete). The client adds no guarantee, and a call that fails in transit
|
|
44
|
+
* leaves it unknown whether the move happened.
|
|
45
|
+
*/
|
|
40
46
|
move(source: string, target: string): Promise<boolean>;
|
|
41
47
|
copy(source: string, target: string): Promise<boolean>;
|
|
42
48
|
}
|
package/dist/client.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,QAAQ,EACR,SAAS,EACT,QAAQ,EACR,WAAW,EACX,WAAW,EACZ,MAAM,2BAA2B,CAAC;
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,QAAQ,EACR,SAAS,EACT,QAAQ,EACR,WAAW,EACX,WAAW,EACZ,MAAM,2BAA2B,CAAC;AAEnC,OAAO,KAAK,EAAE,QAAQ,EAAc,MAAM,YAAY,CAAC;AAEvD,MAAM,WAAW,qBAAqB;IACpC,wFAAwF;IACxF,GAAG,EAAE,QAAQ,CAAC;IACd,8FAA8F;IAC9F,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,4FAA4F;IAC5F,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,qEAAqE;IACrE,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,sCAAsC;IACtC,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAyCD,0FAA0F;AAC1F,qBAAa,cAAe,YAAW,QAAQ;;IAO7C,YAAY,OAAO,EAAE,qBAAqB,EAMzC;IAED;;;OAGG;IACI,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,GAAE,WAAgB,GAAG,aAAa,CAAC,UAAU,CAAC,CA4C9E;IAED;;;OAGG;IACG,KAAK,CACT,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,QAAQ,CAAC,UAAU,CAAC,GAAG,aAAa,CAAC,UAAU,CAAC,GACxD,OAAO,CAAC,IAAI,CAAC,CAEf;IAED;;;OAGG;IACG,MAAM,CACV,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,QAAQ,CAAC,UAAU,CAAC,GAAG,aAAa,CAAC,UAAU,CAAC,GACxD,OAAO,CAAC,IAAI,CAAC,CAEf;IAsCK,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAEvC;IAED,6EAA6E;IACtE,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,GAAE,WAAgB,GAAG,aAAa,CAAC,QAAQ,CAAC,CAY5E;IAED,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,GAAG,SAAS,CAAC,CAElD;IACD,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAErC;IACD,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAErC;IACD;;;;;OAKG;IACG,IAAI,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAM3D;IACD,IAAI,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAErD;CACF;AAED,kCAAkC;AAClC,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,qBAAqB,GAAG,cAAc,CAEhF"}
|
package/dist/esm/index.js
CHANGED
|
@@ -1,3 +1,45 @@
|
|
|
1
|
+
// ../webrun-files/dist/esm/index.js
|
|
2
|
+
/**
|
|
3
|
+
* Thrown by a `move` done as copy-then-delete when the copy succeeded but the source
|
|
4
|
+
* could not be removed, even on a second attempt. The move is half done: the target
|
|
5
|
+
* already holds the moved content (an existing target was replaced), and the source
|
|
6
|
+
* still exists. A `false` would wrongly say that nothing happened, so this is thrown
|
|
7
|
+
* instead.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* try {
|
|
11
|
+
* await files.move("/repo/index.lock", "/repo/index");
|
|
12
|
+
* } catch (error) {
|
|
13
|
+
* if (error instanceof MoveIncompleteError) await files.remove(error.source);
|
|
14
|
+
* else throw error;
|
|
15
|
+
* }
|
|
16
|
+
*/
|
|
17
|
+
var MoveIncompleteError = class extends Error {
|
|
18
|
+
source;
|
|
19
|
+
target;
|
|
20
|
+
name = "MoveIncompleteError";
|
|
21
|
+
constructor(source, target, options) {
|
|
22
|
+
super(`move ${source} -> ${target}: the target was replaced, but the source could not be removed`, options);
|
|
23
|
+
this.source = source;
|
|
24
|
+
this.target = target;
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* Path manipulation utilities for virtual filesystem paths.
|
|
29
|
+
* All paths use forward slashes and start with "/".
|
|
30
|
+
*/
|
|
31
|
+
/**
|
|
32
|
+
* Normalizes a file path to a consistent format.
|
|
33
|
+
* - Adds leading slash if missing
|
|
34
|
+
* - Removes trailing slash
|
|
35
|
+
* - Removes `.` segments
|
|
36
|
+
* - Collapses multiple slashes
|
|
37
|
+
*/
|
|
38
|
+
function normalizePath(filePath) {
|
|
39
|
+
const segments = filePath.split("/").filter((s) => !!s && s !== ".");
|
|
40
|
+
if (segments.length === 0) return "/";
|
|
41
|
+
return `/${segments.join("/")}`;
|
|
42
|
+
}
|
|
1
43
|
// src/client.ts
|
|
2
44
|
const toBytes$1 = (v) => v instanceof Uint8Array ? v : ArrayBuffer.isView(v) ? new Uint8Array(v.buffer, v.byteOffset, v.byteLength) : new Uint8Array(v);
|
|
3
45
|
const newStreamId$1 = () => crypto.randomUUID().replace(/-/g, "");
|
|
@@ -154,8 +196,18 @@ var FilesRpcClient = class {
|
|
|
154
196
|
remove(path) {
|
|
155
197
|
return this.#rpc.remove(path, { requestId: newStreamId$1() });
|
|
156
198
|
}
|
|
157
|
-
|
|
158
|
-
|
|
199
|
+
/**
|
|
200
|
+
* One `move` call: the server's backend does the move, so it is exactly as atomic as
|
|
201
|
+
* that backend's `move` (node, mem, sqlite: atomic; s3 or a cross-mount composite:
|
|
202
|
+
* copy then delete). The client adds no guarantee, and a call that fails in transit
|
|
203
|
+
* leaves it unknown whether the move happened.
|
|
204
|
+
*/
|
|
205
|
+
async move(source, target) {
|
|
206
|
+
try {
|
|
207
|
+
return await this.#rpc.move(source, target, { requestId: newStreamId$1() });
|
|
208
|
+
} catch (error) {
|
|
209
|
+
throw moveIncomplete(error, source, target);
|
|
210
|
+
}
|
|
159
211
|
}
|
|
160
212
|
copy(source, target) {
|
|
161
213
|
return this.#rpc.copy(source, target, { requestId: newStreamId$1() });
|
|
@@ -165,6 +217,16 @@ var FilesRpcClient = class {
|
|
|
165
217
|
function newFilesRpcClient(options) {
|
|
166
218
|
return new FilesRpcClient(options);
|
|
167
219
|
}
|
|
220
|
+
/**
|
|
221
|
+
* A server-side `MoveIncompleteError` arrives as a plain error: over http with its name and
|
|
222
|
+
* message, through a structured clone (rpc transports) with its message only. Recognised by
|
|
223
|
+
* either, it is rebuilt with the client's paths, the original kept as its cause.
|
|
224
|
+
*/
|
|
225
|
+
function moveIncomplete(error, source, target) {
|
|
226
|
+
const { name, message } = error ?? {};
|
|
227
|
+
return name === "MoveIncompleteError" || typeof message === "string" && MOVE_INCOMPLETE_MESSAGE.test(message) ? new MoveIncompleteError(normalizePath(source), normalizePath(target), { cause: error }) : error;
|
|
228
|
+
}
|
|
229
|
+
const MOVE_INCOMPLETE_MESSAGE = /^move .* -> .*: the target was replaced, but the source could not be removed$/s;
|
|
168
230
|
// src/errors.ts
|
|
169
231
|
/**
|
|
170
232
|
* An error of the protocol itself. The code also leads the message (`UNKNOWN_STREAM: …`), since
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@statewalker/webrun-files-rpc",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "FilesApi over any RPC: streamed reads and writes as chunks kept and awaited by (streamId, chunkId)",
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
"src"
|
|
35
35
|
],
|
|
36
36
|
"dependencies": {
|
|
37
|
-
"@statewalker/webrun-files": "^0.
|
|
37
|
+
"@statewalker/webrun-files": "^0.11.0"
|
|
38
38
|
},
|
|
39
39
|
"devDependencies": {
|
|
40
40
|
"@types/node": "^26.6.4",
|
|
@@ -42,8 +42,8 @@
|
|
|
42
42
|
"rolldown": "^1.2.12",
|
|
43
43
|
"typescript": "^7.0.2",
|
|
44
44
|
"vitest": "^4.1.4 || ^5.0.3",
|
|
45
|
-
"@statewalker/webrun-files-
|
|
46
|
-
"@statewalker/webrun-files-
|
|
45
|
+
"@statewalker/webrun-files-mem": "^0.11.0",
|
|
46
|
+
"@statewalker/webrun-files-tests": "^3.0.0"
|
|
47
47
|
},
|
|
48
48
|
"sideEffects": false,
|
|
49
49
|
"publishConfig": {
|
package/src/client.ts
CHANGED
|
@@ -5,6 +5,7 @@ import type {
|
|
|
5
5
|
ListOptions,
|
|
6
6
|
ReadOptions,
|
|
7
7
|
} from "@statewalker/webrun-files";
|
|
8
|
+
import { MoveIncompleteError, normalizePath } from "@statewalker/webrun-files";
|
|
8
9
|
import type { FilesRpc, ReadResult } from "./types.js";
|
|
9
10
|
|
|
10
11
|
export interface FilesRpcClientOptions {
|
|
@@ -211,8 +212,18 @@ export class FilesRpcClient implements FilesApi {
|
|
|
211
212
|
remove(path: string): Promise<boolean> {
|
|
212
213
|
return this.#rpc.remove(path, { requestId: newStreamId() });
|
|
213
214
|
}
|
|
214
|
-
|
|
215
|
-
|
|
215
|
+
/**
|
|
216
|
+
* One `move` call: the server's backend does the move, so it is exactly as atomic as
|
|
217
|
+
* that backend's `move` (node, mem, sqlite: atomic; s3 or a cross-mount composite:
|
|
218
|
+
* copy then delete). The client adds no guarantee, and a call that fails in transit
|
|
219
|
+
* leaves it unknown whether the move happened.
|
|
220
|
+
*/
|
|
221
|
+
async move(source: string, target: string): Promise<boolean> {
|
|
222
|
+
try {
|
|
223
|
+
return await this.#rpc.move(source, target, { requestId: newStreamId() });
|
|
224
|
+
} catch (error) {
|
|
225
|
+
throw moveIncomplete(error, source, target);
|
|
226
|
+
}
|
|
216
227
|
}
|
|
217
228
|
copy(source: string, target: string): Promise<boolean> {
|
|
218
229
|
return this.#rpc.copy(source, target, { requestId: newStreamId() });
|
|
@@ -223,3 +234,21 @@ export class FilesRpcClient implements FilesApi {
|
|
|
223
234
|
export function newFilesRpcClient(options: FilesRpcClientOptions): FilesRpcClient {
|
|
224
235
|
return new FilesRpcClient(options);
|
|
225
236
|
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* A server-side `MoveIncompleteError` arrives as a plain error: over http with its name and
|
|
240
|
+
* message, through a structured clone (rpc transports) with its message only. Recognised by
|
|
241
|
+
* either, it is rebuilt with the client's paths, the original kept as its cause.
|
|
242
|
+
*/
|
|
243
|
+
function moveIncomplete(error: unknown, source: string, target: string): unknown {
|
|
244
|
+
const { name, message } = (error ?? {}) as { name?: unknown; message?: unknown };
|
|
245
|
+
const recognised =
|
|
246
|
+
name === "MoveIncompleteError" ||
|
|
247
|
+
(typeof message === "string" && MOVE_INCOMPLETE_MESSAGE.test(message));
|
|
248
|
+
return recognised
|
|
249
|
+
? new MoveIncompleteError(normalizePath(source), normalizePath(target), { cause: error })
|
|
250
|
+
: error;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const MOVE_INCOMPLETE_MESSAGE =
|
|
254
|
+
/^move .* -> .*: the target was replaced, but the source could not be removed$/s;
|