Add API for propagating cursor locations (#61)

This commit is contained in:
Andras Schmelczer 2025-06-08 20:20:52 +01:00 • committed by GitHub
commit e8b9bf40c5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
30 changed files with 1929 additions and 2228 deletions

View file

@ -15,23 +15,21 @@
"dependencies": {
"byte-base64": "^1.1.0",
"minimatch": "^10.0.1",
"openapi-fetch": "0.14.0",
"openapi-typescript": "7.6.1",
"p-queue": "^8.1.0",
"uuid": "^11.1.0"
},
"devDependencies": {
"@types/jest": "^29.5.14",
"@types/node": "^22.15.27",
"@types/node": "^22.15.30",
"jest": "^29.7.0",
"sync_lib": "file:../../backend/sync_lib/pkg",
"ts-jest": "^29.3.4",
"ts-loader": "^9.5.2",
"tslib": "2.8.1",
"typescript": "5.8.3",
"webpack": "^5.98.0",
"webpack": "^5.99.9",
"webpack-cli": "^6.0.1",
"webpack-merge": "^6.0.1",
"ws": "^8.18.2"
}
}
}

View file

@ -19,7 +19,8 @@ export type {
Cursor
} from "./file-operations/filesystem-operations";
export type { PersistenceProvider } from "./persistence/persistence";
export type { CursorSpan } from "./services/types/CursorSpan";
export type { ClientCursors } from "./services/types/ClientCursors";
export type { NetworkConnectionStatus } from "./types/network-connection-status";
export { DocumentUpdateStatus } from "./types/document-update-status";
export { SyncClient } from "./sync-client";

View file

