Improve synchronization flow

This commit is contained in:
Hakan Shehu
2024-10-10 14:13:54 +02:00
parent cf72237e55
commit 4df3aefc41
54 changed files with 1529 additions and 1034 deletions

View File

@@ -118,7 +118,7 @@
"prettier-plugin-tailwindcss": "^0.6.8",
"tailwindcss": "^3.4.13",
"ts-node": "^10.9.2",
"typescript": "^5.6.2",
"typescript": "^5.6.3",
"vite": "^5.4.8"
}
},
@@ -13590,9 +13590,9 @@
}
},
"node_modules/typescript": {
"version": "5.6.2",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.2.tgz",
"integrity": "sha512-NW8ByodCSNCwZeghjN3o+JX5OFH0Ojg6sadjEKY4huZ52TqbJTJnDo5+Tw98lSy63NZvi4n+ez5m2u5d4PkZyw==",
"version": "5.6.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz",
"integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==",
"devOptional": true,
"license": "Apache-2.0",
"bin": {

View File

@@ -40,7 +40,7 @@
"prettier-plugin-tailwindcss": "^0.6.8",
"tailwindcss": "^3.4.13",
"ts-node": "^10.9.2",
"typescript": "^5.6.2",
"typescript": "^5.6.3",
"vite": "^5.4.8"
},
"keywords": [],

View File

@@ -1,6 +1,7 @@
import { MessageHandler, MessageMap } from '@/operations/messages';
import { ServerChangeMessageHandler } from '@/main/handlers/messages/server-change';
import { ServerChangeAckMessageHandler } from '@/main/handlers/messages/server-change-ack';
import { ServerChangeResultMessageHandler } from '@/main/handlers/messages/server-change-result';
import { ServerChangeBatchMessageHandler } from '@/main/handlers/messages/server-change-batch';
type MessageHandlerMap = {
[K in keyof MessageMap]: MessageHandler<MessageMap[K]>;
@@ -8,5 +9,6 @@ type MessageHandlerMap = {
export const messageHandlerMap: MessageHandlerMap = {
server_change: new ServerChangeMessageHandler(),
server_change_ack: new ServerChangeAckMessageHandler(),
server_change_result: new ServerChangeResultMessageHandler(),
server_change_batch: new ServerChangeBatchMessageHandler(),
};

View File

@@ -0,0 +1,16 @@
import { MessageContext, MessageHandler } from '@/operations/messages';
import { ServerChangeBatchMessageInput } from '@/operations/messages/server-change-batch';
import { synchronizer } from '@/main/synchronizer';
export class ServerChangeBatchMessageHandler
implements MessageHandler<ServerChangeBatchMessageInput>
{
public async handleMessage(
context: MessageContext,
input: ServerChangeBatchMessageInput,
): Promise<void> {
for (const change of input.changes) {
await synchronizer.handleServerChange(context.accountId, change);
}
}
}

View File

@@ -1,13 +1,13 @@
import { MessageContext, MessageHandler } from '@/operations/messages';
import { ServerChangeAckMessageInput } from '@/operations/messages/server-change-ack';
import { ServerChangeResultMessageInput } from '@/operations/messages/server-change-result';
import { socketManager } from '@/main/sockets/socket-manager';
export class ServerChangeAckMessageHandler
implements MessageHandler<ServerChangeAckMessageInput>
export class ServerChangeResultMessageHandler
implements MessageHandler<ServerChangeResultMessageInput>
{
public async handleMessage(
context: MessageContext,
input: ServerChangeAckMessageInput,
input: ServerChangeResultMessageInput,
): Promise<void> {
socketManager.sendMessage(context.accountId, input);
}

View File

@@ -1,6 +1,6 @@
import { MessageContext, MessageHandler } from '@/operations/messages';
import { ServerChangeMessageInput } from '@/operations/messages/server-change';
import { mediator } from '@/main/mediator';
import { synchronizer } from '@/main/synchronizer';
export class ServerChangeMessageHandler
implements MessageHandler<ServerChangeMessageInput>
@@ -9,53 +9,6 @@ export class ServerChangeMessageHandler
context: MessageContext,
input: ServerChangeMessageInput,
): Promise<void> {
if (input.change.table === 'nodes' && input.change.workspaceId) {
await mediator.executeMutation({
type: 'node_sync',
id: input.change.id,
accountId: context.accountId,
workspaceId: input.change.workspaceId,
action: input.change.action,
after: input.change.after,
before: input.change.before,
});
} else if (
input.change.table === 'node_reactions' &&
input.change.workspaceId
) {
await mediator.executeMutation({
type: 'node_reaction_sync',
id: input.change.id,
accountId: context.accountId,
workspaceId: input.change.workspaceId,
action: input.change.action,
after: input.change.after,
before: input.change.before,
});
} else if (
input.change.table === 'node_collaborator' &&
input.change.workspaceId
) {
await mediator.executeMutation({
type: 'node_collaborator_sync',
id: input.change.id,
accountId: context.accountId,
workspaceId: input.change.workspaceId,
action: input.change.action,
after: input.change.after,
before: input.change.before,
});
}
await mediator.executeMessage(
{
accountId: context.accountId,
deviceId: context.deviceId,
},
{
type: 'server_change_ack',
changeId: input.change.id,
},
);
await synchronizer.handleServerChange(context.accountId, input.change);
}
}

View File

@@ -26,11 +26,16 @@ import { WorkspaceAccountRoleUpdateMutationHandler } from '@/main/handlers/mutat
import { WorkspaceAccountsInviteMutationHandler } from '@/main/handlers/mutations/workspace-accounts-invite';
import { WorkspaceCreateMutationHandler } from '@/main/handlers/mutations/workspace-create';
import { WorkspaceUpdateMutationHandler } from '@/main/handlers/mutations/workspace-update';
import { NodeSyncMutationHandler } from '@/main/handlers/mutations/node-sync';
import { NodeReactionSyncMutationHandler } from '@/main/handlers/mutations/node-reaction-sync';
import { NodeCollaboratorSyncMutationHandler } from '@/main/handlers/mutations/node-collaborator-sync';
import { DocumentSaveMutationHandler } from '@/main/handlers/mutations/document-save';
import { AvatarUploadMutationHandler } from '@/main/handlers/mutations/avatar-upload';
import { NodeServerCreateMutationHandler } from '@/main/handlers/mutations/node-server-create';
import { NodeServerUpdateMutationHandler } from '@/main/handlers/mutations/node-server-update';
import { NodeServerDeleteMutationHandler } from '@/main/handlers/mutations/node-server-delete';
import { NodeCollaboratorServerCreateMutationHandler } from '@/main/handlers/mutations/node-collaborator-server-create';
import { NodeCollaboratorServerUpdateMutationHandler } from '@/main/handlers/mutations/node-collaborator-server-update';
import { NodeCollaboratorServerDeleteMutationHandler } from '@/main/handlers/mutations/node-collaborator-server-delete';
import { NodeReactionServerCreateMutationHandler } from '@/main/handlers/mutations/node-reaction-server-create';
import { NodeReactionServerDeleteMutationHandler } from '@/main/handlers/mutations/node-reaction-server-delete';
type MutationHandlerMap = {
[K in keyof MutationMap]: MutationHandler<MutationMap[K]['input']>;
@@ -65,9 +70,17 @@ export const mutationHandlerMap: MutationHandlerMap = {
workspace_accounts_invite: new WorkspaceAccountsInviteMutationHandler(),
workspace_create: new WorkspaceCreateMutationHandler(),
workspace_update: new WorkspaceUpdateMutationHandler(),
node_sync: new NodeSyncMutationHandler(),
node_reaction_sync: new NodeReactionSyncMutationHandler(),
node_collaborator_sync: new NodeCollaboratorSyncMutationHandler(),
document_save: new DocumentSaveMutationHandler(),
avatar_upload: new AvatarUploadMutationHandler(),
node_server_create: new NodeServerCreateMutationHandler(),
node_server_update: new NodeServerUpdateMutationHandler(),
node_server_delete: new NodeServerDeleteMutationHandler(),
node_collaborator_server_create:
new NodeCollaboratorServerCreateMutationHandler(),
node_collaborator_server_update:
new NodeCollaboratorServerUpdateMutationHandler(),
node_collaborator_server_delete:
new NodeCollaboratorServerDeleteMutationHandler(),
node_reaction_server_create: new NodeReactionServerCreateMutationHandler(),
node_reaction_server_delete: new NodeReactionServerDeleteMutationHandler(),
};

View File

@@ -0,0 +1,69 @@
import { databaseManager } from '@/main/data/database-manager';
import { MutationHandler, MutationResult } from '@/operations/mutations';
import { NodeCollaboratorServerCreateMutationInput } from '@/operations/mutations/node-collaborator-server-create';
export class NodeCollaboratorServerCreateMutationHandler
implements MutationHandler<NodeCollaboratorServerCreateMutationInput>
{
public async handleMutation(
input: NodeCollaboratorServerCreateMutationInput,
): Promise<MutationResult<NodeCollaboratorServerCreateMutationInput>> {
const workspace = await databaseManager.appDatabase
.selectFrom('workspaces')
.selectAll()
.where((eb) =>
eb.and([
eb('account_id', '=', input.accountId),
eb('workspace_id', '=', input.workspaceId),
]),
)
.executeTakeFirst();
if (!workspace) {
return {
output: {
success: false,
},
};
}
const userId = workspace.user_id;
const workspaceDatabase =
await databaseManager.getWorkspaceDatabase(userId);
await workspaceDatabase
.insertInto('node_collaborators')
.values({
node_id: input.nodeId,
collaborator_id: input.collaboratorId,
role: input.role,
created_at: input.createdAt,
created_by: input.createdBy,
version_id: input.versionId,
server_created_at: input.serverCreatedAt,
server_version_id: input.versionId,
})
.onConflict((cb) =>
cb
.doUpdateSet({
server_created_at: input.serverCreatedAt,
server_version_id: input.versionId,
})
.where('server_version_id', 'is', null),
)
.execute();
return {
output: {
success: true,
},
changes: [
{
type: 'workspace',
table: 'nodes',
userId: userId,
},
],
};
}
}

View File

@@ -0,0 +1,57 @@
import { databaseManager } from '@/main/data/database-manager';
import { MutationHandler, MutationResult } from '@/operations/mutations';
import { NodeCollaboratorServerDeleteMutationInput } from '@/operations/mutations/node-collaborator-server-delete';
export class NodeCollaboratorServerDeleteMutationHandler
implements MutationHandler<NodeCollaboratorServerDeleteMutationInput>
{
public async handleMutation(
input: NodeCollaboratorServerDeleteMutationInput,
): Promise<MutationResult<NodeCollaboratorServerDeleteMutationInput>> {
const workspace = await databaseManager.appDatabase
.selectFrom('workspaces')
.selectAll()
.where((eb) =>
eb.and([
eb('account_id', '=', input.accountId),
eb('workspace_id', '=', input.workspaceId),
]),
)
.executeTakeFirst();
if (!workspace) {
return {
output: {
success: false,
},
};
}
const userId = workspace.user_id;
const workspaceDatabase =
await databaseManager.getWorkspaceDatabase(userId);
await workspaceDatabase
.deleteFrom('node_collaborators')
.where((eb) =>
eb.and([
eb('node_id', '=', input.nodeId),
eb('collaborator_id', '=', input.collaboratorId),
]),
)
.execute();
return {
output: {
success: true,
},
changes: [
{
type: 'workspace',
table: 'node_collaborators',
userId: userId,
},
],
};
}
}

View File

@@ -0,0 +1,96 @@
import { databaseManager } from '@/main/data/database-manager';
import { MutationHandler, MutationResult } from '@/operations/mutations';
import { NodeCollaboratorServerUpdateMutationInput } from '@/operations/mutations/node-collaborator-server-update';
export class NodeCollaboratorServerUpdateMutationHandler
implements MutationHandler<NodeCollaboratorServerUpdateMutationInput>
{
public async handleMutation(
input: NodeCollaboratorServerUpdateMutationInput,
): Promise<MutationResult<NodeCollaboratorServerUpdateMutationInput>> {
const workspace = await databaseManager.appDatabase
.selectFrom('workspaces')
.selectAll()
.where((eb) =>
eb.and([
eb('account_id', '=', input.accountId),
eb('workspace_id', '=', input.workspaceId),
]),
)
.executeTakeFirst();
if (!workspace) {
return {
output: {
success: false,
},
};
}
const userId = workspace.user_id;
const workspaceDatabase =
await databaseManager.getWorkspaceDatabase(userId);
const nodeCollaborator = await workspaceDatabase
.selectFrom('node_collaborators')
.selectAll()
.where((eb) =>
eb.and([
eb('node_id', '=', input.nodeId),
eb('collaborator_id', '=', input.collaboratorId),
]),
)
.executeTakeFirst();
if (nodeCollaborator.server_version_id === input.versionId) {
return {
output: {
success: true,
},
};
}
if (nodeCollaborator.updated_at) {
const localUpdatedAt = new Date(nodeCollaborator.updated_at);
const serverUpdatedAt = new Date(input.updatedAt);
if (localUpdatedAt > serverUpdatedAt) {
return {
output: {
success: true,
},
};
}
}
await workspaceDatabase
.updateTable('node_collaborators')
.set({
role: input.role,
updated_at: input.updatedAt,
updated_by: input.updatedBy,
version_id: input.versionId,
server_updated_at: input.serverUpdatedAt,
server_version_id: input.versionId,
})
.where((eb) =>
eb.and([
eb('node_id', '=', input.nodeId),
eb('collaborator_id', '=', input.collaboratorId),
]),
)
.execute();
return {
output: {
success: true,
},
changes: [
{
type: 'workspace',
table: 'node_collaborators',
userId: userId,
},
],
};
}
}

View File

@@ -1,160 +0,0 @@
import { databaseManager } from '@/main/data/database-manager';
import { MutationHandler, MutationResult } from '@/operations/mutations';
import { NodeCollaboratorSyncMutationInput } from '@/operations/mutations/node-collaborator-sync';
import { ServerNodeCollaborator, ServerNodeReaction } from '@/types/nodes';
export class NodeCollaboratorSyncMutationHandler
implements MutationHandler<NodeCollaboratorSyncMutationInput>
{
public async handleMutation(
input: NodeCollaboratorSyncMutationInput,
): Promise<MutationResult<NodeCollaboratorSyncMutationInput>> {
const workspace = await databaseManager.appDatabase
.selectFrom('workspaces')
.selectAll()
.where((eb) =>
eb.and([
eb('account_id', '=', input.accountId),
eb('workspace_id', '=', input.workspaceId),
]),
)
.executeTakeFirst();
if (!workspace) {
return {
output: {
success: false,
},
};
}
const userId = workspace.user_id;
if (input.action === 'insert' && input.after) {
await this.insertNodeCollaborator(userId, input.after);
} else if (input.action === 'update' && input.after) {
await this.updateNodeCollaborator(userId, input.after);
} else if (input.action === 'delete' && input.before) {
await this.deleteNodeReaction(userId, input.before);
}
return {
output: {
success: true,
},
changes: [
{
type: 'workspace',
table: 'node_collaborators',
userId: userId,
},
],
};
}
private async insertNodeCollaborator(
userId: string,
nodeCollaborator: ServerNodeCollaborator,
): Promise<void> {
const workspaceDatabase =
await databaseManager.getWorkspaceDatabase(userId);
await workspaceDatabase
.insertInto('node_collaborators')
.values({
node_id: nodeCollaborator.nodeId,
collaborator_id: nodeCollaborator.collaboratorId,
role: nodeCollaborator.role,
created_at: nodeCollaborator.createdAt,
created_by: nodeCollaborator.createdBy,
updated_by: nodeCollaborator.updatedBy,
updated_at: nodeCollaborator.updatedAt,
version_id: nodeCollaborator.versionId,
server_created_at: nodeCollaborator.serverCreatedAt,
server_updated_at: nodeCollaborator.serverUpdatedAt,
server_version_id: nodeCollaborator.versionId,
})
.onConflict((cb) =>
cb
.doUpdateSet({
server_created_at: nodeCollaborator.serverCreatedAt,
server_updated_at: nodeCollaborator.serverUpdatedAt,
server_version_id: nodeCollaborator.versionId,
})
.where('version_id', '=', nodeCollaborator.versionId),
)
.execute();
}
private async updateNodeCollaborator(
userId: string,
nodeCollaborator: ServerNodeCollaborator,
): Promise<void> {
const workspaceDatabase =
await databaseManager.getWorkspaceDatabase(userId);
const existingNodeCollaborator = await workspaceDatabase
.selectFrom('node_collaborators')
.selectAll()
.where((eb) =>
eb.and([
eb('node_id', '=', nodeCollaborator.nodeId),
eb('collaborator_id', '=', nodeCollaborator.collaboratorId),
]),
)
.executeTakeFirst();
if (
existingNodeCollaborator.server_version_id === nodeCollaborator.versionId
) {
return;
}
if (existingNodeCollaborator.updated_at) {
if (!nodeCollaborator.updatedAt) {
return;
}
const localUpdatedAt = new Date(existingNodeCollaborator.updated_at);
const serverUpdatedAt = new Date(nodeCollaborator.updatedAt);
if (localUpdatedAt > serverUpdatedAt) {
return;
}
}
await workspaceDatabase
.updateTable('node_collaborators')
.set({
role: nodeCollaborator.role,
updated_at: nodeCollaborator.updatedAt,
updated_by: nodeCollaborator.updatedBy,
version_id: nodeCollaborator.versionId,
server_created_at: nodeCollaborator.serverCreatedAt,
server_updated_at: nodeCollaborator.serverUpdatedAt,
server_version_id: nodeCollaborator.versionId,
})
.where((eb) =>
eb.and([
eb('node_id', '=', nodeCollaborator.nodeId),
eb('collaborator_id', '=', nodeCollaborator.collaboratorId),
]),
)
.execute();
}
private async deleteNodeReaction(
userId: string,
nodeCollaborator: ServerNodeCollaborator,
): Promise<void> {
const workspaceDatabase =
await databaseManager.getWorkspaceDatabase(userId);
await workspaceDatabase
.deleteFrom('node_collaborators')
.where((eb) =>
eb.and([
eb('node_id', '=', nodeCollaborator.nodeId),
eb('collaborator_id', '=', nodeCollaborator.collaboratorId),
]),
)
.execute();
}
}

View File

@@ -0,0 +1,65 @@
import { databaseManager } from '@/main/data/database-manager';
import { MutationHandler, MutationResult } from '@/operations/mutations';
import { NodeReactionServerCreateMutationInput } from '@/operations/mutations/node-reaction-server-create';
export class NodeReactionServerCreateMutationHandler
implements MutationHandler<NodeReactionServerCreateMutationInput>
{
public async handleMutation(
input: NodeReactionServerCreateMutationInput,
): Promise<MutationResult<NodeReactionServerCreateMutationInput>> {
const workspace = await databaseManager.appDatabase
.selectFrom('workspaces')
.selectAll()
.where((eb) =>
eb.and([
eb('account_id', '=', input.accountId),
eb('workspace_id', '=', input.workspaceId),
]),
)
.executeTakeFirst();
if (!workspace) {
return {
output: {
success: false,
},
};
}
const userId = workspace.user_id;
const workspaceDatabase =
await databaseManager.getWorkspaceDatabase(userId);
await workspaceDatabase
.insertInto('node_reactions')
.values({
node_id: input.nodeId,
actor_id: input.actorId,
reaction: input.reaction,
created_at: input.createdAt,
server_created_at: input.serverCreatedAt,
})
.onConflict((cb) =>
cb
.doUpdateSet({
server_created_at: input.serverCreatedAt,
})
.where('server_created_at', 'is', null),
)
.execute();
return {
output: {
success: true,
},
changes: [
{
type: 'workspace',
table: 'node_reactions',
userId: userId,
},
],
};
}
}

View File

@@ -0,0 +1,58 @@
import { databaseManager } from '@/main/data/database-manager';
import { MutationHandler, MutationResult } from '@/operations/mutations';
import { NodeReactionServerDeleteMutationInput } from '@/operations/mutations/node-reaction-server-delete';
export class NodeReactionServerDeleteMutationHandler
implements MutationHandler<NodeReactionServerDeleteMutationInput>
{
public async handleMutation(
input: NodeReactionServerDeleteMutationInput,
): Promise<MutationResult<NodeReactionServerDeleteMutationInput>> {
const workspace = await databaseManager.appDatabase
.selectFrom('workspaces')
.selectAll()
.where((eb) =>
eb.and([
eb('account_id', '=', input.accountId),
eb('workspace_id', '=', input.workspaceId),
]),
)
.executeTakeFirst();
if (!workspace) {
return {
output: {
success: false,
},
};
}
const userId = workspace.user_id;
const workspaceDatabase =
await databaseManager.getWorkspaceDatabase(userId);
await workspaceDatabase
.deleteFrom('node_reactions')
.where((eb) =>
eb.and([
eb('node_id', '=', input.nodeId),
eb('actor_id', '=', input.actorId),
eb('reaction', '=', input.reaction),
]),
)
.execute();
return {
output: {
success: true,
},
changes: [
{
type: 'workspace',
table: 'node_reactions',
userId: userId,
},
],
};
}
}

View File

@@ -1,94 +0,0 @@
import { databaseManager } from '@/main/data/database-manager';
import { MutationHandler, MutationResult } from '@/operations/mutations';
import { NodeReactionSyncMutationInput } from '@/operations/mutations/node-reaction-sync';
import { ServerNodeReaction } from '@/types/nodes';
export class NodeReactionSyncMutationHandler
implements MutationHandler<NodeReactionSyncMutationInput>
{
public async handleMutation(
input: NodeReactionSyncMutationInput,
): Promise<MutationResult<NodeReactionSyncMutationInput>> {
const workspace = await databaseManager.appDatabase
.selectFrom('workspaces')
.selectAll()
.where((eb) =>
eb.and([
eb('account_id', '=', input.accountId),
eb('workspace_id', '=', input.workspaceId),
]),
)
.executeTakeFirst();
if (!workspace) {
return {
output: {
success: false,
},
};
}
const userId = workspace.user_id;
if (input.action === 'insert' && input.after) {
await this.insertNodeReaction(userId, input.after);
} else if (input.action === 'delete' && input.before) {
await this.deleteNodeReaction(userId, input.before);
}
return {
output: {
success: true,
},
changes: [
{
type: 'workspace',
table: 'node_reactions',
userId: userId,
},
],
};
}
private async insertNodeReaction(
userId: string,
nodeReaction: ServerNodeReaction,
): Promise<void> {
const workspaceDatabase =
await databaseManager.getWorkspaceDatabase(userId);
await workspaceDatabase
.insertInto('node_reactions')
.values({
node_id: nodeReaction.nodeId,
actor_id: nodeReaction.actorId,
reaction: nodeReaction.reaction,
created_at: nodeReaction.createdAt,
server_created_at: nodeReaction.serverCreatedAt,
})
.onConflict((ob) =>
ob.doUpdateSet({
server_created_at: nodeReaction.serverCreatedAt,
}),
)
.execute();
}
private async deleteNodeReaction(
userId: string,
nodeReaction: ServerNodeReaction,
): Promise<void> {
const workspaceDatabase =
await databaseManager.getWorkspaceDatabase(userId);
await workspaceDatabase
.deleteFrom('node_reactions')
.where((eb) =>
eb.and([
eb('node_id', '=', nodeReaction.nodeId),
eb('actor_id', '=', nodeReaction.actorId),
eb('reaction', '=', nodeReaction.reaction),
]),
)
.execute();
}
}

View File

@@ -0,0 +1,79 @@
import { databaseManager } from '@/main/data/database-manager';
import { MutationHandler, MutationResult } from '@/operations/mutations';
import { NodeServerCreateMutationInput } from '@/operations/mutations/node-server-create';
import { toUint8Array } from 'js-base64';
import * as Y from 'yjs';
export class NodeServerCreateMutationHandler
implements MutationHandler<NodeServerCreateMutationInput>
{
public async handleMutation(
input: NodeServerCreateMutationInput,
): Promise<MutationResult<NodeServerCreateMutationInput>> {
const workspace = await databaseManager.appDatabase
.selectFrom('workspaces')
.selectAll()
.where((eb) =>
eb.and([
eb('account_id', '=', input.accountId),
eb('workspace_id', '=', input.workspaceId),
]),
)
.executeTakeFirst();
if (!workspace) {
return {
output: {
success: false,
},
};
}
const userId = workspace.user_id;
const workspaceDatabase =
await databaseManager.getWorkspaceDatabase(userId);
const doc = new Y.Doc({
guid: input.id,
});
Y.applyUpdate(doc, toUint8Array(input.state));
const attributesMap = doc.getMap('attributes');
const attributes = JSON.stringify(attributesMap.toJSON());
await workspaceDatabase
.insertInto('nodes')
.values({
id: input.id,
attributes: attributes,
state: input.state,
created_at: input.createdAt,
created_by: input.createdBy,
version_id: input.versionId,
server_created_at: input.serverCreatedAt,
server_version_id: input.versionId,
})
.onConflict((cb) =>
cb
.doUpdateSet({
server_created_at: input.serverCreatedAt,
server_version_id: input.versionId,
})
.where('version_id', '=', input.versionId),
)
.execute();
return {
output: {
success: true,
},
changes: [
{
type: 'workspace',
table: 'nodes',
userId: userId,
},
],
};
}
}

View File

@@ -0,0 +1,52 @@
import { databaseManager } from '@/main/data/database-manager';
import { MutationHandler, MutationResult } from '@/operations/mutations';
import { NodeServerDeleteMutationInput } from '@/operations/mutations/node-server-delete';
export class NodeServerDeleteMutationHandler
implements MutationHandler<NodeServerDeleteMutationInput>
{
public async handleMutation(
input: NodeServerDeleteMutationInput,
): Promise<MutationResult<NodeServerDeleteMutationInput>> {
const workspace = await databaseManager.appDatabase
.selectFrom('workspaces')
.selectAll()
.where((eb) =>
eb.and([
eb('account_id', '=', input.accountId),
eb('workspace_id', '=', input.workspaceId),
]),
)
.executeTakeFirst();
if (!workspace) {
return {
output: {
success: false,
},
};
}
const userId = workspace.user_id;
const workspaceDatabase =
await databaseManager.getWorkspaceDatabase(userId);
await workspaceDatabase
.deleteFrom('nodes')
.where('id', '=', input.id)
.execute();
return {
output: {
success: true,
},
changes: [
{
type: 'workspace',
table: 'nodes',
userId: userId,
},
],
};
}
}

View File

@@ -0,0 +1,88 @@
import { databaseManager } from '@/main/data/database-manager';
import { MutationHandler, MutationResult } from '@/operations/mutations';
import { NodeServerUpdateMutationInput } from '@/operations/mutations/node-server-update';
import { fromUint8Array, toUint8Array } from 'js-base64';
import * as Y from 'yjs';
export class NodeServerUpdateMutationHandler
implements MutationHandler<NodeServerUpdateMutationInput>
{
public async handleMutation(
input: NodeServerUpdateMutationInput,
): Promise<MutationResult<NodeServerUpdateMutationInput>> {
const workspace = await databaseManager.appDatabase
.selectFrom('workspaces')
.selectAll()
.where((eb) =>
eb.and([
eb('account_id', '=', input.accountId),
eb('workspace_id', '=', input.workspaceId),
]),
)
.executeTakeFirst();
if (!workspace) {
return {
output: {
success: false,
},
};
}
const userId = workspace.user_id;
const workspaceDatabase =
await databaseManager.getWorkspaceDatabase(userId);
const node = await workspaceDatabase
.selectFrom('nodes')
.selectAll()
.where('id', '=', input.id)
.executeTakeFirst();
if (!node) {
return {
output: {
success: false,
},
};
}
const doc = new Y.Doc({
guid: input.id,
});
Y.applyUpdate(doc, toUint8Array(node.state));
Y.applyUpdate(doc, toUint8Array(input.update));
const attributesMap = doc.getMap('attributes');
const attributes = JSON.stringify(attributesMap.toJSON());
const state = fromUint8Array(Y.encodeStateAsUpdate(doc));
await workspaceDatabase
.updateTable('nodes')
.set({
attributes: attributes,
state: state,
updated_at: input.updatedAt,
updated_by: input.updatedBy,
version_id: input.versionId,
server_version_id: input.versionId,
server_updated_at: input.serverUpdatedAt,
})
.where('id', '=', input.id)
.execute();
return {
output: {
success: true,
},
changes: [
{
type: 'workspace',
table: 'nodes',
userId: userId,
},
],
};
}
}

View File

@@ -1,137 +0,0 @@
import { databaseManager } from '@/main/data/database-manager';
import { MutationHandler, MutationResult } from '@/operations/mutations';
import { NodeSyncMutationInput } from '@/operations/mutations/node-sync';
import { ServerNode } from '@/types/nodes';
import { fromUint8Array, toUint8Array } from 'js-base64';
import * as Y from 'yjs';
export class NodeSyncMutationHandler
implements MutationHandler<NodeSyncMutationInput>
{
public async handleMutation(
input: NodeSyncMutationInput,
): Promise<MutationResult<NodeSyncMutationInput>> {
const workspace = await databaseManager.appDatabase
.selectFrom('workspaces')
.selectAll()
.where((eb) =>
eb.and([
eb('account_id', '=', input.accountId),
eb('workspace_id', '=', input.workspaceId),
]),
)
.executeTakeFirst();
if (!workspace) {
return {
output: {
success: false,
},
};
}
const userId = workspace.user_id;
if (input.action === 'insert' && input.after) {
await this.insertNode(userId, input.after);
} else if (input.action === 'update' && input.after) {
await this.updateNode(userId, input.after);
} else if (input.action === 'delete' && input.before) {
await this.deleteNode(userId, input.before);
}
return {
output: {
success: true,
},
changes: [
{
type: 'workspace',
table: 'nodes',
userId: userId,
},
],
};
}
private async insertNode(userId: string, node: ServerNode): Promise<void> {
const workspaceDatabase =
await databaseManager.getWorkspaceDatabase(userId);
await workspaceDatabase
.insertInto('nodes')
.values({
id: node.id,
attributes: JSON.stringify(node.attributes),
state: node.state,
created_at: node.createdAt,
created_by: node.createdBy,
updated_by: node.updatedBy,
updated_at: node.updatedAt,
version_id: node.versionId,
server_created_at: node.serverCreatedAt,
server_updated_at: node.serverUpdatedAt,
server_version_id: node.versionId,
})
.onConflict((cb) =>
cb
.doUpdateSet({
server_created_at: node.serverCreatedAt,
server_updated_at: node.serverUpdatedAt,
server_version_id: node.versionId,
})
.where('version_id', '=', node.versionId),
)
.execute();
}
private async updateNode(userId: string, node: ServerNode): Promise<void> {
const workspaceDatabase =
await databaseManager.getWorkspaceDatabase(userId);
const existingNode = await workspaceDatabase
.selectFrom('nodes')
.selectAll()
.where('id', '=', node.id)
.executeTakeFirst();
if (existingNode.version_id === node.versionId) {
return;
}
const doc = new Y.Doc({
guid: node.id,
});
Y.applyUpdate(doc, toUint8Array(existingNode.state));
Y.applyUpdate(doc, toUint8Array(node.state));
const attributesMap = doc.getMap('attributes');
const attributes = JSON.stringify(attributesMap.toJSON());
const encodedState = fromUint8Array(Y.encodeStateAsUpdate(doc));
await workspaceDatabase
.updateTable('nodes')
.set({
attributes: attributes,
state: encodedState,
updated_at: node.updatedAt,
updated_by: node.updatedBy,
version_id: node.versionId,
server_created_at: node.serverCreatedAt,
server_updated_at: node.serverUpdatedAt,
server_version_id: node.versionId,
})
.where('id', '=', node.id)
.execute();
}
private async deleteNode(userId: string, node: ServerNode): Promise<void> {
const workspaceDatabase =
await databaseManager.getWorkspaceDatabase(userId);
await workspaceDatabase
.deleteFrom('nodes')
.where('id', '=', node.id)
.execute();
}
}

View File

@@ -1,8 +1,12 @@
import { BackoffCalculator } from '@/lib/backoff-calculator';
import { buildAxiosInstance } from '@/lib/servers';
import { SelectWorkspace } from '@/main/data/app/schema';
import { databaseManager } from '@/main/data/database-manager';
import { ServerSyncResponse, WorkspaceSyncData } from '@/types/sync';
import {
ServerChange,
ServerChangeData,
ServerSyncResponse,
} from '@/types/sync';
import { mediator } from '@/main/mediator';
const EVENT_LOOP_INTERVAL = 100;
@@ -26,7 +30,6 @@ class Synchronizer {
private async executeEventLoop() {
try {
await this.checkForWorkspaceSyncs();
await this.checkForWorkspaceChanges();
} catch (error) {
console.log('error', error);
@@ -35,19 +38,140 @@ class Synchronizer {
setTimeout(this.executeEventLoop, EVENT_LOOP_INTERVAL);
}
private async checkForWorkspaceSyncs(): Promise<void> {
const unsyncedWorkspaces = await databaseManager.appDatabase
.selectFrom('workspaces')
.selectAll()
.where('synced', '=', 0)
.execute();
if (unsyncedWorkspaces.length === 0) {
return;
public async handleServerChange(accountId: string, change: ServerChange) {
const executed = await this.executeServerChange(accountId, change.data);
if (executed) {
await mediator.executeMessage(
{
accountId,
deviceId: change.deviceId,
},
{
type: 'server_change_result',
changeId: change.id,
success: executed,
},
);
}
}
for (const workspace of unsyncedWorkspaces) {
await this.syncWorkspace(workspace);
private async executeServerChange(
accountId: string,
data: ServerChangeData,
): Promise<boolean> {
switch (data.type) {
case 'node_create': {
const result = await mediator.executeMutation({
type: 'node_server_create',
accountId: accountId,
workspaceId: data.workspaceId,
id: data.id,
state: data.state,
createdAt: data.createdAt,
serverCreatedAt: data.serverCreatedAt,
createdBy: data.createdBy,
versionId: data.versionId,
});
return result.success;
}
case 'node_update': {
const result = await mediator.executeMutation({
type: 'node_server_update',
accountId: accountId,
workspaceId: data.workspaceId,
id: data.id,
update: data.update,
updatedAt: data.updatedAt,
updatedBy: data.updatedBy,
versionId: data.versionId,
serverUpdatedAt: data.serverUpdatedAt,
});
return result.success;
}
case 'node_delete': {
const result = await mediator.executeMutation({
type: 'node_server_delete',
accountId: accountId,
workspaceId: data.workspaceId,
id: data.id,
});
return result.success;
}
case 'node_collaborator_create': {
const result = await mediator.executeMutation({
type: 'node_collaborator_server_create',
accountId: accountId,
workspaceId: data.workspaceId,
nodeId: data.nodeId,
collaboratorId: data.collaboratorId,
role: data.role,
createdAt: data.createdAt,
createdBy: data.createdBy,
versionId: data.versionId,
serverCreatedAt: data.serverCreatedAt,
});
return result.success;
}
case 'node_collaborator_update': {
const result = await mediator.executeMutation({
type: 'node_collaborator_server_update',
accountId: accountId,
workspaceId: data.workspaceId,
nodeId: data.nodeId,
collaboratorId: data.collaboratorId,
role: data.role,
updatedAt: data.updatedAt,
updatedBy: data.updatedBy,
versionId: data.versionId,
serverUpdatedAt: data.serverUpdatedAt,
});
return result.success;
}
case 'node_collaborator_delete': {
const result = await mediator.executeMutation({
type: 'node_collaborator_server_delete',
accountId: accountId,
workspaceId: data.workspaceId,
nodeId: data.nodeId,
collaboratorId: data.collaboratorId,
});
return result.success;
}
case 'node_reaction_create': {
const result = await mediator.executeMutation({
type: 'node_reaction_server_create',
accountId: accountId,
workspaceId: data.workspaceId,
nodeId: data.nodeId,
actorId: data.actorId,
reaction: data.reaction,
createdAt: data.createdAt,
serverCreatedAt: data.serverCreatedAt,
});
return result.success;
}
case 'node_reaction_delete': {
const result = await mediator.executeMutation({
type: 'node_reaction_server_delete',
accountId: accountId,
workspaceId: data.workspaceId,
nodeId: data.nodeId,
actorId: data.actorId,
reaction: data.reaction,
});
return result.success;
}
default: {
return false;
}
}
}
@@ -63,7 +187,6 @@ class Synchronizer {
'servers.domain',
'servers.attributes',
])
.where('workspaces.synced', '=', 1)
.execute();
for (const workspace of workspaces) {
@@ -147,127 +270,6 @@ class Synchronizer {
}
}
}
private async syncWorkspace(workspace: SelectWorkspace): Promise<void> {
if (this.workspaceBackoffs.has(workspace.user_id)) {
const backoff = this.workspaceBackoffs.get(workspace.user_id);
if (!backoff.canRetry()) {
return;
}
}
try {
const credentials = await databaseManager.appDatabase
.selectFrom('accounts')
.innerJoin('servers', 'accounts.server', 'servers.domain')
.select(['domain', 'attributes', 'token'])
.where('id', '=', workspace.account_id)
.executeTakeFirst();
if (!credentials) {
return;
}
const axios = buildAxiosInstance(
credentials.domain,
credentials.attributes,
credentials.token,
);
const { data } = await axios.get<WorkspaceSyncData>(
`/v1/sync/${workspace.workspace_id}`,
);
await databaseManager.deleteWorkspaceDatabase(
workspace.account_id,
workspace.workspace_id,
workspace.user_id,
);
const workspaceDatabase = await databaseManager.getWorkspaceDatabase(
workspace.user_id,
);
await workspaceDatabase.transaction().execute(async (trx) => {
if (data.nodes.length > 0) {
await trx
.insertInto('nodes')
.values(
data.nodes.map((node) => {
return {
id: node.id,
attributes: JSON.stringify(node.attributes),
state: node.state,
created_at: node.createdAt,
created_by: node.createdBy,
updated_at: node.updatedAt,
updated_by: node.updatedBy,
version_id: node.versionId,
server_created_at: node.serverCreatedAt,
server_updated_at: node.serverUpdatedAt,
server_version_id: node.versionId,
};
}),
)
.execute();
}
if (data.nodeReactions.length > 0) {
await trx
.insertInto('node_reactions')
.values(
data.nodeReactions.map((nodeReaction) => {
return {
node_id: nodeReaction.nodeId,
actor_id: nodeReaction.actorId,
reaction: nodeReaction.reaction,
created_at: nodeReaction.createdAt,
server_created_at: nodeReaction.serverCreatedAt,
};
}),
)
.execute();
}
if (data.nodeCollaborators.length > 0) {
await trx
.insertInto('node_collaborators')
.values(
data.nodeCollaborators.map((nodeCollaborator) => {
return {
node_id: nodeCollaborator.nodeId,
collaborator_id: nodeCollaborator.collaboratorId,
role: nodeCollaborator.role,
created_at: nodeCollaborator.createdAt,
created_by: nodeCollaborator.createdBy,
updated_at: nodeCollaborator.updatedAt,
updated_by: nodeCollaborator.updatedBy,
version_id: nodeCollaborator.versionId,
server_created_at: nodeCollaborator.serverCreatedAt,
server_updated_at: nodeCollaborator.serverUpdatedAt,
server_version_id: nodeCollaborator.versionId,
};
}),
)
.execute();
}
});
await databaseManager.appDatabase
.updateTable('workspaces')
.set({
synced: 1,
})
.where('user_id', '=', workspace.user_id)
.execute();
} catch (error) {
if (!this.workspaceBackoffs.has(workspace.user_id)) {
this.workspaceBackoffs.set(workspace.user_id, new BackoffCalculator());
}
const backoff = this.workspaceBackoffs.get(workspace.user_id);
backoff.increaseError();
}
}
}
export const synchronizer = new Synchronizer();

View File

@@ -1,10 +0,0 @@
export type ServerChangeAckMessageInput = {
type: 'server_change_ack';
changeId: string;
};
declare module '@/operations/messages' {
interface MessageMap {
server_change_ack: ServerChangeAckMessageInput;
}
}

View File

@@ -0,0 +1,12 @@
import { ServerChange } from '@/types/sync';
export type ServerChangeBatchMessageInput = {
type: 'server_change_batch';
changes: ServerChange[];
};
declare module '@/operations/messages' {
interface MessageMap {
server_change_batch: ServerChangeBatchMessageInput;
}
}

View File

@@ -0,0 +1,11 @@
export type ServerChangeResultMessageInput = {
type: 'server_change_result';
changeId: string;
success: boolean;
};
declare module '@/operations/messages' {
interface MessageMap {
server_change_result: ServerChangeResultMessageInput;
}
}

View File

@@ -0,0 +1,25 @@
export type NodeCollaboratorServerCreateMutationInput = {
type: 'node_collaborator_server_create';
accountId: string;
workspaceId: string;
nodeId: string;
collaboratorId: string;
role: string;
createdAt: string;
createdBy: string;
versionId: string;
serverCreatedAt: string;
};
export type NodeCollaboratorServerCreateMutationOutput = {
success: boolean;
};
declare module '@/operations/mutations' {
interface MutationMap {
node_collaborator_server_create: {
input: NodeCollaboratorServerCreateMutationInput;
output: NodeCollaboratorServerCreateMutationOutput;
};
}
}

View File

@@ -0,0 +1,20 @@
export type NodeCollaboratorServerDeleteMutationInput = {
type: 'node_collaborator_server_delete';
accountId: string;
workspaceId: string;
nodeId: string;
collaboratorId: string;
};
export type NodeCollaboratorServerDeleteMutationOutput = {
success: boolean;
};
declare module '@/operations/mutations' {
interface MutationMap {
node_collaborator_server_delete: {
input: NodeCollaboratorServerDeleteMutationInput;
output: NodeCollaboratorServerDeleteMutationOutput;
};
}
}

View File

@@ -0,0 +1,25 @@
export type NodeCollaboratorServerUpdateMutationInput = {
type: 'node_collaborator_server_update';
accountId: string;
workspaceId: string;
nodeId: string;
collaboratorId: string;
role: string;
updatedAt: string;
updatedBy: string;
versionId: string;
serverUpdatedAt: string;
};
export type NodeCollaboratorServerUpdateMutationOutput = {
success: boolean;
};
declare module '@/operations/mutations' {
interface MutationMap {
node_collaborator_server_update: {
input: NodeCollaboratorServerUpdateMutationInput;
output: NodeCollaboratorServerUpdateMutationOutput;
};
}
}

View File

@@ -1,22 +0,0 @@
export type NodeCollaboratorSyncMutationInput = {
type: 'node_collaborator_sync';
accountId: string;
workspaceId: string;
id: string;
action: string;
before: any;
after: any;
};
export type NodeCollaboratorSyncMutationOutput = {
success: boolean;
};
declare module '@/operations/mutations' {
interface MutationMap {
node_collaborator_sync: {
input: NodeCollaboratorSyncMutationInput;
output: NodeCollaboratorSyncMutationOutput;
};
}
}

View File

@@ -0,0 +1,23 @@
export type NodeReactionServerCreateMutationInput = {
type: 'node_reaction_server_create';
accountId: string;
workspaceId: string;
nodeId: string;
actorId: string;
reaction: string;
createdAt: string;
serverCreatedAt: string;
};
export type NodeReactionServerCreateMutationOutput = {
success: boolean;
};
declare module '@/operations/mutations' {
interface MutationMap {
node_reaction_server_create: {
input: NodeReactionServerCreateMutationInput;
output: NodeReactionServerCreateMutationOutput;
};
}
}

View File

@@ -0,0 +1,21 @@
export type NodeReactionServerDeleteMutationInput = {
type: 'node_reaction_server_delete';
accountId: string;
workspaceId: string;
nodeId: string;
actorId: string;
reaction: string;
};
export type NodeReactionServerDeleteMutationOutput = {
success: boolean;
};
declare module '@/operations/mutations' {
interface MutationMap {
node_reaction_server_delete: {
input: NodeReactionServerDeleteMutationInput;
output: NodeReactionServerDeleteMutationOutput;
};
}
}

View File

@@ -1,22 +0,0 @@
export type NodeReactionSyncMutationInput = {
type: 'node_reaction_sync';
accountId: string;
workspaceId: string;
id: string;
action: string;
before: any;
after: any;
};
export type NodeReactionSyncMutationOutput = {
success: boolean;
};
declare module '@/operations/mutations' {
interface MutationMap {
node_reaction_sync: {
input: NodeReactionSyncMutationInput;
output: NodeReactionSyncMutationOutput;
};
}
}

View File

@@ -0,0 +1,24 @@
export type NodeServerCreateMutationInput = {
type: 'node_server_create';
accountId: string;
workspaceId: string;
id: string;
state: string;
createdAt: string;
createdBy: string;
versionId: string;
serverCreatedAt: string;
};
export type NodeServerCreateMutationOutput = {
success: boolean;
};
declare module '@/operations/mutations' {
interface MutationMap {
node_server_create: {
input: NodeServerCreateMutationInput;
output: NodeServerCreateMutationOutput;
};
}
}

View File

@@ -0,0 +1,19 @@
export type NodeServerDeleteMutationInput = {
type: 'node_server_delete';
accountId: string;
workspaceId: string;
id: string;
};
export type NodeServerDeleteMutationOutput = {
success: boolean;
};
declare module '@/operations/mutations' {
interface MutationMap {
node_server_delete: {
input: NodeServerDeleteMutationInput;
output: NodeServerDeleteMutationOutput;
};
}
}

View File

@@ -0,0 +1,24 @@
export type NodeServerUpdateMutationInput = {
type: 'node_server_update';
accountId: string;
workspaceId: string;
id: string;
update: string;
updatedAt: string;
updatedBy: string;
versionId: string;
serverUpdatedAt: string;
};
export type NodeServerUpdateMutationOutput = {
success: boolean;
};
declare module '@/operations/mutations' {
interface MutationMap {
node_server_update: {
input: NodeServerUpdateMutationInput;
output: NodeServerUpdateMutationOutput;
};
}
}

View File

@@ -1,22 +0,0 @@
export type NodeSyncMutationInput = {
type: 'node_sync';
accountId: string;
workspaceId: string;
id: string;
action: string;
before: any;
after: any;
};
export type NodeSyncMutationOutput = {
success: boolean;
};
declare module '@/operations/mutations' {
interface MutationMap {
node_sync: {
input: NodeSyncMutationInput;
output: NodeSyncMutationOutput;
};
}
}

View File

@@ -4,15 +4,6 @@ import {
ServerNodeReaction,
} from '@/types/nodes';
export type ServerChange = {
id: string;
table: string;
action: string;
workspaceId: string | null;
before: any | null;
after: any | null;
};
export type ServerSyncResponse = {
results: ServerSyncChangeResult[];
};
@@ -29,3 +20,98 @@ export type WorkspaceSyncData = {
nodeReactions: ServerNodeReaction[];
nodeCollaborators: ServerNodeCollaborator[];
};
export type ServerChange = {
id: string;
workspaceId: string;
deviceId: string;
data: ServerChangeData;
createdAt: string;
};
export type ServerChangeData =
| ServerNodeCreateChangeData
| ServerNodeUpdateChangeData
| ServerNodeDeleteChangeData
| ServerNodeCollaboratorCreateChangeData
| ServerNodeCollaboratorUpdateChangeData
| ServerNodeCollaboratorDeleteChangeData
| ServerNodeReactionCreateChangeData
| ServerNodeReactionDeleteChangeData;
export type ServerNodeCreateChangeData = {
type: 'node_create';
id: string;
workspaceId: string;
state: string;
createdAt: string;
createdBy: string;
versionId: string;
serverCreatedAt: string;
};
export type ServerNodeUpdateChangeData = {
type: 'node_update';
id: string;
workspaceId: string;
update: string;
updatedAt: string;
updatedBy: string;
versionId: string;
serverUpdatedAt: string;
};
export type ServerNodeDeleteChangeData = {
type: 'node_delete';
id: string;
workspaceId: string;
};
export type ServerNodeCollaboratorCreateChangeData = {
type: 'node_collaborator_create';
nodeId: string;
collaboratorId: string;
role: string;
workspaceId: string;
createdAt: string;
createdBy: string;
versionId: string;
serverCreatedAt: string;
};
export type ServerNodeCollaboratorUpdateChangeData = {
type: 'node_collaborator_update';
nodeId: string;
collaboratorId: string;
workspaceId: string;
role: string;
updatedAt: string;
updatedBy: string;
versionId: string;
serverUpdatedAt: string;
};
export type ServerNodeCollaboratorDeleteChangeData = {
type: 'node_collaborator_delete';
nodeId: string;
collaboratorId: string;
workspaceId: string;
};
export type ServerNodeReactionCreateChangeData = {
type: 'node_reaction_create';
nodeId: string;
actorId: string;
reaction: string;
workspaceId: string;
createdAt: string;
serverCreatedAt: string;
};
export type ServerNodeReactionDeleteChangeData = {
type: 'node_reaction_delete';
nodeId: string;
actorId: string;
reaction: string;
workspaceId: string;
};