mirror of
https://github.com/johannesjo/super-productivity.git
synced 2026-01-23 02:36:05 +00:00
fix(sync): fix simple counter sync and quota exceeded handling
- Add null checks in simple-counter.reducer.ts to prevent crashes when entity doesn't exist (fixes "Cannot read properties of undefined") - Add missing entityIds to updateAllSimpleCounters action - was causing operation log to skip persistence - Add try-catch in operation-log-upload.service.ts to show alert when storage quota exceeded (413 error) - Fix supersync-network-failure.spec.ts test timing - route interception must happen before task creation - Add documentation to CLAUDE.md for running supersync tests with real-time output using --reporter=line
This commit is contained in:
parent
a7cd442551
commit
016e680c5f
5 changed files with 95 additions and 37 deletions
16
CLAUDE.md
16
CLAUDE.md
|
|
@ -48,6 +48,7 @@ npm run test:file <filepath>
|
|||
- Unit tests: `npm test` - Uses Jasmine/Karma, tests are co-located with source files (`.spec.ts`)
|
||||
- E2E tests: `npm run e2e` - Uses Nightwatch, located in `/e2e/src/`
|
||||
- Playwright E2E tests: Located in `/e2e/`
|
||||
|
||||
- `npm run e2e:playwright` - Run all tests with minimal output (shows failures clearly)
|
||||
- `npm run e2e:playwright:file <path>` - Run a single test file with detailed output
|
||||
- Example: `npm run e2e:playwright:file tests/work-view/work-view.spec.ts`
|
||||
|
|
@ -58,6 +59,21 @@ npm run test:file <filepath>
|
|||
- Use `--grep "test name"` to run a single test: `npm run e2e:file <path> -- --grep "test name" --retries=0`
|
||||
- Tests take ~20s each, don't use excessive timeouts
|
||||
- Each test run includes a fresh server start (~5s overhead)
|
||||
- **IMPORTANT for Claude**: When running the full supersync suite, use playwright directly with a line reporter for real-time output (the `npm run e2e:supersync` script buffers output):
|
||||
|
||||
```bash
|
||||
# Start the server first
|
||||
docker compose -f docker-compose.yaml -f docker-compose.e2e.yaml up -d supersync && \
|
||||
until curl -s http://localhost:1901/health > /dev/null 2>&1; do sleep 1; done && \
|
||||
echo 'Server ready!'
|
||||
|
||||
# Run with line reporter for real-time output
|
||||
npx playwright test --config e2e/playwright.config.ts --grep @supersync --reporter=line
|
||||
|
||||
# Stop server when done
|
||||
docker compose -f docker-compose.yaml -f docker-compose.e2e.yaml down supersync
|
||||
```
|
||||
|
||||
- Linting: `npm run lint` - ESLint for TypeScript, Stylelint for SCSS
|
||||
|
||||
## Architecture Overview
|
||||
|
|
|
|||
|
|
@ -617,12 +617,20 @@ base.describe('@supersync Network Failure Recovery', () => {
|
|||
clientA = await createSimulatedClient(browser, baseURL!, 'A', testRunId);
|
||||
await clientA.sync.setupSuperSync(syncConfig);
|
||||
|
||||
const taskName = `Task-${testRunId}-quota`;
|
||||
await clientA.workView.addTask(taskName);
|
||||
await waitForTask(clientA.page, taskName);
|
||||
// Set up dialog handler to dismiss any alerts BEFORE setting up route
|
||||
let alertShown = false;
|
||||
clientA.page.on('dialog', async (dialog) => {
|
||||
console.log(`[Test] Alert shown: ${dialog.message()}`);
|
||||
alertShown = true;
|
||||
await dialog.accept();
|
||||
});
|
||||
|
||||
// Intercept and return storage quota exceeded
|
||||
await clientA.page.route('**/api/sync/ops/**', async (route) => {
|
||||
// Wait for initial sync to complete so we have a baseline
|
||||
await clientA.sync.syncAndWait();
|
||||
|
||||
// Intercept and return storage quota exceeded BEFORE creating task
|
||||
// so the immediate upload service gets the error
|
||||
await clientA.page.route('**/api/sync/ops', async (route) => {
|
||||
if (route.request().method() === 'POST') {
|
||||
console.log('[Test] Simulating storage quota exceeded');
|
||||
await route.fulfill({
|
||||
|
|
@ -645,15 +653,12 @@ base.describe('@supersync Network Failure Recovery', () => {
|
|||
}
|
||||
});
|
||||
|
||||
// Set up dialog handler to dismiss any alerts
|
||||
let alertShown = false;
|
||||
clientA.page.on('dialog', async (dialog) => {
|
||||
console.log(`[Test] Alert shown: ${dialog.message()}`);
|
||||
alertShown = true;
|
||||
await dialog.accept();
|
||||
});
|
||||
// Now create a task - the immediate upload will get 413
|
||||
const taskName = `Task-${testRunId}-quota`;
|
||||
await clientA.workView.addTask(taskName);
|
||||
await waitForTask(clientA.page, taskName);
|
||||
|
||||
// Sync - should fail with quota exceeded
|
||||
// Wait for immediate upload to trigger and hit the 413
|
||||
try {
|
||||
await clientA.sync.triggerSync();
|
||||
await clientA.page.waitForTimeout(3000);
|
||||
|
|
@ -672,7 +677,7 @@ base.describe('@supersync Network Failure Recovery', () => {
|
|||
console.log('[StorageQuota] ✓ Server quota exceeded handling test PASSED');
|
||||
} finally {
|
||||
if (clientA) {
|
||||
await clientA.page.unroute('**/api/sync/ops/**').catch(() => {});
|
||||
await clientA.page.unroute('**/api/sync/ops').catch(() => {});
|
||||
}
|
||||
if (clientA) await closeClient(clientA);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -70,7 +70,9 @@ export const updateAllSimpleCounters = createAction(
|
|||
meta: {
|
||||
isPersistent: true,
|
||||
entityType: 'SIMPLE_COUNTER',
|
||||
entityIds: counterProps.items.map((item) => item.id),
|
||||
opType: OpType.Update,
|
||||
isBulk: true,
|
||||
} satisfies PersistentActionMeta,
|
||||
}),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -110,40 +110,51 @@ const _reducer = createReducer<SimpleCounterState>(
|
|||
return newState;
|
||||
}),
|
||||
|
||||
on(setSimpleCounterCounterToday, (state, { id, newVal, today }) =>
|
||||
adapter.updateOne(
|
||||
on(setSimpleCounterCounterToday, (state, { id, newVal, today }) => {
|
||||
const entity = state.entities[id];
|
||||
if (!entity) {
|
||||
return state;
|
||||
}
|
||||
return adapter.updateOne(
|
||||
{
|
||||
id,
|
||||
changes: {
|
||||
countOnDay: {
|
||||
...(state.entities[id] as SimpleCounter).countOnDay,
|
||||
...entity.countOnDay,
|
||||
[today]: newVal,
|
||||
},
|
||||
},
|
||||
},
|
||||
state,
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
|
||||
on(setSimpleCounterCounterForDate, (state, { id, newVal, date }) =>
|
||||
adapter.updateOne(
|
||||
on(setSimpleCounterCounterForDate, (state, { id, newVal, date }) => {
|
||||
const entity = state.entities[id];
|
||||
if (!entity) {
|
||||
return state;
|
||||
}
|
||||
return adapter.updateOne(
|
||||
{
|
||||
id,
|
||||
changes: {
|
||||
countOnDay: {
|
||||
...(state.entities[id] as SimpleCounter).countOnDay,
|
||||
...entity.countOnDay,
|
||||
[date]: newVal,
|
||||
},
|
||||
},
|
||||
},
|
||||
state,
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
|
||||
// Non-persistent local UI update for ClickCounter
|
||||
// Sync happens via setSimpleCounterCounterToday with absolute value
|
||||
on(increaseSimpleCounterCounterToday, (state, { id, increaseBy, today }) => {
|
||||
const oldEntity = state.entities[id] as SimpleCounter;
|
||||
const oldEntity = state.entities[id];
|
||||
if (!oldEntity) {
|
||||
return state;
|
||||
}
|
||||
const currentTotalCount = oldEntity.countOnDay || {};
|
||||
const currentVal = currentTotalCount[today] || 0;
|
||||
const newValForToday = currentVal + increaseBy;
|
||||
|
|
@ -163,7 +174,10 @@ const _reducer = createReducer<SimpleCounterState>(
|
|||
|
||||
// Non-persistent local UI update for ClickCounter
|
||||
on(decreaseSimpleCounterCounterToday, (state, { id, decreaseBy, today }) => {
|
||||
const oldEntity = state.entities[id] as SimpleCounter;
|
||||
const oldEntity = state.entities[id];
|
||||
if (!oldEntity) {
|
||||
return state;
|
||||
}
|
||||
const currentTotalCount = oldEntity.countOnDay || {};
|
||||
const currentVal = currentTotalCount[today] || 0;
|
||||
const newValForToday = Math.max(0, currentVal - decreaseBy);
|
||||
|
|
@ -183,7 +197,10 @@ const _reducer = createReducer<SimpleCounterState>(
|
|||
|
||||
// Non-persistent local tick for StopWatch - immediate UI update
|
||||
on(tickSimpleCounterLocal, (state, { id, increaseBy, today }) => {
|
||||
const oldEntity = state.entities[id] as SimpleCounter;
|
||||
const oldEntity = state.entities[id];
|
||||
if (!oldEntity) {
|
||||
return state;
|
||||
}
|
||||
const currentTotalCount = oldEntity.countOnDay || {};
|
||||
const currentVal = currentTotalCount[today] || 0;
|
||||
const newValForToday = currentVal + increaseBy;
|
||||
|
|
@ -230,15 +247,19 @@ const _reducer = createReducer<SimpleCounterState>(
|
|||
);
|
||||
}),
|
||||
|
||||
on(toggleSimpleCounterCounter, (state, { id }) =>
|
||||
adapter.updateOne(
|
||||
on(toggleSimpleCounterCounter, (state, { id }) => {
|
||||
const entity = state.entities[id];
|
||||
if (!entity) {
|
||||
return state;
|
||||
}
|
||||
return adapter.updateOne(
|
||||
{
|
||||
id,
|
||||
changes: { isOn: !(state.entities[id] as SimpleCounter).isOn },
|
||||
changes: { isOn: !entity.isOn },
|
||||
},
|
||||
state,
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
|
||||
on(setSimpleCounterCounterOn, (state, { id }) =>
|
||||
adapter.updateOne(
|
||||
|
|
|
|||
|
|
@ -212,11 +212,25 @@ export class OperationLogUploadService {
|
|||
`OperationLogUploadService: Uploading batch of ${chunk.length} ops via API`,
|
||||
);
|
||||
|
||||
const response = await syncProvider.uploadOps(
|
||||
chunk,
|
||||
clientId,
|
||||
lastKnownServerSeq,
|
||||
);
|
||||
let response;
|
||||
try {
|
||||
response = await syncProvider.uploadOps(chunk, clientId, lastKnownServerSeq);
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : 'Unknown error';
|
||||
OpLog.error(`OperationLogUploadService: Upload failed: ${message}`);
|
||||
|
||||
// Check for storage quota exceeded - show strong alert
|
||||
if (
|
||||
message.includes('STORAGE_QUOTA_EXCEEDED') ||
|
||||
message.includes('Storage quota exceeded')
|
||||
) {
|
||||
alert(
|
||||
'Sync storage is full! Your data is NOT syncing to the server. ' +
|
||||
'Please archive old tasks or upgrade your plan to continue syncing.',
|
||||
);
|
||||
}
|
||||
throw err; // Re-throw to propagate the error
|
||||
}
|
||||
|
||||
// Mark successfully accepted ops as synced
|
||||
const acceptedSeqs = response.results
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue