@offlinejs/conflicts 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,45 @@
1
+ import { EntityRecord, ConflictResolver } from '@offlinejs/types';
2
+
3
+ type FieldMergeStrategy = "client" | "server" | "lastWriteWins" | "max" | "min" | "setUnion" | "growOnly";
4
+ type FieldMergePolicy<TRecord extends EntityRecord> = Partial<Record<keyof TRecord | string, FieldMergeStrategy>>;
5
+ declare const createFieldMergeResolver: <TRecord extends EntityRecord>(policy: FieldMergePolicy<TRecord>) => ConflictResolver<TRecord>;
6
+ declare const mergeGrowOnlyCounter: (client?: number, server?: number) => number;
7
+ declare const mergePositiveNegativeCounter: (client: {
8
+ decrement: number;
9
+ increment: number;
10
+ }, server: {
11
+ decrement: number;
12
+ increment: number;
13
+ }) => {
14
+ decrement: number;
15
+ increment: number;
16
+ value: number;
17
+ };
18
+ declare const mergeSetUnion: <TValue>(client?: TValue[], server?: TValue[]) => TValue[];
19
+ declare const mergeLastWriteWinsRegister: <TValue>(client: {
20
+ timestamp: number;
21
+ value: TValue;
22
+ }, server: {
23
+ timestamp: number;
24
+ value: TValue;
25
+ }) => TValue;
26
+ /** OR-Map style merge: union keys, LWW per key when timestamps are present. */
27
+ declare const mergeOrMap: (client?: Record<string, unknown>, server?: Record<string, unknown>) => Record<string, unknown>;
28
+ declare const mergeWithTombstones: <TValue>(client: Array<{
29
+ id: string;
30
+ deleted?: boolean;
31
+ value: TValue;
32
+ updatedAt?: number;
33
+ }>, server: Array<{
34
+ id: string;
35
+ deleted?: boolean;
36
+ value: TValue;
37
+ updatedAt?: number;
38
+ }>) => Array<{
39
+ id: string;
40
+ deleted?: boolean;
41
+ value: TValue;
42
+ updatedAt?: number;
43
+ }>;
44
+
45
+ export { type FieldMergePolicy, type FieldMergeStrategy, createFieldMergeResolver, mergeGrowOnlyCounter, mergeLastWriteWinsRegister, mergeOrMap, mergePositiveNegativeCounter, mergeSetUnion, mergeWithTombstones };
package/dist/index.js ADDED
@@ -0,0 +1,105 @@
1
+ // src/index.ts
2
+ var createFieldMergeResolver = (policy) => {
3
+ return ({ client, server }) => {
4
+ if (!client) {
5
+ return server;
6
+ }
7
+ if (!server) {
8
+ return client;
9
+ }
10
+ const merged = { ...server, ...client, id: client.id };
11
+ for (const [field, strategy] of Object.entries(policy)) {
12
+ if (strategy) {
13
+ merged[field] = mergeField(strategy, client[field], server[field], client, server);
14
+ }
15
+ }
16
+ return merged;
17
+ };
18
+ };
19
+ var mergeGrowOnlyCounter = (client = 0, server = 0) => Math.max(client, server);
20
+ var mergePositiveNegativeCounter = (client, server) => {
21
+ const increment = Math.max(client.increment, server.increment);
22
+ const decrement = Math.max(client.decrement, server.decrement);
23
+ return {
24
+ decrement,
25
+ increment,
26
+ value: increment - decrement
27
+ };
28
+ };
29
+ var mergeSetUnion = (client = [], server = []) => [
30
+ .../* @__PURE__ */ new Set([...server, ...client])
31
+ ];
32
+ var mergeLastWriteWinsRegister = (client, server) => client.timestamp >= server.timestamp ? client.value : server.value;
33
+ var mergeOrMap = (client = {}, server = {}) => {
34
+ const keys = /* @__PURE__ */ new Set([...Object.keys(server), ...Object.keys(client)]);
35
+ const merged = {};
36
+ for (const key of keys) {
37
+ const clientValue = client[key];
38
+ const serverValue = server[key];
39
+ if (clientValue === void 0) {
40
+ merged[key] = serverValue;
41
+ continue;
42
+ }
43
+ if (serverValue === void 0) {
44
+ merged[key] = clientValue;
45
+ continue;
46
+ }
47
+ if (isTimestamped(clientValue) && isTimestamped(serverValue)) {
48
+ merged[key] = mergeLastWriteWinsRegister(clientValue, serverValue);
49
+ continue;
50
+ }
51
+ merged[key] = clientValue;
52
+ }
53
+ return merged;
54
+ };
55
+ var mergeWithTombstones = (client, server) => {
56
+ const byId = /* @__PURE__ */ new Map();
57
+ for (const entry of [...server, ...client]) {
58
+ const existing = byId.get(entry.id);
59
+ if (!existing) {
60
+ byId.set(entry.id, entry);
61
+ continue;
62
+ }
63
+ const existingTime = existing.updatedAt ?? 0;
64
+ const nextTime = entry.updatedAt ?? 0;
65
+ if (nextTime >= existingTime) {
66
+ byId.set(entry.id, entry);
67
+ }
68
+ }
69
+ return [...byId.values()].filter((entry) => !entry.deleted);
70
+ };
71
+ var mergeField = (strategy, clientValue, serverValue, clientRecord, serverRecord) => {
72
+ if (strategy === "client") {
73
+ return clientValue;
74
+ }
75
+ if (strategy === "server") {
76
+ return serverValue;
77
+ }
78
+ if (strategy === "max" || strategy === "growOnly") {
79
+ return Math.max(Number(clientValue ?? 0), Number(serverValue ?? 0));
80
+ }
81
+ if (strategy === "min") {
82
+ return Math.min(Number(clientValue ?? 0), Number(serverValue ?? 0));
83
+ }
84
+ if (strategy === "setUnion") {
85
+ return mergeSetUnion(
86
+ Array.isArray(clientValue) ? clientValue : [],
87
+ Array.isArray(serverValue) ? serverValue : []
88
+ );
89
+ }
90
+ if (isTimestamped(clientValue) && isTimestamped(serverValue)) {
91
+ return mergeLastWriteWinsRegister(clientValue, serverValue);
92
+ }
93
+ const clientTimestamp = Number(
94
+ clientValue?.updatedAt ?? clientRecord.updatedAt ?? 0
95
+ );
96
+ const serverTimestamp = Number(
97
+ serverValue?.updatedAt ?? serverRecord.updatedAt ?? 0
98
+ );
99
+ return clientTimestamp >= serverTimestamp ? clientValue : serverValue;
100
+ };
101
+ var isTimestamped = (value) => typeof value === "object" && value !== null && "timestamp" in value && "value" in value && typeof value.timestamp === "number";
102
+
103
+ export { createFieldMergeResolver, mergeGrowOnlyCounter, mergeLastWriteWinsRegister, mergeOrMap, mergePositiveNegativeCounter, mergeSetUnion, mergeWithTombstones };
104
+ //# sourceMappingURL=index.js.map
105
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["import type { ConflictResolver, EntityRecord } from \"@offlinejs/types\";\n\nexport type FieldMergeStrategy =\n | \"client\"\n | \"server\"\n | \"lastWriteWins\"\n | \"max\"\n | \"min\"\n | \"setUnion\"\n | \"growOnly\";\n\nexport type FieldMergePolicy<TRecord extends EntityRecord> = Partial<\n Record<keyof TRecord | string, FieldMergeStrategy>\n>;\n\nexport const createFieldMergeResolver = <TRecord extends EntityRecord>(\n policy: FieldMergePolicy<TRecord>\n): ConflictResolver<TRecord> => {\n return ({ client, server }) => {\n if (!client) {\n return server;\n }\n\n if (!server) {\n return client;\n }\n\n const merged: EntityRecord = { ...server, ...client, id: client.id };\n\n for (const [field, strategy] of Object.entries(policy)) {\n if (strategy) {\n merged[field] = mergeField(strategy, client[field], server[field], client, server);\n }\n }\n\n return merged as TRecord;\n };\n};\n\nexport const mergeGrowOnlyCounter = (client = 0, server = 0): number => Math.max(client, server);\n\nexport const mergePositiveNegativeCounter = (\n client: { decrement: number; increment: number },\n server: { decrement: number; increment: number }\n): { decrement: number; increment: number; value: number } => {\n const increment = Math.max(client.increment, server.increment);\n const decrement = Math.max(client.decrement, server.decrement);\n\n return {\n decrement,\n increment,\n value: increment - decrement\n };\n};\n\nexport const mergeSetUnion = <TValue>(client: TValue[] = [], server: TValue[] = []): TValue[] => [\n ...new Set([...server, ...client])\n];\n\nexport const mergeLastWriteWinsRegister = <TValue>(\n client: { timestamp: number; value: TValue },\n server: { timestamp: number; value: TValue }\n): TValue => (client.timestamp >= server.timestamp ? client.value : server.value);\n\n/** OR-Map style merge: union keys, LWW per key when timestamps are present. */\nexport const mergeOrMap = (\n client: Record<string, unknown> = {},\n server: Record<string, unknown> = {}\n): Record<string, unknown> => {\n const keys = new Set([...Object.keys(server), ...Object.keys(client)]);\n const merged: Record<string, unknown> = {};\n\n for (const key of keys) {\n const clientValue = client[key];\n const serverValue = server[key];\n\n if (clientValue === undefined) {\n merged[key] = serverValue;\n continue;\n }\n\n if (serverValue === undefined) {\n merged[key] = clientValue;\n continue;\n }\n\n if (\n isTimestamped(clientValue) &&\n isTimestamped(serverValue)\n ) {\n merged[key] = mergeLastWriteWinsRegister(clientValue, serverValue);\n continue;\n }\n\n merged[key] = clientValue;\n }\n\n return merged;\n};\n\nexport const mergeWithTombstones = <TValue>(\n client: Array<{ id: string; deleted?: boolean; value: TValue; updatedAt?: number }>,\n server: Array<{ id: string; deleted?: boolean; value: TValue; updatedAt?: number }>\n): Array<{ id: string; deleted?: boolean; value: TValue; updatedAt?: number }> => {\n const byId = new Map<string, { id: string; deleted?: boolean; value: TValue; updatedAt?: number }>();\n\n for (const entry of [...server, ...client]) {\n const existing = byId.get(entry.id);\n if (!existing) {\n byId.set(entry.id, entry);\n continue;\n }\n\n const existingTime = existing.updatedAt ?? 0;\n const nextTime = entry.updatedAt ?? 0;\n if (nextTime >= existingTime) {\n byId.set(entry.id, entry);\n }\n }\n\n return [...byId.values()].filter((entry) => !entry.deleted);\n};\n\nconst mergeField = (\n strategy: FieldMergeStrategy,\n clientValue: unknown,\n serverValue: unknown,\n clientRecord: EntityRecord,\n serverRecord: EntityRecord\n): unknown => {\n if (strategy === \"client\") {\n return clientValue;\n }\n\n if (strategy === \"server\") {\n return serverValue;\n }\n\n if (strategy === \"max\" || strategy === \"growOnly\") {\n return Math.max(Number(clientValue ?? 0), Number(serverValue ?? 0));\n }\n\n if (strategy === \"min\") {\n return Math.min(Number(clientValue ?? 0), Number(serverValue ?? 0));\n }\n\n if (strategy === \"setUnion\") {\n return mergeSetUnion(\n Array.isArray(clientValue) ? clientValue : [],\n Array.isArray(serverValue) ? serverValue : []\n );\n }\n\n if (isTimestamped(clientValue) && isTimestamped(serverValue)) {\n return mergeLastWriteWinsRegister(clientValue, serverValue);\n }\n\n const clientTimestamp = Number(\n (clientValue as { updatedAt?: number } | undefined)?.updatedAt ??\n clientRecord.updatedAt ??\n 0\n );\n const serverTimestamp = Number(\n (serverValue as { updatedAt?: number } | undefined)?.updatedAt ??\n serverRecord.updatedAt ??\n 0\n );\n\n return clientTimestamp >= serverTimestamp ? clientValue : serverValue;\n};\n\nconst isTimestamped = (\n value: unknown\n): value is { timestamp: number; value: unknown } =>\n typeof value === \"object\" &&\n value !== null &&\n \"timestamp\" in value &&\n \"value\" in value &&\n typeof (value as { timestamp: unknown }).timestamp === \"number\";\n"]}
package/package.json ADDED
@@ -0,0 +1,31 @@
1
+ {
2
+ "name": "@offlinejs/conflicts",
3
+ "version": "0.1.0",
4
+ "description": "CRDT-friendly conflict resolver helpers for OfflineJS.",
5
+ "type": "module",
6
+ "sideEffects": false,
7
+ "exports": {
8
+ ".": {
9
+ "types": "./dist/index.d.ts",
10
+ "import": "./dist/index.js"
11
+ }
12
+ },
13
+ "files": [
14
+ "dist"
15
+ ],
16
+ "dependencies": {
17
+ "@offlinejs/types": "0.1.0"
18
+ },
19
+ "devDependencies": {
20
+ "tsup": "latest",
21
+ "typescript": "latest"
22
+ },
23
+ "publishConfig": {
24
+ "access": "public",
25
+ "registry": "https://registry.npmjs.org/"
26
+ },
27
+ "scripts": {
28
+ "build": "tsup",
29
+ "typecheck": "tsc --noEmit"
30
+ }
31
+ }