Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
"cozy-logger": "^1.10.4",
"cozy-minilog": "^3.3.1",
"cozy-pouch-link": "^60.10.1",
"cozy-realtime": "^5.7.0",
"cozy-realtime": "^5.8.0",
"cozy-tsconfig": "^1.2.0",
"flexsearch": "^0.7.43",
"lodash": "^4.17.21",
Expand Down
1 change: 1 addition & 0 deletions src/@types/cozy-client.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,7 @@ declare module 'cozy-client' {
addReferencesTo: (references: object, dirs: object[]) => Promise<void>
// eslint-disable-next-line @typescript-eslint/no-explicit-any
launch: (trigger: any) => any
fetchSharedDrives: () => Promise<{ data: Array<{ _id: string }> }>
}

export default class CozyClient {
Expand Down
9 changes: 8 additions & 1 deletion src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React from 'react'
import React, { useEffect } from 'react'

import { useClient } from 'cozy-client'
import flag from 'cozy-flags'
import Minilog from 'cozy-minilog'

import { ParentWindowProvider } from '@/dataproxy/parent/ParentWindowProvider'
Expand All @@ -15,6 +16,12 @@ const App = (): JSX.Element => {
const client = useClient()
const { worker, workerState } = useSharedWorker()

useEffect(() => {
if (flag('dataproxy.cleanPouch') === true) {
worker.forceSyncPouch({ clean: true })
}
}, [])

return (
<div className="content">
<h1>Cozy DataProxy</h1>
Expand Down
1 change: 1 addition & 0 deletions src/consts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,6 @@ export const ACCOUNTS_DOCTYPE = 'io.cozy.accounts'
export const KONNECTORS_DOCTYPE = 'io.cozy.konnectors'
export const TRIGGERS_DOCTYPE = 'io.cozy.triggers'
export const SETTINGS_DOCTYPE = 'io.cozy.settings'
export const SHARED_DRIVE_FILE_DOCTYPE = 'io.cozy.files.shareddrives'

export const LOCALSTORAGE_KEY_DELETING_DATA = 'deletingLocalData'
9 changes: 7 additions & 2 deletions src/dataproxy/common/DataProxyInterface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,18 @@ export interface SearchOptions {

export interface DataProxyWorker {
search: (query: string, options: SearchOptions) => unknown
setup: (clientData: ClientData) => Promise<void>
forceSyncPouch: () => void
setup: (
clientData: ClientData,
options?: { sharedDriveIds: string[] }
) => Promise<void>
forceSyncPouch: (options?: { clean: boolean }) => Promise<void>
requestLink: (
definition: QueryDefinition | Mutation,
options?: QueryOptions | MutationOptions
) => Promise<unknown>
reconnectRealtime: () => void
addSharedDrive: (driveId: string) => void
removeSharedDrive: (driveId: string) => void
}

export interface DataProxyWorkerPartialState {
Expand Down
36 changes: 28 additions & 8 deletions src/dataproxy/worker/SharedWorkerProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import * as Comlink from 'comlink'
import React, { useState, useEffect, ReactNode } from 'react'

import { useClient } from 'cozy-client'
import flag from 'cozy-flags'
import Minilog from 'cozy-minilog'

import {
Expand All @@ -11,6 +12,7 @@ import {
} from '@/dataproxy/common/DataProxyInterface'
import { TabCountSync } from '@/dataproxy/common/TabCountSync'
import { removeStaleLocalData } from '@/dataproxy/worker/data'
import { useSharedDriveRealtime } from '@/dataproxy/worker/useSharedDriveRealtime'

const log = Minilog('👷‍♂️ [SharedWorkerProvider]')

Expand Down Expand Up @@ -41,13 +43,28 @@ export const SharedWorkerProvider = React.memo(
const [tabCount, setTabCount] = useState(0)
const client = useClient()

useSharedDriveRealtime(client, worker)

useEffect(() => {
if (!client) return

const doAsync = async (): Promise<void> => {
// Cleanup any remaining local data
await removeStaleLocalData()

let sharedDriveIds: string[] = []

// Fetch shared drives only if the feature is enabled
if (flag('drive.shared-drive.enabled')) {
const { data: sharedDrives } = await client
.collection('io.cozy.sharings')
.fetchSharedDrives()

sharedDriveIds = sharedDrives
.filter(drive => !drive.owner)
.map((drive: { _id: string }) => drive._id)
}

log.debug('Init SharedWorker')

let obj: Comlink.Remote<DataProxyWorker>
Expand Down Expand Up @@ -78,14 +95,17 @@ export const SharedWorkerProvider = React.memo(
log.debug('Provide CozyClient data to Worker')
const { uri, token } = client.getStackClient()

await obj.setup({
uri,
token: token.token,
instanceOptions: client.instanceOptions,
capabilities: client.capabilities,
useRemoteData
})
setWorker(() => obj)
await obj.setup(
{
uri,
token: token.token,
instanceOptions: client.instanceOptions,
capabilities: client.capabilities,
useRemoteData
},
{ sharedDriveIds } as { sharedDriveIds: string[] }
)
setWorker((): Comlink.Remote<DataProxyWorker> => obj)
}

doAsync()
Expand Down
8 changes: 7 additions & 1 deletion src/dataproxy/worker/data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,13 @@ export const queryIsTrustedDevice = async (
.fetchJSON('GET', '/settings/sessions/current')

const isLongRun = resp?.data?.attributes?.long_run
return !!isLongRun
const isUnDefined = isLongRun === undefined

if (isUnDefined) {
return true // special case for twake instances with linagora SSO
} else {
return !!isLongRun
}
}

const deleteDatabases = async (): Promise<void> => {
Expand Down
207 changes: 207 additions & 0 deletions src/dataproxy/worker/useSharedDriveRealtime.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
import { renderHook } from '@testing-library/react'

import type CozyClient from 'cozy-client'
import flag from 'cozy-flags'

import { DataProxyWorker } from '@/dataproxy/common/DataProxyInterface'

import { useSharedDriveRealtime } from './useSharedDriveRealtime'

jest.mock('cozy-flags')

describe('useSharedDriveRealtime', () => {
const mockFlag = flag as jest.MockedFunction<typeof flag>

let mockSubscribe: jest.Mock
let mockUnsubscribe: jest.Mock
let mockAddSharedDrive: jest.Mock
let mockRemoveSharedDrive: jest.Mock

type RealtimeHandler = (event: unknown) => void

const getHandler = (event: 'created' | 'deleted'): RealtimeHandler => {
const calls = mockSubscribe.mock.calls as Array<
[string, string, RealtimeHandler]
>
return calls.find(call => call[0] === event)?.[2] as RealtimeHandler
}

const createMockClient = (hasRealtime = true): CozyClient => {
const client = {
plugins: hasRealtime
? {
realtime: {
subscribe: mockSubscribe,
unsubscribe: mockUnsubscribe
}
}
: {}
} as unknown as CozyClient
return client
}

const createMockWorker = (): DataProxyWorker => {
return {
addSharedDrive: mockAddSharedDrive,
removeSharedDrive: mockRemoveSharedDrive
} as unknown as DataProxyWorker
}

beforeEach(() => {
mockSubscribe = jest.fn()
mockUnsubscribe = jest.fn()
mockAddSharedDrive = jest.fn()
mockRemoveSharedDrive = jest.fn()
mockFlag.mockReturnValue(true)
})

afterEach(() => {
jest.clearAllMocks()
})

it('should not subscribe when client is null', () => {
renderHook(() => useSharedDriveRealtime(null, createMockWorker()))

expect(mockSubscribe).not.toHaveBeenCalled()
})

it('should not subscribe when feature flag is disabled', () => {
mockFlag.mockReturnValue(false)

renderHook(() =>
useSharedDriveRealtime(createMockClient(), createMockWorker())
)

expect(mockSubscribe).not.toHaveBeenCalled()
})

it('should throw error when realtime plugin is missing', () => {
const consoleErrorSpy = jest
.spyOn(console, 'error')
.mockImplementation(() => {})

expect(() => {
renderHook(() =>
useSharedDriveRealtime(createMockClient(false), createMockWorker())
)
}).toThrow('You must include the realtime plugin to use RealTimeQueries')

consoleErrorSpy.mockRestore()
})

it('should subscribe to created and deleted events', () => {
renderHook(() =>
useSharedDriveRealtime(createMockClient(), createMockWorker())
)

expect(mockSubscribe).toHaveBeenCalledTimes(2)
expect(mockSubscribe).toHaveBeenCalledWith(
'created',
'io.cozy.files',
expect.any(Function)
)
expect(mockSubscribe).toHaveBeenCalledWith(
'deleted',
'io.cozy.files',
expect.any(Function)
)
})

it('should handle file created event for shared drive', () => {
const mockWorker = createMockWorker()
renderHook(() => useSharedDriveRealtime(createMockClient(), mockWorker))

// Get the created event handler
const createdHandler = getHandler('created')

expect(createdHandler).toBeDefined()

// Simulate a shared drive creation event
const event = {
dir_id: 'io.cozy.files.shared-drives-dir',
class: 'shortcut',
referenced_by: [{ id: 'shared-drive-123' }]
}

createdHandler(event)

expect(mockAddSharedDrive).toHaveBeenCalledWith('shared-drive-123')
})

it('should handle file deleted event for shared drive', () => {
const mockWorker = createMockWorker()
renderHook(() => useSharedDriveRealtime(createMockClient(), mockWorker))

// Get the deleted event handler
const deletedHandler = getHandler('deleted')

expect(deletedHandler).toBeDefined()

// Simulate a shared drive deletion event
const event = {
dir_id: 'io.cozy.files.shared-drives-dir',
class: 'shortcut',
referenced_by: [{ id: 'shared-drive-456' }]
}

deletedHandler(event)

expect(mockRemoveSharedDrive).toHaveBeenCalledWith('shared-drive-456')
})

it('should ignore events with wrong dir_id', () => {
const mockWorker = createMockWorker()
renderHook(() => useSharedDriveRealtime(createMockClient(), mockWorker))

const createdHandler = getHandler('created')

const event = {
dir_id: 'some-other-dir',
class: 'shortcut',
referenced_by: [{ id: 'shared-drive-123' }]
}

createdHandler?.(event)

expect(mockAddSharedDrive).not.toHaveBeenCalled()
})

it('should ignore events with invalid event type', () => {
const mockWorker = createMockWorker()
renderHook(() => useSharedDriveRealtime(createMockClient(), mockWorker))

const createdHandler = getHandler('created')

// Invalid event types
createdHandler?.(null)
createdHandler?.(undefined)
createdHandler?.('string')
createdHandler?.(123)

expect(mockAddSharedDrive).not.toHaveBeenCalled()
})

it('should unsubscribe on unmount', () => {
const { unmount } = renderHook(() =>
useSharedDriveRealtime(createMockClient(), createMockWorker())
)

// Get the handlers that were subscribed
const createdHandler = getHandler('created')
const deletedHandler = getHandler('deleted')

unmount()

expect(mockUnsubscribe).toHaveBeenCalledTimes(2)
expect(mockUnsubscribe).toHaveBeenCalledWith(
'created',
'io.cozy.files',
createdHandler
)
expect(mockUnsubscribe).toHaveBeenCalledWith(
'deleted',
'io.cozy.files',
deletedHandler
)
})
})
Loading