sh3-server 0.11.4 → 0.11.7
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/app/assets/{icons-CnAqUqbR.svg → icons-2eVU8BA9.svg} +5 -0
- package/app/assets/index-BXP4ACTa.css +1 -0
- package/app/assets/index-D9SiE54u.js +18 -0
- package/app/assets/index-D9SiE54u.js.map +1 -0
- package/app/index.html +2 -2
- package/dist/doc-store/store.d.ts +1 -0
- package/dist/doc-store/store.js +46 -2
- package/dist/routes/docs.js +37 -12
- package/package.json +1 -1
- package/app/assets/index-ApG6yN5d.js +0 -18
- package/app/assets/index-ApG6yN5d.js.map +0 -1
- package/app/assets/index-B3RaonGk.css +0 -1
package/app/index.html
CHANGED
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
6
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
|
7
7
|
<title>SH3</title>
|
|
8
|
-
<script type="module" crossorigin src="/assets/index-
|
|
9
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
8
|
+
<script type="module" crossorigin src="/assets/index-D9SiE54u.js"></script>
|
|
9
|
+
<link rel="stylesheet" crossorigin href="/assets/index-BXP4ACTa.css">
|
|
10
10
|
</head>
|
|
11
11
|
<body>
|
|
12
12
|
<div id="app"></div>
|
|
@@ -64,6 +64,7 @@ export declare class TenantDocStore {
|
|
|
64
64
|
syncState: 'synced' | 'pending';
|
|
65
65
|
}>;
|
|
66
66
|
delete(tenant: string, shardId: string, path: string): Promise<void>;
|
|
67
|
+
rename(tenant: string, shardId: string, oldPath: string, newPath: string): Promise<void>;
|
|
67
68
|
applyFromPeer(tenant: string, input: ApplyFromPeerInput): Promise<ApplyResult>;
|
|
68
69
|
listConflicts(tenant: string): Promise<ConflictRef[]>;
|
|
69
70
|
readConflict(tenant: string, shardId: string, path: string): Promise<ConflictFile | null>;
|
package/dist/doc-store/store.js
CHANGED
|
@@ -6,12 +6,12 @@
|
|
|
6
6
|
* conflict bucket on Mode B mismatch. Consumed by the docs HTTP router
|
|
7
7
|
* and by the ServerShardContext.documents(tenant) API.
|
|
8
8
|
*/
|
|
9
|
-
import { readFile, writeFile, mkdir, rm, readdir, stat } from 'node:fs/promises';
|
|
9
|
+
import { readFile, writeFile, mkdir, rm, readdir, stat, rename as fsRename } from 'node:fs/promises';
|
|
10
10
|
import { readdirSync, existsSync } from 'node:fs';
|
|
11
11
|
import { dirname, join, relative } from 'node:path';
|
|
12
12
|
import { resolveSyncMode } from './policy.js';
|
|
13
13
|
import { filterReservedMeta } from './reserved.js';
|
|
14
|
-
import { readMeta, writeMeta } from './meta.js';
|
|
14
|
+
import { readMeta, writeMeta, deleteMeta } from './meta.js';
|
|
15
15
|
export class TenantDocStore {
|
|
16
16
|
#dataDir;
|
|
17
17
|
#policy;
|
|
@@ -225,6 +225,50 @@ export class TenantDocStore {
|
|
|
225
225
|
if (role === 'primary')
|
|
226
226
|
await this.#tick.bump(tenant);
|
|
227
227
|
}
|
|
228
|
+
// ---------- Mode A rename ----------
|
|
229
|
+
async rename(tenant, shardId, oldPath, newPath) {
|
|
230
|
+
const oldCp = this.#contentPath(tenant, shardId, oldPath);
|
|
231
|
+
const newCp = this.#contentPath(tenant, shardId, newPath);
|
|
232
|
+
if (!(await this.exists(tenant, shardId, oldPath))) {
|
|
233
|
+
throw new Error(`Document not found at ${oldPath}`);
|
|
234
|
+
}
|
|
235
|
+
if (await this.exists(tenant, shardId, newPath)) {
|
|
236
|
+
throw new Error(`Document already exists at ${newPath}`);
|
|
237
|
+
}
|
|
238
|
+
// Atomic content move (single fs.rename when on same volume).
|
|
239
|
+
await mkdir(dirname(newCp), { recursive: true });
|
|
240
|
+
await fsRename(oldCp, newCp);
|
|
241
|
+
// Migrate metadata: read old sidecar, bump version, write at new path,
|
|
242
|
+
// delete old sidecar.
|
|
243
|
+
const role = this.roles.get(tenant);
|
|
244
|
+
const prev = (await readMeta(this.#dataDir, tenant, shardId, oldPath)) ?? {};
|
|
245
|
+
const prevKnown = typeof prev?.lastKnownVersion === 'number' ? prev.lastKnownVersion : 0;
|
|
246
|
+
let version;
|
|
247
|
+
let lastKnownVersion;
|
|
248
|
+
let syncState;
|
|
249
|
+
if (role === 'primary') {
|
|
250
|
+
version = prevKnown + 1;
|
|
251
|
+
lastKnownVersion = version;
|
|
252
|
+
syncState = 'synced';
|
|
253
|
+
}
|
|
254
|
+
else {
|
|
255
|
+
version = prevKnown;
|
|
256
|
+
lastKnownVersion = prevKnown;
|
|
257
|
+
syncState = 'pending';
|
|
258
|
+
}
|
|
259
|
+
const policy = await this.#policy.get(tenant);
|
|
260
|
+
const syncMode = resolveSyncMode(policy, newPath);
|
|
261
|
+
await writeMeta(this.#dataDir, tenant, shardId, newPath, {
|
|
262
|
+
...prev,
|
|
263
|
+
version,
|
|
264
|
+
lastKnownVersion,
|
|
265
|
+
syncMode,
|
|
266
|
+
syncState,
|
|
267
|
+
});
|
|
268
|
+
await deleteMeta(this.#dataDir, tenant, shardId, oldPath);
|
|
269
|
+
if (role === 'primary')
|
|
270
|
+
await this.#tick.bump(tenant);
|
|
271
|
+
}
|
|
228
272
|
// ---------- Mode B — applyFromPeer ----------
|
|
229
273
|
async applyFromPeer(tenant, input) {
|
|
230
274
|
const { shardId, path, content, incomingVersion, expectedLocalVersion, origin } = input;
|
package/dist/routes/docs.js
CHANGED
|
@@ -105,25 +105,50 @@ export function createDocsRouter(store, settings) {
|
|
|
105
105
|
status: (await store.exists(tenant, shard, filePath)) ? 200 : 404,
|
|
106
106
|
});
|
|
107
107
|
});
|
|
108
|
-
// Conflict resolve — must match before the generic PUT so the
|
|
109
|
-
//
|
|
108
|
+
// Conflict resolve and rename — must match before the generic PUT so the
|
|
109
|
+
// suffixes aren't captured as part of the file path.
|
|
110
110
|
router.post('/:tenant/:shard/*', async (c) => {
|
|
111
111
|
const { tenant, shard } = c.req.param();
|
|
112
112
|
if (isReservedShardId(shard))
|
|
113
113
|
return c.json({ error: 'Reserved shard id' }, 400);
|
|
114
114
|
const rawPath = c.req.path.replace(`/api/docs/${tenant}/${shard}/`, '');
|
|
115
|
-
if (
|
|
116
|
-
|
|
115
|
+
if (rawPath.endsWith('/resolve')) {
|
|
116
|
+
const filePath = rawPath.replace(/\/resolve$/, '');
|
|
117
|
+
if (!filePath)
|
|
118
|
+
return c.json({ error: 'Missing file path' }, 400);
|
|
119
|
+
const body = await c.req.json().catch(() => null);
|
|
120
|
+
if (!body || typeof body.choice === 'undefined') {
|
|
121
|
+
return c.json({ error: 'Body must include { choice }' }, 400);
|
|
122
|
+
}
|
|
123
|
+
await store.resolveConflict(tenant, shard, filePath, body.choice);
|
|
124
|
+
return c.json({ ok: true });
|
|
117
125
|
}
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
126
|
+
if (rawPath.endsWith('/rename')) {
|
|
127
|
+
const oldPath = rawPath.replace(/\/rename$/, '');
|
|
128
|
+
if (!oldPath)
|
|
129
|
+
return c.json({ error: 'Missing file path' }, 400);
|
|
130
|
+
const body = await c.req.json().catch(() => null);
|
|
131
|
+
if (!body || typeof body.to !== 'string' || body.to.length === 0) {
|
|
132
|
+
return c.json({ error: 'Body must include { to: string }' }, 400);
|
|
133
|
+
}
|
|
134
|
+
if (body.to === oldPath) {
|
|
135
|
+
return c.json({ error: 'Rename target must differ from source' }, 400);
|
|
136
|
+
}
|
|
137
|
+
try {
|
|
138
|
+
await store.rename(tenant, shard, oldPath, body.to);
|
|
139
|
+
}
|
|
140
|
+
catch (err) {
|
|
141
|
+
const msg = String(err?.message ?? err);
|
|
142
|
+
if (/not found/i.test(msg))
|
|
143
|
+
return c.json({ error: msg }, 404);
|
|
144
|
+
if (/already exists/i.test(msg))
|
|
145
|
+
return c.json({ error: msg }, 409);
|
|
146
|
+
throw err;
|
|
147
|
+
}
|
|
148
|
+
const meta = await store.readMeta(tenant, shard, body.to);
|
|
149
|
+
return c.json({ ok: true, version: meta?.version });
|
|
124
150
|
}
|
|
125
|
-
|
|
126
|
-
return c.json({ ok: true });
|
|
151
|
+
return c.json({ error: 'Unknown docs POST endpoint' }, 404);
|
|
127
152
|
});
|
|
128
153
|
// Write
|
|
129
154
|
router.put('/:tenant/:shard/*', async (c) => {
|