@narumitw/pi-sync 0.18.0 → 0.24.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/package.json +3 -3
- package/src/s3-client.ts +45 -8
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@narumitw/pi-sync",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.24.0",
|
|
4
4
|
"description": "Pi extension that syncs Pi configuration through Cloudflare R2 or S3-compatible storage.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -30,8 +30,8 @@
|
|
|
30
30
|
},
|
|
31
31
|
"devDependencies": {
|
|
32
32
|
"@biomejs/biome": "2.5.3",
|
|
33
|
-
"@earendil-works/pi-coding-agent": "0.80.
|
|
34
|
-
"typescript": "
|
|
33
|
+
"@earendil-works/pi-coding-agent": "0.80.10",
|
|
34
|
+
"typescript": "7.0.2"
|
|
35
35
|
},
|
|
36
36
|
"repository": {
|
|
37
37
|
"type": "git",
|
package/src/s3-client.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createHash, createHmac } from "node:crypto";
|
|
2
|
+
import { setTimeout as sleep } from "node:timers/promises";
|
|
2
3
|
import { encodeKey, posixJoin } from "./paths.js";
|
|
3
4
|
import type { LatestPointer, RemoteObject, Snapshot, SyncConfig } from "./types.js";
|
|
4
5
|
|
|
@@ -34,17 +35,53 @@ export class S3Client {
|
|
|
34
35
|
}
|
|
35
36
|
|
|
36
37
|
async getJson<T>(key: string): Promise<RemoteObject<T>> {
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
38
|
+
const maxAttempts = 3;
|
|
39
|
+
let lastError: unknown;
|
|
40
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
41
|
+
const object = await this.request("GET", key);
|
|
42
|
+
if (object.status === 404) return { missing: true };
|
|
43
|
+
if (!object.ok) throw new Error(`S3 GET failed (${object.status}): ${await object.text()}`);
|
|
44
|
+
const body = await object.text();
|
|
45
|
+
// R2 can intermittently return an empty 200 body (read-after-write
|
|
46
|
+
// inconsistency on a long-lived keep-alive connection), which makes
|
|
47
|
+
// response.json() throw "JSON Parse error: Unexpected EOF" under Bun.
|
|
48
|
+
// Retry so the transient blip is absorbed instead of surfacing as a
|
|
49
|
+
// "pi-sync auto sync skipped" warning on every session start.
|
|
50
|
+
if (body.length > 0) {
|
|
51
|
+
return { value: JSON.parse(body) as T, etag: normalizeEtag(object.headers.get("etag")), missing: false };
|
|
52
|
+
}
|
|
53
|
+
lastError = new Error(`S3 GET returned an empty body for ${key}`);
|
|
54
|
+
if (attempt < maxAttempts) {
|
|
55
|
+
await sleep(250 * attempt);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
throw lastError;
|
|
41
59
|
}
|
|
42
60
|
|
|
43
61
|
async getBuffer(key: string): Promise<RemoteObject<Buffer>> {
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
62
|
+
const maxAttempts = 3;
|
|
63
|
+
let lastError: unknown;
|
|
64
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
65
|
+
const object = await this.request("GET", key);
|
|
66
|
+
if (object.status === 404) return { missing: true };
|
|
67
|
+
if (!object.ok) throw new Error(`S3 GET failed (${object.status}): ${await object.text()}`);
|
|
68
|
+
const buffer = Buffer.from(await object.arrayBuffer());
|
|
69
|
+
// R2 can intermittently return an empty 200 body on a long-lived
|
|
70
|
+
// keep-alive connection (same root cause as the getJson retry
|
|
71
|
+
// above). getBuffer is only used for snapshot .json.gz payloads,
|
|
72
|
+
// which are always non-empty, so an empty body is a transient
|
|
73
|
+
// blip, not a legitimate response. Retry so the checksum guard
|
|
74
|
+
// downstream doesn't surface it as "Remote snapshot checksum
|
|
75
|
+
// mismatch".
|
|
76
|
+
if (buffer.length > 0) {
|
|
77
|
+
return { value: buffer, etag: normalizeEtag(object.headers.get("etag")), missing: false };
|
|
78
|
+
}
|
|
79
|
+
lastError = new Error(`S3 GET returned an empty body for ${key}`);
|
|
80
|
+
if (attempt < maxAttempts) {
|
|
81
|
+
await sleep(250 * attempt);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
throw lastError;
|
|
48
85
|
}
|
|
49
86
|
|
|
50
87
|
async putJson(key: string, value: unknown) {
|