@vorplex/core 0.0.15 → 0.0.21

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.
package/dist/index.d.ts CHANGED
@@ -62,6 +62,9 @@ export * from './modules/state/adaptors/array/array-adaptor.util';
62
62
  export * from './modules/state/adaptors/entity/entity-adaptor.util';
63
63
  export * from './modules/state/adaptors/entity/entity-map.type';
64
64
  export * from './modules/state/adaptors/entity/entity.interface';
65
+ export * from './modules/storage/in-memory.model';
66
+ export * from './modules/storage/local-storage.model';
67
+ export * from './modules/storage/storage-provider.interface';
65
68
  export * from './modules/string/string.util';
66
69
  export * from './modules/subscribable/subscribable.model';
67
70
  export * from './modules/subscribable/subscription.interface';
package/dist/index.js CHANGED
@@ -63,6 +63,9 @@ export * from './modules/state/adaptors/array/array-adaptor.util';
63
63
  export * from './modules/state/adaptors/entity/entity-adaptor.util';
64
64
  export * from './modules/state/adaptors/entity/entity-map.type';
65
65
  export * from './modules/state/adaptors/entity/entity.interface';
66
+ export * from './modules/storage/in-memory.model';
67
+ export * from './modules/storage/local-storage.model';
68
+ export * from './modules/storage/storage-provider.interface';
66
69
  export * from './modules/string/string.util';
67
70
  export * from './modules/subscribable/subscribable.model';
68
71
  export * from './modules/subscribable/subscription.interface';
@@ -1,19 +1,39 @@
1
1
  export type ArrayChange = Record<`$${number}` | `$${number}+`, any | typeof $Changes.deleted>;
