From 30d4c9ccc20b638a964e05facb2e8c0113fcc726 Mon Sep 17 00:00:00 2001 From: Andrew Bulat Date: Tue, 5 Nov 2024 04:04:14 +0000 Subject: [PATCH 1/5] Add `deep-equal` package Also configure LiveObjects plugin default bundle to not include `deep-equal` package in the output. --- grunt/esbuild/build.js | 1 + package-lock.json | 144 +++++++++-------------------------------- package.json | 2 + 3 files changed, 33 insertions(+), 114 deletions(-) diff --git a/grunt/esbuild/build.js b/grunt/esbuild/build.js index 728d48b79..915f7f795 100644 --- a/grunt/esbuild/build.js +++ b/grunt/esbuild/build.js @@ -82,6 +82,7 @@ const liveObjectsPluginConfig = { entryPoints: ['src/plugins/liveobjects/index.ts'], plugins: [umdWrapper.default({ libraryName: 'AblyLiveObjectsPlugin', amdNamedModule: false })], outfile: 'build/liveobjects.js', + external: ['deep-equal'], }; const liveObjectsPluginCdnConfig = { diff --git a/package-lock.json b/package-lock.json index 63325af50..e6fa48ce9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "license": "Apache-2.0", "dependencies": { "@ably/msgpack-js": "^0.4.0", + "deep-equal": "^2.2.3", "fastestsmallesttextencoderdecoder": "^1.0.22", "got": "^11.8.5", "ulid": "^2.3.0", @@ -23,6 +24,7 @@ "@babel/traverse": "^7.23.7", "@testing-library/react": "^13.3.0", "@types/cli-table": "^0.3.4", + "@types/deep-equal": "^1.0.4", "@types/jmespath": "^0.15.2", "@types/node": "^18.0.0", "@types/request": "^2.48.7", @@ -1557,6 +1559,12 @@ "integrity": "sha512-GsALrTL69mlwbAw/MHF1IPTadSLZQnsxe7a80G8l4inN/iEXCOcVeT/S7aRc6hbhqzL9qZ314kHPDQnQ3ev+HA==", "dev": true }, + "node_modules/@types/deep-equal": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@types/deep-equal/-/deep-equal-1.0.4.tgz", + "integrity": "sha512-tqdiS4otQP4KmY0PR3u6KbZ5EWvhNdUoS/jc93UuK23C220lOZ/9TvjfxdPcKvqwwDVtmtSCrnr0p/2dirAxkA==", + "dev": true + }, "node_modules/@types/eslint": { "version": "8.56.2", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.2.tgz", @@ -2320,7 +2328,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.0.tgz", "integrity": "sha512-LPuwb2P+NrQw3XhxGc36+XSvuBPopovXYTR9Ew++Du9Yb/bx5AzBfrIsBoj0EZUifjQU+sHL21sseZ3jerWO/A==", - "dev": true, "dependencies": { "call-bind": "^1.0.2", "is-array-buffer": "^3.0.1" @@ -2513,7 +2520,6 @@ "version": "1.0.5", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz", "integrity": "sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==", - "dev": true, "engines": { "node": ">= 0.4" }, @@ -2749,7 +2755,6 @@ "version": "1.0.5", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz", "integrity": "sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==", - "dev": true, "dependencies": { "function-bind": "^1.1.2", "get-intrinsic": "^1.2.1", @@ -3288,7 +3293,6 @@ "version": "2.2.3", "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-2.2.3.tgz", "integrity": "sha512-ZIwpnevOurS8bpT4192sqAowWM76JDKSHYzMLty3BZGSswgq6pBaH3DhCSW5xVAZICZyKdOBPjwww5wfgT/6PA==", - "dev": true, "dependencies": { "array-buffer-byte-length": "^1.0.0", "call-bind": "^1.0.5", @@ -3319,8 +3323,7 @@ "node_modules/deep-equal/node_modules/isarray": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", - "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", - "dev": true + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==" }, "node_modules/deep-for-each": { "version": "3.0.0", @@ -3349,7 +3352,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.1.tgz", "integrity": "sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==", - "dev": true, "dependencies": { "get-intrinsic": "^1.2.1", "gopd": "^1.0.1", @@ -3363,7 +3365,6 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", - "dev": true, "dependencies": { "define-data-property": "^1.0.1", "has-property-descriptors": "^1.0.0", @@ -3663,7 +3664,6 @@ "version": "1.1.3", "resolved": "https://registry.npmjs.org/es-get-iterator/-/es-get-iterator-1.1.3.tgz", "integrity": "sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw==", - "dev": true, "dependencies": { "call-bind": "^1.0.2", "get-intrinsic": "^1.1.3", @@ -3682,8 +3682,7 @@ "node_modules/es-get-iterator/node_modules/isarray": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", - "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", - "dev": true + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==" }, "node_modules/es-iterator-helpers": { "version": "1.0.15", @@ -5013,7 +5012,6 @@ "version": "0.3.3", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", - "dev": true, "dependencies": { "is-callable": "^1.1.3" } @@ -5111,7 +5109,6 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "dev": true, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -5144,7 +5141,6 @@ "version": "1.2.3", "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", - "dev": true, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -5180,7 +5176,6 @@ "version": "1.2.2", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.2.tgz", "integrity": "sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA==", - "dev": true, "dependencies": { "function-bind": "^1.1.2", "has-proto": "^1.0.1", @@ -5385,7 +5380,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", - "dev": true, "dependencies": { "get-intrinsic": "^1.1.3" }, @@ -5792,7 +5786,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==", - "dev": true, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -5810,7 +5803,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.1.tgz", "integrity": "sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==", - "dev": true, "dependencies": { "get-intrinsic": "^1.2.2" }, @@ -5822,7 +5814,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==", - "dev": true, "engines": { "node": ">= 0.4" }, @@ -5834,7 +5825,6 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", - "dev": true, "engines": { "node": ">= 0.4" }, @@ -5846,7 +5836,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", - "dev": true, "dependencies": { "has-symbols": "^1.0.2" }, @@ -5861,7 +5850,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz", "integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==", - "dev": true, "dependencies": { "function-bind": "^1.1.2" }, @@ -6077,7 +6065,6 @@ "version": "1.0.6", "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.6.tgz", "integrity": "sha512-Xj6dv+PsbtwyPpEflsejS+oIZxmMlV44zAhG479uYu89MsjcYOhCFnNyKrkJrihbsiasQyY0afoCl/9BLR65bg==", - "dev": true, "dependencies": { "get-intrinsic": "^1.2.2", "hasown": "^2.0.0", @@ -6122,7 +6109,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz", "integrity": "sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==", - "dev": true, "dependencies": { "call-bind": "^1.0.2", "has-tostringtag": "^1.0.0" @@ -6138,7 +6124,6 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.2.tgz", "integrity": "sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w==", - "dev": true, "dependencies": { "call-bind": "^1.0.2", "get-intrinsic": "^1.2.0", @@ -6167,7 +6152,6 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==", - "dev": true, "dependencies": { "has-bigints": "^1.0.1" }, @@ -6191,7 +6175,6 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", - "dev": true, "dependencies": { "call-bind": "^1.0.2", "has-tostringtag": "^1.0.0" @@ -6213,7 +6196,6 @@ "version": "1.2.7", "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", - "dev": true, "engines": { "node": ">= 0.4" }, @@ -6237,7 +6219,6 @@ "version": "1.0.5", "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", - "dev": true, "dependencies": { "has-tostringtag": "^1.0.0" }, @@ -6324,7 +6305,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.2.tgz", "integrity": "sha512-cOZFQQozTha1f4MxLFzlgKYPTyj26picdZTx82hbc/Xf4K/tZOOXSCkMvU4pKioRXGDLJRn0GM7Upe7kR721yg==", - "dev": true, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -6354,7 +6334,6 @@ "version": "1.0.7", "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz", "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==", - "dev": true, "dependencies": { "has-tostringtag": "^1.0.0" }, @@ -6396,7 +6375,6 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", - "dev": true, "dependencies": { "call-bind": "^1.0.2", "has-tostringtag": "^1.0.0" @@ -6424,7 +6402,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.2.tgz", "integrity": "sha512-+2cnTEZeY5z/iXGbLhPrOAaK/Mau5k5eXq9j14CpRTftq0pAJu2MwVRSZhyZWBzx3o6X795Lz6Bpb6R0GKf37g==", - "dev": true, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -6433,7 +6410,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz", "integrity": "sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==", - "dev": true, "dependencies": { "call-bind": "^1.0.2" }, @@ -6445,7 +6421,6 @@ "version": "1.0.7", "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", - "dev": true, "dependencies": { "has-tostringtag": "^1.0.0" }, @@ -6460,7 +6435,6 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", - "dev": true, "dependencies": { "has-symbols": "^1.0.2" }, @@ -6475,7 +6449,6 @@ "version": "1.1.12", "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.12.tgz", "integrity": "sha512-Z14TF2JNG8Lss5/HMqt0//T9JeHXttXy5pH/DBU4vi98ozO2btxzq9MwYDZYnKwU8nRsz/+GVFVRDq3DkVuSPg==", - "dev": true, "dependencies": { "which-typed-array": "^1.1.11" }, @@ -6502,7 +6475,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.1.tgz", "integrity": "sha512-NSBR4kH5oVj1Uwvv970ruUkCV7O1mzgVFO4/rev2cLRda9Tm9HrL70ZPut4rOHgY0FNrUu9BCbXA2sdQ+x0chA==", - "dev": true, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -6523,7 +6495,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.2.tgz", "integrity": "sha512-t2yVvttHkQktwnNNmBQ98AhENLdPUTDTE21uPqAQ0ARwQfGeQKRVS0NNurH7bTf7RrvcVn1OOge45CnBeHCSmg==", - "dev": true, "dependencies": { "call-bind": "^1.0.2", "get-intrinsic": "^1.1.1" @@ -7590,7 +7561,6 @@ "version": "1.13.1", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==", - "dev": true, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -7599,7 +7569,6 @@ "version": "1.1.5", "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.5.tgz", "integrity": "sha512-3cyDsyHgtmi7I7DfSSI2LDp6SK2lwvtbg0p0R1e0RvTqF5ceGx+K2dfSjm1bKDMVCFEDAQvy+o8c6a7VujOddw==", - "dev": true, "dependencies": { "call-bind": "^1.0.2", "define-properties": "^1.1.3" @@ -7615,7 +7584,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", - "dev": true, "engines": { "node": ">= 0.4" } @@ -7624,7 +7592,6 @@ "version": "4.1.5", "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.5.tgz", "integrity": "sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ==", - "dev": true, "dependencies": { "call-bind": "^1.0.5", "define-properties": "^1.2.1", @@ -8565,7 +8532,6 @@ "version": "1.5.1", "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.1.tgz", "integrity": "sha512-sy6TXMN+hnP/wMy+ISxg3krXx7BAtWVO4UouuCN/ziM9UEne0euamVNafDfvC83bRNr95y0V5iijeDQFUNpvrg==", - "dev": true, "dependencies": { "call-bind": "^1.0.2", "define-properties": "^1.2.0", @@ -8995,7 +8961,6 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.0.tgz", "integrity": "sha512-4DBHDoyHlM1IRPGYcoxexgh67y4ueR53FKV1yyxwFMY7aCqcN/38M1+SwZ/qJQ8iLv7+ck385ot4CcisOAPT9w==", - "dev": true, "dependencies": { "define-data-property": "^1.1.1", "function-bind": "^1.1.2", @@ -9011,7 +8976,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.1.tgz", "integrity": "sha512-tMNCiqYVkXIZgc2Hnoy2IvC/f8ezc5koaRFkCjrpWzGpCd3qbZXPzVy9MAZzK1ch/X0jvSkojys3oqJN0qCmdA==", - "dev": true, "dependencies": { "define-data-property": "^1.0.1", "functions-have-names": "^1.2.3", @@ -9113,7 +9077,6 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", - "dev": true, "dependencies": { "call-bind": "^1.0.0", "get-intrinsic": "^1.0.2", @@ -9304,7 +9267,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.0.0.tgz", "integrity": "sha512-iCGQj+0l0HOdZ2AEeBADlsRC+vsnDsZsbdSiH1yNSjcfKM7fdpCMfqAL/dwF5BLiw/XhRft/Wax6zQbhq2BcjQ==", - "dev": true, "dependencies": { "internal-slot": "^1.0.4" }, @@ -10865,7 +10827,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", - "dev": true, "dependencies": { "is-bigint": "^1.0.1", "is-boolean-object": "^1.1.0", @@ -10913,7 +10874,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.1.tgz", "integrity": "sha512-W8xeTUwaln8i3K/cY1nGXzdnVZlidBcagyNFtBdD5kxnb4TvGKR7FfSIS3mYpwWS1QUCutfKz8IY8RjftB0+1A==", - "dev": true, "dependencies": { "is-map": "^2.0.1", "is-set": "^2.0.1", @@ -10928,7 +10888,6 @@ "version": "1.1.13", "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.13.tgz", "integrity": "sha512-P5Nra0qjSncduVPEAr7xhoF5guty49ArDTwzJ/yNuPIbZppyRxFQsRCWrocxIY+CnMVG+qfbU2FmDKyvSGClow==", - "dev": true, "dependencies": { "available-typed-arrays": "^1.0.5", "call-bind": "^1.0.4", @@ -12171,6 +12130,12 @@ "integrity": "sha512-GsALrTL69mlwbAw/MHF1IPTadSLZQnsxe7a80G8l4inN/iEXCOcVeT/S7aRc6hbhqzL9qZ314kHPDQnQ3ev+HA==", "dev": true }, + "@types/deep-equal": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@types/deep-equal/-/deep-equal-1.0.4.tgz", + "integrity": "sha512-tqdiS4otQP4KmY0PR3u6KbZ5EWvhNdUoS/jc93UuK23C220lOZ/9TvjfxdPcKvqwwDVtmtSCrnr0p/2dirAxkA==", + "dev": true + }, "@types/eslint": { "version": "8.56.2", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.2.tgz", @@ -12770,7 +12735,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.0.tgz", "integrity": "sha512-LPuwb2P+NrQw3XhxGc36+XSvuBPopovXYTR9Ew++Du9Yb/bx5AzBfrIsBoj0EZUifjQU+sHL21sseZ3jerWO/A==", - "dev": true, "requires": { "call-bind": "^1.0.2", "is-array-buffer": "^3.0.1" @@ -12913,8 +12877,7 @@ "available-typed-arrays": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz", - "integrity": "sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==", - "dev": true + "integrity": "sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==" }, "aws-sdk": { "version": "2.1539.0", @@ -13095,7 +13058,6 @@ "version": "1.0.5", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz", "integrity": "sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==", - "dev": true, "requires": { "function-bind": "^1.1.2", "get-intrinsic": "^1.2.1", @@ -13494,7 +13456,6 @@ "version": "2.2.3", "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-2.2.3.tgz", "integrity": "sha512-ZIwpnevOurS8bpT4192sqAowWM76JDKSHYzMLty3BZGSswgq6pBaH3DhCSW5xVAZICZyKdOBPjwww5wfgT/6PA==", - "dev": true, "requires": { "array-buffer-byte-length": "^1.0.0", "call-bind": "^1.0.5", @@ -13519,8 +13480,7 @@ "isarray": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", - "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", - "dev": true + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==" } } }, @@ -13548,7 +13508,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.1.tgz", "integrity": "sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==", - "dev": true, "requires": { "get-intrinsic": "^1.2.1", "gopd": "^1.0.1", @@ -13559,7 +13518,6 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", - "dev": true, "requires": { "define-data-property": "^1.0.1", "has-property-descriptors": "^1.0.0", @@ -13790,7 +13748,6 @@ "version": "1.1.3", "resolved": "https://registry.npmjs.org/es-get-iterator/-/es-get-iterator-1.1.3.tgz", "integrity": "sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw==", - "dev": true, "requires": { "call-bind": "^1.0.2", "get-intrinsic": "^1.1.3", @@ -13806,8 +13763,7 @@ "isarray": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", - "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", - "dev": true + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==" } } }, @@ -14765,7 +14721,6 @@ "version": "0.3.3", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", - "dev": true, "requires": { "is-callable": "^1.1.3" } @@ -14834,8 +14789,7 @@ "function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "dev": true + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==" }, "function.prototype.name": { "version": "1.1.6", @@ -14858,8 +14812,7 @@ "functions-have-names": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", - "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", - "dev": true + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==" }, "gensync": { "version": "1.0.0-beta.2", @@ -14883,7 +14836,6 @@ "version": "1.2.2", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.2.tgz", "integrity": "sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA==", - "dev": true, "requires": { "function-bind": "^1.1.2", "has-proto": "^1.0.1", @@ -15032,7 +14984,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", - "dev": true, "requires": { "get-intrinsic": "^1.1.3" } @@ -15346,8 +15297,7 @@ "has-bigints": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", - "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==", - "dev": true + "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==" }, "has-flag": { "version": "4.0.0", @@ -15359,7 +15309,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.1.tgz", "integrity": "sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==", - "dev": true, "requires": { "get-intrinsic": "^1.2.2" } @@ -15367,20 +15316,17 @@ "has-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", - "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==", - "dev": true + "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==" }, "has-symbols": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", - "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", - "dev": true + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==" }, "has-tostringtag": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", - "dev": true, "requires": { "has-symbols": "^1.0.2" } @@ -15389,7 +15335,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz", "integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==", - "dev": true, "requires": { "function-bind": "^1.1.2" } @@ -15551,7 +15496,6 @@ "version": "1.0.6", "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.6.tgz", "integrity": "sha512-Xj6dv+PsbtwyPpEflsejS+oIZxmMlV44zAhG479uYu89MsjcYOhCFnNyKrkJrihbsiasQyY0afoCl/9BLR65bg==", - "dev": true, "requires": { "get-intrinsic": "^1.2.2", "hasown": "^2.0.0", @@ -15584,7 +15528,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz", "integrity": "sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==", - "dev": true, "requires": { "call-bind": "^1.0.2", "has-tostringtag": "^1.0.0" @@ -15594,7 +15537,6 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.2.tgz", "integrity": "sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w==", - "dev": true, "requires": { "call-bind": "^1.0.2", "get-intrinsic": "^1.2.0", @@ -15614,7 +15556,6 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==", - "dev": true, "requires": { "has-bigints": "^1.0.1" } @@ -15632,7 +15573,6 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", - "dev": true, "requires": { "call-bind": "^1.0.2", "has-tostringtag": "^1.0.0" @@ -15647,8 +15587,7 @@ "is-callable": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", - "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", - "dev": true + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==" }, "is-core-module": { "version": "2.13.1", @@ -15663,7 +15602,6 @@ "version": "1.0.5", "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", - "dev": true, "requires": { "has-tostringtag": "^1.0.0" } @@ -15716,8 +15654,7 @@ "is-map": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.2.tgz", - "integrity": "sha512-cOZFQQozTha1f4MxLFzlgKYPTyj26picdZTx82hbc/Xf4K/tZOOXSCkMvU4pKioRXGDLJRn0GM7Upe7kR721yg==", - "dev": true + "integrity": "sha512-cOZFQQozTha1f4MxLFzlgKYPTyj26picdZTx82hbc/Xf4K/tZOOXSCkMvU4pKioRXGDLJRn0GM7Upe7kR721yg==" }, "is-negative-zero": { "version": "2.0.2", @@ -15735,7 +15672,6 @@ "version": "1.0.7", "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz", "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==", - "dev": true, "requires": { "has-tostringtag": "^1.0.0" } @@ -15765,7 +15701,6 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", - "dev": true, "requires": { "call-bind": "^1.0.2", "has-tostringtag": "^1.0.0" @@ -15783,14 +15718,12 @@ "is-set": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.2.tgz", - "integrity": "sha512-+2cnTEZeY5z/iXGbLhPrOAaK/Mau5k5eXq9j14CpRTftq0pAJu2MwVRSZhyZWBzx3o6X795Lz6Bpb6R0GKf37g==", - "dev": true + "integrity": "sha512-+2cnTEZeY5z/iXGbLhPrOAaK/Mau5k5eXq9j14CpRTftq0pAJu2MwVRSZhyZWBzx3o6X795Lz6Bpb6R0GKf37g==" }, "is-shared-array-buffer": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz", "integrity": "sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==", - "dev": true, "requires": { "call-bind": "^1.0.2" } @@ -15799,7 +15732,6 @@ "version": "1.0.7", "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", - "dev": true, "requires": { "has-tostringtag": "^1.0.0" } @@ -15808,7 +15740,6 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", - "dev": true, "requires": { "has-symbols": "^1.0.2" } @@ -15817,7 +15748,6 @@ "version": "1.1.12", "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.12.tgz", "integrity": "sha512-Z14TF2JNG8Lss5/HMqt0//T9JeHXttXy5pH/DBU4vi98ozO2btxzq9MwYDZYnKwU8nRsz/+GVFVRDq3DkVuSPg==", - "dev": true, "requires": { "which-typed-array": "^1.1.11" } @@ -15834,8 +15764,7 @@ "is-weakmap": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.1.tgz", - "integrity": "sha512-NSBR4kH5oVj1Uwvv970ruUkCV7O1mzgVFO4/rev2cLRda9Tm9HrL70ZPut4rOHgY0FNrUu9BCbXA2sdQ+x0chA==", - "dev": true + "integrity": "sha512-NSBR4kH5oVj1Uwvv970ruUkCV7O1mzgVFO4/rev2cLRda9Tm9HrL70ZPut4rOHgY0FNrUu9BCbXA2sdQ+x0chA==" }, "is-weakref": { "version": "1.0.2", @@ -15850,7 +15779,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.2.tgz", "integrity": "sha512-t2yVvttHkQktwnNNmBQ98AhENLdPUTDTE21uPqAQ0ARwQfGeQKRVS0NNurH7bTf7RrvcVn1OOge45CnBeHCSmg==", - "dev": true, "requires": { "call-bind": "^1.0.2", "get-intrinsic": "^1.1.1" @@ -16657,14 +16585,12 @@ "object-inspect": { "version": "1.13.1", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", - "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==", - "dev": true + "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==" }, "object-is": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.5.tgz", "integrity": "sha512-3cyDsyHgtmi7I7DfSSI2LDp6SK2lwvtbg0p0R1e0RvTqF5ceGx+K2dfSjm1bKDMVCFEDAQvy+o8c6a7VujOddw==", - "dev": true, "requires": { "call-bind": "^1.0.2", "define-properties": "^1.1.3" @@ -16673,14 +16599,12 @@ "object-keys": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", - "dev": true + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==" }, "object.assign": { "version": "4.1.5", "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.5.tgz", "integrity": "sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ==", - "dev": true, "requires": { "call-bind": "^1.0.5", "define-properties": "^1.2.1", @@ -17349,7 +17273,6 @@ "version": "1.5.1", "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.1.tgz", "integrity": "sha512-sy6TXMN+hnP/wMy+ISxg3krXx7BAtWVO4UouuCN/ziM9UEne0euamVNafDfvC83bRNr95y0V5iijeDQFUNpvrg==", - "dev": true, "requires": { "call-bind": "^1.0.2", "define-properties": "^1.2.0", @@ -17665,7 +17588,6 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.0.tgz", "integrity": "sha512-4DBHDoyHlM1IRPGYcoxexgh67y4ueR53FKV1yyxwFMY7aCqcN/38M1+SwZ/qJQ8iLv7+ck385ot4CcisOAPT9w==", - "dev": true, "requires": { "define-data-property": "^1.1.1", "function-bind": "^1.1.2", @@ -17678,7 +17600,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.1.tgz", "integrity": "sha512-tMNCiqYVkXIZgc2Hnoy2IvC/f8ezc5koaRFkCjrpWzGpCd3qbZXPzVy9MAZzK1ch/X0jvSkojys3oqJN0qCmdA==", - "dev": true, "requires": { "define-data-property": "^1.0.1", "functions-have-names": "^1.2.3", @@ -17758,7 +17679,6 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", - "dev": true, "requires": { "call-bind": "^1.0.0", "get-intrinsic": "^1.0.2", @@ -17907,7 +17827,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.0.0.tgz", "integrity": "sha512-iCGQj+0l0HOdZ2AEeBADlsRC+vsnDsZsbdSiH1yNSjcfKM7fdpCMfqAL/dwF5BLiw/XhRft/Wax6zQbhq2BcjQ==", - "dev": true, "requires": { "internal-slot": "^1.0.4" } @@ -18978,7 +18897,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", - "dev": true, "requires": { "is-bigint": "^1.0.1", "is-boolean-object": "^1.1.0", @@ -19019,7 +18937,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.1.tgz", "integrity": "sha512-W8xeTUwaln8i3K/cY1nGXzdnVZlidBcagyNFtBdD5kxnb4TvGKR7FfSIS3mYpwWS1QUCutfKz8IY8RjftB0+1A==", - "dev": true, "requires": { "is-map": "^2.0.1", "is-set": "^2.0.1", @@ -19031,7 +18948,6 @@ "version": "1.1.13", "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.13.tgz", "integrity": "sha512-P5Nra0qjSncduVPEAr7xhoF5guty49ArDTwzJ/yNuPIbZppyRxFQsRCWrocxIY+CnMVG+qfbU2FmDKyvSGClow==", - "dev": true, "requires": { "available-typed-arrays": "^1.0.5", "call-bind": "^1.0.4", diff --git a/package.json b/package.json index 3cad5645e..b8f9ae029 100644 --- a/package.json +++ b/package.json @@ -47,6 +47,7 @@ ], "dependencies": { "@ably/msgpack-js": "^0.4.0", + "deep-equal": "^2.2.3", "fastestsmallesttextencoderdecoder": "^1.0.22", "got": "^11.8.5", "ulid": "^2.3.0", @@ -72,6 +73,7 @@ "@babel/traverse": "^7.23.7", "@testing-library/react": "^13.3.0", "@types/cli-table": "^0.3.4", + "@types/deep-equal": "^1.0.4", "@types/jmespath": "^0.15.2", "@types/node": "^18.0.0", "@types/request": "^2.48.7", From b40dc9f7a8dfe393723bc8f77cc01522fbc1903d Mon Sep 17 00:00:00 2001 From: Andrew Bulat Date: Tue, 5 Nov 2024 04:25:35 +0000 Subject: [PATCH 2/5] Implement direct object subscription for Live Objects - subscription callback is invoked when the live object data is updated via incoming state operation - subscription callback is invoked when the live object data is updated via a sync sequence (once sync sequence is applied to all objects) - update object is passed to a callback function that describes a granular update made to the live object Resolves DTP-958 --- src/plugins/liveobjects/livecounter.ts | 32 ++++-- src/plugins/liveobjects/livemap.ts | 116 ++++++++++++++++++--- src/plugins/liveobjects/liveobject.ts | 75 ++++++++++++- src/plugins/liveobjects/liveobjects.ts | 11 +- src/plugins/liveobjects/liveobjectspool.ts | 2 - 5 files changed, 203 insertions(+), 33 deletions(-) diff --git a/src/plugins/liveobjects/livecounter.ts b/src/plugins/liveobjects/livecounter.ts index 61d17d9a2..5ec413434 100644 --- a/src/plugins/liveobjects/livecounter.ts +++ b/src/plugins/liveobjects/livecounter.ts @@ -1,4 +1,4 @@ -import { LiveObject, LiveObjectData } from './liveobject'; +import { LiveObject, LiveObjectData, LiveObjectUpdate, LiveObjectUpdateNoop } from './liveobject'; import { LiveObjects } from './liveobjects'; import { StateCounter, StateCounterOp, StateMessage, StateOperation, StateOperationAction } from './statemessage'; import { Timeserial } from './timeserial'; @@ -7,7 +7,11 @@ export interface LiveCounterData extends LiveObjectData { data: number; } -export class LiveCounter extends LiveObject { +export interface LiveCounterUpdate extends LiveObjectUpdate { + update: { inc: number }; +} + +export class LiveCounter extends LiveObject { constructor( liveObjects: LiveObjects, private _created: boolean, @@ -62,16 +66,19 @@ export class LiveCounter extends LiveObject { ); } + let update: LiveCounterUpdate | LiveObjectUpdateNoop; switch (op.action) { case StateOperationAction.COUNTER_CREATE: - this._applyCounterCreate(op.counter); + update = this._applyCounterCreate(op.counter); break; case StateOperationAction.COUNTER_INC: if (this._client.Utils.isNil(op.counterOp)) { this._throwNoPayloadError(op); + // leave an explicit return here, so that TS knows that update object is always set after the switch statement. + return; } else { - this._applyCounterInc(op.counterOp); + update = this._applyCounterInc(op.counterOp); } break; @@ -84,12 +91,18 @@ export class LiveCounter extends LiveObject { } this.setRegionalTimeserial(opRegionalTimeserial); + this.notifyUpdated(update); } protected _getZeroValueData(): LiveCounterData { return { data: 0 }; } + protected _updateFromDataDiff(currentDataRef: LiveCounterData, newDataRef: LiveCounterData): LiveCounterUpdate { + const counterDiff = newDataRef.data - currentDataRef.data; + return { update: { inc: counterDiff } }; + } + private _throwNoPayloadError(op: StateOperation): void { throw new this._client.ErrorInfo( `No payload found for ${op.action} op for LiveCounter objectId=${this.getObjectId()}`, @@ -98,7 +111,7 @@ export class LiveCounter extends LiveObject { ); } - private _applyCounterCreate(op: StateCounter | undefined): void { + private _applyCounterCreate(op: StateCounter | undefined): LiveCounterUpdate | LiveObjectUpdateNoop { if (this.isCreated()) { // skip COUNTER_CREATE op if this counter is already created this._client.Logger.logAction( @@ -107,14 +120,14 @@ export class LiveCounter extends LiveObject { 'LiveCounter._applyCounterCreate()', `skipping applying COUNTER_CREATE op on a counter instance as it is already created; objectId=${this._objectId}`, ); - return; + return { noop: true }; } if (this._client.Utils.isNil(op)) { // if a counter object is missing for the COUNTER_CREATE op, the initial value is implicitly 0 in this case. // we need to SUM the initial value to the current value due to the reasons below, but since it's a 0, we can skip addition operation this.setCreated(true); - return; + return { update: { inc: 0 } }; } // note that it is intentional to SUM the incoming count from the create op. @@ -122,9 +135,12 @@ export class LiveCounter extends LiveObject { // so it is missing the initial value that we're going to add now. this._dataRef.data += op.count ?? 0; this.setCreated(true); + + return { update: { inc: op.count ?? 0 } }; } - private _applyCounterInc(op: StateCounterOp): void { + private _applyCounterInc(op: StateCounterOp): LiveCounterUpdate { this._dataRef.data += op.amount; + return { update: { inc: op.amount } }; } } diff --git a/src/plugins/liveobjects/livemap.ts b/src/plugins/liveobjects/livemap.ts index b0427a9a0..97129f896 100644 --- a/src/plugins/liveobjects/livemap.ts +++ b/src/plugins/liveobjects/livemap.ts @@ -1,5 +1,7 @@ +import deepEqual from 'deep-equal'; + import type BaseClient from 'common/lib/client/baseclient'; -import { LiveObject, LiveObjectData } from './liveobject'; +import { LiveObject, LiveObjectData, LiveObjectUpdate, LiveObjectUpdateNoop } from './liveobject'; import { LiveObjects } from './liveobjects'; import { MapSemantics, @@ -40,7 +42,11 @@ export interface LiveMapData extends LiveObjectData { data: Map; } -export class LiveMap extends LiveObject { +export interface LiveMapUpdate extends LiveObjectUpdate { + update: { [keyName: string]: 'updated' | 'removed' }; +} + +export class LiveMap extends LiveObject { constructor( liveObjects: LiveObjects, private _semantics: MapSemantics, @@ -60,6 +66,9 @@ export class LiveMap extends LiveObject { return new LiveMap(liveobjects, MapSemantics.LWW, null, objectId, regionalTimeserial); } + /** + * @internal + */ static liveMapDataFromMapEntries(client: BaseClient, entries: Record): LiveMapData { const liveMapData: LiveMapData = { data: new Map(), @@ -122,7 +131,7 @@ export class LiveMap extends LiveObject { let size = 0; for (const value of this._dataRef.data.values()) { if (value.tombstone === true) { - // should not count deleted entries + // should not count removed entries continue; } @@ -144,24 +153,29 @@ export class LiveMap extends LiveObject { ); } + let update: LiveMapUpdate | LiveObjectUpdateNoop; switch (op.action) { case StateOperationAction.MAP_CREATE: - this._applyMapCreate(op.map); + update = this._applyMapCreate(op.map); break; case StateOperationAction.MAP_SET: if (this._client.Utils.isNil(op.mapOp)) { this._throwNoPayloadError(op); + // leave an explicit return here, so that TS knows that update object is always set after the switch statement. + return; } else { - this._applyMapSet(op.mapOp, DefaultTimeserial.calculateTimeserial(this._client, msg.serial)); + update = this._applyMapSet(op.mapOp, DefaultTimeserial.calculateTimeserial(this._client, msg.serial)); } break; case StateOperationAction.MAP_REMOVE: if (this._client.Utils.isNil(op.mapOp)) { this._throwNoPayloadError(op); + // leave an explicit return here, so that TS knows that update object is always set after the switch statement. + return; } else { - this._applyMapRemove(op.mapOp, DefaultTimeserial.calculateTimeserial(this._client, msg.serial)); + update = this._applyMapRemove(op.mapOp, DefaultTimeserial.calculateTimeserial(this._client, msg.serial)); } break; @@ -174,12 +188,67 @@ export class LiveMap extends LiveObject { } this.setRegionalTimeserial(opRegionalTimeserial); + this.notifyUpdated(update); } protected _getZeroValueData(): LiveMapData { return { data: new Map() }; } + protected _updateFromDataDiff(currentDataRef: LiveMapData, newDataRef: LiveMapData): LiveMapUpdate { + const update: LiveMapUpdate = { update: {} }; + + for (const [key, currentEntry] of currentDataRef.data.entries()) { + // any non-tombstoned properties that exist on a current map, but not in the new data - got removed + if (currentEntry.tombstone === false && !newDataRef.data.has(key)) { + update.update[key] = 'removed'; + } + } + + for (const [key, newEntry] of newDataRef.data.entries()) { + if (!currentDataRef.data.has(key)) { + // if property does not exist in the current map, but new data has it as a non-tombstoned property - got updated + if (newEntry.tombstone === false) { + update.update[key] = 'updated'; + continue; + } + + // otherwise, if new data has this prop tombstoned - do nothing, as property didn't exist anyway + if (newEntry.tombstone === true) { + continue; + } + } + + // properties that exist both in current and new map data need to have their values compared to decide on the update type + const currentEntry = currentDataRef.data.get(key)!; + + // compare tombstones first + if (currentEntry.tombstone === true && newEntry.tombstone === false) { + // current prop is tombstoned, but new is not. it means prop was updated to a meaningful value + update.update[key] = 'updated'; + continue; + } + if (currentEntry.tombstone === false && newEntry.tombstone === true) { + // current prop is not tombstoned, but new is. it means prop was removed + update.update[key] = 'removed'; + continue; + } + if (currentEntry.tombstone === true && newEntry.tombstone === true) { + // both props are tombstoned - treat as noop, as there is no data to compare. + continue; + } + + // both props exist and are not tombstoned, need to compare values with deep equals to see if it was changed + const valueChanged = !deepEqual(currentEntry.data, newEntry.data, { strict: true }); + if (valueChanged) { + update.update[key] = 'updated'; + continue; + } + } + + return update; + } + private _throwNoPayloadError(op: StateOperation): void { throw new this._client.ErrorInfo( `No payload found for ${op.action} op for LiveMap objectId=${this.getObjectId()}`, @@ -188,11 +257,11 @@ export class LiveMap extends LiveObject { ); } - private _applyMapCreate(op: StateMap | undefined): void { + private _applyMapCreate(op: StateMap | undefined): LiveMapUpdate | LiveObjectUpdateNoop { if (this._client.Utils.isNil(op)) { // if a map object is missing for the MAP_CREATE op, the initial value is implicitly an empty map. // in this case there is nothing to merge into the current map, so we can just end processing the op. - return; + return { update: {} }; } if (this._semantics !== op.semantics) { @@ -203,6 +272,7 @@ export class LiveMap extends LiveObject { ); } + const aggregatedUpdate: LiveMapUpdate | LiveObjectUpdateNoop = { update: {} }; // in order to apply MAP_CREATE op for an existing map, we should merge their underlying entries keys. // we can do this by iterating over entries from MAP_CREATE op and apply changes on per-key basis as if we had MAP_SET, MAP_REMOVE operations. Object.entries(op.entries ?? {}).forEach(([key, entry]) => { @@ -210,17 +280,28 @@ export class LiveMap extends LiveObject { const opOriginTimeserial = entry.timeserial ? DefaultTimeserial.calculateTimeserial(this._client, entry.timeserial) : DefaultTimeserial.zeroValueTimeserial(this._client); + let update: LiveMapUpdate | LiveObjectUpdateNoop; if (entry.tombstone === true) { - // entry in MAP_CREATE op is deleted, try to apply MAP_REMOVE op - this._applyMapRemove({ key }, opOriginTimeserial); + // entry in MAP_CREATE op is removed, try to apply MAP_REMOVE op + update = this._applyMapRemove({ key }, opOriginTimeserial); } else { - // entry in MAP_CREATE op is not deleted, try to set it via MAP_SET op - this._applyMapSet({ key, data: entry.data }, opOriginTimeserial); + // entry in MAP_CREATE op is not removed, try to set it via MAP_SET op + update = this._applyMapSet({ key, data: entry.data }, opOriginTimeserial); + } + + // skip noop updates + if ((update as LiveObjectUpdateNoop).noop) { + return; } + + // otherwise copy update data to aggregated update + Object.assign(aggregatedUpdate.update, update.update); }); + + return aggregatedUpdate; } - private _applyMapSet(op: StateMapOp, opOriginTimeserial: Timeserial): void { + private _applyMapSet(op: StateMapOp, opOriginTimeserial: Timeserial): LiveMapUpdate | LiveObjectUpdateNoop { const { ErrorInfo, Utils } = this._client; const existingEntry = this._dataRef.data.get(op.key); @@ -235,7 +316,7 @@ export class LiveMap extends LiveObject { 'LiveMap._applyMapSet()', `skipping update for key="${op.key}": op timeserial ${opOriginTimeserial.toString()} <= entry timeserial ${existingEntry.timeserial.toString()}; objectId=${this._objectId}`, ); - return; + return { noop: true }; } if (Utils.isNil(op.data) || (Utils.isNil(op.data.value) && Utils.isNil(op.data.objectId))) { @@ -270,9 +351,10 @@ export class LiveMap extends LiveObject { }; this._dataRef.data.set(op.key, newEntry); } + return { update: { [op.key]: 'updated' } }; } - private _applyMapRemove(op: StateMapOp, opOriginTimeserial: Timeserial): void { + private _applyMapRemove(op: StateMapOp, opOriginTimeserial: Timeserial): LiveMapUpdate | LiveObjectUpdateNoop { const existingEntry = this._dataRef.data.get(op.key); if ( existingEntry && @@ -285,7 +367,7 @@ export class LiveMap extends LiveObject { 'LiveMap._applyMapRemove()', `skipping remove for key="${op.key}": op timeserial ${opOriginTimeserial.toString()} <= entry timeserial ${existingEntry.timeserial.toString()}; objectId=${this._objectId}`, ); - return; + return { noop: true }; } if (existingEntry) { @@ -300,5 +382,7 @@ export class LiveMap extends LiveObject { }; this._dataRef.data.set(op.key, newEntry); } + + return { update: { [op.key]: 'removed' } }; } } diff --git a/src/plugins/liveobjects/liveobject.ts b/src/plugins/liveobjects/liveobject.ts index 70a294779..e95eb95d7 100644 --- a/src/plugins/liveobjects/liveobject.ts +++ b/src/plugins/liveobjects/liveobject.ts @@ -1,31 +1,76 @@ import type BaseClient from 'common/lib/client/baseclient'; +import type EventEmitter from 'common/lib/util/eventemitter'; import { LiveObjects } from './liveobjects'; import { StateMessage, StateOperation } from './statemessage'; import { DefaultTimeserial, Timeserial } from './timeserial'; +enum LiveObjectEvents { + Updated = 'Updated', +} + export interface LiveObjectData { data: any; } -export abstract class LiveObject { +export interface LiveObjectUpdate { + update: any; +} + +export interface LiveObjectUpdateNoop { + // have optional update field with undefined type so it's not possible to create a noop object with a meaningful update property. + update?: undefined; + noop: true; +} + +export interface SubscribeResponse { + unsubscribe(): void; +} + +export abstract class LiveObject< + TData extends LiveObjectData = LiveObjectData, + TUpdate extends LiveObjectUpdate = LiveObjectUpdate, +> { protected _client: BaseClient; - protected _dataRef: T; + protected _eventEmitter: EventEmitter; + protected _dataRef: TData; protected _objectId: string; protected _regionalTimeserial: Timeserial; constructor( protected _liveObjects: LiveObjects, - initialData?: T | null, + initialData?: TData | null, objectId?: string, regionalTimeserial?: Timeserial, ) { this._client = this._liveObjects.getClient(); + this._eventEmitter = new this._client.EventEmitter(this._client.logger); this._dataRef = initialData ?? this._getZeroValueData(); this._objectId = objectId ?? this._createObjectId(); // use zero value timeserial by default, so any future operation can be applied for this object this._regionalTimeserial = regionalTimeserial ?? DefaultTimeserial.zeroValueTimeserial(this._client); } + subscribe(listener: (update: TUpdate) => void): SubscribeResponse { + this._eventEmitter.on(LiveObjectEvents.Updated, listener); + + const unsubscribe = () => { + this._eventEmitter.off(LiveObjectEvents.Updated, listener); + }; + + return { unsubscribe }; + } + + unsubscribe(listener: (update: TUpdate) => void): void { + // current implementation of the EventEmitter will remove all listeners if .off is called without arguments or with nullish arguments. + // or when called with just an event argument, it will remove all listeners for the event. + // thus we need to check that listener does actually exist before calling .off. + if (this._client.Utils.isNil(listener)) { + return; + } + + this._eventEmitter.off(LiveObjectEvents.Updated, listener); + } + /** * @internal */ @@ -41,10 +86,14 @@ export abstract class LiveObject { } /** + * Sets a new data reference for the LiveObject and returns an update object that describes the changes applied based on the object's previous value. + * * @internal */ - setData(newDataRef: T): void { + setData(newDataRef: TData): TUpdate { + const update = this._updateFromDataDiff(this._dataRef, newDataRef); this._dataRef = newDataRef; + return update; } /** @@ -54,6 +103,18 @@ export abstract class LiveObject { this._regionalTimeserial = regionalTimeserial; } + /** + * @internal + */ + notifyUpdated(update: TUpdate | LiveObjectUpdateNoop): void { + // should not emit update event if update was noop + if ((update as LiveObjectUpdateNoop).noop) { + return; + } + + this._eventEmitter.emit(LiveObjectEvents.Updated, update); + } + private _createObjectId(): string { // TODO: implement object id generation based on live object type and initial value return Math.random().toString().substring(2); @@ -63,5 +124,9 @@ export abstract class LiveObject { * @internal */ abstract applyOperation(op: StateOperation, msg: StateMessage, opRegionalTimeserial: Timeserial): void; - protected abstract _getZeroValueData(): T; + protected abstract _getZeroValueData(): TData; + /** + * Calculate the update object based on the current Live Object data and incoming new data. + */ + protected abstract _updateFromDataDiff(currentDataRef: TData, newDataRef: TData): TUpdate; } diff --git a/src/plugins/liveobjects/liveobjects.ts b/src/plugins/liveobjects/liveobjects.ts index 9b1981f63..a5070ec99 100644 --- a/src/plugins/liveobjects/liveobjects.ts +++ b/src/plugins/liveobjects/liveobjects.ts @@ -4,7 +4,7 @@ import type EventEmitter from 'common/lib/util/eventemitter'; import type * as API from '../../../ably'; import { LiveCounter } from './livecounter'; import { LiveMap } from './livemap'; -import { LiveObject } from './liveobject'; +import { LiveObject, LiveObjectUpdate } from './liveobject'; import { LiveObjectsPool, ROOT_OBJECT_ID } from './liveobjectspool'; import { StateMessage } from './statemessage'; import { LiveCounterDataEntry, SyncLiveObjectsDataPool } from './syncliveobjectsdatapool'; @@ -199,6 +199,7 @@ export class LiveObjects { } const receivedObjectIds = new Set(); + const existingObjectUpdates: { object: LiveObject; update: LiveObjectUpdate }[] = []; for (const [objectId, entry] of this._syncLiveObjectsDataPool.entries()) { receivedObjectIds.add(objectId); @@ -206,11 +207,14 @@ export class LiveObjects { const regionalTimeserialObj = DefaultTimeserial.calculateTimeserial(this._client, entry.regionalTimeserial); if (existingObject) { - existingObject.setData(entry.objectData); + const update = existingObject.setData(entry.objectData); existingObject.setRegionalTimeserial(regionalTimeserialObj); if (existingObject instanceof LiveCounter) { existingObject.setCreated((entry as LiveCounterDataEntry).created); } + // store updates for existing objects to call subscription callbacks for all of them once the SYNC sequence is completed. + // this will ensure that clients get notified about changes only once everything was applied. + existingObjectUpdates.push({ object: existingObject, update }); continue; } @@ -235,5 +239,8 @@ export class LiveObjects { // need to remove LiveObject instances from the LiveObjectsPool for which objectIds were not received during the SYNC sequence this._liveObjectsPool.deleteExtraObjectIds([...receivedObjectIds]); + + // call subscription callbacks for all updated existing objects + existingObjectUpdates.forEach(({ object, update }) => object.notifyUpdated(update)); } } diff --git a/src/plugins/liveobjects/liveobjectspool.ts b/src/plugins/liveobjects/liveobjectspool.ts index aef14cc8d..a4ea8c92e 100644 --- a/src/plugins/liveobjects/liveobjectspool.ts +++ b/src/plugins/liveobjects/liveobjectspool.ts @@ -89,7 +89,6 @@ export class LiveObjectsPool { if (this.get(stateOperation.objectId)) { // object wich such id already exists (we may have created a zero-value object before, or this is a duplicate *_CREATE op), // so delegate application of the op to that object - // TODO: invoke subscription callbacks for an object when applied this.get(stateOperation.objectId)!.applyOperation(stateOperation, stateMessage, regionalTimeserial); break; } @@ -111,7 +110,6 @@ export class LiveObjectsPool { // we create a zero-value object for the provided object id, and apply operation for that zero-value object. // when we eventually receive a corresponding *_CREATE op for that object, its application will be handled by that zero-value object. this.createZeroValueObjectIfNotExists(stateOperation.objectId); - // TODO: invoke subscription callbacks for an object when applied this.get(stateOperation.objectId)!.applyOperation(stateOperation, stateMessage, regionalTimeserial); break; From 07cafc44cbdd1db56a65783f6e76d2b39a55b740 Mon Sep 17 00:00:00 2001 From: Andrew Bulat Date: Tue, 5 Nov 2024 04:28:41 +0000 Subject: [PATCH 3/5] Implement .unsubscribeAll() method on Live Objects Resolves DTP-959 --- src/plugins/liveobjects/liveobject.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/plugins/liveobjects/liveobject.ts b/src/plugins/liveobjects/liveobject.ts index e95eb95d7..78244ab65 100644 --- a/src/plugins/liveobjects/liveobject.ts +++ b/src/plugins/liveobjects/liveobject.ts @@ -71,6 +71,10 @@ export abstract class LiveObject< this._eventEmitter.off(LiveObjectEvents.Updated, listener); } + unsubscribeAll(): void { + this._eventEmitter.off(LiveObjectEvents.Updated); + } + /** * @internal */ From f8fda9821f73a82bdd14f6e8a556c66a8d653d03 Mon Sep 17 00:00:00 2001 From: Andrew Bulat Date: Tue, 5 Nov 2024 06:24:00 +0000 Subject: [PATCH 4/5] Add LiveObjects subscriptions tests Also add `deep-equal` to the list of named dependencies for tests so that browser tests know where to get this package. --- test/common/globals/named_dependencies.js | 1 + test/realtime/live_objects.test.js | 521 +++++++++++++++++++++- 2 files changed, 519 insertions(+), 3 deletions(-) diff --git a/test/common/globals/named_dependencies.js b/test/common/globals/named_dependencies.js index b303f0dfc..1c2692824 100644 --- a/test/common/globals/named_dependencies.js +++ b/test/common/globals/named_dependencies.js @@ -22,6 +22,7 @@ define(function () { async: { browser: 'node_modules/async/lib/async' }, chai: { browser: 'node_modules/chai/chai', node: 'node_modules/chai/chai' }, ulid: { browser: 'node_modules/ulid/dist/index.umd', node: 'node_modules/ulid/dist/index.umd' }, + 'deep-equal': { browser: 'node_modules/deep-equal/index', node: 'node_modules/deep-equal/index' }, private_api_recorder: { browser: 'test/common/modules/private_api_recorder', node: 'test/common/modules/private_api_recorder', diff --git a/test/realtime/live_objects.test.js b/test/realtime/live_objects.test.js index a1a9ffaef..935324527 100644 --- a/test/realtime/live_objects.test.js +++ b/test/realtime/live_objects.test.js @@ -562,7 +562,7 @@ define(['ably', 'shared_helper', 'chai', 'live_objects', 'live_objects_helper'], ), ).to.not.exist; - // create map with references. need to created referenced objects first to obtain their object ids + // create map with references. need to create referenced objects first to obtain their object ids const { objectId: referencedMapObjectId } = await liveObjectsHelper.stateRequest( channelName, liveObjectsHelper.mapCreateOp({ entries: { stringKey: { data: { value: 'stringValue' } } } }), @@ -905,7 +905,7 @@ define(['ably', 'shared_helper', 'chai', 'live_objects', 'live_objects_helper'], }); } - const operationsDuringSyncSequence = [ + const applyOperationsDuringSyncScenarios = [ { description: 'state operation messages are buffered during STATE_SYNC sequence', action: async (ctx) => { @@ -1206,7 +1206,7 @@ define(['ably', 'shared_helper', 'chai', 'live_objects', 'live_objects_helper'], }, ]; - for (const scenario of operationsDuringSyncSequence) { + for (const scenario of applyOperationsDuringSyncScenarios) { if (scenario.skip === true) { continue; } @@ -1231,6 +1231,521 @@ define(['ably', 'shared_helper', 'chai', 'live_objects', 'live_objects_helper'], }, client); }); } + + const subscriptionCallbacksScenarios = [ + { + description: 'can subscribe to the incoming COUNTER_INC operation on a LiveCounter', + action: async (ctx) => { + const { root, liveObjectsHelper, channelName, sampleCounterKey, sampleCounterObjectId } = ctx; + + const counter = root.get(sampleCounterKey); + const subscriptionPromise = new Promise((resolve, reject) => + counter.subscribe((update) => { + try { + expect(update).to.deep.equal( + { update: { inc: 1 } }, + 'Check counter subscription callback is called with an expected update object for COUNTER_INC operation', + ); + resolve(); + } catch (error) { + reject(error); + } + }), + ); + + await liveObjectsHelper.stateRequest( + channelName, + liveObjectsHelper.counterIncOp({ + objectId: sampleCounterObjectId, + amount: 1, + }), + ); + + await subscriptionPromise; + }, + }, + + { + description: 'can subscribe to multiple incoming operations on a LiveCounter', + action: async (ctx) => { + const { root, liveObjectsHelper, channelName, sampleCounterKey, sampleCounterObjectId } = ctx; + + const counter = root.get(sampleCounterKey); + const expectedCounterIncrements = [100, -100, Number.MAX_SAFE_INTEGER, -Number.MAX_SAFE_INTEGER]; + let currentUpdateIndex = 0; + + const subscriptionPromise = new Promise((resolve, reject) => + counter.subscribe((update) => { + try { + const expectedInc = expectedCounterIncrements[currentUpdateIndex]; + expect(update).to.deep.equal( + { update: { inc: expectedInc } }, + `Check counter subscription callback is called with an expected update object for ${currentUpdateIndex + 1} times`, + ); + + if (currentUpdateIndex === expectedCounterIncrements.length - 1) { + resolve(); + } + + currentUpdateIndex++; + } catch (error) { + reject(error); + } + }), + ); + + for (const increment of expectedCounterIncrements) { + await liveObjectsHelper.stateRequest( + channelName, + liveObjectsHelper.counterIncOp({ + objectId: sampleCounterObjectId, + amount: increment, + }), + ); + } + + await subscriptionPromise; + }, + }, + + { + description: 'can subscribe to the incoming MAP_SET operation on a LiveMap', + action: async (ctx) => { + const { root, liveObjectsHelper, channelName, sampleMapKey, sampleMapObjectId } = ctx; + + const map = root.get(sampleMapKey); + const subscriptionPromise = new Promise((resolve, reject) => + map.subscribe((update) => { + try { + expect(update).to.deep.equal( + { update: { stringKey: 'updated' } }, + 'Check map subscription callback is called with an expected update object for MAP_SET operation', + ); + resolve(); + } catch (error) { + reject(error); + } + }), + ); + + await liveObjectsHelper.stateRequest( + channelName, + liveObjectsHelper.mapSetOp({ + objectId: sampleMapObjectId, + key: 'stringKey', + data: { value: 'stringValue' }, + }), + ); + + await subscriptionPromise; + }, + }, + + { + description: 'can subscribe to the incoming MAP_REMOVE operation on a LiveMap', + action: async (ctx) => { + const { root, liveObjectsHelper, channelName, sampleMapKey, sampleMapObjectId } = ctx; + + const map = root.get(sampleMapKey); + const subscriptionPromise = new Promise((resolve, reject) => + map.subscribe((update) => { + try { + expect(update).to.deep.equal( + { update: { stringKey: 'deleted' } }, + 'Check map subscription callback is called with an expected update object for MAP_REMOVE operation', + ); + resolve(); + } catch (error) { + reject(error); + } + }), + ); + + await liveObjectsHelper.stateRequest( + channelName, + liveObjectsHelper.mapRemoveOp({ + objectId: sampleMapObjectId, + key: 'stringKey', + }), + ); + + await subscriptionPromise; + }, + }, + + { + description: 'can subscribe to multiple incoming operations on a LiveMap', + action: async (ctx) => { + const { root, liveObjectsHelper, channelName, sampleMapKey, sampleMapObjectId } = ctx; + + const map = root.get(sampleMapKey); + const expectedMapUpdates = [ + { update: { foo: 'updated' } }, + { update: { bar: 'updated' } }, + { update: { foo: 'deleted' } }, + { update: { baz: 'updated' } }, + { update: { bar: 'deleted' } }, + ]; + let currentUpdateIndex = 0; + + const subscriptionPromise = new Promise((resolve, reject) => + map.subscribe((update) => { + try { + expect(update).to.deep.equal( + expectedMapUpdates[currentUpdateIndex], + `Check map subscription callback is called with an expected update object for ${currentUpdateIndex + 1} times`, + ); + + if (currentUpdateIndex === expectedMapUpdates.length - 1) { + resolve(); + } + + currentUpdateIndex++; + } catch (error) { + reject(error); + } + }), + ); + + await liveObjectsHelper.stateRequest( + channelName, + liveObjectsHelper.mapSetOp({ + objectId: sampleMapObjectId, + key: 'foo', + data: { value: 'something' }, + }), + ); + + await liveObjectsHelper.stateRequest( + channelName, + liveObjectsHelper.mapSetOp({ + objectId: sampleMapObjectId, + key: 'bar', + data: { value: 'something' }, + }), + ); + + await liveObjectsHelper.stateRequest( + channelName, + liveObjectsHelper.mapRemoveOp({ + objectId: sampleMapObjectId, + key: 'foo', + }), + ); + + await liveObjectsHelper.stateRequest( + channelName, + liveObjectsHelper.mapSetOp({ + objectId: sampleMapObjectId, + key: 'baz', + data: { value: 'something' }, + }), + ); + + await liveObjectsHelper.stateRequest( + channelName, + liveObjectsHelper.mapRemoveOp({ + objectId: sampleMapObjectId, + key: 'bar', + }), + ); + + await subscriptionPromise; + }, + }, + + { + description: 'can unsubscribe from LiveCounter updates via returned "unsubscribe" callback', + action: async (ctx) => { + const { root, liveObjectsHelper, channelName, sampleCounterKey, sampleCounterObjectId } = ctx; + + const counter = root.get(sampleCounterKey); + let callbackCalled = 0; + const subscriptionPromise = new Promise((resolve) => { + const { unsubscribe } = counter.subscribe(() => { + callbackCalled++; + // unsubscribe from future updates after the first call + unsubscribe(); + resolve(); + }); + }); + + const increments = 3; + for (let i = 0; i < increments; i++) { + await liveObjectsHelper.stateRequest( + channelName, + liveObjectsHelper.counterIncOp({ + objectId: sampleCounterObjectId, + amount: 1, + }), + ); + } + + await subscriptionPromise; + + expect(counter.value()).to.equal(3, 'Check counter has final expected value after all increments'); + expect(callbackCalled).to.equal(1, 'Check subscription callback was only called once'); + }, + }, + + { + description: 'can unsubscribe from LiveCounter updates via LiveCounter.unsubscribe() call', + action: async (ctx) => { + const { root, liveObjectsHelper, channelName, sampleCounterKey, sampleCounterObjectId } = ctx; + + const counter = root.get(sampleCounterKey); + let callbackCalled = 0; + const subscriptionPromise = new Promise((resolve) => { + const listener = () => { + callbackCalled++; + // unsubscribe from future updates after the first call + counter.unsubscribe(listener); + resolve(); + }; + + counter.subscribe(listener); + }); + + const increments = 3; + for (let i = 0; i < increments; i++) { + await liveObjectsHelper.stateRequest( + channelName, + liveObjectsHelper.counterIncOp({ + objectId: sampleCounterObjectId, + amount: 1, + }), + ); + } + + await subscriptionPromise; + + expect(counter.value()).to.equal(3, 'Check counter has final expected value after all increments'); + expect(callbackCalled).to.equal(1, 'Check subscription callback was only called once'); + }, + }, + + { + description: 'can remove all LiveCounter update listeners via LiveCounter.unsubscribeAll() call', + action: async (ctx) => { + const { root, liveObjectsHelper, channelName, sampleCounterKey, sampleCounterObjectId } = ctx; + + const counter = root.get(sampleCounterKey); + const callbacks = 3; + const callbacksCalled = new Array(callbacks).fill(0); + const subscriptionPromises = []; + + for (let i = 0; i < callbacks; i++) { + const promise = new Promise((resolve) => { + counter.subscribe(() => { + callbacksCalled[i]++; + resolve(); + }); + }); + subscriptionPromises.push(promise); + } + + const increments = 3; + for (let i = 0; i < increments; i++) { + await liveObjectsHelper.stateRequest( + channelName, + liveObjectsHelper.counterIncOp({ + objectId: sampleCounterObjectId, + amount: 1, + }), + ); + + if (i === 0) { + // unsub all after first operation + counter.unsubscribeAll(); + } + } + + await Promise.all(subscriptionPromises); + + expect(counter.value()).to.equal(3, 'Check counter has final expected value after all increments'); + callbacksCalled.forEach((x) => expect(x).to.equal(1, 'Check subscription callbacks were called once each')); + }, + }, + + { + description: 'can unsubscribe from LiveMap updates via returned "unsubscribe" callback', + action: async (ctx) => { + const { root, liveObjectsHelper, channelName, sampleMapKey, sampleMapObjectId } = ctx; + + const map = root.get(sampleMapKey); + let callbackCalled = 0; + const subscriptionPromise = new Promise((resolve) => { + const { unsubscribe } = map.subscribe(() => { + callbackCalled++; + // unsubscribe from future updates after the first call + unsubscribe(); + resolve(); + }); + }); + + const mapSets = 3; + for (let i = 0; i < mapSets; i++) { + await liveObjectsHelper.stateRequest( + channelName, + liveObjectsHelper.mapSetOp({ + objectId: sampleMapObjectId, + key: `foo-${i}`, + data: { value: 'exists' }, + }), + ); + } + + await subscriptionPromise; + + for (let i = 0; i < mapSets; i++) { + expect(map.get(`foo-${i}`)).to.equal( + 'exists', + `Check map has value for key "foo-${i}" after all map sets`, + ); + } + expect(callbackCalled).to.equal(1, 'Check subscription callback was only called once'); + }, + }, + + { + description: 'can unsubscribe from LiveMap updates via LiveMap.unsubscribe() call', + action: async (ctx) => { + const { root, liveObjectsHelper, channelName, sampleMapKey, sampleMapObjectId } = ctx; + + const map = root.get(sampleMapKey); + let callbackCalled = 0; + const subscriptionPromise = new Promise((resolve) => { + const listener = () => { + callbackCalled++; + // unsubscribe from future updates after the first call + map.unsubscribe(listener); + resolve(); + }; + + map.subscribe(listener); + }); + + const mapSets = 3; + for (let i = 0; i < mapSets; i++) { + await liveObjectsHelper.stateRequest( + channelName, + liveObjectsHelper.mapSetOp({ + objectId: sampleMapObjectId, + key: `foo-${i}`, + data: { value: 'exists' }, + }), + ); + } + + await subscriptionPromise; + + for (let i = 0; i < mapSets; i++) { + expect(map.get(`foo-${i}`)).to.equal( + 'exists', + `Check map has value for key "foo-${i}" after all map sets`, + ); + } + expect(callbackCalled).to.equal(1, 'Check subscription callback was only called once'); + }, + }, + + { + description: 'can remove all LiveMap update listeners via LiveMap.unsubscribeAll() call', + action: async (ctx) => { + const { root, liveObjectsHelper, channelName, sampleMapKey, sampleMapObjectId } = ctx; + + const map = root.get(sampleMapKey); + const callbacks = 3; + const callbacksCalled = new Array(callbacks).fill(0); + const subscriptionPromises = []; + + for (let i = 0; i < callbacks; i++) { + const promise = new Promise((resolve) => { + map.subscribe(() => { + callbacksCalled[i]++; + resolve(); + }); + }); + subscriptionPromises.push(promise); + } + + const mapSets = 3; + for (let i = 0; i < mapSets; i++) { + await liveObjectsHelper.stateRequest( + channelName, + liveObjectsHelper.mapSetOp({ + objectId: sampleMapObjectId, + key: `foo-${i}`, + data: { value: 'exists' }, + }), + ); + + if (i === 0) { + // unsub all after first operation + map.unsubscribeAll(); + } + } + + await Promise.all(subscriptionPromises); + + for (let i = 0; i < mapSets; i++) { + expect(map.get(`foo-${i}`)).to.equal( + 'exists', + `Check map has value for key "foo-${i}" after all map sets`, + ); + } + callbacksCalled.forEach((x) => expect(x).to.equal(1, 'Check subscription callbacks were called once each')); + }, + }, + ]; + + for (const scenario of subscriptionCallbacksScenarios) { + if (scenario.skip === true) { + continue; + } + + /** @nospec */ + it(scenario.description, async function () { + const helper = this.test.helper; + const liveObjectsHelper = new LiveObjectsHelper(helper); + const client = RealtimeWithLiveObjects(helper); + + await helper.monitorConnectionThenCloseAndFinish(async () => { + const channelName = scenario.description; + const channel = client.channels.get(channelName, channelOptionsWithLiveObjects()); + const liveObjects = channel.liveObjects; + + await channel.attach(); + const root = await liveObjects.getRoot(); + + const sampleMapKey = 'sampleMap'; + const sampleCounterKey = 'sampleCounter'; + + // prepare map and counter objects for use by the scenario + const { objectId: sampleMapObjectId } = await liveObjectsHelper.createAndSetOnMap(channelName, { + mapObjectId: 'root', + key: sampleMapKey, + createOp: liveObjectsHelper.mapCreateOp(), + }); + const { objectId: sampleCounterObjectId } = await liveObjectsHelper.createAndSetOnMap(channelName, { + mapObjectId: 'root', + key: sampleCounterKey, + createOp: liveObjectsHelper.counterCreateOp(), + }); + + await scenario.action({ + root, + liveObjectsHelper, + channelName, + channel, + sampleMapKey, + sampleMapObjectId, + sampleCounterKey, + sampleCounterObjectId, + }); + }, client); + }); + } }); /** @nospec */ From 4bca1c797caa207de96f3ff767d916f2426489b8 Mon Sep 17 00:00:00 2001 From: Andrew Bulat Date: Tue, 5 Nov 2024 06:51:13 +0000 Subject: [PATCH 5/5] Update moduleReport to include `external` field for LiveObjects plugin --- scripts/moduleReport.ts | 30 ++++++++++++++++++++++-------- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/scripts/moduleReport.ts b/scripts/moduleReport.ts index 083f18ab9..0eae4017a 100644 --- a/scripts/moduleReport.ts +++ b/scripts/moduleReport.ts @@ -37,6 +37,18 @@ const functions = [ { name: 'constructPresenceMessage', transitiveImports: [] }, ]; +// List of all buildable plugins available as a separate export +interface PluginInfo { + description: string; + path: string; + external?: string[]; +} + +const buildablePlugins: Record<'push' | 'liveObjects', PluginInfo> = { + push: { description: 'Push', path: './build/push.js', external: ['ulid'] }, + liveObjects: { description: 'LiveObjects', path: './build/liveobjects.js', external: ['deep-equal'] }, +}; + function formatBytes(bytes: number) { const kibibytes = bytes / 1024; const formatted = kibibytes.toFixed(2); @@ -70,7 +82,7 @@ function getModularBundleInfo(exports: string[]): BundleInfo { } // Uses esbuild to create a bundle containing the named exports from a given module -function getBundleInfo(modulePath: string, exports?: string[]): BundleInfo { +function getBundleInfo(modulePath: string, exports?: string[], external?: string[]): BundleInfo { const outfile = exports ? exports.join('') : 'all'; const exportTarget = exports ? `{ ${exports.join(', ')} }` : '*'; const result = esbuild.buildSync({ @@ -84,7 +96,7 @@ function getBundleInfo(modulePath: string, exports?: string[]): BundleInfo { outfile, write: false, sourcemap: 'external', - external: ['ulid'], + external, }); const pathHasBase = (component: string) => { @@ -183,9 +195,9 @@ async function calculateAndCheckFunctionSizes(): Promise { return output; } -async function calculatePluginSize(options: { path: string; description: string }): Promise { +async function calculatePluginSize(options: PluginInfo): Promise { const output: Output = { tableRows: [], errors: [] }; - const pluginBundleInfo = getBundleInfo(options.path); + const pluginBundleInfo = getBundleInfo(options.path, undefined, options.external); const sizes = { rawByteSize: pluginBundleInfo.byteSize, gzipEncodedByteSize: (await promisify(gzip)(pluginBundleInfo.code)).byteLength, @@ -200,11 +212,11 @@ async function calculatePluginSize(options: { path: string; description: string } async function calculatePushPluginSize(): Promise { - return calculatePluginSize({ path: './build/push.js', description: 'Push' }); + return calculatePluginSize(buildablePlugins.push); } async function calculateLiveObjectsPluginSize(): Promise { - return calculatePluginSize({ path: './build/liveobjects.js', description: 'LiveObjects' }); + return calculatePluginSize(buildablePlugins.liveObjects); } async function calculateAndCheckMinimalUsefulRealtimeBundleSize(): Promise { @@ -291,7 +303,8 @@ async function checkBaseRealtimeFiles() { } async function checkPushPluginFiles() { - const pushPluginBundleInfo = getBundleInfo('./build/push.js'); + const { path, external } = buildablePlugins.push; + const pushPluginBundleInfo = getBundleInfo(path, undefined, external); // These are the files that are allowed to contribute >= `threshold` bytes to the Push bundle. const allowedFiles = new Set([ @@ -305,7 +318,8 @@ async function checkPushPluginFiles() { } async function checkLiveObjectsPluginFiles() { - const pluginBundleInfo = getBundleInfo('./build/liveobjects.js'); + const { path, external } = buildablePlugins.liveObjects; + const pluginBundleInfo = getBundleInfo(path, undefined, external); // These are the files that are allowed to contribute >= `threshold` bytes to the LiveObjects bundle. const allowedFiles = new Set([