@homeostate/core 0.0.0 → 0.1.1

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 ADDED
@@ -0,0 +1,7 @@
1
+ # @homeostate/core
2
+
3
+ ## 0.1.1
4
+
5
+ ### Patch Changes
6
+
7
+ - 8bd452e: Init release
package/README.md CHANGED
@@ -3,22 +3,52 @@
3
3
  > **Draft / placeholder release.** `0.0.0` reserves the name while the API is still
4
4
  > being designed. Nothing here is stable — do not depend on it yet.
5
5
 
6
- State-manager agnostic sync engine between a store adapter and a [Yjs](https://github.com/yjs/yjs) document.
6
+ State-manager and CRDT-backend agnostic sync engine. It keeps a store, reached through a
7
+ `StoreAdapter`, in sync with a `CrdtBackend` such as
8
+ [`@homeostate/crdt-yjs`](https://github.com/mixedrays/homeostate/tree/main/packages/crdt-yjs),
9
+ [`@homeostate/crdt-loro`](https://github.com/mixedrays/homeostate/tree/main/packages/crdt-loro), or
10
+ [`@homeostate/crdt-automerge`](https://github.com/mixedrays/homeostate/tree/main/packages/crdt-automerge).
7
11
 
8
12
  ## Install
9
13
 
10
14
  ```bash
11
- npm install @homeostate/core yjs
15
+ npm install @homeostate/core @homeostate/crdt-yjs yjs
12
16
  ```
13
17
 
14
- `yjs` is a peer dependency.
18
+ `@homeostate/core` has no runtime dependencies; pick a backend package for the CRDT library
19
+ you use.
15
20
 
16
21
  ## Usage
17
22
 
18
23
  ```ts
19
- import { createSyncEngine } from '@homeostate/core';
24
+ import { createSyncEngine } from "@homeostate/core";
25
+ import { createYjsBackend } from "@homeostate/crdt-yjs";
26
+
27
+ const engine = createSyncEngine(createYjsBackend(doc, "shared"), adapter, {
28
+ seed: "if-empty",
29
+ });
30
+ engine.connect();
20
31
  ```
21
32
 
33
+ `createMemoryBackend()` is a plain-JSON backend without replication, meant for tests.
34
+ `getChanges` is exported so a backend can turn a `write(next)` into fine-grained operations.
35
+
36
+ ## Connecting
37
+
38
+ `connect()` reconciles the store and the backend per key, over the view the `filter` allows:
39
+
40
+ - A key the backend holds wins over the store's value, so a peer joining a populated room
41
+ adopts it rather than overwriting it.
42
+ - A synced key the backend does not hold stays in the store. With the default
43
+ `seed: 'if-empty'` those keys are written into the backend in one write; with
44
+ `seed: 'never'` they are left to the next local change.
45
+ - Keys the `filter` excludes are never read into the store and never written out.
46
+
47
+ Afterwards the backend owns the synced document: each local change replaces it with the
48
+ store's filtered state, and a key removed from the backend is removed from the store. A
49
+ reconnect adopts the backend again, so edits made while disconnected are dropped unless
50
+ they are still in the store's local-only keys.
51
+
22
52
  See the [repository](https://github.com/mixedrays/homeostate) for the full workspace,
23
53
  store adapters, and a runnable playground.
24
54
 
@@ -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
+ ```
@@ -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
- * A record that documents a change to an entry in an array or object.
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
@@ -1 +1 @@
1
- {"version":3,"file":"change.d.ts","sourceRoot":"","sources":["../src/change.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,oBAAY,UAAU;IACpB,iBAAiB;IACjB,IAAI,SAAS;IACb,4BAA4B;IAC5B,MAAM,WAAW;IACjB,4BAA4B;IAC5B,MAAM,WAAW;IACjB,2BAA2B;IAC3B,MAAM,WAAW;IACjB,uEAAuE;IACvE,OAAO,YAAY;CACpB;AAED;;GAEG;AACH,MAAM,MAAM,MAAM,GAAG,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,EAAE,OAAO,CAAC,CAAC"}
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
@@ -3,8 +3,6 @@
3
3
  */
4
4
  export var ChangeType;
5
5
  (function (ChangeType) {
6
- /** No change. */
7
- ChangeType["NONE"] = "none";
8
6
  /** A value was inserted. */
9
7
  ChangeType["INSERT"] = "insert";
10
8
  /** A value was replaced. */
@@ -1 +1 @@
1
- {"version":3,"file":"change.js","sourceRoot":"","sources":["../src/change.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,MAAM,CAAN,IAAY,UAWX;AAXD,WAAY,UAAU;IACpB,iBAAiB;IACjB,2BAAa,CAAA;IACb,4BAA4B;IAC5B,+BAAiB,CAAA;IACjB,4BAA4B;IAC5B,+BAAiB,CAAA;IACjB,2BAA2B;IAC3B,+BAAiB,CAAA;IACjB,uEAAuE;IACvE,iCAAmB,CAAA;AACrB,CAAC,EAXW,UAAU,KAAV,UAAU,QAWrB"}
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 './change.js';
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
@@ -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,KAAK,QAAQ,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,KAAK,CAAC,OAAO,CAAC,GAAG,MAAM,CAAC;AAYlE,eAAO,MAAM,UAAU,GAAI,GAAG,QAAQ,EAAE,GAAG,QAAQ,KAAG,MAAM,EAK3D,CAAC"}
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 './change.js';
2
- const isDiffable = (v) => isArray(v) || isString(v) || (v !== null && typeof v === 'object');
3
- const isArray = (d) => d instanceof Array;
4
- const isString = (d) => typeof d === 'string';
5
- const isRecord = (d) => !isArray(d) && !isString(d);
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
- else if (isArray(a) && isArray(b))
23
+ if (isArray(a) && isArray(b))
10
24
  return getArrayChanges(a, b);
11
- else if (isRecord(a) && isRecord(b))
25
+ if (isRecord(a) && isRecord(b))
12
26
  return getRecordChanges(a, b);
13
- else
14
- return [];
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
- else if (a.length === 0) {
20
- return b
21
- .split('')
22
- .map((character, index) => [ChangeType.INSERT, index, character]);
23
- }
24
- else if (b.length === 0) {
25
- return a.split('').map(() => [ChangeType.DELETE, 0, undefined]);
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
- else if (!hasCommonSubsequence(a, b)) {
28
- const deletes = a
29
- .split('')
30
- .map(() => [ChangeType.DELETE, 0, undefined]);
31
- const inserts = b
32
- .split('')
33
- .map((character, index) => [ChangeType.INSERT, index, character]);
34
- return deletes.concat(inserts);
35
- }
36
- else {
37
- const m = a.length, n = b.length;
38
- const reverse = m >= n;
39
- return reverse ? _diffText(b, a, reverse) : _diffText(a, b, reverse);
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 changeList = [];
44
- let finalIndices = 0;
45
- let bOffset = 0;
46
- for (let index = 0; index < a.length; index++) {
47
- const value = a[index];
48
- const bIndex = index + bOffset;
49
- if (b[bIndex] === undefined)
50
- changeList.push([ChangeType.DELETE, index, undefined]);
51
- else if (isDiffable(value) && isDiffable(b[bIndex])) {
52
- const currentDiff = getChanges(value, b[bIndex]);
53
- const nextDiff = typeof b[bIndex + 1] === 'undefined'
54
- ? []
55
- : getChanges(value, b[bIndex + 1]);
56
- if (typeof b[bIndex + 1] !== 'undefined' && nextDiff.length === 0) {
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
- else if (value !== b[bIndex] && value === b[bIndex + 1]) {
69
- changeList.push([ChangeType.INSERT, bIndex, b[bIndex]]);
70
- finalIndices += 2;
71
- bOffset++;
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
- else if (value !== b[bIndex] && value !== b[bIndex + 1]) {
74
- changeList.push([ChangeType.UPDATE, bIndex, b[bIndex]]);
75
- finalIndices++;
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
- return changeList;
103
+ flush();
104
+ return changes;
84
105
  };
85
106
  const getRecordChanges = (a, b) => {
86
- const changeList = [];
87
- Object.entries(a).forEach(([property, value]) => {
88
- if (!(property in b) && !(value instanceof Function))
89
- changeList.push([ChangeType.DELETE, property, undefined]);
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
- changeList.push([ChangeType.INSERT, property, value]);
94
- else if (isDiffable(a[property]) && isDiffable(value)) {
95
- const d = getChanges(a[property], value);
96
- if (d.length !== 0)
97
- changeList.push([ChangeType.PENDING, property, d]);
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
- else if (a[property] !== value)
100
- changeList.push([ChangeType.UPDATE, property, value]);
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
- * An adaptation of Wu et al. O(NP) text diff. (See docs/text-diff)
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 _diffText = (a, b, isReversed) => {
123
- const m = a.length, n = b.length;
124
- const offset = m;
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 size = m + n + 1;
127
- const frontierPoints = [];
128
- for (let i = 0; i < size; i++)
129
- frontierPoints[i] = -1;
130
- const path = [];
131
- for (let i = 0; i < size; i++)
132
- path[i] = -1;
133
- const pathPositions = [];
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 && a[x] === b[y]) {
146
+ while (x < m && y < n && equal(s[x], t[y])) {
138
147
  x++;
139
148
  y++;
140
149
  }
141
- path[k + offset] = pathPositions.length;
142
- pathPositions[pathPositions.length] = {
143
- x: x,
144
- y: y,
145
- k: p > q ? path[k + offset - 1] : path[k + offset + 1],
146
- };
147
- return y;
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 = -1;
150
- do {
151
- p++;
152
- for (let k = -p; k < delta; k++) {
153
- frontierPoints[k + offset] = snake(k, frontierPoints[k + offset - 1] + 1, frontierPoints[k + offset + 1]);
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
- for (let k = delta + p; k > delta; k--) {
156
- frontierPoints[k + offset] = snake(k, frontierPoints[k + offset - 1] + 1, frontierPoints[k + offset + 1]);
180
+ else if (end.y - end.x < y - x) {
181
+ ops.push(op(swapped ? "ins" : "del"));
182
+ x++;
157
183
  }
158
- frontierPoints[delta + offset] = snake(delta, frontierPoints[delta + offset - 1] + 1, frontierPoints[delta + offset + 1]);
159
- } while (frontierPoints[delta + offset] !== n);
160
- let k = path[delta + offset];
161
- const editPath = [];
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 changeList;
190
+ return ops;
201
191
  };
202
192
  //# sourceMappingURL=diff.js.map