2
- export interface ChangeConflicts {
2
+ export interface ChangeCompareResult {
3
+ similarities: any;
3
4
  conflicts: any;
4
5
  differences: any;
5
6
  }
6
7
  export interface ChangeRebase<T = any> {
8
+ /**
9
+ * The remote with local changes
10
+ */
7
11
  result: T;
8
- conflicts?: {
9
- local: ChangeConflicts;
10
- remote: ChangeConflicts;
11
- };
12
- merge?: {
13
- source: T;
14
- remote: T;
15
- local: T;
16
- result: T;
12
+ /**
13
+ * Only defined when the rebase contains conflicts.
14
+ * Contains rebase conflicts and merge
15
+ */
16
+ conflict?: {
17
+ local: ChangeCompareResult;
18
+ remote: ChangeCompareResult;
19
+ merge?: {
20
+ /**
21
+ * The source with remote and local none conflicting changes
22
+ */
23
+ source: T;
24
+ /**
25
+ * The `.source` with remote conflict changes
26
+ */
27
+ remote: T;
28
+ /**
29
+ * The `.source` with local conflict changes
30
+ */
31
+ local: T;
32
+ /**
33
+ * The remote with local none conflicting changes
34
+ */
35
+ result: T;
36
+ };
17
37
  };
18
38
  }
19
39
  export declare class $Changes {
@@ -22,15 +42,11 @@ export declare class $Changes {
22
42
  static getArrayChanges(a: any[], b: any[]): ArrayChange | undefined;
23
43
  static getObjectChanges(a: Record<string, any>, b: Record<string, any>): Record<string, any | typeof $Changes.deleted>;
24
44
  static get(a: any, b: any): undefined | any | Record<string, any | typeof $Changes.deleted> | ArrayChange;
25
- static classifyPathsByOverlap(changesA: any, changesB: any): {
26
- conflicts: string[];
27
- differences: string[];
28
- };
29
- static resolveConflicts(changesA: any, changesB: any): ChangeConflicts;
45
+ static compareChanges(changesA: any, changesB: any): ChangeCompareResult;
30
46
  private static pathsOverlap;
31
47
  static hasConflict(changesA: any, changesB: any): boolean;
32
48
  static asValue(changes: any): any;
33
- static apply(value: any, changes: any): any;
49
+ static apply(value: any, ...changes: any[]): any;
34
50
  static rebase(source: any, local: any, remote: any): ChangeRebase;
35
51
  /**
36
52
  * Excludes specific paths from a changes object and cleans up empty containers.
@@ -107,33 +107,31 @@ export class $Changes {
107
107
  return $Changes.getArrayChanges(a, b);
108
108
  return $Changes.getObjectChanges(a, b);
109
109
  }
110
- static classifyPathsByOverlap(changesA, changesB) {
110
+ static compareChanges(changesA, changesB) {
111
111
  const aPaths = $Object.getPaths(changesA);
112
112
  const bPaths = $Object.getPaths(changesB);
113
+ const similarities = new Set();
113
114
  const conflicts = new Set();
114
115
  const differences = new Set();
115
- for (const aPath of aPaths) {
116
- if (bPaths.some(bPath => this.pathsOverlap(aPath, bPath)))
117
- conflicts.add(aPath);
118
- else
119
- differences.add(aPath);
120
- }
121
116
  for (const bPath of bPaths) {
122
- if (aPaths.some(aPath => this.pathsOverlap(aPath, bPath)))
123
- conflicts.add(bPath);
124
- else
117
+ let overlapped;
118
+ for (const aPath of aPaths) {
119
+ if (this.pathsOverlap(aPath, bPath)) {
120
+ overlapped = true;
121
+ const longestPath = aPath.length > bPath.length ? aPath : bPath;
122
+ if ($Value.equals($Value.get(changesA, longestPath), $Value.get(changesB, longestPath)))
123
+ similarities.add(bPath);
124
+ else
125
+ conflicts.add(bPath);
126
+ }
127
+ }
128
+ if (!overlapped)
125
129
  differences.add(bPath);
126
130
  }
127
131
  return {
128
- conflicts: Array.from(conflicts),
129
- differences: Array.from(differences)
130
- };
131
- }
132
- static resolveConflicts(changesA, changesB) {
133
- const { conflicts, differences } = this.classifyPathsByOverlap(changesA, changesB);
134
- return {
135
- differences: $Changes.excludeChanges(changesB, conflicts),
136
- conflicts: $Changes.excludeChanges(changesB, differences)
132
+ similarities: $Changes.excludeChanges(changesB, [...conflicts, ...differences]),
133
+ differences: $Changes.excludeChanges(changesB, [...similarities, ...conflicts]),
134
+ conflicts: $Changes.excludeChanges(changesB, [...similarities, ...differences])
137
135
  };
138
136
  }
139
137
  static pathsOverlap(a, b) {
@@ -151,8 +149,11 @@ export class $Changes {
151
149
  const bPaths = $Object.getPaths(changesB);
152
150
  for (const bPath of bPaths) {
153
151
  for (const aPath of aPaths) {
154
- if (this.pathsOverlap(aPath, bPath))
155
- return true;
152
+ if (this.pathsOverlap(aPath, bPath)) {
153
+ const longestPath = aPath.length > bPath.length ? aPath : bPath;
154
+ if (!$Value.equals($Value.get(changesA, longestPath), $Value.get(changesB, longestPath)))
155
+ return true;
156
+ }
156
157
  }
157
158
  }
158
159
  return false;
@@ -164,7 +165,6 @@ export class $Changes {
164
165
  return changes.map(item => $Changes.asValue(item));
165
166
  }
166
167
  if (typeof changes === 'object' && changes !== null) {
167
- // cannot materialize array index changes without a base; strip them
168
168
  if ($Changes.isArrayIndexChange(changes))
169
169
  return undefined;
170
170
  const result = {};
@@ -177,80 +177,86 @@ export class $Changes {
177
177
  }
178
178
  return changes;
179
179
  }
180
- static apply(value, changes) {
181
- function applyArrayChange(originalArray, change) {
182
- if (!$Changes.isArrayIndexChange(change))
183
- return $Value.clone(change);
184
- const result = $Value.clone(originalArray);
185
- const entries = Object.entries(change)
186
- .map(([k, v]) => {
187
- const isInsert = k.endsWith('+');
188
- const nStr = isInsert ? k.slice(1, -1) : k.slice(1);
189
- return [Number.parseInt(nStr, 10), isInsert, v];
190
- })
191
- .filter(([i]) => !Number.isNaN(i))
192
- .sort(([iA, insA, vA], [iB, insB, vB]) => {
193
- const aDel = vA === $Changes.deleted;
194
- const bDel = vB === $Changes.deleted;
195
- // deletes first, highest -> lowest
196
- if (aDel !== bDel)
197
- return aDel ? -1 : 1;
198
- if (aDel && bDel)
199
- return iB - iA;
200
- // inserts next, lowest -> highest
201
- if (insA !== insB)
202
- return insA ? -1 : 1;
203
- if (insA && insB)
180
+ static apply(value, ...changes) {
181
+ function apply(value, changes) {
182
+ function applyArrayChange(originalArray, change) {
183
+ if (!$Changes.isArrayIndexChange(change))
184
+ return $Value.clone(change);
185
+ const result = $Value.clone(originalArray);
186
+ const entries = Object.entries(change)
187
+ .map(([k, v]) => {
188
+ const isInsert = k.endsWith('+');
189
+ const nStr = isInsert ? k.slice(1, -1) : k.slice(1);
190
+ return [Number.parseInt(nStr, 10), isInsert, v];
191
+ })
192
+ .filter(([i]) => !Number.isNaN(i))
193
+ .sort(([iA, insA, vA], [iB, insB, vB]) => {
194
+ const aDel = vA === $Changes.deleted;
195
+ const bDel = vB === $Changes.deleted;
196
+ // deletes first, highest -> lowest
197
+ if (aDel !== bDel)
198
+ return aDel ? -1 : 1;
199
+ if (aDel && bDel)
200
+ return iB - iA;
201
+ // inserts next, lowest -> highest
202
+ if (insA !== insB)
203
+ return insA ? -1 : 1;
204
+ if (insA && insB)
205
+ return iA - iB;
206
+ // sets last, lowest -> highest
204
207
  return iA - iB;
205
- // sets last, lowest -> highest
206
- return iA - iB;
207
- });
208
- for (const [index, isInsert, op] of entries) {
209
- if (op === $Changes.deleted) {
210
- if (index >= 0 && index < result.length)
211
- result.splice(index, 1);
212
- }
213
- else if (isInsert) {
214
- const v = $Value.clone(op);
215
- const idx = Math.min(Math.max(index, 0), result.length);
216
- result.splice(idx, 0, v);
217
- }
218
- else {
219
- if (index >= result.length)
220
- result.length = index + 1;
221
- result[index] = $Changes.apply(result[index], op);
208
+ });
209
+ for (const [index, isInsert, op] of entries) {
210
+ if (op === $Changes.deleted) {
211
+ if (index >= 0 && index < result.length)
212
+ result.splice(index, 1);
213
+ }
214
+ else if (isInsert) {
215
+ const v = $Value.clone(op);
216
+ const idx = Math.min(Math.max(index, 0), result.length);
217
+ result.splice(idx, 0, v);
218
+ }
219
+ else {
220
+ if (index >= result.length)
221
+ result.length = index + 1;
222
+ result[index] = $Changes.apply(result[index], op);
223
+ }
222
224
  }
225
+ return result;
223
226
  }
224
- return result;
225
- }
226
- if (changes === undefined)
227
- return value;
228
- if (value == null || typeof changes !== 'object')
229
- return $Changes.asValue(changes);
230
- if (Array.isArray(value)) {
231
- return applyArrayChange(value, changes);
232
- }
233
- else if (typeof value === 'object') {
234
- if (Array.isArray(changes))
227
+ if (changes === undefined)
228
+ return value;
229
+ if (value == null || typeof changes !== 'object')
235
230
  return $Changes.asValue(changes);
236
- const result = { ...value };
237
- for (const [key, change] of Object.entries(changes)) {
238
- if (change === $Changes.deleted) {
239
- delete result[key];
240
- }
241
- else if (Array.isArray(result[key])) {
242
- result[key] = applyArrayChange(result[key] || [], change);
243
- }
244
- else if (typeof change === 'object' && change !== null) {
245
- result[key] = $Changes.apply(result[key], change);
246
- }
247
- else {
248
- result[key] = change;
231
+ if (Array.isArray(value)) {
232
+ return applyArrayChange(value, changes);
233
+ }
234
+ else if (typeof value === 'object') {
235
+ if (Array.isArray(changes))
236
+ return $Changes.asValue(changes);
237
+ const result = { ...value };
238
+ for (const [key, change] of Object.entries(changes)) {
239
+ if (change === $Changes.deleted) {
240
+ delete result[key];
241
+ }
242
+ else if (Array.isArray(result[key])) {
243
+ result[key] = applyArrayChange(result[key] || [], change);
244
+ }
245
+ else if (typeof change === 'object' && change !== null) {
246
+ result[key] = $Changes.apply(result[key], change);
247
+ }
248
+ else {
249
+ result[key] = change;
250
+ }
249
251
  }
252
+ return result;
250
253
  }
251
- return result;
254
+ return $Changes.asValue(changes);
255
+ }
256
+ for (const change of changes) {
257
+ value = apply(value, change);
252
258
  }
253
- return $Changes.asValue(changes);
259
+ return value;
254
260
  }
255
261
  static rebase(source, local, remote) {
256
262
  const localChanges = $Changes.get(source, local);
@@ -267,20 +273,20 @@ export class $Changes {
267
273
  return { result };
268
274
  }
269
275
  else {
270
- const localConflict = $Changes.resolveConflicts(remoteChanges, localChanges);
271
- const remoteConflict = $Changes.resolveConflicts(localChanges, remoteChanges);
272
- const sourceWithDifferences = $Changes.apply($Changes.apply(source, remoteConflict.differences), localConflict.differences);
276
+ const localConflict = $Changes.compareChanges(remoteChanges, localChanges);
277
+ const remoteConflict = $Changes.compareChanges(localChanges, remoteChanges);
278
+ const sourceWithDifferences = $Changes.apply(source, remoteConflict.differences, localConflict.differences, remoteConflict.similarities, localConflict.similarities);
273
279
  const mergedWithLocalDifferences = $Changes.apply(remote, localConflict.differences);
274
280
  return {
275
- conflicts: {
281
+ conflict: {
276
282
  local: localConflict,
277
283
  remote: remoteConflict,
278
- },
279
- merge: {
280
- source: sourceWithDifferences,
281
- remote: $Changes.apply(sourceWithDifferences, remoteConflict.conflicts),
282
- local: $Changes.apply(sourceWithDifferences, localConflict.conflicts),
283
- result: mergedWithLocalDifferences
284
+ merge: {
285
+ source: sourceWithDifferences,
286
+ remote: $Changes.apply(sourceWithDifferences, remoteConflict.conflicts),
287
+ local: $Changes.apply(sourceWithDifferences, localConflict.conflicts),
288
+ result: mergedWithLocalDifferences
289
+ },
284
290
  },
285
291
  result: $Changes.apply(mergedWithLocalDifferences, localConflict.conflicts)
286
292
  };
@@ -0,0 +1,17 @@
1
+ import { StorageProvider } from './storage-provider.interface';
2
+ export declare class InMemoryStorage implements StorageProvider {
3
+ private static storage;
4
+ private static getStore;
5
+ static get<T = any>(database: string, store: string, key: string): Promise<T | null>;
6
+ static set(database: string, store: string, key: string, value: any): Promise<void>;
7
+ static delete(database: string, store: string, key: string): Promise<void>;
8
+ static clear(database: string, store: string): Promise<void>;
9
+ static keys(database: string, store: string): Promise<string[]>;
10
+ static getAll<T = any>(database: string, store: string): Promise<T[]>;
11
+ get<T = any>(database: string, store: string, key: string): Promise<T | null>;
12
+ set(database: string, store: string, key: string, value: any): Promise<void>;
13
+ delete(database: string, store: string, key: string): Promise<void>;
14
+ clear(database: string, store: string): Promise<void>;
15
+ keys(database: string, store: string): Promise<string[]>;
16
+ getAll<T = any>(database: string, store: string): Promise<T[]>;
17
+ }
@@ -0,0 +1,35 @@
1
+ export class InMemoryStorage {
2
+ static storage = new Map();
3
+ static getStore(database, store) {
4
+ if (!this.storage.has(database))
5
+ this.storage.set(database, new Map());
6
+ const db = this.storage.get(database);
7
+ if (!db.has(store))
8
+ db.set(store, new Map());
9
+ return db.get(store);
10
+ }
11
+ static async get(database, store, key) {
12
+ return this.getStore(database, store).get(key) ?? null;
13
+ }
14
+ static async set(database, store, key, value) {
15
+ this.getStore(database, store).set(key, value);
16
+ }
17
+ static async delete(database, store, key) {
18
+ this.getStore(database, store).delete(key);
19
+ }
20
+ static async clear(database, store) {
21
+ this.getStore(database, store).clear();
22
+ }
23
+ static async keys(database, store) {
24
+ return [...this.getStore(database, store).keys()];
25
+ }
26
+ static async getAll(database, store) {
27
+ return [...this.getStore(database, store).values()];
28
+ }
29
+ async get(database, store, key) { return InMemoryStorage.get(database, store, key); }
30
+ async set(database, store, key, value) { return InMemoryStorage.set(database, store, key, value); }
31
+ async delete(database, store, key) { return InMemoryStorage.delete(database, store, key); }
32
+ async clear(database, store) { return InMemoryStorage.clear(database, store); }
33
+ async keys(database, store) { return InMemoryStorage.keys(database, store); }
34
+ async getAll(database, store) { return InMemoryStorage.getAll(database, store); }
35
+ }
@@ -0,0 +1,16 @@
1
+ import { StorageProvider } from './storage-provider.interface';
2
+ export declare class LocalStorage implements StorageProvider {
3
+ private static key;
4
+ static get<T = any>(database: string, store: string, key: string): Promise<T | null>;
5
+ static set(database: string, store: string, key: string, value: any): Promise<void>;
6
+ static delete(database: string, store: string, key: string): Promise<void>;
7
+ static clear(database: string, store: string): Promise<void>;
8
+ static keys(database: string, store: string): Promise<string[]>;
9
+ static getAll<T = any>(database: string, store: string): Promise<T[]>;
10
+ get<T = any>(database: string, store: string, key: string): Promise<T | null>;
11
+ set(database: string, store: string, key: string, value: any): Promise<void>;
12
+ delete(database: string, store: string, key: string): Promise<void>;
13
+ clear(database: string, store: string): Promise<void>;
14
+ keys(database: string, store: string): Promise<string[]>;
15
+ getAll<T = any>(database: string, store: string): Promise<T[]>;
16
+ }
@@ -0,0 +1,49 @@
1
+ export class LocalStorage {
2
+ static key(database, store, key) {
3
+ return `${database}:${store}:${key ?? ''}`;
4
+ }
5
+ static async get(database, store, key) {
6
+ const raw = localStorage.getItem(this.key(database, store, key));
7
+ return raw != null ? JSON.parse(raw) : null;
8
+ }
9
+ static async set(database, store, key, value) {
10
+ localStorage.setItem(this.key(database, store, key), JSON.stringify(value));
11
+ }
12
+ static async delete(database, store, key) {
13
+ localStorage.removeItem(this.key(database, store, key));
14
+ }
15
+ static async clear(database, store) {
16
+ const prefix = this.key(database, store);
17
+ for (let i = localStorage.length - 1; i >= 0; i--) {
18
+ const key = localStorage.key(i);
19
+ if (key?.startsWith(prefix))
20
+ localStorage.removeItem(key);
21
+ }
22
+ }
23
+ static async keys(database, store) {
24
+ const prefix = this.key(database, store);
25
+ const result = [];
26
+ for (let i = 0; i < localStorage.length; i++) {
27
+ const key = localStorage.key(i);
28
+ if (key?.startsWith(prefix))
29
+ result.push(key.slice(prefix.length));
30
+ }
31
+ return result;
32
+ }
33
+ static async getAll(database, store) {
34
+ const prefix = this.key(database, store);
35
+ const result = [];
36
+ for (let i = 0; i < localStorage.length; i++) {
37
+ const key = localStorage.key(i);
38
+ if (key?.startsWith(prefix))
39
+ result.push(JSON.parse(localStorage.getItem(key)));
40
+ }
41
+ return result;
42
+ }
43
+ async get(database, store, key) { return LocalStorage.get(database, store, key); }
44
+ async set(database, store, key, value) { return LocalStorage.set(database, store, key, value); }
45
+ async delete(database, store, key) { return LocalStorage.delete(database, store, key); }
46
+ async clear(database, store) { return LocalStorage.clear(database, store); }
47
+ async keys(database, store) { return LocalStorage.keys(database, store); }
48
+ async getAll(database, store) { return LocalStorage.getAll(database, store); }
49
+ }
@@ -0,0 +1,8 @@
1
+ export interface StorageProvider<TDatabase extends string = string, TStore extends string = string> {
2
+ get<T = any>(database: TDatabase, store: TStore, key: string): Promise<T | null>;
3
+ set(database: TDatabase, store: TStore, key: string, value: any): Promise<void>;
4
+ delete(database: TDatabase, store: TStore, key: string): Promise<void>;
5
+ clear(database: TDatabase, store: TStore): Promise<void>;
6
+ keys(database: TDatabase, store: TStore): Promise<string[]>;
7
+ getAll<T = any>(database: TDatabase, store: TStore): Promise<T[]>;
8
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vorplex/core",
3
- "version": "0.0.15",
3
+ "version": "0.0.21",
4
4
  "type": "module",
5
5
  "sideEffects": false,
6
6
  "files": [
@@ -18,13 +18,11 @@
18
18
  "tslib": "2.8.1"
19
19
  },
20
20
  "devDependencies": {
21
- "@types/jest": "29.5.14",
22
- "jest": "29.7.0",
23
- "ts-jest": "29.4.5"
21
+ "vitest": "3.2.4"
24
22
  },
25
23
  "scripts": {
26
24
  "build": "tsc -b --force tsconfig.build.json",
27
- "test": "jest",
25
+ "test": "vitest run",
28
26
  "npm:publish": "pnpm publish --registry=https://registry.npmjs.org/"
29
27
  }
30
28
  }