claude
This commit is contained in:
parent
39c5591d36
commit
35877b69da
46 changed files with 3149 additions and 1851 deletions
|
|
@ -0,0 +1,9 @@
|
|||
export class FileAlreadyExistsError extends Error {
|
||||
public constructor(
|
||||
message: string,
|
||||
public readonly filePath: string
|
||||
) {
|
||||
super(message);
|
||||
this.name = "FileAlreadyExistsError";
|
||||
}
|
||||
}
|
||||
|
|
@ -1,15 +1,14 @@
|
|||
import { describe, it } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import type { RelativePath } from "../sync-operations/types";
|
||||
import { FileOperations, MoveOnConflict } from "./file-operations";
|
||||
import { FileOperations } from "./file-operations";
|
||||
import { Logger } from "../tracing/logger";
|
||||
import { assertSetContainsExactly } from "../utils/assert-set-contains-exactly";
|
||||
import type { FileSystemOperations } from "./filesystem-operations";
|
||||
import type { TextWithCursors } from "reconcile-text";
|
||||
import type { ServerConfig, ServerConfigData } from "../services/server-config";
|
||||
import { CONFLICT_PATH_REGEX } from "../sync-operations/conflict-path";
|
||||
import { removeFromArray } from "../utils/remove-from-array";
|
||||
import { ExpectedFsEvents } from "../sync-operations/expected-fs-events";
|
||||
import { FileAlreadyExistsError } from "../errors/file-already-exists-error";
|
||||
|
||||
class MockServerConfig implements Pick<ServerConfig, "getConfig"> {
|
||||
public async getConfig(): Promise<ServerConfigData> {
|
||||
|
|
@ -79,173 +78,78 @@ function makeOps(): {
|
|||
return { fs, ops };
|
||||
}
|
||||
|
||||
function singleConflictPath(
|
||||
names: Set<string>,
|
||||
expectedNonConflictNames: string[]
|
||||
): string {
|
||||
const expected = new Set(expectedNonConflictNames);
|
||||
const conflicts = Array.from(names).filter((name) => !expected.has(name));
|
||||
assert.equal(
|
||||
conflicts.length,
|
||||
1,
|
||||
`expected exactly one conflict-path entry, got ${JSON.stringify(conflicts)}`
|
||||
);
|
||||
assert.ok(
|
||||
CONFLICT_PATH_REGEX.test(conflicts[0]),
|
||||
`expected ${conflicts[0]} to match the conflict-path pattern`
|
||||
);
|
||||
return conflicts[0];
|
||||
}
|
||||
|
||||
describe("File operations", () => {
|
||||
it("move to empty target just renames the file", async () => {
|
||||
it("create writes the file at the requested path", async () => {
|
||||
const { fs, ops } = makeOps();
|
||||
|
||||
await ops.create("a", new Uint8Array(), MoveOnConflict.EXISTING);
|
||||
const result = await ops.create("a", new Uint8Array());
|
||||
|
||||
assertSetContainsExactly(fs.names, "a");
|
||||
assert.equal(result.actualPath, "a");
|
||||
});
|
||||
|
||||
it("create throws FileAlreadyExistsError when the path is occupied", async () => {
|
||||
const { fs, ops } = makeOps();
|
||||
|
||||
await ops.create("note.md", new Uint8Array());
|
||||
await assert.rejects(
|
||||
ops.create("note.md", new Uint8Array()),
|
||||
FileAlreadyExistsError
|
||||
);
|
||||
|
||||
// The original file is left intact and no other entries appeared.
|
||||
assertSetContainsExactly(fs.names, "note.md");
|
||||
});
|
||||
|
||||
it("move to an empty target just renames the file", async () => {
|
||||
const { fs, ops } = makeOps();
|
||||
|
||||
await ops.create("a", new Uint8Array());
|
||||
assertSetContainsExactly(fs.names, "a");
|
||||
|
||||
await ops.move("a", "b", MoveOnConflict.EXISTING);
|
||||
const result = await ops.move("a", "b");
|
||||
assertSetContainsExactly(fs.names, "b");
|
||||
assert.equal(result.actualPath, "b");
|
||||
});
|
||||
|
||||
it("create with EXISTING displaces the existing file to a conflict path", async () => {
|
||||
it("move with same source and target is a no-op", async () => {
|
||||
const { fs, ops } = makeOps();
|
||||
|
||||
await ops.create("note.md", new Uint8Array(), MoveOnConflict.EXISTING);
|
||||
await ops.create("note.md", new Uint8Array(), MoveOnConflict.EXISTING);
|
||||
await ops.create("a", new Uint8Array());
|
||||
const result = await ops.move("a", "a");
|
||||
|
||||
// The original `note.md` location now holds the new file; the previous
|
||||
// contents were displaced to a conflict path.
|
||||
const conflict = singleConflictPath(fs.names, ["note.md"]);
|
||||
assert.ok(
|
||||
conflict.endsWith("-note.md"),
|
||||
`conflict name should preserve the original filename, got ${conflict}`
|
||||
);
|
||||
assertSetContainsExactly(fs.names, "a");
|
||||
assert.equal(result.actualPath, "a");
|
||||
});
|
||||
|
||||
it("create with NEW redirects the new file to a conflict path", async () => {
|
||||
it("move throws FileAlreadyExistsError when the target is occupied", async () => {
|
||||
const { fs, ops } = makeOps();
|
||||
|
||||
await ops.create("note.md", new Uint8Array(), MoveOnConflict.EXISTING);
|
||||
await ops.create("note.md", new Uint8Array(), MoveOnConflict.NEW);
|
||||
await ops.create("source.md", new Uint8Array());
|
||||
await ops.create("dest.md", new Uint8Array());
|
||||
|
||||
// The original `note.md` is untouched; the new file went to a conflict path.
|
||||
const conflict = singleConflictPath(fs.names, ["note.md"]);
|
||||
assert.ok(
|
||||
conflict.endsWith("-note.md"),
|
||||
`conflict name should preserve the original filename, got ${conflict}`
|
||||
await assert.rejects(
|
||||
ops.move("source.md", "dest.md"),
|
||||
FileAlreadyExistsError
|
||||
);
|
||||
|
||||
// Both files are left intact — no displacement happens.
|
||||
assertSetContainsExactly(fs.names, "source.md", "dest.md");
|
||||
});
|
||||
|
||||
it("move with EXISTING displaces the target to a conflict path", async () => {
|
||||
it("create works for nested paths (parent-directory creation)", async () => {
|
||||
const { fs, ops } = makeOps();
|
||||
|
||||
await ops.create(
|
||||
"source.md",
|
||||
new Uint8Array(),
|
||||
MoveOnConflict.EXISTING
|
||||
);
|
||||
await ops.create("dest.md", new Uint8Array(), MoveOnConflict.EXISTING);
|
||||
|
||||
await ops.move("source.md", "dest.md", MoveOnConflict.EXISTING);
|
||||
|
||||
// `dest.md` now holds what used to be at `source.md`; the original
|
||||
// `dest.md` moved to a conflict path in the same directory.
|
||||
const conflict = singleConflictPath(fs.names, ["dest.md"]);
|
||||
assert.ok(
|
||||
conflict.endsWith("-dest.md"),
|
||||
`conflict should preserve the original filename, got ${conflict}`
|
||||
);
|
||||
await ops.create("a/b.c/d", new Uint8Array());
|
||||
assertSetContainsExactly(fs.names, "a/b.c/d");
|
||||
});
|
||||
|
||||
it("move with NEW redirects the moved file to a conflict path", async () => {
|
||||
it("move works for nested target paths (parent-directory creation)", async () => {
|
||||
const { fs, ops } = makeOps();
|
||||
|
||||
await ops.create(
|
||||
"source.md",
|
||||
new Uint8Array(),
|
||||
MoveOnConflict.EXISTING
|
||||
);
|
||||
await ops.create("dest.md", new Uint8Array(), MoveOnConflict.EXISTING);
|
||||
await ops.create("source", new Uint8Array());
|
||||
await ops.move("source", "a/b.c/dest");
|
||||
|
||||
await ops.move("source.md", "dest.md", MoveOnConflict.NEW);
|
||||
|
||||
// The original `dest.md` is untouched; the moved file went to a conflict path.
|
||||
const conflict = singleConflictPath(fs.names, ["dest.md"]);
|
||||
assert.ok(
|
||||
conflict.endsWith("-dest.md"),
|
||||
`conflict should preserve the original filename, got ${conflict}`
|
||||
);
|
||||
});
|
||||
|
||||
it("preserves the parent directory when generating a conflict path", async () => {
|
||||
const { fs, ops } = makeOps();
|
||||
|
||||
await ops.create("a/b.c/d", new Uint8Array(), MoveOnConflict.EXISTING);
|
||||
await ops.create("a/b.c/e", new Uint8Array(), MoveOnConflict.EXISTING);
|
||||
await ops.move("a/b.c/d", "a/b.c/e", MoveOnConflict.EXISTING);
|
||||
|
||||
const conflict = singleConflictPath(fs.names, ["a/b.c/e"]);
|
||||
assert.ok(
|
||||
conflict.startsWith("a/b.c/"),
|
||||
`conflict should live in the same directory, got ${conflict}`
|
||||
);
|
||||
assert.ok(
|
||||
conflict.endsWith("-e"),
|
||||
`conflict should preserve the filename, got ${conflict}`
|
||||
);
|
||||
});
|
||||
|
||||
it("handles dotfiles without mangling the extension", async () => {
|
||||
const { fs, ops } = makeOps();
|
||||
|
||||
await ops.create(
|
||||
".gitignore",
|
||||
new Uint8Array(),
|
||||
MoveOnConflict.EXISTING
|
||||
);
|
||||
await ops.create("temp", new Uint8Array(), MoveOnConflict.EXISTING);
|
||||
await ops.move("temp", ".gitignore", MoveOnConflict.EXISTING);
|
||||
|
||||
const conflict = singleConflictPath(fs.names, [".gitignore"]);
|
||||
assert.ok(
|
||||
conflict.endsWith("-.gitignore"),
|
||||
`conflict should preserve the dotfile name verbatim, got ${conflict}`
|
||||
);
|
||||
|
||||
await ops.create(
|
||||
".config.json",
|
||||
new Uint8Array(),
|
||||
MoveOnConflict.EXISTING
|
||||
);
|
||||
await ops.create("temp2", new Uint8Array(), MoveOnConflict.EXISTING);
|
||||
await ops.move("temp2", ".config.json", MoveOnConflict.EXISTING);
|
||||
|
||||
// Now one conflict for .gitignore, one for .config.json.
|
||||
const conflicts = Array.from(fs.names).filter(
|
||||
(name) => name !== ".gitignore" && name !== ".config.json"
|
||||
);
|
||||
assert.equal(conflicts.length, 2);
|
||||
assert.ok(conflicts.every((c) => CONFLICT_PATH_REGEX.test(c)));
|
||||
assert.ok(conflicts.some((c) => c.endsWith("-.gitignore")));
|
||||
assert.ok(conflicts.some((c) => c.endsWith("-.config.json")));
|
||||
});
|
||||
|
||||
it("generates a fresh conflict path on every displacement", async () => {
|
||||
const { fs, ops } = makeOps();
|
||||
|
||||
await ops.create("x", new Uint8Array(), MoveOnConflict.EXISTING);
|
||||
await ops.create("x", new Uint8Array(), MoveOnConflict.EXISTING);
|
||||
await ops.create("x", new Uint8Array(), MoveOnConflict.EXISTING);
|
||||
|
||||
const conflicts = Array.from(fs.names);
|
||||
removeFromArray(conflicts, "x");
|
||||
assert.equal(conflicts.length, 2);
|
||||
assert.ok(conflicts.every((c) => CONFLICT_PATH_REGEX.test(c)));
|
||||
assert.notEqual(
|
||||
conflicts[0],
|
||||
conflicts[1],
|
||||
"each displacement should produce a unique conflict path"
|
||||
);
|
||||
assertSetContainsExactly(fs.names, "a/b.c/dest");
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -6,28 +6,19 @@ import type { TextWithCursors } from "reconcile-text";
|
|||
import { reconcile } from "reconcile-text";
|
||||
import { isFileTypeMergable } from "../utils/is-file-type-mergable";
|
||||
import { isBinary } from "../utils/is-binary";
|
||||
import { buildConflictFileName } from "../sync-operations/conflict-path";
|
||||
import type { ServerConfig } from "../services/server-config";
|
||||
import { FileNotFoundError } from "../errors/file-not-found-error";
|
||||
import { FileAlreadyExistsError } from "../errors/file-already-exists-error";
|
||||
import type { ExpectedFsEvents } from "../sync-operations/expected-fs-events";
|
||||
|
||||
export enum MoveOnConflict {
|
||||
EXISTING = "EXISTING",
|
||||
NEW = "NEW"
|
||||
}
|
||||
|
||||
/**
|
||||
* Outcome of a `move`/`create`. `actualPath` is where the new file
|
||||
* ended up (which may differ from the requested path under
|
||||
* `MoveOnConflict.NEW` if the target was occupied). `displacedTo` is
|
||||
* set only when an existing file at the requested path was bumped to
|
||||
* a `conflict-…` path under `MoveOnConflict.EXISTING`; the caller
|
||||
* uses it to repoint any tracking for the displaced doc before its
|
||||
* own follow-up `setDocument` clobbers the old slot.
|
||||
* Outcome of a `move`/`create`. `actualPath` is where the file ended up;
|
||||
* with the conflict-path machinery removed it is always equal to the
|
||||
* requested path. The shape is preserved so callers don't all need to
|
||||
* change.
|
||||
*/
|
||||
export interface FileOpResult {
|
||||
actualPath: RelativePath;
|
||||
displacedTo?: RelativePath;
|
||||
}
|
||||
|
||||
export class FileOperations {
|
||||
|
|
@ -55,17 +46,6 @@ export class FileOperations {
|
|||
return [pathParts.join("/"), fileName];
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a local-only conflict path for a file the client has to set aside.
|
||||
* Format: `<dir>/conflict-<uuid>-<originalName>` — UUID makes collisions
|
||||
* statistically impossible, so no disk probe / lock dance is needed.
|
||||
*/
|
||||
private static buildConflictPath(path: RelativePath): RelativePath {
|
||||
const [directory, fileName] = FileOperations.getParentDirAndFileName(path);
|
||||
const conflictName = buildConflictFileName(fileName);
|
||||
return directory ? `${directory}/${conflictName}` : conflictName;
|
||||
}
|
||||
|
||||
public async listFilesRecursively(
|
||||
root: RelativePath | undefined = undefined
|
||||
): Promise<RelativePath[]> {
|
||||
|
|
@ -79,29 +59,32 @@ export class FileOperations {
|
|||
/**
|
||||
* Create a file at the specified path.
|
||||
*
|
||||
* If a file with the same name already exists, it is moved before creating the new one.
|
||||
* Parent directories are created if necessary.
|
||||
* Throws `FileAlreadyExistsError` if a file already lives at `path`.
|
||||
* Parent directories are created if necessary. The reconciler is the
|
||||
* only caller that places files now and pre-checks for conflicts;
|
||||
* the throw guards against a TOCTOU race rather than being a normal
|
||||
* code path.
|
||||
*/
|
||||
public async create(
|
||||
path: RelativePath,
|
||||
newContent: Uint8Array,
|
||||
moveOnConflict: MoveOnConflict
|
||||
newContent: Uint8Array
|
||||
): Promise<FileOpResult> {
|
||||
const result = await this.ensureClearPath(path, moveOnConflict);
|
||||
// ensureClearPath leaves actualPath empty: either the file never
|
||||
// existed, or it was just renamed away. The upcoming write therefore
|
||||
// looks like a fresh create to the watcher.
|
||||
this.expectedFsEvents.expectCreate(result.actualPath);
|
||||
try {
|
||||
await this.fs.write(
|
||||
result.actualPath,
|
||||
this.toNativeLineEndings(newContent)
|
||||
if (await this.fs.exists(path)) {
|
||||
throw new FileAlreadyExistsError(
|
||||
`Refusing to create '${path}': file already exists`,
|
||||
path
|
||||
);
|
||||
}
|
||||
await this.createParentDirectories(path);
|
||||
|
||||
this.expectedFsEvents.expectCreate(path);
|
||||
try {
|
||||
await this.fs.write(path, this.toNativeLineEndings(newContent));
|
||||
} catch (e) {
|
||||
this.expectedFsEvents.unexpectCreate(result.actualPath);
|
||||
this.expectedFsEvents.unexpectCreate(path);
|
||||
throw e;
|
||||
}
|
||||
return result;
|
||||
return { actualPath: path };
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -132,7 +115,8 @@ export class FileOperations {
|
|||
if (
|
||||
!isFileTypeMergable(
|
||||
path,
|
||||
(await this.serverConfig.getConfig()).mergeableFileExtensions
|
||||
(await this.serverConfig.getConfig())
|
||||
.mergeableFileExtensions
|
||||
) ||
|
||||
isBinary(expectedContent) ||
|
||||
isBinary(newContent)
|
||||
|
|
@ -225,64 +209,39 @@ export class FileOperations {
|
|||
return this.fs.exists(path);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Move the file at `oldPath` to `newPath`.
|
||||
*
|
||||
* Throws `FileAlreadyExistsError` if a file already lives at `newPath`
|
||||
* (and `oldPath !== newPath`). The reconciler is the only caller that
|
||||
* relocates tracked records and pre-checks for conflicts; the throw
|
||||
* guards against a TOCTOU race.
|
||||
*/
|
||||
public async move(
|
||||
oldPath: RelativePath,
|
||||
newPath: RelativePath,
|
||||
moveOnConflict: MoveOnConflict
|
||||
newPath: RelativePath
|
||||
): Promise<FileOpResult> {
|
||||
if (oldPath === newPath) {
|
||||
return { actualPath: oldPath };
|
||||
}
|
||||
|
||||
const cleared = await this.ensureClearPath(newPath, moveOnConflict);
|
||||
this.expectedFsEvents.expectRename(oldPath, cleared.actualPath);
|
||||
if (await this.fs.exists(newPath)) {
|
||||
throw new FileAlreadyExistsError(
|
||||
`Refusing to move '${oldPath}' onto '${newPath}': target already exists`,
|
||||
newPath
|
||||
);
|
||||
}
|
||||
await this.createParentDirectories(newPath);
|
||||
|
||||
this.expectedFsEvents.expectRename(oldPath, newPath);
|
||||
try {
|
||||
await this.fs.rename(oldPath, cleared.actualPath);
|
||||
await this.fs.rename(oldPath, newPath);
|
||||
} catch (e) {
|
||||
this.expectedFsEvents.unexpectRename(oldPath, cleared.actualPath);
|
||||
this.expectedFsEvents.unexpectRename(oldPath, newPath);
|
||||
throw e;
|
||||
}
|
||||
await this.deletingEmptyParentDirectoriesOfDeletedFile(oldPath);
|
||||
return cleared;
|
||||
}
|
||||
|
||||
private async ensureClearPath(
|
||||
path: RelativePath,
|
||||
moveOnConflict: MoveOnConflict
|
||||
): Promise<FileOpResult> {
|
||||
if (await this.fs.exists(path)) {
|
||||
const conflictPath = FileOperations.buildConflictPath(path);
|
||||
|
||||
if (moveOnConflict === MoveOnConflict.NEW) {
|
||||
return { actualPath: conflictPath };
|
||||
}
|
||||
|
||||
this.logger.debug(
|
||||
`Displacing existing file at ${path} to '${conflictPath}' to make room`
|
||||
);
|
||||
|
||||
// The displaced file's rename will fire as a watcher event;
|
||||
// register `expectRename` so the watcher dedups it. The
|
||||
// caller is responsible for the queue bookkeeping (relocating
|
||||
// the displaced doc's tracking) using the `displacedTo` we
|
||||
// return.
|
||||
this.expectedFsEvents.expectRename(path, conflictPath);
|
||||
try {
|
||||
await this.fs.rename(path, conflictPath);
|
||||
} catch (e) {
|
||||
this.expectedFsEvents.unexpectRename(path, conflictPath);
|
||||
throw e;
|
||||
}
|
||||
return { actualPath: path, displacedTo: conflictPath };
|
||||
}
|
||||
|
||||
this.logger.debug(
|
||||
`No existing file at ${path}, creating parent directories if needed`
|
||||
);
|
||||
await this.createParentDirectories(path);
|
||||
return { actualPath: path };
|
||||
return { actualPath: newPath };
|
||||
}
|
||||
|
||||
private async deletingEmptyParentDirectoriesOfDeletedFile(
|
||||
|
|
|
|||
|
|
@ -37,7 +37,6 @@ export type { AuthenticationError } from "./errors/authentication-error";
|
|||
export type { MaybeOutdatedClientCursors } from "./types/maybe-outdated-client-cursors";
|
||||
export { DocumentSyncStatus } from "./types/document-sync-status";
|
||||
export { SyncClient } from "./sync-client";
|
||||
export { CONFLICT_PATH_REGEX } from "./sync-operations/conflict-path";
|
||||
export type { TextWithCursors, CursorPosition } from "reconcile-text";
|
||||
|
||||
export const debugging = {
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ export const DEFAULT_SETTINGS: SyncSettings = {
|
|||
webSocketRetryIntervalMs: 3500,
|
||||
diffCacheSizeMB: 4,
|
||||
enableTelemetry: false,
|
||||
networkRetryIntervalMs: 1000,
|
||||
networkRetryIntervalMs: 1000
|
||||
};
|
||||
|
||||
export class Settings {
|
||||
|
|
|
|||
|
|
@ -35,7 +35,8 @@ export class ServerConfig {
|
|||
const shouldUpgradeClient =
|
||||
config.supportedApiVersion > SUPPORTED_API_VERSION;
|
||||
throw new ServerVersionMismatchError(
|
||||
`Unsupported API version: ${config.supportedApiVersion}. Consider upgrading the ${shouldUpgradeClient ? "client" : "sync-server"
|
||||
`Unsupported API version: ${config.supportedApiVersion}. Consider upgrading the ${
|
||||
shouldUpgradeClient ? "client" : "sync-server"
|
||||
} to ensure compatibility`
|
||||
);
|
||||
}
|
||||
|
|
@ -90,6 +91,11 @@ export class ServerConfig {
|
|||
return this.config;
|
||||
}
|
||||
|
||||
public reset(): void {
|
||||
this.response = undefined;
|
||||
this.config = undefined;
|
||||
}
|
||||
|
||||
private async startPing(): Promise<PingResponse> {
|
||||
const pending = this.syncService.ping().catch((e: unknown) => {
|
||||
if (this.response === pending) {
|
||||
|
|
@ -100,9 +106,4 @@ export class ServerConfig {
|
|||
this.response = pending;
|
||||
return pending;
|
||||
}
|
||||
|
||||
public reset(): void {
|
||||
this.response = undefined;
|
||||
this.config = undefined;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -71,7 +71,9 @@ export class SyncService {
|
|||
response: Response,
|
||||
operation: string
|
||||
): Promise<void> {
|
||||
if (response.ok) { return; }
|
||||
if (response.ok) {
|
||||
return;
|
||||
}
|
||||
const message = `Failed to ${operation}: ${await SyncService.errorFromResponse(response)}`;
|
||||
// 429 is the only 4xx the server uses for *transient* contention
|
||||
// (`WriteBusyError` → HTTP 429). Every other 4xx means the request
|
||||
|
|
@ -183,7 +185,8 @@ export class SyncService {
|
|||
(await response.json()) as DocumentUpdateResponse; // eslint-disable-line @typescript-eslint/no-unsafe-type-assertion
|
||||
|
||||
this.logger.debug(
|
||||
`Updated document ${JSON.stringify(result)} with id ${result.documentId
|
||||
`Updated document ${JSON.stringify(result)} with id ${
|
||||
result.documentId
|
||||
}}`
|
||||
);
|
||||
|
||||
|
|
@ -231,7 +234,8 @@ export class SyncService {
|
|||
(await response.json()) as DocumentUpdateResponse; // eslint-disable-line @typescript-eslint/no-unsafe-type-assertion
|
||||
|
||||
this.logger.debug(
|
||||
`Updated document ${JSON.stringify(result)} with id ${result.documentId
|
||||
`Updated document ${JSON.stringify(result)} with id ${
|
||||
result.documentId
|
||||
}}`
|
||||
);
|
||||
|
||||
|
|
@ -240,14 +244,12 @@ export class SyncService {
|
|||
}
|
||||
|
||||
public async delete({
|
||||
documentId,
|
||||
documentId
|
||||
}: {
|
||||
documentId: DocumentId;
|
||||
}): Promise<DocumentVersionWithoutContent> {
|
||||
return this.retryForever(async () => {
|
||||
this.logger.debug(
|
||||
`Delete document with id ${documentId}`
|
||||
);
|
||||
this.logger.debug(`Delete document with id ${documentId}`);
|
||||
|
||||
// The server identifies the document by its URL path; no body
|
||||
// is needed. Sending one was a leftover of an earlier shape.
|
||||
|
|
@ -264,9 +266,7 @@ export class SyncService {
|
|||
const result: DocumentVersionWithoutContent =
|
||||
(await response.json()) as DocumentVersionWithoutContent; // eslint-disable-line @typescript-eslint/no-unsafe-type-assertion
|
||||
|
||||
this.logger.debug(
|
||||
`Deleted document with id ${documentId}`
|
||||
);
|
||||
this.logger.debug(`Deleted document with id ${documentId}`);
|
||||
|
||||
return result;
|
||||
});
|
||||
|
|
@ -338,7 +338,7 @@ export class SyncService {
|
|||
return this.retryForever(async () => {
|
||||
this.logger.debug(
|
||||
"Getting all documents" +
|
||||
(since != null ? ` since ${since}` : "")
|
||||
(since != null ? ` since ${since}` : "")
|
||||
);
|
||||
|
||||
const url = new URL(this.getUrl("/documents"));
|
||||
|
|
|
|||
|
|
@ -1,4 +1,8 @@
|
|||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { DocumentWithCursors } from "./DocumentWithCursors";
|
||||
|
||||
export interface ClientCursors { userName: string, deviceId: string, documentsWithCursors: DocumentWithCursors[], }
|
||||
export interface ClientCursors {
|
||||
userName: string;
|
||||
deviceId: string;
|
||||
documentsWithCursors: DocumentWithCursors[];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,7 @@
|
|||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export interface CreateDocumentVersion { relative_path: string, last_seen_vault_update_id: number, content: number[], }
|
||||
export interface CreateDocumentVersion {
|
||||
relative_path: string;
|
||||
last_seen_vault_update_id: number;
|
||||
content: number[];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { DocumentWithCursors } from "./DocumentWithCursors";
|
||||
|
||||
export interface CursorPositionFromClient { documentsWithCursors: DocumentWithCursors[], }
|
||||
export interface CursorPositionFromClient {
|
||||
documentsWithCursors: DocumentWithCursors[];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { ClientCursors } from "./ClientCursors";
|
||||
|
||||
export interface CursorPositionFromServer { clients: ClientCursors[], }
|
||||
export interface CursorPositionFromServer {
|
||||
clients: ClientCursors[];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,6 @@
|
|||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export interface CursorSpan { start: number, end: number, }
|
||||
export interface CursorSpan {
|
||||
start: number;
|
||||
end: number;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,4 +5,6 @@ import type { DocumentVersionWithoutContent } from "./DocumentVersionWithoutCont
|
|||
/**
|
||||
* Response to a create/update document request.
|
||||
*/
|
||||
export type DocumentUpdateResponse = { "type": "FastForwardUpdate" } & DocumentVersionWithoutContent | { "type": "MergingUpdate" } & DocumentVersion;
|
||||
export type DocumentUpdateResponse =
|
||||
| ({ type: "FastForwardUpdate" } & DocumentVersionWithoutContent)
|
||||
| ({ type: "MergingUpdate" } & DocumentVersion);
|
||||
|
|
|
|||
|
|
@ -1,3 +1,12 @@
|
|||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export interface DocumentVersion { vaultUpdateId: number, documentId: string, relativePath: string, updatedDate: string, contentBase64: string, isDeleted: boolean, userId: string, deviceId: string, }
|
||||
export interface DocumentVersion {
|
||||
vaultUpdateId: number;
|
||||
documentId: string;
|
||||
relativePath: string;
|
||||
updatedDate: string;
|
||||
contentBase64: string;
|
||||
isDeleted: boolean;
|
||||
userId: string;
|
||||
deviceId: string;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,16 @@
|
|||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export interface DocumentVersionWithoutContent { vaultUpdateId: number, documentId: string, relativePath: string, updatedDate: string, isDeleted: boolean, userId: string, deviceId: string, contentSize: number,
|
||||
/**
|
||||
* True iff this is the first version of the document
|
||||
*/
|
||||
isNewFile: boolean, }
|
||||
export interface DocumentVersionWithoutContent {
|
||||
vaultUpdateId: number;
|
||||
documentId: string;
|
||||
relativePath: string;
|
||||
updatedDate: string;
|
||||
isDeleted: boolean;
|
||||
userId: string;
|
||||
deviceId: string;
|
||||
contentSize: number;
|
||||
/**
|
||||
* True iff this is the first version of the document
|
||||
*/
|
||||
isNewFile: boolean;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,9 @@
|
|||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { CursorSpan } from "./CursorSpan";
|
||||
|
||||
export interface DocumentWithCursors { vaultUpdateId: number | null, documentId: string, relativePath: string, cursors: CursorSpan[], }
|
||||
export interface DocumentWithCursors {
|
||||
vaultUpdateId: number | null;
|
||||
documentId: string;
|
||||
relativePath: string;
|
||||
cursors: CursorSpan[];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,8 +4,10 @@ import type { DocumentVersionWithoutContent } from "./DocumentVersionWithoutCont
|
|||
/**
|
||||
* Response to a fetch latest documents request.
|
||||
*/
|
||||
export interface FetchLatestDocumentsResponse { latestDocuments: DocumentVersionWithoutContent[],
|
||||
/**
|
||||
* The update ID of the latest document in the response.
|
||||
*/
|
||||
lastUpdateId: bigint, }
|
||||
export interface FetchLatestDocumentsResponse {
|
||||
latestDocuments: DocumentVersionWithoutContent[];
|
||||
/**
|
||||
* The update ID of the latest document in the response.
|
||||
*/
|
||||
lastUpdateId: bigint;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,4 +4,8 @@ import type { VaultInfo } from "./VaultInfo";
|
|||
/**
|
||||
* Response to listing vaults accessible to the authenticated user.
|
||||
*/
|
||||
export interface ListVaultsResponse { vaults: VaultInfo[], hasMore: boolean, userName: string, }
|
||||
export interface ListVaultsResponse {
|
||||
vaults: VaultInfo[];
|
||||
hasMore: boolean;
|
||||
userName: string;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,22 +3,23 @@
|
|||
/**
|
||||
* Response to a ping request.
|
||||
*/
|
||||
export interface PingResponse {
|
||||
/**
|
||||
* Semantic version of the server.
|
||||
*/
|
||||
serverVersion: string,
|
||||
/**
|
||||
* Whether the client is authenticated based on the sent Authorization
|
||||
* header.
|
||||
*/
|
||||
isAuthenticated: boolean,
|
||||
/**
|
||||
* List of file extensions that are allowed to be merged.
|
||||
*/
|
||||
mergeableFileExtensions: string[],
|
||||
/**
|
||||
* API version ensuring backwards & forwards compatibility between the client
|
||||
* and server.
|
||||
*/
|
||||
supportedApiVersion: number, }
|
||||
export interface PingResponse {
|
||||
/**
|
||||
* Semantic version of the server.
|
||||
*/
|
||||
serverVersion: string;
|
||||
/**
|
||||
* Whether the client is authenticated based on the sent Authorization
|
||||
* header.
|
||||
*/
|
||||
isAuthenticated: boolean;
|
||||
/**
|
||||
* List of file extensions that are allowed to be merged.
|
||||
*/
|
||||
mergeableFileExtensions: string[];
|
||||
/**
|
||||
* API version ensuring backwards & forwards compatibility between the client
|
||||
* and server.
|
||||
*/
|
||||
supportedApiVersion: number;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,7 @@
|
|||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export interface SerializedError { errorType: string, message: string, causes: string[], }
|
||||
export interface SerializedError {
|
||||
errorType: string;
|
||||
message: string;
|
||||
causes: string[];
|
||||
}
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue