Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: Move waiting for lock out of adapters into controller #1488

Merged
merged 3 commits into from
Dec 20, 2023
Merged
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
6 changes: 6 additions & 0 deletions _locales/en/messages.json
Original file line number Diff line number Diff line change
Expand Up @@ -671,5 +671,11 @@
},
"LabelImportsuccessful": {
"message": "Successfully imported profile(s)"
},
"DescriptionSyncinprogress": {
"message": "Synchronization in progress."
},
"DescriptionSyncscheduled": {
"message": "This profile will be synced soon. We're either waiting for other devices of yours, or other profiles on this device, to finish syncing."
}
}
9 changes: 1 addition & 8 deletions manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,6 @@
"128": "icons/logo_128.png"
},

"browser_specific_settings": {
"gecko": {
"id": "floccus@handmadeideas.org",
"strict_min_version": "109.0"
}
},

"default_locale": "en",

"permissions": ["alarms", "bookmarks", "storage", "unlimitedStorage", "tabs", "identity"],
Expand All @@ -42,6 +35,6 @@
},

"background": {
"scripts": ["dist/js/background-script.js"]
"service_worker": "dist/js/background-script.js"
}
}
8 changes: 8 additions & 0 deletions src/errors/Error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -305,3 +305,11 @@ export class MissingPermissionsError extends FloccusError {
Object.setPrototypeOf(this, MissingPermissionsError.prototype)
}
}

export class ResourceLockedError extends FloccusError {
constructor() {
super(`E037: Resource is locked`)
this.code = 37
Object.setPrototypeOf(this, MissingPermissionsError.prototype)
}
}
25 changes: 22 additions & 3 deletions src/lib/Account.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { IResource, TLocalTree } from './interfaces/Resource'
import { Capacitor } from '@capacitor/core'
import IAccount from './interfaces/Account'
import Mappings from './Mappings'
import { ResourceLockedError } from '../errors/Error'