@ -8,6 +8,7 @@ export interface SyncSettings {
isSyncEnabled: boolean;
maxFileSizeMB: number;
ignorePatterns: string[];
webSocketRetryIntervalMs: number;
}
export const DEFAULT_SETTINGS: SyncSettings = {
@ -17,7 +18,8 @@ export const DEFAULT_SETTINGS: SyncSettings = {
syncConcurrency: 1,
isSyncEnabled: false,
maxFileSizeMB: 10,
ignorePatterns: []
ignorePatterns: [],
webSocketRetryIntervalMs: 3500
};
export class Settings {

View file

@ -51,7 +51,10 @@ export class ConnectionStatus {
logger: Logger,
fetch: typeof globalThis.fetch = globalThis.fetch
): typeof globalThis.fetch {
return async (input: RequestInfo | URL): Promise<Response> => {
return async (
input: RequestInfo | URL,
init?: RequestInit
): Promise<Response> => {
while (!this.canFetch) {
await this.until;
}
@ -63,7 +66,7 @@ export class ConnectionStatus {
? input.clone()
: input;
const fetchPromise = fetch(_input);
const fetchPromise = fetch(_input, init);
// We only want to catch rejections from `this.until`
let result: symbol | Response | undefined = undefined;

View file

@ -1,16 +1,21 @@
import type { Client } from "openapi-fetch";
import createClient from "openapi-fetch";
import type { components, paths } from "./types"; // generated by openapi-typescript
import type {
DocumentId,
RelativePath,
VaultUpdateId
} from "../persistence/database";
import type { Logger } from "../tracing/logger";
import type { Settings } from "../persistence/settings";
import type { ConnectionStatus } from "./connection-status";
import { sleep } from "../utils/sleep";
import { SyncResetError } from "./sync-reset-error";
import type { SerializedError } from "./types/SerializedError";
import type { DocumentVersionWithoutContent } from "./types/DocumentVersionWithoutContent";
import type { DocumentUpdateResponse } from "./types/DocumentUpdateResponse";
import type { DocumentVersion } from "./types/DocumentVersion";
import type { FetchLatestDocumentsResponse } from "./types/FetchLatestDocumentsResponse";
import type { PingResponse } from "./types/PingResponse";
import type { DeleteDocumentVersion } from "./types/DeleteDocumentVersion";
export interface CheckConnectionResult {
isSuccessful: boolean;
@ -19,47 +24,28 @@ export interface CheckConnectionResult {
export class SyncService {
private static readonly NETWORK_RETRY_INTERVAL_MS = 1000;
private client: Client<paths>;
private pingClient: Client<paths>;
private readonly client: typeof globalThis.fetch;
private readonly pingClient: typeof globalThis.fetch;
public constructor(
private readonly deviceId: string,
private readonly connectionStatus: ConnectionStatus,
private readonly settings: Settings,
private readonly logger: Logger,
private readonly fetchImplementation: typeof globalThis.fetch = globalThis.fetch
fetchImplementation: typeof globalThis.fetch = globalThis.fetch
) {
[this.client, this.pingClient] = this.createClient(
this.settings.getSettings().remoteUri
// ensure that if it's called a method, `this` won't be bound to the instance
const unboundFetch: typeof globalThis.fetch = async (...args) =>
fetchImplementation(...args);
this.client = this.connectionStatus.getFetchImplementation(
this.logger,
unboundFetch
);
settings.addOnSettingsChangeListener((newSettings, oldSettings) => {
if (newSettings.remoteUri === oldSettings.remoteUri) {
return;
}
[this.client, this.pingClient] = this.createClient(
newSettings.remoteUri
);
});
this.pingClient = unboundFetch;
}
private get deviceIdHeader(): string {
// @ts-expect-error, injected by webpack
const packageVersion = __CURRENT_VERSION__; // eslint-disable-line
const platform =
typeof navigator !== "undefined"
? navigator.platform // eslint-disable-line @typescript-eslint/no-deprecated
: typeof process !== "undefined"
? process.platform
: "unknown";
return `vault-link/${packageVersion} (${this.deviceId}; ${platform})`;
}
private static formatError(
error: components["schemas"]["SerializedError"]
): string {
private static formatError(error: SerializedError): string {
let result = error.message;
if (error.causes.length > 0) {
const causes = error.causes.join(", ");
@ -77,47 +63,39 @@ export class SyncService {
documentId?: DocumentId;
relativePath: RelativePath;
contentBytes: Uint8Array;
}): Promise<components["schemas"]["DocumentVersionWithoutContent"]> {
const { vaultName } = this.settings.getSettings();
}): Promise<DocumentVersionWithoutContent> {
return this.withRetries(async () => {
const formData = new FormData();
if (documentId !== undefined) {
formData.append("document_id", documentId);
}
formData.append("relative_path", relativePath);
formData.append("device_id", this.deviceId);
formData.append("content", new Blob([contentBytes]));
const response = await this.client.POST(
"/vaults/{vault_id}/documents",
{
params: {
path: {
vault_id: vaultName
},
header: {
"device-id": this.deviceIdHeader
}
},
// eslint-disable-next-line
body: formData as any // FormData is not supported by openapi-fetch
}
);
const response = await this.client(this.getUrl("/documents"), {
method: "POST",
body: formData,
headers: this.getDefaultHeaders()
});
if (!response.data) {
const result: SerializedError | DocumentVersionWithoutContent =
(await response.json()) as // eslint-disable-line @typescript-eslint/no-unsafe-type-assertion
| SerializedError
| DocumentVersionWithoutContent;
if ("errorType" in result) {
throw new Error(
`Failed to create document: ${SyncService.formatError(response.error)}`
`Failed to create document: ${SyncService.formatError(result)}`
);
}
this.logger.debug(
`Created document ${JSON.stringify(response.data)} with id ${
response.data.documentId
`Created document ${JSON.stringify(result)} with id ${
result.documentId
}`
);
return response.data;
return result;
});
}
@ -131,9 +109,7 @@ export class SyncService {
documentId: DocumentId;
relativePath: RelativePath;
contentBytes: Uint8Array;
}): Promise<components["schemas"]["DocumentUpdateResponse"]> {
const { vaultName } = this.settings.getSettings();
}): Promise<DocumentUpdateResponse> {
return this.withRetries(async () => {
this.logger.debug(
`Updating document ${documentId} with parent version ${parentVersionId} and relative path ${relativePath}`
@ -141,39 +117,35 @@ export class SyncService {
const formData = new FormData();
formData.append("parent_version_id", parentVersionId.toString());
formData.append("relative_path", relativePath);
formData.append("device_id", this.deviceId);
formData.append("content", new Blob([contentBytes]));
const response = await this.client.PUT(
"/vaults/{vault_id}/documents/{document_id}",
const response = await this.client(
this.getUrl(`/documents/${documentId}`),
{
params: {
path: {
vault_id: vaultName,
document_id: documentId
},
header: {
"device-id": this.deviceIdHeader
}
},
// eslint-disable-next-line
body: formData as any // FormData is not supported by openapi-fetch
method: "PUT",
body: formData,
headers: this.getDefaultHeaders()
}
);
if (!response.data) {
const result: SerializedError | DocumentUpdateResponse =
(await response.json()) as // eslint-disable-line @typescript-eslint/no-unsafe-type-assertion
| SerializedError
| DocumentUpdateResponse;
if ("errorType" in result) {
throw new Error(
`Failed to update document: ${SyncService.formatError(response.error)}`
`Failed to update document: ${SyncService.formatError(result)}`
);
}
this.logger.debug(
`Updated document ${JSON.stringify(response.data)} with id ${
response.data.documentId
}`
`Updated document ${JSON.stringify(result)} with id ${
result.documentId
}}`
);
return response.data;
return result;
});
}
@ -183,39 +155,39 @@ export class SyncService {
}: {
documentId: DocumentId;
relativePath: RelativePath;
}): Promise<components["schemas"]["DocumentVersionWithoutContent"]> {
}): Promise<DocumentVersionWithoutContent> {
return this.withRetries(async () => {
const { vaultName } = this.settings.getSettings();
const response = await this.client.DELETE(
"/vaults/{vault_id}/documents/{document_id}",
const request: DeleteDocumentVersion = {
relativePath
};
const response = await this.client(
this.getUrl(`/documents/${documentId}`),
{
params: {
path: {
vault_id: vaultName,
document_id: documentId
},
header: {
"device-id": this.deviceIdHeader
}
},
body: {
relativePath,
deviceId: this.deviceId
method: "DELETE",
body: JSON.stringify(request),
headers: {
"Content-Type": "application/json",
...this.getDefaultHeaders()
}
}
);
if (response.error) {
throw new Error(`Failed to delete document`);
const result: SerializedError | DocumentVersionWithoutContent =
(await response.json()) as // eslint-disable-line @typescript-eslint/no-unsafe-type-assertion
| SerializedError
| DocumentVersionWithoutContent;
if ("errorType" in result) {
throw new Error(
`Failed to delete document: ${SyncService.formatError(result)}`
);
}
this.logger.debug(
`Deleted document ${relativePath} with id ${documentId}`
);
return response.data;
return result;
});
}
@ -223,100 +195,77 @@ export class SyncService {
documentId
}: {
documentId: DocumentId;
}): Promise<components["schemas"]["DocumentVersion"]> {
const { vaultName } = this.settings.getSettings();
}): Promise<DocumentVersion> {
return this.withRetries(async () => {
const response = await this.client.GET(
"/vaults/{vault_id}/documents/{document_id}",
const response = await this.client(
this.getUrl(`/documents/${documentId}`),
{
params: {
path: {
vault_id: vaultName,
document_id: documentId
}
}
headers: this.getDefaultHeaders()
}
);
if (!response.data) {
const result: SerializedError | DocumentVersion =
(await response.json()) as SerializedError | DocumentVersion; // eslint-disable-line @typescript-eslint/no-unsafe-type-assertion
if ("errorType" in result) {
throw new Error(
`Failed to get document: ${SyncService.formatError(response.error)}`
`Failed to get document: ${SyncService.formatError(result)}`
);
}
this.logger.debug(
`Get document ${response.data.relativePath} with id ${response.data.documentId}`
`Get document ${result.relativePath} with id ${result.documentId}`
);
return response.data;
return result;
});
}
public async getAll(
since?: VaultUpdateId
): Promise<components["schemas"]["FetchLatestDocumentsResponse"]> {
): Promise<FetchLatestDocumentsResponse> {
return this.withRetries(async () => {
const { vaultName } = this.settings.getSettings();
const url = new URL(this.getUrl("/documents"));
if (since !== undefined) {
url.searchParams.append("since", since.toString());
}
const response = await this.client(url.toString(), {
headers: this.getDefaultHeaders()
});
const response = await this.client.GET(
"/vaults/{vault_id}/documents",
{
params: {
path: {
vault_id: vaultName
},
query: {
since_update_id: since
}
}
}
);
const result: SerializedError | FetchLatestDocumentsResponse =
(await response.json()) as // eslint-disable-line @typescript-eslint/no-unsafe-type-assertion
| SerializedError
| FetchLatestDocumentsResponse;
const { error } = response;
if (error) {
if ("errorType" in result) {
throw new Error(
`Failed to get documents: ${SyncService.formatError(response.error)}`
`Failed to get documents: ${SyncService.formatError(result)}`
);
}
this.logger.debug(
`Got ${response.data.latestDocuments.length} document metadata`
`Got ${result.latestDocuments.length} document metadata`
);
return response.data;
return result;
});
}
public async checkConnection(): Promise<CheckConnectionResult> {
const { vaultName } = this.settings.getSettings();
try {
const response = await this.pingClient.GET(
"/vaults/{vault_id}/ping",
{
params: {
header: {
authorization: `Bearer ${this.settings.getSettings().token}`
},
path: {
vault_id: vaultName
}
}
}
);
const response = await this.pingClient(this.getUrl("/ping"), {
headers: this.getDefaultHeaders()
});
const result: PingResponse | SerializedError =
(await response.json()) as PingResponse | SerializedError; // eslint-disable-line @typescript-eslint/no-unsafe-type-assertion
this.logger.debug(
`Ping response: ${JSON.stringify(response.data)}`
);
if (!response.data) {
if ("errorType" in result) {
throw new Error(
`Failed to ping server: ${SyncService.formatError(response.error)}`
`Failed to ping server: ${SyncService.formatError(result)}`
);
}
const result = response.data;
if (result.isAuthenticated) {
return {
isSuccessful: true,
@ -336,29 +285,17 @@ export class SyncService {
}
}
/**
* Create a client and a ping client for the given remote URI.
*/
private createClient(remoteUri: string): [Client<paths>, Client<paths>] {
return [
createClient<paths>({
baseUrl: remoteUri,
fetch: this.connectionStatus.getFetchImplementation(
this.logger,
this.fetchImplementation
),
headers: {
authorization: `Bearer ${this.settings.getSettings().token}`
}
}),
createClient<paths>({
baseUrl: remoteUri,
fetch: this.fetchImplementation,
headers: {
authorization: `Bearer ${this.settings.getSettings().token}`
}
})
];
private getUrl(path: string): string {
const { vaultName, remoteUri } = this.settings.getSettings();
const safeRemoteUri = remoteUri.replace(/\/+$/, "");
return `${safeRemoteUri}/vaults/${vaultName}${path}`;
}
private getDefaultHeaders(): Record<string, string> {
return {
"device-id": this.deviceId,
authorization: `Bearer ${this.settings.getSettings().token}`
};
}
private async withRetries<T>(fn: () => Promise<T>): Promise<T> {

View file

@ -1,655 +0,0 @@
/**
* This file was auto-generated by openapi-typescript.
* Do not make direct changes to the file.
*/
export interface paths {
"/vaults/{vault_id}/documents": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get: {
parameters: {
query?: {
since_update_id?: number | null;
};
header?: never;
path: {
vault_id: string;
};
cookie?: never;
};
requestBody?: never;
responses: {
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["FetchLatestDocumentsResponse"];
};
};
default: {
headers: {
[name: string]: unknown;
};
content: {
/** @example {
* "causes": [],
* "message": "An error has occurred"
* } */
"application/json": components["schemas"]["SerializedError"];
};
};
};
};
put?: never;
post: {
parameters: {
query?: never;
header: {
"device-id": string;
};
path: {
vault_id: string;
};
cookie?: never;
};
requestBody: {
content: {
"multipart/form-data": components["schemas"]["CreateDocumentVersionMultipart"];
};
};
responses: {
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["DocumentVersionWithoutContent"];
};
};
default: {
headers: {
[name: string]: unknown;
};
content: {
/** @example {
* "causes": [],
* "message": "An error has occurred"
* } */
"application/json": components["schemas"]["SerializedError"];
};
};
};
};
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/vaults/{vault_id}/documents/json": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get?: never;
put?: never;
post: {
parameters: {
query?: never;
header: {
"device-id": string;
};
path: {
vault_id: string;
};
cookie?: never;
};
requestBody: {
content: {
"application/json": components["schemas"]["CreateDocumentVersion"];
};
};
responses: {
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["DocumentVersionWithoutContent"];
};
};
default: {
headers: {
[name: string]: unknown;
};
content: {
/** @example {
* "causes": [],
* "message": "An error has occurred"
* } */
"application/json": components["schemas"]["SerializedError"];
};
};
};
};
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/vaults/{vault_id}/documents/{document_id}": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get: {
parameters: {
query?: never;
header?: never;
path: {
document_id: string;
vault_id: string;
};
cookie?: never;
};
requestBody?: never;
responses: {
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["DocumentVersion"];
};
};
default: {
headers: {
[name: string]: unknown;
};
content: {
/** @example {
* "causes": [],
* "message": "An error has occurred"
* } */
"application/json": components["schemas"]["SerializedError"];
};
};
};
};
put: {
parameters: {
query?: never;
header: {
"device-id": string;
};
path: {
document_id: string;
vault_id: string;
};
cookie?: never;
};
requestBody: {
content: {
"multipart/form-data": components["schemas"]["UpdateDocumentVersionMultipart"];
};
};
responses: {
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["DocumentUpdateResponse"];
};
};
default: {
headers: {
[name: string]: unknown;
};
content: {
/** @example {
* "causes": [],
* "message": "An error has occurred"
* } */
"application/json": components["schemas"]["SerializedError"];
};
};
};
};
post?: never;
delete: {
parameters: {
query?: never;
header: {
"device-id": string;
};
path: {
document_id: string;
vault_id: string;
};
cookie?: never;
};
requestBody: {
content: {
"application/json": components["schemas"]["DeleteDocumentVersion"];
};
};
responses: {
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["DocumentVersionWithoutContent"];
};
};
default: {
headers: {
[name: string]: unknown;
};
content: {
/** @example {
* "causes": [],
* "message": "An error has occurred"
* } */
"application/json": components["schemas"]["SerializedError"];
};
};
};
};
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/vaults/{vault_id}/documents/{document_id}/json": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get?: never;
put: {
parameters: {
query?: never;
header: {
"device-id": string;
};
path: {
document_id: string;
vault_id: string;
};
cookie?: never;
};
requestBody: {
content: {
"application/json": components["schemas"]["UpdateDocumentVersion"];
};
};
responses: {
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["DocumentUpdateResponse"];
};
};
default: {
headers: {
[name: string]: unknown;
};
content: {
/** @example {
* "causes": [],
* "message": "An error has occurred"
* } */
"application/json": components["schemas"]["SerializedError"];
};
};
};
};
post?: never;
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/vaults/{vault_id}/documents/{document_id}/versions/{version_id}": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get?: never;
put: {
parameters: {
query?: never;
header?: never;
path: {
document_id: string;
vault_id: string;
vault_update_id: number;
};
cookie?: never;
};
requestBody?: never;
responses: {
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["DocumentVersion"];
};
};
default: {
headers: {
[name: string]: unknown;
};
content: {
/** @example {
* "causes": [],
* "message": "An error has occurred"
* } */
"application/json": components["schemas"]["SerializedError"];
};
};
};
};
post?: never;
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/vaults/{vault_id}/documents/{document_id}/versions/{version_id}/content": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get?: never;
put: {
parameters: {
query?: never;
header?: never;
path: {
document_id: string;
vault_id: string;
vault_update_id: number;
};
cookie?: never;
};
requestBody?: never;
responses: {
/** @description byte stream */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/octet-stream": unknown;
};
};
default: {
headers: {
[name: string]: unknown;
};
content: {
/** @example {
* "causes": [],
* "message": "An error has occurred"
* } */
"application/json": components["schemas"]["SerializedError"];
};
};
};
};
post?: never;
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/vaults/{vault_id}/ping": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get: {
parameters: {
query?: never;
header?: {
authorization?: string;
};
path: {
vault_id: string;
};
cookie?: never;
};
requestBody?: never;
responses: {
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["PingResponse"];
};
};
default: {
headers: {
[name: string]: unknown;
};
content: {
/** @example {
* "causes": [],
* "message": "An error has occurred"
* } */
"application/json": components["schemas"]["SerializedError"];
};
};
};
};
put?: never;
post?: never;
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
}
export type webhooks = Record<string, never>;
export interface components {
schemas: {
Array_of_uint8: number[];
CreateDocumentPathParams: {
vault_id: string;
};
CreateDocumentVersion: {
contentBase64: string;
deviceId?: string | null;
/**
* Format: uuid
* @description The client can decide the document id (if it wishes to) in order to help with syncing. If the client does not provide a document id, the server will generate one. If the client provides a document id it must not already exist in the database.
*/
documentId?: string | null;
relativePath: string;
};
CreateDocumentVersionMultipart: {
content: components["schemas"]["Array_of_uint8"];
device_id?: string | null;
/** Format: uuid */
document_id?: string | null;
relative_path: string;
};
DeleteDocumentPathParams: {
/** Format: uuid */
document_id: string;
vault_id: string;
};
DeleteDocumentVersion: {
deviceId?: string | null;
relativePath: string;
};
/** @description Response to an update document request. */
DocumentUpdateResponse:
| {
/** Format: uint64 */
contentSize: number;
deviceId: string;
/** Format: uuid */
documentId: string;
isDeleted: boolean;
relativePath: string;
/** @enum {string} */
type: "FastForwardUpdate";
/** Format: date-time */
updatedDate: string;
userId: string;
/** Format: int64 */
vaultUpdateId: number;
}
| {
contentBase64: string;
deviceId: string;
/** Format: uuid */
documentId: string;
isDeleted: boolean;
relativePath: string;
/** @enum {string} */
type: "MergingUpdate";
/** Format: date-time */
updatedDate: string;
userId: string;
/** Format: int64 */
vaultUpdateId: number;
};
DocumentVersion: {
contentBase64: string;
deviceId: string;
/** Format: uuid */
documentId: string;
isDeleted: boolean;
relativePath: string;
/** Format: date-time */
updatedDate: string;
userId: string;
/** Format: int64 */
vaultUpdateId: number;
};
DocumentVersionWithoutContent: {
/** Format: uint64 */
contentSize: number;
deviceId: string;
/** Format: uuid */
documentId: string;
isDeleted: boolean;
relativePath: string;
/** Format: date-time */
updatedDate: string;
userId: string;
/** Format: int64 */
vaultUpdateId: number;
};
FetchDocumentVersionContentPathParams: {
/** Format: uuid */
document_id: string;
vault_id: string;
/** Format: int64 */
vault_update_id: number;
};
FetchDocumentVersionPathParams: {
/** Format: uuid */
document_id: string;
vault_id: string;
/** Format: int64 */
vault_update_id: number;
};
FetchLatestDocumentVersionPathParams: {
/** Format: uuid */
document_id: string;
vault_id: string;
};
FetchLatestDocumentsPathParams: {
vault_id: string;
};
/** @description Response to a fetch latest documents request. */
FetchLatestDocumentsResponse: {
/**
* Format: int64
* @description The update ID of the latest document in the response.
*/
lastUpdateId: number;
latestDocuments: components["schemas"]["DocumentVersionWithoutContent"][];
};
PingPathParams: {
vault_id: string;
};
/** @description Response to a ping request. */
PingResponse: {
/** @description Whether the client is authenticated based on the sent Authorization header. */
isAuthenticated: boolean;
/** @description Semantic version of the server. */
serverVersion: string;
};
QueryParams: {
/** Format: int64 */
since_update_id?: number | null;
};
SerializedError: {
causes: string[];
message: string;
};
UpdateDocumentPathParams: {
/** Format: uuid */
document_id: string;
vault_id: string;
};
UpdateDocumentVersion: {
contentBase64: string;
deviceId?: string | null;
/** Format: int64 */
parentVersionId: number;
relativePath: string;
};
UpdateDocumentVersionMultipart: {
content: components["schemas"]["Array_of_uint8"];
deviceId?: string | null;
/** Format: int64 */
parentVersionId: number;
relativePath: string;
};
WebsocketPathParams: {
vault_id: string;
};
};
responses: never;
parameters: never;
requestBodies: never;
headers: never;
pathItems: never;
}
export type $defs = Record<string, never>;
export type operations = Record<string, never>;

View file

@ -0,0 +1,8 @@
// 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 ClientCursors {
userName: string;
deviceId: string;
cursors: Partial<Record<string, CursorSpan[]>>;
}

View file

@ -0,0 +1,13 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export interface CreateDocumentVersion {
/**
* The client can decide the document id (if it wishes to) in order
* to help with syncing. If the client does not provide a document id,
* the server will generate one. If the client provides a document id
* it must not already exist in the database.
*/
document_id: string | null;
relative_path: string;
content: number[];
}

View file

@ -0,0 +1,6 @@
// 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 CursorPositionFromClient {
documentToCursors: Partial<Record<string, CursorSpan[]>>;
}

View file

@ -0,0 +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[];
}

View file

@ -0,0 +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;
}

View file

@ -0,0 +1,5 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export interface DeleteDocumentVersion {
relativePath: string;
}

View file

@ -0,0 +1,10 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { DocumentVersion } from "./DocumentVersion";
import type { DocumentVersionWithoutContent } from "./DocumentVersionWithoutContent";
/**
* Response to an update document request.
*/
export type DocumentUpdateResponse =
| ({ type: "FastForwardUpdate" } & DocumentVersionWithoutContent)
| ({ type: "MergingUpdate" } & DocumentVersion);

View file

@ -0,0 +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;
}

View file

@ -0,0 +1,12 @@
// 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;
}

View file

@ -0,0 +1,13 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { DocumentVersionWithoutContent } from "./DocumentVersionWithoutContent";
/**
* Response to a fetch latest documents request.
*/
export interface FetchLatestDocumentsResponse {
latestDocuments: DocumentVersionWithoutContent[];
/**
* The update ID of the latest document in the response.
*/
lastUpdateId: bigint;
}

View file

@ -0,0 +1,16 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
/**
* 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;
}

View file

@ -0,0 +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[];
}

View file

@ -0,0 +1,7 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export interface UpdateDocumentVersion {
parent_version_id: bigint;
relative_path: string;
content: number[];
}

View file

@ -0,0 +1,7 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { CursorPositionFromClient } from "./CursorPositionFromClient";
import type { WebSocketHandshake } from "./WebSocketHandshake";
export type WebSocketClientMessage =
| ({ type: "handshake" } & WebSocketHandshake)
| ({ type: "cursorPositions" } & CursorPositionFromClient);

Some files were not shown because too many files have changed in this diff Show more