diff --git a/package-lock.json b/package-lock.json index caf395f..b42db86 100644 --- a/package-lock.json +++ b/package-lock.json @@ -940,6 +940,12 @@ "color-convert": "1.9.3" } }, + "ansicolors": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/ansicolors/-/ansicolors-0.3.2.tgz", + "integrity": "sha1-ZlWX3oap/+Oqm/vmyuXG6kJrSXk=", + "dev": true + }, "anymatch": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz", @@ -1279,6 +1285,16 @@ "integrity": "sha512-/F3t/Yo8LEdRSEPCmI15fLu5vepVh9UCg/9inJXF5AAfW7xRRJkbaM2ut52iRMQMnGCLQouLbFdbOA+VEFOIsg==", "dev": true }, + "cardinal": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/cardinal/-/cardinal-2.1.1.tgz", + "integrity": "sha1-fMEFXYItISlU0HsIXeolHMe8VQU=", + "dev": true, + "requires": { + "ansicolors": "0.3.2", + "redeyed": "2.1.1" + } + }, "chalk": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", @@ -1355,6 +1371,25 @@ "restore-cursor": "2.0.0" } }, + "cli-table": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/cli-table/-/cli-table-0.3.1.tgz", + "integrity": "sha1-9TsFJmqLGguTSz0IIebi3FkUriM=", + "dev": true, + "requires": { + "colors": "1.0.3" + } + }, + "cli-usage": { + "version": "0.1.9", + "resolved": "https://registry.npmjs.org/cli-usage/-/cli-usage-0.1.9.tgz", + "integrity": "sha512-MIJJnLu89KTRoGN1ix9dwvKYUPUP7tUL+YGKNH/7mFmy8n3aWNznQKK8FU7PsFVQxePW5rxBp0lupzeSjRiXTA==", + "dev": true, + "requires": { + "marked": "0.6.2", + "marked-terminal": "3.2.0" + } + }, "cli-width": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.2.0.tgz", @@ -1386,6 +1421,12 @@ "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", "dev": true }, + "colors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.0.3.tgz", + "integrity": "sha1-BDP0TYCWgP3rYO0mDxsMJi6CpAs=", + "dev": true + }, "commander": { "version": "2.20.0", "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.0.tgz", @@ -2094,6 +2135,15 @@ "integrity": "sha1-LUH1Y+H+QA7Uli/hpNXGp1Od9/Y=", "dev": true }, + "exec-sh": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/exec-sh/-/exec-sh-0.2.2.tgz", + "integrity": "sha512-FIUCJz1RbuS0FKTdaAafAByGS0CPvU3R0MeHxgtl+djzCc//F8HakL8GzmVNZanasTbTAY/3DRFA0KpVqj/eAw==", + "dev": true, + "requires": { + "merge": "1.2.1" + } + }, "execa": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", @@ -2176,6 +2226,17 @@ "dev": true, "requires": { "is-plain-object": "2.0.4" + }, + "dependencies": { + "is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dev": true, + "requires": { + "isobject": "3.0.1" + } + } } } } @@ -3015,6 +3076,12 @@ "integrity": "sha512-6uHUhOPEBgQ24HM+r6b/QwWfZq+yiFcipKFrOFiBEnWdy5sdzYoi+pJeQaPI5qOLRFqWmAXUPQNsielzdLoecA==", "dev": true }, + "growly": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/growly/-/growly-1.3.0.tgz", + "integrity": "sha1-8QdIy+dq+WS3yWyTxrzCivEgwIE=", + "dev": true + }, "has": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", @@ -3425,15 +3492,6 @@ "integrity": "sha1-caUMhCnfync8kqOQpKA7OfzVHT4=", "dev": true }, - "is-plain-object": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", - "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", - "dev": true, - "requires": { - "isobject": "3.0.1" - } - }, "is-promise": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.1.0.tgz", @@ -3612,6 +3670,105 @@ "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==", "dev": true }, + "lodash._arraycopy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/lodash._arraycopy/-/lodash._arraycopy-3.0.0.tgz", + "integrity": "sha1-due3wfH7klRzdIeKVi7Qaj5Q9uE=", + "dev": true + }, + "lodash._arrayeach": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/lodash._arrayeach/-/lodash._arrayeach-3.0.0.tgz", + "integrity": "sha1-urFWsqkNPxu9XGU0AzSeXlkz754=", + "dev": true + }, + "lodash._baseassign": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/lodash._baseassign/-/lodash._baseassign-3.2.0.tgz", + "integrity": "sha1-jDigmVAPIVrQnlnxci/QxSv+Ck4=", + "dev": true, + "requires": { + "lodash._basecopy": "3.0.1", + "lodash.keys": "3.1.2" + } + }, + "lodash._baseclone": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lodash._baseclone/-/lodash._baseclone-3.3.0.tgz", + "integrity": "sha1-MDUZv2OT/n5C802LYw73eU41Qrc=", + "dev": true, + "requires": { + "lodash._arraycopy": "3.0.0", + "lodash._arrayeach": "3.0.0", + "lodash._baseassign": "3.2.0", + "lodash._basefor": "3.0.3", + "lodash.isarray": "3.0.4", + "lodash.keys": "3.1.2" + } + }, + "lodash._basecopy": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/lodash._basecopy/-/lodash._basecopy-3.0.1.tgz", + "integrity": "sha1-jaDmqHbPNEwK2KVIghEd08XHyjY=", + "dev": true + }, + "lodash._basefor": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash._basefor/-/lodash._basefor-3.0.3.tgz", + "integrity": "sha1-dVC06SGO8J+tJDQ7YSAhx5tMIMI=", + "dev": true + }, + "lodash._bindcallback": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/lodash._bindcallback/-/lodash._bindcallback-3.0.1.tgz", + "integrity": "sha1-5THCdkTPi1epnhftlbNcdIeJOS4=", + "dev": true + }, + "lodash._getnative": { + "version": "3.9.1", + "resolved": "https://registry.npmjs.org/lodash._getnative/-/lodash._getnative-3.9.1.tgz", + "integrity": "sha1-VwvH3t5G1hzc3mh9ZdPuy6o6r/U=", + "dev": true + }, + "lodash.clonedeep": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-3.0.2.tgz", + "integrity": "sha1-oKHkDYKl6on/WxR7hETtY9koJ9s=", + "dev": true, + "requires": { + "lodash._baseclone": "3.3.0", + "lodash._bindcallback": "3.0.1" + } + }, + "lodash.isarguments": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", + "integrity": "sha1-L1c9hcaiQon/AGY7SRwdM4/zRYo=", + "dev": true + }, + "lodash.isarray": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/lodash.isarray/-/lodash.isarray-3.0.4.tgz", + "integrity": "sha1-eeTriMNqgSKvhvhEqpvNhRtfu1U=", + "dev": true + }, + "lodash.keys": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/lodash.keys/-/lodash.keys-3.1.2.tgz", + "integrity": "sha1-TbwEcrFWvlCgsoaFXRvQsMZWCYo=", + "dev": true, + "requires": { + "lodash._getnative": "3.9.1", + "lodash.isarguments": "3.1.0", + "lodash.isarray": "3.0.4" + } + }, + "lodash.toarray": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.toarray/-/lodash.toarray-4.4.0.tgz", + "integrity": "sha1-JMS/zWsvuji/0FlNsRedjptlZWE=", + "dev": true + }, "loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -3660,6 +3817,32 @@ "object-visit": "1.0.1" } }, + "marked": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/marked/-/marked-0.6.2.tgz", + "integrity": "sha512-LqxwVH3P/rqKX4EKGz7+c2G9r98WeM/SW34ybhgNGhUQNKtf1GmmSkJ6cDGJ/t6tiyae49qRkpyTw2B9HOrgUA==", + "dev": true + }, + "marked-terminal": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/marked-terminal/-/marked-terminal-3.2.0.tgz", + "integrity": "sha512-Yr1yVS0BbDG55vx7be1D0mdv+jGs9AW563o/Tt/7FTsId2J0yqhrTeXAqq/Q0DyyXltIn6CSxzesQuFqXgafjQ==", + "dev": true, + "requires": { + "ansi-escapes": "3.2.0", + "cardinal": "2.1.1", + "chalk": "2.4.2", + "cli-table": "0.3.1", + "node-emoji": "1.10.0", + "supports-hyperlinks": "1.0.1" + } + }, + "merge": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/merge/-/merge-1.2.1.tgz", + "integrity": "sha512-VjFo4P5Whtj4vsLzsYBu5ayHhoHJ0UqNm7ibvShmbmoz7tGi0vXaoJbGdB+GmDMLUdg8DpQXEIeVDAe8MaABvQ==", + "dev": true + }, "micromatch": { "version": "3.1.10", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", @@ -3728,6 +3911,17 @@ "dev": true, "requires": { "is-plain-object": "2.0.4" + }, + "dependencies": { + "is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dev": true, + "requires": { + "isobject": "3.0.1" + } + } } } } @@ -3749,6 +3943,11 @@ } } }, + "mout": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/mout/-/mout-1.1.0.tgz", + "integrity": "sha512-XsP0vf4As6BfqglxZqbqQ8SR6KQot2AgxvR0gG+WtUkf90vUXchMOZQtPf/Hml1rEffJupqL/tIrU6EYhsUQjw==" + }, "mri": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/mri/-/mri-1.1.4.tgz", @@ -3817,12 +4016,36 @@ "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", "dev": true }, + "node-emoji": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-1.10.0.tgz", + "integrity": "sha512-Yt3384If5H6BYGVHiHwTL+99OzJKHhgp82S8/dktEK73T26BazdgZ4JZh92xSVtGNJvz9UbXdNAc5hcrXV42vw==", + "dev": true, + "requires": { + "lodash.toarray": "4.4.0" + } + }, "node-modules-regexp": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/node-modules-regexp/-/node-modules-regexp-1.0.0.tgz", "integrity": "sha1-jZ2+KJZKSsVxLpExZCEHxx6Q7EA=", "dev": true }, + "node-notifier": { + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/node-notifier/-/node-notifier-4.6.1.tgz", + "integrity": "sha1-BW0UJE89zBzq3+aK+c/wxUc6M/M=", + "dev": true, + "requires": { + "cli-usage": "0.1.9", + "growly": "1.3.0", + "lodash.clonedeep": "3.0.2", + "minimist": "1.2.0", + "semver": "5.7.0", + "shellwords": "0.1.1", + "which": "1.3.1" + } + }, "node-releases": { "version": "1.1.22", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.22.tgz", @@ -4398,14 +4621,25 @@ "dev": true }, "react": { - "version": "16.8.6", - "resolved": "https://registry.npmjs.org/react/-/react-16.8.6.tgz", - "integrity": "sha512-pC0uMkhLaHm11ZSJULfOBqV4tIZkx87ZLvbbQYunNixAAvjnC+snJCg0XQXn9VIsttVsbZP/H/ewzgsd5fxKXw==", + "version": "16.9.0-alpha.0", + "resolved": "https://registry.npmjs.org/react/-/react-16.9.0-alpha.0.tgz", + "integrity": "sha512-y4bu7rJvtnPPsIwOj7sp5Y2SqlOb0jFupfkdjWxxn8ZeqzUARgpR9wJBUVwW1/QosVdOblmApjo/j6iiAXnebA==", "requires": { "loose-envify": "1.4.0", "object-assign": "4.1.1", "prop-types": "15.7.2", - "scheduler": "0.13.6" + "scheduler": "0.14.0" + }, + "dependencies": { + "scheduler": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.14.0.tgz", + "integrity": "sha512-9CgbS06Kki2f4R9FjLSITjZo5BZxPsryiRNyL3LpvrM9WxcVmhlqAOc9E+KQbeI2nqej4JIIbOsfdL51cNb4Iw==", + "requires": { + "loose-envify": "1.4.0", + "object-assign": "4.1.1" + } + } } }, "react-is": { @@ -4414,15 +4648,33 @@ "integrity": "sha512-aUk3bHfZ2bRSVFFbbeVS4i+lNPZr3/WM5jT2J5omUVV1zzcs1nAaf3l51ctA5FFvCRbhrH0bdAsRRQddFJZPtA==" }, "react-test-renderer": { - "version": "16.8.6", - "resolved": "https://registry.npmjs.org/react-test-renderer/-/react-test-renderer-16.8.6.tgz", - "integrity": "sha512-H2srzU5IWYT6cZXof6AhUcx/wEyJddQ8l7cLM/F7gDXYyPr4oq+vCIxJYXVGhId1J706sqziAjuOEjyNkfgoEw==", + "version": "16.9.0-alpha.0", + "resolved": "https://registry.npmjs.org/react-test-renderer/-/react-test-renderer-16.9.0-alpha.0.tgz", + "integrity": "sha512-eDl0oVFo6PGY1wpYFs0ezBpZhOgVce5TSta9UPLanshTi4z8NhlM6IgO8KBdioQ5H5/pmyGxOVtpUxJOt19NAQ==", "dev": true, "requires": { "object-assign": "4.1.1", "prop-types": "15.7.2", - "react-is": "16.8.6", - "scheduler": "0.13.6" + "react-is": "16.9.0-alpha.0", + "scheduler": "0.14.0" + }, + "dependencies": { + "react-is": { + "version": "16.9.0-alpha.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.9.0-alpha.0.tgz", + "integrity": "sha512-psl0ePLTFliYfwcbwvimLgTNN156ZdeWB4zvP7dV/6lTAqWMHFfidg/mSZ2fFgE1LMNN8ZJOLl2DfZ8yg+3ETA==", + "dev": true + }, + "scheduler": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.14.0.tgz", + "integrity": "sha512-9CgbS06Kki2f4R9FjLSITjZo5BZxPsryiRNyL3LpvrM9WxcVmhlqAOc9E+KQbeI2nqej4JIIbOsfdL51cNb4Iw==", + "dev": true, + "requires": { + "loose-envify": "1.4.0", + "object-assign": "4.1.1" + } + } } }, "read-pkg": { @@ -4530,6 +4782,15 @@ "readable-stream": "2.3.6" } }, + "redeyed": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/redeyed/-/redeyed-2.1.1.tgz", + "integrity": "sha1-iYS1gV2ZyyIEacme7v/jiRPmzAs=", + "dev": true, + "requires": { + "esprima": "4.0.1" + } + }, "regenerate": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.0.tgz", @@ -4732,15 +4993,6 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "dev": true }, - "scheduler": { - "version": "0.13.6", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.13.6.tgz", - "integrity": "sha512-IWnObHt413ucAYKsD9J1QShUKkbKLQQHdxRyw73sw4FN26iWr3DY/H34xGPe4nmL1DwXyWmSWmMrA9TfQbE/XQ==", - "requires": { - "loose-envify": "1.4.0", - "object-assign": "4.1.1" - } - }, "semver": { "version": "5.7.0", "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.0.tgz", @@ -4773,6 +5025,15 @@ "requires": { "is-extendable": "0.1.1" } + }, + "is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dev": true, + "requires": { + "isobject": "3.0.1" + } } } }, @@ -4804,6 +5065,12 @@ "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", "dev": true }, + "shellwords": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/shellwords/-/shellwords-0.1.1.tgz", + "integrity": "sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww==", + "dev": true + }, "signal-exit": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", @@ -5126,6 +5393,24 @@ "has-flag": "3.0.0" } }, + "supports-hyperlinks": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-1.0.1.tgz", + "integrity": "sha512-HHi5kVSefKaJkGYXbDuKbUGRVxqnWGn3J2e39CYcNJEfWciGq2zYtOhXLTlvrOZW1QU7VX67w7fMmWafHX9Pfw==", + "dev": true, + "requires": { + "has-flag": "2.0.0", + "supports-color": "5.5.0" + }, + "dependencies": { + "has-flag": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-2.0.0.tgz", + "integrity": "sha1-6CB68cx7MNRGzHC3NLXovhj4jVE=", + "dev": true + } + } + }, "table": { "version": "5.4.0", "resolved": "https://registry.npmjs.org/table/-/table-5.4.0.tgz", @@ -5233,6 +5518,17 @@ } } }, + "tap-notify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/tap-notify/-/tap-notify-1.0.0.tgz", + "integrity": "sha1-2VZKV9xVv8VY0/hkActH8B77cBg=", + "dev": true, + "requires": { + "node-notifier": "4.6.1", + "tap-parser": "1.3.2", + "through2": "2.0.5" + } + }, "tap-parser": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/tap-parser/-/tap-parser-1.3.2.tgz", @@ -5451,6 +5747,17 @@ "is-extendable": "0.1.1", "is-plain-object": "2.0.4", "to-object-path": "0.3.0" + }, + "dependencies": { + "is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dev": true, + "requires": { + "isobject": "3.0.1" + } + } } } } @@ -5539,6 +5846,16 @@ "spdx-expression-parse": "3.0.0" } }, + "watch": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/watch/-/watch-1.0.2.tgz", + "integrity": "sha1-NApxe952Vyb6CqB9ch4BR6VR3ww=", + "dev": true, + "requires": { + "exec-sh": "0.2.2", + "minimist": "1.2.0" + } + }, "which": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", diff --git a/package.json b/package.json index b681ae5..7b55cca 100644 --- a/package.json +++ b/package.json @@ -1,12 +1,14 @@ { "name": "@retrium/react-sharedb", - "version": "0.1.0-alpha.3", + "version": "0.1.0-alpha.5", "description": "", "main": "./lib/index.js", "scripts": { "prebuild": "rimraf lib", "build": "babel ./src --ignore '**/*.test.js' --out-dir ./lib", - "test": "tape -r @babel/register -r tape-catch './src/**.test.js' | tap-diff", + "test": "tape -r @babel/register -r tape-catch './src/**/*.test.js*' | tap-notify | tap-diff", + "test:watch": "watch 'clear && npm run test' ./src", + "test:debug": "node --inspect-brk -r @babel/register -r tape-catch ./node_modules/.bin/tape './src/**/*.test.js*'", "prepublish": "npm run build", "lint": "eslint src", "pretty": "pretty-quick --staged" @@ -40,14 +42,17 @@ "husky": "^2.3.0", "prettier": "^1.17.1", "pretty-quick": "^1.11.0", - "react-test-renderer": "^16.8.6", + "react-test-renderer": "^16.9.0-alpha.0", "rimraf": "^2.6.3", "tap-diff": "^0.1.1", + "tap-notify": "^1.0.0", "tape": "^4.10.2", - "tape-catch": "^1.0.6" + "tape-catch": "^1.0.6", + "watch": "^1.0.2" }, "dependencies": { - "react": "^16.8.6", + "mout": "^1.1.0", + "react": "^16.9.0-alpha.0", "sharedb": "^1.0.0-beta.23" } } diff --git a/src/useSharedState.js b/src/useSharedState.js index f4a854b..88d7b5f 100644 --- a/src/useSharedState.js +++ b/src/useSharedState.js @@ -1,7 +1,10 @@ // @flow -import { useContext, useEffect, useState, useCallback } from 'react'; - +import { useContext, useEffect, useState, useCallback, useRef } from 'react'; +import { isPlainObject, deepClone } from 'mout/lang'; +import { equals as arrayShallowEquals } from 'mout/array'; +import { equals as objectShallowEquals } from 'mout/object'; +import { identity } from 'mout/function' import { ShareContext } from './SharedStateProvider'; /** questions @@ -10,8 +13,10 @@ import { ShareContext } from './SharedStateProvider'; export function useSharedState( collection: string, - doc_id: string -): [any, (mixed) => Promise] { + doc_id: string, + projector?: any => any, + deps?: Array +): [any, ((any) => any | any) => Promise] { const connection = useContext(ShareContext); if (!connection) { @@ -30,19 +35,41 @@ export function useSharedState( throw maybe_doc._error; default: { // 'resolved' + const has_projector = !!projector; + const should_clone = true; + + const memo_projector = useCallback(projector || identity, deps); + + const [{ state }, setState] = useState({ + state: has_projector + ? should_clone + ? deepClone(memo_projector(maybe_doc.data)) + : memo_projector(maybe_doc.data) + : maybe_doc.data, + }); + + const state_ref = useRef(state); - const [{ state }, setState] = useState({ state: maybe_doc.data }); const dispatch = useCallback( - action => { - // todo: allow action to be a function that takes unprojected/unmapped state - // and returns an action + (action: any => any | any): Promise => { // todo: add validation that prevents submitting ops when the doc has been destroyed - - return new Promise(resolve => { - maybe_doc.submitOp(action, () => { - // todo: figure out how errors are to be handled - resolve(); - }); + return new Promise((resolve, reject) => { + try { + maybe_doc.submitOp( + typeof action === 'function' + ? action(maybe_doc.data) + : action, + err => { + if (err) { + reject(err); + } else { + resolve(); + } + } + ); + } catch (err) { + reject(err); + } }); }, [maybe_doc] @@ -50,10 +77,81 @@ export function useSharedState( useEffect(() => { const handle_op = () => { - setState({ state: maybe_doc.data }); + if (!has_projector) { + state_ref.current = maybe_doc.data; + setState({ state: maybe_doc.data }); + return; + } + + const projected_state = memo_projector(maybe_doc.data); + const { current: current_projected_state } = state_ref; + + if (typeof projected_state !== typeof current_projected_state) { + // rerender + const state = should_clone + ? deepClone(projected_state) + : projected_state; + + state_ref.current = state; + setState({ state }); + return; + } + + if ( + Array.isArray(projected_state) && + Array.isArray(current_projected_state) + ) { + // if lengths are not the same then rerender + if ( + !arrayShallowEquals(projected_state, current_projected_state) + ) { + const state = should_clone + ? deepClone(projected_state) + : projected_state; + + state_ref.current = state; + setState({ state }); + return; + } + + return; + } + + if ( + isPlainObject(projected_state) && + isPlainObject(current_projected_state) + ) { + // if they have a different amount of keys, rerender: + if ( + !objectShallowEquals(projected_state, current_projected_state) + ) { + const state = should_clone + ? deepClone(projected_state) + : projected_state; + + state_ref.current = state; + setState({ state }); + return; + } + + return; + } + + // if we're here it's likely a number, bool, string, etc. so do an equality check + if (projected_state !== current_projected_state) { + // rerender + setState({ state: projected_state }); + state_ref.current = projected_state; + } }; maybe_doc.addListener('op', handle_op); + return () => { + maybe_doc.removeListener('op', handle_op); + }; + }, [should_clone, maybe_doc, has_projector, memo_projector]); + + useEffect(() => { // increment the subscription ref count to indicate that this component is now subscribed to the doc maybe_doc._subscription_ref_count++; @@ -62,7 +160,6 @@ export function useSharedState( // return; // } // todo: delay destroying the doc from ram for a given timeout. - maybe_doc.removeListener('op', handle_op); // decrement the subscription ref count to indicate that this component is no longer subscribed to the doc maybe_doc._subscription_ref_count--; diff --git a/src/useSharedState.projector.test.js b/src/useSharedState.projector.test.js new file mode 100644 index 0000000..97c574f --- /dev/null +++ b/src/useSharedState.projector.test.js @@ -0,0 +1,816 @@ +// @flow +import test from 'tape'; +import sharedb from 'sharedb'; +import React, { Suspense, useRef, useState } from 'react'; +import { useSharedState } from './useSharedState'; +import TestRenderer, { act } from 'react-test-renderer'; +import { SharedStateProvider } from './SharedStateProvider'; + +function sleep(ms) { + return new Promise(resolve => setTimeout(resolve, ms)); +} + +function Loading() { + return
; +} + +function Mock() { + return
; +} + +test('react-sharedb: useSharedState projector correctly filters the state', async assert => { + // Create a client connection to an in memory sharedb database to simulate + // a frontend connection to sharedb running on a server. + const server = new sharedb.Backend(); + const local = server.connect(); + const remote = server.connect(); + + const collection = 'collection'; + const doc_id = 'id:5456563'; + const doc_state = { + doc_id: doc_id, + count1: 0, + }; + + // create the doc via the secondary connection + const remote_doc = remote.get(collection, doc_id); + remote_doc.subscribe(); + remote_doc.create(doc_state); + + const MockWrapper = () => { + const [count1, submit] = useSharedState( + collection, + doc_id, + ({ count1 }) => count1 + ); + + return ; + }; + + let renderer = {}; + + act(() => { + // initial render of the react tree + renderer = TestRenderer.create( + + }> + + + + ); + }); + + const { root, unmount } = renderer; + + // allow the sharedb server to flush updates + await act(() => sleep(0)); + + { + //const remote_doc = remote.get(collection, doc_id); + const [mock_instance] = root.findAllByType(Mock); + + assert.equals( + mock_instance.props.count1, + 0, + "The 's count1 prop is 0." + ); + + act(() => { + mock_instance.props.onSubmit({ + p: ['count1'], + oi: 1, + }); + }); + } + + // allow the sharedb server to flush updates + await act(() => sleep(0)); + + { + const [mock_instance] = root.findAllByType(Mock); + + assert.equals( + mock_instance.props.count1, + 1, + "The 's count prop is 1," + ); + + assert.equals( + remote_doc.data.count1, + 1, + "And the remote doc's count is also 1" + ); + } + + remote_doc.unsubscribe(); + remote_doc.destroy(); + + unmount(); + assert.end(); +}); + +test('react-sharedb: useSharedState updating projected array tuple', async assert => { + // Create a client connection to an in memory sharedb database to simulate + // a frontend connection to sharedb running on a server. + const server = new sharedb.Backend(); + const local = server.connect(); + const remote = server.connect(); + + const collection = 'collection'; + const doc_id = 'id:5456563'; + const doc_state = { + doc_id: doc_id, + prop1: 0, + prop2: '', + prop3: '', + }; + + // create the doc via the secondary connection + const remote_doc = remote.get(collection, doc_id); + remote_doc.subscribe(); + remote_doc.create(doc_state); + + const MockWrapper = () => { + const [items, submit] = useSharedState( + collection, + doc_id, + ({ prop1, prop2 }) => [prop1, prop2] + ); + + const render_count_ref = useRef(0); + + render_count_ref.current++; + + return ( + render_count_ref.current} + onSubmit={submit} + /> + ); + }; + + let renderer = {}; + + act(() => { + // initial render of the react tree + renderer = TestRenderer.create( + + }> + + + + ); + }); + + const { root, unmount } = renderer; + + // allow the sharedb server to flush updates + await act(() => sleep(0)); + + { + //const remote_doc = remote.get(collection, doc_id); + const [mock_instance] = root.findAllByType(Mock); + + assert.equals( + mock_instance.props.getRenderCount(), + 1, + 'Rendered exactly 1 time' + ); + + // submit an operation to change a prop + act(() => { + mock_instance.props.onSubmit({ + p: ['prop1'], + oi: 1, + }); + }); + } + + await act(() => sleep(0)); + + { + //const remote_doc = remote.get(collection, doc_id); + const [mock_instance] = root.findAllByType(Mock); + + assert.equals( + mock_instance.props.getRenderCount(), + 2, + 'Rendered exactly 2 times' + ); + + assert.equals( + mock_instance.props.items[0], + 1, + 'Updated the value correctly' + ); + + act(() => { + mock_instance.props.onSubmit({ + p: ['prop3'], + oi: 1, + }); + }); + } + + await act(() => sleep(0)); + + { + //const remote_doc = remote.get(collection, doc_id); + const [mock_instance] = root.findAllByType(Mock); + + assert.equals( + mock_instance.props.getRenderCount(), + 2, + 'Rendered exactly 2 times after updating non-projected value.' + ); + } + + remote_doc.unsubscribe(); + remote_doc.destroy(); + + unmount(); + assert.end(); +}); + +test('react-sharedb: useSharedState updating directly projected array in doc state', async assert => { + // Create a client connection to an in memory sharedb database to simulate + // a frontend connection to sharedb running on a server. + const server = new sharedb.Backend(); + const local = server.connect(); + const remote = server.connect(); + + const collection = 'collection'; + const doc_id = 'id:5456563'; + const doc_state = { + doc_id: doc_id, + list: [], + prop1: '', + }; + + // create the doc via the secondary connection + const remote_doc = remote.get(collection, doc_id); + remote_doc.subscribe(); + remote_doc.create(doc_state); + + const MockWrapper = () => { + const [list, submit] = useSharedState( + collection, + doc_id, + ({ list }) => list + ); + + const render_count_ref = useRef(0); + + render_count_ref.current++; + + return ( + render_count_ref.current} + onSubmit={submit} + /> + ); + }; + + let renderer = {}; + + act(() => { + // initial render of the react tree + renderer = TestRenderer.create( + + }> + + + + ); + }); + + const { root, unmount } = renderer; + + // allow the sharedb server to flush updates + await act(() => sleep(0)); + + { + //const remote_doc = remote.get(collection, doc_id); + const [mock_instance] = root.findAllByType(Mock); + + assert.equals( + mock_instance.props.getRenderCount(), + 1, + 'Rendered exactly 1 time' + ); + + // submit an operation to add item to list + act(() => { + mock_instance.props.onSubmit({ + p: ['list', 0], + li: 0, + }); + }); + } + + // allow the sharedb server to flush updates + await act(() => sleep(0)); + + { + const [mock_instance] = root.findAllByType(Mock); + + assert.equals( + mock_instance.props.getRenderCount(), + 2, + 'Rendered exactly 2 times' + ); + + assert.equals( + mock_instance.props.list[0], + 0, + 'Adding an item to the list.' + ); + + // submit an operation to add item to list + act(() => { + mock_instance.props.onSubmit({ + p: ['list', 1], + li: 1, + }); + }); + } + + // allow the sharedb server to flush updates + await act(() => sleep(0)); + + { + const [mock_instance] = root.findAllByType(Mock); + + // assert.comment(JSON.stringify(remote_doc.data.list)); + // assert.comment(JSON.stringify(mock_instance.props)); + + assert.equals( + mock_instance.props.getRenderCount(), + 3, + 'Rendered exactly 3 times' + ); + + assert.equals( + mock_instance.props.list[1], + 1, + 'Adding a second item to the list.' + ); + + // submit an operation to add item to list + act(() => { + mock_instance.props.onSubmit({ + p: ['list', 1], + ld: 1, + }); + }); + } + + { + const [mock_instance] = root.findAllByType(Mock); + + assert.equals( + mock_instance.props.getRenderCount(), + 4, + 'Rendered exactly 4 times' + ); + + assert.equals( + mock_instance.props.list[1], + undefined, + 'Removing an item from the list.' + ); + + act(() => { + mock_instance.props.onSubmit({ + p: ['prop1'], + oi: 1, + }); + }); + } + + await act(() => sleep(0)); + + { + //const remote_doc = remote.get(collection, doc_id); + const [mock_instance] = root.findAllByType(Mock); + + assert.equals( + mock_instance.props.getRenderCount(), + 4, + 'Rendered exactly 4 times after updating non-projected value.' + ); + } + + remote_doc.unsubscribe(); + remote_doc.destroy(); + + unmount(); + assert.end(); +}); + +test('react-sharedb: useSharedState updating projected plain object tuple', async assert => { + // Create a client connection to an in memory sharedb database to simulate + // a frontend connection to sharedb running on a server. + const server = new sharedb.Backend(); + const local = server.connect(); + const remote = server.connect(); + + const collection = 'collection'; + const doc_id = 'id:5456563'; + const doc_state = { + doc_id: doc_id, + prop1: 0, + prop2: '', + prop3: '', + }; + + // create the doc via the secondary connection + const remote_doc = remote.get(collection, doc_id); + remote_doc.subscribe(); + remote_doc.create(doc_state); + + const MockWrapper = () => { + const [{ prop1, prop2 }, submit] = useSharedState( + collection, + doc_id, + ({ prop1, prop2 }) => ({ prop1, prop2 }) + ); + + const render_count_ref = useRef(0); + + render_count_ref.current++; + + return ( + render_count_ref.current} + onSubmit={submit} + /> + ); + }; + + let renderer = {}; + + act(() => { + // initial render of the react tree + renderer = TestRenderer.create( + + }> + + + + ); + }); + + const { root, unmount } = renderer; + + // allow the sharedb server to flush updates + await act(() => sleep(0)); + + { + //const remote_doc = remote.get(collection, doc_id); + const [mock_instance] = root.findAllByType(Mock); + + assert.equals( + mock_instance.props.getRenderCount(), + 1, + 'Rendered exactly 1 time' + ); + + // submit an operation to change a prop + act(() => { + mock_instance.props.onSubmit({ + p: ['prop1'], + oi: 1, + }); + }); + } + + await act(() => sleep(0)); + + { + //const remote_doc = remote.get(collection, doc_id); + const [mock_instance] = root.findAllByType(Mock); + + assert.equals( + mock_instance.props.getRenderCount(), + 2, + 'Rendered exactly 2 time' + ); + + assert.equals( + mock_instance.props.prop1, + 1, + 'The property was properly updated' + ); + + act(() => { + mock_instance.props.onSubmit({ + p: ['prop3'], + oi: 1, + }); + }); + } + + await act(() => sleep(0)); + + { + //const remote_doc = remote.get(collection, doc_id); + const [mock_instance] = root.findAllByType(Mock); + + assert.equals( + mock_instance.props.getRenderCount(), + 2, + 'Rendered exactly 2 times after updating non-projected value' + ); + } + + remote_doc.unsubscribe(); + remote_doc.destroy(); + + unmount(); + assert.end(); +}); + +test('react-sharedb: useSharedState updating directly projected object in doc state', async assert => { + // Create a client connection to an in memory sharedb database to simulate + // a frontend connection to sharedb running on a server. + const server = new sharedb.Backend(); + const local = server.connect(); + const remote = server.connect(); + + const collection = 'collection'; + const doc_id = 'id:5456563'; + const doc_state = { + doc_id: doc_id, + thing: { + prop1: 'one', + prop2: 0, + prop3: '', + }, + }; + + // create the doc via the secondary connection + const remote_doc = remote.get(collection, doc_id); + remote_doc.subscribe(); + remote_doc.create(doc_state); + + const MockWrapper = () => { + const [{ prop1, prop2 }, submit] = useSharedState( + collection, + doc_id, + ({ thing }) => thing + ); + + const render_count_ref = useRef(0); + + render_count_ref.current++; + + return ( + render_count_ref.current} + onSubmit={submit} + /> + ); + }; + + let renderer = {}; + + act(() => { + // initial render of the react tree + renderer = TestRenderer.create( + + }> + + + + ); + }); + + const { root, unmount } = renderer; + + // allow the sharedb server to flush updates + await act(() => sleep(0)); + + { + const [mock_instance] = root.findAllByType(Mock); + + assert.equals( + mock_instance.props.getRenderCount(), + 1, + 'Rendered exactly 1 time' + ); + + // submit an operation to change a prop + act(() => { + mock_instance.props.onSubmit({ + p: ['thing', 'prop1'], + oi: 'two', + }); + }); + } + + await act(() => sleep(0)); + + { + const [mock_instance] = root.findAllByType(Mock); + + assert.equals(mock_instance.props.prop1, 'two', 'The prop was updated.'); + + assert.equals( + mock_instance.props.getRenderCount(), + 2, + 'Rendered exactly 2 times' + ); + + // submit op but don't change prop - make sure there's no re-render + act(() => { + mock_instance.props.onSubmit({ + p: ['thing', 'prop2'], + oi: 0, + }); + }); + } + + await act(() => sleep(0)); + + { + const [mock_instance] = root.findAllByType(Mock); + + assert.equals( + mock_instance.props.getRenderCount(), + 2, + 'Rendered exactly 2 times after updating doc but not changing value' + ); + } + + remote_doc.unsubscribe(); + remote_doc.destroy(); + + unmount(); + assert.end(); +}); + +test('react-sharedb: passing a function in to dispatch', async assert => { + const server = new sharedb.Backend(); + const local = server.connect(); + const remote = server.connect(); + + const collection = 'collection'; + const doc_id = 'id:5456563'; + const doc_state = { + doc_id: doc_id, + count1: 0, + count2: 2, + }; + + // create the doc via the secondary connection + const remote_doc = remote.get(collection, doc_id); + remote_doc.subscribe(); + remote_doc.create(doc_state); + + let renderer = {}; + + const MockWrapper = () => { + const [count1, submit] = useSharedState( + collection, + doc_id, + ({ count1 }) => count1 + ); + + return ; + }; + + act(() => { + // initial render of the react tree + renderer = TestRenderer.create( + + }> + + + + ); + }); + + const { root, unmount } = renderer; + + // allow the sharedb server to flush updates + await act(() => sleep(0)); + + { + const [mock_instance] = root.findAllByType(Mock); + + act(() => { + mock_instance.props.onSubmit(state => { + return { + p: ['count1'], + oi: state.count2, + }; + }); + }); + } + + // allow the sharedb server to flush updates + await act(() => sleep(0)); + + { + const [mock_instance] = root.findAllByType(Mock); + + assert.equals( + mock_instance.props.count1, + 2, + "The 's count1 prop is now equal to its count2 prop." + ); + } + + remote_doc.unsubscribe(); + remote_doc.destroy(); + + unmount(); + assert.end(); +}); + +test('react-sharedb: passing in deps', async assert => { + const server = new sharedb.Backend(); + const local = server.connect(); + const remote = server.connect(); + + const collection = 'collection'; + const doc_id = 'id:5456563'; + const doc_state = { + doc_id: doc_id, + count1: 0, + }; + + // create the doc via the secondary connection + const remote_doc = remote.get(collection, doc_id); + remote_doc.subscribe(); + remote_doc.create(doc_state); + + let renderer = {}; + + const MockWrapper = () => { + const [num, setNum] = useState(0); + const [projection, onSubmit] = useSharedState( + collection, + doc_id, + () => num, + [] + ); + + return ; + }; + + act(() => { + // initial render of the react tree + renderer = TestRenderer.create( + + }> + + + + ); + }); + + const { root, unmount } = renderer; + + // allow the sharedb server to flush updates + await act(() => sleep(0)); + + { + const [mock_instance] = root.findAllByType(Mock); + + assert.equals(mock_instance.props.projection, 0, 'Projected value is 0'); + + act(() => { + mock_instance.props.setNum(num => num + 1); + }); + + act(() => { + mock_instance.props.onSubmit({ + p: ['count1'], + oi: 1, + }); + }); + } + + // allow the sharedb server to flush updates + await act(() => sleep(0)); + + { + const [mock_instance] = root.findAllByType(Mock); + + assert.equals(mock_instance.props.projection, 0, 'Projected value is still 0'); + } + + remote_doc.unsubscribe(); + remote_doc.destroy(); + + unmount(); + assert.end(); +}); diff --git a/src/useSharedState.test.js b/src/useSharedState.test.js index 527585c..4ccdd3f 100644 --- a/src/useSharedState.test.js +++ b/src/useSharedState.test.js @@ -5,9 +5,9 @@ import sharedb from 'sharedb'; import React, { Suspense } from 'react'; import type { Node } from 'react'; -import TestRenderer from 'react-test-renderer'; +import TestRenderer, { act } from 'react-test-renderer'; -import { SharedState, SharedStateProvider } from './index'; +import { SharedState, SharedStateProvider, useSharedState } from './index'; function sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); @@ -288,19 +288,25 @@ test("react-sharedb: useSharedReducer causes a re-render when the doc's state ch const remote_doc = remote.get(collection, doc_id); remote_doc.create(doc_state); + let renderer = {}; + + act(() => { + renderer = TestRenderer.create( + + }> + + {(state, submit) => } + + + + ); + }); + // initial render of the react tree - const { root, unmount } = TestRenderer.create( - - }> - - {(state, submit) => } - - - - ); + const { root, unmount } = renderer; // allow the sharedb server to flush updates - await sleep(0); + await act(() => sleep(0)); { const [mock_instance] = root.findAllByType(Mock); @@ -313,13 +319,15 @@ test("react-sharedb: useSharedReducer causes a re-render when the doc's state ch } // submit an operation to increment the count property by one - remote_doc.submitOp({ - p: ['count'], - na: 1, + act(() => { + remote_doc.submitOp({ + p: ['count'], + na: 1, + }); }); // allow the sharedb server to flush updates - await sleep(0); + await act(() => sleep(0)); { const [mock_instance] = root.findAllByType(Mock); @@ -331,6 +339,8 @@ test("react-sharedb: useSharedReducer causes a re-render when the doc's state ch ); } + remote_doc.unsubscribe(); + remote_doc.destroy(); unmount(); assert.end(); }); @@ -354,19 +364,25 @@ test('react-sharedb: when submitting operations via the submit function returned remote_doc.subscribe(); remote_doc.create(doc_state); + let renderer = {}; + + act(() => { + renderer = TestRenderer.create( + + }> + + {(state, submit) => } + + + + ); + }); + // initial render of the react tree - const { root, unmount } = TestRenderer.create( - - }> - - {(state, submit) => } - - - - ); + const { root, unmount } = renderer; // allow the sharedb server to flush updates - await sleep(0); + await act(() => sleep(0)); { //const remote_doc = remote.get(collection, doc_id); @@ -385,14 +401,16 @@ test('react-sharedb: when submitting operations via the submit function returned ); // submit an operation to increment the count property by one - mock_instance.props.onSubmit({ - p: ['count'], - na: 1, + act(() => { + mock_instance.props.onSubmit({ + p: ['count'], + na: 1, + }); }); } // allow the sharedb server to flush updates - await sleep(0); + await act(() => sleep(0)); { const [mock_instance] = root.findAllByType(Mock);