Merge remote-tracking branch 'origin/master' into task-settings

This commit is contained in:
Ivan Kalashnikov 2026-01-18 19:43:35 +07:00
commit f51edc4e9b
16 changed files with 337 additions and 61 deletions

View file

@ -7,7 +7,7 @@ on:
jobs:
ios-app-store-release:
runs-on: macos-latest
runs-on: macos-26
env:
UNSPLASH_KEY: ${{ secrets.UNSPLASH_KEY }}
UNSPLASH_CLIENT_ID: ${{ secrets.UNSPLASH_CLIENT_ID }}
@ -25,6 +25,15 @@ jobs:
with:
persist-credentials: false
- name: Verify Xcode and SDK version
run: |
echo "=== Xcode Version ==="
xcodebuild -version
echo ""
echo "=== SDK Information ==="
xcrun --show-sdk-version
xcodebuild -showsdks | grep -E "(iOS|iphoneos)"
# work around for npm installs from git+https://github.com/johannesjo/J2M.git
- name: Reconfigure git to use HTTP authentication
run: >

View file

@ -59,21 +59,9 @@ jobs:
- name: Test Unit
run: npm run test
- name: Start WebDAV Server
run: |
docker compose up -d webdav
# Wait for WebDAV server to be ready
chmod +x ./scripts/wait-for-webdav.sh
./scripts/wait-for-webdav.sh
- name: Test E2E
run: npm run e2e
- name: Print WebDAV logs on failure
if: ${{ failure() }}
run: docker compose logs webdav
- name: 'Upload E2E results on failure'
if: ${{ failure() }}
uses: actions/upload-artifact@v6

108
.github/workflows/e2e-scheduled.yml vendored Normal file
View file

