Skip to content

Commit 146536d

Browse files
Copilothotlong
andcommitted
feat: add @objectql/protocol-sync package for server-side sync protocol
Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
1 parent a94a60a commit 146536d

9 files changed

Lines changed: 887 additions & 1 deletion

File tree

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
{
2+
"name": "@objectql/protocol-sync",
3+
"version": "4.2.0",
4+
"description": "Server-side sync protocol for ObjectQL - delta computation, change tracking, and conflict resolution",
5+
"type": "module",
6+
"main": "./dist/index.js",
7+
"types": "./dist/index.d.ts",
8+
"exports": {
9+
".": {
10+
"types": "./dist/index.d.ts",
11+
"default": "./dist/index.js"
12+
}
13+
},
14+
"scripts": {
15+
"build": "tsc",
16+
"test": "vitest run"
17+
},
18+
"dependencies": {
19+
"@objectql/types": "workspace:*"
20+
},
21+
"devDependencies": {
22+
"typescript": "^5.3.3",
23+
"vitest": "^1.0.4"
24+
},
25+
"keywords": ["objectql", "objectstack", "sync", "protocol", "offline", "plugin"],
26+
"license": "MIT"
27+
}
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
/**
2+
* ObjectQL Sync Protocol — Server-side Change Log
3+
* Copyright (c) 2026-present ObjectStack Inc.
4+
*
5+
* This source code is licensed under the MIT license found in the
6+
* LICENSE file in the root directory of this source tree.
7+
*/
8+
9+
import type { SyncServerChange, MutationOperation } from '@objectql/types';
10+
11+
/**
12+
* A checkpoint-indexed entry in the server change log.
13+
*/
14+
export interface ChangeLogEntry extends SyncServerChange {
15+
/** Monotonic checkpoint sequence */
16+
readonly checkpointSeq: number;
17+
}
18+
19+
/**
20+
* Server-side append-only change log.
21+
* Records all mutations for delta computation during sync.
22+
*/
23+
export class ChangeLog {
24+
private readonly entries: ChangeLogEntry[] = [];
25+
private seq = 0;
26+
private readonly retentionMs: number;
27+
28+
constructor(retentionDays = 30) {
29+
this.retentionMs = retentionDays * 24 * 60 * 60 * 1000;
30+
}
31+
32+
/** Record a change in the log */
33+
record(change: {
34+
objectName: string;
35+
recordId: string | number;
36+
operation: MutationOperation;
37+
data?: Record<string, unknown>;
38+
serverVersion: number;
39+
}): ChangeLogEntry {
40+
this.seq += 1;
41+
const entry: ChangeLogEntry = {
42+
objectName: change.objectName,
43+
recordId: change.recordId,
44+
operation: change.operation,
45+
data: change.data,
46+
serverVersion: change.serverVersion,
47+
timestamp: new Date().toISOString(),
48+
checkpointSeq: this.seq,
49+
};
50+
this.entries.push(entry);
51+
return entry;
52+
}
53+
54+
/** Get changes since a checkpoint (exclusive) */
55+
getChangesSince(checkpoint: string | null): readonly ChangeLogEntry[] {
56+
if (checkpoint === null) {
57+
return [...this.entries];
58+
}
59+
const seq = parseInt(checkpoint, 10);
60+
if (isNaN(seq)) return [...this.entries];
61+
return this.entries.filter(e => e.checkpointSeq > seq);
62+
}
63+
64+
/** Get current checkpoint string */
65+
getCurrentCheckpoint(): string {
66+
return String(this.seq);
67+
}
68+
69+
/** Prune old entries based on retention policy */
70+
prune(): number {
71+
const cutoff = Date.now() - this.retentionMs;
72+
const before = this.entries.length;
73+
for (let i = this.entries.length - 1; i >= 0; i--) {
74+
if (new Date(this.entries[i].timestamp).getTime() < cutoff) {
75+
this.entries.splice(i, 1);
76+
}
77+
}
78+
return before - this.entries.length;
79+
}
80+
81+
/** Get total entries */
82+
get size(): number {
83+
return this.entries.length;
84+
}
85+
}

0 commit comments

Comments
 (0)