docs/sync-and-op-log/operation-rules.md
Last Updated: July 2026 Related: Operation Log Architecture
This document establishes the core rules and principles for designing the Operation Log store and defining new Operations. Adherence to these rules ensures data integrity, synchronization reliability, and system performance.
ops table are append-only. Lifecycle metadata may advance in place as described in 1.2.seq <= lastAppliedOpSeq), and either:
pending, archive_pending, and failed remote work is retained regardless of age.SUP_OPS, its operation ID, payload, action, vector clock, source, and sequence MUST NOT be modified.syncedAt, rejectedAt, reducerRejectedAt, applicationStatus, retryCount, lastRetryAt, and error. These fields describe local delivery/application state; they do not rewrite the operation peers exchange.SUP_OPS) is the ultimate source of truth for the application state.state_cache and runtime NgRx store are projections derived from the log.state_cache (snapshot).Snapshot + Tail Ops.UPDATE_TASK { id: "A", changes: { title: "New" } }UPDATE_ALL_TASKS { [ ... entire tasks array ... ] }SYNC_IMPORT and BACKUP_IMPORT are allowed to replace large chunks of state but must be treated as special "reset" events.CREATE with an existing ID must be ignored (not merged or updated). If updates are needed, a separate UPDATE operation must follow.DELETE on a missing entity should be a no-op.UPDATE on a missing entity should be queued for retry (see 3.4 Dependency Awareness).Date objects (use timestamp numbers).undefined (use null or omit the key, depending on semantics).vectorClock.OperationLogEffects (or equivalent creator) captures the clock at the moment of creation.schemaVersion.CURRENT_SCHEMA_VERSION from SchemaMigrationService at the time of creation.OpTypes (CRT, UPD, DEL, MOV) rather than a generic CHANGE.REPAIR snapshot sent to SuperSync must carry a top-level
repairBaseServerSeq equal to the server cursor included in the repaired state.user_sync_state.lastSeq row lock in the upload transaction.
A mismatch returns REPAIR_STALE; it must not prune history or consume quota.GET /api/sync/ops advertises capabilities.causalRepairSnapshots: true.
This makes a new client fail closed against an older server.REPAIR cycle at the end if needed.OperationLogStore should not contain logic specific to a sync provider (Dropbox, WebDAV).getUnsynced(), markSynced(), markRejected() as generic methods.DependencyQueue until the parent arrives.Status (December 2025): Tombstones are DEFERRED. After comprehensive evaluation, the current event-sourced architecture provides sufficient safeguards without explicit tombstones. See
todo.mdItem 1 for the full evaluation.
SYNC_IMPORT, BACKUP_IMPORT, and REPAIR bypass these limits but
must be clearly marked as bulk operations. Imports trigger immediate snapshot
creation; only causal (base-marked) repairs may be cached as server snapshots.inject(LOCAL_ACTIONS) instead of inject(Actions).Actions.Example:
@Injectable()
export class MyEffects {
private _actions$ = inject(LOCAL_ACTIONS); // ✅ Correct for side effects
showSnack$ = createEffect(
() =>
this._actions$.pipe(
ofType(completeTask),
tap(() => this.snackService.show('Task completed!')),
),
{ dispatch: false },
);
}
this._actions$.pipe(ofType(...))) over selector-based effects (this._store$.select(...)).LOCAL_ACTIONS filtering.HydrationStateService.isApplyingRemoteOps().ArchiveOperationHandler, NOT by regular effects.ArchiveOperationHandlerEffects routes through ArchiveOperationHandler (via LOCAL_ACTIONS)OperationApplierService calls ArchiveOperationHandler directly after dispatchtagSharedMetaReducer, not in an effect.OperationCaptureService automatically captures all entity changes from a single action.operation-capture.meta-reducer calls OperationCaptureService.incrementPending() with the action; the persist effect computes the changes via OperationCaptureService.extractEntityChanges() and decrements the pending counter in a finally.entityChanges[] array containing all affected entities.See operation-log.const.ts for all configurable values:
| Constant | Value | Description |
|---|---|---|
COMPACTION_TRIGGER | 500 ops | Operations before automatic compaction |
COMPACTION_RETENTION_MS | 7 days | Synced ops older than this may be deleted |
EMERGENCY_COMPACTION_RETENTION_MS | 1 day | Shorter retention for quota exceeded |
MAX_COMPACTION_FAILURES | 3 | Failures before user notification |
MAX_DOWNLOAD_OPS_IN_MEMORY | 50,000 | Bounds memory during API download |
REMOTE_OP_FILE_RETENTION_MS | 14 days | Server-side operation file retention |
When adding a new persistent action:
meta.isPersistent: true to the actionmeta.entityType and meta.opTypeLOCAL_ACTIONSArchiveOperationHandlerACTION_AFFECTED_ENTITIES if multi-entity