[
  {
    "path": ".babelrc",
    "content": "{\n  \"presets\": [\n    [\n      \"@babel/preset-env\"\n    ]\n  ]\n}\n"
  },
  {
    "path": ".eslintrc.json",
    "content": "{\n    \"env\": {\n        \"browser\": true,\n        \"commonjs\": true,\n        \"es2015\": true,\n        \"node\": true,\n        \"mocha\": true\n    },\n    \"ignorePatterns\": \"dist/**\",\n    \"extends\": [\"eslint:recommended\", \"plugin:compat/recommended\"],\n    \"parserOptions\": {\n        \"ecmaVersion\": 2015\n    },\n    \"rules\": {\n        \"accessor-pairs\": \"error\",\n        \"array-bracket-newline\": \"off\",\n        \"array-bracket-spacing\": \"off\",\n        \"array-callback-return\": \"error\",\n        \"array-element-newline\": \"off\",\n        \"arrow-body-style\": \"error\",\n        \"arrow-parens\": \"off\",\n        \"arrow-spacing\": [\n            \"error\",\n            {\n                \"after\": true,\n                \"before\": true\n            }\n        ],\n        \"block-scoped-var\": \"error\",\n        \"block-spacing\": \"error\",\n        \"brace-style\": [\n            \"error\",\n            \"1tbs\", { \"allowSingleLine\": true }\n        ],\n        \"camelcase\": \"off\",\n        \"capitalized-comments\": \"off\",\n        \"class-methods-use-this\": \"off\",\n        \"comma-dangle\": \"off\",\n        \"comma-spacing\": [\n            \"error\",\n            {\n                \"after\": true,\n                \"before\": false\n            }\n        ],\n        \"comma-style\": [\n            \"error\",\n            \"last\"\n        ],\n        \"complexity\": \"off\",\n        \"computed-property-spacing\": [\n            \"off\",\n            \"never\"\n        ],\n        \"consistent-return\": \"off\",\n        \"consistent-this\": \"error\",\n        \"curly\": \"off\",\n        \"default-case\": \"off\",\n        \"default-case-last\": \"error\",\n        \"default-param-last\": \"error\",\n        \"dot-location\": [\n            \"error\",\n            \"property\"\n        ],\n        \"dot-notation\": \"error\",\n        \"eol-last\": \"error\",\n        \"eqeqeq\": \"off\",\n        \"func-call-spacing\": \"off\",\n        \"func-name-matching\": \"error\",\n        \"func-names\": \"off\",\n        \"func-style\": [\n            \"error\",\n            \"declaration\"\n        ],\n        \"function-paren-newline\": \"off\",\n        \"generator-star-spacing\": \"error\",\n        \"grouped-accessor-pairs\": \"error\",\n        \"guard-for-in\": \"error\",\n        \"id-denylist\": \"error\",\n        \"id-length\": \"off\",\n        \"id-match\": \"error\",\n        \"implicit-arrow-linebreak\": \"off\",\n        \"indent\": \"off\",\n        \"init-declarations\": \"off\",\n        \"jsx-quotes\": \"error\",\n        \"key-spacing\": \"off\",\n        \"keyword-spacing\": [\n            \"error\",\n            {\n                \"after\": true,\n                \"before\": true\n            }\n        ],\n        \"line-comment-position\": \"off\",\n        \"linebreak-style\": \"off\",\n        \"lines-around-comment\": \"off\",\n        \"lines-between-class-members\": \"error\",\n        \"max-classes-per-file\": \"off\",\n        \"max-depth\": \"off\",\n        \"max-len\": \"off\",\n        \"max-lines\": \"off\",\n        \"max-lines-per-function\": \"off\",\n        \"max-nested-callbacks\": \"error\",\n        \"max-params\": \"off\",\n        \"max-statements\": \"off\",\n        \"max-statements-per-line\": \"off\",\n        \"multiline-comment-style\": [\n            \"error\",\n            \"separate-lines\"\n        ],\n        \"new-parens\": \"error\",\n        \"newline-per-chained-call\": \"off\",\n        \"no-alert\": \"error\",\n        \"no-array-constructor\": \"error\",\n        \"no-await-in-loop\": \"error\",\n        \"no-bitwise\": \"off\",\n        \"no-caller\": \"error\",\n        \"no-confusing-arrow\": \"error\",\n        \"no-console\": \"error\",\n        \"no-constant-condition\": [\n            \"error\",\n            {\n                \"checkLoops\": false\n            }\n        ],\n        \"no-constructor-return\": \"error\",\n        \"no-continue\": \"off\",\n        \"no-div-regex\": \"error\",\n        \"no-duplicate-imports\": \"error\",\n        \"no-else-return\": \"off\",\n        \"no-empty-function\": \"off\",\n        \"no-eq-null\": \"error\",\n        \"no-eval\": \"error\",\n        \"no-extend-native\": \"error\",\n        \"no-extra-bind\": \"error\",\n        \"no-extra-label\": \"error\",\n        \"no-extra-parens\": \"off\",\n        \"no-floating-decimal\": \"error\",\n        \"no-implicit-coercion\": \"off\",\n        \"no-implicit-globals\": \"error\",\n        \"no-implied-eval\": \"error\",\n        \"no-inline-comments\": \"off\",\n        \"no-invalid-this\": \"error\",\n        \"no-iterator\": \"error\",\n        \"no-label-var\": \"error\",\n        \"no-labels\": \"error\",\n        \"no-lone-blocks\": \"error\",\n        \"no-lonely-if\": \"off\",\n        \"no-loop-func\": \"off\",\n        \"no-loss-of-precision\": \"error\",\n        \"no-magic-numbers\": \"off\",\n        \"no-mixed-operators\": \"off\",\n        \"no-multi-assign\": \"error\",\n        \"no-multi-spaces\": \"off\",\n        \"no-multi-str\": \"error\",\n        \"no-multiple-empty-lines\": \"error\",\n        \"no-negated-condition\": \"off\",\n        \"no-nested-ternary\": \"off\",\n        \"no-new\": \"error\",\n        \"no-new-func\": \"error\",\n        \"no-new-object\": \"error\",\n        \"no-new-wrappers\": \"error\",\n        \"no-nonoctal-decimal-escape\": \"error\",\n        \"no-octal-escape\": \"error\",\n        \"no-param-reassign\": \"off\",\n        \"no-plusplus\": \"off\",\n        \"no-promise-executor-return\": \"error\",\n        \"no-proto\": \"error\",\n        \"no-restricted-exports\": \"error\",\n        \"no-restricted-globals\": \"error\",\n        \"no-restricted-imports\": \"error\",\n        \"no-restricted-properties\": \"error\",\n        \"no-restricted-syntax\": \"error\",\n        \"no-return-assign\": \"off\",\n        \"no-return-await\": \"error\",\n        \"no-script-url\": \"error\",\n        \"no-self-compare\": \"error\",\n        \"no-sequences\": \"error\",\n        \"no-shadow\": \"off\",\n        \"no-tabs\": \"error\",\n        \"no-template-curly-in-string\": \"error\",\n        \"no-ternary\": \"off\",\n        \"no-throw-literal\": \"error\",\n        \"no-trailing-spaces\": \"error\",\n        \"no-undef-init\": \"off\",\n        \"no-undefined\": \"off\",\n        \"no-underscore-dangle\": \"off\",\n        \"no-unmodified-loop-condition\": \"error\",\n        \"no-unneeded-ternary\": \"error\",\n        \"no-unreachable-loop\": \"error\",\n        \"no-unsafe-optional-chaining\": \"error\",\n        \"no-unused-expressions\": \"error\",\n        \"no-unused-vars\": [\"error\", { \"args\": \"after-used\" }],\n        \"no-use-before-define\": \"off\",\n        \"no-useless-backreference\": \"error\",\n        \"no-useless-call\": \"error\",\n        \"no-useless-computed-key\": \"error\",\n        \"no-useless-concat\": \"error\",\n        \"no-useless-constructor\": \"error\",\n        \"no-useless-rename\": \"error\",\n        \"no-useless-return\": \"error\",\n        \"no-var\": \"error\",\n        \"no-void\": \"error\",\n        \"no-warning-comments\": \"off\",\n        \"no-whitespace-before-property\": \"off\",\n        \"nonblock-statement-body-position\": \"error\",\n        \"object-curly-newline\": \"error\",\n        \"object-curly-spacing\": \"off\",\n        \"object-property-newline\": \"off\",\n        \"object-shorthand\": \"error\",\n        \"one-var\": \"off\",\n        \"one-var-declaration-per-line\": \"off\",\n        \"operator-assignment\": \"off\",\n        \"operator-linebreak\": \"error\",\n        \"padded-blocks\": \"off\",\n        \"padding-line-between-statements\": \"error\",\n        \"prefer-arrow-callback\": \"off\",\n        \"prefer-const\": \"off\",\n        \"prefer-destructuring\": \"off\",\n        \"prefer-exponentiation-operator\": \"error\",\n        \"prefer-named-capture-group\": \"off\",\n        \"prefer-numeric-literals\": \"error\",\n        \"prefer-object-spread\": \"off\",\n        \"prefer-promise-reject-errors\": \"error\",\n        \"prefer-regex-literals\": \"error\",\n        \"prefer-rest-params\": \"error\",\n        \"prefer-spread\": \"error\",\n        \"prefer-template\": \"off\",\n        \"quote-props\": \"off\",\n        \"quotes\": \"off\",\n        \"radix\": \"error\",\n        \"require-atomic-updates\": \"error\",\n        \"require-await\": \"error\",\n        \"require-unicode-regexp\": \"off\",\n        \"rest-spread-spacing\": \"error\",\n        \"semi\": \"off\",\n        \"semi-spacing\": [\n            \"error\",\n            {\n                \"after\": true,\n                \"before\": false\n            }\n        ],\n        \"semi-style\": [\n            \"error\",\n            \"first\"\n        ],\n        \"sort-imports\": \"error\",\n        \"sort-keys\": \"off\",\n        \"sort-vars\": \"off\",\n        \"space-before-blocks\": \"error\",\n        \"space-before-function-paren\": \"off\",\n        \"space-in-parens\": [\n            \"error\",\n            \"never\"\n        ],\n        \"space-infix-ops\": \"error\",\n        \"space-unary-ops\": \"error\",\n        \"spaced-comment\": [\n            \"error\",\n            \"always\"\n        ],\n        \"strict\": [\n            \"error\",\n            \"never\"\n        ],\n        \"switch-colon-spacing\": \"error\",\n        \"symbol-description\": \"error\",\n        \"template-curly-spacing\": [\n            \"error\",\n            \"never\"\n        ],\n        \"template-tag-spacing\": \"error\",\n        \"unicode-bom\": [\n            \"error\",\n            \"never\"\n        ],\n        \"vars-on-top\": \"error\",\n        \"wrap-iife\": \"error\",\n        \"wrap-regex\": \"off\",\n        \"yield-star-spacing\": \"error\",\n        \"yoda\": [\n            \"error\",\n            \"never\"\n        ]\n    }\n}\n"
  },
  {
    "path": ".github/workflows/automerge-ci.yml",
    "content": "name: CI\non: [push, pull_request]\n\njobs:\n  node-build:\n    runs-on: ubuntu-latest\n    strategy:\n      matrix:\n        node-version: [12.x, 14.x, 16.x]\n    steps:\n    - name: Check out repo\n      uses: actions/checkout@v2\n    - name: Use Node.js ${{ matrix.node-version }}\n      uses: actions/setup-node@v2\n      with:\n        node-version: ${{ matrix.node-version }}\n        cache: 'yarn'\n    - name: Install dependencies\n      run: yarn\n    - name: ESLint\n      run: yarn lint\n    - name: Test suite\n      run: yarn test\n    - name: Bundle\n      run: yarn build\n    - name: Test suite using bundle\n      run: TEST_DIST=1 yarn test\n    - name: Load bundled code\n      run: node -e \"const Automerge = require(\\\"./dist/automerge\\\")\"\n\n# browsertest:\n#   runs-on: ubuntu-latest\n#   # Don't run this job when triggered from a forked repository, since the secrets\n#   # (Sauce Labs credentials) are not available in that context\n#   if: ${{ github.repository == 'automerge/automerge' }}\n#   steps:\n#     - uses: actions/checkout@v2\n#     - name: Use Node.js\n#       uses: actions/setup-node@v2\n#       with:\n#         node-version: 16.x\n#         cache: 'yarn'\n#     - name: Install dependencies\n#       run: yarn\n#     - name: Bundle\n#       run: yarn build\n#     - name: Sauce Connect\n#       uses: saucelabs/sauce-connect-action@v1\n#       with:\n#         username: ${{ secrets.SAUCE_USERNAME }}\n#         accessKey: ${{ secrets.SAUCE_ACCESS_KEY }}\n#         tunnelIdentifier: github-action-tunnel\n#         scVersion: 4.7.0\n#     - name: Run browser tests\n#       run: node_modules/.bin/karma start karma.sauce.js\n#       env:\n#         SAUCE_USERNAME: ${{secrets.SAUCE_USERNAME}}\n#         SAUCE_ACCESS_KEY: ${{secrets.SAUCE_ACCESS_KEY}}\n\n  npm-publish:\n    name: npm-publish\n    if: ${{ github.repository == 'automerge/automerge' && github.ref == 'refs/heads/main' }}\n#   needs: [ node-build, browsertest ]\n    needs: [ node-build ]\n    runs-on: ubuntu-latest\n    steps:\n    - name: Check out repo\n      uses: actions/checkout@v2\n    - name: Use Node.js\n      uses: actions/setup-node@v2\n      with:\n        node-version: 16\n    - name: Install dependencies\n      run: yarn install\n    - name: npm publish if version has been updated\n      uses: JS-DevTools/npm-publish@v1\n      with:\n        token: ${{ secrets.NPM_AUTH_TOKEN }}\n        check-version: true\n"
  },
  {
    "path": ".gitignore",
    "content": "/coverage\n/dist\n/node_modules\n.nyc_output\n.vscode\n"
  },
  {
    "path": ".mocharc.yaml",
    "content": "use_strict: true\nrequire:\n  - ts-node/register\n  - tsconfig-paths/register\nwatch-files:\n  - 'src/*.js'\n  - 'frontend/*.js'\n  - 'backend/*.js'\n  - 'test/*.js'\n  - 'test/*.ts'\nspec:\n  - 'test/*test*.js'\n  - 'test/*test*.ts'\n"
  },
  {
    "path": "@types/automerge/index.d.ts",
    "content": "declare module 'automerge' {\n  /**\n   * The return type of `Automerge.init<T>()`, `Automerge.change<T>()`, etc. where `T` is the\n   * original type. It is a recursively frozen version of the original type.\n   */\n  type Doc<T> = FreezeObject<T>\n\n  type ChangeFn<T> = (doc: T) => void\n\n  // Automerge.* functions\n\n  function init<T>(options?: InitOptions<T>): Doc<T>\n  function from<T>(initialState: T | Doc<T>, options?: InitOptions<T>): Doc<T>\n  function clone<T>(doc: Doc<T>, options?: InitOptions<T>): Doc<T>\n  function free<T>(doc: Doc<T>): void\n\n  type InitOptions<T> =\n    | string // = actorId\n    | { \n      actorId?: string\n      deferActorId?: boolean\n      freeze?: boolean\n      patchCallback?: PatchCallback<T>\n      observable?: Observable\n    }\n\n  type ChangeOptions<T> =\n    | string // = message\n    | {\n      message?: string\n      time?: number\n      patchCallback?: PatchCallback<T>\n    }\n\n  type PatchCallback<T> = (patch: Patch, before: T, after: T, local: boolean, changes: BinaryChange[]) => void\n  type ObserverCallback<T> = (diff: MapDiff | ListDiff | ValueDiff, before: T, after: T, local: boolean, changes: BinaryChange[]) => void\n\n  class Observable {\n    observe<T>(object: T, callback: ObserverCallback<T>): void\n  }\n\n  function merge<T>(localdoc: Doc<T>, remotedoc: Doc<T>): Doc<T>\n\n  function change<T>(doc: Doc<T>, options: ChangeOptions<T>, callback: ChangeFn<T>): Doc<T>\n  function change<T>(doc: Doc<T>, callback: ChangeFn<T>): Doc<T>\n  function emptyChange<D extends Doc<any>>(doc: D, options?: ChangeOptions<D>): D\n  function applyChanges<T>(doc: Doc<T>, changes: BinaryChange[]): [Doc<T>, Patch]\n  function equals<T>(val1: T, val2: T): boolean\n  function encodeChange(change: Change): BinaryChange\n  function decodeChange(binaryChange: BinaryChange): Change\n\n  function getActorId<T>(doc: Doc<T>): string\n  function getAllChanges<T>(doc: Doc<T>): BinaryChange[]\n  function getChanges<T>(olddoc: Doc<T>, newdoc: Doc<T>): BinaryChange[]\n  function getConflicts<T>(doc: Doc<T>, key: keyof T): any\n  function getHistory<T>(doc: Doc<T>): State<T>[]\n  function getLastLocalChange<T>(doc: Doc<T>): BinaryChange\n  function getObjectById<T>(doc: Doc<T>, objectId: OpId): any\n  function getObjectId(object: any): OpId\n\n  function load<T>(data: BinaryDocument, options?: InitOptions<T>): Doc<T>\n  function save<T>(doc: Doc<T>): BinaryDocument\n\n  function generateSyncMessage<T>(doc: Doc<T>, syncState: SyncState): [SyncState, BinarySyncMessage?]\n  function receiveSyncMessage<T>(doc: Doc<T>, syncState: SyncState, message: BinarySyncMessage): [Doc<T>, SyncState, Patch?]\n  function initSyncState(): SyncState\n\n  // custom CRDT types\n\n  class TableRow {\n    readonly id: UUID\n  }\n\n  class Table<T> {\n    constructor()\n    add(item: T): UUID\n    byId(id: UUID): T & TableRow\n    count: number\n    ids: UUID[]\n    remove(id: UUID): void\n    rows: (T & TableRow)[]\n  }\n\n  class List<T> extends Array<T> {\n    insertAt?(index: number, ...args: T[]): List<T>\n    deleteAt?(index: number, numDelete?: number): List<T>\n  }\n\n  class Text extends List<string> {\n    constructor(text?: string | string[])\n    get(index: number): string\n    toSpans<T>(): (string | T)[]\n  }\n\n  // Note that until https://github.com/Microsoft/TypeScript/issues/2361 is addressed, we\n  // can't treat a Counter like a literal number without force-casting it as a number.\n  // This won't compile:\n  //   `assert.strictEqual(c + 10, 13) // Operator '+' cannot be applied to types 'Counter' and '10'.ts(2365)`\n  // But this will:\n  //   `assert.strictEqual(c as unknown as number + 10, 13)`\n  class Counter extends Number {\n    constructor(value?: number)\n    increment(delta?: number): void\n    decrement(delta?: number): void\n    toString(): string\n    valueOf(): number\n    value: number\n  }\n\n  class Int { constructor(value: number) }\n  class Uint { constructor(value: number) }\n  class Float64 { constructor(value: number) }\n\n  // Readonly variants\n\n  type ReadonlyTable<T> = ReadonlyArray<T> & Table<T>\n  type ReadonlyList<T> = ReadonlyArray<T> & List<T>\n  type ReadonlyText = ReadonlyList<string> & Text\n\n  // Front & back\n\n  namespace Frontend {\n    function applyPatch<T>(doc: Doc<T>, patch: Patch, backendState?: BackendState): Doc<T>\n    function change<T>(doc: Doc<T>, message: string | undefined, callback: ChangeFn<T>): [Doc<T>, Change]\n    function change<T>(doc: Doc<T>, callback: ChangeFn<T>): [Doc<T>, Change]\n    function emptyChange<T>(doc: Doc<T>, message?: string): [Doc<T>, Change]\n    function from<T>(initialState: T | Doc<T>, options?: InitOptions<T>): [Doc<T>, Change]\n    function getActorId<T>(doc: Doc<T>): string\n    function getBackendState<T>(doc: Doc<T>): BackendState\n    function getConflicts<T>(doc: Doc<T>, key: keyof T): any\n    function getElementIds(list: any): string[]\n    function getLastLocalChange<T>(doc: Doc<T>): BinaryChange\n    function getObjectById<T>(doc: Doc<T>, objectId: OpId): Doc<T>\n    function getObjectId<T>(doc: Doc<T>): OpId\n    function init<T>(options?: InitOptions<T>): Doc<T>\n    function setActorId<T>(doc: Doc<T>, actorId: string): Doc<T>\n  }\n\n  namespace Backend {\n    function applyChanges(state: BackendState, changes: BinaryChange[]): [BackendState, Patch]\n    function applyLocalChange(state: BackendState, change: Change): [BackendState, Patch, BinaryChange]\n    function clone(state: BackendState): BackendState\n    function free(state: BackendState): void\n    function getAllChanges(state: BackendState): BinaryChange[]\n    function getChangeByHash(state: BackendState, hash: Hash): BinaryChange\n    function getChanges(state: BackendState, haveDeps: Hash[]): BinaryChange[]\n    function getChangesAdded(state1: BackendState, state2: BackendState): BinaryChange[]\n    function getHeads(state: BackendState): Hash[]\n    function getMissingDeps(state: BackendState, heads?: Hash[]): Hash[]\n    function getPatch(state: BackendState): Patch\n    function init(): BackendState\n    function load(data: BinaryDocument): BackendState\n    function loadChanges(state: BackendState, changes: BinaryChange[]): BackendState\n    function save(state: BackendState): BinaryDocument\n    function generateSyncMessage(state: BackendState, syncState: SyncState): [SyncState, BinarySyncMessage?]\n    function receiveSyncMessage(state: BackendState, syncState: SyncState, message: BinarySyncMessage): [BackendState, SyncState, Patch?]\n    function encodeSyncMessage(message: SyncMessage): BinarySyncMessage\n    function decodeSyncMessage(bytes: BinarySyncMessage): SyncMessage\n    function initSyncState(): SyncState\n    function encodeSyncState(syncState: SyncState): BinarySyncState\n    function decodeSyncState(bytes: BinarySyncState): SyncState\n  }\n\n  // Internals\n\n  type Hash = string // 64-digit hex string\n  type OpId = string // of the form `${counter}@${actorId}`\n\n  type UUID = string\n  type UUIDGenerator = () => UUID\n  interface UUIDFactory extends UUIDGenerator {\n    setFactory: (generator: UUIDGenerator) => void\n    reset: () => void\n  }\n  const uuid: UUIDFactory\n\n  interface Clock {\n    [actorId: string]: number\n  }\n\n  interface State<T> {\n    change: Change\n    snapshot: T\n  }\n\n  interface BackendState {\n    // no public methods or properties\n  }\n\n  type BinaryChange = Uint8Array & { __binaryChange: true }\n  type BinaryDocument = Uint8Array & { __binaryDocument: true }\n  type BinarySyncState = Uint8Array & { __binarySyncState: true }\n  type BinarySyncMessage = Uint8Array & { __binarySyncMessage: true }\n\n  interface SyncState {\n    // no public methods or properties\n  }\n\n  interface SyncMessage {\n    heads: Hash[]\n    need: Hash[]\n    have: SyncHave[]\n    changes: BinaryChange[]\n  }\n\n  interface SyncHave {\n    lastSync: Hash[]\n    bloom: Uint8Array\n  }\n\n  interface Change {\n    message: string\n    actor: string\n    time: number\n    seq: number\n    startOp: number\n    hash?: Hash\n    deps: Hash[]\n    ops: Op[]\n  }\n\n  interface Op {\n    action: OpAction\n    obj: OpId\n    key: string | number\n    insert: boolean\n    elemId?: OpId\n    child?: OpId\n    value?: number | boolean | string | null\n    datatype?: DataType\n    pred?: OpId[]\n    values?: (number | boolean | string | null)[]\n    multiOp?: number\n  }\n\n  interface Patch {\n    actor?: string\n    seq?: number\n    pendingChanges: number\n    clock: Clock\n    deps: Hash[]\n    diffs: MapDiff\n    maxOp: number\n  }\n\n  // Describes changes to a map (in which case propName represents a key in the\n  // map) or a table object (in which case propName is the primary key of a row).\n  interface MapDiff {\n    objectId: OpId        // ID of object being updated\n    type: 'map' | 'table' // type of object being updated\n    // For each key/property that is changing, props contains one entry\n    // (properties that are not changing are not listed). The nested object is\n    // empty if the property is being deleted, contains one opId if it is set to\n    // a single value, and contains multiple opIds if there is a conflict.\n    props: {[propName: string]: {[opId: string]: MapDiff | ListDiff | ValueDiff }}\n  }\n\n  // Describes changes to a list or Automerge.Text object, in which each element\n  // is identified by its index.\n  interface ListDiff {\n    objectId: OpId        // ID of object being updated\n    type: 'list' | 'text' // type of objct being updated\n    // This array contains edits in the order they should be applied.\n    edits: (SingleInsertEdit | MultiInsertEdit | UpdateEdit | RemoveEdit)[]\n  }\n\n  // Describes the insertion of a single element into a list or text object.\n  // The element can be a nested object.\n  interface SingleInsertEdit {\n    action: 'insert'\n    index: number   // the list index at which to insert the new element\n    elemId: OpId    // the unique element ID of the new list element\n    opId: OpId      // ID of the operation that assigned this value\n    value: MapDiff | ListDiff | ValueDiff\n  }\n\n  // Describes the insertion of a consecutive sequence of primitive values into\n  // a list or text object. In the case of text, the values are strings (each\n  // character as a separate string value). Each inserted value is given a\n  // consecutive element ID: starting with `elemId` for the first value, the\n  // subsequent values are given elemIds with the same actor ID and incrementing\n  // counters. To insert non-primitive values, use SingleInsertEdit.\n  interface MultiInsertEdit {\n    action: 'multi-insert'\n    index: number   // the list index at which to insert the first value\n    elemId: OpId    // the unique ID of the first inserted element\n    values: number[] | boolean[] | string[] | null[] // list of values to insert\n    datatype?: DataType // all values must be of the same datatype\n  }\n\n  // Describes the update of the value or nested object at a particular index\n  // of a list or text object. In the case where there are multiple conflicted\n  // values at the same list index, multiple UpdateEdits with the same index\n  // (but different opIds) appear in the edits array of ListDiff.\n  interface UpdateEdit {\n    action: 'update'\n    index: number   // the list index to update\n    opId: OpId      // ID of the operation that assigned this value\n    value: MapDiff | ListDiff | ValueDiff\n  }\n\n  // Describes the deletion of one or more consecutive elements from a list or\n  // text object.\n  interface RemoveEdit {\n    action: 'remove'\n    index: number   // index of the first list element to remove\n    count: number   // number of list elements to remove\n  }\n\n  // Describes a primitive value, optionally tagged with a datatype that\n  // indicates how the value should be interpreted.\n  interface ValueDiff {\n    type: 'value'\n    value: number | boolean | string | null\n    datatype?: DataType\n  }\n\n  type OpAction =\n    | 'del'\n    | 'inc'\n    | 'set'\n    | 'link'\n    | 'makeText'\n    | 'makeTable'\n    | 'makeList'\n    | 'makeMap'\n\n  type CollectionType =\n    | 'list' //..\n    | 'map'\n    | 'table'\n    | 'text'\n\n  type DataType =\n    | 'int'\n    | 'uint'\n    | 'float64'\n    | 'counter'\n    | 'timestamp'\n\n  // TYPE UTILITY FUNCTIONS\n\n  // Type utility function: Freeze\n  // Generates a readonly version of a given object, array, or map type applied recursively to the nested members of the root type.\n  // It's like TypeScript's `readonly`, but goes all the way down a tree.\n\n  // prettier-ignore\n  type Freeze<T> =\n    T extends Function ? T\n    : T extends Text ? ReadonlyText\n    : T extends Table<infer T> ? FreezeTable<T>\n    : T extends List<infer T> ? FreezeList<T>\n    : T extends Array<infer T> ? FreezeArray<T>\n    : T extends Map<infer K, infer V> ? FreezeMap<K, V>\n    : T extends string & infer O ? string & O\n    : FreezeObject<T>\n\n  interface FreezeTable<T> extends ReadonlyTable<Freeze<T>> {}\n  interface FreezeList<T> extends ReadonlyList<Freeze<T>> {}\n  interface FreezeArray<T> extends ReadonlyArray<Freeze<T>> {}\n  interface FreezeMap<K, V> extends ReadonlyMap<Freeze<K>, Freeze<V>> {}\n  type FreezeObject<T> = { readonly [P in keyof T]: Freeze<T[P]> }\n}\n"
  },
  {
    "path": "CHANGELOG.md",
    "content": "# Changelog\n\nAutomerge adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html) for assigning\nversion numbers. However, any feature that is not documented or labelled as \"experimental\" may\nchange without warning in a minor release.\n\nAll notable changes to Automerge will be documented in this file, which\nis based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/).\n\n## Performance branch (working towards Automerge 1.0 release candidate)\n\n- **Changed**: The data format for storing/transmitting changes (as returned by\n  `Automerge.getChanges()`) and documents (as returned by `Automerge.save()`) have changed.\n  They now use a binary encoding that is much more compact than the old JSON format. This is a\n  breaking change, but we will provide an upgrade tool to migrate any existing Automerge docs\n  to the new format.\n- **Changed**: `Automerge.applyChanges()` now returns a two-element array, where the first\n  element is the updated document, and the second element is a patch describing the changes that\n  have been made (including any conflicts that have arisen). This simplifies applications that\n  need to update some other state, such as a user interface, to reflect changes in a document.\n- **Changed** [#339]: `Automerge.Connection`, `Automerge.DocSet`, and `Automerge.WatchableDoc`\n  have been removed, and replaced with a new Automerge sync protocol that is implemented by the\n  functions `Automerge.generateSyncMessage()` and `Automerge.receiveSyncMessage()`.\n  ([@ept], [@pvh], [@orionz], [@alexjg], [@jeffa5])\n- **Changed**: The frontend/backend protocol (change requests generated by `Frontend.change()`\n  and patches returned by `Backend.applyChanges()`) has changed. The new format will be\n  documented separately.\n- **Removed**: The undo/redo feature is removed for now. The original implementation was a bit of\n  a hack, and we decided not to support the hack for 1.0. Instead, we will bring back a\n  better-designed version of this feature in the future.\n- **Changed**: Actor IDs are now required to consist only of lowercase hexadecimal digits\n  (UUIDs can still be used, but the hyphens need to be removed).\n- **Changed**: `Automerge.getConflicts()` now returns *all* conflicting values, including the\n  value chosen as default resolution.\n- **Changed**: Multiple references to the same object in an Automerge document are no longer\n  allowed. In other words, the document is now required to be a tree, not a DAG.\n- **Changed**: We no longer assume that the backend state is immutable, giving us greater freedom\n  to implement the backend in a way that maximises performance. (Frontend state and Automerge\n  documents remain immutable as before.) This restricts certain usage patterns: for example, if\n  you update a document, you cannot then take a reference to the document state before that\n  update and update or query it. If you want to be able to continue referencing an old document\n  state, you can copy it using `Automerge.clone()`.\n- **Removed**: `Automerge.diff` and `Backend.merge`, because they depended on the assumption of\n  an immutable backend. We hope to bring back a better implementation of `Automerge.diff` in the\n  future.\n- **Changed**: Dependencies between changes are now expressed by referencing the hashes of\n  dependencies, rather than their actorId and sequence number. APIs have changed accordingly:\n  `Backend.getMissingDeps` now returns a list of hashes rather than a vector clock; the second\n  argument of `Backend.getChanges()` is now also a list of hashes. This change also affects\n  network sync protocols, e.g. based on `Automerge.Connection`, which need to now exchange\n  hashes rather than vector clocks.\n- **Removed**: `Automerge.getMissingDeps()`; use `Backend.getMissingDeps()` instead.\n- **Removed**: `Backend.getMissingChanges()`; use `Backend.getChanges()` instead.\n- **Removed**: `Backend.getChangesForActor()` since it does not fit with a hash chaining approach.\n- **Added**: `Frontend.getLastLocalChange()` returns the binary encoding of the last change made\n  by the local actor, and `Backend.getHeads()` returns the latest change hashes in the current\n  document state.\n- **Changed**: `Backend.applyLocalChange()` now returns an array of three values: the updated\n  backend state, the patch to apply to the frontend, and the binary encoding of the change.\n- **Added** [#308]: Experimental `Automerge.Observable` API allows an application to receive\na callback whenever a document (or some object within a document) changes ([@ept])\n\n## [Unreleased]\n\n## [0.14.2] — 2021-01-12\n\n- **Fixed** [#301]: Handling out-of-bounds argument in `Array.slice()` ([@pierreprinetti])\n- **Fixed** [#261]: Support calling `Array.indexOf()` with an object ([@philschatz])\n\n## [0.14.1] — 2020-05-25\n\n- **Fixed** [#249]: Corrected TypeScript declaration for `Automerge.Table.rows` ([@lauritzsh])\n- **Fixed** [#252]: Corrected TypeScript declaration for `WatchableDoc` ([@vincentcapicotto])\n- **Fixed** [#258]: Changes whose dependencies are missing are now preserved when saving and\n  reloading a document ([@KarenSarmiento], [@ept])\n- **Changed** [#260]: If you try to assign an object that is already in an Automerge document,\n  you now get a more descriptive error message ([@johannesjo], [@ept])\n\n## [0.14.0] — 2020-03-25\n\n- **Removed** [#236]: Undocumented `Automerge.Table` API that allowed rows to be added by\n  providing an array of values. Now rows must be given as an object ([@HerbCaudill])\n- **Removed** [#241]: Constructor of `Automerge.Table` no longer takes an array of columns, and\n  the `columns` property of `Automerge.Table` is also removed ([@ept])\n- **Changed** [#242]: Rows of `Automerge.Table` now automatically get an `id` property containing\n  the primary key of that row ([@ept])\n- **Removed** [#243]: `Automerge.Table` objects no longer have a `set()` method. Use `add()` or\n  `remove()` instead ([@ept])\n- **Removed** support for Node 8, which is no longer being maintained\n- **Added** [#194], [#238]: `Automerge.Text` objects may now contain objects as well as strings;\n  new method `Text.toSpans()` that concatenates characters while leaving objects unchanged\n  ([@pvh], [@ept], [@nornagon])\n\n## [0.13.0] — 2020-02-24\n\n- **Added** [#232]: New API `Automerge.getAllChanges()` returns all changes ([@ept])\n- **Fixed** [#230]: `Text.deleteAt` allows zero characters to be deleted ([@skokenes])\n- **Fixed** [#219]: `canUndo` is false immediately after `Automerge.from` ([@ept])\n- **Fixed** [#215]: Adjust TypeScript definition of `Freeze<T>` ([@jeffpeterson])\n\n## [0.12.1] — 2019-08-22\n\n- **Fixed** [#184]: Corrected TypeScript type definition for `Automerge.DocSet` ([@HerbCaudill])\n- **Fixed** [#174]: If `.filter()`, `.find()` or similar methods are used inside a change callback,\n  the objects they return can now be mutated ([@ept], [@airhorns])\n- **Fixed** [#199]: `Automerge.Text.toString()` now returns the unadulterated text ([@Gozala])\n- **Added** [#210]: New method `DocSet.removeDoc()` ([@brentkeller])\n\n## [0.12.0] — 2019-08-07\n\n- **Changed** [#183]: `Frontend.from()` now accepts initialization options ([@HerbCaudill], [@ept])\n- **Changed** [#180]: Mutation methods on `Automerge.Text` are now available without having to\n  assign the object to a document ([@ept])\n- **Added** [#181]: Can now specify an initial value when creating `Automerge.Text` objects\n  ([@Gozala], [@ept])\n- **Fixed** [#202]: Stack overflow error when making large changes ([@HerbCaudill], [@ept])\n\n## [0.11.0] — 2019-07-13\n\n- **Added** [#127]: New `Automerge.from` function creates a new document and initializes it\n  with an initial state given as an argument ([@HerbCaudill], [@ept])\n- **Added** [#155]: Type definitions now allow TypeScript applications to use Automerge with\n  static type-checking ([@HerbCaudill], [@airhorns], [@aslakhellesoy], [@ept])\n- **Changed** [#177]: Automerge documents are no longer made immutable with `Object.freeze`\n  by default, due to the performance cost. Use the `{freeze: true}` option to continue\n  using immutable objects. ([@izuchukwu], [@ept])\n- **Fixed** [#165]: Undo/redo now work when using separate frontend and backend ([@ept])\n\n## [0.10.1] — 2019-05-17\n\n- **Fixed** [#151]: Exception \"Duplicate list element ID\" after a list element was added and\n  removed again in the same change callback ([@ept], [@minhhien1996])\n- **Changed** [#163]: Calling `JSON.stringify` on an Automerge document containing\n  `Automerge.Text`, `Automerge.Table` or `Automerge.Counter` now serializes those objects in a\n  clean way, rather than dumping the object's internal properties ([@ept])\n\n## [0.10.0] — 2019-02-04\n\n- **Added** [#29]: New `Automerge.Table` datatype provides an unordered collection of records,\n  like a relational database ([@ept])\n- **Added** [#139]: JavaScript Date objects are now supported in Automerge documents ([@ept])\n- **Added** [#147]: New `Automerge.Counter` datatype provides a CRDT counter ([@ept])\n- **Removed** [#148]: `Automerge.inspect` has been removed ([@ept])\n- **Fixed** [#145]: Exception \"Duplicate list element ID\" after reloading document from disk\n  ([@ept])\n- **Changed** [#150]: Underscore-prefixed property names are now allowed in map objects;\n  `doc.object._objectId` is now `Automerge.getObjectId(doc.object)`,\n  `doc.object._conflicts.property` is now `Automerge.getConflicts(doc.object, 'property')`,\n  and `doc._actorId` is now `Automerge.getActorId(doc)`. ([@ept])\n\n## [0.9.2] — 2018-11-05\n\n- **Fixed** [#128]: Fixed crash when Text object was modified in the same change as\n  another object ([@CGNonofr])\n- **Fixed** [#129]: Prevent application of duplicate requests in `applyLocalChange()` ([@ept])\n- **Changed** [#130]: Frontend API no longer uses `Frontend.getRequests()`; instead, frontend\n  change functions now return request objects directly ([@ept])\n\n## [0.9.1] — 2018-09-27\n\n- **Changed** [#126]: Backend no longer needs to know the actorId of the local node ([@ept])\n- **Changed** [#126]: Frontend can now be initialized without actorId, as long as you call\n  `setActorId` before you make the first change ([@ept])\n- **Changed** [#120]: Undo and redo must now be initiated by the frontend, not the backend ([@ept])\n- **Fixed** [#120]: Fixed bug that would cause sequence numbers to be reused in some concurrent\n  executions ([@ept])\n- **Fixed** [#125]: Exceptions now throw Error objects rather than plain strings ([@wincent])\n\n## [0.9.0] — 2018-09-18\n\n- **Added** [#112]: Added `Automerge.undo()` and `Automerge.redo()` ([@ept])\n- **Added** [#118]: Introduced new Frontend and Backend APIs, and refactored existing APIs to use\n  them; this allows some of the work to be moved to a background thread, and provides better\n  modularisation ([@ept])\n- **Removed** Removed the experimental Immutable.js-compatible API (`Automerge.initImmutable()`),\n  a casualty of the refactoring in [#118] ([@ept])\n\n## [0.8.0] — 2018-08-02\n\n- **Added** [#106]: New `doc._get(UUID)` method allows looking up an object by its `_objectId`\n  inside an `Automerge.change()` callback ([@mattkrick])\n- **Added** [#109]: Export `OpSet.getMissingChanges` on the Automerge object ([@mattkrick])\n- **Added** [#111]: New `Automerge.emptyChange()` allows a \"change\" record to be created without\n  actually changing the document ([@ept])\n- **Changed** [#110]: Require that the change message in `Automerge.change()` must be a string\n  ([@ept])\n- **Changed** [#111]: If `Automerge.change()` does not modify the document, the function now\n  returns the original document object untouched ([@ept])\n\n## [0.7.11] — 2018-06-26\n\n- **Fixed** [#97]: `delete` operator no longer throws an exception if the property doesn't exist\n  ([@salzhrani], [@EthanRBrown])\n- **Fixed** [#104]: Fix an error when loading the webpack-packaged version of Automerge in Node.js\n  ([@ept])\n\n## [0.7.10] — 2018-06-12\n\n- **Added** [#93]: Allow the UUID implementation to be replaced for testing purposes ([@kpruden])\n- **Added** [#74]: Automerge.diff() now includes the path from the root to the modified object\n  ([@ept])\n\n## [0.7.9] — 2018-05-25\n\n- **Fixed** [#90]: Compatibility with Node 10 ([@aslakhellesoy])\n\n## [0.7.8] — 2018-05-15\n\n- **Fixed** [#91]: Improve performance of changes that modify many list or map elements ([@ept])\n\n## [0.7.7] — 2018-04-24\n\n- **Changed** [#87]: Remove babel-polyfill from transpiled library ([@EthanRBrown])\n\n## 0.7.4, 0.7.5, [0.7.6] — 2018-04-19\n\n- Version bump to fix a build tooling issue\n\n## [0.7.3] — 2018-04-19\n\n- **Changed** [#85]: Publish Babel-transpiled code to npm to improve compatibility ([@EthanRBrown])\n\n## [0.7.2] — 2018-04-17\n\n- **Changed** [#83]: Changed `_objectId` property on Automerge map objects to be non-enumerable\n  ([@EthanRBrown], [@ept])\n- **Changed** [#84]: Changed `_conflicts`, `_state`, and `_actorId` to be non-enumerable\n  properties ([@ept])\n- **Fixed** [#77]: Fixed exception when a list element is inserted and updated in the same change\n  callback ([@mmcgrana], [@ept])\n- **Fixed** [#78]: Better error message when trying to use an unsupported datatype ([@ept])\n\n## [0.7.1] — 2018-02-26\n\n- **Fixed** [#69]: `Automerge.load` generates random actorId if none specified ([@saranrapjs])\n- **Fixed** [#64]: `Automerge.applyChanges()` allows changes to be applied out-of-order\n  ([@jimpick], [@ept])\n\n## [0.7.0] — 2018-01-15\n\n- **Added** [#62]: Initial support for Immutable.js API compatibility (read-only for now)\n  ([@ept], [@jeffpeterson])\n- **Added** [#45]: Added experimental APIs `Automerge.getMissingDeps`,\n  `Automerge.getChangesForActor`, and `Automerge.WatchableDoc` to support integration with dat\n  hypercore ([@pvh], [@ept])\n- **Added** [#46]: Automerge list objects now also have a `_conflicts` property that records\n  concurrent assignments to the same list index, just like map objects have had all along ([@ept])\n- **Changed** [#60]: `splice` in an `Automerge.change()` callback returns an array of deleted\n  elements (to match behaviour of\n  [`Array#splice`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/splice)).\n  ([@aslakhellesoy])\n- **Fixed** [#57]: Tests now work on operating systems with case-sensitive filesystems ([@mmmm1998])\n\n## [0.6.0] — 2017-12-13\n\n- **Added** [#44]: New APIs `Automerge.getChanges` and `Automerge.applyChanges` to provide more\n  flexibility for network protocol layer ([@ept])\n- **Added** [#41]: New `Automerge.Text` datatype, which is more efficient than a list for\n  character-by-character editing of text ([@ept])\n- **Added** [#40]: Lists are now backed by a new indexed skip list data structure, which is\n  faster ([@ept])\n- **Changed** [#38]: To save memory, `Automerge.getHistory` now builds snapshots of past states\n  only when requested, rather than remembering them by default ([@ept])\n\n## [0.5.0] — 2017-09-19\n\n- **Added** [#37]: Added `Automerge.diff` to find the differences between to Automerge documents\n  ([@ept])\n- **Added** [#37]: Added support for incremental cache maintenance, bringing a 20x speedup for a\n  1,000-element list ([@ept])\n- **Added** [#36]: Added `Automerge.Connection` and `Automerge.DocSet` classes to support\n  peer-to-peer network protocols ([@ept], [@pvh])\n- **Changed**: Renamed `Automerge.changeset` to `Automerge.change` ([@ept])\n\n## [0.4.3] — 2017-08-16\n\n- **Fixed** [#34]: Fixed a bug that caused list elements to sometimes disappear\n  ([@aslakhellesoy], [@ept])\n- **Fixed** [#32]: Fixed a test failure in recent Node.js versions ([@aslakhellesoy])\n\n## [0.4.2] — 2017-06-29\n\n- **Added**: Set up Karma to run tests in web browsers ([@ept])\n- **Added**: Set up Webpack to produce bundled JavaScript file for web browsers ([@ept])\n\n## [0.4.1] — 2017-06-26\n\n- **Changed**: `Automerge.getHistory` API now uses the object cache, which should be faster ([@ept])\n\n## [0.4.0] — 2017-06-23\n\n- **Changed**: Automerge documents are now just regular JavaScript objects, and Proxy is used only\n  within `Automerge.changeset` callbacks. Previously everything used Proxy. ([@ept])\n- **Changed**: [#30]: Made `_objectId` an enumerable property, so that it is visible by default\n  ([@ept])\n- **Changed**: Support all standard JavaScript array methods and iterators on list proxy object\n  ([@ept])\n\n## [0.3.0] — 2017-06-13\n\n- First public release.\n\n\n[Unreleased]: https://github.com/automerge/automerge/compare/v0.14.2...HEAD\n[0.14.2]: https://github.com/automerge/automerge/compare/v0.14.1...v0.14.2\n[0.14.1]: https://github.com/automerge/automerge/compare/v0.14.0...v0.14.1\n[0.14.0]: https://github.com/automerge/automerge/compare/v0.13.1...v0.14.0\n[0.13.0]: https://github.com/automerge/automerge/compare/v0.12.1...v0.13.0\n[0.12.1]: https://github.com/automerge/automerge/compare/v0.12.0...v0.12.1\n[0.12.0]: https://github.com/automerge/automerge/compare/v0.11.0...v0.12.0\n[0.11.0]: https://github.com/automerge/automerge/compare/v0.10.1...v0.11.0\n[0.10.1]: https://github.com/automerge/automerge/compare/v0.10.0...v0.10.1\n[0.10.0]: https://github.com/automerge/automerge/compare/v0.9.2...v0.10.0\n[0.9.2]: https://github.com/automerge/automerge/compare/v0.9.1...v0.9.2\n[0.9.1]: https://github.com/automerge/automerge/compare/v0.9.0...v0.9.1\n[0.9.0]: https://github.com/automerge/automerge/compare/v0.8.0...v0.9.0\n[0.8.0]: https://github.com/automerge/automerge/compare/v0.7.11...v0.8.0\n[0.7.11]: https://github.com/automerge/automerge/compare/v0.7.10...v0.7.11\n[0.7.10]: https://github.com/automerge/automerge/compare/v0.7.9...v0.7.10\n[0.7.9]: https://github.com/automerge/automerge/compare/v0.7.8...v0.7.9\n[0.7.8]: https://github.com/automerge/automerge/compare/v0.7.7...v0.7.8\n[0.7.7]: https://github.com/automerge/automerge/compare/v0.7.6...v0.7.7\n[0.7.6]: https://github.com/automerge/automerge/compare/v0.7.3...v0.7.6\n[0.7.3]: https://github.com/automerge/automerge/compare/v0.7.2...v0.7.3\n[0.7.2]: https://github.com/automerge/automerge/compare/v0.7.1...v0.7.2\n[0.7.1]: https://github.com/automerge/automerge/compare/v0.7.0...v0.7.1\n[0.7.0]: https://github.com/automerge/automerge/compare/v0.6.0...v0.7.0\n[0.6.0]: https://github.com/automerge/automerge/compare/v0.5.0...v0.6.0\n[0.5.0]: https://github.com/automerge/automerge/compare/v0.4.3...v0.5.0\n[0.4.3]: https://github.com/automerge/automerge/compare/v0.4.2...v0.4.3\n[0.4.2]: https://github.com/automerge/automerge/compare/v0.4.1...v0.4.2\n[0.4.1]: https://github.com/automerge/automerge/compare/v0.4.0...v0.4.2\n[0.4.0]: https://github.com/automerge/automerge/compare/v0.3.0...v0.4.0\n[0.3.0]: https://github.com/automerge/automerge/compare/v0.2.0...v0.3.0\n\n[#339]: https://github.com/automerge/automerge/pull/339\n[#308]: https://github.com/automerge/automerge/pull/308\n[#301]: https://github.com/automerge/automerge/pull/301\n[#261]: https://github.com/automerge/automerge/pull/261\n[#260]: https://github.com/automerge/automerge/issues/260\n[#258]: https://github.com/automerge/automerge/issues/258\n[#252]: https://github.com/automerge/automerge/pull/252\n[#249]: https://github.com/automerge/automerge/pull/249\n[#243]: https://github.com/automerge/automerge/pull/243\n[#242]: https://github.com/automerge/automerge/pull/242\n[#241]: https://github.com/automerge/automerge/pull/241\n[#238]: https://github.com/automerge/automerge/pull/238\n[#236]: https://github.com/automerge/automerge/pull/236\n[#232]: https://github.com/automerge/automerge/pull/232\n[#230]: https://github.com/automerge/automerge/issues/230\n[#219]: https://github.com/automerge/automerge/issues/219\n[#210]: https://github.com/automerge/automerge/pull/210\n[#202]: https://github.com/automerge/automerge/issues/202\n[#199]: https://github.com/automerge/automerge/pull/199\n[#194]: https://github.com/automerge/automerge/issues/194\n[#184]: https://github.com/automerge/automerge/pull/184\n[#183]: https://github.com/automerge/automerge/pull/183\n[#181]: https://github.com/automerge/automerge/pull/181\n[#180]: https://github.com/automerge/automerge/issues/180\n[#177]: https://github.com/automerge/automerge/issues/177\n[#174]: https://github.com/automerge/automerge/issues/174\n[#165]: https://github.com/automerge/automerge/pull/165\n[#163]: https://github.com/automerge/automerge/pull/163\n[#155]: https://github.com/automerge/automerge/pull/155\n[#151]: https://github.com/automerge/automerge/issues/151\n[#150]: https://github.com/automerge/automerge/pull/150\n[#148]: https://github.com/automerge/automerge/pull/148\n[#147]: https://github.com/automerge/automerge/pull/147\n[#145]: https://github.com/automerge/automerge/issues/145\n[#139]: https://github.com/automerge/automerge/pull/139\n[#130]: https://github.com/automerge/automerge/pull/130\n[#129]: https://github.com/automerge/automerge/pull/129\n[#128]: https://github.com/automerge/automerge/pull/128\n[#127]: https://github.com/automerge/automerge/issues/127\n[#126]: https://github.com/automerge/automerge/pull/126\n[#125]: https://github.com/automerge/automerge/pull/125\n[#120]: https://github.com/automerge/automerge/pull/120\n[#118]: https://github.com/automerge/automerge/pull/118\n[#112]: https://github.com/automerge/automerge/pull/112\n[#111]: https://github.com/automerge/automerge/pull/111\n[#110]: https://github.com/automerge/automerge/pull/110\n[#109]: https://github.com/automerge/automerge/pull/109\n[#106]: https://github.com/automerge/automerge/issues/106\n[#104]: https://github.com/automerge/automerge/issues/104\n[#97]: https://github.com/automerge/automerge/issues/97\n[#93]: https://github.com/automerge/automerge/pull/93\n[#91]: https://github.com/automerge/automerge/pull/91\n[#90]: https://github.com/automerge/automerge/pull/90\n[#87]: https://github.com/automerge/automerge/pull/87\n[#85]: https://github.com/automerge/automerge/pull/85\n[#84]: https://github.com/automerge/automerge/pull/84\n[#83]: https://github.com/automerge/automerge/pull/83\n[#78]: https://github.com/automerge/automerge/issues/78\n[#77]: https://github.com/automerge/automerge/pull/77\n[#74]: https://github.com/automerge/automerge/pull/74\n[#69]: https://github.com/automerge/automerge/pull/69\n[#64]: https://github.com/automerge/automerge/pull/64\n[#62]: https://github.com/automerge/automerge/pull/62\n[#60]: https://github.com/automerge/automerge/pull/60\n[#57]: https://github.com/automerge/automerge/pull/57\n[#46]: https://github.com/automerge/automerge/issues/46\n[#45]: https://github.com/automerge/automerge/pull/45\n[#44]: https://github.com/automerge/automerge/pull/44\n[#41]: https://github.com/automerge/automerge/pull/41\n[#40]: https://github.com/automerge/automerge/pull/40\n[#38]: https://github.com/automerge/automerge/issues/38\n[#37]: https://github.com/automerge/automerge/pull/37\n[#36]: https://github.com/automerge/automerge/pull/36\n[#34]: https://github.com/automerge/automerge/pull/34\n[#32]: https://github.com/automerge/automerge/pull/32\n[#30]: https://github.com/automerge/automerge/pull/30\n[#29]: https://github.com/automerge/automerge/issues/29\n\n[@airhorns]: https://github.com/airhorns\n[@alexjg]: https://github.com/alexjg\n[@aslakhellesoy]: https://github.com/aslakhellesoy\n[@brentkeller]: https://github.com/brentkeller\n[@CGNonofr]: https://github.com/CGNonofr\n[@EthanRBrown]: https://github.com/EthanRBrown\n[@Gozala]: https://github.com/Gozala\n[@HerbCaudill]: https://github.com/HerbCaudill\n[@izuchukwu]: https://github.com/izuchukwu\n[@jeffa5]: https://github.com/jeffa5\n[@jeffpeterson]: https://github.com/jeffpeterson\n[@jimpick]: https://github.com/jimpick\n[@johannesjo]: https://github.com/johannesjo\n[@ept]: https://github.com/ept\n[@KarenSarmiento]: https://github.com/KarenSarmiento\n[@kpruden]: https://github.com/kpruden\n[@lauritzsh]: https://github.com/lauritzsh\n[@mattkrick]: https://github.com/mattkrick\n[@minhhien1996]: https://github.com/minhhien1996\n[@mmcgrana]: https://github.com/mmcgrana\n[@mmmm1998]: https://github.com/mmmm1998\n[@nornagon]: https://github.com/nornagon\n[@orionz]: https://github.com/orionz\n[@pierreprinetti]: https://github.com/pierreprinetti\n[@philschatz]: https://github.com/philschatz\n[@pvh]: https://github.com/pvh\n[@salzhrani]: https://github.com/salzhrani\n[@saranrapjs]: https://github.com/saranrapjs\n[@skokenes]: https://github.com/skokenes\n[@vincentcapicotto]: https://github.com/vincentcapicotto\n[@wincent]: https://github.com/wincent\n"
  },
  {
    "path": "LICENSE",
    "content": "Copyright (c) 2017-present Martin Kleppmann, Ink & Switch LLC, and the Automerge contributors\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "<img src='./img/sign.svg' width='500' alt='Automerge logo' />\n\n## Deprecation Notice\n\nAutomerge now has a shiny new implementation at https://github.com/automerge/automerge. This repository is the original pure javascript implementation. All development effort has shifted to the new implementation which is written in Rust and so can easily be ported to other platforms. \n\n## Original Readme\n\n💬 [Join the Automerge Slack community](https://join.slack.com/t/automerge/shared_invite/zt-e4p3760n-kKh7r3KRH1YwwNfiZM8ktw)\n\n[![Build Status](https://github.com/automerge/automerge/actions/workflows/automerge-ci.yml/badge.svg)](https://github.com/automerge/automerge/actions/workflows/automerge-ci.yml)\n[![Browser Test Status](https://app.saucelabs.com/buildstatus/automerge)](https://app.saucelabs.com/open_sauce/user/automerge/builds)\n\nAutomerge is a library of data structures for building collaborative applications in JavaScript.\n\nPlease see [automerge.org](http://automerge.org/) for documentation.\n\nFor a set of extensible examples in TypeScript, see [automerge-repo](https://github.com/automerge/automerge-repo)\n\n## Setup\n\nIf you're using npm, `npm install automerge`. If you're using yarn, `yarn add automerge`. Then you\ncan import it with `require('automerge')` as in [the example below](#usage) (or\n`import * as Automerge from 'automerge'` if using ES2015 or TypeScript).\n\nOtherwise, clone this repository, and then you can use the following commands:\n\n- `yarn install` — installs dependencies.\n- `yarn test` — runs the test suite in Node.\n- `yarn run browsertest` — runs the test suite in web browsers.\n- `yarn build` — creates a bundled JS file `dist/automerge.js` for web browsers. It includes the\n  dependencies and is set up so that you can load through a script tag.\n\n## Meta\n\nCopyright 2017–2021, the Automerge contributors. Released under the terms of the\nMIT license (see `LICENSE`).\n"
  },
  {
    "path": "backend/backend.js",
    "content": "const { encodeChange } = require('./columnar')\nconst { BackendDoc } = require('./new')\nconst { backendState } = require('./util')\n\n/**\n * Returns an empty node state.\n */\nfunction init() {\n  return {state: new BackendDoc(), heads: []}\n}\n\nfunction clone(backend) {\n  return {state: backendState(backend).clone(), heads: backend.heads}\n}\n\nfunction free(backend) {\n  backend.state = null\n  backend.frozen = true\n}\n\n/**\n * Applies a list of `changes` from remote nodes to the node state `backend`.\n * Returns a two-element array `[state, patch]` where `state` is the updated\n * node state, and `patch` describes the modifications that need to be made\n * to the document objects to reflect these changes.\n */\nfunction applyChanges(backend, changes) {\n  const state = backendState(backend)\n  const patch = state.applyChanges(changes)\n  backend.frozen = true\n  return [{state, heads: state.heads}, patch]\n}\n\nfunction hashByActor(state, actorId, index) {\n  if (state.hashesByActor[actorId] && state.hashesByActor[actorId][index]) {\n    return state.hashesByActor[actorId][index]\n  }\n  if (!state.haveHashGraph) {\n    state.computeHashGraph()\n    if (state.hashesByActor[actorId] && state.hashesByActor[actorId][index]) {\n      return state.hashesByActor[actorId][index]\n    }\n  }\n  throw new RangeError(`Unknown change: actorId = ${actorId}, seq = ${index + 1}`)\n}\n\n/**\n * Takes a single change request `request` made by the local user, and applies\n * it to the node state `backend`. Returns a three-element array `[backend, patch, binaryChange]`\n * where `backend` is the updated node state,`patch` confirms the\n * modifications to the document objects, and `binaryChange` is a binary-encoded form of\n * the change submitted.\n */\nfunction applyLocalChange(backend, change) {\n  const state = backendState(backend)\n  if (change.seq <= state.clock[change.actor] || 0) {\n    throw new RangeError('Change request has already been applied')\n  }\n\n  // Add the local actor's last change hash to deps. We do this because when frontend\n  // and backend are on separate threads, the frontend may fire off several local\n  // changes in sequence before getting a response from the backend; since the binary\n  // encoding and hashing is done by the backend, the frontend does not know the hash\n  // of its own last change in this case. Rather than handle this situation as a\n  // special case, we say that the frontend includes only specifies other actors'\n  // deps in changes it generates, and the dependency from the local actor's last\n  // change is always added here in the backend.\n  //\n  // Strictly speaking, we should check whether the local actor's last change is\n  // indirectly reachable through a different actor's change; in that case, it is not\n  // necessary to add this dependency. However, it doesn't do any harm either (only\n  // using a few extra bytes of storage).\n  if (change.seq > 1) {\n    const lastHash = hashByActor(state, change.actor, change.seq - 2)\n    if (!lastHash) {\n      throw new RangeError(`Cannot find hash of localChange before seq=${change.seq}`)\n    }\n    let deps = {[lastHash]: true}\n    for (let hash of change.deps) deps[hash] = true\n    change.deps = Object.keys(deps).sort()\n  }\n\n  const binaryChange = encodeChange(change)\n  const patch = state.applyChanges([binaryChange], true)\n  backend.frozen = true\n\n  // On the patch we send out, omit the last local change hash\n  const lastHash = hashByActor(state, change.actor, change.seq - 1)\n  patch.deps = patch.deps.filter(head => head !== lastHash)\n  return [{state, heads: state.heads}, patch, binaryChange]\n}\n\n/**\n * Returns the state of the document serialised to an Uint8Array.\n */\nfunction save(backend) {\n  return backendState(backend).save()\n}\n\n/**\n * Loads the document and/or changes contained in an Uint8Array, and returns a\n * backend initialised with this state.\n */\nfunction load(data) {\n  const state = new BackendDoc(data)\n  return {state, heads: state.heads}\n}\n\n/**\n * Applies a list of `changes` to the node state `backend`, and returns the updated\n * state with those changes incorporated. Unlike `applyChanges()`, this function\n * does not produce a patch describing the incremental modifications, making it\n * a little faster when loading a document from disk. When all the changes have\n * been loaded, you can use `getPatch()` to construct the latest document state.\n */\nfunction loadChanges(backend, changes) {\n  const state = backendState(backend)\n  state.applyChanges(changes)\n  backend.frozen = true\n  return {state, heads: state.heads}\n}\n\n/**\n * Returns a patch that, when applied to an empty document, constructs the\n * document tree in the state described by the node state `backend`.\n */\nfunction getPatch(backend) {\n  return backendState(backend).getPatch()\n}\n\n/**\n * Returns an array of hashes of the current \"head\" changes (i.e. those changes\n * that no other change depends on).\n */\nfunction getHeads(backend) {\n  return backend.heads\n}\n\n/**\n * Returns the full history of changes that have been applied to a document.\n */\nfunction getAllChanges(backend) {\n  return getChanges(backend, [])\n}\n\n/**\n * Returns all changes that are newer than or concurrent to the changes\n * identified by the hashes in `haveDeps`. If `haveDeps` is an empty array, all\n * changes are returned. Throws an exception if any of the given hashes is unknown.\n */\nfunction getChanges(backend, haveDeps) {\n  if (!Array.isArray(haveDeps)) {\n    throw new TypeError('Pass an array of hashes to Backend.getChanges()')\n  }\n  return backendState(backend).getChanges(haveDeps)\n}\n\n/**\n * Returns all changes that are present in `backend2` but not in `backend1`.\n * Intended for use in situations where the two backends are for different actors.\n * To get the changes added between an older and a newer document state of the same\n * actor, use `getChanges()` instead. `getChangesAdded()` throws an exception if\n * one of the backend states is frozen (i.e. if it is not the latest state of that\n * backend instance; this distinction matters when the backend is mutable).\n */\nfunction getChangesAdded(backend1, backend2) {\n  return backendState(backend2).getChangesAdded(backendState(backend1))\n}\n\n/**\n * If the backend has applied a change with the given `hash` (given as a\n * hexadecimal string), returns that change (as a byte array). Returns undefined\n * if no change with that hash has been applied. A change with missing\n * dependencies does not count as having been applied.\n */\nfunction getChangeByHash(backend, hash) {\n  return backendState(backend).getChangeByHash(hash)\n}\n\n/**\n * Returns the hashes of any missing dependencies, i.e. where we have applied a\n * change that has a dependency on a change we have not seen.\n *\n * If the argument `heads` is given (an array of hexadecimal strings representing\n * hashes as returned by `getHeads()`), this function also ensures that all of\n * those hashes resolve to either a change that has been applied to the document,\n * or that has been enqueued for later application once missing dependencies have\n * arrived. Any missing heads hashes are included in the returned array.\n */\nfunction getMissingDeps(backend, heads = []) {\n  return backendState(backend).getMissingDeps(heads)\n}\n\nmodule.exports = {\n  init, clone, free, applyChanges, applyLocalChange, save, load, loadChanges, getPatch,\n  getHeads, getAllChanges, getChanges, getChangesAdded, getChangeByHash, getMissingDeps\n}\n"
  },
  {
    "path": "backend/columnar.js",
    "content": "const pako = require('pako')\nconst { copyObject, parseOpId, equalBytes } = require('../src/common')\nconst {\n  utf8ToString, hexStringToBytes, bytesToHexString,\n  Encoder, Decoder, RLEEncoder, RLEDecoder, DeltaEncoder, DeltaDecoder, BooleanEncoder, BooleanDecoder\n} = require('./encoding')\n\n// Maybe we should be using the platform's built-in hash implementation?\n// Node has the crypto module: https://nodejs.org/api/crypto.html and browsers have\n// https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/digest\n// However, the WebCrypto API is asynchronous (returns promises), which would\n// force all our APIs to become asynchronous as well, which would be annoying.\n//\n// I think on balance, it's safe enough to use a random library off npm:\n// - We only need one hash function (not a full suite of crypto algorithms);\n// - SHA256 is quite simple and has fairly few opportunities for subtle bugs\n//   (compared to asymmetric cryptography anyway);\n// - It does not need a secure source of random bits and does not need to be\n//   constant-time;\n// - I have reviewed the source code and it seems pretty reasonable.\nconst { Hash } = require('fast-sha256')\n\n// These bytes don't mean anything, they were generated randomly\nconst MAGIC_BYTES = new Uint8Array([0x85, 0x6f, 0x4a, 0x83])\n\nconst CHUNK_TYPE_DOCUMENT = 0\nconst CHUNK_TYPE_CHANGE = 1\nconst CHUNK_TYPE_DEFLATE = 2 // like CHUNK_TYPE_CHANGE but with DEFLATE compression\n\n// Minimum number of bytes in a value before we enable DEFLATE compression (there is no point\n// compressing very short values since compression may actually make them bigger)\nconst DEFLATE_MIN_SIZE = 256\n\n// The least-significant 3 bits of a columnId indicate its datatype\nconst COLUMN_TYPE = {\n  GROUP_CARD: 0, ACTOR_ID: 1, INT_RLE: 2, INT_DELTA: 3, BOOLEAN: 4,\n  STRING_RLE: 5, VALUE_LEN: 6, VALUE_RAW: 7\n}\n\n// The 4th-least-significant bit of a columnId is set if the column is DEFLATE-compressed\nconst COLUMN_TYPE_DEFLATE = 8\n\n// In the values in a column of type VALUE_LEN, the bottom four bits indicate the type of the value,\n// one of the following types in VALUE_TYPE. The higher bits indicate the length of the value in the\n// associated VALUE_RAW column (in bytes).\nconst VALUE_TYPE = {\n  NULL: 0, FALSE: 1, TRUE: 2, LEB128_UINT: 3, LEB128_INT: 4, IEEE754: 5,\n  UTF8: 6, BYTES: 7, COUNTER: 8, TIMESTAMP: 9, MIN_UNKNOWN: 10, MAX_UNKNOWN: 15\n}\n\n// make* actions must be at even-numbered indexes in this list\nconst ACTIONS = ['makeMap', 'set', 'makeList', 'del', 'makeText', 'inc', 'makeTable', 'link']\n\nconst OBJECT_TYPE = {makeMap: 'map', makeList: 'list', makeText: 'text', makeTable: 'table'}\n\nconst COMMON_COLUMNS = [\n  {columnName: 'objActor',  columnId: 0 << 4 | COLUMN_TYPE.ACTOR_ID},\n  {columnName: 'objCtr',    columnId: 0 << 4 | COLUMN_TYPE.INT_RLE},\n  {columnName: 'keyActor',  columnId: 1 << 4 | COLUMN_TYPE.ACTOR_ID},\n  {columnName: 'keyCtr',    columnId: 1 << 4 | COLUMN_TYPE.INT_DELTA},\n  {columnName: 'keyStr',    columnId: 1 << 4 | COLUMN_TYPE.STRING_RLE},\n  {columnName: 'idActor',   columnId: 2 << 4 | COLUMN_TYPE.ACTOR_ID},\n  {columnName: 'idCtr',     columnId: 2 << 4 | COLUMN_TYPE.INT_DELTA},\n  {columnName: 'insert',    columnId: 3 << 4 | COLUMN_TYPE.BOOLEAN},\n  {columnName: 'action',    columnId: 4 << 4 | COLUMN_TYPE.INT_RLE},\n  {columnName: 'valLen',    columnId: 5 << 4 | COLUMN_TYPE.VALUE_LEN},\n  {columnName: 'valRaw',    columnId: 5 << 4 | COLUMN_TYPE.VALUE_RAW},\n  {columnName: 'chldActor', columnId: 6 << 4 | COLUMN_TYPE.ACTOR_ID},\n  {columnName: 'chldCtr',   columnId: 6 << 4 | COLUMN_TYPE.INT_DELTA}\n]\n\nconst CHANGE_COLUMNS = COMMON_COLUMNS.concat([\n  {columnName: 'predNum',   columnId: 7 << 4 | COLUMN_TYPE.GROUP_CARD},\n  {columnName: 'predActor', columnId: 7 << 4 | COLUMN_TYPE.ACTOR_ID},\n  {columnName: 'predCtr',   columnId: 7 << 4 | COLUMN_TYPE.INT_DELTA}\n])\n\nconst DOC_OPS_COLUMNS = COMMON_COLUMNS.concat([\n  {columnName: 'succNum',   columnId: 8 << 4 | COLUMN_TYPE.GROUP_CARD},\n  {columnName: 'succActor', columnId: 8 << 4 | COLUMN_TYPE.ACTOR_ID},\n  {columnName: 'succCtr',   columnId: 8 << 4 | COLUMN_TYPE.INT_DELTA}\n])\n\nconst DOCUMENT_COLUMNS = [\n  {columnName: 'actor',     columnId: 0 << 4 | COLUMN_TYPE.ACTOR_ID},\n  {columnName: 'seq',       columnId: 0 << 4 | COLUMN_TYPE.INT_DELTA},\n  {columnName: 'maxOp',     columnId: 1 << 4 | COLUMN_TYPE.INT_DELTA},\n  {columnName: 'time',      columnId: 2 << 4 | COLUMN_TYPE.INT_DELTA},\n  {columnName: 'message',   columnId: 3 << 4 | COLUMN_TYPE.STRING_RLE},\n  {columnName: 'depsNum',   columnId: 4 << 4 | COLUMN_TYPE.GROUP_CARD},\n  {columnName: 'depsIndex', columnId: 4 << 4 | COLUMN_TYPE.INT_DELTA},\n  {columnName: 'extraLen',  columnId: 5 << 4 | COLUMN_TYPE.VALUE_LEN},\n  {columnName: 'extraRaw',  columnId: 5 << 4 | COLUMN_TYPE.VALUE_RAW}\n]\n\n/**\n * Maps an opId of the form {counter: 12345, actorId: 'someActorId'} to the form\n * {counter: 12345, actorNum: 123, actorId: 'someActorId'}, where the actorNum\n * is the index into the `actorIds` array.\n */\nfunction actorIdToActorNum(opId, actorIds) {\n  if (!opId || !opId.actorId) return opId\n  const counter = opId.counter\n  const actorNum = actorIds.indexOf(opId.actorId)\n  if (actorNum < 0) throw new RangeError('missing actorId') // should not happen\n  return {counter, actorNum, actorId: opId.actorId}\n}\n\n/**\n * Comparison function to pass to Array.sort(), which compares two opIds in the\n * form produced by `actorIdToActorNum` so that they are sorted in increasing\n * Lamport timestamp order (sorted first by counter, then by actorId).\n */\nfunction compareParsedOpIds(id1, id2) {\n  if (id1.counter < id2.counter) return -1\n  if (id1.counter > id2.counter) return +1\n  if (id1.actorId < id2.actorId) return -1\n  if (id1.actorId > id2.actorId) return +1\n  return 0\n}\n\n/**\n * Takes `changes`, an array of changes (represented as JS objects). Returns an\n * object `{changes, actorIds}`, where `changes` is a copy of the argument in\n * which all string opIds have been replaced with `{counter, actorNum}` objects,\n * and where `actorIds` is a lexicographically sorted array of actor IDs occurring\n * in any of the operations. `actorNum` is an index into that array of actorIds.\n * If `single` is true, the actorId of the author of the change is moved to the\n * beginning of the array of actorIds, so that `actorNum` is zero when referencing\n * the author of the change itself. This special-casing is omitted if `single` is\n * false.\n */\nfunction parseAllOpIds(changes, single) {\n  const actors = {}, newChanges = []\n  for (let change of changes) {\n    change = copyObject(change)\n    actors[change.actor] = true\n    change.ops = expandMultiOps(change.ops, change.startOp, change.actor)\n    change.ops = change.ops.map(op => {\n      op = copyObject(op)\n      if (op.obj !== '_root') op.obj = parseOpId(op.obj)\n      if (op.elemId && op.elemId !== '_head') op.elemId = parseOpId(op.elemId)\n      if (op.child) op.child = parseOpId(op.child)\n      if (op.pred) op.pred = op.pred.map(parseOpId)\n      if (op.obj.actorId) actors[op.obj.actorId] = true\n      if (op.elemId && op.elemId.actorId) actors[op.elemId.actorId] = true\n      if (op.child && op.child.actorId) actors[op.child.actorId] = true\n      for (let pred of op.pred) actors[pred.actorId] = true\n      return op\n    })\n    newChanges.push(change)\n  }\n\n  let actorIds = Object.keys(actors).sort()\n  if (single) {\n    actorIds = [changes[0].actor].concat(actorIds.filter(actor => actor !== changes[0].actor))\n  }\n  for (let change of newChanges) {\n    change.actorNum = actorIds.indexOf(change.actor)\n    for (let i = 0; i < change.ops.length; i++) {\n      let op = change.ops[i]\n      op.id = {counter: change.startOp + i, actorNum: change.actorNum, actorId: change.actor}\n      op.obj = actorIdToActorNum(op.obj, actorIds)\n      op.elemId = actorIdToActorNum(op.elemId, actorIds)\n      op.child = actorIdToActorNum(op.child, actorIds)\n      op.pred = op.pred.map(pred => actorIdToActorNum(pred, actorIds))\n    }\n  }\n  return {changes: newChanges, actorIds}\n}\n\n/**\n * Encodes the `obj` property of operation `op` into the two columns\n * `objActor` and `objCtr`.\n */\nfunction encodeObjectId(op, columns) {\n  if (op.obj === '_root') {\n    columns.objActor.appendValue(null)\n    columns.objCtr.appendValue(null)\n  } else if (op.obj.actorNum >= 0 && op.obj.counter > 0) {\n    columns.objActor.appendValue(op.obj.actorNum)\n    columns.objCtr.appendValue(op.obj.counter)\n  } else {\n    throw new RangeError(`Unexpected objectId reference: ${JSON.stringify(op.obj)}`)\n  }\n}\n\n/**\n * Encodes the `key` and `elemId` properties of operation `op` into the three\n * columns `keyActor`, `keyCtr`, and `keyStr`.\n */\nfunction encodeOperationKey(op, columns) {\n  if (op.key) {\n    columns.keyActor.appendValue(null)\n    columns.keyCtr.appendValue(null)\n    columns.keyStr.appendValue(op.key)\n  } else if (op.elemId === '_head' && op.insert) {\n    columns.keyActor.appendValue(null)\n    columns.keyCtr.appendValue(0)\n    columns.keyStr.appendValue(null)\n  } else if (op.elemId && op.elemId.actorNum >= 0 && op.elemId.counter > 0) {\n    columns.keyActor.appendValue(op.elemId.actorNum)\n    columns.keyCtr.appendValue(op.elemId.counter)\n    columns.keyStr.appendValue(null)\n  } else {\n    throw new RangeError(`Unexpected operation key: ${JSON.stringify(op)}`)\n  }\n}\n\n/**\n * Encodes the `action` property of operation `op` into the `action` column.\n */\nfunction encodeOperationAction(op, columns) {\n  const actionCode = ACTIONS.indexOf(op.action)\n  if (actionCode >= 0) {\n    columns.action.appendValue(actionCode)\n  } else if (typeof op.action === 'number') {\n    columns.action.appendValue(op.action)\n  } else {\n    throw new RangeError(`Unexpected operation action: ${op.action}`)\n  }\n}\n\n/**\n * Given the datatype for a number, determine the typeTag and the value to encode\n * otherwise guess\n */\nfunction getNumberTypeAndValue(op) {\n  switch (op.datatype) {\n    case \"counter\":\n      return [ VALUE_TYPE.COUNTER, op.value ]\n    case \"timestamp\":\n      return [ VALUE_TYPE.TIMESTAMP, op.value ]\n    case \"uint\":\n      return [ VALUE_TYPE.LEB128_UINT, op.value ]\n    case \"int\":\n      return [ VALUE_TYPE.LEB128_INT, op.value ]\n    case \"float64\": {\n      const buf64 = new ArrayBuffer(8), view64 = new DataView(buf64)\n      view64.setFloat64(0, op.value, true)\n      return [ VALUE_TYPE.IEEE754,  new Uint8Array(buf64) ]\n    }\n    default:\n      // increment operators get resolved here ...\n      if (Number.isInteger(op.value) && op.value <= Number.MAX_SAFE_INTEGER && op.value >= Number.MIN_SAFE_INTEGER) {\n        return [ VALUE_TYPE.LEB128_INT, op.value ]\n      } else {\n        const buf64 = new ArrayBuffer(8), view64 = new DataView(buf64)\n        view64.setFloat64(0, op.value, true)\n        return [ VALUE_TYPE.IEEE754,  new Uint8Array(buf64) ]\n      }\n  }\n}\n\n/**\n * Encodes the `value` property of operation `op` into the two columns\n * `valLen` and `valRaw`.\n */\nfunction encodeValue(op, columns) {\n  if ((op.action !== 'set' && op.action !== 'inc') || op.value === null) {\n    columns.valLen.appendValue(VALUE_TYPE.NULL)\n  } else if (op.value === false) {\n    columns.valLen.appendValue(VALUE_TYPE.FALSE)\n  } else if (op.value === true) {\n    columns.valLen.appendValue(VALUE_TYPE.TRUE)\n  } else if (typeof op.value === 'string') {\n    const numBytes = columns.valRaw.appendRawString(op.value)\n    columns.valLen.appendValue(numBytes << 4 | VALUE_TYPE.UTF8)\n  } else if (ArrayBuffer.isView(op.value)) {\n    const numBytes = columns.valRaw.appendRawBytes(new Uint8Array(op.value.buffer))\n    columns.valLen.appendValue(numBytes << 4 | VALUE_TYPE.BYTES)\n  } else if (typeof op.value === 'number') {\n    let [typeTag, value] = getNumberTypeAndValue(op)\n    let numBytes\n    if (typeTag === VALUE_TYPE.LEB128_UINT) {\n      numBytes = columns.valRaw.appendUint53(value)\n    } else if (typeTag === VALUE_TYPE.IEEE754) {\n      numBytes = columns.valRaw.appendRawBytes(value)\n    } else {\n      numBytes = columns.valRaw.appendInt53(value)\n    }\n    columns.valLen.appendValue(numBytes << 4 | typeTag)\n  } else if (typeof op.datatype === 'number' && op.datatype >= VALUE_TYPE.MIN_UNKNOWN &&\n             op.datatype <= VALUE_TYPE.MAX_UNKNOWN && op.value instanceof Uint8Array) {\n    const numBytes = columns.valRaw.appendRawBytes(op.value)\n    columns.valLen.appendValue(numBytes << 4 | op.datatype)\n  } else if (op.datatype) {\n      throw new RangeError(`Unknown datatype ${op.datatype} for value ${op.value}`)\n  } else {\n    throw new RangeError(`Unsupported value in operation: ${op.value}`)\n  }\n}\n\n/**\n * Given `sizeTag` (an unsigned integer read from a VALUE_LEN column) and `bytes` (a Uint8Array\n * read from a VALUE_RAW column, with length `sizeTag >> 4`), this function returns an object of the\n * form `{value: value, datatype: datatypeTag}` where `value` is a JavaScript primitive datatype\n * corresponding to the value, and `datatypeTag` is a datatype annotation such as 'counter'.\n */\nfunction decodeValue(sizeTag, bytes) {\n  if (sizeTag === VALUE_TYPE.NULL) {\n    return {value: null}\n  } else if (sizeTag === VALUE_TYPE.FALSE) {\n    return {value: false}\n  } else if (sizeTag === VALUE_TYPE.TRUE) {\n    return {value: true}\n  } else if (sizeTag % 16 === VALUE_TYPE.UTF8) {\n    return {value: utf8ToString(bytes)}\n  } else {\n    if (sizeTag % 16 === VALUE_TYPE.LEB128_UINT) {\n      return {value: new Decoder(bytes).readUint53(), datatype: \"uint\"}\n    } else if (sizeTag % 16 === VALUE_TYPE.LEB128_INT) {\n      return {value: new Decoder(bytes).readInt53(), datatype: \"int\"}\n    } else if (sizeTag % 16 === VALUE_TYPE.IEEE754) {\n      const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength)\n      if (bytes.byteLength === 8) {\n        return {value: view.getFloat64(0, true), datatype: \"float64\"}\n      } else {\n        throw new RangeError(`Invalid length for floating point number: ${bytes.byteLength}`)\n      }\n    } else if (sizeTag % 16 === VALUE_TYPE.COUNTER) {\n      return {value: new Decoder(bytes).readInt53(), datatype: 'counter'}\n    } else if (sizeTag % 16 === VALUE_TYPE.TIMESTAMP) {\n      return {value: new Decoder(bytes).readInt53(), datatype: 'timestamp'}\n    } else {\n      return {value: bytes, datatype: sizeTag % 16}\n    }\n  }\n}\n\n/**\n * Reads one value from the column `columns[colIndex]` and interprets it based\n * on the column type. `actorIds` is a list of actors that appear in the change;\n * `actorIds[0]` is the actorId of the change's author. Mutates the `result`\n * object with the value, and returns the number of columns processed (this is 2\n * in the case of a pair of VALUE_LEN and VALUE_RAW columns, which are processed\n * in one go).\n */\nfunction decodeValueColumns(columns, colIndex, actorIds, result) {\n  const { columnId, columnName, decoder } = columns[colIndex]\n  if (columnId % 8 === COLUMN_TYPE.VALUE_LEN && colIndex + 1 < columns.length &&\n      columns[colIndex + 1].columnId === columnId + 1) {\n    const sizeTag = decoder.readValue()\n    const rawValue = columns[colIndex + 1].decoder.readRawBytes(sizeTag >> 4)\n    const { value, datatype } = decodeValue(sizeTag, rawValue)\n    result[columnName] = value\n    if (datatype) result[columnName + '_datatype'] = datatype\n    return 2\n  } else if (columnId % 8 === COLUMN_TYPE.ACTOR_ID) {\n    const actorNum = decoder.readValue()\n    if (actorNum === null) {\n      result[columnName] = null\n    } else {\n      if (!actorIds[actorNum]) throw new RangeError(`No actor index ${actorNum}`)\n      result[columnName] = actorIds[actorNum]\n    }\n  } else {\n    result[columnName] = decoder.readValue()\n  }\n  return 1\n}\n\n/**\n * Encodes an array of operations in a set of columns. The operations need to\n * be parsed with `parseAllOpIds()` beforehand. If `forDocument` is true, we use\n * the column structure of a whole document, otherwise we use the column\n * structure for an individual change. Returns an array of\n * `{columnId, columnName, encoder}` objects.\n */\nfunction encodeOps(ops, forDocument) {\n  const columns = {\n    objActor  : new RLEEncoder('uint'),\n    objCtr    : new RLEEncoder('uint'),\n    keyActor  : new RLEEncoder('uint'),\n    keyCtr    : new DeltaEncoder(),\n    keyStr    : new RLEEncoder('utf8'),\n    insert    : new BooleanEncoder(),\n    action    : new RLEEncoder('uint'),\n    valLen    : new RLEEncoder('uint'),\n    valRaw    : new Encoder(),\n    chldActor : new RLEEncoder('uint'),\n    chldCtr   : new DeltaEncoder()\n  }\n\n  if (forDocument) {\n    columns.idActor   = new RLEEncoder('uint')\n    columns.idCtr     = new DeltaEncoder()\n    columns.succNum   = new RLEEncoder('uint')\n    columns.succActor = new RLEEncoder('uint')\n    columns.succCtr   = new DeltaEncoder()\n  } else {\n    columns.predNum   = new RLEEncoder('uint')\n    columns.predCtr   = new DeltaEncoder()\n    columns.predActor = new RLEEncoder('uint')\n  }\n\n  for (let op of ops) {\n    encodeObjectId(op, columns)\n    encodeOperationKey(op, columns)\n    columns.insert.appendValue(!!op.insert)\n    encodeOperationAction(op, columns)\n    encodeValue(op, columns)\n\n    if (op.child && op.child.counter) {\n      columns.chldActor.appendValue(op.child.actorNum)\n      columns.chldCtr.appendValue(op.child.counter)\n    } else {\n      columns.chldActor.appendValue(null)\n      columns.chldCtr.appendValue(null)\n    }\n\n    if (forDocument) {\n      columns.idActor.appendValue(op.id.actorNum)\n      columns.idCtr.appendValue(op.id.counter)\n      columns.succNum.appendValue(op.succ.length)\n      op.succ.sort(compareParsedOpIds)\n      for (let i = 0; i < op.succ.length; i++) {\n        columns.succActor.appendValue(op.succ[i].actorNum)\n        columns.succCtr.appendValue(op.succ[i].counter)\n      }\n    } else {\n      columns.predNum.appendValue(op.pred.length)\n      op.pred.sort(compareParsedOpIds)\n      for (let i = 0; i < op.pred.length; i++) {\n        columns.predActor.appendValue(op.pred[i].actorNum)\n        columns.predCtr.appendValue(op.pred[i].counter)\n      }\n    }\n  }\n\n  let columnList = []\n  for (let {columnName, columnId} of forDocument ? DOC_OPS_COLUMNS : CHANGE_COLUMNS) {\n    if (columns[columnName]) columnList.push({columnId, columnName, encoder: columns[columnName]})\n  }\n  return columnList.sort((a, b) => a.columnId - b.columnId)\n}\n\nfunction validDatatype(value, datatype) {\n  if (datatype === undefined) {\n    return (typeof value === 'string' || typeof value === 'boolean' || value === null)\n  } else {\n    return typeof value === 'number'\n  }\n}\n\nfunction expandMultiOps(ops, startOp, actor) {\n  let opNum = startOp\n  let expandedOps = []\n  for (const op of ops) {\n    if (op.action === 'set' && op.values && op.insert) {\n      if (op.pred.length !== 0) throw new RangeError('multi-insert pred must be empty')\n      let lastElemId = op.elemId\n      const datatype = op.datatype\n      for (const value of op.values) {\n        if (!validDatatype(value, datatype)) throw new RangeError(`Decode failed: bad value/datatype association (${value},${datatype})`)\n        expandedOps.push({action: 'set', obj: op.obj, elemId: lastElemId, datatype, value, pred: [], insert: true})\n        lastElemId = `${opNum}@${actor}`\n        opNum += 1\n      }\n    } else if (op.action === 'del' && op.multiOp > 1) {\n      if (op.pred.length !== 1) throw new RangeError('multiOp deletion must have exactly one pred')\n      const startElemId = parseOpId(op.elemId), startPred = parseOpId(op.pred[0])\n      for (let i = 0; i < op.multiOp; i++) {\n        const elemId = `${startElemId.counter + i}@${startElemId.actorId}`\n        const pred = [`${startPred.counter + i}@${startPred.actorId}`]\n        expandedOps.push({action: 'del', obj: op.obj, elemId, pred})\n        opNum += 1\n      }\n    } else {\n      expandedOps.push(op)\n      opNum += 1\n    }\n  }\n  return expandedOps\n}\n\n/**\n * Takes a change as decoded by `decodeColumns`, and changes it into the form\n * expected by the rest of the backend. If `forDocument` is true, we use the op\n * structure of a whole document, otherwise we use the op structure for an\n * individual change.\n */\nfunction decodeOps(ops, forDocument) {\n  const newOps = []\n  for (let op of ops) {\n    const obj = (op.objCtr === null) ? '_root' : `${op.objCtr}@${op.objActor}`\n    const elemId = op.keyStr ? undefined : (op.keyCtr === 0 ? '_head' : `${op.keyCtr}@${op.keyActor}`)\n    const action = ACTIONS[op.action] || op.action\n    const newOp = elemId ? {obj, elemId, action} : {obj, key: op.keyStr, action}\n    newOp.insert = !!op.insert\n    if (ACTIONS[op.action] === 'set' || ACTIONS[op.action] === 'inc') {\n      newOp.value = op.valLen\n      if (op.valLen_datatype) newOp.datatype = op.valLen_datatype\n    }\n    if (!!op.chldCtr !== !!op.chldActor) {\n      throw new RangeError(`Mismatched child columns: ${op.chldCtr} and ${op.chldActor}`)\n    }\n    if (op.chldCtr !== null) newOp.child = `${op.chldCtr}@${op.chldActor}`\n    if (forDocument) {\n      newOp.id = `${op.idCtr}@${op.idActor}`\n      newOp.succ = op.succNum.map(succ => `${succ.succCtr}@${succ.succActor}`)\n      checkSortedOpIds(op.succNum.map(succ => ({counter: succ.succCtr, actorId: succ.succActor})))\n    } else {\n      newOp.pred = op.predNum.map(pred => `${pred.predCtr}@${pred.predActor}`)\n      checkSortedOpIds(op.predNum.map(pred => ({counter: pred.predCtr, actorId: pred.predActor})))\n    }\n    newOps.push(newOp)\n  }\n  return newOps\n}\n\n/**\n * Throws an exception if the opIds in the given array are not in sorted order.\n */\nfunction checkSortedOpIds(opIds) {\n  let last = null\n  for (let opId of opIds) {\n    if (last && compareParsedOpIds(last, opId) !== -1) {\n      throw new RangeError('operation IDs are not in ascending order')\n    }\n    last = opId\n  }\n}\n\nfunction encoderByColumnId(columnId) {\n  if ((columnId & 7) === COLUMN_TYPE.INT_DELTA) {\n    return new DeltaEncoder()\n  } else if ((columnId & 7) === COLUMN_TYPE.BOOLEAN) {\n    return new BooleanEncoder()\n  } else if ((columnId & 7) === COLUMN_TYPE.STRING_RLE) {\n    return new RLEEncoder('utf8')\n  } else if ((columnId & 7) === COLUMN_TYPE.VALUE_RAW) {\n    return new Encoder()\n  } else {\n    return new RLEEncoder('uint')\n  }\n}\n\nfunction decoderByColumnId(columnId, buffer) {\n  if ((columnId & 7) === COLUMN_TYPE.INT_DELTA) {\n    return new DeltaDecoder(buffer)\n  } else if ((columnId & 7) === COLUMN_TYPE.BOOLEAN) {\n    return new BooleanDecoder(buffer)\n  } else if ((columnId & 7) === COLUMN_TYPE.STRING_RLE) {\n    return new RLEDecoder('utf8', buffer)\n  } else if ((columnId & 7) === COLUMN_TYPE.VALUE_RAW) {\n    return new Decoder(buffer)\n  } else {\n    return new RLEDecoder('uint', buffer)\n  }\n}\n\nfunction makeDecoders(columns, columnSpec) {\n  const emptyBuf = new Uint8Array(0)\n  let decoders = [], columnIndex = 0, specIndex = 0\n\n  while (columnIndex < columns.length || specIndex < columnSpec.length) {\n    if (columnIndex === columns.length ||\n        (specIndex < columnSpec.length && columnSpec[specIndex].columnId < columns[columnIndex].columnId)) {\n      const {columnId, columnName} = columnSpec[specIndex]\n      decoders.push({columnId, columnName, decoder: decoderByColumnId(columnId, emptyBuf)})\n      specIndex++\n    } else if (specIndex === columnSpec.length || columns[columnIndex].columnId < columnSpec[specIndex].columnId) {\n      const {columnId, buffer} = columns[columnIndex]\n      decoders.push({columnId, decoder: decoderByColumnId(columnId, buffer)})\n      columnIndex++\n    } else { // columns[columnIndex].columnId === columnSpec[specIndex].columnId\n      const {columnId, buffer} = columns[columnIndex], {columnName} = columnSpec[specIndex]\n      decoders.push({columnId, columnName, decoder: decoderByColumnId(columnId, buffer)})\n      columnIndex++\n      specIndex++\n    }\n  }\n  return decoders\n}\n\nfunction decodeColumns(columns, actorIds, columnSpec) {\n  columns = makeDecoders(columns, columnSpec)\n  let parsedRows = []\n  while (columns.some(col => !col.decoder.done)) {\n    let row = {}, col = 0\n    while (col < columns.length) {\n      const columnId = columns[col].columnId\n      let groupId = columnId >> 4, groupCols = 1\n      while (col + groupCols < columns.length && columns[col + groupCols].columnId >> 4 === groupId) {\n        groupCols++\n      }\n\n      if (columnId % 8 === COLUMN_TYPE.GROUP_CARD) {\n        const values = [], count = columns[col].decoder.readValue()\n        for (let i = 0; i < count; i++) {\n          let value = {}\n          for (let colOffset = 1; colOffset < groupCols; colOffset++) {\n            decodeValueColumns(columns, col + colOffset, actorIds, value)\n          }\n          values.push(value)\n        }\n        row[columns[col].columnName] = values\n        col += groupCols\n      } else {\n        col += decodeValueColumns(columns, col, actorIds, row)\n      }\n    }\n    parsedRows.push(row)\n  }\n  return parsedRows\n}\n\nfunction decodeColumnInfo(decoder) {\n  // A number that is all 1 bits except for the bit that indicates whether a column is\n  // deflate-compressed. We ignore this bit when checking whether columns are sorted by ID.\n  const COLUMN_ID_MASK = (-1 ^ COLUMN_TYPE_DEFLATE) >>> 0\n\n  let lastColumnId = -1, columns = [], numColumns = decoder.readUint53()\n  for (let i = 0; i < numColumns; i++) {\n    const columnId = decoder.readUint53(), bufferLen = decoder.readUint53()\n    if ((columnId & COLUMN_ID_MASK) <= (lastColumnId & COLUMN_ID_MASK)) {\n      throw new RangeError('Columns must be in ascending order')\n    }\n    lastColumnId = columnId\n    columns.push({columnId, bufferLen})\n  }\n  return columns\n}\n\nfunction encodeColumnInfo(encoder, columns) {\n  const nonEmptyColumns = columns.filter(column => column.encoder.buffer.byteLength > 0)\n  encoder.appendUint53(nonEmptyColumns.length)\n  for (let column of nonEmptyColumns) {\n    encoder.appendUint53(column.columnId)\n    encoder.appendUint53(column.encoder.buffer.byteLength)\n  }\n}\n\nfunction decodeChangeHeader(decoder) {\n  const numDeps = decoder.readUint53(), deps = []\n  for (let i = 0; i < numDeps; i++) {\n    deps.push(bytesToHexString(decoder.readRawBytes(32)))\n  }\n  let change = {\n    actor:   decoder.readHexString(),\n    seq:     decoder.readUint53(),\n    startOp: decoder.readUint53(),\n    time:    decoder.readInt53(),\n    message: decoder.readPrefixedString(),\n    deps\n  }\n  const actorIds = [change.actor], numActorIds = decoder.readUint53()\n  for (let i = 0; i < numActorIds; i++) actorIds.push(decoder.readHexString())\n  change.actorIds = actorIds\n  return change\n}\n\n/**\n * Assembles a chunk of encoded data containing a checksum, headers, and a\n * series of encoded columns. Calls `encodeHeaderCallback` with an encoder that\n * should be used to add the headers. The columns should be given as `columns`.\n */\nfunction encodeContainer(chunkType, encodeContentsCallback) {\n  const CHECKSUM_SIZE = 4 // checksum is first 4 bytes of SHA-256 hash of the rest of the data\n  const HEADER_SPACE = MAGIC_BYTES.byteLength + CHECKSUM_SIZE + 1 + 5 // 1 byte type + 5 bytes length\n  const body = new Encoder()\n  // Make space for the header at the beginning of the body buffer. We will\n  // copy the header in here later. This is cheaper than copying the body since\n  // the body is likely to be much larger than the header.\n  body.appendRawBytes(new Uint8Array(HEADER_SPACE))\n  encodeContentsCallback(body)\n\n  const bodyBuf = body.buffer\n  const header = new Encoder()\n  header.appendByte(chunkType)\n  header.appendUint53(bodyBuf.byteLength - HEADER_SPACE)\n\n  // Compute the hash over chunkType, length, and body\n  const headerBuf = header.buffer\n  const sha256 = new Hash()\n  sha256.update(headerBuf)\n  sha256.update(bodyBuf.subarray(HEADER_SPACE))\n  const hash = sha256.digest(), checksum = hash.subarray(0, CHECKSUM_SIZE)\n\n  // Copy header into the body buffer so that they are contiguous\n  bodyBuf.set(MAGIC_BYTES, HEADER_SPACE - headerBuf.byteLength - CHECKSUM_SIZE - MAGIC_BYTES.byteLength)\n  bodyBuf.set(checksum,    HEADER_SPACE - headerBuf.byteLength - CHECKSUM_SIZE)\n  bodyBuf.set(headerBuf,   HEADER_SPACE - headerBuf.byteLength)\n  return {hash, bytes: bodyBuf.subarray(HEADER_SPACE - headerBuf.byteLength - CHECKSUM_SIZE - MAGIC_BYTES.byteLength)}\n}\n\nfunction decodeContainerHeader(decoder, computeHash) {\n  if (!equalBytes(decoder.readRawBytes(MAGIC_BYTES.byteLength), MAGIC_BYTES)) {\n    throw new RangeError('Data does not begin with magic bytes 85 6f 4a 83')\n  }\n  const expectedHash = decoder.readRawBytes(4)\n  const hashStartOffset = decoder.offset\n  const chunkType = decoder.readByte()\n  const chunkLength = decoder.readUint53()\n  const header = {chunkType, chunkLength, chunkData: decoder.readRawBytes(chunkLength)}\n\n  if (computeHash) {\n    const sha256 = new Hash()\n    sha256.update(decoder.buf.subarray(hashStartOffset, decoder.offset))\n    const binaryHash = sha256.digest()\n    if (!equalBytes(binaryHash.subarray(0, 4), expectedHash)) {\n      throw new RangeError('checksum does not match data')\n    }\n    header.hash = bytesToHexString(binaryHash)\n  }\n  return header\n}\n\nfunction encodeChange(changeObj) {\n  const { changes, actorIds } = parseAllOpIds([changeObj], true)\n  const change = changes[0]\n\n  const { hash, bytes } = encodeContainer(CHUNK_TYPE_CHANGE, encoder => {\n    if (!Array.isArray(change.deps)) throw new TypeError('deps is not an array')\n    encoder.appendUint53(change.deps.length)\n    for (let hash of change.deps.slice().sort()) {\n      encoder.appendRawBytes(hexStringToBytes(hash))\n    }\n    encoder.appendHexString(change.actor)\n    encoder.appendUint53(change.seq)\n    encoder.appendUint53(change.startOp)\n    encoder.appendInt53(change.time)\n    encoder.appendPrefixedString(change.message || '')\n    encoder.appendUint53(actorIds.length - 1)\n    for (let actor of actorIds.slice(1)) encoder.appendHexString(actor)\n\n    const columns = encodeOps(change.ops, false)\n    encodeColumnInfo(encoder, columns)\n    for (let column of columns) encoder.appendRawBytes(column.encoder.buffer)\n    if (change.extraBytes) encoder.appendRawBytes(change.extraBytes)\n  })\n\n  const hexHash = bytesToHexString(hash)\n  if (changeObj.hash && changeObj.hash !== hexHash) {\n    throw new RangeError(`Change hash does not match encoding: ${changeObj.hash} != ${hexHash}`)\n  }\n  return (bytes.byteLength >= DEFLATE_MIN_SIZE) ? deflateChange(bytes) : bytes\n}\n\nfunction decodeChangeColumns(buffer) {\n  if (buffer[8] === CHUNK_TYPE_DEFLATE) buffer = inflateChange(buffer)\n  const decoder = new Decoder(buffer)\n  const header = decodeContainerHeader(decoder, true)\n  const chunkDecoder = new Decoder(header.chunkData)\n  if (!decoder.done) throw new RangeError('Encoded change has trailing data')\n  if (header.chunkType !== CHUNK_TYPE_CHANGE) throw new RangeError(`Unexpected chunk type: ${header.chunkType}`)\n\n  const change = decodeChangeHeader(chunkDecoder)\n  const columns = decodeColumnInfo(chunkDecoder)\n  for (let i = 0; i < columns.length; i++) {\n    if ((columns[i].columnId & COLUMN_TYPE_DEFLATE) !== 0) {\n      throw new RangeError('change must not contain deflated columns')\n    }\n    columns[i].buffer = chunkDecoder.readRawBytes(columns[i].bufferLen)\n  }\n  if (!chunkDecoder.done) {\n    const restLen = chunkDecoder.buf.byteLength - chunkDecoder.offset\n    change.extraBytes = chunkDecoder.readRawBytes(restLen)\n  }\n\n  change.columns = columns\n  change.hash = header.hash\n  return change\n}\n\n/**\n * Decodes one change in binary format into its JS object representation.\n */\nfunction decodeChange(buffer) {\n  const change = decodeChangeColumns(buffer)\n  change.ops = decodeOps(decodeColumns(change.columns, change.actorIds, CHANGE_COLUMNS), false)\n  delete change.actorIds\n  delete change.columns\n  return change\n}\n\n/**\n * Decodes the header fields of a change in binary format, but does not decode\n * the operations. Saves work when we only need to inspect the headers. Only\n * computes the hash of the change if `computeHash` is true.\n */\nfunction decodeChangeMeta(buffer, computeHash) {\n  if (buffer[8] === CHUNK_TYPE_DEFLATE) buffer = inflateChange(buffer)\n  const header = decodeContainerHeader(new Decoder(buffer), computeHash)\n  if (header.chunkType !== CHUNK_TYPE_CHANGE) {\n    throw new RangeError('Buffer chunk type is not a change')\n  }\n  const meta = decodeChangeHeader(new Decoder(header.chunkData))\n  meta.change = buffer\n  if (computeHash) meta.hash = header.hash\n  return meta\n}\n\n/**\n * Compresses a binary change using DEFLATE.\n */\nfunction deflateChange(buffer) {\n  const header = decodeContainerHeader(new Decoder(buffer), false)\n  if (header.chunkType !== CHUNK_TYPE_CHANGE) throw new RangeError(`Unexpected chunk type: ${header.chunkType}`)\n  const compressed = pako.deflateRaw(header.chunkData)\n  const encoder = new Encoder()\n  encoder.appendRawBytes(buffer.subarray(0, 8)) // copy MAGIC_BYTES and checksum\n  encoder.appendByte(CHUNK_TYPE_DEFLATE)\n  encoder.appendUint53(compressed.byteLength)\n  encoder.appendRawBytes(compressed)\n  return encoder.buffer\n}\n\n/**\n * Decompresses a binary change that has been compressed with DEFLATE.\n */\nfunction inflateChange(buffer) {\n  const header = decodeContainerHeader(new Decoder(buffer), false)\n  if (header.chunkType !== CHUNK_TYPE_DEFLATE) throw new RangeError(`Unexpected chunk type: ${header.chunkType}`)\n  const decompressed = pako.inflateRaw(header.chunkData)\n  const encoder = new Encoder()\n  encoder.appendRawBytes(buffer.subarray(0, 8)) // copy MAGIC_BYTES and checksum\n  encoder.appendByte(CHUNK_TYPE_CHANGE)\n  encoder.appendUint53(decompressed.byteLength)\n  encoder.appendRawBytes(decompressed)\n  return encoder.buffer\n}\n\n/**\n * Takes an Uint8Array that may contain multiple concatenated changes, and\n * returns an array of subarrays, each subarray containing one change.\n */\nfunction splitContainers(buffer) {\n  let decoder = new Decoder(buffer), chunks = [], startOffset = 0\n  while (!decoder.done) {\n    decodeContainerHeader(decoder, false)\n    chunks.push(buffer.subarray(startOffset, decoder.offset))\n    startOffset = decoder.offset\n  }\n  return chunks\n}\n\n/**\n * Decodes a list of changes from the binary format into JS objects.\n * `binaryChanges` is an array of `Uint8Array` objects.\n */\nfunction decodeChanges(binaryChanges) {\n  let decoded = []\n  for (let binaryChange of binaryChanges) {\n    for (let chunk of splitContainers(binaryChange)) {\n      if (chunk[8] === CHUNK_TYPE_DOCUMENT) {\n        decoded = decoded.concat(decodeDocument(chunk))\n      } else if (chunk[8] === CHUNK_TYPE_CHANGE || chunk[8] === CHUNK_TYPE_DEFLATE) {\n        decoded.push(decodeChange(chunk))\n      } else {\n        // ignoring chunk of unknown type\n      }\n    }\n  }\n  return decoded\n}\n\nfunction sortOpIds(a, b) {\n  if (a === b) return 0\n  if (a === '_root') return -1\n  if (b === '_root') return +1\n  const a_ = parseOpId(a), b_ = parseOpId(b)\n  if (a_.counter < b_.counter) return -1\n  if (a_.counter > b_.counter) return +1\n  if (a_.actorId < b_.actorId) return -1\n  if (a_.actorId > b_.actorId) return +1\n  return 0\n}\n\n/**\n * Takes a set of operations `ops` loaded from an encoded document, and\n * reconstructs the changes that they originally came from.\n * Does not return anything, only mutates `changes`.\n */\nfunction groupChangeOps(changes, ops) {\n  let changesByActor = {} // map from actorId to array of changes by that actor\n  for (let change of changes) {\n    change.ops = []\n    if (!changesByActor[change.actor]) changesByActor[change.actor] = []\n    if (change.seq !== changesByActor[change.actor].length + 1) {\n      throw new RangeError(`Expected seq = ${changesByActor[change.actor].length + 1}, got ${change.seq}`)\n    }\n    if (change.seq > 1 && changesByActor[change.actor][change.seq - 2].maxOp > change.maxOp) {\n      throw new RangeError('maxOp must increase monotonically per actor')\n    }\n    changesByActor[change.actor].push(change)\n  }\n\n  let opsById = {}\n  for (let op of ops) {\n    if (op.action === 'del') throw new RangeError('document should not contain del operations')\n    op.pred = opsById[op.id] ? opsById[op.id].pred : []\n    opsById[op.id] = op\n    for (let succ of op.succ) {\n      if (!opsById[succ]) {\n        if (op.elemId) {\n          const elemId = op.insert ? op.id : op.elemId\n          opsById[succ] = {id: succ, action: 'del', obj: op.obj, elemId, pred: []}\n        } else {\n          opsById[succ] = {id: succ, action: 'del', obj: op.obj, key: op.key, pred: []}\n        }\n      }\n      opsById[succ].pred.push(op.id)\n    }\n    delete op.succ\n  }\n  for (let op of Object.values(opsById)) {\n    if (op.action === 'del') ops.push(op)\n  }\n\n  for (let op of ops) {\n    const { counter, actorId } = parseOpId(op.id)\n    const actorChanges = changesByActor[actorId]\n    // Binary search to find the change that should contain this operation\n    let left = 0, right = actorChanges.length\n    while (left < right) {\n      const index = Math.floor((left + right) / 2)\n      if (actorChanges[index].maxOp < counter) {\n        left = index + 1\n      } else {\n        right = index\n      }\n    }\n    if (left >= actorChanges.length) {\n      throw new RangeError(`Operation ID ${op.id} outside of allowed range`)\n    }\n    actorChanges[left].ops.push(op)\n  }\n\n  for (let change of changes) {\n    change.ops.sort((op1, op2) => sortOpIds(op1.id, op2.id))\n    change.startOp = change.maxOp - change.ops.length + 1\n    delete change.maxOp\n    for (let i = 0; i < change.ops.length; i++) {\n      const op = change.ops[i], expectedId = `${change.startOp + i}@${change.actor}`\n      if (op.id !== expectedId) {\n        throw new RangeError(`Expected opId ${expectedId}, got ${op.id}`)\n      }\n      delete op.id\n    }\n  }\n}\n\nfunction decodeDocumentChanges(changes, expectedHeads) {\n  let heads = {} // change hashes that are not a dependency of any other change\n  for (let i = 0; i < changes.length; i++) {\n    let change = changes[i]\n    change.deps = []\n    for (let index of change.depsNum.map(d => d.depsIndex)) {\n      if (!changes[index] || !changes[index].hash) {\n        throw new RangeError(`No hash for index ${index} while processing index ${i}`)\n      }\n      const hash = changes[index].hash\n      change.deps.push(hash)\n      if (heads[hash]) delete heads[hash]\n    }\n    change.deps.sort()\n    delete change.depsNum\n\n    if (change.extraLen_datatype !== VALUE_TYPE.BYTES) {\n      throw new RangeError(`Bad datatype for extra bytes: ${VALUE_TYPE.BYTES}`)\n    }\n    change.extraBytes = change.extraLen\n    delete change.extraLen_datatype\n\n    // Encoding and decoding again to compute the hash of the change\n    changes[i] = decodeChange(encodeChange(change))\n    heads[changes[i].hash] = true\n  }\n\n  const actualHeads = Object.keys(heads).sort()\n  let headsEqual = (actualHeads.length === expectedHeads.length), i = 0\n  while (headsEqual && i < actualHeads.length) {\n    headsEqual = (actualHeads[i] === expectedHeads[i])\n    i++\n  }\n  if (!headsEqual) {\n    throw new RangeError(`Mismatched heads hashes: expected ${expectedHeads.join(', ')}, got ${actualHeads.join(', ')}`)\n  }\n}\n\nfunction encodeDocumentHeader(doc) {\n  const { changesColumns, opsColumns, actorIds, heads, headsIndexes, extraBytes } = doc\n  for (let column of changesColumns) deflateColumn(column)\n  for (let column of opsColumns) deflateColumn(column)\n\n  return encodeContainer(CHUNK_TYPE_DOCUMENT, encoder => {\n    encoder.appendUint53(actorIds.length)\n    for (let actor of actorIds) {\n      encoder.appendHexString(actor)\n    }\n    encoder.appendUint53(heads.length)\n    for (let head of heads.sort()) {\n      encoder.appendRawBytes(hexStringToBytes(head))\n    }\n    encodeColumnInfo(encoder, changesColumns)\n    encodeColumnInfo(encoder, opsColumns)\n    for (let column of changesColumns) encoder.appendRawBytes(column.encoder.buffer)\n    for (let column of opsColumns) encoder.appendRawBytes(column.encoder.buffer)\n    for (let index of headsIndexes) encoder.appendUint53(index)\n    if (extraBytes) encoder.appendRawBytes(extraBytes)\n  }).bytes\n}\n\nfunction decodeDocumentHeader(buffer) {\n  const documentDecoder = new Decoder(buffer)\n  const header = decodeContainerHeader(documentDecoder, true)\n  const decoder = new Decoder(header.chunkData)\n  if (!documentDecoder.done) throw new RangeError('Encoded document has trailing data')\n  if (header.chunkType !== CHUNK_TYPE_DOCUMENT) throw new RangeError(`Unexpected chunk type: ${header.chunkType}`)\n\n  const actorIds = [], numActors = decoder.readUint53()\n  for (let i = 0; i < numActors; i++) {\n    actorIds.push(decoder.readHexString())\n  }\n  const heads = [], headsIndexes = [], numHeads = decoder.readUint53()\n  for (let i = 0; i < numHeads; i++) {\n    heads.push(bytesToHexString(decoder.readRawBytes(32)))\n  }\n\n  const changesColumns = decodeColumnInfo(decoder)\n  const opsColumns = decodeColumnInfo(decoder)\n  for (let i = 0; i < changesColumns.length; i++) {\n    changesColumns[i].buffer = decoder.readRawBytes(changesColumns[i].bufferLen)\n    inflateColumn(changesColumns[i])\n  }\n  for (let i = 0; i < opsColumns.length; i++) {\n    opsColumns[i].buffer = decoder.readRawBytes(opsColumns[i].bufferLen)\n    inflateColumn(opsColumns[i])\n  }\n  if (!decoder.done) {\n    for (let i = 0; i < numHeads; i++) headsIndexes.push(decoder.readUint53())\n  }\n\n  const extraBytes = decoder.readRawBytes(decoder.buf.byteLength - decoder.offset)\n  return { changesColumns, opsColumns, actorIds, heads, headsIndexes, extraBytes }\n}\n\nfunction decodeDocument(buffer) {\n  const { changesColumns, opsColumns, actorIds, heads } = decodeDocumentHeader(buffer)\n  const changes = decodeColumns(changesColumns, actorIds, DOCUMENT_COLUMNS)\n  const ops = decodeOps(decodeColumns(opsColumns, actorIds, DOC_OPS_COLUMNS), true)\n  groupChangeOps(changes, ops)\n  decodeDocumentChanges(changes, heads)\n  return changes\n}\n\n/**\n * DEFLATE-compresses the given column if it is large enough to make the compression worthwhile.\n */\nfunction deflateColumn(column) {\n  if (column.encoder.buffer.byteLength >= DEFLATE_MIN_SIZE) {\n    column.encoder = {buffer: pako.deflateRaw(column.encoder.buffer)}\n    column.columnId |= COLUMN_TYPE_DEFLATE\n  }\n}\n\n/**\n * Decompresses the given column if it is DEFLATE-compressed.\n */\nfunction inflateColumn(column) {\n  if ((column.columnId & COLUMN_TYPE_DEFLATE) !== 0) {\n    column.buffer = pako.inflateRaw(column.buffer)\n    column.columnId ^= COLUMN_TYPE_DEFLATE\n  }\n}\n\nmodule.exports = {\n  COLUMN_TYPE, VALUE_TYPE, ACTIONS, OBJECT_TYPE, DOC_OPS_COLUMNS, CHANGE_COLUMNS, DOCUMENT_COLUMNS,\n  encoderByColumnId, decoderByColumnId, makeDecoders, decodeValue,\n  splitContainers, encodeChange, decodeChangeColumns, decodeChange, decodeChangeMeta, decodeChanges,\n  encodeDocumentHeader, decodeDocumentHeader, decodeDocument\n}\n"
  },
  {
    "path": "backend/encoding.js",
    "content": "/**\n * UTF-8 decoding and encoding using API that is supported in Node >= 12 and modern browsers:\n * https://developer.mozilla.org/en-US/docs/Web/API/TextEncoder/encode\n * https://developer.mozilla.org/en-US/docs/Web/API/TextDecoder/decode\n * If you're running in an environment where it's not available, please use a polyfill, such as:\n * https://github.com/anonyco/FastestSmallestTextEncoderDecoder\n */\nconst utf8encoder = new TextEncoder()\nconst utf8decoder = new TextDecoder('utf-8')\n\nfunction stringToUtf8(string) {\n  return utf8encoder.encode(string)\n}\n\nfunction utf8ToString(buffer) {\n  return utf8decoder.decode(buffer)\n}\n\n/**\n * Converts a string consisting of hexadecimal digits into an Uint8Array.\n */\nfunction hexStringToBytes(value) {\n  if (typeof value !== 'string') {\n    throw new TypeError('value is not a string')\n  }\n  if (!/^([0-9a-f][0-9a-f])*$/.test(value)) {\n    throw new RangeError('value is not hexadecimal')\n  }\n  if (value === '') {\n    return new Uint8Array(0)\n  } else {\n    return new Uint8Array(value.match(/../g).map(b => parseInt(b, 16)))\n  }\n}\n\nconst NIBBLE_TO_HEX = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f']\nconst BYTE_TO_HEX = new Array(256)\nfor (let i = 0; i < 256; i++) {\n  BYTE_TO_HEX[i] = `${NIBBLE_TO_HEX[(i >>> 4) & 0xf]}${NIBBLE_TO_HEX[i & 0xf]}`;\n}\n\n/**\n * Converts a Uint8Array into the equivalent hexadecimal string.\n */\nfunction bytesToHexString(bytes) {\n  let hex = '', len = bytes.byteLength\n  for (let i = 0; i < len; i++) {\n    hex += BYTE_TO_HEX[bytes[i]]\n  }\n  return hex\n}\n\n/**\n * Wrapper around an Uint8Array that allows values to be appended to the buffer,\n * and that automatically grows the buffer when space runs out.\n */\nclass Encoder {\n  constructor() {\n    this.buf = new Uint8Array(16)\n    this.offset = 0\n  }\n\n  /**\n   * Returns the byte array containing the encoded data.\n   */\n  get buffer() {\n    this.finish()\n    return this.buf.subarray(0, this.offset)\n  }\n\n  /**\n   * Reallocates the encoder's buffer to be bigger.\n   */\n  grow(minSize = 0) {\n    let newSize = this.buf.byteLength * 4\n    while (newSize < minSize) newSize *= 2\n    const newBuf = new Uint8Array(newSize)\n    newBuf.set(this.buf, 0)\n    this.buf = newBuf\n    return this\n  }\n\n  /**\n   * Appends one byte (0 to 255) to the buffer.\n   */\n  appendByte(value) {\n    if (this.offset >= this.buf.byteLength) this.grow()\n    this.buf[this.offset] = value\n    this.offset += 1\n  }\n\n  /**\n   * Encodes a 32-bit nonnegative integer in a variable number of bytes using\n   * the LEB128 encoding scheme (https://en.wikipedia.org/wiki/LEB128) and\n   * appends it to the buffer. Returns the number of bytes written.\n   */\n  appendUint32(value) {\n    if (!Number.isInteger(value)) throw new RangeError('value is not an integer')\n    if (value < 0 || value > 0xffffffff) throw new RangeError('number out of range')\n\n    const numBytes = Math.max(1, Math.ceil((32 - Math.clz32(value)) / 7))\n    if (this.offset + numBytes > this.buf.byteLength) this.grow()\n\n    for (let i = 0; i < numBytes; i++) {\n      this.buf[this.offset + i] = (value & 0x7f) | (i === numBytes - 1 ? 0x00 : 0x80)\n      value >>>= 7 // zero-filling right shift\n    }\n    this.offset += numBytes\n    return numBytes\n  }\n\n  /**\n   * Encodes a 32-bit signed integer in a variable number of bytes using the\n   * LEB128 encoding scheme (https://en.wikipedia.org/wiki/LEB128) and appends\n   * it to the buffer. Returns the number of bytes written.\n   */\n  appendInt32(value) {\n    if (!Number.isInteger(value)) throw new RangeError('value is not an integer')\n    if (value < -0x80000000 || value > 0x7fffffff) throw new RangeError('number out of range')\n\n    const numBytes = Math.ceil((33 - Math.clz32(value >= 0 ? value : -value - 1)) / 7)\n    if (this.offset + numBytes > this.buf.byteLength) this.grow()\n\n    for (let i = 0; i < numBytes; i++) {\n      this.buf[this.offset + i] = (value & 0x7f) | (i === numBytes - 1 ? 0x00 : 0x80)\n      value >>= 7 // sign-propagating right shift\n    }\n    this.offset += numBytes\n    return numBytes\n  }\n\n  /**\n   * Encodes a nonnegative integer in a variable number of bytes using the LEB128\n   * encoding scheme, up to the maximum size of integers supported by JavaScript\n   * (53 bits).\n   */\n  appendUint53(value) {\n    if (!Number.isInteger(value)) throw new RangeError('value is not an integer')\n    if (value < 0 || value > Number.MAX_SAFE_INTEGER) {\n      throw new RangeError('number out of range')\n    }\n    const high32 = Math.floor(value / 0x100000000)\n    const low32 = (value & 0xffffffff) >>> 0 // right shift to interpret as unsigned\n    return this.appendUint64(high32, low32)\n  }\n\n  /**\n   * Encodes a signed integer in a variable number of bytes using the LEB128\n   * encoding scheme, up to the maximum size of integers supported by JavaScript\n   * (53 bits).\n   */\n  appendInt53(value) {\n    if (!Number.isInteger(value)) throw new RangeError('value is not an integer')\n    if (value < Number.MIN_SAFE_INTEGER || value > Number.MAX_SAFE_INTEGER) {\n      throw new RangeError('number out of range')\n    }\n    const high32 = Math.floor(value / 0x100000000)\n    const low32 = (value & 0xffffffff) >>> 0 // right shift to interpret as unsigned\n    return this.appendInt64(high32, low32)\n  }\n\n  /**\n   * Encodes a 64-bit nonnegative integer in a variable number of bytes using\n   * the LEB128 encoding scheme, and appends it to the buffer. The number is\n   * given as two 32-bit halves since JavaScript cannot accurately represent\n   * integers with more than 53 bits in a single variable.\n   */\n  appendUint64(high32, low32) {\n    if (!Number.isInteger(high32) || !Number.isInteger(low32)) {\n      throw new RangeError('value is not an integer')\n    }\n    if (high32 < 0 || high32 > 0xffffffff || low32 < 0 || low32 > 0xffffffff) {\n      throw new RangeError('number out of range')\n    }\n    if (high32 === 0) return this.appendUint32(low32)\n\n    const numBytes = Math.ceil((64 - Math.clz32(high32)) / 7)\n    if (this.offset + numBytes > this.buf.byteLength) this.grow()\n    for (let i = 0; i < 4; i++) {\n      this.buf[this.offset + i] = (low32 & 0x7f) | 0x80\n      low32 >>>= 7 // zero-filling right shift\n    }\n    this.buf[this.offset + 4] = (low32 & 0x0f) | ((high32 & 0x07) << 4) | (numBytes === 5 ? 0x00 : 0x80)\n    high32 >>>= 3\n    for (let i = 5; i < numBytes; i++) {\n      this.buf[this.offset + i] = (high32 & 0x7f) | (i === numBytes - 1 ? 0x00 : 0x80)\n      high32 >>>= 7\n    }\n    this.offset += numBytes\n    return numBytes\n  }\n\n  /**\n   * Encodes a 64-bit signed integer in a variable number of bytes using the\n   * LEB128 encoding scheme, and appends it to the buffer. The number is given\n   * as two 32-bit halves since JavaScript cannot accurately represent integers\n   * with more than 53 bits in a single variable. The sign of the 64-bit\n   * number is determined by the sign of the `high32` half; the sign of the\n   * `low32` half is ignored.\n   */\n  appendInt64(high32, low32) {\n    if (!Number.isInteger(high32) || !Number.isInteger(low32)) {\n      throw new RangeError('value is not an integer')\n    }\n    if (high32 < -0x80000000 || high32 > 0x7fffffff || low32 < -0x80000000 || low32 > 0xffffffff) {\n      throw new RangeError('number out of range')\n    }\n    low32 >>>= 0 // interpret as unsigned\n    if (high32 === 0 && low32 <= 0x7fffffff) return this.appendInt32(low32)\n    if (high32 === -1 && low32 >= 0x80000000) return this.appendInt32(low32 - 0x100000000)\n\n    const numBytes = Math.ceil((65 - Math.clz32(high32 >= 0 ? high32 : -high32 - 1)) / 7)\n    if (this.offset + numBytes > this.buf.byteLength) this.grow()\n    for (let i = 0; i < 4; i++) {\n      this.buf[this.offset + i] = (low32 & 0x7f) | 0x80\n      low32 >>>= 7 // zero-filling right shift\n    }\n    this.buf[this.offset + 4] = (low32 & 0x0f) | ((high32 & 0x07) << 4) | (numBytes === 5 ? 0x00 : 0x80)\n    high32 >>= 3 // sign-propagating right shift\n    for (let i = 5; i < numBytes; i++) {\n      this.buf[this.offset + i] = (high32 & 0x7f) | (i === numBytes - 1 ? 0x00 : 0x80)\n      high32 >>= 7\n    }\n    this.offset += numBytes\n    return numBytes\n  }\n\n  /**\n   * Appends the contents of byte buffer `data` to the buffer. Returns the\n   * number of bytes appended.\n   */\n  appendRawBytes(data) {\n    if (this.offset + data.byteLength > this.buf.byteLength) {\n      this.grow(this.offset + data.byteLength)\n    }\n    this.buf.set(data, this.offset)\n    this.offset += data.byteLength\n    return data.byteLength\n  }\n\n  /**\n   * Appends a UTF-8 string to the buffer, without any metadata. Returns the\n   * number of bytes appended.\n   */\n  appendRawString(value) {\n    if (typeof value !== 'string') throw new TypeError('value is not a string')\n    return this.appendRawBytes(stringToUtf8(value))\n  }\n\n  /**\n   * Appends the contents of byte buffer `data` to the buffer, prefixed with the\n   * number of bytes in the buffer (as a LEB128-encoded unsigned integer).\n   */\n  appendPrefixedBytes(data) {\n    this.appendUint53(data.byteLength)\n    this.appendRawBytes(data)\n    return this\n  }\n\n  /**\n   * Appends a UTF-8 string to the buffer, prefixed with its length in bytes\n   * (where the length is encoded as an unsigned LEB128 integer).\n   */\n  appendPrefixedString(value) {\n    if (typeof value !== 'string') throw new TypeError('value is not a string')\n    this.appendPrefixedBytes(stringToUtf8(value))\n    return this\n  }\n\n  /**\n   * Takes a value, which must be a string consisting only of hexadecimal\n   * digits, maps it to a byte array, and appends it to the buffer, prefixed\n   * with its length in bytes.\n   */\n  appendHexString(value) {\n    this.appendPrefixedBytes(hexStringToBytes(value))\n    return this\n  }\n\n  /**\n   * Flushes any unwritten data to the buffer. Call this before reading from\n   * the buffer constructed by this Encoder.\n   */\n  finish() {\n  }\n}\n\n/**\n * Counterpart to Encoder. Wraps a Uint8Array buffer with a cursor indicating\n * the current decoding position, and allows values to be incrementally read by\n * decoding the bytes at the current position.\n */\nclass Decoder {\n  constructor(buffer) {\n    if (!(buffer instanceof Uint8Array)) {\n      throw new TypeError(`Not a byte array: ${buffer}`)\n    }\n    this.buf = buffer\n    this.offset = 0\n  }\n\n  /**\n   * Returns false if there is still data to be read at the current decoding\n   * position, and true if we are at the end of the buffer.\n   */\n  get done() {\n    return this.offset === this.buf.byteLength\n  }\n\n  /**\n   * Resets the cursor position, so that the next read goes back to the\n   * beginning of the buffer.\n   */\n  reset() {\n    this.offset = 0\n  }\n\n  /**\n   * Moves the current decoding position forward by the specified number of\n   * bytes, without decoding anything.\n   */\n  skip(bytes) {\n    if (this.offset + bytes > this.buf.byteLength) {\n      throw new RangeError('cannot skip beyond end of buffer')\n    }\n    this.offset += bytes\n  }\n\n  /**\n   * Reads one byte (0 to 255) from the buffer.\n   */\n  readByte() {\n    this.offset += 1\n    return this.buf[this.offset - 1]\n  }\n\n  /**\n   * Reads a LEB128-encoded unsigned integer from the current position in the buffer.\n   * Throws an exception if the value doesn't fit in a 32-bit unsigned int.\n   */\n  readUint32() {\n    let result = 0, shift = 0\n    while (this.offset < this.buf.byteLength) {\n      const nextByte = this.buf[this.offset]\n      if (shift === 28 && (nextByte & 0xf0) !== 0) { // more than 5 bytes, or value > 0xffffffff\n        throw new RangeError('number out of range')\n      }\n      result = (result | (nextByte & 0x7f) << shift) >>> 0 // right shift to interpret value as unsigned\n      shift += 7\n      this.offset++\n      if ((nextByte & 0x80) === 0) return result\n    }\n    throw new RangeError('buffer ended with incomplete number')\n  }\n\n  /**\n   * Reads a LEB128-encoded signed integer from the current position in the buffer.\n   * Throws an exception if the value doesn't fit in a 32-bit signed int.\n   */\n  readInt32() {\n    let result = 0, shift = 0\n    while (this.offset < this.buf.byteLength) {\n      const nextByte = this.buf[this.offset]\n      if ((shift === 28 && (nextByte & 0x80) !== 0) || // more than 5 bytes\n          (shift === 28 && (nextByte & 0x40) === 0 && (nextByte & 0x38) !== 0) || // positive int > 0x7fffffff\n          (shift === 28 && (nextByte & 0x40) !== 0 && (nextByte & 0x38) !== 0x38)) { // negative int < -0x80000000\n        throw new RangeError('number out of range')\n      }\n      result |= (nextByte & 0x7f) << shift\n      shift += 7\n      this.offset++\n\n      if ((nextByte & 0x80) === 0) {\n        if ((nextByte & 0x40) === 0 || shift > 28) {\n          return result // positive, or negative value that doesn't need sign-extending\n        } else {\n          return result | (-1 << shift) // sign-extend negative integer\n        }\n      }\n    }\n    throw new RangeError('buffer ended with incomplete number')\n  }\n\n  /**\n   * Reads a LEB128-encoded unsigned integer from the current position in the\n   * buffer. Allows any integer that can be safely represented by JavaScript\n   * (up to 2^53 - 1), and throws an exception outside of that range.\n   */\n  readUint53() {\n    const { low32, high32 } = this.readUint64()\n    if (high32 < 0 || high32 > 0x1fffff) {\n      throw new RangeError('number out of range')\n    }\n    return high32 * 0x100000000 + low32\n  }\n\n  /**\n   * Reads a LEB128-encoded signed integer from the current position in the\n   * buffer. Allows any integer that can be safely represented by JavaScript\n   * (between -(2^53 - 1) and 2^53 - 1), throws an exception outside of that range.\n   */\n  readInt53() {\n    const { low32, high32 } = this.readInt64()\n    if (high32 < -0x200000 || (high32 === -0x200000 && low32 === 0) || high32 > 0x1fffff) {\n      throw new RangeError('number out of range')\n    }\n    return high32 * 0x100000000 + low32\n  }\n\n  /**\n   * Reads a LEB128-encoded unsigned integer from the current position in the\n   * buffer. Throws an exception if the value doesn't fit in a 64-bit unsigned\n   * int. Returns the number in two 32-bit halves, as an object of the form\n   * `{high32, low32}`.\n   */\n  readUint64() {\n    let low32 = 0, high32 = 0, shift = 0\n    while (this.offset < this.buf.byteLength && shift <= 28) {\n      const nextByte = this.buf[this.offset]\n      low32 = (low32 | (nextByte & 0x7f) << shift) >>> 0 // right shift to interpret value as unsigned\n      if (shift === 28) {\n        high32 = (nextByte & 0x70) >>> 4\n      }\n      shift += 7\n      this.offset++\n      if ((nextByte & 0x80) === 0) return { high32, low32 }\n    }\n\n    shift = 3\n    while (this.offset < this.buf.byteLength) {\n      const nextByte = this.buf[this.offset]\n      if (shift === 31 && (nextByte & 0xfe) !== 0) { // more than 10 bytes, or value > 2^64 - 1\n        throw new RangeError('number out of range')\n      }\n      high32 = (high32 | (nextByte & 0x7f) << shift) >>> 0\n      shift += 7\n      this.offset++\n      if ((nextByte & 0x80) === 0) return { high32, low32 }\n    }\n    throw new RangeError('buffer ended with incomplete number')\n  }\n\n  /**\n   * Reads a LEB128-encoded signed integer from the current position in the\n   * buffer. Throws an exception if the value doesn't fit in a 64-bit signed\n   * int. Returns the number in two 32-bit halves, as an object of the form\n   * `{high32, low32}`. The `low32` half is always non-negative, and the\n   * sign of the `high32` half indicates the sign of the 64-bit number.\n   */\n  readInt64() {\n    let low32 = 0, high32 = 0, shift = 0\n    while (this.offset < this.buf.byteLength && shift <= 28) {\n      const nextByte = this.buf[this.offset]\n      low32 = (low32 | (nextByte & 0x7f) << shift) >>> 0 // right shift to interpret value as unsigned\n      if (shift === 28) {\n        high32 = (nextByte & 0x70) >>> 4\n      }\n      shift += 7\n      this.offset++\n      if ((nextByte & 0x80) === 0) {\n        if ((nextByte & 0x40) !== 0) { // sign-extend negative integer\n          if (shift < 32) low32 = (low32 | (-1 << shift)) >>> 0\n          high32 |= -1 << Math.max(shift - 32, 0)\n        }\n        return { high32, low32 }\n      }\n    }\n\n    shift = 3\n    while (this.offset < this.buf.byteLength) {\n      const nextByte = this.buf[this.offset]\n      // On the 10th byte there are only two valid values: all 7 value bits zero\n      // (if the value is positive) or all 7 bits one (if the value is negative)\n      if (shift === 31 && nextByte !== 0 && nextByte !== 0x7f) {\n        throw new RangeError('number out of range')\n      }\n      high32 |= (nextByte & 0x7f) << shift\n      shift += 7\n      this.offset++\n      if ((nextByte & 0x80) === 0) {\n        if ((nextByte & 0x40) !== 0 && shift < 32) { // sign-extend negative integer\n          high32 |= -1 << shift\n        }\n        return { high32, low32 }\n      }\n    }\n    throw new RangeError('buffer ended with incomplete number')\n  }\n\n  /**\n   * Extracts a subarray `length` bytes in size, starting from the current\n   * position in the buffer, and moves the position forward.\n   */\n  readRawBytes(length) {\n    const start = this.offset\n    if (start + length > this.buf.byteLength) {\n      throw new RangeError('subarray exceeds buffer size')\n    }\n    this.offset += length\n    return this.buf.subarray(start, this.offset)\n  }\n\n  /**\n   * Extracts `length` bytes from the buffer, starting from the current position,\n   * and returns the UTF-8 string decoding of those bytes.\n   */\n  readRawString(length) {\n    return utf8ToString(this.readRawBytes(length))\n  }\n\n  /**\n   * Extracts a subarray from the current position in the buffer, prefixed with\n   * its length in bytes (encoded as an unsigned LEB128 integer).\n   */\n  readPrefixedBytes() {\n    return this.readRawBytes(this.readUint53())\n  }\n\n  /**\n   * Reads a UTF-8 string from the current position in the buffer, prefixed with its\n   * length in bytes (where the length is encoded as an unsigned LEB128 integer).\n   */\n  readPrefixedString() {\n    return utf8ToString(this.readPrefixedBytes())\n  }\n\n  /**\n   * Reads a byte array from the current position in the buffer, prefixed with its\n   * length in bytes. Returns that byte array converted to a hexadecimal string.\n   */\n  readHexString() {\n    return bytesToHexString(this.readPrefixedBytes())\n  }\n}\n\n/**\n * An encoder that uses run-length encoding to compress sequences of repeated\n * values. The constructor argument specifies the type of values, which may be\n * either 'int', 'uint', or 'utf8'. Besides valid values of the selected\n * datatype, values may also be null.\n *\n * The encoded buffer starts with a LEB128-encoded signed integer, the\n * repetition count. The interpretation of the following values depends on this\n * repetition count:\n *   - If this number is a positive value n, the next value in the buffer\n *     (encoded as the specified datatype) is repeated n times in the sequence.\n *   - If the repetition count is a negative value -n, then the next n values\n *     (encoded as the specified datatype) in the buffer are treated as a\n *     literal, i.e. they appear in the sequence without any further\n *     interpretation or repetition.\n *   - If the repetition count is zero, then the next value in the buffer is a\n *     LEB128-encoded unsigned integer indicating the number of null values\n *     that appear at the current position in the sequence.\n *\n * After one of these three has completed, the process repeats, starting again\n * with a repetition count, until we reach the end of the buffer.\n */\nclass RLEEncoder extends Encoder {\n  constructor(type) {\n    super()\n    this.type = type\n    this.state = 'empty'\n    this.lastValue = undefined\n    this.count = 0\n    this.literal = []\n  }\n\n  /**\n   * Appends a new value to the sequence. If `repetitions` is given, the value is repeated\n   * `repetitions` times.\n   */\n  appendValue(value, repetitions = 1) {\n    this._appendValue(value, repetitions)\n  }\n\n  /**\n   * Like `appendValue()`, but this method is not overridden by `DeltaEncoder`.\n   */\n  _appendValue(value, repetitions = 1) {\n    if (repetitions <= 0) return\n    if (this.state === 'empty') {\n      this.state = (value === null ? 'nulls' : (repetitions === 1 ? 'loneValue' : 'repetition'))\n      this.lastValue = value\n      this.count = repetitions\n    } else if (this.state === 'loneValue') {\n      if (value === null) {\n        this.flush()\n        this.state = 'nulls'\n        this.count = repetitions\n      } else if (value === this.lastValue) {\n        this.state = 'repetition'\n        this.count = 1 + repetitions\n      } else if (repetitions > 1) {\n        this.flush()\n        this.state = 'repetition'\n        this.count = repetitions\n        this.lastValue = value\n      } else {\n        this.state = 'literal'\n        this.literal = [this.lastValue]\n        this.lastValue = value\n      }\n    } else if (this.state === 'repetition') {\n      if (value === null) {\n        this.flush()\n        this.state = 'nulls'\n        this.count = repetitions\n      } else if (value === this.lastValue) {\n        this.count += repetitions\n      } else if (repetitions > 1) {\n        this.flush()\n        this.state = 'repetition'\n        this.count = repetitions\n        this.lastValue = value\n      } else {\n        this.flush()\n        this.state = 'loneValue'\n        this.lastValue = value\n      }\n    } else if (this.state === 'literal') {\n      if (value === null) {\n        this.literal.push(this.lastValue)\n        this.flush()\n        this.state = 'nulls'\n        this.count = repetitions\n      } else if (value === this.lastValue) {\n        this.flush()\n        this.state = 'repetition'\n        this.count = 1 + repetitions\n      } else if (repetitions > 1) {\n        this.literal.push(this.lastValue)\n        this.flush()\n        this.state = 'repetition'\n        this.count = repetitions\n        this.lastValue = value\n      } else {\n        this.literal.push(this.lastValue)\n        this.lastValue = value\n      }\n    } else if (this.state === 'nulls') {\n      if (value === null) {\n        this.count += repetitions\n      } else if (repetitions > 1) {\n        this.flush()\n        this.state = 'repetition'\n        this.count = repetitions\n        this.lastValue = value\n      } else {\n        this.flush()\n        this.state = 'loneValue'\n        this.lastValue = value\n      }\n    }\n  }\n\n  /**\n   * Copies values from the RLEDecoder `decoder` into this encoder. The `options` object may\n   * contain the following keys:\n   *  - `count`: The number of values to copy. If not specified, copies all remaining values.\n   *  - `sumValues`: If true, the function computes the sum of all numeric values as they are\n   *    copied (null values are counted as zero), and returns that number.\n   *  - `sumShift`: If set, values are shifted right by `sumShift` bits before adding to the sum.\n   *\n   * Returns an object of the form `{nonNullValues, sum}` where `nonNullValues` is the number of\n   * non-null values copied, and `sum` is the sum (only if the `sumValues` option is set).\n   */\n  copyFrom(decoder, options = {}) {\n    const { count, sumValues, sumShift } = options\n    if (!(decoder instanceof RLEDecoder) || (decoder.type !== this.type)) {\n      throw new TypeError('incompatible type of decoder')\n    }\n    let remaining = (typeof count === 'number' ? count : Number.MAX_SAFE_INTEGER)\n    let nonNullValues = 0, sum = 0\n    if (count && remaining > 0 && decoder.done) throw new RangeError(`cannot copy ${count} values`)\n    if (remaining === 0 || decoder.done) return sumValues ? {nonNullValues, sum} : {nonNullValues}\n\n    // Copy a value so that we have a well-defined starting state. NB: when super.copyFrom() is\n    // called by the DeltaEncoder subclass, the following calls to readValue() and appendValue()\n    // refer to the overridden methods, while later readRecord(), readRawValue() and _appendValue()\n    // calls refer to the non-overridden RLEDecoder/RLEEncoder methods.\n    let firstValue = decoder.readValue()\n    if (firstValue === null) {\n      const numNulls = Math.min(decoder.count + 1, remaining)\n      remaining -= numNulls\n      decoder.count -= numNulls - 1\n      this.appendValue(null, numNulls)\n      if (count && remaining > 0 && decoder.done) throw new RangeError(`cannot copy ${count} values`)\n      if (remaining === 0 || decoder.done) return sumValues ? {nonNullValues, sum} : {nonNullValues}\n      firstValue = decoder.readValue()\n      if (firstValue === null) throw new RangeError('null run must be followed by non-null value')\n    }\n    this.appendValue(firstValue)\n    remaining--\n    nonNullValues++\n    if (sumValues) sum += (sumShift ? (firstValue >>> sumShift) : firstValue)\n    if (count && remaining > 0 && decoder.done) throw new RangeError(`cannot copy ${count} values`)\n    if (remaining === 0 || decoder.done) return sumValues ? {nonNullValues, sum} : {nonNullValues}\n\n    // Copy data at the record level without expanding repetitions\n    let firstRun = (decoder.count > 0)\n    while (remaining > 0 && !decoder.done) {\n      if (!firstRun) decoder.readRecord()\n      const numValues = Math.min(decoder.count, remaining)\n      decoder.count -= numValues\n\n      if (decoder.state === 'literal') {\n        nonNullValues += numValues\n        for (let i = 0; i < numValues; i++) {\n          if (decoder.done) throw new RangeError('incomplete literal')\n          const value = decoder.readRawValue()\n          if (value === decoder.lastValue) throw new RangeError('Repetition of values is not allowed in literal')\n          decoder.lastValue = value\n          this._appendValue(value)\n          if (sumValues) sum += (sumShift ? (value >>> sumShift) : value)\n        }\n      } else if (decoder.state === 'repetition') {\n        nonNullValues += numValues\n        if (sumValues) sum += numValues * (sumShift ? (decoder.lastValue >>> sumShift) : decoder.lastValue)\n        const value = decoder.lastValue\n        this._appendValue(value)\n        if (numValues > 1) {\n          this._appendValue(value)\n          if (this.state !== 'repetition') throw new RangeError(`Unexpected state ${this.state}`)\n          this.count += numValues - 2\n        }\n      } else if (decoder.state === 'nulls') {\n        this._appendValue(null)\n        if (this.state !== 'nulls') throw new RangeError(`Unexpected state ${this.state}`)\n        this.count += numValues - 1\n      }\n\n      firstRun = false\n      remaining -= numValues\n    }\n    if (count && remaining > 0 && decoder.done) throw new RangeError(`cannot copy ${count} values`)\n    return sumValues ? {nonNullValues, sum} : {nonNullValues}\n  }\n\n  /**\n   * Private method, do not call from outside the class.\n   */\n  flush() {\n    if (this.state === 'loneValue') {\n      this.appendInt32(-1)\n      this.appendRawValue(this.lastValue)\n    } else if (this.state === 'repetition') {\n      this.appendInt53(this.count)\n      this.appendRawValue(this.lastValue)\n    } else if (this.state === 'literal') {\n      this.appendInt53(-this.literal.length)\n      for (let v of this.literal) this.appendRawValue(v)\n    } else if (this.state === 'nulls') {\n      this.appendInt32(0)\n      this.appendUint53(this.count)\n    }\n    this.state = 'empty'\n  }\n\n  /**\n   * Private method, do not call from outside the class.\n   */\n  appendRawValue(value) {\n    if (this.type === 'int') {\n      this.appendInt53(value)\n    } else if (this.type === 'uint') {\n      this.appendUint53(value)\n    } else if (this.type === 'utf8') {\n      this.appendPrefixedString(value)\n    } else {\n      throw new RangeError(`Unknown RLEEncoder datatype: ${this.type}`)\n    }\n  }\n\n  /**\n   * Flushes any unwritten data to the buffer. Call this before reading from\n   * the buffer constructed by this Encoder.\n   */\n  finish() {\n    if (this.state === 'literal') this.literal.push(this.lastValue)\n    // Don't write anything if the only values we have seen are nulls\n    if (this.state !== 'nulls' || this.offset > 0) this.flush()\n  }\n}\n\n/**\n * Counterpart to RLEEncoder: reads values from an RLE-compressed sequence,\n * returning nulls and repeated values as required.\n */\nclass RLEDecoder extends Decoder {\n  constructor(type, buffer) {\n    super(buffer)\n    this.type = type\n    this.lastValue = undefined\n    this.count = 0\n    this.state = undefined\n  }\n\n  /**\n   * Returns false if there is still data to be read at the current decoding\n   * position, and true if we are at the end of the buffer.\n   */\n  get done() {\n    return (this.count === 0) && (this.offset === this.buf.byteLength)\n  }\n\n  /**\n   * Resets the cursor position, so that the next read goes back to the\n   * beginning of the buffer.\n   */\n  reset() {\n    this.offset = 0\n    this.lastValue = undefined\n    this.count = 0\n    this.state = undefined\n  }\n\n  /**\n   * Returns the next value (or null) in the sequence.\n   */\n  readValue() {\n    if (this.done) return null\n    if (this.count === 0) this.readRecord()\n    this.count -= 1\n    if (this.state === 'literal') {\n      const value = this.readRawValue()\n      if (value === this.lastValue) throw new RangeError('Repetition of values is not allowed in literal')\n      this.lastValue = value\n      return value\n    } else {\n      return this.lastValue\n    }\n  }\n\n  /**\n   * Discards the next `numSkip` values in the sequence.\n   */\n  skipValues(numSkip) {\n    while (numSkip > 0 && !this.done) {\n      if (this.count === 0) {\n        this.count = this.readInt53()\n        if (this.count > 0) {\n          this.lastValue = (this.count <= numSkip) ? this.skipRawValues(1) : this.readRawValue()\n          this.state = 'repetition'\n        } else if (this.count < 0) {\n          this.count = -this.count\n          this.state = 'literal'\n        } else { // this.count == 0\n          this.count = this.readUint53()\n          this.lastValue = null\n          this.state = 'nulls'\n        }\n      }\n\n      const consume = Math.min(numSkip, this.count)\n      if (this.state === 'literal') this.skipRawValues(consume)\n      numSkip -= consume\n      this.count -= consume\n    }\n  }\n\n  /**\n   * Private method, do not call from outside the class.\n   * Reads a repetition count from the buffer and sets up the state appropriately.\n   */\n  readRecord() {\n    this.count = this.readInt53()\n    if (this.count > 1) {\n      const value = this.readRawValue()\n      if ((this.state === 'repetition' || this.state === 'literal') && this.lastValue === value) {\n        throw new RangeError('Successive repetitions with the same value are not allowed')\n      }\n      this.state = 'repetition'\n      this.lastValue = value\n    } else if (this.count === 1) {\n      throw new RangeError('Repetition count of 1 is not allowed, use a literal instead')\n    } else if (this.count < 0) {\n      this.count = -this.count\n      if (this.state === 'literal') throw new RangeError('Successive literals are not allowed')\n      this.state = 'literal'\n    } else { // this.count == 0\n      if (this.state === 'nulls') throw new RangeError('Successive null runs are not allowed')\n      this.count = this.readUint53()\n      if (this.count === 0) throw new RangeError('Zero-length null runs are not allowed')\n      this.lastValue = null\n      this.state = 'nulls'\n    }\n  }\n\n  /**\n   * Private method, do not call from outside the class.\n   * Reads one value of the datatype configured on construction.\n   */\n  readRawValue() {\n    if (this.type === 'int') {\n      return this.readInt53()\n    } else if (this.type === 'uint') {\n      return this.readUint53()\n    } else if (this.type === 'utf8') {\n      return this.readPrefixedString()\n    } else {\n      throw new RangeError(`Unknown RLEDecoder datatype: ${this.type}`)\n    }\n  }\n\n  /**\n   * Private method, do not call from outside the class.\n   * Skips over `num` values of the datatype configured on construction.\n   */\n  skipRawValues(num) {\n    if (this.type === 'utf8') {\n      for (let i = 0; i < num; i++) this.skip(this.readUint53())\n    } else {\n      while (num > 0 && this.offset < this.buf.byteLength) {\n        if ((this.buf[this.offset] & 0x80) === 0) num--\n        this.offset++\n      }\n      if (num > 0) throw new RangeError('cannot skip beyond end of buffer')\n    }\n  }\n}\n\n/**\n * A variant of RLEEncoder: rather than storing the actual values passed to\n * appendValue(), this version stores only the first value, and for all\n * subsequent values it stores the difference to the previous value. This\n * encoding is good when values tend to come in sequentially incrementing runs,\n * because the delta between successive values is 1, and repeated values of 1\n * are easily compressed with run-length encoding.\n *\n * Null values are also allowed, as with RLEEncoder.\n */\nclass DeltaEncoder extends RLEEncoder {\n  constructor() {\n    super('int')\n    this.absoluteValue = 0\n  }\n\n  /**\n   * Appends a new integer value to the sequence. If `repetitions` is given, the value is repeated\n   * `repetitions` times.\n   */\n  appendValue(value, repetitions = 1) {\n    if (repetitions <= 0) return\n    if (typeof value === 'number') {\n      super.appendValue(value - this.absoluteValue, 1)\n      this.absoluteValue = value\n      if (repetitions > 1) super.appendValue(0, repetitions - 1)\n    } else {\n      super.appendValue(value, repetitions)\n    }\n  }\n\n  /**\n   * Copies values from the DeltaDecoder `decoder` into this encoder. The `options` object may\n   * contain the key `count`, indicating the number of values to copy. If not specified, copies\n   * all remaining values in the decoder.\n   */\n  copyFrom(decoder, options = {}) {\n    if (options.sumValues) {\n      throw new RangeError('unsupported options for DeltaEncoder.copyFrom()')\n    }\n    if (!(decoder instanceof DeltaDecoder)) {\n      throw new TypeError('incompatible type of decoder')\n    }\n\n    let remaining = options.count\n    if (remaining > 0 && decoder.done) throw new RangeError(`cannot copy ${remaining} values`)\n    if (remaining === 0 || decoder.done) return\n\n    // Copy any null values, and the first non-null value, so that appendValue() computes the\n    // difference between the encoder's last value and the decoder's first (absolute) value.\n    let value = decoder.readValue(), nulls = 0\n    this.appendValue(value)\n    if (value === null) {\n      nulls = decoder.count + 1\n      if (remaining !== undefined && remaining < nulls) nulls = remaining\n      decoder.count -= nulls - 1\n      this.count += nulls - 1\n      if (remaining > nulls && decoder.done) throw new RangeError(`cannot copy ${remaining} values`)\n      if (remaining === nulls || decoder.done) return\n\n      // The next value read is certain to be non-null because we're not at the end of the decoder,\n      // and a run of nulls must be followed by a run of non-nulls.\n      if (decoder.count === 0) this.appendValue(decoder.readValue())\n    }\n\n    // Once we have the first value, the subsequent relative values can be copied verbatim without\n    // any further processing. Note that the first value copied by super.copyFrom() is an absolute\n    // value, while subsequent values are relative. Thus, the sum of all of the (non-null) copied\n    // values must equal the absolute value of the final element copied.\n    if (remaining !== undefined) remaining -= nulls + 1\n    const { nonNullValues, sum } = super.copyFrom(decoder, {count: remaining, sumValues: true})\n    if (nonNullValues > 0) {\n      this.absoluteValue = sum\n      decoder.absoluteValue = sum\n    }\n  }\n}\n\n/**\n * Counterpart to DeltaEncoder: reads values from a delta-compressed sequence of\n * numbers (may include null values).\n */\nclass DeltaDecoder extends RLEDecoder {\n  constructor(buffer) {\n    super('int', buffer)\n    this.absoluteValue = 0\n  }\n\n  /**\n   * Resets the cursor position, so that the next read goes back to the\n   * beginning of the buffer.\n   */\n  reset() {\n    this.offset = 0\n    this.lastValue = undefined\n    this.count = 0\n    this.state = undefined\n    this.absoluteValue = 0\n  }\n\n  /**\n   * Returns the next integer (or null) value in the sequence.\n   */\n  readValue() {\n    const value = super.readValue()\n    if (value === null) return null\n    this.absoluteValue += value\n    return this.absoluteValue\n  }\n\n  /**\n   * Discards the next `numSkip` values in the sequence.\n   */\n  skipValues(numSkip) {\n    while (numSkip > 0 && !this.done) {\n      if (this.count === 0) this.readRecord()\n      const consume = Math.min(numSkip, this.count)\n      if (this.state === 'literal') {\n        for (let i = 0; i < consume; i++) {\n          this.lastValue = this.readRawValue()\n          this.absoluteValue += this.lastValue\n        }\n      } else if (this.state === 'repetition') {\n        this.absoluteValue += consume * this.lastValue\n      }\n      numSkip -= consume\n      this.count -= consume\n    }\n  }\n}\n\n/**\n * Encodes a sequence of boolean values by mapping it to a sequence of integers:\n * the number of false values, followed by the number of true values, followed\n * by the number of false values, and so on. Each number is encoded as a LEB128\n * unsigned integer. This encoding is a bit like RLEEncoder, except that we\n * only encode the repetition count but not the actual value, since the values\n * just alternate between false and true (starting with false).\n */\nclass BooleanEncoder extends Encoder {\n  constructor() {\n    super()\n    this.lastValue = false\n    this.count = 0\n  }\n\n  /**\n   * Appends a new value to the sequence. If `repetitions` is given, the value is repeated\n   * `repetitions` times.\n   */\n  appendValue(value, repetitions = 1) {\n    if (value !== false && value !== true) {\n      throw new RangeError(`Unsupported value for BooleanEncoder: ${value}`)\n    }\n    if (repetitions <= 0) return\n    if (this.lastValue === value) {\n      this.count += repetitions\n    } else {\n      this.appendUint53(this.count)\n      this.lastValue = value\n      this.count = repetitions\n    }\n  }\n\n  /**\n   * Copies values from the BooleanDecoder `decoder` into this encoder. The `options` object may\n   * contain the key `count`, indicating the number of values to copy. If not specified, copies\n   * all remaining values in the decoder.\n   */\n  copyFrom(decoder, options = {}) {\n    if (!(decoder instanceof BooleanDecoder)) {\n      throw new TypeError('incompatible type of decoder')\n    }\n\n    const { count } = options\n    let remaining = (typeof count === 'number' ? count : Number.MAX_SAFE_INTEGER)\n    if (count && remaining > 0 && decoder.done) throw new RangeError(`cannot copy ${count} values`)\n    if (remaining === 0 || decoder.done) return\n\n    // Copy one value to bring decoder and encoder state into sync, then finish that value's repetitions\n    this.appendValue(decoder.readValue())\n    remaining--\n    const firstCopy = Math.min(decoder.count, remaining)\n    this.count += firstCopy\n    decoder.count -= firstCopy\n    remaining -= firstCopy\n\n    while (remaining > 0 && !decoder.done) {\n      decoder.count = decoder.readUint53()\n      if (decoder.count === 0) throw new RangeError('Zero-length runs are not allowed')\n      decoder.lastValue = !decoder.lastValue\n      this.appendUint53(this.count)\n\n      const numCopied = Math.min(decoder.count, remaining)\n      this.count = numCopied\n      this.lastValue = decoder.lastValue\n      decoder.count -= numCopied\n      remaining -= numCopied\n    }\n\n    if (count && remaining > 0 && decoder.done) throw new RangeError(`cannot copy ${count} values`)\n  }\n\n  /**\n   * Flushes any unwritten data to the buffer. Call this before reading from\n   * the buffer constructed by this Encoder.\n   */\n  finish() {\n    if (this.count > 0) {\n      this.appendUint53(this.count)\n      this.count = 0\n    }\n  }\n}\n\n/**\n * Counterpart to BooleanEncoder: reads boolean values from a runlength-encoded\n * sequence.\n */\nclass BooleanDecoder extends Decoder {\n  constructor(buffer) {\n    super(buffer)\n    this.lastValue = true // is negated the first time we read a count\n    this.firstRun = true\n    this.count = 0\n  }\n\n  /**\n   * Returns false if there is still data to be read at the current decoding\n   * position, and true if we are at the end of the buffer.\n   */\n  get done() {\n    return (this.count === 0) && (this.offset === this.buf.byteLength)\n  }\n\n  /**\n   * Resets the cursor position, so that the next read goes back to the\n   * beginning of the buffer.\n   */\n  reset() {\n    this.offset = 0\n    this.lastValue = true\n    this.firstRun = true\n    this.count = 0\n  }\n\n  /**\n   * Returns the next value in the sequence.\n   */\n  readValue() {\n    if (this.done) return false\n    while (this.count === 0) {\n      this.count = this.readUint53()\n      this.lastValue = !this.lastValue\n      if (this.count === 0 && !this.firstRun) {\n        throw new RangeError('Zero-length runs are not allowed')\n      }\n      this.firstRun = false\n    }\n    this.count -= 1\n    return this.lastValue\n  }\n\n  /**\n   * Discards the next `numSkip` values in the sequence.\n   */\n  skipValues(numSkip) {\n    while (numSkip > 0 && !this.done) {\n      if (this.count === 0) {\n        this.count = this.readUint53()\n        this.lastValue = !this.lastValue\n        if (this.count === 0 && !this.firstRun) {\n          throw new RangeError('Zero-length runs are not allowed')\n        }\n        this.firstRun = false\n      }\n      if (this.count < numSkip) {\n        numSkip -= this.count\n        this.count = 0\n      } else {\n        this.count -= numSkip\n        numSkip = 0\n      }\n    }\n  }\n}\n\nmodule.exports = {\n  stringToUtf8, utf8ToString, hexStringToBytes, bytesToHexString,\n  Encoder, Decoder, RLEEncoder, RLEDecoder, DeltaEncoder, DeltaDecoder, BooleanEncoder, BooleanDecoder\n}\n"
  },
  {
    "path": "backend/index.js",
    "content": "const { init, clone, free, applyChanges, applyLocalChange, save, load, loadChanges, getPatch, getHeads, getAllChanges, getChanges, getChangesAdded, getChangeByHash, getMissingDeps } = require(\"./backend\")\nconst { receiveSyncMessage, generateSyncMessage, encodeSyncMessage, decodeSyncMessage, encodeSyncState, decodeSyncState, initSyncState } = require('./sync')\n\nmodule.exports = {\n  init, clone, free, applyChanges, applyLocalChange, save, load, loadChanges, getPatch,\n  getHeads, getAllChanges, getChanges, getChangesAdded, getChangeByHash, getMissingDeps,\n  receiveSyncMessage, generateSyncMessage, encodeSyncMessage, decodeSyncMessage, encodeSyncState, decodeSyncState, initSyncState\n}\n"
  },
  {
    "path": "backend/new.js",
    "content": "const { parseOpId, copyObject } = require('../src/common')\nconst { COLUMN_TYPE, VALUE_TYPE, ACTIONS, OBJECT_TYPE, DOC_OPS_COLUMNS, CHANGE_COLUMNS, DOCUMENT_COLUMNS,\n  encoderByColumnId, decoderByColumnId, makeDecoders, decodeValue,\n  encodeChange, decodeChangeColumns, decodeChangeMeta, decodeChanges, decodeDocumentHeader, encodeDocumentHeader } = require('./columnar')\n\nconst MAX_BLOCK_SIZE = 600 // operations\nconst BLOOM_BITS_PER_ENTRY = 10, BLOOM_NUM_PROBES = 7 // 1% false positive rate\nconst BLOOM_FILTER_SIZE = Math.floor(BLOOM_BITS_PER_ENTRY * MAX_BLOCK_SIZE / 8) // bytes\n\nconst objActorIdx = 0, objCtrIdx = 1, keyActorIdx = 2, keyCtrIdx = 3, keyStrIdx = 4,\n  idActorIdx = 5, idCtrIdx = 6, insertIdx = 7, actionIdx = 8, valLenIdx = 9, valRawIdx = 10,\n  predNumIdx = 13, predActorIdx = 14, predCtrIdx = 15, succNumIdx = 13, succActorIdx = 14, succCtrIdx = 15\n\nconst PRED_COLUMN_IDS = CHANGE_COLUMNS\n  .filter(column => ['predNum', 'predActor', 'predCtr'].includes(column.columnName))\n  .map(column => column.columnId)\n\n/**\n * Updates `objectTree`, which is a tree of nested objects, so that afterwards\n * `objectTree[path[0]][path[1]][...] === value`. Only the root object is mutated, whereas any\n * nested objects are copied before updating. This means that once the root object has been\n * shallow-copied, this function can be used to update it without mutating the previous version.\n */\nfunction deepCopyUpdate(objectTree, path, value) {\n  if (path.length === 1) {\n    objectTree[path[0]] = value\n  } else {\n    let child = Object.assign({}, objectTree[path[0]])\n    deepCopyUpdate(child, path.slice(1), value)\n    objectTree[path[0]] = child\n  }\n}\n\n/**\n * Scans a block of document operations, encoded as columns `docCols`, to find the position at which\n * an operation (or sequence of operations) `ops` should be applied. `actorIds` is the array that\n * maps actor numbers to hexadecimal actor IDs. `resumeInsertion` is true if we're performing a list\n * insertion and we already found the reference element in a previous block, but we reached the end\n * of that previous block while scanning for the actual insertion position, and so we're continuing\n * the scan in a subsequent block.\n *\n * Returns an object with keys:\n * - `found`: false if we were scanning for a reference element in a list but couldn't find it;\n *    true otherwise.\n * - `skipCount`: the number of operations, counted from the start of the block, after which the\n *   new operations should be inserted or applied.\n * - `visibleCount`: if modifying a list object, the number of visible (i.e. non-deleted) list\n *   elements that precede the position where the new operations should be applied.\n */\nfunction seekWithinBlock(ops, docCols, actorIds, resumeInsertion) {\n  for (let col of docCols) col.decoder.reset()\n  const { objActor, objCtr, keyActor, keyCtr, keyStr, idActor, idCtr, insert } = ops\n  const [objActorD, objCtrD, /* keyActorD */, /* keyCtrD */, keyStrD, idActorD, idCtrD, insertD, actionD,\n    /* valLenD */, /* valRawD */, /* chldActorD */, /* chldCtrD */, succNumD] = docCols.map(col => col.decoder)\n  let skipCount = 0, visibleCount = 0, elemVisible = false, nextObjActor = null, nextObjCtr = null\n  let nextIdActor = null, nextIdCtr = null, nextKeyStr = null, nextInsert = null, nextSuccNum = 0\n\n  // Seek to the beginning of the object being updated\n  if (objCtr !== null && !resumeInsertion) {\n    while (!objCtrD.done || !objActorD.done || !actionD.done) {\n      nextObjCtr = objCtrD.readValue()\n      nextObjActor = actorIds[objActorD.readValue()]\n      actionD.skipValues(1)\n      if (nextObjCtr === null || !nextObjActor || nextObjCtr < objCtr ||\n          (nextObjCtr === objCtr && nextObjActor < objActor)) {\n        skipCount += 1\n      } else {\n        break\n      }\n    }\n  }\n  if ((nextObjCtr !== objCtr || nextObjActor !== objActor) && !resumeInsertion) {\n    return {found: true, skipCount, visibleCount}\n  }\n\n  // Seek to the appropriate key (if string key is used)\n  if (keyStr !== null) {\n    keyStrD.skipValues(skipCount)\n    while (!keyStrD.done) {\n      const objActorIndex = objActorD.readValue()\n      nextObjActor = objActorIndex === null ? null : actorIds[objActorIndex]\n      nextObjCtr = objCtrD.readValue()\n      nextKeyStr = keyStrD.readValue()\n      if (nextKeyStr !== null && nextKeyStr < keyStr &&\n          nextObjCtr === objCtr && nextObjActor === objActor) {\n        skipCount += 1\n      } else {\n        break\n      }\n    }\n    return {found: true, skipCount, visibleCount}\n  }\n\n  idCtrD.skipValues(skipCount)\n  idActorD.skipValues(skipCount)\n  insertD.skipValues(skipCount)\n  succNumD.skipValues(skipCount)\n  nextIdCtr = idCtrD.readValue()\n  nextIdActor = actorIds[idActorD.readValue()]\n  nextInsert = insertD.readValue()\n  nextSuccNum = succNumD.readValue()\n\n  // If we are inserting into a list, an opId key is used, and we need to seek to a position *after*\n  // the referenced operation. Moreover, we need to skip over any existing operations with a greater\n  // opId than the new insertion, for CRDT convergence on concurrent insertions in the same place.\n  if (insert) {\n    // If insertion is not at the head, search for the reference element\n    if (!resumeInsertion && keyCtr !== null && keyCtr > 0 && keyActor !== null) {\n      skipCount += 1\n      while (!idCtrD.done && !idActorD.done && (nextIdCtr !== keyCtr || nextIdActor !== keyActor)) {\n        if (nextInsert) elemVisible = false\n        if (nextSuccNum === 0 && !elemVisible) {\n          visibleCount += 1\n          elemVisible = true\n        }\n        nextIdCtr = idCtrD.readValue()\n        nextIdActor = actorIds[idActorD.readValue()]\n        nextObjCtr = objCtrD.readValue()\n        nextObjActor = actorIds[objActorD.readValue()]\n        nextInsert = insertD.readValue()\n        nextSuccNum = succNumD.readValue()\n        if (nextObjCtr === objCtr && nextObjActor === objActor) skipCount += 1; else break\n      }\n      if (nextObjCtr !== objCtr || nextObjActor !== objActor || nextIdCtr !== keyCtr ||\n          nextIdActor !== keyActor || !nextInsert) {\n        return {found: false, skipCount, visibleCount}\n      }\n      if (nextInsert) elemVisible = false\n      if (nextSuccNum === 0 && !elemVisible) {\n        visibleCount += 1\n        elemVisible = true\n      }\n\n      // Set up the next* variables to the operation following the reference element\n      if (idCtrD.done || idActorD.done) return {found: true, skipCount, visibleCount}\n      nextIdCtr = idCtrD.readValue()\n      nextIdActor = actorIds[idActorD.readValue()]\n      nextObjCtr = objCtrD.readValue()\n      nextObjActor = actorIds[objActorD.readValue()]\n      nextInsert = insertD.readValue()\n      nextSuccNum = succNumD.readValue()\n    }\n\n    // Skip over any list elements with greater ID than the new one, and any non-insertions\n    while ((!nextInsert || nextIdCtr > idCtr || (nextIdCtr === idCtr && nextIdActor > idActor)) &&\n           nextObjCtr === objCtr && nextObjActor === objActor) {\n      skipCount += 1\n      if (nextInsert) elemVisible = false\n      if (nextSuccNum === 0 && !elemVisible) {\n        visibleCount += 1\n        elemVisible = true\n      }\n      if (!idCtrD.done && !idActorD.done) {\n        nextIdCtr = idCtrD.readValue()\n        nextIdActor = actorIds[idActorD.readValue()]\n        nextObjCtr = objCtrD.readValue()\n        nextObjActor = actorIds[objActorD.readValue()]\n        nextInsert = insertD.readValue()\n        nextSuccNum = succNumD.readValue()\n      } else {\n        break\n      }\n    }\n\n  } else if (keyCtr !== null && keyCtr > 0 && keyActor !== null) {\n    // If we are updating an existing list element, seek to just before the referenced ID\n    while ((!nextInsert || nextIdCtr !== keyCtr || nextIdActor !== keyActor) &&\n           nextObjCtr === objCtr && nextObjActor === objActor) {\n      skipCount += 1\n      if (nextInsert) elemVisible = false\n      if (nextSuccNum === 0 && !elemVisible) {\n        visibleCount += 1\n        elemVisible = true\n      }\n      if (!idCtrD.done && !idActorD.done) {\n        nextIdCtr = idCtrD.readValue()\n        nextIdActor = actorIds[idActorD.readValue()]\n        nextObjCtr = objCtrD.readValue()\n        nextObjActor = actorIds[objActorD.readValue()]\n        nextInsert = insertD.readValue()\n        nextSuccNum = succNumD.readValue()\n      } else {\n        break\n      }\n    }\n    if (nextObjCtr !== objCtr || nextObjActor !== objActor || nextIdCtr !== keyCtr ||\n        nextIdActor !== keyActor || !nextInsert) {\n      return {found: false, skipCount, visibleCount}\n    }\n  }\n  return {found: true, skipCount, visibleCount}\n}\n\n/**\n * Returns the number of list elements that should be added to a list index when skipping over the\n * block with index `blockIndex` in the list object with object ID consisting of actor number\n * `objActorNum` and counter `objCtr`.\n */\nfunction visibleListElements(docState, blockIndex, objActorNum, objCtr) {\n  const thisBlock = docState.blocks[blockIndex]\n  const nextBlock = docState.blocks[blockIndex + 1]\n\n  if (thisBlock.lastObjectActor !== objActorNum || thisBlock.lastObjectCtr !== objCtr ||\n      thisBlock.numVisible === undefined) {\n    return 0\n\n    // If a list element is split across the block boundary, don't double-count it\n  } else if (thisBlock.lastVisibleActor === nextBlock.firstVisibleActor &&\n             thisBlock.lastVisibleActor !== undefined &&\n             thisBlock.lastVisibleCtr === nextBlock.firstVisibleCtr &&\n             thisBlock.lastVisibleCtr !== undefined) {\n    return thisBlock.numVisible - 1\n  } else {\n    return thisBlock.numVisible\n  }\n}\n\n/**\n * Scans the blocks of document operations to find the position where a new operation should be\n * inserted. Returns an object with keys:\n * - `blockIndex`: the index of the block into which we should insert the new operation\n * - `skipCount`: the number of operations, counted from the start of the block, after which the\n *   new operations should be inserted or merged.\n * - `visibleCount`: if modifying a list object, the number of visible (i.e. non-deleted) list\n *   elements that precede the position where the new operations should be applied.\n */\nfunction seekToOp(docState, ops) {\n  const { objActor, objActorNum, objCtr, keyActor, keyCtr, keyStr } = ops\n  let blockIndex = 0, totalVisible = 0\n\n  // Skip any blocks that contain only objects with lower objectIds\n  if (objCtr !== null) {\n    while (blockIndex < docState.blocks.length - 1) {\n      const blockActor = docState.blocks[blockIndex].lastObjectActor === undefined ? undefined\n        : docState.actorIds[docState.blocks[blockIndex].lastObjectActor]\n      const blockCtr = docState.blocks[blockIndex].lastObjectCtr\n      if (blockCtr === null || blockCtr < objCtr || (blockCtr === objCtr && blockActor < objActor)) {\n        blockIndex++\n      } else {\n        break\n      }\n    }\n  }\n\n  if (keyStr !== null) {\n    // String key is used. First skip any blocks that contain only lower keys\n    while (blockIndex < docState.blocks.length - 1) {\n      const { lastObjectActor, lastObjectCtr, lastKey } = docState.blocks[blockIndex]\n      if (objCtr === lastObjectCtr && objActorNum === lastObjectActor &&\n          lastKey !== undefined && lastKey < keyStr) blockIndex++; else break\n    }\n\n    // When we have a candidate block, decode it to find the exact insertion position\n    const {skipCount} = seekWithinBlock(ops, docState.blocks[blockIndex].columns, docState.actorIds, false)\n    return {blockIndex, skipCount, visibleCount: 0}\n\n  } else {\n    // List operation\n    const insertAtHead = keyCtr === null || keyCtr === 0 || keyActor === null\n    const keyActorNum = keyActor === null ? null : docState.actorIds.indexOf(keyActor)\n    let resumeInsertion = false\n\n    while (true) {\n      // Search for the reference element, skipping any blocks whose Bloom filter does not contain\n      // the reference element. We only do this if not inserting at the head (in which case there is\n      // no reference element), or if we already found the reference element in an earlier block (in\n      // which case we have resumeInsertion === true). The latter case arises with concurrent\n      // insertions at the same position, and so we have to scan beyond the reference element to\n      // find the actual insertion position, and that further scan crosses a block boundary.\n      if (!insertAtHead && !resumeInsertion) {\n        while (blockIndex < docState.blocks.length - 1 &&\n               docState.blocks[blockIndex].lastObjectActor === objActorNum &&\n               docState.blocks[blockIndex].lastObjectCtr === objCtr &&\n               !bloomFilterContains(docState.blocks[blockIndex].bloom, keyActorNum, keyCtr)) {\n          // If we reach the end of the list object without a Bloom filter hit, the reference element\n          // doesn't exist\n          if (docState.blocks[blockIndex].lastObjectCtr > objCtr) {\n            throw new RangeError(`Reference element not found: ${keyCtr}@${keyActor}`)\n          }\n\n          // Add up number of visible list elements in any blocks we skip, for list index computation\n          totalVisible += visibleListElements(docState, blockIndex, objActorNum, objCtr)\n          blockIndex++\n        }\n      }\n\n      // We have a candidate block. Decode it to see whether it really contains the reference element\n      const {found, skipCount, visibleCount} = seekWithinBlock(ops,\n                                                               docState.blocks[blockIndex].columns,\n                                                               docState.actorIds,\n                                                               resumeInsertion)\n\n      if (blockIndex === docState.blocks.length - 1 ||\n          docState.blocks[blockIndex].lastObjectActor !== objActorNum ||\n          docState.blocks[blockIndex].lastObjectCtr !== objCtr) {\n        // Last block: if we haven't found the reference element by now, it's an error\n        if (found) {\n          return {blockIndex, skipCount, visibleCount: totalVisible + visibleCount}\n        } else {\n          throw new RangeError(`Reference element not found: ${keyCtr}@${keyActor}`)\n        }\n\n      } else if (found && skipCount < docState.blocks[blockIndex].numOps) {\n        // The insertion position lies within the current block\n        return {blockIndex, skipCount, visibleCount: totalVisible + visibleCount}\n      }\n\n      // Reference element not found and there are still blocks left ==> it was probably a false positive.\n      // Reference element found, but we skipped all the way to the end of the block ==> we need to\n      // continue scanning the next block to find the actual insertion position.\n      // Either way, go back round the loop again to skip blocks until the next Bloom filter hit.\n      resumeInsertion = found && ops.insert\n      totalVisible += visibleListElements(docState, blockIndex, objActorNum, objCtr)\n      blockIndex++\n    }\n  }\n}\n\n/**\n * Updates Bloom filter `bloom`, given as a Uint8Array, to contain the list element ID consisting of\n * counter `elemIdCtr` and actor number `elemIdActor`. We don't actually bother computing a hash\n * function, since those two integers serve perfectly fine as input. We turn the two integers into a\n * sequence of probe indexes using the triple hashing algorithm from the following paper:\n *\n * Peter C. Dillinger and Panagiotis Manolios. Bloom Filters in Probabilistic Verification.\n * 5th International Conference on Formal Methods in Computer-Aided Design (FMCAD), November 2004.\n * http://www.ccis.northeastern.edu/home/pete/pub/bloom-filters-verification.pdf\n */\nfunction bloomFilterAdd(bloom, elemIdActor, elemIdCtr) {\n  let modulo = 8 * bloom.byteLength, x = elemIdCtr % modulo, y = elemIdActor % modulo\n\n  // Use one step of FNV-1a to compute a third value from the two inputs.\n  // Taken from http://www.isthe.com/chongo/tech/comp/fnv/index.html\n  // The prime is just over 2^24, so elemIdCtr can be up to about 2^29 = 500 million before the\n  // result of the multiplication exceeds 2^53. And even if it does exceed 2^53 and loses precision,\n  // that shouldn't be a problem as it should still be deterministic, and the Bloom filter\n  // computation only needs to be internally consistent within this library.\n  let z = ((elemIdCtr ^ elemIdActor) * 16777619 >>> 0) % modulo\n\n  for (let i = 0; i < BLOOM_NUM_PROBES; i++) {\n    bloom[x >>> 3] |= 1 << (x & 7)\n    x = (x + y) % modulo\n    y = (y + z) % modulo\n  }\n}\n\n/**\n * Returns true if the list element ID consisting of counter `elemIdCtr` and actor number\n * `elemIdActor` is likely to be contained in the Bloom filter `bloom`.\n */\nfunction bloomFilterContains(bloom, elemIdActor, elemIdCtr) {\n  let modulo = 8 * bloom.byteLength, x = elemIdCtr % modulo, y = elemIdActor % modulo\n  let z = ((elemIdCtr ^ elemIdActor) * 16777619 >>> 0) % modulo\n\n  // See comments in the bloomFilterAdd function for an explanation\n  for (let i = 0; i < BLOOM_NUM_PROBES; i++) {\n    if ((bloom[x >>> 3] & (1 << (x & 7))) === 0) {\n      return false\n    }\n    x = (x + y) % modulo\n    y = (y + z) % modulo\n  }\n  return true\n}\n\n/**\n * Reads the relevant columns of a block of operations and updates that block to contain the\n * metadata we need to efficiently figure out where to insert new operations.\n */\nfunction updateBlockMetadata(block) {\n  block.bloom = new Uint8Array(BLOOM_FILTER_SIZE)\n  block.numOps = 0\n  block.lastKey = undefined\n  block.numVisible = undefined\n  block.lastObjectActor = undefined\n  block.lastObjectCtr = undefined\n  block.firstVisibleActor = undefined\n  block.firstVisibleCtr = undefined\n  block.lastVisibleActor = undefined\n  block.lastVisibleCtr = undefined\n\n  for (let col of block.columns) col.decoder.reset()\n  const [objActorD, objCtrD, keyActorD, keyCtrD, keyStrD, idActorD, idCtrD, insertD, /* actionD */,\n    /* valLenD */, /* valRawD */, /* chldActorD */, /* chldCtrD */, succNumD] = block.columns.map(col => col.decoder)\n\n  while (!idCtrD.done) {\n    block.numOps += 1\n    const objActor = objActorD.readValue(), objCtr = objCtrD.readValue()\n    const keyActor = keyActorD.readValue(), keyCtr = keyCtrD.readValue(), keyStr = keyStrD.readValue()\n    const idActor = idActorD.readValue(), idCtr = idCtrD.readValue()\n    const insert = insertD.readValue(), succNum = succNumD.readValue()\n\n    if (block.lastObjectActor !== objActor || block.lastObjectCtr !== objCtr) {\n      block.numVisible = 0\n      block.lastObjectActor = objActor\n      block.lastObjectCtr = objCtr\n    }\n\n    if (keyStr !== null) {\n      // Map key: for each object, record the highest key contained in the block\n      block.lastKey = keyStr\n    } else if (insert || keyCtr !== null) {\n      // List element\n      block.lastKey = undefined\n      const elemIdActor = insert ? idActor : keyActor\n      const elemIdCtr = insert ? idCtr : keyCtr\n      bloomFilterAdd(block.bloom, elemIdActor, elemIdCtr)\n\n      // If the list element is visible, update the block metadata accordingly\n      if (succNum === 0) {\n        if (block.firstVisibleActor === undefined) block.firstVisibleActor = elemIdActor\n        if (block.firstVisibleCtr === undefined) block.firstVisibleCtr = elemIdCtr\n        if (block.lastVisibleActor !== elemIdActor || block.lastVisibleCtr !== elemIdCtr) {\n          block.numVisible += 1\n          block.lastVisibleActor = elemIdActor\n          block.lastVisibleCtr = elemIdCtr\n        }\n      }\n    }\n  }\n}\n\n/**\n * Updates a block's metadata based on an operation being added to a block.\n */\nfunction addBlockOperation(block, op, actorIds, isChangeOp) {\n  if (op[keyStrIdx] !== null) {\n    // TODO this comparison should use UTF-8 encoding, not JavaScript's UTF-16\n    if (block.lastObjectCtr === op[objCtrIdx] && block.lastObjectActor === op[objActorIdx] &&\n        (block.lastKey === undefined || block.lastKey < op[keyStrIdx])) {\n      block.lastKey = op[keyStrIdx]\n    }\n  } else {\n    // List element\n    const elemIdActor = op[insertIdx] ? op[idActorIdx] : op[keyActorIdx]\n    const elemIdCtr = op[insertIdx] ? op[idCtrIdx] : op[keyCtrIdx]\n    bloomFilterAdd(block.bloom, elemIdActor, elemIdCtr)\n\n    // Set lastVisible on the assumption that this is the last op in the block; if there are further\n    // ops after this one in the block, lastVisible will be overwritten again later.\n    if (op[succNumIdx] === 0 || isChangeOp) {\n      if (block.firstVisibleActor === undefined) block.firstVisibleActor = elemIdActor\n      if (block.firstVisibleCtr === undefined) block.firstVisibleCtr = elemIdCtr\n      block.lastVisibleActor = elemIdActor\n      block.lastVisibleCtr = elemIdCtr\n    }\n  }\n\n  // Keep track of the largest objectId contained within a block\n  if (block.lastObjectCtr === undefined ||\n      op[objActorIdx] !== null && op[objCtrIdx] !== null &&\n      (block.lastObjectCtr === null || block.lastObjectCtr < op[objCtrIdx] ||\n       (block.lastObjectCtr === op[objCtrIdx] && actorIds[block.lastObjectActor] < actorIds[op[objActorIdx]]))) {\n    block.lastObjectActor = op[objActorIdx]\n    block.lastObjectCtr = op[objCtrIdx]\n    block.lastKey = (op[keyStrIdx] !== null ? op[keyStrIdx] : undefined)\n    block.numVisible = 0\n  }\n}\n\n/**\n * Takes a block containing too many operations, and splits it into a sequence of adjacent blocks of\n * roughly equal size.\n */\nfunction splitBlock(block) {\n  for (let col of block.columns) col.decoder.reset()\n\n  // Make each of the resulting blocks between 50% and 80% full (leaving a bit of space in each\n  // block so that it doesn't get split again right away the next time an operation is added).\n  // The upper bound cannot be lower than 75% since otherwise we would end up with a block less than\n  // 50% full when going from two to three blocks.\n  const numBlocks = Math.ceil(block.numOps / (0.8 * MAX_BLOCK_SIZE))\n  let blocks = [], opsSoFar = 0\n\n  for (let i = 1; i <= numBlocks; i++) {\n    const opsToCopy = Math.ceil(i * block.numOps / numBlocks) - opsSoFar\n    const encoders = block.columns.map(col => ({columnId: col.columnId, encoder: encoderByColumnId(col.columnId)}))\n    copyColumns(encoders, block.columns, opsToCopy)\n    const decoders = encoders.map(col => {\n      const decoder = decoderByColumnId(col.columnId, col.encoder.buffer)\n      return {columnId: col.columnId, decoder}\n    })\n\n    const newBlock = {columns: decoders}\n    updateBlockMetadata(newBlock)\n    blocks.push(newBlock)\n    opsSoFar += opsToCopy\n  }\n\n  return blocks\n}\n\n/**\n * Takes an array of blocks and concatenates the corresponding columns across all of the blocks.\n */\nfunction concatBlocks(blocks) {\n  const encoders = blocks[0].columns.map(col => ({columnId: col.columnId, encoder: encoderByColumnId(col.columnId)}))\n\n  for (let block of blocks) {\n    for (let col of block.columns) col.decoder.reset()\n    copyColumns(encoders, block.columns, block.numOps)\n  }\n  return encoders\n}\n\n/**\n * Copies `count` rows from the set of input columns `inCols` to the set of output columns\n * `outCols`. The input columns are given as an array of `{columnId, decoder}` objects, and the\n * output columns are given as an array of `{columnId, encoder}` objects. Both are sorted in\n * increasing order of columnId. If there is no matching input column for a given output column, it\n * is filled in with `count` blank values (according to the column type).\n */\nfunction copyColumns(outCols, inCols, count) {\n  if (count === 0) return\n  let inIndex = 0, lastGroup = -1, lastCardinality = 0, valueColumn = -1, valueBytes = 0\n  for (let outCol of outCols) {\n    while (inIndex < inCols.length && inCols[inIndex].columnId < outCol.columnId) inIndex++\n    let inCol = null\n    if (inIndex < inCols.length && inCols[inIndex].columnId === outCol.columnId &&\n        inCols[inIndex].decoder.buf.byteLength > 0) {\n      inCol = inCols[inIndex].decoder\n    }\n    const colCount = (outCol.columnId >> 4 === lastGroup) ? lastCardinality : count\n\n    if (outCol.columnId % 8 === COLUMN_TYPE.GROUP_CARD) {\n      lastGroup = outCol.columnId >> 4\n      if (inCol) {\n        lastCardinality = outCol.encoder.copyFrom(inCol, {count, sumValues: true}).sum\n      } else {\n        outCol.encoder.appendValue(0, count)\n        lastCardinality = 0\n      }\n    } else if (outCol.columnId % 8 === COLUMN_TYPE.VALUE_LEN) {\n      if (inCol) {\n        if (inIndex + 1 === inCols.length || inCols[inIndex + 1].columnId !== outCol.columnId + 1) {\n          throw new RangeError('VALUE_LEN column without accompanying VALUE_RAW column')\n        }\n        valueColumn = outCol.columnId + 1\n        valueBytes = outCol.encoder.copyFrom(inCol, {count: colCount, sumValues: true, sumShift: 4}).sum\n      } else {\n        outCol.encoder.appendValue(null, colCount)\n        valueColumn = outCol.columnId + 1\n        valueBytes = 0\n      }\n    } else if (outCol.columnId % 8 === COLUMN_TYPE.VALUE_RAW) {\n      if (outCol.columnId !== valueColumn) {\n        throw new RangeError('VALUE_RAW column without accompanying VALUE_LEN column')\n      }\n      if (valueBytes > 0) {\n        outCol.encoder.appendRawBytes(inCol.readRawBytes(valueBytes))\n      }\n    } else { // ACTOR_ID, INT_RLE, INT_DELTA, BOOLEAN, or STRING_RLE\n      if (inCol) {\n        outCol.encoder.copyFrom(inCol, {count: colCount})\n      } else {\n        const blankValue = (outCol.columnId % 8 === COLUMN_TYPE.BOOLEAN) ? false : null\n        outCol.encoder.appendValue(blankValue, colCount)\n      }\n    }\n  }\n}\n\n/**\n * Parses one operation from a set of columns. The argument `columns` contains a list of objects\n * with `columnId` and `decoder` properties. Returns an array in which the i'th element is the\n * value read from the i'th column in `columns`. Does not interpret datatypes; the only\n * interpretation of values is that if `actorTable` is given, a value `v` in a column of type\n * ACTOR_ID is replaced with `actorTable[v]`.\n */\nfunction readOperation(columns, actorTable) {\n  let operation = [], colValue, lastGroup = -1, lastCardinality = 0, valueColumn = -1, valueBytes = 0\n  for (let col of columns) {\n    if (col.columnId % 8 === COLUMN_TYPE.VALUE_RAW) {\n      if (col.columnId !== valueColumn) throw new RangeError('unexpected VALUE_RAW column')\n      colValue = col.decoder.readRawBytes(valueBytes)\n    } else if (col.columnId % 8 === COLUMN_TYPE.GROUP_CARD) {\n      lastGroup = col.columnId >> 4\n      lastCardinality = col.decoder.readValue() || 0\n      colValue = lastCardinality\n    } else if (col.columnId >> 4 === lastGroup) {\n      colValue = []\n      if (col.columnId % 8 === COLUMN_TYPE.VALUE_LEN) {\n        valueColumn = col.columnId + 1\n        valueBytes = 0\n      }\n      for (let i = 0; i < lastCardinality; i++) {\n        let value = col.decoder.readValue()\n        if (col.columnId % 8 === COLUMN_TYPE.ACTOR_ID && actorTable && typeof value === 'number') {\n          value = actorTable[value]\n        }\n        if (col.columnId % 8 === COLUMN_TYPE.VALUE_LEN) {\n          valueBytes += colValue >>> 4\n        }\n        colValue.push(value)\n      }\n    } else {\n      colValue = col.decoder.readValue()\n      if (col.columnId % 8 === COLUMN_TYPE.ACTOR_ID && actorTable && typeof colValue === 'number') {\n        colValue = actorTable[colValue]\n      }\n      if (col.columnId % 8 === COLUMN_TYPE.VALUE_LEN) {\n        valueColumn = col.columnId + 1\n        valueBytes = colValue >>> 4\n      }\n    }\n\n    operation.push(colValue)\n  }\n  return operation\n}\n\n/**\n * Appends `operation`, in the form returned by `readOperation()`, to the columns in `outCols`. The\n * argument `inCols` provides metadata about the types of columns in `operation`; the value\n * `operation[i]` comes from the column `inCols[i]`.\n */\nfunction appendOperation(outCols, inCols, operation) {\n  let inIndex = 0, lastGroup = -1, lastCardinality = 0\n  for (let outCol of outCols) {\n    while (inIndex < inCols.length && inCols[inIndex].columnId < outCol.columnId) inIndex++\n\n    if (inIndex < inCols.length && inCols[inIndex].columnId === outCol.columnId) {\n      const colValue = operation[inIndex]\n      if (outCol.columnId % 8 === COLUMN_TYPE.GROUP_CARD) {\n        lastGroup = outCol.columnId >> 4\n        lastCardinality = colValue\n        outCol.encoder.appendValue(colValue)\n      } else if (outCol.columnId >> 4 === lastGroup) {\n        if (!Array.isArray(colValue) || colValue.length !== lastCardinality) {\n          throw new RangeError('bad group value')\n        }\n        for (let v of colValue) outCol.encoder.appendValue(v)\n      } else if (outCol.columnId % 8 === COLUMN_TYPE.VALUE_RAW) {\n        if (colValue) outCol.encoder.appendRawBytes(colValue)\n      } else {\n        outCol.encoder.appendValue(colValue)\n      }\n    } else if (outCol.columnId % 8 === COLUMN_TYPE.GROUP_CARD) {\n      lastGroup = outCol.columnId >> 4\n      lastCardinality = 0\n      outCol.encoder.appendValue(0)\n    } else if (outCol.columnId % 8 !== COLUMN_TYPE.VALUE_RAW) {\n      const count = (outCol.columnId >> 4 === lastGroup) ? lastCardinality : 1\n      let blankValue = null\n      if (outCol.columnId % 8 === COLUMN_TYPE.BOOLEAN) blankValue = false\n      if (outCol.columnId % 8 === COLUMN_TYPE.VALUE_LEN) blankValue = 0\n      outCol.encoder.appendValue(blankValue, count)\n    }\n  }\n}\n\n/**\n * Parses the next operation from block `blockIndex` of the document. Returns an object of the form\n * `{docOp, blockIndex}` where `docOp` is an operation in the form returned by `readOperation()`,\n * and `blockIndex` is the block number to use on the next call (it moves on to the next block when\n * we reach the end of the current block). `docOp` is null if there are no more operations.\n */\nfunction readNextDocOp(docState, blockIndex) {\n  let block = docState.blocks[blockIndex]\n  if (!block.columns[actionIdx].decoder.done) {\n    return {docOp: readOperation(block.columns), blockIndex}\n  } else if (blockIndex === docState.blocks.length - 1) {\n    return {docOp: null, blockIndex}\n  } else {\n    blockIndex += 1\n    block = docState.blocks[blockIndex]\n    for (let col of block.columns) col.decoder.reset()\n    return {docOp: readOperation(block.columns), blockIndex}\n  }\n}\n\n/**\n * Parses the next operation from a sequence of changes. `changeState` serves as the state of this\n * pseudo-iterator, and it is mutated to reflect the new operation. In particular,\n * `changeState.nextOp` is set to the operation that was read, and `changeState.done` is set to true\n * when we have finished reading the last operation in the last change.\n */\nfunction readNextChangeOp(docState, changeState) {\n  // If we've finished reading one change, move to the next change that contains at least one op\n  while (changeState.changeIndex < changeState.changes.length - 1 &&\n         (!changeState.columns || changeState.columns[actionIdx].decoder.done)) {\n    changeState.changeIndex += 1\n    const change = changeState.changes[changeState.changeIndex]\n    changeState.columns = makeDecoders(change.columns, CHANGE_COLUMNS)\n    changeState.opCtr = change.startOp\n\n    // If it's an empty change (no ops), set its maxOp here since it won't be set below\n    if (changeState.columns[actionIdx].decoder.done) {\n      change.maxOp = change.startOp - 1\n    }\n\n    // Update docState based on the information in the change\n    updateBlockColumns(docState, changeState.columns)\n    const {actorIds, actorTable} = getActorTable(docState.actorIds, change)\n    docState.actorIds = actorIds\n    changeState.actorTable = actorTable\n    changeState.actorIndex = docState.actorIds.indexOf(change.actorIds[0])\n  }\n\n  // Reached the end of the last change?\n  if (changeState.columns[actionIdx].decoder.done) {\n    changeState.done = true\n    changeState.nextOp = null\n    return\n  }\n\n  changeState.nextOp = readOperation(changeState.columns, changeState.actorTable)\n  changeState.nextOp[idActorIdx] = changeState.actorIndex\n  changeState.nextOp[idCtrIdx] = changeState.opCtr\n  changeState.changes[changeState.changeIndex].maxOp = changeState.opCtr\n  if (changeState.opCtr > docState.maxOp) docState.maxOp = changeState.opCtr\n  changeState.opCtr += 1\n\n  const op = changeState.nextOp\n  if ((op[objCtrIdx] === null && op[objActorIdx] !== null) ||\n      (op[objCtrIdx] !== null && op[objActorIdx] === null)) {\n    throw new RangeError(`Mismatched object reference: (${op[objCtrIdx]}, ${op[objActorIdx]})`)\n  }\n  if ((op[keyCtrIdx] === null && op[keyActorIdx] !== null) ||\n      (op[keyCtrIdx] === 0    && op[keyActorIdx] !== null) ||\n      (op[keyCtrIdx] >   0    && op[keyActorIdx] === null)) {\n    throw new RangeError(`Mismatched operation key: (${op[keyCtrIdx]}, ${op[keyActorIdx]})`)\n  }\n}\n\nfunction emptyObjectPatch(objectId, type) {\n  if (type === 'list' || type === 'text') {\n    return {objectId, type, edits: []}\n  } else {\n    return {objectId, type, props: {}}\n  }\n}\n\n/**\n * Returns true if the two given operation IDs have the same actor ID, and the counter of `id2` is\n * exactly `delta` greater than the counter of `id1`.\n */\nfunction opIdDelta(id1, id2, delta = 1) {\n  const parsed1 = parseOpId(id1), parsed2 = parseOpId(id2)\n  return parsed1.actorId === parsed2.actorId && parsed1.counter + delta === parsed2.counter\n}\n\n/**\n * Appends a list edit operation (insert, update, remove) to an array of existing operations. If the\n * last existing operation can be extended (as a multi-op), we do that.\n */\nfunction appendEdit(existingEdits, nextEdit) {\n  if (existingEdits.length === 0) {\n    existingEdits.push(nextEdit)\n    return\n  }\n\n  let lastEdit = existingEdits[existingEdits.length - 1]\n  if (lastEdit.action === 'insert' && nextEdit.action === 'insert' &&\n      lastEdit.index === nextEdit.index - 1 &&\n      lastEdit.value.type === 'value' && nextEdit.value.type === 'value' &&\n      lastEdit.elemId === lastEdit.opId && nextEdit.elemId === nextEdit.opId &&\n      opIdDelta(lastEdit.elemId, nextEdit.elemId, 1) &&\n      lastEdit.value.datatype === nextEdit.value.datatype &&\n      typeof lastEdit.value.value === typeof nextEdit.value.value) {\n    lastEdit.action = 'multi-insert'\n    if (nextEdit.value.datatype) lastEdit.datatype = nextEdit.value.datatype\n    lastEdit.values = [lastEdit.value.value, nextEdit.value.value]\n    delete lastEdit.value\n    delete lastEdit.opId\n\n  } else if (lastEdit.action === 'multi-insert' && nextEdit.action === 'insert' &&\n             lastEdit.index + lastEdit.values.length === nextEdit.index &&\n             nextEdit.value.type === 'value' && nextEdit.elemId === nextEdit.opId &&\n             opIdDelta(lastEdit.elemId, nextEdit.elemId, lastEdit.values.length) &&\n             lastEdit.datatype === nextEdit.value.datatype &&\n             typeof lastEdit.values[0] === typeof nextEdit.value.value) {\n    lastEdit.values.push(nextEdit.value.value)\n\n  } else if (lastEdit.action === 'remove' && nextEdit.action === 'remove' &&\n             lastEdit.index === nextEdit.index) {\n    lastEdit.count += nextEdit.count\n\n  } else {\n    existingEdits.push(nextEdit)\n  }\n}\n\n/**\n * `edits` is an array of (SingleInsertEdit | MultiInsertEdit | UpdateEdit | RemoveEdit) list edits\n * for a patch. This function appends an UpdateEdit to this array. A conflict is represented by\n * having several consecutive edits with the same index, and this can be realised by calling\n * `appendUpdate` several times for the same list element. On the first such call, `firstUpdate`\n * must be true.\n *\n * It is possible that coincidentally the previous edit (potentially arising from a different\n * change) is for the same index. If this is the case, to avoid accidentally treating consecutive\n * updates for the same index as a conflict, we remove the previous edit for the same index. This is\n * safe because the previous edit is overwritten by the new edit being appended, and we know that\n * it's for the same list elements because there are no intervening insertions/deletions that could\n * have changed the indexes.\n */\nfunction appendUpdate(edits, index, elemId, opId, value, firstUpdate) {\n  let insert = false\n  if (firstUpdate) {\n    // Pop all edits for the same index off the end of the edits array. This sequence may begin with\n    // either an insert or an update. If it's an insert, we remember that fact, and use it below.\n    while (!insert && edits.length > 0) {\n      const lastEdit = edits[edits.length - 1]\n      if ((lastEdit.action === 'insert' || lastEdit.action === 'update') && lastEdit.index === index) {\n        edits.pop()\n        insert = (lastEdit.action === 'insert')\n      } else if (lastEdit.action === 'multi-insert' && lastEdit.index + lastEdit.values.length - 1 === index) {\n        lastEdit.values.pop()\n        insert = true\n      } else {\n        break\n      }\n    }\n  }\n\n  // If we popped an insert edit off the edits array, we need to turn the new update into an insert\n  // in order to ensure the list element still gets inserted (just with a new value).\n  if (insert) {\n    appendEdit(edits, {action: 'insert', index, elemId, opId, value})\n  } else {\n    appendEdit(edits, {action: 'update', index, opId, value})\n  }\n}\n\n/**\n * `edits` is an array of (SingleInsertEdit | MultiInsertEdit | UpdateEdit | RemoveEdit) list edits\n * for a patch. We assume that there is a suffix of this array that consists of an insertion at\n * position `index`, followed by zero or more UpdateEdits at the same index. This function rewrites\n * that suffix to be all updates instead. This is needed because sometimes when generating a patch\n * we think we are performing a list insertion, but then it later turns out that there was already\n * an existing value at that list element, and so we actually need to do an update, not an insert.\n *\n * If the suffix is preceded by one or more updates at the same index, those earlier updates are\n * removed by `appendUpdate()` to ensure we don't inadvertently treat them as part of the same\n * conflict.\n */\nfunction convertInsertToUpdate(edits, index, elemId) {\n  let updates = []\n  while (edits.length > 0) {\n    let lastEdit = edits[edits.length - 1]\n    if (lastEdit.action === 'insert') {\n      if (lastEdit.index !== index) throw new RangeError('last edit has unexpected index')\n      updates.unshift(edits.pop())\n      break\n    } else if (lastEdit.action === 'update') {\n      if (lastEdit.index !== index) throw new RangeError('last edit has unexpected index')\n      updates.unshift(edits.pop())\n    } else {\n      // It's impossible to encounter a remove edit here because the state machine in\n      // updatePatchProperty() ensures that a property can have either an insert or a remove edit,\n      // but not both. It's impossible to encounter a multi-insert here because multi-inserts always\n      // have equal elemId and opId (i.e. they can only be used for the operation that first inserts\n      // an element, but not for any subsequent assignments to that list element); moreover,\n      // convertInsertToUpdate is only called if an insert action is followed by a non-overwritten\n      // document op. The fact that there is a non-overwritten document op after another op on the\n      // same list element implies that the original insertion op for that list element must be\n      // overwritten, and thus the original insertion op cannot have given rise to a multi-insert.\n      throw new RangeError('last edit has unexpected action')\n    }\n  }\n\n  // Now take the edits we popped off and push them back onto the list again\n  let firstUpdate = true\n  for (let update of updates) {\n    appendUpdate(edits, index, elemId, update.opId, update.value, firstUpdate)\n    firstUpdate = false\n  }\n}\n\n/**\n * Updates `patches` to reflect the operation `op` within the document with state `docState`.\n * Can be called multiple times if there are multiple operations for the same property (e.g. due\n * to a conflict). `propState` is an object that carries over state between such successive\n * invocations for the same property. If the current object is a list, `listIndex` is the index\n * into that list (counting only visible elements). If the operation `op` was already previously\n * in the document, `oldSuccNum` is the value of `op[succNumIdx]` before the current change was\n * applied (allowing us to determine whether this operation was overwritten or deleted in the\n * current change). `oldSuccNum` must be undefined if the operation came from the current change.\n * If we are creating an incremental patch as a result of applying one or more changes, `newBlock`\n * is the block to which the operations are getting written; we will update the metadata on this\n * block. `newBlock` should be null if we are creating a patch for the whole document.\n */\nfunction updatePatchProperty(patches, newBlock, objectId, op, docState, propState, listIndex, oldSuccNum) {\n  const isWholeDoc = !newBlock\n  const type = op[actionIdx] < ACTIONS.length ? OBJECT_TYPE[ACTIONS[op[actionIdx]]] : null\n  const opId = `${op[idCtrIdx]}@${docState.actorIds[op[idActorIdx]]}`\n  const elemIdActor = op[insertIdx] ? op[idActorIdx] : op[keyActorIdx]\n  const elemIdCtr = op[insertIdx] ? op[idCtrIdx] : op[keyCtrIdx]\n  const elemId = op[keyStrIdx] ? op[keyStrIdx] : `${elemIdCtr}@${docState.actorIds[elemIdActor]}`\n\n  // When the change contains a new make* operation (i.e. with an even-numbered action), record the\n  // new parent-child relationship in objectMeta. TODO: also handle link/move operations.\n  if (op[actionIdx] % 2 === 0 && !docState.objectMeta[opId]) {\n    docState.objectMeta[opId] = {parentObj: objectId, parentKey: elemId, opId, type, children: {}}\n    deepCopyUpdate(docState.objectMeta, [objectId, 'children', elemId, opId], {objectId: opId, type, props: {}})\n  }\n\n  // firstOp is true if the current operation is the first of a sequence of ops for the same key\n  const firstOp = !propState[elemId]\n  if (!propState[elemId]) propState[elemId] = {visibleOps: [], hasChild: false}\n\n  // An operation is overwritten if it is a document operation that has at least one successor\n  const isOverwritten = (oldSuccNum !== undefined && op[succNumIdx] > 0)\n\n  // Record all visible values for the property, and whether it has any child object\n  if (!isOverwritten) {\n    propState[elemId].visibleOps.push(op)\n    propState[elemId].hasChild = propState[elemId].hasChild || (op[actionIdx] % 2) === 0 // even-numbered action == make* operation\n  }\n\n  // If one or more of the values of the property is a child object, we update objectMeta to store\n  // all of the visible values of the property (even the non-child-object values). Then, when we\n  // subsequently process an update within that child object, we can construct the patch to\n  // contain the conflicting values.\n  const prevChildren = docState.objectMeta[objectId].children[elemId]\n  if (propState[elemId].hasChild || (prevChildren && Object.keys(prevChildren).length > 0)) {\n    let values = {}\n    for (let visible of propState[elemId].visibleOps) {\n      const opId = `${visible[idCtrIdx]}@${docState.actorIds[visible[idActorIdx]]}`\n      if (ACTIONS[visible[actionIdx]] === 'set') {\n        values[opId] = Object.assign({type: 'value'}, decodeValue(visible[valLenIdx], visible[valRawIdx]))\n      } else if (visible[actionIdx] % 2 === 0) {\n        const objType = visible[actionIdx] < ACTIONS.length ? OBJECT_TYPE[ACTIONS[visible[actionIdx]]] : null\n        values[opId] = emptyObjectPatch(opId, objType)\n      }\n    }\n\n    // Copy so that objectMeta is not modified if an exception is thrown while applying change\n    deepCopyUpdate(docState.objectMeta, [objectId, 'children', elemId], values)\n  }\n\n  let patchKey, patchValue\n\n  // For counters, increment operations are succs to the set operation that created the counter,\n  // but in this case we want to add the values rather than overwriting them.\n  if (isOverwritten && ACTIONS[op[actionIdx]] === 'set' && (op[valLenIdx] & 0x0f) === VALUE_TYPE.COUNTER) {\n    // This is the initial set operation that creates a counter. Initialise the counter state\n    // to contain all successors of the set operation. Only if we later find that each of these\n    // successor operations is an increment, we make the counter visible in the patch.\n    if (!propState[elemId]) propState[elemId] = {visibleOps: [], hasChild: false}\n    if (!propState[elemId].counterStates) propState[elemId].counterStates = {}\n    let counterStates = propState[elemId].counterStates\n    let counterState = {opId, value: decodeValue(op[valLenIdx], op[valRawIdx]).value, succs: {}}\n\n    for (let i = 0; i < op[succNumIdx]; i++) {\n      const succOp = `${op[succCtrIdx][i]}@${docState.actorIds[op[succActorIdx][i]]}`\n      counterStates[succOp] = counterState\n      counterState.succs[succOp] = true\n    }\n\n  } else if (ACTIONS[op[actionIdx]] === 'inc') {\n    // Incrementing a previously created counter.\n    if (!propState[elemId] || !propState[elemId].counterStates || !propState[elemId].counterStates[opId]) {\n      throw new RangeError(`increment operation ${opId} for unknown counter`)\n    }\n    let counterState = propState[elemId].counterStates[opId]\n    counterState.value += decodeValue(op[valLenIdx], op[valRawIdx]).value\n    delete counterState.succs[opId]\n\n    if (Object.keys(counterState.succs).length === 0) {\n      patchKey = counterState.opId\n      patchValue = {type: 'value', datatype: 'counter', value: counterState.value}\n      // TODO if the counter is in a list element, we need to add a 'remove' action when deleted\n    }\n\n  } else if (!isOverwritten) {\n    // Add the value to the patch if it is not overwritten (i.e. if it has no succs).\n    if (ACTIONS[op[actionIdx]] === 'set') {\n      patchKey = opId\n      patchValue = Object.assign({type: 'value'}, decodeValue(op[valLenIdx], op[valRawIdx]))\n    } else if (op[actionIdx] % 2 === 0) { // even-numbered action == make* operation\n      if (!patches[opId]) patches[opId] = emptyObjectPatch(opId, type)\n      patchKey = opId\n      patchValue = patches[opId]\n    }\n  }\n\n  if (!patches[objectId]) patches[objectId] = emptyObjectPatch(objectId, docState.objectMeta[objectId].type)\n  const patch = patches[objectId]\n\n  // Updating a list or text object (with elemId key)\n  if (op[keyStrIdx] === null) {\n    // If we come across any document op that was previously non-overwritten/non-deleted, that\n    // means the current list element already had a value before this change was applied, and\n    // therefore the current element cannot be an insert. If we already registered an insert, we\n    // have to convert it into an update.\n    if (oldSuccNum === 0 && !isWholeDoc && propState[elemId].action === 'insert') {\n      propState[elemId].action = 'update'\n      convertInsertToUpdate(patch.edits, listIndex, elemId)\n      if (newBlock && newBlock.lastObjectActor === op[objActorIdx] && newBlock.lastObjectCtr === op[objCtrIdx]) {\n        newBlock.numVisible -= 1\n      }\n    }\n\n    if (patchValue) {\n      // If the op has a non-overwritten value and it came from the change, it's an insert.\n      // (It's not necessarily the case that op[insertIdx] is true: if a list element is concurrently\n      // deleted and updated, the node that first processes the deletion and then the update will\n      // observe the update as a re-insertion of the deleted list element.)\n      if (!propState[elemId].action && (oldSuccNum === undefined || isWholeDoc)) {\n        propState[elemId].action = 'insert'\n        appendEdit(patch.edits, {action: 'insert', index: listIndex, elemId, opId: patchKey, value: patchValue})\n        if (newBlock && newBlock.lastObjectActor === op[objActorIdx] && newBlock.lastObjectCtr === op[objCtrIdx]) {\n          newBlock.numVisible += 1\n        }\n\n      // If the property has a value and it's not an insert, then it must be an update.\n      // We might have previously registered it as a remove, in which case we convert it to update.\n      } else if (propState[elemId].action === 'remove') {\n        let lastEdit = patch.edits[patch.edits.length - 1]\n        if (lastEdit.action !== 'remove') throw new RangeError('last edit has unexpected type')\n        if (lastEdit.count > 1) lastEdit.count -= 1; else patch.edits.pop()\n        propState[elemId].action = 'update'\n        appendUpdate(patch.edits, listIndex, elemId, patchKey, patchValue, true)\n        if (newBlock && newBlock.lastObjectActor === op[objActorIdx] && newBlock.lastObjectCtr === op[objCtrIdx]) {\n          newBlock.numVisible += 1\n        }\n\n      } else {\n        // A 'normal' update\n        appendUpdate(patch.edits, listIndex, elemId, patchKey, patchValue, !propState[elemId].action)\n        if (!propState[elemId].action) propState[elemId].action = 'update'\n      }\n\n    } else if (oldSuccNum === 0 && !propState[elemId].action) {\n      // If the property used to have a non-overwritten/non-deleted value, but no longer, it's a remove\n      propState[elemId].action = 'remove'\n      appendEdit(patch.edits, {action: 'remove', index: listIndex, count: 1})\n      if (newBlock && newBlock.lastObjectActor === op[objActorIdx] && newBlock.lastObjectCtr === op[objCtrIdx]) {\n        newBlock.numVisible -= 1\n      }\n    }\n\n  } else if (patchValue || !isWholeDoc) {\n    // Updating a map or table (with string key)\n    if (firstOp || !patch.props[op[keyStrIdx]]) patch.props[op[keyStrIdx]] = {}\n    if (patchValue) patch.props[op[keyStrIdx]][patchKey] = patchValue\n  }\n}\n\n/**\n * Applies operations (from one or more changes) to the document by merging the sequence of change\n * ops into the sequence of document ops. The two inputs are `changeState` and `docState`\n * respectively. Assumes that the decoders of both sets of columns are at the position where we want\n * to start merging. `patches` is mutated to reflect the effect of the change operations. `ops` is\n * the operation sequence to apply (as decoded by `groupRelatedOps()`). `docState` is as\n * documented in `applyOps()`. If the operations are updating a list or text object, `listIndex`\n * is the number of visible elements that precede the position at which we start merging.\n * `blockIndex` is the document block number from which we are currently reading.\n */\nfunction mergeDocChangeOps(patches, newBlock, outCols, changeState, docState, listIndex, blockIndex) {\n  const firstOp = changeState.nextOp, insert = firstOp[insertIdx]\n  const objActor = firstOp[objActorIdx], objCtr = firstOp[objCtrIdx]\n  const objectId = objActor === null ? '_root' : `${objCtr}@${docState.actorIds[objActor]}`\n  const idActorIndex = changeState.actorIndex, idActor = docState.actorIds[idActorIndex]\n  let foundListElem = false, elemVisible = false, propState = {}, docOp\n  ;({ docOp, blockIndex } = readNextDocOp(docState, blockIndex))\n  let docOpsConsumed = (docOp === null ? 0 : 1)\n  let docOpOldSuccNum = (docOp === null ? 0 : docOp[succNumIdx])\n  let changeOp = null, changeOps = [], changeCols = [], predSeen = [], lastChangeKey = null\n  changeState.objectIds.add(objectId)\n\n  // Merge the two inputs: the sequence of ops in the doc, and the sequence of ops in the change.\n  // At each iteration, we either output the doc's op (possibly updated based on the change's ops)\n  // or output an op from the change.\n  while (true) {\n    // The array `changeOps` contains operations from the change(s) we're applying. When the array\n    // is empty, we load changes from the change. Typically we load only a single operation at a\n    // time, with two exceptions: 1. all operations that update the same key or list element in the\n    // same object are put into changeOps at the same time (this is needed so that we can update the\n    // succ columns of the document ops correctly); 2. a run of consecutive insertions is also\n    // placed into changeOps in one go.\n    //\n    // When we have processed all the ops in changeOps we try to see whether there are further\n    // operations that we can also process while we're at it. Those operations must be for the same\n    // object, they must be for a key or list element that appears later in the document, they must\n    // either all be insertions or all be non-insertions, and if insertions, they must be\n    // consecutive. If these conditions are satisfied, that means the operations can be processed in\n    // the same pass. If we encounter an operation that does not meet these conditions, we leave\n    // changeOps empty, and this function returns after having processed any remaining document ops.\n    //\n    // Any operations that could not be processed in a single pass remain in changeState; applyOps\n    // will seek to the appropriate position and then call mergeDocChangeOps again.\n    if (changeOps.length === 0) {\n      foundListElem = false\n\n      let nextOp = changeState.nextOp\n      while (!changeState.done && nextOp[idActorIdx] === idActorIndex && nextOp[insertIdx] === insert &&\n             nextOp[objActorIdx] === firstOp[objActorIdx] && nextOp[objCtrIdx] === firstOp[objCtrIdx]) {\n\n        // Check if the operation's pred references a previous operation in changeOps\n        const lastOp = (changeOps.length > 0) ? changeOps[changeOps.length - 1] : null\n        let isOverwrite = false\n        for (let i = 0; i < nextOp[predNumIdx]; i++) {\n          for (let prevOp of changeOps) {\n            if (nextOp[predActorIdx][i] === prevOp[idActorIdx] && nextOp[predCtrIdx][i] === prevOp[idCtrIdx]) {\n              isOverwrite = true\n            }\n          }\n        }\n\n        // If any of the following `if` statements is true, we add `nextOp` to `changeOps`. If they\n        // are all false, we break out of the loop and stop adding to `changeOps`.\n        if (nextOp === firstOp) {\n          // First change operation in a mergeDocChangeOps call is always used\n        } else if (insert && lastOp !== null && nextOp[keyStrIdx] === null &&\n                   nextOp[keyActorIdx] === lastOp[idActorIdx] &&\n                   nextOp[keyCtrIdx] === lastOp[idCtrIdx]) {\n          // Collect consecutive insertions\n        } else if (!insert && lastOp !== null && nextOp[keyStrIdx] !== null &&\n                   nextOp[keyStrIdx] === lastOp[keyStrIdx] && !isOverwrite) {\n          // Collect several updates to the same key\n        } else if (!insert && lastOp !== null &&\n                   nextOp[keyStrIdx] === null && lastOp[keyStrIdx] === null &&\n                   nextOp[keyActorIdx] === lastOp[keyActorIdx] &&\n                   nextOp[keyCtrIdx] === lastOp[keyCtrIdx] && !isOverwrite) {\n          // Collect several updates to the same list element\n        } else if (!insert && lastOp === null && nextOp[keyStrIdx] === null &&\n                   docOp && docOp[insertIdx] && docOp[keyStrIdx] === null &&\n                   docOp[idActorIdx] === nextOp[keyActorIdx] &&\n                   docOp[idCtrIdx] === nextOp[keyCtrIdx]) {\n          // When updating/deleting list elements, keep going if the next elemId in the change\n          // equals the next elemId in the doc (i.e. we're updating several consecutive elements)\n        } else if (!insert && lastOp === null && nextOp[keyStrIdx] !== null &&\n                   lastChangeKey !== null && lastChangeKey < nextOp[keyStrIdx]) {\n          // Allow a single mergeDocChangeOps call to process changes to several keys in the same\n          // object, provided that they appear in ascending order\n        } else break\n\n        lastChangeKey = (nextOp !== null) ? nextOp[keyStrIdx] : null\n        changeOps.push(changeState.nextOp)\n        changeCols.push(changeState.columns)\n        predSeen.push(new Array(changeState.nextOp[predNumIdx]))\n        readNextChangeOp(docState, changeState)\n        nextOp = changeState.nextOp\n      }\n    }\n\n    if (changeOps.length > 0) changeOp = changeOps[0]\n    const inCorrectObject = docOp && docOp[objActorIdx] === changeOp[objActorIdx] && docOp[objCtrIdx] === changeOp[objCtrIdx]\n    const keyMatches      = docOp && docOp[keyStrIdx] !== null && docOp[keyStrIdx] === changeOp[keyStrIdx]\n    const listElemMatches = docOp && docOp[keyStrIdx] === null && changeOp[keyStrIdx] === null &&\n      ((!docOp[insertIdx] && docOp[keyActorIdx] === changeOp[keyActorIdx] && docOp[keyCtrIdx] === changeOp[keyCtrIdx]) ||\n        (docOp[insertIdx] && docOp[idActorIdx]  === changeOp[keyActorIdx] && docOp[idCtrIdx]  === changeOp[keyCtrIdx]))\n\n    // We keep going until we run out of ops in the change, except that even when we run out, we\n    // keep going until we have processed all doc ops for the current key/list element.\n    if (changeOps.length === 0 && !(inCorrectObject && (keyMatches || listElemMatches))) break\n\n    let takeDocOp = false, takeChangeOps = 0\n\n    // The change operations come first if we are inserting list elements (seekToOp already\n    // determines the correct insertion position), if there is no document operation, if the next\n    // document operation is for a different object, or if the change op's string key is\n    // lexicographically first (TODO check ordering of keys beyond the basic multilingual plane).\n    if (insert || !inCorrectObject ||\n        (docOp[keyStrIdx] === null && changeOp[keyStrIdx] !== null) ||\n        (docOp[keyStrIdx] !== null && changeOp[keyStrIdx] !== null && changeOp[keyStrIdx] < docOp[keyStrIdx])) {\n      // Take the operations from the change\n      takeChangeOps = changeOps.length\n      if (!inCorrectObject && !foundListElem && changeOp[keyStrIdx] === null && !changeOp[insertIdx]) {\n        // This can happen if we first update one list element, then another one earlier in the\n        // list. That is not allowed: list element updates must occur in ascending order.\n        throw new RangeError(\"could not find list element with ID: \" +\n                             `${changeOp[keyCtrIdx]}@${docState.actorIds[changeOp[keyActorIdx]]}`)\n      }\n\n    } else if (keyMatches || listElemMatches || foundListElem) {\n      // The doc operation is for the same key or list element in the same object as the change\n      // ops, so we merge them. First, if any of the change ops' `pred` matches the opId of the\n      // document operation, we update the document operation's `succ` accordingly.\n      for (let opIndex = 0; opIndex < changeOps.length; opIndex++) {\n        const op = changeOps[opIndex]\n        for (let i = 0; i < op[predNumIdx]; i++) {\n          if (op[predActorIdx][i] === docOp[idActorIdx] && op[predCtrIdx][i] === docOp[idCtrIdx]) {\n            // Insert into the doc op's succ list such that the lists remains sorted\n            let j = 0\n            while (j < docOp[succNumIdx] && (docOp[succCtrIdx][j] < op[idCtrIdx] ||\n                   docOp[succCtrIdx][j] === op[idCtrIdx] && docState.actorIds[docOp[succActorIdx][j]] < idActor)) j++\n            docOp[succCtrIdx].splice(j, 0, op[idCtrIdx])\n            docOp[succActorIdx].splice(j, 0, idActorIndex)\n            docOp[succNumIdx]++\n            predSeen[opIndex][i] = true\n            break\n          }\n        }\n      }\n\n      if (listElemMatches) foundListElem = true\n\n      if (foundListElem && !listElemMatches) {\n        // If the previous docOp was for the correct list element, and the current docOp is for\n        // the wrong list element, then place the current changeOp before the docOp.\n        takeChangeOps = changeOps.length\n\n      } else if (changeOps.length === 0 || docOp[idCtrIdx] < changeOp[idCtrIdx] ||\n          (docOp[idCtrIdx] === changeOp[idCtrIdx] && docState.actorIds[docOp[idActorIdx]] < idActor)) {\n        // When we have several operations for the same object and the same key, we want to keep\n        // them sorted in ascending order by opId. Here we have docOp with a lower opId, so we\n        // output it first.\n        takeDocOp = true\n        updatePatchProperty(patches, newBlock, objectId, docOp, docState, propState, listIndex, docOpOldSuccNum)\n\n        // A deletion op in the change is represented in the document only by its entries in the\n        // succ list of the operations it overwrites; it has no separate row in the set of ops.\n        for (let i = changeOps.length - 1; i >= 0; i--) {\n          let deleted = true\n          for (let j = 0; j < changeOps[i][predNumIdx]; j++) {\n            if (!predSeen[i][j]) deleted = false\n          }\n          if (ACTIONS[changeOps[i][actionIdx]] === 'del' && deleted) {\n            changeOps.splice(i, 1)\n            changeCols.splice(i, 1)\n            predSeen.splice(i, 1)\n          }\n        }\n\n      } else if (docOp[idCtrIdx] === changeOp[idCtrIdx] && docState.actorIds[docOp[idActorIdx]] === idActor) {\n        throw new RangeError(`duplicate operation ID: ${changeOp[idCtrIdx]}@${idActor}`)\n      } else {\n        // The changeOp has the lower opId, so we output it first.\n        takeChangeOps = 1\n      }\n    } else {\n      // The document operation comes first if its string key is lexicographically first, or if\n      // we're using opId keys and the keys don't match (i.e. we scan the document until we find a\n      // matching key).\n      takeDocOp = true\n    }\n\n    if (takeDocOp) {\n      appendOperation(outCols, docState.blocks[blockIndex].columns, docOp)\n      addBlockOperation(newBlock, docOp, docState.actorIds, false)\n\n      if (docOp[insertIdx] && elemVisible) {\n        elemVisible = false\n        listIndex++\n      }\n      if (docOp[succNumIdx] === 0) elemVisible = true\n      newBlock.numOps++\n      ;({ docOp, blockIndex } = readNextDocOp(docState, blockIndex))\n      if (docOp !== null) {\n        docOpsConsumed++\n        docOpOldSuccNum = docOp[succNumIdx]\n      }\n    }\n\n    if (takeChangeOps > 0) {\n      for (let i = 0; i < takeChangeOps; i++) {\n        let op = changeOps[i]\n        // Check that we've seen all ops mentioned in `pred` (they must all have lower opIds than\n        // the change op's own opId, so we must have seen them already)\n        for (let j = 0; j < op[predNumIdx]; j++) {\n          if (!predSeen[i][j]) {\n            throw new RangeError(`no matching operation for pred: ${op[predCtrIdx][j]}@${docState.actorIds[op[predActorIdx][j]]}`)\n          }\n        }\n        appendOperation(outCols, changeCols[i], op)\n        addBlockOperation(newBlock, op, docState.actorIds, true)\n        updatePatchProperty(patches, newBlock, objectId, op, docState, propState, listIndex)\n\n        if (op[insertIdx]) {\n          elemVisible = false\n          listIndex++\n        } else {\n          elemVisible = true\n        }\n      }\n\n      if (takeChangeOps === changeOps.length) {\n        changeOps.length = 0\n        changeCols.length = 0\n        predSeen.length = 0\n      } else {\n        changeOps.splice(0, takeChangeOps)\n        changeCols.splice(0, takeChangeOps)\n        predSeen.splice(0, takeChangeOps)\n      }\n      newBlock.numOps += takeChangeOps\n    }\n  }\n\n  if (docOp) {\n    appendOperation(outCols, docState.blocks[blockIndex].columns, docOp)\n    newBlock.numOps++\n    addBlockOperation(newBlock, docOp, docState.actorIds, false)\n  }\n  return {docOpsConsumed, blockIndex}\n}\n\n/**\n * Applies operations from the change (or series of changes) in `changeState` to the document\n * `docState`. Passing `changeState` to `readNextChangeOp` allows iterating over the change ops.\n * `docState` is an object with keys:\n *   - `actorIds` is an array of actorIds (as hex strings) occurring in the document (values in\n *     the document's objActor/keyActor/idActor/... columns are indexes into this array).\n *   - `blocks` is an array of all the blocks of operations in the document.\n *   - `objectMeta` is a map from objectId to metadata about that object.\n *\n * `docState` is mutated to contain the updated document state.\n * `patches` is a patch object that is mutated to reflect the operations applied by this function.\n */\nfunction applyOps(patches, changeState, docState) {\n  const [objActorNum, objCtr, keyActorNum, keyCtr, keyStr, idActorNum, idCtr, insert] = changeState.nextOp\n  const objActor = objActorNum === null ? null : docState.actorIds[objActorNum]\n  const keyActor = keyActorNum === null ? null : docState.actorIds[keyActorNum]\n  const ops = {\n    objActor, objActorNum, objCtr, keyActor, keyActorNum, keyCtr, keyStr,\n    idActor: docState.actorIds[idActorNum], idCtr, insert,\n    objId: objActor === null ? '_root' : `${objCtr}@${objActor}`\n  }\n\n  const {blockIndex, skipCount, visibleCount} = seekToOp(docState, ops)\n  const block = docState.blocks[blockIndex]\n  for (let col of block.columns) col.decoder.reset()\n\n  const resetFirstVisible = (skipCount === 0) || (block.firstVisibleActor === undefined) ||\n    (!insert && block.firstVisibleActor === keyActorNum && block.firstVisibleCtr === keyCtr)\n  const newBlock = {\n    columns: undefined,\n    bloom: new Uint8Array(block.bloom),\n    numOps: skipCount,\n    lastKey: block.lastKey,\n    numVisible: block.numVisible,\n    lastObjectActor: block.lastObjectActor,\n    lastObjectCtr: block.lastObjectCtr,\n    firstVisibleActor: resetFirstVisible ? undefined : block.firstVisibleActor,\n    firstVisibleCtr: resetFirstVisible ? undefined : block.firstVisibleCtr,\n    lastVisibleActor: undefined,\n    lastVisibleCtr: undefined\n  }\n\n  // Copy the operations up to the insertion position (the first skipCount operations)\n  const outCols = block.columns.map(col => ({columnId: col.columnId, encoder: encoderByColumnId(col.columnId)}))\n  copyColumns(outCols, block.columns, skipCount)\n\n  // Apply the operations from the change. This may cause blockIndex to move forwards if the\n  // property being updated straddles a block boundary.\n  const {blockIndex: lastBlockIndex, docOpsConsumed} =\n    mergeDocChangeOps(patches, newBlock, outCols, changeState, docState, visibleCount, blockIndex)\n\n  // Copy the remaining operations after the insertion position\n  const lastBlock = docState.blocks[lastBlockIndex]\n  let copyAfterMerge = -skipCount - docOpsConsumed\n  for (let i = blockIndex; i <= lastBlockIndex; i++) copyAfterMerge += docState.blocks[i].numOps\n  copyColumns(outCols, lastBlock.columns, copyAfterMerge)\n  newBlock.numOps += copyAfterMerge\n\n  for (let col of lastBlock.columns) {\n    if (!col.decoder.done) throw new RangeError(`excess ops in column ${col.columnId}`)\n  }\n\n  newBlock.columns = outCols.map(col => {\n    const decoder = decoderByColumnId(col.columnId, col.encoder.buffer)\n    return {columnId: col.columnId, decoder}\n  })\n\n  if (blockIndex === lastBlockIndex && newBlock.numOps <= MAX_BLOCK_SIZE) {\n    // The result is just one output block\n    if (copyAfterMerge > 0 && block.lastVisibleActor !== undefined && block.lastVisibleCtr !== undefined) {\n      // It's possible that none of the ops after the merge point are visible, in which case the\n      // lastVisible may not be strictly correct, because it may refer to an operation before the\n      // merge point rather than a list element inserted by the current change. However, this doesn't\n      // matter, because the only purpose for which we need it is to check whether one block ends with\n      // the same visible element as the next block starts with (to avoid double-counting its index);\n      // if the last list element of a block is invisible, the exact value of lastVisible doesn't\n      // matter since it will be different from the next block's firstVisible in any case.\n      newBlock.lastVisibleActor = block.lastVisibleActor\n      newBlock.lastVisibleCtr = block.lastVisibleCtr\n    }\n\n    docState.blocks[blockIndex] = newBlock\n\n  } else {\n    // Oversized output block must be split into smaller blocks\n    const newBlocks = splitBlock(newBlock)\n    docState.blocks.splice(blockIndex, lastBlockIndex - blockIndex + 1, ...newBlocks)\n  }\n}\n\n/**\n * Updates the columns in a document's operation blocks to contain all the columns in a change\n * (including any column types we don't recognise, which have been generated by a future version\n * of Automerge).\n */\nfunction updateBlockColumns(docState, changeCols) {\n  // Check that the columns of a change appear at the index at which we expect them to be\n  if (changeCols[objActorIdx ].columnId !== CHANGE_COLUMNS[objActorIdx ].columnId || CHANGE_COLUMNS[objActorIdx ].columnName !== 'objActor'  ||\n      changeCols[objCtrIdx   ].columnId !== CHANGE_COLUMNS[objCtrIdx   ].columnId || CHANGE_COLUMNS[objCtrIdx   ].columnName !== 'objCtr'    ||\n      changeCols[keyActorIdx ].columnId !== CHANGE_COLUMNS[keyActorIdx ].columnId || CHANGE_COLUMNS[keyActorIdx ].columnName !== 'keyActor'  ||\n      changeCols[keyCtrIdx   ].columnId !== CHANGE_COLUMNS[keyCtrIdx   ].columnId || CHANGE_COLUMNS[keyCtrIdx   ].columnName !== 'keyCtr'    ||\n      changeCols[keyStrIdx   ].columnId !== CHANGE_COLUMNS[keyStrIdx   ].columnId || CHANGE_COLUMNS[keyStrIdx   ].columnName !== 'keyStr'    ||\n      changeCols[idActorIdx  ].columnId !== CHANGE_COLUMNS[idActorIdx  ].columnId || CHANGE_COLUMNS[idActorIdx  ].columnName !== 'idActor'   ||\n      changeCols[idCtrIdx    ].columnId !== CHANGE_COLUMNS[idCtrIdx    ].columnId || CHANGE_COLUMNS[idCtrIdx    ].columnName !== 'idCtr'     ||\n      changeCols[insertIdx   ].columnId !== CHANGE_COLUMNS[insertIdx   ].columnId || CHANGE_COLUMNS[insertIdx   ].columnName !== 'insert'    ||\n      changeCols[actionIdx   ].columnId !== CHANGE_COLUMNS[actionIdx   ].columnId || CHANGE_COLUMNS[actionIdx   ].columnName !== 'action'    ||\n      changeCols[valLenIdx   ].columnId !== CHANGE_COLUMNS[valLenIdx   ].columnId || CHANGE_COLUMNS[valLenIdx   ].columnName !== 'valLen'    ||\n      changeCols[valRawIdx   ].columnId !== CHANGE_COLUMNS[valRawIdx   ].columnId || CHANGE_COLUMNS[valRawIdx   ].columnName !== 'valRaw'    ||\n      changeCols[predNumIdx  ].columnId !== CHANGE_COLUMNS[predNumIdx  ].columnId || CHANGE_COLUMNS[predNumIdx  ].columnName !== 'predNum'   ||\n      changeCols[predActorIdx].columnId !== CHANGE_COLUMNS[predActorIdx].columnId || CHANGE_COLUMNS[predActorIdx].columnName !== 'predActor' ||\n      changeCols[predCtrIdx  ].columnId !== CHANGE_COLUMNS[predCtrIdx  ].columnId || CHANGE_COLUMNS[predCtrIdx  ].columnName !== 'predCtr') {\n    throw new RangeError('unexpected columnId')\n  }\n\n  // Check if there any columns in the change that are not in the document, apart from pred*\n  const docCols = docState.blocks[0].columns\n  if (!changeCols.every(changeCol => PRED_COLUMN_IDS.includes(changeCol.columnId) ||\n                                     docCols.find(docCol => docCol.columnId === changeCol.columnId))) {\n    let allCols = docCols.map(docCol => ({columnId: docCol.columnId}))\n    for (let changeCol of changeCols) {\n      const { columnId } = changeCol\n      if (!PRED_COLUMN_IDS.includes(columnId) && !docCols.find(docCol => docCol.columnId === columnId)) {\n        allCols.push({columnId})\n      }\n    }\n    allCols.sort((a, b) => a.columnId - b.columnId)\n\n    for (let blockIndex = 0; blockIndex < docState.blocks.length; blockIndex++) {\n      let block = copyObject(docState.blocks[blockIndex])\n      block.columns = makeDecoders(block.columns.map(col => ({columnId: col.columnId, buffer: col.decoder.buf})), allCols)\n      docState.blocks[blockIndex] = block\n    }\n  }\n}\n\n/**\n * Takes a decoded change header, including an array of actorIds. Returns an object of the form\n * `{actorIds, actorTable}`, where `actorIds` is an updated array of actorIds appearing in the\n * document (including the new change's actorId). `actorTable` is an array of integers where\n * `actorTable[i]` contains the document's actor index for the actor that has index `i` in the\n * change (`i == 0` is the author of the change).\n */\nfunction getActorTable(actorIds, change) {\n  if (actorIds.indexOf(change.actorIds[0]) < 0) {\n    if (change.seq !== 1) {\n      throw new RangeError(`Seq ${change.seq} is the first change for actor ${change.actorIds[0]}`)\n    }\n    // Use concat, not push, so that the original array is not mutated\n    actorIds = actorIds.concat([change.actorIds[0]])\n  }\n  const actorTable = [] // translate from change's actor index to doc's actor index\n  for (let actorId of change.actorIds) {\n    const index = actorIds.indexOf(actorId)\n    if (index < 0) {\n      throw new RangeError(`actorId ${actorId} is not known to document`)\n    }\n    actorTable.push(index)\n  }\n  return {actorIds, actorTable}\n}\n\n/**\n * Finalises the patch for a change. `patches` is a map from objectIds to patch for that\n * particular object, `objectIds` is the array of IDs of objects that are created or updated in the\n * change, and `docState` is an object containing various bits of document state, including\n * `objectMeta`, a map from objectIds to metadata about that object (such as its parent in the\n * document tree). Mutates `patches` such that child objects are linked into their parent object,\n * all the way to the root object.\n */\nfunction setupPatches(patches, objectIds, docState) {\n  for (let objectId of objectIds) {\n    let meta = docState.objectMeta[objectId], childMeta = null, patchExists = false\n    while (true) {\n      const hasChildren = childMeta && Object.keys(meta.children[childMeta.parentKey]).length > 0\n      if (!patches[objectId]) patches[objectId] = emptyObjectPatch(objectId, meta.type)\n\n      if (childMeta && hasChildren) {\n        if (meta.type === 'list' || meta.type === 'text') {\n          // In list/text objects, parentKey is an elemID. First see if it already appears in an edit\n          for (let edit of patches[objectId].edits) {\n            if (edit.opId && meta.children[childMeta.parentKey][edit.opId]) {\n              patchExists = true\n            }\n          }\n\n          // If we need to add an edit, we first have to translate the elemId into an index\n          if (!patchExists) {\n            const obj = parseOpId(objectId), elem = parseOpId(childMeta.parentKey)\n            const seekPos = {\n              objActor: obj.actorId,  objCtr: obj.counter,\n              keyActor: elem.actorId, keyCtr: elem.counter,\n              objActorNum: docState.actorIds.indexOf(obj.actorId),\n              keyActorNum: docState.actorIds.indexOf(elem.actorId),\n              keyStr:   null,         insert: false,\n              objId:    objectId\n            }\n            const { visibleCount } = seekToOp(docState, seekPos)\n\n            for (let [opId, value] of Object.entries(meta.children[childMeta.parentKey])) {\n              let patchValue = value\n              if (value.objectId) {\n                if (!patches[value.objectId]) patches[value.objectId] = emptyObjectPatch(value.objectId, value.type)\n                patchValue = patches[value.objectId]\n              }\n              const edit = {action: 'update', index: visibleCount, opId, value: patchValue}\n              appendEdit(patches[objectId].edits, edit)\n            }\n          }\n\n        } else {\n          // Non-list object: parentKey is the name of the property being updated (a string)\n          if (!patches[objectId].props[childMeta.parentKey]) {\n            patches[objectId].props[childMeta.parentKey] = {}\n          }\n          let values = patches[objectId].props[childMeta.parentKey]\n\n          for (let [opId, value] of Object.entries(meta.children[childMeta.parentKey])) {\n            if (values[opId]) {\n              patchExists = true\n            } else if (value.objectId) {\n              if (!patches[value.objectId]) patches[value.objectId] = emptyObjectPatch(value.objectId, value.type)\n              values[opId] = patches[value.objectId]\n            } else {\n              values[opId] = value\n            }\n          }\n        }\n      }\n\n      if (patchExists || !meta.parentObj || (childMeta && !hasChildren)) break\n      childMeta = meta\n      objectId = meta.parentObj\n      meta = docState.objectMeta[objectId]\n    }\n  }\n  return patches\n}\n\n/**\n * Takes an array of decoded changes and applies them to a document. `docState` contains a bunch of\n * fields describing the document state. This function mutates `docState` to contain the updated\n * document state, and mutates `patches` to contain a patch to return to the frontend. Only the\n * top-level `docState` object is mutated; all nested objects within it are treated as immutable.\n * `objectIds` is mutated to contain the IDs of objects that are updated in any of the changes.\n *\n * The function detects duplicate changes that we've already applied by looking up each change's\n * hash in `docState.changeIndexByHash`. If we deferred the hash graph computation, that structure\n * will be incomplete, and we run the risk of applying the same change twice. However, we still have\n * the sequence numbers for detecting duplicates. If `throwExceptions` is true, we assume that the\n * set of change hashes is complete, and therefore a duplicate sequence number indicates illegal\n * behaviour. If `throwExceptions` is false, and we detect a possible sequence number reuse, we\n * don't throw an exception but instead enqueue all of the changes. This gives us a chance to\n * recompute the hash graph and eliminate duplicates before raising an error to the application.\n *\n * Returns a two-element array `[applied, enqueued]`, where `applied` is an array of changes that\n * have been applied to the document, and `enqueued` is an array of changes that have not yet been\n * applied because they are missing a dependency.\n */\nfunction applyChanges(patches, decodedChanges, docState, objectIds, throwExceptions) {\n  let heads = new Set(docState.heads), changeHashes = new Set()\n  let clock = copyObject(docState.clock)\n  let applied = [], enqueued = []\n\n  for (let change of decodedChanges) {\n    // Skip any duplicate changes that we have already seen\n    if (docState.changeIndexByHash[change.hash] !== undefined || changeHashes.has(change.hash)) continue\n\n    const expectedSeq = (clock[change.actor] || 0) + 1\n    let causallyReady = true\n\n    for (let dep of change.deps) {\n      const depIndex = docState.changeIndexByHash[dep]\n      if ((depIndex === undefined || depIndex === -1) && !changeHashes.has(dep)) {\n        causallyReady = false\n      }\n    }\n\n    if (!causallyReady) {\n      enqueued.push(change)\n    } else if (change.seq < expectedSeq) {\n      if (throwExceptions) {\n        throw new RangeError(`Reuse of sequence number ${change.seq} for actor ${change.actor}`)\n      } else {\n        return [[], decodedChanges]\n      }\n    } else if (change.seq > expectedSeq) {\n      throw new RangeError(`Skipped sequence number ${expectedSeq} for actor ${change.actor}`)\n    } else {\n      clock[change.actor] = change.seq\n      changeHashes.add(change.hash)\n      for (let dep of change.deps) heads.delete(dep)\n      heads.add(change.hash)\n      applied.push(change)\n    }\n  }\n\n  if (applied.length > 0) {\n    let changeState = {changes: applied, changeIndex: -1, objectIds}\n    readNextChangeOp(docState, changeState)\n    while (!changeState.done) applyOps(patches, changeState, docState)\n\n    docState.heads = [...heads].sort()\n    docState.clock = clock\n  }\n  return [applied, enqueued]\n}\n\n/**\n * Scans the operations in a document and generates a patch that can be sent to the frontend to\n * instantiate the current state of the document. `objectMeta` is mutated to contain information\n * about the parent and children of each object in the document.\n */\nfunction documentPatch(docState) {\n  for (let col of docState.blocks[0].columns) col.decoder.reset()\n  let propState = {}, docOp = null, blockIndex = 0\n  let patches = {_root: {objectId: '_root', type: 'map', props: {}}}\n  let lastObjActor = null, lastObjCtr = null, objectId = '_root', elemVisible = false, listIndex = 0\n\n  while (true) {\n    ({ docOp, blockIndex } = readNextDocOp(docState, blockIndex))\n    if (docOp === null) break\n    if (docOp[objActorIdx] !== lastObjActor || docOp[objCtrIdx] !== lastObjCtr) {\n      objectId = `${docOp[objCtrIdx]}@${docState.actorIds[docOp[objActorIdx]]}`\n      lastObjActor = docOp[objActorIdx]\n      lastObjCtr = docOp[objCtrIdx]\n      propState = {}\n      listIndex = 0\n      elemVisible = false\n    }\n\n    if (docOp[insertIdx] && elemVisible) {\n      elemVisible = false\n      listIndex++\n    }\n    if (docOp[succNumIdx] === 0) elemVisible = true\n    if (docOp[idCtrIdx] > docState.maxOp) docState.maxOp = docOp[idCtrIdx]\n    for (let i = 0; i < docOp[succNumIdx]; i++) {\n      if (docOp[succCtrIdx][i] > docState.maxOp) docState.maxOp = docOp[succCtrIdx][i]\n    }\n\n    updatePatchProperty(patches, null, objectId, docOp, docState, propState, listIndex, docOp[succNumIdx])\n  }\n  return patches._root\n}\n\n/**\n * Takes an encoded document whose headers have been parsed using `decodeDocumentHeader()` and reads\n * from it the list of changes. Returns the document's current vector clock, i.e. an object mapping\n * each actor ID (as a hex string) to the number of changes seen from that actor. Also returns an\n * array of the actorIds whose most recent change has no dependents (i.e. the actors that\n * contributed the current heads of the document), and an array of encoders that has been\n * initialised to contain the columns of the changes list.\n */\nfunction readDocumentChanges(doc) {\n  const columns = makeDecoders(doc.changesColumns, DOCUMENT_COLUMNS)\n  const actorD = columns[0].decoder, seqD = columns[1].decoder\n  const depsNumD = columns[5].decoder, depsIndexD = columns[6].decoder\n  if (columns[0].columnId !== DOCUMENT_COLUMNS[0].columnId || DOCUMENT_COLUMNS[0].columnName !== 'actor' ||\n      columns[1].columnId !== DOCUMENT_COLUMNS[1].columnId || DOCUMENT_COLUMNS[1].columnName !== 'seq' ||\n      columns[5].columnId !== DOCUMENT_COLUMNS[5].columnId || DOCUMENT_COLUMNS[5].columnName !== 'depsNum' ||\n      columns[6].columnId !== DOCUMENT_COLUMNS[6].columnId || DOCUMENT_COLUMNS[6].columnName !== 'depsIndex') {\n    throw new RangeError('unexpected columnId')\n  }\n\n  let numChanges = 0, clock = {}, actorNums = [], headIndexes = new Set()\n  while (!actorD.done) {\n    const actorNum = actorD.readValue(), seq = seqD.readValue(), depsNum = depsNumD.readValue()\n    const actorId = doc.actorIds[actorNum]\n    if (seq !== 1 && seq !== clock[actorId] + 1) {\n      throw new RangeError(`Expected seq ${clock[actorId] + 1}, got ${seq} for actor ${actorId}`)\n    }\n    actorNums.push(actorNum)\n    clock[actorId] = seq\n    headIndexes.add(numChanges)\n    for (let j = 0; j < depsNum; j++) headIndexes.delete(depsIndexD.readValue())\n    numChanges++\n  }\n  const headActors = [...headIndexes].map(index => doc.actorIds[actorNums[index]]).sort()\n\n  for (let col of columns) col.decoder.reset()\n  const encoders = columns.map(col => ({columnId: col.columnId, encoder: encoderByColumnId(col.columnId)}))\n  copyColumns(encoders, columns, numChanges)\n  return {clock, headActors, encoders, numChanges}\n}\n\n/**\n * Records the metadata about a change in the appropriate columns.\n */\nfunction appendChange(columns, change, actorIds, changeIndexByHash) {\n  appendOperation(columns, DOCUMENT_COLUMNS, [\n    actorIds.indexOf(change.actor), // actor\n    change.seq, // seq\n    change.maxOp, // maxOp\n    change.time, // time\n    change.message, // message\n    change.deps.length, // depsNum\n    change.deps.map(dep => changeIndexByHash[dep]), // depsIndex\n    change.extraBytes ? (change.extraBytes.byteLength << 4 | VALUE_TYPE.BYTES) : VALUE_TYPE.BYTES, // extraLen\n    change.extraBytes // extraRaw\n  ])\n}\n\nclass BackendDoc {\n  constructor(buffer) {\n    this.maxOp = 0\n    this.haveHashGraph = false\n    this.changes = []\n    this.changeIndexByHash = {}\n    this.dependenciesByHash = {}\n    this.dependentsByHash = {}\n    this.hashesByActor = {}\n    this.actorIds = []\n    this.heads = []\n    this.clock = {}\n    this.queue = []\n    this.objectMeta = {_root: {parentObj: null, parentKey: null, opId: null, type: 'map', children: {}}}\n\n    if (buffer) {\n      const doc = decodeDocumentHeader(buffer)\n      const {clock, headActors, encoders, numChanges} = readDocumentChanges(doc)\n      this.binaryDoc = buffer\n      this.changes = new Array(numChanges)\n      this.actorIds = doc.actorIds\n      this.heads = doc.heads\n      this.clock = clock\n      this.changesEncoders = encoders\n      this.extraBytes = doc.extraBytes\n\n      // If there is a single head, we can unambiguously point at the actorId and sequence number of\n      // the head hash without having to reconstruct the hash graph\n      if (doc.heads.length === 1 && headActors.length === 1) {\n        this.hashesByActor[headActors[0]] = []\n        this.hashesByActor[headActors[0]][clock[headActors[0]] - 1] = doc.heads[0]\n      }\n\n      // The encoded document gives each change an index, and expresses dependencies in terms of\n      // those indexes. Initialise the translation table from hash to index.\n      if (doc.heads.length === doc.headsIndexes.length) {\n        for (let i = 0; i < doc.heads.length; i++) {\n          this.changeIndexByHash[doc.heads[i]] = doc.headsIndexes[i]\n        }\n      } else if (doc.heads.length === 1) {\n        // If there is only one head, it must be the last change\n        this.changeIndexByHash[doc.heads[0]] = numChanges - 1\n      } else {\n        // We know the heads hashes, but not their indexes\n        for (let head of doc.heads) this.changeIndexByHash[head] = -1\n      }\n\n      this.blocks = [{columns: makeDecoders(doc.opsColumns, DOC_OPS_COLUMNS)}]\n      updateBlockMetadata(this.blocks[0])\n      if (this.blocks[0].numOps > MAX_BLOCK_SIZE) {\n        this.blocks = splitBlock(this.blocks[0])\n      }\n\n      let docState = {blocks: this.blocks, actorIds: this.actorIds, objectMeta: this.objectMeta, maxOp: 0}\n      this.initPatch = documentPatch(docState)\n      this.maxOp = docState.maxOp\n\n    } else {\n      this.haveHashGraph = true\n      this.changesEncoders = DOCUMENT_COLUMNS.map(col => ({columnId: col.columnId, encoder: encoderByColumnId(col.columnId)}))\n      this.blocks = [{\n        columns: makeDecoders([], DOC_OPS_COLUMNS),\n        bloom: new Uint8Array(BLOOM_FILTER_SIZE),\n        numOps: 0,\n        lastKey: undefined,\n        numVisible: undefined,\n        lastObjectActor: undefined,\n        lastObjectCtr: undefined,\n        firstVisibleActor: undefined,\n        firstVisibleCtr: undefined,\n        lastVisibleActor: undefined,\n        lastVisibleCtr: undefined\n      }]\n    }\n  }\n\n  /**\n   * Makes a copy of this BackendDoc that can be independently modified.\n   */\n  clone() {\n    if (!this.haveHashGraph) this.computeHashGraph()\n    let copy = new BackendDoc()\n    copy.maxOp = this.maxOp\n    copy.haveHashGraph = this.haveHashGraph\n    copy.changes = this.changes.slice()\n    copy.changeIndexByHash = copyObject(this.changeIndexByHash)\n    copy.dependenciesByHash = copyObject(this.dependenciesByHash)\n    copy.dependentsByHash = Object.entries(this.dependentsByHash).reduce((acc, [k, v]) => { acc[k] = v.slice(); return acc }, {})\n    copy.hashesByActor = Object.entries(this.hashesByActor).reduce((acc, [k, v]) => { acc[k] = v.slice(); return acc }, {})\n    copy.actorIds = this.actorIds // immutable, no copying needed\n    copy.heads = this.heads // immutable, no copying needed\n    copy.clock = this.clock // immutable, no copying needed\n    copy.blocks = this.blocks // immutable, no copying needed\n    copy.objectMeta = this.objectMeta // immutable, no copying needed\n    copy.queue = this.queue // immutable, no copying needed\n    return copy\n  }\n\n  /**\n   * Parses the changes given as Uint8Arrays in `changeBuffers`, and applies them to the current\n   * document. Returns a patch to apply to the frontend. If an exception is thrown, the document\n   * object is not modified.\n   */\n  applyChanges(changeBuffers, isLocal = false) {\n    if (changeBuffers instanceof Uint8Array) {\n      throw new TypeError('applyChanges takes an array of Uint8Arrays, not just a single Uint8Array')\n    }\n    if (!Array.isArray(changeBuffers)) {\n      throw new TypeError('applyChanges takes an array of Uint8Arrays')\n    }\n\n    // decoded change has the form { actor, seq, startOp, time, message, deps, actorIds, hash, columns, buffer }\n    let decodedChanges = changeBuffers.map(buffer => {\n      const decoded = decodeChangeColumns(buffer)\n      decoded.buffer = buffer\n      return decoded\n    })\n\n    let patches = {_root: {objectId: '_root', type: 'map', props: {}}}\n    let docState = {\n      maxOp: this.maxOp,\n      changeIndexByHash: this.changeIndexByHash,\n      actorIds: this.actorIds,\n      heads: this.heads,\n      clock: this.clock,\n      blocks: this.blocks.slice(),\n      objectMeta: Object.assign({}, this.objectMeta)\n    }\n    let queue = (this.queue.length === 0) ? decodedChanges : decodedChanges.concat(this.queue)\n    let allApplied = [], objectIds = new Set()\n\n    while (true) {\n      const [applied, enqueued] = applyChanges(patches, queue, docState, objectIds, this.haveHashGraph)\n      queue = enqueued\n      for (let i = 0; i < applied.length; i++) {\n        docState.changeIndexByHash[applied[i].hash] = this.changes.length + allApplied.length + i\n      }\n      if (applied.length > 0) allApplied = allApplied.concat(applied)\n      if (queue.length === 0) break\n\n      // If we are missing a dependency, and we haven't computed the hash graph yet, first compute\n      // the hashes to see if we actually have it already\n      if (applied.length === 0) {\n        if (this.haveHashGraph) break\n        this.computeHashGraph()\n        docState.changeIndexByHash = this.changeIndexByHash\n      }\n    }\n\n    setupPatches(patches, objectIds, docState)\n\n    // Update the document state only if `applyChanges` does not throw an exception\n    for (let change of allApplied) {\n      this.changes.push(change.buffer)\n      if (!this.hashesByActor[change.actor]) this.hashesByActor[change.actor] = []\n      this.hashesByActor[change.actor][change.seq - 1] = change.hash\n      this.changeIndexByHash[change.hash] = this.changes.length - 1\n      this.dependenciesByHash[change.hash] = change.deps\n      this.dependentsByHash[change.hash] = []\n      for (let dep of change.deps) {\n        if (!this.dependentsByHash[dep]) this.dependentsByHash[dep] = []\n        this.dependentsByHash[dep].push(change.hash)\n      }\n      appendChange(this.changesEncoders, change, docState.actorIds, this.changeIndexByHash)\n    }\n\n    this.maxOp        = docState.maxOp\n    this.actorIds     = docState.actorIds\n    this.heads        = docState.heads\n    this.clock        = docState.clock\n    this.blocks       = docState.blocks\n    this.objectMeta   = docState.objectMeta\n    this.queue        = queue\n    this.binaryDoc    = null\n    this.initPatch    = null\n\n    let patch = {\n      maxOp: this.maxOp, clock: this.clock, deps: this.heads,\n      pendingChanges: this.queue.length, diffs: patches._root\n    }\n    if (isLocal && decodedChanges.length === 1) {\n      patch.actor = decodedChanges[0].actor\n      patch.seq = decodedChanges[0].seq\n    }\n    return patch\n  }\n\n  /**\n   * Reconstructs the full change history of a document, and initialises the variables that allow us\n   * to traverse the hash graph of changes and their dependencies. When a compressed document is\n   * loaded we defer the computation of this hash graph to make loading faster, but if the hash\n   * graph is later needed (e.g. for the sync protocol), this function fills it in.\n   */\n  computeHashGraph() {\n    const binaryDoc = this.save()\n    this.haveHashGraph = true\n    this.changes = []\n    this.changeIndexByHash = {}\n    this.dependenciesByHash = {}\n    this.dependentsByHash = {}\n    this.hashesByActor = {}\n    this.clock = {}\n\n    for (let change of decodeChanges([binaryDoc])) {\n      const binaryChange = encodeChange(change) // TODO: avoid decoding and re-encoding again\n      this.changes.push(binaryChange)\n      this.changeIndexByHash[change.hash] = this.changes.length - 1\n      this.dependenciesByHash[change.hash] = change.deps\n      this.dependentsByHash[change.hash] = []\n      for (let dep of change.deps) this.dependentsByHash[dep].push(change.hash)\n      if (change.seq === 1) this.hashesByActor[change.actor] = []\n      this.hashesByActor[change.actor].push(change.hash)\n      const expectedSeq = (this.clock[change.actor] || 0) + 1\n      if (change.seq !== expectedSeq) {\n        throw new RangeError(`Expected seq ${expectedSeq}, got seq ${change.seq} from actor ${change.actor}`)\n      }\n      this.clock[change.actor] = change.seq\n    }\n  }\n\n  /**\n   * Returns all the changes that need to be sent to another replica. `haveDeps` is a list of change\n   * hashes (as hex strings) of the heads that the other replica has. The changes in `haveDeps` and\n   * any of their transitive dependencies will not be returned; any changes later than or concurrent\n   * to the hashes in `haveDeps` will be returned. If `haveDeps` is an empty array, all changes are\n   * returned. Throws an exception if any of the given hashes are not known to this replica.\n   */\n  getChanges(haveDeps) {\n    if (!this.haveHashGraph) this.computeHashGraph()\n\n    // If the other replica has nothing, return all changes in history order\n    if (haveDeps.length === 0) {\n      return this.changes.slice()\n    }\n\n    // Fast path for the common case where all new changes depend only on haveDeps\n    let stack = [], seenHashes = {}, toReturn = []\n    for (let hash of haveDeps) {\n      seenHashes[hash] = true\n      const successors = this.dependentsByHash[hash]\n      if (!successors) throw new RangeError(`hash not found: ${hash}`)\n      stack.push(...successors)\n    }\n\n    // Depth-first traversal of the hash graph to find all changes that depend on `haveDeps`\n    while (stack.length > 0) {\n      const hash = stack.pop()\n      seenHashes[hash] = true\n      toReturn.push(hash)\n      if (!this.dependenciesByHash[hash].every(dep => seenHashes[dep])) {\n        // If a change depends on a hash we have not seen, abort the traversal and fall back to the\n        // slower algorithm. This will sometimes abort even if all new changes depend on `haveDeps`,\n        // because our depth-first traversal is not necessarily a topological sort of the graph.\n        break\n      }\n      stack.push(...this.dependentsByHash[hash])\n    }\n\n    // If the traversal above has encountered all the heads, and was not aborted early due to\n    // a missing dependency, then the set of changes it has found is complete, so we can return it\n    if (stack.length === 0 && this.heads.every(head => seenHashes[head])) {\n      return toReturn.map(hash => this.changes[this.changeIndexByHash[hash]])\n    }\n\n    // If we haven't encountered all of the heads, we have to search harder. This will happen if\n    // changes were added that are concurrent to `haveDeps`\n    stack = haveDeps.slice()\n    seenHashes = {}\n    while (stack.length > 0) {\n      const hash = stack.pop()\n      if (!seenHashes[hash]) {\n        const deps = this.dependenciesByHash[hash]\n        if (!deps) throw new RangeError(`hash not found: ${hash}`)\n        stack.push(...deps)\n        seenHashes[hash] = true\n      }\n    }\n\n    return this.changes.filter(change => !seenHashes[decodeChangeMeta(change, true).hash])\n  }\n\n  /**\n   * Returns all changes that are present in this BackendDoc, but not present in the `other`\n   * BackendDoc.\n   */\n  getChangesAdded(other) {\n    if (!this.haveHashGraph) this.computeHashGraph()\n\n    // Depth-first traversal from the heads through the dependency graph,\n    // until we reach a change that is already present in opSet1\n    let stack = this.heads.slice(), seenHashes = {}, toReturn = []\n    while (stack.length > 0) {\n      const hash = stack.pop()\n      if (!seenHashes[hash] && other.changeIndexByHash[hash] === undefined) {\n        seenHashes[hash] = true\n        toReturn.push(hash)\n        stack.push(...this.dependenciesByHash[hash])\n      }\n    }\n\n    // Return those changes in the reverse of the order in which the depth-first search\n    // found them. This is not necessarily a topological sort, but should usually be close.\n    return toReturn.reverse().map(hash => this.changes[this.changeIndexByHash[hash]])\n  }\n\n  getChangeByHash(hash) {\n    if (!this.haveHashGraph) this.computeHashGraph()\n    return this.changes[this.changeIndexByHash[hash]]\n  }\n\n  /**\n   * Returns the hashes of any missing dependencies, i.e. where we have tried to apply a change that\n   * has a dependency on a change we have not seen.\n   *\n   * If the argument `heads` is given (an array of hexadecimal strings representing hashes as\n   * returned by `getHeads()`), this function also ensures that all of those hashes resolve to\n   * either a change that has been applied to the document, or that has been enqueued for later\n   * application once missing dependencies have arrived. Any missing heads hashes are included in\n   * the returned array.\n   */\n  getMissingDeps(heads = []) {\n    if (!this.haveHashGraph) this.computeHashGraph()\n\n    let allDeps = new Set(heads), inQueue = new Set()\n    for (let change of this.queue) {\n      inQueue.add(change.hash)\n      for (let dep of change.deps) allDeps.add(dep)\n    }\n\n    let missing = []\n    for (let hash of allDeps) {\n      if (this.changeIndexByHash[hash] === undefined && !inQueue.has(hash)) missing.push(hash)\n    }\n    return missing.sort()\n  }\n\n  /**\n   * Serialises the current document state into a single byte array.\n   */\n  save() {\n    if (this.binaryDoc) return this.binaryDoc\n\n    // Getting the byte array for the changes columns finalises their encoders, after which we can\n    // no longer append values to them. We therefore copy their data over to fresh encoders.\n    const newEncoders = this.changesEncoders.map(col => ({columnId: col.columnId, encoder: encoderByColumnId(col.columnId)}))\n    const decoders = this.changesEncoders.map(col => {\n      const decoder = decoderByColumnId(col.columnId, col.encoder.buffer)\n      return {columnId: col.columnId, decoder}\n    })\n    copyColumns(newEncoders, decoders, this.changes.length)\n\n    this.binaryDoc = encodeDocumentHeader({\n      changesColumns: this.changesEncoders,\n      opsColumns: concatBlocks(this.blocks),\n      actorIds: this.actorIds, // TODO: sort actorIds (requires transforming all actorId columns in opsColumns)\n      heads: this.heads,\n      headsIndexes: this.heads.map(hash => this.changeIndexByHash[hash]),\n      extraBytes: this.extraBytes\n    })\n    this.changesEncoders = newEncoders\n    return this.binaryDoc\n  }\n\n  /**\n   * Returns a patch from which we can initialise the current state of the backend.\n   */\n  getPatch() {\n    const objectMeta = {_root: {parentObj: null, parentKey: null, opId: null, type: 'map', children: {}}}\n    const docState = {blocks: this.blocks, actorIds: this.actorIds, objectMeta, maxOp: 0}\n    const diffs = this.initPatch ? this.initPatch : documentPatch(docState)\n    return {\n      maxOp: this.maxOp, clock: this.clock, deps: this.heads,\n      pendingChanges: this.queue.length, diffs\n    }\n  }\n}\n\nmodule.exports = { MAX_BLOCK_SIZE, BackendDoc, bloomFilterContains }\n"
  },
  {
    "path": "backend/sync.js",
    "content": "/**\n * Implementation of the data synchronisation protocol that brings a local and a remote document\n * into the same state. This is typically used when two nodes have been disconnected for some time,\n * and need to exchange any changes that happened while they were disconnected. The two nodes that\n * are syncing could be client and server, or server and client, or two peers with symmetric roles.\n *\n * The protocol is based on this paper: Martin Kleppmann and Heidi Howard. Byzantine Eventual\n * Consistency and the Fundamental Limits of Peer-to-Peer Databases. https://arxiv.org/abs/2012.00472\n *\n * The protocol assumes that every time a node successfully syncs with another node, it remembers\n * the current heads (as returned by `Backend.getHeads()`) after the last sync with that node. The\n * next time we try to sync with the same node, we start from the assumption that the other node's\n * document version is no older than the outcome of the last sync, so we only need to exchange any\n * changes that are more recent than the last sync. This assumption may not be true if the other\n * node did not correctly persist its state (perhaps it crashed before writing the result of the\n * last sync to disk), and we fall back to sending the entire document in this case.\n */\n\nconst Backend = require('./backend')\nconst { hexStringToBytes, bytesToHexString, Encoder, Decoder } = require('./encoding')\nconst { decodeChangeMeta } = require('./columnar')\nconst { copyObject } = require('../src/common')\n\nconst HASH_SIZE = 32 // 256 bits = 32 bytes\nconst MESSAGE_TYPE_SYNC = 0x42 // first byte of a sync message, for identification\nconst PEER_STATE_TYPE = 0x43 // first byte of an encoded peer state, for identification\n\n// These constants correspond to a 1% false positive rate. The values can be changed without\n// breaking compatibility of the network protocol, since the parameters used for a particular\n// Bloom filter are encoded in the wire format.\nconst BITS_PER_ENTRY = 10, NUM_PROBES = 7\n\n/**\n * A Bloom filter implementation that can be serialised to a byte array for transmission\n * over a network. The entries that are added are assumed to already be SHA-256 hashes,\n * so this implementation does not perform its own hashing.\n */\nclass BloomFilter {\n  constructor (arg) {\n    if (Array.isArray(arg)) {\n      // arg is an array of SHA256 hashes in hexadecimal encoding\n      this.numEntries = arg.length\n      this.numBitsPerEntry = BITS_PER_ENTRY\n      this.numProbes = NUM_PROBES\n      this.bits = new Uint8Array(Math.ceil(this.numEntries * this.numBitsPerEntry / 8))\n      for (let hash of arg) this.addHash(hash)\n    } else if (arg instanceof Uint8Array) {\n      if (arg.byteLength === 0) {\n        this.numEntries = 0\n        this.numBitsPerEntry = 0\n        this.numProbes = 0\n        this.bits = arg\n      } else {\n        const decoder = new Decoder(arg)\n        this.numEntries = decoder.readUint32()\n        this.numBitsPerEntry = decoder.readUint32()\n        this.numProbes = decoder.readUint32()\n        this.bits = decoder.readRawBytes(Math.ceil(this.numEntries * this.numBitsPerEntry / 8))\n      }\n    } else {\n      throw new TypeError('invalid argument')\n    }\n  }\n\n  /**\n   * Returns the Bloom filter state, encoded as a byte array.\n   */\n  get bytes() {\n    if (this.numEntries === 0) return new Uint8Array(0)\n    const encoder = new Encoder()\n    encoder.appendUint32(this.numEntries)\n    encoder.appendUint32(this.numBitsPerEntry)\n    encoder.appendUint32(this.numProbes)\n    encoder.appendRawBytes(this.bits)\n    return encoder.buffer\n  }\n\n  /**\n   * Given a SHA-256 hash (as hex string), returns an array of probe indexes indicating which bits\n   * in the Bloom filter need to be tested or set for this particular entry. We do this by\n   * interpreting the first 12 bytes of the hash as three little-endian 32-bit unsigned integers,\n   * and then using triple hashing to compute the probe indexes. The algorithm comes from:\n   *\n   * Peter C. Dillinger and Panagiotis Manolios. Bloom Filters in Probabilistic Verification.\n   * 5th International Conference on Formal Methods in Computer-Aided Design (FMCAD), November 2004.\n   * http://www.ccis.northeastern.edu/home/pete/pub/bloom-filters-verification.pdf\n   */\n  getProbes(hash) {\n    const hashBytes = hexStringToBytes(hash), modulo = 8 * this.bits.byteLength\n    if (hashBytes.byteLength !== 32) throw new RangeError(`Not a 256-bit hash: ${hash}`)\n    // on the next three lines, the right shift means interpret value as unsigned\n    let x = ((hashBytes[0] | hashBytes[1] << 8 | hashBytes[2]  << 16 | hashBytes[3]  << 24) >>> 0) % modulo\n    let y = ((hashBytes[4] | hashBytes[5] << 8 | hashBytes[6]  << 16 | hashBytes[7]  << 24) >>> 0) % modulo\n    let z = ((hashBytes[8] | hashBytes[9] << 8 | hashBytes[10] << 16 | hashBytes[11] << 24) >>> 0) % modulo\n    const probes = [x]\n    for (let i = 1; i < this.numProbes; i++) {\n      x = (x + y) % modulo\n      y = (y + z) % modulo\n      probes.push(x)\n    }\n    return probes\n  }\n\n  /**\n   * Sets the Bloom filter bits corresponding to a given SHA-256 hash (given as hex string).\n   */\n  addHash(hash) {\n    for (let probe of this.getProbes(hash)) {\n      this.bits[probe >>> 3] |= 1 << (probe & 7)\n    }\n  }\n\n  /**\n   * Tests whether a given SHA-256 hash (given as hex string) is contained in the Bloom filter.\n   */\n  containsHash(hash) {\n    if (this.numEntries === 0) return false\n    for (let probe of this.getProbes(hash)) {\n      if ((this.bits[probe >>> 3] & (1 << (probe & 7))) === 0) {\n        return false\n      }\n    }\n    return true\n  }\n}\n\n/**\n * Encodes a sorted array of SHA-256 hashes (as hexadecimal strings) into a byte array.\n */\nfunction encodeHashes(encoder, hashes) {\n  if (!Array.isArray(hashes)) throw new TypeError('hashes must be an array')\n  encoder.appendUint32(hashes.length)\n  for (let i = 0; i < hashes.length; i++) {\n    if (i > 0 && hashes[i - 1] >= hashes[i]) throw new RangeError('hashes must be sorted')\n    const bytes = hexStringToBytes(hashes[i])\n    if (bytes.byteLength !== HASH_SIZE) throw new TypeError('heads hashes must be 256 bits')\n    encoder.appendRawBytes(bytes)\n  }\n}\n\n/**\n * Decodes a byte array in the format returned by encodeHashes(), and returns its content as an\n * array of hex strings.\n */\nfunction decodeHashes(decoder) {\n  let length = decoder.readUint32(), hashes = []\n  for (let i = 0; i < length; i++) {\n    hashes.push(bytesToHexString(decoder.readRawBytes(HASH_SIZE)))\n  }\n  return hashes\n}\n\n/**\n * Takes a sync message of the form `{heads, need, have, changes}` and encodes it as a byte array for\n * transmission.\n */\nfunction encodeSyncMessage(message) {\n  const encoder = new Encoder()\n  encoder.appendByte(MESSAGE_TYPE_SYNC)\n  encodeHashes(encoder, message.heads)\n  encodeHashes(encoder, message.need)\n  encoder.appendUint32(message.have.length)\n  for (let have of message.have) {\n    encodeHashes(encoder, have.lastSync)\n    encoder.appendPrefixedBytes(have.bloom)\n  }\n  encoder.appendUint32(message.changes.length)\n  for (let change of message.changes) {\n    encoder.appendPrefixedBytes(change)\n  }\n  return encoder.buffer\n}\n\n/**\n * Takes a binary-encoded sync message and decodes it into the form `{heads, need, have, changes}`.\n */\nfunction decodeSyncMessage(bytes) {\n  const decoder = new Decoder(bytes)\n  const messageType = decoder.readByte()\n  if (messageType !== MESSAGE_TYPE_SYNC) {\n    throw new RangeError(`Unexpected message type: ${messageType}`)\n  }\n  const heads = decodeHashes(decoder)\n  const need = decodeHashes(decoder)\n  const haveCount = decoder.readUint32()\n  let message = {heads, need, have: [], changes: []}\n  for (let i = 0; i < haveCount; i++) {\n    const lastSync = decodeHashes(decoder)\n    const bloom = decoder.readPrefixedBytes(decoder)\n    message.have.push({lastSync, bloom})\n  }\n  const changeCount = decoder.readUint32()\n  for (let i = 0; i < changeCount; i++) {\n    const change = decoder.readPrefixedBytes()\n    message.changes.push(change)\n  }\n  // Ignore any trailing bytes -- they can be used for extensions by future versions of the protocol\n  return message\n}\n\n/**\n * Takes a SyncState and encodes as a byte array those parts of the state that should persist across\n * an application restart or disconnect and reconnect. The ephemeral parts of the state that should\n * be cleared on reconnect are not encoded.\n */\nfunction encodeSyncState(syncState) {\n  const encoder = new Encoder()\n  encoder.appendByte(PEER_STATE_TYPE)\n  encodeHashes(encoder, syncState.sharedHeads)\n  return encoder.buffer\n}\n\n/**\n * Takes a persisted peer state as encoded by `encodeSyncState` and decodes it into a SyncState\n * object. The parts of the peer state that were not encoded are initialised with default values.\n */\nfunction decodeSyncState(bytes) {\n  const decoder = new Decoder(bytes)\n  const recordType = decoder.readByte()\n  if (recordType !== PEER_STATE_TYPE) {\n    throw new RangeError(`Unexpected record type: ${recordType}`)\n  }\n  const sharedHeads = decodeHashes(decoder)\n  return Object.assign(initSyncState(), { sharedHeads })\n}\n\n/**\n * Constructs a Bloom filter containing all changes that are not one of the hashes in\n * `lastSync` or its transitive dependencies. In other words, the filter contains those\n * changes that have been applied since the version identified by `lastSync`. Returns\n * an object of the form `{lastSync, bloom}` as required for the `have` field of a sync\n * message.\n */\nfunction makeBloomFilter(backend, lastSync) {\n  const newChanges = Backend.getChanges(backend, lastSync)\n  const hashes = newChanges.map(change => decodeChangeMeta(change, true).hash)\n  return {lastSync, bloom: new BloomFilter(hashes).bytes}\n}\n\n/**\n * Call this function when a sync message is received from another node. The `message` argument\n * needs to already have been decoded using `decodeSyncMessage()`. This function determines the\n * changes that we need to send to the other node in response. Returns an array of changes (as\n * byte arrays).\n */\nfunction getChangesToSend(backend, have, need) {\n  if (have.length === 0) {\n    return need.map(hash => Backend.getChangeByHash(backend, hash)).filter(change => change !== undefined)\n  }\n\n  let lastSyncHashes = {}, bloomFilters = []\n  for (let h of have) {\n    for (let hash of h.lastSync) lastSyncHashes[hash] = true\n    bloomFilters.push(new BloomFilter(h.bloom))\n  }\n\n  // Get all changes that were added since the last sync\n  const changes = Backend.getChanges(backend, Object.keys(lastSyncHashes))\n    .map(change => decodeChangeMeta(change, true))\n\n  let changeHashes = {}, dependents = {}, hashesToSend = {}\n  for (let change of changes) {\n    changeHashes[change.hash] = true\n\n    // For each change, make a list of changes that depend on it\n    for (let dep of change.deps) {\n      if (!dependents[dep]) dependents[dep] = []\n      dependents[dep].push(change.hash)\n    }\n\n    // Exclude any change hashes contained in one or more Bloom filters\n    if (bloomFilters.every(bloom => !bloom.containsHash(change.hash))) {\n      hashesToSend[change.hash] = true\n    }\n  }\n\n  // Include any changes that depend on a Bloom-negative change\n  let stack = Object.keys(hashesToSend)\n  while (stack.length > 0) {\n    const hash = stack.pop()\n    if (dependents[hash]) {\n      for (let dep of dependents[hash]) {\n        if (!hashesToSend[dep]) {\n          hashesToSend[dep] = true\n          stack.push(dep)\n        }\n      }\n    }\n  }\n\n  // Include any explicitly requested changes\n  let changesToSend = []\n  for (let hash of need) {\n    hashesToSend[hash] = true\n    if (!changeHashes[hash]) { // Change is not among those returned by getMissingChanges()?\n      const change = Backend.getChangeByHash(backend, hash)\n      if (change) changesToSend.push(change)\n    }\n  }\n\n  // Return changes in the order they were returned by getMissingChanges()\n  for (let change of changes) {\n    if (hashesToSend[change.hash]) changesToSend.push(change.change)\n  }\n  return changesToSend\n}\n\nfunction initSyncState() {\n  return {\n    sharedHeads: [],\n    lastSentHeads: [],\n    theirHeads: null,\n    theirNeed: null,\n    theirHave: null,\n    sentHashes: {},\n  }\n}\n\nfunction compareArrays(a, b) {\n    return (a.length === b.length) && a.every((v, i) => v === b[i])\n}\n\n/**\n * Given a backend and what we believe to be the state of our peer, generate a message which tells\n * them about we have and includes any changes we believe they need\n */\nfunction generateSyncMessage(backend, syncState) {\n  if (!backend) {\n    throw new Error(\"generateSyncMessage called with no Automerge document\")\n  }\n  if (!syncState) {\n    throw new Error(\"generateSyncMessage requires a syncState, which can be created with initSyncState()\")\n  }\n\n  let { sharedHeads, lastSentHeads, theirHeads, theirNeed, theirHave, sentHashes } = syncState\n  const ourHeads = Backend.getHeads(backend)\n\n  // Hashes to explicitly request from the remote peer: any missing dependencies of unapplied\n  // changes, and any of the remote peer's heads that we don't know about\n  const ourNeed = Backend.getMissingDeps(backend, theirHeads || [])\n\n  // There are two reasons why ourNeed may be nonempty: 1. we might be missing dependencies due to\n  // Bloom filter false positives; 2. we might be missing heads that the other peer mentioned\n  // because they (intentionally) only sent us a subset of changes. In case 1, we leave the `have`\n  // field of the message empty because we just want to fill in the missing dependencies for now.\n  // In case 2, or if ourNeed is empty, we send a Bloom filter to request any unsent changes.\n  let ourHave = []\n  if (!theirHeads || ourNeed.every(hash => theirHeads.includes(hash))) {\n    ourHave = [makeBloomFilter(backend, sharedHeads)]\n  }\n\n  // Fall back to a full re-sync if the sender's last sync state includes hashes\n  // that we don't know. This could happen if we crashed after the last sync and\n  // failed to persist changes that the other node already sent us.\n  if (theirHave && theirHave.length > 0) {\n    const lastSync = theirHave[0].lastSync\n    if (!lastSync.every(hash => Backend.getChangeByHash(backend, hash))) {\n      // we need to queue them to send us a fresh sync message, the one they sent is uninteligible so we don't know what they need\n      const resetMsg = {heads: ourHeads, need: [], have: [{ lastSync: [], bloom: new Uint8Array(0) }], changes: []}\n      return [syncState, encodeSyncMessage(resetMsg)]\n    }\n  }\n\n  // XXX: we should limit ourselves to only sending a subset of all the messages, probably limited by a total message size\n  //      these changes should ideally be RLE encoded but we haven't implemented that yet.\n  let changesToSend = Array.isArray(theirHave) && Array.isArray(theirNeed) ? getChangesToSend(backend, theirHave, theirNeed) : []\n\n  // If the heads are equal, we're in sync and don't need to do anything further\n  const headsUnchanged = Array.isArray(lastSentHeads) && compareArrays(ourHeads, lastSentHeads)\n  const headsEqual = Array.isArray(theirHeads) && compareArrays(ourHeads, theirHeads)\n  if (headsUnchanged && headsEqual && changesToSend.length === 0) {\n    // no need to send a sync message if we know we're synced!\n    return [syncState, null]\n  }\n\n  // TODO: this recomputes the SHA-256 hash of each change; we should restructure this to avoid the\n  // unnecessary recomputation\n  changesToSend = changesToSend.filter(change => !sentHashes[decodeChangeMeta(change, true).hash])\n\n  // Regular response to a sync message: send any changes that the other node\n  // doesn't have. We leave the \"have\" field empty because the previous message\n  // generated by `syncStart` already indicated what changes we have.\n  const syncMessage = {heads: ourHeads, have: ourHave, need: ourNeed, changes: changesToSend}\n  if (changesToSend.length > 0) {\n    sentHashes = copyObject(sentHashes)\n    for (const change of changesToSend) {\n      sentHashes[decodeChangeMeta(change, true).hash] = true\n    }\n  }\n\n  syncState = Object.assign({}, syncState, {lastSentHeads: ourHeads, sentHashes})\n  return [syncState, encodeSyncMessage(syncMessage)]\n}\n\n/**\n * Computes the heads that we share with a peer after we have just received some changes from that\n * peer and applied them. This may not be sufficient to bring our heads in sync with the other\n * peer's heads, since they may have only sent us a subset of their outstanding changes.\n *\n * `myOldHeads` are the local heads before the most recent changes were applied, `myNewHeads` are\n * the local heads after those changes were applied, and `ourOldSharedHeads` is the previous set of\n * shared heads. Applying the changes will have replaced some heads with others, but some heads may\n * have remained unchanged (because they are for branches on which no changes have been added). Any\n * such unchanged heads remain in the sharedHeads. Any sharedHeads that were replaced by applying\n * changes are also replaced as sharedHeads. This is safe because if we received some changes from\n * another peer, that means that peer had those changes, and therefore we now both know about them.\n */\nfunction advanceHeads(myOldHeads, myNewHeads, ourOldSharedHeads) {\n  const newHeads = myNewHeads.filter((head) => !myOldHeads.includes(head))\n  const commonHeads = ourOldSharedHeads.filter((head) => myNewHeads.includes(head))\n  const advancedHeads = [...new Set([...newHeads, ...commonHeads])].sort()\n  return advancedHeads\n}\n\n\n/**\n * Given a backend, a message message and the state of our peer, apply any changes, update what\n * we believe about the peer, and (if there were applied changes) produce a patch for the frontend\n */\nfunction receiveSyncMessage(backend, oldSyncState, binaryMessage) {\n  if (!backend) {\n    throw new Error(\"generateSyncMessage called with no Automerge document\")\n  }\n  if (!oldSyncState) {\n    throw new Error(\"generateSyncMessage requires a syncState, which can be created with initSyncState()\")\n  }\n\n  let { sharedHeads, lastSentHeads, sentHashes } = oldSyncState, patch = null\n  const message = decodeSyncMessage(binaryMessage)\n  const beforeHeads = Backend.getHeads(backend)\n\n  // If we received changes, we try to apply them to the document. There may still be missing\n  // dependencies due to Bloom filter false positives, in which case the backend will enqueue the\n  // changes without applying them. The set of changes may also be incomplete if the sender decided\n  // to break a large set of changes into chunks.\n  if (message.changes.length > 0) {\n    [backend, patch] = Backend.applyChanges(backend, message.changes)\n    sharedHeads = advanceHeads(beforeHeads, Backend.getHeads(backend), sharedHeads)\n  }\n\n  // If heads are equal, indicate we don't need to send a response message\n  if (message.changes.length === 0 && compareArrays(message.heads, beforeHeads)) {\n    lastSentHeads = message.heads\n  }\n\n  // If all of the remote heads are known to us, that means either our heads are equal, or we are\n  // ahead of the remote peer. In this case, take the remote heads to be our shared heads.\n  const knownHeads = message.heads.filter(head => Backend.getChangeByHash(backend, head))\n  if (knownHeads.length === message.heads.length) {\n    sharedHeads = message.heads\n    // If the remote peer has lost all its data, reset our state to perform a full resync\n    if (message.heads.length === 0) {\n      lastSentHeads = []\n      sentHashes = []\n    }\n  } else {\n    // If some remote heads are unknown to us, we add all the remote heads we know to\n    // sharedHeads, but don't remove anything from sharedHeads. This might cause sharedHeads to\n    // contain some redundant hashes (where one hash is actually a transitive dependency of\n    // another), but this will be cleared up as soon as we know all the remote heads.\n    sharedHeads = [...new Set(knownHeads.concat(sharedHeads))].sort()\n  }\n\n  const syncState = {\n    sharedHeads, // what we have in common to generate an efficient bloom filter\n    lastSentHeads,\n    theirHave: message.have, // the information we need to calculate the changes they need\n    theirHeads: message.heads,\n    theirNeed: message.need,\n    sentHashes\n  }\n  return [backend, syncState, patch]\n}\n\nmodule.exports = {\n  receiveSyncMessage, generateSyncMessage,\n  encodeSyncMessage, decodeSyncMessage,\n  initSyncState, encodeSyncState, decodeSyncState,\n  BloomFilter // BloomFilter is a private API, exported only for testing purposes\n}\n"
  },
  {
    "path": "backend/util.js",
    "content": "function backendState(backend) {\n  if (backend.frozen) {\n    throw new Error(\n      'Attempting to use an outdated Automerge document that has already been updated. ' +\n      'Please use the latest document state, or call Automerge.clone() if you really ' +\n      'need to use this old document state.'\n    )\n  }\n  return backend.state\n}\n\nmodule.exports = {\n  backendState\n}\n"
  },
  {
    "path": "frontend/apply_patch.js",
    "content": "const { isObject, copyObject, parseOpId } = require('../src/common')\nconst { OBJECT_ID, CONFLICTS, ELEM_IDS } = require('./constants')\nconst { instantiateText } = require('./text')\nconst { instantiateTable } = require('./table')\nconst { Counter } = require('./counter')\n\n/**\n * Reconstructs the value from the patch object `patch`.\n */\nfunction getValue(patch, object, updated) {\n  if (patch.objectId) {\n    // If the objectId of the existing object does not match the objectId in the patch,\n    // that means the patch is replacing the object with a new one made from scratch\n    if (object && object[OBJECT_ID] !== patch.objectId) {\n      object = undefined\n    }\n    return interpretPatch(patch, object, updated)\n  } else if (patch.datatype === 'timestamp') {\n    // Timestamp: value is milliseconds since 1970 epoch\n    return new Date(patch.value)\n  } else if (patch.datatype === 'counter') {\n    return new Counter(patch.value)\n  } else {\n    // Primitive value (int, uint, float64, string, boolean, or null)\n    return patch.value\n  }\n}\n\n/**\n * Compares two strings, interpreted as Lamport timestamps of the form\n * 'counter@actorId'. Returns 1 if ts1 is greater, or -1 if ts2 is greater.\n */\nfunction lamportCompare(ts1, ts2) {\n  const regex = /^(\\d+)@(.*)$/\n  const time1 = regex.test(ts1) ? parseOpId(ts1) : {counter: 0, actorId: ts1}\n  const time2 = regex.test(ts2) ? parseOpId(ts2) : {counter: 0, actorId: ts2}\n  if (time1.counter < time2.counter) return -1\n  if (time1.counter > time2.counter) return  1\n  if (time1.actorId < time2.actorId) return -1\n  if (time1.actorId > time2.actorId) return  1\n  return 0\n}\n\n/**\n * `props` is an object of the form:\n * `{key1: {opId1: {...}, opId2: {...}}, key2: {opId3: {...}}}`\n * where the outer object is a mapping from property names to inner objects,\n * and the inner objects are a mapping from operation ID to sub-patch.\n * This function interprets that structure and updates the objects `object` and\n * `conflicts` to reflect it. For each key, the greatest opId (by Lamport TS\n * order) is chosen as the default resolution; that op's value is assigned\n * to `object[key]`. Moreover, all the opIds and values are packed into a\n * conflicts object of the form `{opId1: value1, opId2: value2}` and assigned\n * to `conflicts[key]`. If there is no conflict, the conflicts object contains\n * just a single opId-value mapping.\n */\nfunction applyProperties(props, object, conflicts, updated) {\n  if (!props) return\n\n  for (let key of Object.keys(props)) {\n    const values = {}, opIds = Object.keys(props[key]).sort(lamportCompare).reverse()\n    for (let opId of opIds) {\n      const subpatch = props[key][opId]\n      if (conflicts[key] && conflicts[key][opId]) {\n        values[opId] = getValue(subpatch, conflicts[key][opId], updated)\n      } else {\n        values[opId] = getValue(subpatch, undefined, updated)\n      }\n    }\n\n    if (opIds.length === 0) {\n      delete object[key]\n      delete conflicts[key]\n    } else {\n      object[key] = values[opIds[0]]\n      conflicts[key] = values\n    }\n  }\n}\n\n/**\n * Creates a writable copy of an immutable map object. If `originalObject`\n * is undefined, creates an empty object with ID `objectId`.\n */\nfunction cloneMapObject(originalObject, objectId) {\n  const object    = copyObject(originalObject)\n  const conflicts = copyObject(originalObject ? originalObject[CONFLICTS] : undefined)\n  Object.defineProperty(object, OBJECT_ID, {value: objectId})\n  Object.defineProperty(object, CONFLICTS, {value: conflicts})\n  return object\n}\n\n/**\n * Updates the map object `obj` according to the modifications described in\n * `patch`, or creates a new object if `obj` is undefined. Mutates `updated`\n * to map the objectId to the new object, and returns the new object.\n */\nfunction updateMapObject(patch, obj, updated) {\n  const objectId = patch.objectId\n  if (!updated[objectId]) {\n    updated[objectId] = cloneMapObject(obj, objectId)\n  }\n\n  const object = updated[objectId]\n  applyProperties(patch.props, object, object[CONFLICTS], updated)\n  return object\n}\n\n/**\n * Updates the table object `obj` according to the modifications described in\n * `patch`, or creates a new object if `obj` is undefined. Mutates `updated`\n * to map the objectId to the new object, and returns the new object.\n */\nfunction updateTableObject(patch, obj, updated) {\n  const objectId = patch.objectId\n  if (!updated[objectId]) {\n    updated[objectId] = obj ? obj._clone() : instantiateTable(objectId)\n  }\n\n  const object = updated[objectId]\n\n  for (let key of Object.keys(patch.props || {})) {\n    const opIds = Object.keys(patch.props[key])\n\n    if (opIds.length === 0) {\n      object.remove(key)\n    } else if (opIds.length === 1) {\n      const subpatch = patch.props[key][opIds[0]]\n      object._set(key, getValue(subpatch, object.byId(key), updated), opIds[0])\n    } else {\n      throw new RangeError('Conflicts are not supported on properties of a table')\n    }\n  }\n  return object\n}\n\n/**\n * Creates a writable copy of an immutable list object. If `originalList` is\n * undefined, creates an empty list with ID `objectId`.\n */\nfunction cloneListObject(originalList, objectId) {\n  const list = originalList ? originalList.slice() : [] // slice() makes a shallow clone\n  const conflicts = (originalList && originalList[CONFLICTS]) ? originalList[CONFLICTS].slice() : []\n  const elemIds = (originalList && originalList[ELEM_IDS]) ? originalList[ELEM_IDS].slice() : []\n  Object.defineProperty(list, OBJECT_ID, {value: objectId})\n  Object.defineProperty(list, CONFLICTS, {value: conflicts})\n  Object.defineProperty(list, ELEM_IDS,  {value: elemIds})\n  return list\n}\n\n/**\n * Updates the list object `obj` according to the modifications described in\n * `patch`, or creates a new object if `obj` is undefined. Mutates `updated`\n * to map the objectId to the new object, and returns the new object.\n */\nfunction updateListObject(patch, obj, updated) {\n  const objectId = patch.objectId\n  if (!updated[objectId]) {\n    updated[objectId] = cloneListObject(obj, objectId)\n  }\n\n  const list = updated[objectId], conflicts = list[CONFLICTS], elemIds = list[ELEM_IDS]\n  for (let i = 0; i < patch.edits.length; i++) {\n    const edit = patch.edits[i]\n\n    if (edit.action === 'insert' || edit.action === 'update') {\n      const oldValue = conflicts[edit.index] && conflicts[edit.index][edit.opId]\n      let lastValue = getValue(edit.value, oldValue, updated)\n      let values = {[edit.opId]: lastValue}\n\n      // Successive updates for the same index are an indication of a conflict on that list element.\n      // Edits are sorted in increasing order by Lamport timestamp, so the last value (with the\n      // greatest timestamp) is the default resolution of the conflict.\n      while (i < patch.edits.length - 1 && patch.edits[i + 1].index === edit.index &&\n             patch.edits[i + 1].action === 'update') {\n        i++\n        const conflict = patch.edits[i]\n        const oldValue2 = conflicts[conflict.index] && conflicts[conflict.index][conflict.opId]\n        lastValue = getValue(conflict.value, oldValue2, updated)\n        values[conflict.opId] = lastValue\n      }\n\n      if (edit.action === 'insert') {\n        list.splice(edit.index, 0, lastValue)\n        conflicts.splice(edit.index, 0, values)\n        elemIds.splice(edit.index, 0, edit.elemId)\n      } else {\n        list[edit.index] = lastValue\n        conflicts[edit.index] = values\n      }\n\n    } else if (edit.action === 'multi-insert') {\n      const startElemId = parseOpId(edit.elemId), newElems = [], newValues = [], newConflicts = []\n      const datatype = edit.datatype\n      edit.values.forEach((value, index) => {\n        const elemId = `${startElemId.counter + index}@${startElemId.actorId}`\n        value = getValue({ value, datatype }, undefined, updated)\n        newValues.push(value)\n        newConflicts.push({[elemId]: {value, datatype, type: 'value'}})\n        newElems.push(elemId)\n      })\n      list.splice(edit.index, 0, ...newValues)\n      conflicts.splice(edit.index, 0, ...newConflicts)\n      elemIds.splice(edit.index, 0, ...newElems)\n\n    } else if (edit.action === 'remove') {\n      list.splice(edit.index, edit.count)\n      conflicts.splice(edit.index, edit.count)\n      elemIds.splice(edit.index, edit.count)\n    }\n  }\n  return list\n}\n\n/**\n * Updates the text object `obj` according to the modifications described in\n * `patch`, or creates a new object if `obj` is undefined. Mutates `updated`\n * to map the objectId to the new object, and returns the new object.\n */\nfunction updateTextObject(patch, obj, updated) {\n  const objectId = patch.objectId\n  let elems\n  if (updated[objectId]) {\n    elems = updated[objectId].elems\n  } else if (obj) {\n    elems = obj.elems.slice()\n  } else {\n    elems = []\n  }\n\n  for (const edit of patch.edits) {\n    if (edit.action === 'insert') {\n      const value = getValue(edit.value, undefined, updated)\n      const elem = {elemId: edit.elemId, pred: [edit.opId], value}\n      elems.splice(edit.index, 0, elem)\n\n    } else if (edit.action === 'multi-insert') {\n      const startElemId = parseOpId(edit.elemId)\n      const datatype = edit.datatype\n      const newElems = edit.values.map((value, index) => {\n        value = getValue({ datatype, value }, undefined, updated)\n        const elemId = `${startElemId.counter + index}@${startElemId.actorId}`\n        return {elemId, pred: [elemId], value}\n      })\n      elems.splice(edit.index, 0, ...newElems)\n\n    } else if (edit.action === 'update') {\n      const elemId = elems[edit.index].elemId\n      const value = getValue(edit.value, elems[edit.index].value, updated)\n      elems[edit.index] = {elemId, pred: [edit.opId], value}\n\n    } else if (edit.action === 'remove') {\n      elems.splice(edit.index, edit.count)\n    }\n  }\n\n  updated[objectId] = instantiateText(objectId, elems)\n  return updated[objectId]\n}\n\n/**\n * Applies the patch object `patch` to the read-only document object `obj`.\n * Clones a writable copy of `obj` and places it in `updated` (indexed by\n * objectId), if that has not already been done. Returns the updated object.\n */\nfunction interpretPatch(patch, obj, updated) {\n  // Return original object if it already exists and isn't being modified\n  if (isObject(obj) && (!patch.props || Object.keys(patch.props).length === 0) &&\n      (!patch.edits || patch.edits.length === 0) && !updated[patch.objectId]) {\n    return obj\n  }\n\n  if (patch.type === 'map') {\n    return updateMapObject(patch, obj, updated)\n  } else if (patch.type === 'table') {\n    return updateTableObject(patch, obj, updated)\n  } else if (patch.type === 'list') {\n    return updateListObject(patch, obj, updated)\n  } else if (patch.type === 'text') {\n    return updateTextObject(patch, obj, updated)\n  } else {\n    throw new TypeError(`Unknown object type: ${patch.type}`)\n  }\n}\n\n/**\n * Creates a writable copy of the immutable document root object `root`.\n */\nfunction cloneRootObject(root) {\n  if (root[OBJECT_ID] !== '_root') {\n    throw new RangeError(`Not the root object: ${root[OBJECT_ID]}`)\n  }\n  return cloneMapObject(root, '_root')\n}\n\nmodule.exports = {\n  interpretPatch, cloneRootObject\n}\n"
  },
  {
    "path": "frontend/constants.js",
    "content": "// Properties of the document root object\nconst OPTIONS   = Symbol('_options')   // object containing options passed to init()\nconst CACHE     = Symbol('_cache')     // map from objectId to immutable object\nconst STATE     = Symbol('_state')     // object containing metadata about current state (e.g. sequence numbers)\n\n// Properties of all Automerge objects\nconst OBJECT_ID = Symbol('_objectId')  // the object ID of the current object (string)\nconst CONFLICTS = Symbol('_conflicts') // map or list (depending on object type) of conflicts\nconst CHANGE    = Symbol('_change')    // the context object on proxy objects used in change callback\nconst ELEM_IDS  = Symbol('_elemIds')   // list containing the element ID of each list element\n\nmodule.exports = {\n  OPTIONS, CACHE, STATE, OBJECT_ID, CONFLICTS, CHANGE, ELEM_IDS\n}\n"
  },
  {
    "path": "frontend/context.js",
    "content": "const { CACHE, OBJECT_ID, CONFLICTS, ELEM_IDS, STATE } = require('./constants')\nconst { interpretPatch } = require('./apply_patch')\nconst { Text } = require('./text')\nconst { Table } = require('./table')\nconst { Counter, getWriteableCounter } = require('./counter')\nconst { Int, Uint, Float64 } = require('./numbers')\nconst { isObject, parseOpId, createArrayOfNulls } = require('../src/common')\nconst uuid = require('../src/uuid')\n\n\n/**\n * An instance of this class is passed to `rootObjectProxy()`. The methods are\n * called by proxy object mutation functions to query the current object state\n * and to apply the requested changes.\n */\nclass Context {\n  constructor (doc, actorId, applyPatch) {\n    this.actorId = actorId\n    this.nextOpNum = doc[STATE].maxOp + 1\n    this.cache = doc[CACHE]\n    this.updated = {}\n    this.ops = []\n    this.applyPatch = applyPatch ? applyPatch : interpretPatch\n  }\n\n  /**\n   * Adds an operation object to the list of changes made in the current context.\n   */\n  addOp(operation) {\n    this.ops.push(operation)\n\n    if (operation.action === 'set' && operation.values) {\n      this.nextOpNum += operation.values.length\n    } else if (operation.action === 'del' && operation.multiOp) {\n      this.nextOpNum += operation.multiOp\n    } else {\n      this.nextOpNum += 1\n    }\n  }\n\n  /**\n   * Returns the operation ID of the next operation to be added to the context.\n   */\n  nextOpId() {\n    return `${this.nextOpNum}@${this.actorId}`\n  }\n\n  /**\n   * Takes a value and returns an object describing the value (in the format used by patches).\n   */\n  getValueDescription(value) {\n    if (!['object', 'boolean', 'number', 'string'].includes(typeof value)) {\n      throw new TypeError(`Unsupported type of value: ${typeof value}`)\n    }\n\n    if (isObject(value)) {\n      if (value instanceof Date) {\n        // Date object, represented as milliseconds since epoch\n        return {type: 'value', value: value.getTime(), datatype: 'timestamp'}\n\n      } else if (value instanceof Int) {\n        return {type: 'value', value: value.value, datatype: 'int'}\n      } else if (value instanceof Uint) {\n        return {type: 'value', value: value.value, datatype: 'uint'}\n      } else if (value instanceof Float64) {\n        return {type: 'value', value: value.value, datatype: 'float64'}\n      } else if (value instanceof Counter) {\n        // Counter object\n        return {type: 'value', value: value.value, datatype: 'counter'}\n\n      } else {\n        // Nested object (map, list, text, or table)\n        const objectId = value[OBJECT_ID], type = this.getObjectType(objectId)\n        if (!objectId) {\n          throw new RangeError(`Object ${JSON.stringify(value)} has no objectId`)\n        }\n        if (type === 'list' || type === 'text') {\n          return {objectId, type, edits: []}\n        } else {\n          return {objectId, type, props: {}}\n        }\n      }\n    } else if (typeof value === 'number') {\n      if (Number.isInteger(value) && value <= Number.MAX_SAFE_INTEGER && value >= Number.MIN_SAFE_INTEGER) {\n        return {type: 'value', value, datatype: 'int'}\n      } else {\n        return {type: 'value', value, datatype: 'float64'}\n      }\n    } else {\n      // Primitive value (string, boolean, or null)\n      return {type: 'value', value}\n    }\n  }\n\n  /**\n   * Builds the values structure describing a single property in a patch. Finds all the values of\n   * property `key` of `object` (there might be multiple values in the case of a conflict), and\n   * returns an object that maps operation IDs to descriptions of values.\n   */\n  getValuesDescriptions(path, object, key) {\n    if (object instanceof Table) {\n      // Table objects don't have conflicts, since rows are identified by their unique objectId\n      const value = object.byId(key)\n      const opId = object.opIds[key]\n      return value ? {[opId]: this.getValueDescription(value)} : {}\n    } else if (object instanceof Text) {\n      // Text objects don't support conflicts\n      const value = object.get(key)\n      const elemId = object.getElemId(key)\n      return value ? {[elemId]: this.getValueDescription(value)} : {}\n    } else {\n      // Map or list objects\n      const conflicts = object[CONFLICTS][key], values = {}\n      if (!conflicts) {\n        throw new RangeError(`No children at key ${key} of path ${JSON.stringify(path)}`)\n      }\n      for (let opId of Object.keys(conflicts)) {\n        values[opId] = this.getValueDescription(conflicts[opId])\n      }\n      return values\n    }\n  }\n\n  /**\n   * Returns the value at property `key` of object `object`. In the case of a conflict, returns\n   * the value whose assignment operation has the ID `opId`.\n   */\n  getPropertyValue(object, key, opId) {\n    if (object instanceof Table) {\n      return object.byId(key)\n    } else if (object instanceof Text) {\n      return object.get(key)\n    } else {\n      return object[CONFLICTS][key][opId]\n    }\n  }\n\n  /**\n   * Recurses along `path` into the patch object `patch`, creating nodes along the way as needed\n   * by mutating the patch object. Returns the subpatch at the given path.\n   */\n  getSubpatch(patch, path) {\n    if (path.length == 0) return patch\n    let subpatch = patch, object = this.getObject('_root')\n\n    for (let pathElem of path) {\n      let values = this.getValuesDescriptions(path, object, pathElem.key)\n      if (subpatch.props) {\n        if (!subpatch.props[pathElem.key]) {\n          subpatch.props[pathElem.key] = values\n        }\n      } else if (subpatch.edits) {\n        for (const opId of Object.keys(values)) {\n          subpatch.edits.push({action: 'update', index: pathElem.key, opId, value: values[opId]})\n        }\n      }\n\n      let nextOpId = null\n      for (let opId of Object.keys(values)) {\n        if (values[opId].objectId === pathElem.objectId) {\n          nextOpId = opId\n        }\n      }\n      if (!nextOpId) {\n        throw new RangeError(`Cannot find path object with objectId ${pathElem.objectId}`)\n      }\n\n      subpatch = values[nextOpId]\n      object = this.getPropertyValue(object, pathElem.key, nextOpId)\n    }\n\n    return subpatch\n  }\n\n  /**\n   * Returns an object (not proxied) from the cache or updated set, as appropriate.\n   */\n  getObject(objectId) {\n    const object = this.updated[objectId] || this.cache[objectId]\n    if (!object) throw new RangeError(`Target object does not exist: ${objectId}`)\n    return object\n  }\n\n  /**\n   * Returns a string that is either 'map', 'table', 'list', or 'text', indicating\n   * the type of the object with ID `objectId`.\n   */\n  getObjectType(objectId) {\n    if (objectId === '_root') return 'map'\n    const object = this.getObject(objectId)\n    if (object instanceof Text) return 'text'\n    if (object instanceof Table) return 'table'\n    if (Array.isArray(object)) return 'list'\n    return 'map'\n  }\n\n  /**\n   * Returns the value associated with the property named `key` on the object\n   * at path `path`. If the value is an object, returns a proxy for it.\n   */\n  getObjectField(path, objectId, key) {\n    if (!['string', 'number'].includes(typeof key)) return\n    const object = this.getObject(objectId)\n\n    if (object[key] instanceof Counter) {\n      return getWriteableCounter(object[key].value, this, path, objectId, key)\n\n    } else if (isObject(object[key])) {\n      const childId = object[key][OBJECT_ID]\n      const subpath = path.concat([{key, objectId: childId}])\n      // The instantiateObject function is added to the context object by rootObjectProxy()\n      return this.instantiateObject(subpath, childId)\n\n    } else {\n      return object[key]\n    }\n  }\n\n  /**\n   * Recursively creates Automerge versions of all the objects and nested objects in `value`,\n   * constructing a patch and operations that describe the object tree. The new object is\n   * assigned to the property `key` in the object with ID `obj`. If the object is a list or\n   * text, `key` must be set to the list index being updated, and `elemId` must be set to the\n   * elemId of the element being updated. If `insert` is true, we insert a new list element\n   * (or text character) at index `key`, and `elemId` must be the elemId of the immediate\n   * predecessor element (or the string '_head' if inserting at index 0). If the assignment\n   * overwrites a previous value at this key/element, `pred` must be set to the array of the\n   * prior operations we are overwriting (empty array if there is no existing value).\n   */\n  createNestedObjects(obj, key, value, insert, pred, elemId) {\n    if (value[OBJECT_ID]) {\n      throw new RangeError('Cannot create a reference to an existing document object')\n    }\n    const objectId = this.nextOpId()\n\n    if (value instanceof Text) {\n      // Create a new Text object\n      this.addOp(elemId ? {action: 'makeText', obj, elemId, insert, pred}\n                        : {action: 'makeText', obj, key, insert, pred})\n      const subpatch = {objectId, type: 'text', edits: []}\n      this.insertListItems(subpatch, 0, [...value], true)\n      return subpatch\n\n    } else if (value instanceof Table) {\n      // Create a new Table object\n      if (value.count > 0) {\n        throw new RangeError('Assigning a non-empty Table object is not supported')\n      }\n      this.addOp(elemId ? {action: 'makeTable', obj, elemId, insert, pred}\n                        : {action: 'makeTable', obj, key, insert, pred})\n      return {objectId, type: 'table', props: {}}\n\n    } else if (Array.isArray(value)) {\n      // Create a new list object\n      this.addOp(elemId ? {action: 'makeList', obj, elemId, insert, pred}\n                        : {action: 'makeList', obj, key, insert, pred})\n      const subpatch = {objectId, type: 'list', edits: []}\n      this.insertListItems(subpatch, 0, value, true)\n      return subpatch\n\n    } else {\n      // Create a new map object\n      this.addOp(elemId ? {action: 'makeMap', obj, elemId, insert, pred}\n                        : {action: 'makeMap', obj, key, insert, pred})\n      let props = {}\n      for (let nested of Object.keys(value).sort()) {\n        const opId = this.nextOpId()\n        const valuePatch = this.setValue(objectId, nested, value[nested], false, [])\n        props[nested] = {[opId]: valuePatch}\n      }\n      return {objectId, type: 'map', props}\n    }\n  }\n\n  /**\n   * Records an assignment to a particular key in a map, or a particular index in a list.\n   * `objectId` is the ID of the object being modified, `key` is the property name or list\n   * index being updated, and `value` is the new value being assigned. If `insert` is true,\n   * a new list element is inserted at index `key`, and `value` is assigned to that new list\n   * element. `pred` is an array of opIds for previous values of the property being assigned,\n   * which are overwritten by this operation. If the object being modified is a list or text,\n   * `elemId` is the element ID of the list element being updated (if insert=false), or the\n   * element ID of the list element immediately preceding the insertion (if insert=true).\n   *\n   * Returns a patch describing the new value. The return value is of the form\n   * `{objectId, type, props}` if `value` is an object, or `{value, datatype}` if it is a\n   * primitive value. For string, number, boolean, or null the datatype is omitted.\n   */\n  setValue(objectId, key, value, insert, pred, elemId) {\n    if (!objectId) {\n      throw new RangeError('setValue needs an objectId')\n    }\n    if (key === '') {\n      throw new RangeError('The key of a map entry must not be an empty string')\n    }\n\n    if (isObject(value) && !(value instanceof Date) && !(value instanceof Counter) && !(value instanceof Int) && !(value instanceof Uint) && !(value instanceof Float64)) {\n      // Nested object (map, list, text, or table)\n      return this.createNestedObjects(objectId, key, value, insert, pred, elemId)\n    } else {\n      // Date or counter object, or primitive value (number, string, boolean, or null)\n      const description = this.getValueDescription(value)\n      const op = {action: 'set', obj: objectId, insert, value: description.value, pred}\n      if (elemId) op.elemId = elemId; else op.key = key\n      if (description.datatype) op.datatype = description.datatype\n      this.addOp(op)\n      return description\n    }\n  }\n\n  /**\n   * Constructs a new patch, calls `callback` with the subpatch at the location `path`,\n   * and then immediately applies the patch to the document.\n   */\n  applyAtPath(path, callback) {\n    let diff = {objectId: '_root', type: 'map', props: {}}\n    callback(this.getSubpatch(diff, path))\n    this.applyPatch(diff, this.cache._root, this.updated)\n  }\n\n  /**\n   * Updates the map object at path `path`, setting the property with name\n   * `key` to `value`.\n   */\n  setMapKey(path, key, value) {\n    if (typeof key !== 'string') {\n      throw new RangeError(`The key of a map entry must be a string, not ${typeof key}`)\n    }\n\n    const objectId = path.length === 0 ? '_root' : path[path.length - 1].objectId\n    const object = this.getObject(objectId)\n    if (object[key] instanceof Counter) {\n      throw new RangeError('Cannot overwrite a Counter object; use .increment() or .decrement() to change its value.')\n    }\n\n    // If the assigned field value is the same as the existing value, and\n    // the assignment does not resolve a conflict, do nothing\n    if (object[key] !== value || Object.keys(object[CONFLICTS][key] || {}).length > 1 || value === undefined) {\n      this.applyAtPath(path, subpatch => {\n        const pred = getPred(object, key)\n        const opId = this.nextOpId()\n        const valuePatch = this.setValue(objectId, key, value, false, pred)\n        subpatch.props[key] = {[opId]: valuePatch}\n      })\n    }\n  }\n\n  /**\n   * Updates the map object at path `path`, deleting the property `key`.\n   */\n  deleteMapKey(path, key) {\n    const objectId = path.length === 0 ? '_root' : path[path.length - 1].objectId\n    const object = this.getObject(objectId)\n\n    if (object[key] !== undefined) {\n      const pred = getPred(object, key)\n      this.addOp({action: 'del', obj: objectId, key, insert: false, pred})\n      this.applyAtPath(path, subpatch => {\n        subpatch.props[key] = {}\n      })\n    }\n  }\n\n  /**\n   * Inserts a sequence of new list elements `values` into a list, starting at position `index`.\n   * `newObject` is true if we are creating a new list object, and false if we are updating an\n   * existing one. `subpatch` is the patch for the list object being modified. Mutates\n   * `subpatch` to reflect the sequence of values.\n   */\n  insertListItems(subpatch, index, values, newObject) {\n    const list = newObject ? [] : this.getObject(subpatch.objectId)\n    if (index < 0 || index > list.length) {\n      throw new RangeError(`List index ${index} is out of bounds for list of length ${list.length}`)\n    }\n    if (values.length === 0) return\n\n    let elemId = getElemId(list, index, true)\n    const allPrimitive = values.every(v => typeof v === 'string' || typeof v === 'number' ||\n                                           typeof v === 'boolean' || v === null ||\n                                           (isObject(v) && (v instanceof Date || v instanceof Counter || v instanceof Int ||\n                                                            v instanceof Uint || v instanceof Float64)))\n    const allValueDescriptions = allPrimitive ? values.map(v => this.getValueDescription(v)) : []\n    const allDatatypesSame = allValueDescriptions.every(t => t.datatype === allValueDescriptions[0].datatype)\n\n    if (allPrimitive && allDatatypesSame && values.length > 1) {\n      const nextElemId = this.nextOpId()\n      const datatype = allValueDescriptions[0].datatype\n      const values = allValueDescriptions.map(v => v.value)\n      const op = {action: 'set', obj: subpatch.objectId, elemId, insert: true, values, pred: []}\n      const edit = {action: 'multi-insert', elemId: nextElemId, index, values}\n      if (datatype) {\n        op.datatype = datatype\n        edit.datatype = datatype\n      }\n      this.addOp(op)\n      subpatch.edits.push(edit)\n    } else {\n      for (let offset = 0; offset < values.length; offset++) {\n        let nextElemId = this.nextOpId()\n        const valuePatch = this.setValue(subpatch.objectId, index + offset, values[offset], true, [], elemId)\n        elemId = nextElemId\n        subpatch.edits.push({action: 'insert', index: index + offset, elemId, opId: elemId, value: valuePatch})\n      }\n    }\n  }\n\n  /**\n   * Updates the list object at path `path`, replacing the current value at\n   * position `index` with the new value `value`.\n   */\n  setListIndex(path, index, value) {\n    const objectId = path.length === 0 ? '_root' : path[path.length - 1].objectId\n    const list = this.getObject(objectId)\n\n    // Assignment past the end of the list => insert nulls followed by new value\n    if (index >= list.length) {\n      const insertions = createArrayOfNulls(index - list.length)\n      insertions.push(value)\n      return this.splice(path, list.length, 0, insertions)\n    }\n    if (list[index] instanceof Counter) {\n      throw new RangeError('Cannot overwrite a Counter object; use .increment() or .decrement() to change its value.')\n    }\n\n    // If the assigned list element value is the same as the existing value, and\n    // the assignment does not resolve a conflict, do nothing\n    if (list[index] !== value || Object.keys(list[CONFLICTS][index] || {}).length > 1 || value === undefined) {\n      this.applyAtPath(path, subpatch => {\n        const pred = getPred(list, index)\n        const opId = this.nextOpId()\n        const valuePatch = this.setValue(objectId, index, value, false, pred, getElemId(list, index))\n        subpatch.edits.push({action: 'update', index, opId, value: valuePatch})\n      })\n    }\n  }\n\n  /**\n   * Updates the list object at path `path`, deleting `deletions` list elements starting from\n   * list index `start`, and inserting the list of new elements `insertions` at that position.\n   */\n  splice(path, start, deletions, insertions) {\n    const objectId = path.length === 0 ? '_root' : path[path.length - 1].objectId\n    let list = this.getObject(objectId)\n    if (start < 0 || deletions < 0 || start > list.length - deletions) {\n      throw new RangeError(`${deletions} deletions starting at index ${start} are out of bounds for list of length ${list.length}`)\n    }\n    if (deletions === 0 && insertions.length === 0) return\n\n    let patch = {diffs: {objectId: '_root', type: 'map', props: {}}}\n    let subpatch = this.getSubpatch(patch.diffs, path)\n\n    if (deletions > 0) {\n      let op, lastElemParsed, lastPredParsed\n      for (let i = 0; i < deletions; i++) {\n        if (this.getObjectField(path, objectId, start + i) instanceof Counter) {\n          // This may seem bizarre, but it's really fiddly to implement deletion of counters from\n          // lists, and I doubt anyone ever needs to do this, so I'm just going to throw an\n          // exception for now. The reason is: a counter is created by a set operation with counter\n          // datatype, and subsequent increment ops are successors to the set operation. Normally, a\n          // set operation with successor indicates a value that has been overwritten, so a set\n          // operation with successors is normally invisible. Counters are an exception, because the\n          // increment operations don't make the set operation invisible. When a counter appears in\n          // a map, this is not too bad: if all successors are increments, then the counter remains\n          // visible; if one or more successors are deletions, it goes away. However, when deleting\n          // a list element, we have the additional challenge that we need to distinguish between a\n          // list element that is being deleted by the current change (in which case we need to put\n          // a 'remove' action in the patch's edits for that list) and a list element that was\n          // already deleted previously (in which case the patch should not reflect the deletion).\n          // This can be done, but as I said, it's fiddly. If someone wants to pick this up in the\n          // future, hopefully the above description will be enough to get you started. Good luck!\n          throw new TypeError('Unsupported operation: deleting a counter from a list')\n        }\n\n        // Any sequences of deletions with consecutive elemId and pred values get combined into a\n        // single multiOp; any others become individual deletion operations. This optimisation only\n        // kicks in if the user deletes a sequence of elements at once (in a single call to splice);\n        // it might be nice to also detect such runs of deletions in the case where the user deletes\n        // a sequence of list elements one by one.\n        const thisElem = getElemId(list, start + i), thisElemParsed = parseOpId(thisElem)\n        const thisPred = getPred(list, start + i)\n        const thisPredParsed = (thisPred.length === 1) ? parseOpId(thisPred[0]) : undefined\n\n        if (op && lastElemParsed && lastPredParsed && thisPredParsed &&\n            lastElemParsed.actorId === thisElemParsed.actorId && lastElemParsed.counter + 1 === thisElemParsed.counter &&\n            lastPredParsed.actorId === thisPredParsed.actorId && lastPredParsed.counter + 1 === thisPredParsed.counter) {\n          op.multiOp = (op.multiOp || 1) + 1\n        } else {\n          if (op) this.addOp(op)\n          op = {action: 'del', obj: objectId, elemId: thisElem, insert: false, pred: thisPred}\n        }\n        lastElemParsed = thisElemParsed\n        lastPredParsed = thisPredParsed\n      }\n      this.addOp(op)\n      subpatch.edits.push({action: 'remove', index: start, count: deletions})\n    }\n\n    if (insertions.length > 0) {\n      this.insertListItems(subpatch, start, insertions, false)\n    }\n    this.applyPatch(patch.diffs, this.cache._root, this.updated)\n  }\n\n  /**\n   * Updates the table object at path `path`, adding a new entry `row`.\n   * Returns the objectId of the new row.\n   */\n  addTableRow(path, row) {\n    if (!isObject(row) || Array.isArray(row)) {\n      throw new TypeError('A table row must be an object')\n    }\n    if (row[OBJECT_ID]) {\n      throw new TypeError('Cannot reuse an existing object as table row')\n    }\n    if (row.id) {\n      throw new TypeError('A table row must not have an \"id\" property; it is generated automatically')\n    }\n\n    const id = uuid()\n    const valuePatch = this.setValue(path[path.length - 1].objectId, id, row, false, [])\n    this.applyAtPath(path, subpatch => {\n      subpatch.props[id] = {[valuePatch.objectId]: valuePatch}\n    })\n    return id\n  }\n\n  /**\n   * Updates the table object at path `path`, deleting the row with ID `rowId`.\n   * `pred` is the opId of the operation that originally created the row.\n   */\n  deleteTableRow(path, rowId, pred) {\n    const objectId = path[path.length - 1].objectId, table = this.getObject(objectId)\n\n    if (table.byId(rowId)) {\n      this.addOp({action: 'del', obj: objectId, key: rowId, insert: false, pred: [pred]})\n      this.applyAtPath(path, subpatch => {\n        subpatch.props[rowId] = {}\n      })\n    }\n  }\n\n  /**\n   * Adds the integer `delta` to the value of the counter located at property\n   * `key` in the object at path `path`.\n   */\n  increment(path, key, delta) {\n    const objectId = path.length === 0 ? '_root' : path[path.length - 1].objectId\n    const object = this.getObject(objectId)\n    if (!(object[key] instanceof Counter)) {\n      throw new TypeError('Only counter values can be incremented')\n    }\n\n    // TODO what if there is a conflicting value on the same key as the counter?\n    const type = this.getObjectType(objectId)\n    const value = object[key].value + delta\n    const opId = this.nextOpId()\n    const pred = getPred(object, key)\n\n    if (type === 'list' || type === 'text') {\n      const elemId = getElemId(object, key, false)\n      this.addOp({action: 'inc', obj: objectId, elemId, value: delta, insert: false, pred})\n    } else {\n      this.addOp({action: 'inc', obj: objectId, key, value: delta, insert: false, pred})\n    }\n\n    this.applyAtPath(path, subpatch => {\n      if (type === 'list' || type === 'text') {\n        subpatch.edits.push({action: 'update', index: key, opId, value: {value, datatype: 'counter'}})\n      } else {\n        subpatch.props[key] = {[opId]: {value, datatype: 'counter'}}\n      }\n    })\n  }\n}\n\nfunction getPred(object, key) {\n  if (object instanceof Table) {\n    return [object.opIds[key]]\n  } else if (object instanceof Text) {\n    return object.elems[key].pred\n  } else if (object[CONFLICTS]) {\n    return object[CONFLICTS][key] ? Object.keys(object[CONFLICTS][key]) : []\n  } else {\n    return []\n  }\n}\n\nfunction getElemId(list, index, insert = false) {\n  if (insert) {\n    if (index === 0) return '_head'\n    index -= 1\n  }\n  if (list[ELEM_IDS]) return list[ELEM_IDS][index]\n  if (list.getElemId) return list.getElemId(index)\n  throw new RangeError(`Cannot find elemId at list index ${index}`)\n}\n\nmodule.exports = {\n  Context\n}\n"
  },
  {
    "path": "frontend/counter.js",
    "content": "/**\n * The most basic CRDT: an integer value that can be changed only by\n * incrementing and decrementing. Since addition of integers is commutative,\n * the value trivially converges.\n */\nclass Counter {\n  constructor(value) {\n    this.value = value || 0\n    Object.freeze(this)\n  }\n\n  /**\n   * A peculiar JavaScript language feature from its early days: if the object\n   * `x` has a `valueOf()` method that returns a number, you can use numerical\n   * operators on the object `x` directly, such as `x + 1` or `x < 4`.\n   * This method is also called when coercing a value to a string by\n   * concatenating it with another string, as in `x + ''`.\n   * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/valueOf\n   */\n  valueOf() {\n    return this.value\n  }\n\n  /**\n   * Returns the counter value as a decimal string. If `x` is a counter object,\n   * this method is called e.g. when you do `['value: ', x].join('')` or when\n   * you use string interpolation: `value: ${x}`.\n   */\n  toString() {\n    return this.valueOf().toString()\n  }\n\n  /**\n   * Returns the counter value, so that a JSON serialization of an Automerge\n   * document represents the counter simply as an integer.\n   */\n  toJSON() {\n    return this.value\n  }\n}\n\n/**\n * An instance of this class is used when a counter is accessed within a change\n * callback.\n */\nclass WriteableCounter extends Counter {\n  /**\n   * Increases the value of the counter by `delta`. If `delta` is not given,\n   * increases the value of the counter by 1.\n   */\n  increment(delta) {\n    delta = typeof delta === 'number' ? delta : 1\n    this.context.increment(this.path, this.key, delta)\n    this.value += delta\n    return this.value\n  }\n\n  /**\n   * Decreases the value of the counter by `delta`. If `delta` is not given,\n   * decreases the value of the counter by 1.\n   */\n  decrement(delta) {\n    return this.increment(typeof delta === 'number' ? -delta : -1)\n  }\n}\n\n/**\n * Returns an instance of `WriteableCounter` for use in a change callback.\n * `context` is the proxy context that keeps track of the mutations.\n * `objectId` is the ID of the object containing the counter, and `key` is\n * the property name (key in map, or index in list) where the counter is\n * located.\n*/\nfunction getWriteableCounter(value, context, path, objectId, key) {\n  const instance = Object.create(WriteableCounter.prototype)\n  instance.value = value\n  instance.context = context\n  instance.path = path\n  instance.objectId = objectId\n  instance.key = key\n  return instance\n}\n\nmodule.exports = { Counter, getWriteableCounter }\n"
  },
  {
    "path": "frontend/index.js",
    "content": "const { OPTIONS, CACHE, STATE, OBJECT_ID, CONFLICTS, CHANGE, ELEM_IDS } = require('./constants')\nconst { isObject, copyObject } = require('../src/common')\nconst uuid = require('../src/uuid')\nconst { interpretPatch, cloneRootObject } = require('./apply_patch')\nconst { rootObjectProxy } = require('./proxies')\nconst { Context } = require('./context')\nconst { Text } = require('./text')\nconst { Table } = require('./table')\nconst { Counter } = require('./counter')\nconst { Float64, Int, Uint } = require('./numbers')\nconst { Observable } = require('./observable')\n\n/**\n * Actor IDs must consist only of hexadecimal digits so that they can be encoded\n * compactly in binary form.\n */\nfunction checkActorId(actorId) {\n  if (typeof actorId !== 'string') {\n    throw new TypeError(`Unsupported type of actorId: ${typeof actorId}`)\n  }\n  if (!/^[0-9a-f]+$/.test(actorId)) {\n    throw new RangeError('actorId must consist only of lowercase hex digits')\n  }\n  if (actorId.length % 2 !== 0) {\n    throw new RangeError('actorId must consist of an even number of digits')\n  }\n}\n\n/**\n * Takes a set of objects that have been updated (in `updated`) and an updated state object\n * `state`, and returns a new immutable document root object based on `doc` that reflects\n * those updates.\n */\nfunction updateRootObject(doc, updated, state) {\n  let newDoc = updated._root\n  if (!newDoc) {\n    newDoc = cloneRootObject(doc[CACHE]._root)\n    updated._root = newDoc\n  }\n  Object.defineProperty(newDoc, OPTIONS,  {value: doc[OPTIONS]})\n  Object.defineProperty(newDoc, CACHE,    {value: updated})\n  Object.defineProperty(newDoc, STATE,    {value: state})\n\n  if (doc[OPTIONS].freeze) {\n    for (let objectId of Object.keys(updated)) {\n      if (updated[objectId] instanceof Table) {\n        updated[objectId]._freeze()\n      } else if (updated[objectId] instanceof Text) {\n        Object.freeze(updated[objectId].elems)\n        Object.freeze(updated[objectId])\n      } else {\n        Object.freeze(updated[objectId])\n        Object.freeze(updated[objectId][CONFLICTS])\n      }\n    }\n  }\n\n  for (let objectId of Object.keys(doc[CACHE])) {\n    if (!updated[objectId]) {\n      updated[objectId] = doc[CACHE][objectId]\n    }\n  }\n\n  if (doc[OPTIONS].freeze) {\n    Object.freeze(updated)\n  }\n  return newDoc\n}\n\n/**\n * Adds a new change request to the list of pending requests, and returns an\n * updated document root object.\n * The details of the change are taken from the context object `context`.\n * `options` contains properties that may affect how the change is processed; in\n * particular, the `message` property of `options` is an optional human-readable\n * string describing the change.\n */\nfunction makeChange(doc, context, options) {\n  const actor = getActorId(doc)\n  if (!actor) {\n    throw new Error('Actor ID must be initialized with setActorId() before making a change')\n  }\n  const state = copyObject(doc[STATE])\n  state.seq += 1\n\n  const change = {\n    actor,\n    seq: state.seq,\n    startOp: state.maxOp + 1,\n    deps: state.deps,\n    time: (options && typeof options.time === 'number') ? options.time\n                                                        : Math.round(new Date().getTime() / 1000),\n    message: (options && typeof options.message === 'string') ? options.message : '',\n    ops: context.ops\n  }\n\n  if (doc[OPTIONS].backend) {\n    const [backendState, patch, binaryChange] = doc[OPTIONS].backend.applyLocalChange(state.backendState, change)\n    state.backendState = backendState\n    state.lastLocalChange = binaryChange\n    // NOTE: When performing a local change, the patch is effectively applied twice -- once by the\n    // context invoking interpretPatch as soon as any change is made, and the second time here\n    // (after a round-trip through the backend). This is perhaps more robust, as changes only take\n    // effect in the form processed by the backend, but the downside is a performance cost.\n    // Should we change this?\n    const newDoc = applyPatchToDoc(doc, patch, state, true)\n    const patchCallback = options && options.patchCallback || doc[OPTIONS].patchCallback\n    if (patchCallback) patchCallback(patch, doc, newDoc, true, [binaryChange])\n    return [newDoc, change]\n\n  } else {\n    const queuedRequest = {actor, seq: change.seq, before: doc}\n    state.requests = state.requests.concat([queuedRequest])\n    state.maxOp = state.maxOp + countOps(change.ops)\n    state.deps = []\n    return [updateRootObject(doc, context ? context.updated : {}, state), change]\n  }\n}\n\nfunction countOps(ops) {\n  let count = 0\n  for (const op of ops) {\n    if (op.action === 'set' && op.values) {\n      count += op.values.length\n    } else {\n      count += 1\n    }\n  }\n  return count\n}\n\n/**\n * Returns the binary encoding of the last change made by the local actor.\n */\nfunction getLastLocalChange(doc) {\n  return doc[STATE] && doc[STATE].lastLocalChange ? doc[STATE].lastLocalChange : null\n}\n\n/**\n * Applies the changes described in `patch` to the document with root object\n * `doc`. The state object `state` is attached to the new root object.\n * `fromBackend` should be set to `true` if the patch came from the backend,\n * and to `false` if the patch is a transient local (optimistically applied)\n * change from the frontend.\n */\nfunction applyPatchToDoc(doc, patch, state, fromBackend) {\n  const actor = getActorId(doc)\n  const updated = {}\n  interpretPatch(patch.diffs, doc, updated)\n\n  if (fromBackend) {\n    if (!patch.clock) throw new RangeError('patch is missing clock field')\n    if (patch.clock[actor] && patch.clock[actor] > state.seq) {\n      state.seq = patch.clock[actor]\n    }\n    state.clock = patch.clock\n    state.deps  = patch.deps\n    state.maxOp = Math.max(state.maxOp, patch.maxOp)\n  }\n  return updateRootObject(doc, updated, state)\n}\n\n/**\n * Creates an empty document object with no changes.\n */\nfunction init(options) {\n  if (typeof options === 'string') {\n    options = {actorId: options}\n  } else if (typeof options === 'undefined') {\n    options = {}\n  } else if (!isObject(options)) {\n    throw new TypeError(`Unsupported value for init() options: ${options}`)\n  }\n\n  if (!options.deferActorId) {\n    if (options.actorId === undefined) {\n      options.actorId = uuid()\n    }\n    checkActorId(options.actorId)\n  }\n\n  if (options.observable) {\n    const patchCallback = options.patchCallback, observable = options.observable\n    options.patchCallback = (patch, before, after, local, changes) => {\n      if (patchCallback) patchCallback(patch, before, after, local, changes)\n      observable.patchCallback(patch, before, after, local, changes)\n    }\n  }\n\n  const root = {}, cache = {_root: root}\n  const state = {seq: 0, maxOp: 0, requests: [], clock: {}, deps: []}\n  if (options.backend) {\n    state.backendState = options.backend.init()\n    state.lastLocalChange = null\n  }\n  Object.defineProperty(root, OBJECT_ID, {value: '_root'})\n  Object.defineProperty(root, OPTIONS,   {value: Object.freeze(options)})\n  Object.defineProperty(root, CONFLICTS, {value: Object.freeze({})})\n  Object.defineProperty(root, CACHE,     {value: Object.freeze(cache)})\n  Object.defineProperty(root, STATE,     {value: Object.freeze(state)})\n  return Object.freeze(root)\n}\n\n/**\n * Returns a new document object initialized with the given state.\n */\nfunction from(initialState, options) {\n  return change(init(options), 'Initialization', doc => Object.assign(doc, initialState))\n}\n\n\n/**\n * Changes a document `doc` according to actions taken by the local user.\n * `options` is an object that can contain the following properties:\n *  - `message`: an optional descriptive string that is attached to the change.\n * If `options` is a string, it is treated as `message`.\n *\n * The actual change is made within the callback function `callback`, which is\n * given a mutable version of the document as argument. Returns a two-element\n * array `[doc, request]` where `doc` is the updated document, and `request`\n * is the change request to send to the backend. If nothing was actually\n * changed, returns the original `doc` and a `null` change request.\n */\nfunction change(doc, options, callback) {\n  if (doc[OBJECT_ID] !== '_root') {\n    throw new TypeError('The first argument to Automerge.change must be the document root')\n  }\n  if (doc[CHANGE]) {\n    throw new TypeError('Calls to Automerge.change cannot be nested')\n  }\n  if (typeof options === 'function' && callback === undefined) {\n    [options, callback] = [callback, options]\n  }\n  if (typeof options === 'string') {\n    options = {message: options}\n  }\n  if (options !== undefined && !isObject(options)) {\n    throw new TypeError('Unsupported type of options')\n  }\n\n  const actorId = getActorId(doc)\n  if (!actorId) {\n    throw new Error('Actor ID must be initialized with setActorId() before making a change')\n  }\n  const context = new Context(doc, actorId)\n  callback(rootObjectProxy(context))\n\n  if (Object.keys(context.updated).length === 0) {\n    // If the callback didn't change anything, return the original document object unchanged\n    return [doc, null]\n  } else {\n    return makeChange(doc, context, options)\n  }\n}\n\n/**\n * Triggers a new change request on the document `doc` without actually\n * modifying its data. `options` is an object as described in the documentation\n * for the `change` function. This function can be useful for acknowledging the\n * receipt of some message (as it's incorported into the `deps` field of the\n * change). Returns a two-element array `[doc, request]` where `doc` is the\n * updated document, and `request` is the change request to send to the backend.\n */\nfunction emptyChange(doc, options) {\n  if (doc[OBJECT_ID] !== '_root') {\n    throw new TypeError('The first argument to Automerge.emptyChange must be the document root')\n  }\n  if (typeof options === 'string') {\n    options = {message: options}\n  }\n  if (options !== undefined && !isObject(options)) {\n    throw new TypeError('Unsupported type of options')\n  }\n\n  const actorId = getActorId(doc)\n  if (!actorId) {\n    throw new Error('Actor ID must be initialized with setActorId() before making a change')\n  }\n  return makeChange(doc, new Context(doc, actorId), options)\n}\n\n/**\n * Applies `patch` to the document root object `doc`. This patch must come\n * from the backend; it may be the result of a local change or a remote change.\n * If it is the result of a local change, the `seq` field from the change\n * request should be included in the patch, so that we can match them up here.\n */\nfunction applyPatch(doc, patch, backendState = undefined) {\n  if (doc[OBJECT_ID] !== '_root') {\n    throw new TypeError('The first argument to Frontend.applyPatch must be the document root')\n  }\n  const state = copyObject(doc[STATE])\n\n  if (doc[OPTIONS].backend) {\n    if (!backendState) {\n      throw new RangeError('applyPatch must be called with the updated backend state')\n    }\n    state.backendState = backendState\n    return applyPatchToDoc(doc, patch, state, true)\n  }\n\n  let baseDoc\n\n  if (state.requests.length > 0) {\n    baseDoc = state.requests[0].before\n    if (patch.actor === getActorId(doc)) {\n      if (state.requests[0].seq !== patch.seq) {\n        throw new RangeError(`Mismatched sequence number: patch ${patch.seq} does not match next request ${state.requests[0].seq}`)\n      }\n      state.requests = state.requests.slice(1)\n    } else {\n      state.requests = state.requests.slice()\n    }\n  } else {\n    baseDoc = doc\n    state.requests = []\n  }\n\n  let newDoc = applyPatchToDoc(baseDoc, patch, state, true)\n  if (state.requests.length === 0) {\n    return newDoc\n  } else {\n    state.requests[0] = copyObject(state.requests[0])\n    state.requests[0].before = newDoc\n    return updateRootObject(doc, {}, state)\n  }\n}\n\n/**\n * Returns the Automerge object ID of the given object.\n */\nfunction getObjectId(object) {\n  return object[OBJECT_ID]\n}\n\n/**\n * Returns the object with the given Automerge object ID. Note: when called\n * within a change callback, the returned object is read-only (not a mutable\n * proxy object).\n */\nfunction getObjectById(doc, objectId) {\n  // It would be nice to return a proxied object in a change callback.\n  // However, that requires knowing the path from the root to the current\n  // object, which we don't have if we jumped straight to the object by its ID.\n  // If we maintained an index from object ID to parent ID we could work out the path.\n  if (doc[CHANGE]) {\n    throw new TypeError('Cannot use getObjectById in a change callback')\n  }\n  return doc[CACHE][objectId]\n}\n\n/**\n * Returns the Automerge actor ID of the given document.\n */\nfunction getActorId(doc) {\n  return doc[STATE].actorId || doc[OPTIONS].actorId\n}\n\n/**\n * Sets the Automerge actor ID on the document object `doc`, returning a\n * document object with updated metadata.\n */\nfunction setActorId(doc, actorId) {\n  checkActorId(actorId)\n  const state = Object.assign({}, doc[STATE], {actorId})\n  return updateRootObject(doc, {}, state)\n}\n\n/**\n * Fetches the conflicts on the property `key` of `object`, which may be any\n * object in a document. If `object` is a list, then `key` must be a list\n * index; if `object` is a map, then `key` must be a property name.\n */\nfunction getConflicts(object, key) {\n  if (object[CONFLICTS] && object[CONFLICTS][key] &&\n      Object.keys(object[CONFLICTS][key]).length > 1) {\n    return object[CONFLICTS][key]\n  }\n}\n\n/**\n * Returns the backend state associated with the document `doc` (only used if\n * a backend implementation is passed to `init()`).\n */\nfunction getBackendState(doc, callerName = null, argPos = 'first') {\n  if (doc[OBJECT_ID] !== '_root') {\n    // Most likely cause of passing an array here is forgetting to deconstruct the return value of\n    // Automerge.applyChanges().\n    const extraMsg = Array.isArray(doc) ? '. Note: Automerge.applyChanges now returns an array.' : ''\n    if (callerName) {\n      throw new TypeError(`The ${argPos} argument to Automerge.${callerName} must be the document root${extraMsg}`)\n    } else {\n      throw new TypeError(`Argument is not an Automerge document root${extraMsg}`)\n    }\n  }\n  return doc[STATE].backendState\n}\n\n/**\n * Given an array or text object from an Automerge document, returns an array\n * containing the unique element ID of each list element/character.\n */\nfunction getElementIds(list) {\n  if (list instanceof Text) {\n    return list.elems.map(elem => elem.elemId)\n  } else {\n    return list[ELEM_IDS]\n  }\n}\n\nmodule.exports = {\n  init, from, change, emptyChange, applyPatch,\n  getObjectId, getObjectById, getActorId, setActorId, getConflicts, getLastLocalChange,\n  getBackendState, getElementIds,\n  Text, Table, Counter, Observable, Float64, Int, Uint\n}\n"
  },
  {
    "path": "frontend/numbers.js",
    "content": "// Convience classes to allow users to stricly specify the number type they want\n\nclass Int {\n  constructor(value) {\n    if (!(Number.isInteger(value) && value <= Number.MAX_SAFE_INTEGER && value >= Number.MIN_SAFE_INTEGER)) {\n      throw new RangeError(`Value ${value} cannot be a uint`)\n    }\n    this.value = value\n    Object.freeze(this)\n  }\n}\n\nclass Uint {\n  constructor(value) {\n    if (!(Number.isInteger(value) && value <= Number.MAX_SAFE_INTEGER && value >= 0)) {\n      throw new RangeError(`Value ${value} cannot be a uint`)\n    }\n    this.value = value\n    Object.freeze(this)\n  }\n}\n\nclass Float64 {\n  constructor(value) {\n    if (typeof value !== 'number') {\n      throw new RangeError(`Value ${value} cannot be a float64`)\n    }\n    this.value = value || 0.0\n    Object.freeze(this)\n  }\n}\n\nmodule.exports = { Int, Uint, Float64 }\n"
  },
  {
    "path": "frontend/observable.js",
    "content": "const { OBJECT_ID, CONFLICTS } = require('./constants')\n\n/**\n * Allows an application to register a callback when a particular object in\n * a document changes.\n *\n * NOTE: This API is experimental and may change without warning in minor releases.\n */\nclass Observable {\n  constructor() {\n    this.observers = {} // map from objectId to array of observers for that object\n  }\n\n  /**\n   * Called by an Automerge document when `patch` is applied. `before` is the\n   * state of the document before the patch, and `after` is the state after\n   * applying it. `local` is true if the update is a result of locally calling\n   * `Automerge.change()`, and false otherwise. `changes` is an array of\n   * changes that were applied to the document (as Uint8Arrays).\n   */\n  patchCallback(patch, before, after, local, changes) {\n    this._objectUpdate(patch.diffs, before, after, local, changes)\n  }\n\n  /**\n   * Recursively walks a patch and calls the callbacks for all objects that\n   * appear in the patch.\n   */\n  _objectUpdate(diff, before, after, local, changes) {\n    if (!diff.objectId) return\n    if (this.observers[diff.objectId]) {\n      for (let callback of this.observers[diff.objectId]) {\n        callback(diff, before, after, local, changes)\n      }\n    }\n\n    if (diff.type === 'map' && diff.props) {\n      for (const propName of Object.keys(diff.props)) {\n        for (const opId of Object.keys(diff.props[propName])) {\n          this._objectUpdate(diff.props[propName][opId],\n                             before && before[CONFLICTS] && before[CONFLICTS][propName] && before[CONFLICTS][propName][opId],\n                             after && after[CONFLICTS] && after[CONFLICTS][propName] && after[CONFLICTS][propName][opId],\n                             local, changes)\n        }\n      }\n\n    } else if (diff.type === 'table' && diff.props) {\n      for (const rowId of Object.keys(diff.props)) {\n        for (const opId of Object.keys(diff.props[rowId])) {\n          this._objectUpdate(diff.props[rowId][opId],\n                             before && before.byId(rowId),\n                             after && after.byId(rowId),\n                             local, changes)\n        }\n      }\n\n    } else if (diff.type === 'list' && diff.edits) {\n      let offset = 0\n      for (const edit of diff.edits) {\n        if (edit.action === 'insert') {\n          offset -= 1\n          this._objectUpdate(edit.value, undefined,\n                             after && after[CONFLICTS] && after[CONFLICTS][edit.index] && after[CONFLICTS][edit.index][edit.elemId],\n                             local, changes)\n        } else if (edit.action === 'multi-insert') {\n          offset -= edit.values.length\n        } else if (edit.action === 'update') {\n          this._objectUpdate(edit.value,\n                             before && before[CONFLICTS] && before[CONFLICTS][edit.index + offset] &&\n                               before[CONFLICTS][edit.index + offset][edit.opId],\n                             after && after[CONFLICTS] && after[CONFLICTS][edit.index] && after[CONFLICTS][edit.index][edit.opId],\n                             local, changes)\n        } else if (edit.action === 'remove') {\n          offset += edit.count\n        }\n      }\n\n    } else if (diff.type === 'text' && diff.edits) {\n      let offset = 0\n      for (const edit of diff.edits) {\n        if (edit.action === 'insert') {\n          offset -= 1\n          this._objectUpdate(edit.value, undefined, after && after.get(edit.index), local, changes)\n        } else if (edit.action === 'multi-insert') {\n          offset -= edit.values.length\n        } else if (edit.action === 'update') {\n          this._objectUpdate(edit.value,\n                             before && before.get(edit.index + offset),\n                             after && after.get(edit.index),\n                             local, changes)\n        } else if (edit.action === 'remove') {\n          offset += edit.count\n        }\n      }\n    }\n  }\n\n  /**\n   * Call this to register a callback that will get called whenever a particular\n   * object in a document changes. The callback is passed five arguments: the\n   * part of the patch describing the update to that object, the old state of\n   * the object, the new state of the object, a boolean that is true if the\n   * change is the result of calling `Automerge.change()` locally, and the array\n   * of binary changes applied to the document.\n   */\n  observe(object, callback) {\n    const objectId = object[OBJECT_ID]\n    if (!objectId) throw new TypeError('The observed object must be part of an Automerge document')\n    if (!this.observers[objectId]) this.observers[objectId] = []\n    this.observers[objectId].push(callback)\n  }\n}\n\nmodule.exports = { Observable }\n"
  },
  {
    "path": "frontend/proxies.js",
    "content": "const { OBJECT_ID, CHANGE, STATE } = require('./constants')\nconst { isObject, createArrayOfNulls } = require('../src/common')\nconst { Text } = require('./text')\nconst { Table } = require('./table')\n\nfunction parseListIndex(key) {\n  if (typeof key === 'string' && /^[0-9]+$/.test(key)) key = parseInt(key, 10)\n  if (typeof key !== 'number') {\n    throw new TypeError('A list index must be a number, but you passed ' + JSON.stringify(key))\n  }\n  if (key < 0 || isNaN(key) || key === Infinity || key === -Infinity) {\n    throw new RangeError('A list index must be positive, but you passed ' + key)\n  }\n  return key\n}\n\nfunction listMethods(context, listId, path) {\n  const methods = {\n    deleteAt(index, numDelete) {\n      context.splice(path, parseListIndex(index), numDelete || 1, [])\n      return this\n    },\n\n    fill(value, start, end) {\n      let list = context.getObject(listId)\n      for (let index = parseListIndex(start || 0); index < parseListIndex(end || list.length); index++) {\n        context.setListIndex(path, index, value)\n      }\n      return this\n    },\n\n    indexOf(o, start = 0) {\n      const id = isObject(o) ? o[OBJECT_ID] : undefined\n      if (id) {\n        const list = context.getObject(listId)\n        for (let index = start; index < list.length; index++) {\n          if (list[index][OBJECT_ID] === id) {\n            return index\n          }\n        }\n        return -1\n      } else {\n        return context.getObject(listId).indexOf(o, start)\n      }\n    },\n\n    insertAt(index, ...values) {\n      context.splice(path, parseListIndex(index), 0, values)\n      return this\n    },\n\n    pop() {\n      let list = context.getObject(listId)\n      if (list.length == 0) return\n      const last = context.getObjectField(path, listId, list.length - 1)\n      context.splice(path, list.length - 1, 1, [])\n      return last\n    },\n\n    push(...values) {\n      let list = context.getObject(listId)\n      context.splice(path, list.length, 0, values)\n      // need to getObject() again because the list object above may be immutable\n      return context.getObject(listId).length\n    },\n\n    shift() {\n      let list = context.getObject(listId)\n      if (list.length == 0) return\n      const first = context.getObjectField(path, listId, 0)\n      context.splice(path, 0, 1, [])\n      return first\n    },\n\n    splice(start, deleteCount, ...values) {\n      let list = context.getObject(listId)\n      start = parseListIndex(start)\n      if (deleteCount === undefined || deleteCount > list.length - start) {\n        deleteCount = list.length - start\n      }\n      const deleted = []\n      for (let n = 0; n < deleteCount; n++) {\n        deleted.push(context.getObjectField(path, listId, start + n))\n      }\n      context.splice(path, start, deleteCount, values)\n      return deleted\n    },\n\n    unshift(...values) {\n      context.splice(path, 0, 0, values)\n      return context.getObject(listId).length\n    }\n  }\n\n  for (let iterator of ['entries', 'keys', 'values']) {\n    let list = context.getObject(listId)\n    methods[iterator] = () => list[iterator]()\n  }\n\n  // Read-only methods that can delegate to the JavaScript built-in implementations\n  for (let method of ['concat', 'every', 'filter', 'find', 'findIndex', 'forEach', 'includes',\n                      'join', 'lastIndexOf', 'map', 'reduce', 'reduceRight',\n                      'slice', 'some', 'toLocaleString', 'toString']) {\n    methods[method] = (...args) => {\n      const list = context.getObject(listId)\n        .map((item, index) => context.getObjectField(path, listId, index))\n      return list[method](...args)\n    }\n  }\n\n  return methods\n}\n\nconst MapHandler = {\n  get (target, key) {\n    const { context, objectId, path } = target\n    if (key === OBJECT_ID) return objectId\n    if (key === CHANGE) return context\n    if (key === STATE) return {actorId: context.actorId}\n    return context.getObjectField(path, objectId, key)\n  },\n\n  set (target, key, value) {\n    const { context, path, readonly } = target\n    if (Array.isArray(readonly) && readonly.indexOf(key) >= 0) {\n      throw new RangeError(`Object property \"${key}\" cannot be modified`)\n    }\n    context.setMapKey(path, key, value)\n    return true\n  },\n\n  deleteProperty (target, key) {\n    const { context, path, readonly } = target\n    if (Array.isArray(readonly) && readonly.indexOf(key) >= 0) {\n      throw new RangeError(`Object property \"${key}\" cannot be modified`)\n    }\n    context.deleteMapKey(path, key)\n    return true\n  },\n\n  has (target, key) {\n    const { context, objectId } = target\n    return [OBJECT_ID, CHANGE].includes(key) || (key in context.getObject(objectId))\n  },\n\n  getOwnPropertyDescriptor (target, key) {\n    const { context, objectId } = target\n    const object = context.getObject(objectId)\n    if (key in object) {\n      return {\n        configurable: true, enumerable: true,\n        value: context.getObjectField(objectId, key)\n      }\n    }\n  },\n\n  ownKeys (target) {\n    const { context, objectId } = target\n    return Object.keys(context.getObject(objectId))\n  }\n}\n\nconst ListHandler = {\n  get (target, key) {\n    const [context, objectId, path] = target\n    if (key === Symbol.iterator) return context.getObject(objectId)[Symbol.iterator]\n    if (key === OBJECT_ID) return objectId\n    if (key === CHANGE) return context\n    if (key === 'length') return context.getObject(objectId).length\n    if (typeof key === 'string' && /^[0-9]+$/.test(key)) {\n      return context.getObjectField(path, objectId, parseListIndex(key))\n    }\n    return listMethods(context, objectId, path)[key]\n  },\n\n  set (target, key, value) {\n    const [context, objectId, path] = target\n    if (key === 'length') {\n      if (typeof value !== 'number') {\n        throw new RangeError(\"Invalid array length\")\n      }\n      const length = context.getObject(objectId).length\n      if (length > value) {\n        context.splice(path, value, length - value, [])\n      } else {\n        context.splice(path, length, 0, createArrayOfNulls(value - length))\n      }\n    } else {\n      context.setListIndex(path, parseListIndex(key), value)\n    }\n    return true\n  },\n\n  deleteProperty (target, key) {\n    const [context, /* objectId */, path] = target\n    context.splice(path, parseListIndex(key), 1, [])\n    return true\n  },\n\n  has (target, key) {\n    const [context, objectId, /* path */] = target\n    if (typeof key === 'string' && /^[0-9]+$/.test(key)) {\n      return parseListIndex(key) < context.getObject(objectId).length\n    }\n    return ['length', OBJECT_ID, CHANGE].includes(key)\n  },\n\n  getOwnPropertyDescriptor (target, key) {\n    const [context, objectId, /* path */] = target\n    const object = context.getObject(objectId)\n\n    if (key === 'length') return {writable: true, value: object.length}\n    if (key === OBJECT_ID) return {configurable: false, enumerable: false, value: objectId}\n\n    if (typeof key === 'string' && /^[0-9]+$/.test(key)) {\n      const index = parseListIndex(key)\n      if (index < object.length) return {\n        configurable: true, enumerable: true,\n        value: context.getObjectField(objectId, index)\n      }\n    }\n  },\n\n  ownKeys (target) {\n    const [context, objectId, /* path */] = target\n    const object = context.getObject(objectId)\n    let keys = ['length']\n    for (let key of Object.keys(object)) keys.push(key)\n    return keys\n  }\n}\n\nfunction mapProxy(context, objectId, path, readonly) {\n  return new Proxy({context, objectId, path, readonly}, MapHandler)\n}\n\nfunction listProxy(context, objectId, path) {\n  return new Proxy([context, objectId, path], ListHandler)\n}\n\n/**\n * Instantiates a proxy object for the given `objectId`.\n * This function is added as a method to the context object by rootObjectProxy().\n * When it is called, `this` is the context object.\n * `readonly` is a list of map property names that cannot be modified.\n */\nfunction instantiateProxy(path, objectId, readonly) {\n  const object = this.getObject(objectId)\n  if (Array.isArray(object)) {\n    return listProxy(this, objectId, path)\n  } else if (object instanceof Text || object instanceof Table) {\n    return object.getWriteable(this, path)\n  } else {\n    return mapProxy(this, objectId, path, readonly)\n  }\n}\n\nfunction rootObjectProxy(context) {\n  context.instantiateObject = instantiateProxy\n  return mapProxy(context, '_root', [])\n}\n\nmodule.exports = { rootObjectProxy }\n"
  },
  {
    "path": "frontend/table.js",
    "content": "const { OBJECT_ID, CONFLICTS } = require('./constants')\nconst { isObject, copyObject } = require('../src/common')\n\nfunction compareRows(properties, row1, row2) {\n  for (let prop of properties) {\n    if (row1[prop] === row2[prop]) continue\n\n    if (typeof row1[prop] === 'number' && typeof row2[prop] === 'number') {\n      return row1[prop] - row2[prop]\n    } else {\n      const prop1 = '' + row1[prop], prop2 = '' + row2[prop]\n      if (prop1 === prop2) continue\n      if (prop1 < prop2) return -1; else return +1\n    }\n  }\n  return 0\n}\n\n\n/**\n * A relational-style unordered collection of records (rows). Each row is an\n * object that maps column names to values. The set of rows is represented by\n * a map from UUID to row object.\n */\nclass Table {\n  /**\n   * This constructor is used by application code when creating a new Table\n   * object within a change callback.\n   */\n  constructor() {\n    this.entries = Object.freeze({})\n    this.opIds = Object.freeze({})\n    Object.freeze(this)\n  }\n\n  /**\n   * Looks up a row in the table by its unique ID.\n   */\n  byId(id) {\n    return this.entries[id]\n  }\n\n  /**\n   * Returns an array containing the unique IDs of all rows in the table, in no\n   * particular order.\n   */\n  get ids() {\n    return Object.keys(this.entries).filter(key => {\n      const entry = this.entries[key]\n      return isObject(entry) && entry.id === key\n    })\n  }\n\n  /**\n   * Returns the number of rows in the table.\n   */\n  get count() {\n    return this.ids.length\n  }\n\n  /**\n   * Returns an array containing all of the rows in the table, in no particular\n   * order.\n   */\n  get rows() {\n    return this.ids.map(id => this.byId(id))\n  }\n\n  /**\n   * The standard JavaScript `filter()` method, which passes each row to the\n   * callback function and returns all rows for which the it returns true.\n   */\n  filter(callback, thisArg) {\n    return this.rows.filter(callback, thisArg)\n  }\n\n  /**\n   * The standard JavaScript `find()` method, which passes each row to the\n   * callback function and returns the first row for which it returns true.\n   */\n  find(callback, thisArg) {\n    return this.rows.find(callback, thisArg)\n  }\n\n  /**\n   * The standard JavaScript `map()` method, which passes each row to the\n   * callback function and returns a list of its return values.\n   */\n  map(callback, thisArg) {\n    return this.rows.map(callback, thisArg)\n  }\n\n  /**\n  * Returns the list of rows, sorted by one of the following:\n  * - If a function argument is given, it compares rows as per\n  *   https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort#Description\n  * - If a string argument is given, it is interpreted as a column name and\n  *   rows are sorted according to that column.\n  * - If an array of strings is given, it is interpreted as a list of column\n  *   names, and rows are sorted lexicographically by those columns.\n  * - If no argument is given, it sorts by row ID by default.\n  */\n  sort(arg) {\n    if (typeof arg === 'function') {\n      return this.rows.sort(arg)\n    } else if (typeof arg === 'string') {\n      return this.rows.sort((row1, row2) => compareRows([arg], row1, row2))\n    } else if (Array.isArray(arg)) {\n      return this.rows.sort((row1, row2) => compareRows(arg, row1, row2))\n    } else if (arg === undefined) {\n      return this.rows.sort((row1, row2) => compareRows(['id'], row1, row2))\n    } else {\n      throw new TypeError(`Unsupported sorting argument: ${arg}`)\n    }\n  }\n\n  /**\n   * When iterating over a table, you get all rows in the table, in no\n   * particular order.\n   */\n  [Symbol.iterator] () {\n    let rows = this.rows, index = -1\n    return {\n      next () {\n        index += 1\n        if (index < rows.length) {\n          return {done: false, value: rows[index]}\n        } else {\n          return {done: true}\n        }\n      }\n    }\n  }\n\n  /**\n   * Returns a shallow clone of this object. This clone is used while applying\n   * a patch to the table, and `freeze()` is called on it when we have finished\n   * applying the patch.\n   */\n  _clone() {\n    if (!this[OBJECT_ID]) {\n      throw new RangeError('clone() requires the objectId to be set')\n    }\n    return instantiateTable(this[OBJECT_ID], copyObject(this.entries), copyObject(this.opIds))\n  }\n\n  /**\n   * Sets the entry with key `id` to `value`. `opId` is the ID of the operation\n   * performing this assignment. This method is for internal use only; it is\n   * not part of the public API of Automerge.Table.\n   */\n  _set(id, value, opId) {\n    if (Object.isFrozen(this.entries)) {\n      throw new Error('A table can only be modified in a change function')\n    }\n    if (isObject(value) && !Array.isArray(value)) {\n      Object.defineProperty(value, 'id', {value: id, enumerable: true})\n    }\n    this.entries[id] = value\n    this.opIds[id] = opId\n  }\n\n  /**\n   * Removes the row with unique ID `id` from the table.\n   */\n  remove(id) {\n    if (Object.isFrozen(this.entries)) {\n      throw new Error('A table can only be modified in a change function')\n    }\n    delete this.entries[id]\n    delete this.opIds[id]\n  }\n\n  /**\n   * Makes this object immutable. This is called after a change has been made.\n   */\n  _freeze() {\n    Object.freeze(this.entries)\n    Object.freeze(this.opIds)\n    Object.freeze(this)\n  }\n\n  /**\n   * Returns a writeable instance of this table. This instance is returned when\n   * the table is accessed within a change callback. `context` is the proxy\n   * context that keeps track of the mutations.\n   */\n  getWriteable(context, path) {\n    if (!this[OBJECT_ID]) {\n      throw new RangeError('getWriteable() requires the objectId to be set')\n    }\n\n    const instance = Object.create(WriteableTable.prototype)\n    instance[OBJECT_ID] = this[OBJECT_ID]\n    instance.context = context\n    instance.entries = this.entries\n    instance.opIds = this.opIds\n    instance.path = path\n    return instance\n  }\n\n  /**\n   * Returns an object containing the table entries, indexed by objectID,\n   * for serializing an Automerge document to JSON.\n   */\n  toJSON() {\n    const rows = {}\n    for (let id of this.ids) rows[id] = this.byId(id)\n    return rows\n  }\n}\n\n/**\n * An instance of this class is used when a table is accessed within a change\n * callback.\n */\nclass WriteableTable extends Table {\n  /**\n   * Returns a proxied version of the row with ID `id`. This row object can be\n   * modified within a change callback.\n   */\n  byId(id) {\n    if (isObject(this.entries[id]) && this.entries[id].id === id) {\n      const objectId = this.entries[id][OBJECT_ID]\n      const path = this.path.concat([{key: id, objectId}])\n      return this.context.instantiateObject(path, objectId, ['id'])\n    }\n  }\n\n  /**\n   * Adds a new row to the table. The row is given as a map from\n   * column name to value. Returns the objectId of the new row.\n   */\n  add(row) {\n    return this.context.addTableRow(this.path, row)\n  }\n\n  /**\n   * Removes the row with ID `id` from the table. Throws an exception if the row\n   * does not exist in the table.\n   */\n  remove(id) {\n    if (isObject(this.entries[id]) && this.entries[id].id === id) {\n      this.context.deleteTableRow(this.path, id, this.opIds[id])\n    } else {\n      throw new RangeError(`There is no row with ID ${id} in this table`)\n    }\n  }\n}\n\n/**\n * This function is used to instantiate a Table object in the context of\n * applying a patch (see apply_patch.js).\n */\nfunction instantiateTable(objectId, entries, opIds) {\n  const instance = Object.create(Table.prototype)\n  if (!objectId) {\n    throw new RangeError('instantiateTable requires an objectId to be given')\n  }\n  instance[OBJECT_ID] = objectId\n  instance[CONFLICTS] = Object.freeze({})\n  instance.entries = entries || {}\n  instance.opIds = opIds || {}\n  return instance\n}\n\nmodule.exports = { Table, instantiateTable }\n"
  },
  {
    "path": "frontend/text.js",
    "content": "const { OBJECT_ID } = require('./constants')\nconst { isObject } = require('../src/common')\n\nclass Text {\n  constructor (text) {\n    if (typeof text === 'string') {\n      const elems = [...text].map(value => ({value}))\n      return instantiateText(undefined, elems) // eslint-disable-line\n    } else if (Array.isArray(text)) {\n      const elems = text.map(value => ({value}))\n      return instantiateText(undefined, elems) // eslint-disable-line\n    } else if (text === undefined) {\n      return instantiateText(undefined, []) // eslint-disable-line\n    } else {\n      throw new TypeError(`Unsupported initial value for Text: ${text}`)\n    }\n  }\n\n  get length () {\n    return this.elems.length\n  }\n\n  get (index) {\n    const value = this.elems[index].value\n    if (this.context && isObject(value)) {\n      const objectId = value[OBJECT_ID]\n      const path = this.path.concat([{key: index, objectId}])\n      return this.context.instantiateObject(path, objectId)\n    } else {\n      return value\n    }\n  }\n\n  getElemId (index) {\n    return this.elems[index].elemId\n  }\n\n  /**\n   * Iterates over the text elements character by character, including any\n   * inline objects.\n   */\n  [Symbol.iterator] () {\n    let elems = this.elems, index = -1\n    return {\n      next () {\n        index += 1\n        if (index < elems.length) {\n          return {done: false, value: elems[index].value}\n        } else {\n          return {done: true}\n        }\n      }\n    }\n  }\n\n  /**\n   * Returns the content of the Text object as a simple string, ignoring any\n   * non-character elements.\n   */\n  toString() {\n    // Concatting to a string is faster than creating an array and then\n    // .join()ing for small (<100KB) arrays.\n    // https://jsperf.com/join-vs-loop-w-type-test\n    let str = ''\n    for (const elem of this.elems) {\n      if (typeof elem.value === 'string') str += elem.value\n    }\n    return str\n  }\n\n  /**\n   * Returns the content of the Text object as a sequence of strings,\n   * interleaved with non-character elements.\n   *\n   * For example, the value ['a', 'b', {x: 3}, 'c', 'd'] has spans:\n   * => ['ab', {x: 3}, 'cd']\n   */\n  toSpans() {\n    let spans = []\n    let chars = ''\n    for (const elem of this.elems) {\n      if (typeof elem.value === 'string') {\n        chars += elem.value\n      } else {\n        if (chars.length > 0) {\n          spans.push(chars)\n          chars = ''\n        }\n        spans.push(elem.value)\n      }\n    }\n    if (chars.length > 0) {\n      spans.push(chars)\n    }\n    return spans\n  }\n\n  /**\n   * Returns the content of the Text object as a simple string, so that the\n   * JSON serialization of an Automerge document represents text nicely.\n   */\n  toJSON() {\n    return this.toString()\n  }\n\n  /**\n   * Returns a writeable instance of this object. This instance is returned when\n   * the text object is accessed within a change callback. `context` is the\n   * proxy context that keeps track of the mutations.\n   */\n  getWriteable(context, path) {\n    if (!this[OBJECT_ID]) {\n      throw new RangeError('getWriteable() requires the objectId to be set')\n    }\n\n    const instance = instantiateText(this[OBJECT_ID], this.elems)\n    instance.context = context\n    instance.path = path\n    return instance\n  }\n\n  /**\n   * Updates the list item at position `index` to a new value `value`.\n   */\n  set (index, value) {\n    if (this.context) {\n      this.context.setListIndex(this.path, index, value)\n    } else if (!this[OBJECT_ID]) {\n      this.elems[index].value = value\n    } else {\n      throw new TypeError('Automerge.Text object cannot be modified outside of a change block')\n    }\n    return this\n  }\n\n  /**\n   * Inserts new list items `values` starting at position `index`.\n   */\n  insertAt(index, ...values) {\n    if (this.context) {\n      this.context.splice(this.path, index, 0, values)\n    } else if (!this[OBJECT_ID]) {\n      this.elems.splice(index, 0, ...values.map(value => ({value})))\n    } else {\n      throw new TypeError('Automerge.Text object cannot be modified outside of a change block')\n    }\n    return this\n  }\n\n  /**\n   * Deletes `numDelete` list items starting at position `index`.\n   * if `numDelete` is not given, one item is deleted.\n   */\n  deleteAt(index, numDelete = 1) {\n    if (this.context) {\n      this.context.splice(this.path, index, numDelete, [])\n    } else if (!this[OBJECT_ID]) {\n      this.elems.splice(index, numDelete)\n    } else {\n      throw new TypeError('Automerge.Text object cannot be modified outside of a change block')\n    }\n    return this\n  }\n}\n\n// Read-only methods that can delegate to the JavaScript built-in array\nfor (let method of ['concat', 'every', 'filter', 'find', 'findIndex', 'forEach', 'includes',\n                    'indexOf', 'join', 'lastIndexOf', 'map', 'reduce', 'reduceRight',\n                    'slice', 'some', 'toLocaleString']) {\n  Text.prototype[method] = function (...args) {\n    const array = [...this]\n    return array[method](...args)\n  }\n}\n\nfunction instantiateText(objectId, elems) {\n  const instance = Object.create(Text.prototype)\n  instance[OBJECT_ID] = objectId\n  instance.elems = elems\n  return instance\n}\n\nmodule.exports = { Text, instantiateText }\n"
  },
  {
    "path": "karma.conf.js",
    "content": "const path = require('path')\nconst webpack = require('webpack')\nconst webpackConfig = require('./webpack.config.js')\n\n// Karma-Webpack needs these gone\ndelete webpackConfig.entry\ndelete webpackConfig.output.filename\n\n// Don't mix dist/\nwebpackConfig.output.path = path.join(webpackConfig.output.path, 'test')\n\n// You're importing *a lot* of Node-specific code so the bundle is huge...\nwebpackConfig.plugins = [\n  new webpack.DefinePlugin({\n    'process.env.TEST_DIST': JSON.stringify(process.env.TEST_DIST) || '1',\n    'process.env.NODE_DEBUG': false,\n  }),\n  ...(webpackConfig.plugins || []),\n]\n\nmodule.exports = function(config) {\n  config.set({\n    frameworks: ['webpack', 'mocha', 'karma-typescript'],\n    files: [\n      { pattern: 'test/*test*.js', watched: false },\n      { pattern: 'test/*test*.ts' },\n    ],\n    preprocessors: {\n      'test/*test*.js': ['webpack'],\n      'test/*test*.ts': ['karma-typescript'],\n    },\n    webpack: webpackConfig,\n    browsers: ['Chrome', 'Firefox', 'Safari'],\n    singleRun: true,\n    // Webpack can handle Typescript via ts-loader\n    karmaTypescriptConfig: {\n      tsconfig: './tsconfig.json',\n      bundlerOptions: {\n        resolve: {\n          alias: { automerge: './src/automerge.js' }\n        }\n      },\n      compilerOptions: {\n        allowJs: true,\n        sourceMap: true,\n      }\n    }\n  })\n}\n"
  },
  {
    "path": "karma.sauce.js",
    "content": "const path = require('path')\nconst webpack = require('webpack')\nconst webpackConfig = require(\"./webpack.config.js\")\n\n// Karma-Webpack needs these gone\ndelete webpackConfig.entry\ndelete webpackConfig.output.filename\n\n// Don't mix dist/\nwebpackConfig.output.path = path.join(webpackConfig.output.path, 'test')\n\n// You're importing *a lot* of Node-specific code so the bundle is huge...\nwebpackConfig.plugins = [\n  new webpack.DefinePlugin({\n    'process.env.TEST_DIST': JSON.stringify(process.env.TEST_DIST) || '1',\n    'process.env.NODE_DEBUG': false,\n  }),\n  ...(webpackConfig.plugins || []),\n]\n\nmodule.exports = function(config) {\n  if (!process.env.SAUCE_USERNAME || !process.env.SAUCE_ACCESS_KEY) {\n    console.log('Make sure the SAUCE_USERNAME and SAUCE_ACCESS_KEY environment variables are set.') // eslint-disable-line\n    process.exit(1)\n  }\n\n  // Browsers to run on Sauce Labs\n  // Check out https://saucelabs.com/platforms for all browser/OS combos\n  const customLaunchers = {\n    sl_chrome: {\n      base: 'SauceLabs',\n      browserName: 'chrome',\n      platform: 'Windows 10',\n      version: 'latest'\n    },\n    sl_firefox: {\n      base: 'SauceLabs',\n      browserName: 'firefox',\n      platform: 'Windows 10',\n      version: 'latest'\n    },\n    sl_edge: {\n      base: 'SauceLabs',\n      browserName: 'MicrosoftEdge',\n      platform: 'Windows 10',\n      version: 'latest'\n    },\n    sl_safari_mac: {\n      base: 'SauceLabs',\n      browserName: 'safari',\n      platform: 'macOS 10.15',\n      version: 'latest'\n    }\n  }\n\n  config.set({\n    frameworks: ['webpack', 'mocha', 'karma-typescript'],\n    files: [\n      { pattern: 'test/*test*.js', watched: false },\n      { pattern: 'test/*test*.ts' },\n    ],\n    preprocessors: {\n      'test/*test*.js': ['webpack'],\n      'test/*test*.ts': ['karma-typescript'],\n    },\n    webpack: webpackConfig,\n    karmaTypescriptConfig: {\n      tsconfig: './tsconfig.json',\n      bundlerOptions: {\n        resolve: {\n          alias: { automerge: './src/automerge.js' }\n        }\n      },\n      compilerOptions: {\n        allowJs: true,\n        sourceMap: true,\n      }\n    },\n    port: 9876,\n    captureTimeout: 120000,\n    sauceLabs: {\n      testName: 'Automerge unit tests',\n      startConnect: false, // Sauce Connect is started in GitHub action\n      tunnelIdentifier: 'github-action-tunnel'\n    },\n    customLaunchers,\n    browsers: Object.keys(customLaunchers),\n    reporters: ['progress', 'saucelabs'],\n    singleRun: true\n  })\n}\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"automerge\",\n  \"version\": \"1.0.1-preview.7\",\n  \"description\": \"Data structures for building collaborative applications\",\n  \"main\": \"src/automerge.js\",\n  \"browser\": \"dist/automerge.js\",\n  \"types\": \"@types/automerge/index.d.ts\",\n  \"scripts\": {\n    \"browsertest\": \"karma start\",\n    \"coverage\": \"nyc --reporter=html --reporter=text mocha\",\n    \"test\": \"mocha\",\n    \"testwasm\": \"mocha --file test/wasm.js\",\n    \"build\": \"webpack && copyfiles --flat @types/automerge/index.d.ts dist\",\n    \"prepublishOnly\": \"npm run-script build\",\n    \"lint\": \"eslint .\"\n  },\n  \"author\": \"\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+ssh://git@github.com/automerge/automerge.git\"\n  },\n  \"bugs\": {\n    \"url\": \"https://github.com/automerge/automerge/issues\"\n  },\n  \"homepage\": \"https://github.com/automerge/automerge\",\n  \"license\": \"MIT\",\n  \"files\": [\n    \"/src/**\",\n    \"/frontend/**\",\n    \"/backend/**\",\n    \"/test/**\",\n    \"/@types/**\",\n    \"/dist/**\",\n    \"/img/**\",\n    \"/*.md\",\n    \"/LICENSE\",\n    \"/.babelrc\",\n    \"/.eslintrc.json\",\n    \"/.mocharc.yaml\",\n    \"/karma.*.js\",\n    \"/tsconfig.json\",\n    \"/webpack.config.js\"\n  ],\n  \"dependencies\": {\n    \"fast-sha256\": \"^1.3.0\",\n    \"pako\": \"^2.0.3\",\n    \"uuid\": \"^3.4.0\"\n  },\n  \"devDependencies\": {\n    \"@types/mocha\": \"^8.2.1\",\n    \"@types/node\": \"^14.14.31\",\n    \"copyfiles\": \"^2.4.1\",\n    \"eslint\": \"^7.24.0\",\n    \"eslint-plugin-compat\": \"^3.9.0\",\n    \"karma\": \"^6.1.1\",\n    \"karma-chrome-launcher\": \"^3.1.0\",\n    \"karma-firefox-launcher\": \"^2.1.0\",\n    \"karma-mocha\": \"^2.0.1\",\n    \"karma-safari-launcher\": \"^1.0.0\",\n    \"karma-sauce-launcher\": \"^4.3.5\",\n    \"karma-typescript\": \"^5.4.0\",\n    \"karma-webpack\": \"^5.0.0\",\n    \"mocha\": \"^8.3.0\",\n    \"nyc\": \"^15.1.0\",\n    \"sinon\": \"^9.2.4\",\n    \"ts-node\": \"^9.1.1\",\n    \"tsconfig-paths\": \"^3.9.0\",\n    \"typescript\": \"^4.1.5\",\n    \"watchify\": \"^4.0.0\",\n    \"webpack\": \"^5.24.0\",\n    \"webpack-cli\": \"^4.5.0\"\n  },\n  \"resolutions\": {\n    \"karma-sauce-launcher/selenium-webdriver\": \"4.0.0-alpha.7\"\n  },\n  \"browserslist\": {\n    \"production\": [\n      \"defaults\",\n      \"not IE 11\",\n      \"maintained node versions\"\n    ],\n    \"web\": [\n      \"defaults\",\n      \"not IE 11\"\n    ]\n  }\n}\n"
  },
  {
    "path": "src/automerge.js",
    "content": "const uuid = require('./uuid')\nconst Frontend = require('../frontend')\nconst { OPTIONS } = require('../frontend/constants')\nconst { encodeChange, decodeChange } = require('../backend/columnar')\nconst { isObject } = require('./common')\nlet backend = require('../backend') // mutable: can be overridden with setDefaultBackend()\n\n/**\n * Automerge.* API\n * The functions in this file constitute the publicly facing Automerge API which combines\n * the features of the Frontend (a document interface) and the backend (CRDT operations)\n */\n\nfunction init(options) {\n  if (typeof options === 'string') {\n    options = {actorId: options}\n  } else if (typeof options === 'undefined') {\n    options = {}\n  } else if (!isObject(options)) {\n    throw new TypeError(`Unsupported options for init(): ${options}`)\n  }\n  return Frontend.init(Object.assign({backend}, options))\n}\n\n/**\n * Returns a new document object initialized with the given state.\n */\nfunction from(initialState, options) {\n  const changeOpts = {message: 'Initialization'}\n  return change(init(options), changeOpts, doc => Object.assign(doc, initialState))\n}\n\nfunction change(doc, options, callback) {\n  const [newDoc] = Frontend.change(doc, options, callback)\n  return newDoc\n}\n\nfunction emptyChange(doc, options) {\n  const [newDoc] = Frontend.emptyChange(doc, options)\n  return newDoc\n}\n\nfunction clone(doc, options = {}) {\n  const state = backend.clone(Frontend.getBackendState(doc, 'clone'))\n  return applyPatch(init(options), backend.getPatch(state), state, [], options)\n}\n\nfunction free(doc) {\n  backend.free(Frontend.getBackendState(doc, 'free'))\n}\n\nfunction load(data, options = {}) {\n  const state = backend.load(data)\n  return applyPatch(init(options), backend.getPatch(state), state, [data], options)\n}\n\nfunction save(doc) {\n  return backend.save(Frontend.getBackendState(doc, 'save'))\n}\n\nfunction merge(localDoc, remoteDoc) {\n  const localState = Frontend.getBackendState(localDoc, 'merge')\n  const remoteState = Frontend.getBackendState(remoteDoc, 'merge', 'second')\n  const changes = backend.getChangesAdded(localState, remoteState)\n  const [updatedDoc] = applyChanges(localDoc, changes)\n  return updatedDoc\n}\n\nfunction getChanges(oldDoc, newDoc) {\n  const oldState = Frontend.getBackendState(oldDoc, 'getChanges')\n  const newState = Frontend.getBackendState(newDoc, 'getChanges', 'second')\n  return backend.getChanges(newState, backend.getHeads(oldState))\n}\n\nfunction getAllChanges(doc) {\n  return backend.getAllChanges(Frontend.getBackendState(doc, 'getAllChanges'))\n}\n\nfunction applyPatch(doc, patch, backendState, changes, options) {\n  const newDoc = Frontend.applyPatch(doc, patch, backendState)\n  const patchCallback = options.patchCallback || doc[OPTIONS].patchCallback\n  if (patchCallback) {\n    patchCallback(patch, doc, newDoc, false, changes)\n  }\n  return newDoc\n}\n\nfunction applyChanges(doc, changes, options = {}) {\n  const oldState = Frontend.getBackendState(doc, 'applyChanges')\n  const [newState, patch] = backend.applyChanges(oldState, changes)\n  return [applyPatch(doc, patch, newState, changes, options), patch]\n}\n\nfunction equals(val1, val2) {\n  if (!isObject(val1) || !isObject(val2)) return val1 === val2\n  const keys1 = Object.keys(val1).sort(), keys2 = Object.keys(val2).sort()\n  if (keys1.length !== keys2.length) return false\n  for (let i = 0; i < keys1.length; i++) {\n    if (keys1[i] !== keys2[i]) return false\n    if (!equals(val1[keys1[i]], val2[keys2[i]])) return false\n  }\n  return true\n}\n\nfunction getHistory(doc) {\n  const actor = Frontend.getActorId(doc)\n  const history = getAllChanges(doc)\n  return history.map((change, index) => ({\n      get change () {\n        return decodeChange(change)\n      },\n      get snapshot () {\n        const state = backend.loadChanges(backend.init(), history.slice(0, index + 1))\n        return Frontend.applyPatch(init(actor), backend.getPatch(state), state)\n      }\n    })\n  )\n}\n\nfunction generateSyncMessage(doc, syncState) {\n  const state = Frontend.getBackendState(doc, 'generateSyncMessage')\n  return backend.generateSyncMessage(state, syncState)\n}\n\nfunction receiveSyncMessage(doc, oldSyncState, message) {\n  const oldBackendState = Frontend.getBackendState(doc, 'receiveSyncMessage')\n  const [backendState, syncState, patch] = backend.receiveSyncMessage(oldBackendState, oldSyncState, message)\n  if (!patch) return [doc, syncState, patch]\n\n  // The patchCallback is passed as argument all changes that are applied.\n  // We get those from the sync message if a patchCallback is present.\n  let changes = null\n  if (doc[OPTIONS].patchCallback) {\n    changes = backend.decodeSyncMessage(message).changes\n  }\n  return [applyPatch(doc, patch, backendState, changes, {}), syncState, patch]\n}\n\nfunction initSyncState() {\n  return backend.initSyncState()\n}\n\n/**\n * Replaces the default backend implementation with a different one.\n * This allows you to switch to using the Rust/WebAssembly implementation.\n */\nfunction setDefaultBackend(newBackend) {\n  backend = newBackend\n}\n\nmodule.exports = {\n  init, from, change, emptyChange, clone, free,\n  load, save, merge, getChanges, getAllChanges, applyChanges,\n  encodeChange, decodeChange, equals, getHistory, uuid,\n  Frontend, setDefaultBackend, generateSyncMessage, receiveSyncMessage, initSyncState,\n  get Backend() { return backend }\n}\n\nfor (let name of ['getObjectId', 'getObjectById', 'getActorId',\n     'setActorId', 'getConflicts', 'getLastLocalChange',\n     'Text', 'Table', 'Counter', 'Observable', 'Int', 'Uint', 'Float64']) {\n  module.exports[name] = Frontend[name]\n}\n"
  },
  {
    "path": "src/common.js",
    "content": "function isObject(obj) {\n  return typeof obj === 'object' && obj !== null\n}\n\n/**\n * Returns a shallow copy of the object `obj`. Faster than `Object.assign({}, obj)`.\n * https://jsperf.com/cloning-large-objects/1\n */\nfunction copyObject(obj) {\n  if (!isObject(obj)) return {}\n  let copy = {}\n  for (let key of Object.keys(obj)) {\n    copy[key] = obj[key]\n  }\n  return copy\n}\n\n/**\n * Takes a string in the form that is used to identify operations (a counter concatenated\n * with an actor ID, separated by an `@` sign) and returns an object `{counter, actorId}`.\n */\nfunction parseOpId(opId) {\n  const match = /^(\\d+)@(.*)$/.exec(opId || '')\n  if (!match) {\n    throw new RangeError(`Not a valid opId: ${opId}`)\n  }\n  return {counter: parseInt(match[1], 10), actorId: match[2]}\n}\n\n/**\n * Returns true if the two byte arrays contain the same data, false if not.\n */\nfunction equalBytes(array1, array2) {\n  if (!(array1 instanceof Uint8Array) || !(array2 instanceof Uint8Array)) {\n    throw new TypeError('equalBytes can only compare Uint8Arrays')\n  }\n  if (array1.byteLength !== array2.byteLength) return false\n  for (let i = 0; i < array1.byteLength; i++) {\n    if (array1[i] !== array2[i]) return false\n  }\n  return true\n}\n\n/**\n * Creates an array containing the value `null` repeated `length` times.\n */\nfunction createArrayOfNulls(length) {\n  const array = new Array(length)\n  for (let i = 0; i < length; i++) array[i] = null\n  return array\n}\n\nmodule.exports = {\n  isObject, copyObject, parseOpId, equalBytes, createArrayOfNulls\n}\n"
  },
  {
    "path": "src/uuid.js",
    "content": "const { v4: uuid } = require('uuid')\n\nfunction defaultFactory() {\n  return uuid().replace(/-/g, '')\n}\n\nlet factory = defaultFactory\n\nfunction makeUuid() {\n  return factory()\n}\n\nmakeUuid.setFactory = newFactory => { factory = newFactory }\nmakeUuid.reset = () => { factory = defaultFactory }\n\nmodule.exports = makeUuid\n"
  },
  {
    "path": "test/backend_test.js",
    "content": "/* eslint-disable no-unused-vars */\nconst assert = require('assert')\nconst Automerge = process.env.TEST_DIST === '1' ? require('../dist/automerge') : require('../src/automerge')\nconst Backend = Automerge.Backend\nconst { encodeChange, decodeChange } = require('../backend/columnar')\nconst uuid = require('../src/uuid')\n\nfunction hash(change) {\n  return decodeChange(encodeChange(change)).hash\n}\n\ndescribe('Automerge.Backend', () => {\n  describe('incremental diffs', () => {\n    it('should assign to a key in a map', () => {\n      const actor = uuid()\n      const change1 = {actor, seq: 1, startOp: 1, time: 0, deps: [], ops: [\n        {action: 'set', obj: '_root', key: 'bird', value: 'magpie', pred: []}\n      ]}\n      const s0 = Backend.init()\n      const [s1, patch1] = Backend.applyChanges(s0, [encodeChange(change1)])\n      assert.deepStrictEqual(patch1, {\n        clock: {[actor]: 1}, deps: [hash(change1)], maxOp: 1, pendingChanges: 0,\n        diffs: {objectId: '_root', type: 'map', props: {\n          bird: {[`1@${actor}`]: {type: 'value', value: 'magpie'}}\n        }}\n      })\n    })\n\n    it('should increment a key in a map', () => {\n      const actor = uuid()\n      const change1 = {actor, seq: 1, startOp: 1, time: 0, deps: [], ops: [\n        {action: 'set', obj: '_root', key: 'counter', value: 1, datatype: 'counter', pred: []}\n      ]}\n      const change2 = {actor, seq: 2, startOp: 2, time: 0, deps: [hash(change1)], ops: [\n        {action: 'inc', obj: '_root', key: 'counter', value: 2, pred: [`1@${actor}`]}\n      ]}\n      const s0 = Backend.init()\n      const [s1, patch1] = Backend.applyChanges(s0, [encodeChange(change1)])\n      const [s2, patch2] = Backend.applyChanges(s1, [encodeChange(change2)])\n      assert.deepStrictEqual(patch2, {\n        clock: {[actor]: 2}, deps: [hash(change2)], maxOp: 2, pendingChanges: 0,\n        diffs: {objectId: '_root', type: 'map', props: {\n          counter: {[`1@${actor}`]: {type: 'value', value: 3, datatype: 'counter'}}\n        }}\n      })\n    })\n\n    it('should make a conflict on assignment to the same key', () => {\n      const change1 = {actor: '111111', seq: 1, startOp: 1, time: 0, deps: [], ops: [\n        {action: 'set', obj: '_root', key: 'bird', value: 'magpie', pred: []}\n      ]}\n      const change2 = {actor: '222222', seq: 1, startOp: 2, time: 0, deps: [hash(change1)], ops: [\n        {action: 'set', obj: '_root', key: 'bird', value: 'blackbird', pred: []}\n      ]}\n      const s0 = Backend.init()\n      const [s1, patch1] = Backend.applyChanges(s0, [encodeChange(change1)])\n      const [s2, patch2] = Backend.applyChanges(s1, [encodeChange(change2)])\n      assert.deepStrictEqual(patch2, {\n        clock: {111111: 1, 222222: 1}, deps: [hash(change2)], maxOp: 2, pendingChanges: 0,\n        diffs: {objectId: '_root', type: 'map', props: {\n          bird: {\n            '1@111111': {type: 'value', value: 'magpie'},\n            '2@222222': {type: 'value', value: 'blackbird'}\n          }\n        }}\n      })\n    })\n\n    it('should delete a key from a map', () => {\n      const actor = uuid()\n      const change1 = {actor, seq: 1, startOp: 1, time: 0, deps: [], ops: [\n        {action: 'set', obj: '_root', key: 'bird', value: 'magpie', pred: []}\n      ]}\n      const change2 = {actor, seq: 2, startOp: 2, time: 0, deps: [hash(change1)], ops: [\n        {action: 'del', obj: '_root', key: 'bird', pred: [`1@${actor}`]}\n      ]}\n      const s0 = Backend.init()\n      const [s1, patch1] = Backend.applyChanges(s0, [encodeChange(change1)])\n      const [s2, patch2] = Backend.applyChanges(s1, [encodeChange(change2)])\n      assert.deepStrictEqual(patch2, {\n        clock: {[actor]: 2}, deps: [hash(change2)], maxOp: 2, pendingChanges: 0,\n        diffs: {objectId: '_root', type: 'map', props: {bird: {}}}\n      })\n    })\n\n    it('should create nested maps', () => {\n      const actor = uuid()\n      const change1 = {actor, seq: 1, startOp: 1, time: 0, deps: [], ops: [\n        {action: 'makeMap', obj: '_root', key: 'birds', pred: []},\n        {action: 'set', obj: `1@${actor}`, key: 'wrens', value: 3, pred: []}\n      ]}\n      const s0 = Backend.init()\n      const [s1, patch1] = Backend.applyChanges(s0, [encodeChange(change1)])\n      assert.deepStrictEqual(patch1, {\n        clock: {[actor]: 1}, deps: [hash(change1)], maxOp: 2, pendingChanges: 0,\n        diffs: {objectId: '_root', type: 'map', props: {birds: {[`1@${actor}`]: {\n          objectId: `1@${actor}`, type: 'map', props: {wrens: {[`2@${actor}`]: {type: 'value', value: 3, datatype: 'int'}}}\n        }}}}\n      })\n    })\n\n    it('should assign to keys in nested maps', () => {\n      const actor = uuid()\n      const change1 = {actor, seq: 1, startOp: 1, time: 0, deps: [], ops: [\n        {action: 'makeMap', obj: '_root', key: 'birds', pred: []},\n        {action: 'set', obj: `1@${actor}`, key: 'wrens', value: 3, pred: []}\n      ]}\n      const change2 = {actor, seq: 2, startOp: 3, time: 0, deps: [hash(change1)], ops: [\n        {action: 'set', obj: `1@${actor}`, key: 'sparrows', value: 15, pred: []}\n      ]}\n      const s0 = Backend.init()\n      const [s1, patch1] = Backend.applyChanges(s0, [encodeChange(change1)])\n      const [s2, patch2] = Backend.applyChanges(s1, [encodeChange(change2)])\n      assert.deepStrictEqual(patch2, {\n        clock: {[actor]: 2}, deps: [hash(change2)], maxOp: 3, pendingChanges: 0,\n        diffs: {objectId: '_root', type: 'map', props: {birds: {[`1@${actor}`]: {\n          objectId: `1@${actor}`, type: 'map', props: {sparrows: {[`3@${actor}`]: {type: 'value', value: 15, datatype: 'int'}}}\n        }}}}\n      })\n    })\n\n    it('should handle deletion of nested maps', () => {\n      const actor = uuid()\n      const change1 = {actor, seq: 1, startOp: 1, time: 0, deps: [], ops: [\n        {action: 'makeMap', obj: '_root', key: 'birds', pred: []},\n        {action: 'set', obj: `1@${actor}`, key: 'wrens', value: 3, pred: []}\n      ]}\n      const change2 = {actor, seq: 2, startOp: 3, time: 0, deps: [hash(change1)], ops: [\n        {action: 'del', obj: '_root', key: 'birds', pred: [`1@${actor}`]}\n      ]}\n      const s0 = Backend.init()\n      const [s1, patch1] = Backend.applyChanges(s0, [change1, change2].map(encodeChange))\n      assert.deepStrictEqual(patch1, {\n        clock: {[actor]: 2}, deps: [hash(change2)], maxOp: 3, pendingChanges: 0,\n        diffs: {objectId: '_root', type: 'map', props: {birds: {}}}\n      })\n    })\n\n    it('should handle conflicts on nested maps', () => {\n      const actor1 = uuid(), actor2 = uuid()\n      const change1 = {actor: actor1, seq: 1, startOp: 1, time: 0, deps: [], ops: [\n        {action: 'makeMap', obj: '_root', key: 'birds', pred: []},\n        {action: 'set', obj: `1@${actor1}`, key: 'wrens', value: 3, pred: []}\n      ]}\n      const change2 = {actor: actor1, seq: 2, startOp: 3, time: 0, deps: [hash(change1)], ops: [\n        {action: 'makeMap', obj: '_root', key: 'birds', pred: [`1@${actor1}`]},\n        {action: 'set', obj: `3@${actor1}`, key: 'hawks', value: 1, pred: []}\n      ]}\n      const change3 = {actor: actor2, seq: 1, startOp: 3, time: 0, deps: [hash(change1)], ops: [\n        {action: 'makeMap', obj: '_root', key: 'birds', pred: [`1@${actor1}`]},\n        {action: 'set', obj: `3@${actor2}`, key: 'sparrows', value: 15, pred: []}\n      ]}\n      const s0 = Backend.init()\n      const [s1, patch1] = Backend.applyChanges(s0, [change1, change2, change3].map(encodeChange))\n      assert.deepStrictEqual(patch1, {\n        clock: {[actor1]: 2, [actor2]: 1}, deps: [hash(change2), hash(change3)].sort(), maxOp: 4, pendingChanges: 0,\n        diffs: {objectId: '_root', type: 'map', props: {birds: {\n          [`3@${actor1}`]: {objectId: `3@${actor1}`, type: 'map', props: {\n            hawks: {[`4@${actor1}`]: {type: 'value', value: 1, datatype: 'int'}}\n          }},\n          [`3@${actor2}`]: {objectId: `3@${actor2}`, type: 'map', props: {\n            sparrows: {[`4@${actor2}`]: {type: 'value', value: 15, datatype: 'int'}}\n          }}\n        }}}\n      })\n    })\n\n    it('should handle updates inside conflicted map keys', () => {\n      const actor1 = uuid(), actor2 = uuid()\n      const change1 = {actor: actor1, seq: 1, startOp: 1, time: 0, deps: [], ops: [\n        {action: 'makeMap', obj: '_root', key: 'birds', pred: []},\n        {action: 'set', obj: `1@${actor1}`, key: 'hawks', value: 1, pred: []}\n      ]}\n      const change2 = {actor: actor2, seq: 1, startOp: 1, time: 0, deps: [], ops: [\n        {action: 'makeMap', obj: '_root', key: 'birds', pred: []},\n        {action: 'set', obj: `1@${actor2}`, key: 'sparrows', value: 15, pred: []}\n      ]}\n      const change3 = {actor: actor1, seq: 2, startOp: 3, time: 0, deps: [hash(change1), hash(change2)].sort(), ops: [\n        {action: 'set', obj: `1@${actor2}`, key: 'sparrows', value: 17, pred: [`2@${actor2}`]}\n      ]}\n      const s0 = Backend.init()\n      const [s1, patch1] = Backend.applyChanges(s0, [encodeChange(change1), encodeChange(change2)])\n      const [s2, patch2] = Backend.applyChanges(s1, [encodeChange(change3)])\n      assert.deepStrictEqual(patch2, {\n        clock: {[actor1]: 2, [actor2]: 1}, deps: [hash(change3)], maxOp: 3, pendingChanges: 0,\n        diffs: {objectId: '_root', type: 'map', props: {birds: {\n          [`1@${actor1}`]: {objectId: `1@${actor1}`, type: 'map', props: {}},\n          [`1@${actor2}`]: {objectId: `1@${actor2}`, type: 'map', props: {\n            sparrows: {[`3@${actor1}`]: {type: 'value', value: 17, datatype: 'int'}}\n          }}\n        }}}\n      })\n    })\n\n    it('should handle updates inside deleted maps', () => {\n      const actor1 = uuid(), actor2 = uuid()\n      const change1 = {actor: actor1, seq: 1, startOp: 1, time: 0, deps: [], ops: [\n        {action: 'makeMap', obj: '_root', key: 'birds', pred: []},\n        {action: 'set', obj: `1@${actor1}`, key: 'hawks', value: 1, pred: []}\n      ]}\n      const change2 = {actor: actor2, seq: 1, startOp: 3, time: 0, deps: [hash(change1)], ops: [\n        {action: 'del', obj: '_root', key: 'birds', pred: [`1@${actor1}`]}\n      ]}\n      const change3 = {actor: actor1, seq: 2, startOp: 3, time: 0, deps: [hash(change1)], ops: [\n        {action: 'set', obj: `1@${actor1}`, key: 'hawks', value: 2, pred: [`2@${actor1}`]}\n      ]}\n      const s0 = Backend.init()\n      const [s1, patch1] = Backend.applyChanges(s0, [encodeChange(change1), encodeChange(change2)])\n      const [s2, patch2] = Backend.applyChanges(s1, [encodeChange(change3)])\n      assert.deepStrictEqual(patch1, {\n        clock: {[actor1]: 1, [actor2]: 1}, deps: [hash(change2)], maxOp: 3, pendingChanges: 0,\n        diffs: {objectId: '_root', type: 'map', props: {birds: {}}}\n      })\n      assert.deepStrictEqual(patch2, {\n        clock: {[actor1]: 2, [actor2]: 1}, deps: [hash(change2), hash(change3)].sort(), maxOp: 3, pendingChanges: 0,\n        diffs: {objectId: '_root', type: 'map', props: {}}\n      })\n    })\n\n    it('should create lists', () => {\n      const actor = uuid()\n      const change1 = {actor, seq: 1, startOp: 1, time: 0, deps: [], ops: [\n        {action: 'makeList', obj: '_root', key: 'birds', pred: []},\n        {action: 'set', obj: `1@${actor}`, elemId: '_head', insert: true, value: 'chaffinch', pred: []}\n      ]}\n      const s0 = Backend.init()\n      const [s1, patch1] = Backend.applyChanges(s0, [encodeChange(change1)])\n      assert.deepStrictEqual(patch1, {\n        clock: {[actor]: 1}, deps: [hash(change1)], maxOp: 2, pendingChanges: 0,\n        diffs: {objectId: '_root', type: 'map', props: {birds: {[`1@${actor}`]: {\n          objectId: `1@${actor}`, type: 'list', edits: [\n            {action: 'insert', index: 0, elemId: `2@${actor}`, opId: `2@${actor}`, value: {type: 'value', value: 'chaffinch'}}\n          ]\n        }}}}\n      })\n    })\n\n    it('should apply updates inside lists', () => {\n      const actor = uuid()\n      const change1 = {actor, seq: 1, startOp: 1, time: 0, deps: [], ops: [\n        {action: 'makeList', obj: '_root', key: 'birds', pred: []},\n        {action: 'set', obj: `1@${actor}`, elemId: '_head', insert: true, value: 'chaffinch', pred: []}\n      ]}\n      const change2 = {actor, seq: 2, startOp: 3, time: 0, deps: [hash(change1)], ops: [\n        {action: 'set', obj: `1@${actor}`, elemId: `2@${actor}`, value: 'greenfinch', pred: [`2@${actor}`]}\n      ]}\n      const s0 = Backend.init()\n      const [s1, patch1] = Backend.applyChanges(s0, [encodeChange(change1)])\n      const [s2, patch2] = Backend.applyChanges(s1, [encodeChange(change2)])\n      assert.deepStrictEqual(patch2, {\n        clock: {[actor]: 2}, deps: [hash(change2)], maxOp: 3, pendingChanges: 0,\n        diffs: {objectId: '_root', type: 'map', props: {birds: {[`1@${actor}`]: {\n          objectId: `1@${actor}`, type: 'list', edits: [\n            {action: 'update', opId: `3@${actor}`, index: 0, value: {type: 'value', value: 'greenfinch'}}\n          ]\n        }}}}\n      })\n    })\n\n    it('should apply updates to objects inside list elements', () => {\n      const actor = uuid()\n      const change1 = {actor, seq: 1, startOp: 1, time: 0, deps: [], ops: [\n        {action: 'makeList', obj: '_root', key: 'todos', pred: []},\n        {action: 'makeMap', obj: `1@${actor}`, elemId: '_head', insert: true, pred: []},\n        {action: 'set', obj: `2@${actor}`, key: 'title', value: 'buy milk', pred: []},\n        {action: 'set', obj: `2@${actor}`, key: 'done', value: false, pred: []}\n      ]}\n      // insert a new list element and update the existing list element in the same change\n      const change2 = {actor, seq: 2, startOp: 5, time: 0, deps: [hash(change1)], ops: [\n        {action: 'makeMap', obj: `1@${actor}`, elemId: '_head', insert: true, pred: []},\n        {action: 'set', obj: `5@${actor}`, key: 'title', value: 'water plants', pred: []},\n        {action: 'set', obj: `5@${actor}`, key: 'done', value: false, pred: []},\n        {action: 'set', obj: `2@${actor}`, key: 'done', value: true, pred: [`4@${actor}`]}\n      ]}\n      const s0 = Backend.init()\n      const [s1, patch1] = Backend.applyChanges(s0, [encodeChange(change1)])\n      const [s2, patch2] = Backend.applyChanges(s1, [encodeChange(change2)])\n      assert.deepStrictEqual(patch2, {\n        clock: {[actor]: 2}, deps: [hash(change2)], maxOp: 8, pendingChanges: 0,\n        diffs: {objectId: '_root', type: 'map', props: {todos: {[`1@${actor}`]: {\n          objectId: `1@${actor}`, type: 'list', edits: [\n            {action: 'insert', index: 0, elemId: `5@${actor}`, opId: `5@${actor}`, value: {\n              objectId: `5@${actor}`, type: 'map', props: {\n                title: {[`6@${actor}`]: {type: 'value', value: 'water plants'}},\n                done: {[`7@${actor}`]: {type: 'value', value: false}}\n              }\n            }},\n            {action: 'update', index: 1, opId: `2@${actor}`, value: {\n              objectId: `2@${actor}`, type: 'map', props: {\n                done: {[`8@${actor}`]: {type: 'value', value: true}}\n              }\n            }}\n          ]\n        }}}}\n      })\n    })\n\n    it('should apply updates inside conflicted list elements', () => {\n      const actor1 = '01234567', actor2 = '89abcdef'\n      const change1 = {actor: actor1, seq: 1, startOp: 1, time: 0, deps: [], ops: [\n        {action: 'makeList', obj: '_root', key: 'todos', pred: []},\n        {action: 'makeMap', obj: `1@${actor1}`, elemId: '_head', insert: true, pred: []}\n      ]}\n      const change2 = {actor: actor1, seq: 2, startOp: 3, time: 0, deps: [hash(change1)], ops: [\n        {action: 'makeMap', obj: `1@${actor1}`, elemId: `2@${actor1}`, pred: [`2@${actor1}`]},\n        {action: 'set', obj: `3@${actor1}`, key: 'title', value: 'buy milk', pred: []},\n        {action: 'set', obj: `3@${actor1}`, key: 'done', value: false, pred: []}\n      ]}\n      const change3 = {actor: actor2, seq: 1, startOp: 3, time: 0, deps: [hash(change1)], ops: [\n        {action: 'makeMap', obj: `1@${actor1}`, elemId: `2@${actor1}`, pred: [`2@${actor1}`]},\n        {action: 'set', obj: `3@${actor2}`, key: 'title', value: 'water plants', pred: []},\n        {action: 'set', obj: `3@${actor2}`, key: 'done', value: false, pred: []}\n      ]}\n      const change4 = {actor: actor1, seq: 3, startOp: 6, time: 0, deps: [hash(change2), hash(change3)].sort(), ops: [\n        {action: 'set', obj: `3@${actor1}`, key: 'done', value: true, pred: [`5@${actor1}`]}\n      ]}\n      const s0 = Backend.init()\n      const [s1, patch1] = Backend.applyChanges(s0, [change1, change2, change3].map(encodeChange))\n      const [s2, patch2] = Backend.applyChanges(s1, [encodeChange(change4)])\n      assert.deepStrictEqual(patch2, {\n        clock: {[actor1]: 3, [actor2]: 1}, deps: [hash(change4)], maxOp: 6, pendingChanges: 0,\n        diffs: {objectId: '_root', type: 'map', props: {todos: {[`1@${actor1}`]: {\n          objectId: `1@${actor1}`, type: 'list', edits: [\n            {action: 'update', index: 0, opId: `3@${actor1}`, value: {\n              objectId: `3@${actor1}`, type: 'map', props: {\n                done: {[`6@${actor1}`]: {type: 'value', value: true}}\n              }\n            }},\n            {action: 'update', index: 0, opId: `3@${actor2}`, value: {\n              objectId: `3@${actor2}`, type: 'map', props: {}\n            }}\n          ]\n        }}}}\n      })\n    })\n\n    it('should overwrite list elements', () => {\n      const actor = uuid()\n      const change1 = {actor, seq: 1, startOp: 1, time: 0, deps: [], ops: [\n        {action: 'makeList', obj: '_root', key: 'todos', pred: []},\n        {action: 'makeMap', obj: `1@${actor}`, elemId: '_head', insert: true, pred: []},\n        {action: 'set', obj: `2@${actor}`, key: 'title', value: 'buy milk', pred: []},\n        {action: 'set', obj: `2@${actor}`, key: 'done', value: false, pred: []}\n      ]}\n      const change2 = {actor, seq: 2, startOp: 5, time: 0, deps: [hash(change1)], ops: [\n        {action: 'makeMap', obj: `1@${actor}`, elemId: `2@${actor}`, insert: false, pred: [`2@${actor}`]},\n        {action: 'set', obj: `5@${actor}`, key: 'title', value: 'water plants', pred: []},\n        {action: 'set', obj: `5@${actor}`, key: 'done', value: false, pred: []}\n      ]}\n      const s0 = Backend.init()\n      const [s1, patch1] = Backend.applyChanges(s0, [encodeChange(change1), encodeChange(change2)])\n      assert.deepStrictEqual(patch1, {\n        clock: {[actor]: 2}, deps: [hash(change2)], maxOp: 7, pendingChanges: 0,\n        diffs: {objectId: '_root', type: 'map', props: {todos: {[`1@${actor}`]: {\n          objectId: `1@${actor}`, type: 'list', edits: [\n            {action: 'insert', index: 0, elemId: `2@${actor}`, opId: `5@${actor}`, value: {\n              objectId: `5@${actor}`, type: 'map', props: {\n                title: {[`6@${actor}`]: {type: 'value', value: 'water plants'}},\n                done: {[`7@${actor}`]: {type: 'value', value: false}}\n              }\n            }}\n          ]\n        }}}}\n      })\n    })\n\n    it('should delete list elements', () => {\n      const actor = uuid()\n      const change1 = {actor, seq: 1, startOp: 1, time: 0, deps: [], ops: [\n        {action: 'makeList', obj: '_root', key: 'birds', pred: []},\n        {action: 'set', obj: `1@${actor}`, elemId: '_head', insert: true, value: 'chaffinch', pred: []}\n      ]}\n      const change2 = {actor, seq: 2, startOp: 3, time: 0, deps: [hash(change1)], ops: [\n        {action: 'del', obj: `1@${actor}`, elemId: `2@${actor}`, pred: [`2@${actor}`]}\n      ]}\n      const s0 = Backend.init()\n      const [s1, patch1] = Backend.applyChanges(s0, [encodeChange(change1)])\n      const [s2, patch2] = Backend.applyChanges(s1, [encodeChange(change2)])\n      assert.deepStrictEqual(patch2, {\n        clock: {[actor]: 2}, deps: [hash(change2)], maxOp: 3, pendingChanges: 0,\n        diffs: {objectId: '_root', type: 'map', props: {birds: {[`1@${actor}`]: {\n          objectId: `1@${actor}`, type: 'list', edits: [\n            {action: 'remove', index: 0, count: 1}\n          ]\n        }}}}\n      })\n    })\n\n    it('should handle list element insertion and deletion in the same change', () => {\n      const actor = uuid()\n      const change1 = {actor, seq: 1, startOp: 1, time: 0, deps: [], ops: [\n        {action: 'makeList', obj: '_root', key: 'birds', pred: []}\n      ]}\n      const change2 = {actor, seq: 2, startOp: 2, time: 0, deps: [hash(change1)], ops: [\n        {action: 'set', obj: `1@${actor}`, elemId: '_head', insert: true, value: 'chaffinch', pred: []},\n        {action: 'del', obj: `1@${actor}`, elemId: `2@${actor}`, pred: [`2@${actor}`]}\n      ]}\n      const s0 = Backend.init()\n      const [s1, patch1] = Backend.applyChanges(s0, [encodeChange(change1)])\n      const [s2, patch2] = Backend.applyChanges(s1, [encodeChange(change2)])\n      assert.deepStrictEqual(patch2, {\n        clock: {[actor]: 2}, deps: [hash(change2)], maxOp: 3, pendingChanges: 0,\n        diffs: {objectId: '_root', type: 'map', props: {birds: {[`1@${actor}`]: {\n          objectId: `1@${actor}`, type: 'list', edits: [\n            {action: 'insert', index: 0, elemId: `2@${actor}`, opId: `2@${actor}`, value: {type: 'value', value: 'chaffinch'}},\n            {action: 'remove', index: 0, count: 1}\n          ]\n        }}}}\n      })\n    })\n\n    it('should handle changes within conflicted objects', () => {\n      const actor1 = uuid(), actor2 = uuid()\n      const change1 = {actor: actor1, seq: 1, startOp: 1, time: 0, deps: [], ops: [\n        {action: 'makeList', obj: '_root', key: 'conflict', pred: []}\n      ]}\n      const change2 = {actor: actor2, seq: 1, startOp: 1, time: 0, deps: [], ops: [\n        {action: 'makeMap',  obj: '_root', key: 'conflict', pred: []}\n      ]}\n      const change3 = {actor: actor2, seq: 2, startOp: 2, time: 0, deps: [hash(change2)], ops: [\n        {action: 'set', obj: `1@${actor2}`, key: 'sparrows', value: 12, pred: []}\n      ]}\n      const s0 = Backend.init()\n      const [s1, patch1] = Backend.applyChanges(s0, [encodeChange(change1)])\n      const [s2, patch2] = Backend.applyChanges(s1, [encodeChange(change2)])\n      const [s3, patch3] = Backend.applyChanges(s2, [encodeChange(change3)])\n      assert.deepStrictEqual(patch3, {\n        clock: {[actor1]: 1, [actor2]: 2}, maxOp: 2, pendingChanges: 0,\n        deps: [hash(change1), hash(change3)].sort(),\n        diffs: {objectId: '_root', type: 'map', props: {conflict: {\n          [`1@${actor1}`]: {objectId: `1@${actor1}`, type: 'list', edits: []},\n          [`1@${actor2}`]: {objectId: `1@${actor2}`, type: 'map', props: {sparrows: {[`2@${actor2}`]: {type: 'value', value: 12, datatype: 'int'}}}}\n        }}}\n      })\n    })\n\n    it('should support Date objects at the root', () => {\n      const now = new Date()\n      const actor = uuid(), change = {actor, seq: 1, startOp: 1, time: 0, deps: [], ops: [\n        {action: 'set', obj: '_root', key: 'now', value: now.getTime(), datatype: 'timestamp', pred: []}\n      ]}\n      const s0 = Backend.init()\n      const [s1, patch] = Backend.applyChanges(s0, [encodeChange(change)])\n      assert.deepStrictEqual(patch, {\n        clock: {[actor]: 1}, deps: [hash(change)], maxOp: 1, pendingChanges: 0,\n        diffs: {objectId: '_root', type: 'map', props: {\n          now: {[`1@${actor}`]: {type: 'value', value: now.getTime(), datatype: 'timestamp'}}\n        }}\n      })\n    })\n\n    it('should support Date objects in a list', () => {\n      const now = new Date(), actor = uuid()\n      const change = {actor, seq: 1, startOp: 1, time: 0, deps: [], ops: [\n        {action: 'makeList', obj: '_root', key: 'list', pred: []},\n        {action: 'set', obj: `1@${actor}`, elemId: '_head', insert: true, value: now.getTime(), datatype: 'timestamp', pred: []}\n      ]}\n      const s0 = Backend.init()\n      const [s1, patch] = Backend.applyChanges(s0, [encodeChange(change)])\n      assert.deepStrictEqual(patch, {\n        clock: {[actor]: 1}, deps: [hash(change)], maxOp: 2, pendingChanges: 0,\n        diffs: {objectId: '_root', type: 'map', props: {list: {[`1@${actor}`]: {\n          objectId: `1@${actor}`, type: 'list', edits: [\n            {action: 'insert', index: 0, elemId: `2@${actor}`, opId: `2@${actor}`,\n              value: {type: 'value', value: now.getTime(), datatype: 'timestamp'}}\n          ]\n        }}}}\n      })\n    })\n\n    it('should handle updates to an object that has been deleted', () => {\n      const actor1 = uuid(), actor2 = uuid()\n      const change1 = {actor: actor1, seq: 1, startOp: 1, time: 0, deps: [], ops: [\n        {action: 'makeMap', obj: '_root', key: 'birds', pred: []},\n        {action: 'set', obj: `1@${actor1}`, key: 'blackbirds', value: 2, pred: []}\n      ]}\n      const change2 = {actor: actor2, seq: 1, startOp: 3, time: 0, deps: [hash(change1)], ops: [\n        {action: 'del', obj: '_root', key: 'birds', pred: [`1@${actor1}`]}\n      ]}\n      const change3 = {actor: actor1, seq: 2, startOp: 3, time: 0, deps: [hash(change1)], ops: [\n        {action: 'set', obj: `1@${actor1}`, key: 'blackbirds', value: 2, pred: []}\n      ]}\n      const s0 = Backend.init()\n      const [s1, patch1] = Backend.applyChanges(s0, [encodeChange(change1)])\n      const [s2, patch2] = Backend.applyChanges(s1, [encodeChange(change2)])\n      const [s3, patch3] = Backend.applyChanges(s2, [encodeChange(change3)])\n      assert.deepStrictEqual(patch3, {\n        clock: {[actor1]: 2, [actor2]: 1}, maxOp: 3, pendingChanges: 0,\n        deps: [hash(change2), hash(change3)].sort(),\n        diffs: {objectId: '_root', type: 'map', props: {}}\n      })\n    })\n\n    it('should handle updates to a deleted list element', () => {\n      const actor1 = uuid(), actor2 = uuid()\n      const change1 = {actor: actor1, seq: 1, startOp: 1, time: 0, deps: [], ops: [\n        {action: 'makeList', obj: '_root', key: 'todos', pred: []},\n        {action: 'makeMap', obj: `1@${actor1}`, elemId: '_head', insert: true, pred: []},\n        {action: 'set', obj: `2@${actor1}`, key: 'title', value: 'buy milk', pred: []},\n        {action: 'set', obj: `2@${actor1}`, key: 'done', value: false, pred: []}\n      ]}\n      const change2 = {actor: actor2, seq: 1, startOp: 5, time: 0, deps: [hash(change1)], ops: [\n        {action: 'del', obj: `1@${actor1}`, elemId: `2@${actor1}`, pred: [`2@${actor1}`]}\n      ]}\n      const change3 = {actor: actor1, seq: 2, startOp: 5, time: 0, deps: [hash(change1)], ops: [\n        {action: 'set', obj: `2@${actor1}`, key: 'done', value: true, pred: [`4@${actor1}`]}\n      ]}\n      const s0 = Backend.init()\n      const [s1, patch1] = Backend.applyChanges(s0, [change1, change2].map(encodeChange))\n      const [s2, patch2] = Backend.applyChanges(s1, [encodeChange(change3)])\n      assert.deepStrictEqual(patch1, {\n        clock: {[actor1]: 1, [actor2]: 1}, deps: [hash(change2)], maxOp: 5, pendingChanges: 0,\n        diffs: {objectId: '_root', type: 'map', props: {todos: {[`1@${actor1}`]: {\n          objectId: `1@${actor1}`, type: 'list', edits: [\n            {action: 'insert', index: 0, elemId: `2@${actor1}`, opId: `2@${actor1}`, value: {\n              objectId: `2@${actor1}`, type: 'map', props: {\n                title: {[`3@${actor1}`]: {type: 'value', value: 'buy milk'}},\n                done: {[`4@${actor1}`]: {type: 'value', value: false}}\n              }\n            }},\n            {action: 'remove', index: 0, count: 1}\n          ]\n        }}}}\n      })\n      assert.deepStrictEqual(patch2, {\n        clock: {[actor1]: 2, [actor2]: 1}, deps: [hash(change2), hash(change3)].sort(), maxOp: 5, pendingChanges: 0,\n        diffs: {objectId: '_root', type: 'map', props: {}}\n      })\n    })\n\n    it('should handle nested maps in lists', () => {\n      const actor = uuid()\n      const change1 = {actor, seq: 1, startOp: 1, time: 0, deps: [], ops: [\n        {action: 'makeList', obj: '_root', key: 'todos', pred: []},\n        {action: 'set', obj: `1@${actor}`, insert: true, elemId: '_head', pred: [], value: 'first'},\n        {action: 'makeMap', obj: `1@${actor}`, elemId: `2@${actor}`, insert: true, pred: []},\n        {action: 'set', obj: `3@${actor}`, key: 'title', value: 'water plants', pred: []},\n        {action: 'set', obj: `3@${actor}`, key: 'done', value: false, pred: []}\n      ]}\n      const s0 = Backend.init()\n      const [s1, patch1] = Backend.applyChanges(s0, [encodeChange(change1)])\n      assert.deepStrictEqual(patch1, {\n        clock: {[actor]: 1}, deps: [hash(change1)], maxOp: 5, pendingChanges: 0,\n        diffs: {objectId: '_root', type: 'map', props: {todos: {[`1@${actor}`]: {\n          objectId: `1@${actor}`, type: 'list',\n          edits: [\n            {action: 'insert', index: 0, elemId: `2@${actor}`, opId: `2@${actor}`, value: {type: 'value', value: 'first'}},\n            {action: 'insert', index: 1, elemId: `3@${actor}`, opId: `3@${actor}`, value: {\n              type: 'map',\n              objectId: `3@${actor}`,\n              props: {\n                title: {[`4@${actor}`]: {type: 'value', value: 'water plants'}},\n                done:  {[`5@${actor}`]: {type: 'value', value: false}}\n              }\n            }}\n          ]\n        }}}}\n      })\n    })\n\n    it('should support inserting multiple elements in one op (int)', () => {\n      const actor = uuid()\n      const change1 = {actor, seq: 1, startOp: 1, time: 0, deps: [], ops: [\n        {action: 'makeList', obj: '_root', key: 'todos', pred: []},\n        {action: 'set', obj: `1@${actor}`, insert: true, elemId: '_head', pred: [], datatype: 'int', values: [1, 2, 3, 4, 5]},\n      ]}\n      const s0 = Backend.init()\n      const [s1, patch1] = Backend.applyChanges(s0, [encodeChange(change1)])\n      assert.deepStrictEqual(patch1, {\n        clock: {[actor]: 1}, deps: [hash(change1)], maxOp: 6, pendingChanges: 0,\n        diffs: {objectId: '_root', type: 'map', props: {todos: {[`1@${actor}`]: {\n          objectId: `1@${actor}`, type: 'list', edits: [\n            {action: 'multi-insert', index: 0, elemId: `2@${actor}`, datatype: 'int', values: [1, 2, 3, 4, 5]}\n          ]\n        }}}}\n      })\n    })\n\n    it('should support inserting multiple elements in one op (bool)', () => {\n      const actor = uuid()\n      const change1 = {actor, seq: 1, startOp: 1, time: 0, deps: [], ops: [\n        {action: 'makeList', obj: '_root', key: 'todos', pred: []},\n        {action: 'set', obj: `1@${actor}`, insert: true, elemId: '_head', pred: [], values: [true, true, false, true, false]},\n      ]}\n      const s0 = Backend.init()\n      const [s1, patch1] = Backend.applyChanges(s0, [encodeChange(change1)])\n      assert.deepStrictEqual(patch1, {\n        clock: {[actor]: 1}, deps: [hash(change1)], maxOp: 6, pendingChanges: 0,\n        diffs: {objectId: '_root', type: 'map', props: {todos: {[`1@${actor}`]: {\n          objectId: `1@${actor}`, type: 'list', edits: [\n            {action: 'multi-insert', index: 0, elemId: `2@${actor}`, values: [true, true, false, true, false]}\n          ]\n        }}}}\n      })\n    })\n\n    it('should support inserting multiple elements in one op (null)', () => {\n      const actor = uuid()\n      const change1 = {actor, seq: 1, startOp: 1, time: 0, deps: [], ops: [\n        {action: 'makeList', obj: '_root', key: 'todos', pred: []},\n        {action: 'set', obj: `1@${actor}`, insert: true, elemId: '_head', pred: [], values: [null, null, null]},\n      ]}\n      const s0 = Backend.init()\n      const [s1, patch1] = Backend.applyChanges(s0, [encodeChange(change1)])\n      assert.deepStrictEqual(patch1, {\n        clock: {[actor]: 1}, deps: [hash(change1)], maxOp: 4, pendingChanges: 0,\n        diffs: {objectId: '_root', type: 'map', props: {todos: {[`1@${actor}`]: {\n          objectId: `1@${actor}`, type: 'list', edits: [\n            {action: 'multi-insert', index: 0, elemId: `2@${actor}`, values: [null, null, null]}\n          ]\n        }}}}\n      })\n    })\n\n    it('should support inserting multiple elements in one op (uint)', () => {\n      const actor = uuid()\n      const change1 = {actor, seq: 1, startOp: 1, time: 0, deps: [], ops: [\n        {action: 'makeList', obj: '_root', key: 'todos', pred: []},\n        {action: 'set', obj: `1@${actor}`, insert: true, elemId: '_head', pred: [], datatype: 'uint', values: [1, 2, 3, 4, 5]},\n      ]}\n      const s0 = Backend.init()\n      const [s1, patch1] = Backend.applyChanges(s0, [encodeChange(change1)])\n      assert.deepStrictEqual(patch1, {\n        clock: {[actor]: 1}, deps: [hash(change1)], maxOp: 6, pendingChanges: 0,\n        diffs: {objectId: '_root', type: 'map', props: {todos: {[`1@${actor}`]: {\n          objectId: `1@${actor}`, type: 'list', edits: [\n            {action: 'multi-insert', index: 0, elemId: `2@${actor}`, datatype: 'uint', values: [1, 2, 3, 4, 5]}\n          ]\n        }}}}\n      })\n    })\n\n    it('should support inserting multiple elements in one op (float64)', () => {\n      const actor = uuid()\n      const change1 = {actor, seq: 1, startOp: 1, time: 0, deps: [], ops: [\n        {action: 'makeList', obj: '_root', key: 'todos', pred: []},\n        {action: 'set', obj: `1@${actor}`, insert: true, elemId: '_head', pred: [], datatype: 'float64', values: [1, 2, 3, 4, 5]},\n      ]}\n      const s0 = Backend.init()\n      const [s1, patch1] = Backend.applyChanges(s0, [encodeChange(change1)])\n      assert.deepStrictEqual(patch1, {\n        clock: {[actor]: 1}, deps: [hash(change1)], maxOp: 6, pendingChanges: 0,\n        diffs: {objectId: '_root', type: 'map', props: {todos: {[`1@${actor}`]: {\n          objectId: `1@${actor}`, type: 'list', edits: [\n            {action: 'multi-insert', index: 0, elemId: `2@${actor}`, datatype: 'float64', values: [1, 2, 3, 4, 5]}\n          ]\n        }}}}\n      })\n    })\n\n    it('should support inserting multiple elements in one op (timestamp)', () => {\n      const actor = uuid()\n      const change1 = {actor, seq: 1, startOp: 1, time: 0, deps: [], ops: [\n        {action: 'makeList', obj: '_root', key: 'todos', pred: []},\n        {action: 'set', obj: `1@${actor}`, insert: true, elemId: '_head', pred: [], datatype: 'timestamp', values: [1, 2, 3, 4, 5]},\n      ]}\n      const s0 = Backend.init()\n      const [s1, patch1] = Backend.applyChanges(s0, [encodeChange(change1)])\n      assert.deepStrictEqual(patch1, {\n        clock: {[actor]: 1}, deps: [hash(change1)], maxOp: 6, pendingChanges: 0,\n        diffs: {objectId: '_root', type: 'map', props: {todos: {[`1@${actor}`]: {\n          objectId: `1@${actor}`, type: 'list', edits: [\n            {action: 'multi-insert', index: 0, elemId: `2@${actor}`, datatype: 'timestamp', values: [1, 2, 3, 4, 5]}\n          ]\n        }}}}\n      })\n    })\n\n    it('should support inserting multiple elements in one op (counter)', () => {\n      const actor = uuid()\n      const change1 = {actor, seq: 1, startOp: 1, time: 0, deps: [], ops: [\n        {action: 'makeList', obj: '_root', key: 'todos', pred: []},\n        {action: 'set', obj: `1@${actor}`, insert: true, elemId: '_head', pred: [], datatype: 'counter', values: [1, 2, 3, 4, 5]},\n      ]}\n      const s0 = Backend.init()\n      const [s1, patch1] = Backend.applyChanges(s0, [encodeChange(change1)])\n      assert.deepStrictEqual(patch1, {\n        clock: {[actor]: 1}, deps: [hash(change1)], maxOp: 6, pendingChanges: 0,\n        diffs: {objectId: '_root', type: 'map', props: {todos: {[`1@${actor}`]: {\n          objectId: `1@${actor}`, type: 'list', edits: [\n            {action: 'multi-insert', index: 0, elemId: `2@${actor}`, datatype: 'counter', values: [1, 2, 3, 4, 5]}\n          ]\n        }}}}\n      })\n    })\n\n    it('should throw an error if the datatype does not match the values', () => {\n      const actor = uuid()\n      const change1 = {actor, seq: 1, startOp: 1, time: 0, deps: [], ops: [\n        {action: 'makeList', obj: '_root', key: 'todos', pred: []},\n        {action: 'set', obj: `1@${actor}`, insert: true, elemId: '_head', pred: [], datatype: 'int', values: [1, true, 'hello']},\n      ]}\n      const s0 = Backend.init()\n      assert.throws(() => { Backend.applyLocalChange(s0, change1) }, /Decode failed/)\n    })\n\n    it('should support deleting multiple elements in one op', () => {\n      const actor = uuid()\n      const change1 = {actor, seq: 1, startOp: 1, time: 0, deps: [], ops: [\n        {action: 'makeList', obj: '_root', key: 'todos', pred: []},\n        {action: 'set', obj: `1@${actor}`, insert: true, elemId: '_head', pred: [], datatype: 'int', values: [1, 2, 3, 4, 5]},\n      ]}\n      const change2 = {actor, seq: 2, startOp: 7, time: 0, deps: [hash(change1)], ops: [\n        {action: 'del', obj: `1@${actor}`, elemId: `3@${actor}`, multiOp: 3, pred: [`3@${actor}`]}\n      ]}\n      const s0 = Backend.init()\n      const [s1, patch1] = Backend.applyChanges(s0, [encodeChange(change1)])\n      const [s2, patch2] = Backend.applyChanges(s1, [encodeChange(change2)])\n      assert.deepStrictEqual(patch2, {\n        clock: {[actor]: 2}, deps: [hash(change2)], maxOp: 9, pendingChanges: 0,\n        diffs: {objectId: '_root', type: 'map', props: {todos: {[`1@${actor}`]: {\n          objectId: `1@${actor}`, type: 'list', edits: [\n            {action: 'remove', index: 1, count: 3}\n          ]\n        }}}}\n      })\n    })\n  })\n\n  describe('applyLocalChange()', () => {\n    it('should apply change requests', () => {\n      const change1 = {actor: '111111', seq: 1, time: 0, startOp: 1, deps: [], ops: [\n        {action: 'set', obj: '_root', key: 'bird', value: 'magpie', pred: []}\n      ]}\n      const s0 = Backend.init()\n      const [s1, patch1] = Backend.applyLocalChange(s0, change1)\n      const changes01 = Backend.getAllChanges(s1).map(decodeChange)\n      assert.deepStrictEqual(patch1, {\n        actor: '111111', seq: 1, clock: {'111111': 1}, deps: [], maxOp: 1, pendingChanges: 0,\n        diffs: {objectId: '_root', type: 'map', props: {\n          bird: {'1@111111': {type: 'value', value: 'magpie'}}\n        }}\n      })\n      assert.deepStrictEqual(changes01, [{\n        hash: '2c2845859ce4336936f56410f9161a09ba269f48aee5826782f1c389ec01d054',\n        actor: '111111', seq: 1, startOp: 1, time: 0, message: '', deps: [], ops: [\n          {action: 'set', obj: '_root', key: 'bird', insert: false, value: 'magpie', pred: []}\n        ]\n      }])\n    })\n\n    it('should throw an exception on duplicate requests', () => {\n      const actor = uuid()\n      const change1 = {actor, seq: 1, time: 0, startOp: 1, deps: [], ops: [\n        {action: 'set', obj: '_root', key: 'bird', value: 'magpie', pred: []}\n      ]}\n      const change2 = {actor, seq: 2, time: 0, startOp: 2, deps: [], ops: [\n        {action: 'set', obj: '_root', key: 'bird', value: 'jay', pred: []}\n      ]}\n      const s0 = Backend.init()\n      const [s1, patch1] = Backend.applyLocalChange(s0, change1)\n      const [s2, patch2] = Backend.applyLocalChange(s1, change2)\n      assert.throws(() => Backend.applyLocalChange(s2, change1), /Change request has already been applied/)\n      assert.throws(() => Backend.applyLocalChange(s2, change2), /Change request has already been applied/)\n    })\n\n    it('should handle frontend and backend changes happening concurrently', () => {\n      const local1 = {actor: '111111', seq: 1, time: 0, startOp: 1, deps: [], ops: [\n        {action: 'set', obj: '_root', key: 'bird', value: 'magpie', pred: []}\n      ]}\n      const local2 = {actor: '111111', seq: 2, time: 0, startOp: 2, deps: [], ops: [\n        {action: 'set', obj: '_root', key: 'bird', value: 'jay', pred: ['1@111111']}\n      ]}\n      const remote1 = {actor: '222222', seq: 1, startOp: 1, time: 0, deps: [], ops: [\n        {action: 'set', obj: '_root', key: 'fish', value: 'goldfish', pred: []}\n      ]}\n      const s0 = Backend.init()\n      const [s1, patch1] = Backend.applyLocalChange(s0, local1)\n      const [s2, patch2] = Backend.applyChanges(s1, [encodeChange(remote1)])\n      const [s3, patch3] = Backend.applyLocalChange(s2, local2)\n      const changes = Backend.getAllChanges(s3).map(decodeChange)\n      assert.deepStrictEqual(changes, [\n        {hash: '2c2845859ce4336936f56410f9161a09ba269f48aee5826782f1c389ec01d054',\n        actor: '111111', seq: 1, startOp: 1, time: 0, message: '', deps: [], ops: [\n          {action: 'set', obj: '_root', key: 'bird', insert: false, value: 'magpie', pred: []}\n        ]},\n        {hash: 'efc7e9b1b809364fb1b7029d2838dd3c7cf539eea595b22f9ae665505187f6c4',\n        actor: '222222', seq: 1, startOp: 1, time: 0, message: '', deps: [], ops: [\n          {action: 'set', obj: '_root', key: 'fish', insert: false, value: 'goldfish', pred: []}\n        ]},\n        {hash: 'e7ed7a790432aba39fe7ad75fa9e02a9fc8d8e9ee4ec8c81dcc93da15a561f8a',\n        actor: '111111', seq: 2, startOp: 2, time: 0, message: '', deps: [changes[0].hash], ops: [\n          {action: 'set', obj: '_root', key: 'bird', insert: false, value: 'jay', pred: ['1@111111']}\n        ]}\n      ])\n    })\n\n    it('should detect conflicts based on the frontend version', () => {\n      const local1 = {requestType: 'change', actor: '111111', seq: 1, time: 0, startOp: 1, deps: [], ops: [\n        {action: 'set', obj: '_root', key: 'bird', value: 'goldfinch', pred: []}\n      ]}\n      // remote1 depends on local1; the deps field is filled in below when we've computed the hash\n      const remote1 = {actor: '222222', seq: 1, startOp: 2, time: 0, deps: [], ops: [\n        {action: 'set', obj: '_root', key: 'bird', value: 'magpie', pred: ['1@111111']}\n      ]}\n      // local2 is concurrent with remote1 (because version < 2)\n      const local2 = {requestType: 'change', actor: '111111', seq: 2, time: 0, startOp: 2, deps: [], ops: [\n        {action: 'set', obj: '_root', key: 'bird', value: 'jay', pred: ['1@111111']}\n      ]}\n      const s0 = Backend.init()\n      const [s1, patch1] = Backend.applyLocalChange(s0, local1)\n      remote1.deps.push(Backend.getAllChanges(s1).map(decodeChange)[0].hash)\n      const [s2, patch2] = Backend.applyChanges(s1, [encodeChange(remote1)])\n      const [s3, patch3] = Backend.applyLocalChange(s2, local2)\n      const changes = Backend.getAllChanges(s3).map(decodeChange)\n      assert.deepStrictEqual(patch3, {\n        actor: '111111', seq: 2, clock: {'111111': 2, '222222': 1},\n        deps: [hash(remote1)], maxOp: 2, pendingChanges: 0,\n        diffs: {objectId: '_root', type: 'map', props: {\n          bird: {'2@222222': {type: 'value', value: 'magpie'}, '2@111111': {type: 'value', value: 'jay'}}\n        }}\n      })\n      assert.deepStrictEqual(changes[2], {\n        hash: '7a00e28d7fbf179708a1b0045c7f9bad93366c0e69f9af15e830dae9970a9d19',\n        actor: '111111', seq: 2, startOp: 2, time: 0, message: '', deps: [changes[0].hash], ops: [\n          {action: 'set', obj: '_root', key: 'bird', insert: false, value: 'jay', pred: ['1@111111']}\n        ]\n      })\n    })\n\n    it('should transform list indexes into element IDs', () => {\n      const remote1 = {actor: '222222', seq: 1, startOp: 1, time: 0, deps: [], ops: [\n        {obj: '_root', action: 'makeList', key: 'birds', pred: []}\n      ]}\n      const remote2 = {actor: '222222', seq: 2, startOp: 2, time: 0, deps: [hash(remote1)], ops: [\n        {obj: '1@222222', action: 'set', elemId: '_head', insert: true, value: 'magpie', pred: []}\n      ]}\n      const local1 = {actor: '111111', seq: 1, startOp: 2, time: 0, deps: [hash(remote1)], ops: [\n        {obj: '1@222222', action: 'set', elemId: '_head', insert: true, value: 'goldfinch', pred: []}\n      ]}\n      const local2 = {actor: '111111', seq: 2, startOp: 3, time: 0, deps: [], ops: [\n        {obj: '1@222222', action: 'set', elemId: '2@111111', insert: true, value: 'wagtail', pred: []}\n      ]}\n      const local3 = {actor: '111111', seq: 3, startOp: 4, time: 0, deps: [hash(remote2)], ops: [\n        {obj: '1@222222', action: 'set', elemId: '2@222222', value: 'Magpie', pred: ['2@222222']},\n        {obj: '1@222222', action: 'set', elemId: '2@111111', value: 'Goldfinch', pred: ['2@111111']}\n      ]}\n      const s0 = Backend.init()\n      const [s1, patch1] = Backend.applyChanges(s0, [encodeChange(remote1)])\n      const [s2, patch2] = Backend.applyLocalChange(s1, local1)\n      const [s3, patch3] = Backend.applyChanges(s2, [encodeChange(remote2)])\n      const [s4, patch4] = Backend.applyLocalChange(s3, local2)\n      const [s5, patch5] = Backend.applyLocalChange(s4, local3)\n      const changes = Backend.getAllChanges(s5).map(decodeChange)\n      assert.deepStrictEqual(changes[1], {\n        hash: '06392148c4a0dfff8b346ad58a3261cc15187cbf8a58779f78d54251126d4ccc',\n        actor: '111111', seq: 1, startOp: 2, time: 0, message: '', deps: [hash(remote1)], ops: [\n          {obj: '1@222222', action: 'set', elemId: '_head', insert: true, value: 'goldfinch', pred: []}\n        ]\n      })\n      assert.deepStrictEqual(changes[3], {\n        hash: '2801c386ec2a140376f3bef285a6e6d294a2d8fb7a180da4fbb6e2bc4f550dd9',\n        actor: '111111', seq: 2, startOp: 3, time: 0, message: '', deps: [changes[1].hash], ops: [\n          {obj: '1@222222', action: 'set', elemId: '2@111111', insert: true, value: 'wagtail', pred: []}\n        ]\n      })\n      assert.deepStrictEqual(changes[4], {\n        hash: '734f1dad5fb2f10970bae2baa6ce100c3b85b43072b3799d8f2e15bcd21297fc',\n        actor: '111111', seq: 3, startOp: 4, time: 0, message: '',\n        deps: [hash(remote2), changes[3].hash].sort(), ops: [\n          {obj: '1@222222', action: 'set', elemId: '2@222222', insert: false, value: 'Magpie',    pred: ['2@222222']},\n          {obj: '1@222222', action: 'set', elemId: '2@111111', insert: false, value: 'Goldfinch', pred: ['2@111111']}\n        ]\n      })\n    })\n\n    it('should handle list element insertion and deletion in the same change', () => {\n      const local1 = {requestType: 'change', actor: '111111', seq: 1, startOp: 1, deps: [], time: 0, ops: [\n        {obj: '_root', action: 'makeList', key: 'birds', pred: []}\n      ]}\n      const local2 = {requestType: 'change', actor: '111111', seq: 2, startOp: 2, deps: [], time: 0, ops: [\n        {obj: '1@111111', action: 'set', elemId: '_head', insert: true, value: 'magpie', pred: []},\n        {obj: '1@111111', action: 'del', elemId: '2@111111', pred: ['2@111111']}\n      ]}\n      const s0 = Backend.init()\n      const [s1, patch1] = Backend.applyLocalChange(s0, local1)\n      const [s2, patch2] = Backend.applyLocalChange(s1, local2)\n      const changes = Backend.getAllChanges(s2).map(decodeChange)\n      assert.deepStrictEqual(patch2, {\n        actor: '111111', seq: 2, clock: {'111111': 2}, deps: [], maxOp: 3, pendingChanges: 0,\n        diffs: {objectId: '_root', type: 'map', props: {\n          birds: {'1@111111': {objectId: '1@111111', type: 'list',\n            edits: [\n              {action: 'insert', index: 0, elemId: '2@111111', opId: '2@111111', value: {type: 'value', value: 'magpie'}},\n              {action: 'remove', index: 0, count: 1}\n            ]}}\n        }}\n      })\n      assert.deepStrictEqual(changes, [{\n        hash: changes[0].hash, actor: '111111', seq: 1, startOp: 1, time: 0, message: '', deps: [], ops: [\n          {obj: '_root', action: 'makeList', key: 'birds', insert: false, pred: []}\n        ]\n      }, {\n        hash: 'deef4c9b9ca378844144c4bbc5d82a52f30c95a8624f13f243fe8f1214e8e833',\n        actor: '111111', seq: 2, startOp: 2, time: 0, message: '', deps: [changes[0].hash], ops: [\n          {obj: '1@111111', action: 'set', elemId: '_head', insert: true, value: 'magpie', pred: []},\n          {obj: '1@111111', action: 'del', elemId: '2@111111', insert: false, pred: ['2@111111']}\n        ]\n      }])\n    })\n\n    it('should compress changes with DEFLATE', () => {\n      let longString = ''\n      for (let i = 0; i < 1024; i++) longString += 'a'\n      const change1 = {actor: '111111', seq: 1, time: 0, startOp: 1, deps: [], ops: [\n        {action: 'set', obj: '_root', key: 'longString', value: longString, pred: []}\n      ]}\n      const [s1, patch1] = Backend.applyLocalChange(Backend.init(), change1)\n      const changes = Backend.getAllChanges(s1)\n      const [s2, patch2] = Backend.applyChanges(Backend.init(), changes)\n      assert.ok(changes[0].byteLength < 100)\n      assert.deepStrictEqual(patch2, {\n        clock: {'111111': 1}, deps: [hash(change1)], maxOp: 1, pendingChanges: 0,\n        diffs: {objectId: '_root', type: 'map', props: {\n          longString: {'1@111111': {type: 'value', value: longString}}\n        }}\n      })\n    })\n\n    it('should support inserting multiple elements in one change (int)', () => {\n      const actor = uuid()\n      const localChange = {actor, seq: 1, startOp: 1, time: 0, deps: [], ops: [\n        {action: 'makeList', obj: '_root', key: 'todos', pred: []},\n        {action: 'set', obj: `1@${actor}`, insert: true, elemId: '_head', pred: [], datatype: 'int', values: [1, 2, 3, 4, 5]},\n      ]}\n      const s0 = Backend.init()\n      const [s1, patch1] = Backend.applyLocalChange(s0, localChange)\n      const changes = Backend.getChanges(s1, []).map(decodeChange)\n      assert.deepStrictEqual(patch1, {\n        clock: {[actor]: 1}, deps: [], maxOp: 6, actor, seq: 1, pendingChanges: 0,\n        diffs: {objectId: '_root', type: 'map', props: {todos: {[`1@${actor}`]: {\n          objectId: `1@${actor}`, type: 'list', edits: [\n            {action: 'multi-insert', index: 0, elemId: `2@${actor}`, datatype: 'int', values: [1, 2, 3, 4, 5]}\n          ]\n        }}}}\n      })\n    })\n\n    it('should support inserting multiple elements in one change (float64)', () => {\n      const actor = uuid()\n      const localChange = {actor, seq: 1, startOp: 1, time: 0, deps: [], ops: [\n        {action: 'makeList', obj: '_root', key: 'todos', pred: []},\n        {action: 'set', obj: `1@${actor}`, insert: true, elemId: '_head', pred: [], datatype: 'float64', values: [1, 2, 3.3, 4, 5]},\n      ]}\n      const s0 = Backend.init()\n      const [s1, patch1] = Backend.applyLocalChange(s0, localChange)\n      const changes = Backend.getChanges(s1, []).map(decodeChange)\n      assert.deepStrictEqual(patch1, {\n        clock: {[actor]: 1}, deps: [], maxOp: 6, actor, seq: 1, pendingChanges: 0,\n        diffs: {objectId: '_root', type: 'map', props: {todos: {[`1@${actor}`]: {\n          objectId: `1@${actor}`, type: 'list', edits: [\n            {action: 'multi-insert', index: 0, elemId: `2@${actor}`, datatype: 'float64', values: [1, 2, 3.3, 4, 5]}\n          ]\n        }}}}\n      })\n    })\n\n    it('should support deleting multiple elements in one op', () => {\n      const actor = uuid()\n      const change1 = {actor, seq: 1, startOp: 1, time: 0, deps: [], ops: [\n        {action: 'makeList', obj: '_root', key: 'todos', pred: []},\n        {action: 'set', obj: `1@${actor}`, insert: true, elemId: '_head', pred: [], datatype: 'int', values: [1, 2, 3, 4, 5]}\n      ]}\n      const change2 = {actor, seq: 2, startOp: 7, time: 0, deps: [hash(change1)], ops: [\n        {action: 'del', obj: `1@${actor}`, elemId: `3@${actor}`, multiOp: 3, pred: [`3@${actor}`]}\n      ]}\n      const s0 = Backend.init()\n      const [s1, patch1] = Backend.applyLocalChange(s0, change1)\n      const [s2, patch2] = Backend.applyLocalChange(s1, change2)\n      assert.deepStrictEqual(patch2, {\n        clock: {[actor]: 2}, deps: [], maxOp: 9, actor, seq: 2, pendingChanges: 0,\n        diffs: {objectId: '_root', type: 'map', props: {todos: {[`1@${actor}`]: {\n          objectId: `1@${actor}`, type: 'list', edits: [\n            {action: 'remove', index: 1, count: 3}\n          ]\n        }}}}\n      })\n    })\n\n    it('should allow a conflict to be resolved', () => {\n      const change1 = {actor: '111111', seq: 1, startOp: 1, time: 0, deps: [], ops: [\n        {action: 'set', obj: '_root', key: 'bird', value: 'magpie', pred: []}\n      ]}\n      const change2 = {actor: '222222', seq: 1, startOp: 1, time: 0, deps: [], ops: [\n        {action: 'set', obj: '_root', key: 'bird', value: 'blackbird', pred: []}\n      ]}\n      const change3 = {actor: '333333', seq: 1, startOp: 2, time: 0, deps: [hash(change1), hash(change2)], ops: [\n        {action: 'set', obj: '_root', key: 'bird', value: 'robin', pred: ['1@111111', '1@222222']}\n      ]}\n      const s0 = Backend.init()\n      const [s1, patch1] = Backend.applyChanges(s0, [encodeChange(change1), encodeChange(change2)])\n      const [s2, patch2] = Backend.applyLocalChange(s1, change3)\n      assert.deepStrictEqual(patch2, {\n        clock: {111111: 1, 222222: 1, 333333: 1}, deps: [], actor: '333333', seq: 1, maxOp: 2, pendingChanges: 0,\n        diffs: {objectId: '_root', type: 'map', props: {\n          bird: {'2@333333': {type: 'value', value: 'robin'}}\n        }}\n      })\n\n      // Check that we can change the order of `pred` without affecting the outcome\n      change3.ops[0].pred.reverse()\n      const s3 = Backend.init()\n      const [s4, patch4] = Backend.applyChanges(s3, [encodeChange(change1), encodeChange(change2)])\n      const [s5, patch5] = Backend.applyLocalChange(s4, change3)\n      assert.deepStrictEqual(Backend.getHeads(s2), Backend.getHeads(s5))\n    })\n  })\n\n  describe('save() and load()', () => {\n    it('should reconstruct changes that resolve conflicts', () => {\n      const actor1 = '8765', actor2 = '1234'\n      const change1 = {actor: actor1, seq: 1, startOp: 1, time: 0, deps: [], ops: [\n        {action: 'set', obj: '_root', key: 'bird', value: 'magpie', pred: []}\n      ]}\n      const change2 = {actor: actor2, seq: 1, startOp: 1, time: 0, deps: [], ops: [\n        {action: 'set', obj: '_root', key: 'bird', value: 'blackbird', pred: []}\n      ]}\n      const change3 = {actor: actor1, seq: 2, startOp: 2, time: 0, deps: [hash(change1), hash(change2)], ops: [\n        {action: 'set', obj: '_root', key: 'bird', value: 'robin', pred: [`1@${actor1}`, `1@${actor2}`]}\n      ]}\n      const s1 = Backend.loadChanges(Backend.init(), [change1, change2, change3].map(encodeChange))\n      const s2 = Backend.load(Backend.save(s1))\n      assert.deepStrictEqual(Backend.getHeads(s2), [hash(change3)])\n    })\n\n    it('should compress columns with DEFLATE', () => {\n      let longString = ''\n      for (let i = 0; i < 1024; i++) longString += 'a'\n      const change1 = {actor: '111111', seq: 1, time: 0, startOp: 1, deps: [], ops: [\n        {action: 'set', obj: '_root', key: 'longString', value: longString, pred: []}\n      ]}\n      const doc = Backend.save(Backend.loadChanges(Backend.init(), [encodeChange(change1)]))\n      const patch = Backend.getPatch(Backend.load(doc))\n      assert.ok(doc.byteLength < 200)\n      assert.deepStrictEqual(patch, {\n        clock: {'111111': 1}, deps: [hash(change1)], maxOp: 1, pendingChanges: 0,\n        diffs: {objectId: '_root', type: 'map', props: {\n          longString: {'1@111111': {type: 'value', value: longString}}\n        }}\n      })\n    })\n\n    it('should load floats correctly', () => {\n        // This was generated from saving a document in the Rust backend\n        // Rust code:\n        // ```\n        // let initial_state_json: serde_json::Value = serde_json::from_str(r#\"{ \"birds\": 3.0 }\"#).unwrap();\n        // let value = Value::from_json(&initial_state_json);\n        // let (mut frontend, change) = Frontend::new_with_initial_state(value).unwrap();\n        // let mut backend = Backend::init();\n        // backend.apply_local_change(change).unwrap();\n        // let bytes = backend.save().unwrap();\n        // ```\n        const bytes = Uint8Array.from([133, 111, 74, 131, 233, 181, 157, 86, 0, 144, 1, 1, 16, 228, 91, 238, 197, 233, 52, 66, 187, 138, 75, 115, 104, 190, 195, 159, 200, 1, 221, 158, 172, 238, 121, 38, 160, 123, 25, 33, 97, 124, 142, 27, 86, 224, 238, 83, 14, 157, 207, 233, 8, 110, 91, 151, 172, 38, 120, 221, 38, 162, 7, 1, 2, 3, 2, 19, 2, 35, 7, 53, 16, 64, 2, 86, 2, 8, 21, 7, 33, 2, 35, 2, 52, 1, 66, 2, 86, 3, 87, 8, 128, 1, 2, 127, 0, 127, 1, 127, 1, 127, 243, 145, 234, 194, 149, 47, 127, 14, 73, 110, 105, 116, 105, 97, 108, 105, 122, 97, 116, 105, 111, 110, 127, 0, 127, 7, 127, 5, 98, 105, 114, 100, 115, 127, 0, 127, 1, 1, 127, 1, 127, 133, 1, 0, 0, 0, 0, 0, 0, 8, 64, 127, 0])\n        const doc = Automerge.load(bytes)\n        assert.deepStrictEqual(doc, { birds: 3.0 })\n    });\n  })\n\n  describe('getPatch()', () => {\n    it('should include the most recent value for a key', () => {\n      const actor = uuid()\n      const change1 = {actor, seq: 1, startOp: 1, time: 0, deps: [], ops: [\n        {action: 'set', obj: '_root', key: 'bird', value: 'magpie', pred: []}\n      ]}\n      const change2 = {actor, seq: 2, startOp: 2, time: 0, deps: [hash(change1)], ops: [\n        {action: 'set', obj: '_root', key: 'bird', value: 'blackbird', pred: [`1@${actor}`]}\n      ]}\n      const s1 = Backend.loadChanges(Backend.init(), [change1, change2].map(encodeChange))\n      assert.deepStrictEqual(Backend.getPatch(s1), {\n        clock: {[actor]: 2}, deps: [hash(change2)], maxOp: 2, pendingChanges: 0,\n        diffs: {objectId: '_root', type: 'map', props: {\n          bird: {[`2@${actor}`]: {type: 'value', value: 'blackbird'}}\n        }}\n      })\n    })\n\n    it('should include conflicting values for a key', () => {\n      const change1 = {actor: '111111', seq: 1, startOp: 1, time: 0, deps: [], ops: [\n        {action: 'set', obj: '_root', key: 'bird', value: 'magpie', pred: []}\n      ]}\n      const change2 = {actor: '222222', seq: 1, startOp: 1, time: 0, deps: [], ops: [\n        {action: 'set', obj: '_root', key: 'bird', value: 'blackbird', pred: []}\n      ]}\n      const s1 = Backend.loadChanges(Backend.init(), [change1, change2].map(encodeChange))\n      assert.deepStrictEqual(Backend.getPatch(s1), {\n        clock: {111111: 1, 222222: 1},\n        deps: [hash(change1), hash(change2)].sort(), maxOp: 1, pendingChanges: 0,\n        diffs: {objectId: '_root', type: 'map', props: {\n          bird: {'1@111111': {type: 'value', value: 'magpie'}, '1@222222': {type: 'value', value: 'blackbird'}}\n        }}\n      })\n    })\n\n    it('should handle counter increments at a key in a map', () => {\n      const actor = uuid()\n      const change1 = {actor, seq: 1, startOp: 1, time: 0, deps: [], ops: [\n        {action: 'set', obj: '_root', key: 'counter', value: 1, datatype: 'counter', pred: []}\n      ]}\n      const change2 = {actor, seq: 2, startOp: 2, time: 0, deps: [hash(change1)], ops: [\n        {action: 'inc', obj: '_root', key: 'counter', value: 2, pred: [`1@${actor}`]}\n      ]}\n      const s1 = Backend.loadChanges(Backend.init(), [change1, change2].map(encodeChange))\n      assert.deepStrictEqual(Backend.getPatch(s1), {\n        clock: {[actor]: 2}, deps: [hash(change2)], maxOp: 2, pendingChanges: 0,\n        diffs: {objectId: '_root', type: 'map', props: {\n          counter: {[`1@${actor}`]: {type: 'value', value: 3, datatype: 'counter'}}\n        }}\n      })\n    })\n\n    it('should handle deletion of a counter', () => {\n      const actor = uuid()\n      const change1 = {actor, seq: 1, startOp: 1, time: 0, deps: [], ops: [\n        {action: 'set', obj: '_root', key: 'counter', value: 1, datatype: 'counter', pred: []}\n      ]}\n      const change2 = {actor, seq: 2, startOp: 2, time: 0, deps: [hash(change1)], ops: [\n        {action: 'inc', obj: '_root', key: 'counter', value: 2, pred: [`1@${actor}`]}\n      ]}\n      const change3 = {actor, seq: 3, startOp: 3, time: 0, deps: [hash(change2)], ops: [\n        {action: 'del', obj: '_root', key: 'counter', pred: [`1@${actor}`]}\n      ]}\n      const s1 = Backend.loadChanges(Backend.init(), [change1, change2, change3].map(encodeChange))\n      assert.deepStrictEqual(Backend.getPatch(s1), {\n        clock: {[actor]: 3}, deps: [hash(change3)], maxOp: 3, pendingChanges: 0,\n        diffs: {objectId: '_root', type: 'map', props: {}}\n      })\n    })\n\n    it('should create nested maps', () => {\n      const actor = uuid()\n      const change1 = {actor, seq: 1, startOp: 1, time: 0, deps: [], ops: [\n        {action: 'makeMap', obj: '_root', key: 'birds', pred: []},\n        {action: 'set', obj: `1@${actor}`, key: 'wrens', value: 3,     pred: []}\n      ]}\n      const change2 = {actor, seq: 2, startOp: 3, time: 0, deps: [hash(change1)], ops: [\n        {action: 'del', obj: `1@${actor}`, key: 'wrens', pred: [`2@${actor}`]},\n        {action: 'set', obj: `1@${actor}`, key: 'sparrows', value: 15, pred: []}\n      ]}\n      const s1 = Backend.loadChanges(Backend.init(), [change1, change2].map(encodeChange))\n      assert.deepStrictEqual(Backend.getPatch(s1), {\n        clock: {[actor]: 2}, deps: [hash(change2)], maxOp: 4, pendingChanges: 0,\n        diffs: {objectId: '_root', type: 'map', props: {birds: {[`1@${actor}`]: {\n          objectId: `1@${actor}`, type: 'map', props: {sparrows: {[`4@${actor}`]: {type: 'value', value: 15, datatype: 'int'}}}\n        }}}}\n      })\n    })\n\n    it('should create lists', () => {\n      const actor = uuid()\n      const change1 = {actor, seq: 1, startOp: 1, time: 0, deps: [], ops: [\n        {action: 'makeList', obj: '_root', key: 'birds', pred: []},\n        {action: 'set', obj: `1@${actor}`, elemId: '_head', insert: true, value: 'chaffinch', pred: []}\n      ]}\n      const s1 = Backend.loadChanges(Backend.init(), [encodeChange(change1)])\n      assert.deepStrictEqual(Backend.getPatch(s1), {\n        clock: {[actor]: 1}, deps: [hash(change1)], maxOp: 2, pendingChanges: 0,\n        diffs: {objectId: '_root', type: 'map', props: {birds: {[`1@${actor}`]: {\n          objectId: `1@${actor}`, type: 'list', edits: [\n            {action: 'insert', index: 0, elemId: `2@${actor}`, opId: `2@${actor}`, value: {type: 'value', value: 'chaffinch'}}\n          ]\n        }}}}\n      })\n    })\n\n    it('should include the latest state of a list', () => {\n      const actor = uuid()\n      const change1 = {actor, seq: 1, startOp: 1, time: 0, deps: [], ops: [\n        {action: 'makeList', obj: '_root', key: 'birds', pred: []},\n        {action: 'set', obj: `1@${actor}`, elemId: '_head',      insert: true, value: 'chaffinch', pred: []},\n        {action: 'set', obj: `1@${actor}`, elemId: `2@${actor}`, insert: true, value: 'goldfinch', pred: []}\n      ]}\n      const change2 = {actor, seq: 2, startOp: 4, time: 0, deps: [hash(change1)], ops: [\n        {action: 'del', obj: `1@${actor}`, elemId: `2@${actor}`, pred: [`2@${actor}`]},\n        {action: 'set', obj: `1@${actor}`, elemId: `2@${actor}`, insert: true, value: 'greenfinch', pred: []},\n        {action: 'set', obj: `1@${actor}`, elemId: `3@${actor}`, value: 'goldfinches!!', pred: [`3@${actor}`]}\n      ]}\n      const s1 = Backend.loadChanges(Backend.init(), [change1, change2].map(encodeChange))\n      assert.deepStrictEqual(Backend.getPatch(s1), {\n        clock: {[actor]: 2}, deps: [hash(change2)], maxOp: 6, pendingChanges: 0,\n        diffs: {objectId: '_root', type: 'map', props: {birds: {[`1@${actor}`]: {\n          objectId: `1@${actor}`, type: 'list',\n          edits: [\n            {action: 'insert', index: 0, elemId: `5@${actor}`, opId: `5@${actor}`, value: {type: 'value', value: 'greenfinch'}},\n            {action: 'insert', index: 1, elemId: `3@${actor}`, opId: `6@${actor}`, value: {type: 'value', value: 'goldfinches!!'}}\n          ]\n        }}}}\n      })\n    })\n\n    it('should handle conflicts on list elements', () => {\n      const actor1 = '01234567', actor2 = '89abcdef'\n      const change1 = {actor: actor1, seq: 1, startOp: 1, time: 0, deps: [], ops: [\n        {action: 'makeList', obj: '_root', key: 'birds', pred: []},\n        {action: 'set', obj: `1@${actor1}`, elemId: '_head',      insert: true, value: 'chaffinch', pred: []},\n        {action: 'set', obj: `1@${actor1}`, elemId: `2@${actor1}`, insert: true, value: 'magpie', pred: []}\n      ]}\n      const change2 = {actor: actor1, seq: 2, startOp: 4, time: 0, deps: [hash(change1)], ops: [\n        {action: 'set', obj: `1@${actor1}`, elemId: `2@${actor1}`, value: 'greenfinch', pred: [`2@${actor1}`]}\n      ]}\n      const change3 = {actor: actor2, seq: 1, startOp: 4, time: 0, deps: [hash(change1)], ops: [\n        {action: 'set', obj: `1@${actor1}`, elemId: `2@${actor1}`, value: 'goldfinch', pred: [`2@${actor1}`]}\n      ]}\n      const s1 = Backend.loadChanges(Backend.init(), [change1, change2, change3].map(encodeChange))\n      assert.deepStrictEqual(Backend.getPatch(s1), {\n        clock: {[actor1]: 2, [actor2]: 1}, deps: [hash(change2), hash(change3)].sort(), maxOp: 4, pendingChanges: 0,\n        diffs: {objectId: '_root', type: 'map', props: {birds: {[`1@${actor1}`]: {\n          objectId: `1@${actor1}`, type: 'list',\n          edits: [\n            {action: 'insert', index: 0, elemId: `2@${actor1}`, opId: `4@${actor1}`, value: {type: 'value', value: 'greenfinch'}},\n            {action: 'update', index: 0, opId: `4@${actor2}`, value: {type: 'value', value: 'goldfinch'}},\n            {action: 'insert', index: 1, elemId: `3@${actor1}`, opId: `3@${actor1}`, value: {type: 'value', value: 'magpie'}}\n          ]\n        }}}}\n      })\n    })\n\n    it('should handle nested maps in lists', () => {\n      const actor = uuid()\n      const change = {actor, seq: 1, startOp: 1, time: 0, deps: [], ops: [\n        {action: 'makeList', obj: '_root', key: 'todos', pred: []},\n        {action: 'makeMap', obj: `1@${actor}`, elemId: '_head', insert: true, pred: []},\n        {action: 'set', obj: `2@${actor}`, key: 'title', value: 'water plants', pred: []},\n        {action: 'set', obj: `2@${actor}`, key: 'done', value: false, pred: []}\n      ]}\n      const s1 = Backend.loadChanges(Backend.init(), [encodeChange(change)])\n      assert.deepStrictEqual(Backend.getPatch(s1), {\n        clock: {[actor]: 1}, deps: [hash(change)], maxOp: 4, pendingChanges: 0,\n        diffs: {objectId: '_root', type: 'map', props: {todos: {[`1@${actor}`]: {\n          objectId: `1@${actor}`, type: 'list',\n          edits: [\n            {action: 'insert', index: 0, elemId: `2@${actor}`, opId: `2@${actor}`, value: {\n              type: 'map',\n              objectId: `2@${actor}`,\n              props: {\n                title: {[`3@${actor}`]: {type: 'value', value: 'water plants'}},\n                done:  {[`4@${actor}`]: {type: 'value', value: false}}\n              }\n            }}\n          ]\n        }}}}\n      })\n    })\n\n    it('should include Date objects at the root', () => {\n      const now = new Date()\n      const actor = uuid(), change = {actor, seq: 1, startOp: 1, time: 0, deps: [], ops: [\n        {action: 'set', obj: '_root', key: 'now', value: now.getTime(), datatype: 'timestamp', pred: []}\n      ]}\n      const s1 = Backend.loadChanges(Backend.init(), [encodeChange(change)])\n      assert.deepStrictEqual(Backend.getPatch(s1), {\n        clock: {[actor]: 1}, deps: [hash(change)], maxOp: 1, pendingChanges: 0,\n        diffs: {objectId: '_root', type: 'map', props: {\n          now: {[`1@${actor}`]: {type: 'value', value: now.getTime(), datatype: 'timestamp'}}\n        }}\n      })\n    })\n\n    it('should include Date objects in a list', () => {\n      const now = new Date(), actor = uuid()\n      const change = {actor, seq: 1, startOp: 1, time: 0, deps: [], ops: [\n        {action: 'makeList', obj: '_root', key: 'list', pred: []},\n        {action: 'set', obj: `1@${actor}`, elemId: '_head', insert: true, value: now.getTime(), datatype: 'timestamp', pred: []}\n      ]}\n      const s1 = Backend.loadChanges(Backend.init(), [encodeChange(change)])\n      assert.deepStrictEqual(Backend.getPatch(s1), {\n        clock: {[actor]: 1}, deps: [hash(change)], maxOp: 2, pendingChanges: 0,\n        diffs: {objectId: '_root', type: 'map', props: {list: {[`1@${actor}`]: {\n          objectId: `1@${actor}`, type: 'list', edits: [\n            {action: 'insert', index: 0, elemId: `2@${actor}`, opId: `2@${actor}`,\n             value: {type: 'value', value: now.getTime(), datatype: 'timestamp'}}\n          ]\n        }}}}\n      })\n    })\n\n    it('should condense multiple inserts into a single edit', () => {\n      const actor = uuid()\n      const change1 = {actor, seq: 1, startOp: 1, time: 0, deps: [], ops: [\n        {action: 'makeList', obj: '_root', key: 'birds', pred: []},\n        {action: 'set', obj: `1@${actor}`, elemId: '_head',      insert: true, value: 'chaffinch', pred: []},\n        {action: 'set', obj: `1@${actor}`, elemId: `2@${actor}`, insert: true, value: 'goldfinch', pred: []},\n        {action: 'set', obj: `1@${actor}`, elemId: `3@${actor}`, insert: true, values: ['bullfinch', 'greenfinch'], pred: []}\n      ]}\n      const s1 = Backend.loadChanges(Backend.init(), [change1].map(encodeChange))\n      assert.deepStrictEqual(Backend.getPatch(s1), {\n        clock: {[actor]: 1}, deps: [hash(change1)], maxOp: 5, pendingChanges: 0,\n        diffs: {objectId: '_root', type: 'map', props: {birds: {[`1@${actor}`]: {\n          objectId: `1@${actor}`, type: 'list',\n          edits: [\n            {action: 'multi-insert', index: 0, elemId: `2@${actor}`, values: [\n              'chaffinch',\n              'goldfinch',\n              'bullfinch',\n              'greenfinch',\n            ]}\n          ]\n        }}}}\n      })\n    })\n\n    it('should use a multi-insert only for consecutive elemIds', () => {\n      const actor = uuid()\n      const change1 = {actor, seq: 1, startOp: 1, time: 0, deps: [], ops: [\n        {action: 'makeList', obj: '_root', key: 'birds', pred: []},\n        {action: 'set', obj: `1@${actor}`, elemId: '_head',      insert: true, value: 'chaffinch', pred: []},\n        {action: 'set', obj: `1@${actor}`, elemId: `2@${actor}`, insert: true, value: 'goldfinch', pred: []},\n        {action: 'set', obj: `1@${actor}`, elemId: '_head',      insert: true, values: ['bullfinch', 'greenfinch'], pred: []}\n      ]}\n      const s1 = Backend.loadChanges(Backend.init(), [change1].map(encodeChange))\n      assert.deepStrictEqual(Backend.getPatch(s1), {\n        clock: {[actor]: 1}, deps: [hash(change1)], maxOp: 5, pendingChanges: 0,\n        diffs: {objectId: '_root', type: 'map', props: {birds: {[`1@${actor}`]: {\n          objectId: `1@${actor}`, type: 'list', edits: [\n            {action: 'multi-insert', index: 0, elemId: `4@${actor}`, values: ['bullfinch', 'greenfinch']},\n            {action: 'multi-insert', index: 2, elemId: `2@${actor}`, values: ['chaffinch', 'goldfinch']}\n          ]\n        }}}}\n      })\n    })\n  })\n})\n"
  },
  {
    "path": "test/columnar_test.js",
    "content": "const assert = require('assert')\nconst { checkEncoded } = require('./helpers')\nconst Automerge = process.env.TEST_DIST === '1' ? require('../dist/automerge') : require('../src/automerge')\nconst { encodeChange, decodeChange } = require('../backend/columnar')\n\ndescribe('change encoding', () => {\n  it('should encode text edits', () => {\n    const change1 = {actor: 'aaaa', seq: 1, startOp: 1, time: 9, message: '', deps: [], ops: [\n      {action: 'makeText', obj: '_root', key: 'text', insert: false, pred: []},\n      {action: 'set', obj: '1@aaaa', elemId: '_head', insert: true, value: 'h', pred: []},\n      {action: 'del', obj: '1@aaaa', elemId: '2@aaaa', insert: false, pred: ['2@aaaa']},\n      {action: 'set', obj: '1@aaaa', elemId: '_head', insert: true, value: 'H', pred: []},\n      {action: 'set', obj: '1@aaaa', elemId: '4@aaaa', insert: true, value: 'i', pred: []}\n    ]}\n    checkEncoded(encodeChange(change1), [\n      0x85, 0x6f, 0x4a, 0x83, // magic bytes\n      0xe2, 0xbd, 0xfb, 0xf5, // checksum\n      1, 94, 0, 2, 0xaa, 0xaa, // chunkType: change, length, deps, actor 'aaaa'\n      1, 1, 9, 0, 0, // seq, startOp, time, message, actor list\n      12, 0x01, 4, 0x02, 4, // column count, objActor, objCtr\n      0x11, 8, 0x13, 7, 0x15, 8, // keyActor, keyCtr, keyStr\n      0x34, 4, 0x42, 6, // insert, action\n      0x56, 6, 0x57, 3, // valLen, valRaw\n      0x70, 6, 0x71, 2, 0x73, 2, // predNum, predActor, predCtr\n      0, 1, 4, 0, // objActor column: null, 0, 0, 0, 0\n      0, 1, 4, 1, // objCtr column: null, 1, 1, 1, 1\n      0, 2, 0x7f, 0, 0, 1, 0x7f, 0, // keyActor column: null, null, 0, null, 0\n      0, 1, 0x7c, 0, 2, 0x7e, 4, // keyCtr column: null, 0, 2, 0, 4\n      0x7f, 4, 0x74, 0x65, 0x78, 0x74, 0, 4, // keyStr column: 'text', null, null, null, null\n      1, 1, 1, 2, // insert column: false, true, false, true, true\n      0x7d, 4, 1, 3, 2, 1, // action column: makeText, set, del, set, set\n      0x7d, 0, 0x16, 0, 2, 0x16, // valLen column: 0, 0x16, 0, 0x16, 0x16\n      0x68, 0x48, 0x69, // valRaw column: 'h', 'H', 'i'\n      2, 0, 0x7f, 1, 2, 0, // predNum column: 0, 0, 1, 0, 0\n      0x7f, 0, // predActor column: 0\n      0x7f, 2 // predCtr column: 2\n    ])\n    const decoded = decodeChange(encodeChange(change1))\n    assert.deepStrictEqual(decoded, Object.assign({hash: decoded.hash}, change1))\n  })\n\n  it('should require strict ordering of preds', () => {\n    const change = new Uint8Array([\n      133, 111, 74, 131, 31, 229, 112, 44, 1, 105, 1, 58, 30, 190, 100, 253, 180, 180, 66, 49, 126,\n      81, 142, 10, 3, 35, 140, 189, 231, 34, 145, 57, 66, 23, 224, 149, 64, 97, 88, 140, 168, 194,\n      229, 4, 244, 209, 58, 138, 67, 140, 1, 152, 236, 250, 2, 0, 1, 4, 55, 234, 66, 242, 8, 21, 11,\n      52, 1, 66, 2, 86, 3, 87, 10, 112, 2, 113, 3, 115, 4, 127, 9, 99, 111, 109, 109, 111, 110, 86,\n      97, 114, 1, 127, 1, 127, 166, 1, 52, 48, 57, 49, 52, 57, 52, 53, 56, 50, 127, 2, 126, 0, 1,\n      126, 139, 1, 0\n    ])\n    assert.throws(() => { decodeChange(change) }, /operation IDs are not in ascending order/)\n  })\n\n  describe('with trailing bytes', () => {\n    let change = new Uint8Array([\n      0x85, 0x6f, 0x4a, 0x83, // magic bytes\n      0xb2, 0x98, 0x9e, 0xa9, // checksum\n      1, 61, 0, 2, 0x12, 0x34, // chunkType: change, length, deps, actor '1234'\n      1, 1, 252, 250, 220, 255, 5, // seq, startOp, time\n      14, 73, 110, 105, 116, 105, 97, 108, 105, 122, 97, 116, 105, 111, 110, // message: 'Initialization'\n      0, 6, // actor list, column count\n      0x15, 3, 0x34, 1, 0x42, 2, // keyStr, insert, action\n      0x56, 2, 0x57, 1, 0x70, 2, // valLen, valRaw, predNum\n      0x7f, 1, 0x78, // keyStr: 'x'\n      1, // insert: false\n      0x7f, 1, // action: set\n      0x7f, 19, // valLen: 1 byte of type uint\n      1, // valRaw: 1\n      0x7f, 0, // predNum: 0\n      0, 1, 2, 3, 4, 5, 6, 7, 8, 9 // 10 trailing bytes\n    ])\n\n    it('should allow decoding and re-encoding', () => {\n      // NOTE: This calls the JavaScript encoding and decoding functions, even when the WebAssembly\n      // backend is loaded. Should the wasm backend export its own functions for testing?\n      checkEncoded(change, encodeChange(decodeChange(change)))\n    })\n\n    it('should be preserved in document encoding', () => {\n      const [doc] = Automerge.applyChanges(Automerge.init(), [change])\n      const [reconstructed] = Automerge.getAllChanges(Automerge.load(Automerge.save(doc)))\n      checkEncoded(change, reconstructed)\n    })\n  })\n})\n"
  },
  {
    "path": "test/context_test.js",
    "content": "const assert = require('assert')\nconst sinon = require('sinon')\nconst { Context } = require('../frontend/context')\nconst { CACHE, OBJECT_ID, CONFLICTS, STATE, ELEM_IDS } = require('../frontend/constants')\nconst { Counter } = require('../frontend/counter')\nconst { Table, instantiateTable } = require('../frontend/table')\nconst { Text } = require('../frontend/text')\nconst uuid = require('../src/uuid')\n\ndescribe('Proxying context', () => {\n  let context, applyPatch\n\n  beforeEach(() => {\n    applyPatch = sinon.spy()\n    context = new Context({[STATE]: { maxOp: 0 }, [CACHE]: {_root: {}}}, uuid(), applyPatch)\n  })\n\n  describe('.setMapKey', () => {\n    it('should assign a primitive value to a map key', () => {\n      context.setMapKey([], 'sparrows', 5)\n      assert(applyPatch.calledOnce)\n      assert.deepStrictEqual(applyPatch.firstCall.args[0], {objectId: '_root', type: 'map', props: {\n        sparrows: {[`1@${context.actorId}`]: {value: 5, datatype: 'int', type: 'value'}}\n      }})\n      assert.deepStrictEqual(context.ops, [\n        {obj: '_root', action: 'set', key: 'sparrows', insert: false, datatype: 'int', value: 5, pred: []}\n      ])\n    })\n\n    it('should do nothing if the value was not changed', () => {\n      context.cache._root = {[OBJECT_ID]: '_root', goldfinches: 3, [CONFLICTS]: {goldfinches: {'1@actor1': 3}}}\n      context.setMapKey([], 'goldfinches', 3)\n      assert(applyPatch.notCalled)\n      assert.deepStrictEqual(context.ops, [])\n    })\n\n    it('should allow a conflict to be resolved', () => {\n      context.cache._root = {[OBJECT_ID]: '_root', goldfinches: 5, [CONFLICTS]: {goldfinches: {'1@actor1': 3, '2@actor2': 5}}}\n      context.setMapKey([], 'goldfinches', 3)\n      assert(applyPatch.calledOnce)\n      assert.deepStrictEqual(applyPatch.firstCall.args[0], {objectId: '_root', type: 'map', props: {\n        goldfinches: {[`1@${context.actorId}`]: {value: 3, datatype: 'int', type: 'value'}}\n      }})\n      assert.deepStrictEqual(context.ops, [\n        {obj: '_root', action: 'set', key: 'goldfinches', insert: false, datatype: 'int', value: 3, pred: ['1@actor1', '2@actor2']}\n      ])\n    })\n\n    it('should create nested maps', () => {\n      context.setMapKey([], 'birds', {goldfinches: 3})\n      assert(applyPatch.calledOnce)\n      const objectId = applyPatch.firstCall.args[0].props.birds[`1@${context.actorId}`].objectId\n      assert.deepStrictEqual(applyPatch.firstCall.args[0], {objectId: '_root', type: 'map', props: {\n        birds: {[`1@${context.actorId}`]: {objectId, type: 'map', props: {\n          goldfinches: {[`2@${context.actorId}`]: {value: 3, datatype: 'int', type: 'value'}}\n        }}}\n      }})\n      assert.deepStrictEqual(context.ops, [\n        {obj: '_root', action: 'makeMap', key: 'birds', insert: false, pred: []},\n        {obj: objectId, action: 'set', key: 'goldfinches', insert: false, datatype: 'int', value: 3, pred: []}\n      ])\n    })\n\n    it('should perform assignment inside nested maps', () => {\n      const objectId = uuid(), child = {[OBJECT_ID]: objectId}\n      context.cache[objectId] = child\n      context.cache._root = {[OBJECT_ID]: '_root', [CONFLICTS]: {birds: {'1@actor1': child}}, birds: child}\n      context.setMapKey([{key: 'birds', objectId}], 'goldfinches', 3)\n      assert(applyPatch.calledOnce)\n      assert.deepStrictEqual(applyPatch.firstCall.args[0], {objectId: '_root', type: 'map', props: {\n        birds: {'1@actor1': {objectId, type: 'map', props: {\n          goldfinches: {[`1@${context.actorId}`]: {value: 3, datatype: 'int', type: 'value'}}\n        }}}\n      }})\n      assert.deepStrictEqual(context.ops, [\n        {obj: objectId, action: 'set', key: 'goldfinches', insert: false, datatype: 'int', value: 3, pred: []}\n      ])\n    })\n\n    it('should perform assignment inside conflicted maps', () => {\n      const objectId1 = uuid(), child1 = {[OBJECT_ID]: objectId1}\n      const objectId2 = uuid(), child2 = {[OBJECT_ID]: objectId2}\n      context.cache[objectId1] = child1\n      context.cache[objectId2] = child2\n      context.cache._root = {[OBJECT_ID]: '_root', birds: child2,\n        [CONFLICTS]: {birds: {'1@actor1': child1, '1@actor2': child2}}}\n      context.setMapKey([{key: 'birds', objectId: objectId2}], 'goldfinches', 3)\n      assert(applyPatch.calledOnce)\n      assert.deepStrictEqual(applyPatch.firstCall.args[0], {objectId: '_root', type: 'map', props: {birds: {\n        '1@actor1': {objectId: objectId1, type: 'map', props: {}},\n        '1@actor2': {objectId: objectId2, type: 'map', props: {\n          goldfinches: {[`1@${context.actorId}`]: {value: 3, datatype: 'int', type: 'value'}}\n        }}\n      }}})\n      assert.deepStrictEqual(context.ops, [\n        {obj: objectId2, action: 'set', key: 'goldfinches', insert: false, datatype: 'int', value: 3, pred: []}\n      ])\n    })\n\n    it('should handle conflict values of various types', () => {\n      const objectId = uuid(), child = {[OBJECT_ID]: objectId}, dateValue = new Date()\n      context.cache[objectId] = child\n      context.cache._root = {[OBJECT_ID]: '_root', values: child, [CONFLICTS]: {values: {\n        '1@actor1': dateValue, '1@actor2': new Counter(), '1@actor3': 42, '1@actor4': null, '1@actor5': child\n      }}}\n      context.setMapKey([{key: 'values', objectId}], 'goldfinches', 3)\n      assert(applyPatch.calledOnce)\n      assert.deepStrictEqual(applyPatch.firstCall.args[0], {objectId: '_root', type: 'map', props: {values: {\n        '1@actor1': {value: dateValue.getTime(), datatype: 'timestamp', type: 'value'},\n        '1@actor2': {value: 0, datatype: 'counter', type: 'value'},\n        '1@actor3': {value: 42, datatype: 'int', type: 'value'},\n        '1@actor4': {value: null, type: 'value'},\n        '1@actor5': {objectId, type: 'map', props: {goldfinches: {[`1@${context.actorId}`]: {value: 3, type: 'value', datatype: 'int' }}}}\n      }}})\n      assert.deepStrictEqual(context.ops, [\n        {obj: objectId, action: 'set', key: 'goldfinches', insert: false, datatype: 'int', value: 3, pred: []}\n      ])\n    })\n\n    it('should create nested lists', () => {\n      context.setMapKey([], 'birds', ['sparrow', 'goldfinch'])\n      assert(applyPatch.calledOnce)\n      const objectId = applyPatch.firstCall.args[0].props.birds[`1@${context.actorId}`].objectId\n      assert.deepStrictEqual(applyPatch.firstCall.args[0], {objectId: '_root', type: 'map', props: {\n        birds: {[`1@${context.actorId}`]: {objectId, type: 'list', edits: [\n          {action: 'multi-insert', index: 0, elemId: `2@${context.actorId}`, values: ['sparrow', 'goldfinch']}\n        ]}}\n      }})\n      assert.deepStrictEqual(context.ops, [\n        {obj: '_root', action: 'makeList', key: 'birds', insert: false, pred: []},\n        {obj: objectId, action: 'set', elemId: '_head', insert: true, values: ['sparrow', 'goldfinch'], pred: []}\n      ])\n    })\n\n    it('should create nested Text objects', () => {\n      context.setMapKey([], 'text', new Text('hi'))\n      const objectId = applyPatch.firstCall.args[0].props.text[`1@${context.actorId}`].objectId\n      assert(applyPatch.calledOnce)\n      assert.deepStrictEqual(applyPatch.firstCall.args[0], {objectId: '_root', type: 'map', props: {\n        text: {[`1@${context.actorId}`]: {objectId, type: 'text', edits: [\n          {action: 'multi-insert', index: 0, elemId: `2@${context.actorId}`, values: ['h', 'i']}\n        ]}}\n      }})\n      assert.deepStrictEqual(context.ops, [\n        {obj: '_root', action: 'makeText', key: 'text', insert: false, pred: []},\n        {obj: objectId, action: 'set', elemId: '_head', insert: true, values: ['h', 'i'], pred: []}\n      ])\n    })\n\n    it('should create nested Table objects', () => {\n      context.setMapKey([], 'books', new Table())\n      assert(applyPatch.calledOnce)\n      const objectId = applyPatch.firstCall.args[0].props.books[`1@${context.actorId}`].objectId\n      assert.deepStrictEqual(applyPatch.firstCall.args[0], {objectId: '_root', type: 'map', props: {\n        books: {[`1@${context.actorId}`]: {objectId, type: 'table', props: {}}}\n      }})\n      assert.deepStrictEqual(context.ops, [\n        {obj: '_root', action: 'makeTable', key: 'books', insert: false, pred: []}\n      ])\n    })\n\n    it('should allow assignment of Date values', () => {\n      const now = new Date()\n      context.setMapKey([], 'now', now)\n      assert(applyPatch.calledOnce)\n      assert.deepStrictEqual(applyPatch.firstCall.args[0], {objectId: '_root', type: 'map', props: {\n        now: {[`1@${context.actorId}`]: {value: now.getTime(), datatype: 'timestamp', type: 'value'}}\n      }})\n      assert.deepStrictEqual(context.ops, [\n        {obj: '_root', action: 'set', key: 'now', insert: false, value: now.getTime(), datatype: 'timestamp', pred: []}\n      ])\n    })\n\n    it('should allow assignment of Counter values', () => {\n      const counter = new Counter(3)\n      context.setMapKey([], 'counter', counter)\n      assert(applyPatch.calledOnce)\n      assert.deepStrictEqual(applyPatch.firstCall.args[0], {objectId: '_root', type: 'map', props: {\n        counter: {[`1@${context.actorId}`]: {value: 3, datatype: 'counter', type: 'value'}}\n      }})\n      assert.deepStrictEqual(context.ops, [\n        {obj: '_root', action: 'set', key: 'counter', insert: false, value: 3, datatype: 'counter', pred: []}\n      ])\n    })\n  })\n\n  describe('.deleteMapKey', () => {\n    it('should remove an existing key', () => {\n      context.cache._root = {[OBJECT_ID]: '_root', goldfinches: 3, [CONFLICTS]: {goldfinches: {'1@actor1': 3}}}\n      context.deleteMapKey([], 'goldfinches')\n      assert(applyPatch.calledOnce)\n      assert.deepStrictEqual(applyPatch.firstCall.args[0], {objectId: '_root', type: 'map', props: {goldfinches: {}}})\n      assert.deepStrictEqual(context.ops, [\n        {obj: '_root', action: 'del', key: 'goldfinches', insert: false, pred: ['1@actor1']}\n      ])\n    })\n\n    it('should do nothing if the key does not exist', () => {\n      context.cache._root = {[OBJECT_ID]: '_root', goldfinches: 3, [CONFLICTS]: {goldfinches: {'1@actor1': 3}}}\n      context.deleteMapKey([], 'sparrows')\n      assert(applyPatch.notCalled)\n      assert.deepStrictEqual(context.ops, [])\n    })\n\n    it('should update a nested object', () => {\n      const objectId = uuid(), child = {[OBJECT_ID]: objectId, [CONFLICTS]: {goldfinches: {'5@actor1': 3}}, goldfinches: 3}\n      context.cache[objectId] = child\n      context.cache._root = {[OBJECT_ID]: '_root', [CONFLICTS]: {birds: {'1@actor1': child}}, birds: child}\n      context.deleteMapKey([{key: 'birds', objectId}], 'goldfinches')\n      assert(applyPatch.calledOnce)\n      assert.deepStrictEqual(applyPatch.firstCall.args[0], {objectId: '_root', type: 'map', props: {\n        birds: {'1@actor1': {objectId, type: 'map', props: {goldfinches: {}}}}\n      }})\n      assert.deepStrictEqual(context.ops, [\n        {obj: objectId, action: 'del', key: 'goldfinches', insert: false, pred: ['5@actor1']}\n      ])\n    })\n  })\n\n  describe('list manipulation', () => {\n    let listId, list\n\n    beforeEach(() => {\n      listId = uuid()\n      list = ['swallow', 'magpie']\n      Object.defineProperty(list, OBJECT_ID, {value: listId})\n      Object.defineProperty(list, CONFLICTS, {value: [{'1@xxx': 'swallow'}, {'2@xxx': 'magpie'}]})\n      Object.defineProperty(list, ELEM_IDS,  {value: ['1@xxx', '2@xxx']})\n      context.cache[listId] = list\n      context.cache._root = {[OBJECT_ID]: '_root', birds: list, [CONFLICTS]: {birds: {'1@actor1': list}}}\n    })\n\n    it('should overwrite an existing list element', () => {\n      context.setListIndex([{key: 'birds', objectId: listId}], 0, 'starling')\n      assert(applyPatch.calledOnce)\n      assert.deepStrictEqual(applyPatch.firstCall.args[0], {objectId: '_root', type: 'map', props: {\n        birds: {'1@actor1': {objectId: listId, type: 'list', edits: [\n          {action: 'update', index: 0, opId: `1@${context.actorId}`, value: {value: 'starling', type: 'value'}}\n        ]}}\n      }})\n      assert.deepStrictEqual(context.ops, [\n        {obj: listId, action: 'set', elemId: '1@xxx', insert: false, value: 'starling', pred: ['1@xxx']}\n      ])\n    })\n\n    it('should create nested objects on assignment', () => {\n      context.setListIndex([{key: 'birds', objectId: listId}], 1, {english: 'goldfinch', latin: 'carduelis'})\n      assert(applyPatch.calledOnce)\n      const nestedId = applyPatch.firstCall.args[0].props.birds['1@actor1'].edits[0].value.objectId\n      assert.deepStrictEqual(applyPatch.firstCall.args[0], {objectId: '_root', type: 'map', props: {\n        birds: {'1@actor1': {objectId: listId, type: 'list', edits: [{\n          action: 'update', index: 1, opId: `1@${context.actorId}`, value: {\n            objectId: nestedId, type: 'map', props: {\n              english: {[`2@${context.actorId}`]: {value: 'goldfinch', type: 'value'}},\n              latin: {[`3@${context.actorId}`]: {value: 'carduelis', type: 'value'}}\n            }\n          }\n        }]}}\n      }})\n      assert.deepStrictEqual(context.ops, [\n        {obj: listId, action: 'makeMap', elemId: '2@xxx', insert: false, pred: ['2@xxx']},\n        {obj: nestedId, action: 'set', key: 'english', insert: false, value: 'goldfinch', pred: []},\n        {obj: nestedId, action: 'set', key: 'latin', insert: false, value: 'carduelis', pred: []}\n      ])\n    })\n\n    it('should create nested objects on insertion', () => {\n      context.splice([{key: 'birds', objectId: listId}], 2, 0, [{english: 'goldfinch', latin: 'carduelis'}])\n      assert(applyPatch.calledOnce)\n      const nestedId = applyPatch.firstCall.args[0].props.birds['1@actor1'].edits[0].value.objectId\n      assert.deepStrictEqual(applyPatch.firstCall.args[0], {objectId: '_root', type: 'map', props: {\n        birds: {'1@actor1': {objectId: listId, type: 'list', edits: [\n          {action: 'insert', index: 2, elemId: `1@${context.actorId}`, opId: `1@${context.actorId}`, value: {\n            objectId: nestedId, type: 'map', props: {\n              english: {[`2@${context.actorId}`]: {value: 'goldfinch', type: 'value'}},\n              latin: {[`3@${context.actorId}`]: {value: 'carduelis', type: 'value'}}\n            }\n          }}\n        ]}}\n      }})\n      assert.deepStrictEqual(context.ops, [\n        {obj: listId, action: 'makeMap', elemId: '2@xxx', insert: true, pred: []},\n        {obj: nestedId, action: 'set', key: 'english', insert: false, value: 'goldfinch', pred: []},\n        {obj: nestedId, action: 'set', key: 'latin', insert: false, value: 'carduelis', pred: []}\n      ])\n    })\n\n    it('should generate multi-inserts when splicing arrays of primitives', () => {\n      context.splice([{key: 'birds', objectId: listId}], 2, 0, ['goldfinch', 'greenfinch'])\n      assert(applyPatch.calledOnce)\n      assert.deepStrictEqual(applyPatch.firstCall.args[0], {objectId: '_root', type: 'map', props: {\n        birds: {'1@actor1': {objectId: listId, type: 'list', edits: [\n          {action: 'multi-insert', index: 2, elemId: `1@${context.actorId}`, values: ['goldfinch', 'greenfinch']}\n        ]}}\n      }})\n      assert.deepStrictEqual(context.ops, [\n        {obj: listId, action: 'set', elemId: '2@xxx', insert: true, values: ['goldfinch', 'greenfinch'], pred: []}\n      ])\n    })\n\n    it('should support deleting list elements', () => {\n      context.splice([{key: 'birds', objectId: listId}], 0, 1, [])\n      assert(applyPatch.calledOnce)\n      assert.deepStrictEqual(applyPatch.firstCall.args[0], {objectId: '_root', type: 'map', props: {\n        birds: {'1@actor1': {objectId: listId, type: 'list', edits: [\n          {action: 'remove', index: 0, count: 1}\n        ]}}\n      }})\n      assert.deepStrictEqual(context.ops, [\n        {obj: listId, action: 'del', elemId: '1@xxx', insert: false, pred: ['1@xxx']}\n      ])\n    })\n\n    it('should support deleting multiple list elements as a multiOp', () => {\n      context.splice([{key: 'birds', objectId: listId}], 0, 2, [])\n      assert(applyPatch.calledOnce)\n      assert.deepStrictEqual(applyPatch.firstCall.args[0], {objectId: '_root', type: 'map', props: {\n        birds: {'1@actor1': {objectId: listId, type: 'list', edits: [\n          {action: 'remove', index: 0, count: 2}\n        ]}}\n      }})\n      assert.deepStrictEqual(context.ops, [\n        {obj: listId, action: 'del', elemId: '1@xxx', multiOp: 2, insert: false, pred: ['1@xxx']}\n      ])\n    })\n\n    it('should use multiOps for consecutive runs of elemIds', () => {\n      list.unshift('sparrow')\n      list[ELEM_IDS].unshift('3@xxx')\n      list[CONFLICTS].unshift({'3@xxx': 'sparrow'})\n      context.splice([{key: 'birds', objectId: listId}], 0, 3, [])\n      assert(applyPatch.calledOnce)\n      assert.deepStrictEqual(applyPatch.firstCall.args[0], {objectId: '_root', type: 'map', props: {\n        birds: {'1@actor1': {objectId: listId, type: 'list', edits: [\n          {action: 'remove', index: 0, count: 3}\n        ]}}\n      }})\n      assert.deepStrictEqual(context.ops, [\n        {obj: listId, action: 'del', elemId: '3@xxx', insert: false, pred: ['3@xxx']},\n        {obj: listId, action: 'del', elemId: '1@xxx', multiOp: 2, insert: false, pred: ['1@xxx']}\n      ])\n    })\n\n    it('should use multiOps for consecutive runs of preds', () => {\n      list[1] = 'sparrow'\n      list[CONFLICTS][1] = {'3@xxx': 'sparrow'}\n      context.splice([{key: 'birds', objectId: listId}], 0, 2, [])\n      assert(applyPatch.calledOnce)\n      assert.deepStrictEqual(applyPatch.firstCall.args[0], {objectId: '_root', type: 'map', props: {\n        birds: {'1@actor1': {objectId: listId, type: 'list', edits: [\n          {action: 'remove', index: 0, count: 2}\n        ]}}\n      }})\n      assert.deepStrictEqual(context.ops, [\n        {obj: listId, action: 'del', elemId: '1@xxx', insert: false, pred: ['1@xxx']},\n        {obj: listId, action: 'del', elemId: '2@xxx', insert: false, pred: ['3@xxx']}\n      ])\n    })\n\n    it('should support list splicing', () => {\n      context.splice([{key: 'birds', objectId: listId}], 0, 1, ['starling', 'goldfinch'])\n      assert(applyPatch.calledOnce)\n      assert.deepStrictEqual(applyPatch.firstCall.args[0], {objectId: '_root', type: 'map', props: {\n        birds: {'1@actor1': {objectId: listId, type: 'list', edits: [\n          {action: 'remove', index: 0, count: 1},\n          {action: 'multi-insert', index: 0, elemId: `2@${context.actorId}`, values: ['starling', 'goldfinch']}\n        ]}}\n      }})\n      assert.deepStrictEqual(context.ops, [\n        {obj: listId, action: 'del', elemId: '1@xxx', insert: false, pred: ['1@xxx']},\n        {obj: listId, action: 'set', elemId: '_head', insert: true, values: ['starling', 'goldfinch'], pred: []}\n      ])\n    })\n  })\n\n  describe('Table manipulation', () => {\n    let tableId, table\n\n    beforeEach(() => {\n      tableId = uuid()\n      table = instantiateTable(tableId)\n      context.cache[tableId] = table\n      context.cache._root = {[OBJECT_ID]: '_root', books: table, [CONFLICTS]: {books: {'1@actor1': table}}}\n    })\n\n    it('should add a table row', () => {\n      const rowId = context.addTableRow([{key: 'books', objectId: tableId}], {author: 'Mary Shelley', title: 'Frankenstein'})\n      assert(applyPatch.calledOnce)\n      assert.deepStrictEqual(applyPatch.firstCall.args[0], {objectId: '_root', type: 'map', props: {\n        books: {'1@actor1': {objectId: tableId, type: 'table', props: {\n          [rowId]: {[`1@${context.actorId}`]: {objectId: `1@${context.actorId}`, type: 'map', props: {\n            author: {[`2@${context.actorId}`]: {value: 'Mary Shelley', type: 'value'}},\n            title: {[`3@${context.actorId}`]: {value: 'Frankenstein', type: 'value'}}\n          }}}\n        }}}\n      }})\n      assert.deepStrictEqual(context.ops, [\n        {obj: tableId, action: 'makeMap', key: rowId, insert: false, pred: []},\n        {obj: `1@${context.actorId}`, action: 'set', key: 'author', insert: false, value: 'Mary Shelley', pred: []},\n        {obj: `1@${context.actorId}`, action: 'set', key: 'title', insert: false, value: 'Frankenstein', pred: []}\n      ])\n    })\n\n    it('should delete a table row', () => {\n      const rowId = uuid()\n      const row = {author: 'Mary Shelley', title: 'Frankenstein'}\n      row[OBJECT_ID] = rowId\n      table.entries[rowId] = row\n      context.deleteTableRow([{key: 'books', objectId: tableId}], rowId, '5@actor1')\n      assert(applyPatch.calledOnce)\n      assert.deepStrictEqual(applyPatch.firstCall.args[0], {objectId: '_root', type: 'map', props: {\n        books: {'1@actor1': {objectId: tableId, type: 'table', props: {[rowId]: {}}}}\n      }})\n      assert.deepStrictEqual(context.ops, [\n        {obj: tableId, action: 'del', key: rowId, insert: false, pred: ['5@actor1']}\n      ])\n    })\n  })\n\n  it('should increment a counter', () => {\n    const counter = new Counter()\n    context.cache._root = {[OBJECT_ID]: '_root', counter, [CONFLICTS]: {counter: {'1@actor1': counter}}}\n    context.increment([], 'counter', 1)\n    assert(applyPatch.calledOnce)\n    assert.deepStrictEqual(applyPatch.firstCall.args[0], {objectId: '_root', type: 'map', props: {\n      counter: {[`1@${context.actorId}`]: {value: 1, datatype: 'counter'}}\n    }})\n    assert.deepStrictEqual(context.ops, [{obj: '_root', action: 'inc', key: 'counter', insert: false, value: 1, pred: ['1@actor1']}])\n  })\n})\n"
  },
  {
    "path": "test/encoding_test.js",
    "content": "const assert = require('assert')\nconst { checkEncoded } = require('./helpers')\nconst { Encoder, Decoder, RLEEncoder, RLEDecoder, DeltaEncoder, DeltaDecoder, BooleanEncoder, BooleanDecoder } = require('../backend/encoding')\n\ndescribe('Binary encoding', () => {\n  describe('Encoder and Decoder', () => {\n    describe('32-bit LEB128 encoding', () => {\n      it('should encode unsigned integers', () => {\n        function encode(value) {\n          const encoder = new Encoder()\n          encoder.appendUint32(value)\n          return encoder\n        }\n        checkEncoded(encode(0), [0])\n        checkEncoded(encode(1), [1])\n        checkEncoded(encode(0x42), [0x42])\n        checkEncoded(encode(0x7f), [0x7f])\n        checkEncoded(encode(0x80), [0x80, 0x01])\n        checkEncoded(encode(0xff), [0xff, 0x01])\n        checkEncoded(encode(0x1234), [0xb4, 0x24])\n        checkEncoded(encode(0x3fff), [0xff, 0x7f])\n        checkEncoded(encode(0x4000), [0x80, 0x80, 0x01])\n        checkEncoded(encode(0x5678), [0xf8, 0xac, 0x01])\n        checkEncoded(encode(0xfffff), [0xff, 0xff, 0x3f])\n        checkEncoded(encode(0x1fffff), [0xff, 0xff, 0x7f])\n        checkEncoded(encode(0x200000), [0x80, 0x80, 0x80, 0x01])\n        checkEncoded(encode(0xfffffff), [0xff, 0xff, 0xff, 0x7f])\n        checkEncoded(encode(0x10000000), [0x80, 0x80, 0x80, 0x80, 0x01])\n        checkEncoded(encode(0x7fffffff), [0xff, 0xff, 0xff, 0xff, 0x07])\n        checkEncoded(encode(0x87654321), [0xa1, 0x86, 0x95, 0xbb, 0x08])\n        checkEncoded(encode(0xffffffff), [0xff, 0xff, 0xff, 0xff, 0x0f])\n      })\n\n      it('should round-trip unsigned integers', () => {\n        const examples = [\n          0, 1, 0x42, 0x7f, 0x80, 0xff, 0x1234, 0x3fff, 0x4000, 0x5678, 0xfffff, 0x1fffff,\n          0x200000, 0xfffffff, 0x10000000, 0x7fffffff, 0x87654321, 0xffffffff\n        ]\n        for (let value of examples) {\n          const encoder = new Encoder()\n          encoder.appendUint32(value)\n          const decoder = new Decoder(encoder.buffer)\n          assert.strictEqual(decoder.readUint32(), value)\n          assert.strictEqual(decoder.done, true)\n        }\n      })\n\n      it('should encode signed integers', () => {\n        function encode(value) {\n          const encoder = new Encoder()\n          encoder.appendInt32(value)\n          return encoder\n        }\n        checkEncoded(encode(0), [0])\n        checkEncoded(encode(1), [1])\n        checkEncoded(encode(-1), [0x7f])\n        checkEncoded(encode(0x3f), [0x3f])\n        checkEncoded(encode(0x40), [0xc0, 0x00])\n        checkEncoded(encode(-0x3f), [0x41])\n        checkEncoded(encode(-0x40), [0x40])\n        checkEncoded(encode(-0x41), [0xbf, 0x7f])\n        checkEncoded(encode(0x1fff), [0xff, 0x3f])\n        checkEncoded(encode(0x2000), [0x80, 0xc0, 0x00])\n        checkEncoded(encode(-0x2000), [0x80, 0x40])\n        checkEncoded(encode(-0x2001), [0xff, 0xbf, 0x7f])\n        checkEncoded(encode(0xfffff), [0xff, 0xff, 0x3f])\n        checkEncoded(encode(0x100000), [0x80, 0x80, 0xc0, 0x00])\n        checkEncoded(encode(-0x100000), [0x80, 0x80, 0x40])\n        checkEncoded(encode(-0x100001), [0xff, 0xff, 0xbf, 0x7f])\n        checkEncoded(encode(0x7ffffff), [0xff, 0xff, 0xff, 0x3f])\n        checkEncoded(encode(0x8000000), [0x80, 0x80, 0x80, 0xc0, 0x00])\n        checkEncoded(encode(-0x8000000), [0x80, 0x80, 0x80, 0x40])\n        checkEncoded(encode(-0x8000001), [0xff, 0xff, 0xff, 0xbf, 0x7f])\n        checkEncoded(encode(0x76543210), [0x90, 0xe4, 0xd0, 0xb2, 0x07])\n        checkEncoded(encode(-0x76543210), [0xf0, 0x9b, 0xaf, 0xcd, 0x78])\n        checkEncoded(encode(0x7fffffff), [0xff, 0xff, 0xff, 0xff, 0x07])\n        checkEncoded(encode(-0x80000000), [0x80, 0x80, 0x80, 0x80, 0x78])\n      })\n\n      it('should round-trip signed integers', () => {\n        const examples = [\n          0, 1, -1, 0x3f, 0x40, -0x3f, -0x40, -0x41, 0x1fff, 0x2000, -0x2000,\n          -0x2001, 0xfffff, 0x100000, -0x100000, -0x100001, 0x7ffffff, 0x8000000,\n          -0x8000000, -0x8000001, 0x76543210, -0x76543210, 0x7fffffff, -0x80000000\n        ]\n        for (let value of examples) {\n          const encoder = new Encoder()\n          encoder.appendInt32(value)\n          const decoder = new Decoder(encoder.buffer)\n          assert.strictEqual(decoder.readInt32(), value)\n          assert.strictEqual(decoder.done, true)\n        }\n      })\n\n      it('should not encode values that are out of range', () => {\n        assert.throws(() => { new Encoder().appendUint32(0x100000000) }, /out of range/)\n        assert.throws(() => { new Encoder().appendUint32(Number.MAX_SAFE_INTEGER) }, /out of range/)\n        assert.throws(() => { new Encoder().appendUint32(-1) }, /out of range/)\n        assert.throws(() => { new Encoder().appendUint32(-0x80000000) }, /out of range/)\n        assert.throws(() => { new Encoder().appendUint32(Number.NEGATIVE_INFINITY) }, /not an integer/)\n        assert.throws(() => { new Encoder().appendUint32(Number.NaN) }, /not an integer/)\n        assert.throws(() => { new Encoder().appendUint32(Math.PI) }, /not an integer/)\n        assert.throws(() => { new Encoder().appendInt32(0x80000000) }, /out of range/)\n        assert.throws(() => { new Encoder().appendInt32(Number.MAX_SAFE_INTEGER) }, /out of range/)\n        assert.throws(() => { new Encoder().appendInt32(-0x80000001) }, /out of range/)\n        assert.throws(() => { new Encoder().appendInt32(Number.NEGATIVE_INFINITY) }, /not an integer/)\n        assert.throws(() => { new Encoder().appendInt32(Number.NaN) }, /not an integer/)\n        assert.throws(() => { new Encoder().appendInt32(Math.PI) }, /not an integer/)\n      })\n\n      it('should not decode values that are out of range', () => {\n        assert.throws(() => { new Decoder(new Uint8Array([0x80, 0x80, 0x80, 0x80, 0x80, 0x00])).readUint32() }, /out of range/)\n        assert.throws(() => { new Decoder(new Uint8Array([0x80, 0x80, 0x80, 0x80, 0x80, 0x00])).readInt32() }, /out of range/)\n        assert.throws(() => { new Decoder(new Uint8Array([0x80, 0x80, 0x80, 0x80, 0x10])).readUint32() }, /out of range/)\n        assert.throws(() => { new Decoder(new Uint8Array([0x80, 0x80, 0x80, 0x80, 0x08])).readInt32() }, /out of range/)\n        assert.throws(() => { new Decoder(new Uint8Array([0xff, 0xff, 0xff, 0xff, 0x77])).readInt32() }, /out of range/)\n        assert.throws(() => { new Decoder(new Uint8Array([0x80, 0x80])).readUint32() }, /incomplete number/)\n        assert.throws(() => { new Decoder(new Uint8Array([0x80, 0x80])).readInt32() }, /incomplete number/)\n      })\n    })\n\n    describe('53-bit LEB128 encoding', () => {\n      it('should encode unsigned integers', () => {\n        function encode(value) {\n          const encoder = new Encoder()\n          encoder.appendUint53(value)\n          return encoder\n        }\n        checkEncoded(encode(0), [0])\n        checkEncoded(encode(0x7f), [0x7f])\n        checkEncoded(encode(0x80), [0x80, 0x01])\n        checkEncoded(encode(0x3fff), [0xff, 0x7f])\n        checkEncoded(encode(0x4000), [0x80, 0x80, 0x01])\n        checkEncoded(encode(0x1fffff), [0xff, 0xff, 0x7f])\n        checkEncoded(encode(0x200000), [0x80, 0x80, 0x80, 0x01])\n        checkEncoded(encode(0xfffffff), [0xff, 0xff, 0xff, 0x7f])\n        checkEncoded(encode(0x10000000), [0x80, 0x80, 0x80, 0x80, 0x01])\n        checkEncoded(encode(0xffffffff), [0xff, 0xff, 0xff, 0xff, 0x0f])\n        checkEncoded(encode(0x100000000), [0x80, 0x80, 0x80, 0x80, 0x10])\n        checkEncoded(encode(0x7ffffffff), [0xff, 0xff, 0xff, 0xff, 0x7f])\n        checkEncoded(encode(0x800000000), [0x80, 0x80, 0x80, 0x80, 0x80, 0x01])\n        checkEncoded(encode(0x3ffffffffff), [0xff, 0xff, 0xff, 0xff, 0xff, 0x7f])\n        checkEncoded(encode(0x40000000000), [0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x01])\n        checkEncoded(encode(0x2000000000000), [0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x01])\n        checkEncoded(encode(0x123456789abcde), [0xde, 0xf9, 0xea, 0xc4, 0xe7, 0x8a, 0x8d, 0x09])\n        checkEncoded(encode(Number.MAX_SAFE_INTEGER), [0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x0f])\n      })\n\n      it('should round-trip unsigned integers', () => {\n        const examples = [\n          0, 0x7f, 0x80, 0x3fff, 0x4000, 0x1fffff, 0x200000, 0xfffffff, 0x10000000,\n          0xffffffff, 0x100000000, 0x7ffffffff, 0x800000000, 0x3ffffffffff,\n          0x40000000000, 0x2000000000000, 0x123456789abcde, Number.MAX_SAFE_INTEGER\n        ]\n        for (let value of examples) {\n          const encoder = new Encoder()\n          encoder.appendUint53(value)\n          const decoder = new Decoder(encoder.buffer)\n          assert.strictEqual(decoder.readUint53(), value)\n          assert.strictEqual(decoder.done, true)\n        }\n      })\n\n      it('should encode signed integers', () => {\n        function encode(value) {\n          const encoder = new Encoder()\n          encoder.appendInt53(value)\n          return encoder\n        }\n        checkEncoded(encode(0), [0])\n        checkEncoded(encode(1), [1])\n        checkEncoded(encode(-1), [0x7f])\n        checkEncoded(encode(0x3f), [0x3f])\n        checkEncoded(encode(-0x40), [0x40])\n        checkEncoded(encode(0x40), [0xc0, 0x00])\n        checkEncoded(encode(-0x41), [0xbf, 0x7f])\n        checkEncoded(encode(0x1fff), [0xff, 0x3f])\n        checkEncoded(encode(-0x2000), [0x80, 0x40])\n        checkEncoded(encode(0x2000), [0x80, 0xc0, 0x00])\n        checkEncoded(encode(-0x2001), [0xff, 0xbf, 0x7f])\n        checkEncoded(encode(0xfffff), [0xff, 0xff, 0x3f])\n        checkEncoded(encode(-0x100000), [0x80, 0x80, 0x40])\n        checkEncoded(encode(0x100000), [0x80, 0x80, 0xc0, 0x00])\n        checkEncoded(encode(-0x100001), [0xff, 0xff, 0xbf, 0x7f])\n        checkEncoded(encode(0x7ffffff), [0xff, 0xff, 0xff, 0x3f])\n        checkEncoded(encode(-0x8000000), [0x80, 0x80, 0x80, 0x40])\n        checkEncoded(encode(0x8000000), [0x80, 0x80, 0x80, 0xc0, 0x00])\n        checkEncoded(encode(-0x8000001), [0xff, 0xff, 0xff, 0xbf, 0x7f])\n        checkEncoded(encode(0x7fffffff), [0xff, 0xff, 0xff, 0xff, 0x07])\n        checkEncoded(encode(0x80000000), [0x80, 0x80, 0x80, 0x80, 0x08])\n        checkEncoded(encode(-0x80000000), [0x80, 0x80, 0x80, 0x80, 0x78])\n        checkEncoded(encode(-0x80000001), [0xff, 0xff, 0xff, 0xff, 0x77])\n        checkEncoded(encode(0x3ffffffff), [0xff, 0xff, 0xff, 0xff, 0x3f])\n        checkEncoded(encode(-0x400000000), [0x80, 0x80, 0x80, 0x80, 0x40])\n        checkEncoded(encode(0x400000000), [0x80, 0x80, 0x80, 0x80, 0xc0, 0x00])\n        checkEncoded(encode(-0x400000001), [0xff, 0xff, 0xff, 0xff, 0xbf, 0x7f])\n        checkEncoded(encode(0x1ffffffffff), [0xff, 0xff, 0xff, 0xff, 0xff, 0x3f])\n        checkEncoded(encode(-0x20000000000), [0x80, 0x80, 0x80, 0x80, 0x80, 0x40])\n        checkEncoded(encode(0x20000000000), [0x80, 0x80, 0x80, 0x80, 0x80, 0xc0, 0x00])\n        checkEncoded(encode(-0x20000000001), [0xff, 0xff, 0xff, 0xff, 0xff, 0xbf, 0x7f])\n        checkEncoded(encode(0xffffffffffff), [0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x3f])\n        checkEncoded(encode(-0x1000000000000), [0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x40])\n        checkEncoded(encode(0x1000000000000), [0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0xc0, 0x00])\n        checkEncoded(encode(-0x1000000000001), [0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xbf, 0x7f])\n        checkEncoded(encode(0x123456789abcde), [0xde, 0xf9, 0xea, 0xc4, 0xe7, 0x8a, 0x8d, 0x09])\n        checkEncoded(encode(Number.MAX_SAFE_INTEGER), [0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x0f])\n        checkEncoded(encode(Number.MIN_SAFE_INTEGER), [0x81, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x70])\n      })\n\n      it('should round-trip signed integers', () => {\n        const examples = [\n          0, 1, -1, 0x3f, -0x40, 0x40, -0x41, 0x1fff, -0x2000, 0x2000, -0x2001, 0xfffff,\n          -0x100000, 0x100000, -0x100001, 0x7ffffff, -0x8000000, 0x8000000, -0x8000001,\n          0x7fffffff, 0x80000000, -0x80000000, -0x80000001, 0x3ffffffff, -0x400000000,\n          0x400000000, -0x400000001, 0x1ffffffffff, -0x20000000000, 0x20000000000,\n          -0x20000000001, 0xffffffffffff, -0x1000000000000, 0x1000000000000,\n          -0x1000000000001, Number.MAX_SAFE_INTEGER, Number.MIN_SAFE_INTEGER,\n          0x123, -0x123, 0x1234, -0x1234, 0x12345, -0x12345, 0x123456, -0x123456,\n          0x1234567, -0x1234567, 0x12345678, -0x12345678, 0x123456789, -0x123456789,\n          0x123456789a, -0x123456789a, 0x123456789ab, -0x123456789ab, 0x123456789abc,\n          -0x123456789abc, 0x123456789abcd, -0x123456789abcd, 0x123456789abcde,\n          -0x123456789abcde\n        ]\n        for (let value of examples) {\n          const encoder = new Encoder()\n          encoder.appendInt53(value)\n          const decoder = new Decoder(encoder.buffer)\n          assert.strictEqual(decoder.readInt53(), value)\n          assert.strictEqual(decoder.done, true)\n        }\n      })\n\n      it('should not encode values that are out of range', () => {\n        assert.throws(() => { new Encoder().appendUint53(Number.MAX_SAFE_INTEGER + 1) }, /out of range/)\n        assert.throws(() => { new Encoder().appendUint53(-1) }, /out of range/)\n        assert.throws(() => { new Encoder().appendUint53(-0x80000000) }, /out of range/)\n        assert.throws(() => { new Encoder().appendUint53(Number.MIN_SAFE_INTEGER) }, /out of range/)\n        assert.throws(() => { new Encoder().appendUint53(Number.NEGATIVE_INFINITY) }, /not an integer/)\n        assert.throws(() => { new Encoder().appendUint53(Number.NaN) }, /not an integer/)\n        assert.throws(() => { new Encoder().appendUint53(Math.PI) }, /not an integer/)\n        assert.throws(() => { new Encoder().appendInt53(Number.MAX_SAFE_INTEGER + 1) }, /out of range/)\n        assert.throws(() => { new Encoder().appendInt53(Number.MIN_SAFE_INTEGER - 1) }, /out of range/)\n        assert.throws(() => { new Encoder().appendInt53(Number.NEGATIVE_INFINITY) }, /not an integer/)\n        assert.throws(() => { new Encoder().appendInt53(Number.NaN) }, /not an integer/)\n        assert.throws(() => { new Encoder().appendInt53(Math.PI) }, /not an integer/)\n      })\n\n      it('should not decode values that are out of range', () => {\n        assert.throws(() => { new Decoder(new Uint8Array([0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x10])).readUint53() }, /out of range/)\n        assert.throws(() => { new Decoder(new Uint8Array([0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x10])).readInt53() }, /out of range/)\n        assert.throws(() => { new Decoder(new Uint8Array([0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x70])).readInt53() }, /out of range/)\n        assert.throws(() => { new Decoder(new Uint8Array([0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x6f])).readInt53() }, /out of range/)\n        assert.throws(() => { new Decoder(new Uint8Array([0x80, 0x80])).readUint53() }, /incomplete number/)\n        assert.throws(() => { new Decoder(new Uint8Array([0x80, 0x80])).readInt53() }, /incomplete number/)\n      })\n    })\n\n    describe('64-bit LEB128 encoding', () => {\n      it('should encode unsigned integers', () => {\n        function encode(high32, low32) {\n          const encoder = new Encoder()\n          encoder.appendUint64(high32, low32)\n          return encoder\n        }\n        checkEncoded(encode(0, 0), [0])\n        checkEncoded(encode(0, 0x7f), [0x7f])\n        checkEncoded(encode(0, 0x80), [0x80, 0x01])\n        checkEncoded(encode(0, 0x3fff), [0xff, 0x7f])\n        checkEncoded(encode(0, 0x4000), [0x80, 0x80, 0x01])\n        checkEncoded(encode(0, 0x1fffff), [0xff, 0xff, 0x7f])\n        checkEncoded(encode(0, 0x200000), [0x80, 0x80, 0x80, 0x01])\n        checkEncoded(encode(0, 0xfffffff), [0xff, 0xff, 0xff, 0x7f])\n        checkEncoded(encode(0, 0x10000000), [0x80, 0x80, 0x80, 0x80, 0x01])\n        checkEncoded(encode(0, 0xffffffff), [0xff, 0xff, 0xff, 0xff, 0x0f])\n        checkEncoded(encode(0x1, 0x00000000), [0x80, 0x80, 0x80, 0x80, 0x10])\n        checkEncoded(encode(0x7, 0xffffffff), [0xff, 0xff, 0xff, 0xff, 0x7f])\n        checkEncoded(encode(0x8, 0x00000000), [0x80, 0x80, 0x80, 0x80, 0x80, 0x01])\n        checkEncoded(encode(0x3ff, 0xffffffff), [0xff, 0xff, 0xff, 0xff, 0xff, 0x7f])\n        checkEncoded(encode(0x400, 0x00000000), [0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x01])\n        checkEncoded(encode(0x1ffff, 0xffffffff), [0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x7f])\n        checkEncoded(encode(0x20000, 0x00000000), [0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x01])\n        checkEncoded(encode(0xffffff, 0xffffffff), [0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x7f])\n        checkEncoded(encode(0x1000000, 0x00000000), [0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x01])\n        checkEncoded(encode(0xffffffff, 0xffffffff), [0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x01])\n      })\n\n      it('should round-trip unsigned integers', () => {\n        const examples = [\n          {high32: 0, low32: 0}, {high32: 0, low32: 0x7f}, {high32: 0, low32: 0x80},\n          {high32: 0, low32: 0x3fff}, {high32: 0, low32: 0x4000}, {high32: 0, low32: 0x1fffff},\n          {high32: 0, low32: 0x200000}, {high32: 0, low32: 0xfffffff},\n          {high32: 0, low32: 0x10000000}, {high32: 0, low32: 0xffffffff},\n          {high32: 0x1, low32: 0x00000000}, {high32: 0x7, low32: 0xffffffff},\n          {high32: 0x8, low32: 0x00000000}, {high32: 0x3ff, low32: 0xffffffff},\n          {high32: 0x400, low32: 0x00000000}, {high32: 0x1ffff, low32: 0xffffffff},\n          {high32: 0x20000, low32: 0x00000000}, {high32: 0xffffff, low32: 0xffffffff},\n          {high32: 0x1000000, low32: 0x00000000}, {high32: 0xffffffff, low32: 0xffffffff},\n          {high32: 0, low32: 0x123}, {high32: 0, low32: 0x1234}, {high32: 0, low32: 0x12345},\n          {high32: 0, low32: 0x123456}, {high32: 0, low32: 0x1234567},\n          {high32: 0, low32: 0x12345678}, {high32: 0x9, low32: 0x12345678},\n          {high32: 0x98, low32: 0x12345678}, {high32: 0x987, low32: 0x12345678},\n          {high32: 0x9876, low32: 0x12345678}, {high32: 0x98765, low32: 0x12345678},\n          {high32: 0x987654, low32: 0x12345678}, {high32: 0x9876543, low32: 0x12345678},\n          {high32: 0x98765432, low32: 0x12345678}\n        ]\n        for (let value of examples) {\n          const encoder = new Encoder()\n          encoder.appendUint64(value.high32, value.low32)\n          const decoder = new Decoder(encoder.buffer)\n          assert.deepStrictEqual(decoder.readUint64(), value)\n          assert.strictEqual(decoder.done, true)\n        }\n      })\n\n      it('should encode signed integers', () => {\n        function encode(high32, low32) {\n          const encoder = new Encoder()\n          encoder.appendInt64(high32, low32)\n          return encoder\n        }\n        checkEncoded(encode(0, 0), [0])\n        checkEncoded(encode(0, 1), [1])\n        checkEncoded(encode(-1, -1), [0x7f])\n        checkEncoded(encode(0, 0x3f), [0x3f])\n        checkEncoded(encode(-1, -0x40), [0x40])\n        checkEncoded(encode(0, 0x40), [0xc0, 0x00])\n        checkEncoded(encode(-1, -0x41), [0xbf, 0x7f])\n        checkEncoded(encode(0, 0x1fff), [0xff, 0x3f])\n        checkEncoded(encode(-1, -0x2000), [0x80, 0x40])\n        checkEncoded(encode(0, 0x2000), [0x80, 0xc0, 0x00])\n        checkEncoded(encode(-1, -0x2001), [0xff, 0xbf, 0x7f])\n        checkEncoded(encode(0, 0xfffff), [0xff, 0xff, 0x3f])\n        checkEncoded(encode(-1, -0x100000), [0x80, 0x80, 0x40])\n        checkEncoded(encode(0, 0x100000), [0x80, 0x80, 0xc0, 0x00])\n        checkEncoded(encode(-1, -0x100001), [0xff, 0xff, 0xbf, 0x7f])\n        checkEncoded(encode(0, 0x7ffffff), [0xff, 0xff, 0xff, 0x3f])\n        checkEncoded(encode(-1, -0x8000000), [0x80, 0x80, 0x80, 0x40])\n        checkEncoded(encode(0, 0x8000000), [0x80, 0x80, 0x80, 0xc0, 0x00])\n        checkEncoded(encode(-1, -0x8000001), [0xff, 0xff, 0xff, 0xbf, 0x7f])\n        checkEncoded(encode(0, 0x7fffffff), [0xff, 0xff, 0xff, 0xff, 0x07])\n        checkEncoded(encode(0, 0x80000000), [0x80, 0x80, 0x80, 0x80, 0x08])\n        checkEncoded(encode(0, 0xffffffff), [0xff, 0xff, 0xff, 0xff, 0x0f])\n        checkEncoded(encode(-1, -0x80000000), [0x80, 0x80, 0x80, 0x80, 0x78])\n        checkEncoded(encode(-1, 0x7fffffff), [0xff, 0xff, 0xff, 0xff, 0x77])\n        checkEncoded(encode(-1, 1), [0x81, 0x80, 0x80, 0x80, 0x70])\n        checkEncoded(encode(-1, 0), [0x80, 0x80, 0x80, 0x80, 0x70])\n        checkEncoded(encode(-2, -1), [0xff, 0xff, 0xff, 0xff, 0x6f])\n        checkEncoded(encode(3, 0xffffffff), [0xff, 0xff, 0xff, 0xff, 0x3f])\n        checkEncoded(encode(-4, 0), [0x80, 0x80, 0x80, 0x80, 0x40])\n        checkEncoded(encode(4, 0), [0x80, 0x80, 0x80, 0x80, 0xc0, 0x00])\n        checkEncoded(encode(-5, -1), [0xff, 0xff, 0xff, 0xff, 0xbf, 0x7f])\n        checkEncoded(encode(0x1ff, 0xffffffff), [0xff, 0xff, 0xff, 0xff, 0xff, 0x3f])\n        checkEncoded(encode(-0x200, 0), [0x80, 0x80, 0x80, 0x80, 0x80, 0x40])\n        checkEncoded(encode(0x200, 0), [0x80, 0x80, 0x80, 0x80, 0x80, 0xc0, 0x00])\n        checkEncoded(encode(-0x201, -1), [0xff, 0xff, 0xff, 0xff, 0xff, 0xbf, 0x7f])\n        checkEncoded(encode(0xffff, 0xffffffff), [0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x3f])\n        checkEncoded(encode(-0x10000, 0), [0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x40])\n        checkEncoded(encode(0x10000, 0), [0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0xc0, 0x00])\n        checkEncoded(encode(-0x10001, -1), [0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xbf, 0x7f])\n        checkEncoded(encode(0x7fffff, 0xffffffff), [0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x3f])\n        checkEncoded(encode(-0x800000, 0), [0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x40])\n        checkEncoded(encode(0x800000, 0), [0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0xc0, 0x00])\n        checkEncoded(encode(-0x800001, -1), [0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xbf, 0x7f])\n        checkEncoded(encode(0x3fffffff, 0xffffffff), [0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x3f])\n        checkEncoded(encode(-0x40000000, 0), [0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x40])\n        checkEncoded(encode(0x40000000, 0), [0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0xc0, 0x00])\n        checkEncoded(encode(-0x40000001, -1), [0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xbf, 0x7f])\n        checkEncoded(encode(0x7fffffff, 0xffffffff), [0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00])\n        checkEncoded(encode(-0x80000000, 0), [0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x7f])\n      })\n\n      it('should round-trip signed integers', () => {\n        const examples = [\n          {high32: 0, low32: 0}, {high32: 0, low32: 1}, {high32: -1, low32: -1 >>> 0},\n          {high32: 0, low32: 0x3f}, {high32: -1, low32: -0x40 >>> 0}, {high32: 0, low32: 0x40},\n          {high32: -1, low32: -0x41 >>> 0}, {high32: 0, low32: 0x1fff}, {high32: -1, low32: -0x2000 >>> 0},\n          {high32: 0, low32: 0x2000}, {high32: -1, low32: -0x2001 >>> 0},\n          {high32: 0, low32: 0xfffff}, {high32: -1, low32: -0x100000 >>> 0},\n          {high32: 0, low32: 0x100000}, {high32: -1, low32: -0x100001 >>> 0},\n          {high32: 0, low32: 0x7ffffff}, {high32: -1, low32: -0x8000000 >>> 0},\n          {high32: 0, low32: 0x8000000}, {high32: -1, low32: -0x8000001 >>> 0},\n          {high32: 0, low32: 0x7fffffff}, {high32: 0, low32: 0x80000000},\n          {high32: 0, low32: 0xffffffff}, {high32: -1, low32: -0x80000000 >>> 0},\n          {high32: -1, low32: 0x7fffffff}, {high32: -1, low32: 1}, {high32: -1, low32: 0},\n          {high32: -2, low32: -1 >>> 0}, {high32: 3, low32: 0xffffffff}, {high32: -4, low32: 0},\n          {high32: 4, low32: 0}, {high32: -5, low32: -1 >>> 0}, {high32: 0x1ff, low32: 0xffffffff},\n          {high32: -0x200, low32: 0}, {high32: 0x200, low32: 0}, {high32: -0x201, low32: -1 >>> 0},\n          {high32: 0xffff, low32: 0xffffffff}, {high32: -0x10000, low32: 0},\n          {high32: 0x10000, low32: 0}, {high32: -0x10001, low32: -1 >>> 0},\n          {high32: 0x7fffff, low32: 0xffffffff}, {high32: -0x800000, low32: 0},\n          {high32: 0x800000, low32: 0}, {high32: -0x800001, low32: -1 >>> 0},\n          {high32: 0x3fffffff, low32: 0xffffffff}, {high32: -0x40000000, low32: 0},\n          {high32: 0x40000000, low32: 0}, {high32: -0x40000001, low32: -1 >>> 0},\n          {high32: 0x7fffffff, low32: 0xffffffff}, {high32: -0x80000000, low32: 0},\n          {high32: 0, low32: 0x123}, {high32: -1, low32: -0x123 >>> 0},\n          {high32: 0, low32: 0x1234}, {high32: -1, low32: -0x1234 >>> 0},\n          {high32: 0, low32: 0x12345}, {high32: -1, low32: -0x12345 >>> 0},\n          {high32: 0, low32: 0x123456}, {high32: -1, low32: -0x123456 >>> 0},\n          {high32: 0, low32: 0x1234567}, {high32: -1, low32: -0x1234567 >>> 0},\n          {high32: 0, low32: 0x12345678}, {high32: -1, low32: -0x12345678 >>> 0},\n          {high32: 0x9, low32: 0x12345678}, {high32: -0x9, low32: -0x12345678 >>> 0},\n          {high32: 0x98, low32: 0x12345678}, {high32: -0x98, low32: -0x12345678 >>> 0},\n          {high32: 0x987, low32: 0x12345678}, {high32: -0x987, low32: -0x12345678 >>> 0},\n          {high32: 0x9876, low32: 0x12345678}, {high32: -0x9876, low32: -0x12345678 >>> 0},\n          {high32: 0x98765, low32: 0x12345678}, {high32: -0x98765, low32: -0x12345678 >>> 0},\n          {high32: 0x987654, low32: 0x12345678}, {high32: -0x987654, low32: -0x12345678 >>> 0},\n          {high32: 0x9876543, low32: 0x12345678}, {high32: -0x9876543, low32: -0x12345678 >>> 0},\n          {high32: 0x78765432, low32: 0x12345678}, {high32: -0x78765432, low32: -0x12345678 >>> 0}\n        ]\n        for (let value of examples) {\n          const encoder = new Encoder()\n          encoder.appendInt64(value.high32, value.low32)\n          const decoder = new Decoder(encoder.buffer)\n          assert.deepStrictEqual(decoder.readInt64(), value)\n          assert.strictEqual(decoder.done, true)\n        }\n      })\n\n      it('should not encode values that are out of range', () => {\n        assert.throws(() => { new Encoder().appendUint64(0, 0x100000000) }, /out of range/)\n        assert.throws(() => { new Encoder().appendUint64(0x100000000, 0) }, /out of range/)\n        assert.throws(() => { new Encoder().appendUint64(0, -1) }, /out of range/)\n        assert.throws(() => { new Encoder().appendUint64(-1, 0) }, /out of range/)\n        assert.throws(() => { new Encoder().appendUint64(123, Number.NaN) }, /not an integer/)\n        assert.throws(() => { new Encoder().appendUint64(123, Math.PI) }, /not an integer/)\n        assert.throws(() => { new Encoder().appendInt64(0, 0x100000000) }, /out of range/)\n        assert.throws(() => { new Encoder().appendInt64(0x80000000, 0) }, /out of range/)\n        assert.throws(() => { new Encoder().appendInt64(0, -0x80000001) }, /out of range/)\n        assert.throws(() => { new Encoder().appendInt64(-0x80000001, 0) }, /out of range/)\n        assert.throws(() => { new Encoder().appendInt64(123, Number.NaN) }, /not an integer/)\n        assert.throws(() => { new Encoder().appendInt64(123, Math.PI) }, /not an integer/)\n      })\n\n      it('should not decode values that are out of range', () => {\n        assert.throws(() => {\n          new Decoder(new Uint8Array([0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x00])).readUint64()\n        }, /out of range/)\n        assert.throws(() => {\n          new Decoder(new Uint8Array([0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x00])).readInt64()\n        }, /out of range/)\n        assert.throws(() => {\n          new Decoder(new Uint8Array([0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x02])).readUint64()\n        }, /out of range/)\n        assert.throws(() => {\n          new Decoder(new Uint8Array([0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x01])).readInt64()\n        }, /out of range/)\n        assert.throws(() => {\n          new Decoder(new Uint8Array([0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x7e])).readInt64()\n        }, /out of range/)\n        assert.throws(() => { new Decoder(new Uint8Array([0x80, 0x80])).readUint64() }, /incomplete number/)\n        assert.throws(() => { new Decoder(new Uint8Array([0x80, 0x80])).readInt64() }, /incomplete number/)\n      })\n    })\n\n    describe('UTF-8 encoding', () => {\n      it('should encode strings', () => {\n        checkEncoded(new Encoder().appendPrefixedString(''), [0])\n        checkEncoded(new Encoder().appendPrefixedString('a'), [1, 0x61])\n        checkEncoded(new Encoder().appendPrefixedString('Oh là là'), [10, 79, 104, 32, 108, 195, 160, 32, 108, 195, 160])\n        checkEncoded(new Encoder().appendPrefixedString('😄'), [4, 0xf0, 0x9f, 0x98, 0x84])\n      })\n\n      it('should round-trip strings', () => {\n        assert.strictEqual(new Decoder(new Encoder().appendPrefixedString('').buffer).readPrefixedString(), '')\n        assert.strictEqual(new Decoder(new Encoder().appendPrefixedString('a').buffer).readPrefixedString(), 'a')\n        assert.strictEqual(new Decoder(new Encoder().appendPrefixedString('Oh là là').buffer).readPrefixedString(), 'Oh là là')\n        assert.strictEqual(new Decoder(new Encoder().appendPrefixedString('😄').buffer).readPrefixedString(), '😄')\n      })\n\n      it('should encode multiple strings', () => {\n        const encoder = new Encoder()\n        encoder.appendPrefixedString('one')\n        encoder.appendPrefixedString('two')\n        encoder.appendPrefixedString('three')\n        const decoder = new Decoder(encoder.buffer)\n        assert.strictEqual(decoder.readPrefixedString(), 'one')\n        assert.strictEqual(decoder.readPrefixedString(), 'two')\n        assert.strictEqual(decoder.readPrefixedString(), 'three')\n      })\n    })\n\n    describe('hex encoding', () => {\n      it('should encode hex strings', () => {\n        checkEncoded(new Encoder().appendHexString(''), [0])\n        checkEncoded(new Encoder().appendHexString('00'), [1, 0])\n        checkEncoded(new Encoder().appendHexString('0123'), [2, 1, 0x23])\n        checkEncoded(new Encoder().appendHexString('fedcba9876543210'), [8, 0xfe, 0xdc, 0xba, 0x98, 0x76, 0x54, 0x32, 0x10])\n      })\n\n      it('should round-trip strings', () => {\n        assert.strictEqual(new Decoder(new Encoder().appendHexString('').buffer).readHexString(), '')\n        assert.strictEqual(new Decoder(new Encoder().appendHexString('00').buffer).readHexString(), '00')\n        assert.strictEqual(new Decoder(new Encoder().appendHexString('0123').buffer).readHexString(), '0123')\n        assert.strictEqual(new Decoder(new Encoder().appendHexString('fedcba9876543210').buffer).readHexString(), 'fedcba9876543210')\n      })\n\n      it('should not allow malformed hex strings', () => {\n        assert.throws(() => { new Encoder().appendHexString(0x1234) }, /value is not a string/)\n        assert.throws(() => { new Encoder().appendHexString('abcd-ef') }, /value is not hexadecimal/)\n        assert.throws(() => { new Encoder().appendHexString('0') }, /value is not hexadecimal/)\n        assert.throws(() => { new Encoder().appendHexString('ABCD') }, /value is not hexadecimal/)\n        assert.throws(() => { new Encoder().appendHexString('zz') }, /value is not hexadecimal/)\n      })\n    })\n  })\n\n  describe('RLEEncoder and RLEDecoder', () => {\n    function encodeRLE(type, values) {\n      const encoder = new RLEEncoder(type)\n      for (let value of values) encoder.appendValue(value)\n      return encoder.buffer\n    }\n\n    function decodeRLE(type, buffer) {\n      if (Array.isArray(buffer)) buffer = new Uint8Array(buffer)\n      const decoder = new RLEDecoder(type, buffer), values = []\n      while (!decoder.done) values.push(decoder.readValue())\n      return values\n    }\n\n    it('should encode sequences without nulls', () => {\n      checkEncoded(encodeRLE('uint', []), [])\n      checkEncoded(encodeRLE('uint', [1, 2, 3]), [0x7d, 1, 2, 3])\n      checkEncoded(encodeRLE('uint', [0, 1, 2, 2, 3]), [0x7e, 0, 1, 2, 2, 0x7f, 3])\n      checkEncoded(encodeRLE('uint', [1, 1, 1, 1, 1, 1]), [6, 1])\n      checkEncoded(encodeRLE('uint', [1, 1, 1, 4, 4, 4]), [3, 1, 3, 4])\n      checkEncoded(encodeRLE('uint', [0xff]), [0x7f, 0xff, 0x01])\n      checkEncoded(encodeRLE('int', [-0x40]), [0x7f, 0x40])\n    })\n\n    it('should encode sequences containing nulls', () => {\n      checkEncoded(encodeRLE('uint', [null, 1]), [0, 1, 0x7f, 1])\n      checkEncoded(encodeRLE('uint', [1, null]), [0x7f, 1, 0, 1])\n      checkEncoded(encodeRLE('uint', [1, 1, 1, null]), [3, 1, 0, 1])\n      checkEncoded(encodeRLE('uint', [null, null, null, 3, 4, 5, null]), [0, 3, 0x7d, 3, 4, 5, 0, 1])\n      checkEncoded(encodeRLE('uint', [null, null, null, 9, 9, 9]), [0, 3, 3, 9])\n      checkEncoded(encodeRLE('uint', [1, 1, 1, 1, 1, null, null, null, 1]), [5, 1, 0, 3, 0x7f, 1])\n    })\n\n    it('should round-trip sequences without nulls', () => {\n      assert.deepStrictEqual(decodeRLE('uint', encodeRLE('uint', [])), [])\n      assert.deepStrictEqual(decodeRLE('uint', encodeRLE('uint', [1, 2, 3])), [1, 2, 3])\n      assert.deepStrictEqual(decodeRLE('uint', encodeRLE('uint', [0, 1, 2, 2, 3])), [0, 1, 2, 2, 3])\n      assert.deepStrictEqual(decodeRLE('uint', encodeRLE('uint', [1, 1, 1, 1, 1, 1])), [1, 1, 1, 1, 1, 1])\n      assert.deepStrictEqual(decodeRLE('uint', encodeRLE('uint', [1, 1, 1, 4, 4, 4])), [1, 1, 1, 4, 4, 4])\n      assert.deepStrictEqual(decodeRLE('uint', encodeRLE('uint', [0xff])), [0xff])\n      assert.deepStrictEqual(decodeRLE('int', encodeRLE('int', [-0x40])), [-0x40])\n    })\n\n    it('should round-trip sequences containing nulls', () => {\n      assert.deepStrictEqual(decodeRLE('uint', encodeRLE('uint', [null, 1])), [null, 1])\n      assert.deepStrictEqual(decodeRLE('uint', encodeRLE('uint', [1, null])), [1, null])\n      assert.deepStrictEqual(decodeRLE('uint', encodeRLE('uint', [1, 1, 1, null])), [1, 1, 1, null])\n      assert.deepStrictEqual(decodeRLE('uint', encodeRLE('uint', [null, null, null, 3, 4, 5, null])), [null, null, null, 3, 4, 5, null])\n      assert.deepStrictEqual(decodeRLE('uint', encodeRLE('uint', [null, null, null, 9, 9, 9])), [null, null, null, 9, 9, 9])\n      assert.deepStrictEqual(decodeRLE('uint', encodeRLE('uint', [1, 1, 1, 1, 1, null, null, null, 1])), [1, 1, 1, 1, 1, null, null, null, 1])\n    })\n\n    it('should support encoding string values', () => {\n      checkEncoded(encodeRLE('utf8', ['a']), [0x7f, 1, 0x61])\n      checkEncoded(encodeRLE('utf8', ['a', 'b', 'c', 'd']), [0x7c, 1, 0x61, 1, 0x62, 1, 0x63, 1, 0x64])\n      checkEncoded(encodeRLE('utf8', ['a', 'a', 'a', 'a']), [4, 1, 0x61])\n      checkEncoded(encodeRLE('utf8', ['a', 'a', null, null, 'a', 'a']), [2, 1, 0x61, 0, 2, 2, 1, 0x61])\n      checkEncoded(encodeRLE('utf8', [null, null, null, null, 'abc']), [0, 4, 0x7f, 3, 0x61, 0x62, 0x63])\n    })\n\n    it('should round-trip sequences of string values', () => {\n      assert.deepStrictEqual(decodeRLE('utf8', encodeRLE('utf8', ['a'])), ['a'])\n      assert.deepStrictEqual(decodeRLE('utf8', encodeRLE('utf8', ['a', 'b', 'c', 'd'])), ['a', 'b', 'c', 'd'])\n      assert.deepStrictEqual(decodeRLE('utf8', encodeRLE('utf8', ['a', 'a', 'a', 'a'])), ['a', 'a', 'a', 'a'])\n      assert.deepStrictEqual(decodeRLE('utf8', encodeRLE('utf8', ['a', 'a', null, null, 'a', 'a'])), ['a', 'a', null, null, 'a', 'a'])\n      assert.deepStrictEqual(decodeRLE('utf8', encodeRLE('utf8', [null, null, null, null, 'abc'])), [null, null, null, null, 'abc'])\n    })\n\n    it('should allow repetition counts to be specified', () => {\n      let e\n      e = new RLEEncoder('uint'); e.appendValue(3, 0); checkEncoded(e, [])\n      e = new RLEEncoder('uint'); e.appendValue(3, 10); checkEncoded(e, [10, 3])\n      e = new RLEEncoder('uint'); e.appendValue(3, 10); e.appendValue(3, 10); checkEncoded(e, [20, 3])\n      e = new RLEEncoder('uint'); e.appendValue(3, 10); e.appendValue(4, 10); checkEncoded(e, [10, 3, 10, 4])\n      e = new RLEEncoder('uint'); e.appendValue(3, 10); e.appendValue(null, 10); checkEncoded(e, [10, 3, 0, 10])\n      e = new RLEEncoder('uint'); e.appendValue(1); e.appendValue(1, 2); checkEncoded(e, [3, 1])\n      e = new RLEEncoder('uint'); e.appendValue(1); e.appendValue(2, 3); checkEncoded(e, [0x7f, 1, 3, 2])\n      e = new RLEEncoder('uint'); e.appendValue(1); e.appendValue(2); e.appendValue(3, 3); checkEncoded(e, [0x7e, 1, 2, 3, 3])\n      e = new RLEEncoder('uint'); e.appendValue(null); e.appendValue(3, 3); checkEncoded(e, [0, 1, 3, 3])\n      e = new RLEEncoder('uint'); e.appendValue(null); e.appendValue(null, 3); e.appendValue(1); checkEncoded(e, [0, 4, 0x7f, 1])\n    })\n\n    it('should return an empty buffer if the values are only nulls', () => {\n      assert.strictEqual(encodeRLE('uint', []).byteLength, 0)\n      assert.strictEqual(encodeRLE('uint', [null]).byteLength, 0)\n      assert.strictEqual(encodeRLE('uint', [null, null, null, null]).byteLength, 0)\n    })\n\n    it('should strictly enforce canonical encoded form', () => {\n      assert.throws(() => { decodeRLE('int', [1, 1]) }, /Repetition count of 1 is not allowed/)\n      assert.throws(() => { decodeRLE('int', [2, 1, 2, 1]) }, /Successive repetitions with the same value/)\n      assert.throws(() => { decodeRLE('int', [0, 1, 0, 2]) }, /Successive null runs are not allowed/)\n      assert.throws(() => { decodeRLE('int', [0, 0]) }, /Zero-length null runs are not allowed/)\n      assert.throws(() => { decodeRLE('int', [0x7f, 1, 0x7f, 2]) }, /Successive literals are not allowed/)\n      assert.throws(() => { decodeRLE('int', [0x7d, 1, 2, 2]) }, /Repetition of values is not allowed/)\n      assert.throws(() => { decodeRLE('int', [2, 0, 0x7e, 0, 1]) }, /Repetition of values is not allowed/)\n      assert.throws(() => { decodeRLE('int', [0x7e, 1, 2, 2, 2]) }, /Successive repetitions with the same value/)\n    })\n\n    it('should allow skipping string values', () => {\n      const example = [null, null, null, 'a', 'a', 'a', 'b', 'c', 'd', 'e']\n      const encoded = encodeRLE('utf8', example)\n      for (let skipNum = 0; skipNum < example.length; skipNum++) {\n        const decoder = new RLEDecoder('utf8', encoded), values = []\n        decoder.skipValues(skipNum)\n        while (!decoder.done) values.push(decoder.readValue())\n        assert.deepStrictEqual(values, example.slice(skipNum), `skipping ${skipNum} values failed`)\n      }\n    })\n\n    it('should allow skipping integer values', () => {\n      const example = [null, null, null, 1, 1, 1, 2, 3, 4, 5]\n      const encoded = encodeRLE('uint', example)\n      for (let skipNum = 0; skipNum < example.length; skipNum++) {\n        const decoder = new RLEDecoder('uint', encoded), values = []\n        decoder.skipValues(skipNum)\n        while (!decoder.done) values.push(decoder.readValue())\n        assert.deepStrictEqual(values, example.slice(skipNum), `skipping ${skipNum} values failed`)\n      }\n    })\n\n    describe('copying from a decoder', () => {\n      function doCopy(input1, input2, options = {}) {\n        let encoder1 = input1\n        if (Array.isArray(input1)) {\n          encoder1 = new RLEEncoder('uint')\n          for (let value of input1) encoder1.appendValue(value)\n        }\n\n        const encoder2 = new RLEEncoder('uint')\n        for (let value of input2) encoder2.appendValue(value)\n        const decoder2 = new RLEDecoder('uint', encoder2.buffer)\n        if (options.skip) decoder2.skipValues(options.skip)\n        encoder1.copyFrom(decoder2, options)\n        return encoder1\n      }\n\n      it('should copy a sequence', () => {\n        checkEncoded(doCopy([], [0, 1, 2]), [0x7d, 0, 1, 2])\n        checkEncoded(doCopy([0, 1, 2], []), [0x7d, 0, 1, 2])\n        checkEncoded(doCopy([0, 1, 2], [3, 4, 5, 6]), [0x79, 0, 1, 2, 3, 4, 5, 6])\n        checkEncoded(doCopy([0, 1], [2, 3, 4, 4, 4]), [0x7c, 0, 1, 2, 3, 3, 4])\n        checkEncoded(doCopy([0, 1, 2], [3, 4, 4, 4]), [0x7c, 0, 1, 2, 3, 3, 4])\n        checkEncoded(doCopy([0, 1, 2], [3, 3, 3, 4, 4, 4]), [0x7d, 0, 1, 2, 3, 3, 3, 4])\n        checkEncoded(doCopy([0, 1, 2], [null, null, 4, 4, 4]), [0x7d, 0, 1, 2, 0, 2, 3, 4])\n        checkEncoded(doCopy([0, 1, 2], [3, 4, 4, null, null]), [0x7c, 0, 1, 2, 3, 2, 4, 0, 2])\n        checkEncoded(doCopy([0, 1, 2], [3, 4, 4, 5, 6, 6]), [0x7c, 0, 1, 2, 3, 2, 4, 0x7f, 5, 2, 6])\n        checkEncoded(doCopy([0, 1, 2], [2, 2, 3, 3, 4, 5, 6]), [0x7e, 0, 1, 3, 2, 2, 3, 0x7d, 4, 5, 6])\n        checkEncoded(doCopy([0, 0, 0], [0, 0, 0]), [6, 0])\n        checkEncoded(doCopy([0, 0, 0], [0, 1, 1]), [4, 0, 2, 1])\n        checkEncoded(doCopy([0, 0, 0], [1, 2, 2]), [3, 0, 0x7f, 1, 2, 2])\n        checkEncoded(doCopy([0, 0, 0], [1, 2, 3]), [3, 0, 0x7d, 1, 2, 3])\n        checkEncoded(doCopy([0, 0, 0], [null, null, 2, 2]), [3, 0, 0, 2, 2, 2])\n        checkEncoded(doCopy([0, 0, 0], [null, 0, 0, 0]), [3, 0, 0, 1, 3, 0])\n        checkEncoded(doCopy([0, 0, null], [null, 0, 0]), [2, 0, 0, 2, 2, 0])\n        checkEncoded(doCopy([0, 0, null], [0, 0, 0]), [2, 0, 0, 1, 3, 0])\n        checkEncoded(doCopy([0, 0, null], [1, 2, 3]), [2, 0, 0, 1, 0x7d, 1, 2, 3])\n      })\n\n      it('should copy multiple sequences', () => {\n        checkEncoded(doCopy(doCopy([0, 0, 1], [1, 2]), [2, 3]), [2, 0, 2, 1, 2, 2, 0x7f, 3])\n        checkEncoded(doCopy(doCopy([0], [0, 0, 1, 1, 2]), [2, 3, 3, 4]), [3, 0, 2, 1, 2, 2, 2, 3, 0x7f, 4])\n        checkEncoded(doCopy(doCopy([0, 1, 2], [3, 4]), [5, 6]), [0x79, 0, 1, 2, 3, 4, 5, 6])\n        checkEncoded(doCopy(doCopy([0, 0, 0], [0, 0, 1, 1]), [1, 1]), [5, 0, 4, 1])\n        checkEncoded(doCopy(doCopy([0, null], [null, 1, null]), [null, 2]), [0x7f, 0, 0, 2, 0x7f, 1, 0, 2, 0x7f, 2])\n      })\n\n      it('should copy a sub-sequence', () => {\n        checkEncoded(doCopy([0, 1, 2], [3, 4, 5, 6], {skip: 0, count: 0}), [0x7d, 0, 1, 2])\n        checkEncoded(doCopy([0, 1, 2], [3, 4, 5, 6], {skip: 0, count: 1}), [0x7c, 0, 1, 2, 3])\n        checkEncoded(doCopy([0, 1, 2], [3, 4, 5, 6], {skip: 0, count: 2}), [0x7b, 0, 1, 2, 3, 4])\n        checkEncoded(doCopy([0, 1, 2], [3, 4, 5, 6], {skip: 0, count: 4}), [0x79, 0, 1, 2, 3, 4, 5, 6])\n        checkEncoded(doCopy([0, 1, 2], [3, 4, 5, 6], {skip: 1, count: 1}), [0x7c, 0, 1, 2, 4])\n        checkEncoded(doCopy([0, 1, 2], [3, 4, 5, 6], {skip: 1, count: 2}), [0x7b, 0, 1, 2, 4, 5])\n        checkEncoded(doCopy([0, 1, 2], [3, 3, 3, 3], {skip: 0, count: 2}), [0x7d, 0, 1, 2, 2, 3])\n        checkEncoded(doCopy([0, 0, 0], [0, 0, 0, 0], {skip: 0, count: 2}), [5, 0])\n        checkEncoded(doCopy([0, 0], [0, 0, 1, 1, 1], {skip: 0, count: 4}), [4, 0, 2, 1])\n        checkEncoded(doCopy([0, 0], [0, 0, 1, 1, 2, 2], {skip: 1, count: 4}), [3, 0, 2, 1, 0x7f, 2])\n        checkEncoded(doCopy([0, 0], [1, 1, 2, 3, 4, 5], {skip: 0, count: 3}), [2, 0, 2, 1, 0x7f, 2])\n        checkEncoded(doCopy([null], [null, 1, 1, null], {skip: 0, count: 2}), [0, 2, 0x7f, 1])\n        checkEncoded(doCopy([null], [null, 1, 1, null], {skip: 1, count: 3}), [0, 1, 2, 1, 0, 1])\n        checkEncoded(doCopy([], [null, null, null, 0, 0], {skip: 0, count: 5}), [0, 3, 2, 0])\n      })\n\n      it('should allow insertion into a sequence', () => {\n        const decoder1 = new RLEDecoder('uint', encodeRLE('uint', [0, 1, 2, 3, 4, 5, 6]))\n        const decoder2 = new RLEDecoder('uint', encodeRLE('uint', [3, 3, 3]))\n        const encoder = new RLEEncoder('uint')\n        encoder.copyFrom(decoder1, {count: 4})\n        encoder.copyFrom(decoder2)\n        encoder.copyFrom(decoder1)\n        checkEncoded(encoder, [0x7d, 0, 1, 2, 4, 3, 0x7d, 4, 5, 6])\n      })\n\n      it('should allow insertion into repetition run', () => {\n        const decoder1 = new RLEDecoder('uint', encodeRLE('uint', [1, 2, 3, 3, 4]))\n        const decoder2 = new RLEDecoder('uint', encodeRLE('uint', [5]))\n        const encoder = new RLEEncoder('uint')\n        encoder.copyFrom(decoder1, {count: 3})\n        encoder.copyFrom(decoder2)\n        encoder.copyFrom(decoder1)\n        checkEncoded(encoder, [0x7a, 1, 2, 3, 5, 3, 4])\n      })\n\n      it('should allow copying from a decoder starting with nulls', () => {\n        const decoder = new RLEDecoder('uint', new Uint8Array([0, 2, 0x7f, 0])) // null, null, 0\n        new RLEEncoder('uint').copyFrom(decoder, {count: 1})\n        assert.strictEqual(decoder.readValue(), null)\n        assert.strictEqual(decoder.readValue(), 0)\n        decoder.reset()\n        new RLEEncoder('uint').copyFrom(decoder, {count: 2})\n        assert.strictEqual(decoder.readValue(), 0)\n      })\n\n      it('should compute the sum of values copied', () => {\n        const encoder1 = new RLEEncoder('uint'), encoder2 = new RLEEncoder('uint')\n        for (let v of [1, 2, 3, 10, 10, 10]) encoder2.appendValue(v)\n        assert.deepStrictEqual(\n          encoder1.copyFrom(new RLEDecoder('uint', encoder2.buffer), {sumValues: true}),\n          {nonNullValues: 6, sum: 36})\n        assert.deepStrictEqual(\n          encoder1.copyFrom(new RLEDecoder('uint', encoder2.buffer), {sumValues: true, sumShift: 2}),\n          {nonNullValues: 6, sum: 6})\n      })\n\n      it('should throw an exception if the decoder has too few values', () => {\n        assert.throws(() => { doCopy([0, 1, 2], [], {count: 1}) }, /cannot copy 1 values/)\n        assert.throws(() => { doCopy([0, 1, 2], [3], {count: 2}) }, /cannot copy 2 values/)\n        assert.throws(() => { doCopy([0, 1, 2], [3, 4, 5, 6], {count: 5}) }, /cannot copy 5 values/)\n        assert.throws(() => { doCopy([0, 1, 2], [3], {count: 2}) }, /cannot copy 2 values/)\n        assert.throws(() => { doCopy([0, 1, 2], [3, 3, 3], {count: 4}) }, /cannot copy 4 values/)\n        assert.throws(() => { doCopy([0, 1, 2], [3, 3, 4, 4, 5, 5], {count: 7}) }, /cannot copy 7 values/)\n        assert.throws(() => { new RLEEncoder('uint').copyFrom(new RLEDecoder('uint', new Uint8Array([0x7e, 1]))) }, /incomplete literal/)\n        assert.throws(() => { new RLEEncoder('uint').copyFrom(new RLEDecoder('uint', new Uint8Array([2, 1, 0x7f, 1]))) }, /Repetition of values/)\n      })\n\n      it('should check the type of the decoder', () => {\n        const encoder1 = new RLEEncoder('uint')\n        assert.throws(() => { encoder1.copyFrom(new Decoder(new Uint8Array(0))) }, /incompatible type of decoder/)\n        assert.throws(() => { encoder1.copyFrom(new RLEDecoder('int', new Uint8Array(0))) }, /incompatible type of decoder/)\n      })\n    })\n  })\n\n  describe('DeltaEncoder and DeltaDecoder', () => {\n    function encodeDelta(values) {\n      const encoder = new DeltaEncoder()\n      for (let value of values) encoder.appendValue(value)\n      return encoder.buffer\n    }\n\n    function decodeDelta(buffer) {\n      const decoder = new DeltaDecoder(buffer), values = []\n      while (!decoder.done) values.push(decoder.readValue())\n      return values\n    }\n\n    it('should encode sequences', () => {\n      checkEncoded(encodeDelta([]), [])\n      checkEncoded(encodeDelta([18, 2, 9, 15, 16, 19, 25]), [0x79, 18, 0x70, 7, 6, 1, 3, 6])\n      checkEncoded(encodeDelta([1, 2, 3, 4, 5, 6, 7, 8]), [8, 1])\n      checkEncoded(encodeDelta([10, 11, 12, 13, 14, 15]), [0x7f, 10, 5, 1])\n      checkEncoded(encodeDelta([10, 11, 12, 13, 0, 1, 2, 3]), [0x7f, 10, 3, 1, 0x7f, 0x73, 3, 1])\n      checkEncoded(encodeDelta([0, 1, 2, 3, null, null, null, 4, 5, 6]), [0x7f, 0, 3, 1, 0, 3, 3, 1])\n      checkEncoded(encodeDelta([-64, -60, -56, -52, -48, -44, -40, -36]), [0x7f, 0x40, 7, 4])\n    })\n\n    it('should encode-decode round-trip sequences', () => {\n      assert.deepStrictEqual(decodeDelta(encodeDelta([])), [])\n      assert.deepStrictEqual(decodeDelta(encodeDelta([18, 2, 9, 15, 16, 19, 25])), [18, 2, 9, 15, 16, 19, 25])\n      assert.deepStrictEqual(decodeDelta(encodeDelta([1, 2, 3, 4, 5, 6, 7, 8])), [1, 2, 3, 4, 5, 6, 7, 8])\n      assert.deepStrictEqual(decodeDelta(encodeDelta([10, 11, 12, 13, 14, 15])), [10, 11, 12, 13, 14, 15])\n      assert.deepStrictEqual(decodeDelta(encodeDelta([10, 11, 12, 13, 0, 1, 2, 3])), [10, 11, 12, 13, 0, 1, 2, 3])\n      assert.deepStrictEqual(decodeDelta(encodeDelta([0, 1, 2, 3, null, null, null, 4, 5, 6])), [0, 1, 2, 3, null, null, null, 4, 5, 6])\n      assert.deepStrictEqual(decodeDelta(encodeDelta([-64, -60, -56, -52, -48, -44, -40, -36])), [-64, -60, -56, -52, -48, -44, -40, -36])\n    })\n\n    it('should allow repetition counts to be specified', () => {\n      let e\n      e = new DeltaEncoder(); e.appendValue(3, 0); checkEncoded(e, [])\n      e = new DeltaEncoder(); e.appendValue(3, 10); checkEncoded(e, [0x7f, 3, 9, 0])\n      e = new DeltaEncoder(); e.appendValue(1, 3); e.appendValue(1, 3); checkEncoded(e, [0x7f, 1, 5, 0])\n    })\n\n    it('should allow skipping values', () => {\n      const example = [null, null, null, 10, 11, 12, 13, 14, 15, 16, 1, 2, 3, 40, 11, 13, 21, 103]\n      const encoded = encodeDelta(example)\n      for (let skipNum = 0; skipNum < example.length; skipNum++) {\n        const decoder = new DeltaDecoder(encoded), values = []\n        decoder.skipValues(skipNum)\n        while (!decoder.done) values.push(decoder.readValue())\n        assert.deepStrictEqual(values, example.slice(skipNum), `skipping ${skipNum} values failed`)\n      }\n    })\n\n    describe('copying from a decoder', () => {\n      function doCopy(input1, input2, options = {}) {\n        let encoder1 = input1\n        if (Array.isArray(input1)) {\n          encoder1 = new DeltaEncoder()\n          for (let value of input1) encoder1.appendValue(value)\n        }\n\n        const encoder2 = new DeltaEncoder()\n        for (let value of input2) encoder2.appendValue(value)\n        const decoder2 = new DeltaDecoder(encoder2.buffer)\n        if (options.skip) decoder2.skipValues(options.skip)\n        encoder1.copyFrom(decoder2, options)\n        return encoder1\n      }\n\n      it('should copy a sequence', () => {\n        checkEncoded(doCopy([], [0, 0, 0]), [3, 0])\n        checkEncoded(doCopy([0, 0, 0], []), [3, 0])\n        checkEncoded(doCopy([0, 0, 0], [0, 0, 0]), [6, 0])\n        checkEncoded(doCopy([1, 2, 3], [4, 5, 6]), [6, 1])\n        checkEncoded(doCopy([1, 2, 3], [4, 10, 20]), [4, 1, 0x7e, 6, 10])\n        checkEncoded(doCopy([1, 2, 3], [1, 2, 3, 4]), [3, 1, 0x7f, 0x7e, 3, 1])\n        checkEncoded(doCopy([0, 1, 3], [6, 10, 15]), [0x7a, 0, 1, 2, 3, 4, 5])\n        checkEncoded(doCopy([0, 1, 3], [5, 9, 14]), [0x7e, 0, 1, 2, 2, 0x7e, 4, 5])\n        checkEncoded(doCopy([1, 2, 4], [5, 6, 8, 9, 10, 12]), [2, 1, 0x7f, 2, 2, 1, 0x7f, 2, 2, 1, 0x7f, 2])\n        checkEncoded(doCopy([4, 4, 4], [4, 4, 4, 5, 6, 7]), [0x7f, 4, 5, 0, 3, 1])\n        checkEncoded(doCopy([0, 1, 4], [9, 6, 2, 5, 3]), [0x78, 0, 1, 3, 5, 0x7d, 0x7c, 3, 0x7e])\n        checkEncoded(doCopy([1, 2, 3], [null, 4, 5, 6]), [3, 1, 0, 1, 3, 1])\n        checkEncoded(doCopy([1, 2, 3], [null, 6, 6, 6]), [3, 1, 0, 1, 0x7f, 3, 2, 0])\n        checkEncoded(doCopy([1, 2, 3], [null, null, 4, 5, 7, 9]), [3, 1, 0, 2, 2, 1, 2, 2])\n        checkEncoded(doCopy([1, 2, null], [3, 4, 5]), [2, 1, 0, 1, 3, 1])\n        checkEncoded(doCopy([1, 2, null], [6, 6, 6]), [2, 1, 0, 1, 0x7f, 4, 2, 0])\n        checkEncoded(doCopy([1, 2, null], [null, 3, 4]), [2, 1, 0, 2, 2, 1])\n        checkEncoded(doCopy([1, 2, null], [null, 6, 6]), [2, 1, 0, 2, 0x7e, 4, 0])\n      })\n\n      it('should copy a sub-sequence', () => {\n        checkEncoded(doCopy([1, 2, 3], [4, 5, 6, 7], {count: 2}), [5, 1])\n        checkEncoded(doCopy([1, 2, 3], [null, null, 4], {count: 1}), [3, 1, 0, 1])\n        checkEncoded(doCopy([1, 2, 3], [null, null, 4], {count: 2}), [3, 1, 0, 2])\n      })\n\n      it('should copy non-ascending sequences', () => {\n        const decoder = new DeltaDecoder(new Uint8Array([2, 1, 0x7e, 2, 0x7f])) // 1, 2, 4, 3\n        const encoder = new DeltaEncoder()\n        encoder.copyFrom(decoder, {count: 4})\n        encoder.appendValue(5)\n        checkEncoded(encoder, [2, 1, 0x7d, 2, 0x7f, 2]) // 1, 2, 4, 3, 5\n      })\n\n      it('should be able to pause and resume copying', () => {\n        const numValues = 13 // 1, 3, 4, 2, null, 3, 4, 5, null, null, 4, 2, -1\n        const bytes = [0x7c, 1, 2, 1, 0x7e, 0, 1, 3, 1, 0, 2, 0x7d, 0x7f, 0x7e, 0x7d]\n        const decoder = new DeltaDecoder(new Uint8Array(bytes))\n        for (let i = 0; i <= numValues; i++) {\n          const encoder = new DeltaEncoder()\n          encoder.copyFrom(decoder, {count: i})\n          encoder.copyFrom(decoder, {count: numValues - i})\n          checkEncoded(encoder, bytes)\n          decoder.reset()\n        }\n      })\n\n      it('should handle copying followed by appending', () => {\n        const encoder1 = doCopy([], [1, 2, 3])\n        encoder1.appendValue(4)\n        checkEncoded(encoder1, [4, 1])\n\n        const encoder2 = doCopy([5], [6, null, null, null, 7, 8])\n        encoder2.appendValue(9)\n        checkEncoded(encoder2, [0x7e, 5, 1, 0, 3, 3, 1])\n\n        const encoder3 = doCopy([1], [2])\n        encoder3.appendValue(3)\n        checkEncoded(encoder3, [3, 1])\n      })\n\n      it('should throw an exception if the decoder has too few values', () => {\n        assert.throws(() => { doCopy([0, 1, 2], [], {count: 1}) }, /cannot copy 1 values/)\n        assert.throws(() => { doCopy([0, 1, 2], [null, 3], {count: 3}) }, /cannot copy 1 values/)\n        assert.throws(() => { new DeltaEncoder().copyFrom(new DeltaDecoder(new Uint8Array([0, 2])), {count: 3}) }, /cannot copy 3 values/)\n      })\n\n      it('should check the arguments are valid', () => {\n        const encoder1 = new DeltaEncoder('uint')\n        assert.throws(() => { encoder1.copyFrom(new Decoder(new Uint8Array(0))) }, /incompatible type of decoder/)\n        assert.throws(() => { encoder1.copyFrom(new DeltaDecoder(new Uint8Array(0)), {sumValues: true}) }, /unsupported options/)\n      })\n    })\n  })\n\n  describe('BooleanEncoder and BooleanDecoder', () => {\n    function encodeBools(values) {\n      const encoder = new BooleanEncoder()\n      for (let value of values) encoder.appendValue(value)\n      return encoder.buffer\n    }\n\n    function decodeBools(buffer) {\n      if (Array.isArray(buffer)) buffer = new Uint8Array(buffer)\n      const decoder = new BooleanDecoder(buffer), values = []\n      while (!decoder.done) values.push(decoder.readValue())\n      return values\n    }\n\n    it('should encode sequences of booleans', () => {\n      checkEncoded(encodeBools([]), [])\n      checkEncoded(encodeBools([false]), [1])\n      checkEncoded(encodeBools([true]), [0, 1])\n      checkEncoded(encodeBools([false, false, false, true, true]), [3, 2])\n      checkEncoded(encodeBools([true, true, true, false, false]), [0, 3, 2])\n      checkEncoded(encodeBools([true, false, true, false, true, true, false]), [0, 1, 1, 1, 1, 2, 1])\n    })\n\n    it('should encode-decode round-trip booleans', () => {\n      assert.deepStrictEqual(decodeBools(encodeBools([])), [])\n      assert.deepStrictEqual(decodeBools(encodeBools([false])), [false])\n      assert.deepStrictEqual(decodeBools(encodeBools([true])), [true])\n      assert.deepStrictEqual(decodeBools(encodeBools([false, false, false, true, true])), [false, false, false, true, true])\n      assert.deepStrictEqual(decodeBools(encodeBools([true, true, true, false, false])), [true, true, true, false, false])\n      assert.deepStrictEqual(decodeBools(encodeBools([true, false, true, false, true, true, false])), [true, false, true, false, true, true, false])\n    })\n\n    it('should not allow non-boolean values', () => {\n      assert.throws(() => { encodeBools([42]) }, /Unsupported value/)\n      assert.throws(() => { encodeBools([null]) }, /Unsupported value/)\n      assert.throws(() => { encodeBools(['false']) }, /Unsupported value/)\n      assert.throws(() => { encodeBools([undefined]) }, /Unsupported value/)\n    })\n\n    it('should allow repetition counts to be specified', () => {\n      let e\n      e = new BooleanEncoder(); e.appendValue(false, 0); checkEncoded(e, [])\n      e = new BooleanEncoder(); e.appendValue(false, 2); e.appendValue(false, 2); checkEncoded(e, [4])\n      e = new BooleanEncoder(); e.appendValue(true, 2); e.appendValue(false, 2); checkEncoded(e, [0, 2, 2])\n    })\n\n    it('should allow skipping values', () => {\n      const example = [false, false, false, true, true, true, false, true, false, true]\n      const encoded = encodeBools(example)\n      for (let skipNum = 0; skipNum < example.length; skipNum++) {\n        const decoder = new BooleanDecoder(encoded), values = []\n        decoder.skipValues(skipNum)\n        while (!decoder.done) values.push(decoder.readValue())\n        assert.deepStrictEqual(values, example.slice(skipNum), `skipping ${skipNum} values failed`)\n      }\n    })\n\n    it('should strictly enforce canonical encoded form', () => {\n      assert.throws(() => { decodeBools([1, 0]) }, /Zero-length runs are not allowed/)\n      assert.throws(() => { decodeBools([1, 1, 0]) }, /Zero-length runs are not allowed/)\n      const decoder = new BooleanDecoder(new Uint8Array([2, 0, 1]))\n      decoder.skipValues(1)\n      assert.throws(() => { decoder.skipValues(2) }, /Zero-length runs are not allowed/)\n    })\n\n    describe('copying from a decoder', () => {\n      function doCopy(input1, input2, options = {}) {\n        let encoder1 = input1\n        if (Array.isArray(input1)) {\n          encoder1 = new BooleanEncoder()\n          for (let value of input1) encoder1.appendValue(value)\n        }\n\n        const encoder2 = new BooleanEncoder()\n        for (let value of input2) encoder2.appendValue(value)\n        const decoder2 = new BooleanDecoder(encoder2.buffer)\n        if (options.skip) decoder2.skipValues(options.skip)\n        encoder1.copyFrom(decoder2, options)\n        return encoder1\n      }\n\n      it('should copy a sequence', () => {\n        checkEncoded(doCopy([false, false, true], []), [2, 1])\n        checkEncoded(doCopy([], [false, false, true, true]), [2, 2])\n        checkEncoded(doCopy([false, false], [false, false, true, true]), [4, 2])\n        checkEncoded(doCopy([true, true], [false, false, true, true]), [0, 2, 2, 2])\n        checkEncoded(doCopy([true, true], [true, true]), [0, 4])\n      })\n\n      it('should copy a sub-sequence', () => {\n        checkEncoded(doCopy([false], [false, false, false, true], {count: 2}), [3])\n        checkEncoded(doCopy([false], [true, true, true, true], {count: 3}), [1, 3])\n        checkEncoded(doCopy([false], [false, true, true, true], {skip: 1}), [1, 3])\n        checkEncoded(doCopy([false], [false, true, true, true], {skip: 2}), [1, 2])\n      })\n\n      it('should throw an exception if the decoder has too few values', () => {\n        assert.throws(() => { doCopy([false], [], {count: 1}) }, /cannot copy 1 values/)\n        assert.throws(() => { doCopy([false], [true, false], {count: 3}) }, /cannot copy 3 values/)\n      })\n\n      it('should check the arguments are valid', () => {\n        assert.throws(() => { new BooleanEncoder().copyFrom(new Decoder(new Uint8Array(0))) }, /incompatible type of decoder/)\n        assert.throws(() => { new BooleanEncoder().copyFrom(new BooleanDecoder(new Uint8Array([2, 0]))) }, /Zero-length runs/)\n      })\n    })\n  })\n})\n"
  },
  {
    "path": "test/frontend_test.js",
    "content": "const assert = require('assert')\nconst Frontend = require('../frontend')\nconst { decodeChange } = require('../backend/columnar')\nconst { Backend } = require('../src/automerge')\nconst uuid = require('../src/uuid')\nconst { STATE } = require('../frontend/constants')\nconst UUID_PATTERN = /^[0-9a-f]{32}$/\n\ndescribe('Automerge.Frontend', () => {\n  describe('initializing', () => {\n    it('should be an empty object by default', () => {\n      const doc = Frontend.init()\n      assert.deepStrictEqual(doc, {})\n      assert(UUID_PATTERN.test(Frontend.getActorId(doc).toString()))\n    })\n\n    it('should allow actorId assignment to be deferred', () => {\n      let doc0 = Frontend.init({ deferActorId: true })\n      assert.strictEqual(Frontend.getActorId(doc0), undefined)\n      assert.throws(() => { Frontend.change(doc0, doc => doc.foo = 'bar') }, /Actor ID must be initialized with setActorId/)\n      const doc1 = Frontend.setActorId(doc0, uuid())\n      const [doc2] = Frontend.change(doc1, doc => doc.foo = 'bar')\n      assert.deepStrictEqual(doc2, { foo: 'bar' })\n    })\n\n    it('should allow instantiating from an existing object', () => {\n      const initialState = {\n        birds: {\n          wrens: 3,\n          magpies: 4\n        }\n      }\n      const [doc] = Frontend.from(initialState)\n      assert.deepStrictEqual(doc, initialState)\n    })\n\n    it('should accept an empty object as initial state', () => {\n      const [doc] = Frontend.from({})\n      assert.deepStrictEqual(doc, {})\n    })\n  })\n\n  describe('performing changes', () => {\n    it('should return the unmodified document if nothing changed', () => {\n      const doc0 = Frontend.init()\n      const [doc1] = Frontend.change(doc0, () => {})\n      assert.strictEqual(doc1, doc0)\n    })\n\n    it('should set root object properties', () => {\n      const actor = uuid()\n      const [doc, change] = Frontend.change(Frontend.init(actor), doc => doc.bird = 'magpie')\n      assert.deepStrictEqual(doc, {bird: 'magpie'})\n      assert.deepStrictEqual(change, {\n        actor, seq: 1, time: change.time, message: '', startOp: 1, deps: [], ops: [\n          {obj: '_root', action: 'set', key: 'bird', insert: false, value: 'magpie', pred: []}\n        ]\n      })\n    })\n\n    it('should create nested maps', () => {\n      const [doc, change] = Frontend.change(Frontend.init(), doc => doc.birds = {wrens: 3})\n      const birds = Frontend.getObjectId(doc.birds), actor = Frontend.getActorId(doc)\n      assert.deepStrictEqual(doc, {birds: {wrens: 3}})\n      assert.deepStrictEqual(change, {\n        actor, seq: 1, time: change.time, message: '', startOp: 1, deps: [], ops: [\n          {obj: '_root', action: 'makeMap', key: 'birds', insert: false, pred: []},\n          {obj: birds,   action: 'set',     key: 'wrens', insert: false, datatype: 'int', value: 3, pred: []}\n        ]\n      })\n    })\n\n    it('should apply updates inside nested maps', () => {\n      const [doc1] = Frontend.change(Frontend.init(), doc => doc.birds = {wrens: 3})\n      const [doc2, change2] = Frontend.change(doc1, doc => doc.birds.sparrows = 15)\n      const birds = Frontend.getObjectId(doc2.birds), actor = Frontend.getActorId(doc1)\n      assert.deepStrictEqual(doc1, {birds: {wrens: 3}})\n      assert.deepStrictEqual(doc2, {birds: {wrens: 3, sparrows: 15}})\n      assert.deepStrictEqual(change2, {\n        actor, seq: 2, time: change2.time, message: '', startOp: 3, deps: [], ops: [\n          {obj: birds, action: 'set', key: 'sparrows', insert: false, datatype: 'int', value: 15, pred: []}\n        ]\n      })\n    })\n\n    it('should delete keys in maps', () => {\n      const actor = uuid()\n      const [doc1] = Frontend.change(Frontend.init(actor), doc => { doc.magpies = 2; doc.sparrows = 15 })\n      const [doc2, change2] = Frontend.change(doc1, doc => delete doc.magpies)\n      assert.deepStrictEqual(doc1, {magpies: 2, sparrows: 15})\n      assert.deepStrictEqual(doc2, {sparrows: 15})\n      assert.deepStrictEqual(change2, {\n        actor, seq: 2, time: change2.time, message: '', startOp: 3, deps: [], ops: [\n          {obj: '_root', action: 'del', key: 'magpies', insert: false, pred: [ `1@${actor}`]}\n        ]\n      })\n    })\n\n    it('should create lists', () => {\n      const [doc, change] = Frontend.change(Frontend.init(), doc => doc.birds = ['chaffinch'])\n      const actor = Frontend.getActorId(doc)\n      assert.deepStrictEqual(doc, {birds: ['chaffinch']})\n      assert.deepStrictEqual(change, {\n        actor, seq: 1, time: change.time, message: '', startOp: 1, deps: [], ops: [\n          {obj: '_root', action: 'makeList', key: 'birds', insert: false, pred: []},\n          {obj: `1@${actor}`, action: 'set', elemId: '_head', insert: true, value: 'chaffinch', pred: []}\n        ]\n      })\n    })\n\n    it('should apply updates inside lists', () => {\n      const [doc1] = Frontend.change(Frontend.init(), doc => doc.birds = ['chaffinch'])\n      const [doc2, change2] = Frontend.change(doc1, doc => doc.birds[0] = 'greenfinch')\n      const birds = Frontend.getObjectId(doc2.birds), actor = Frontend.getActorId(doc2)\n      assert.deepStrictEqual(doc1, {birds: ['chaffinch']})\n      assert.deepStrictEqual(doc2, {birds: ['greenfinch']})\n      assert.deepStrictEqual(change2, {\n        actor, seq: 2, time: change2.time, message: '', startOp: 3, deps: [], ops: [\n          {obj: birds, action: 'set', elemId: `2@${actor}`, insert: false, value: 'greenfinch', pred: [ `2@${actor}` ]}\n        ]\n      })\n    })\n\n    it('should insert nulls when indexing out of upper-bound range', () => {\n      const [doc1] = Frontend.change(Frontend.init(), doc => doc.birds = ['chaffinch'])\n      const [doc2, change2] = Frontend.change(doc1, doc => doc.birds[3] = 'greenfinch')\n      const birds = Frontend.getObjectId(doc2.birds), actor = Frontend.getActorId(doc2)\n      assert.deepStrictEqual(doc1, {birds: ['chaffinch']})\n      assert.deepStrictEqual(doc2, {birds: ['chaffinch', null, null, 'greenfinch']})\n      assert.deepStrictEqual(change2, {\n        actor, seq: 2, startOp: 3, deps: [], time: change2.time, message: '',  ops: [\n          {action: 'set', obj: birds, elemId: `2@${actor}`, insert: true, values: [null, null, 'greenfinch'], pred: []}\n        ]\n      })\n    })\n\n    it('should delete list elements', () => {\n      const [doc1] = Frontend.change(Frontend.init(), doc => doc.birds = ['chaffinch', 'goldfinch'])\n      const [doc2, change2] = Frontend.change(doc1, doc => doc.birds.deleteAt(0))\n      const birds = Frontend.getObjectId(doc2.birds), actor = Frontend.getActorId(doc2)\n      assert.deepStrictEqual(doc1, {birds: ['chaffinch', 'goldfinch']})\n      assert.deepStrictEqual(doc2, {birds: ['goldfinch']})\n      assert.deepStrictEqual(change2, {\n        actor, seq: 2, time: change2.time, message: '', startOp: 4, deps: [], ops: [\n          {obj: birds, action: 'del', elemId: `2@${actor}`, insert: false, pred: [`2@${actor}`]}\n        ]\n      })\n    })\n\n    it('should store Date objects as timestamps', () => {\n      const now = new Date()\n      const [doc, change] = Frontend.change(Frontend.init(), doc => doc.now = now)\n      const actor = Frontend.getActorId(doc)\n      assert.strictEqual(doc.now instanceof Date, true)\n      assert.strictEqual(doc.now.getTime(), now.getTime())\n      assert.deepStrictEqual(change, {\n        actor, seq: 1, time: change.time, message: '', startOp: 1, deps: [], ops: [\n          {obj: '_root', action: 'set', key: 'now', insert: false, value: now.getTime(), datatype: 'timestamp', pred: []}\n        ]\n      })\n    })\n\n    describe('counters', () => {\n      it('should handle counters inside maps', () => {\n        const [doc1, change1] = Frontend.change(Frontend.init(), doc => {\n          doc.wrens = new Frontend.Counter()\n          assert.strictEqual(doc.wrens.value, 0)\n        })\n        const [doc2, change2] = Frontend.change(doc1, doc => {\n          doc.wrens.increment()\n          assert.strictEqual(doc.wrens.value, 1)\n        })\n        const actor = Frontend.getActorId(doc2)\n        assert.deepStrictEqual(doc1, {wrens: new Frontend.Counter(0)})\n        assert.deepStrictEqual(doc2, {wrens: new Frontend.Counter(1)})\n        assert.deepStrictEqual(change1, {\n          actor, seq: 1, time: change1.time, message: '', startOp: 1, deps: [], ops: [\n            {obj: '_root', action: 'set', key: 'wrens', insert: false, value: 0, datatype: 'counter', pred: []}\n          ]\n        })\n        assert.deepStrictEqual(change2, {\n          actor, seq: 2, time: change2.time, message: '', startOp: 2, deps: [], ops: [\n            {obj: '_root', action: 'inc', key: 'wrens', insert: false, value: 1, pred: [`1@${actor}`]}\n          ]\n        })\n      })\n\n      it('should handle counters inside lists', () => {\n        const [doc1, change1] = Frontend.change(Frontend.init(), doc => {\n          doc.counts = [new Frontend.Counter(1)]\n          assert.strictEqual(doc.counts[0].value, 1)\n        })\n        const [doc2, change2] = Frontend.change(doc1, doc => {\n          doc.counts[0].increment(2)\n          assert.strictEqual(doc.counts[0].value, 3)\n        })\n        const counts = Frontend.getObjectId(doc2.counts), actor = Frontend.getActorId(doc2)\n        assert.deepStrictEqual(doc1, {counts: [new Frontend.Counter(1)]})\n        assert.deepStrictEqual(doc2, {counts: [new Frontend.Counter(3)]})\n        assert.deepStrictEqual(change1, {\n          actor, deps: [], seq: 1, time: change1.time, message: '', startOp: 1, ops: [\n            {obj: '_root', action: 'makeList', key: 'counts', insert: false, pred: []},\n            {obj: counts, action: 'set', elemId: '_head', insert: true, value: 1, datatype: 'counter', pred: []}\n          ]\n        })\n        assert.deepStrictEqual(change2, {\n          actor, deps: [], seq: 2, time: change2.time, message: '', startOp: 3, ops: [\n            {obj: counts, action: 'inc', elemId: `2@${actor}`, insert: false, value: 2, pred: [`2@${actor}`]}\n          ]\n        })\n      })\n\n      it('should refuse to overwrite a property with a counter value', () => {\n        const [doc1] = Frontend.change(Frontend.init(), doc => {\n          doc.counter = new Frontend.Counter()\n          doc.list = [new Frontend.Counter()]\n        })\n        assert.throws(() => Frontend.change(doc1, doc => doc.counter++), /Cannot overwrite a Counter object/)\n        assert.throws(() => Frontend.change(doc1, doc => doc.list[0] = 3), /Cannot overwrite a Counter object/)\n      })\n\n      it('should make counter objects behave like primitive numbers', () => {\n        const [doc1] = Frontend.change(Frontend.init(), doc => doc.birds = new Frontend.Counter(3))\n        assert.equal(doc1.birds, 3) // they are equal according to ==, but not strictEqual according to ===\n        assert.notStrictEqual(doc1.birds, 3)\n        assert(doc1.birds < 4)\n        assert(doc1.birds >= 0)\n        assert(!(doc1.birds <= 2))\n        assert.strictEqual(doc1.birds + 10, 13)\n        assert.strictEqual(`I saw ${doc1.birds} birds`, 'I saw 3 birds')\n        assert.strictEqual(['I saw', doc1.birds, 'birds'].join(' '), 'I saw 3 birds')\n      })\n\n      it('should allow counters to be serialized to JSON', () => {\n        const [doc1] = Frontend.change(Frontend.init(), doc => doc.birds = new Frontend.Counter())\n        assert.strictEqual(JSON.stringify(doc1), '{\"birds\":0}')\n      })\n    })\n  })\n\n  describe('backend concurrency', () => {\n    function getRequests(doc) {\n      return doc[STATE].requests.map(req => ({actor: req.actor, seq: req.seq}))\n    }\n\n    it('should use version and sequence number from the backend', () => {\n      const local = uuid(), remote1 = uuid(), remote2 = uuid()\n      const patch1 = {\n        clock: {[local]: 4, [remote1]: 11, [remote2]: 41}, maxOp: 4, deps: [],\n        diffs: {objectId: '_root', type: 'map', props: {blackbirds: {[local]: {type: 'value', value: 24}}}}\n      }\n      let doc1 = Frontend.applyPatch(Frontend.init(local), patch1)\n      let [doc2, change] = Frontend.change(doc1, doc => doc.partridges = 1)\n      assert.deepStrictEqual(change, {\n        actor: local, seq: 5, deps: [], startOp: 5, time: change.time, message: '', ops: [\n          {obj: '_root', action: 'set', key: 'partridges', insert: false, datatype: 'int', value: 1, pred: []}\n        ]\n      })\n      assert.deepStrictEqual(getRequests(doc2), [{actor: local, seq: 5}])\n    })\n\n    it('should remove pending requests once handled', () => {\n      const actor = uuid()\n      let [doc1, change1] = Frontend.change(Frontend.init(actor), doc => doc.blackbirds = 24)\n      let [doc2, change2] = Frontend.change(doc1, doc => doc.partridges = 1)\n      assert.deepStrictEqual(change1, {\n        actor, seq: 1, deps: [], startOp: 1, time: change1.time, message: '', ops: [\n          {obj: '_root', action: 'set', key: 'blackbirds', insert: false, datatype: 'int', value: 24, pred: []}\n        ]\n      })\n      assert.deepStrictEqual(change2, {\n        actor, seq: 2, deps: [], startOp: 2, time: change2.time, message: '', ops: [\n          {obj: '_root', action: 'set', key: 'partridges', insert: false, datatype: 'int', value: 1, pred: []}\n        ]\n      })\n      assert.deepStrictEqual(getRequests(doc2), [{actor, seq: 1}, {actor, seq: 2}])\n\n      doc2 = Frontend.applyPatch(doc2, {\n        actor, seq: 1, clock: {[actor]: 1}, diffs: {\n          objectId: '_root', type: 'map', props: {blackbirds: {[actor]: {type: 'value', value: 24}}}\n        }\n      })\n      assert.deepStrictEqual(getRequests(doc2), [{actor, seq: 2}])\n      assert.deepStrictEqual(doc2, {blackbirds: 24, partridges: 1})\n\n      doc2 = Frontend.applyPatch(doc2, {\n        actor, seq: 2, clock: {[actor]: 2}, diffs: {\n          objectId: '_root', type: 'map', props: {partridges: {[actor]: {type: 'value', value: 1}}}\n        }\n      })\n      assert.deepStrictEqual(doc2, {blackbirds: 24, partridges: 1})\n      assert.deepStrictEqual(getRequests(doc2), [])\n    })\n\n    it('should leave the request queue unchanged on remote patches', () => {\n      const actor = uuid(), other = uuid()\n      let [doc, req] = Frontend.change(Frontend.init(actor), doc => doc.blackbirds = 24)\n      assert.deepStrictEqual(req, {\n        actor, seq: 1, deps: [], startOp: 1, time: req.time, message: '', ops: [\n          {obj: '_root', action: 'set', key: 'blackbirds', insert: false, datatype: 'int', value: 24, pred: []}\n        ]\n      })\n      assert.deepStrictEqual(getRequests(doc), [{actor, seq: 1}])\n\n      doc = Frontend.applyPatch(doc, {\n        clock: {[other]: 1}, diffs: {\n          objectId: '_root', type: 'map', props: {pheasants: {[other]: {type: 'value', value: 2}}}\n        }\n      })\n      assert.deepStrictEqual(doc, {blackbirds: 24})\n      assert.deepStrictEqual(getRequests(doc), [{actor, seq: 1}])\n\n      doc = Frontend.applyPatch(doc, {\n        actor, seq: 1, clock: {[actor]: 1, [other]: 1}, diffs: {\n          objectId: '_root', type: 'map', props: {blackbirds: {[actor]: {type: 'value', value: 24}}}\n        }\n      })\n      assert.deepStrictEqual(doc, {blackbirds: 24, pheasants: 2})\n      assert.deepStrictEqual(getRequests(doc), [])\n    })\n\n    it('should not allow request patches to be applied out of order', () => {\n      const [doc1] = Frontend.change(Frontend.init(), doc => doc.blackbirds = 24)\n      const [doc2] = Frontend.change(doc1, doc => doc.partridges = 1)\n      const actor = Frontend.getActorId(doc2)\n      const diffs = {objectId: '_root', type: 'map', props: {partridges: {[actor]: {type: 'value', value: 1}}}}\n      assert.throws(() => {\n        Frontend.applyPatch(doc2, {actor, seq: 2, clock: {[actor]: 2}, diffs})\n      }, /Mismatched sequence number/)\n    })\n\n    it('should handle concurrent insertions into lists', () => {\n      let [doc1] = Frontend.change(Frontend.init(), doc => doc.birds = ['goldfinch'])\n      const birds = Frontend.getObjectId(doc1.birds), actor = Frontend.getActorId(doc1)\n      doc1 = Frontend.applyPatch(doc1, {\n        actor, seq: 1, clock: {[actor]: 1}, maxOp: 2,\n        diffs: {objectId: '_root', type: 'map', props: {\n          birds: {[actor]: {objectId: birds, type: 'list', edits: [\n            {action: 'insert', elemId: `2@${actor}`, opId: `2@${actor}`, index: 0, value: {type: 'value', value: 'goldfinch'}}\n          ]}}\n        }}\n      })\n      assert.deepStrictEqual(doc1, {birds: ['goldfinch']})\n      assert.deepStrictEqual(getRequests(doc1), [])\n\n      const [doc2] = Frontend.change(doc1, doc => {\n        doc.birds.insertAt(0, 'chaffinch')\n        doc.birds.insertAt(2, 'greenfinch')\n      })\n      assert.deepStrictEqual(doc2, {birds: ['chaffinch', 'goldfinch', 'greenfinch']})\n\n      const remoteActor = uuid()\n      const doc3 = Frontend.applyPatch(doc2, {\n        clock: {[actor]: 1, [remoteActor]: 1}, maxOp: 4,\n        diffs: {objectId: '_root', type: 'map', props: {\n          birds: {[actor]: {objectId: birds, type: 'list', edits: [\n            {action: 'insert', elemId: `1@${remoteActor}`, opId: `1@${remoteActor}`, index: 1, value: {type: 'value', value: 'bullfinch'}}\n          ]}}\n        }}\n      })\n      // The addition of 'bullfinch' does not take effect yet: it is queued up until the pending\n      // request has made its round-trip through the backend.\n      assert.deepStrictEqual(doc3, {birds: ['chaffinch', 'goldfinch', 'greenfinch']})\n\n      const doc4 = Frontend.applyPatch(doc3, {\n        actor, seq: 2, clock: {[actor]: 2, [remoteActor]: 1}, maxOp: 4,\n        diffs: {objectId: '_root', type: 'map', props: {\n          birds: {[actor]: {objectId: birds, type: 'list', edits: [\n            {action: 'insert', index: 0, elemId: `3@${actor}`, opId: `3@${actor}`, value: {type: 'value', value: 'chaffinch'}},\n            {action: 'insert', index: 2, elemId: `4@${actor}`, opId: `4@${actor}`, value: {type: 'value', value: 'greenfinch'}}\n          ]}}\n        }}\n      })\n      assert.deepStrictEqual(doc4, {birds: ['chaffinch', 'goldfinch', 'greenfinch', 'bullfinch']})\n      assert.deepStrictEqual(getRequests(doc4), [])\n    })\n\n    it('should allow interleaving of patches and changes', () => {\n      const actor = uuid()\n      const [doc1, change1] = Frontend.change(Frontend.init(actor), doc => doc.number = 1)\n      const [doc2, change2] = Frontend.change(doc1, doc => doc.number = 2)\n      assert.deepStrictEqual(change1, {\n        actor, deps: [], startOp: 1, seq: 1, time: change1.time, message: '', ops: [\n          {obj: '_root', action: 'set', key: 'number', insert: false, datatype: 'int', value: 1, pred: []}\n        ]\n      })\n      assert.deepStrictEqual(change2, {\n        actor, deps: [], startOp: 2, seq: 2, time: change2.time, message: '', ops: [\n          {obj: '_root', action: 'set', key: 'number', insert: false, datatype: 'int', value: 2, pred: [`1@${actor}`]}\n        ]\n      })\n      const state0 = Backend.init()\n      const [/* state1 */, patch1, /* binChange1 */] = Backend.applyLocalChange(state0, change1)\n      const doc2a = Frontend.applyPatch(doc2, patch1)\n      const [/* doc3 */, change3] = Frontend.change(doc2a, doc => doc.number = 3)\n      assert.deepStrictEqual(change3, {\n        actor, seq: 3, startOp: 3, time: change3.time, message: '', deps: [], ops: [\n          {obj: '_root', action: 'set', key: 'number', insert: false, datatype: 'int', value: 3, pred: [`2@${actor}`]}\n        ]\n      })\n    })\n\n    it('deps are filled in if the frontend does not have the latest patch', () => {\n      const actor1 = uuid(), actor2 = uuid()\n      const [/* doc1 */, change1] = Frontend.change(Frontend.init(actor1), doc => doc.number = 1)\n      const [/* state1 */, /* patch1 */, binChange1] = Backend.applyLocalChange(Backend.init(), change1)\n\n      const [state1a, patch1a] = Backend.applyChanges(Backend.init(), [binChange1])\n      const doc1a = Frontend.applyPatch(Frontend.init(actor2), patch1a)\n      const [doc2, change2] = Frontend.change(doc1a, doc => doc.number = 2)\n      const [doc3, change3] = Frontend.change(doc2, doc => doc.number = 3)\n      assert.deepStrictEqual(change2, {\n        actor: actor2, seq: 1, startOp: 2, deps: [decodeChange(binChange1).hash], time: change2.time, message: '', ops: [\n          {obj: '_root', action: 'set', key: 'number', insert: false, datatype: 'int', value: 2, pred: [`1@${actor1}`]}\n        ]\n      })\n      assert.deepStrictEqual(change3, {\n        actor: actor2, seq: 2, startOp: 3, deps: [], time: change3.time, message: '', ops: [\n          {obj: '_root', action: 'set', key: 'number', insert: false, datatype: 'int', value: 3, pred: [`2@${actor2}`]}\n        ]\n      })\n\n      const [state2, patch2, binChange2] = Backend.applyLocalChange(state1a, change2)\n      const [state3, patch3, binChange3] = Backend.applyLocalChange(state2, change3)\n      assert.deepStrictEqual(decodeChange(binChange2).deps, [decodeChange(binChange1).hash])\n      assert.deepStrictEqual(decodeChange(binChange3).deps, [decodeChange(binChange2).hash])\n      assert.deepStrictEqual(patch1a.deps, [decodeChange(binChange1).hash])\n      assert.deepStrictEqual(patch2.deps, [])\n\n      const doc2a = Frontend.applyPatch(doc3, patch2)\n      const doc3a = Frontend.applyPatch(doc2a, patch3)\n      const [/* doc4 */, change4] = Frontend.change(doc3a, doc => doc.number = 4)\n      assert.deepStrictEqual(change4, {\n        actor: actor2, seq: 3, startOp: 4, time: change4.time, message: '', deps: [], ops: [\n          {obj: '_root', action: 'set', key: 'number', insert: false, datatype: 'int', value: 4, pred: [`3@${actor2}`]}\n        ]\n      })\n      const [/* state4 */, /* patch4 */, binChange4] = Backend.applyLocalChange(state3, change4)\n      assert.deepStrictEqual(decodeChange(binChange4).deps, [decodeChange(binChange3).hash])\n    })\n  })\n\n  describe('applying patches', () => {\n    it('should set root object properties', () => {\n      const actor = uuid()\n      const patch = {\n        clock: {[actor]: 1},\n        diffs: {objectId: '_root', type: 'map', props: {bird: {[actor]: {type: 'value', value: 'magpie'}}}}\n      }\n      const doc = Frontend.applyPatch(Frontend.init(), patch)\n      assert.deepStrictEqual(doc, {bird: 'magpie'})\n    })\n\n    it('should reveal conflicts on root object properties', () => {\n      const patch = {\n        clock: {actor1: 1, actor2: 1},\n        diffs: {objectId: '_root', type: 'map', props: {\n          favoriteBird: {actor1: {type: 'value', value: 'robin'}, actor2: {type: 'value', value: 'wagtail'}}\n        }}\n      }\n      const doc = Frontend.applyPatch(Frontend.init(), patch)\n      assert.deepStrictEqual(doc, {favoriteBird: 'wagtail'})\n      assert.deepStrictEqual(Frontend.getConflicts(doc, 'favoriteBird'), {actor1: 'robin', actor2: 'wagtail'})\n    })\n\n    it('should create nested maps', () => {\n      const birds = uuid(), actor = uuid()\n      const patch = {\n        clock: {[actor]: 1},\n        diffs: {objectId: '_root', type: 'map', props: {birds: {[actor]: {\n          objectId: birds, type: 'map', props: {wrens: {[actor]: {value: 3}}}\n        }}}}\n      }\n      const doc = Frontend.applyPatch(Frontend.init(), patch)\n      assert.deepStrictEqual(doc, {birds: {wrens: 3}})\n    })\n\n    it('should apply updates inside nested maps', () => {\n      const birds = uuid(), actor = uuid()\n      const patch1 = {\n        clock: {[actor]: 1},\n        diffs: {objectId: '_root', type: 'map', props: {birds: {[actor]: {\n          objectId: birds, type: 'map', props: {wrens: {[actor]: {type: 'value', value: 3}}}\n        }}}}\n      }\n      const patch2 = {\n        clock: {[actor]: 2},\n        diffs: {objectId: '_root', type: 'map', props: {birds: {[actor]: {\n          objectId: birds, type: 'map', props: {sparrows: {[actor]: {type: 'value', value: 15}}}\n        }}}}\n      }\n      const doc1 = Frontend.applyPatch(Frontend.init(), patch1)\n      const doc2 = Frontend.applyPatch(doc1, patch2)\n      assert.deepStrictEqual(doc1, {birds: {wrens: 3}})\n      assert.deepStrictEqual(doc2, {birds: {wrens: 3, sparrows: 15}})\n    })\n\n    it('should apply updates inside map key conflicts', () => {\n      const birds1 = uuid(), birds2 = uuid()\n      const patch1 = {\n        clock: {[birds1]: 1, [birds2]: 1},\n        diffs: {objectId: '_root', type: 'map', props: {favoriteBirds: {\n          actor1: {objectId: birds1, type: 'map', props: {blackbirds: {actor1: {type: 'value', value: 1}}}},\n          actor2: {objectId: birds2, type: 'map', props: {wrens:      {actor2: {type: 'value', value: 3}}}}\n        }}}\n      }\n      const patch2 = {\n        clock: {[birds1]: 2, [birds2]: 1},\n        diffs: {objectId: '_root', type: 'map', props: {favoriteBirds: {\n          actor1: {objectId: birds1, type: 'map', props: {blackbirds: {actor1: {value: 2}}}},\n          actor2: {objectId: birds2, type: 'map'}\n        }}}\n      }\n      const doc1 = Frontend.applyPatch(Frontend.init(), patch1)\n      const doc2 = Frontend.applyPatch(doc1, patch2)\n      assert.deepStrictEqual(doc1, {favoriteBirds: {wrens: 3}})\n      assert.deepStrictEqual(doc2, {favoriteBirds: {wrens: 3}})\n      assert.deepStrictEqual(Frontend.getConflicts(doc1, 'favoriteBirds'), {actor1: {blackbirds: 1}, actor2: {wrens: 3}})\n      assert.deepStrictEqual(Frontend.getConflicts(doc2, 'favoriteBirds'), {actor1: {blackbirds: 2}, actor2: {wrens: 3}})\n    })\n\n    it('should structure-share unmodified objects', () => {\n      const birds = uuid(), mammals = uuid(), actor = uuid()\n      const patch1 = {\n        clock: {[actor]: 1},\n        diffs: {objectId: '_root', type: 'map', props: {\n          birds:   {[actor]: {objectId: birds,     type: 'map', props: {wrens:   {[actor]: {value: 3}}}}},\n          mammals: {[actor]: {objectId: mammals,   type: 'map', props: {badgers: {[actor]: {value: 1}}}}}\n        }}\n      }\n      const patch2 = {\n        clock: {[actor]: 2},\n        diffs: {objectId: '_root', type: 'map', props: {\n          birds:   {[actor]: {objectId: birds,     type: 'map', props: {sparrows: {[actor]: {value: 15}}}}}\n        }}\n      }\n      const doc1 = Frontend.applyPatch(Frontend.init(), patch1)\n      const doc2 = Frontend.applyPatch(doc1, patch2)\n      assert.deepStrictEqual(doc1, {birds: {wrens: 3}, mammals: {badgers: 1}})\n      assert.deepStrictEqual(doc2, {birds: {wrens: 3, sparrows: 15}, mammals: {badgers: 1}})\n      assert.strictEqual(doc1.mammals, doc2.mammals)\n    })\n\n    it('should delete keys in maps', () => {\n      const actor = uuid()\n      const patch1 = {\n        clock: {[actor]: 1},\n        diffs: {objectId: '_root', type: 'map', props: {\n          magpies: {[actor]: {value: 2}}, sparrows: {[actor]: {value: 15}}\n        }}\n      }\n      const patch2 = {\n        clock: {[actor]: 2},\n        diffs: {objectId: '_root', type: 'map', props: {\n          magpies: {}\n        }}\n      }\n      const doc1 = Frontend.applyPatch(Frontend.init(), patch1)\n      const doc2 = Frontend.applyPatch(doc1, patch2)\n      assert.deepStrictEqual(doc1, {magpies: 2, sparrows: 15})\n      assert.deepStrictEqual(doc2, {sparrows: 15})\n    })\n\n    it('should create lists', () => {\n      const birds = uuid(), actor = uuid()\n      const patch = {\n        clock: {[actor]: 1},\n        diffs: {objectId: '_root', type: 'map', props: {birds: {[actor]: {\n          objectId: birds, type: 'list', edits: [\n            {action: 'insert', index: 0, elemId: `2@${actor}`, opId: `2@${actor}`, value: {value: 'chaffinch'}}\n          ]\n        }}}}\n      }\n      const doc = Frontend.applyPatch(Frontend.init(), patch)\n      assert.deepStrictEqual(doc, {birds: ['chaffinch']})\n    })\n\n    it('should apply updates inside lists', () => {\n      const birds = uuid(), actor = uuid()\n      const patch1 = {\n        clock: {[actor]: 1},\n        diffs: {objectId: '_root', type: 'map', props: {birds: {[actor]: {\n          objectId: birds, type: 'list', edits: [\n            {action: 'insert', index: 0, elemId: `2@${actor}`, opId: `2@${actor}`, value: {value: 'chaffinch'}}\n          ]\n        }}}}\n      }\n      const patch2 = {\n        clock: {[actor]: 2},\n        diffs: {objectId: '_root', type: 'map', props: {birds: {[actor]: {\n          objectId: birds, type: 'list',\n          edits: [{action: 'update', index: 0, opId: `3@${actor}`, value: {value: 'greenfinch'}}]\n        }}}}\n      }\n      const doc1 = Frontend.applyPatch(Frontend.init(), patch1)\n      const doc2 = Frontend.applyPatch(doc1, patch2)\n      assert.deepStrictEqual(doc1, {birds: ['chaffinch']})\n      assert.deepStrictEqual(doc2, {birds: ['greenfinch']})\n    })\n\n    it('should apply updates inside list element conflicts', () => {\n      const actor1 = '01234567', actor2 = '89abcdef', birds = `1@${actor1}`\n      const patch1 = {\n        clock: {[actor1]: 2, [actor2]: 1}, diffs: {objectId: '_root', type: 'map', props: {birds: {[birds]: {\n          objectId: birds, type: 'list', edits: [\n            {action: 'insert', index: 0, elemId: `2@${actor1}`, opId: `2@${actor1}`, value: {\n              objectId: `2@${actor1}`, type: 'map', props: {\n                species: {[`3@${actor1}`]: {type: 'value', value: 'woodpecker'}},\n                numSeen: {[`4@${actor1}`]: {type: 'value', value: 1}}\n              }\n            }},\n            {action: 'update', index: 0, opId: `2@${actor2}`, value: {\n              objectId: `2@${actor2}`, type: 'map', props: {\n                species: {[`3@${actor2}`]: {type: 'value', value: 'lapwing'}},\n                numSeen: {[`4@${actor2}`]: {type: 'value', value: 2}}\n              }\n            }}\n          ]\n        }}}}\n      }\n      const patch2 = {\n        clock: {[actor1]: 3, [actor2]: 1}, diffs: {objectId: '_root', type: 'map', props: {birds: {[birds]: {\n          objectId: birds, type: 'list', edits: [\n            {action: 'update', index: 0, opId: `2@${actor1}`, value: {\n              objectId: `2@${actor1}`, type: 'map', props: {\n                numSeen: {[`5@${actor1}`]: {type: 'value', value: 2}}\n              }\n            }},\n            {action: 'update', index: 0, opId: `2@${actor2}`, value: {\n              objectId: `2@${actor2}`, type: 'map', props: {}\n            }}\n          ]\n        }}}}\n      }\n      const patch3 = {\n        clock: {[actor1]: 3, [actor2]: 1}, diffs: {objectId: '_root', type: 'map', props: {birds: {[birds]: {\n          objectId: birds, type: 'list', edits: [\n            {action: 'update', index: 0, opId: `2@${actor1}`, value: {\n              objectId: `2@${actor1}`, type: 'map', props: {\n                numSeen: {[`6@${actor1}`]: {type: 'value', value: 2}}\n              }\n            }}\n          ]\n        }}}}\n      }\n      const doc1 = Frontend.applyPatch(Frontend.init(), patch1)\n      const doc2 = Frontend.applyPatch(doc1, patch2)\n      const doc3 = Frontend.applyPatch(doc2, patch3)\n      assert.deepStrictEqual(doc1, {birds: [{species: 'lapwing', numSeen: 2}]})\n      assert.deepStrictEqual(doc2, {birds: [{species: 'lapwing', numSeen: 2}]})\n      assert.deepStrictEqual(doc3, {birds: [{species: 'woodpecker', numSeen: 2}]})\n      assert.strictEqual(doc1.birds[0], doc2.birds[0])\n      assert.deepStrictEqual(Frontend.getConflicts(doc1.birds, 0), {\n        [`2@${actor1}`]: {species: 'woodpecker', numSeen: 1},\n        [`2@${actor2}`]: {species: 'lapwing',    numSeen: 2}\n      })\n      assert.deepStrictEqual(Frontend.getConflicts(doc2.birds, 0), {\n        [`2@${actor1}`]: {species: 'woodpecker', numSeen: 2},\n        [`2@${actor2}`]: {species: 'lapwing',    numSeen: 2}\n      })\n      assert.deepStrictEqual(Frontend.getConflicts(doc3.birds, 0), undefined)\n    })\n\n    it('should apply multiinserts on lists', () => {\n      const actor = uuid()\n      const patch1 = {\n        clock: {[actor]: 1}, diffs: {objectId: '_root', type: 'map', props: {birds: {[`@${actor}`]: {\n          objectId: `1@${actor}`, type: 'list', edits: [\n            {action: 'multi-insert', index: 0, elemId: `2@${actor}`, values: [\"chaffinch\", \"goldfinch\", \"wren\"]}\n          ]\n        }}}}\n      }\n      const doc = Frontend.applyPatch(Frontend.init(), patch1)\n      assert.deepStrictEqual(doc, {birds: [\"chaffinch\", \"goldfinch\", \"wren\"]})\n    })\n\n    it('should delete list elements', () => {\n      const birds = uuid(), actor = uuid()\n      const patch1 = {\n        clock: {[actor]: 1},\n        diffs: {objectId: '_root', type: 'map', props: {birds: {[`1@${actor}`]: {\n          objectId: birds, type: 'list',\n          edits: [\n            {action: 'insert', index: 0, elemId: `2@${actor}`, opId: `2@${actor}`, value: {value: 'chaffinch'}},\n            {action: 'insert', index: 1, elemId: `3@${actor}`, opId: `3@${actor}`, value: {value: 'goldfinch'}}\n          ]\n        }}}}\n      }\n      const patch2 = {\n        clock: {[actor]: 2},\n        diffs: {objectId: '_root', type: 'map', props: {birds: {[`1@${actor}`]: {\n          objectId: birds, type: 'list', props: {},\n          edits: [{action: 'remove', index: 0, count: 1}]\n        }}}}\n      }\n      const doc1 = Frontend.applyPatch(Frontend.init(), patch1)\n      const doc2 = Frontend.applyPatch(doc1, patch2)\n      assert.deepStrictEqual(doc1, {birds: ['chaffinch', 'goldfinch']})\n      assert.deepStrictEqual(doc2, {birds: ['goldfinch']})\n    })\n\n    it('should delete multiple list elements', () => {\n      const birds = uuid(), actor = uuid()\n      const patch1 = {\n        clock: {[actor]: 1},\n        diffs: {objectId: '_root', type: 'map', props: {birds: {[`1@${actor}`]: {\n          objectId: birds, type: 'list', edits: [\n            {action: 'insert', index: 0, elemId: `2@${actor}`, opId: `2@${actor}`, value: {value: 'chaffinch'}},\n            {action: 'insert', index: 1, elemId: `3@${actor}`, opId: `3@${actor}`, value: {value: 'goldfinch'}}\n          ]\n        }}}}\n      }\n      const patch2 = {\n        clock: {[actor]: 2},\n        diffs: {objectId: '_root', type: 'map', props: {birds: {[`1@${actor}`]: {\n          objectId: birds, type: 'list', props: {},\n          edits: [{action: 'remove', index: 0, count: 2}]\n        }}}}\n      }\n      const doc1 = Frontend.applyPatch(Frontend.init(), patch1)\n      const doc2 = Frontend.applyPatch(doc1, patch2)\n      assert.deepStrictEqual(doc1, {birds: ['chaffinch', 'goldfinch']})\n      assert.deepStrictEqual(doc2, {birds: []})\n    })\n\n    it('should apply updates at different levels of the object tree', () => {\n      const actor = uuid()\n      const patch1 = {\n        clock: {[actor]: 1},\n        diffs: {objectId: '_root', type: 'map', props: {\n          counts: {[`1@${actor}`]: {objectId: `1@${actor}`, type: 'map', props: {\n            magpies: {[`2@${actor}`]: {value: 2}}\n          }}},\n          details: {[`3@${actor}`]: {objectId: `3@${actor}`, type: 'list',\n            edits: [{action: 'insert', index: 0, elemId: `4@${actor}`, opId: `4@${actor}`, value: {\n              objectId: `4@${actor}`, type: 'map', props: {\n                species: {[`5@${actor}`]: {type: 'value', value: 'magpie'}},\n                family: {[`6@${actor}`]: {type: 'value', value: 'corvidae'}}\n              }\n            }}]}}\n        }}\n      }\n      const patch2 = {\n        clock: {[actor]: 2},\n        diffs: {objectId: '_root', type: 'map', props: {\n          counts: {[`1@${actor}`]: {objectId: `1@${actor}`, type: 'map', props: {\n            magpies: {[`7@${actor}`]: {type: 'value', value: 3}}\n          }}},\n          details: {[`3@${actor}`]: {objectId: `3@${actor}`, type: 'list', edits: [\n            {action: 'update', index: 0, opId: `4@${actor}`, value: {\n              objectId: `4@${actor}`, type: 'map', props: {\n                species: {[`8@${actor}`]: {type: 'value', value: 'Eurasian magpie'}}\n              }\n            }}\n          ]}}\n        }}\n      }\n      const doc1 = Frontend.applyPatch(Frontend.init(), patch1)\n      const doc2 = Frontend.applyPatch(doc1, patch2)\n      assert.deepStrictEqual(doc1, {counts: {magpies: 2}, details: [{species: 'magpie', family: 'corvidae'}]})\n      assert.deepStrictEqual(doc2, {counts: {magpies: 3}, details: [{species: 'Eurasian magpie', family: 'corvidae'}]})\n    })\n  })\n\n  it('should create text objects', () => {\n    const actor = uuid()\n    const patch1 = {\n      clock: {[actor]: 1},\n      diffs: {objectId: '_root', type: 'map', props: {\n        text: {[`1@${actor}`]: {objectId: `1@${actor}`, type: 'text', edits: [\n          {action: 'insert', index: 0, elemId: `2@${actor}`, opId: `2@${actor}`, value: {type: 'value', value: '1'}},\n          {action: 'multi-insert', index: 1, elemId: `3@${actor}`, values: ['2', '3', '4']}\n        ]}}\n      }}\n    }\n    const doc = Frontend.applyPatch(Frontend.init(), patch1)\n    assert.deepStrictEqual(doc.text.toString(), '1234')\n  })\n})\n"
  },
  {
    "path": "test/fuzz_test.js",
    "content": "/**\n * Miniature implementation of a subset of Automerge, which is used below as definition of the\n * expected behaviour during fuzz testing. Supports the following:\n *  - only map, list, and primitive datatypes (no table, text, counter, or date objects)\n *  - no undo/redo\n *  - no conflicts on concurrent updates to the same field (uses last-writer-wins instead)\n *  - no API for creating new changes (you need to create change objects yourself)\n *  - no buffering of changes that are missing their causal dependencies\n *  - no saving or loading in serialised form\n *  - relies on object mutation (no immutability)\n */\nclass Micromerge {\n  constructor() {\n    this.byActor = {} // map from actorId to array of changes\n    this.byObjId = {_root: {}} // objects, keyed by the ID of the operation that created the object\n    this.metadata = {_root: {}} // map from objID to object with CRDT metadata for each object field\n  }\n\n  get root() {\n    return this.byObjId._root\n  }\n\n  /**\n   * Updates the document state by applying the change object `change`, in the format documented here:\n   * https://github.com/automerge/automerge/blob/performance/BINARY_FORMAT.md#json-representation-of-changes\n   */\n  applyChange(change) {\n    // Check that the change's dependencies are met\n    const lastSeq = this.byActor[change.actor] ? this.byActor[change.actor].length : 0\n    if (change.seq !== lastSeq + 1) {\n      throw new RangeError(`Expected sequence number ${lastSeq + 1}, got ${change.seq}`)\n    }\n    for (let [actor, dep] of Object.entries(change.deps || {})) {\n      if (!this.byActor[actor] || this.byActor[actor].length < dep) {\n        throw new RangeError(`Missing dependency: change ${dep} by actor ${actor}`)\n      }\n    }\n\n    if (!this.byActor[change.actor]) this.byActor[change.actor] = []\n    this.byActor[change.actor].push(change)\n\n    change.ops.forEach((op, index) => {\n      this.applyOp(Object.assign({opId: `${change.startOp + index}@${change.actor}`}, op))\n    })\n  }\n\n  /**\n   * Updates the document state with one of the operations from a change.\n   */\n  applyOp(op) {\n    if (!this.metadata[op.obj]) throw new RangeError(`Object does not exist: ${op.obj}`)\n    if (op.action === 'makeMap') {\n      this.byObjId[op.opId] = {}\n      this.metadata[op.opId] = {}\n    } else if (op.action === 'makeList') {\n      this.byObjId[op.opId] = []\n      this.metadata[op.opId] = []\n    } else if (op.action !== 'set' && op.action !== 'del') {\n      throw new RangeError(`Unsupported operation type: ${op.action}`)\n    }\n\n    if (Array.isArray(this.metadata[op.obj])) {\n      if (op.insert) this.applyListInsert(op); else this.applyListUpdate(op)\n    } else if (!this.metadata[op.obj][op.key] || this.compareOpIds(this.metadata[op.obj][op.key], op.opId)) {\n      this.metadata[op.obj][op.key] = op.opId\n      if (op.action === 'del') {\n        delete this.byObjId[op.obj][op.key]\n      } else if (op.action.startsWith('make')) {\n        this.byObjId[op.obj][op.key] = this.byObjId[op.opId]\n      } else {\n        this.byObjId[op.obj][op.key] = op.value\n      }\n    }\n  }\n\n  /**\n   * Applies a list insertion operation.\n   */\n  applyListInsert(op) {\n    const meta = this.metadata[op.obj]\n    const value = op.action.startsWith('make') ? this.byObjId[op.opId] : op.value\n    let {index, visible} =\n      (op.key === '_head') ? {index: -1, visible: 0} : this.findListElement(op.obj, op.key)\n    if (index >= 0 && !meta[index].deleted) visible++\n    index++\n    while (index < meta.length && this.compareOpIds(op.opId, meta[index].elemId)) {\n      if (!meta[index].deleted) visible++\n      index++\n    }\n    meta.splice(index, 0, {elemId: op.opId, valueId: op.opId, deleted: false})\n    this.byObjId[op.obj].splice(visible, 0, value)\n  }\n\n  /**\n   * Applies a list element update (setting the value of a list element, or deleting a list element).\n   */\n  applyListUpdate(op) {\n    const {index, visible} = this.findListElement(op.obj, op.key)\n    const meta = this.metadata[op.obj][index]\n    if (op.action === 'del') {\n      if (!meta.deleted) this.byObjId[op.obj].splice(visible, 1)\n      meta.deleted = true\n    } else if (this.compareOpIds(meta.valueId, op.opId)) {\n      if (!meta.deleted) {\n        this.byObjId[op.obj][visible] = op.action.startsWith('make') ? this.byObjId[op.opId] : op.value\n      }\n      meta.valueId = op.opId\n    }\n  }\n\n  /**\n   * Searches for the list element with ID `elemId` in the object with ID `objId`. Returns an object\n   * `{index, visible}` where `index` is the index of the element in the metadata array, and\n   * `visible` is the number of non-deleted elements that precede the specified element.\n   */\n  findListElement(objectId, elemId) {\n    let index = 0, visible = 0, meta = this.metadata[objectId]\n    while (index < meta.length && meta[index].elemId !== elemId) {\n      if (!meta[index].deleted) visible++\n      index++\n    }\n    if (index === meta.length) throw new RangeError(`List element not found: ${elemId}`)\n    return {index, visible}\n  }\n\n  /**\n   * Compares two operation IDs in the form `counter@actor`. Returns true if `id1` has a lower counter\n   * than `id2`, or if the counter values are the same and `id1` has an actorId that sorts\n   * lexicographically before the actorId of `id2`.\n   */\n  compareOpIds(id1, id2) {\n    const regex = /^([0-9]+)@(.*)$/\n    const match1 = regex.exec(id1), match2 = regex.exec(id2)\n    const counter1 = parseInt(match1[1], 10), counter2 = parseInt(match2[1], 10)\n    return (counter1 < counter2) || (counter1 === counter2 && match1[2] < match2[2])\n  }\n}\n\n\n/* TESTS */\n\nconst assert = require('assert')\n\nconst change1 = {actor: '1234', seq: 1, deps: {}, startOp: 1, ops: [\n  {action: 'set',      obj: '_root',  key: 'title',  insert: false, value: 'Hello'},\n  {action: 'makeList', obj: '_root',  key: 'tags',   insert: false},\n  {action: 'set',      obj: '2@1234', key: '_head',  insert: true,  value: 'foo'}\n]}\n\nconst change2 = {actor: '1234', seq: 2, deps: {}, startOp: 4, ops: [\n  {action: 'set',      obj: '_root',  key: 'title',  insert: false, value: 'Hello 1'},\n  {action: 'set',      obj: '2@1234', key: '3@1234', insert: true,  value: 'bar'},\n  {action: 'del',      obj: '2@1234', key: '3@1234', insert: false}\n]}\n\nconst change3 = {actor: 'abcd', seq: 1, deps: {'1234': 1}, startOp: 4, ops: [\n  {action: 'set',      obj: '_root',  key: 'title',  insert: false, value: 'Hello 2'},\n  {action: 'set',      obj: '2@1234', key: '3@1234', insert: true,  value: 'baz'}\n]}\n\nlet doc1 = new Micromerge(), doc2 = new Micromerge()\nfor (let c of [change1, change2, change3]) doc1.applyChange(c)\nfor (let c of [change1, change3, change2]) doc2.applyChange(c)\nassert.deepStrictEqual(doc1.root, {title: 'Hello 2', tags: ['baz', 'bar']})\nassert.deepStrictEqual(doc2.root, {title: 'Hello 2', tags: ['baz', 'bar']})\n\nconst change4 = {actor: '2345', seq: 1, deps: {}, startOp: 1, ops: [\n  {action: 'makeList', obj: '_root',  key: 'todos',  insert: false},\n  {action: 'set',      obj: '1@2345', key: '_head',  insert: true,  value: 'Task 1'},\n  {action: 'set',      obj: '1@2345', key: '2@2345', insert: true,  value: 'Task 2'}\n]}\n\nlet doc3 = new Micromerge()\ndoc3.applyChange(change4)\nassert.deepStrictEqual(doc3.root, {todos: ['Task 1', 'Task 2']})\n\nconst change5 = {actor: '2345', seq: 2, deps: {}, startOp: 4, ops: [\n  {action: 'del',      obj: '1@2345', key: '2@2345', insert: false},\n  {action: 'set',      obj: '1@2345', key: '3@2345', insert: true,  value: 'Task 3'}\n]}\ndoc3.applyChange(change5)\nassert.deepStrictEqual(doc3.root, {todos: ['Task 2', 'Task 3']})\n\nconst change6 = {actor: '2345', seq: 3, deps: {}, startOp: 6, ops: [\n  {action: 'del',      obj: '1@2345', key: '3@2345', insert: false},\n  {action: 'set',      obj: '1@2345', key: '5@2345', insert: false, value: 'Task 3b'},\n  {action: 'set',      obj: '1@2345', key: '5@2345', insert: true,  value: 'Task 4'}\n]}\ndoc3.applyChange(change6)\nassert.deepStrictEqual(doc3.root, {todos: ['Task 3b', 'Task 4']})\n"
  },
  {
    "path": "test/helpers.js",
    "content": "const assert = require('assert')\nconst { Encoder } = require('../backend/encoding')\n\n// Assertion that succeeds if the first argument deepStrictEquals at least one of the\n// subsequent arguments (but we don't care which one)\nfunction assertEqualsOneOf(actual, ...expected) {\n  assert(expected.length > 0)\n  for (let i = 0; i < expected.length; i++) {\n    try {\n      assert.deepStrictEqual(actual, expected[i])\n      return // if we get here without an exception, that means success\n    } catch (e) {\n      if (!e.name.match(/^AssertionError/) || i === expected.length - 1) throw e\n    }\n  }\n}\n\n/**\n * Asserts that the byte array maintained by `encoder` contains the same byte\n * sequence as the array `bytes`.\n */\nfunction checkEncoded(encoder, bytes, detail) {\n  const encoded = (encoder instanceof Encoder) ? encoder.buffer : encoder\n  const expected = new Uint8Array(bytes)\n  const message = (detail ? `${detail}: ` : '') + `${encoded} expected to equal ${expected}`\n  assert(encoded.byteLength === expected.byteLength, message)\n  for (let i = 0; i < encoded.byteLength; i++) {\n    assert(encoded[i] === expected[i], message)\n  }\n}\n\nmodule.exports = { assertEqualsOneOf, checkEncoded }\n"
  },
  {
    "path": "test/new_backend_test.js",
    "content": "const assert = require('assert')\nconst { checkEncoded } = require('./helpers')\nconst { DOC_OPS_COLUMNS, encodeChange, decodeChange } = require('../backend/columnar')\nconst { MAX_BLOCK_SIZE, BackendDoc, bloomFilterContains } = require('../backend/new')\nconst uuid = require('../src/uuid')\n\nfunction checkColumns(block, expectedCols) {\n  for (let actual of block.columns) {\n    const {columnName} = DOC_OPS_COLUMNS.find(({columnId}) => columnId === actual.columnId) || {columnName: actual.columnId.toString()}\n    if (expectedCols[columnName]) {\n      checkEncoded(actual.decoder.buf, expectedCols[columnName], `${columnName} column`)\n    } else if (columnName !== 'chldActor' && columnName !== 'chldCtr') {\n      throw new Error(`Unexpected column ${columnName}`)\n    }\n  }\n  for (let expectedName of Object.keys(expectedCols)) {\n    const {columnId} = DOC_OPS_COLUMNS.find(({columnName}) => columnName === expectedName) || {columnId: parseInt(expectedName, 10)}\n    if (!block.columns.find(actual => actual.columnId === columnId)) {\n      throw new Error(`Missing column ${expectedName}`)\n    }\n  }\n}\n\nfunction hash(change) {\n  return decodeChange(encodeChange(change)).hash\n}\n\n\ndescribe('BackendDoc applying changes', () => {\n  it('should overwrite root object properties (1)', () => {\n    const actor = uuid()\n    const change1 = {actor, seq: 1, startOp: 1, time: 0, deps: [], ops: [\n      {action: 'set', obj: '_root', key: 'x', datatype: 'uint', value: 3, pred: []},\n      {action: 'set', obj: '_root', key: 'y', datatype: 'uint', value: 4, pred: []}\n    ]}\n    const change2 = {actor, seq: 2, startOp: 3, time: 0, deps: [hash(change1)], ops: [\n      {action: 'set', obj: '_root', key: 'x', datatype: 'uint', value: 5, pred: [`1@${actor}`]}\n    ]}\n    const backend = new BackendDoc()\n    assert.deepStrictEqual(backend.applyChanges([encodeChange(change1)]), {\n      maxOp: 2, clock: {[actor]: 1}, deps: [hash(change1)], pendingChanges: 0,\n      diffs: {objectId: '_root', type: 'map', props: {\n        x: {[`1@${actor}`]: {type: 'value', value: 3, datatype: 'uint'}},\n        y: {[`2@${actor}`]: {type: 'value', value: 4, datatype: 'uint'}}\n      }}\n    })\n    assert.deepStrictEqual(backend.applyChanges([encodeChange(change2)]), {\n      maxOp: 3, clock: {[actor]: 2}, deps: [hash(change2)], pendingChanges: 0,\n      diffs: {objectId: '_root', type: 'map', props: {\n        x: {[`3@${actor}`]: {type: 'value', value: 5, datatype: 'uint'}}\n      }}\n    })\n    checkColumns(backend.blocks[0], {\n      objActor: [],\n      objCtr:   [],\n      keyActor: [],\n      keyCtr:   [],\n      keyStr:   [2, 1, 0x78, 0x7f, 1, 0x79], // 'x', 'x', 'y'\n      idActor:  [3, 0],\n      idCtr:    [0x7d, 1, 2, 0x7f], // 1, 3, 2\n      insert:   [3],\n      action:   [3, 1],\n      valLen:   [3, 0x13],\n      valRaw:   [3, 5, 4],\n      succNum:  [0x7f, 1, 2, 0],\n      succActor: [0x7f, 0],\n      succCtr:   [0x7f, 3]\n    })\n    assert.strictEqual(backend.blocks[0].lastKey, 'y')\n    assert.strictEqual(backend.blocks[0].numOps, 3)\n    assert.strictEqual(backend.blocks[0].lastObjectActor, null)\n    assert.strictEqual(backend.blocks[0].lastObjectCtr, null)\n  })\n\n  it('should overwrite root object properties (2)', () => {\n    const actor = uuid()\n    const change1 = {actor, seq: 1, startOp: 1, time: 0, deps: [], ops: [\n      {action: 'set', obj: '_root', key: 'x', datatype: 'uint', value: 3, pred: []},\n      {action: 'set', obj: '_root', key: 'y', datatype: 'uint', value: 4, pred: []}\n    ]}\n    const change2 = {actor, seq: 2, startOp: 3, time: 0, deps: [hash(change1)], ops: [\n      {action: 'set', obj: '_root', key: 'y', datatype: 'uint', value: 5, pred: [`2@${actor}`]},\n      {action: 'set', obj: '_root', key: 'z', datatype: 'uint', value: 6, pred: []}\n    ]}\n    const backend = new BackendDoc()\n    assert.deepStrictEqual(backend.applyChanges([encodeChange(change1)]), {\n      maxOp: 2, clock: {[actor]: 1}, deps: [hash(change1)], pendingChanges: 0,\n      diffs: {objectId: '_root', type: 'map', props: {\n        x: {[`1@${actor}`]: {type: 'value', value: 3, datatype: 'uint'}},\n        y: {[`2@${actor}`]: {type: 'value', value: 4, datatype: 'uint'}}\n      }}\n    })\n    assert.deepStrictEqual(backend.applyChanges([encodeChange(change2)]), {\n      maxOp: 4, clock: {[actor]: 2}, deps: [hash(change2)], pendingChanges: 0,\n      diffs: {objectId: '_root', type: 'map', props: {\n        y: {[`3@${actor}`]: {type: 'value', value: 5, datatype: 'uint'}},\n        z: {[`4@${actor}`]: {type: 'value', value: 6, datatype: 'uint'}}\n      }}\n    })\n    checkColumns(backend.blocks[0], {\n      objActor: [],\n      objCtr:   [],\n      keyActor: [],\n      keyCtr:   [],\n      keyStr:   [0x7f, 1, 0x78, 2, 1, 0x79, 0x7f, 1, 0x7a], // 'x', 'y', 'y', 'z'\n      idActor:  [4, 0],\n      idCtr:    [4, 1],\n      insert:   [4],\n      action:   [4, 1],\n      valLen:   [4, 0x13],\n      valRaw:   [3, 4, 5, 6],\n      succNum:  [0x7e, 0, 1, 2, 0],\n      succActor: [0x7f, 0],\n      succCtr:   [0x7f, 3]\n    })\n    assert.strictEqual(backend.blocks[0].lastKey, 'z')\n    assert.strictEqual(backend.blocks[0].numOps, 4)\n    assert.strictEqual(backend.blocks[0].lastObjectActor, null)\n    assert.strictEqual(backend.blocks[0].lastObjectCtr, null)\n  })\n\n  it('should allow concurrent overwrites of the same value', () => {\n    const actor1 = '01234567', actor2 = '89abcdef', actor3 = 'fedcba98'\n    const change1 = {actor: actor1, seq: 1, startOp: 1, time: 0, deps: [], ops: [\n      {action: 'set', obj: '_root', key: 'x', datatype: 'uint', value: 1, pred: []}\n    ]}\n    const change2 = {actor: actor1, seq: 2, startOp: 2, time: 0, deps: [hash(change1)], ops: [\n      {action: 'set', obj: '_root', key: 'x', datatype: 'uint', value: 2, pred: [`1@${actor1}`]}\n    ]}\n    const change3 = {actor: actor2, seq: 1, startOp: 2, time: 0, deps: [hash(change1)], ops: [\n      {action: 'set', obj: '_root', key: 'x', datatype: 'uint', value: 3, pred: [`1@${actor1}`]}\n    ]}\n    const change4 = {actor: actor3, seq: 1, startOp: 2, time: 0, deps: [hash(change1)], ops: [\n      {action: 'set', obj: '_root', key: 'x', datatype: 'uint', value: 4, pred: [`1@${actor1}`]}\n    ]}\n    const backend1 = new BackendDoc(), backend2 = new BackendDoc()\n    backend1.applyChanges([encodeChange(change1)])\n    assert.deepStrictEqual(backend1.applyChanges([encodeChange(change2)]), {\n      maxOp: 2, clock: {[actor1]: 2}, deps: [hash(change2)], pendingChanges: 0,\n      diffs: {objectId: '_root', type: 'map', props: {x: {\n        [`2@${actor1}`]: {type: 'value', value: 2, datatype: 'uint'}\n      }}}\n    })\n    assert.deepStrictEqual(backend1.applyChanges([encodeChange(change3)]), {\n      maxOp: 2, clock: {[actor1]: 2, [actor2]: 1}, deps: [hash(change2), hash(change3)].sort(), pendingChanges: 0,\n      diffs: {objectId: '_root', type: 'map', props: { x: {\n        [`2@${actor1}`]: {type: 'value', value: 2, datatype: 'uint'},\n        [`2@${actor2}`]: {type: 'value', value: 3, datatype: 'uint'}\n      }}}\n    })\n    assert.deepStrictEqual(backend1.applyChanges([encodeChange(change4)]), {\n      maxOp: 2, clock: {[actor1]: 2, [actor2]: 1, [actor3]: 1}, pendingChanges: 0,\n      deps: [hash(change2), hash(change3), hash(change4)].sort(),\n      diffs: {objectId: '_root', type: 'map', props: {x: {\n        [`2@${actor1}`]: {type: 'value', value: 2, datatype: 'uint'},\n        [`2@${actor2}`]: {type: 'value', value: 3, datatype: 'uint'},\n        [`2@${actor3}`]: {type: 'value', value: 4, datatype: 'uint'}\n      }}}\n    })\n    backend2.applyChanges([encodeChange(change1)])\n    assert.deepStrictEqual(backend2.applyChanges([encodeChange(change4)]), {\n      maxOp: 2, clock: {[actor1]: 1, [actor3]: 1}, deps: [hash(change4)], pendingChanges: 0,\n      diffs: {objectId: '_root', type: 'map', props: {x: {\n        [`2@${actor3}`]: {type: 'value', value: 4, datatype: 'uint'}\n      }}}\n    })\n    assert.deepStrictEqual(backend2.applyChanges([encodeChange(change3)]), {\n      maxOp: 2, clock: {[actor1]: 1, [actor2]: 1, [actor3]: 1}, pendingChanges: 0,\n      deps: [hash(change3), hash(change4)].sort(),\n      diffs: {objectId: '_root', type: 'map', props: {x: {\n        [`2@${actor2}`]: {type: 'value', value: 3, datatype: 'uint'},\n        [`2@${actor3}`]: {type: 'value', value: 4, datatype: 'uint'}\n      }}}\n    })\n    assert.deepStrictEqual(backend2.applyChanges([encodeChange(change2)]), {\n      maxOp: 2, clock: {[actor1]: 2, [actor2]: 1, [actor3]: 1}, pendingChanges: 0,\n      deps: [hash(change2), hash(change3), hash(change4)].sort(),\n      diffs: {objectId: '_root', type: 'map', props: {x: {\n        [`2@${actor1}`]: {type: 'value', value: 2, datatype: 'uint'},\n        [`2@${actor2}`]: {type: 'value', value: 3, datatype: 'uint'},\n        [`2@${actor3}`]: {type: 'value', value: 4, datatype: 'uint'}\n      }}}\n    })\n    checkColumns(backend1.blocks[0], {\n      objActor: [],\n      objCtr:   [],\n      keyActor: [],\n      keyCtr:   [],\n      keyStr:   [4, 1, 0x78], // 4x 'x'\n      idActor:  [2, 0, 0x7e, 1, 2], // 0, 0, 1, 2\n      idCtr:    [2, 1, 2, 0], // 1, 2, 2, 2\n      insert:   [4],\n      action:   [4, 1],\n      valLen:   [4, 0x13],\n      valRaw:   [1, 2, 3, 4],\n      succNum:  [0x7f, 3, 3, 0], // 3, 0, 0, 0\n      succActor: [0x7d, 0, 1, 2], // 0, 1, 2\n      succCtr:   [0x7f, 2, 2, 0] // 2, 2, 2\n    })\n    assert.strictEqual(backend1.blocks[0].lastKey, 'x')\n    assert.strictEqual(backend1.blocks[0].numOps, 4)\n    assert.strictEqual(backend1.blocks[0].lastObjectActor, null)\n    assert.strictEqual(backend1.blocks[0].lastObjectCtr, null)\n    // The two backends are not identical because actors appear in a different order\n    checkColumns(backend2.blocks[0], {\n      objActor: [],\n      objCtr:   [],\n      keyActor: [],\n      keyCtr:   [],\n      keyStr:   [4, 1, 0x78], // 4x 'x'\n      idActor:  [2, 0, 0x7e, 2, 1], // 0, 0, 2, 1 <-- different from backend1\n      idCtr:    [2, 1, 2, 0], // 1, 2, 2, 2\n      insert:   [4],\n      action:   [4, 1],\n      valLen:   [4, 0x13],\n      valRaw:   [1, 2, 3, 4],\n      succNum:  [0x7f, 3, 3, 0], // 3, 0, 0, 0\n      succActor: [0x7d, 0, 2, 1], // 0, 2, 1 <-- different from backend1\n      succCtr:   [0x7f, 2, 2, 0] // 2, 2, 2\n    })\n    assert.strictEqual(backend2.blocks[0].lastKey, 'x')\n    assert.strictEqual(backend2.blocks[0].numOps, 4)\n  })\n\n  it('should allow a conflict to be resolved', () => {\n    const actor1 = '01234567', actor2 = '89abcdef'\n    const change1 = {actor: actor1, seq: 1, startOp: 1, time: 0, deps: [], ops: [\n      {action: 'set', obj: '_root', key: 'x', datatype: 'uint', value: 1, pred: []}\n    ]}\n    const change2 = {actor: actor2, seq: 1, startOp: 1, time: 0, deps: [], ops: [\n      {action: 'set', obj: '_root', key: 'x', datatype: 'uint', value: 2, pred: []}\n    ]}\n    const change3 = {actor: actor1, seq: 2, startOp: 2, time: 0, deps: [hash(change1), hash(change2)], ops: [\n      {action: 'set', obj: '_root', key: 'x', datatype: 'uint', value: 3, pred: [`1@${actor1}`, `1@${actor2}`]}\n    ]}\n    const backend = new BackendDoc()\n    assert.deepStrictEqual(backend.applyChanges([encodeChange(change1)]), {\n      maxOp: 1, clock: {[actor1]: 1}, deps: [hash(change1)], pendingChanges: 0,\n      diffs: {objectId: '_root', type: 'map', props: {x: {\n        [`1@${actor1}`]: {type: 'value', value: 1, datatype: 'uint'}\n      }}}\n    })\n    assert.deepStrictEqual(backend.applyChanges([encodeChange(change2)]), {\n      maxOp: 1, clock: {[actor1]: 1, [actor2]: 1}, deps: [hash(change1), hash(change2)].sort(), pendingChanges: 0,\n      diffs: {objectId: '_root', type: 'map', props: {x: {\n        [`1@${actor1}`]: {type: 'value', value: 1, datatype: 'uint'},\n        [`1@${actor2}`]: {type: 'value', value: 2, datatype: 'uint'}\n      }}}\n    })\n    assert.deepStrictEqual(backend.applyChanges([encodeChange(change3)]), {\n      maxOp: 2, clock: {[actor1]: 2, [actor2]: 1}, deps: [hash(change3)], pendingChanges: 0,\n      diffs: {objectId: '_root', type: 'map', props: {x: {\n        [`2@${actor1}`]: {type: 'value', value: 3, datatype: 'uint'}\n      }}}\n    })\n    checkColumns(backend.blocks[0], {\n      objActor: [],\n      objCtr:   [],\n      keyActor: [],\n      keyCtr:   [],\n      keyStr:   [3, 1, 0x78], // 3x 'x'\n      idActor:  [0x7d, 0, 1, 0], // 0, 1, 0\n      idCtr:    [0x7d, 1, 0, 1], // 1, 1, 2\n      insert:   [3],\n      action:   [3, 1],\n      valLen:   [3, 0x13],\n      valRaw:   [1, 2, 3],\n      succNum:  [2, 1, 0x7f, 0], // 1, 1, 0\n      succActor: [2, 0],\n      succCtr:   [0x7e, 2, 0] // 2, 2\n    })\n    assert.strictEqual(backend.blocks[0].lastKey, 'x')\n    assert.strictEqual(backend.blocks[0].numOps, 3)\n  })\n\n  it('should throw an error if the predecessor operation does not exist (1)', () => {\n    const actor = uuid()\n    const change1 = {actor, seq: 1, startOp: 1, time: 0, deps: [], ops: [\n      {action: 'set', obj: '_root', key: 'x', datatype: 'uint', value: 1, pred: []},\n      {action: 'set', obj: '_root', key: 'y', datatype: 'uint', value: 2, pred: []}\n    ]}\n    const change2 = {actor, seq: 2, startOp: 3, time: 0, deps: [hash(change1)], ops: [\n      {action: 'set', obj: '_root', key: 'x', datatype: 'uint', value: 3, pred: [`2@${actor}`]}\n    ]}\n    const backend = new BackendDoc()\n    backend.applyChanges([encodeChange(change1)])\n    assert.throws(() => { backend.applyChanges([encodeChange(change2)]) }, /no matching operation for pred/)\n  })\n\n  it('should throw an error if the predecessor operation does not exist (2)', () => {\n    const actor1 = '01234567', actor2 = '89abcdef'\n    const change1 = {actor: actor1, seq: 1, startOp: 1, time: 0, deps: [], ops: [\n      {action: 'set', obj: '_root', key: 'x', datatype: 'uint', value: 1, pred: []}\n    ]}\n    const change2 = {actor: actor2, seq: 1, startOp: 1, time: 0, deps: [], ops: [\n      {action: 'set', obj: '_root', key: 'w', datatype: 'uint', value: 2, pred: []},\n      {action: 'set', obj: '_root', key: 'x', datatype: 'uint', value: 2, pred: []}\n    ]}\n    const change3 = {actor: actor1, seq: 2, startOp: 2, time: 0, deps: [hash(change1), hash(change2)], ops: [\n      {action: 'set', obj: '_root', key: 'x', datatype: 'uint', value: 3, pred: [`1@${actor2}`]}\n    ]}\n    const backend = new BackendDoc()\n    backend.applyChanges([encodeChange(change1)])\n    backend.applyChanges([encodeChange(change2)])\n    assert.throws(() => { backend.applyChanges([encodeChange(change3)]) }, /no matching operation for pred/)\n  })\n\n  it('should create and update nested maps', () => {\n    const actor = uuid()\n    const change1 = {actor, seq: 1, startOp: 1, time: 0, deps: [], ops: [\n      {action: 'makeMap', obj: '_root',      key: 'map',             pred: []},\n      {action: 'set',     obj: `1@${actor}`, key: 'x',   value: 'a', pred: []},\n      {action: 'set',     obj: `1@${actor}`, key: 'y',   value: 'b', pred: []},\n      {action: 'set',     obj: `1@${actor}`, key: 'z',   value: 'c', pred: []}\n    ]}\n    const change2 = {actor, seq: 2, startOp: 5, time: 0, deps: [hash(change1)], ops: [\n      {action: 'set',     obj: `1@${actor}`, key: 'y',    value: 'B', pred: [`3@${actor}`]}\n    ]}\n    const backend = new BackendDoc()\n    assert.deepStrictEqual(backend.applyChanges([encodeChange(change1)]), {\n      maxOp: 4, clock: {[actor]: 1}, deps: [hash(change1)], pendingChanges: 0,\n      diffs: {objectId: '_root', type: 'map', props: {map: {[`1@${actor}`]: {\n        objectId: `1@${actor}`, type: 'map', props: {\n          x: {[`2@${actor}`]: {type: 'value', value: 'a'}},\n          y: {[`3@${actor}`]: {type: 'value', value: 'b'}},\n          z: {[`4@${actor}`]: {type: 'value', value: 'c'}}\n        }\n      }}}}\n    })\n    assert.deepStrictEqual(backend.applyChanges([encodeChange(change2)]), {\n      maxOp: 5, clock: {[actor]: 2}, deps: [hash(change2)], pendingChanges: 0,\n      diffs: {objectId: '_root', type: 'map', props: {map: {[`1@${actor}`]: {\n        objectId: `1@${actor}`, type: 'map', props: {y: {[`5@${actor}`]: {type: 'value', value: 'B'}}}\n      }}}}\n    })\n    checkColumns(backend.blocks[0], {\n      objActor: [0, 1, 4, 0],\n      objCtr:   [0, 1, 4, 1],\n      keyActor: [],\n      keyCtr:   [],\n      keyStr:   [0x7e, 3, 0x6d, 0x61, 0x70, 1, 0x78, 2, 1, 0x79, 0x7f, 1, 0x7a], // 'map', 'x', 'y', 'y', 'z'\n      idActor:  [5, 0],\n      idCtr:    [3, 1, 0x7e, 2, 0x7f], // 1, 2, 3, 5, 4\n      insert:   [5],\n      action:   [0x7f, 0, 4, 1], // makeMap, 4x set\n      valLen:   [0x7f, 0, 4, 0x16], // null, 4x 1-byte string\n      valRaw:   [0x61, 0x62, 0x42, 0x63], // 'a', 'b', 'B', 'c'\n      succNum:  [2, 0, 0x7f, 1, 2, 0],\n      succActor: [0x7f, 0],\n      succCtr:   [0x7f, 5]\n    })\n    assert.strictEqual(backend.blocks[0].lastKey, 'z')\n    assert.strictEqual(backend.blocks[0].numOps, 5)\n    assert.strictEqual(backend.blocks[0].lastObjectActor, 0)\n    assert.strictEqual(backend.blocks[0].lastObjectCtr, 1)\n  })\n\n  it('should create nested maps several levels deep', () => {\n    const actor = uuid()\n    const change1 = {actor, seq: 1, startOp: 1, time: 0, deps: [], ops: [\n      {action: 'makeMap', obj: '_root',      key: 'a',           pred: []},\n      {action: 'makeMap', obj: `1@${actor}`, key: 'b',           pred: []},\n      {action: 'makeMap', obj: `2@${actor}`, key: 'c',           pred: []},\n      {action: 'set',     obj: `3@${actor}`, key: 'd', datatype: 'uint', value: 1, pred: []}\n    ]}\n    const change2 = {actor, seq: 2, startOp: 5, time: 0, deps: [hash(change1)], ops: [\n      {action: 'set',     obj: `3@${actor}`, key: 'd', datatype: 'uint', value: 2, pred: [`4@${actor}`]}\n    ]}\n    const backend = new BackendDoc()\n    assert.deepStrictEqual(backend.applyChanges([encodeChange(change1)]), {\n      maxOp: 4, clock: {[actor]: 1}, deps: [hash(change1)], pendingChanges: 0,\n      diffs: {objectId: '_root', type: 'map', props: {a: {[`1@${actor}`]: {\n        objectId: `1@${actor}`, type: 'map', props: {b: {[`2@${actor}`]: {\n          objectId: `2@${actor}`, type: 'map', props: {c: {[`3@${actor}`]: {\n            objectId: `3@${actor}`, type: 'map', props: {d: {[`4@${actor}`]: {\n              type: 'value', value: 1, datatype: 'uint'\n            }}}\n          }}}\n        }}}\n      }}}}\n    })\n    assert.deepStrictEqual(backend.applyChanges([encodeChange(change2)]), {\n      maxOp: 5, clock: {[actor]: 2}, deps: [hash(change2)], pendingChanges: 0,\n      diffs: {objectId: '_root', type: 'map', props: {a: {[`1@${actor}`]: {\n        objectId: `1@${actor}`, type: 'map', props: {b: {[`2@${actor}`]: {\n          objectId: `2@${actor}`, type: 'map', props: {c: {[`3@${actor}`]: {\n            objectId: `3@${actor}`, type: 'map', props: {d: {[`5@${actor}`]: {\n              type: 'value', value: 2, datatype: 'uint'\n            }}}\n          }}}\n        }}}\n      }}}}\n    })\n    checkColumns(backend.blocks[0], {\n      objActor: [0, 1, 4, 0],\n      objCtr:   [0, 1, 0x7e, 1, 2, 2, 3], // null, 1, 2, 3, 3\n      keyActor: [],\n      keyCtr:   [],\n      keyStr:   [0x7d, 1, 0x61, 1, 0x62, 1, 0x63, 2, 1, 0x64], // 'a', 'b', 'c', 'd', 'd'\n      idActor:  [5, 0],\n      idCtr:    [5, 1], // 1, 2, 3, 4, 5\n      insert:   [5],\n      action:   [3, 0, 2, 1], // 3x makeMap, 2x set\n      valLen:   [3, 0, 2, 0x13], // 3x null, 2x uint\n      valRaw:   [1, 2],\n      succNum:  [3, 0, 0x7e, 1, 0], // 0, 0, 0, 1, 0\n      succActor: [0x7f, 0],\n      succCtr:   [0x7f, 5]\n    })\n    assert.strictEqual(backend.blocks[0].lastKey, 'd')\n    assert.strictEqual(backend.blocks[0].numOps, 5)\n    assert.strictEqual(backend.blocks[0].lastObjectActor, 0)\n    assert.strictEqual(backend.blocks[0].lastObjectCtr, 3)\n  })\n\n  it('should create a text object', () => {\n    const actor = uuid()\n    const change1 = {actor, seq: 1, startOp: 1, time: 0, deps: [], ops: [\n      {action: 'makeText', obj: '_root',      key: 'text',     insert: false,             pred: []},\n      {action: 'set',      obj: `1@${actor}`, elemId: '_head', insert: true,  value: 'a', pred: []}\n    ]}\n    const backend = new BackendDoc()\n    assert.deepStrictEqual(backend.applyChanges([encodeChange(change1)]), {\n      maxOp: 2, clock: {[actor]: 1}, deps: [hash(change1)], pendingChanges: 0,\n      diffs: {objectId: '_root', type: 'map', props: {text: {[`1@${actor}`]: {\n        objectId: `1@${actor}`, type: 'text', edits: [\n          {action: 'insert', index: 0, elemId: `2@${actor}`, opId: `2@${actor}`, value: {type: 'value', value: 'a'}}\n        ]\n      }}}}\n    })\n    checkColumns(backend.blocks[0], {\n      objActor: [0, 1, 0x7f, 0],\n      objCtr:   [0, 1, 0x7f, 1],\n      keyActor: [],\n      keyCtr:   [0, 1, 0x7f, 0],\n      keyStr:   [0x7f, 4, 0x74, 0x65, 0x78, 0x74, 0, 1], // 'text', null\n      idActor:  [2, 0],\n      idCtr:    [2, 1],\n      insert:   [1, 1],\n      action:   [0x7e, 4, 1],\n      valLen:   [0x7e, 0, 0x16],\n      valRaw:   [0x61],\n      succNum:  [2, 0],\n      succActor: [],\n      succCtr:   []\n    })\n    assert.strictEqual(backend.blocks[0].numOps, 2)\n    assert.strictEqual(backend.blocks[0].lastKey, undefined)\n    assert.strictEqual(backend.blocks[0].numVisible, 1)\n    assert.strictEqual(backend.blocks[0].lastObjectActor, 0)\n    assert.strictEqual(backend.blocks[0].lastObjectCtr, 1)\n    assert.strictEqual(backend.blocks[0].firstVisibleActor, 0)\n    assert.strictEqual(backend.blocks[0].firstVisibleCtr, 2)\n    assert.strictEqual(backend.blocks[0].lastVisibleActor, 0)\n    assert.strictEqual(backend.blocks[0].lastVisibleCtr, 2)\n    assert.strictEqual(bloomFilterContains(backend.blocks[0].bloom, 0, 2), true)\n    assert.strictEqual(bloomFilterContains(backend.blocks[0].bloom, 0, 3), false)\n  })\n\n  it('should insert text characters', () => {\n    const actor = uuid()\n    const change1 = {actor, seq: 1, startOp: 1, time: 0, deps: [], ops: [\n      {action: 'makeText', obj: '_root',      key: 'text',          insert: false,             pred: []},\n      {action: 'set',      obj: `1@${actor}`, elemId: '_head',      insert: true,  value: 'a', pred: []},\n      {action: 'set',      obj: `1@${actor}`, elemId: `2@${actor}`, insert: true,  value: 'b', pred: []}\n    ]}\n    const change2 = {actor, seq: 2, startOp: 4, time: 0, deps: [hash(change1)], ops: [\n      {action: 'set',      obj: `1@${actor}`, elemId: `3@${actor}`, insert: true,  value: 'c', pred: []},\n      {action: 'set',      obj: `1@${actor}`, elemId: `4@${actor}`, insert: true,  value: 'd', pred: []}\n    ]}\n    const backend = new BackendDoc()\n    assert.deepStrictEqual(backend.applyChanges([encodeChange(change1)]), {\n      maxOp: 3, clock: {[actor]: 1}, deps: [hash(change1)], pendingChanges: 0,\n      diffs: {objectId: '_root', type: 'map', props: {text: {[`1@${actor}`]: {\n        objectId: `1@${actor}`, type: 'text', edits: [\n          {action: 'multi-insert', index: 0, elemId: `2@${actor}`, values: ['a', 'b']}\n        ]\n      }}}}\n    })\n    assert.deepStrictEqual(backend.applyChanges([encodeChange(change2)]), {\n      maxOp: 5, clock: {[actor]: 2}, deps: [hash(change2)], pendingChanges: 0,\n      diffs: {objectId: '_root', type: 'map', props: {text: {[`1@${actor}`]: {\n        objectId: `1@${actor}`, type: 'text', edits: [\n          {action: 'multi-insert', index: 2, elemId: `4@${actor}`, values: ['c', 'd']}\n        ]\n      }}}}\n    })\n    checkColumns(backend.blocks[0], {\n      objActor: [0, 1, 4, 0],\n      objCtr:   [0, 1, 4, 1],\n      keyActor: [0, 2, 3, 0],\n      keyCtr:   [0, 1, 0x7e, 0, 2, 2, 1], // null, 0, 2, 3, 4\n      keyStr:   [0x7f, 4, 0x74, 0x65, 0x78, 0x74, 0, 4], // 'text', 4x null\n      idActor:  [5, 0],\n      idCtr:    [5, 1],\n      insert:   [1, 4],\n      action:   [0x7f, 4, 4, 1], // makeText, 4x set\n      valLen:   [0x7f, 0, 4, 0x16], // null, 4x 1-byte string\n      valRaw:   [0x61, 0x62, 0x63, 0x64], // 'a', 'b', 'c', 'd'\n      succNum:  [5, 0],\n      succActor: [],\n      succCtr:   []\n    })\n    assert.strictEqual(backend.blocks[0].numOps, 5)\n    assert.strictEqual(backend.blocks[0].lastKey, undefined)\n    assert.strictEqual(backend.blocks[0].numVisible, 4)\n    assert.strictEqual(backend.blocks[0].lastObjectActor, 0)\n    assert.strictEqual(backend.blocks[0].lastObjectCtr, 1)\n    assert.strictEqual(backend.blocks[0].firstVisibleActor, 0)\n    assert.strictEqual(backend.blocks[0].firstVisibleCtr, 2)\n    assert.strictEqual(backend.blocks[0].lastVisibleActor, 0)\n    assert.strictEqual(backend.blocks[0].lastVisibleCtr, 5)\n    assert.strictEqual(bloomFilterContains(backend.blocks[0].bloom, 0, 2), true)\n    assert.strictEqual(bloomFilterContains(backend.blocks[0].bloom, 0, 3), true)\n    assert.strictEqual(bloomFilterContains(backend.blocks[0].bloom, 0, 4), true)\n    assert.strictEqual(bloomFilterContains(backend.blocks[0].bloom, 0, 5), true)\n    assert.strictEqual(bloomFilterContains(backend.blocks[0].bloom, 1, 2), false)\n  })\n\n  it('should throw an error if the reference element of an insertion does not exist', () => {\n    const actor = uuid()\n    const change1 = {actor, seq: 1, startOp: 1, time: 0, deps: [], ops: [\n      {action: 'makeText', obj: '_root',      key: 'text',          insert: false,             pred: []},\n      {action: 'set',      obj: `1@${actor}`, elemId: '_head',      insert: true,  value: 'a', pred: []},\n      {action: 'set',      obj: `1@${actor}`, elemId: `2@${actor}`, insert: true,  value: 'b', pred: []},\n      {action: 'makeMap',  obj: '_root',      key: 'map',           insert: false,             pred: []},\n      {action: 'set',      obj: `4@${actor}`, key: 'foo',           insert: false, value: 'c', pred: []}\n    ]}\n    const change2 = {actor, seq: 2, startOp: 6, time: 0, deps: [], ops: [\n      {action: 'set',      obj: `1@${actor}`, elemId: `4@${actor}`, insert: true,  value: 'd', pred: []}\n    ]}\n    const backend = new BackendDoc()\n    assert.deepStrictEqual(backend.applyChanges([encodeChange(change1)]), {\n      maxOp: 5, clock: {[actor]: 1}, deps: [hash(change1)], pendingChanges: 0,\n      diffs: {objectId: '_root', type: 'map', props: {\n        text: {[`1@${actor}`]: {\n          objectId: `1@${actor}`, type: 'text', edits: [\n            {action: 'multi-insert', index: 0, elemId: `2@${actor}`, values: ['a', 'b']}\n          ]\n        }},\n        map: {[`4@${actor}`]: {objectId: `4@${actor}`, type: 'map', props: {\n          foo: {[`5@${actor}`]: {type: 'value', value: 'c'}}\n        }}}\n      }}\n    })\n    assert.strictEqual(backend.blocks[0].lastObjectActor, 0)\n    assert.strictEqual(backend.blocks[0].lastObjectCtr, 4)\n    assert.throws(() => { backend.applyChanges([encodeChange(change2)]) }, /Reference element not found/)\n  })\n\n  it('should handle non-consecutive insertions', () => {\n    const actor = uuid()\n    const change1 = {actor, seq: 1, startOp: 1, time: 0, deps: [], ops: [\n      {action: 'makeText', obj: '_root',      key: 'text',          insert: false,             pred: []},\n      {action: 'set',      obj: `1@${actor}`, elemId: '_head',      insert: true,  value: 'a', pred: []},\n      {action: 'set',      obj: `1@${actor}`, elemId: `2@${actor}`, insert: true,  value: 'c', pred: []}\n    ]}\n    const change2 = {actor, seq: 2, startOp: 4, time: 0, deps: [hash(change1)], ops: [\n      {action: 'set',      obj: `1@${actor}`, elemId: `2@${actor}`, insert: true,  value: 'b', pred: []},\n      {action: 'set',      obj: `1@${actor}`, elemId: `3@${actor}`, insert: true,  value: 'd', pred: []}\n    ]}\n    const backend = new BackendDoc()\n    assert.deepStrictEqual(backend.applyChanges([encodeChange(change1)]), {\n      maxOp: 3, clock: {[actor]: 1}, deps: [hash(change1)], pendingChanges: 0,\n      diffs: {objectId: '_root', type: 'map', props: {text: {[`1@${actor}`]: {\n        objectId: `1@${actor}`, type: 'text', edits: [\n          {action: 'multi-insert', index: 0, elemId: `2@${actor}`, values: ['a', 'c']}\n        ]\n      }}}}\n    })\n    assert.deepStrictEqual(backend.applyChanges([encodeChange(change2)]), {\n      maxOp: 5, clock: {[actor]: 2}, deps: [hash(change2)], pendingChanges: 0,\n      diffs: {objectId: '_root', type: 'map', props: {text: {[`1@${actor}`]: {\n        objectId: `1@${actor}`, type: 'text', edits: [\n          {action: 'insert', index: 1, elemId: `4@${actor}`, opId: `4@${actor}`, value: {type: 'value', value: 'b'}},\n          {action: 'insert', index: 3, elemId: `5@${actor}`, opId: `5@${actor}`, value: {type: 'value', value: 'd'}}\n        ]\n      }}}}\n    })\n    checkColumns(backend.blocks[0], {\n      objActor: [0, 1, 4, 0],\n      objCtr:   [0, 1, 4, 1],\n      keyActor: [0, 2, 3, 0],\n      keyCtr:   [0, 1, 0x7c, 0, 2, 0, 1], // null, 0, 2, 2, 3\n      keyStr:   [0x7f, 4, 0x74, 0x65, 0x78, 0x74, 0, 4], // 'text', 4x null\n      idActor:  [5, 0],\n      idCtr:    [2, 1, 0x7d, 2, 0x7f, 2], // 1, 2, 4, 3, 5\n      insert:   [1, 4],\n      action:   [0x7f, 4, 4, 1], // makeText, 4x set\n      valLen:   [0x7f, 0, 4, 0x16], // null, 4x 1-byte string\n      valRaw:   [0x61, 0x62, 0x63, 0x64], // 'a', 'b', 'c', 'd'\n      succNum:  [5, 0],\n      succActor: [],\n      succCtr:   []\n    })\n    assert.strictEqual(backend.blocks[0].numOps, 5)\n    assert.strictEqual(backend.blocks[0].lastKey, undefined)\n    assert.strictEqual(backend.blocks[0].numVisible, 4)\n    assert.strictEqual(backend.blocks[0].lastObjectActor, 0)\n    assert.strictEqual(backend.blocks[0].lastObjectCtr, 1)\n    assert.strictEqual(backend.blocks[0].firstVisibleActor, 0)\n    assert.strictEqual(backend.blocks[0].firstVisibleCtr, 2)\n    assert.strictEqual(backend.blocks[0].lastVisibleActor, 0)\n    assert.strictEqual(backend.blocks[0].lastVisibleCtr, 5)\n  })\n\n  it('should delete the first character', () => {\n    const actor = uuid()\n    const change1 = {actor, seq: 1, startOp: 1, time: 0, deps: [], ops: [\n      {action: 'makeText', obj: '_root',      key: 'text',     insert: false,             pred: []},\n      {action: 'set',      obj: `1@${actor}`, elemId: '_head', insert: true,  value: 'a', pred: []}\n    ]}\n    const change2 = {actor, seq: 2, startOp: 3, time: 0, deps: [hash(change1)], ops: [\n      {action: 'del',      obj: `1@${actor}`, elemId: `2@${actor}`, pred: [`2@${actor}`]}\n    ]}\n    const backend = new BackendDoc()\n    assert.deepStrictEqual(backend.applyChanges([encodeChange(change1)]), {\n      maxOp: 2, clock: {[actor]: 1}, deps: [hash(change1)], pendingChanges: 0,\n      diffs: {objectId: '_root', type: 'map', props: {text: {[`1@${actor}`]: {\n        objectId: `1@${actor}`, type: 'text', edits: [\n          {action: 'insert', index: 0, elemId: `2@${actor}`, opId: `2@${actor}`, value: {type: 'value', value: 'a'}}\n        ]\n      }}}}\n    })\n    assert.deepStrictEqual(backend.applyChanges([encodeChange(change2)]), {\n      maxOp: 3, clock: {[actor]: 2}, deps: [hash(change2)], pendingChanges: 0,\n      diffs: {objectId: '_root', type: 'map', props: {text: {[`1@${actor}`]: {\n        objectId: `1@${actor}`, type: 'text', edits: [{action: 'remove', index: 0, count: 1}]\n      }}}}\n    })\n    checkColumns(backend.blocks[0], {\n      objActor: [0, 1, 0x7f, 0],\n      objCtr:   [0, 1, 0x7f, 1],\n      keyActor: [],\n      keyCtr:   [0, 1, 0x7f, 0],\n      keyStr:   [0x7f, 4, 0x74, 0x65, 0x78, 0x74, 0, 1], // 'text', null\n      idActor:  [2, 0],\n      idCtr:    [2, 1],\n      insert:   [1, 1],\n      action:   [0x7e, 4, 1],\n      valLen:   [0x7e, 0, 0x16],\n      valRaw:   [0x61],\n      succNum:  [0x7e, 0, 1],\n      succActor: [0x7f, 0],\n      succCtr:   [0x7f, 3]\n    })\n    assert.strictEqual(backend.blocks[0].numOps, 2)\n    assert.strictEqual(backend.blocks[0].numVisible, 0)\n    assert.strictEqual(backend.blocks[0].lastObjectActor, 0)\n    assert.strictEqual(backend.blocks[0].lastObjectCtr, 1)\n    assert.strictEqual(backend.blocks[0].firstVisibleActor, undefined)\n    assert.strictEqual(backend.blocks[0].firstVisibleCtr, undefined)\n    assert.strictEqual(backend.blocks[0].lastVisibleActor, undefined)\n    assert.strictEqual(backend.blocks[0].lastVisibleCtr, undefined)\n    assert.strictEqual(bloomFilterContains(backend.blocks[0].bloom, 0, 2), true)\n  })\n\n  it('should delete a character in the middle', () => {\n    const actor = uuid()\n    const change1 = {actor, seq: 1, startOp: 1, time: 0, deps: [], ops: [\n      {action: 'makeText', obj: '_root',      key: 'text',          insert: false,             pred: []},\n      {action: 'set',      obj: `1@${actor}`, elemId: '_head',      insert: true,  value: 'a', pred: []},\n      {action: 'set',      obj: `1@${actor}`, elemId: `2@${actor}`, insert: true,  value: 'b', pred: []},\n      {action: 'set',      obj: `1@${actor}`, elemId: `3@${actor}`, insert: true,  value: 'c', pred: []}\n    ]}\n    const change2 = {actor, seq: 2, startOp: 5, time: 0, deps: [hash(change1)], ops: [\n      {action: 'del',      obj: `1@${actor}`, elemId: `3@${actor}`, insert: false, pred: [`3@${actor}`]}\n    ]}\n    const backend = new BackendDoc()\n    assert.deepStrictEqual(backend.applyChanges([encodeChange(change1)]), {\n      maxOp: 4, clock: {[actor]: 1}, deps: [hash(change1)], pendingChanges: 0,\n      diffs: {objectId: '_root', type: 'map', props: {text: {[`1@${actor}`]: {\n        objectId: `1@${actor}`, type: 'text', edits: [\n          {action: 'multi-insert', index: 0, elemId: `2@${actor}`, values: ['a', 'b', 'c']}\n        ]\n      }}}}\n    })\n    assert.deepStrictEqual(backend.applyChanges([encodeChange(change2)]), {\n      maxOp: 5, clock: {[actor]: 2}, deps: [hash(change2)], pendingChanges: 0,\n      diffs: {objectId: '_root', type: 'map', props: {text: {[`1@${actor}`]: {\n        objectId: `1@${actor}`, type: 'text', edits: [{action: 'remove', index: 1, count: 1}]\n      }}}}\n    })\n    checkColumns(backend.blocks[0], {\n      objActor: [0, 1, 3, 0],\n      objCtr:   [0, 1, 3, 1],\n      keyActor: [0, 2, 2, 0],\n      keyCtr:   [0, 1, 0x7d, 0, 2, 1], // null, 0, 2, 3\n      keyStr:   [0x7f, 4, 0x74, 0x65, 0x78, 0x74, 0, 3], // 'text', 3x null\n      idActor:  [4, 0],\n      idCtr:    [4, 1],\n      insert:   [1, 3],\n      action:   [0x7f, 4, 3, 1], // makeText, set, set, set\n      valLen:   [0x7f, 0, 3, 0x16], // null, 3x 1-byte string\n      valRaw:   [0x61, 0x62, 0x63], // 'a', 'b', 'c'\n      succNum:  [2, 0, 0x7e, 1, 0],\n      succActor: [0x7f, 0],\n      succCtr:   [0x7f, 5]\n    })\n    assert.strictEqual(backend.blocks[0].numOps, 4)\n    assert.strictEqual(backend.blocks[0].numVisible, 2)\n    assert.strictEqual(backend.blocks[0].lastObjectActor, 0)\n    assert.strictEqual(backend.blocks[0].lastObjectCtr, 1)\n    assert.strictEqual(backend.blocks[0].firstVisibleActor, 0)\n    assert.strictEqual(backend.blocks[0].firstVisibleCtr, 2)\n    assert.strictEqual(backend.blocks[0].lastVisibleActor, 0)\n    assert.strictEqual(backend.blocks[0].lastVisibleCtr, 4)\n  })\n\n  it('should throw an error if a deleted element does not exist', () => {\n    const actor = uuid()\n    const change1 = {actor, seq: 1, startOp: 1, time: 0, deps: [], ops: [\n      {action: 'makeText', obj: '_root',      key: 'text',          insert: false,             pred: []},\n      {action: 'set',      obj: `1@${actor}`, elemId: '_head',      insert: true,  value: 'a', pred: []},\n      {action: 'set',      obj: `1@${actor}`, elemId: `2@${actor}`, insert: true,  value: 'b', pred: []}\n    ]}\n    const change2 = {actor, seq: 2, startOp: 4, time: 0, deps: [], ops: [\n      {action: 'del',      obj: `1@${actor}`, elemId: `1@${actor}`, insert: false, pred: [`1@${actor}`]}\n    ]}\n    const backend = new BackendDoc()\n    backend.applyChanges([encodeChange(change1)])\n    assert.throws(() => { backend.applyChanges([encodeChange(change2)]) }, /Reference element not found/)\n  })\n\n  it('should apply concurrent insertions at the same position', () => {\n    const actor1 = '01234567', actor2 = '89abcdef'\n    const change1 = {actor: actor1, seq: 1, startOp: 1, time: 0, deps: [], ops: [\n      {action: 'makeText', obj: '_root',       key: 'text',           insert: false,             pred: []},\n      {action: 'set',      obj: `1@${actor1}`, elemId: '_head',       insert: true,  value: 'a', pred: []}\n    ]}\n    const change2 = {actor: actor1, seq: 2, startOp: 3, time: 0, deps: [hash(change1)], ops: [\n      {action: 'set',      obj: `1@${actor1}`, elemId: `2@${actor1}`, insert: true,  value: 'c', pred: []}\n    ]}\n    const change3 = {actor: actor2, seq: 1, startOp: 3, time: 0, deps: [hash(change1)], ops: [\n      {action: 'set',      obj: `1@${actor1}`, elemId: `2@${actor1}`, insert: true,  value: 'b', pred: []}\n    ]}\n    const backend1 = new BackendDoc(), backend2 = new BackendDoc()\n    assert.deepStrictEqual(backend1.applyChanges([encodeChange(change1)]), {\n      maxOp: 2, clock: {[actor1]: 1}, deps: [hash(change1)], pendingChanges: 0,\n      diffs: {objectId: '_root', type: 'map', props: {text: {[`1@${actor1}`]: {\n        objectId: `1@${actor1}`, type: 'text', edits: [\n          {action: 'insert', index: 0, elemId: `2@${actor1}`, opId: `2@${actor1}`, value: {type: 'value', value: 'a'}}\n        ]\n      }}}}\n    })\n    assert.deepStrictEqual(backend1.applyChanges([encodeChange(change2)]), {\n      maxOp: 3, clock: {[actor1]: 2}, deps: [hash(change2)], pendingChanges: 0,\n      diffs: {objectId: '_root', type: 'map', props: {text: {[`1@${actor1}`]: {\n        objectId: `1@${actor1}`, type: 'text', edits: [\n          {action: 'insert', index: 1, elemId: `3@${actor1}`, opId: `3@${actor1}`, value: {type: 'value', value: 'c'}}\n        ]\n      }}}}\n    })\n    assert.deepStrictEqual(backend1.applyChanges([encodeChange(change3)]), {\n      maxOp: 3, clock: {[actor1]: 2, [actor2]: 1}, deps: [hash(change2), hash(change3)].sort(), pendingChanges: 0,\n      diffs: {objectId: '_root', type: 'map', props: {text: {[`1@${actor1}`]: {\n        objectId: `1@${actor1}`, type: 'text', edits: [\n          {action: 'insert', index: 1, elemId: `3@${actor2}`, opId: `3@${actor2}`, value: {type: 'value', value: 'b'}}\n        ]\n      }}}}\n    })\n    assert.deepStrictEqual(backend2.applyChanges([encodeChange(change1)]), {\n      maxOp: 2, clock: {[actor1]: 1}, deps: [hash(change1)], pendingChanges: 0,\n      diffs: {objectId: '_root', type: 'map', props: {text: {[`1@${actor1}`]: {\n        objectId: `1@${actor1}`, type: 'text', edits: [\n          {action: 'insert', index: 0, elemId: `2@${actor1}`, opId: `2@${actor1}`, value: {type: 'value', value: 'a'}}\n        ]\n      }}}}\n    })\n    assert.deepStrictEqual(backend2.applyChanges([encodeChange(change3)]), {\n      maxOp: 3, clock: {[actor1]: 1, [actor2]: 1}, deps: [hash(change3)], pendingChanges: 0,\n      diffs: {objectId: '_root', type: 'map', props: {text: {[`1@${actor1}`]: {\n        objectId: `1@${actor1}`, type: 'text', edits: [\n          {action: 'insert', index: 1, elemId: `3@${actor2}`, opId: `3@${actor2}`, value: {type: 'value', value: 'b'}}\n        ]\n      }}}}\n    })\n    assert.deepStrictEqual(backend2.applyChanges([encodeChange(change2)]), {\n      maxOp: 3, clock: {[actor1]: 2, [actor2]: 1}, deps: [hash(change2), hash(change3)].sort(), pendingChanges: 0,\n      diffs: {objectId: '_root', type: 'map', props: {text: {[`1@${actor1}`]: {\n        objectId: `1@${actor1}`, type: 'text', edits: [\n          {action: 'insert', index: 2, elemId: `3@${actor1}`, opId: `3@${actor1}`, value: {type: 'value', value: 'c'}}\n        ]\n      }}}}\n    })\n    for (let backend of [backend1, backend2]) {\n      checkColumns(backend.blocks[0], {\n        objActor: [0, 1, 3, 0],\n        objCtr:   [0, 1, 3, 1],\n        keyActor: [0, 2, 2, 0],\n        keyCtr:   [0, 1, 0x7d, 0, 2, 0], // null, 0, 2, 2\n        keyStr:   [0x7f, 4, 0x74, 0x65, 0x78, 0x74, 0, 3], // 'text', 3x null\n        idActor:  [2, 0, 0x7e, 1, 0], // 0, 0, 1, 0\n        idCtr:    [3, 1, 0x7f, 0], // 1, 2, 3, 3\n        insert:   [1, 3], // false, true, true, true\n        action:   [0x7f, 4, 3, 1], // makeText, set, set, set\n        valLen:   [0x7f, 0, 3, 0x16], // null, 3x 1-byte string\n        valRaw:   [0x61, 0x62, 0x63], // 'a', 'b', 'c'\n        succNum:  [4, 0],\n        succActor: [],\n        succCtr:   []\n      })\n      assert.strictEqual(backend.blocks[0].numOps, 4)\n      assert.strictEqual(backend.blocks[0].numVisible, 3)\n      assert.strictEqual(backend.blocks[0].lastObjectActor, 0)\n      assert.strictEqual(backend.blocks[0].lastObjectCtr, 1)\n      assert.strictEqual(backend.blocks[0].firstVisibleActor, 0)\n      assert.strictEqual(backend.blocks[0].firstVisibleCtr, 2)\n      assert.strictEqual(backend.blocks[0].lastVisibleActor, 0)\n      assert.strictEqual(backend.blocks[0].lastVisibleCtr, 3)\n    }\n  })\n\n  it('should apply concurrent insertions at the head', () => {\n    const actor1 = '01234567', actor2 = '89abcdef'\n    const change1 = {actor: actor1, seq: 1, startOp: 1, time: 0, deps: [], ops: [\n      {action: 'makeText', obj: '_root',       key: 'text',           insert: false,             pred: []},\n      {action: 'set',      obj: `1@${actor1}`, elemId: '_head',       insert: true,  value: 'd', pred: []}\n    ]}\n    const change2 = {actor: actor1, seq: 2, startOp: 3, time: 0, deps: [hash(change1)], ops: [\n      {action: 'set',      obj: `1@${actor1}`, elemId: '_head',       insert: true,  value: 'c', pred: []}\n    ]}\n    const change3 = {actor: actor2, seq: 1, startOp: 3, time: 0, deps: [hash(change1)], ops: [\n      {action: 'set',      obj: `1@${actor1}`, elemId: '_head',       insert: true,  value: 'a', pred: []},\n      {action: 'set',      obj: `1@${actor1}`, elemId: `3@${actor2}`, insert: true,  value: 'b', pred: []}\n    ]}\n    const backend1 = new BackendDoc(), backend2 = new BackendDoc()\n    assert.deepStrictEqual(backend1.applyChanges([encodeChange(change1)]), {\n      maxOp: 2, clock: {[actor1]: 1}, deps: [hash(change1)], pendingChanges: 0,\n      diffs: {objectId: '_root', type: 'map', props: {text: {[`1@${actor1}`]: {\n        objectId: `1@${actor1}`, type: 'text', edits: [\n          {action: 'insert', index: 0, elemId: `2@${actor1}`, opId: `2@${actor1}`, value: {type: 'value', value: 'd'}}\n        ]\n      }}}}\n    })\n    assert.deepStrictEqual(backend1.applyChanges([encodeChange(change2)]), {\n      maxOp: 3, clock: {[actor1]: 2}, deps: [hash(change2)], pendingChanges: 0,\n      diffs: {objectId: '_root', type: 'map', props: {text: {[`1@${actor1}`]: {\n        objectId: `1@${actor1}`, type: 'text', edits: [\n          {action: 'insert', index: 0, elemId: `3@${actor1}`, opId: `3@${actor1}`, value: {type: 'value', value: 'c'}}\n        ]\n      }}}}\n    })\n    assert.deepStrictEqual(backend1.applyChanges([encodeChange(change3)]), {\n      maxOp: 4, clock: {[actor1]: 2, [actor2]: 1}, deps: [hash(change2), hash(change3)].sort(), pendingChanges: 0,\n      diffs: {objectId: '_root', type: 'map', props: {text: {[`1@${actor1}`]: {\n        objectId: `1@${actor1}`, type: 'text', edits: [\n          {action: 'multi-insert', index: 0, elemId: `3@${actor2}`, values: ['a', 'b']}\n        ]\n      }}}}\n    })\n    assert.deepStrictEqual(backend2.applyChanges([encodeChange(change1)]), {\n      maxOp: 2, clock: {[actor1]: 1}, deps: [hash(change1)], pendingChanges: 0,\n      diffs: {objectId: '_root', type: 'map', props: {text: {[`1@${actor1}`]: {\n        objectId: `1@${actor1}`, type: 'text', edits: [\n          {action: 'insert', index: 0, elemId: `2@${actor1}`, opId: `2@${actor1}`, value: {type: 'value', value: 'd'}}\n        ]\n      }}}}\n    })\n    assert.deepStrictEqual(backend2.applyChanges([encodeChange(change3)]), {\n      maxOp: 4, clock: {[actor1]: 1, [actor2]: 1}, deps: [hash(change3)], pendingChanges: 0,\n      diffs: {objectId: '_root', type: 'map', props: {text: {[`1@${actor1}`]: {\n        objectId: `1@${actor1}`, type: 'text', edits: [\n          {action: 'multi-insert', index: 0, elemId: `3@${actor2}`, values: ['a', 'b']}\n        ]\n      }}}}\n    })\n    assert.deepStrictEqual(backend2.applyChanges([encodeChange(change2)]), {\n      maxOp: 4, clock: {[actor1]: 2, [actor2]: 1}, deps: [hash(change2), hash(change3)].sort(), pendingChanges: 0,\n      diffs: {objectId: '_root', type: 'map', props: {text: {[`1@${actor1}`]: {\n        objectId: `1@${actor1}`, type: 'text', edits: [\n          {action: 'insert', index: 2, elemId: `3@${actor1}`, opId: `3@${actor1}`, value: {type: 'value', value: 'c'}}\n        ]\n      }}}}\n    })\n    for (let backend of [backend1, backend2]) {\n      checkColumns(backend.blocks[0], {\n        objActor: [0, 1, 4, 0],\n        objCtr:   [0, 1, 4, 1],\n        keyActor: [0, 2, 0x7f, 1, 0, 2], // null, null, 1, null, null\n        keyCtr:   [0, 1, 0x7c, 0, 3, 0x7d, 0], // null, 0, 3, 0, 0\n        keyStr:   [0x7f, 4, 0x74, 0x65, 0x78, 0x74, 0, 4], // 'text', 4x null\n        idActor:  [0x7f, 0, 2, 1, 2, 0], // 0, 1, 1, 0, 0\n        idCtr:    [0x7d, 1, 2, 1, 2, 0x7f], // 1, 3, 4, 3, 2\n        insert:   [1, 4], // false, true, true, true, true\n        action:   [0x7f, 4, 4, 1], // makeText, set, set, set, set\n        valLen:   [0x7f, 0, 4, 0x16], // null, 4x 1-byte string\n        valRaw:   [0x61, 0x62, 0x63, 0x64], // 'a', 'b', 'c', 'd'\n        succNum:  [5, 0],\n        succActor: [],\n        succCtr:   []\n      })\n      assert.strictEqual(backend.blocks[0].numOps, 5)\n      assert.strictEqual(backend.blocks[0].numVisible, 4)\n      assert.strictEqual(backend.blocks[0].lastObjectActor, 0)\n      assert.strictEqual(backend.blocks[0].lastObjectCtr, 1)\n      // firstVisible is incorrect -- it should strictly be (1,3) rather than (0,2) -- but that\n      // doesn't matter since in any case it'll be different from the previous block's lastVisible\n      assert.strictEqual(backend.blocks[0].firstVisibleActor, 0)\n      assert.strictEqual(backend.blocks[0].firstVisibleCtr, 2)\n      assert.strictEqual(backend.blocks[0].lastVisibleActor, 0)\n      assert.strictEqual(backend.blocks[0].lastVisibleCtr, 2)\n      assert.strictEqual(bloomFilterContains(backend.blocks[0].bloom, 0, 2), true)\n      assert.strictEqual(bloomFilterContains(backend.blocks[0].bloom, 0, 3), true)\n      assert.strictEqual(bloomFilterContains(backend.blocks[0].bloom, 1, 3), true)\n      assert.strictEqual(bloomFilterContains(backend.blocks[0].bloom, 1, 4), true)\n      // The chance of a false positive is extremely low since the filter only contains 4 elements\n      for (let i = 5; i < 100; i++) assert.strictEqual(bloomFilterContains(backend.blocks[0].bloom, 1, i), false)\n    }\n  })\n\n  it('should perform multiple list element updates', () => {\n    const actor = uuid()\n    const change1 = {actor, seq: 1, startOp: 1, time: 0, deps: [], ops: [\n      {action: 'makeText', obj: '_root',      key: 'text',          insert: false,             pred: []},\n      {action: 'set',      obj: `1@${actor}`, elemId: '_head',      insert: true,  value: 'a', pred: []},\n      {action: 'set',      obj: `1@${actor}`, elemId: `2@${actor}`, insert: true,  value: 'b', pred: []},\n      {action: 'set',      obj: `1@${actor}`, elemId: `3@${actor}`, insert: true,  value: 'c', pred: []}\n    ]}\n    const change2 = {actor, seq: 2, startOp: 5, time: 0, deps: [hash(change1)], ops: [\n      {action: 'set',      obj: `1@${actor}`, elemId: `2@${actor}`, insert: false, value: 'A', pred: [`2@${actor}`]},\n      {action: 'set',      obj: `1@${actor}`, elemId: `4@${actor}`, insert: false, value: 'C', pred: [`4@${actor}`]}\n    ]}\n    const backend = new BackendDoc()\n    assert.deepStrictEqual(backend.applyChanges([encodeChange(change1)]), {\n      maxOp: 4, clock: {[actor]: 1}, deps: [hash(change1)], pendingChanges: 0,\n      diffs: {objectId: '_root', type: 'map', props: {text: {[`1@${actor}`]: {\n        objectId: `1@${actor}`, type: 'text', edits: [\n          {action: 'multi-insert', index: 0, elemId: `2@${actor}`, values: ['a', 'b', 'c']}\n        ]\n      }}}}\n    })\n    assert.deepStrictEqual(backend.applyChanges([encodeChange(change2)]), {\n      maxOp: 6, clock: {[actor]: 2}, deps: [hash(change2)], pendingChanges: 0,\n      diffs: {objectId: '_root', type: 'map', props: {text: {[`1@${actor}`]: {\n        objectId: `1@${actor}`, type: 'text', edits: [\n          {action: 'update', index: 0, opId: `5@${actor}`, value: {type: 'value', value: 'A'}},\n          {action: 'update', index: 2, opId: `6@${actor}`, value: {type: 'value', value: 'C'}}\n        ]\n      }}}}\n    })\n    checkColumns(backend.blocks[0], {\n      objActor: [0, 1, 5, 0],\n      objCtr:   [0, 1, 5, 1],\n      keyActor: [0, 2, 4, 0],\n      keyCtr:   [0, 1, 0x7d, 0, 2, 0, 2, 1], // null, 0, 2, 2, 3, 4\n      keyStr:   [0x7f, 4, 0x74, 0x65, 0x78, 0x74, 0, 5], // 'text', 5x null\n      idActor:  [6, 0],\n      idCtr:    [2, 1, 0x7c, 3, 0x7e, 1, 2], // 1, 2, 5, 3, 4, 6\n      insert:   [1, 1, 1, 2, 1], // false, true, false, true, true, false\n      action:   [0x7f, 4, 5, 1], // makeText, 5x set\n      valLen:   [0x7f, 0, 5, 0x16], // null, 5x 1-byte string\n      valRaw:   [0x61, 0x41, 0x62, 0x63, 0x43], // 'a', 'A', 'b', 'c', 'C'\n      succNum:  [0x7e, 0, 1, 2, 0, 0x7e, 1, 0], // 0, 1, 0, 0, 1, 0\n      succActor: [2, 0],\n      succCtr:   [0x7e, 5, 1] // 5, 6\n    })\n    assert.strictEqual(backend.blocks[0].numOps, 6)\n    assert.strictEqual(backend.blocks[0].numVisible, 3)\n    assert.strictEqual(backend.blocks[0].lastObjectActor, 0)\n    assert.strictEqual(backend.blocks[0].lastObjectCtr, 1)\n    assert.strictEqual(backend.blocks[0].firstVisibleActor, 0)\n    assert.strictEqual(backend.blocks[0].firstVisibleCtr, 2)\n    assert.strictEqual(backend.blocks[0].lastVisibleActor, 0)\n    assert.strictEqual(backend.blocks[0].lastVisibleCtr, 4)\n  })\n\n  it('should allow list element updates in reverse order', () => {\n    const actor = uuid()\n    const change1 = {actor, seq: 1, startOp: 1, time: 0, deps: [], ops: [\n      {action: 'makeText', obj: '_root',      key: 'text',          insert: false,             pred: []},\n      {action: 'set',      obj: `1@${actor}`, elemId: '_head',      insert: true,  value: 'a', pred: []},\n      {action: 'set',      obj: `1@${actor}`, elemId: `2@${actor}`, insert: true,  value: 'b', pred: []},\n      {action: 'set',      obj: `1@${actor}`, elemId: `3@${actor}`, insert: true,  value: 'c', pred: []}\n    ]}\n    const change2 = {actor, seq: 2, startOp: 5, time: 0, deps: [hash(change1)], ops: [\n      {action: 'set',      obj: `1@${actor}`, elemId: `4@${actor}`, insert: false, value: 'C', pred: [`4@${actor}`]},\n      {action: 'set',      obj: `1@${actor}`, elemId: `2@${actor}`, insert: false, value: 'A', pred: [`2@${actor}`]}\n    ]}\n    const backend = new BackendDoc()\n    backend.applyChanges([encodeChange(change1)])\n    assert.deepStrictEqual(backend.applyChanges([encodeChange(change2)]), {\n      maxOp: 6, clock: {[actor]: 2}, deps: [hash(change2)], pendingChanges: 0,\n      diffs: {objectId: '_root', type: 'map', props: {text: {[`1@${actor}`]: {\n        objectId: `1@${actor}`, type: 'text', edits: [\n          {action: 'update', index: 2, opId: `5@${actor}`, value: {type: 'value', value: 'C'}},\n          {action: 'update', index: 0, opId: `6@${actor}`, value: {type: 'value', value: 'A'}}\n        ]\n      }}}}\n    })\n    checkColumns(backend.blocks[0], {\n      objActor: [0, 1, 5, 0],\n      objCtr:   [0, 1, 5, 1],\n      keyActor: [0, 2, 4, 0], // null, null, 0, 0, 0, 0\n      keyCtr:   [0, 1, 0x7d, 0, 2, 0, 2, 1], // null, 0, 2, 2, 3, 4\n      keyStr:   [0x7f, 4, 0x74, 0x65, 0x78, 0x74, 0, 5], // 'text', 5x null\n      idActor:  [6, 0],\n      idCtr:    [2, 1, 0x7e, 4, 0x7d, 2, 1], // 1, 2, 6, 3, 4, 5\n      insert:   [1, 1, 1, 2, 1], // false, true, false, true, true, false\n      action:   [0x7f, 4, 5, 1], // makeText, 5x set\n      valLen:   [0x7f, 0, 5, 0x16], // null, 5x 1-byte string\n      valRaw:   [0x61, 0x41, 0x62, 0x63, 0x43], // 'a', 'A', 'b', 'c', 'C'\n      succNum:  [0x7e, 0, 1, 2, 0, 0x7e, 1, 0], // 0, 1, 0, 0, 1, 0\n      succActor: [2, 0],\n      succCtr:   [0x7e, 6, 0x7f] // 6, 5\n    })\n    assert.strictEqual(backend.blocks[0].numOps, 6)\n    assert.strictEqual(backend.blocks[0].numVisible, 3)\n    assert.strictEqual(backend.blocks[0].lastObjectActor, 0)\n    assert.strictEqual(backend.blocks[0].lastObjectCtr, 1)\n    assert.strictEqual(backend.blocks[0].firstVisibleActor, 0)\n    assert.strictEqual(backend.blocks[0].firstVisibleCtr, 2)\n    assert.strictEqual(backend.blocks[0].lastVisibleActor, 0)\n    assert.strictEqual(backend.blocks[0].lastVisibleCtr, 4)\n  })\n\n  it('should handle nested objects inside list elements', () => {\n    const actor = uuid()\n    const change1 = {actor, seq: 1, startOp: 1, time: 0, deps: [], ops: [\n      {action: 'makeList', obj: '_root',      key: 'list',          insert: false,           pred: []},\n      {action: 'set',      obj: `1@${actor}`, elemId: '_head',      insert: true, datatype: 'uint', value: 1, pred: []},\n      {action: 'makeMap',  obj: `1@${actor}`, elemId: `2@${actor}`, insert: true,            pred: []}\n    ]}\n    const change2 = {actor, seq: 2, startOp: 4, time: 0, deps: [hash(change1)], ops: [\n      {action: 'set',      obj: `3@${actor}`, key: 'x',          insert: false, datatype: 'uint', value: 2, pred: []}\n    ]}\n    const backend = new BackendDoc()\n    assert.deepStrictEqual(backend.applyChanges([encodeChange(change1)]), {\n      maxOp: 3, clock: {[actor]: 1}, deps: [hash(change1)], pendingChanges: 0,\n      diffs: {objectId: '_root', type: 'map', props: {list: {[`1@${actor}`]: {\n        objectId: `1@${actor}`, type: 'list', edits: [\n          {action: 'insert', index: 0, elemId: `2@${actor}`, opId: `2@${actor}`, value: {\n            type: 'value', value: 1, datatype: 'uint'\n          }},\n          {action: 'insert', index: 1, elemId: `3@${actor}`, opId: `3@${actor}`, value: {\n            objectId: `3@${actor}`, type: 'map', props: {}\n          }}\n        ]\n      }}}}\n    })\n    assert.deepStrictEqual(backend.applyChanges([encodeChange(change2)]), {\n      maxOp: 4, clock: {[actor]: 2}, deps: [hash(change2)], pendingChanges: 0,\n      diffs: {objectId: '_root', type: 'map', props: {list: {[`1@${actor}`]: {\n        objectId: `1@${actor}`, type: 'list', edits: [\n          {action: 'update', index: 1, opId: `3@${actor}`, value: {\n            objectId: `3@${actor}`, type: 'map', props: {x: {[`4@${actor}`]: {\n              type: 'value', value: 2, datatype: 'uint'\n            }}}\n          }}\n        ]\n      }}}}\n    })\n    checkColumns(backend.blocks[0], {\n      objActor: [0, 1, 3, 0],\n      objCtr:   [0, 1, 2, 1, 0x7f, 3], // null, 1, 1, 3\n      keyActor: [0, 2, 0x7f, 0, 0, 1], // null, null, 0, null\n      keyCtr:   [0, 1, 0x7e, 0, 2, 0, 1], // null, 0, 2, null\n      keyStr:   [0x7f, 4, 0x6c, 0x69, 0x73, 0x74, 0, 2, 0x7f, 1, 0x78], // 'list', null, null, 'x'\n      idActor:  [4, 0],\n      idCtr:    [4, 1],\n      insert:   [1, 2, 1], // false, true, true, false\n      action:   [0x7c, 2, 1, 0, 1], // makeList, set, makeMap, set\n      valLen:   [0x7c, 0, 0x13, 0, 0x13], // null, uint, null, uint\n      valRaw:   [1, 2],\n      succNum:  [4, 0],\n      succActor: [],\n      succCtr:   []\n    })\n    assert.strictEqual(backend.blocks[0].numOps, 4)\n    assert.strictEqual(backend.blocks[0].lastKey, 'x')\n    assert.strictEqual(backend.blocks[0].numVisible, 0)\n    assert.strictEqual(backend.blocks[0].lastObjectActor, 0)\n    assert.strictEqual(backend.blocks[0].lastObjectCtr, 3)\n    assert.strictEqual(backend.blocks[0].firstVisibleActor, 0)\n    assert.strictEqual(backend.blocks[0].firstVisibleCtr, 2)\n    assert.strictEqual(backend.blocks[0].lastVisibleActor, undefined)\n    assert.strictEqual(backend.blocks[0].lastVisibleCtr, undefined)\n  })\n\n  it('should handle multiple list objects', () => {\n    const actor = uuid()\n    const change1 = {actor, seq: 1, startOp: 1, time: 0, deps: [], ops: [\n      {action: 'makeList', obj: '_root',      key: 'list1',         insert: false,           pred: []},\n      {action: 'set',      obj: `1@${actor}`, elemId: '_head',      insert: true, datatype: 'uint', value: 1, pred: []},\n      {action: 'makeList', obj: '_root',      key: 'list2',         insert: false,           pred: []},\n      {action: 'set',      obj: `3@${actor}`, elemId: '_head',      insert: true, datatype: 'uint', value: 2, pred: []}\n    ]}\n    const change2 = {actor, seq: 2, startOp: 5, time: 0, deps: [hash(change1)], ops: [\n      {action: 'set',      obj: `1@${actor}`, elemId: `2@${actor}`, insert: true, datatype: 'uint', value: 3, pred: []}\n    ]}\n    const backend = new BackendDoc()\n    assert.deepStrictEqual(backend.applyChanges([encodeChange(change1)]), {\n      maxOp: 4, clock: {[actor]: 1}, deps: [hash(change1)], pendingChanges: 0,\n      diffs: {objectId: '_root', type: 'map', props: {\n        list1: {[`1@${actor}`]: {objectId: `1@${actor}`, type: 'list', edits: [\n          {action: 'insert', index: 0, elemId: `2@${actor}`, opId: `2@${actor}`, value: {\n            type: 'value', value: 1, datatype: 'uint'\n          }}\n        ]}},\n        list2: {[`3@${actor}`]: {objectId: `3@${actor}`, type: 'list', edits: [\n          {action: 'insert', index: 0, elemId: `4@${actor}`, opId: `4@${actor}`, value: {\n            type: 'value', value: 2, datatype: 'uint'\n          }}\n        ]}}\n      }}\n    })\n    assert.deepStrictEqual(backend.applyChanges([encodeChange(change2)]), {\n      maxOp: 5, clock: {[actor]: 2}, deps: [hash(change2)], pendingChanges: 0,\n      diffs: {objectId: '_root', type: 'map', props: {\n        list1: {[`1@${actor}`]: {objectId: `1@${actor}`, type: 'list', edits: [\n          {action: 'insert', index: 1, elemId: `5@${actor}`, opId: `5@${actor}`, value: {\n            type: 'value', value: 3, datatype: 'uint'\n          }}\n        ]}}\n      }}\n    })\n    checkColumns(backend.blocks[0], {\n      objActor: [0, 2, 3, 0],\n      objCtr:   [0, 2, 2, 1, 0x7f, 3], // null, null, 1, 1, 3\n      keyActor: [0, 3, 0x7f, 0, 0, 1], // null, null, null, 0, null\n      keyCtr:   [0, 2, 0x7d, 0, 2, 0x7e], // null, null, 0, 2, 0\n      keyStr:   [0x7e, 5, 0x6c, 0x69, 0x73, 0x74, 0x31, 5, 0x6c, 0x69, 0x73, 0x74, 0x32, 0, 3], // 'list1', 'list2', null, null, null\n      idActor:  [5, 0],\n      idCtr:    [0x7b, 1, 2, 0x7f, 3, 0x7f], // 1, 3, 2, 5, 4\n      insert:   [2, 3], // false, false, true, true, true\n      action:   [2, 2, 3, 1], // 2x makeList, 3x set\n      valLen:   [2, 0, 3, 0x13], // 2x null, 3x uint\n      valRaw:   [1, 3, 2],\n      succNum:  [5, 0],\n      succActor: [],\n      succCtr:   []\n    })\n    assert.strictEqual(backend.blocks[0].numOps, 5)\n    assert.strictEqual(backend.blocks[0].lastKey, undefined)\n    assert.strictEqual(backend.blocks[0].numVisible, 1)\n    assert.strictEqual(backend.blocks[0].lastObjectActor, 0)\n    assert.strictEqual(backend.blocks[0].lastObjectCtr, 3)\n    assert.strictEqual(backend.blocks[0].firstVisibleActor, 0)\n    assert.strictEqual(backend.blocks[0].firstVisibleCtr, 2)\n    assert.strictEqual(backend.blocks[0].lastVisibleActor, 0)\n    assert.strictEqual(backend.blocks[0].lastVisibleCtr, 4)\n  })\n\n  it('should handle a counter inside a map', () => {\n    const actor = uuid()\n    const change1 = {actor, seq: 1, startOp: 1, time: 0, deps: [], ops: [\n      {action: 'set', obj: '_root', key: 'counter', value: 1, datatype: 'counter', pred: []}\n    ]}\n    const change2 = {actor, seq: 2, startOp: 2, time: 0, deps: [hash(change1)], ops: [\n      {action: 'inc', obj: '_root', key: 'counter', datatype: 'uint', value: 2, pred: [`1@${actor}`]}\n    ]}\n    const change3 = {actor, seq: 3, startOp: 3, time: 0, deps: [hash(change2)], ops: [\n      {action: 'inc', obj: '_root', key: 'counter', datatype: 'uint', value: 3, pred: [`1@${actor}`]}\n    ]}\n    const backend = new BackendDoc()\n    assert.deepStrictEqual(backend.applyChanges([encodeChange(change1)]), {\n      maxOp: 1, clock: {[actor]: 1}, deps: [hash(change1)], pendingChanges: 0,\n      diffs: {objectId: '_root', type: 'map', props: {\n        counter: {[`1@${actor}`]: {type: 'value', value: 1, datatype: 'counter'}}\n      }}\n    })\n    assert.deepStrictEqual(backend.applyChanges([encodeChange(change2)]), {\n      maxOp: 2, clock: {[actor]: 2}, deps: [hash(change2)], pendingChanges: 0,\n      diffs: {objectId: '_root', type: 'map', props: {\n        counter: {[`1@${actor}`]: {type: 'value', value: 3, datatype: 'counter'}}\n      }}\n    })\n    assert.deepStrictEqual(backend.applyChanges([encodeChange(change3)]), {\n      maxOp: 3, clock: {[actor]: 3}, deps: [hash(change3)], pendingChanges: 0,\n      diffs: {objectId: '_root', type: 'map', props: {\n        counter: {[`1@${actor}`]: {type: 'value', value: 6, datatype: 'counter'}}\n      }}\n    })\n    checkColumns(backend.blocks[0], {\n      objActor: [],\n      objCtr:   [],\n      keyActor: [],\n      keyCtr:   [],\n      keyStr:   [3, 7, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x65, 0x72], // 3x 'counter'\n      idActor:  [3, 0],\n      idCtr:    [3, 1],\n      insert:   [3],\n      action:   [0x7f, 1, 2, 5], // set, inc, inc\n      valLen:   [0x7f, 0x18, 2, 0x13], // counter, uint, uint\n      valRaw:   [1, 2, 3],\n      succNum:  [0x7f, 2, 2, 0], // 2, 0, 0\n      succActor: [2, 0],\n      succCtr:   [0x7e, 2, 1] // 2, 3\n    })\n    assert.strictEqual(backend.blocks[0].lastKey, 'counter')\n    assert.strictEqual(backend.blocks[0].numOps, 3)\n    assert.strictEqual(backend.blocks[0].lastObjectActor, null)\n    assert.strictEqual(backend.blocks[0].lastObjectCtr, null)\n  })\n\n  it('should handle a counter inside a list element', () => {\n    const actor = uuid()\n    const change1 = {actor, seq: 1, startOp: 1, time: 0, deps: [], ops: [\n      {action: 'makeList', obj: '_root',      key: 'list',          insert: false, pred: []},\n      {action: 'set',      obj: `1@${actor}`, elemId: '_head',      insert: true,  pred: [], value: 1, datatype: 'counter'}\n    ]}\n    const change2 = {actor, seq: 2, startOp: 3, time: 0, deps: [hash(change1)], ops: [\n      {action: 'inc',      obj: `1@${actor}`, elemId: `2@${actor}`, datatype: 'uint', value: 2, pred: [`2@${actor}`]}\n    ]}\n    const backend = new BackendDoc()\n    assert.deepStrictEqual(backend.applyChanges([encodeChange(change1)]), {\n      maxOp: 2, clock: {[actor]: 1}, deps: [hash(change1)], pendingChanges: 0,\n      diffs: {objectId: '_root', type: 'map', props: {list: {[`1@${actor}`]: {\n        objectId: `1@${actor}`, type: 'list', edits: [\n          {action: 'insert', index: 0, elemId: `2@${actor}`, opId: `2@${actor}`, value: {\n            type: 'value', value: 1, datatype: 'counter'\n          }}\n        ]\n      }}}}\n    })\n    assert.deepStrictEqual(backend.applyChanges([encodeChange(change2)]), {\n      maxOp: 3, clock: {[actor]: 2}, deps: [hash(change2)], pendingChanges: 0,\n      diffs: {objectId: '_root', type: 'map', props: {list: {[`1@${actor}`]: {\n        objectId: `1@${actor}`, type: 'list', edits: [\n          {action: 'update', index: 0, opId: `2@${actor}`, value: {\n            type: 'value', value: 3, datatype: 'counter'\n          }}\n        ]\n      }}}}\n    })\n    checkColumns(backend.blocks[0], {\n      objActor: [0, 1, 2, 0],\n      objCtr:   [0, 1, 2, 1],\n      keyActor: [0, 2, 0x7f, 0], // null, null, 0\n      keyCtr:   [0, 1, 0x7e, 0, 2], // null, 0, 2\n      keyStr:   [0x7f, 4, 0x6c, 0x69, 0x73, 0x74, 0, 2], // 'list', null, null\n      idActor:  [3, 0],\n      idCtr:    [3, 1], // 1, 2, 3\n      insert:   [1, 1, 1], // false, true, false\n      action:   [0x7d, 2, 1, 5], // makeList, set, inc\n      valLen:   [0x7d, 0, 0x18, 0x13], // null, counter, uint\n      valRaw:   [1, 2],\n      succNum:  [0x7d, 0, 1, 0],\n      succActor: [0x7f, 0],\n      succCtr:   [0x7f, 3]\n    })\n    assert.strictEqual(backend.blocks[0].numOps, 3)\n    assert.strictEqual(backend.blocks[0].lastKey, undefined)\n    assert.strictEqual(backend.blocks[0].numVisible, 1)\n    assert.strictEqual(backend.blocks[0].lastObjectActor, 0)\n    assert.strictEqual(backend.blocks[0].lastObjectCtr, 1)\n    assert.strictEqual(backend.blocks[0].firstVisibleActor, 0)\n    assert.strictEqual(backend.blocks[0].firstVisibleCtr, 2)\n    assert.strictEqual(backend.blocks[0].lastVisibleActor, 0)\n    assert.strictEqual(backend.blocks[0].lastVisibleCtr, 2)\n  })\n\n  it('should delete a counter from a map', () => {\n    const actor = uuid()\n    const change1 = {actor, seq: 1, startOp: 1, time: 0, deps: [], ops: [\n      {action: 'set', obj: '_root', key: 'counter', value: 1, datatype: 'counter', pred: []}\n    ]}\n    const change2 = {actor, seq: 2, startOp: 2, time: 0, deps: [hash(change1)], ops: [\n      {action: 'inc', obj: '_root', key: 'counter', value: 2, datatype: 'uint', pred: [`1@${actor}`]}\n    ]}\n    const change3 = {actor, seq: 3, startOp: 3, time: 0, deps: [hash(change2)], ops: [\n      {action: 'del', obj: '_root', key: 'counter', pred: [`1@${actor}`]}\n    ]}\n    const backend = new BackendDoc()\n    backend.applyChanges([encodeChange(change1)])\n    assert.deepStrictEqual(backend.applyChanges([encodeChange(change2)]), {\n      maxOp: 2, clock: {[actor]: 2}, deps: [hash(change2)], pendingChanges: 0,\n      diffs: {objectId: '_root', type: 'map', props: {\n        counter: {[`1@${actor}`]: {type: 'value', value: 3, datatype: 'counter'}}\n      }}\n    })\n    assert.deepStrictEqual(backend.applyChanges([encodeChange(change3)]), {\n      maxOp: 3, clock: {[actor]: 3}, deps: [hash(change3)], pendingChanges: 0,\n      diffs: {objectId: '_root', type: 'map', props: {counter: {}}}\n    })\n    assert.strictEqual(backend.blocks[0].lastKey, 'counter')\n    assert.strictEqual(backend.blocks[0].numOps, 2)\n    assert.strictEqual(backend.blocks[0].lastObjectActor, null)\n    assert.strictEqual(backend.blocks[0].lastObjectCtr, null)\n  })\n\n  it('should handle conflicts inside list elements', () => {\n    const actor1 = '01234567', actor2 = '89abcdef'\n    const change1 = {actor: actor1, seq: 1, startOp: 1, time: 0, deps: [], ops: [\n      {action: 'makeList', obj: '_root',       key: 'list',           insert: false,           pred: []},\n      {action: 'set',      obj: `1@${actor1}`, elemId: '_head',       insert: true,  datatype: 'uint', value: 1, pred: []}\n    ]}\n    const change2 = {actor: actor1, seq: 2, startOp: 3, time: 0, deps: [hash(change1)], ops: [\n      {action: 'set',      obj: `1@${actor1}`, elemId: `2@${actor1}`, insert: false, datatype: 'uint', value: 2, pred: [`2@${actor1}`]}\n    ]}\n    const change3 = {actor: actor2, seq: 1, startOp: 3, time: 0, deps: [hash(change1)], ops: [\n      {action: 'set',      obj: `1@${actor1}`, elemId: `2@${actor1}`, insert: false, datatype: 'uint', value: 3, pred: [`2@${actor1}`]}\n    ]}\n    const backend1 = new BackendDoc(), backend2 = new BackendDoc()\n    assert.deepStrictEqual(backend1.applyChanges([encodeChange(change1)]), {\n      maxOp: 2, clock: {[actor1]: 1}, deps: [hash(change1)], pendingChanges: 0,\n      diffs: {objectId: '_root', type: 'map', props: {list: {[`1@${actor1}`]: {\n        objectId: `1@${actor1}`, type: 'list', edits: [\n          {action: 'insert', index: 0, elemId: `2@${actor1}`, opId: `2@${actor1}`, value: {\n            type: 'value', value: 1, datatype: 'uint'\n          }}\n        ]\n      }}}}\n    })\n    assert.deepStrictEqual(backend1.applyChanges([encodeChange(change2)]), {\n      maxOp: 3, clock: {[actor1]: 2}, deps: [hash(change2)], pendingChanges: 0,\n      diffs: {objectId: '_root', type: 'map', props: {list: {[`1@${actor1}`]: {\n        objectId: `1@${actor1}`, type: 'list', edits: [\n          {action: 'update', index: 0, opId: `3@${actor1}`, value: {type: 'value', value: 2, datatype: 'uint'}}\n        ]\n      }}}}\n    })\n    assert.deepStrictEqual(backend1.applyChanges([encodeChange(change3)]), {\n      maxOp: 3, clock: {[actor1]: 2, [actor2]: 1}, deps: [hash(change2), hash(change3)].sort(), pendingChanges: 0,\n      diffs: {objectId: '_root', type: 'map', props: {list: {[`1@${actor1}`]: {\n        objectId: `1@${actor1}`, type: 'list', edits: [\n          {action: 'update', index: 0, opId: `3@${actor1}`, value: {type: 'value', value: 2, datatype: 'uint'}},\n          {action: 'update', index: 0, opId: `3@${actor2}`, value: {type: 'value', value: 3, datatype: 'uint'}}\n        ]\n      }}}}\n    })\n    backend2.applyChanges([encodeChange(change1)])\n    assert.deepStrictEqual(backend2.applyChanges([encodeChange(change3)]), {\n      maxOp: 3, clock: {[actor1]: 1, [actor2]: 1}, deps: [hash(change3)], pendingChanges: 0,\n      diffs: {objectId: '_root', type: 'map', props: {list: {[`1@${actor1}`]: {\n        objectId: `1@${actor1}`, type: 'list', edits: [\n          {action: 'update', index: 0, opId: `3@${actor2}`, value: {type: 'value', value: 3, datatype: 'uint'}}\n        ]\n      }}}}\n    })\n    assert.deepStrictEqual(backend2.applyChanges([encodeChange(change2)]), {\n      maxOp: 3, clock: {[actor1]: 2, [actor2]: 1}, deps: [hash(change2), hash(change3)].sort(), pendingChanges: 0,\n      diffs: {objectId: '_root', type: 'map', props: {list: {[`1@${actor1}`]: {\n        objectId: `1@${actor1}`, type: 'list', edits: [\n          {action: 'update', index: 0, opId: `3@${actor1}`, value: {type: 'value', value: 2, datatype: 'uint'}},\n          {action: 'update', index: 0, opId: `3@${actor2}`, value: {type: 'value', value: 3, datatype: 'uint'}}\n        ]\n      }}}}\n    })\n    for (let backend of [backend1, backend2]) {\n      checkColumns(backend.blocks[0], {\n        objActor: [0, 1, 3, 0],\n        objCtr:   [0, 1, 3, 1],\n        keyActor: [0, 2, 2, 0],\n        keyCtr:   [0, 1, 0x7d, 0, 2, 0], // null, 0, 2, 2\n        keyStr:   [0x7f, 4, 0x6c, 0x69, 0x73, 0x74, 0, 3], // 'list', 3x null\n        idActor:  [3, 0, 0x7f, 1],\n        idCtr:    [3, 1, 0x7f, 0], // 1, 2, 3, 3\n        insert:   [1, 1, 2], // false, true, false, false\n        action:   [0x7f, 2, 3, 1], // makeList, 3x set\n        valLen:   [0x7f, 0, 3, 0x13], // null, 3x uint\n        valRaw:   [1, 2, 3],\n        succNum:  [0x7e, 0, 2, 2, 0], // 0, 1, 0, 0\n        succActor: [0x7e, 0, 1],\n        succCtr:   [0x7e, 3, 0] // 3, 3\n      })\n      assert.strictEqual(backend.blocks[0].numOps, 4)\n      assert.strictEqual(backend.blocks[0].lastKey, undefined)\n      assert.strictEqual(backend.blocks[0].numVisible, 1)\n      assert.strictEqual(backend.blocks[0].lastObjectActor, 0)\n      assert.strictEqual(backend.blocks[0].lastObjectCtr, 1)\n      assert.strictEqual(backend.blocks[0].firstVisibleActor, 0)\n      assert.strictEqual(backend.blocks[0].firstVisibleCtr, 2)\n      assert.strictEqual(backend.blocks[0].lastVisibleActor, 0)\n      assert.strictEqual(backend.blocks[0].lastVisibleCtr, 2)\n    }\n  })\n\n  it('should allow conflicts to be introduced by a single change', () => {\n    const actor = uuid()\n    const change1 = {actor, seq: 1, startOp: 1, time: 0, deps: [], ops: [\n      {action: 'makeText', obj: '_root',      key: 'text',          insert: false,             pred: []},\n      {action: 'set',      obj: `1@${actor}`, elemId: '_head',      insert: true,  value: 'a', pred: []},\n      {action: 'set',      obj: `1@${actor}`, elemId: `2@${actor}`, insert: true,  value: 'b', pred: []}\n    ]}\n    const change2 = {actor, seq: 2, startOp: 4, time: 0, deps: [hash(change1)], ops: [\n      {action: 'set',      obj: `1@${actor}`, elemId: `2@${actor}`, insert: false, value: 'x', pred: [`2@${actor}`]},\n      {action: 'set',      obj: `1@${actor}`, elemId: `2@${actor}`, insert: false, value: 'y', pred: [`2@${actor}`]}\n    ]}\n    const backend = new BackendDoc()\n    assert.deepStrictEqual(backend.applyChanges([encodeChange(change1)]), {\n      maxOp: 3, clock: {[actor]: 1}, deps: [hash(change1)], pendingChanges: 0,\n      diffs: {objectId: '_root', type: 'map', props: {text: {[`1@${actor}`]: {\n        objectId: `1@${actor}`, type: 'text', edits: [\n          {action: 'multi-insert', index: 0, elemId: `2@${actor}`, values: ['a', 'b']}\n        ]\n      }}}}\n    })\n    assert.deepStrictEqual(backend.applyChanges([encodeChange(change2)]), {\n      maxOp: 5, clock: {[actor]: 2}, deps: [hash(change2)], pendingChanges: 0,\n      diffs: {objectId: '_root', type: 'map', props: {text: {[`1@${actor}`]: {\n        objectId: `1@${actor}`, type: 'text', edits: [\n          {action: 'update', index: 0, opId: `4@${actor}`, value: {type: 'value', value: 'x'}},\n          {action: 'update', index: 0, opId: `5@${actor}`, value: {type: 'value', value: 'y'}}\n        ]\n      }}}}\n    })\n    checkColumns(backend.blocks[0], {\n      objActor: [0, 1, 4, 0],\n      objCtr:   [0, 1, 4, 1],\n      keyActor: [0, 2, 3, 0],\n      keyCtr:   [0, 1, 0x7e, 0, 2, 2, 0], // null, 0, 2, 2, 2\n      keyStr:   [0x7f, 4, 0x74, 0x65, 0x78, 0x74, 0, 4], // 'text', 4x null\n      idActor:  [5, 0],\n      idCtr:    [2, 1, 0x7d, 2, 1, 0x7e], // 1, 2, 4, 5, 3\n      insert:   [1, 1, 2, 1], // false, true, false, false, true\n      action:   [0x7f, 4, 4, 1], // makeText, 4x set\n      valLen:   [0x7f, 0, 4, 0x16], // null, 4x 1-byte string\n      valRaw:   [0x61, 0x78, 0x79, 0x62], // 'a', 'x', 'y', 'b'\n      succNum:  [0x7e, 0, 2, 3, 0], // 0, 2, 0, 0, 0\n      succActor: [2, 0],\n      succCtr:   [0x7e, 4, 1] // 4, 5\n    })\n    assert.strictEqual(backend.blocks[0].numOps, 5)\n    assert.strictEqual(backend.blocks[0].lastKey, undefined)\n    assert.strictEqual(backend.blocks[0].numVisible, 2)\n    assert.strictEqual(backend.blocks[0].lastObjectActor, 0)\n    assert.strictEqual(backend.blocks[0].lastObjectCtr, 1)\n    assert.strictEqual(backend.blocks[0].firstVisibleActor, 0)\n    assert.strictEqual(backend.blocks[0].firstVisibleCtr, 2)\n    assert.strictEqual(backend.blocks[0].lastVisibleActor, 0)\n    assert.strictEqual(backend.blocks[0].lastVisibleCtr, 3)\n  })\n\n  it('should allow conflicts to arise on a multi-inserted element', () => {\n    const actor = uuid()\n    const change1 = {actor, seq: 1, startOp: 1, time: 0, deps: [], ops: [\n      {action: 'makeText', obj: '_root',      key: 'text',          insert: false,             pred: []},\n      {action: 'set',      obj: `1@${actor}`, elemId: '_head',      insert: true,  value: 'a', pred: []},\n      {action: 'set',      obj: `1@${actor}`, elemId: `2@${actor}`, insert: true,  value: 'b', pred: []}\n    ]}\n    const change2 = {actor, seq: 2, startOp: 4, time: 0, deps: [hash(change1)], ops: [\n      {action: 'set',      obj: `1@${actor}`, elemId: `3@${actor}`, insert: false, value: 'x', pred: [`3@${actor}`]},\n      {action: 'set',      obj: `1@${actor}`, elemId: `3@${actor}`, insert: false, value: 'y', pred: [`3@${actor}`]}\n    ]}\n    const backend = new BackendDoc()\n    assert.deepStrictEqual(backend.applyChanges([encodeChange(change1), encodeChange(change2)]), {\n      maxOp: 5, clock: {[actor]: 2}, deps: [hash(change2)], pendingChanges: 0,\n      diffs: {objectId: '_root', type: 'map', props: {text: {[`1@${actor}`]: {\n        objectId: `1@${actor}`, type: 'text', edits: [\n          {action: 'multi-insert', index: 0, elemId: `2@${actor}`, values: ['a']},\n          {action: 'insert', index: 1, elemId: `3@${actor}`, opId: `4@${actor}`, value: {type: 'value', value: 'x'}},\n          {action: 'update', index: 1, opId: `5@${actor}`, value: {type: 'value', value: 'y'}}\n        ]\n      }}}}\n    })\n    checkColumns(backend.blocks[0], {\n      objActor: [0, 1, 4, 0],\n      objCtr:   [0, 1, 4, 1],\n      keyActor: [0, 2, 3, 0],\n      keyCtr:   [0, 1, 0x7c, 0, 2, 1, 0], // null, 0, 2, 3, 3\n      keyStr:   [0x7f, 4, 0x74, 0x65, 0x78, 0x74, 0, 4], // 'text', 4x null\n      idActor:  [5, 0],\n      idCtr:    [5, 1], // 1, 2, 3, 4, 5\n      insert:   [1, 2, 2], // false, true, true, false, false\n      action:   [0x7f, 4, 4, 1], // makeText, 4x set\n      valLen:   [0x7f, 0, 4, 0x16], // null, 4x 1-byte string\n      valRaw:   [0x61, 0x62, 0x78, 0x79], // 'a', 'b', 'x', 'y'\n      succNum:  [2, 0, 0x7f, 2, 2, 0], // 0, 0, 2, 0, 0\n      succActor: [2, 0],\n      succCtr:   [0x7e, 4, 1] // 4, 5\n    })\n    assert.strictEqual(backend.blocks[0].numOps, 5)\n    assert.strictEqual(backend.blocks[0].lastKey, undefined)\n    assert.strictEqual(backend.blocks[0].numVisible, 2)\n    assert.strictEqual(backend.blocks[0].lastObjectActor, 0)\n    assert.strictEqual(backend.blocks[0].lastObjectCtr, 1)\n    assert.strictEqual(backend.blocks[0].firstVisibleActor, 0)\n    assert.strictEqual(backend.blocks[0].firstVisibleCtr, 2)\n    assert.strictEqual(backend.blocks[0].lastVisibleActor, 0)\n    assert.strictEqual(backend.blocks[0].lastVisibleCtr, 3)\n  })\n\n  it('should convert inserts to updates when needed', () => {\n    const actor1 = '01234567', actor2 = '89abcdef'\n    const change1 = {actor: actor1, seq: 1, startOp: 1, time: 0, deps: [], ops: [\n      {action: 'makeText', obj: '_root',       key: 'text',           insert: false,             pred: []},\n      {action: 'set',      obj: `1@${actor1}`, elemId: '_head',       insert: true,  value: 'c', pred: []}\n    ]}\n    const change2 = {actor: actor1, seq: 2, startOp: 3, time: 0, deps: [hash(change1)], ops: [\n      {action: 'set',      obj: `1@${actor1}`, elemId: '_head',       insert: true,  value: 'a', pred: []},\n      {action: 'set',      obj: `1@${actor1}`, elemId: `3@${actor1}`, insert: true,  value: 'b', pred: []},\n      {action: 'set',      obj: `1@${actor1}`, elemId: `2@${actor1}`, insert: false, value: 'C', pred: [`2@${actor1}`]}\n    ]}\n    const change3 = {actor: actor2, seq: 1, startOp: 3, time: 0, deps: [hash(change1)], ops: [\n      {action: 'set',      obj: `1@${actor1}`, elemId: `2@${actor1}`, insert: false, value: 'x', pred: [`2@${actor1}`]},\n      {action: 'set',      obj: `1@${actor1}`, elemId: `2@${actor1}`, insert: false, value: 'y', pred: [`2@${actor1}`]}\n    ]}\n    const backend = new BackendDoc()\n    assert.deepStrictEqual(backend.applyChanges([encodeChange(change1), encodeChange(change2)]), {\n      maxOp: 5, clock: {[actor1]: 2}, deps: [hash(change2)], pendingChanges: 0,\n      diffs: {objectId: '_root', type: 'map', props: {text: {[`1@${actor1}`]: {\n        objectId: `1@${actor1}`, type: 'text', edits: [\n          {action: 'insert', index: 0, elemId: `2@${actor1}`, opId: `2@${actor1}`, value: {type: 'value', value: 'c'}},\n          {action: 'multi-insert', index: 0, elemId: `3@${actor1}`, values: ['a', 'b']},\n          {action: 'update', index: 2, opId: `5@${actor1}`, value: {type: 'value', value: 'C'}}\n        ]\n      }}}}\n    })\n    assert.deepStrictEqual(backend.applyChanges([encodeChange(change3)]), {\n      maxOp: 5, clock: {[actor1]: 2, [actor2]: 1}, deps: [hash(change2), hash(change3)].sort(), pendingChanges: 0,\n      diffs: {objectId: '_root', type: 'map', props: {text: {[`1@${actor1}`]: {\n        objectId: `1@${actor1}`, type: 'text', edits: [\n          {action: 'update', index: 2, opId: `3@${actor2}`, value: {type: 'value', value: 'x'}},\n          {action: 'update', index: 2, opId: `4@${actor2}`, value: {type: 'value', value: 'y'}},\n          {action: 'update', index: 2, opId: `5@${actor1}`, value: {type: 'value', value: 'C'}}\n        ]\n      }}}}\n    })\n    // Order of operations in the document:\n    // {action: 'makeText', id: `1@${actor1}`, obj: '_root',       key: 'text',           insert: false,             succ: []},\n    // {action: 'set',      id: `3@${actor1}`, obj: `1@${actor1}`, elemId: '_head',       insert: true,  value: 'a', succ: []},\n    // {action: 'set',      id: `4@${actor1}`, obj: `1@${actor1}`, elemId: `3@${actor1}`, insert: true,  value: 'b', succ: []},\n    // {action: 'set',      id: `2@${actor1}`, obj: `1@${actor1}`, elemId: '_head',       insert: true,  value: 'c', succ: [`3@${actor2}`, `4@${actor2}`, `5@${actor1}`]},\n    // {action: 'set',      id: `3@${actor2}`, obj: `1@${actor1}`, elemId: `2@${actor1}`, insert: false, value: 'x', succ: []},\n    // {action: 'set',      id: `4@${actor2}`, obj: `1@${actor1}`, elemId: `2@${actor1}`, insert: false, value: 'y', succ: []},\n    // {action: 'set',      id: `5@${actor1}`, obj: `1@${actor1}`, elemId: `2@${actor1}`, insert: false, value: 'C', succ: []}\n    checkColumns(backend.blocks[0], {\n      objActor: [0, 1, 6, 0],\n      objCtr:   [0, 1, 6, 1],\n      keyActor: [0, 2, 0x7f, 0, 0, 1, 3, 0], // null, null, 0, null, 0, 0, 0\n      keyCtr:   [0, 1, 0x7c, 0, 3, 0x7d, 2, 2, 0], // null, 0, 3, 0, 2, 2, 2\n      keyStr:   [0x7f, 4, 0x74, 0x65, 0x78, 0x74, 0, 6], // 'text', 6x null\n      idActor:  [4, 0, 2, 1, 0x7f, 0], // 4x actor1, 2x actor2, 1x actor1\n      idCtr:    [0x7c, 1, 2, 1, 0x7e, 3, 1], // 1, 3, 4, 2, 3, 4, 5\n      insert:   [1, 3, 3], // 1x false, 3x true, 3x false\n      action:   [0x7f, 4, 6, 1], // makeText, 6x set\n      valLen:   [0x7f, 0, 6, 0x16], // null, 6x 1-byte string\n      valRaw:   [0x61, 0x62, 0x63, 0x78, 0x79, 0x43], // 'a', 'b', 'c', 'x', 'y', 'C'\n      succNum:  [3, 0, 0x7f, 3, 3, 0], // 0, 0, 0, 3, 0, 0, 0\n      succActor: [2, 1, 0x7f, 0], // actor2, actor2, actor1\n      succCtr:   [0x7f, 3, 2, 1] // 3, 4, 5\n    })\n    assert.strictEqual(backend.blocks[0].numOps, 7)\n    assert.strictEqual(backend.blocks[0].lastKey, undefined)\n    assert.strictEqual(backend.blocks[0].numVisible, 3)\n    assert.strictEqual(backend.blocks[0].lastObjectActor, 0)\n    assert.strictEqual(backend.blocks[0].lastObjectCtr, 1)\n    // firstVisible is incorrect -- it should strictly be (0,3) rather than (0,2) -- but that\n    // doesn't matter since in any case it'll be different from the previous block's lastVisible\n    assert.strictEqual(backend.blocks[0].firstVisibleActor, 0)\n    assert.strictEqual(backend.blocks[0].firstVisibleCtr, 2)\n    assert.strictEqual(backend.blocks[0].lastVisibleActor, 0)\n    assert.strictEqual(backend.blocks[0].lastVisibleCtr, 2)\n  })\n\n  it('should allow a further conflict to be added to an existing conflict', () => {\n    const actor1 = '01234567', actor2 = '89abcdef'\n    const change1 = {actor: actor1, seq: 1, startOp: 1, time: 0, deps: [], ops: [\n      {action: 'makeText', obj: '_root',       key: 'text',           insert: false,             pred: []},\n      {action: 'set',      obj: `1@${actor1}`, elemId: '_head',       insert: true,  value: 'a', pred: []}\n    ]}\n    const change2 = {actor: actor1, seq: 2, startOp: 3, time: 0, deps: [hash(change1)], ops: [\n      {action: 'set',      obj: `1@${actor1}`, elemId: `2@${actor1}`, insert: false, value: 'b', pred: [`2@${actor1}`]},\n      {action: 'set',      obj: `1@${actor1}`, elemId: `2@${actor1}`, insert: false, value: 'c', pred: [`2@${actor1}`]}\n    ]}\n    const change3 = {actor: actor2, seq: 1, startOp: 3, time: 0, deps: [hash(change1)], ops: [\n      {action: 'set',      obj: `1@${actor1}`, elemId: `2@${actor1}`, insert: false, value: 'x', pred: [`2@${actor1}`]}\n    ]}\n    const backend = new BackendDoc()\n    assert.deepStrictEqual(backend.applyChanges([change1, change2, change3].map(encodeChange)), {\n      maxOp: 4, clock: {[actor1]: 2, [actor2]: 1}, deps: [hash(change2), hash(change3)].sort(), pendingChanges: 0,\n      diffs: {objectId: '_root', type: 'map', props: {text: {[`1@${actor1}`]: {\n        objectId: `1@${actor1}`, type: 'text', edits: [\n          {action: 'insert', index: 0, elemId: `2@${actor1}`, opId: `3@${actor1}`, value: {type: 'value', value: 'b'}},\n          {action: 'update', index: 0, opId: `3@${actor2}`, value: {type: 'value', value: 'x'}},\n          {action: 'update', index: 0, opId: `4@${actor1}`, value: {type: 'value', value: 'c'}}\n        ]\n      }}}}\n    })\n    // Order of operations in the document:\n    // {action: 'makeText', id: `1@${actor1}`, obj: '_root',       key: 'text',           insert: false,             succ: []},\n    // {action: 'set',      id: `2@${actor1}`, obj: `1@${actor1}`, elemId: '_head',       insert: true,  value: 'a', succ: [`3@${actor1}`, `3@${actor2}`, `4@${actor1}`]},\n    // {action: 'set',      id: `3@${actor1}`, obj: `1@${actor1}`, elemId: `2@${actor1}`, insert: false, value: 'b', succ: []},\n    // {action: 'set',      id: `3@${actor2}`, obj: `1@${actor1}`, elemId: `2@${actor1}`, insert: false, value: 'x', succ: []},\n    // {action: 'set',      id: `4@${actor1}`, obj: `1@${actor1}`, elemId: `2@${actor1}`, insert: false, value: 'c', succ: []}\n    checkColumns(backend.blocks[0], {\n      objActor: [0, 1, 4, 0],\n      objCtr:   [0, 1, 4, 1],\n      keyActor: [0, 2, 3, 0],\n      keyCtr:   [0, 1, 0x7e, 0, 2, 2, 0], // null, 0, 2, 2, 2\n      keyStr:   [0x7f, 4, 0x74, 0x65, 0x78, 0x74, 0, 4], // 'text', 4x null\n      idActor:  [3, 0, 0x7e, 1, 0], // 3x actor1, 1x actor2, 1x actor1\n      idCtr:    [3, 1, 0x7e, 0, 1], // 1, 2, 3, 3, 4\n      insert:   [1, 1, 3], // false, true, false, false, false\n      action:   [0x7f, 4, 4, 1], // makeText, 4x set\n      valLen:   [0x7f, 0, 4, 0x16], // null, 4x 1-byte string\n      valRaw:   [0x61, 0x62, 0x78, 0x63], // 'a', 'b', 'x', 'c'\n      succNum:  [0x7e, 0, 3, 3, 0], // 0, 3, 0, 0, 0\n      succActor: [0x7d, 0, 1, 0], // actor1, actor2, actor1\n      succCtr:   [0x7d, 3, 0, 1] // 3, 3, 4\n    })\n    assert.strictEqual(backend.blocks[0].numOps, 5)\n    assert.strictEqual(backend.blocks[0].lastKey, undefined)\n    assert.strictEqual(backend.blocks[0].numVisible, 1)\n    assert.strictEqual(backend.blocks[0].lastObjectActor, 0)\n    assert.strictEqual(backend.blocks[0].lastObjectCtr, 1)\n    assert.strictEqual(backend.blocks[0].firstVisibleActor, 0)\n    assert.strictEqual(backend.blocks[0].firstVisibleCtr, 2)\n    assert.strictEqual(backend.blocks[0].lastVisibleActor, 0)\n    assert.strictEqual(backend.blocks[0].lastVisibleCtr, 2)\n  })\n\n  it('should allow element deletes and overwrites in the same change', () => {\n    const actor = uuid()\n    const change1 = {actor, seq: 1, startOp: 1, time: 0, deps: [], ops: [\n      {action: 'makeText', obj: '_root',      key: 'text',          insert: false,             pred: []},\n      {action: 'set',      obj: `1@${actor}`, elemId: '_head',      insert: true,  value: 'a', pred: []},\n      {action: 'set',      obj: `1@${actor}`, elemId: `2@${actor}`, insert: true,  value: 'b', pred: []}\n    ]}\n    const change2 = {actor, seq: 2, startOp: 4, time: 0, deps: [hash(change1)], ops: [\n      {action: 'del',      obj: `1@${actor}`, elemId: `2@${actor}`, insert: false,             pred: [`2@${actor}`]},\n      {action: 'set',      obj: `1@${actor}`, elemId: `3@${actor}`, insert: false, value: 'x', pred: [`3@${actor}`]}\n    ]}\n    const backend = new BackendDoc()\n    assert.deepStrictEqual(backend.applyChanges([encodeChange(change1), encodeChange(change2)]), {\n      maxOp: 5, clock: {[actor]: 2}, deps: [hash(change2)], pendingChanges: 0,\n      diffs: {objectId: '_root', type: 'map', props: {text: {[`1@${actor}`]: {\n        objectId: `1@${actor}`, type: 'text', edits: [\n          {action: 'multi-insert', index: 0, elemId: `2@${actor}`, values: ['a', 'b']},\n          {action: 'remove', index: 0, count: 1},\n          {action: 'update', index: 0, opId: `5@${actor}`, value: {type: 'value', value: 'x'}}\n        ]\n      }}}}\n    })\n    checkColumns(backend.blocks[0], {\n      objActor: [0, 1, 3, 0],\n      objCtr:   [0, 1, 3, 1],\n      keyActor: [0, 2, 2, 0],\n      keyCtr:   [0, 1, 0x7d, 0, 2, 1], // null, 0, 2, 3\n      keyStr:   [0x7f, 4, 0x74, 0x65, 0x78, 0x74, 0, 3], // 'text', 3x null\n      idActor:  [4, 0],\n      idCtr:    [3, 1, 0x7f, 2], // 1, 2, 3, 5\n      insert:   [1, 2, 1], // false, true, true, false\n      action:   [0x7f, 4, 3, 1], // makeText, 3x set\n      valLen:   [0x7f, 0, 3, 0x16], // null, 3x 1-byte string\n      valRaw:   [0x61, 0x62, 0x78], // 'a', 'b', 'x'\n      succNum:  [0x7f, 0, 2, 1, 0x7f, 0], // 0, 1, 1, 0\n      succActor: [2, 0],\n      succCtr:   [0x7e, 4, 1] // 4, 5\n    })\n    assert.strictEqual(backend.blocks[0].numOps, 4)\n    assert.strictEqual(backend.blocks[0].lastKey, undefined)\n    assert.strictEqual(backend.blocks[0].numVisible, 1)\n    assert.strictEqual(backend.blocks[0].lastObjectActor, 0)\n    assert.strictEqual(backend.blocks[0].lastObjectCtr, 1)\n    assert.strictEqual(backend.blocks[0].firstVisibleActor, 0)\n    assert.strictEqual(backend.blocks[0].firstVisibleCtr, 3)\n    assert.strictEqual(backend.blocks[0].lastVisibleActor, 0)\n    assert.strictEqual(backend.blocks[0].lastVisibleCtr, 3)\n  })\n\n  it('should allow concurrent deletion and assignment of the same list element', () => {\n    const actor1 = '01234567', actor2 = '89abcdef'\n    const change1 = {actor: actor1, seq: 1, startOp: 1, time: 0, deps: [], ops: [\n      {action: 'makeList', obj: '_root',       key: 'list',           insert: false,           pred: []},\n      {action: 'set',      obj: `1@${actor1}`, elemId: '_head',       insert: true,  datatype: 'uint', value: 1, pred: []}\n    ]}\n    const change2 = {actor: actor1, seq: 2, startOp: 3, time: 0, deps: [hash(change1)], ops: [\n      {action: 'del',      obj: `1@${actor1}`, elemId: `2@${actor1}`, insert: false,           pred: [`2@${actor1}`]}\n    ]}\n    const change3 = {actor: actor2, seq: 1, startOp: 3, time: 0, deps: [hash(change1)], ops: [\n      {action: 'set',      obj: `1@${actor1}`, elemId: `2@${actor1}`, insert: false, datatype: 'uint', value: 2, pred: [`2@${actor1}`]}\n    ]}\n    const backend1 = new BackendDoc(), backend2 = new BackendDoc()\n    assert.deepStrictEqual(backend1.applyChanges([encodeChange(change1), encodeChange(change2)]), {\n      maxOp: 3, clock: {[actor1]: 2}, deps: [hash(change2)], pendingChanges: 0,\n      diffs: {objectId: '_root', type: 'map', props: {list: {[`1@${actor1}`]: {\n        objectId: `1@${actor1}`, type: 'list', edits: [\n          {action: 'insert', index: 0, elemId: `2@${actor1}`, opId: `2@${actor1}`, value: {\n            type: 'value', value: 1, datatype: 'uint'\n          }},\n          {action: 'remove', index: 0, count: 1}\n        ]\n      }}}}\n    })\n    assert.deepStrictEqual(backend1.applyChanges([encodeChange(change3)]), {\n      maxOp: 3, clock: {[actor1]: 2, [actor2]: 1}, deps: [hash(change2), hash(change3)].sort(), pendingChanges: 0,\n      diffs: {objectId: '_root', type: 'map', props: {list: {[`1@${actor1}`]: {\n        objectId: `1@${actor1}`, type: 'list', edits: [\n          {action: 'insert', index: 0, elemId: `2@${actor1}`, opId: `3@${actor2}`, value: {\n            type: 'value', value: 2, datatype: 'uint'\n          }}\n        ]\n      }}}}\n    })\n    assert.deepStrictEqual(backend2.applyChanges([encodeChange(change1), encodeChange(change3)]), {\n      maxOp: 3, clock: {[actor1]: 1, [actor2]: 1}, deps: [hash(change3)], pendingChanges: 0,\n      diffs: {objectId: '_root', type: 'map', props: {list: {[`1@${actor1}`]: {\n        objectId: `1@${actor1}`, type: 'list', edits: [\n          {action: 'insert', index: 0, elemId: `2@${actor1}`, opId: `3@${actor2}`, value: {\n            type: 'value', value: 2, datatype: 'uint'\n          }}\n        ]\n      }}}}\n    })\n    assert.deepStrictEqual(backend2.applyChanges([encodeChange(change2)]), {\n      maxOp: 3, clock: {[actor1]: 2, [actor2]: 1}, deps: [hash(change2), hash(change3)].sort(), pendingChanges: 0,\n      diffs: {objectId: '_root', type: 'map', props: {list: {[`1@${actor1}`]: {\n        objectId: `1@${actor1}`, type: 'list', edits: [\n          {action: 'update', index: 0, opId: `3@${actor2}`, value: {\n            type: 'value', value: 2, datatype: 'uint'\n          }}\n        ]\n      }}}}\n    })\n    for (let backend of [backend1, backend2]) {\n      checkColumns(backend.blocks[0], {\n        objActor: [0, 1, 2, 0], // null, actor1, actor1\n        objCtr:   [0, 1, 2, 1], // null, 1, 1\n        keyActor: [0, 2, 0x7f, 0], // null, null, actor1\n        keyCtr:   [0, 1, 0x7e, 0, 2], // null, 0, 2\n        keyStr:   [0x7f, 4, 0x6c, 0x69, 0x73, 0x74, 0, 2], // 'list', null, null\n        idActor:  [2, 0, 0x7f, 1], // actor1, actor1, actor2\n        idCtr:    [3, 1], // 1, 2, 3\n        insert:   [1, 1, 1], // false, true, false\n        action:   [0x7f, 2, 2, 1], // makeList, 2x set\n        valLen:   [0x7f, 0, 2, 0x13], // null, 2x 1-byte uint\n        valRaw:   [1, 2],\n        succNum:  [0x7d, 0, 2, 0], // 0, 2, 0\n        succActor: [0x7e, 0, 1], // 0, 1\n        succCtr:   [0x7e, 3, 0] // 3, 3\n      })\n      assert.strictEqual(backend.blocks[0].numOps, 3)\n      assert.strictEqual(backend.blocks[0].lastKey, undefined)\n      assert.strictEqual(backend.blocks[0].numVisible, 1)\n      assert.strictEqual(backend.blocks[0].lastObjectActor, 0)\n      assert.strictEqual(backend.blocks[0].lastObjectCtr, 1)\n      assert.strictEqual(backend.blocks[0].firstVisibleActor, 0)\n      assert.strictEqual(backend.blocks[0].firstVisibleCtr, 2)\n      assert.strictEqual(backend.blocks[0].lastVisibleActor, 0)\n      assert.strictEqual(backend.blocks[0].lastVisibleCtr, 2)\n    }\n  })\n\n  it('should handle updates inside conflicted properties', () => {\n    const actor1 = '01234567', actor2 = '89abcdef'\n    const change1 = {actor: actor1, seq: 1, startOp: 1, time: 0, deps: [], ops: [\n      {action: 'makeMap', obj: '_root',       key: 'map',         pred: []},\n      {action: 'set',     obj: `1@${actor1}`, key: 'x', datatype: 'uint', value: 1, pred: []}\n    ]}\n    const change2 = {actor: actor2, seq: 1, startOp: 1, time: 0, deps: [], ops: [\n      {action: 'makeMap', obj: '_root',       key: 'map',         pred: []},\n      {action: 'set',     obj: `1@${actor2}`, key: 'y', datatype: 'uint', value: 2, pred: []}\n    ]}\n    const change3 = {actor: actor1, seq: 2, startOp: 3, time: 0, deps: [hash(change1), hash(change2)], ops: [\n      {action: 'set',     obj: `1@${actor1}`, key: 'x', datatype: 'uint', value: 3, pred: [`2@${actor1}`]}\n    ]}\n    const backend = new BackendDoc()\n    assert.deepStrictEqual(backend.applyChanges([encodeChange(change1)]), {\n      maxOp: 2, clock: {[actor1]: 1}, deps: [hash(change1)], pendingChanges: 0,\n      diffs: {objectId: '_root', type: 'map', props: {map: {\n        [`1@${actor1}`]: {objectId: `1@${actor1}`, type: 'map', props: {x: {[`2@${actor1}`]: {\n          type: 'value', value: 1, datatype: 'uint'\n        }}}}\n      }}}\n    })\n    assert.deepStrictEqual(backend.applyChanges([encodeChange(change2)]), {\n      maxOp: 2, clock: {[actor1]: 1, [actor2]: 1}, deps: [hash(change1), hash(change2)].sort(), pendingChanges: 0,\n      diffs: {objectId: '_root', type: 'map', props: {map: {\n        [`1@${actor1}`]: {objectId: `1@${actor1}`, type: 'map', props: {}},\n        [`1@${actor2}`]: {objectId: `1@${actor2}`, type: 'map', props: {y: {[`2@${actor2}`]: {\n          type: 'value', value: 2, datatype: 'uint'\n        }}}}\n      }}}\n    })\n    assert.deepStrictEqual(backend.applyChanges([encodeChange(change3)]), {\n      maxOp: 3, clock: {[actor1]: 2, [actor2]: 1}, deps: [hash(change3)], pendingChanges: 0,\n      diffs: {objectId: '_root', type: 'map', props: {map: {\n        [`1@${actor1}`]: {objectId: `1@${actor1}`, type: 'map', props: {x: {[`3@${actor1}`]: {\n          type: 'value', value: 3, datatype: 'uint'\n        }}}},\n        [`1@${actor2}`]: {objectId: `1@${actor2}`, type: 'map', props: {}}\n      }}}\n    })\n    checkColumns(backend.blocks[0], {\n      objActor: [0, 2, 2, 0, 0x7f, 1],\n      objCtr:   [0, 2, 3, 1],\n      keyActor: [],\n      keyCtr:   [],\n      keyStr:   [2, 3, 0x6d, 0x61, 0x70, 2, 1, 0x78, 0x7f, 1, 0x79], // 'map', 'map', 'x', 'x', 'y'\n      idActor:  [0x7e, 0, 1, 2, 0, 0x7f, 1], // 0, 1, 0, 0, 1\n      idCtr:    [0x7e, 1, 0, 2, 1, 0x7f, 0x7f], // 1, 1, 2, 3, 2\n      insert:   [5],\n      action:   [2, 0, 3, 1], // 2x makeMap, 3x set\n      valLen:   [2, 0, 3, 0x13], // 2x null, 3x uint\n      valRaw:   [1, 3, 2],\n      succNum:  [2, 0, 0x7f, 1, 2, 0], // 0, 0, 1, 0, 0\n      succActor: [0x7f, 0],\n      succCtr:   [0x7f, 3]\n    })\n    assert.strictEqual(backend.blocks[0].lastKey, 'y')\n    assert.strictEqual(backend.blocks[0].numOps, 5)\n    assert.strictEqual(backend.blocks[0].lastObjectActor, 1)\n    assert.strictEqual(backend.blocks[0].lastObjectCtr, 1)\n  })\n\n  it('should allow a conflict consisting of a nested object and a value', () => {\n    const actor1 = '01234567', actor2 = '89abcdef'\n    const change1 = {actor: actor1, seq: 1, startOp: 1, time: 0, deps: [], ops: [\n      {action: 'makeMap', obj: '_root',       key: 'x',           pred: []},\n      {action: 'set',     obj: `1@${actor1}`, key: 'y', datatype: 'uint', value: 2, pred: []}\n    ]}\n    const change2 = {actor: actor2, seq: 1, startOp: 1, time: 0, deps: [], ops: [\n      {action: 'set',     obj: '_root',       key: 'x', datatype: 'uint', value: 1, pred: []}\n    ]}\n    const change3 = {actor: actor1, seq: 2, startOp: 3, time: 0, deps: [hash(change1), hash(change2)], ops: [\n      {action: 'set',     obj: `1@${actor1}`, key: 'y', datatype: 'uint', value: 3, pred: [`2@${actor1}`]}\n    ]}\n    const backend = new BackendDoc()\n    assert.deepStrictEqual(backend.applyChanges([encodeChange(change1)]), {\n      maxOp: 2, clock: {[actor1]: 1}, deps: [hash(change1)], pendingChanges: 0,\n      diffs: {objectId: '_root', type: 'map', props: {x: {\n        [`1@${actor1}`]: {objectId: `1@${actor1}`, type: 'map', props: {y: {[`2@${actor1}`]: {\n          type: 'value', value: 2, datatype: 'uint'\n        }}}}\n      }}}\n    })\n    assert.deepStrictEqual(backend.applyChanges([encodeChange(change2)]), {\n      maxOp: 2, clock: {[actor1]: 1, [actor2]: 1}, deps: [hash(change1), hash(change2)].sort(), pendingChanges: 0,\n      diffs: {objectId: '_root', type: 'map', props: {x: {\n        [`1@${actor1}`]: {objectId: `1@${actor1}`, type: 'map', props: {}},\n        [`1@${actor2}`]: {type: 'value', value: 1, datatype: 'uint'}\n      }}}\n    })\n    assert.deepStrictEqual(backend.applyChanges([encodeChange(change3)]), {\n      maxOp: 3, clock: {[actor1]: 2, [actor2]: 1}, deps: [hash(change3)], pendingChanges: 0,\n      diffs: {objectId: '_root', type: 'map', props: {x: {\n        [`1@${actor1}`]: {objectId: `1@${actor1}`, type: 'map', props: {y: {[`3@${actor1}`]: {\n          type: 'value', value: 3, datatype: 'uint'\n        }}}},\n        [`1@${actor2}`]: {type: 'value', value: 1, datatype: 'uint'}\n      }}}\n    })\n    checkColumns(backend.blocks[0], {\n      objActor: [0, 2, 2, 0],\n      objCtr:   [0, 2, 2, 1],\n      keyActor: [],\n      keyCtr:   [],\n      keyStr:   [2, 1, 0x78, 2, 1, 0x79], // 'x', 'x', 'y', 'y'\n      idActor:  [0x7e, 0, 1, 2, 0], // 0, 1, 0, 0\n      idCtr:    [0x7e, 1, 0, 2, 1], // 1, 1, 2, 3\n      insert:   [4],\n      action:   [0x7f, 0, 3, 1], // makeMap, 3x set\n      valLen:   [0x7f, 0, 3, 0x13], // null, 3x uint\n      valRaw:   [1, 2, 3],\n      succNum:  [2, 0, 0x7e, 1, 0], // 0, 0, 1, 0\n      succActor: [0x7f, 0],\n      succCtr:   [0x7f, 3]\n    })\n    assert.strictEqual(backend.blocks[0].lastKey, 'y')\n    assert.strictEqual(backend.blocks[0].numOps, 4)\n    assert.strictEqual(backend.blocks[0].lastObjectActor, 0)\n    assert.strictEqual(backend.blocks[0].lastObjectCtr, 1)\n  })\n\n  it('should allow changes containing unknown columns, actions, and datatypes', () => {\n    const change = new Uint8Array([\n      0x85, 0x6f, 0x4a, 0x83, // magic bytes\n      0xad, 0xfb, 0x1a, 0x69, // checksum\n      1, 51, 0, 2, 0x12, 0x34, // chunkType: change, length, deps, actor '1234'\n      1, 1, 0, 0, // seq, startOp, time, message\n      0, 9, // actor list, column count\n      0x15, 3, 0x34, 1, 0x42, 2, // keyStr, insert, action\n      0x56, 2, 0x57, 4, 0x70, 2, // valLen, valRaw, predNum\n      0xf0, 1, 2, 0xf1, 1, 2, 0xf3, 1, 2, // unknown column group (3 columns of type GROUP_CARD, ACTOR_ID, INT_DELTA)\n      0x7f, 1, 0x78, // keyStr: 'x'\n      1, // insert: false\n      0x7f, 17, // unknown action type: 17\n      0x7f, 0x4e, // valLen: 4 bytes of unknown type 14\n      1, 2, 3, 4, // valRaw: 4 bytes\n      0x7f, 0, // predNum: 0\n      0x7f, 2, // unknown cardinality column: 2 values\n      2, 0, // unknown actor column: 0, 0\n      2, 1 // unknown delta column: 1, 2\n    ])\n    const backend = new BackendDoc()\n    assert.deepStrictEqual(backend.applyChanges([change]), {\n      maxOp: 1, clock: {'1234': 1}, deps: [decodeChange(change).hash], pendingChanges: 0,\n      diffs: {objectId: '_root', type: 'map', props: {x: {}}}\n    })\n    checkColumns(backend.blocks[0], {\n      objActor: [],\n      objCtr:   [],\n      keyActor: [],\n      keyCtr:   [],\n      keyStr:   [0x7f, 1, 0x78],\n      idActor:  [0x7f, 0],\n      idCtr:    [0x7f, 1],\n      insert:   [1],\n      action:   [0x7f, 17],\n      valLen:   [0x7f, 0x4e],\n      valRaw:   [1, 2, 3, 4],\n      succNum:  [0x7f, 0],\n      succActor: [],\n      succCtr:   [],\n      240:      [0x7f, 2],\n      241:      [2, 0],\n      243:      [2, 1]\n    })\n    assert.strictEqual(backend.blocks[0].lastKey, 'x')\n    assert.strictEqual(backend.blocks[0].numOps, 1)\n    assert.strictEqual(backend.blocks[0].lastObjectActor, null)\n    assert.strictEqual(backend.blocks[0].lastObjectCtr, null)\n  })\n\n  it('should split a long insertion into multiple blocks', () => {\n    const actor = uuid()\n    const change = {actor, seq: 1, startOp: 1, time: 0, deps: [], ops: [\n      {action: 'makeText', obj: '_root',      key: 'text',     insert: false,             pred: []},\n      {action: 'set',      obj: `1@${actor}`, elemId: '_head', insert: true,  value: 'a', pred: []}\n    ]}\n    for (let i = 2; i <= MAX_BLOCK_SIZE; i++) {\n      change.ops.push({action: 'set', obj: `1@${actor}`, elemId: `${i}@${actor}`, insert: true, value: 'a', pred: []})\n    }\n    const backend = new BackendDoc()\n    const patch = backend.applyChanges([encodeChange(change)])\n    const edits = patch.diffs.props.text[`1@${actor}`].edits\n    assert.strictEqual(edits.length, 1)\n    assert.strictEqual(edits[0].action, 'multi-insert')\n    assert.strictEqual(edits[0].values.length, MAX_BLOCK_SIZE)\n    assert.strictEqual(backend.blocks.length, 2)\n    assert.strictEqual(bloomFilterContains(backend.blocks[0].bloom, 0, 2), true)\n    assert.strictEqual(bloomFilterContains(backend.blocks[0].bloom, 0, MAX_BLOCK_SIZE / 2 + 1), true)\n    assert.strictEqual(bloomFilterContains(backend.blocks[0].bloom, 0, MAX_BLOCK_SIZE / 2 + 2), false)\n    assert.strictEqual(bloomFilterContains(backend.blocks[0].bloom, 0, MAX_BLOCK_SIZE + 1), false)\n    assert.strictEqual(bloomFilterContains(backend.blocks[1].bloom, 0, 2), false)\n    assert.strictEqual(bloomFilterContains(backend.blocks[1].bloom, 0, MAX_BLOCK_SIZE / 2 + 1), false)\n    assert.strictEqual(bloomFilterContains(backend.blocks[1].bloom, 0, MAX_BLOCK_SIZE / 2 + 2), true)\n    assert.strictEqual(bloomFilterContains(backend.blocks[1].bloom, 0, MAX_BLOCK_SIZE + 1), true)\n    const sizeByte1 = 0x80 | 0x7f & (MAX_BLOCK_SIZE / 2), sizeByte2 = (MAX_BLOCK_SIZE / 2) >>> 7\n    checkColumns(backend.blocks[0], {\n      objActor: [0, 1, sizeByte1, sizeByte2, 0],\n      objCtr:   [0, 1, sizeByte1, sizeByte2, 1],\n      keyActor: [0, 2, sizeByte1 - 1, sizeByte2, 0],\n      keyCtr:   [0, 1, 0x7e, 0, 2, sizeByte1 - 2, sizeByte2, 1], // null, 0, 2, 3, 4, ...\n      keyStr:   [0x7f, 4, 0x74, 0x65, 0x78, 0x74, 0, sizeByte1, sizeByte2], // 'text', nulls\n      idActor:  [sizeByte1 + 1, sizeByte2, 0],\n      idCtr:    [sizeByte1 + 1, sizeByte2, 1],\n      insert:   [1, sizeByte1, sizeByte2],\n      action:   [0x7f, 4, sizeByte1, sizeByte2, 1],\n      valLen:   [0x7f, 0, sizeByte1, sizeByte2, 0x16],\n      valRaw:   new Array(MAX_BLOCK_SIZE / 2).fill(0x61),\n      succNum:  [sizeByte1 + 1, sizeByte2, 0],\n      succActor: [],\n      succCtr:   []\n    })\n    checkColumns(backend.blocks[1], {\n      objActor: [sizeByte1, sizeByte2, 0],\n      objCtr:   [sizeByte1, sizeByte2, 1],\n      keyActor: [sizeByte1, sizeByte2, 0],\n      keyCtr:   [0x7f, sizeByte1 + 1, sizeByte2, sizeByte1 - 1, sizeByte2, 1],\n      keyStr:   [],\n      idActor:  [sizeByte1, sizeByte2, 0],\n      idCtr:    [0x7f, sizeByte1 + 2, sizeByte2, sizeByte1 - 1, sizeByte2, 1],\n      insert:   [0, sizeByte1, sizeByte2],\n      action:   [sizeByte1, sizeByte2, 1],\n      valLen:   [sizeByte1, sizeByte2, 0x16],\n      valRaw:   new Array(MAX_BLOCK_SIZE / 2).fill(0x61),\n      succNum:  [sizeByte1, sizeByte2, 0],\n      succActor: [],\n      succCtr:   []\n    })\n  })\n\n  it('should split a sequence of short insertions into multiple blocks', () => {\n    const actor = uuid(), backend = new BackendDoc()\n    const change1 = {actor, seq: 1, startOp: 1, time: 0, deps: [], ops: [\n      {action: 'makeText', obj: '_root',      key: 'text',     insert: false,             pred: []},\n      {action: 'set',      obj: `1@${actor}`, elemId: '_head', insert: true,  value: 'a', pred: []}\n    ]}\n    backend.applyChanges([encodeChange(change1)])\n    for (let i = 2; i <= MAX_BLOCK_SIZE; i++) {\n      const change2 = {actor, seq: i, startOp: i + 1, time: 0, deps: backend.heads, ops: [\n        {action: 'set', obj: `1@${actor}`, elemId: `${i}@${actor}`, insert: true, value: 'a', pred: []}\n      ]}\n      assert.deepStrictEqual(backend.applyChanges([encodeChange(change2)]), {\n        maxOp: i + 1, clock: {[actor]: i}, deps: [hash(change2)], pendingChanges: 0,\n        diffs: {objectId: '_root', type: 'map', props: {text: {[`1@${actor}`]: {\n          objectId: `1@${actor}`, type: 'text', edits: [\n            {action: 'insert', index: i - 1, elemId: `${i + 1}@${actor}`, opId: `${i + 1}@${actor}`, value: {type: 'value', value: 'a'}}\n          ]\n        }}}}\n      })\n    }\n    assert.strictEqual(backend.blocks.length, 2)\n    assert.strictEqual(bloomFilterContains(backend.blocks[0].bloom, 0, 2), true)\n    assert.strictEqual(bloomFilterContains(backend.blocks[0].bloom, 0, MAX_BLOCK_SIZE / 2 + 1), true)\n    assert.strictEqual(bloomFilterContains(backend.blocks[0].bloom, 0, MAX_BLOCK_SIZE / 2 + 2), false)\n    assert.strictEqual(bloomFilterContains(backend.blocks[0].bloom, 0, MAX_BLOCK_SIZE + 1), false)\n    assert.strictEqual(bloomFilterContains(backend.blocks[1].bloom, 0, 2), false)\n    assert.strictEqual(bloomFilterContains(backend.blocks[1].bloom, 0, MAX_BLOCK_SIZE / 2 + 1), false)\n    assert.strictEqual(bloomFilterContains(backend.blocks[1].bloom, 0, MAX_BLOCK_SIZE / 2 + 2), true)\n    assert.strictEqual(bloomFilterContains(backend.blocks[1].bloom, 0, MAX_BLOCK_SIZE + 1), true)\n    const sizeByte1 = 0x80 | 0x7f & (MAX_BLOCK_SIZE / 2), sizeByte2 = (MAX_BLOCK_SIZE / 2) >>> 7\n    checkColumns(backend.blocks[0], {\n      objActor: [0, 1, sizeByte1, sizeByte2, 0],\n      objCtr:   [0, 1, sizeByte1, sizeByte2, 1],\n      keyActor: [0, 2, sizeByte1 - 1, sizeByte2, 0],\n      keyCtr:   [0, 1, 0x7e, 0, 2, sizeByte1 - 2, sizeByte2, 1], // null, 0, 2, 3, 4, ...\n      keyStr:   [0x7f, 4, 0x74, 0x65, 0x78, 0x74, 0, sizeByte1, sizeByte2], // 'text', nulls\n      idActor:  [sizeByte1 + 1, sizeByte2, 0],\n      idCtr:    [sizeByte1 + 1, sizeByte2, 1],\n      insert:   [1, sizeByte1, sizeByte2],\n      action:   [0x7f, 4, sizeByte1, sizeByte2, 1],\n      valLen:   [0x7f, 0, sizeByte1, sizeByte2, 0x16],\n      valRaw:   new Array(MAX_BLOCK_SIZE / 2).fill(0x61),\n      succNum:  [sizeByte1 + 1, sizeByte2, 0],\n      succActor: [],\n      succCtr:   []\n    })\n    checkColumns(backend.blocks[1], {\n      objActor: [sizeByte1, sizeByte2, 0],\n      objCtr:   [sizeByte1, sizeByte2, 1],\n      keyActor: [sizeByte1, sizeByte2, 0],\n      keyCtr:   [0x7f, sizeByte1 + 1, sizeByte2, sizeByte1 - 1, sizeByte2, 1],\n      keyStr:   [],\n      idActor:  [sizeByte1, sizeByte2, 0],\n      idCtr:    [0x7f, sizeByte1 + 2, sizeByte2, sizeByte1 - 1, sizeByte2, 1],\n      insert:   [0, sizeByte1, sizeByte2],\n      action:   [sizeByte1, sizeByte2, 1],\n      valLen:   [sizeByte1, sizeByte2, 0x16],\n      valRaw:   new Array(MAX_BLOCK_SIZE / 2).fill(0x61),\n      succNum:  [sizeByte1, sizeByte2, 0],\n      succActor: [],\n      succCtr:   []\n    })\n  })\n\n  it('should handle insertions with Bloom filter false positives', () => {\n    const actor = uuid()\n    const change1 = {actor, seq: 1, startOp: 1, time: 0, deps: [], ops: [\n      {action: 'makeText', obj: '_root',      key: 'text',     insert: false,             pred: []},\n      {action: 'set',      obj: `1@${actor}`, elemId: '_head', insert: true,  value: 'a', pred: []}\n    ]}\n    for (let i = 2; i <= 2 * MAX_BLOCK_SIZE; i++) {\n      change1.ops.push({action: 'set', obj: `1@${actor}`, elemId: `${i}@${actor}`, insert: true, value: 'a', pred: []})\n    }\n    const backend = new BackendDoc(), startOp = 2 * MAX_BLOCK_SIZE + 2\n    backend.applyChanges([encodeChange(change1)])\n    assert.strictEqual(backend.blocks.length, 3)\n    let keyCtr = backend.blocks[1].firstVisibleCtr\n    while (keyCtr <= backend.blocks[backend.blocks.length - 1].lastVisibleCtr) {\n      if (bloomFilterContains(backend.blocks[0].bloom, 0, keyCtr)) break\n      keyCtr++\n    }\n    if (keyCtr > backend.blocks[backend.blocks.length - 1].lastVisibleCtr) {\n      throw new Error('no false positive found')\n    }\n    const change2 = {actor, seq: 2, startOp, time: 0, deps: [hash(change1)], ops: [\n      {action: 'set', obj: `1@${actor}`, elemId: `${keyCtr}@${actor}`, insert: true,  value: 'a', pred: []}\n    ]}\n    const patch = backend.applyChanges([encodeChange(change2)])\n    assert.deepStrictEqual(patch.diffs.props.text[`1@${actor}`].edits, [{\n      action: 'insert',\n      index: keyCtr - 1,\n      elemId: `${startOp}@${actor}`,\n      opId: `${startOp}@${actor}`,\n      value: {type: 'value', value: 'a'}\n    }])\n  })\n\n  it('should delete many consecutive characters', () => {\n    const actor = uuid()\n    const change1 = {actor, seq: 1, startOp: 1, time: 0, deps: [], ops: [\n      {action: 'makeText', obj: '_root',      key: 'text',     insert: false,             pred: []},\n      {action: 'set',      obj: `1@${actor}`, elemId: '_head', insert: true,  value: 'a', pred: []}\n    ]}\n    for (let i = 2; i <= MAX_BLOCK_SIZE; i++) {\n      change1.ops.push({action: 'set', obj: `1@${actor}`, elemId: `${i}@${actor}`, insert: true, value: 'a', pred: []})\n    }\n    const change2 = {actor, seq: 2, startOp: MAX_BLOCK_SIZE + 3, time: 0, deps: [], ops: []}\n    for (let i = 2; i <= MAX_BLOCK_SIZE + 1; i++) {\n      change2.ops.push({action: 'del', obj: `1@${actor}`, elemId: `${i}@${actor}`, insert: false, pred: [`${i}@${actor}`]})\n    }\n    const backend = new BackendDoc()\n    backend.applyChanges([encodeChange(change1)])\n    const patch = backend.applyChanges([encodeChange(change2)])\n    assert.deepStrictEqual(patch.diffs.props.text[`1@${actor}`].edits, [{action: 'remove', index: 0, count: MAX_BLOCK_SIZE}])\n    assert.strictEqual(backend.blocks.length, 2)\n    const sizeByte1 = 0x80 | 0x7f & (MAX_BLOCK_SIZE / 2), sizeByte2 = (MAX_BLOCK_SIZE / 2) >>> 7\n    const firstSucc = MAX_BLOCK_SIZE + 3, secondSucc = MAX_BLOCK_SIZE + 3 + MAX_BLOCK_SIZE / 2\n    checkColumns(backend.blocks[0], {\n      objActor: [0, 1, sizeByte1, sizeByte2, 0],\n      objCtr:   [0, 1, sizeByte1, sizeByte2, 1],\n      keyActor: [0, 2, sizeByte1 - 1, sizeByte2, 0],\n      keyCtr:   [0, 1, 0x7e, 0, 2, sizeByte1 - 2, sizeByte2, 1], // null, 0, 2, 3, 4, ...\n      keyStr:   [0x7f, 4, 0x74, 0x65, 0x78, 0x74, 0, sizeByte1, sizeByte2], // 'text', nulls\n      idActor:  [sizeByte1 + 1, sizeByte2, 0],\n      idCtr:    [sizeByte1 + 1, sizeByte2, 1],\n      insert:   [1, sizeByte1, sizeByte2],\n      action:   [0x7f, 4, sizeByte1, sizeByte2, 1],\n      valLen:   [0x7f, 0, sizeByte1, sizeByte2, 0x16],\n      valRaw:   new Array(MAX_BLOCK_SIZE / 2).fill(0x61),\n      succNum:  [0x7f, 0, sizeByte1, sizeByte2, 1],\n      succActor: [sizeByte1, sizeByte2, 0],\n      succCtr:   [0x7f, 0x80 | (0x7f & firstSucc), firstSucc >>> 7, sizeByte1 - 1, sizeByte2, 1]\n    })\n    checkColumns(backend.blocks[1], {\n      objActor: [sizeByte1, sizeByte2, 0],\n      objCtr:   [sizeByte1, sizeByte2, 1],\n      keyActor: [sizeByte1, sizeByte2, 0],\n      keyCtr:   [0x7f, sizeByte1 + 1, sizeByte2, sizeByte1 - 1, sizeByte2, 1],\n      keyStr:   [],\n      idActor:  [sizeByte1, sizeByte2, 0],\n      idCtr:    [0x7f, sizeByte1 + 2, sizeByte2, sizeByte1 - 1, sizeByte2, 1],\n      insert:   [0, sizeByte1, sizeByte2],\n      action:   [sizeByte1, sizeByte2, 1],\n      valLen:   [sizeByte1, sizeByte2, 0x16],\n      valRaw:   new Array(MAX_BLOCK_SIZE / 2).fill(0x61),\n      succNum:  [sizeByte1, sizeByte2, 1],\n      succActor: [sizeByte1, sizeByte2, 0],\n      succCtr:   [0x7f, 0x80 | (0x7f & secondSucc), secondSucc >>> 7, sizeByte1 - 1, sizeByte2, 1]\n    })\n  })\n\n  it('should update an object that appears after a long text object', () => {\n    const actor = uuid()\n    const change1 = {actor, seq: 1, startOp: 1, time: 0, deps: [], ops: [\n      {action: 'makeText', obj: '_root',      key: 'text1',    insert: false,             pred: []},\n      {action: 'makeText', obj: '_root',      key: 'text2',    insert: false,             pred: []},\n      {action: 'set',      obj: `2@${actor}`, elemId: '_head', insert: true,  value: 'x', pred: []},\n      {action: 'set',      obj: `1@${actor}`, elemId: '_head', insert: true,  value: 'a', pred: []}\n    ]}\n    for (let i = 4; i <= MAX_BLOCK_SIZE; i++) {\n      change1.ops.push({action: 'set', obj: `1@${actor}`, elemId: `${i}@${actor}`, insert: true, value: 'a', pred: []})\n    }\n    const change2 = {actor, seq: 2, startOp: MAX_BLOCK_SIZE + 3, time: 0, deps: [], ops: [\n      {action: 'set',      obj: `2@${actor}`, elemId: `3@${actor}`, insert: true, value: 'x', pred: []}\n    ]}\n    const backend = new BackendDoc()\n    backend.applyChanges([encodeChange(change1)])\n    assert.deepStrictEqual(backend.applyChanges([encodeChange(change2)]).diffs.props, {text2: {[`2@${actor}`]: {\n      objectId: `2@${actor}`, type: 'text', edits: [{\n        action: 'insert',\n        index: 1,\n        opId: `${MAX_BLOCK_SIZE + 3}@${actor}`,\n        elemId: `${MAX_BLOCK_SIZE + 3}@${actor}`,\n        value: {type: 'value', value: 'x'}\n      }]\n    }}})\n  })\n\n  it('should place root object operations before a long text object', () => {\n    const actor = uuid()\n    const change = {actor, seq: 1, startOp: 1, time: 0, deps: [], ops: [\n      {action: 'makeText', obj: '_root',      key: 'text',     insert: false,             pred: []},\n      {action: 'set',      obj: `1@${actor}`, elemId: '_head', insert: true,  value: 'a', pred: []}\n    ]}\n    for (let i = 2; i <= MAX_BLOCK_SIZE; i++) {\n      change.ops.push({action: 'set', obj: `1@${actor}`, elemId: `${i}@${actor}`, insert: true, value: 'a', pred: []})\n    }\n    change.ops.push({action: 'set', obj: '_root', key: 'z', insert: false, value: 'zzz', pred: []})\n    const backend = new BackendDoc()\n    backend.applyChanges([encodeChange(change)])\n    const sizeByte1 = 0x80 | 0x7f & (MAX_BLOCK_SIZE / 2), sizeByte2 = (MAX_BLOCK_SIZE / 2) >>> 7\n    checkColumns(backend.blocks[0], {\n      objActor: [0, 2, sizeByte1, sizeByte2, 0],\n      objCtr:   [0, 2, sizeByte1, sizeByte2, 1],\n      keyActor: [0, 3, sizeByte1 - 1, sizeByte2, 0],\n      keyCtr:   [0, 2, 0x7e, 0, 2, sizeByte1 - 2, sizeByte2, 1], // null, null, 0, 2, 3, 4, ...\n      keyStr:   [0x7e, 4, 0x74, 0x65, 0x78, 0x74, 1, 0x7a, 0, sizeByte1, sizeByte2], // 'text', 'z', nulls\n      idActor:  [sizeByte1 + 2, sizeByte2, 0],\n      idCtr:    [0x7d, 1,\n                 0x80 | 0x7f & (MAX_BLOCK_SIZE + 1), 0x7f & (MAX_BLOCK_SIZE + 1) >>> 7,\n                 0x80 | 0x7f & -MAX_BLOCK_SIZE,      0x7f & -MAX_BLOCK_SIZE      >>> 7,\n                 sizeByte1 - 1, sizeByte2, 1],\n      insert:   [2, sizeByte1, sizeByte2],\n      action:   [0x7f, 4, sizeByte1 + 1, sizeByte2, 1],\n      valLen:   [0x7e, 0, 0x36, sizeByte1, sizeByte2, 0x16],\n      valRaw:   [0x7a, 0x7a, 0x7a].concat(new Array(MAX_BLOCK_SIZE / 2).fill(0x61)),\n      succNum:  [sizeByte1 + 2, sizeByte2, 0],\n      succActor: [],\n      succCtr:   []\n    })\n    checkColumns(backend.blocks[1], {\n      objActor: [sizeByte1, sizeByte2, 0],\n      objCtr:   [sizeByte1, sizeByte2, 1],\n      keyActor: [sizeByte1, sizeByte2, 0],\n      keyCtr:   [0x7f, sizeByte1 + 1, sizeByte2, sizeByte1 - 1, sizeByte2, 1],\n      keyStr:   [],\n      idActor:  [sizeByte1, sizeByte2, 0],\n      idCtr:    [0x7f, sizeByte1 + 2, sizeByte2, sizeByte1 - 1, sizeByte2, 1],\n      insert:   [0, sizeByte1, sizeByte2],\n      action:   [sizeByte1, sizeByte2, 1],\n      valLen:   [sizeByte1, sizeByte2, 0x16],\n      valRaw:   new Array(MAX_BLOCK_SIZE / 2).fill(0x61),\n      succNum:  [sizeByte1, sizeByte2, 0],\n      succActor: [],\n      succCtr:   []\n    })\n  })\n})\n"
  },
  {
    "path": "test/observable_test.js",
    "content": "const assert = require('assert')\nconst Automerge = process.env.TEST_DIST === '1' ? require('../dist/automerge') : require('../src/automerge')\n\ndescribe('Automerge.Observable', () => {\n  it('allows registering a callback on the root object', () => {\n    let observable = new Automerge.Observable(), callbackChanges\n    let doc = Automerge.init({observable}), actor = Automerge.getActorId(doc)\n    observable.observe(doc, (diff, before, after, local, changes) => {\n      callbackChanges = changes\n      assert.deepStrictEqual(diff, {\n        objectId: '_root', type: 'map', props: {bird: {[`1@${actor}`]: {type: 'value', value: 'Goldfinch'}}}\n      })\n      assert.deepStrictEqual(before, {})\n      assert.deepStrictEqual(after, {bird: 'Goldfinch'})\n      assert.strictEqual(local, true)\n      assert.strictEqual(changes.length, 1)\n    })\n    doc = Automerge.change(doc, doc => doc.bird = 'Goldfinch')\n    assert.strictEqual(callbackChanges.length, 1)\n    assert.ok(callbackChanges[0] instanceof Uint8Array)\n    assert.strictEqual(callbackChanges[0], Automerge.getLastLocalChange(doc))\n  })\n\n  it('allows registering a callback on a text object', () => {\n    let observable = new Automerge.Observable(), callbackCalled = false\n    let doc = Automerge.from({text: new Automerge.Text()}, {observable})\n    let actor = Automerge.getActorId(doc)\n    observable.observe(doc.text, (diff, before, after, local) => {\n      callbackCalled = true\n      assert.deepStrictEqual(diff, {\n        objectId: `1@${actor}`, type: 'text', edits: [\n          {action: 'multi-insert', index: 0, elemId: `2@${actor}`, values: ['a', 'b', 'c']}\n        ]\n      })\n      assert.deepStrictEqual(before.toString(), '')\n      assert.deepStrictEqual(after.toString(), 'abc')\n      assert.deepStrictEqual(local, true)\n    })\n    doc = Automerge.change(doc, doc => doc.text.insertAt(0, 'a', 'b', 'c'))\n    assert.strictEqual(callbackCalled, true)\n  })\n\n  it('should call the callback when applying remote changes', () => {\n    let observable = new Automerge.Observable(), callbackChanges\n    let local = Automerge.from({text: new Automerge.Text()}, {observable})\n    let remote = Automerge.init()\n    const localId = Automerge.getActorId(local), remoteId = Automerge.getActorId(remote)\n    observable.observe(local.text, (diff, before, after, local, changes) => {\n      callbackChanges = changes\n      assert.deepStrictEqual(diff, {\n        objectId: `1@${localId}`, type: 'text', edits: [\n          {action: 'insert', index: 0, elemId: `2@${remoteId}`, opId: `2@${remoteId}`, value: {type: 'value', value: 'a'}}\n        ]\n      })\n      assert.deepStrictEqual(before.toString(), '')\n      assert.deepStrictEqual(after.toString(), 'a')\n      assert.deepStrictEqual(local, false)\n    })\n    ;[remote] = Automerge.applyChanges(remote, Automerge.getAllChanges(local))\n    remote = Automerge.change(remote, doc => doc.text.insertAt(0, 'a'))\n    const allChanges = Automerge.getAllChanges(remote)\n    ;[local] = Automerge.applyChanges(local, allChanges)\n    assert.strictEqual(callbackChanges, allChanges)\n  })\n\n  it('should observe objects nested inside list elements', () => {\n    let observable = new Automerge.Observable(), callbackCalled = false\n    let doc = Automerge.from({todos: [{title: 'Buy milk', done: false}]}, {observable})\n    const actor = Automerge.getActorId(doc)\n    observable.observe(doc.todos[0], (diff, before, after, local) => {\n      callbackCalled = true\n      assert.deepStrictEqual(diff, {\n        objectId: `2@${actor}`, type: 'map', props: {done: {[`5@${actor}`]: {type: 'value', value: true}}}\n      })\n      assert.deepStrictEqual(before, {title: 'Buy milk', done: false})\n      assert.deepStrictEqual(after, {title: 'Buy milk', done: true})\n      assert.strictEqual(local, true)\n    })\n    doc = Automerge.change(doc, doc => doc.todos[0].done = true)\n    assert.strictEqual(callbackCalled, true)\n  })\n\n  it('should provide before and after states if list indexes changed', () => {\n    let observable = new Automerge.Observable(), callbackCalled = false\n    let doc = Automerge.from({todos: [{title: 'Buy milk', done: false}]}, {observable})\n    const actor = Automerge.getActorId(doc)\n    observable.observe(doc.todos[0], (diff, before, after, local) => {\n      callbackCalled = true\n      assert.deepStrictEqual(diff, {\n        objectId: `2@${actor}`, type: 'map', props: {done: {[`8@${actor}`]: {type: 'value', value: true}}}\n      })\n      assert.deepStrictEqual(before, {title: 'Buy milk', done: false})\n      assert.deepStrictEqual(after, {title: 'Buy milk', done: true})\n      assert.strictEqual(local, true)\n    })\n    doc = Automerge.change(doc, doc => {\n      doc.todos.unshift({title: 'Water plants', done: false})\n      doc.todos[1].done = true\n    })\n    assert.strictEqual(callbackCalled, true)\n  })\n\n  it('should observe rows inside tables', () => {\n    let observable = new Automerge.Observable(), callbackCalled = false\n    let doc = Automerge.init({observable}), actor = Automerge.getActorId(doc), rowId\n    doc = Automerge.change(doc, doc => {\n      doc.todos = new Automerge.Table()\n      rowId = doc.todos.add({title: 'Buy milk', done: false})\n    })\n    observable.observe(doc.todos.byId(rowId), (diff, before, after, local) => {\n      callbackCalled = true\n      assert.deepStrictEqual(diff, {\n        objectId: `2@${actor}`, type: 'map', props: {done: {[`5@${actor}`]: {type: 'value', value: true}}}\n      })\n      assert.deepStrictEqual(before, {id: rowId, title: 'Buy milk', done: false})\n      assert.deepStrictEqual(after, {id: rowId, title: 'Buy milk', done: true})\n      assert.strictEqual(local, true)\n    })\n    doc = Automerge.change(doc, doc => doc.todos.byId(rowId).done = true)\n    assert.strictEqual(callbackCalled, true)\n  })\n\n  it('should observe nested objects inside text', () => {\n    let observable = new Automerge.Observable(), callbackCalled = false\n    let doc = Automerge.init({observable}), actor = Automerge.getActorId(doc)\n    doc = Automerge.change(doc, doc => {\n      doc.text = new Automerge.Text()\n      doc.text.insertAt(0, 'a', 'b', {start: 'bold'}, 'c', {end: 'bold'})\n    })\n    observable.observe(doc.text.get(2), (diff, before, after, local) => {\n      callbackCalled = true\n      assert.deepStrictEqual(diff, {\n        objectId: `4@${actor}`, type: 'map', props: {start: {[`9@${actor}`]: {type: 'value', value: 'italic'}}}\n      })\n      assert.deepStrictEqual(before, {start: 'bold'})\n      assert.deepStrictEqual(after, {start: 'italic'})\n      assert.strictEqual(local, true)\n    })\n    doc = Automerge.change(doc, doc => doc.text.get(2).start = 'italic')\n    assert.strictEqual(callbackCalled, true)\n  })\n\n  it('should not allow observers on non-document objects', () => {\n    let observable = new Automerge.Observable()\n    let doc = Automerge.init({observable})\n    assert.throws(() => {\n      Automerge.change(doc, doc => {\n        const text = new Automerge.Text()\n        doc.text = text\n        observable.observe(text, () => {})\n      })\n    }, /The observed object must be part of an Automerge document/)\n  })\n\n  it('should allow multiple observers', () => {\n    let observable = new Automerge.Observable(), called1 = false, called2 = false\n    let doc = Automerge.init({observable})\n    observable.observe(doc, () => { called1 = true })\n    observable.observe(doc, () => { called2 = true })\n    Automerge.change(doc, doc => doc.foo = 'bar')\n    assert.strictEqual(called1, true)\n    assert.strictEqual(called2, true)\n  })\n})\n"
  },
  {
    "path": "test/proxies_test.js",
    "content": "const assert = require('assert')\nconst Automerge = process.env.TEST_DIST === '1' ? require('../dist/automerge') : require('../src/automerge')\nconst { assertEqualsOneOf } = require('./helpers')\nconst UUID_PATTERN = /^[0-9a-f]{32}$/\n\ndescribe('Automerge proxy API', () => {\n  describe('root object', () => {\n    it('should have a fixed object ID', () => {\n      Automerge.change(Automerge.init(), doc => {\n        assert.strictEqual(Automerge.getObjectId(doc), '_root')\n      })\n    })\n\n    it('should know its actor ID', () => {\n      Automerge.change(Automerge.init(), doc => {\n        assert(UUID_PATTERN.test(Automerge.getActorId(doc).toString()))\n        assert.notEqual(Automerge.getActorId(doc), '_root')\n        assert.strictEqual(Automerge.getActorId(Automerge.init('01234567')), '01234567')\n      })\n    })\n\n    it('should expose keys as object properties', () => {\n      Automerge.change(Automerge.init(), doc => {\n        doc.key1 = 'value1'\n        assert.strictEqual(doc.key1, 'value1')\n      })\n    })\n\n    it('should return undefined for unknown properties', () => {\n      Automerge.change(Automerge.init(), doc => {\n        assert.strictEqual(doc.someProperty, undefined)\n      })\n    })\n\n    it('should support the \"in\" operator', () => {\n      Automerge.change(Automerge.init(), doc => {\n        assert.strictEqual('key1' in doc, false)\n        doc.key1 = 'value1'\n        assert.strictEqual('key1' in doc, true)\n      })\n    })\n\n    it('should support Object.keys()', () => {\n      Automerge.change(Automerge.init(), doc => {\n        assert.deepStrictEqual(Object.keys(doc), [])\n        doc.key1 = 'value1'\n        assert.deepStrictEqual(Object.keys(doc), ['key1'])\n        doc.key2 = 'value2'\n        assertEqualsOneOf(Object.keys(doc), ['key1', 'key2'], ['key2', 'key1'])\n      })\n    })\n\n    it('should support Object.getOwnPropertyNames()', () => {\n      Automerge.change(Automerge.init(), doc => {\n        assert.deepStrictEqual(Object.getOwnPropertyNames(doc), [])\n        doc.key1 = 'value1'\n        assert.deepStrictEqual(Object.getOwnPropertyNames(doc), ['key1'])\n        doc.key2 = 'value2'\n        assertEqualsOneOf(Object.getOwnPropertyNames(doc), ['key1', 'key2'], ['key2', 'key1'])\n      })\n    })\n\n    it('should support bulk assignment with Object.assign()', () => {\n      Automerge.change(Automerge.init(), doc => {\n        Object.assign(doc, {key1: 'value1', key2: 'value2'})\n        assert.deepStrictEqual(doc, {key1: 'value1', key2: 'value2'})\n      })\n    })\n\n    it('should support JSON.stringify()', () => {\n      Automerge.change(Automerge.init(), doc => {\n        assert.deepStrictEqual(JSON.stringify(doc), '{}')\n        doc.key1 = 'value1'\n        assert.deepStrictEqual(JSON.stringify(doc), '{\"key1\":\"value1\"}')\n        doc.key2 = 'value2'\n        assert.deepStrictEqual(JSON.parse(JSON.stringify(doc)), {\n          key1: 'value1', key2: 'value2'\n        })\n      })\n    })\n\n    it('should allow access to an object by id', () => {\n      const doc = Automerge.change(Automerge.init(), doc => {\n        doc.deepObj = {}\n        doc.deepObj.deepList = []\n        const listId = Automerge.getObjectId(doc.deepObj.deepList)\n        assert.throws(() => { Automerge.getObjectById(doc, listId) }, /Cannot use getObjectById in a change callback/)\n      })\n\n      const objId = Automerge.getObjectId(doc.deepObj)\n      assert.strictEqual(Automerge.getObjectById(doc, objId), doc.deepObj)\n      const listId = Automerge.getObjectId(doc.deepObj.deepList)\n      assert.strictEqual(Automerge.getObjectById(doc, listId), doc.deepObj.deepList)\n    })\n  })\n\n  describe('list object', () => {\n    let root\n    beforeEach(() => {\n      root = Automerge.change(Automerge.init(), doc => {\n        doc.list = [1, 2, 3]\n        doc.empty = []\n        doc.listObjects = [ {id: \"first\"}, {id: \"second\"} ]\n      })\n    })\n\n    it('should look like a JavaScript array', () => {\n      Automerge.change(root, doc => {\n        assert.strictEqual(Array.isArray(doc.list), true)\n        assert.strictEqual(typeof doc.list, 'object')\n        assert.strictEqual(toString.call(doc.list), '[object Array]')\n      })\n    })\n\n    it('should have a length property', () => {\n      Automerge.change(root, doc => {\n        assert.strictEqual(doc.empty.length, 0)\n        assert.strictEqual(doc.list.length, 3)\n      })\n    })\n\n    it('should allow entries to be fetched by index', () => {\n      Automerge.change(root, doc => {\n        assert.strictEqual(doc.list[0],   1)\n        assert.strictEqual(doc.list['0'], 1)\n        assert.strictEqual(doc.list[1],   2)\n        assert.strictEqual(doc.list['1'], 2)\n        assert.strictEqual(doc.list[2],   3)\n        assert.strictEqual(doc.list['2'], 3)\n        assert.strictEqual(doc.list[3],   undefined)\n        assert.strictEqual(doc.list['3'], undefined)\n        assert.strictEqual(doc.list[-1],  undefined)\n        assert.strictEqual(doc.list.someProperty, undefined)\n      })\n    })\n\n    it('should support the \"in\" operator', () => {\n      Automerge.change(root, doc => {\n        assert.strictEqual(0 in doc.list, true)\n        assert.strictEqual('0' in doc.list, true)\n        assert.strictEqual(3 in doc.list, false)\n        assert.strictEqual('3' in doc.list, false)\n        assert.strictEqual('length' in doc.list, true)\n        assert.strictEqual('someProperty' in doc.list, false)\n      })\n    })\n\n    it('should support Object.keys()', () => {\n      Automerge.change(root, doc => {\n        assert.deepStrictEqual(Object.keys(doc.list), ['0', '1', '2'])\n      })\n    })\n\n    it('should support Object.getOwnPropertyNames()', () => {\n      Automerge.change(root, doc => {\n        assert.deepStrictEqual(Object.getOwnPropertyNames(doc.list), ['length', '0', '1', '2'])\n      })\n    })\n\n    it('should support JSON.stringify()', () => {\n      Automerge.change(root, doc => {\n        assert.deepStrictEqual(JSON.parse(JSON.stringify(doc)), {\n          list: [1, 2, 3], empty: [], listObjects: [ {id: \"first\"}, {id: \"second\"} ]\n        })\n        assert.deepStrictEqual(JSON.stringify(doc.list), '[1,2,3]')\n      })\n    })\n\n    it('should support iteration', () => {\n      Automerge.change(root, doc => {\n        let copy = []\n        for (let x of doc.list) copy.push(x)\n        assert.deepStrictEqual(copy, [1, 2, 3])\n\n        // spread operator also uses iteration protocol\n        assert.deepStrictEqual([0, ...doc.list, 4], [0, 1, 2, 3, 4])\n      })\n    })\n\n    describe('should support standard array read-only operations', () => {\n      it('concat()', () => {\n        Automerge.change(root, doc => {\n          assert.deepStrictEqual(doc.list.concat([4, 5, 6]), [1, 2, 3, 4, 5, 6])\n          assert.deepStrictEqual(doc.list.concat([4], [5, [6]]), [1, 2, 3, 4, 5, [6]])\n        })\n      })\n\n      it('entries()', () => {\n        Automerge.change(root, doc => {\n          let copy = []\n          for (let x of doc.list.entries()) copy.push(x)\n          assert.deepStrictEqual(copy, [[0, 1], [1, 2], [2, 3]])\n          assert.deepStrictEqual([...doc.list.entries()], [[0, 1], [1, 2], [2, 3]])\n        })\n      })\n\n      it('every()', () => {\n        Automerge.change(root, doc => {\n          assert.strictEqual(doc.empty.every(() => false), true)\n          assert.strictEqual(doc.list.every(val => val > 0), true)\n          assert.strictEqual(doc.list.every(val => val > 2), false)\n          assert.strictEqual(doc.list.every((val, index) => index < 3), true)\n          // check that in the callback, 'this' is set to the second argument of 'every'\n          doc.list.every(function () { assert.strictEqual(this.hello, 'world'); return true }, {hello: 'world'})\n        })\n      })\n\n      it('filter()', () => {\n        Automerge.change(root, doc => {\n          assert.deepStrictEqual(doc.empty.filter(() => false), [])\n          assert.deepStrictEqual(doc.list.filter(num => num % 2 === 1), [1, 3])\n          assert.deepStrictEqual(doc.list.filter(() => true), [1, 2, 3])\n          doc.list.filter(function () { assert.strictEqual(this.hello, 'world'); return true }, {hello: 'world'})\n        })\n      })\n\n      it('find()', () => {\n        Automerge.change(root, doc => {\n          assert.strictEqual(doc.empty.find(() => true), undefined)\n          assert.strictEqual(doc.list.find(num => num >= 2), 2)\n          assert.strictEqual(doc.list.find(num => num >= 4), undefined)\n          doc.list.find(function () { assert.strictEqual(this.hello, 'world'); return true }, {hello: 'world'})\n        })\n      })\n\n      it('findIndex()', () => {\n        Automerge.change(root, doc => {\n          assert.strictEqual(doc.empty.findIndex(() => true), -1)\n          assert.strictEqual(doc.list.findIndex(num => num >= 2), 1)\n          assert.strictEqual(doc.list.findIndex(num => num >= 4), -1)\n          doc.list.findIndex(function () { assert.strictEqual(this.hello, 'world'); return true }, {hello: 'world'})\n        })\n      })\n\n      it('forEach()', () => {\n        Automerge.change(root, doc => {\n          doc.empty.forEach(() => { assert.fail('was called', 'not called', 'callback error') })\n          let binary = []\n          doc.list.forEach(num => binary.push(num.toString(2)))\n          assert.deepStrictEqual(binary, ['1', '10', '11'])\n          doc.list.forEach(function () { assert.strictEqual(this.hello, 'world'); return true }, {hello: 'world'})\n        })\n      })\n\n      it('includes()', () => {\n        Automerge.change(root, doc => {\n          assert.strictEqual(doc.empty.includes(3), false)\n          assert.strictEqual(doc.list.includes(3), true)\n          assert.strictEqual(doc.list.includes(1, 1), false)\n          assert.strictEqual(doc.list.includes(2, -2), true)\n          assert.strictEqual(doc.list.includes(0), false)\n        })\n      })\n\n      it('indexOf()', () => {\n        Automerge.change(root, doc => {\n          assert.strictEqual(doc.empty.indexOf(3), -1)\n          assert.strictEqual(doc.list.indexOf(3), 2)\n          assert.strictEqual(doc.list.indexOf(1, 1), -1)\n          assert.strictEqual(doc.list.indexOf(2, -2), 1)\n          assert.strictEqual(doc.list.indexOf(0), -1)\n          assert.strictEqual(doc.list.indexOf(undefined), -1)\n        })\n      })\n\n      it('indexOf() with objects', () => {\n        Automerge.change(root, doc => {\n          assert.strictEqual(doc.listObjects.indexOf(doc.listObjects[0]), 0)\n          assert.strictEqual(doc.listObjects.indexOf(doc.listObjects[1]), 1)\n\n          assert.strictEqual(doc.listObjects.indexOf(doc.listObjects[0], 0), 0)\n          assert.strictEqual(doc.listObjects.indexOf(doc.listObjects[0], 1), -1)\n          assert.strictEqual(doc.listObjects.indexOf(doc.listObjects[1], 0), 1)\n          assert.strictEqual(doc.listObjects.indexOf(doc.listObjects[1], 1), 1)\n        })\n      })\n\n      it('join()', () => {\n        Automerge.change(root, doc => {\n          assert.strictEqual(doc.empty.join(', '), '')\n          assert.strictEqual(doc.list.join(), '1,2,3')\n          assert.strictEqual(doc.list.join(''), '123')\n          assert.strictEqual(doc.list.join(', '), '1, 2, 3')\n        })\n      })\n\n      it('keys()', () => {\n        Automerge.change(root, doc => {\n          let keys = []\n          for (let x of doc.list.keys()) keys.push(x)\n          assert.deepStrictEqual(keys, [0, 1, 2])\n          assert.deepStrictEqual([...doc.list.keys()], [0, 1, 2])\n        })\n      })\n\n      it('lastIndexOf()', () => {\n        Automerge.change(root, doc => {\n          assert.strictEqual(doc.empty.lastIndexOf(3), -1)\n          assert.strictEqual(doc.list.lastIndexOf(3), 2)\n          assert.strictEqual(doc.list.lastIndexOf(3, 1), -1)\n          assert.strictEqual(doc.list.lastIndexOf(3, -1), 2)\n          assert.strictEqual(doc.list.lastIndexOf(0), -1)\n        })\n      })\n\n      it('map()', () => {\n        Automerge.change(root, doc => {\n          assert.deepStrictEqual(doc.empty.map(num => num * 2), [])\n          assert.deepStrictEqual(doc.list.map(num => num * 2), [2, 4, 6])\n          assert.deepStrictEqual(doc.list.map((num, index) => index + '->' + num), ['0->1', '1->2', '2->3'])\n          doc.list.map(function () { assert.strictEqual(this.hello, 'world'); return true }, {hello: 'world'})\n        })\n      })\n\n      it('reduce()', () => {\n        Automerge.change(root, doc => {\n          assert.strictEqual(doc.empty.reduce((sum, val) => sum + val, 0), 0)\n          assert.strictEqual(doc.list.reduce((sum, val) => sum + val, 0), 6)\n          assert.strictEqual(doc.list.reduce((sum, val) => sum + val, ''), '123')\n          assert.strictEqual(doc.list.reduce((sum, val) => sum + val), 6)\n          assert.strictEqual(doc.list.reduce((sum, val, index) => ((index % 2 === 0) ? (sum + val) : sum), 0), 4)\n        })\n      })\n\n      it('reduceRight()', () => {\n        Automerge.change(root, doc => {\n          assert.strictEqual(doc.empty.reduceRight((sum, val) => sum + val, 0), 0)\n          assert.strictEqual(doc.list.reduceRight((sum, val) => sum + val, 0), 6)\n          assert.strictEqual(doc.list.reduceRight((sum, val) => sum + val, ''), '321')\n          assert.strictEqual(doc.list.reduceRight((sum, val) => sum + val), 6)\n          assert.strictEqual(doc.list.reduceRight((sum, val, index) => ((index % 2 === 0) ? (sum + val) : sum), 0), 4)\n        })\n      })\n\n      it('slice()', () => {\n        Automerge.change(root, doc => {\n          assert.deepStrictEqual(doc.empty.slice(), [])\n          assert.deepStrictEqual(doc.list.slice(2), [3])\n          assert.deepStrictEqual(doc.list.slice(-2), [2, 3])\n          assert.deepStrictEqual(doc.list.slice(0, 0), [])\n          assert.deepStrictEqual(doc.list.slice(0, 1), [1])\n          assert.deepStrictEqual(doc.list.slice(0, -1), [1, 2])\n        })\n      })\n\n      it('some()', () => {\n        Automerge.change(root, doc => {\n          assert.strictEqual(doc.empty.some(() => true), false)\n          assert.strictEqual(doc.list.some(val => val > 2), true)\n          assert.strictEqual(doc.list.some(val => val > 4), false)\n          assert.strictEqual(doc.list.some((val, index) => index > 2), false)\n          doc.list.some(function () { assert.strictEqual(this.hello, 'world'); return true }, {hello: 'world'})\n        })\n      })\n\n      it('toString()', () => {\n        Automerge.change(root, doc => {\n          assert.strictEqual(doc.empty.toString(), '')\n          assert.strictEqual(doc.list.toString(), '1,2,3')\n        })\n      })\n\n      it('values()', () => {\n        Automerge.change(root, doc => {\n          let values = []\n          for (let x of doc.list.values()) values.push(x)\n          assert.deepStrictEqual(values, [1, 2, 3])\n          assert.deepStrictEqual([...doc.list.values()], [1, 2, 3])\n        })\n      })\n\n      it('should allow mutation of objects returned from built in list iteration', () => {\n        root = Automerge.change(Automerge.init({freeze: true}), doc => {\n          doc.objects = [{id: 1, value: 'one'}, {id: 2, value: 'two'}]\n        })\n        root = Automerge.change(root, doc => {\n          for (let obj of doc.objects) if (obj.id === 1) obj.value = 'ONE!'\n        })\n        assert.deepStrictEqual(root, {objects: [{id: 1, value: 'ONE!'}, {id: 2, value: 'two'}]})\n      })\n\n      it('should allow mutation of objects returned from readonly list methods', () => {\n        root = Automerge.change(Automerge.init({freeze: true}), doc => {\n          doc.objects = [{id: 1, value: 'one'}, {id: 2, value: 'two'}]\n        })\n        root = Automerge.change(root, doc => {\n          doc.objects.find(obj => obj.id === 1).value = 'ONE!'\n        })\n        assert.deepStrictEqual(root, {objects: [{id: 1, value: 'ONE!'}, {id: 2, value: 'two'}]})\n      })\n    })\n\n    describe('should support standard mutation methods', () => {\n      it('fill()', () => {\n        root = Automerge.change(root, doc => doc.list.fill('a'))\n        assert.deepStrictEqual(root.list, ['a', 'a', 'a'])\n        root = Automerge.change(root, doc => doc.list.fill('c', 1).fill('b', 1, 2))\n        assert.deepStrictEqual(root.list, ['a', 'b', 'c'])\n      })\n\n      it('pop()', () => {\n        root = Automerge.change(root, doc => assert.strictEqual(doc.list.pop(), 3))\n        assert.deepStrictEqual(root.list, [1, 2])\n        root = Automerge.change(root, doc => assert.strictEqual(doc.list.pop(), 2))\n        assert.deepStrictEqual(root.list, [1])\n        root = Automerge.change(root, doc => assert.strictEqual(doc.list.pop(), 1))\n        assert.deepStrictEqual(root.list, [])\n        root = Automerge.change(root, doc => assert.strictEqual(doc.list.pop(), undefined))\n        assert.deepStrictEqual(root.list, [])\n      })\n\n      it('push()', () => {\n        root = Automerge.change(root, doc => doc.noodles = [])\n        root = Automerge.change(root, doc => doc.noodles.push('udon', 'soba'))\n        root = Automerge.change(root, doc => doc.noodles.push('ramen'))\n        assert.deepStrictEqual(root.noodles, ['udon', 'soba', 'ramen'])\n        assert.strictEqual(root.noodles[0], 'udon')\n        assert.strictEqual(root.noodles[1], 'soba')\n        assert.strictEqual(root.noodles[2], 'ramen')\n        assert.strictEqual(root.noodles.length, 3)\n      })\n\n      it('shift()', () => {\n        root = Automerge.change(root, doc => assert.strictEqual(doc.list.shift(), 1))\n        assert.deepStrictEqual(root.list, [2, 3])\n        root = Automerge.change(root, doc => assert.strictEqual(doc.list.shift(), 2))\n        assert.deepStrictEqual(root.list, [3])\n        root = Automerge.change(root, doc => assert.strictEqual(doc.list.shift(), 3))\n        assert.deepStrictEqual(root.list, [])\n        root = Automerge.change(root, doc => assert.strictEqual(doc.list.shift(), undefined))\n        assert.deepStrictEqual(root.list, [])\n      })\n\n      it('splice()', () => {\n        root = Automerge.change(root, doc => assert.deepStrictEqual(doc.list.splice(1), [2, 3]))\n        assert.deepStrictEqual(root.list, [1])\n        root = Automerge.change(root, doc => assert.deepStrictEqual(doc.list.splice(0, 0, 'a', 'b', 'c'), []))\n        assert.deepStrictEqual(root.list, ['a', 'b', 'c', 1])\n        root = Automerge.change(root, doc => assert.deepStrictEqual(doc.list.splice(1, 2, '-->'), ['b', 'c']))\n        assert.deepStrictEqual(root.list, ['a', '-->', 1])\n        root = Automerge.change(root, doc => assert.deepStrictEqual(doc.list.splice(2, 200, 2), [1]))\n        assert.deepStrictEqual(root.list, ['a', '-->', 2])\n      })\n\n      it('unshift()', () => {\n        root = Automerge.change(root, doc => doc.noodles = [])\n        root = Automerge.change(root, doc => doc.noodles.unshift('soba', 'udon'))\n        root = Automerge.change(root, doc => doc.noodles.unshift('ramen'))\n        assert.deepStrictEqual(root.noodles, ['ramen', 'soba', 'udon'])\n        assert.strictEqual(root.noodles[0], 'ramen')\n        assert.strictEqual(root.noodles[1], 'soba')\n        assert.strictEqual(root.noodles[2], 'udon')\n        assert.strictEqual(root.noodles.length, 3)\n      })\n    })\n  })\n})\n"
  },
  {
    "path": "test/sync_test.js",
    "content": "const assert = require('assert')\nconst Automerge = process.env.TEST_DIST === '1' ? require('../dist/automerge') : require('../src/automerge')\nconst { BloomFilter } = require('../backend/sync')\nconst { decodeChangeMeta } = require('../backend/columnar')\nconst { decodeSyncMessage, encodeSyncMessage, decodeSyncState, encodeSyncState, initSyncState } = Automerge.Backend\n\nfunction getHeads(doc) {\n  return Automerge.Backend.getHeads(Automerge.Frontend.getBackendState(doc))\n}\n\nfunction getMissingDeps(doc) {\n  return Automerge.Backend.getMissingDeps(Automerge.Frontend.getBackendState(doc))\n}\n\nfunction sync(a, b, aSyncState = initSyncState(), bSyncState = initSyncState()) {\n  const MAX_ITER = 10\n  let aToBmsg = null, bToAmsg = null, i = 0\n  do {\n    [aSyncState, aToBmsg] = Automerge.generateSyncMessage(a, aSyncState)\n    ;[bSyncState, bToAmsg] = Automerge.generateSyncMessage(b, bSyncState)\n\n    if (aToBmsg) {\n      [b, bSyncState] = Automerge.receiveSyncMessage(b, bSyncState, aToBmsg)\n    }\n    if (bToAmsg) {\n      [a, aSyncState] = Automerge.receiveSyncMessage(a, aSyncState, bToAmsg)\n    }\n\n    if (i++ > MAX_ITER) {\n      throw new Error(`Did not synchronize within ${MAX_ITER} iterations. Do you have a bug causing an infinite loop?`)\n    }\n  } while (aToBmsg || bToAmsg)\n\n  return [a, b, aSyncState, bSyncState]\n}\n\ndescribe('Data sync protocol', () => {\n  describe('with docs already in sync', () => {\n    describe('an empty local doc', () => {\n      it('should send a sync message implying no local data', () => {\n        let n1 = Automerge.init()\n        let s1 = initSyncState()\n        let m1\n        ;[s1, m1] = Automerge.generateSyncMessage(n1, s1)\n        const message = decodeSyncMessage(m1)\n        assert.deepStrictEqual(message.heads, [])\n        assert.deepStrictEqual(message.need, [])\n        assert.deepStrictEqual(message.have.length, 1)\n        assert.deepStrictEqual(message.have[0].lastSync, [])\n        assert.deepStrictEqual(message.have[0].bloom.byteLength, 0)\n        assert.deepStrictEqual(message.changes, [])\n      })\n\n      it('should not reply if we have no data as well', () => {\n        let n1 = Automerge.init(), n2 = Automerge.init()\n        let s1 = initSyncState(), s2 = initSyncState()\n        let m1 = null, m2 = null\n        ;[s1, m1] = Automerge.generateSyncMessage(n1, s1)\n        ;[n2, s2] = Automerge.receiveSyncMessage(n2, s2, m1)\n        ;[s2, m2] = Automerge.generateSyncMessage(n2, s2)\n        assert.deepStrictEqual(m2, null)\n      })\n    })\n\n    describe('documents with data', () => {\n      it('repos with equal heads do not need a reply message', () => {\n        let n1 = Automerge.init(), n2 = Automerge.init()\n        let s1 = initSyncState(), s2 = initSyncState()\n        let m1 = null, m2 = null\n\n        // make two nodes with the same changes\n        n1 = Automerge.change(n1, {time: 0}, doc => doc.n = [])\n        for (let i = 0; i < 10; i++) n1 = Automerge.change(n1, {time: 0}, doc => doc.n.push(i))\n        ;[n2] = Automerge.applyChanges(n2, Automerge.getAllChanges(n1))\n        assert.deepStrictEqual(n1, n2)\n\n        // generate a naive sync message\n        ;[s1, m1] = Automerge.generateSyncMessage(n1, s1)\n        assert.deepStrictEqual(s1.lastSentHeads, getHeads(n1))\n\n        // heads are equal so this message should be null\n        ;[n2, s2] = Automerge.receiveSyncMessage(n2, s2, m1)\n        ;[s2, m2] = Automerge.generateSyncMessage(n2, s2)\n        assert.strictEqual(m2, null)\n      })\n\n      it('n1 should offer all changes to n2 when starting from nothing', () => {\n        let n1 = Automerge.init(), n2 = Automerge.init()\n\n        // make changes for n1 that n2 should request\n        n1 = Automerge.change(n1, {time: 0}, doc => doc.n = [])\n        for (let i = 0; i < 10; i++) n1 = Automerge.change(n1, {time: 0}, doc => doc.n.push(i))\n\n        assert.notDeepStrictEqual(n1, n2)\n        const [after1, after2] = sync(n1, n2)\n        assert.deepStrictEqual(after1, after2)\n      })\n\n      it('should sync peers where one has commits the other does not', () => {\n        let n1 = Automerge.init(), n2 = Automerge.init()\n\n        // make changes for n1 that n2 should request\n        n1 = Automerge.change(n1, {time: 0}, doc => doc.n = [])\n        for (let i = 0; i < 10; i++) n1 = Automerge.change(n1, {time: 0}, doc => doc.n.push(i))\n\n        assert.notDeepStrictEqual(n1, n2)\n        ;[n1, n2] = sync(n1, n2)\n        assert.deepStrictEqual(n1, n2)\n      })\n\n      it('should work with prior sync state', () => {\n        // create & synchronize two nodes\n        let n1 = Automerge.init(), n2 = Automerge.init()\n        let s1 = initSyncState(), s2 = initSyncState()\n\n        for (let i = 0; i < 5; i++) n1 = Automerge.change(n1, {time: 0}, doc => doc.x = i)\n        ;[n1, n2, s1, s2] = sync(n1, n2)\n\n        // modify the first node further\n        for (let i = 5; i < 10; i++) n1 = Automerge.change(n1, {time: 0}, doc => doc.x = i)\n\n        assert.notDeepStrictEqual(n1, n2)\n        ;[n1, n2, s1, s2] = sync(n1, n2, s1, s2)\n        assert.deepStrictEqual(n1, n2)\n      })\n\n      it('should not generate messages once synced', () => {\n        // create & synchronize two nodes\n        let n1 = Automerge.init('abc123'), n2 = Automerge.init('def456')\n        let s1 = initSyncState(), s2 = initSyncState()\n\n        let message, patch\n        for (let i = 0; i < 5; i++) n1 = Automerge.change(n1, {time: 0}, doc => doc.x = i)\n        for (let i = 0; i < 5; i++) n2 = Automerge.change(n2, {time: 0}, doc => doc.y = i)\n\n        // n1 reports what it has\n        ;[s1, message] = Automerge.generateSyncMessage(n1, s1, n1)\n\n        // n2 receives that message and sends changes along with what it has\n        ;[n2, s2, patch] = Automerge.receiveSyncMessage(n2, s2, message)\n        ;[s2, message] = Automerge.generateSyncMessage(n2, s2)\n        assert.deepStrictEqual(decodeSyncMessage(message).changes.length, 5)\n        assert.deepStrictEqual(patch, null) // no changes arrived\n\n        // n1 receives the changes and replies with the changes it now knows n2 needs\n        ;[n1, s1, patch] = Automerge.receiveSyncMessage(n1, s1, message)\n        ;[s1, message] = Automerge.generateSyncMessage(n1, s1)\n        assert.deepStrictEqual(decodeSyncMessage(message).changes.length, 5)\n        assert.deepStrictEqual(patch.diffs.props, {y: {'5@def456': {type: 'value', value: 4, datatype: 'int'}}}) // changes arrived\n\n        // n2 applies the changes and sends confirmation ending the exchange\n        ;[n2, s2, patch] = Automerge.receiveSyncMessage(n2, s2, message)\n        ;[s2, message] = Automerge.generateSyncMessage(n2, s2)\n        assert.deepStrictEqual(patch.diffs.props, {x: {'5@abc123': {type: 'value', value: 4, datatype: 'int'}}}) // changes arrived\n\n        // n1 receives the message and has nothing more to say\n        ;[n1, s1, patch] = Automerge.receiveSyncMessage(n1, s1, message)\n        ;[s1, message] = Automerge.generateSyncMessage(n1, s1)\n        assert.deepStrictEqual(message, null)\n        assert.deepStrictEqual(patch, null) // no changes arrived\n\n        // n2 also has nothing left to say\n        ;[s2, message] = Automerge.generateSyncMessage(n2, s2)\n        assert.deepStrictEqual(message, null)\n      })\n\n      it('should allow simultaneous messages during synchronization', () => {\n        // create & synchronize two nodes\n        let n1 = Automerge.init('abc123'), n2 = Automerge.init('def456')\n        let s1 = initSyncState(), s2 = initSyncState()\n        for (let i = 0; i < 5; i++) n1 = Automerge.change(n1, {time: 0}, doc => doc.x = i)\n        for (let i = 0; i < 5; i++) n2 = Automerge.change(n2, {time: 0}, doc => doc.y = i)\n        const head1 = getHeads(n1)[0], head2 = getHeads(n2)[0]\n\n        // both sides report what they have but have no shared peer state\n        let msg1to2, msg2to1\n        ;[s1, msg1to2] = Automerge.generateSyncMessage(n1, s1)\n        ;[s2, msg2to1] = Automerge.generateSyncMessage(n2, s2)\n        assert.deepStrictEqual(decodeSyncMessage(msg1to2).changes.length, 0)\n        assert.deepStrictEqual(decodeSyncMessage(msg1to2).have[0].lastSync.length, 0)\n        assert.deepStrictEqual(decodeSyncMessage(msg2to1).changes.length, 0)\n        assert.deepStrictEqual(decodeSyncMessage(msg2to1).have[0].lastSync.length, 0)\n\n        // n1 and n2 receives that message and update sync state but make no patch\n        let patch1, patch2\n        ;[n1, s1, patch1] = Automerge.receiveSyncMessage(n1, s1, msg2to1)\n        assert.deepStrictEqual(patch1, null) // no changes arrived, so no patch\n        ;[n2, s2, patch2] = Automerge.receiveSyncMessage(n2, s2, msg1to2)\n        assert.deepStrictEqual(patch2, null) // no changes arrived, so no patch\n\n        // now both reply with their local changes the other lacks\n        // (standard warning that 1% of the time this will result in a \"need\" message)\n        ;[s1, msg1to2] = Automerge.generateSyncMessage(n1, s1)\n        assert.deepStrictEqual(decodeSyncMessage(msg1to2).changes.length, 5)\n        ;[s2, msg2to1] = Automerge.generateSyncMessage(n2, s2)\n        assert.deepStrictEqual(decodeSyncMessage(msg2to1).changes.length, 5)\n\n        // both should now apply the changes and update the frontend\n        ;[n1, s1, patch1] = Automerge.receiveSyncMessage(n1, s1, msg2to1)\n        assert.deepStrictEqual(getMissingDeps(n1), [])\n        assert.notDeepStrictEqual(patch1, null)\n        assert.deepStrictEqual(n1, {x: 4, y: 4})\n\n        ;[n2, s2, patch2] = Automerge.receiveSyncMessage(n2, s2, msg1to2)\n        assert.deepStrictEqual(getMissingDeps(n2), [])\n        assert.notDeepStrictEqual(patch2, null)\n        assert.deepStrictEqual(n2, {x: 4, y: 4})\n\n        // The response acknowledges the changes received, and sends no further changes\n        ;[s1, msg1to2] = Automerge.generateSyncMessage(n1, s1)\n        assert.deepStrictEqual(decodeSyncMessage(msg1to2).changes.length, 0)\n        ;[s2, msg2to1] = Automerge.generateSyncMessage(n2, s2)\n        assert.deepStrictEqual(decodeSyncMessage(msg2to1).changes.length, 0)\n\n        // After receiving acknowledgements, their shared heads should be equal\n        ;[n1, s1, patch1] = Automerge.receiveSyncMessage(n1, s1, msg2to1)\n        ;[n2, s2, patch2] = Automerge.receiveSyncMessage(n2, s2, msg1to2)\n        assert.deepStrictEqual(s1.sharedHeads, [head1, head2].sort())\n        assert.deepStrictEqual(s2.sharedHeads, [head1, head2].sort())\n        assert.deepStrictEqual(patch1, null)\n        assert.deepStrictEqual(patch2, null)\n\n        // We're in sync, no more messages required\n        ;[s1, msg1to2] = Automerge.generateSyncMessage(n1, s1)\n        ;[s2, msg2to1] = Automerge.generateSyncMessage(n2, s2)\n        assert.deepStrictEqual(msg1to2, null)\n        assert.deepStrictEqual(msg2to1, null)\n\n        // If we make one more change, and start another sync, its lastSync should be updated\n        n1 = Automerge.change(n1, {time: 0}, doc => doc.x = 5)\n        ;[s1, msg1to2] = Automerge.generateSyncMessage(n1, s1)\n        assert.deepStrictEqual(decodeSyncMessage(msg1to2).have[0].lastSync, [head1, head2].sort())\n      })\n\n      it('should assume sent changes were recieved until we hear otherwise', () => {\n        let n1 = Automerge.init('01234567'), n2 = Automerge.init('89abcdef')\n        let s1 = initSyncState(), message = null\n\n        n1 = Automerge.change(n1, {time: 0}, doc => doc.items = [])\n        ;[n1, n2, s1, /* s2 */] = sync(n1, n2)\n\n        n1 = Automerge.change(n1, {time: 0}, doc => doc.items.push('x'))\n        ;[s1, message] = Automerge.generateSyncMessage(n1, s1)\n        assert.deepStrictEqual(decodeSyncMessage(message).changes.length, 1)\n\n        n1 = Automerge.change(n1, {time: 0}, doc => doc.items.push('y'))\n        ;[s1, message] = Automerge.generateSyncMessage(n1, s1)\n        assert.deepStrictEqual(decodeSyncMessage(message).changes.length, 1)\n\n        n1 = Automerge.change(n1, {time: 0}, doc => doc.items.push('z'))\n        ;[s1, message] = Automerge.generateSyncMessage(n1, s1)\n        assert.deepStrictEqual(decodeSyncMessage(message).changes.length, 1)\n      })\n\n      it('should work regardless of who initiates the exchange', () => {\n        // create & synchronize two nodes\n        let n1 = Automerge.init(), n2 = Automerge.init()\n        let s1 = initSyncState(), s2 = initSyncState()\n\n        for (let i = 0; i < 5; i++) n1 = Automerge.change(n1, {time: 0}, doc => doc.x = i)\n        ;[n1, n2, s1, s2] = sync(n1, n2, s1, s2)\n\n        // modify the first node further\n        for (let i = 5; i < 10; i++) n1 = Automerge.change(n1, {time: 0}, doc => doc.x = i)\n\n        assert.notDeepStrictEqual(n1, n2)\n        ;[n1, n2, s1, s2] = sync(n1, n2, s1, s2)\n        assert.deepStrictEqual(n1, n2)\n      })\n    })\n  })\n\n  describe('with diverged documents', () => {\n    it('should work without prior sync state', () => {\n      // Scenario:                                                            ,-- c10 <-- c11 <-- c12 <-- c13 <-- c14\n      // c0 <-- c1 <-- c2 <-- c3 <-- c4 <-- c5 <-- c6 <-- c7 <-- c8 <-- c9 <-+\n      //                                                                      `-- c15 <-- c16 <-- c17\n      // lastSync is undefined.\n\n      // create two peers both with divergent commits\n      let n1 = Automerge.init('01234567'), n2 = Automerge.init('89abcdef')\n      for (let i = 0; i < 10; i++) n1 = Automerge.change(n1, {time: 0}, doc => doc.x = i)\n\n      ;[n1, n2] = sync(n1, n2)\n\n      for (let i = 10; i < 15; i++) n1 = Automerge.change(n1, {time: 0}, doc => doc.x = i)\n      for (let i = 15; i < 18; i++) n2 = Automerge.change(n2, {time: 0}, doc => doc.x = i)\n\n      assert.notDeepStrictEqual(n1, n2)\n      ;[n1, n2] = sync(n1, n2)\n      assert.deepStrictEqual(getHeads(n1), getHeads(n2))\n      assert.deepStrictEqual(n1, n2)\n    })\n\n    it('should work with prior sync state', () => {\n      // Scenario:                                                            ,-- c10 <-- c11 <-- c12 <-- c13 <-- c14\n      // c0 <-- c1 <-- c2 <-- c3 <-- c4 <-- c5 <-- c6 <-- c7 <-- c8 <-- c9 <-+\n      //                                                                      `-- c15 <-- c16 <-- c17\n      // lastSync is c9.\n\n      // create two peers both with divergent commits\n      let n1 = Automerge.init('01234567'), n2 = Automerge.init('89abcdef')\n      let s1 = initSyncState(), s2 = initSyncState()\n\n      for (let i = 0; i < 10; i++) n1 = Automerge.change(n1, {time: 0}, doc => doc.x = i)\n      ;[n1, n2, s1, s2] = sync(n1, n2, s1, s2)\n\n      for (let i = 10; i < 15; i++) n1 = Automerge.change(n1, {time: 0}, doc => doc.x = i)\n      for (let i = 15; i < 18; i++) n2 = Automerge.change(n2, {time: 0}, doc => doc.x = i)\n      s1 = decodeSyncState(encodeSyncState(s1))\n      s2 = decodeSyncState(encodeSyncState(s2))\n\n      assert.notDeepStrictEqual(n1, n2)\n      ;[n1, n2, s1, s2] = sync(n1, n2, s1, s2)\n      assert.deepStrictEqual(getHeads(n1), getHeads(n2))\n      assert.deepStrictEqual(n1, n2)\n    })\n\n    it('should ensure non-empty state after sync', () => {\n      let n1 = Automerge.init('01234567'), n2 = Automerge.init('89abcdef')\n      let s1 = initSyncState(), s2 = initSyncState()\n\n      for (let i = 0; i < 3; i++) n1 = Automerge.change(n1, {time: 0}, doc => doc.x = i)\n      ;[n1, n2, s1, s2] = sync(n1, n2, s1, s2)\n\n      assert.deepStrictEqual(s1.sharedHeads, getHeads(n1))\n      assert.deepStrictEqual(s2.sharedHeads, getHeads(n1))\n    })\n\n    it('should re-sync after one node crashed with data loss', () => {\n      // Scenario:     (r)                  (n2)                 (n1)\n      // c0 <-- c1 <-- c2 <-- c3 <-- c4 <-- c5 <-- c6 <-- c7 <-- c8\n      // n2 has changes {c0, c1, c2}, n1's lastSync is c5, and n2's lastSync is c2.\n      // we want to successfully sync (n1) with (r), even though (n1) believes it's talking to (n2)\n      let n1 = Automerge.init('01234567'), n2 = Automerge.init('89abcdef')\n      let s1 = initSyncState(), s2 = initSyncState()\n\n      // n1 makes three changes, which we sync to n2\n      for (let i = 0; i < 3; i++) n1 = Automerge.change(n1, {time: 0}, doc => doc.x = i)\n      ;[n1, n2, s1, s2] = sync(n1, n2, s1, s2)\n\n      // save a copy of n2 as \"r\" to simulate recovering from crash\n      let r, rSyncState\n      ;[r, rSyncState] = [Automerge.clone(n2), s2]\n\n      // sync another few commits\n      for (let i = 3; i < 6; i++) n1 = Automerge.change(n1, {time: 0}, doc => doc.x = i)\n      ;[n1, n2, s1, s2] = sync(n1, n2, s1, s2)\n      // everyone should be on the same page here\n      assert.deepStrictEqual(getHeads(n1), getHeads(n2))\n      assert.deepStrictEqual(n1, n2)\n\n      // now make a few more changes, then attempt to sync the fully-up-to-date n1 with the confused r\n      for (let i = 6; i < 9; i++) n1 = Automerge.change(n1, {time: 0}, doc => doc.x = i)\n      s1 = decodeSyncState(encodeSyncState(s1))\n      rSyncState = decodeSyncState(encodeSyncState(rSyncState))\n\n      assert.notDeepStrictEqual(getHeads(n1), getHeads(r))\n      assert.notDeepStrictEqual(n1, r)\n      assert.deepStrictEqual(n1, {x: 8})\n      assert.deepStrictEqual(r, {x: 2})\n      ;[n1, r, s1, rSyncState] = sync(n1, r, s1, rSyncState)\n      assert.deepStrictEqual(getHeads(n1), getHeads(r))\n      assert.deepStrictEqual(n1, r)\n    })\n\n    it('should resync after one node experiences data loss without disconnecting', () => {\n      let n1 = Automerge.init('01234567'), n2 = Automerge.init('89abcdef')\n      let s1 = initSyncState(), s2 = initSyncState()\n\n      // n1 makes three changes, which we sync to n2\n      for (let i = 0; i < 3; i++) n1 = Automerge.change(n1, {time: 0}, doc => doc.x = i)\n      ;[n1, n2, s1, s2] = sync(n1, n2, s1, s2)\n\n      assert.deepStrictEqual(getHeads(n1), getHeads(n2))\n      assert.deepStrictEqual(n1, n2)\n\n      let n2AfterDataLoss = Automerge.init('89abcdef')\n\n      // \"n2\" now has no data, but n1 still thinks it does. Note we don't do\n      // decodeSyncState(encodeSyncState(s1)) in order to simulate data loss without disconnecting\n      ;[n1, n2, s1, s2] = sync(n1, n2AfterDataLoss, s1, initSyncState())\n      assert.deepStrictEqual(getHeads(n1), getHeads(n2))\n      assert.deepStrictEqual(n1, n2)\n    })\n\n    it('should handle changes concurrent to the last sync heads', () => {\n      let n1 = Automerge.init('01234567'), n2 = Automerge.init('89abcdef'), n3 = Automerge.init('fedcba98')\n      let s12 = initSyncState(), s21 = initSyncState(), s23 = initSyncState(), s32 = initSyncState()\n\n      // Change 1 is known to all three nodes\n      n1 = Automerge.change(n1, {time: 0}, doc => doc.x = 1)\n      ;[n1, n2, s12, s21] = sync(n1, n2, s12, s21)\n      ;[n2, n3, s23, s32] = sync(n2, n3, s23, s32)\n\n      // Change 2 is known to n1 and n2\n      n1 = Automerge.change(n1, {time: 0}, doc => doc.x = 2)\n      ;[n1, n2, s12, s21] = sync(n1, n2, s12, s21)\n\n      // Each of the three nodes makes one change (changes 3, 4, 5)\n      n1 = Automerge.change(n1, {time: 0}, doc => doc.x = 3)\n      n2 = Automerge.change(n2, {time: 0}, doc => doc.x = 4)\n      n3 = Automerge.change(n3, {time: 0}, doc => doc.x = 5)\n\n      // Apply n3's latest change to n2. If running in Node, turn the Uint8Array into a Buffer, to\n      // simulate transmission over a network (see https://github.com/automerge/automerge/pull/362)\n      let change = Automerge.getLastLocalChange(n3)\n      if (typeof Buffer === 'function') change = Buffer.from(change)\n      ;[n2] = Automerge.applyChanges(n2, [change])\n\n      // Now sync n1 and n2. n3's change is concurrent to n1 and n2's last sync heads\n      ;[n1, n2, s12, s21] = sync(n1, n2, s12, s21)\n      assert.deepStrictEqual(getHeads(n1), getHeads(n2))\n      assert.deepStrictEqual(n1, n2)\n    })\n\n    it('should handle histories with lots of branching and merging', () => {\n      let n1 = Automerge.init('01234567'), n2 = Automerge.init('89abcdef'), n3 = Automerge.init('fedcba98')\n      n1 = Automerge.change(n1, {time: 0}, doc => doc.x = 0)\n      ;[n2] = Automerge.applyChanges(n2, [Automerge.getLastLocalChange(n1)])\n      ;[n3] = Automerge.applyChanges(n3, [Automerge.getLastLocalChange(n1)])\n      n3 = Automerge.change(n3, {time: 0}, doc => doc.x = 1)\n\n      //        - n1c1 <------ n1c2 <------ n1c3 <-- etc. <-- n1c20 <------ n1c21\n      //       /          \\/           \\/                              \\/\n      //      /           /\\           /\\                              /\\\n      // c0 <---- n2c1 <------ n2c2 <------ n2c3 <-- etc. <-- n2c20 <------ n2c21\n      //      \\                                                          /\n      //       ---------------------------------------------- n3c1 <-----\n      for (let i = 1; i < 20; i++) {\n        n1 = Automerge.change(n1, {time: 0}, doc => doc.n1 = i)\n        n2 = Automerge.change(n2, {time: 0}, doc => doc.n2 = i)\n        const change1 = Automerge.getLastLocalChange(n1)\n        const change2 = Automerge.getLastLocalChange(n2)\n        ;[n1] = Automerge.applyChanges(n1, [change2])\n        ;[n2] = Automerge.applyChanges(n2, [change1])\n      }\n\n      let s1 = initSyncState(), s2 = initSyncState()\n      ;[n1, n2, s1, s2] = sync(n1, n2, s1, s2)\n\n      // Having n3's last change concurrent to the last sync heads forces us into the slower code path\n      ;[n2] = Automerge.applyChanges(n2, [Automerge.getLastLocalChange(n3)])\n      n1 = Automerge.change(n1, {time: 0}, doc => doc.n1 = 'final')\n      n2 = Automerge.change(n2, {time: 0}, doc => doc.n2 = 'final')\n\n      ;[n1, n2, s1, s2] = sync(n1, n2, s1, s2)\n      assert.deepStrictEqual(getHeads(n1), getHeads(n2))\n      assert.deepStrictEqual(n1, n2)\n    })\n  })\n\n  describe('with false positives', () => {\n    // NOTE: the following tests use brute force to search for Bloom filter false positives. The\n    // tests make change hashes deterministic by fixing the actorId and change timestamp to be\n    // constants. The loop that searches for false positives is then initialised such that it finds\n    // a false positive on its first iteration. However, if anything changes about the encoding of\n    // changes (causing their hashes to change) or if the Bloom filter configuration is changed,\n    // then the false positive will no longer be the first loop iteration. The tests should still\n    // pass because the loop will run until a false positive is found, but they will be slower.\n\n    it('should handle a false-positive head', () => {\n      // Scenario:                                                            ,-- n1\n      // c0 <-- c1 <-- c2 <-- c3 <-- c4 <-- c5 <-- c6 <-- c7 <-- c8 <-- c9 <-+\n      //                                                                      `-- n2\n      // where n2 is a false positive in the Bloom filter containing {n1}.\n      // lastSync is c9.\n      let n1 = Automerge.init('01234567'), n2 = Automerge.init('89abcdef')\n      let s1 = initSyncState(), s2 = initSyncState()\n\n      for (let i = 0; i < 10; i++) n1 = Automerge.change(n1, {time: 0}, doc => doc.x = i)\n      ;[n1, n2, s1, s2] = sync(n1, n2)\n      for (let i = 1; ; i++) { // search for false positive; see comment above\n        const n1up = Automerge.change(Automerge.clone(n1, {actorId: '01234567'}), {time: 0}, doc => doc.x = `${i} @ n1`)\n        const n2up = Automerge.change(Automerge.clone(n2, {actorId: '89abcdef'}), {time: 0}, doc => doc.x = `${i} @ n2`)\n        if (new BloomFilter(getHeads(n1up)).containsHash(getHeads(n2up)[0])) {\n          n1 = n1up; n2 = n2up; break\n        }\n      }\n      const allHeads = [...getHeads(n1), ...getHeads(n2)].sort()\n      s1 = decodeSyncState(encodeSyncState(s1))\n      s2 = decodeSyncState(encodeSyncState(s2))\n      ;[n1, n2, s1, s2] = sync(n1, n2, s1, s2)\n      assert.deepStrictEqual(getHeads(n1), allHeads)\n      assert.deepStrictEqual(getHeads(n2), allHeads)\n    })\n\n    describe('with a false-positive dependency', () => {\n      let n1, n2, s1, s2, n1hash2, n2hash2\n\n      beforeEach(() => {\n        // Scenario:                                                            ,-- n1c1 <-- n1c2\n        // c0 <-- c1 <-- c2 <-- c3 <-- c4 <-- c5 <-- c6 <-- c7 <-- c8 <-- c9 <-+\n        //                                                                      `-- n2c1 <-- n2c2\n        // where n2c1 is a false positive in the Bloom filter containing {n1c1, n1c2}.\n        // lastSync is c9.\n        n1 = Automerge.init('01234567')\n        n2 = Automerge.init('89abcdef')\n        s1 = initSyncState()\n        s2 = initSyncState()\n        for (let i = 0; i < 10; i++) n1 = Automerge.change(n1, {time: 0}, doc => doc.x = i)\n        ;[n1, n2, s1, s2] = sync(n1, n2)\n\n        let n1hash1, n2hash1\n        for (let i = 29; ; i++) { // search for false positive; see comment above\n          const n1us1 = Automerge.change(Automerge.clone(n1, {actorId: '01234567'}), {time: 0}, doc => doc.x = `${i} @ n1`)\n          const n2us1 = Automerge.change(Automerge.clone(n2, {actorId: '89abcdef'}), {time: 0}, doc => doc.x = `${i} @ n2`)\n          n1hash1 = getHeads(n1us1)[0]; n2hash1 = getHeads(n2us1)[0]\n          const n1us2 = Automerge.change(n1us1, {time: 0}, doc => doc.x = 'final @ n1')\n          const n2us2 = Automerge.change(n2us1, {time: 0}, doc => doc.x = 'final @ n2')\n          n1hash2 = getHeads(n1us2)[0]; n2hash2 = getHeads(n2us2)[0]\n          if (new BloomFilter([n1hash1, n1hash2]).containsHash(n2hash1)) {\n            n1 = n1us2; n2 = n2us2; break\n          }\n        }\n      })\n\n      it('should sync two nodes without connection reset', () => {\n        [n1, n2, s1, s2] = sync(n1, n2, s1, s2)\n        assert.deepStrictEqual(getHeads(n1), [n1hash2, n2hash2].sort())\n        assert.deepStrictEqual(getHeads(n2), [n1hash2, n2hash2].sort())\n      })\n\n      it('should sync two nodes with connection reset', () => {\n        s1 = decodeSyncState(encodeSyncState(s1))\n        s2 = decodeSyncState(encodeSyncState(s2))\n        ;[n1, n2, s1, s2] = sync(n1, n2, s1, s2)\n        assert.deepStrictEqual(getHeads(n1), [n1hash2, n2hash2].sort())\n        assert.deepStrictEqual(getHeads(n2), [n1hash2, n2hash2].sort())\n      })\n\n      it('should sync three nodes', () => {\n        s1 = decodeSyncState(encodeSyncState(s1))\n        s2 = decodeSyncState(encodeSyncState(s2))\n\n        // First n1 and n2 exchange Bloom filters\n        let m1, m2\n        ;[s1, m1] = Automerge.generateSyncMessage(n1, s1)\n        ;[s2, m2] = Automerge.generateSyncMessage(n2, s2)\n        ;[n1, s1] = Automerge.receiveSyncMessage(n1, s1, m2)\n        ;[n2, s2] = Automerge.receiveSyncMessage(n2, s2, m1)\n\n        // Then n1 and n2 send each other their changes, except for the false positive\n        ;[s1, m1] = Automerge.generateSyncMessage(n1, s1)\n        ;[s2, m2] = Automerge.generateSyncMessage(n2, s2)\n        ;[n1, s1] = Automerge.receiveSyncMessage(n1, s1, m2)\n        ;[n2, s2] = Automerge.receiveSyncMessage(n2, s2, m1)\n        assert.strictEqual(decodeSyncMessage(m1).changes.length, 2) // n1c1 and n1c2\n        assert.strictEqual(decodeSyncMessage(m2).changes.length, 1) // only n2c2; change n2c1 is not sent\n\n        // n3 is a node that doesn't have the missing change. Nevertheless n1 is going to ask n3 for it\n        let n3 = Automerge.init('fedcba98'), s13 = initSyncState(), s31 = initSyncState()\n        ;[n1, n3, s13, s31] = sync(n1, n3, s13, s31)\n        assert.deepStrictEqual(getHeads(n1), [n1hash2])\n        assert.deepStrictEqual(getHeads(n3), [n1hash2])\n      })\n    })\n\n    it('should not require an additional request when a false-positive depends on a true-negative', () => {\n      // Scenario:                         ,-- n1c1 <-- n1c2 <-- n1c3\n      // c0 <-- c1 <-- c2 <-- c3 <-- c4 <-+\n      //                                   `-- n2c1 <-- n2c2 <-- n2c3\n      // where n2c2 is a false positive in the Bloom filter containing {n1c1, n1c2, n1c3}.\n      // lastSync is c4.\n      let n1 = Automerge.init('01234567'), n2 = Automerge.init('89abcdef')\n      let s1 = initSyncState(), s2 = initSyncState()\n      let n1hash3, n2hash3\n\n      for (let i = 0; i < 5; i++) n1 = Automerge.change(n1, {time: 0}, doc => doc.x = i)\n      ;[n1, n2, s1, s2] = sync(n1, n2)\n      for (let i = 86; ; i++) { // search for false positive; see comment above\n        const n1us1 = Automerge.change(Automerge.clone(n1, {actorId: '01234567'}), {time: 0}, doc => doc.x = `${i} @ n1`)\n        const n2us1 = Automerge.change(Automerge.clone(n2, {actorId: '89abcdef'}), {time: 0}, doc => doc.x = `${i} @ n2`)\n        const n1hash1 = getHeads(n1us1)[0]\n        const n1us2 = Automerge.change(n1us1, {time: 0}, doc => doc.x = `${i + 1} @ n1`)\n        const n2us2 = Automerge.change(n2us1, {time: 0}, doc => doc.x = `${i + 1} @ n2`)\n        const n1hash2 = getHeads(n1us2)[0], n2hash2 = getHeads(n2us2)[0]\n        const n1up3 = Automerge.change(n1us2, {time: 0}, doc => doc.x = 'final @ n1')\n        const n2up3 = Automerge.change(n2us2, {time: 0}, doc => doc.x = 'final @ n2')\n        n1hash3 = getHeads(n1up3)[0]; n2hash3 = getHeads(n2up3)[0]\n        if (new BloomFilter([n1hash1, n1hash2, n1hash3]).containsHash(n2hash2)) {\n          n1 = n1up3; n2 = n2up3; break\n        }\n      }\n      const bothHeads = [n1hash3, n2hash3].sort()\n      s1 = decodeSyncState(encodeSyncState(s1))\n      s2 = decodeSyncState(encodeSyncState(s2))\n      ;[n1, n2, s1, s2] = sync(n1, n2, s1, s2)\n      assert.deepStrictEqual(getHeads(n1), bothHeads)\n      assert.deepStrictEqual(getHeads(n2), bothHeads)\n    })\n\n    it('should handle chains of false-positives', () => {\n      // Scenario:                         ,-- c5\n      // c0 <-- c1 <-- c2 <-- c3 <-- c4 <-+\n      //                                   `-- n2c1 <-- n2c2 <-- n2c3\n      // where n2c1 and n2c2 are both false positives in the Bloom filter containing {c5}.\n      // lastSync is c4.\n      let n1 = Automerge.init('01234567'), n2 = Automerge.init('89abcdef')\n      let s1 = initSyncState(), s2 = initSyncState()\n\n      for (let i = 0; i < 5; i++) n1 = Automerge.change(n1, {time: 0}, doc => doc.x = i)\n      ;[n1, n2, s1, s2] = sync(n1, n2, s1, s2)\n      n1 = Automerge.change(n1, {time: 0}, doc => doc.x = 5)\n      for (let i = 2; ; i++) { // search for false positive; see comment above\n        const n2us1 = Automerge.change(Automerge.clone(n2, {actorId: '89abcdef'}), {time: 0}, doc => doc.x = `${i} @ n2`)\n        if (new BloomFilter(getHeads(n1)).containsHash(getHeads(n2us1)[0])) {\n          n2 = n2us1; break\n        }\n      }\n      for (let i = 141; ; i++) { // search for false positive; see comment above\n        const n2us2 = Automerge.change(Automerge.clone(n2, {actorId: '89abcdef'}), {time: 0}, doc => doc.x = `${i} again`)\n        if (new BloomFilter(getHeads(n1)).containsHash(getHeads(n2us2)[0])) {\n          n2 = n2us2; break\n        }\n      }\n      n2 = Automerge.change(n2, {time: 0}, doc => doc.x = 'final @ n2')\n\n      const allHeads = [...getHeads(n1), ...getHeads(n2)].sort()\n      s1 = decodeSyncState(encodeSyncState(s1))\n      s2 = decodeSyncState(encodeSyncState(s2))\n      ;[n1, n2, s1, s2] = sync(n1, n2, s1, s2)\n      assert.deepStrictEqual(getHeads(n1), allHeads)\n      assert.deepStrictEqual(getHeads(n2), allHeads)\n    })\n\n    it('should allow the false-positive hash to be explicitly requested', () => {\n      // Scenario:                                                            ,-- n1\n      // c0 <-- c1 <-- c2 <-- c3 <-- c4 <-- c5 <-- c6 <-- c7 <-- c8 <-- c9 <-+\n      //                                                                      `-- n2\n      // where n2 causes a false positive in the Bloom filter containing {n1}.\n      let n1 = Automerge.init('01234567'), n2 = Automerge.init('89abcdef')\n      let s1 = initSyncState(), s2 = initSyncState()\n      let message\n\n      for (let i = 0; i < 10; i++) n1 = Automerge.change(n1, {time: 0}, doc => doc.x = i)\n      ;[n1, n2, s1, s2] = sync(n1, n2)\n      s1 = decodeSyncState(encodeSyncState(s1))\n      s2 = decodeSyncState(encodeSyncState(s2))\n\n      for (let i = 1; ; i++) { // brute-force search for false positive; see comment above\n        const n1up = Automerge.change(Automerge.clone(n1, {actorId: '01234567'}), {time: 0}, doc => doc.x = `${i} @ n1`)\n        const n2up = Automerge.change(Automerge.clone(n2, {actorId: '89abcdef'}), {time: 0}, doc => doc.x = `${i} @ n2`)\n        // check if the bloom filter on n2 will believe n1 already has a particular hash\n        // this will mean n2 won't offer that data to n2 by receiving a sync message from n1\n        if (new BloomFilter(getHeads(n1up)).containsHash(getHeads(n2up)[0])) {\n          n1 = n1up; n2 = n2up; break\n        }\n      }\n\n      // n1 creates a sync message for n2 with an ill-fated bloom\n      [s1, message] = Automerge.generateSyncMessage(n1, s1)\n      assert.strictEqual(decodeSyncMessage(message).changes.length, 0)\n\n      // n2 receives it and DOESN'T send a change back\n      ;[n2, s2] = Automerge.receiveSyncMessage(n2, s2, message)\n      ;[s2, message] = Automerge.generateSyncMessage(n2, s2)\n      assert.strictEqual(decodeSyncMessage(message).changes.length, 0)\n\n      // n1 should now realize it's missing that change and request it explicitly\n      ;[n1, s1] = Automerge.receiveSyncMessage(n1, s1, message)\n      ;[s1, message] = Automerge.generateSyncMessage(n1, s1)\n      assert.deepStrictEqual(decodeSyncMessage(message).need, getHeads(n2))\n\n      // n2 should fulfill that request\n      ;[n2, s2] = Automerge.receiveSyncMessage(n2, s2, message)\n      ;[s2, message] = Automerge.generateSyncMessage(n2, s2)\n      assert.strictEqual(decodeSyncMessage(message).changes.length, 1)\n\n      // n1 should apply the change and the two should now be in sync\n      ;[n1, s1] = Automerge.receiveSyncMessage(n1, s1, message)\n      assert.deepStrictEqual(getHeads(n1), getHeads(n2))\n    })\n  })\n\n  describe('protocol features', () => {\n    it('should allow multiple Bloom filters', () => {\n      // Scenario:           ,-- n1c1 <-- n1c2 <-- n1c3\n      // c0 <-- c1 <-- c2 <-+--- n2c1 <-- n2c2 <-- n2c3\n      //                     `-- n3c1 <-- n3c2 <-- n3c3\n      // n1 has {c0, c1, c2, n1c1, n1c2, n1c3, n2c1, n2c2};\n      // n2 has {c0, c1, c2, n1c1, n1c2, n2c1, n2c2, n2c3};\n      // n3 has {c0, c1, c2, n3c1, n3c2, n3c3}.\n      let n1 = Automerge.init('01234567'), n2 = Automerge.init('89abcdef'), n3 = Automerge.init('76543210')\n      let s13 = initSyncState(), s12 = initSyncState(), s21 = initSyncState()\n      let s32 = initSyncState(), s31 = initSyncState(), s23 = initSyncState()\n      let message1, message2, message3\n\n      for (let i = 0; i < 3; i++) n1 = Automerge.change(n1, {time: 0}, doc => doc.x = i)\n      // sync all 3 nodes\n      ;[n1, n2, s12, s21] = sync(n1, n2) // eslint-disable-line no-unused-vars -- kept for consistency\n      ;[n1, n3, s13, s31] = sync(n1, n3)\n      ;[n3, n2, s32, s23] = sync(n3, n2)\n      for (let i = 0; i < 2; i++) n1 = Automerge.change(n1, {time: 0}, doc => doc.x = `${i} @ n1`)\n      for (let i = 0; i < 2; i++) n2 = Automerge.change(n2, {time: 0}, doc => doc.x = `${i} @ n2`)\n      ;[n1] = Automerge.applyChanges(n1, Automerge.getAllChanges(n2))\n      ;[n2] = Automerge.applyChanges(n2, Automerge.getAllChanges(n1))\n      n1 = Automerge.change(n1, {time: 0}, doc => doc.x = `3 @ n1`)\n      n2 = Automerge.change(n2, {time: 0}, doc => doc.x = `3 @ n2`)\n      for (let i = 0; i < 3; i++) n3 = Automerge.change(n3, {time: 0}, doc => doc.x = `${i} @ n3`)\n      const n1c3 = getHeads(n1)[0], n2c3 = getHeads(n2)[0], n3c3 = getHeads(n3)[0]\n      s13 = decodeSyncState(encodeSyncState(s13))\n      s31 = decodeSyncState(encodeSyncState(s31))\n      s23 = decodeSyncState(encodeSyncState(s23))\n      s32 = decodeSyncState(encodeSyncState(s32))\n\n      // Now n3 concurrently syncs with n1 and n2. Doing this naively would result in n3 receiving\n      // changes {n1c1, n1c2, n2c1, n2c2} twice (those are the changes that both n1 and n2 have, but\n      // that n3 does not have). We want to prevent this duplication.\n      ;[s13, message1] = Automerge.generateSyncMessage(n1, s13) // message from n1 to n3\n      assert.strictEqual(decodeSyncMessage(message1).changes.length, 0)\n      ;[n3, s31] = Automerge.receiveSyncMessage(n3, s31, message1)\n      ;[s31, message3] = Automerge.generateSyncMessage(n3, s31) // message from n3 to n1\n      assert.strictEqual(decodeSyncMessage(message3).changes.length, 3) // {n3c1, n3c2, n3c3}\n      ;[n1, s13] = Automerge.receiveSyncMessage(n1, s13, message3)\n\n      // Copy the Bloom filter received from n1 into the message sent from n3 to n2. This Bloom\n      // filter indicates what changes n3 is going to receive from n1.\n      ;[s32, message3] = Automerge.generateSyncMessage(n3, s32) // message from n3 to n2\n      const modifiedMessage = decodeSyncMessage(message3)\n      modifiedMessage.have.push(decodeSyncMessage(message1).have[0])\n      assert.strictEqual(modifiedMessage.changes.length, 0)\n      ;[n2, s23] = Automerge.receiveSyncMessage(n2, s23, encodeSyncMessage(modifiedMessage))\n\n      // n2 replies to n3, sending only n2c3 (the one change that n2 has but n1 doesn't)\n      ;[s23, message2] = Automerge.generateSyncMessage(n2, s23)\n      assert.strictEqual(decodeSyncMessage(message2).changes.length, 1) // {n2c3}\n      ;[n3, s32] = Automerge.receiveSyncMessage(n3, s32, message2)\n\n      // n1 replies to n3\n      ;[s13, message1] = Automerge.generateSyncMessage(n1, s13)\n      assert.strictEqual(decodeSyncMessage(message1).changes.length, 5) // {n1c1, n1c2, n1c3, n2c1, n2c2}\n      ;[n3, s31] = Automerge.receiveSyncMessage(n3, s31, message1)\n      assert.deepStrictEqual(getHeads(n3), [n1c3, n2c3, n3c3].sort())\n    })\n\n    it('should allow any change to be requested', () => {\n      let n1 = Automerge.init('01234567'), n2 = Automerge.init('89abcdef')\n      let s1 = initSyncState(), s2 = initSyncState()\n      let message = null\n\n      for (let i = 0; i < 3; i++) n1 = Automerge.change(n1, {time: 0}, doc => doc.x = i)\n      const lastSync = getHeads(n1)\n      for (let i = 3; i < 6; i++) n1 = Automerge.change(n1, {time: 0}, doc => doc.x = i)\n\n      ;[n1, n2, s1, s2] = sync(n1, n2)\n      s1.lastSentHeads = [] // force generateSyncMessage to return a message even though nothing changed\n      ;[s1, message] = Automerge.generateSyncMessage(n1, s1)\n      const modMsg = decodeSyncMessage(message)\n      modMsg.need = lastSync // re-request change 2\n      ;[n2, s2] = Automerge.receiveSyncMessage(n2, s2, encodeSyncMessage(modMsg))\n      ;[s1, message] = Automerge.generateSyncMessage(n2, s2)\n      assert.strictEqual(decodeSyncMessage(message).changes.length, 1)\n      assert.strictEqual(Automerge.decodeChange(decodeSyncMessage(message).changes[0]).hash, lastSync[0])\n    })\n\n    it('should ignore requests for a nonexistent change', () => {\n      let n1 = Automerge.init('01234567'), n2 = Automerge.init('89abcdef')\n      let s1 = initSyncState(), s2 = initSyncState()\n      let message = null\n\n      for (let i = 0; i < 3; i++) n1 = Automerge.change(n1, {time: 0}, doc => doc.x = i)\n      ;[n2] = Automerge.applyChanges(n2, Automerge.getAllChanges(n1))\n      ;[s1, message] = Automerge.generateSyncMessage(n1, s1)\n      message.need = ['0000000000000000000000000000000000000000000000000000000000000000']\n      ;[n2, s2] = Automerge.receiveSyncMessage(n2, s2, message)\n      ;[s2, message] = Automerge.generateSyncMessage(n2, s2)\n      assert.strictEqual(message, null)\n    })\n\n    it('should allow a subset of changes to be sent', () => {\n      //       ,-- c1 <-- c2\n      // c0 <-+\n      //       `-- c3 <-- c4 <-- c5 <-- c6 <-- c7 <-- c8\n      let n1 = Automerge.init('01234567'), n2 = Automerge.init('89abcdef'), n3 = Automerge.init('76543210')\n      let s1 = initSyncState(), s2 = initSyncState()\n      let msg, decodedMsg\n\n      n1 = Automerge.change(n1, {time: 0}, doc => doc.x = 0)\n      n3 = Automerge.merge(n3, n1)\n      for (let i = 1; i <= 2; i++) n1 = Automerge.change(n1, {time: 0}, doc => doc.x = i) // n1 has {c0, c1, c2}\n      for (let i = 3; i <= 4; i++) n3 = Automerge.change(n3, {time: 0}, doc => doc.x = i) // n3 has {c0, c3, c4}\n      const c2 = getHeads(n1)[0], c4 = getHeads(n3)[0]\n      n2 = Automerge.merge(n2, n3) // n2 has {c0, c3, c4}\n\n      // Sync n1 and n2, so their shared heads are {c2, c4}\n      ;[n1, n2, s1, s2] = sync(n1, n2)\n      s1 = decodeSyncState(encodeSyncState(s1))\n      s2 = decodeSyncState(encodeSyncState(s2))\n      assert.deepStrictEqual(s1.sharedHeads, [c2, c4].sort())\n      assert.deepStrictEqual(s2.sharedHeads, [c2, c4].sort())\n\n      // n2 and n3 apply {c5, c6, c7, c8}\n      n3 = Automerge.change(n3, {time: 0}, doc => doc.x = 5)\n      const change5 = Automerge.getLastLocalChange(n3)\n      n3 = Automerge.change(n3, {time: 0}, doc => doc.x = 6)\n      const change6 = Automerge.getLastLocalChange(n3), c6 = getHeads(n3)[0]\n      for (let i = 7; i <= 8; i++) n3 = Automerge.change(n3, {time: 0}, doc => doc.x = i)\n      const c8 = getHeads(n3)[0]\n      n2 = Automerge.merge(n2, n3)\n\n      // Now n1 initiates a sync with n2, and n2 replies with {c5, c6}. n2 does not send {c7, c8}\n      ;[s1, msg] = Automerge.generateSyncMessage(n1, s1)\n      ;[n2, s2] = Automerge.receiveSyncMessage(n2, s2, msg)\n      ;[s2, msg] = Automerge.generateSyncMessage(n2, s2)\n      decodedMsg = decodeSyncMessage(msg)\n      decodedMsg.changes = [change5, change6]\n      msg = encodeSyncMessage(decodedMsg)\n      const sentHashes = {}\n      sentHashes[decodeChangeMeta(change5, true).hash] = true\n      sentHashes[decodeChangeMeta(change6, true).hash] = true\n      s2.sentHashes = sentHashes\n      ;[n1, s1] = Automerge.receiveSyncMessage(n1, s1, msg)\n      assert.deepStrictEqual(s1.sharedHeads, [c2, c6].sort())\n\n      // n1 replies, confirming the receipt of {c5, c6} and requesting the remaining changes\n      ;[s1, msg] = Automerge.generateSyncMessage(n1, s1)\n      ;[n2, s2] = Automerge.receiveSyncMessage(n2, s2, msg)\n      assert.deepStrictEqual(decodeSyncMessage(msg).need, [c8])\n      assert.deepStrictEqual(decodeSyncMessage(msg).have[0].lastSync, [c2, c6].sort())\n      assert.deepStrictEqual(s1.sharedHeads, [c2, c6].sort())\n      assert.deepStrictEqual(s2.sharedHeads, [c2, c6].sort())\n\n      // n2 sends the remaining changes {c7, c8}\n      ;[s2, msg] = Automerge.generateSyncMessage(n2, s2)\n      ;[n1, s1] = Automerge.receiveSyncMessage(n1, s1, msg)\n      assert.strictEqual(decodeSyncMessage(msg).changes.length, 2)\n      assert.deepStrictEqual(s1.sharedHeads, [c2, c8].sort())\n    })\n  })\n})\n"
  },
  {
    "path": "test/table_test.js",
    "content": "const assert = require('assert')\nconst Automerge = process.env.TEST_DIST === '1' ? require('../dist/automerge') : require('../src/automerge')\nconst Frontend = Automerge.Frontend\nconst uuid = require('../src/uuid')\nconst { assertEqualsOneOf } = require('./helpers')\n\n// Example data\nconst DDIA = {\n  authors: ['Kleppmann, Martin'],\n  title: 'Designing Data-Intensive Applications',\n  isbn: '1449373321'\n}\nconst RSDP = {\n  authors: ['Cachin, Christian', 'Guerraoui, Rachid', 'Rodrigues, Luís'],\n  title: 'Introduction to Reliable and Secure Distributed Programming',\n  isbn: '3-642-15259-7'\n}\n\ndescribe('Automerge.Table', () => {\n  describe('Frontend', () => {\n    it('should generate ops to create a table', () => {\n      const actor = uuid()\n      const [, change] = Frontend.change(Frontend.init(actor), doc => {\n        doc.books = new Automerge.Table()\n      })\n      assert.deepStrictEqual(change, {\n        actor, seq: 1, time: change.time, message: '', startOp: 1, deps: [], ops: [\n          {obj: '_root', action: 'makeTable', key: 'books', insert: false, pred: []}\n        ]\n      })\n    })\n\n    it('should generate ops to insert a row', () => {\n      const actor = uuid()\n      const [doc1] = Frontend.change(Frontend.init(actor), doc => {\n        doc.books = new Automerge.Table()\n      })\n      let rowId\n      const [doc2, change2] = Frontend.change(doc1, doc => {\n        rowId = doc.books.add({authors: 'Kleppmann, Martin', title: 'Designing Data-Intensive Applications'})\n      })\n      const books = Frontend.getObjectId(doc2.books)\n      const rowObjID = Frontend.getObjectId(doc2.books.entries[rowId])\n      assert.deepStrictEqual(change2, {\n        actor, seq: 2, time: change2.time, message: '', startOp: 2, deps: [], ops: [\n          {obj: books, action: 'makeMap', key: rowId, insert: false, pred: []},\n          {obj: rowObjID, action: 'set', key: 'authors', insert: false, value: 'Kleppmann, Martin', pred: []},\n          {obj: rowObjID, action: 'set', key: 'title', insert: false, value: 'Designing Data-Intensive Applications', pred: []}\n        ]\n      })\n    })\n  })\n\n  describe('with one row', () => {\n    let s1, rowId, rowWithId\n\n    beforeEach(() => {\n      s1 = Automerge.change(Automerge.init({freeze: true}), doc => {\n        doc.books = new Automerge.Table()\n        rowId = doc.books.add(DDIA)\n      })\n      rowWithId = Object.assign({id: rowId}, DDIA)\n    })\n\n    it('should look up a row by ID', () => {\n      const row = s1.books.byId(rowId)\n      assert.deepStrictEqual(row, rowWithId)\n    })\n\n    it('should return the row count', () => {\n      assert.strictEqual(s1.books.count, 1)\n    })\n\n    it('should return a list of row IDs', () => {\n      assert.deepStrictEqual(s1.books.ids, [rowId])\n    })\n\n    it('should allow iterating over rows', () => {\n      assert.deepStrictEqual([...s1.books], [rowWithId])\n    })\n\n    it('should support standard array methods', () => {\n      assert.deepStrictEqual(s1.books.filter(book => book.isbn === '1449373321'), [rowWithId])\n      assert.deepStrictEqual(s1.books.filter(book => book.isbn === '9781449373320'), [])\n      assert.deepStrictEqual(s1.books.find(book => book.isbn === '1449373321'), rowWithId)\n      assert.strictEqual(s1.books.find(book => book.isbn === '9781449373320'), undefined)\n      assert.deepStrictEqual(s1.books.map(book => book.title), ['Designing Data-Intensive Applications'])\n    })\n\n    it('should be immutable', () => {\n      assert.strictEqual(s1.books.add, undefined)\n      assert.throws(() => s1.books.remove(rowId), /can only be modified in a change function/)\n    })\n\n    it('should save and reload', () => {\n      // FIXME - the bug is in parseAllOpIds()\n      // maps and tables with a string key that has an `@` gets\n      // improperly encoded as an opId\n      const s2 = Automerge.load(Automerge.save(s1))\n      assert.deepStrictEqual(s2.books.byId(rowId), rowWithId)\n    })\n\n    it('should allow a row to be updated', () => {\n      const s2 = Automerge.change(s1, doc => {\n        doc.books.byId(rowId).isbn = '9781449373320'\n      })\n      assert.deepStrictEqual(s2.books.byId(rowId), {\n        id: rowId,\n        authors: ['Kleppmann, Martin'],\n        title: 'Designing Data-Intensive Applications',\n        isbn: '9781449373320'\n      })\n    })\n\n    it('should allow a row to be removed', () => {\n      const s2 = Automerge.change(s1, doc => {\n        doc.books.remove(rowId)\n      })\n      assert.strictEqual(s2.books.count, 0)\n      assert.deepStrictEqual([...s2.books], [])\n    })\n\n    it('should not allow a row ID to be specified', () => {\n      assert.throws(() => {\n        Automerge.change(s1, doc => {\n          doc.books.add(Object.assign({id: 'beafbfde-8e44-4a5f-b679-786e2ebba03f'}, RSDP))\n        })\n      }, /A table row must not have an \"id\" property/)\n    })\n\n    it('should not allow a row ID to be modified', () => {\n      assert.throws(() => {\n        Automerge.change(s1, doc => {\n          doc.books.byId(rowId).id = 'beafbfde-8e44-4a5f-b679-786e2ebba03f'\n        })\n      }, /Object property \"id\" cannot be modified/)\n    })\n  })\n\n  it('should allow concurrent row insertion', () => {\n    const a0 = Automerge.change(Automerge.init(), doc => {\n      doc.books = new Automerge.Table()\n    })\n    const b0 = Automerge.merge(Automerge.init(), a0)\n\n    let ddia, rsdp\n    const a1 = Automerge.change(a0, doc => { ddia = doc.books.add(DDIA) })\n    const b1 = Automerge.change(b0, doc => { rsdp = doc.books.add(RSDP) })\n    const a2 = Automerge.merge(a1, b1)\n    assert.deepStrictEqual(a2.books.byId(ddia), Object.assign({id: ddia}, DDIA))\n    assert.deepStrictEqual(a2.books.byId(rsdp), Object.assign({id: rsdp}, RSDP))\n    assert.strictEqual(a2.books.count, 2)\n    assertEqualsOneOf(a2.books.ids, [ddia, rsdp], [rsdp, ddia])\n  })\n\n  it('should allow row creation, update, and deletion in the same change', () => {\n    const doc = Automerge.change(Automerge.init(), doc => {\n      doc.table = new Automerge.Table()\n      const id = doc.table.add({})\n      doc.table.byId(id).x = 3\n      doc.table.remove(id)\n    })\n    assert.strictEqual(doc.table.count, 0)\n  })\n\n  it('should allow rows to be sorted in various ways', () => {\n    let ddia, rsdp\n    const s = Automerge.change(Automerge.init(), doc => {\n      doc.books = new Automerge.Table()\n      ddia = doc.books.add(DDIA)\n      rsdp = doc.books.add(RSDP)\n    })\n    const ddiaWithId = Object.assign({id: ddia}, DDIA)\n    const rsdpWithId = Object.assign({id: rsdp}, RSDP)\n    assert.deepStrictEqual(s.books.sort('title'), [ddiaWithId, rsdpWithId])\n    assert.deepStrictEqual(s.books.sort(['authors', 'title']), [rsdpWithId, ddiaWithId])\n    assert.deepStrictEqual(s.books.sort(row1 => ((row1.isbn === '1449373321') ? -1 : +1)), [ddiaWithId, rsdpWithId])\n  })\n\n  it('should allow serialization to JSON', () => {\n    let ddia\n    const s = Automerge.change(Automerge.init(), doc => {\n      doc.books = new Automerge.Table()\n      ddia = doc.books.add(DDIA)\n    })\n    const ddiaWithId = Object.assign({id: ddia}, DDIA)\n    assert.deepStrictEqual(JSON.parse(JSON.stringify(s)), {books: {[ddia]: ddiaWithId}})\n  })\n})\n"
  },
  {
    "path": "test/test.js",
    "content": "const assert = require('assert')\nconst Automerge = process.env.TEST_DIST === '1' ? require('../dist/automerge') : require('../src/automerge')\nconst { assertEqualsOneOf } = require('./helpers')\nconst { decodeChange } = require('../backend/columnar')\nconst UUID_PATTERN = /^[0-9a-f]{32}$/\nconst OPID_PATTERN = /^[0-9]+@[0-9a-f]{32}$/\n\ndescribe('Automerge', () => {\n\n  describe('initialization ', () => {\n    it('should initially be an empty map', () => {\n      const doc = Automerge.init()\n      assert.deepStrictEqual(doc, {})\n    })\n\n    it('should allow instantiating from an existing object', () => {\n      const initialState = { birds: { wrens: 3, magpies: 4 } }\n      const doc = Automerge.from(initialState)\n      assert.deepStrictEqual(doc, initialState)\n    })\n\n    it('should allow merging of an object initialized with `from`', () => {\n      let doc1 = Automerge.from({ cards: [] })\n      let doc2 = Automerge.merge(Automerge.init(), doc1)\n      assert.deepStrictEqual(doc2, { cards: [] })\n    })\n\n    it('should allow passing an actorId when instantiating from an existing object', () => {\n      const actorId = '1234'\n      let doc = Automerge.from({ foo: 1 }, actorId)\n      assert.strictEqual(Automerge.getActorId(doc), '1234')\n    })\n\n    it('accepts an empty object as initial state', () => {\n      const doc = Automerge.from({})\n      assert.deepStrictEqual(doc, {})\n    })\n\n    it('accepts an array as initial state, but converts it to an object', () => {\n      const doc = Automerge.from(['a', 'b', 'c'])\n      assert.deepStrictEqual(doc, { '0': 'a', '1': 'b', '2': 'c' })\n    })\n\n    it('accepts strings as initial values, but treats them as an array of characters', () => {\n      const doc = Automerge.from('abc')\n      assert.deepStrictEqual(doc, { '0': 'a', '1': 'b', '2': 'c' })\n    })\n\n    it('ignores numbers provided as initial values', () => {\n      const doc = Automerge.from(123)\n      assert.deepStrictEqual(doc, {})\n    })\n\n    it('ignores booleans provided as initial values', () => {\n      const doc1 = Automerge.from(false)\n      assert.deepStrictEqual(doc1, {})\n      const doc2 = Automerge.from(true)\n      assert.deepStrictEqual(doc2, {})\n    })\n  })\n\n  describe('sequential use', () => {\n    let s1, s2\n    beforeEach(() => {\n      s1 = Automerge.init()\n    })\n\n    it('should not mutate objects', () => {\n      s2 = Automerge.change(s1, doc => doc.foo = 'bar')\n      assert.strictEqual(s1.foo, undefined)\n      assert.strictEqual(s2.foo, 'bar')\n    })\n\n    it('changes should be retrievable', () => {\n      const change1 = Automerge.getLastLocalChange(s1)\n      s2 = Automerge.change(s1, doc => doc.foo = 'bar')\n      const change2 = Automerge.getLastLocalChange(s2)\n      assert.strictEqual(change1, null)\n      const change = decodeChange(change2)\n      assert.deepStrictEqual(change, {\n        actor: change.actor, deps: [], seq: 1, startOp: 1,\n        hash: change.hash, message: '', time: change.time,\n        ops: [{obj: '_root', key: 'foo', action: 'set', insert: false, value: 'bar', pred: []}]\n      })\n    })\n\n    it('should not register any conflicts on repeated assignment', () => {\n      assert.strictEqual(Automerge.getConflicts(s1, 'foo'), undefined)\n      s1 = Automerge.change(s1, 'change', doc => doc.foo = 'one')\n      assert.strictEqual(Automerge.getConflicts(s1, 'foo'), undefined)\n      s1 = Automerge.change(s1, 'change', doc => doc.foo = 'two')\n      assert.strictEqual(Automerge.getConflicts(s1, 'foo'), undefined)\n    })\n\n    describe('changes', () => {\n      it('should group several changes', () => {\n        s2 = Automerge.change(s1, 'change message', doc => {\n          doc.first = 'one'\n          assert.strictEqual(doc.first, 'one')\n          doc.second = 'two'\n          assert.deepStrictEqual(doc, {\n            first: 'one', second: 'two'\n          })\n        })\n        assert.deepStrictEqual(s1, {})\n        assert.deepStrictEqual(s2, {first: 'one', second: 'two'})\n      })\n\n      it('should freeze objects if desired', () => {\n        s1 = Automerge.init({freeze: true})\n        s2 = Automerge.change(s1, doc => doc.foo = 'bar')\n        try {\n          s2.foo = 'lemon'\n        } catch (e) { /* deliberately ignored */ }\n        assert.strictEqual(s2.foo, 'bar')\n\n        let deleted = false\n        try {\n          deleted = delete s2.foo\n        } catch (e) { /* deliberately ignored */ }\n        assert.strictEqual(s2.foo, 'bar')\n        assert.strictEqual(deleted, false)\n\n        Automerge.change(s2, () => {\n          try {\n            s2.foo = 'lemon'\n          } catch (e) { /* deliberately ignored */ }\n          assert.strictEqual(s2.foo, 'bar')\n        })\n\n        assert.throws(() => { Object.assign(s2, {x: 4}) })\n        assert.strictEqual(s2.x, undefined)\n      })\n\n      it('should allow repeated reading and writing of values', () => {\n        s2 = Automerge.change(s1, 'change message', doc => {\n          doc.value = 'a'\n          assert.strictEqual(doc.value, 'a')\n          doc.value = 'b'\n          doc.value = 'c'\n          assert.strictEqual(doc.value, 'c')\n        })\n        assert.deepStrictEqual(s1, {})\n        assert.deepStrictEqual(s2, {value: 'c'})\n      })\n\n      it('should not record conflicts when writing the same field several times within one change', () => {\n        s1 = Automerge.change(s1, 'change message', doc => {\n          doc.value = 'a'\n          doc.value = 'b'\n          doc.value = 'c'\n        })\n        assert.strictEqual(s1.value, 'c')\n        assert.strictEqual(Automerge.getConflicts(s1, 'value'), undefined)\n      })\n\n      it('should return the unchanged state object if nothing changed', () => {\n        s2 = Automerge.change(s1, () => {})\n        assert.strictEqual(s2, s1)\n      })\n\n      it('should ignore field updates that write the existing value', () => {\n        s1 = Automerge.change(s1, doc => doc.field = 123)\n        s2 = Automerge.change(s1, doc => doc.field = 123)\n        assert.strictEqual(s2, s1)\n      })\n\n      it('should not ignore field updates that resolve a conflict', () => {\n        s2 = Automerge.merge(Automerge.init(), s1)\n        s1 = Automerge.change(s1, doc => doc.field = 123)\n        s2 = Automerge.change(s2, doc => doc.field = 321)\n        s1 = Automerge.merge(s1, s2)\n        assert.strictEqual(Object.keys(Automerge.getConflicts(s1, 'field')).length, 2)\n        const resolved = Automerge.change(s1, doc => doc.field = s1.field)\n        assert.notStrictEqual(resolved, s1)\n        assert.deepStrictEqual(resolved, {field: s1.field})\n        assert.strictEqual(Automerge.getConflicts(resolved, 'field'), undefined)\n      })\n\n      it('should ignore list element updates that write the existing value', () => {\n        s1 = Automerge.change(s1, doc => doc.list = [123])\n        s2 = Automerge.change(s1, doc => doc.list[0] = 123)\n        assert.strictEqual(s2, s1)\n      })\n\n      it('should not ignore list element updates that resolve a conflict', () => {\n        s1 = Automerge.change(s1, doc => doc.list = [1])\n        s2 = Automerge.merge(Automerge.init(), s1)\n        s1 = Automerge.change(s1, doc => doc.list[0] = 123)\n        s2 = Automerge.change(s2, doc => doc.list[0] = 321)\n        s1 = Automerge.merge(s1, s2)\n        assert.deepStrictEqual(Automerge.getConflicts(s1.list, 0), {\n          [`3@${Automerge.getActorId(s1)}`]: 123,\n          [`3@${Automerge.getActorId(s2)}`]: 321\n        })\n        const resolved = Automerge.change(s1, doc => doc.list[0] = s1.list[0])\n        assert.deepStrictEqual(resolved, s1)\n        assert.notStrictEqual(resolved, s1)\n        assert.strictEqual(Automerge.getConflicts(resolved.list, 0), undefined)\n      })\n\n      it('should sanity-check arguments', () => {\n        s1 = Automerge.change(s1, doc => doc.nested = {})\n        assert.throws(() => { Automerge.change({},        doc => doc.foo = 'bar') }, /must be the document root/)\n        assert.throws(() => { Automerge.change(s1.nested, doc => doc.foo = 'bar') }, /must be the document root/)\n      })\n\n      it('should not allow nested change blocks', () => {\n        assert.throws(() => {\n          Automerge.change(s1, doc1 => {\n            Automerge.change(doc1, doc2 => {\n              doc2.foo = 'bar'\n            })\n          })\n        }, /Calls to Automerge.change cannot be nested/)\n        assert.throws(() => {\n          s1 = Automerge.change(s1, doc1 => {\n            s2 = Automerge.change(s1, doc2 => doc2.two = 2)\n            doc1.one = 1\n          })\n        }, /Attempting to use an outdated Automerge document/)\n      })\n\n      it('should not allow the same base document to be used for multiple changes', () => {\n        assert.throws(() => {\n          Automerge.change(s1, doc => doc.one = 1)\n          Automerge.change(s1, doc => doc.two = 2)\n        }, /Attempting to use an outdated Automerge document/)\n      })\n\n      it('should allow a document to be cloned', () => {\n        s1 = Automerge.change(s1, doc => doc.zero = 0)\n        s2 = Automerge.clone(s1)\n        s1 = Automerge.change(s1, doc => doc.one = 1)\n        s2 = Automerge.change(s2, doc => doc.two = 2)\n        assert.deepStrictEqual(s1, {zero: 0, one: 1})\n        assert.deepStrictEqual(s2, {zero: 0, two: 2})\n        Automerge.free(s1)\n        Automerge.free(s2)\n      })\n\n      it('should apply changes to a clone', () => {\n        s1 = Automerge.change(s1, doc => doc.x = 1)\n        s1 = Automerge.change(s1, doc => doc.x = 2)\n        const changes = Automerge.getAllChanges(s1)\n        s2 = Automerge.clone(Automerge.load(Automerge.save(s1)))\n        ;[s2] = Automerge.applyChanges(s2, changes)\n        assert.strictEqual(s2.x, 2)\n      })\n\n      it('should work with Object.assign merges', () => {\n        s1 = Automerge.change(s1, doc1 => {\n          doc1.stuff = {foo: 'bar', baz: 'blur'}\n        })\n        s1 = Automerge.change(s1, doc1 => {\n          doc1.stuff = Object.assign({}, doc1.stuff, {baz: 'updated!'})\n        })\n        assert.deepStrictEqual(s1, {stuff: {foo: 'bar', baz: 'updated!'}})\n      })\n\n      it('should support Date objects in maps', () => {\n        const now = new Date()\n        s1 = Automerge.change(s1, doc => doc.now = now)\n        let changes = Automerge.getAllChanges(s1)\n        ;[s2] = Automerge.applyChanges(Automerge.init(), changes)\n        assert.strictEqual(s2.now instanceof Date, true)\n        assert.strictEqual(s2.now.getTime(), now.getTime())\n      })\n\n      it('should support Date objects in lists', () => {\n        const now = new Date()\n        s1 = Automerge.change(s1, doc => doc.list = [now])\n        let changes = Automerge.getAllChanges(s1)\n        ;[s2] = Automerge.applyChanges(Automerge.init(), changes)\n        assert.strictEqual(s2.list[0] instanceof Date, true)\n        assert.strictEqual(s2.list[0].getTime(), now.getTime())\n      })\n\n      it('should support many Date objects in lists', () => {\n        const now1 = new Date()\n        const now2 = new Date()\n        const now3 = new Date()\n        s1 = Automerge.change(s1, doc => doc.list = [now1, now2, now3])\n        let changes = Automerge.getAllChanges(s1)\n        ;[s2] = Automerge.applyChanges(Automerge.init(), changes)\n        assert.strictEqual(s2.list[0] instanceof Date, true)\n        assert.strictEqual(s2.list[0].getTime(), now1.getTime())\n        assert.strictEqual(s2.list[1] instanceof Date, true)\n        assert.strictEqual(s2.list[1].getTime(), now2.getTime())\n        assert.strictEqual(s2.list[2] instanceof Date, true)\n        assert.strictEqual(s2.list[2].getTime(), now3.getTime())\n      })\n\n      it('should call patchCallback if supplied', () => {\n        const callbacks = [], actor = Automerge.getActorId(s1)\n        const s2 = Automerge.change(s1, {\n          patchCallback: (patch, before, after, local) => callbacks.push({patch, before, after, local})\n        }, doc => {\n          doc.birds = ['Goldfinch']\n        })\n        assert.strictEqual(callbacks.length, 1)\n        assert.deepStrictEqual(callbacks[0].patch, {\n          actor, seq: 1, maxOp: 2, deps: [], clock: {[actor]: 1}, pendingChanges: 0,\n          diffs: {objectId: '_root', type: 'map', props: {birds: {[`1@${actor}`]: {\n            objectId: `1@${actor}`, type: 'list', edits: [\n              {action: 'insert', index: 0, elemId: `2@${actor}`, opId: `2@${actor}`, value: {'type': 'value', value: 'Goldfinch'}}\n            ]\n          }}}}\n        })\n        assert.strictEqual(callbacks[0].before, s1)\n        assert.strictEqual(callbacks[0].after, s2)\n        assert.strictEqual(callbacks[0].local, true)\n      })\n\n      it('should call a patchCallback set up on document initialisation', () => {\n        const callbacks = []\n        s1 = Automerge.init({\n          patchCallback: (patch, before, after, local) => callbacks.push({patch, before, after, local})\n        })\n        const s2 = Automerge.change(s1, doc => doc.bird = 'Goldfinch')\n        const actor = Automerge.getActorId(s1)\n        assert.strictEqual(callbacks.length, 1)\n        assert.deepStrictEqual(callbacks[0].patch, {\n          actor, seq: 1, maxOp: 1, deps: [], clock: {[actor]: 1}, pendingChanges: 0,\n          diffs: {objectId: '_root', type: 'map', props: {bird: {[`1@${actor}`]: {type: 'value', value: 'Goldfinch'}}}}\n        })\n        assert.strictEqual(callbacks[0].before, s1)\n        assert.strictEqual(callbacks[0].after, s2)\n        assert.strictEqual(callbacks[0].local, true)\n      })\n    })\n\n    describe('emptyChange()', () => {\n      it('should append an empty change to the history', () => {\n        s1 = Automerge.change(s1, 'first change', doc => doc.field = 123)\n        s2 = Automerge.emptyChange(s1, 'empty change')\n        assert.notStrictEqual(s2, s1)\n        assert.deepStrictEqual(s2, s1)\n        assert.deepStrictEqual(Automerge.getHistory(s2).map(state => state.change.message),\n                         ['first change', 'empty change'])\n      })\n\n      it('should reference dependencies', () => {\n        s1 = Automerge.change(s1, doc => doc.field = 123)\n        s2 = Automerge.merge(Automerge.init(), s1)\n        s2 = Automerge.change(s2, doc => doc.other = 'hello')\n        s1 = Automerge.emptyChange(Automerge.merge(s1, s2))\n        const history = Automerge.getHistory(s1)\n        const emptyChange = history[2].change\n        assert.deepStrictEqual(emptyChange.deps, [history[0].change.hash, history[1].change.hash].sort())\n        assert.deepStrictEqual(emptyChange.ops, [])\n      })\n\n      it('should encode and decode correctly', () => {\n        s1 = Automerge.emptyChange(s1)\n        s1 = Automerge.change(s1, doc => doc.z = 1)\n        s1 = Automerge.change(s1, doc => doc.z = 1000)\n        const changes = Automerge.getAllChanges(Automerge.load(Automerge.save(s1)))\n        ;[s2] = Automerge.applyChanges(Automerge.init(), changes)\n        const heads1 = Automerge.Backend.getHeads(Automerge.Frontend.getBackendState(s1))\n        const heads2 = Automerge.Backend.getHeads(Automerge.Frontend.getBackendState(s2))\n        assert.deepStrictEqual(heads1, heads2)\n        assert.deepStrictEqual(s1, s2)\n      })\n    })\n\n    describe('root object', () => {\n      it('should handle single-property assignment', () => {\n        s1 = Automerge.change(s1, 'set bar', doc => doc.foo = 'bar')\n        s1 = Automerge.change(s1, 'set zap', doc => doc.zip = 'zap')\n        assert.strictEqual(s1.foo, 'bar')\n        assert.strictEqual(s1.zip, 'zap')\n        assert.deepStrictEqual(s1, {foo: 'bar', zip: 'zap'})\n      })\n\n      it('should allow floating-point values', () => {\n        s1 = Automerge.change(s1, doc => doc.number = 1589032171.1)\n        assert.strictEqual(s1.number, 1589032171.1)\n      })\n\n      it('should handle multi-property assignment', () => {\n        s1 = Automerge.change(s1, 'multi-assign', doc => {\n          Object.assign(doc, {foo: 'bar', answer: 42})\n        })\n        assert.strictEqual(s1.foo, 'bar')\n        assert.strictEqual(s1.answer, 42)\n        assert.deepStrictEqual(s1, {foo: 'bar', answer: 42})\n      })\n\n      it('should handle root property deletion', () => {\n        s1 = Automerge.change(s1, 'set foo', doc => { doc.foo = 'bar'; doc.something = null })\n        s1 = Automerge.change(s1, 'del foo', doc => { delete doc.foo })\n        assert.strictEqual(s1.foo, undefined)\n        assert.strictEqual(s1.something, null)\n        assert.deepStrictEqual(s1, {something: null})\n      })\n\n      it('should follow JS delete behavior', () => {\n        s1 = Automerge.change(s1, 'set foo', doc => { doc.foo = 'bar' })\n        let deleted\n        s1 = Automerge.change(s1, 'del foo', doc => {\n          deleted = delete doc.foo\n        })\n        assert.strictEqual(deleted, true)\n        let deleted2\n        assert.doesNotThrow(() => {\n          s1 = Automerge.change(s1, 'del baz', doc => {\n            deleted2 = delete doc.baz\n          })\n        })\n        assert.strictEqual(deleted2, true)\n      })\n\n      it('should allow the type of a property to be changed', () => {\n        s1 = Automerge.change(s1, 'set number', doc => doc.prop = 123)\n        assert.strictEqual(s1.prop, 123)\n        s1 = Automerge.change(s1, 'set string', doc => doc.prop = '123')\n        assert.strictEqual(s1.prop, '123')\n        s1 = Automerge.change(s1, 'set null', doc => doc.prop = null)\n        assert.strictEqual(s1.prop, null)\n        s1 = Automerge.change(s1, 'set bool', doc => doc.prop = true)\n        assert.strictEqual(s1.prop, true)\n      })\n\n      it('should require property names to be valid', () => {\n        assert.throws(() => {\n          Automerge.change(s1, 'foo', doc => doc[''] = 'x')\n        }, /must not be an empty string/)\n      })\n\n      it('should not allow assignment of unsupported datatypes', () => {\n        Automerge.change(s1, doc => {\n          assert.throws(() => { doc.foo = undefined },         /Unsupported type of value: undefined/)\n          assert.throws(() => { doc.foo = {prop: undefined} }, /Unsupported type of value: undefined/)\n          assert.throws(() => { doc.foo = () => {} },          /Unsupported type of value: function/)\n          assert.throws(() => { doc.foo = Symbol('foo') },     /Unsupported type of value: symbol/)\n        })\n      })\n    })\n\n    describe('nested maps', () => {\n      it('should assign an objectId to nested maps', () => {\n        s1 = Automerge.change(s1, doc => { doc.nested = {} })\n        assert.strictEqual(OPID_PATTERN.test(Automerge.getObjectId(s1.nested)), true)\n        assert.notEqual(Automerge.getObjectId(s1.nested), '_root')\n      })\n\n      it('should handle assignment of a nested property', () => {\n        s1 = Automerge.change(s1, 'first change', doc => {\n          doc.nested = {}\n          doc.nested.foo = 'bar'\n        })\n        s1 = Automerge.change(s1, 'second change', doc => {\n          doc.nested.one = 1\n        })\n        assert.deepStrictEqual(s1, {nested: {foo: 'bar', one: 1}})\n        assert.deepStrictEqual(s1.nested, {foo: 'bar', one: 1})\n        assert.strictEqual(s1.nested.foo, 'bar')\n        assert.strictEqual(s1.nested.one, 1)\n      })\n\n      it('should handle assignment of an object literal', () => {\n        s1 = Automerge.change(s1, doc => {\n          doc.textStyle = {bold: false, fontSize: 12}\n        })\n        assert.deepStrictEqual(s1, {textStyle: {bold: false, fontSize: 12}})\n        assert.deepStrictEqual(s1.textStyle, {bold: false, fontSize: 12})\n        assert.strictEqual(s1.textStyle.bold, false)\n        assert.strictEqual(s1.textStyle.fontSize, 12)\n      })\n\n      it('should handle assignment of multiple nested properties', () => {\n        s1 = Automerge.change(s1, doc => {\n          doc.textStyle = {bold: false, fontSize: 12}\n          Object.assign(doc.textStyle, {typeface: 'Optima', fontSize: 14})\n        })\n        assert.strictEqual(s1.textStyle.typeface, 'Optima')\n        assert.strictEqual(s1.textStyle.bold, false)\n        assert.strictEqual(s1.textStyle.fontSize, 14)\n        assert.deepStrictEqual(s1.textStyle, {typeface: 'Optima', bold: false, fontSize: 14})\n      })\n\n      it('should handle arbitrary-depth nesting', () => {\n        s1 = Automerge.change(s1, doc => {\n          doc.a = {b: {c: {d: {e: {f: {g: 'h'}}}}}}\n        })\n        s1 = Automerge.change(s1, doc => {\n          doc.a.b.c.d.e.f.i = 'j'\n        })\n        assert.deepStrictEqual(s1, {a: { b: { c: { d: { e: { f: { g: 'h', i: 'j'}}}}}}})\n        assert.strictEqual(s1.a.b.c.d.e.f.g, 'h')\n        assert.strictEqual(s1.a.b.c.d.e.f.i, 'j')\n      })\n\n      it('should allow an old object to be replaced with a new one', () => {\n        s1 = Automerge.change(s1, 'change 1', doc => {\n          doc.myPet = {species: 'dog', legs: 4, breed: 'dachshund'}\n        })\n        s2 = Automerge.change(s1, 'change 2', doc => {\n          doc.myPet = {species: 'koi', variety: '紅白', colors: {red: true, white: true, black: false}}\n        })\n        assert.deepStrictEqual(s1.myPet, {\n          species: 'dog', legs: 4, breed: 'dachshund'\n        })\n        assert.strictEqual(s1.myPet.breed, 'dachshund')\n        assert.deepStrictEqual(s2.myPet, {\n          species: 'koi', variety: '紅白',\n          colors: {red: true, white: true, black: false}\n        })\n        assert.strictEqual(s2.myPet.breed, undefined)\n        assert.strictEqual(s2.myPet.variety, '紅白')\n      })\n\n      it('should allow fields to be changed between primitive and nested map', () => {\n        s1 = Automerge.change(s1, doc => doc.color = '#ff7f00')\n        assert.strictEqual(s1.color, '#ff7f00')\n        s1 = Automerge.change(s1, doc => doc.color = {red: 255, green: 127, blue: 0})\n        assert.deepStrictEqual(s1.color, {red: 255, green: 127, blue: 0})\n        s1 = Automerge.change(s1, doc => doc.color = '#ff7f00')\n        assert.strictEqual(s1.color, '#ff7f00')\n      })\n\n      it('should not allow several references to the same map object', () => {\n        s1 = Automerge.change(s1, doc => doc.object = {})\n        assert.throws(() => {\n          Automerge.change(s1, doc => { doc.x = doc.object })\n        }, /Cannot create a reference to an existing document object/)\n        assert.throws(() => {\n          Automerge.change(s1, doc => { doc.x = s1.object })\n        }, /Cannot create a reference to an existing document object/)\n        assert.throws(() => {\n          Automerge.change(s1, doc => { doc.x = {}; doc.y = doc.x })\n        }, /Cannot create a reference to an existing document object/)\n      })\n\n      it('should not allow object-copying idioms', () => {\n        s1 = Automerge.change(s1, doc => {\n          doc.items = [{id: 'id1', name: 'one'}, {id: 'id2', name: 'two'}]\n        })\n        // People who have previously worked with immutable state in JavaScript may be tempted\n        // to use idioms like this, which don't work well with Automerge -- see e.g.\n        // https://github.com/automerge/automerge/issues/260\n        assert.throws(() => {\n          Automerge.change(s1, doc => {\n            doc.items = [...doc.items, {id: 'id3', name: 'three'}]\n          })\n        }, /Cannot create a reference to an existing document object/)\n      })\n\n      it('should handle deletion of properties within a map', () => {\n        s1 = Automerge.change(s1, 'set style', doc => {\n          doc.textStyle = {typeface: 'Optima', bold: false, fontSize: 12}\n        })\n        s1 = Automerge.change(s1, 'non-bold', doc => delete doc.textStyle.bold)\n        assert.strictEqual(s1.textStyle.bold, undefined)\n        assert.deepStrictEqual(s1.textStyle, {typeface: 'Optima', fontSize: 12})\n      })\n\n      it('should handle deletion of references to a map', () => {\n        s1 = Automerge.change(s1, 'make rich text doc', doc => {\n          Object.assign(doc, {title: 'Hello', textStyle: {typeface: 'Optima', fontSize: 12}})\n        })\n        s1 = Automerge.change(s1, doc => delete doc.textStyle)\n        assert.strictEqual(s1.textStyle, undefined)\n        assert.deepStrictEqual(s1, {title: 'Hello'})\n      })\n\n      it('should validate field names', () => {\n        s1 = Automerge.change(s1, doc => doc.nested = {})\n        assert.throws(() => { Automerge.change(s1, doc => doc.nested[''] = 'x') }, /must not be an empty string/)\n        assert.throws(() => { Automerge.change(s1, doc => doc.nested = {'': 'x'}) }, /must not be an empty string/)\n      })\n    })\n\n    describe('lists', () => {\n      it('should allow elements to be inserted', () => {\n        s1 = Automerge.change(s1, doc => doc.noodles = [])\n        s1 = Automerge.change(s1, doc => doc.noodles.insertAt(0, 'udon', 'soba'))\n        s1 = Automerge.change(s1, doc => doc.noodles.insertAt(1, 'ramen'))\n        assert.deepStrictEqual(s1, {noodles: ['udon', 'ramen', 'soba']})\n        assert.deepStrictEqual(s1.noodles, ['udon', 'ramen', 'soba'])\n        assert.strictEqual(s1.noodles[0], 'udon')\n        assert.strictEqual(s1.noodles[1], 'ramen')\n        assert.strictEqual(s1.noodles[2], 'soba')\n        assert.strictEqual(s1.noodles.length, 3)\n      })\n\n      it('should handle assignment of a list literal', () => {\n        s1 = Automerge.change(s1, doc => doc.noodles = ['udon', 'ramen', 'soba'])\n        assert.deepStrictEqual(s1, {noodles: ['udon', 'ramen', 'soba']})\n        assert.deepStrictEqual(s1.noodles, ['udon', 'ramen', 'soba'])\n        assert.strictEqual(s1.noodles[0], 'udon')\n        assert.strictEqual(s1.noodles[1], 'ramen')\n        assert.strictEqual(s1.noodles[2], 'soba')\n        assert.strictEqual(s1.noodles[3], undefined)\n        assert.strictEqual(s1.noodles.length, 3)\n      })\n\n      it('should only allow numeric indexes', () => {\n        s1 = Automerge.change(s1, doc => doc.noodles = ['udon', 'ramen', 'soba'])\n        s1 = Automerge.change(s1, doc => doc.noodles[1] = 'Ramen!')\n        assert.strictEqual(s1.noodles[1], 'Ramen!')\n        s1 = Automerge.change(s1, doc => doc.noodles['1'] = 'RAMEN!!!')\n        assert.strictEqual(s1.noodles[1], 'RAMEN!!!')\n        assert.throws(() => { Automerge.change(s1, doc => doc.noodles.favourite = 'udon') }, /list index must be a number/)\n        assert.throws(() => { Automerge.change(s1, doc => doc.noodles[''] = 'udon') }, /list index must be a number/)\n        assert.throws(() => { Automerge.change(s1, doc => doc.noodles['1e6'] = 'udon') }, /list index must be a number/)\n      })\n\n      it('should handle deletion of list elements', () => {\n        s1 = Automerge.change(s1, doc => doc.noodles = ['udon', 'ramen', 'soba'])\n        s1 = Automerge.change(s1, doc => delete doc.noodles[1])\n        assert.deepStrictEqual(s1.noodles, ['udon', 'soba'])\n        s1 = Automerge.change(s1, doc => doc.noodles.deleteAt(1))\n        assert.deepStrictEqual(s1.noodles, ['udon'])\n        assert.strictEqual(s1.noodles[0], 'udon')\n        assert.strictEqual(s1.noodles[1], undefined)\n        assert.strictEqual(s1.noodles[2], undefined)\n        assert.strictEqual(s1.noodles.length, 1)\n      })\n\n      it('should handle assignment of individual list indexes', () => {\n        s1 = Automerge.change(s1, doc => doc.japaneseFood = ['udon', 'ramen', 'soba'])\n        s1 = Automerge.change(s1, doc => doc.japaneseFood[1] = 'sushi')\n        assert.deepStrictEqual(s1.japaneseFood, ['udon', 'sushi', 'soba'])\n        assert.strictEqual(s1.japaneseFood[0], 'udon')\n        assert.strictEqual(s1.japaneseFood[1], 'sushi')\n        assert.strictEqual(s1.japaneseFood[2], 'soba')\n        assert.strictEqual(s1.japaneseFood[3], undefined)\n        assert.strictEqual(s1.japaneseFood.length, 3)\n      })\n\n      it('should treat out-by-one assignment as insertion', () => {\n        s1 = Automerge.change(s1, doc => doc.japaneseFood = ['udon'])\n        s1 = Automerge.change(s1, doc => doc.japaneseFood[1] = 'sushi')\n        assert.deepStrictEqual(s1.japaneseFood, ['udon', 'sushi'])\n        assert.strictEqual(s1.japaneseFood[0], 'udon')\n        assert.strictEqual(s1.japaneseFood[1], 'sushi')\n        assert.strictEqual(s1.japaneseFood[2], undefined)\n        assert.strictEqual(s1.japaneseFood.length, 2)\n      })\n\n      it('should allow bulk assignment of multiple list indexes', () => {\n        s1 = Automerge.change(s1, doc => doc.noodles = ['udon', 'ramen', 'soba'])\n        s1 = Automerge.change(s1, doc => Object.assign(doc.noodles, {0: 'うどん', 2: 'そば'}))\n        assert.deepStrictEqual(s1.noodles, ['うどん', 'ramen', 'そば'])\n        assert.strictEqual(s1.noodles[0], 'うどん')\n        assert.strictEqual(s1.noodles[1], 'ramen')\n        assert.strictEqual(s1.noodles[2], 'そば')\n        assert.strictEqual(s1.noodles.length, 3)\n      })\n\n      it('should handle nested objects', () => {\n        s1 = Automerge.change(s1, doc => doc.noodles = [{type: 'ramen', dishes: ['tonkotsu', 'shoyu']}])\n        s1 = Automerge.change(s1, doc => doc.noodles.push({type: 'udon', dishes: ['tempura udon']}))\n        s1 = Automerge.change(s1, doc => doc.noodles[0].dishes.push('miso'))\n        assert.deepStrictEqual(s1, {noodles: [\n          {type: 'ramen', dishes: ['tonkotsu', 'shoyu', 'miso']},\n          {type: 'udon', dishes: ['tempura udon']}\n        ]})\n        assert.deepStrictEqual(s1.noodles[0], {\n          type: 'ramen', dishes: ['tonkotsu', 'shoyu', 'miso']\n        })\n        assert.deepStrictEqual(s1.noodles[1], {\n          type: 'udon', dishes: ['tempura udon']\n        })\n      })\n\n      it('should handle nested lists', () => {\n        s1 = Automerge.change(s1, doc => doc.noodleMatrix = [['ramen', 'tonkotsu', 'shoyu']])\n        s1 = Automerge.change(s1, doc => doc.noodleMatrix.push(['udon', 'tempura udon']))\n        s1 = Automerge.change(s1, doc => doc.noodleMatrix[0].push('miso'))\n        assert.deepStrictEqual(s1.noodleMatrix, [['ramen', 'tonkotsu', 'shoyu', 'miso'], ['udon', 'tempura udon']])\n        assert.deepStrictEqual(s1.noodleMatrix[0], ['ramen', 'tonkotsu', 'shoyu', 'miso'])\n        assert.deepStrictEqual(s1.noodleMatrix[1], ['udon', 'tempura udon'])\n      })\n\n      it('should handle deep nesting', () => {\n        s1 = Automerge.change(s1, doc => doc.nesting = {\n          maps: { m1: { m2: { foo: \"bar\", baz: {} }, m2a: { } } },\n          lists: [ [ 1, 2, 3 ], [ [ 3, 4, 5, [6]], 7 ] ],\n          mapsinlists: [ { foo: \"bar\" }, [ { bar: \"baz\" } ] ],\n          listsinmaps: { foo: [1, 2, 3], bar: [ [ { baz: \"123\" } ] ] }\n        })\n        s1 = Automerge.change(s1, doc => {\n          doc.nesting.maps.m1a = \"123\"\n          doc.nesting.maps.m1.m2.baz.xxx = \"123\"\n          delete doc.nesting.maps.m1.m2a\n          doc.nesting.lists.shift()\n          doc.nesting.lists[0][0].pop()\n          doc.nesting.lists[0][0].push(100)\n          doc.nesting.mapsinlists[0].foo = \"baz\"\n          doc.nesting.mapsinlists[1][0].foo = \"bar\"\n          delete doc.nesting.mapsinlists[1]\n          doc.nesting.listsinmaps.foo.push(4)\n          doc.nesting.listsinmaps.bar[0][0].baz = \"456\"\n          delete doc.nesting.listsinmaps.bar\n        })\n        assert.deepStrictEqual(s1, { nesting: {\n          maps: { m1: { m2: { foo: \"bar\", baz: { xxx: \"123\" } } }, m1a: \"123\" },\n          lists: [ [ [ 3, 4, 5, 100 ], 7 ] ],\n          mapsinlists: [ { foo: \"baz\" } ],\n          listsinmaps: { foo: [1, 2, 3, 4] }\n        }})\n      })\n\n      it('should handle replacement of the entire list', () => {\n        s1 = Automerge.change(s1, doc => doc.noodles = ['udon', 'soba', 'ramen'])\n        s1 = Automerge.change(s1, doc => doc.japaneseNoodles = doc.noodles.slice())\n        s1 = Automerge.change(s1, doc => doc.noodles = ['wonton', 'pho'])\n        assert.deepStrictEqual(s1, {\n          noodles: ['wonton', 'pho'],\n          japaneseNoodles: ['udon', 'soba', 'ramen']\n        })\n        assert.deepStrictEqual(s1.noodles, ['wonton', 'pho'])\n        assert.strictEqual(s1.noodles[0], 'wonton')\n        assert.strictEqual(s1.noodles[1], 'pho')\n        assert.strictEqual(s1.noodles[2], undefined)\n        assert.strictEqual(s1.noodles.length, 2)\n      })\n\n      it('should allow assignment to change the type of a list element', () => {\n        s1 = Automerge.change(s1, doc => doc.noodles = ['udon', 'soba', 'ramen'])\n        assert.deepStrictEqual(s1.noodles, ['udon', 'soba', 'ramen'])\n        s1 = Automerge.change(s1, doc => doc.noodles[1] = {type: 'soba', options: ['hot', 'cold']})\n        assert.deepStrictEqual(s1.noodles, ['udon', {type: 'soba', options: ['hot', 'cold']}, 'ramen'])\n        s1 = Automerge.change(s1, doc => doc.noodles[1] = ['hot soba', 'cold soba'])\n        assert.deepStrictEqual(s1.noodles, ['udon', ['hot soba', 'cold soba'], 'ramen'])\n        s1 = Automerge.change(s1, doc => doc.noodles[1] = 'soba is the best')\n        assert.deepStrictEqual(s1.noodles, ['udon', 'soba is the best', 'ramen'])\n      })\n\n      it('should allow list creation and assignment in the same change callback', () => {\n        s1 = Automerge.change(Automerge.init(), doc => {\n          doc.letters = ['a', 'b', 'c']\n          doc.letters[1] = 'd'\n        })\n        assert.strictEqual(s1.letters[1], 'd')\n      })\n\n      it('should allow adding and removing list elements in the same change callback', () => {\n        s1 = Automerge.change(Automerge.init(), doc => doc.noodles = [])\n        s1 = Automerge.change(s1, doc => {\n          doc.noodles.push('udon')\n          doc.noodles.deleteAt(0)\n        })\n        assert.deepStrictEqual(s1, {noodles: []})\n        // do the add-remove cycle twice, test for #151 (https://github.com/automerge/automerge/issues/151)\n        s1 = Automerge.change(s1, doc => {\n          doc.noodles.push('soba')\n          doc.noodles.deleteAt(0)\n        })\n        assert.deepStrictEqual(s1, {noodles: []})\n      })\n\n      it('should handle arbitrary-depth nesting', () => {\n        s1 = Automerge.change(s1, doc => doc.maze = [[[[[[[['noodles', ['here']]]]]]]]])\n        s1 = Automerge.change(s1, doc => doc.maze[0][0][0][0][0][0][0][1].unshift('found'))\n        assert.deepStrictEqual(s1.maze, [[[[[[[['noodles', ['found', 'here']]]]]]]]])\n        assert.deepStrictEqual(s1.maze[0][0][0][0][0][0][0][1][1], 'here')\n      })\n\n      it('should not allow several references to the same list object', () => {\n        s1 = Automerge.change(s1, doc => doc.list = [])\n        assert.throws(() => {\n          Automerge.change(s1, doc => { doc.x = doc.list })\n        }, /Cannot create a reference to an existing document object/)\n        assert.throws(() => {\n          Automerge.change(s1, doc => { doc.x = s1.list })\n        }, /Cannot create a reference to an existing document object/)\n        assert.throws(() => {\n          Automerge.change(s1, doc => { doc.x = []; doc.y = doc.x })\n        }, /Cannot create a reference to an existing document object/)\n      })\n\n      it('concurrent edits insert in reverse actorid order if counters equal', () => {\n        s1 = Automerge.init('aaaa')\n        s2 = Automerge.init('bbbb')\n        s1 = Automerge.change(s1, doc => doc.list = [])\n        s2 = Automerge.merge(s2, s1)\n        s1 = Automerge.change(s1, doc => doc.list.splice(0, 0, \"2@aaaa\"))\n        s2 = Automerge.change(s2, doc => doc.list.splice(0, 0, \"2@bbbb\"))\n        s2 = Automerge.merge(s2, s1)\n        assert.deepStrictEqual(s2.list, [\"2@bbbb\", \"2@aaaa\"])\n      })\n\n      it('concurrent edits insert in reverse counter order if different', () => {\n        s1 = Automerge.init('aaaa')\n        s2 = Automerge.init('bbbb')\n        s1 = Automerge.change(s1, doc => doc.list = [])\n        s2 = Automerge.merge(s2, s1)\n        s1 = Automerge.change(s1, doc => doc.list.splice(0, 0, \"2@aaaa\"))\n        s2 = Automerge.change(s2, doc => doc.foo = \"2@bbbb\")\n        s2 = Automerge.change(s2, doc => doc.list.splice(0, 0, \"3@bbbb\"))\n        s2 = Automerge.merge(s2, s1)\n        assert.deepStrictEqual(s2.list, [\"3@bbbb\", \"2@aaaa\"])\n      })\n    })\n\n    describe('numbers', () => {\n      it('should default to int for positive numbers', () => {\n        const s1 = Automerge.change(Automerge.init(), doc => doc.number = 1)\n        const binChange = Automerge.getLastLocalChange(s1)\n        const change = decodeChange(binChange)\n        assert.deepStrictEqual(change.ops[0], { action: 'set', datatype: 'int', insert: false, key: 'number', obj: '_root', pred: [], value: 1 })\n      })\n\n      it('should default to int for negative numbers', () => {\n        const s1 = Automerge.change(Automerge.init(), doc => doc.number = -1)\n        const binChange = Automerge.getLastLocalChange(s1)\n        const change = decodeChange(binChange)\n        assert.deepStrictEqual(change.ops[0], { action: 'set', datatype: 'int', insert: false, key: 'number', obj: '_root', pred: [], value: -1 })\n      })\n\n      it('should default to float64 for floats', () => {\n        const s1 = Automerge.change(Automerge.init(), doc => doc.number = 1.1)\n        const binChange = Automerge.getLastLocalChange(s1)\n        const change = decodeChange(binChange)\n        assert.deepStrictEqual(change.ops[0], { action: 'set', datatype: 'float64', insert: false, key: 'number', obj: '_root', pred: [], value: 1.1 })\n      })\n\n      it('float64 can be specificed manually', () => {\n        const s1 = Automerge.change(Automerge.init(), doc => doc.number = new Automerge.Float64(3))\n        const binChange = Automerge.getLastLocalChange(s1)\n        const change = decodeChange(binChange)\n        assert.deepStrictEqual(change.ops[0], { action: 'set', datatype: 'float64', insert: false, key: 'number', obj: '_root', pred: [], value: 3 })\n      })\n\n      it('int can be specificed manually', () => {\n        const s1 = Automerge.change(Automerge.init(), doc => doc.number = new Automerge.Int(3))\n        const binChange = Automerge.getLastLocalChange(s1)\n        const change = decodeChange(binChange)\n        assert.deepStrictEqual(change.ops[0], { action: 'set', datatype: 'int', insert: false, key: 'number', obj: '_root', pred: [], value: 3 })\n      })\n\n      it('uint can be specificed manually', () => {\n        const s1 = Automerge.change(Automerge.init(), doc => doc.number = new Automerge.Uint(3))\n        const binChange = Automerge.getLastLocalChange(s1)\n        const change = decodeChange(binChange)\n        assert.deepStrictEqual(change.ops[0], { action: 'set', datatype: 'uint', insert: false, key: 'number', obj: '_root', pred: [], value: 3 })\n      })\n    })\n\n    describe('counters', () => {\n      it('should allow deleting counters from maps', () => {\n        const s1 = Automerge.change(Automerge.init(), doc => doc.birds = {wrens: new Automerge.Counter(1)})\n        const s2 = Automerge.change(s1, doc => doc.birds.wrens.increment(2))\n        const s3 = Automerge.change(s2, doc => delete doc.birds.wrens)\n        assert.deepStrictEqual(s2, {birds: {wrens: new Automerge.Counter(3)}})\n        assert.deepStrictEqual(s3, {birds: {}})\n      })\n\n      it('should not allow deleting counters from lists', () => {\n        const s1 = Automerge.change(Automerge.init(), doc => doc.recordings = [new Automerge.Counter(1)])\n        const s2 = Automerge.change(s1, doc => doc.recordings[0].increment(2))\n        assert.deepStrictEqual(s2, {recordings: [new Automerge.Counter(3)]})\n        assert.throws(() => { Automerge.change(s2, doc => doc.recordings.deleteAt(0)) }, /Unsupported operation/)\n      })\n\n      it('should allow putting multiple counters in a list', () => {\n        const s1 = Automerge.from({ counters: [ new Automerge.Counter(1), new Automerge.Counter(2) ] })\n        assert.deepStrictEqual(s1, {counters: [ new Automerge.Counter(1), new Automerge.Counter(2) ] })\n      })\n\n      it('should allow putting counters in a list with non counters', () => {\n        let date = new Date()\n        const s1 = Automerge.from({ counters: [ new Automerge.Counter(1), -1, new Automerge.Counter(2), 2.2, true, date ] })\n        assert.deepStrictEqual(s1, {counters: [ new Automerge.Counter(1), -1, new Automerge.Counter(2), 2.2, true, date ] })\n      })\n    })\n  })\n\n  describe('concurrent use', () => {\n    let s1, s2, s3\n    beforeEach(() => {\n      s1 = Automerge.init()\n      s2 = Automerge.init()\n      s3 = Automerge.init()\n    })\n\n    it('should merge concurrent updates of different properties', () => {\n      s1 = Automerge.change(s1, doc => doc.foo = 'bar')\n      s2 = Automerge.change(s2, doc => doc.hello = 'world')\n      s3 = Automerge.merge(s1, s2)\n      assert.strictEqual(s3.foo, 'bar')\n      assert.strictEqual(s3.hello, 'world')\n      assert.deepStrictEqual(s3, {foo: 'bar', hello: 'world'})\n      assert.strictEqual(Automerge.getConflicts(s3, 'foo'), undefined)\n      assert.strictEqual(Automerge.getConflicts(s3, 'hello'), undefined)\n    })\n\n    it('should add concurrent increments of the same property', () => {\n      s1 = Automerge.change(s1, doc => doc.counter = new Automerge.Counter())\n      s2 = Automerge.merge(s2, s1)\n      s1 = Automerge.change(s1, doc => doc.counter.increment())\n      s2 = Automerge.change(s2, doc => doc.counter.increment(2))\n      s3 = Automerge.merge(s1, s2)\n      assert.strictEqual(s1.counter.value, 1)\n      assert.strictEqual(s2.counter.value, 2)\n      assert.strictEqual(s3.counter.value, 3)\n      assert.strictEqual(Automerge.getConflicts(s3, 'counter'), undefined)\n    })\n\n    it('should add increments only to the values they precede', () => {\n      s1 = Automerge.change(s1, doc => doc.counter = new Automerge.Counter(0))\n      s1 = Automerge.change(s1, doc => doc.counter.increment())\n      s2 = Automerge.change(s2, doc => doc.counter = new Automerge.Counter(100))\n      s2 = Automerge.change(s2, doc => doc.counter.increment(3))\n      s3 = Automerge.merge(s1, s2)\n      if (Automerge.getActorId(s1) > Automerge.getActorId(s2)) {\n        assert.deepStrictEqual(s3, {counter: new Automerge.Counter(1)})\n      } else {\n        assert.deepStrictEqual(s3, {counter: new Automerge.Counter(103)})\n      }\n      assert.deepStrictEqual(Automerge.getConflicts(s3, 'counter'), {\n        [`1@${Automerge.getActorId(s1)}`]: new Automerge.Counter(1),\n        [`1@${Automerge.getActorId(s2)}`]: new Automerge.Counter(103)\n      })\n    })\n\n    it('should detect concurrent updates of the same field', () => {\n      s1 = Automerge.change(s1, doc => doc.field = 'one')\n      s2 = Automerge.change(s2, doc => doc.field = 'two')\n      s3 = Automerge.merge(s1, s2)\n      if (Automerge.getActorId(s1) > Automerge.getActorId(s2)) {\n        assert.deepStrictEqual(s3, {field: 'one'})\n      } else {\n        assert.deepStrictEqual(s3, {field: 'two'})\n      }\n      assert.deepStrictEqual(Automerge.getConflicts(s3, 'field'), {\n        [`1@${Automerge.getActorId(s1)}`]: 'one',\n        [`1@${Automerge.getActorId(s2)}`]: 'two'\n      })\n    })\n\n    it('should detect concurrent updates of the same list element', () => {\n      s1 = Automerge.change(s1, doc => doc.birds = ['finch'])\n      s2 = Automerge.merge(s2, s1)\n      s1 = Automerge.change(s1, doc => doc.birds[0] = 'greenfinch')\n      s2 = Automerge.change(s2, doc => doc.birds[0] = 'goldfinch')\n      s3 = Automerge.merge(s1, s2)\n      if (Automerge.getActorId(s1) > Automerge.getActorId(s2)) {\n        assert.deepStrictEqual(s3.birds, ['greenfinch'])\n      } else {\n        assert.deepStrictEqual(s3.birds, ['goldfinch'])\n      }\n      assert.deepStrictEqual(Automerge.getConflicts(s3.birds, 0), {\n        [`3@${Automerge.getActorId(s1)}`]: 'greenfinch',\n        [`3@${Automerge.getActorId(s2)}`]: 'goldfinch'\n      })\n    })\n\n    it('should handle assignment conflicts of different types', () => {\n      s1 = Automerge.change(s1, doc => doc.field = 'string')\n      s2 = Automerge.change(s2, doc => doc.field = ['list'])\n      s3 = Automerge.change(s3, doc => doc.field = {thing: 'map'})\n      s1 = Automerge.merge(Automerge.merge(s1, s2), s3)\n      assertEqualsOneOf(s1.field, 'string', ['list'], {thing: 'map'})\n      assert.deepStrictEqual(Automerge.getConflicts(s1, 'field'), {\n        [`1@${Automerge.getActorId(s1)}`]: 'string',\n        [`1@${Automerge.getActorId(s2)}`]: ['list'],\n        [`1@${Automerge.getActorId(s3)}`]: {thing: 'map'}\n      })\n    })\n\n    it('should handle changes within a conflicting map field', () => {\n      s1 = Automerge.change(s1, doc => doc.field = 'string')\n      s2 = Automerge.change(s2, doc => doc.field = {})\n      s2 = Automerge.change(s2, doc => doc.field.innerKey = 42)\n      s3 = Automerge.merge(s1, s2)\n      assertEqualsOneOf(s3.field, 'string', {innerKey: 42})\n      assert.deepStrictEqual(Automerge.getConflicts(s3, 'field'), {\n        [`1@${Automerge.getActorId(s1)}`]: 'string',\n        [`1@${Automerge.getActorId(s2)}`]: {innerKey: 42}\n      })\n    })\n\n    it('should handle changes within a conflicting list element', () => {\n      s1 = Automerge.change(s1, doc => doc.list = ['hello'])\n      s2 = Automerge.merge(s2, s1)\n      s1 = Automerge.change(s1, doc => doc.list[0] = {map1: true})\n      s1 = Automerge.change(s1, doc => doc.list[0].key = 1)\n      s2 = Automerge.change(s2, doc => doc.list[0] = {map2: true})\n      s2 = Automerge.change(s2, doc => doc.list[0].key = 2)\n      s3 = Automerge.merge(s1, s2)\n      if (Automerge.getActorId(s1) > Automerge.getActorId(s2)) {\n        assert.deepStrictEqual(s3.list, [{map1: true, key: 1}])\n      } else {\n        assert.deepStrictEqual(s3.list, [{map2: true, key: 2}])\n      }\n      assert.deepStrictEqual(Automerge.getConflicts(s3.list, 0), {\n        [`3@${Automerge.getActorId(s1)}`]: {map1: true, key: 1},\n        [`3@${Automerge.getActorId(s2)}`]: {map2: true, key: 2}\n      })\n    })\n\n    it('should not merge concurrently assigned nested maps', () => {\n      s1 = Automerge.change(s1, doc => doc.config = {background: 'blue'})\n      s2 = Automerge.change(s2, doc => doc.config = {logo_url: 'logo.png'})\n      s3 = Automerge.merge(s1, s2)\n      assertEqualsOneOf(s3.config, {background: 'blue'}, {logo_url: 'logo.png'})\n      assert.deepStrictEqual(Automerge.getConflicts(s3, 'config'), {\n        [`1@${Automerge.getActorId(s1)}`]: {background: 'blue'},\n        [`1@${Automerge.getActorId(s2)}`]: {logo_url: 'logo.png'}\n      })\n    })\n\n    it('should clear conflicts after assigning a new value', () => {\n      s1 = Automerge.change(s1, doc => doc.field = 'one')\n      s2 = Automerge.change(s2, doc => doc.field = 'two')\n      s3 = Automerge.merge(s1, s2)\n      s3 = Automerge.change(s3, doc => doc.field = 'three')\n      assert.deepStrictEqual(s3, {field: 'three'})\n      assert.strictEqual(Automerge.getConflicts(s3, 'field'), undefined)\n      s2 = Automerge.merge(s2, s3)\n      assert.deepStrictEqual(s2, {field: 'three'})\n      assert.strictEqual(Automerge.getConflicts(s2, 'field'), undefined)\n    })\n\n    it('should handle concurrent insertions at different list positions', () => {\n      s1 = Automerge.change(s1, doc => doc.list = ['one', 'three'])\n      s2 = Automerge.merge(s2, s1)\n      s1 = Automerge.change(s1, doc => doc.list.splice(1, 0, 'two'))\n      s2 = Automerge.change(s2, doc => doc.list.push('four'))\n      s3 = Automerge.merge(s1, s2)\n      assert.deepStrictEqual(s3, {list: ['one', 'two', 'three', 'four']})\n      assert.strictEqual(Automerge.getConflicts(s3, 'list'), undefined)\n    })\n\n    it('should handle concurrent insertions at the same list position', () => {\n      s1 = Automerge.change(s1, doc => doc.birds = ['parakeet'])\n      s2 = Automerge.merge(s2, s1)\n      s1 = Automerge.change(s1, doc => doc.birds.push('starling'))\n      s2 = Automerge.change(s2, doc => doc.birds.push('chaffinch'))\n      s3 = Automerge.merge(s1, s2)\n      assertEqualsOneOf(s3.birds, ['parakeet', 'starling', 'chaffinch'], ['parakeet', 'chaffinch', 'starling'])\n      s2 = Automerge.merge(s2, s3)\n      assert.deepStrictEqual(s2, s3)\n    })\n\n    it('should handle concurrent assignment and deletion of a map entry', () => {\n      // Add-wins semantics\n      s1 = Automerge.change(s1, doc => doc.bestBird = 'robin')\n      s2 = Automerge.merge(s2, s1)\n      s1 = Automerge.change(s1, doc => delete doc.bestBird)\n      s2 = Automerge.change(s2, doc => doc.bestBird = 'magpie')\n      s3 = Automerge.merge(s1, s2)\n      assert.deepStrictEqual(s1, {})\n      assert.deepStrictEqual(s2, {bestBird: 'magpie'})\n      assert.deepStrictEqual(s3, {bestBird: 'magpie'})\n      assert.strictEqual(Automerge.getConflicts(s3, 'bestBird'), undefined)\n    })\n\n    it('should handle concurrent assignment and deletion of a list element', () => {\n      // Concurrent assignment ressurects a deleted list element. Perhaps a little\n      // surprising, but consistent with add-wins semantics of maps (see test above)\n      s1 = Automerge.change(s1, doc => doc.birds = ['blackbird', 'thrush', 'goldfinch'])\n      s2 = Automerge.merge(s2, s1)\n      s1 = Automerge.change(s1, doc => doc.birds[1] = 'starling')\n      s2 = Automerge.change(s2, doc => doc.birds.splice(1, 1))\n      s3 = Automerge.merge(s1, s2)\n      assert.deepStrictEqual(s1.birds, ['blackbird', 'starling', 'goldfinch'])\n      assert.deepStrictEqual(s2.birds, ['blackbird', 'goldfinch'])\n      assert.deepStrictEqual(s3.birds, ['blackbird', 'starling', 'goldfinch'])\n    })\n\n    it('should handle insertion after a deleted list element', () => {\n      s1 = Automerge.change(s1, doc => doc.birds = ['blackbird', 'thrush', 'goldfinch'])\n      s2 = Automerge.merge(s2, s1)\n      s1 = Automerge.change(s1, doc => doc.birds.splice(1, 2))\n      s2 = Automerge.change(s2, doc => doc.birds.splice(2, 0, 'starling'))\n      s3 = Automerge.merge(s1, s2)\n      assert.deepStrictEqual(s3, {birds: ['blackbird', 'starling']})\n      assert.deepStrictEqual(Automerge.merge(s2, s3), {birds: ['blackbird', 'starling']})\n    })\n\n    it('should handle concurrent deletion of the same element', () => {\n      s1 = Automerge.change(s1, doc => doc.birds = ['albatross', 'buzzard', 'cormorant'])\n      s2 = Automerge.merge(s2, s1)\n      s1 = Automerge.change(s1, doc => doc.birds.deleteAt(1)) // buzzard\n      s2 = Automerge.change(s2, doc => doc.birds.deleteAt(1)) // buzzard\n      s3 = Automerge.merge(s1, s2)\n      assert.deepStrictEqual(s3.birds, ['albatross', 'cormorant'])\n    })\n\n    it('should handle concurrent deletion of different elements', () => {\n      s1 = Automerge.change(s1, doc => doc.birds =  ['albatross', 'buzzard', 'cormorant'])\n      s2 = Automerge.merge(s2, s1)\n      s1 = Automerge.change(s1, doc => doc.birds.deleteAt(0)) // albatross\n      s2 = Automerge.change(s2, doc => doc.birds.deleteAt(1)) // buzzard\n      s3 = Automerge.merge(s1, s2)\n      assert.deepStrictEqual(s3.birds, ['cormorant'])\n    })\n\n    it('should handle concurrent updates at different levels of the tree', () => {\n      // A delete higher up in the tree overrides an update in a subtree\n      s1 = Automerge.change(s1, doc => doc.animals = {birds: {pink: 'flamingo', black: 'starling'}, mammals: ['badger']})\n      s2 = Automerge.merge(s2, s1)\n      s1 = Automerge.change(s1, doc => doc.animals.birds.brown = 'sparrow')\n      s2 = Automerge.change(s2, doc => delete doc.animals.birds)\n      s3 = Automerge.merge(s1, s2)\n      assert.deepStrictEqual(s1.animals, {\n        birds: {\n          pink: 'flamingo', brown: 'sparrow', black: 'starling'\n        },\n        mammals: ['badger']\n      })\n      assert.deepStrictEqual(s2.animals, {mammals: ['badger']})\n      assert.deepStrictEqual(s3.animals, {mammals: ['badger']})\n    })\n\n    it('should handle updates of concurrently deleted objects', () => {\n      s1 = Automerge.change(s1, doc => doc.birds = {blackbird: {feathers: 'black'}})\n      s2 = Automerge.merge(s2, s1)\n      s1 = Automerge.change(s1, doc => delete doc.birds.blackbird)\n      s2 = Automerge.change(s2, doc => doc.birds.blackbird.beak = 'orange')\n      s3 = Automerge.merge(s1, s2)\n      assert.deepStrictEqual(s1, {birds: {}})\n    })\n\n    it('should not interleave sequence insertions at the same position', () => {\n      s1 = Automerge.change(s1, doc => doc.wisdom = [])\n      s2 = Automerge.merge(s2, s1)\n      s1 = Automerge.change(s1, doc => doc.wisdom.push('to', 'be', 'is', 'to', 'do'))\n      s2 = Automerge.change(s2, doc => doc.wisdom.push('to', 'do', 'is', 'to', 'be'))\n      s3 = Automerge.merge(s1, s2)\n      assertEqualsOneOf(s3.wisdom,\n        ['to', 'be', 'is', 'to', 'do', 'to', 'do', 'is', 'to', 'be'],\n        ['to', 'do', 'is', 'to', 'be', 'to', 'be', 'is', 'to', 'do'])\n      // In case you're wondering: http://quoteinvestigator.com/2013/09/16/do-be-do/\n    })\n\n    describe('multiple insertions at the same list position', () => {\n      it('should handle insertion by greater actor ID', () => {\n        s1 = Automerge.init('aaaa')\n        s2 = Automerge.init('bbbb')\n        s1 = Automerge.change(s1, doc => doc.list = ['two'])\n        s2 = Automerge.merge(s2, s1)\n        s2 = Automerge.change(s2, doc => doc.list.splice(0, 0, 'one'))\n        assert.deepStrictEqual(s2.list, ['one', 'two'])\n      })\n\n      it('should handle insertion by lesser actor ID', () => {\n        s1 = Automerge.init('bbbb')\n        s2 = Automerge.init('aaaa')\n        s1 = Automerge.change(s1, doc => doc.list = ['two'])\n        s2 = Automerge.merge(s2, s1)\n        s2 = Automerge.change(s2, doc => doc.list.splice(0, 0, 'one'))\n        assert.deepStrictEqual(s2.list, ['one', 'two'])\n      })\n\n      it('should handle insertion regardless of actor ID', () => {\n        s1 = Automerge.change(s1, doc => doc.list = ['two'])\n        s2 = Automerge.merge(s2, s1)\n        s2 = Automerge.change(s2, doc => doc.list.splice(0, 0, 'one'))\n        assert.deepStrictEqual(s2.list, ['one', 'two'])\n      })\n\n      it('should make insertion order consistent with causality', () => {\n        s1 = Automerge.change(s1, doc => doc.list = ['four'])\n        s2 = Automerge.merge(s2, s1)\n        s2 = Automerge.change(s2, doc => doc.list.unshift('three'))\n        s1 = Automerge.merge(s1, s2)\n        s1 = Automerge.change(s1, doc => doc.list.unshift('two'))\n        s2 = Automerge.merge(s2, s1)\n        s2 = Automerge.change(s2, doc => doc.list.unshift('one'))\n        assert.deepStrictEqual(s2.list, ['one', 'two', 'three', 'four'])\n      })\n    })\n  })\n\n  describe('saving and loading', () => {\n    it('should save and restore an empty document', () => {\n      let s = Automerge.load(Automerge.save(Automerge.init()))\n      assert.deepStrictEqual(s, {})\n    })\n\n    it('should generate a new random actor ID', () => {\n      let s1 = Automerge.init()\n      let s2 = Automerge.load(Automerge.save(s1))\n      assert.strictEqual(UUID_PATTERN.test(Automerge.getActorId(s1).toString()), true)\n      assert.strictEqual(UUID_PATTERN.test(Automerge.getActorId(s2).toString()), true)\n      assert.notEqual(Automerge.getActorId(s1), Automerge.getActorId(s2))\n    })\n\n    it('should allow a custom actor ID to be set', () => {\n      let s = Automerge.load(Automerge.save(Automerge.init()), '333333')\n      assert.strictEqual(Automerge.getActorId(s), '333333')\n    })\n\n    it('should reconstitute complex datatypes', () => {\n      let s1 = Automerge.change(Automerge.init(), doc => doc.todos = [{title: 'water plants', done: false}])\n      let s2 = Automerge.load(Automerge.save(s1))\n      assert.deepStrictEqual(s2, {todos: [{title: 'water plants', done: false}]})\n    })\n\n    it('should save and load maps with @ symbols in the keys', () => {\n      let s1 = Automerge.change(Automerge.init(), doc => doc[\"123@4567\"] = \"hello\")\n      let s2 = Automerge.load(Automerge.save(s1))\n      assert.deepStrictEqual(s2, { \"123@4567\": \"hello\" })\n    })\n\n    it('should reconstitute conflicts', () => {\n      let s1 = Automerge.change(Automerge.init('111111'), doc => doc.x = 3)\n      let s2 = Automerge.change(Automerge.init('222222'), doc => doc.x = 5)\n      s1 = Automerge.merge(s1, s2)\n      let s3 = Automerge.load(Automerge.save(s1))\n      assert.strictEqual(s1.x, 5)\n      assert.strictEqual(s3.x, 5)\n      assert.deepStrictEqual(Automerge.getConflicts(s1, 'x'), {'1@111111': 3, '1@222222': 5})\n      assert.deepStrictEqual(Automerge.getConflicts(s3, 'x'), {'1@111111': 3, '1@222222': 5})\n    })\n\n    it('should reconstitute element ID counters', () => {\n      const s1 = Automerge.init('01234567')\n      const s2 = Automerge.change(s1, doc => doc.list = ['a'])\n      const listId = Automerge.getObjectId(s2.list)\n      const changes12 = Automerge.getAllChanges(s2).map(decodeChange)\n      assert.deepStrictEqual(changes12, [{\n        hash: changes12[0].hash, actor: '01234567', seq: 1, startOp: 1,\n        time: changes12[0].time, message: '', deps: [], ops: [\n          {obj: '_root', action: 'makeList', key: 'list', insert: false, pred: []},\n          {obj: listId,  action: 'set', elemId: '_head', insert: true, value: 'a', pred: []}\n        ]\n      }])\n      const s3 = Automerge.change(s2, doc => doc.list.deleteAt(0))\n      const s4 = Automerge.load(Automerge.save(s3), '01234567')\n      const s5 = Automerge.change(s4, doc => doc.list.push('b'))\n      const changes45 = Automerge.getAllChanges(s5).map(decodeChange)\n      assert.deepStrictEqual(s5, {list: ['b']})\n      assert.deepStrictEqual(changes45[2], {\n        hash: changes45[2].hash, actor: '01234567', seq: 3, startOp: 4,\n        time: changes45[2].time, message: '', deps: [changes45[1].hash], ops: [\n          {obj: listId, action: 'set', elemId: '_head', insert: true, value: 'b', pred: []}\n        ]\n      })\n    })\n\n    it('should allow a reloaded list to be mutated', () => {\n      let doc = Automerge.change(Automerge.init(), doc => doc.foo = [])\n      doc = Automerge.load(Automerge.save(doc))\n      doc = Automerge.change(doc, 'add', doc => doc.foo.push(1))\n      doc = Automerge.load(Automerge.save(doc))\n      assert.deepStrictEqual(doc.foo, [1])\n    })\n\n    it('should reload a document containing deflated columns', () => {\n      // In this test, the keyCtr column is long enough for deflate compression to kick in, but the\n      // keyStr column is short. Thus, the deflate bit gets set for keyCtr but not for keyStr.\n      // When checking whether the columns appear in ascending order, we must ignore the deflate bit.\n      let doc = Automerge.change(Automerge.init(), doc => {\n        doc.list = []\n        for (let i = 0; i < 200; i++) doc.list.insertAt(Math.floor(Math.random() * i), 'a')\n      })\n      Automerge.load(Automerge.save(doc))\n      let expected = []\n      for (let i = 0; i < 200; i++) expected.push('a')\n      assert.deepStrictEqual(doc, {list: expected})\n    })\n\n    it('should call patchCallback if supplied', () => {\n      const s1 = Automerge.change(Automerge.init(), doc => doc.birds = ['Goldfinch'])\n      const s2 = Automerge.change(s1, doc => doc.birds.push('Chaffinch'))\n      const callbacks = [], actor = Automerge.getActorId(s1)\n      const reloaded = Automerge.load(Automerge.save(s2), {\n        patchCallback(patch, before, after, local) {\n          callbacks.push({patch, before, after, local})\n        }\n      })\n      assert.strictEqual(callbacks.length, 1)\n      assert.deepStrictEqual(callbacks[0].patch, {\n        maxOp: 3, deps: [decodeChange(Automerge.getAllChanges(s2)[1]).hash], clock: {[actor]: 2}, pendingChanges: 0,\n        diffs: {objectId: '_root', type: 'map', props: {birds: {[`1@${actor}`]: {\n          objectId: `1@${actor}`, type: 'list', edits: [\n            {action: 'multi-insert', index: 0, elemId: `2@${actor}`, values: ['Goldfinch', 'Chaffinch']}\n          ]\n        }}}}\n      })\n      assert.deepStrictEqual(callbacks[0].before, {})\n      assert.strictEqual(callbacks[0].after, reloaded)\n      assert.strictEqual(callbacks[0].local, false)\n    })\n\n    it('should reconstruct the original changes if needed', () => {\n      let doc = Automerge.init()\n      for (let i = 0; i < 10; i++) doc = Automerge.change(doc, doc => doc.x = i)\n      doc = Automerge.load(Automerge.save(doc))\n      assert.strictEqual(Automerge.getAllChanges(doc).length, 10)\n    })\n\n    it('should deduplicate changes after saving and reloading', () => {\n      let initChange = Automerge.getLastLocalChange(Automerge.change(Automerge.init('0000'), { time: 0 }, (doc) => {\n        doc.panels = []\n      }))\n      let [s1] = Automerge.applyChanges(Automerge.init(), [initChange])\n      let [s2] = Automerge.applyChanges(Automerge.init(), [initChange])\n      s1 = Automerge.change(s1, doc => doc.panels.push({ id: 'panel1' }))\n      s2 = Automerge.change(s2, doc => doc.panels.push({ id: 'panel2' }))\n      s1 = Automerge.load(Automerge.save(s1))\n      let [s3] = Automerge.applyChanges(s1, Automerge.getAllChanges(s2))\n      assert.strictEqual(s3.panels.length, 2)\n    })\n  })\n\n  describe('history API', () => {\n    it('should return an empty history for an empty document', () => {\n      assert.deepStrictEqual(Automerge.getHistory(Automerge.init()), [])\n    })\n\n    it('should make past document states accessible', () => {\n      let s = Automerge.init()\n      s = Automerge.change(s, doc => doc.config = {background: 'blue'})\n      s = Automerge.change(s, doc => doc.birds = ['mallard'])\n      s = Automerge.change(s, doc => doc.birds.unshift('oystercatcher'))\n      assert.deepStrictEqual(Automerge.getHistory(s).map(state => state.snapshot), [\n        {config: {background: 'blue'}},\n        {config: {background: 'blue'}, birds: ['mallard']},\n        {config: {background: 'blue'}, birds: ['oystercatcher', 'mallard']}\n      ])\n    })\n\n    it('should make change messages accessible', () => {\n      let s = Automerge.init()\n      s = Automerge.change(s, 'Empty Bookshelf', doc => doc.books = [])\n      s = Automerge.change(s, 'Add Orwell', doc => doc.books.push('Nineteen Eighty-Four'))\n      s = Automerge.change(s, 'Add Huxley', doc => doc.books.push('Brave New World'))\n      assert.deepStrictEqual(s.books, ['Nineteen Eighty-Four', 'Brave New World'])\n      assert.deepStrictEqual(Automerge.getHistory(s).map(state => state.change.message),\n                       ['Empty Bookshelf', 'Add Orwell', 'Add Huxley'])\n    })\n  })\n\n  describe('changes API', () => {\n    it('should return an empty list on an empty document', () => {\n      let changes = Automerge.getAllChanges(Automerge.init())\n      assert.deepStrictEqual(changes, [])\n    })\n\n    it('should return an empty list when nothing changed', () => {\n      let s1 = Automerge.change(Automerge.init(), doc => doc.birds = ['Chaffinch'])\n      assert.deepStrictEqual(Automerge.getChanges(s1, s1), [])\n    })\n\n    it('should do nothing when applying an empty list of changes', () => {\n      let s1 = Automerge.change(Automerge.init(), doc => doc.birds = ['Chaffinch'])\n      assert.deepStrictEqual(Automerge.applyChanges(s1, [])[0], s1)\n    })\n\n    it('should throw a useful error if the wrong type of argument is passed to applyChanges', () => {\n      let s1 = Automerge.change(Automerge.init(), doc => doc.birds = ['Chaffinch'])\n      let changes = Automerge.getAllChanges(s1)\n      assert.throws(() => {\n        Automerge.applyChanges(Automerge.init(), changes[0])\n      }, /applyChanges takes an array of Uint8Arrays/)\n      assert.throws(() => {\n        Automerge.applyChanges(Automerge.init(), changes[0].buffer)\n      }, /applyChanges takes an array of Uint8Arrays/)\n      assert.throws(() => {\n        Automerge.applyChanges(Automerge.init(), ['this is a string'])\n      }, /Not a byte array/)\n      assert.throws(() => {\n        Automerge.applyChanges(Automerge.init(), changes.map(change => change.buffer))\n      }, /Not a byte array/)\n    })\n\n    it('should return all changes when compared to an empty document', () => {\n      let s1 = Automerge.change(Automerge.init(), 'Add Chaffinch', doc => doc.birds = ['Chaffinch'])\n      let s2 = Automerge.change(s1, 'Add Bullfinch', doc => doc.birds.push('Bullfinch'))\n      let changes = Automerge.getChanges(Automerge.init(), s2)\n      assert.strictEqual(changes.length, 2)\n    })\n\n    it('should allow a document copy to be reconstructed from scratch', () => {\n      let s1 = Automerge.change(Automerge.init(), 'Add Chaffinch', doc => doc.birds = ['Chaffinch'])\n      let s2 = Automerge.change(s1, 'Add Bullfinch', doc => doc.birds.push('Bullfinch'))\n      let changes = Automerge.getAllChanges(s2)\n      let [s3] = Automerge.applyChanges(Automerge.init(), changes)\n      assert.deepStrictEqual(s3.birds, ['Chaffinch', 'Bullfinch'])\n    })\n\n    it('should return changes since the last given version', () => {\n      let s1 = Automerge.change(Automerge.init(), 'Add Chaffinch', doc => doc.birds = ['Chaffinch'])\n      let changes1 = Automerge.getAllChanges(s1)\n      let s2 = Automerge.change(s1, 'Add Bullfinch', doc => doc.birds.push('Bullfinch'))\n      let changes2 = Automerge.getChanges(s1, s2)\n      assert.strictEqual(changes1.length, 1) // Add Chaffinch\n      assert.strictEqual(changes2.length, 1) // Add Bullfinch\n    })\n\n    it('should incrementally apply changes since the last given version', () => {\n      let s1 = Automerge.change(Automerge.init(), 'Add Chaffinch', doc => doc.birds = ['Chaffinch'])\n      let changes1 = Automerge.getAllChanges(s1)\n      let s2 = Automerge.change(s1, 'Add Bullfinch', doc => doc.birds.push('Bullfinch'))\n      let changes2 = Automerge.getChanges(s1, s2)\n      let [s3] = Automerge.applyChanges(Automerge.init(), changes1)\n      let [s4] = Automerge.applyChanges(s3, changes2)\n      assert.deepStrictEqual(s3.birds, ['Chaffinch'])\n      assert.deepStrictEqual(s4.birds, ['Chaffinch', 'Bullfinch'])\n    })\n\n    it('should handle updates to a list element', () => {\n      let s1 = Automerge.change(Automerge.init(), doc => doc.birds = ['Chaffinch', 'Bullfinch'])\n      let s2 = Automerge.change(s1, doc => doc.birds[0] = 'Goldfinch')\n      let [s3] = Automerge.applyChanges(Automerge.init(), Automerge.getAllChanges(s2))\n      assert.deepStrictEqual(s3.birds, ['Goldfinch', 'Bullfinch'])\n      assert.strictEqual(Automerge.getConflicts(s3.birds, 0), undefined)\n    })\n\n    it('should handle updates to a text object', () => {\n      let s1 = Automerge.change(Automerge.init(), doc => doc.text = new Automerge.Text('ab'))\n      let s2 = Automerge.change(s1, doc => doc.text.set(0, 'A'))\n      let [s3] = Automerge.applyChanges(Automerge.init(), Automerge.getAllChanges(s2))\n      assert.deepStrictEqual([...s3.text], ['A', 'b'])\n    })\n\n    it('should report missing dependencies', () => {\n      let s1 = Automerge.change(Automerge.init(), doc => doc.birds = ['Chaffinch'])\n      let s2 = Automerge.merge(Automerge.init(), s1)\n      s2 = Automerge.change(s2, doc => doc.birds.push('Bullfinch'))\n      let changes = Automerge.getAllChanges(s2)\n      let [s3, patch] = Automerge.applyChanges(Automerge.init(), [changes[1]])\n      assert.deepStrictEqual(s3, {})\n      assert.deepStrictEqual(Automerge.Backend.getMissingDeps(Automerge.Frontend.getBackendState(s3)),\n                             decodeChange(changes[1]).deps)\n      assert.strictEqual(patch.pendingChanges, 1)\n      ;[s3, patch] = Automerge.applyChanges(s3, [changes[0]])\n      assert.deepStrictEqual(s3.birds, ['Chaffinch', 'Bullfinch'])\n      assert.deepStrictEqual(Automerge.Backend.getMissingDeps(Automerge.Frontend.getBackendState(s3)), [])\n      assert.strictEqual(patch.pendingChanges, 0)\n    })\n\n    it('should allow changes to be applied in any order', () => {\n      let s1 = Automerge.change(Automerge.init(), doc => doc.bird = 'Goldfinch')\n      let s2 = Automerge.change(s1, doc => doc.bird = 'Chaffinch')\n      let s3 = Automerge.change(s2, doc => doc.bird = 'Greenfinch')\n      let changes = Automerge.getAllChanges(s3).reverse()\n      let [s4] = Automerge.applyChanges(Automerge.init(), changes)\n      assert.deepStrictEqual(s4, {bird: 'Greenfinch'})\n    })\n\n    it('should report missing dependencies with out-of-order applyChanges', () => {\n      let s0 = Automerge.init()\n      let s1 = Automerge.change(s0, doc => doc.test = ['a'])\n      let changes01 = Automerge.getAllChanges(s1)\n      let s2 = Automerge.change(s1, doc => doc.test = ['b'])\n      let changes12 = Automerge.getChanges(s1, s2)\n      let s3 = Automerge.change(s2, doc => doc.test = ['c'])\n      let changes23 = Automerge.getChanges(s2, s3)\n      let s4 = Automerge.init()\n      let [s5] = Automerge.applyChanges(s4, changes23)\n      let [s6, patch6] = Automerge.applyChanges(s5, changes12)\n      assert.deepStrictEqual(Automerge.Backend.getMissingDeps(Automerge.Frontend.getBackendState(s6)),\n                             [decodeChange(changes01[0]).hash])\n      assert.strictEqual(patch6.pendingChanges, 2)\n    })\n\n    it('should call patchCallback if supplied when applying changes', () => {\n      const s1 = Automerge.change(Automerge.init(), doc => doc.birds = ['Goldfinch'])\n      const callbacks = [], actor = Automerge.getActorId(s1)\n      const before = Automerge.init()\n      const [after, patch] = Automerge.applyChanges(before, Automerge.getAllChanges(s1), {\n        patchCallback(patch, before, after, local) {\n          callbacks.push({patch, before, after, local})\n        }\n      })\n      assert.strictEqual(callbacks.length, 1)\n      assert.deepStrictEqual(callbacks[0].patch, {\n        maxOp: 2, deps: [decodeChange(Automerge.getAllChanges(s1)[0]).hash], clock: {[actor]: 1}, pendingChanges: 0,\n        diffs: {objectId: '_root', type: 'map', props: {birds: {[`1@${actor}`]: {\n          objectId: `1@${actor}`, type: 'list', edits: [\n            {action: 'insert', index: 0, elemId: `2@${actor}`, opId: `2@${actor}`, value: {type: 'value', value: 'Goldfinch'}}\n          ]\n        }}}}\n      })\n      assert.strictEqual(callbacks[0].patch, patch)\n      assert.strictEqual(callbacks[0].before, before)\n      assert.strictEqual(callbacks[0].after, after)\n      assert.strictEqual(callbacks[0].local, false)\n    })\n\n    it('should merge multiple applied changes into one patch', () => {\n      const s1 = Automerge.change(Automerge.init(), doc => doc.birds = ['Goldfinch'])\n      const s2 = Automerge.change(s1, doc => doc.birds.push('Chaffinch'))\n      const patches = [], actor = Automerge.getActorId(s2)\n      Automerge.applyChanges(Automerge.init(), Automerge.getAllChanges(s2),\n                             {patchCallback: p => patches.push(p)})\n      assert.deepStrictEqual(patches, [{\n        maxOp: 3, deps: [decodeChange(Automerge.getAllChanges(s2)[1]).hash], clock: {[actor]: 2}, pendingChanges: 0,\n        diffs: {objectId: '_root', type: 'map', props: {birds: {[`1@${actor}`]: {\n          objectId: `1@${actor}`, type: 'list', edits: [\n            {action: 'multi-insert', index: 0, elemId: `2@${actor}`, values: ['Goldfinch', 'Chaffinch']}\n          ]\n        }}}}\n      }])\n    })\n\n    it('should call a patchCallback registered on doc initialisation', () => {\n      const s1 = Automerge.change(Automerge.init(), doc => doc.bird = 'Goldfinch')\n      const patches = [], actor = Automerge.getActorId(s1)\n      const before = Automerge.init({patchCallback: p => patches.push(p)})\n      Automerge.applyChanges(before, Automerge.getAllChanges(s1))\n      assert.deepStrictEqual(patches, [{\n        maxOp: 1, deps: [decodeChange(Automerge.getAllChanges(s1)[0]).hash], clock: {[actor]: 1}, pendingChanges: 0,\n        diffs: {objectId: '_root', type: 'map', props: {bird: {[`1@${actor}`]: {type: 'value', value: 'Goldfinch'}}}}\n      }])\n    })\n  })\n})\n"
  },
  {
    "path": "test/text_test.js",
    "content": "const assert = require('assert')\nconst Automerge = process.env.TEST_DIST === '1' ? require('../dist/automerge') : require('../src/automerge')\nconst { assertEqualsOneOf } = require('./helpers')\n\nfunction attributeStateToAttributes(accumulatedAttributes) {\n  const attributes = {}\n  Object.entries(accumulatedAttributes).forEach(([key, values]) => {\n    if (values.length && values[0] !== null) {\n      attributes[key] = values[0]\n    }\n  })\n  return attributes\n}\n\nfunction isEquivalent(a, b) {\n  const aProps = Object.getOwnPropertyNames(a)\n  const bProps = Object.getOwnPropertyNames(b)\n\n  if (aProps.length != bProps.length) {\n      return false\n  }\n\n  for (let i = 0; i < aProps.length; i++) {\n    const propName = aProps[i]\n      if (a[propName] !== b[propName]) {\n          return false\n      }\n  }\n\n  return true\n}\n\nfunction isControlMarker(pseudoCharacter) {\n  return typeof pseudoCharacter === 'object' && pseudoCharacter.attributes\n}\n\nfunction opFrom(text, attributes) {\n  let op = { insert: text }\n  if (Object.keys(attributes).length > 0) {\n      op.attributes = attributes\n  }\n  return op\n}\n\nfunction accumulateAttributes(span, accumulatedAttributes) {\n  Object.entries(span).forEach(([key, value]) => {\n    if (!accumulatedAttributes[key]) {\n      accumulatedAttributes[key] = []\n    }\n    if (value === null) {\n      if (accumulatedAttributes[key].length === 0 || accumulatedAttributes[key] === null) {\n        accumulatedAttributes[key].unshift(null)\n      } else {\n        accumulatedAttributes[key].shift()\n      }\n    } else {\n      if (accumulatedAttributes[key][0] === null) {\n        accumulatedAttributes[key].shift()\n      } else {\n        accumulatedAttributes[key].unshift(value)\n      }\n    }\n  })\n  return accumulatedAttributes\n}\n\nfunction automergeTextToDeltaDoc(text) {\n  let ops = []\n  let controlState = {}\n  let currentString = \"\"\n  let attributes = {}\n  text.toSpans().forEach((span) => {\n    if (isControlMarker(span)) {\n      controlState = accumulateAttributes(span.attributes, controlState)\n    } else {\n      let next = attributeStateToAttributes(controlState)\n\n      // if the next span has the same calculated attributes as the current span\n      // don't bother outputting it as a separate span, just let it ride\n      if (typeof span === 'string' && isEquivalent(next, attributes)) {\n          currentString = currentString + span\n          return\n      }\n\n      if (currentString) {\n        ops.push(opFrom(currentString, attributes))\n      }\n\n      // If we've got a string, we might be able to concatenate it to another\n      // same-attributed-string, so remember it and go to the next iteration.\n      if (typeof span === 'string') {\n        currentString = span\n        attributes = next\n      } else {\n        // otherwise we have an embed \"character\" and should output it immediately.\n        // embeds are always one-\"character\" in length.\n        ops.push(opFrom(span, next))\n        currentString = ''\n        attributes = {}\n      }\n    }\n  })\n\n  // at the end, flush any accumulated string out\n  if (currentString) {\n    ops.push(opFrom(currentString, attributes))\n  }\n\n  return ops\n}\n\nfunction inverseAttributes(attributes) {\n  let invertedAttributes = {}\n  Object.keys(attributes).forEach((key) => {\n    invertedAttributes[key] = null\n  })\n  return invertedAttributes\n}\n\nfunction applyDeleteOp(text, offset, op) {\n  let length = op.delete\n  while (length > 0) {\n    if (isControlMarker(text.get(offset))) {\n      offset += 1\n    } else {\n      // we need to not delete control characters, but we do delete embed characters\n      text.deleteAt(offset, 1)\n      length -= 1\n    }\n  }\n  return [text, offset]\n}\n\nfunction applyRetainOp(text, offset, op) {\n  let length = op.retain\n\n  if (op.attributes) {\n    text.insertAt(offset, { attributes: op.attributes })\n    offset += 1\n  }\n\n  while (length > 0) {\n    const char = text.get(offset)\n    offset += 1\n    if (!isControlMarker(char)) {\n      length -= 1\n    }\n  }\n\n  if (op.attributes) {\n    text.insertAt(offset, { attributes: inverseAttributes(op.attributes) })\n    offset += 1\n  }\n\n  return [text, offset]\n}\n\n\nfunction applyInsertOp(text, offset, op) {\n  let originalOffset = offset\n\n  if (typeof op.insert === 'string') {\n    text.insertAt(offset, ...op.insert.split(''))\n    offset += op.insert.length\n  } else {\n    // we have an embed or something similar\n    text.insertAt(offset, op.insert)\n    offset += 1\n  }\n\n  if (op.attributes) {\n    text.insertAt(originalOffset, { attributes: op.attributes })\n    offset += 1\n  }\n  if (op.attributes) {\n    text.insertAt(offset, { attributes: inverseAttributes(op.attributes) })\n    offset += 1\n  }\n  return [text, offset]\n}\n\n// XXX: uhhhhh, why can't I pass in text?\nfunction applyDeltaDocToAutomergeText(delta, doc) {\n  let offset = 0\n\n  delta.forEach(op => {\n    if (op.retain) {\n      [, offset] = applyRetainOp(doc.text, offset, op)\n    } else if (op.delete) {\n      [, offset] = applyDeleteOp(doc.text, offset, op)\n    } else if (op.insert) {\n      [, offset] = applyInsertOp(doc.text, offset, op)\n    }\n  })\n}\n\ndescribe('Automerge.Text', () => {\n  let s1, s2\n  beforeEach(() => {\n    s1 = Automerge.change(Automerge.init(), doc => doc.text = new Automerge.Text())\n    s2 = Automerge.merge(Automerge.init(), s1)\n  })\n\n  it('should support insertion', () => {\n    s1 = Automerge.change(s1, doc => doc.text.insertAt(0, 'a'))\n    assert.strictEqual(s1.text.length, 1)\n    assert.strictEqual(s1.text.get(0), 'a')\n    assert.strictEqual(s1.text.toString(), 'a')\n    assert.strictEqual(s1.text.getElemId(0), `2@${Automerge.getActorId(s1)}`)\n  })\n\n  it('should support deletion', () => {\n    s1 = Automerge.change(s1, doc => doc.text.insertAt(0, 'a', 'b', 'c'))\n    s1 = Automerge.change(s1, doc => doc.text.deleteAt(1, 1))\n    assert.strictEqual(s1.text.length, 2)\n    assert.strictEqual(s1.text.get(0), 'a')\n    assert.strictEqual(s1.text.get(1), 'c')\n    assert.strictEqual(s1.text.toString(), 'ac')\n  })\n\n  it(\"should support implicit and explicit deletion\", () => {\n    s1 = Automerge.change(s1, doc => doc.text.insertAt(0, \"a\", \"b\", \"c\"))\n    s1 = Automerge.change(s1, doc => doc.text.deleteAt(1))\n    s1 = Automerge.change(s1, doc => doc.text.deleteAt(1, 0))\n    assert.strictEqual(s1.text.length, 2)\n    assert.strictEqual(s1.text.get(0), \"a\")\n    assert.strictEqual(s1.text.get(1), \"c\")\n    assert.strictEqual(s1.text.toString(), \"ac\")\n  })\n\n  it('should handle concurrent insertion', () => {\n    s1 = Automerge.change(s1, doc => doc.text.insertAt(0, 'a', 'b', 'c'))\n    s2 = Automerge.change(s2, doc => doc.text.insertAt(0, 'x', 'y', 'z'))\n    s1 = Automerge.merge(s1, s2)\n    assert.strictEqual(s1.text.length, 6)\n    assertEqualsOneOf(s1.text.toString(), 'abcxyz', 'xyzabc')\n    assertEqualsOneOf(s1.text.join(''), 'abcxyz', 'xyzabc')\n  })\n\n  it('should handle text and other ops in the same change', () => {\n    s1 = Automerge.change(s1, doc => {\n      doc.foo = 'bar'\n      doc.text.insertAt(0, 'a')\n    })\n    assert.strictEqual(s1.foo, 'bar')\n    assert.strictEqual(s1.text.toString(), 'a')\n    assert.strictEqual(s1.text.join(''), 'a')\n  })\n\n  it('should serialize to JSON as a simple string', () => {\n    s1 = Automerge.change(s1, doc => doc.text.insertAt(0, 'a', '\"', 'b'))\n    assert.strictEqual(JSON.stringify(s1), '{\"text\":\"a\\\\\"b\"}')\n  })\n\n  it('should allow modification before an object is assigned to a document', () => {\n    s1 = Automerge.change(Automerge.init(), doc => {\n      const text = new Automerge.Text()\n      text.insertAt(0, 'a', 'b', 'c', 'd')\n      text.deleteAt(2)\n      doc.text = text\n      assert.strictEqual(doc.text.toString(), 'abd')\n      assert.strictEqual(doc.text.join(''), 'abd')\n    })\n    assert.strictEqual(s1.text.toString(), 'abd')\n    assert.strictEqual(s1.text.join(''), 'abd')\n  })\n\n  it('should allow modification after an object is assigned to a document', () => {\n    s1 = Automerge.change(Automerge.init(), doc => {\n      const text = new Automerge.Text()\n      doc.text = text\n      doc.text.insertAt(0, 'a', 'b', 'c', 'd')\n      doc.text.deleteAt(2)\n      assert.strictEqual(doc.text.toString(), 'abd')\n      assert.strictEqual(doc.text.join(''), 'abd')\n    })\n    assert.strictEqual(s1.text.join(''), 'abd')\n  })\n\n  it('should not allow modification outside of a change callback', () => {\n    assert.throws(() => s1.text.insertAt(0, 'a'), /Text object cannot be modified outside of a change block/)\n  })\n\n  describe('with initial value', () => {\n    it('should accept a string as initial value', () => {\n      let s1 = Automerge.change(Automerge.init(), doc => doc.text = new Automerge.Text('init'))\n      assert.strictEqual(s1.text.length, 4)\n      assert.strictEqual(s1.text.get(0), 'i')\n      assert.strictEqual(s1.text.get(1), 'n')\n      assert.strictEqual(s1.text.get(2), 'i')\n      assert.strictEqual(s1.text.get(3), 't')\n      assert.strictEqual(s1.text.toString(), 'init')\n    })\n\n    it('should accept an array as initial value', () => {\n      let s1 = Automerge.change(Automerge.init(), doc => doc.text = new Automerge.Text(['i', 'n', 'i', 't']))\n      assert.strictEqual(s1.text.length, 4)\n      assert.strictEqual(s1.text.get(0), 'i')\n      assert.strictEqual(s1.text.get(1), 'n')\n      assert.strictEqual(s1.text.get(2), 'i')\n      assert.strictEqual(s1.text.get(3), 't')\n      assert.strictEqual(s1.text.toString(), 'init')\n    })\n\n    it('should initialize text in Automerge.from()', () => {\n      let s1 = Automerge.from({text: new Automerge.Text('init')})\n      assert.strictEqual(s1.text.length, 4)\n      assert.strictEqual(s1.text.get(0), 'i')\n      assert.strictEqual(s1.text.get(1), 'n')\n      assert.strictEqual(s1.text.get(2), 'i')\n      assert.strictEqual(s1.text.get(3), 't')\n      assert.strictEqual(s1.text.toString(), 'init')\n    })\n\n    it('should encode the initial value as a change', () => {\n      const s1 = Automerge.from({text: new Automerge.Text('init')})\n      const changes = Automerge.getAllChanges(s1)\n      assert.strictEqual(changes.length, 1)\n      const [s2] = Automerge.applyChanges(Automerge.init(), changes)\n      assert.strictEqual(s2.text instanceof Automerge.Text, true)\n      assert.strictEqual(s2.text.toString(), 'init')\n      assert.strictEqual(s2.text.join(''), 'init')\n    })\n\n    it('should allow immediate access to the value', () => {\n      Automerge.change(Automerge.init(), doc => {\n        const text = new Automerge.Text('init')\n        assert.strictEqual(text.length, 4)\n        assert.strictEqual(text.get(0), 'i')\n        assert.strictEqual(text.toString(), 'init')\n        doc.text = text\n        assert.strictEqual(doc.text.length, 4)\n        assert.strictEqual(doc.text.get(0), 'i')\n        assert.strictEqual(doc.text.toString(), 'init')\n      })\n    })\n\n    it('should allow pre-assignment modification of the initial value', () => {\n      let s1 = Automerge.change(Automerge.init(), doc => {\n        const text = new Automerge.Text('init')\n        text.deleteAt(3)\n        assert.strictEqual(text.join(''), 'ini')\n        doc.text = text\n        assert.strictEqual(doc.text.join(''), 'ini')\n        assert.strictEqual(doc.text.toString(), 'ini')\n      })\n      assert.strictEqual(s1.text.toString(), 'ini')\n      assert.strictEqual(s1.text.join(''), 'ini')\n    })\n\n    it('should allow post-assignment modification of the initial value', () => {\n      let s1 = Automerge.change(Automerge.init(), doc => {\n        const text = new Automerge.Text('init')\n        doc.text = text\n        doc.text.deleteAt(0)\n        doc.text.insertAt(0, 'I')\n        assert.strictEqual(doc.text.join(''), 'Init')\n        assert.strictEqual(doc.text.toString(), 'Init')\n      })\n      assert.strictEqual(s1.text.join(''), 'Init')\n      assert.strictEqual(s1.text.toString(), 'Init')\n    })\n  })\n\n  describe('non-textual control characters', () => {\n    let s1\n    beforeEach(() => {\n      s1 = Automerge.change(Automerge.init(), doc => {\n        doc.text = new Automerge.Text()\n        doc.text.insertAt(0, 'a')\n        doc.text.insertAt(1, { attribute: 'bold' })\n      })\n    })\n\n    it('should allow fetching non-textual characters', () => {\n      assert.deepEqual(s1.text.get(1), { attribute: 'bold' })\n      assert.strictEqual(s1.text.getElemId(1), `3@${Automerge.getActorId(s1)}`)\n    })\n\n    it('should include control characters in string length', () => {\n      assert.strictEqual(s1.text.length, 2)\n      assert.strictEqual(s1.text.get(0), 'a')\n    })\n\n    it('should exclude control characters from toString()', () => {\n      assert.strictEqual(s1.text.toString(), 'a')\n    })\n\n    it('should allow control characters to be updated', () => {\n      const s2 = Automerge.change(s1, doc => doc.text.get(1).attribute = 'italic')\n      const s3 = Automerge.load(Automerge.save(s2))\n      assert.strictEqual(s1.text.get(1).attribute, 'bold')\n      assert.strictEqual(s2.text.get(1).attribute, 'italic')\n      assert.strictEqual(s3.text.get(1).attribute, 'italic')\n    })\n\n    describe('spans interface to Text', () => {\n      it('should return a simple string as a single span', () => {\n        let s1 = Automerge.change(Automerge.init(), doc => {\n          doc.text = new Automerge.Text('hello world')\n        })\n        assert.deepEqual(s1.text.toSpans(), ['hello world'])\n      })\n      it('should return an empty string as an empty array', () => {\n        let s1 = Automerge.change(Automerge.init(), doc => {\n          doc.text = new Automerge.Text()\n        })\n        assert.deepEqual(s1.text.toSpans(), [])\n      })\n      it('should split a span at a control character', () => {\n        let s1 = Automerge.change(Automerge.init(), doc => {\n          doc.text = new Automerge.Text('hello world')\n          doc.text.insertAt(5, { attributes: { bold: true } })\n        })\n        assert.deepEqual(s1.text.toSpans(),\n          ['hello', { attributes: { bold: true } }, ' world'])\n      })\n      it('should allow consecutive control characters', () => {\n        let s1 = Automerge.change(Automerge.init(), doc => {\n          doc.text = new Automerge.Text('hello world')\n          doc.text.insertAt(5, { attributes: { bold: true } })\n          doc.text.insertAt(6, { attributes: { italic: true } })\n        })\n        assert.deepEqual(s1.text.toSpans(),\n          ['hello',\n           { attributes: { bold: true } },\n           { attributes: { italic: true } },\n           ' world'\n          ])\n      })\n      it('should allow non-consecutive control characters', () => {\n        let s1 = Automerge.change(Automerge.init(), doc => {\n          doc.text = new Automerge.Text('hello world')\n          doc.text.insertAt(5, { attributes: { bold: true } })\n          doc.text.insertAt(12, { attributes: { italic: true } })\n        })\n        assert.deepEqual(s1.text.toSpans(),\n          ['hello',\n           { attributes: { bold: true } },\n           ' world',\n           { attributes: { italic: true } }\n          ])\n      })\n\n      it('should be convertable into a Quill delta', () => {\n        let s1 = Automerge.change(Automerge.init(), doc => {\n          doc.text = new Automerge.Text('Gandalf the Grey')\n          doc.text.insertAt(0,  { attributes: { bold: true } })\n          doc.text.insertAt(7 + 1, { attributes: { bold: null } })\n          doc.text.insertAt(12 + 2, { attributes: { color: '#cccccc' } })\n        })\n\n        let deltaDoc = automergeTextToDeltaDoc(s1.text)\n\n        // From https://quilljs.com/docs/delta/\n        let expectedDoc = [\n          { insert: 'Gandalf', attributes: { bold: true } },\n          { insert: ' the ' },\n          { insert: 'Grey', attributes: { color: '#cccccc' } }\n        ]\n\n        assert.deepEqual(deltaDoc, expectedDoc)\n      })\n\n      it('should support embeds', () => {\n        let s1 = Automerge.change(Automerge.init(), doc => {\n          doc.text = new Automerge.Text('')\n          doc.text.insertAt(0, { attributes: { link: 'https://quilljs.com' } })\n          doc.text.insertAt(1, {\n            image: 'https://quilljs.com/assets/images/icon.png'\n          })\n          doc.text.insertAt(2, { attributes: { link: null } })\n        })\n\n        let deltaDoc = automergeTextToDeltaDoc(s1.text)\n\n        // From https://quilljs.com/docs/delta/\n        let expectedDoc = [{\n          // An image link\n          insert: {\n            image: 'https://quilljs.com/assets/images/icon.png'\n          },\n          attributes: {\n            link: 'https://quilljs.com'\n          }\n        }]\n\n        assert.deepEqual(deltaDoc, expectedDoc)\n      })\n\n      it('should handle concurrent overlapping spans', () => {\n        let s1 = Automerge.change(Automerge.init(), doc => {\n          doc.text = new Automerge.Text('Gandalf the Grey')\n        })\n\n        let s2 = Automerge.merge(Automerge.init(), s1)\n\n        let s3 = Automerge.change(s1, doc => {\n          doc.text.insertAt(8,  { attributes: { bold: true } })\n          doc.text.insertAt(16 + 1, { attributes: { bold: null } })\n        })\n\n        let s4 = Automerge.change(s2, doc => {\n          doc.text.insertAt(0,  { attributes: { bold: true } })\n          doc.text.insertAt(11 + 1, { attributes: { bold: null } })\n        })\n\n        let merged = Automerge.merge(s3, s4)\n\n        let deltaDoc = automergeTextToDeltaDoc(merged.text)\n\n        // From https://quilljs.com/docs/delta/\n        let expectedDoc = [\n          { insert: 'Gandalf the Grey', attributes: { bold: true } },\n        ]\n\n        assert.deepEqual(deltaDoc, expectedDoc)\n      })\n\n      it('should handle debolding spans', () => {\n        let s1 = Automerge.change(Automerge.init(), doc => {\n          doc.text = new Automerge.Text('Gandalf the Grey')\n        })\n\n        let s2 = Automerge.merge(Automerge.init(), s1)\n\n        let s3 = Automerge.change(s1, doc => {\n          doc.text.insertAt(0,  { attributes: { bold: true } })\n          doc.text.insertAt(16 + 1, { attributes: { bold: null } })\n        })\n\n        let s4 = Automerge.change(s2, doc => {\n          doc.text.insertAt(8,  { attributes: { bold: null } })\n          doc.text.insertAt(11 + 1, { attributes: { bold: true } })\n        })\n\n\n        let merged = Automerge.merge(s3, s4)\n\n        let deltaDoc = automergeTextToDeltaDoc(merged.text)\n\n        // From https://quilljs.com/docs/delta/\n        let expectedDoc = [\n          { insert: 'Gandalf ', attributes: { bold: true } },\n          { insert: 'the' },\n          { insert: ' Grey', attributes: { bold: true } },\n        ]\n\n        assert.deepEqual(deltaDoc, expectedDoc)\n      })\n\n      // xxx: how would this work for colors?\n      it('should handle destyling across destyled spans', () => {\n        let s1 = Automerge.change(Automerge.init(), doc => {\n          doc.text = new Automerge.Text('Gandalf the Grey')\n        })\n\n        let s2 = Automerge.merge(Automerge.init(), s1)\n\n        let s3 = Automerge.change(s1, doc => {\n          doc.text.insertAt(0,  { attributes: { bold: true } })\n          doc.text.insertAt(16 + 1, { attributes: { bold: null } })\n        })\n\n        let s4 = Automerge.change(s2, doc => {\n          doc.text.insertAt(8,  { attributes: { bold: null } })\n          doc.text.insertAt(11 + 1, { attributes: { bold: true } })\n        })\n\n        let merged = Automerge.merge(s3, s4)\n\n        let final = Automerge.change(merged, doc => {\n          doc.text.insertAt(3 + 1, { attributes: { bold: null } })\n          doc.text.insertAt(doc.text.length, { attributes: { bold: true } })\n        })\n\n        let deltaDoc = automergeTextToDeltaDoc(final.text)\n\n        // From https://quilljs.com/docs/delta/\n        let expectedDoc = [\n          { insert: 'Gan', attributes: { bold: true } },\n          { insert: 'dalf the Grey' },\n        ]\n\n        assert.deepEqual(deltaDoc, expectedDoc)\n      })\n\n      it('should apply an insert', () => {\n        let s1 = Automerge.change(Automerge.init(), doc => {\n          doc.text = new Automerge.Text('Hello world')\n        })\n\n        const delta = [\n          { retain: 6 },\n          { insert: 'reader' },\n          { delete: 5 }\n        ]\n\n        let s2 = Automerge.change(s1, doc => {\n          applyDeltaDocToAutomergeText(delta, doc)\n        })\n\n        assert.strictEqual(s2.text.join(''), 'Hello reader')\n      })\n\n      it('should apply an insert with control characters', () => {\n        let s1 = Automerge.change(Automerge.init(), doc => {\n          doc.text = new Automerge.Text('Hello world')\n        })\n\n        const delta = [\n          { retain: 6 },\n          { insert: 'reader', attributes: { bold: true } },\n          { delete: 5 },\n          { insert: '!' }\n        ]\n\n        let s2 = Automerge.change(s1, doc => {\n          applyDeltaDocToAutomergeText(delta, doc)\n        })\n\n        assert.strictEqual(s2.text.toString(), 'Hello reader!')\n        assert.deepEqual(s2.text.toSpans(), [\n          \"Hello \",\n          { attributes: { bold: true } },\n          \"reader\",\n          { attributes: { bold: null } },\n          \"!\"\n        ])\n      })\n\n      it('should account for control characters in retain/delete lengths', () => {\n        let s1 = Automerge.change(Automerge.init(), doc => {\n          doc.text = new Automerge.Text('Hello world')\n          doc.text.insertAt(4, { attributes: { color: '#ccc' } })\n          doc.text.insertAt(10, { attributes: { color: '#f00' } })\n        })\n\n        const delta = [\n          { retain: 6 },\n          { insert: 'reader', attributes: { bold: true } },\n          { delete: 5 },\n          { insert: '!' }\n        ]\n\n        let s2 = Automerge.change(s1, doc => {\n          applyDeltaDocToAutomergeText(delta, doc)\n        })\n\n        assert.strictEqual(s2.text.toString(), 'Hello reader!')\n        assert.deepEqual(s2.text.toSpans(), [\n          \"Hell\",\n          { attributes: { color: '#ccc'} },\n          \"o \",\n          { attributes: { bold: true } },\n          \"reader\",\n          { attributes: { bold: null } },\n          { attributes: { color: '#f00'} },\n          \"!\"\n        ])\n      })\n\n      it('should support embeds', () => {\n        let s1 = Automerge.change(Automerge.init(), doc => {\n          doc.text = new Automerge.Text('')\n        })\n\n        let deltaDoc = [{\n          // An image link\n          insert: {\n            image: 'https://quilljs.com/assets/images/icon.png'\n          },\n          attributes: {\n            link: 'https://quilljs.com'\n          }\n        }]\n\n        let s2 = Automerge.change(s1, doc => {\n          applyDeltaDocToAutomergeText(deltaDoc, doc)\n        })\n\n        assert.deepEqual(s2.text.toSpans(), [\n          { attributes: { link: 'https://quilljs.com' } },\n          { image: 'https://quilljs.com/assets/images/icon.png'},\n          { attributes: { link: null } },\n        ])\n      })\n    })\n  })\n\n  it('should support unicode when creating text', () => {\n    s1 = Automerge.from({\n      text: new Automerge.Frontend.Text('🐦')\n    })\n    assert.strictEqual(s1.text.get(0), '🐦')\n  })\n})\n"
  },
  {
    "path": "test/typescript_test.ts",
    "content": "import * as assert from 'assert'\nimport * as Automerge from 'automerge'\nimport { Backend, Frontend, Counter, Doc } from 'automerge'\n\nconst UUID_PATTERN = /^[0-9a-f]{32}$/\n\ninterface BirdList {\n  birds: Automerge.List<string>\n}\n\ninterface NumberBox {\n  number: number\n}\n\ndescribe('TypeScript support', () => {\n  describe('Automerge.init()', () => {\n    it('should allow a document to be `any`', () => {\n      let s1 = Automerge.init<any>()\n      s1 = Automerge.change(s1, doc => (doc.key = 'value'))\n      assert.strictEqual(s1.key, 'value')\n      assert.strictEqual(s1.nonexistent, undefined)\n      assert.deepStrictEqual(s1, { key: 'value' })\n    })\n\n    it('should allow a document type to be specified as a parameter to `init`', () => {\n      let s1 = Automerge.init<BirdList>()\n\n      // Note: Technically, `s1` is not really a `BirdList` yet but just an empty object.\n      assert.equal(s1.hasOwnProperty('birds'), false)\n\n      // Since we're pulling the wool over TypeScript's eyes, it can't give us compile-time protection\n      // from something like this:\n      // assert.equal(s1.birds.length, 0) // Runtime error: Cannot read property 'length' of undefined\n\n      // Nevertheless this way seems more ergonomical (than having `init` return a type of `{}` or\n      // `Partial<T>`, for example) because it allows us to have a single type for the object\n      // throughout its life, rather than having to recast it once its required fields have\n      // been populated.\n      s1 = Automerge.change(s1, doc => (doc.birds = ['goldfinch']))\n      assert.deepStrictEqual(s1.birds, ['goldfinch'])\n    })\n\n    it('should allow a document type to be specified on the result of `init`', () => {\n      // This is equivalent to passing the type parameter to `init`; note that the result is a\n      // `Doc`, which is frozen\n      let s1: Doc<BirdList> = Automerge.init()\n      let s2 = Automerge.change(s1, doc => (doc.birds = ['goldfinch']))\n      assert.deepStrictEqual(s2.birds, ['goldfinch'])\n    })\n\n    it('should allow a document to be initialized with `from`', () => {\n      const s1 = Automerge.from<BirdList>({ birds: [] })\n      assert.strictEqual(s1.birds.length, 0)\n      const s2 = Automerge.change(s1, doc => doc.birds.push('magpie'))\n      assert.strictEqual(s2.birds[0], 'magpie')\n    })\n\n    it('should allow passing options when initializing with `from`', () => {\n      const actorId = '1234'\n      const s1 = Automerge.from<BirdList>({ birds: [] }, actorId)\n      assert.strictEqual(Automerge.getActorId(s1), '1234')\n      const s2 = Automerge.from<BirdList>({ birds: [] }, { actorId })\n      assert.strictEqual(Automerge.getActorId(s2), '1234')\n    })\n\n    it('should allow the actorId to be configured', () => {\n      let s1 = Automerge.init<BirdList>('111111')\n      assert.strictEqual(Automerge.getActorId(s1), '111111')\n      let s2 = Automerge.init<BirdList>()\n      assert.strictEqual(UUID_PATTERN.test(Automerge.getActorId(s2)), true)\n    })\n\n    it('should allow the freeze option to be passed in', () => {\n      let s1 = Automerge.init<BirdList>({ freeze: true })\n      let s2 = Automerge.change(s1, doc => (doc.birds = []))\n      assert.strictEqual(Object.isFrozen(s2), true)\n      assert.strictEqual(Object.isFrozen(s2.birds), true)\n    })\n\n    it('should allow a frontend to be `any`', () => {\n      const s0 = Frontend.init<any>()\n      const [s1, req1] = Frontend.change(s0, doc => (doc.key = 'value'))\n      assert.strictEqual(s1.key, 'value')\n      assert.strictEqual(s1.nonexistent, undefined)\n      assert.strictEqual(UUID_PATTERN.test(Frontend.getActorId(s1)), true)\n    })\n\n    it('should allow a frontend type to be specified', () => {\n      const s0 = Frontend.init<BirdList>()\n      const [s1, req1] = Frontend.change(s0, doc => (doc.birds = ['goldfinch']))\n      assert.strictEqual(s1.birds[0], 'goldfinch')\n      assert.deepStrictEqual(s1, { birds: ['goldfinch'] })\n    })\n\n    it('should allow a frontend actorId to be configured', () => {\n      const s0 = Frontend.init<NumberBox>('111111')\n      assert.strictEqual(Frontend.getActorId(s0), '111111')\n    })\n\n    it('should allow frontend actorId assignment to be deferred', () => {\n      const s0 = Frontend.init<NumberBox>({ deferActorId: true })\n      assert.strictEqual(Frontend.getActorId(s0), undefined)\n      const s1 = Frontend.setActorId(s0, 'abcdef1234')\n      const [s2, req] = Frontend.change(s1, doc => (doc.number = 15))\n      assert.deepStrictEqual(s2, { number: 15 })\n    })\n\n    it('should allow a frontend to be initialized with `from`', () => {\n      const [s1, req1] = Frontend.from<BirdList>({ birds: [] })\n      assert.strictEqual(s1.birds.length, 0)\n      const [s2, req2] = Frontend.change(s1, doc => doc.birds.push('magpie'))\n      assert.strictEqual(s2.birds[0], 'magpie')\n    })\n\n    it('should allow options to be passed to Frontend.from()', () => {\n      const [s1, req1] = Frontend.from<BirdList>({ birds: []}, { actorId: '1234' })\n      assert.strictEqual(Frontend.getActorId(s1), '1234')\n      assert.deepStrictEqual(s1, { birds: [] })\n      const [s2, req2] = Frontend.from<BirdList>({ birds: []}, '1234')\n      assert.strictEqual(Frontend.getActorId(s2), '1234')\n    })\n\n    it('should allow the length of the array to be increased', () => {\n      let s1: Doc<BirdList> = Automerge.from({ birds: []})\n      let s2 = Automerge.change(s1, doc => doc.birds.length = 1)\n      assert.deepStrictEqual(s2.birds, [null])\n    })\n\n    it('should allow the length of the array to be decreased', () => {\n      let s1: Doc<BirdList> = Automerge.from({ birds: ['1234']})\n      let s2 = Automerge.change(s1, doc => doc.birds.length = 0)\n      assert.deepStrictEqual(s2.birds, [])\n    })\n\n    it('should throw error if length is invalid', () => {\n      let s1: Doc<BirdList> = Automerge.from({ birds: ['1234']})\n      assert.throws(() => Automerge.change(s1, doc => {\n        doc.birds.length = undefined\n      }), \"array length\")\n      assert.throws(() => Automerge.change(s1, doc => {\n        doc.birds.length = NaN\n      }), \"array length\")\n    })\n  })\n\n  describe('saving and loading', () => {\n    it('should allow an `any` type document to be loaded', () => {\n      let s1 = Automerge.init<any>()\n      s1 = Automerge.change(s1, doc => (doc.key = 'value'))\n      let s2: any = Automerge.load(Automerge.save(s1))\n      assert.strictEqual(s2.key, 'value')\n      assert.deepStrictEqual(s2, { key: 'value' })\n    })\n\n    it('should allow a document of declared type to be loaded', () => {\n      let s1 = Automerge.init<BirdList>()\n      s1 = Automerge.change(s1, doc => (doc.birds = ['goldfinch']))\n      let s2 = Automerge.load<BirdList>(Automerge.save(s1))\n      assert.strictEqual(s2.birds[0], 'goldfinch')\n      assert.deepStrictEqual(s2, { birds: ['goldfinch'] })\n      assert.strictEqual(UUID_PATTERN.test(Automerge.getActorId(s2)), true)\n    })\n\n    it('should allow the actorId to be configured', () => {\n      let s1 = Automerge.init<BirdList>()\n      s1 = Automerge.change(s1, doc => (doc.birds = ['goldfinch']))\n      let s2 = Automerge.load<BirdList>(Automerge.save(s1), '111111')\n      assert.strictEqual(Automerge.getActorId(s2), '111111')\n    })\n\n    it('should allow the freeze option to be passed in', () => {\n      let s1 = Automerge.init<BirdList>()\n      s1 = Automerge.change(s1, doc => (doc.birds = ['goldfinch']))\n      let s2 = Automerge.load<BirdList>(Automerge.save(s1), { freeze: true })\n      assert.strictEqual(Object.isFrozen(s2), true)\n      assert.strictEqual(Object.isFrozen(s2.birds), true)\n    })\n  })\n\n  describe('making changes', () => {\n    it('should accept an optional message', () => {\n      let s1 = Automerge.init<BirdList>()\n      s1 = Automerge.change(s1, 'hello', doc => (doc.birds = []))\n      assert.strictEqual(Automerge.getHistory(s1)[0].change.message, 'hello')\n    })\n\n    it('should support list modifications', () => {\n      let s1: Doc<BirdList> = Automerge.init<BirdList>()\n      s1 = Automerge.change(s1, doc => (doc.birds = ['goldfinch']))\n      s1 = Automerge.change(s1, doc => {\n        doc.birds.insertAt(1, 'greenfinch', 'bullfinch', 'chaffinch')\n        doc.birds.deleteAt(0)\n        doc.birds.deleteAt(0, 2)\n      })\n      assert.deepStrictEqual(s1, { birds: ['chaffinch'] })\n    })\n\n    it('should allow empty changes', () => {\n      let s1 = Automerge.init()\n      s1 = Automerge.emptyChange(s1, 'my message')\n      assert.strictEqual(Automerge.getHistory(s1)[0].change.message, 'my message')\n    })\n\n    it('should allow inspection of conflicts', () => {\n      let s1 = Automerge.init<NumberBox>('111111')\n      s1 = Automerge.change(s1, doc => (doc.number = 3))\n      let s2 = Automerge.init<NumberBox>('222222')\n      s2 = Automerge.change(s2, doc => (doc.number = 42))\n      let s3 = Automerge.merge(s1, s2)\n      assert.strictEqual(s3.number, 42)\n      assert.deepStrictEqual(\n        Automerge.getConflicts(s3, 'number'),\n        { '1@111111': 3, '1@222222': 42 })\n    })\n\n    it('should allow changes in the frontend', () => {\n      const s0 = Frontend.init<BirdList>()\n      const [s1, change1] = Frontend.change(s0, doc => (doc.birds = ['goldfinch']))\n      const [s2, change2] = Frontend.change(s1, doc => doc.birds.push('chaffinch'))\n      assert.strictEqual(s2.birds[1], 'chaffinch')\n      assert.deepStrictEqual(s2, { birds: ['goldfinch', 'chaffinch'] })\n      assert.strictEqual(change2.actor, Frontend.getActorId(s0))\n      assert.strictEqual(change2.seq, 2)\n      assert.strictEqual(change2.time > 0, true)\n      assert.strictEqual(change2.message, '')\n    })\n\n    it('should accept a message in the frontend', () => {\n      const s0 = Frontend.init<NumberBox>()\n      const [s1, req1] = Frontend.change(s0, 'test message', doc => (doc.number = 1))\n      assert.strictEqual(req1.message, 'test message')\n      assert.strictEqual(req1.actor, Frontend.getActorId(s0))\n      assert.strictEqual(req1.ops.length, 1)\n    })\n\n    it('should allow empty changes in the frontend', () => {\n      const s0 = Frontend.init<NumberBox>()\n      const [s1, req1] = Frontend.emptyChange(s0, 'nothing happened')\n      assert.strictEqual(req1.message, 'nothing happened')\n      assert.strictEqual(req1.actor, Frontend.getActorId(s0))\n      assert.strictEqual(req1.ops.length, 0)\n    })\n\n    it('should work with split frontend and backend', () => {\n      const s0 = Frontend.init<NumberBox>(),\n        b0 = Backend.init()\n      const [s1, change1] = Frontend.change(s0, doc => (doc.number = 1))\n      const [b1, patch1] = Backend.applyLocalChange(b0, change1)\n      const s2 = Frontend.applyPatch(s1, patch1)\n      assert.strictEqual(s2.number, 1)\n      assert.strictEqual(patch1.actor, Automerge.getActorId(s0))\n      assert.strictEqual(patch1.seq, 1)\n      assert.strictEqual(patch1.diffs.objectId, '_root')\n      assert.strictEqual(patch1.diffs.type, 'map')\n      assert.deepStrictEqual(Object.keys(patch1.diffs.props), ['number'])\n      const value = patch1.diffs.props.number[`1@${Automerge.getActorId(s0)}`]\n      assert.strictEqual((value as Automerge.ValueDiff).value, 1)\n    })\n  })\n\n  describe('getting and applying changes', () => {\n    it('should return an array of change objects', () => {\n      let s1 = Automerge.init<BirdList>()\n      s1 = Automerge.change(s1, doc => (doc.birds = ['goldfinch']))\n      let s2 = Automerge.change(s1, 'add chaffinch', doc => doc.birds.push('chaffinch'))\n      const changes = Automerge.getChanges(s1, s2)\n      assert.strictEqual(changes.length, 1)\n      const change = Automerge.decodeChange(changes[0])\n      assert.strictEqual(change.message, 'add chaffinch')\n      assert.strictEqual(change.actor, Automerge.getActorId(s2))\n      assert.strictEqual(change.seq, 2)\n    })\n\n    it('should include operations in changes', () => {\n      let s1 = Automerge.init<NumberBox>()\n      s1 = Automerge.change(s1, doc => (doc.number = 3))\n      const changes = Automerge.getAllChanges(s1)\n      assert.strictEqual(changes.length, 1)\n      const change = Automerge.decodeChange(changes[0])\n      assert.strictEqual(change.ops.length, 1)\n      assert.strictEqual(change.ops[0].action, 'set')\n      assert.strictEqual(change.ops[0].obj, '_root')\n      assert.strictEqual(change.ops[0].key, 'number')\n      assert.strictEqual(change.ops[0].value, 3)\n    })\n\n    it('should allow changes to be re-applied', () => {\n      let s1 = Automerge.init<BirdList>()\n      s1 = Automerge.change(s1, doc => (doc.birds = []))\n      let s2 = Automerge.change(s1, doc => doc.birds.push('goldfinch'))\n      const changes = Automerge.getAllChanges(s2)\n      let [s3, patch] = Automerge.applyChanges(Automerge.init<BirdList>(), changes)\n      assert.deepStrictEqual(s3.birds, ['goldfinch'])\n    })\n\n    it('should allow concurrent changes to be merged', () => {\n      let s1 = Automerge.init<BirdList>()\n      s1 = Automerge.change(s1, doc => (doc.birds = ['goldfinch']))\n      let s2 = Automerge.merge(Automerge.init<BirdList>(), s1)\n      s1 = Automerge.change(s1, doc => doc.birds.unshift('greenfinch'))\n      s2 = Automerge.change(s2, doc => doc.birds.push('chaffinch'))\n      let s3 = Automerge.merge(s1, s2)\n      assert.deepStrictEqual(s3.birds, ['greenfinch', 'goldfinch', 'chaffinch'])\n    })\n  })\n\n  describe('history inspection', () => {\n    it('should inspect document history', () => {\n      const s0 = Automerge.init<NumberBox>()\n      const s1 = Automerge.change(s0, 'one', doc => (doc.number = 1))\n      const s2 = Automerge.change(s1, 'two', doc => (doc.number = 2))\n      const history = Automerge.getHistory(s2)\n      assert.strictEqual(history.length, 2)\n      assert.strictEqual(history[0].change.message, 'one')\n      assert.strictEqual(history[1].change.message, 'two')\n      assert.strictEqual(history[0].snapshot.number, 1)\n      assert.strictEqual(history[1].snapshot.number, 2)\n    })\n  })\n\n  describe('state inspection', () => {\n    it('should support looking up objects by ID', () => {\n      const s0 = Automerge.init<BirdList>()\n      const s1 = Automerge.change(s0, doc => (doc.birds = ['goldfinch']))\n      const obj = Automerge.getObjectId(s1.birds)\n      assert.strictEqual(Automerge.getObjectById(s1, obj).length, 1)\n      assert.strictEqual(Automerge.getObjectById(s1, obj), s1.birds)\n    })\n\n    it('should allow looking up list element IDs', () => {\n      const s0 = Automerge.init<BirdList>()\n      const s1 = Automerge.change(s0, doc => (doc.birds = ['goldfinch']))\n      const elemIds = Automerge.Frontend.getElementIds(s1.birds)\n      assert.deepStrictEqual(elemIds, [`2@${Automerge.getActorId(s1)}`])\n    })\n  })\n\n  describe('Automerge.Text', () => {\n    interface TextDoc {\n      text: Automerge.Text\n    }\n\n    let doc: Doc<TextDoc>\n\n    beforeEach(() => {\n      doc = Automerge.change(Automerge.init<TextDoc>(), doc => (doc.text = new Automerge.Text()))\n    })\n\n    describe('insertAt', () => {\n      it('should support inserting a single element', () => {\n        doc = Automerge.change(doc, doc => doc.text.insertAt(0, 'abc'))\n        assert.strictEqual(JSON.stringify(doc.text), '\"abc\"')\n      })\n\n      it('should support inserting multiple elements', () => {\n        doc = Automerge.change(doc, doc => doc.text.insertAt(0, 'a', 'b', 'c'))\n        assert.strictEqual(JSON.stringify(doc.text), '\"abc\"')\n      })\n    })\n\n    describe('deleteAt', () => {\n      beforeEach(() => {\n        doc = Automerge.change(doc, doc => doc.text.insertAt(0, 'a', 'b', 'c', 'd', 'e', 'f', 'g'))\n      })\n\n      it('should support deleting a single element without specifying `numDelete`', () => {\n        doc = Automerge.change(doc, doc => doc.text.deleteAt(2))\n        assert.strictEqual(JSON.stringify(doc.text), '\"abdefg\"')\n      })\n\n      it('should support deleting multiple elements', () => {\n        doc = Automerge.change(doc, doc => doc.text.deleteAt(3, 2))\n        assert.strictEqual(JSON.stringify(doc.text), '\"abcfg\"')\n      })\n    })\n\n    describe('get', () => {\n      it('should get the element at the given index', () => {\n        doc = Automerge.change(doc, doc => doc.text.insertAt(0, 'a', 'b', 'cdefg', 'hi', 'jkl'))\n        assert.strictEqual(doc.text.get(0), 'a')\n        assert.strictEqual(doc.text.get(2), 'cdefg')\n      })\n    })\n\n    describe('delegated read-only operations from `Array`', () => {\n      const a = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i']\n      beforeEach(() => {\n        doc = Automerge.change(doc, doc => doc.text.insertAt(0, ...a))\n      })\n\n      it('supports `indexOf`', () => assert.strictEqual(doc.text.indexOf('c'), 2))\n      it('supports `length`', () => assert.strictEqual(doc.text.length, 9))\n      it('supports `concat`', () => assert.strictEqual(doc.text.concat(['j']).length, 10))\n      it('supports `includes`', () => assert.strictEqual(doc.text.includes('q'), false))\n    })\n\n    describe('getElementIds', () => {\n      it('should return the element ID of each character', () => {\n        doc = Automerge.change(doc, doc => doc.text.insertAt(0, 'a', 'b'))\n        const elemIds = Automerge.Frontend.getElementIds(doc.text)\n        assert.deepStrictEqual(elemIds, [`2@${Automerge.getActorId(doc)}`, `3@${Automerge.getActorId(doc)}`])\n      })\n    })\n  })\n\n  describe('Automerge.Table', () => {\n    interface Book {\n      authors: string | string[]\n      title: string\n      isbn?: string\n    }\n\n    interface BookDb {\n      books: Automerge.Table<Book>\n    }\n\n    // Example data\n    const DDIA: Book = {\n      authors: ['Kleppmann, Martin'],\n      title: 'Designing Data-Intensive Applications',\n      isbn: '1449373321',\n    }\n    const RSDP: Book = {\n      authors: ['Cachin, Christian', 'Guerraoui, Rachid', 'Rodrigues, Luís'],\n      title: 'Introduction to Reliable and Secure Distributed Programming',\n      isbn: '3-642-15259-7',\n    }\n\n    let s1: Doc<BookDb>\n    let id: Automerge.UUID\n    let ddiaWithId: Book & Automerge.TableRow\n\n    beforeEach(() => {\n      s1 = Automerge.change(Automerge.init<BookDb>(), doc => {\n        doc.books = new Automerge.Table()\n        id = doc.books.add(DDIA)\n      })\n      ddiaWithId = Object.assign({id}, DDIA)\n    })\n\n    it('supports `byId`', () => assert.deepStrictEqual(s1.books.byId(id), ddiaWithId))\n    it('supports `count`', () => assert.strictEqual(s1.books.count, 1))\n    it('supports `ids`', () => assert.deepStrictEqual(s1.books.ids, [id]))\n    it('supports iteration', () => assert.deepStrictEqual([...s1.books], [ddiaWithId]))\n\n    it('allows adding row properties', () => {\n      // Note that if we add columns and want to actually use them, we need to recast the table to a\n      // new type e.g. without the `ts-ignore` flag, this would throw a type error:\n\n      // @ts-ignore - Property 'publisher' does not exist on type book\n      const p2 = s1.books.byId(id).publisher \n\n      // So we need to create new types\n      interface BookDeluxe extends Book {\n        // ... existing properties, plus:\n        publisher?: string\n      }\n      interface BookDeluxeDb {\n        books: Automerge.Table<BookDeluxe>\n      }\n\n      const s2 = s1 as Doc<BookDeluxeDb> // Cast existing table to new type\n      const s3 = Automerge.change(\n        s2,\n        doc => (doc.books.byId(id).publisher = \"O'Reilly\")\n      )\n\n      // Now we're off to the races\n      const p3 = s3.books.byId(id).publisher\n      assert.strictEqual(p3, \"O'Reilly\")\n    })\n\n    it('supports `remove`', () => {\n      const s2 = Automerge.change(s1, doc => doc.books.remove(id))\n      assert.strictEqual(s2.books.count, 0)\n    })\n\n    describe('supports `add`', () => {\n      it('accepts value passed as object', () => {\n        let bookId: string\n        const s2 = Automerge.change(s1, doc => (bookId = doc.books.add(RSDP)))\n        assert.deepStrictEqual(s2.books.byId(bookId), Object.assign({id: bookId}, RSDP))\n        assert.strictEqual(s2.books.byId(bookId).id, bookId)\n      })\n    })\n\n    describe('standard array operations on rows', () => {\n      it('returns a list of rows', () =>\n        assert.deepEqual(s1.books.rows, [ddiaWithId]))\n      it('supports `filter`', () =>\n        assert.deepStrictEqual(s1.books.filter(book => book.authors.length === 1), [ddiaWithId]))\n      it('supports `find`', () => {\n        assert.deepStrictEqual(s1.books.find(book => book.isbn === '1449373321'), ddiaWithId)})\n      it('supports `map`', () =>\n        assert.deepStrictEqual(s1.books.map<string>(book => book.title), [DDIA.title]))\n    })\n  })\n\n  describe('Automerge.Counter', () => {\n    interface CounterMap {\n      [name: string]: Counter\n    }\n\n    interface CounterList {\n      counts: Counter[]\n    }\n\n    interface BirdCounterMap {\n      birds: CounterMap\n    }\n\n    it('should handle counters inside maps', () => {\n      const doc1 = Automerge.change(Automerge.init<CounterMap>(), doc => {\n        doc.wrens = new Counter()\n      })\n      assert.equal(doc1.wrens, 0)\n\n      const doc2 = Automerge.change(doc1, doc => {\n        doc.wrens.increment()\n      })\n      assert.equal(doc2.wrens, 1)\n    })\n\n    it('should handle counters inside lists', () => {\n      const doc1 = Automerge.change(Automerge.init<CounterList>(), doc => {\n        doc.counts = [new Counter(1)]\n      })\n      assert.equal(doc1.counts[0], 1)\n\n      const doc2 = Automerge.change(doc1, doc => {\n        doc.counts[0].increment(2)\n      })\n      assert.equal(doc2.counts[0].value, 3)\n    })\n\n    describe('counter as numeric primitive', () => {\n      let doc1: CounterMap\n      beforeEach(() => {\n        doc1 = Automerge.change(Automerge.init<CounterMap>(), doc => {\n          doc.birds = new Counter(3)\n        })\n      })\n\n      it('is equal (==) but not strictly equal (===) to its numeric value', () => {\n        assert.equal(doc1.birds, 3)\n        assert.notStrictEqual(doc1.birds, 3)\n      })\n\n      it('has to be explicitly cast to be used as a number', () => {\n        let birdCount: number\n\n        // This is valid javascript, but without the `ts-ignore` flag, it fails to compile:\n        // @ts-ignore\n        birdCount = doc1.birds // Type 'Counter' is not assignable to type 'number'.ts(2322)\n\n        // This is because TypeScript doesn't know about the `.valueOf()` trick.\n        // https://github.com/Microsoft/TypeScript/issues/2361\n\n        // If we want to treat a counter value as a number, we have to explicitly cast it to keep\n        // TypeScript happy.\n\n        // We can cast by putting a `+` in front of it:\n        birdCount = +doc1.birds\n        assert.equal(birdCount < 4, true)\n        assert.equal(birdCount >= 0, true)\n\n        // Or we can be explicit (have to cast as unknown, then number):\n        birdCount = (doc1.birds as unknown) as number\n        assert.equal(birdCount <= 2, false)\n        assert.equal(birdCount + 10, 13)\n      })\n\n      it('is converted to a string using its numeric value', () => {\n        assert.equal(doc1.birds.toString(), '3')\n        assert.equal(`I saw ${doc1.birds} birds`, 'I saw 3 birds')\n        assert.equal(['I saw', doc1.birds, 'birds'].join(' '), 'I saw 3 birds')\n      })\n    })\n  })\n\n  describe('Automerge.Observable', () => {\n    interface TextDoc {\n      text: Automerge.Text\n    }\n\n    it('should call a patchCallback when a document changes', () => {\n      let callbackCalled = false, actor = ''\n      let doc = Automerge.init<TextDoc>({patchCallback: (patch, before, after, local, changes) => {\n        callbackCalled = true\n        assert.deepStrictEqual(patch.diffs.props.text[`1@${actor}`], {\n          objectId: `1@${actor}`, type: 'text', edits: [\n            {action: 'insert', index: 0, elemId: `2@${actor}`, opId: `2@${actor}`, value: {type: 'value', value: 'a'}}\n          ]\n        })\n        assert.deepStrictEqual(before, {})\n        assert.strictEqual(after.text.toString(), 'a')\n        assert.strictEqual(local, true)\n        assert.strictEqual(changes.length, 1)\n        assert.ok(changes[0] instanceof Uint8Array)\n      }})\n      actor = Automerge.getActorId(doc)\n      doc = Automerge.change(doc, doc => doc.text = new Automerge.Text('a'))\n      assert.strictEqual(callbackCalled, true)\n    })\n\n    it('should call an observer when a document changes', () => {\n      let observable = new Automerge.Observable(), callbackCalled = false\n      let doc = Automerge.from({text: new Automerge.Text()}, {observable})\n      let actor = Automerge.getActorId(doc)\n      observable.observe(doc.text, (diff, before, after, local, changes) => {\n        callbackCalled = true\n        if (diff.type == 'text') {\n          assert.deepStrictEqual(diff.edits, [\n            {action: 'insert', index: 0, elemId: `2@${actor}`, opId: `2@${actor}`, value: {type: 'value', value: 'a'}}\n          ])\n        }\n        assert.strictEqual(before.toString(), '')\n        assert.strictEqual(after.toString(), 'a')\n        assert.strictEqual(local, true)\n        assert.strictEqual(changes.length, 1)\n        assert.ok(changes[0] instanceof Uint8Array)\n      })\n      doc = Automerge.change(doc, doc => doc.text.insertAt(0, 'a'))\n      assert.strictEqual(callbackCalled, true)\n    })\n  })\n})\n"
  },
  {
    "path": "test/uuid_test.js",
    "content": "const assert = require('assert')\nconst Automerge = process.env.TEST_DIST === '1' ? require('../dist/automerge') : require('../src/automerge')\n\nconst uuid = Automerge.uuid\n\ndescribe('uuid', () => {\n  afterEach(() => {\n    uuid.reset()\n  })\n\n  describe('default implementation', () => {\n    it('generates unique values', () => {\n      assert.notEqual(uuid(), uuid())\n    })\n  })\n\n  describe('custom implementation', () => {\n    let counter\n\n    function customUuid() {\n      return `custom-uuid-${counter++}`\n    }\n\n    before(() => uuid.setFactory(customUuid))\n    beforeEach(() => counter = 0)\n\n    it('invokes the custom factory', () => {\n      assert.equal(uuid(), 'custom-uuid-0')\n      assert.equal(uuid(), 'custom-uuid-1')\n    })\n  })\n})\n"
  },
  {
    "path": "test/wasm.js",
    "content": "/* eslint-disable no-unused-vars */\n// This file is used for running the test suite against an alternative backend\n// implementation, such as the WebAssembly version compiled from Rust.\n// It needs to be loaded before the test suite files, which can be done with\n// `mocha --file test/wasm.js` (shortcut: `yarn testwasm`).\n// You need to set the environment variable WASM_BACKEND_PATH to the path where\n// the alternative backend module can be found; typically this is something\n// like `../automerge-rs/automerge-backend-wasm`.\n// Since this file relies on an environment variable and filesystem paths, it\n// currently only works in Node, not in a browser.\n\nif (!process.env.WASM_BACKEND_PATH) {\n  throw new RangeError('Please set environment variable WASM_BACKEND_PATH to the path of the WebAssembly backend')\n}\n\nconst assert = require('assert')\nconst Automerge = process.env.TEST_DIST === '1' ? require('../dist/automerge') : require('../src/automerge')\nconst jsBackend = require('../backend')\nconst Frontend = require('../frontend')\nconst { decodeChange } = require('../backend/columnar')\nconst uuid = require('../src/uuid')\n\nconst path = require('path')\nconst wasmBackend = require(path.resolve(process.env.WASM_BACKEND_PATH))\nAutomerge.setDefaultBackend(wasmBackend)\n\ndescribe('JavaScript-WebAssembly interoperability', () => {\n  describe('from JS to Wasm', () => {\n    interopTests(jsBackend, wasmBackend)\n  })\n\n  describe('from Wasm to JS', () => {\n    interopTests(wasmBackend, jsBackend)\n  })\n})\n\nfunction interopTests(sourceBackend, destBackend) {\n  let source, dest\n  beforeEach(() => {\n    source = sourceBackend.init()\n    dest = destBackend.init()\n  })\n\n  it('should set a key in a map', () => {\n    const actor = uuid()\n    const [source1, p1, change1] = sourceBackend.applyLocalChange(source, {\n      actor, seq: 1, time: 0, startOp: 1, deps: [], ops: [\n        {action: 'set', obj: '_root', key: 'bird', value: 'magpie', pred: []}\n      ]\n    })\n    const [dest1, patch] = destBackend.applyChanges(dest, [change1])\n    assert.deepStrictEqual(patch, {\n      clock: {[actor]: 1}, deps: [decodeChange(change1).hash], maxOp: 1, pendingChanges: 0,\n      diffs: {objectId: '_root', type: 'map', props: {\n        bird: {[`1@${actor}`]: {type: 'value', value: 'magpie'}}\n      }}\n    })\n  })\n\n  it('should delete a key from a map', () => {\n    const actor = uuid()\n    const [source1, p1, change1] = sourceBackend.applyLocalChange(source, {\n      actor, seq: 1, startOp: 1, time: 0, deps: [], ops: [\n        {action: 'set', obj: '_root', key: 'bird', value: 'magpie', pred: []}\n      ]\n    })\n    const [source2, p2, change2] = sourceBackend.applyLocalChange(source1, {\n      actor, seq: 2, startOp: 2, time: 0, deps: [], ops: [\n        {action: 'del', obj: '_root', key: 'bird', pred: [`1@${actor}`]}\n      ]\n    })\n    const [dest1, patch1] = destBackend.applyChanges(dest, [change1])\n    const [dest2, patch2] = destBackend.applyChanges(dest1, [change2])\n    assert.deepStrictEqual(patch2, {\n      clock: {[actor]: 2}, deps: [decodeChange(change2).hash], maxOp: 2, pendingChanges: 0,\n      diffs: {objectId: '_root', type: 'map', props: {bird: {}}}\n    })\n  })\n\n  it('should create nested maps', () => {\n    const actor = uuid()\n    const [source1, p1, change1] = sourceBackend.applyLocalChange(source, {\n      actor, seq: 1, startOp: 1, time: 0, deps: [], ops: [\n        {action: 'makeMap', obj: '_root', key: 'birds', pred: []},\n        {action: 'set', obj: `1@${actor}`, key: 'wrens', datatype: 'int', value: 3, pred: []}\n      ]\n    })\n    const [dest1, patch1] = destBackend.applyChanges(dest, [change1])\n    assert.deepStrictEqual(patch1, {\n      clock: {[actor]: 1}, deps: [decodeChange(change1).hash], maxOp: 2, pendingChanges: 0,\n      diffs: {objectId: '_root', type: 'map', props: {birds: {[`1@${actor}`]: {\n        objectId: `1@${actor}`, type: 'map', props: {wrens: {[`2@${actor}`]: {type: 'value', datatype: 'int', value: 3}}}\n      }}}}\n    })\n  })\n\n  it('should create lists', () => {\n    const actor = uuid()\n    const [source1, p1, change1] = sourceBackend.applyLocalChange(source, {\n      actor, seq: 1, startOp: 1, time: 0, deps: [], ops: [\n        {action: 'makeList', obj: '_root', key: 'birds', pred: []},\n        {action: 'set', obj: `1@${actor}`, elemId: '_head', insert: true, value: 'chaffinch', pred: []}\n      ]\n    })\n    const [dest1, patch1] = destBackend.applyChanges(dest, [change1])\n    assert.deepStrictEqual(patch1, {\n      clock: {[actor]: 1}, deps: [decodeChange(change1).hash], maxOp: 2, pendingChanges: 0,\n      diffs: {objectId: '_root', type: 'map', props: {birds: {[`1@${actor}`]: {\n        objectId: `1@${actor}`, type: 'list', edits: [\n          {action: 'insert', index: 0, elemId: `2@${actor}`, opId: `2@${actor}`,\n            value: {type: 'value', value: 'chaffinch'}}\n        ]\n      }}}}\n    })\n  })\n\n  it('should delete list elements', () => {\n    const actor = uuid()\n    const [source1, p1, change1] = sourceBackend.applyLocalChange(source, {\n      actor, seq: 1, startOp: 1, time: 0, deps: [], ops: [\n        {action: 'makeList', obj: '_root', key: 'birds', pred: []},\n        {action: 'set', obj: `1@${actor}`, elemId: '_head', insert: true, value: 'chaffinch', pred: []}\n      ]\n    })\n    const [source2, p2, change2] = sourceBackend.applyLocalChange(source1, {\n      actor, seq: 2, startOp: 3, time: 0, deps: [], ops: [\n        {action: 'del', obj: `1@${actor}`, elemId: `2@${actor}`, pred: [`2@${actor}`]}\n      ]\n    })\n    const [dest1, patch1] = destBackend.applyChanges(dest, [change1])\n    const [dest2, patch2] = destBackend.applyChanges(dest1, [change2])\n    assert.deepStrictEqual(patch2, {\n      clock: {[actor]: 2}, deps: [decodeChange(change2).hash], maxOp: 3, pendingChanges: 0,\n      diffs: {objectId: '_root', type: 'map', props: {birds: {[`1@${actor}`]: {\n        objectId: `1@${actor}`, type: 'list',\n        edits: [{action: 'remove', index: 0, count: 1}]\n      }}}}\n    })\n  })\n\n  it('should support Text objects', () => {\n    const actor = uuid()\n    const [source1, p1, change1] = sourceBackend.applyLocalChange(source, {\n      actor, seq: 1, startOp: 1, time: 0, deps: [], ops: [\n        {action: 'makeText', obj: '_root', key: 'text', pred: []},\n        {action: 'set', obj: `1@${actor}`, elemId: '_head',      insert: true, value: 'a', pred: []},\n        {action: 'set', obj: `1@${actor}`, elemId: `2@${actor}`, insert: true, value: 'b', pred: []},\n        {action: 'set', obj: `1@${actor}`, elemId: `3@${actor}`, insert: true, value: 'c', pred: []}\n      ]\n    })\n    const [dest1, patch1] = destBackend.applyChanges(dest, [change1])\n    assert.deepStrictEqual(patch1, {\n      clock: {[actor]: 1}, deps: [decodeChange(change1).hash], maxOp: 4, pendingChanges: 0,\n      diffs: {objectId: '_root', type: 'map', props: {text: {[`1@${actor}`]: {\n        objectId: `1@${actor}`, type: 'text', edits: [\n          {action: 'multi-insert', index: 0, elemId: `2@${actor}`, values: ['a', 'b', 'c']},\n        ],\n      }}}}\n    })\n  })\n\n  it('should support Table objects', () => {\n    const actor = uuid(), rowId = uuid()\n    const [source1, p1, change1] = sourceBackend.applyLocalChange(source, {\n      actor, seq: 1, startOp: 1, time: 0, deps: [], ops: [\n        {action: 'makeTable', obj: '_root',      key: 'birds',   insert: false, pred: []},\n        {action: 'makeMap',   obj: `1@${actor}`, key: rowId,     insert: false, pred: []},\n        {action: 'set',       obj: `2@${actor}`, key: 'species', insert: false, value: 'Chaffinch', pred: []},\n        {action: 'set',       obj: `2@${actor}`, key: 'colour',  insert: false, value: 'brown',     pred: []}\n      ]\n    })\n    const [dest1, patch1] = destBackend.applyChanges(dest, [change1])\n    assert.deepStrictEqual(patch1, {\n      clock: {[actor]: 1}, deps: [decodeChange(change1).hash], maxOp: 4, pendingChanges: 0,\n      diffs: {objectId: '_root', type: 'map', props: {birds: {[`1@${actor}`]: {\n        objectId: `1@${actor}`, type: 'table', props: {[rowId]: {[`2@${actor}`]: {\n          objectId: `2@${actor}`, type: 'map', props: {\n            species: {[`3@${actor}`]: {type: 'value', value: 'Chaffinch'}},\n            colour:  {[`4@${actor}`]: {type: 'value', value: 'brown'}}\n          }\n        }}}\n      }}}}\n    })\n  })\n\n  it('should support Counter objects', () => {\n    const actor = uuid()\n    const [source1, p1, change1] = sourceBackend.applyLocalChange(source, {\n      actor, seq: 1, startOp: 1, time: 0, deps: [], ops: [\n        {action: 'set', obj: '_root', key: 'counter', value: 1, datatype: 'counter', pred: []}\n      ]\n    })\n    const [source2, p2, change2] = sourceBackend.applyLocalChange(source1, {\n      actor, seq: 2, startOp: 2, time: 0, deps: [], ops: [\n        {action: 'inc', obj: '_root', key: 'counter', value: 2, pred: [`1@${actor}`]}\n      ]\n    })\n    const [dest1, patch1] = destBackend.applyChanges(dest, [change1])\n    const [dest2, patch2] = destBackend.applyChanges(dest1, [change2])\n    assert.deepStrictEqual(patch2, {\n      clock: {[actor]: 2}, deps: [decodeChange(change2).hash], maxOp: 2, pendingChanges: 0,\n      diffs: {objectId: '_root', type: 'map', props: {\n        counter: {[`1@${actor}`]: {type: 'value', value: 3, datatype: 'counter'}}\n      }}\n    })\n  })\n\n  it('should support Date objects', () => {\n    const actor = uuid(), now = new Date()\n    const [source1, p1, change1] = sourceBackend.applyLocalChange(source, {\n      actor, seq: 1, startOp: 1, time: 0, deps: [], ops: [\n        {action: 'set', obj: '_root', key: 'now', value: now.getTime(), datatype: 'timestamp', pred: []}\n      ]\n    })\n    const [dest1, patch1] = destBackend.applyChanges(dest, [change1])\n    assert.deepStrictEqual(patch1, {\n      clock: {[actor]: 1}, deps: [decodeChange(change1).hash], maxOp: 1, pendingChanges: 0,\n      diffs: {objectId: '_root', type: 'map', props: {\n        now: {[`1@${actor}`]: {type: 'value', value: now.getTime(), datatype: 'timestamp'}}\n      }}\n    })\n  })\n\n  it('should support DEFLATE-compressed changes', () => {\n    let longString = '', actor = uuid()\n    for (let i = 0; i < 1024; i++) longString += 'a'\n    const [source1, p1, change1] = sourceBackend.applyLocalChange(source, {\n      actor, seq: 1, time: 0, startOp: 1, deps: [], ops: [\n        {action: 'set', obj: '_root', key: 'longString', value: longString, pred: []}\n      ]\n    })\n    assert.ok(change1.byteLength < 100)\n    const [dest1, patch1] = destBackend.applyChanges(dest, [change1])\n    assert.deepStrictEqual(patch1, {\n      clock: {[actor]: 1}, deps: [decodeChange(change1).hash], maxOp: 1, pendingChanges: 0,\n      diffs: {objectId: '_root', type: 'map', props: {\n        longString: {[`1@${actor}`]: {type: 'value', value: longString}}\n      }}\n    })\n  })\n\n  describe('save() and load()', () => {\n    it('should work for a simple document', () => {\n      const actor = uuid()\n      const [source1, p1, change1] = sourceBackend.applyLocalChange(source, {\n        actor, seq: 1, time: 0, startOp: 1, deps: [], ops: [\n          {action: 'set', obj: '_root', key: 'bird', value: 'magpie', pred: []}\n        ]\n      })\n      const dest1 = destBackend.load(sourceBackend.save(source1))\n      const patch = destBackend.getPatch(dest1)\n      assert.deepStrictEqual(patch, {\n        clock: {[actor]: 1}, deps: [decodeChange(change1).hash], maxOp: 1, pendingChanges: 0,\n        diffs: {objectId: '_root', type: 'map', props: {\n          bird: {[`1@${actor}`]: {type: 'value', value: 'magpie'}}\n        }}\n      })\n    })\n\n    it('should allow DEFLATE-compressed columns', () => {\n      let longString = '', actor = uuid()\n      for (let i = 0; i < 1024; i++) longString += 'a'\n      const [source1, p1, change1] = sourceBackend.applyLocalChange(source, {\n        actor, seq: 1, time: 0, startOp: 1, deps: [], ops: [\n          {action: 'set', obj: '_root', key: 'longString', value: longString, pred: []}\n        ]\n      })\n      const compressedDoc = sourceBackend.save(source1)\n      assert.ok(compressedDoc.byteLength < 200)\n      const patch = destBackend.getPatch(destBackend.load(compressedDoc))\n      assert.deepStrictEqual(patch, {\n        clock: {[actor]: 1}, deps: [decodeChange(change1).hash], maxOp: 1, pendingChanges: 0,\n        diffs: {objectId: '_root', type: 'map', props: {\n          longString: {[`1@${actor}`]: {type: 'value', value: longString}}\n        }}\n      })\n    })\n\n    // TODO need more tests for save() and load()\n  })\n}\n"
  },
  {
    "path": "tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"allowJs\": false,\n    \"baseUrl\": \".\",\n    \"esModuleInterop\": true,\n    \"lib\": [\"dom\", \"esnext.asynciterable\", \"es2017\", \"es2016\", \"es2015\"],\n    \"module\": \"commonjs\",\n    \"moduleResolution\": \"node\",\n    \"paths\": { \"automerge\": [\"*\"]},\n    \"rootDir\": \"\",\n    \"target\": \"es2016\",\n    \"typeRoots\": [\"./@types\", \"./node_modules/@types\"]\n  },\n  \"exclude\": [\"dist/**/*\"]\n}\n"
  },
  {
    "path": "webpack.config.js",
    "content": "const path = require('path')\n\nmodule.exports = {\n  entry: './src/automerge.js',\n  mode: 'development',\n  output: {\n    filename: 'automerge.js',\n    library: 'Automerge',\n    libraryTarget: 'umd',\n    path: path.resolve(__dirname, 'dist'),\n    // https://github.com/webpack/webpack/issues/6525\n    globalObject: 'this',\n    // https://github.com/webpack/webpack/issues/11660\n    chunkLoading: false,\n  },\n  devtool: 'source-map',\n  module: {rules: []},\n  target: \"browserslist:web\"\n}\n"
  }
]