mirror of
https://github.com/johannesjo/super-productivity.git
synced 2026-07-21 02:20:12 +00:00
- Remove outdated feature requests from .github/CONTRIBUTING.md (GitLab support already exists) and add commit message format section - Improve PR template with type-of-change checkboxes and checklist - Update commit guideline links in README and CONTRIBUTING.md to reference the project's own format instead of external angular.js docs - Add "only edit en.json" rule to TRANSLATING.md and clarify workflow - Update add-new-integration.md provider list to match codebase (add Trello, ClickUp, Linear, Azure DevOps, Nextcloud Deck; note GitHub plugin migration; fix type name to BuiltInIssueProviderKey) - Add cross-references between mac certificate docs and remove 240-line duplicate section from update-mac-certificates.md - Clean up update-android-app.md (specify npm version args, collapse deprecated workflow, translate German UI labels to English) - Add context to howto-refresh-snap-credentials.md - Fix fine-grained token note in github-access-token-instructions.md - Fix absolute URL to relative path in gitlab-access-token-instructions.md - Fix grammar in i18n-script-usage.md - Add status headers to all 19 long-term plan files (Planned, Completed, Archived with reason, Investigation Complete) - Fix broken relative link in hybrid-manifest-architecture.md - Delete supersync-scenarios-simplified.md (duplicate of supersync-scenarios.md; known issues already covered there) - Rename vector-clock-pruning-research.md to vector-clock-history-and-alternatives.md for clarity
236 lines
8.7 KiB
Markdown
236 lines
8.7 KiB
Markdown
# Design: Sync Provider Plugins
|
|
|
|
> **Status: Planned**
|
|
|
|
## Goal
|
|
|
|
Enable community developers to build sync providers (Google Drive, OneDrive, S3, etc.) as plugins, using the existing plugin system's runtime loading and sandboxed execution.
|
|
|
|
## Design Decisions
|
|
|
|
- Plugins run in the **same sandbox** as regular plugins (no elevated trust)
|
|
- Plugins handle their **own auth UI** (OAuth flows, credential forms)
|
|
- Credentials stored via a **new `persistDataLocal()` API** (IndexedDB, never synced)
|
|
- **App manages encryption** — plugins only transport opaque bytes
|
|
- Built-in providers (Dropbox, WebDAV, LocalFile, SuperSync) **stay built-in for now**
|
|
- Plugin sync providers are always **file-based** (wrapped by `FileBasedSyncAdapterService`)
|
|
|
|
## Architecture
|
|
|
|
### 1. New Plugin API: `registerSyncProvider()`
|
|
|
|
Added to `PluginAPI` interface. A plugin calls this during initialization:
|
|
|
|
```javascript
|
|
plugin.registerSyncProvider({
|
|
id: 'google-drive',
|
|
label: 'Google Drive',
|
|
icon: 'cloud', // material icon name or inline SVG
|
|
|
|
// Core file operations
|
|
getFileRev: async (path, localRev) => {
|
|
// Return { rev: string } or throw if not found
|
|
},
|
|
downloadFile: async (path) => {
|
|
// Return { rev: string, dataStr: string }
|
|
},
|
|
uploadFile: async (path, dataStr, revToMatch, isForceOverwrite) => {
|
|
// Return { rev: string }
|
|
},
|
|
removeFile: async (path) => {},
|
|
|
|
// State
|
|
isReady: async () => true, // true if configured & authenticated
|
|
|
|
// Optional
|
|
listFiles: async (path) => [], // directory listing
|
|
isUploadForcePossible: true, // can force-overwrite on conflict
|
|
maxConcurrentRequests: 4, // concurrent upload/download limit
|
|
});
|
|
```
|
|
|
|
Only one sync provider per plugin. Calling `registerSyncProvider()` a second time replaces the first.
|
|
|
|
### 2. New Plugin API: `persistDataLocal()` / `loadLocalData()`
|
|
|
|
General-purpose local-only storage. Stored in IndexedDB, never synced.
|
|
|
|
```javascript
|
|
// Store credentials locally
|
|
await plugin.persistDataLocal(
|
|
JSON.stringify({
|
|
accessToken: '...',
|
|
refreshToken: '...',
|
|
}),
|
|
);
|
|
|
|
// Load on startup
|
|
const data = await plugin.loadLocalData();
|
|
const creds = data ? JSON.parse(data) : null;
|
|
```
|
|
|
|
Same constraints as `persistDataSynced()` (1 MB limit, rate limiting), but data stays on-device.
|
|
|
|
### 3. PluginSyncProviderAdapter
|
|
|
|
New class in `src/app/plugins/` that wraps plugin callbacks into `SyncProviderServiceInterface`:
|
|
|
|
```
|
|
src/app/plugins/plugin-sync-provider-adapter.ts
|
|
```
|
|
|
|
- Implements `SyncProviderServiceInterface<SyncProviderId>`
|
|
- Delegates file operations to plugin callbacks via `PluginBridgeService`
|
|
- `privateCfg` uses a no-op credential store (plugin manages its own creds)
|
|
- `isReady()` delegates to the plugin's `isReady()` callback
|
|
|
|
### 4. SyncProviderManager Changes
|
|
|
|
**File**: `src/app/op-log/sync-providers/provider-manager.service.ts`
|
|
|
|
Currently: static `SYNC_PROVIDERS` array populated at construction.
|
|
|
|
Changes:
|
|
|
|
- Add `registerPluginProvider(adapter: PluginSyncProviderAdapter)` method
|
|
- Add `unregisterPluginProvider(providerId: string)` method
|
|
- `SYNC_PROVIDERS` becomes a mutable list (or better: maintain a separate `pluginProviders` map)
|
|
- `SyncProviderId` enum extended with a dynamic/string approach for plugin IDs (e.g., `plugin:google-drive`)
|
|
- `activeProviderId$` and related observables react to plugin provider registration
|
|
|
|
### 5. Sync Settings UI Changes
|
|
|
|
**File**: `src/app/features/config/form-cfgs/sync-form.const.ts`
|
|
|
|
Currently: hardcoded provider dropdown options.
|
|
|
|
Changes:
|
|
|
|
- Provider dropdown dynamically includes registered plugin providers
|
|
- When a plugin provider is selected, instead of showing hardcoded form fields:
|
|
- Show a "Configure [Provider Name]" button
|
|
- Clicking it triggers the plugin's config UI (the plugin can use `plugin.openDialog()`, a side panel, or `plugin.showIndexHtmlAsView()`)
|
|
- Show connection status from the plugin's `isReady()` result
|
|
|
|
### 6. Lifecycle Handling
|
|
|
|
**Startup with plugin sync provider selected:**
|
|
|
|
1. App starts, loads sync config → selected provider is `plugin:google-drive`
|
|
2. `SyncProviderManager` sees unknown provider ID → `isProviderReady$` emits `false`
|
|
3. Plugin system loads and activates the Google Drive plugin
|
|
4. Plugin calls `registerSyncProvider(...)` → adapter registered with manager
|
|
5. Manager detects matching provider → `isProviderReady$` emits `true`
|
|
6. Sync begins
|
|
|
|
**Plugin disabled/uninstalled:**
|
|
|
|
1. `PluginService` calls cleanup → `unregisterPluginProvider('plugin:google-drive')`
|
|
2. `SyncProviderManager` removes the provider → `isProviderReady$` emits `false`
|
|
3. Sync stops
|
|
4. Settings UI shows warning: "Sync provider 'Google Drive' unavailable — enable the plugin or select another provider"
|
|
|
|
**Encryption:**
|
|
|
|
- Managed entirely by the app via `FileBasedSyncAdapterService`
|
|
- Encrypt key stored in app-level config (existing mechanism)
|
|
- Plugin never sees decrypted data and never handles the key
|
|
|
|
## Files to Modify
|
|
|
|
| File | Change |
|
|
| ----------------------------------------------------------- | ------------------------------------------------------------------------------------ |
|
|
| `packages/plugin-api/src/types.ts` | Add `registerSyncProvider()` and `persistDataLocal()`/`loadLocalData()` to API types |
|
|
| `src/app/plugins/plugin-api.ts` | Implement new API methods |
|
|
| `src/app/plugins/plugin-bridge.service.ts` | Add bridge methods for sync provider registration and local data persistence |
|
|
| `src/app/plugins/plugin-cleanup.service.ts` | Unregister sync provider on plugin disable/unload |
|
|
| **New**: `src/app/plugins/plugin-sync-provider-adapter.ts` | Adapter wrapping plugin callbacks → `SyncProviderServiceInterface` |
|
|
| `src/app/op-log/sync-providers/provider-manager.service.ts` | Add `registerPluginProvider()` / `unregisterPluginProvider()`, dynamic provider list |
|
|
| `src/app/op-log/sync-providers/provider.const.ts` | Support dynamic plugin provider IDs alongside the enum |
|
|
| `src/app/features/config/form-cfgs/sync-form.const.ts` | Dynamic provider dropdown, "Configure" button for plugin providers |
|
|
| `src/app/plugins/store/` | Add reducer/actions for local plugin data persistence |
|
|
| `src/app/plugins/plugin-persistence.model.ts` | Add `PluginLocalData` model |
|
|
|
|
## New File
|
|
|
|
**`src/app/plugins/plugin-sync-provider-adapter.ts`**
|
|
|
|
Thin adapter that implements `SyncProviderServiceInterface` by delegating to plugin callbacks. ~50-80 lines.
|
|
|
|
## Verification Plan
|
|
|
|
1. **Unit test**: `PluginSyncProviderAdapter` correctly delegates all methods
|
|
2. **Unit test**: `SyncProviderManager` handles dynamic registration/unregistration
|
|
3. **Integration test**: Plugin registers → appears in settings dropdown → can be selected
|
|
4. **E2E test**: Build a test sync provider plugin that syncs to a local mock, verify full sync cycle works
|
|
5. **Edge case tests**: Plugin not loaded at startup, plugin disabled while active, plugin re-enabled
|
|
|
|
## Example Plugin
|
|
|
|
A minimal Google Drive sync plugin would look like:
|
|
|
|
```javascript
|
|
// manifest.json
|
|
{
|
|
"name": "Google Drive Sync",
|
|
"id": "google-drive-sync",
|
|
"version": "1.0.0",
|
|
"manifestVersion": 1,
|
|
"minSupVersion": "11.0.0",
|
|
"description": "Sync via Google Drive",
|
|
"hooks": [],
|
|
"permissions": ["syncProvider", "localData"]
|
|
}
|
|
|
|
// plugin.js
|
|
const GDRIVE_API = 'https://www.googleapis.com/drive/v3';
|
|
|
|
let credentials = null;
|
|
|
|
async function init() {
|
|
const data = await plugin.loadLocalData();
|
|
credentials = data ? JSON.parse(data) : null;
|
|
}
|
|
|
|
plugin.registerSyncProvider({
|
|
id: 'google-drive',
|
|
label: 'Google Drive',
|
|
icon: 'cloud',
|
|
maxConcurrentRequests: 4,
|
|
isUploadForcePossible: true,
|
|
|
|
isReady: async () => {
|
|
await init();
|
|
return !!credentials?.accessToken;
|
|
},
|
|
|
|
downloadFile: async (path) => {
|
|
// Use fetch() to call Google Drive API
|
|
// Return { rev, dataStr }
|
|
},
|
|
|
|
uploadFile: async (path, dataStr, revToMatch, isForceOverwrite) => {
|
|
// Upload to Google Drive
|
|
// Return { rev }
|
|
},
|
|
|
|
getFileRev: async (path, localRev) => {
|
|
// Check file metadata on Google Drive
|
|
// Return { rev }
|
|
},
|
|
|
|
removeFile: async (path) => {
|
|
// Delete file from Google Drive
|
|
},
|
|
});
|
|
|
|
// Auth UI via menu entry
|
|
plugin.registerMenuEntry({
|
|
label: 'Configure Google Drive Sync',
|
|
icon: 'settings',
|
|
onClick: async () => {
|
|
// Show auth dialog, store credentials
|
|
await plugin.persistDataLocal(JSON.stringify(credentials));
|
|
},
|
|
});
|
|
```
|