@ -0,0 +1,108 @@
name: 'E2E Tests (Scheduled)'
on:
schedule:
- cron: '0 2 * * *' # 2 AM UTC daily
workflow_dispatch:
push:
branches: [master, release/*]
paths:
- 'src/app/imex/sync/**'
- 'src/app/op-log/**'
- 'e2e/tests/**'
- 'docker-compose.yaml'
- 'docker-compose.supersync.yaml'
permissions:
contents: read
jobs:
e2e-tests:
runs-on: ubuntu-latest
timeout-minutes: 60
env:
UNSPLASH_KEY: ${{ secrets.UNSPLASH_KEY }}
UNSPLASH_CLIENT_ID: ${{ secrets.UNSPLASH_CLIENT_ID }}
steps:
- uses: actions/setup-node@v6
with:
node-version: 20
- name: Check out Git repository
uses: actions/checkout@v6
with:
persist-credentials: false
- name: Reconfigure git to use HTTP authentication
run: >
git config --global url."https://github.com/".insteadOf
ssh://git@github.com/
- name: Get npm cache directory
id: npm-cache-dir
run: |
echo "dir=$(npm config get cache)" >> $GITHUB_OUTPUT
- uses: actions/cache@v5
id: npm-cache
with:
path: ${{ steps.npm-cache-dir.outputs.dir }}
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-
- name: Install npm Packages
run: npm i
- name: Install Playwright Browsers
run: |
npx playwright install --with-deps chromium
npx playwright install-deps chromium
- run: npm run env
- name: Create .env file for Docker Compose
run: |
cp .env.example .env
- name: Start Docker Services (WebDAV + SuperSync)
run: |
docker compose up -d webdav
docker compose -f docker-compose.yaml -f docker-compose.supersync.yaml up -d --build supersync
# Wait for WebDAV
chmod +x ./scripts/wait-for-webdav.sh
./scripts/wait-for-webdav.sh
# Wait for SuperSync
echo "⏳ Waiting for SuperSync server..."
until curl -s http://localhost:1901/health > /dev/null 2>&1; do sleep 1; done
echo "✓ SuperSync server ready!"
- name: Run E2E Tests
run: npm run e2e
- name: Stop Docker Services
if: always()
run: |
docker compose down
docker compose -f docker-compose.yaml -f docker-compose.supersync.yaml down supersync
- name: Print Docker Logs on Failure
if: ${{ failure() }}
run: |
echo "=== WebDAV Logs ==="
docker compose logs webdav
echo "=== SuperSync Logs ==="
docker compose -f docker-compose.yaml -f docker-compose.supersync.yaml logs supersync
- name: Upload E2E Results on Failure
if: ${{ failure() }}
uses: actions/upload-artifact@v6
with:
name: e2e-results-${{ github.run_id }}
path: .tmp/e2e-test-results/**/*.*
retention-days: 30

View file

@ -50,18 +50,8 @@ jobs:
- run: npm run lint
- run: npm run int:test # Validate i18n JSON files
- run: npm run test
- name: Start WebDAV Server
run: |
docker compose up -d webdav
# Wait for WebDAV server to be ready
chmod +x ./scripts/wait-for-webdav.sh
./scripts/wait-for-webdav.sh
- run: npm run e2e
- name: Print WebDAV logs on failure
if: ${{ failure() }}
run: docker compose logs webdav
- name: Test E2E
run: npm run e2e
- name: 'Upload E2E results on failure'
if: ${{ failure() }}
uses: actions/upload-artifact@v6

View file

@ -1,3 +1,21 @@
# [17.0.0-RC.11](https://github.com/super-productivity/super-productivity/compare/v17.0.0-RC.10...v17.0.0-RC.11) (2026-01-18)
### Bug Fixes
- **e2e:** create missing .env file and update GitHub Actions syntax ([1f4eecb](https://github.com/super-productivity/super-productivity/commit/1f4eecbe70130dc005b5d99885b7eaf35397ec0d))
# [17.0.0-RC.10](https://github.com/super-productivity/super-productivity/compare/v17.0.0-RC.9...v17.0.0-RC.10) (2026-01-18)
# [17.0.0-RC.9](https://github.com/super-productivity/super-productivity/compare/v17.0.0-RC.8...v17.0.0-RC.9) (2026-01-17)
# [17.0.0-RC.8](https://github.com/super-productivity/super-productivity/compare/v17.0.0-RC.7...v17.0.0-RC.8) (2026-01-17)
### Bug Fixes
- **ci:** add E2E tests to PR workflow ([4d78d7b](https://github.com/super-productivity/super-productivity/commit/4d78d7b9fc9363c5b545f95143c52a003613078e))
- **snap:** remove duplicate plugs from configuration ([dbaaab8](https://github.com/super-productivity/super-productivity/commit/dbaaab8faace09434c58ed6d9054e281df55ba72))
- **sync:** pass clientId to downloadOps for accurate snapshot detection ([978d71f](https://github.com/super-productivity/super-productivity/commit/978d71f40d2ac60f0d7bd33ae8581de29f6cd791))
# [17.0.0-RC.7](https://github.com/super-productivity/super-productivity/compare/v17.0.0-RC.5...v17.0.0-RC.7) (2026-01-17)
### Bug Fixes

View file

@ -20,8 +20,8 @@ android {
minSdkVersion 24
targetSdkVersion 35
compileSdk 35
versionCode 17_00_00_0007
versionName "17.0.0-RC.7"
versionCode 17_00_00_0011
versionName "17.0.0-RC.11"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
manifestPlaceholders = [
hostName : "app.super-productivity.com",

View file

@ -54,9 +54,14 @@ export class TaskPage extends BasePage {
async markTaskAsDone(task: Locator): Promise<void> {
await task.waitFor({ state: 'visible' });
await task.hover();
// Give hover effects time to settle
await this.page.waitForTimeout(100);
const doneBtn = task.locator(TASK_DONE_BTN);
await doneBtn.waitFor({ state: 'visible', timeout: 5000 });
await doneBtn.click();
await waitForAngularStability(this.page);
}

View file

@ -15,6 +15,7 @@
import { test, expect } from '../../fixtures/test.fixture';
import { Page, Locator } from '@playwright/test';
import { WorkViewPage } from '../../pages/work-view.page';
import { waitForAngularStability } from '../../utils/waits';
// Helper to open focus mode and select a task
const openFocusModeWithTask = async (
@ -336,8 +337,15 @@ test.describe('Bug #5954: Pomodoro timer sync issues', () => {
await playButton.click();
await expect(task).toHaveClass(/isCurrent/, { timeout: 5000 });
// Step 2: Mark task as done (this stops tracking)
await taskPage.markTaskAsDone(task);
// Wait for Angular to finish re-rendering the task hover controls
// When isCurrent changes, the hover controls switch from play to pause button
await waitForAngularStability(page);
// Step 2: Mark task as done using keyboard shortcut
// This bypasses the button click issue caused by continuous re-renders
// from the progress bar while tracking is active
await task.focus();
await page.keyboard.press('d'); // Keyboard shortcut for toggle done
await expect(task).toHaveClass(/isDone/, { timeout: 5000 });
await expect(task).not.toHaveClass(/isCurrent/, { timeout: 5000 });

View file

@ -76,17 +76,13 @@ snap:
# https://github.com/super-productivity/super-productivity/issues/4920
FC_CACHEDIR: $SNAP_USER_DATA/.cache/fontconfig
plugs:
- default
- default # Includes: home, desktop, desktop-legacy, x11, wayland, etc.
- password-manager-service
- system-observe
- login-session-observe
# Fix for issue #6031: Add filesystem access for local file sync
# https://github.com/super-productivity/super-productivity/issues/6031
- home # Allows file picker to access user's home directory for sync
- removable-media # Allows sync to external drives/USB storage
# Fix for issue #6031: Add desktop integration for taskbar pinning in Cinnamon
- desktop # Provides D-Bus interfaces for window manager integration
- desktop-legacy # Fallback for older desktop environments
flatpak:
runtimeVersion: '23.08'

4
package-lock.json generated
View file

@ -1,12 +1,12 @@
{
"name": "superProductivity",
"version": "17.0.0-RC.7",
"version": "17.0.0-RC.11",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "superProductivity",
"version": "17.0.0-RC.7",
"version": "17.0.0-RC.11",
"license": "MIT",
"workspaces": [
"packages/*"

View file

@ -1,6 +1,6 @@
{
"name": "superProductivity",
"version": "17.0.0-RC.7",
"version": "17.0.0-RC.11",
"description": "ToDo list and Time Tracking",
"keywords": [
"ToDo",

View file

@ -3364,8 +3364,8 @@ describe('FocusModeEffects', () => {
store.overrideSelector(selectIsFocusModeEnabled, true);
store.refreshState();
// Re-inject effects after setting up selectors
effects = TestBed.inject(FocusModeEffects);
// Dispatch a tick action to trigger the effect
actions$ = of(actions.tick());
// Subscribe to the effect to trigger it
effects.updateBanner$.pipe(take(1)).subscribe(() => {
@ -3396,7 +3396,8 @@ describe('FocusModeEffects', () => {
store.overrideSelector(selectIsFocusModeEnabled, true);
store.refreshState();
effects = TestBed.inject(FocusModeEffects);
// Dispatch a tick action to trigger the effect
actions$ = of(actions.tick());
effects.updateBanner$.pipe(take(1)).subscribe(() => {
expect(bannerServiceMock.open).toHaveBeenCalled();
@ -3425,7 +3426,8 @@ describe('FocusModeEffects', () => {
store.overrideSelector(selectIsFocusModeEnabled, true);
store.refreshState();
effects = TestBed.inject(FocusModeEffects);
// Dispatch a tick action to trigger the effect
actions$ = of(actions.tick());
effects.updateBanner$.pipe(take(1)).subscribe(() => {
expect(bannerServiceMock.open).toHaveBeenCalled();
@ -3456,7 +3458,8 @@ describe('FocusModeEffects', () => {
store.overrideSelector(selectIsFocusModeEnabled, true);
store.refreshState();
effects = TestBed.inject(FocusModeEffects);
// Dispatch a tick action to trigger the effect
actions$ = of(actions.tick());
effects.updateBanner$.pipe(take(1)).subscribe(() => {
expect(bannerServiceMock.open).toHaveBeenCalled();

View file

@ -1133,5 +1133,111 @@ describe('FileBasedSyncAdapterService', () => {
expect(result.snapshotState).toEqual({ tasks: [{ id: 't1' }] });
});
it('should detect snapshot replacement when syncVersion remains at 1 (with excludeClient)', async () => {
// This test verifies the false negative fix using the clientId-based detection.
// Scenario:
// 1. Client A has synced before (lastServerSeq=1, syncVersion=1)
// 2. Client B uploads a snapshot: syncVersion=1, recentOps=[], clientId=client-b
// 3. Client A downloads with excludeClient='client-a'
// Expected: Should detect gap because snapshot.clientId !== excludeClient
// Step 1: Simulate initial download for client-a (has ops)
const initialData = createMockSyncData({
syncVersion: 1,
clientId: 'client-a',
recentOps: [
{
id: 'op1',
c: 'client-a',
a: 'HA',
o: 'CRT',
e: 'TASK',
d: 't1',
p: { title: 'Task 1' },
v: { client_a: 1 },
t: Date.now(),
s: 1,
},
],
});
mockProvider.downloadFile.and.returnValue(
Promise.resolve({ dataStr: addPrefix(initialData), rev: 'rev-1' }),
);
// Download with excludeClient='client-a' to establish baseline
const initialResult = await adapter.downloadOps(0, 'client-a');
expect(initialResult.ops.length).toBe(0); // Own op filtered out
expect(initialResult.latestSeq).toBe(1);
// Update lastServerSeq to 1 (simulates that client-a has synced)
await adapter.setLastServerSeq(1);
// Step 2: Another client (client-b) uploads a snapshot
// syncVersion STAYS at 1 (doesn't increment), recentOps cleared, clientId changes
const snapshotData = createMockSyncData({
syncVersion: 1, // SAME VERSION as before
clientId: 'client-b', // DIFFERENT CLIENT
recentOps: [], // Snapshot cleared ops
state: { tasks: [{ id: 't2', title: 'Task 2' }] },
});
mockProvider.downloadFile.and.returnValue(
Promise.resolve({ dataStr: addPrefix(snapshotData), rev: 'rev-2' }),
);
// Step 3: Client A downloads again with sinceSeq=1 and excludeClient='client-a'
const result = await adapter.downloadOps(1, 'client-a');
// Should detect gap because:
// - syncData.clientId ('client-b') !== excludeClient ('client-a')
// - This means another client uploaded, so gap should be detected
expect(result.gapDetected).toBe(true);
expect(result.ops.length).toBe(0); // Gap detected, no ops returned
});
it('should NOT detect gap when own client uploads snapshot (with excludeClient)', async () => {
// This test verifies false positive prevention using clientId-based detection.
// Scenario:
// 1. Client A uploads a snapshot: syncVersion=1, recentOps=[], clientId=client-a
// 2. Client A immediately downloads with excludeClient='client-a'
// Expected: Should NOT detect gap because snapshot.clientId === excludeClient
// Step 1: Upload snapshot as client-a
const snapshotData = createMockSyncData({
syncVersion: 1,
clientId: 'client-a',
recentOps: [],
state: { tasks: [] },
});
mockProvider.downloadFile.and.returnValue(
Promise.resolve({ dataStr: addPrefix(snapshotData), rev: 'rev-1' }),
);
mockProvider.uploadFile.and.returnValue(Promise.resolve({ rev: 'rev-2' }));
await adapter.uploadSnapshot(
{},
'client-a',
'initial',
{ client_a: 1 },
1,
undefined,
'test-op-id-snapshot',
);
const seqAfterUpload = await adapter.getLastServerSeq();
expect(seqAfterUpload).toBe(1);
// Step 2: Download with excludeClient='client-a' (same client that uploaded)
const result = await adapter.downloadOps(1, 'client-a');
// Should NOT detect gap because:
// - syncData.clientId ('client-a') === excludeClient ('client-a')
// - This means we just uploaded, so no gap
expect(result.gapDetected).toBe(false);
expect(result.snapshotState).toBeUndefined(); // No snapshot state when sinceSeq > 0
});
});
});

View file

@ -1,4 +1,4 @@
import { fakeAsync, TestBed, tick } from '@angular/core/testing';
import { fakeAsync, flush, TestBed, tick } from '@angular/core/testing';
import { OperationLogDownloadService } from './operation-log-download.service';
import { OperationLogStoreService } from '../persistence/operation-log-store.service';
import { LockService } from './lock.service';
@ -12,12 +12,18 @@ import { ActionType, OpType } from '../core/operation.types';
import { CLOCK_DRIFT_THRESHOLD_MS } from '../core/operation-log.const';
import { OpLog } from '../../core/log';
import { T } from '../../t.const';
import { OperationEncryptionService } from './operation-encryption.service';
import { SuperSyncStatusService } from './super-sync-status.service';
import { CLIENT_ID_PROVIDER } from '../util/client-id.provider';
describe('OperationLogDownloadService', () => {
let service: OperationLogDownloadService;
let mockOpLogStore: jasmine.SpyObj<OperationLogStoreService>;
let mockLockService: jasmine.SpyObj<LockService>;
let mockSnackService: jasmine.SpyObj<SnackService>;
let mockEncryptionService: jasmine.SpyObj<OperationEncryptionService>;
let mockSuperSyncStatusService: jasmine.SpyObj<SuperSyncStatusService>;
let mockClientIdProvider: { loadClientId: jasmine.Spy };
beforeEach(() => {
mockOpLogStore = jasmine.createSpyObj('OperationLogStoreService', [
@ -26,17 +32,27 @@ describe('OperationLogDownloadService', () => {
]);
mockLockService = jasmine.createSpyObj('LockService', ['request']);
mockSnackService = jasmine.createSpyObj('SnackService', ['open']);
mockEncryptionService = jasmine.createSpyObj('OperationEncryptionService', [
'decryptOperations',
]);
mockSuperSyncStatusService = jasmine.createSpyObj('SuperSyncStatusService', [
'markRemoteChecked',
]);
mockClientIdProvider = {
loadClientId: jasmine
.createSpy('loadClientId')
.and.returnValue(Promise.resolve('test-client-id')),
};
// Mock OpLog
spyOn(OpLog, 'warn');
spyOn(OpLog, 'normal');
// Default mock implementations
mockLockService.request.and.callFake(
async (_name: string, fn: () => Promise<void>) => {
await fn();
},
);
// Note: Don't use async/await here as it breaks fakeAsync zone context
mockLockService.request.and.callFake((_name: string, fn: () => Promise<void>) => {
return fn();
});
mockOpLogStore.getAppliedOpIds.and.returnValue(Promise.resolve(new Set<string>()));
mockOpLogStore.hasSyncedOps.and.returnValue(Promise.resolve(false));
@ -46,10 +62,15 @@ describe('OperationLogDownloadService', () => {
{ provide: OperationLogStoreService, useValue: mockOpLogStore },
{ provide: LockService, useValue: mockLockService },
{ provide: SnackService, useValue: mockSnackService },
{ provide: OperationEncryptionService, useValue: mockEncryptionService },
{ provide: SuperSyncStatusService, useValue: mockSuperSyncStatusService },
{ provide: CLIENT_ID_PROVIDER, useValue: mockClientIdProvider },
],
});
service = TestBed.inject(OperationLogDownloadService);
// Reset the clock drift warning flag for each test
(service as any).hasWarnedClockDrift = false;
});
describe('downloadRemoteOps', () => {
@ -92,7 +113,11 @@ describe('OperationLogDownloadService', () => {
it('should detect and warn about clock drift after retry', fakeAsync(() => {
const driftMs = CLOCK_DRIFT_THRESHOLD_MS + 60000; // Threshold + 1 min
const serverCurrentTime = Date.now() - driftMs; // Server clock is behind
const initialTime = Date.now();
const serverCurrentTime = initialTime - driftMs; // Server clock is behind
// Spy on Date.now() to return consistent time during the test
spyOn(Date, 'now').and.returnValue(initialTime);
mockApiProvider.downloadOps.and.returnValue(
Promise.resolve({
@ -108,7 +133,7 @@ describe('OperationLogDownloadService', () => {
entityType: 'TASK',
payload: {},
vectorClock: {},
timestamp: Date.now(),
timestamp: initialTime,
schemaVersion: 1,
},
},
@ -127,7 +152,7 @@ describe('OperationLogDownloadService', () => {
expect(OpLog.warn).not.toHaveBeenCalled();
// After 1 second retry, warning should appear
tick(1000);
flush(); // Flush all pending timers
expect(OpLog.warn).toHaveBeenCalledWith(
'OperationLogDownloadService: Clock drift detected',
@ -232,7 +257,11 @@ describe('OperationLogDownloadService', () => {
it('should only warn about clock drift once per session', fakeAsync(() => {
const driftMs = CLOCK_DRIFT_THRESHOLD_MS + 60000; // Threshold + 1 min
const serverCurrentTime = Date.now() - driftMs;
const initialTime = Date.now();
const serverCurrentTime = initialTime - driftMs;
// Spy on Date.now() to return consistent time during the test
spyOn(Date, 'now').and.returnValue(initialTime);
mockApiProvider.downloadOps.and.returnValue(
Promise.resolve({
@ -248,7 +277,7 @@ describe('OperationLogDownloadService', () => {
entityType: 'TASK',
payload: {},
vectorClock: {},
timestamp: Date.now(),
timestamp: initialTime,
schemaVersion: 1,
},
},
@ -262,13 +291,13 @@ describe('OperationLogDownloadService', () => {
// First call - should warn after retry
service.downloadRemoteOps(mockApiProvider);
tick(); // Resolve promises
tick(1000); // Wait for retry
flush(); // Flush all pending timers
expect(OpLog.warn).toHaveBeenCalledTimes(1);
// Second call - should NOT warn again
service.downloadRemoteOps(mockApiProvider);
tick(); // Resolve promises
tick(1000); // Wait for retry (if any)
flush(); // Flush all pending timers (if any)
expect(OpLog.warn).toHaveBeenCalledTimes(1);
}));
@ -321,7 +350,11 @@ describe('OperationLogDownloadService', () => {
// When server sends its current time and it differs significantly from
// client time, we should warn about clock drift
const driftMs = CLOCK_DRIFT_THRESHOLD_MS + 60000; // 6 minutes drift
const serverCurrentTime = Date.now() - driftMs; // Server clock is 6 min behind
const initialTime = Date.now();
const serverCurrentTime = initialTime - driftMs; // Server clock is 6 min behind
// Spy on Date.now() to return consistent time during the test
spyOn(Date, 'now').and.returnValue(initialTime);
mockApiProvider.downloadOps.and.returnValue(
Promise.resolve({
@ -350,7 +383,7 @@ describe('OperationLogDownloadService', () => {
service.downloadRemoteOps(mockApiProvider);
tick(); // Resolve promises
tick(1000); // Wait for retry
flush(); // Flush all pending timers
// Should warn because serverTime differs from client time
expect(OpLog.warn).toHaveBeenCalledWith(
@ -550,7 +583,11 @@ describe('OperationLogDownloadService', () => {
await service.downloadRemoteOps(mockApiProvider, { forceFromSeq0: true });
// Should start from 0, not from lastServerSeq (100)
expect(mockApiProvider.downloadOps).toHaveBeenCalledWith(0, undefined, 500);
expect(mockApiProvider.downloadOps).toHaveBeenCalledWith(
0,
'test-client-id',
500,
);
});
it('should collect all op clocks when forceFromSeq0 is true', async () => {

View file

@ -20,6 +20,7 @@ import { OperationEncryptionService } from './operation-encryption.service';
import { DecryptError } from '../core/errors/sync-errors';
import { SuperSyncStatusService } from './super-sync-status.service';
import { DownloadResult } from '../core/types/sync-results.types';
import { CLIENT_ID_PROVIDER } from '../util/client-id.provider';
// Re-export for consumers that import from this service
export type { DownloadResult } from '../core/types/sync-results.types';
@ -45,6 +46,7 @@ export class OperationLogDownloadService {
private snackService = inject(SnackService);
private encryptionService = inject(OperationEncryptionService);
private superSyncStatusService = inject(SuperSyncStatusService);
private clientIdProvider = inject(CLIENT_ID_PROVIDER);
/** Track if we've already warned about clock drift this session */
private hasWarnedClockDrift = false;
@ -88,8 +90,10 @@ export class OperationLogDownloadService {
await this.lockService.request(LOCK_NAMES.DOWNLOAD, async () => {
const lastServerSeq = forceFromSeq0 ? 0 : await syncProvider.getLastServerSeq();
const appliedOpIds = await this.opLogStore.getAppliedOpIds();
const clientId = await this.clientIdProvider.loadClientId();
OpLog.verbose(
`OperationLogDownloadService: [DEBUG] Starting download. lastServerSeq=${lastServerSeq}, appliedOpIds.size=${appliedOpIds.size}`,
`OperationLogDownloadService: [DEBUG] Starting download. ` +
`lastServerSeq=${lastServerSeq}, appliedOpIds.size=${appliedOpIds.size}, clientId=${clientId}`,
);
if (forceFromSeq0) {
@ -115,7 +119,11 @@ export class OperationLogDownloadService {
break;
}
const response = await syncProvider.downloadOps(sinceSeq, undefined, 500);
const response = await syncProvider.downloadOps(
sinceSeq,
clientId ?? undefined,
500,
);
finalLatestSeq = response.latestSeq;
OpLog.verbose(
`OperationLogDownloadService: [DEBUG] Download response: ops=${response.ops.length}, ` +

View file

@ -1,6 +1,6 @@
// this file is automatically generated by git.version.ts script
export const versions = {
version: '17.0.0-RC.7',
version: '17.0.0-RC.11',
revision: 'NO_REV',
branch: 'NO_BRANCH',
};