feat(core): integrate realtime features (#15003)

This commit is contained in:
DarkSky
2026-05-20 05:48:03 +08:00
committed by GitHub
parent 3e42bbf4fa
commit 9f33d37add
108 changed files with 3069 additions and 2729 deletions
@@ -1,11 +0,0 @@
query listUserAccessTokens {
currentUser {
revealedAccessTokens {
id
name
createdAt
expiresAt
token
}
}
}
@@ -1,6 +0,0 @@
query getWorkspaceEmbeddingStatus($workspaceId: String!) {
queryWorkspaceEmbeddingStatus(workspaceId: $workspaceId) {
total
embedded
}
}
@@ -1,36 +0,0 @@
fragment CurrentUserProfile on UserType {
id
name
email
avatarUrl
emailVerified
features
settings {
receiveInvitationEmail
receiveMentionEmail
receiveCommentEmail
}
quota {
name
blobLimit
storageQuota
historyPeriod
memberLimit
humanReadable {
name
blobLimit
storageQuota
historyPeriod
memberLimit
}
}
quotaUsage {
storageQuota
}
copilot {
quota {
limit
used
}
}
}
@@ -1,7 +0,0 @@
#import './fragments/current-user-profile.gql'
query getCurrentUserProfile {
currentUser {
...CurrentUserProfile
}
}
@@ -1,7 +0,0 @@
query getDocDefaultRole($workspaceId: String!, $docId: String!) {
workspace(id: $workspaceId) {
doc(docId: $docId) {
defaultRole
}
}
}
@@ -1,20 +0,0 @@
query getMembersByWorkspaceId(
$workspaceId: String!
$skip: Int
$take: Int
$query: String
) {
workspace(id: $workspaceId) {
memberCount
members(skip: $skip, take: $take, query: $query) {
id
name
email
avatarUrl
permission
inviteId
emailVerified
status
}
}
}
@@ -1,28 +0,0 @@
query getPageGrantedUsersList(
$pagination: PaginationInput!
$docId: String!
$workspaceId: String!
) {
workspace(id: $workspaceId) {
doc(docId: $docId) {
grantedUsersList(pagination: $pagination) {
totalCount
pageInfo {
endCursor
hasNextPage
}
edges {
node {
role
user {
id
name
email
avatarUrl
}
}
}
}
}
}
}
@@ -1,28 +0,0 @@
query getWorkspaceInfo($workspaceId: String!) {
workspace(id: $workspaceId) {
permissions {
Workspace_Administrators_Manage
Workspace_Blobs_List
Workspace_Blobs_Read
Workspace_Blobs_Write
Workspace_Copilot
Workspace_CreateDoc
Workspace_Delete
Workspace_Organize_Read
Workspace_Payment_Manage
Workspace_Properties_Create
Workspace_Properties_Delete
Workspace_Properties_Read
Workspace_Properties_Update
Workspace_Read
Workspace_Settings_Read
Workspace_Settings_Update
Workspace_Sync
Workspace_TransferOwner
Workspace_Users_Manage
Workspace_Users_Read
}
role
team
}
}
@@ -41,42 +41,6 @@ export const credentialsRequirementsFragment = `fragment CredentialsRequirements
...PasswordLimits
}
}`;
export const currentUserProfileFragment = `fragment CurrentUserProfile on UserType {
id
name
email
avatarUrl
emailVerified
features
settings {
receiveInvitationEmail
receiveMentionEmail
receiveCommentEmail
}
quota {
name
blobLimit
storageQuota
historyPeriod
memberLimit
humanReadable {
name
blobLimit
storageQuota
historyPeriod
memberLimit
}
}
quotaUsage {
storageQuota
}
copilot {
quota {
limit
used
}
}
}`;
export const paginatedCopilotChatsFragment = `fragment PaginatedCopilotChats on PaginatedCopilotHistoriesType {
pageInfo {
hasNextPage
@@ -117,22 +81,6 @@ export const generateUserAccessTokenMutation = {
}`,
};
export const listUserAccessTokensQuery = {
id: 'listUserAccessTokensQuery' as const,
op: 'listUserAccessTokens',
query: `query listUserAccessTokens {
currentUser {
revealedAccessTokens {
id
name
createdAt
expiresAt
token
}
}
}`,
};
export const revokeUserAccessTokenMutation = {
id: 'revokeUserAccessTokenMutation' as const,
op: 'revokeUserAccessToken',
@@ -1287,18 +1235,6 @@ export const matchFilesQuery = {
}`,
};
export const getWorkspaceEmbeddingStatusQuery = {
id: 'getWorkspaceEmbeddingStatusQuery' as const,
op: 'getWorkspaceEmbeddingStatus',
query: `query getWorkspaceEmbeddingStatus($workspaceId: String!) {
queryWorkspaceEmbeddingStatus(workspaceId: $workspaceId) {
total
embedded
}
}`,
deprecations: ["'queryWorkspaceEmbeddingStatus' is deprecated: Use realtime subscription \"workspace.embedding.progress.changed\" instead."],
};
export const queueWorkspaceEmbeddingMutation = {
id: 'queueWorkspaceEmbeddingMutation' as const,
op: 'queueWorkspaceEmbedding',
@@ -1936,17 +1872,6 @@ export const getCurrentUserFeaturesQuery = {
}`,
};
export const getCurrentUserProfileQuery = {
id: 'getCurrentUserProfileQuery' as const,
op: 'getCurrentUserProfile',
query: `query getCurrentUserProfile {
currentUser {
...CurrentUserProfile
}
}
${currentUserProfileFragment}`,
};
export const getCurrentUserQuery = {
id: 'getCurrentUserQuery' as const,
op: 'getCurrentUser',
@@ -1988,18 +1913,6 @@ export const getDocCreatedByUpdatedByListQuery = {
}`,
};
export const getDocDefaultRoleQuery = {
id: 'getDocDefaultRoleQuery' as const,
op: 'getDocDefaultRole',
query: `query getDocDefaultRole($workspaceId: String!, $docId: String!) {
workspace(id: $workspaceId) {
doc(docId: $docId) {
defaultRole
}
}
}`,
};
export const getDocLastAccessedMembersQuery = {
id: 'getDocLastAccessedMembersQuery' as const,
op: 'getDocLastAccessedMembers',
@@ -2118,27 +2031,6 @@ export const getMemberCountByWorkspaceIdQuery = {
}`,
};
export const getMembersByWorkspaceIdQuery = {
id: 'getMembersByWorkspaceIdQuery' as const,
op: 'getMembersByWorkspaceId',
query: `query getMembersByWorkspaceId($workspaceId: String!, $skip: Int, $take: Int, $query: String) {
workspace(id: $workspaceId) {
memberCount
members(skip: $skip, take: $take, query: $query) {
id
name
email
avatarUrl
permission
inviteId
emailVerified
status
}
}
}`,
deprecations: ["'permission' is deprecated: Use role instead"],
};
export const oauthProvidersQuery = {
id: 'oauthProvidersQuery' as const,
op: 'oauthProviders',
@@ -2149,35 +2041,6 @@ export const oauthProvidersQuery = {
}`,
};
export const getPageGrantedUsersListQuery = {
id: 'getPageGrantedUsersListQuery' as const,
op: 'getPageGrantedUsersList',
query: `query getPageGrantedUsersList($pagination: PaginationInput!, $docId: String!, $workspaceId: String!) {
workspace(id: $workspaceId) {
doc(docId: $docId) {
grantedUsersList(pagination: $pagination) {
totalCount
pageInfo {
endCursor
hasNextPage
}
edges {
node {
role
user {
id
name
email
avatarUrl
}
}
}
}
}
}
}`,
};
export const getPublicUserByIdQuery = {
id: 'getPublicUserByIdQuery' as const,
op: 'getPublicUserById',
@@ -2262,39 +2125,6 @@ export const getUserQuery = {
}`,
};
export const getWorkspaceInfoQuery = {
id: 'getWorkspaceInfoQuery' as const,
op: 'getWorkspaceInfo',
query: `query getWorkspaceInfo($workspaceId: String!) {
workspace(id: $workspaceId) {
permissions {
Workspace_Administrators_Manage
Workspace_Blobs_List
Workspace_Blobs_Read
Workspace_Blobs_Write
Workspace_Copilot
Workspace_CreateDoc
Workspace_Delete
Workspace_Organize_Read
Workspace_Payment_Manage
Workspace_Properties_Create
Workspace_Properties_Delete
Workspace_Properties_Read
Workspace_Properties_Update
Workspace_Read
Workspace_Settings_Read
Workspace_Settings_Update
Workspace_Sync
Workspace_TransferOwner
Workspace_Users_Manage
Workspace_Users_Read
}
role
team
}
}`,
};
export const getWorkspacePageByIdQuery = {
id: 'getWorkspacePageByIdQuery' as const,
op: 'getWorkspacePageById',
@@ -2643,17 +2473,6 @@ export const mentionUserMutation = {
}`,
};
export const notificationCountQuery = {
id: 'notificationCountQuery' as const,
op: 'notificationCount',
query: `query notificationCount {
currentUser {
notificationCount
}
}`,
deprecations: ["'notificationCount' is deprecated: Use realtime subscription \"notification.count.changed\" instead."],
};
export const pricesQuery = {
id: 'pricesQuery' as const,
op: 'prices',
@@ -3117,19 +2936,6 @@ export const workspaceByokSettingsQuery = {
}`,
};
export const getWorkspaceConfigQuery = {
id: 'getWorkspaceConfigQuery' as const,
op: 'getWorkspaceConfig',
query: `query getWorkspaceConfig($id: String!) {
workspace(id: $id) {
enableAi
enableSharing
enableUrlPreview
enableDocEmbedding
}
}`,
};
export const setEnableAiMutation = {
id: 'setEnableAiMutation' as const,
op: 'setEnableAi',
@@ -3191,19 +2997,6 @@ export const acceptInviteByInviteIdMutation = {
}`,
};
export const getWorkspaceInviteLinkQuery = {
id: 'getWorkspaceInviteLinkQuery' as const,
op: 'getWorkspaceInviteLink',
query: `query getWorkspaceInviteLink($id: String!) {
workspace(id: $id) {
inviteLink {
link
expireTime
}
}
}`,
};
export const createInviteLinkMutation = {
id: 'createInviteLinkMutation' as const,
op: 'createInviteLink',
@@ -3244,34 +3037,6 @@ export const workspaceInvoicesQuery = {
deprecations: ["'id' is deprecated: removed"],
};
export const workspaceQuotaQuery = {
id: 'workspaceQuotaQuery' as const,
op: 'workspaceQuota',
query: `query workspaceQuota($id: String!) {
workspace(id: $id) {
quota {
name
blobLimit
storageQuota
usedStorageQuota
historyPeriod
memberLimit
memberCount
overcapacityMemberCount
humanReadable {
name
blobLimit
storageQuota
historyPeriod
memberLimit
memberCount
overcapacityMemberCount
}
}
}
}`,
};
export const getWorkspaceRolePermissionsQuery = {
id: 'getWorkspaceRolePermissionsQuery' as const,
op: 'getWorkspaceRolePermissions',
@@ -1,5 +0,0 @@
query notificationCount {
currentUser {
notificationCount
}
}
@@ -1,8 +0,0 @@
query getWorkspaceConfig($id: String!) {
workspace(id: $id) {
enableAi
enableSharing
enableUrlPreview
enableDocEmbedding
}
}
@@ -1,8 +0,0 @@
query getWorkspaceInviteLink($id: String!) {
workspace(id: $id) {
inviteLink {
link
expireTime
}
}
}
@@ -1,23 +0,0 @@
query workspaceQuota($id: String!) {
workspace(id: $id) {
quota {
name
blobLimit
storageQuota
usedStorageQuota
historyPeriod
memberLimit
memberCount
overcapacityMemberCount
humanReadable {
name
blobLimit
storageQuota
historyPeriod
memberLimit
memberCount
overcapacityMemberCount
}
}
}
}
-356
View File
@@ -3785,25 +3785,6 @@ export type GenerateUserAccessTokenMutation = {
};
};
export type ListUserAccessTokensQueryVariables = Exact<{
[key: string]: never;
}>;
export type ListUserAccessTokensQuery = {
__typename?: 'Query';
currentUser: {
__typename?: 'UserType';
revealedAccessTokens: Array<{
__typename?: 'RevealedAccessToken';
id: string;
name: string;
createdAt: string;
expiresAt: string | null;
token: string;
}>;
} | null;
};
export type RevokeUserAccessTokenMutationVariables = Exact<{
id: Scalars['String']['input'];
}>;
@@ -5112,19 +5093,6 @@ export type MatchFilesQuery = {
} | null;
};
export type GetWorkspaceEmbeddingStatusQueryVariables = Exact<{
workspaceId: Scalars['String']['input'];
}>;
export type GetWorkspaceEmbeddingStatusQuery = {
__typename?: 'Query';
queryWorkspaceEmbeddingStatus: {
__typename?: 'ContextWorkspaceEmbeddingStatus';
total: number;
embedded: number;
};
};
export type QueueWorkspaceEmbeddingMutationVariables = Exact<{
workspaceId: Scalars['String']['input'];
docId: Array<Scalars['String']['input']> | Scalars['String']['input'];
@@ -6271,43 +6239,6 @@ export type CredentialsRequirementsFragment = {
};
};
export type CurrentUserProfileFragment = {
__typename?: 'UserType';
id: string;
name: string;
email: string;
avatarUrl: string | null;
emailVerified: boolean;
features: Array<FeatureType>;
settings: {
__typename?: 'UserSettingsType';
receiveInvitationEmail: boolean;
receiveMentionEmail: boolean;
receiveCommentEmail: boolean;
};
quota: {
__typename?: 'UserQuotaType';
name: string;
blobLimit: number;
storageQuota: number;
historyPeriod: number;
memberLimit: number;
humanReadable: {
__typename?: 'UserQuotaHumanReadableType';
name: string;
blobLimit: string;
storageQuota: string;
historyPeriod: string;
memberLimit: string;
};
};
quotaUsage: { __typename?: 'UserQuotaUsageType'; storageQuota: number };
copilot: {
__typename?: 'Copilot';
quota: { __typename?: 'CopilotQuota'; limit: number | null; used: number };
};
};
export type PaginatedCopilotChatsFragment = {
__typename?: 'PaginatedCopilotHistoriesType';
pageInfo: {
@@ -6388,54 +6319,6 @@ export type GetCurrentUserFeaturesQuery = {
} | null;
};
export type GetCurrentUserProfileQueryVariables = Exact<{
[key: string]: never;
}>;
export type GetCurrentUserProfileQuery = {
__typename?: 'Query';
currentUser: {
__typename?: 'UserType';
id: string;
name: string;
email: string;
avatarUrl: string | null;
emailVerified: boolean;
features: Array<FeatureType>;
settings: {
__typename?: 'UserSettingsType';
receiveInvitationEmail: boolean;
receiveMentionEmail: boolean;
receiveCommentEmail: boolean;
};
quota: {
__typename?: 'UserQuotaType';
name: string;
blobLimit: number;
storageQuota: number;
historyPeriod: number;
memberLimit: number;
humanReadable: {
__typename?: 'UserQuotaHumanReadableType';
name: string;
blobLimit: string;
storageQuota: string;
historyPeriod: string;
memberLimit: string;
};
};
quotaUsage: { __typename?: 'UserQuotaUsageType'; storageQuota: number };
copilot: {
__typename?: 'Copilot';
quota: {
__typename?: 'CopilotQuota';
limit: number | null;
used: number;
};
};
} | null;
};
export type GetCurrentUserQueryVariables = Exact<{ [key: string]: never }>;
export type GetCurrentUserQuery = {
@@ -6481,19 +6364,6 @@ export type GetDocCreatedByUpdatedByListQuery = {
};
};
export type GetDocDefaultRoleQueryVariables = Exact<{
workspaceId: Scalars['String']['input'];
docId: Scalars['String']['input'];
}>;
export type GetDocDefaultRoleQuery = {
__typename?: 'Query';
workspace: {
__typename?: 'WorkspaceType';
doc: { __typename?: 'DocType'; defaultRole: DocRole };
};
};
export type GetDocLastAccessedMembersQueryVariables = Exact<{
workspaceId: Scalars['String']['input'];
docId: Scalars['String']['input'];
@@ -6634,32 +6504,6 @@ export type GetMemberCountByWorkspaceIdQuery = {
workspace: { __typename?: 'WorkspaceType'; memberCount: number };
};
export type GetMembersByWorkspaceIdQueryVariables = Exact<{
workspaceId: Scalars['String']['input'];
skip?: InputMaybe<Scalars['Int']['input']>;
take?: InputMaybe<Scalars['Int']['input']>;
query?: InputMaybe<Scalars['String']['input']>;
}>;
export type GetMembersByWorkspaceIdQuery = {
__typename?: 'Query';
workspace: {
__typename?: 'WorkspaceType';
memberCount: number;
members: Array<{
__typename?: 'InviteUserType';
id: string;
name: string | null;
email: string | null;
avatarUrl: string | null;
permission: Permission;
inviteId: string;
emailVerified: boolean | null;
status: WorkspaceMemberStatus;
}>;
};
};
export type OauthProvidersQueryVariables = Exact<{ [key: string]: never }>;
export type OauthProvidersQuery = {
@@ -6670,45 +6514,6 @@ export type OauthProvidersQuery = {
};
};
export type GetPageGrantedUsersListQueryVariables = Exact<{
pagination: PaginationInput;
docId: Scalars['String']['input'];
workspaceId: Scalars['String']['input'];
}>;
export type GetPageGrantedUsersListQuery = {
__typename?: 'Query';
workspace: {
__typename?: 'WorkspaceType';
doc: {
__typename?: 'DocType';
grantedUsersList: {
__typename?: 'PaginatedGrantedDocUserType';
totalCount: number;
pageInfo: {
__typename?: 'PageInfo';
endCursor: string | null;
hasNextPage: boolean;
};
edges: Array<{
__typename?: 'GrantedDocUserTypeEdge';
node: {
__typename?: 'GrantedDocUserType';
role: DocRole;
user: {
__typename?: 'WorkspaceUserType';
id: string;
name: string;
email: string;
avatarUrl: string | null;
};
};
}>;
};
};
};
};
export type GetPublicUserByIdQueryVariables = Exact<{
id: Scalars['String']['input'];
}>;
@@ -6805,42 +6610,6 @@ export type GetUserQuery = {
| null;
};
export type GetWorkspaceInfoQueryVariables = Exact<{
workspaceId: Scalars['String']['input'];
}>;
export type GetWorkspaceInfoQuery = {
__typename?: 'Query';
workspace: {
__typename?: 'WorkspaceType';
role: Permission;
team: boolean;
permissions: {
__typename?: 'WorkspacePermissions';
Workspace_Administrators_Manage: boolean;
Workspace_Blobs_List: boolean;
Workspace_Blobs_Read: boolean;
Workspace_Blobs_Write: boolean;
Workspace_Copilot: boolean;
Workspace_CreateDoc: boolean;
Workspace_Delete: boolean;
Workspace_Organize_Read: boolean;
Workspace_Payment_Manage: boolean;
Workspace_Properties_Create: boolean;
Workspace_Properties_Delete: boolean;
Workspace_Properties_Read: boolean;
Workspace_Properties_Update: boolean;
Workspace_Read: boolean;
Workspace_Settings_Read: boolean;
Workspace_Settings_Update: boolean;
Workspace_Sync: boolean;
Workspace_TransferOwner: boolean;
Workspace_Users_Manage: boolean;
Workspace_Users_Read: boolean;
};
};
};
export type GetWorkspacePageByIdQueryVariables = Exact<{
workspaceId: Scalars['String']['input'];
pageId: Scalars['String']['input'];
@@ -7268,13 +7037,6 @@ export type MentionUserMutation = {
mentionUser: string;
};
export type NotificationCountQueryVariables = Exact<{ [key: string]: never }>;
export type NotificationCountQuery = {
__typename?: 'Query';
currentUser: { __typename?: 'UserType'; notificationCount: number } | null;
};
export type PricesQueryVariables = Exact<{ [key: string]: never }>;
export type PricesQuery = {
@@ -7784,21 +7546,6 @@ export type WorkspaceByokSettingsQuery = {
};
};
export type GetWorkspaceConfigQueryVariables = Exact<{
id: Scalars['String']['input'];
}>;
export type GetWorkspaceConfigQuery = {
__typename?: 'Query';
workspace: {
__typename?: 'WorkspaceType';
enableAi: boolean;
enableSharing: boolean;
enableUrlPreview: boolean;
enableDocEmbedding: boolean;
};
};
export type SetEnableAiMutationVariables = Exact<{
id: Scalars['ID']['input'];
enableAi: Scalars['Boolean']['input'];
@@ -7864,22 +7611,6 @@ export type AcceptInviteByInviteIdMutation = {
acceptInviteById: boolean;
};
export type GetWorkspaceInviteLinkQueryVariables = Exact<{
id: Scalars['String']['input'];
}>;
export type GetWorkspaceInviteLinkQuery = {
__typename?: 'Query';
workspace: {
__typename?: 'WorkspaceType';
inviteLink: {
__typename?: 'InviteLink';
link: string;
expireTime: string;
} | null;
};
};
export type CreateInviteLinkMutationVariables = Exact<{
workspaceId: Scalars['String']['input'];
expireTime: WorkspaceInviteLinkExpireTime;
@@ -7928,38 +7659,6 @@ export type WorkspaceInvoicesQuery = {
};
};
export type WorkspaceQuotaQueryVariables = Exact<{
id: Scalars['String']['input'];
}>;
export type WorkspaceQuotaQuery = {
__typename?: 'Query';
workspace: {
__typename?: 'WorkspaceType';
quota: {
__typename?: 'WorkspaceQuotaType';
name: string;
blobLimit: number;
storageQuota: number;
usedStorageQuota: number;
historyPeriod: number;
memberLimit: number;
memberCount: number;
overcapacityMemberCount: number;
humanReadable: {
__typename?: 'WorkspaceQuotaHumanReadableType';
name: string;
blobLimit: string;
storageQuota: string;
historyPeriod: string;
memberLimit: string;
memberCount: string;
overcapacityMemberCount: string;
};
};
};
};
export type GetWorkspaceRolePermissionsQueryVariables = Exact<{
id: Scalars['String']['input'];
}>;
@@ -8016,11 +7715,6 @@ export type GrantWorkspaceTeamMemberMutation = {
};
export type Queries =
| {
name: 'listUserAccessTokensQuery';
variables: ListUserAccessTokensQueryVariables;
response: ListUserAccessTokensQuery;
}
| {
name: 'adminAllSharedLinksQuery';
variables: AdminAllSharedLinksQueryVariables;
@@ -8136,11 +7830,6 @@ export type Queries =
variables: MatchFilesQueryVariables;
response: MatchFilesQuery;
}
| {
name: 'getWorkspaceEmbeddingStatusQuery';
variables: GetWorkspaceEmbeddingStatusQueryVariables;
response: GetWorkspaceEmbeddingStatusQuery;
}
| {
name: 'getCopilotHistoryIdsQuery';
variables: GetCopilotHistoryIdsQueryVariables;
@@ -8226,11 +7915,6 @@ export type Queries =
variables: GetCurrentUserFeaturesQueryVariables;
response: GetCurrentUserFeaturesQuery;
}
| {
name: 'getCurrentUserProfileQuery';
variables: GetCurrentUserProfileQueryVariables;
response: GetCurrentUserProfileQuery;
}
| {
name: 'getCurrentUserQuery';
variables: GetCurrentUserQueryVariables;
@@ -8241,11 +7925,6 @@ export type Queries =
variables: GetDocCreatedByUpdatedByListQueryVariables;
response: GetDocCreatedByUpdatedByListQuery;
}
| {
name: 'getDocDefaultRoleQuery';
variables: GetDocDefaultRoleQueryVariables;
response: GetDocDefaultRoleQuery;
}
| {
name: 'getDocLastAccessedMembersQuery';
variables: GetDocLastAccessedMembersQueryVariables;
@@ -8271,21 +7950,11 @@ export type Queries =
variables: GetMemberCountByWorkspaceIdQueryVariables;
response: GetMemberCountByWorkspaceIdQuery;
}
| {
name: 'getMembersByWorkspaceIdQuery';
variables: GetMembersByWorkspaceIdQueryVariables;
response: GetMembersByWorkspaceIdQuery;
}
| {
name: 'oauthProvidersQuery';
variables: OauthProvidersQueryVariables;
response: OauthProvidersQuery;
}
| {
name: 'getPageGrantedUsersListQuery';
variables: GetPageGrantedUsersListQueryVariables;
response: GetPageGrantedUsersListQuery;
}
| {
name: 'getPublicUserByIdQuery';
variables: GetPublicUserByIdQueryVariables;
@@ -8311,11 +7980,6 @@ export type Queries =
variables: GetUserQueryVariables;
response: GetUserQuery;
}
| {
name: 'getWorkspaceInfoQuery';
variables: GetWorkspaceInfoQueryVariables;
response: GetWorkspaceInfoQuery;
}
| {
name: 'getWorkspacePageByIdQuery';
variables: GetWorkspacePageByIdQueryVariables;
@@ -8391,11 +8055,6 @@ export type Queries =
variables: ListNotificationsQueryVariables;
response: ListNotificationsQuery;
}
| {
name: 'notificationCountQuery';
variables: NotificationCountQueryVariables;
response: NotificationCountQuery;
}
| {
name: 'pricesQuery';
variables: PricesQueryVariables;
@@ -8426,26 +8085,11 @@ export type Queries =
variables: WorkspaceByokSettingsQueryVariables;
response: WorkspaceByokSettingsQuery;
}
| {
name: 'getWorkspaceConfigQuery';
variables: GetWorkspaceConfigQueryVariables;
response: GetWorkspaceConfigQuery;
}
| {
name: 'getWorkspaceInviteLinkQuery';
variables: GetWorkspaceInviteLinkQueryVariables;
response: GetWorkspaceInviteLinkQuery;
}
| {
name: 'workspaceInvoicesQuery';
variables: WorkspaceInvoicesQueryVariables;
response: WorkspaceInvoicesQuery;
}
| {
name: 'workspaceQuotaQuery';
variables: WorkspaceQuotaQueryVariables;
response: WorkspaceQuotaQuery;
}
| {
name: 'getWorkspaceRolePermissionsQuery';
variables: GetWorkspaceRolePermissionsQueryVariables;
@@ -235,11 +235,19 @@ class SocketManager {
},
};
}
reset() {
this.socket.disconnect();
}
}
const SOCKET_MANAGER_CACHE = new Map<string, SocketManager>();
function getSocketManagerKey(endpoint: string, isSelfHosted: boolean) {
return `${endpoint}:${isSelfHosted ? 'selfhosted' : 'cloud'}`;
}
function getSocketManager(endpoint: string, isSelfHosted: boolean) {
const key = `${endpoint}:${isSelfHosted ? 'selfhosted' : 'cloud'}`;
const key = getSocketManagerKey(endpoint, isSelfHosted);
let manager = SOCKET_MANAGER_CACHE.get(key);
if (!manager) {
manager = new SocketManager(endpoint, isSelfHosted);
@@ -252,6 +260,12 @@ export class SocketConnection extends AutoReconnectConnection<{
socket: Socket;
disconnect: () => void;
}> {
static resetSharedConnection(endpoint: string, isSelfHosted: boolean) {
SOCKET_MANAGER_CACHE.get(
getSocketManagerKey(endpoint, isSelfHosted)
)?.reset();
}
manager = getSocketManager(this.endpoint, this.isSelfHosted);
constructor(
@@ -41,10 +41,15 @@ class FakeSocket {
}
}
const { resetSharedConnection } = vi.hoisted(() => ({
resetSharedConnection: vi.fn(),
}));
const socket = new FakeSocket();
vi.mock('../../impls/cloud/socket', () => ({
SocketConnection: class {
static resetSharedConnection = resetSharedConnection;
readonly inner = { socket };
status = 'connected';
readonly maybeConnection = { socket };
@@ -68,6 +73,7 @@ beforeEach(() => {
socket.nextSubscriptionId = 0;
socket.connected = true;
socket.disconnected = false;
resetSharedConnection.mockClear();
});
test('getRealtimeInputKey is deterministic for realtime subscription inputs', () => {
@@ -124,6 +130,33 @@ test('request rejects server ack error', async () => {
);
});
test('user profile request can bootstrap without authenticated context', async () => {
const manager = new RealtimeManager();
manager.setContext({
endpoint: 'http://server',
isSelfHosted: false,
authenticated: false,
});
socket.nextRequestAck = { data: { user: null } };
await expect(manager.request('user.profile.get', {})).resolves.toEqual({
user: null,
});
});
test('non-bootstrap request still requires authenticated context', async () => {
const manager = new RealtimeManager();
manager.setContext({
endpoint: 'http://server',
isSelfHosted: false,
authenticated: false,
});
await expect(manager.request('notification.count.get', {})).rejects.toThrow(
'Realtime is not authenticated'
);
});
test('request rejects when aborted', async () => {
const manager = new RealtimeManager();
manager.setContext({
@@ -203,7 +236,7 @@ test('unsubscribe leaves server room and clears status', async () => {
});
});
test('context switch disconnects socket and completes subscriptions', async () => {
test('context switch disconnects socket and keeps subscriptions for reauth', async () => {
const manager = new RealtimeManager();
manager.setContext({
endpoint: 'http://server',
@@ -223,11 +256,95 @@ test('context switch disconnects socket and completes subscriptions', async () =
});
expect(socket.disconnected).toBe(true);
expect(completed).toHaveBeenCalled();
expect(completed).not.toHaveBeenCalled();
expect(manager.getStatus()).toMatchObject({
endpoint: 'http://other-server',
connected: false,
subscriptions: 0,
subscriptions: 1,
});
});
test('auth context switch resets shared socket connection', () => {
const manager = new RealtimeManager();
manager.setContext({
endpoint: 'http://server',
isSelfHosted: false,
authenticated: true,
});
manager.setContext({
endpoint: 'http://server',
isSelfHosted: false,
authenticated: false,
});
expect(resetSharedConnection).toHaveBeenCalledWith('http://server', false);
});
test('context switch resubscribes existing subscriptions on next connect', async () => {
const manager = new RealtimeManager();
manager.setContext({
endpoint: 'http://server',
isSelfHosted: false,
authenticated: true,
});
const received: unknown[] = [];
const subscription = manager
.subscribe('notification.count.changed', {})
.subscribe(event => received.push(event));
await vi.waitFor(() => expect(received).toEqual([{ type: 'ready' }]));
manager.setContext({
endpoint: 'http://other-server',
isSelfHosted: false,
authenticated: true,
});
await manager.request('notification.count.get', {});
await vi.waitFor(() =>
expect(
socket.emitted.filter(item => item.event === 'realtime:subscribe')
).toHaveLength(2)
);
expect(received).toEqual([{ type: 'ready' }, { type: 'ready' }]);
subscription.unsubscribe();
});
test('unsubscribe uses resubscribed server subscription id', async () => {
const manager = new RealtimeManager();
manager.setContext({
endpoint: 'http://server',
isSelfHosted: false,
authenticated: true,
});
const subscription = manager
.subscribe('notification.count.changed', {})
.subscribe();
await vi.waitFor(() => expect(manager.getStatus().subscriptions).toBe(1));
manager.setContext({
endpoint: 'http://other-server',
isSelfHosted: false,
authenticated: true,
});
await manager.request('notification.count.get', {});
await vi.waitFor(() =>
expect(
socket.emitted.filter(item => item.event === 'realtime:subscribe')
).toHaveLength(2)
);
subscription.unsubscribe();
expect(manager.getStatus().subscriptions).toBe(0);
expect(socket.emitted.at(-1)).toEqual({
event: 'realtime:unsubscribe',
payload: {
subscriptionId: 'sub-2',
topic: 'notification.count.changed',
input: {},
clientVersion: 'test',
},
});
});
+32 -12
View File
@@ -37,6 +37,7 @@ export class RealtimeManager {
private socketConnection?: SocketConnection;
private socketKey?: string;
private lastError?: { name: string; message: string };
private subscriptionsNeedResubscribe = false;
private readonly subscriptions = new Map<
string,
{
@@ -44,21 +45,32 @@ export class RealtimeManager {
input: RealtimeTopicInputOf<RealtimeTopicName>;
inputKey: string;
subject$: Subject<RealtimeEvent | RealtimeSubscriptionReady>;
onResubscribed: (subscriptionId: string) => void;
}
>();
setContext(context: RealtimeContext) {
const nextContext = { ...context };
const previousContext = this.context;
const changed =
!this.context ||
this.context.endpoint !== nextContext.endpoint ||
this.context.isSelfHosted !== nextContext.isSelfHosted ||
this.context.authenticated !== nextContext.authenticated;
!previousContext ||
previousContext.endpoint !== nextContext.endpoint ||
previousContext.isSelfHosted !== nextContext.isSelfHosted ||
previousContext.authenticated !== nextContext.authenticated;
this.context = nextContext;
if (changed) {
this.resetConnection();
if (
previousContext &&
previousContext.authenticated !== nextContext.authenticated
) {
SocketConnection.resetSharedConnection(
previousContext.endpoint,
previousContext.isSelfHosted
);
}
}
}
@@ -67,7 +79,7 @@ export class RealtimeManager {
input: RealtimeRequestInputOf<Op>,
options?: { timeoutMs?: number; signal?: AbortSignal }
): Promise<RealtimeRequestOutputOf<Op>> {
const socket = await this.connect();
const socket = await this.connect(op === 'user.profile.get');
const timeoutMs = options?.timeoutMs ?? DEFAULT_REQUEST_TIMEOUT;
let timeoutId: ReturnType<typeof setTimeout> | undefined;
let abortHandler: (() => void) | undefined;
@@ -154,6 +166,9 @@ export class RealtimeManager {
input,
inputKey: getRealtimeInputKey(input),
subject$,
onResubscribed: nextSubscriptionId => {
subscriptionId = nextSubscriptionId;
},
});
subscriber.next({
type: 'ready',
@@ -167,7 +182,7 @@ export class RealtimeManager {
}
},
error: error => subscriber.error(error),
complete: () => subscriber.complete(),
complete: () => {},
});
} catch (error) {
this.lastError = normalizeError(error);
@@ -209,8 +224,11 @@ export class RealtimeManager {
};
}
private async connect() {
if (!this.context?.endpoint || !this.context.authenticated) {
private async connect(allowUnauthenticated = false) {
if (
!this.context?.endpoint ||
(!this.context.authenticated && !allowUnauthenticated)
) {
const error = new Error('Realtime is not authenticated');
error.name = 'RealtimeUnauthenticated';
throw error;
@@ -232,6 +250,9 @@ export class RealtimeManager {
this.socketConnection.inner.socket.on('realtime:event', this.handleEvent);
this.socketConnection.inner.socket.off('connect', this.handleReconnect);
this.socketConnection.inner.socket.on('connect', this.handleReconnect);
if (this.subscriptionsNeedResubscribe && this.context.authenticated) {
await this.resubscribeAll();
}
return this.socketConnection.inner.socket;
}
@@ -272,6 +293,7 @@ export class RealtimeManager {
this.subscriptions.delete(subscriptionId);
this.subscriptions.set(ack.data.subscriptionId, subscription);
subscription.onResubscribed(ack.data.subscriptionId);
subscription.subject$.next({
type: 'ready',
});
@@ -281,6 +303,7 @@ export class RealtimeManager {
subscription.subject$.error(error);
}
}
this.subscriptionsNeedResubscribe = false;
}
private resetConnection() {
@@ -297,9 +320,6 @@ export class RealtimeManager {
}
this.socketConnection = undefined;
this.socketKey = undefined;
for (const subscription of this.subscriptions.values()) {
subscription.subject$.complete();
}
this.subscriptions.clear();
this.subscriptionsNeedResubscribe = this.subscriptions.size > 0;
}
}
+191 -3
View File
@@ -1,9 +1,50 @@
import type { CommentChangeObjectType } from '@affine/graphql';
export type RealtimeRequestName = keyof RealtimeRequestMap;
export type RealtimeTopicName = keyof RealtimeTopicMap;
export const WORKSPACE_MEMBERS_REQUEST_TAKE_MAX = 100;
export interface RealtimeRequestMap {
'workspace.access.get': {
input: { workspaceId: string };
output: { access: WorkspaceAccessSnapshot };
};
'workspace.config.get': {
input: { workspaceId: string };
output: { config: WorkspaceConfigSnapshot };
};
'workspace.members.get': {
input: {
workspaceId: string;
skip?: number;
take?: number;
query?: string;
};
output: { members: WorkspaceMemberSnapshot[]; memberCount: number };
};
'workspace.invite-link.get': {
input: { workspaceId: string };
output: { inviteLink: WorkspaceInviteLinkSnapshot | null };
};
'doc.share-state.get': {
input: { workspaceId: string; docId: string };
output: { state: DocShareStateSnapshot | null };
};
'doc.grants.get': {
input: { workspaceId: string; docId: string; pagination: PaginationInput };
output: PaginatedDocGrantedUsersSnapshot;
};
'user.profile.get': {
input: Record<string, never>;
output: { user: CurrentUserProfileSnapshot | null };
};
'user.settings.get': {
input: Record<string, never>;
output: { settings: UserSettingsSnapshot };
};
'user.access-tokens.get': {
input: Record<string, never>;
output: { tokens: AccessTokenSnapshot[] };
};
'notification.count.get': {
input: Record<string, never>;
output: { count: number };
@@ -16,7 +57,7 @@ export interface RealtimeRequestMap {
first?: number;
};
output: {
changes: CommentChangeObjectType[];
changes: CommentChangeSnapshot[];
startCursor: string;
endCursor: string;
hasNextPage: boolean;
@@ -44,6 +85,117 @@ export interface RealtimeRequestMap {
};
}
export type WorkspaceRoleSnapshot =
| 'Owner'
| 'Admin'
| 'Collaborator'
| 'External'
| string;
export type DocRoleSnapshot =
| 'Owner'
| 'Manager'
| 'Editor'
| 'Commenter'
| 'Reader'
| 'External'
| string;
export type PublicDocModeSnapshot = 'Page' | 'Edgeless' | string;
export interface WorkspaceAccessSnapshot {
role: WorkspaceRoleSnapshot;
permissions: Record<string, boolean>;
team: boolean;
}
export interface WorkspaceConfigSnapshot {
enableAi: boolean;
enableSharing: boolean;
enableUrlPreview: boolean;
enableDocEmbedding: boolean;
}
export interface WorkspaceMemberSnapshot {
id: string;
name: string;
email: string;
avatarUrl: string | null;
permission: WorkspaceRoleSnapshot;
role: WorkspaceRoleSnapshot;
inviteId: string;
emailVerified: boolean | null;
status: string;
}
export interface WorkspaceInviteLinkSnapshot {
link: string;
expireTime: string;
}
export interface DocShareStateSnapshot {
public: boolean;
mode: PublicDocModeSnapshot;
defaultRole: DocRoleSnapshot;
}
export interface PaginationInput {
first: number;
offset?: number;
after?: string;
}
export interface DocGrantedUserSnapshot {
role: DocRoleSnapshot;
user: {
id: string;
name: string;
email: string;
avatarUrl: string | null;
};
}
export interface PaginatedDocGrantedUsersSnapshot {
totalCount: number;
pageInfo: {
endCursor: string | null;
hasNextPage: boolean;
};
edges: { node: DocGrantedUserSnapshot }[];
}
export interface CurrentUserProfileSnapshot {
id: string;
name: string;
email: string;
emailVerified: boolean;
hasPassword: boolean | null;
avatarUrl: string | null;
features?: string[];
}
export interface UserSettingsSnapshot {
receiveInvitationEmail: boolean;
receiveMentionEmail: boolean;
receiveCommentEmail: boolean;
}
export interface AccessTokenSnapshot {
id: string;
name: string;
createdAt: string;
expiresAt: string | null;
}
export type CommentChangeActionSnapshot = 'update' | 'delete';
export interface CommentChangeSnapshot {
id: string;
action: CommentChangeActionSnapshot;
item: object;
commentId: string | null;
}
export interface UserQuotaStateSnapshot {
userId: string;
plan: string;
@@ -101,6 +253,42 @@ export type WorkspaceEmbeddingProgressReason =
| 'resync';
export interface RealtimeTopicMap {
'workspace.access.changed': {
input: { workspaceId: string };
event: { changed: true; reason: string };
};
'workspace.config.changed': {
input: { workspaceId: string };
event: { changed: true; reason: string };
};
'workspace.members.changed': {
input: { workspaceId: string };
event: { changed: true; reason: string };
};
'workspace.invite-link.changed': {
input: { workspaceId: string };
event: { changed: true; reason: string };
};
'doc.share-state.changed': {
input: { workspaceId: string; docId: string };
event: { changed: true; reason: string };
};
'doc.grants.changed': {
input: { workspaceId: string; docId: string };
event: { changed: true; reason: string };
};
'user.profile.changed': {
input: Record<string, never>;
event: { changed: true; reason: string };
};
'user.settings.changed': {
input: Record<string, never>;
event: { changed: true; reason: string };
};
'user.access-tokens.changed': {
input: Record<string, never>;
event: { changed: true; reason: string };
};
'notification.count.changed': {
input: Record<string, never>;
event: {