// register Adapters
AdapterFactory.register('nextcloud-folders', async() => (await import('./adapters/NextcloudBookmarks')).default)
Expand Down Expand Up @@ -146,7 +147,7 @@ export default class Account {

Logger.log('Starting sync process for account ' + this.getLabel())
this.syncing = true
await this.setData({ ...this.getData(), syncing: 0.05, error: null })
await this.setData({ ...this.getData(), syncing: 0.05, scheduled: false, error: null })

if (!(await this.isInitialized())) {
await this.init()
Expand All @@ -163,7 +164,23 @@ export default class Account {

if (this.server.onSyncStart) {
const needLock = (strategy || this.getData().strategy) !== 'slave'
const status = await this.server.onSyncStart(needLock)
let status
try {
status = await this.server.onSyncStart(needLock)
} catch (e) {
// Resource locked
if (e.code === 37) {
await this.setData({ ...this.getData(), error: null, syncing: false, scheduled: true })
this.syncing = false
Logger.log(
'Resource is locked, trying again soon'
)
await Logger.persist()
return
} else {
throw e
}
}
if (status === false) {
await this.init()
}
Expand Down Expand Up @@ -210,7 +227,7 @@ export default class Account {
}
await this.syncProcess.sync()

await this.setData({ ...this.getData(), syncing: 1 })
await this.setData({ ...this.getData(), scheduled: false, syncing: 1 })

// update cache
if (localResource.constructor.name !== 'LocalTabs') {
Expand All @@ -234,6 +251,7 @@ export default class Account {
...this.getData(),
error: null,
syncing: false,
scheduled: false,
lastSync: Date.now(),
})

Expand All @@ -250,6 +268,7 @@ export default class Account {
...this.getData(),
error: message,
syncing: false,
scheduled: false,
})
this.syncing = false
if (this.server.onSyncFail) {
Expand Down
32 changes: 15 additions & 17 deletions src/lib/adapters/GoogleDrive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
DecryptionError, FileUnreadableError,
GoogleDriveAuthenticationError, InterruptedSyncError, MissingPermissionsError,
NetworkError,
OAuthTokenError
OAuthTokenError, ResourceLockedError
} from '../../errors/Error'
import { OAuth2Client } from '@byteowls/capacitor-oauth2'
import { Capacitor, CapacitorHttp as Http } from '@capacitor/core'
Expand Down Expand Up @@ -191,7 +191,7 @@ export default class GoogleDriveAdapter extends CachingAdapter {
})
}

async onSyncStart() {
async onSyncStart(needLock = true) {
Logger.log('onSyncStart: begin')

if (Capacitor.getPlatform() === 'web') {
Expand All @@ -204,31 +204,29 @@ export default class GoogleDriveAdapter extends CachingAdapter {

this.accessToken = await this.getAccessToken(this.server.refreshToken)

let file
let startDate = Date.now()
const maxTimeout = LOCK_TIMEOUT
const base = 1.25
for (let i = 0; Date.now() - startDate < maxTimeout; i++) {
const fileList = await this.listFiles('name = ' + "'" + this.server.bookmark_file + "'")
file = fileList.files.filter(file => !file.trashed)[0]
if (file) {
this.fileId = file.id
const fileList = await this.listFiles('name = ' + "'" + this.server.bookmark_file + "'")
const file = fileList.files.filter(file => !file.trashed)[0]
if (file) {
this.fileId = file.id
if (needLock) {
const data = await this.getFileMetadata(file.id, 'appProperties')
if (data.appProperties && data.appProperties.locked && (data.appProperties.locked === true || JSON.parse(data.appProperties.locked))) {
const lockedDate = JSON.parse(data.appProperties.locked)
if (Number.isInteger(lockedDate)) {
startDate = lockedDate
if (!Number.isInteger(lockedDate)) {
throw new ResourceLockedError()
}
if (Date.now() - lockedDate < LOCK_TIMEOUT) {
throw new ResourceLockedError()
}
await this.timeout(base ** i * 1000)
continue
}
}
break
}

if (file) {
this.fileId = file.id
await this.setLock(this.fileId)
if (needLock) {
await this.setLock(this.fileId)
}

let xmlDocText = await this.downloadFile(this.fileId)

Expand Down
19 changes: 7 additions & 12 deletions src/lib/adapters/NextcloudBookmarks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import {
NetworkError,
ParseResponseError,
RedirectError,
RequestTimeoutError,
RequestTimeoutError, ResourceLockedError,
UnexpectedServerResponseError,
UnknownCreateTargetError,
UnknownFolderParentUpdateError,
Expand Down Expand Up @@ -137,26 +137,21 @@ export default class NextcloudBookmarksAdapter implements Adapter, BulkImportRes
})
}

async onSyncStart(): Promise<void> {
async onSyncStart(needLock = true): Promise<void> {
if (Capacitor.getPlatform() === 'web') {
const browser = (await import('../browser-api')).default
if (!(await browser.permissions.contains({ origins: [this.server.url + '/'] }))) {
throw new MissingPermissionsError()
}
}

this.canceled = false
const startDate = Date.now()
const maxTimeout = LOCK_TIMEOUT
const base = 1.25
for (let i = 0; Date.now() - startDate < maxTimeout; i++) {
if (await this.acquireLock()) {
break
} else {
Logger.log('Resource is still locked, trying again in ' + (base ** i) + 's')
await this.timeout(base ** i * 1000)
if (needLock) {
if (!(await this.acquireLock())) {
throw new ResourceLockedError()
}
}

this.canceled = false
this.ended = false
this.lockingInterval = setInterval(() => !this.ended && this.acquireLock(), LOCK_INTERVAL)
}
Expand Down
24 changes: 10 additions & 14 deletions src/lib/adapters/WebDav.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
DecryptionError, FileUnreadableError,
HttpError, InterruptedSyncError,
LockFileError, MissingPermissionsError,
NetworkError, RedirectError,
NetworkError, RedirectError, ResourceLockedError,
SlashError
} from '../../errors/Error'
import { CapacitorHttp as Http } from '@capacitor/core'
Expand Down Expand Up @@ -92,20 +92,16 @@ export default class WebDavAdapter extends CachingAdapter {
}

async obtainLock() {
let res
let startDate = Date.now()
const maxTimeout = LOCK_TIMEOUT
const base = 1.25
for (let i = 0; Date.now() - startDate < maxTimeout; i++) {
res = await this.checkLock()
if (res.status === 200) {
if (res.headers['Last-Modified']) {
const date = new Date(res.headers['Last-Modified'])
startDate = date.valueOf()
const res = await this.checkLock()
if (res.status === 200) {
if (res.headers['Last-Modified']) {
const date = new Date(res.headers['Last-Modified'])
const dateLocked = date.valueOf()
if (Date.now() - dateLocked < LOCK_TIMEOUT) {
throw new ResourceLockedError()
}
await this.timeout(base ** i * 1000)
} else if (res.status !== 200) {
break
} else {
throw new ResourceLockedError()
}
}

Expand Down
40 changes: 18 additions & 22 deletions src/lib/browser/BrowserController.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,9 @@
import packageJson from '../../../package.json'
import BrowserAccountStorage from './BrowserAccountStorage'
import uniqBy from 'lodash/uniqBy'

import PQueue from 'p-queue'
import Account from '../Account'
import { STATUS_ALLGOOD, STATUS_DISABLED, STATUS_ERROR, STATUS_SYNCING } from '../interfaces/Controller'

const STATUS_ERROR = Symbol('error')
const STATUS_SYNCING = Symbol('syncing')
const STATUS_ALLGOOD = Symbol('allgood')
const STATUS_DISABLED = Symbol('disabled')
const INACTIVITY_TIMEOUT = 7 * 1000
const DEFAULT_SYNC_INTERVAL = 15

Expand All @@ -29,6 +24,9 @@
const data = account.getData()
const lastSync = data.lastSync || 0
const interval = data.syncInterval || DEFAULT_SYNC_INTERVAL
if (data.scheduled) {
this.ctl.scheduleSync(accountId)
}
if (
Date.now() >
interval * 1000 * 60 + lastSync
Expand All @@ -42,8 +40,6 @@

export default class BrowserController {
constructor() {
this.jobs = new PQueue({ concurrency: 1 })
this.waiting = {}
this.schedule = {}
this.listeners = []

Expand Down Expand Up @@ -180,7 +176,7 @@
return Promise.resolve(this.unlocked)
}

async onchange(localId, details) {

Check warning on line 179 in src/lib/browser/BrowserController.js

View workflow job for this annotation

GitHub Actions / js node20.x

'details' is defined but never used

Check warning on line 179 in src/lib/browser/BrowserController.js

View workflow job for this annotation

GitHub Actions / init (20.x, 10.x)

'details' is defined but never used
if (!this.enabled) {
return
}
Expand Down Expand Up @@ -265,22 +261,21 @@
return
}

if (this.waiting[accountId]) {
console.log('Account is already queued to be synced')
const status = await this.getStatus()
if (status === STATUS_SYNCING) {
await account.setData({ ...account.getData(), scheduled: true })
return
}

this.waiting[accountId] = true
await account.setData({ ...account.getData(), scheduled: true })

return this.jobs.add(() => this.syncAccount(accountId))
this.syncAccount(accountId)
}

async scheduleAll() {
const accounts = await Account.getAllAccounts()
for (const account of accounts) {
this.scheduleSync(account.id)
await account.setData({...account.getData(), scheduled: true})
}
this.updateStatus()
}

async cancelSync(accountId, keepEnabled) {
Expand All @@ -294,13 +289,11 @@

async syncAccount(accountId, strategy) {
console.log('Called syncAccount ', accountId)
this.waiting[accountId] = false
if (!this.enabled) {
console.log('Flocccus controller is not enabled. Not syncing.')
return
}
let account = await Account.get(accountId)
await account.setData({ ...account.getData(), scheduled: false })
if (account.getData().syncing) {
console.log('Account is already syncing. Not triggering another sync.')
return
Expand Down Expand Up @@ -329,14 +322,14 @@
}
}

async updateBadge() {
async getStatus() {
if (!this.unlocked) {
return this.setStatusBadge(STATUS_ERROR)
return STATUS_ERROR
}
const accounts = await Account.getAllAccounts()
let overallStatus = accounts.reduce((status, account) => {
const accData = account.getData()
if (status === STATUS_SYNCING || accData.syncing) {
if (status === STATUS_SYNCING || accData.syncing || account.syncing) {
return STATUS_SYNCING
} else if (status === STATUS_ERROR || (accData.error && !accData.syncing)) {
return STATUS_ERROR
Expand All @@ -351,7 +344,11 @@
}
}

this.setStatusBadge(overallStatus)
return overallStatus
}

async updateBadge() {
await this.setStatusBadge(await this.getStatus())
}

async setStatusBadge(status) {
Expand All @@ -375,7 +372,6 @@
await acc.setData({
...acc.getData(),
syncing: false,
error: false,
})
}
})
Expand Down
5 changes: 5 additions & 0 deletions src/lib/interfaces/Controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,8 @@ export default interface IController {
getUnlocked():Promise<boolean>;
onLoad():void;
}

export const STATUS_ERROR = Symbol('error')
export const STATUS_SYNCING = Symbol('syncing')
export const STATUS_ALLGOOD = Symbol('allgood')
export const STATUS_DISABLED = Symbol('disabled')
Loading
Loading