@homeostate/core 0.0.0 → 0.1.2
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/CHANGELOG.md +13 -0
- package/README.md +34 -7
- package/THIRD_PARTY_NOTICES.md +1 -1
- package/dist/apply.d.ts +37 -0
- package/dist/apply.d.ts.map +1 -0
- package/dist/apply.js +58 -0
- package/dist/apply.js.map +1 -0
- package/dist/change.d.ts +6 -3
- package/dist/change.d.ts.map +1 -1
- package/dist/change.js +0 -2
- package/dist/change.js.map +1 -1
- package/dist/diff.d.ts +2 -3
- package/dist/diff.d.ts.map +1 -1
- package/dist/diff.js +156 -166
- package/dist/diff.js.map +1 -1
- package/dist/index.d.ts +11 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -2
- package/dist/index.js.map +1 -1
- package/dist/memory-backend.d.ts +11 -0
- package/dist/memory-backend.d.ts.map +1 -0
- package/dist/memory-backend.js +26 -0
- package/dist/memory-backend.js.map +1 -0
- package/dist/patching.d.ts +6 -16
- package/dist/patching.d.ts.map +1 -1
- package/dist/patching.js +46 -137
- package/dist/patching.js.map +1 -1
- package/dist/sync-engine.d.ts +14 -8
- package/dist/sync-engine.d.ts.map +1 -1
- package/dist/sync-engine.js +67 -93
- package/dist/sync-engine.js.map +1 -1
- package/dist/types.d.ts +42 -16
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +3 -3
- package/dist/types.js.map +1 -1
- package/package.json +7 -11
- package/src/apply.ts +87 -0
- package/src/change.ts +23 -0
- package/src/diff.ts +229 -0
- package/src/index.ts +18 -0
- package/src/memory-backend.ts +38 -0
- package/src/patching.ts +86 -0
- package/src/sync-engine.ts +135 -0
- package/src/types.ts +98 -0
- package/dist/mapping.d.ts +0 -19
- package/dist/mapping.d.ts.map +0 -1
- package/dist/mapping.js +0 -45
- package/dist/mapping.js.map +0 -1
package/CHANGELOG.md
ADDED
package/README.md
CHANGED
|
@@ -1,24 +1,51 @@
|
|
|
1
1
|
# @homeostate/core
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
3
|
+
State-manager and CRDT-backend agnostic sync engine. It keeps a store, reached through a
|
|
4
|
+
`StoreAdapter`, in sync with a `CrdtBackend` such as
|
|
5
|
+
[`@homeostate/crdt-yjs`](https://github.com/mixedrays/homeostate/tree/main/packages/crdt-yjs),
|
|
6
|
+
[`@homeostate/crdt-loro`](https://github.com/mixedrays/homeostate/tree/main/packages/crdt-loro), or
|
|
7
|
+
[`@homeostate/crdt-automerge`](https://github.com/mixedrays/homeostate/tree/main/packages/crdt-automerge).
|
|
7
8
|
|
|
8
9
|
## Install
|
|
9
10
|
|
|
10
11
|
```bash
|
|
11
|
-
npm install @homeostate/core yjs
|
|
12
|
+
npm install @homeostate/core @homeostate/crdt-yjs yjs
|
|
12
13
|
```
|
|
13
14
|
|
|
14
|
-
`
|
|
15
|
+
`@homeostate/core` has no runtime dependencies; pick a backend package for the CRDT library
|
|
16
|
+
you use.
|
|
15
17
|
|
|
16
18
|
## Usage
|
|
17
19
|
|
|
18
20
|
```ts
|
|
19
|
-
import { createSyncEngine } from
|
|
21
|
+
import { createSyncEngine } from "@homeostate/core";
|
|
22
|
+
import { createYjsBackend } from "@homeostate/crdt-yjs";
|
|
23
|
+
|
|
24
|
+
const engine = createSyncEngine(createYjsBackend(doc, "shared"), adapter, {
|
|
25
|
+
seed: "if-empty",
|
|
26
|
+
});
|
|
27
|
+
engine.connect();
|
|
20
28
|
```
|
|
21
29
|
|
|
30
|
+
`createMemoryBackend()` is a plain-JSON backend without replication, meant for tests.
|
|
31
|
+
`getChanges` is exported so a backend can turn a `write(next)` into fine-grained operations.
|
|
32
|
+
|
|
33
|
+
## Connecting
|
|
34
|
+
|
|
35
|
+
`connect()` reconciles the store and the backend per key, over the view the `filter` allows:
|
|
36
|
+
|
|
37
|
+
- A key the backend holds wins over the store's value, so a peer joining a populated room
|
|
38
|
+
adopts it rather than overwriting it.
|
|
39
|
+
- A synced key the backend does not hold stays in the store. With the default
|
|
40
|
+
`seed: 'if-empty'` those keys are written into the backend in one write; with
|
|
41
|
+
`seed: 'never'` they are left to the next local change.
|
|
42
|
+
- Keys the `filter` excludes are never read into the store and never written out.
|
|
43
|
+
|
|
44
|
+
Afterwards the backend owns the synced document: each local change replaces it with the
|
|
45
|
+
store's filtered state, and a key removed from the backend is removed from the store. A
|
|
46
|
+
reconnect adopts the backend again, so edits made while disconnected are dropped unless
|
|
47
|
+
they are still in the store's local-only keys.
|
|
48
|
+
|
|
22
49
|
See the [repository](https://github.com/mixedrays/homeostate) for the full workspace,
|
|
23
50
|
store adapters, and a runnable playground.
|
|
24
51
|
|
package/THIRD_PARTY_NOTICES.md
CHANGED
|
@@ -30,4 +30,4 @@ FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
|
|
30
30
|
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
|
31
31
|
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
|
32
32
|
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
33
|
-
```
|
|
33
|
+
```
|
package/dist/apply.d.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { type Change } from "./change.js";
|
|
2
|
+
/**
|
|
3
|
+
* How one store writes into its containers. `applyChanges` decides *what* to write; an
|
|
4
|
+
* implementation of this decides *how*, so the same edit script drives a MobX observable
|
|
5
|
+
* tree, a Valtio proxy, or a plain object.
|
|
6
|
+
*
|
|
7
|
+
* Every method receives a container that already lives in the store, never a copy.
|
|
8
|
+
*/
|
|
9
|
+
export interface ApplyOps {
|
|
10
|
+
/** Assign `value` at `key`, creating the property if the target is a record. */
|
|
11
|
+
set(target: object, key: string | number, value: unknown): void;
|
|
12
|
+
/** Remove the record property `key`. */
|
|
13
|
+
remove(target: object, key: string): void;
|
|
14
|
+
/** `Array.prototype.splice`: drop `deleteCount` elements at `index`, then add `inserted`. */
|
|
15
|
+
splice(target: unknown[], index: number, deleteCount: number, inserted: unknown[]): void;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Revises `value` by the character-level edit script `changes`, which is what `getChanges`
|
|
19
|
+
* returns for two strings. Each index addresses the string as revised by the steps before it.
|
|
20
|
+
*/
|
|
21
|
+
export declare const applyStringChanges: (value: string, changes: Change[]) => string;
|
|
22
|
+
/**
|
|
23
|
+
* Applies `changes` to `target` in place, mutating only the paths the edit script names.
|
|
24
|
+
* Everything else keeps its identity, which is what fine-grained stores react to.
|
|
25
|
+
*
|
|
26
|
+
* `changes` must be `getChanges(before, after)` where `before` describes the shape `target`
|
|
27
|
+
* currently holds — an adapter gets that by diffing against the snapshot it last handed out.
|
|
28
|
+
* A `PENDING` step carries no replacement value, so it can only be followed by recursing into
|
|
29
|
+
* the container already there; a target that does not mirror `before` leaves such a step
|
|
30
|
+
* unapplied rather than writing something wrong.
|
|
31
|
+
*
|
|
32
|
+
* @param target The container to mutate — a record, or an array for a positional edit script.
|
|
33
|
+
* @param changes The edit script to apply, in order.
|
|
34
|
+
* @param ops How this store writes; see {@link ApplyOps}.
|
|
35
|
+
*/
|
|
36
|
+
export declare const applyChanges: (target: object, changes: Change[], ops: ApplyOps) => void;
|
|
37
|
+
//# sourceMappingURL=apply.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"apply.d.ts","sourceRoot":"","sources":["../src/apply.ts"],"names":[],"mappings":"AAAA,OAAO,EAAc,KAAK,MAAM,EAAE,MAAM,aAAa,CAAC;AAItD;;;;;;GAMG;AACH,MAAM,WAAW,QAAQ;IACvB,gFAAgF;IAChF,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,IAAI,CAAC;IAChE,wCAAwC;IACxC,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1C,6FAA6F;IAC7F,MAAM,CACJ,MAAM,EAAE,OAAO,EAAE,EACjB,KAAK,EAAE,MAAM,EACb,WAAW,EAAE,MAAM,EACnB,QAAQ,EAAE,OAAO,EAAE,GAClB,IAAI,CAAC;CACT;AAED;;;GAGG;AACH,eAAO,MAAM,kBAAkB,GAAI,OAAO,MAAM,EAAE,SAAS,MAAM,EAAE,KAAG,MAQ3D,CAAC;AAEZ;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,YAAY,GACvB,QAAQ,MAAM,EACd,SAAS,MAAM,EAAE,EACjB,KAAK,QAAQ,KACZ,IA6BF,CAAC"}
|
package/dist/apply.js
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { ChangeType } from "./change.js";
|
|
2
|
+
/**
|
|
3
|
+
* Revises `value` by the character-level edit script `changes`, which is what `getChanges`
|
|
4
|
+
* returns for two strings. Each index addresses the string as revised by the steps before it.
|
|
5
|
+
*/
|
|
6
|
+
export const applyStringChanges = (value, changes) => changes.reduce((revised, [type, index, inserted]) => {
|
|
7
|
+
const at = index;
|
|
8
|
+
if (type === ChangeType.INSERT)
|
|
9
|
+
return revised.slice(0, at) + inserted + revised.slice(at);
|
|
10
|
+
if (type === ChangeType.DELETE)
|
|
11
|
+
return revised.slice(0, at) + revised.slice(at + 1);
|
|
12
|
+
return revised;
|
|
13
|
+
}, value);
|
|
14
|
+
/**
|
|
15
|
+
* Applies `changes` to `target` in place, mutating only the paths the edit script names.
|
|
16
|
+
* Everything else keeps its identity, which is what fine-grained stores react to.
|
|
17
|
+
*
|
|
18
|
+
* `changes` must be `getChanges(before, after)` where `before` describes the shape `target`
|
|
19
|
+
* currently holds — an adapter gets that by diffing against the snapshot it last handed out.
|
|
20
|
+
* A `PENDING` step carries no replacement value, so it can only be followed by recursing into
|
|
21
|
+
* the container already there; a target that does not mirror `before` leaves such a step
|
|
22
|
+
* unapplied rather than writing something wrong.
|
|
23
|
+
*
|
|
24
|
+
* @param target The container to mutate — a record, or an array for a positional edit script.
|
|
25
|
+
* @param changes The edit script to apply, in order.
|
|
26
|
+
* @param ops How this store writes; see {@link ApplyOps}.
|
|
27
|
+
*/
|
|
28
|
+
export const applyChanges = (target, changes, ops) => {
|
|
29
|
+
const array = Array.isArray(target) ? target : null;
|
|
30
|
+
for (const [type, key, value] of changes) {
|
|
31
|
+
switch (type) {
|
|
32
|
+
case ChangeType.PENDING: {
|
|
33
|
+
const child = target[key];
|
|
34
|
+
if (typeof child === "string")
|
|
35
|
+
ops.set(target, key, applyStringChanges(child, value));
|
|
36
|
+
else if (child !== null && typeof child === "object")
|
|
37
|
+
applyChanges(child, value, ops);
|
|
38
|
+
break;
|
|
39
|
+
}
|
|
40
|
+
case ChangeType.DELETE:
|
|
41
|
+
if (array)
|
|
42
|
+
ops.splice(array, key, 1, []);
|
|
43
|
+
else
|
|
44
|
+
ops.remove(target, key);
|
|
45
|
+
break;
|
|
46
|
+
case ChangeType.INSERT:
|
|
47
|
+
if (array)
|
|
48
|
+
ops.splice(array, key, 0, [value]);
|
|
49
|
+
else
|
|
50
|
+
ops.set(target, key, value);
|
|
51
|
+
break;
|
|
52
|
+
case ChangeType.UPDATE:
|
|
53
|
+
ops.set(target, key, value);
|
|
54
|
+
break;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
//# sourceMappingURL=apply.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"apply.js","sourceRoot":"","sources":["../src/apply.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAe,MAAM,aAAa,CAAC;AAyBtD;;;GAGG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAAG,CAAC,KAAa,EAAE,OAAiB,EAAU,EAAE,CAC7E,OAAO,CAAC,MAAM,CAAC,CAAC,OAAO,EAAE,CAAC,IAAI,EAAE,KAAK,EAAE,QAAQ,CAAC,EAAE,EAAE;IAClD,MAAM,EAAE,GAAG,KAAe,CAAC;IAC3B,IAAI,IAAI,KAAK,UAAU,CAAC,MAAM;QAC5B,OAAO,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,GAAI,QAAmB,GAAG,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;IACzE,IAAI,IAAI,KAAK,UAAU,CAAC,MAAM;QAC5B,OAAO,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,OAAO,CAAC,KAAK,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC;IACtD,OAAO,OAAO,CAAC;AACjB,CAAC,EAAE,KAAK,CAAC,CAAC;AAEZ;;;;;;;;;;;;;GAaG;AACH,MAAM,CAAC,MAAM,YAAY,GAAG,CAC1B,MAAc,EACd,OAAiB,EACjB,GAAa,EACP,EAAE;IACR,MAAM,KAAK,GAAG,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,CAAE,MAAoB,CAAC,CAAC,CAAC,IAAI,CAAC;IAEnE,KAAK,MAAM,CAAC,IAAI,EAAE,GAAG,EAAE,KAAK,CAAC,IAAI,OAAO,EAAE,CAAC;QACzC,QAAQ,IAAI,EAAE,CAAC;YACb,KAAK,UAAU,CAAC,OAAO,CAAC,CAAC,CAAC;gBACxB,MAAM,KAAK,GAAI,MAAgB,CAAC,GAAa,CAAC,CAAC;gBAC/C,IAAI,OAAO,KAAK,KAAK,QAAQ;oBAC3B,GAAG,CAAC,GAAG,CAAC,MAAM,EAAE,GAAG,EAAE,kBAAkB,CAAC,KAAK,EAAE,KAAiB,CAAC,CAAC,CAAC;qBAChE,IAAI,KAAK,KAAK,IAAI,IAAI,OAAO,KAAK,KAAK,QAAQ;oBAClD,YAAY,CAAC,KAAK,EAAE,KAAiB,EAAE,GAAG,CAAC,CAAC;gBAC9C,MAAM;YACR,CAAC;YAED,KAAK,UAAU,CAAC,MAAM;gBACpB,IAAI,KAAK;oBAAE,GAAG,CAAC,MAAM,CAAC,KAAK,EAAE,GAAa,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC;;oBAC9C,GAAG,CAAC,MAAM,CAAC,MAAM,EAAE,GAAa,CAAC,CAAC;gBACvC,MAAM;YAER,KAAK,UAAU,CAAC,MAAM;gBACpB,IAAI,KAAK;oBAAE,GAAG,CAAC,MAAM,CAAC,KAAK,EAAE,GAAa,EAAE,CAAC,EAAE,CAAC,KAAK,CAAC,CAAC,CAAC;;oBACnD,GAAG,CAAC,GAAG,CAAC,MAAM,EAAE,GAAG,EAAE,KAAK,CAAC,CAAC;gBACjC,MAAM;YAER,KAAK,UAAU,CAAC,MAAM;gBACpB,GAAG,CAAC,GAAG,CAAC,MAAM,EAAE,GAAG,EAAE,KAAK,CAAC,CAAC;gBAC5B,MAAM;QACV,CAAC;IACH,CAAC;AACH,CAAC,CAAC"}
|
package/dist/change.d.ts
CHANGED
|
@@ -2,8 +2,6 @@
|
|
|
2
2
|
* Describes the change that needs to be made.
|
|
3
3
|
*/
|
|
4
4
|
export declare enum ChangeType {
|
|
5
|
-
/** No change. */
|
|
6
|
-
NONE = "none",
|
|
7
5
|
/** A value was inserted. */
|
|
8
6
|
INSERT = "insert",
|
|
9
7
|
/** A value was replaced. */
|
|
@@ -14,7 +12,12 @@ export declare enum ChangeType {
|
|
|
14
12
|
PENDING = "pending"
|
|
15
13
|
}
|
|
16
14
|
/**
|
|
17
|
-
*
|
|
15
|
+
* One step in turning a container into another: `[type, key, value]`.
|
|
16
|
+
*
|
|
17
|
+
* Steps apply in order. A numeric key addresses the array or string as revised by the
|
|
18
|
+
* steps before it, so appliers never sort or clamp. `value` is the inserted or replacing
|
|
19
|
+
* value (a string insert may carry several characters), `undefined` for a delete, and the
|
|
20
|
+
* nested `Change[]` for a pending entry.
|
|
18
21
|
*/
|
|
19
22
|
export type Change = [ChangeType, string | number, unknown];
|
|
20
23
|
//# sourceMappingURL=change.d.ts.map
|
package/dist/change.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"change.d.ts","sourceRoot":"","sources":["../src/change.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,oBAAY,UAAU;IACpB,
|
|
1
|
+
{"version":3,"file":"change.d.ts","sourceRoot":"","sources":["../src/change.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,oBAAY,UAAU;IACpB,4BAA4B;IAC5B,MAAM,WAAW;IACjB,4BAA4B;IAC5B,MAAM,WAAW;IACjB,2BAA2B;IAC3B,MAAM,WAAW;IACjB,uEAAuE;IACvE,OAAO,YAAY;CACpB;AAED;;;;;;;GAOG;AACH,MAAM,MAAM,MAAM,GAAG,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,EAAE,OAAO,CAAC,CAAC"}
|
package/dist/change.js
CHANGED
package/dist/change.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"change.js","sourceRoot":"","sources":["../src/change.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,MAAM,CAAN,IAAY,
|
|
1
|
+
{"version":3,"file":"change.js","sourceRoot":"","sources":["../src/change.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,MAAM,CAAN,IAAY,UASX;AATD,WAAY,UAAU;IACpB,4BAA4B;IAC5B,+BAAiB,CAAA;IACjB,4BAA4B;IAC5B,+BAAiB,CAAA;IACjB,2BAA2B;IAC3B,+BAAiB,CAAA;IACjB,uEAAuE;IACvE,iCAAmB,CAAA;AACrB,CAAC,EATW,UAAU,KAAV,UAAU,QASrB"}
|
package/dist/diff.d.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import { type Change } from
|
|
2
|
-
type Diffable = Record<string, unknown> | Array<unknown> | string;
|
|
1
|
+
import { type Change } from "./change.js";
|
|
2
|
+
export type Diffable = Record<string, unknown> | Array<unknown> | string;
|
|
3
3
|
export declare const getChanges: (a: Diffable, b: Diffable) => Change[];
|
|
4
|
-
export {};
|
|
5
4
|
//# sourceMappingURL=diff.d.ts.map
|
package/dist/diff.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"diff.d.ts","sourceRoot":"","sources":["../src/diff.ts"],"names":[],"mappings":"AAAA,OAAO,EAAc,KAAK,MAAM,EAAE,MAAM,aAAa,CAAC;AAEtD,
|
|
1
|
+
{"version":3,"file":"diff.d.ts","sourceRoot":"","sources":["../src/diff.ts"],"names":[],"mappings":"AAAA,OAAO,EAAc,KAAK,MAAM,EAAE,MAAM,aAAa,CAAC;AAEtD,MAAM,MAAM,QAAQ,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,KAAK,CAAC,OAAO,CAAC,GAAG,MAAM,CAAC;AAmCzE,eAAO,MAAM,UAAU,GAAI,GAAG,QAAQ,EAAE,GAAG,QAAQ,KAAG,MAAM,EAK3D,CAAC"}
|
package/dist/diff.js
CHANGED
|
@@ -1,202 +1,192 @@
|
|
|
1
|
-
import { ChangeType } from
|
|
2
|
-
const
|
|
3
|
-
const
|
|
4
|
-
const
|
|
5
|
-
const
|
|
1
|
+
import { ChangeType } from "./change.js";
|
|
2
|
+
const isArray = (value) => Array.isArray(value);
|
|
3
|
+
const isString = (value) => typeof value === "string";
|
|
4
|
+
const isRecord = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
5
|
+
const isDiffable = (value) => isArray(value) || isString(value) || isRecord(value);
|
|
6
|
+
const isSameKind = (a, b) => isString(a) ? isString(b) : isArray(a) ? isArray(b) : isRecord(b);
|
|
7
|
+
const nestedChanges = (a, b) => isDiffable(a) && isDiffable(b) && isSameKind(a, b) ? getChanges(a, b) : null;
|
|
8
|
+
const deepEqual = (a, b) => {
|
|
9
|
+
if (a === b)
|
|
10
|
+
return true;
|
|
11
|
+
if (isArray(a) && isArray(b))
|
|
12
|
+
return (a.length === b.length && a.every((value, i) => deepEqual(value, b[i])));
|
|
13
|
+
if (isRecord(a) && isRecord(b)) {
|
|
14
|
+
const keys = Object.keys(a);
|
|
15
|
+
return (keys.length === Object.keys(b).length &&
|
|
16
|
+
keys.every((key) => key in b && deepEqual(a[key], b[key])));
|
|
17
|
+
}
|
|
18
|
+
return false;
|
|
19
|
+
};
|
|
6
20
|
export const getChanges = (a, b) => {
|
|
7
21
|
if (isString(a) && isString(b))
|
|
8
22
|
return getStringChanges(a, b);
|
|
9
|
-
|
|
23
|
+
if (isArray(a) && isArray(b))
|
|
10
24
|
return getArrayChanges(a, b);
|
|
11
|
-
|
|
25
|
+
if (isRecord(a) && isRecord(b))
|
|
12
26
|
return getRecordChanges(a, b);
|
|
13
|
-
|
|
14
|
-
|
|
27
|
+
return [];
|
|
28
|
+
};
|
|
29
|
+
const sharesCharacter = (a, b) => {
|
|
30
|
+
const characters = new Set(a);
|
|
31
|
+
for (const character of b)
|
|
32
|
+
if (characters.has(character))
|
|
33
|
+
return true;
|
|
34
|
+
return false;
|
|
15
35
|
};
|
|
16
36
|
const getStringChanges = (a, b) => {
|
|
17
37
|
if (a === b)
|
|
18
38
|
return [];
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
.
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
return
|
|
39
|
+
if (!sharesCharacter(a, b)) {
|
|
40
|
+
const deletes = Array.from(a, () => [
|
|
41
|
+
ChangeType.DELETE,
|
|
42
|
+
0,
|
|
43
|
+
undefined,
|
|
44
|
+
]);
|
|
45
|
+
return b.length === 0 ? deletes : [...deletes, [ChangeType.INSERT, 0, b]];
|
|
26
46
|
}
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
.
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
47
|
+
const changes = [];
|
|
48
|
+
let index = 0;
|
|
49
|
+
for (const { step, b: position } of editScript(a, b, (x, y) => x === y)) {
|
|
50
|
+
if (step === "eq")
|
|
51
|
+
index++;
|
|
52
|
+
else if (step === "del")
|
|
53
|
+
changes.push([ChangeType.DELETE, index, undefined]);
|
|
54
|
+
else {
|
|
55
|
+
const last = changes.length > 0 ? changes[changes.length - 1] : undefined;
|
|
56
|
+
if (last !== undefined &&
|
|
57
|
+
last[0] === ChangeType.INSERT &&
|
|
58
|
+
last[1] + last[2].length === index) {
|
|
59
|
+
last[2] = last[2] + b[position];
|
|
60
|
+
}
|
|
61
|
+
else
|
|
62
|
+
changes.push([ChangeType.INSERT, index, b[position]]);
|
|
63
|
+
index++;
|
|
64
|
+
}
|
|
40
65
|
}
|
|
66
|
+
return changes;
|
|
41
67
|
};
|
|
42
68
|
const getArrayChanges = (a, b) => {
|
|
43
|
-
const
|
|
44
|
-
let
|
|
45
|
-
let
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
changeList.push([ChangeType.INSERT, index, b[bIndex]]);
|
|
58
|
-
finalIndices += 2;
|
|
59
|
-
bOffset++;
|
|
60
|
-
}
|
|
61
|
-
else if (currentDiff.length !== 0) {
|
|
62
|
-
changeList.push([ChangeType.PENDING, index, currentDiff]);
|
|
63
|
-
finalIndices++;
|
|
64
|
-
}
|
|
65
|
-
else
|
|
66
|
-
finalIndices++;
|
|
69
|
+
const changes = [];
|
|
70
|
+
let index = 0;
|
|
71
|
+
let deleted = [];
|
|
72
|
+
let inserted = [];
|
|
73
|
+
const flush = () => {
|
|
74
|
+
const pairs = Math.min(deleted.length, inserted.length);
|
|
75
|
+
for (let i = 0; i < pairs; i++) {
|
|
76
|
+
const next = b[inserted[i]];
|
|
77
|
+
const nested = nestedChanges(a[deleted[i]], next);
|
|
78
|
+
if (nested === null)
|
|
79
|
+
changes.push([ChangeType.UPDATE, index, next]);
|
|
80
|
+
else if (nested.length > 0)
|
|
81
|
+
changes.push([ChangeType.PENDING, index, nested]);
|
|
82
|
+
index++;
|
|
67
83
|
}
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
84
|
+
for (let i = pairs; i < deleted.length; i++)
|
|
85
|
+
changes.push([ChangeType.DELETE, index, undefined]);
|
|
86
|
+
for (let i = pairs; i < inserted.length; i++) {
|
|
87
|
+
changes.push([ChangeType.INSERT, index, b[inserted[i]]]);
|
|
88
|
+
index++;
|
|
72
89
|
}
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
90
|
+
deleted = [];
|
|
91
|
+
inserted = [];
|
|
92
|
+
};
|
|
93
|
+
for (const { step, a: from, b: to } of editScript(a, b, deepEqual)) {
|
|
94
|
+
if (step === "del")
|
|
95
|
+
deleted.push(from);
|
|
96
|
+
else if (step === "ins")
|
|
97
|
+
inserted.push(to);
|
|
98
|
+
else {
|
|
99
|
+
flush();
|
|
100
|
+
index++;
|
|
76
101
|
}
|
|
77
|
-
else
|
|
78
|
-
finalIndices++;
|
|
79
|
-
}
|
|
80
|
-
if (finalIndices < b.length) {
|
|
81
|
-
b.slice(a.length).forEach((value, index) => changeList.push([ChangeType.INSERT, finalIndices + index, value]));
|
|
82
102
|
}
|
|
83
|
-
|
|
103
|
+
flush();
|
|
104
|
+
return changes;
|
|
84
105
|
};
|
|
85
106
|
const getRecordChanges = (a, b) => {
|
|
86
|
-
const
|
|
87
|
-
Object.
|
|
88
|
-
if (!(property in b)
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
Object.entries(b).forEach(([property, value]) => {
|
|
107
|
+
const changes = [];
|
|
108
|
+
for (const property of Object.keys(a))
|
|
109
|
+
if (!(property in b))
|
|
110
|
+
changes.push([ChangeType.DELETE, property, undefined]);
|
|
111
|
+
for (const [property, value] of Object.entries(b)) {
|
|
92
112
|
if (!(property in a))
|
|
93
|
-
|
|
94
|
-
else
|
|
95
|
-
const
|
|
96
|
-
if (
|
|
97
|
-
|
|
113
|
+
changes.push([ChangeType.INSERT, property, value]);
|
|
114
|
+
else {
|
|
115
|
+
const nested = nestedChanges(a[property], value);
|
|
116
|
+
if (nested === null) {
|
|
117
|
+
if (a[property] !== value)
|
|
118
|
+
changes.push([ChangeType.UPDATE, property, value]);
|
|
119
|
+
}
|
|
120
|
+
else if (nested.length > 0)
|
|
121
|
+
changes.push([ChangeType.PENDING, property, nested]);
|
|
98
122
|
}
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
});
|
|
102
|
-
return changeList;
|
|
103
|
-
};
|
|
104
|
-
const hasCommonSubsequence = (a, b) => {
|
|
105
|
-
const alphabetOfA = a.split('');
|
|
106
|
-
const alphabetOfB = b.split('');
|
|
107
|
-
let hasCommonSubsequence = false;
|
|
108
|
-
for (const c of alphabetOfA)
|
|
109
|
-
hasCommonSubsequence = hasCommonSubsequence || alphabetOfB.includes(c);
|
|
110
|
-
return hasCommonSubsequence;
|
|
123
|
+
}
|
|
124
|
+
return changes;
|
|
111
125
|
};
|
|
112
126
|
/**
|
|
113
|
-
*
|
|
114
|
-
*
|
|
115
|
-
* Credit to [this JavaScript implementation](https://github.com/cubicdaiya/onp/blob/master/javascript/onp.js).
|
|
116
|
-
*
|
|
117
|
-
* @param a The old string to transform.
|
|
118
|
-
* @param b The new string to transform to.
|
|
119
|
-
* @param isReversed Whether or not a or b have been swapped.
|
|
120
|
-
* @returns A list of changes that that turn a into b.
|
|
127
|
+
* Shortest edit script from `a` to `b` by Wu et al.'s O(NP) algorithm. Adapted from
|
|
128
|
+
* https://github.com/cubicdaiya/onp/blob/master/javascript/onp.js.
|
|
121
129
|
*/
|
|
122
|
-
const
|
|
123
|
-
const
|
|
124
|
-
const
|
|
130
|
+
const editScript = (a, b, equal) => {
|
|
131
|
+
const swapped = a.length > b.length;
|
|
132
|
+
const s = swapped ? b : a;
|
|
133
|
+
const t = swapped ? a : b;
|
|
134
|
+
const m = s.length;
|
|
135
|
+
const n = t.length;
|
|
125
136
|
const delta = n - m;
|
|
126
|
-
const
|
|
127
|
-
const
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
const
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
const snake = (k, p, q) => {
|
|
135
|
-
let y = Math.max(p, q);
|
|
137
|
+
const offset = m + 1;
|
|
138
|
+
const furthest = new Array(m + n + 3).fill(-1);
|
|
139
|
+
const pointAt = new Array(m + n + 3).fill(-1);
|
|
140
|
+
const points = [];
|
|
141
|
+
const snake = (k) => {
|
|
142
|
+
const fromBelow = furthest[k + offset - 1] + 1;
|
|
143
|
+
const fromAbove = furthest[k + offset + 1];
|
|
144
|
+
let y = Math.max(fromBelow, fromAbove);
|
|
136
145
|
let x = y - k;
|
|
137
|
-
while (x < m && y < n &&
|
|
146
|
+
while (x < m && y < n && equal(s[x], t[y])) {
|
|
138
147
|
x++;
|
|
139
148
|
y++;
|
|
140
149
|
}
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
150
|
+
furthest[k + offset] = y;
|
|
151
|
+
pointAt[k + offset] = points.length;
|
|
152
|
+
points.push({
|
|
153
|
+
x,
|
|
154
|
+
y,
|
|
155
|
+
prev: fromBelow > fromAbove
|
|
156
|
+
? pointAt[k + offset - 1]
|
|
157
|
+
: pointAt[k + offset + 1],
|
|
158
|
+
});
|
|
148
159
|
};
|
|
149
|
-
let p =
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
for (let k =
|
|
153
|
-
|
|
160
|
+
for (let p = 0; furthest[delta + offset] !== n; p++) {
|
|
161
|
+
for (let k = -p; k < delta; k++)
|
|
162
|
+
snake(k);
|
|
163
|
+
for (let k = delta + p; k > delta; k--)
|
|
164
|
+
snake(k);
|
|
165
|
+
snake(delta);
|
|
166
|
+
}
|
|
167
|
+
const path = [];
|
|
168
|
+
for (let i = pointAt[delta + offset]; i !== -1; i = points[i].prev)
|
|
169
|
+
path.push(points[i]);
|
|
170
|
+
const ops = [];
|
|
171
|
+
let x = 0;
|
|
172
|
+
let y = 0;
|
|
173
|
+
const op = (step) => swapped ? { step, a: y, b: x } : { step, a: x, b: y };
|
|
174
|
+
for (let i = path.length - 1; i >= 0; i--) {
|
|
175
|
+
const end = path[i];
|
|
176
|
+
if (end.y - end.x > y - x) {
|
|
177
|
+
ops.push(op(swapped ? "del" : "ins"));
|
|
178
|
+
y++;
|
|
154
179
|
}
|
|
155
|
-
|
|
156
|
-
|
|
180
|
+
else if (end.y - end.x < y - x) {
|
|
181
|
+
ops.push(op(swapped ? "ins" : "del"));
|
|
182
|
+
x++;
|
|
157
183
|
}
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
while (k !== -1) {
|
|
163
|
-
editPath[editPath.length] = {
|
|
164
|
-
x: pathPositions[k].x,
|
|
165
|
-
y: pathPositions[k].y,
|
|
166
|
-
};
|
|
167
|
-
k = pathPositions[k].k;
|
|
168
|
-
}
|
|
169
|
-
const changeList = [];
|
|
170
|
-
let x = 0, y = 0, index = -1;
|
|
171
|
-
for (let i = editPath.length - 1; i >= 0; i--) {
|
|
172
|
-
while (x <= editPath[i].x || y <= editPath[i].y) {
|
|
173
|
-
if (editPath[i].y - editPath[i].x > y - x) {
|
|
174
|
-
if (isReversed) {
|
|
175
|
-
changeList[changeList.length] = [ChangeType.DELETE, index, undefined];
|
|
176
|
-
}
|
|
177
|
-
else {
|
|
178
|
-
changeList[changeList.length] = [ChangeType.INSERT, index, b[y - 1]];
|
|
179
|
-
index++;
|
|
180
|
-
}
|
|
181
|
-
y++;
|
|
182
|
-
}
|
|
183
|
-
else if (editPath[i].y - editPath[i].x < y - x) {
|
|
184
|
-
if (isReversed) {
|
|
185
|
-
changeList[changeList.length] = [ChangeType.INSERT, index, a[x - 1]];
|
|
186
|
-
index++;
|
|
187
|
-
}
|
|
188
|
-
else {
|
|
189
|
-
changeList[changeList.length] = [ChangeType.DELETE, index, undefined];
|
|
190
|
-
}
|
|
191
|
-
x++;
|
|
192
|
-
}
|
|
193
|
-
else {
|
|
194
|
-
x++;
|
|
195
|
-
y++;
|
|
196
|
-
index++;
|
|
197
|
-
}
|
|
184
|
+
while (x < end.x) {
|
|
185
|
+
ops.push(op("eq"));
|
|
186
|
+
x++;
|
|
187
|
+
y++;
|
|
198
188
|
}
|
|
199
189
|
}
|
|
200
|
-
return
|
|
190
|
+
return ops;
|
|
201
191
|
};
|
|
202
192
|
//# sourceMappingURL=diff.js.map
|