From 131ff3b47ee7ee03d00336850d8ed585e29f99d6 Mon Sep 17 00:00:00 2001 From: Yuri Guller Date: Sat, 15 Nov 2025 21:31:48 +0200 Subject: [PATCH 01/23] wip --- .github/workflows/quality.yml | 5 +- ci/eslint.config.mjs | 44 +- ci/tools/build-utils.js | 55 +- docs/architecture.md | 12 + package-lock.json | 768 +++++++++++++++--- package.json | 8 +- src/constants.ts | 6 + ...observer.d.ts => object-observer.d.ts.tmp} | 6 +- src/object-observer.js | 713 ---------------- src/object-observer.ts | 250 ++++++ src/observables/abstract-base.ts | 85 ++ src/observables/array.ts | 41 + src/observables/methods/copy-within.ts | 48 ++ src/observables/methods/delete-property.ts | 22 + src/observables/methods/fill.ts | 40 + src/observables/methods/pop.ts | 22 + src/observables/methods/push.ts | 24 + src/observables/methods/reverse.ts | 25 + src/observables/methods/set.ts | 27 + src/observables/methods/shift.ts | 34 + src/observables/methods/sort.ts | 25 + src/observables/methods/splice.ts | 63 ++ src/observables/methods/typed-set.ts | 19 + src/observables/methods/unshift.ts | 35 + src/observables/object.ts | 15 + src/observables/typed-array.ts | 28 + src/structs/change.ts | 35 + sri.json | 7 +- tests/api-base.js | 2 +- tests/api-changes.js | 26 +- tests/browser-host-objects.js | 2 +- tests/cross-instance.js | 4 +- tests/listeners.js | 2 +- tests/object-generics.js | 2 +- tests/object-observer-api.js | 2 +- tests/object-observer-arrays-copy-within.js | 2 +- tests/object-observer-arrays-typed.js | 2 +- tests/object-observer-arrays.js | 2 +- .../object-observer-native-objects-to-skip.js | 2 +- tests/object-observer-objects-async.js | 2 +- tests/object-observer-objects-circular.js | 2 +- tests/object-observer-objects-same-refs.js | 2 +- tests/object-observer-objects.js | 4 +- tests/object-observer-subgraphs.js | 2 +- tests/observable-nested.js | 6 +- tests/observe-specific-paths.js | 2 +- tests/reassignment-of-equals.js | 2 +- tests/revokation.js | 2 +- tests/unobserve.js | 2 +- tests/workers/perf-async-test-a.js | 2 +- tests/workers/perf-async-test-b.js | 2 +- tests/workers/perf-sync-test-a.js | 2 +- tests/workers/perf-sync-test-b.js | 2 +- 53 files changed, 1609 insertions(+), 935 deletions(-) create mode 100644 docs/architecture.md create mode 100644 src/constants.ts rename src/{object-observer.d.ts => object-observer.d.ts.tmp} (97%) delete mode 100644 src/object-observer.js create mode 100644 src/object-observer.ts create mode 100644 src/observables/abstract-base.ts create mode 100644 src/observables/array.ts create mode 100644 src/observables/methods/copy-within.ts create mode 100644 src/observables/methods/delete-property.ts create mode 100644 src/observables/methods/fill.ts create mode 100644 src/observables/methods/pop.ts create mode 100644 src/observables/methods/push.ts create mode 100644 src/observables/methods/reverse.ts create mode 100644 src/observables/methods/set.ts create mode 100644 src/observables/methods/shift.ts create mode 100644 src/observables/methods/sort.ts create mode 100644 src/observables/methods/splice.ts create mode 100644 src/observables/methods/typed-set.ts create mode 100644 src/observables/methods/unshift.ts create mode 100644 src/observables/object.ts create mode 100644 src/observables/typed-array.ts create mode 100644 src/structs/change.ts diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml index db9f273..8699b4c 100644 --- a/.github/workflows/quality.yml +++ b/.github/workflows/quality.yml @@ -6,7 +6,7 @@ on: - main jobs: - quality_pipeline_lint: + quality_pipeline_lint_and_typecheck: runs-on: ubuntu-24.04 timeout-minutes: 5 @@ -25,6 +25,9 @@ jobs: - name: Lint run: npm run lint + - name: Type Check + run: npm run test-types + quality_pipeline_nodejs: runs-on: ubuntu-24.04 timeout-minutes: 5 diff --git a/ci/eslint.config.mjs b/ci/eslint.config.mjs index af98e0d..6440f1b 100644 --- a/ci/eslint.config.mjs +++ b/ci/eslint.config.mjs @@ -1,29 +1,23 @@ -import globals from 'globals'; -import path from 'node:path'; -import { fileURLToPath } from 'node:url'; import js from '@eslint/js'; -import { FlatCompat } from '@eslint/eslintrc'; - -const __filename = fileURLToPath(import.meta.url); -const __dirname = path.dirname(__filename); -const compat = new FlatCompat({ - baseDirectory: __dirname, - recommendedConfig: js.configs.recommended, - allConfig: js.configs.all -}); +import { defineConfig } from 'eslint/config'; +import tseslint from 'typescript-eslint'; +import globals from 'globals'; -export default [...compat.extends('eslint:recommended'), { - languageOptions: { - globals: { - ...globals.browser, - ...globals.node +export default defineConfig( + js.configs.recommended, + tseslint.configs.recommended, + { + languageOptions: { + globals: { + ...globals.browser, + ...globals.node + }, + ecmaVersion: 'latest', + sourceType: 'module' }, - - ecmaVersion: 'latest', - sourceType: 'module' - }, - - rules: { - 'no-shadow': 2 + rules: { + 'no-shadow': 'error', + semi: 'error' + } } -}]; \ No newline at end of file +); \ No newline at end of file diff --git a/ci/tools/build-utils.js b/ci/tools/build-utils.js index a40da00..d2e8c3d 100644 --- a/ci/tools/build-utils.js +++ b/ci/tools/build-utils.js @@ -8,80 +8,58 @@ import * as stdout from './stdout.js'; const SRC_DIR = 'src'; const DIST_DIR = 'dist'; +const MAIN_FILE = 'object-observer.ts'; +const ENTRY_POINT = path.join(SRC_DIR, MAIN_FILE); stdout.writeGreen('Starting the build...'); stdout.writeNewline(); -stdout.writeNewline(); try { await cleanDistDir(); await buildESModule(); - await buildCJSModule(); await buildCDNResources(); } catch (e) { console.error(e); } -stdout.writeGreen('... build done'); +stdout.writeGreen('... done'); stdout.writeNewline(); stdout.writeNewline(); async function cleanDistDir() { - stdout.write(`\tcleaning "dist"...`); + stdout.write(`- cleaning "dist"...`); await fs.rm(DIST_DIR, { recursive: true, force: true }); await fs.mkdir(DIST_DIR); - stdout.writeGreen('\tOK'); + stdout.writeGreen('\t\tOK'); stdout.writeNewline(); } async function buildESModule() { - stdout.write('\tbuilding ESM resources...'); + stdout.write('- building ESM resources...'); - await fs.copyFile(path.join(SRC_DIR, 'object-observer.d.ts'), path.join(DIST_DIR, 'object-observer.d.ts')); - await fs.copyFile(path.join(SRC_DIR, 'object-observer.js'), path.join(DIST_DIR, 'object-observer.js')); - await esbuild.build({ - entryPoints: [path.join(DIST_DIR, 'object-observer.js')], + const config = { + entryPoints: [ENTRY_POINT], + bundle: true, outdir: DIST_DIR, - minify: true, + format: 'esm', + minify: false, sourcemap: true, - sourcesContent: false, - outExtension: { '.js': '.min.js' } - }); - - stdout.writeGreen('\tOK'); - stdout.writeNewline(); -} - -async function buildCJSModule() { - stdout.write('\tbuilding CJS resources...'); - - const baseConfig = { - entryPoints: [path.join(SRC_DIR, 'object-observer.js')], - outdir: path.join(DIST_DIR, 'cjs'), - format: 'cjs', - outExtension: { '.js': '.cjs' } + sourcesContent: false }; - await esbuild.build(baseConfig); - await esbuild.build({ - ...baseConfig, - entryPoints: [path.join(DIST_DIR, 'cjs', 'object-observer.cjs')], - minify: true, - sourcemap: true, - sourcesContent: false, - outExtension: { '.js': '.min.cjs' } - }); + await esbuild.build(config); + await esbuild.build({ ...config, minify: true, outExtension: { '.js': '.min.js' } }); + // await fs.copyFile(path.join(SRC_DIR, 'object-observer.d.ts'), path.join(DIST_DIR, 'object-observer.d.ts')); stdout.writeGreen('\tOK'); stdout.writeNewline(); } async function buildCDNResources() { - stdout.write('\tbuilding CDN resources...'); + stdout.write('- building CDN resources...'); const CDN_DIR = path.join(DIST_DIR, 'cdn'); - await fs.mkdir(CDN_DIR); const files = (await fs.readdir(DIST_DIR)) @@ -92,7 +70,6 @@ async function buildCDNResources() { } const sriMap = await calcIntegrity(CDN_DIR); - await fs.writeFile('sri.json', JSON.stringify(sriMap, null, '\t'), { encoding: 'utf-8' }); stdout.writeGreen('\tOK'); diff --git a/docs/architecture.md b/docs/architecture.md new file mode 100644 index 0000000..7f0a594 --- /dev/null +++ b/docs/architecture.md @@ -0,0 +1,12 @@ +# Architecture + +```mermaid +classDiagram + class ObservableBase { + + } + + ObservableBase <|-- ObservableObject + ObservableBase <|-- ObservableArray + ObservableBase <|-- ObservableTypedArray +``` \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 0286cfa..f954d89 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,21 +17,13 @@ ], "license": "ISC", "devDependencies": { - "@eslint/eslintrc": "^3.3.1", "@eslint/js": "^9.39.1", "@gullerya/just-test": "^4.0.12", "esbuild": "^0.27.0", "eslint": "^9.39.1", - "globals": "^16.5.0" - } - }, - "node_modules/@aashutoshrathi/word-wrap": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz", - "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==", - "dev": true, - "engines": { - "node": ">=0.10.0" + "globals": "^16.5.0", + "typescript": "^5.9.3", + "typescript-eslint": "^8.46.4" } }, "node_modules/@esbuild/aix-ppc64": { @@ -509,9 +501,9 @@ } }, "node_modules/@eslint-community/regexpp": { - "version": "4.12.1", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", - "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", "dev": true, "license": "MIT", "engines": { @@ -533,17 +525,6 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, - "node_modules/@eslint/config-array/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, "node_modules/@eslint/config-array/node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -607,17 +588,6 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, "node_modules/@eslint/eslintrc/node_modules/globals": { "version": "14.0.0", "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", @@ -699,6 +669,21 @@ "url": "https://paypal.me/gullerya?locale.x=en_US" } }, + "node_modules/@gullerya/object-observer": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/@gullerya/object-observer/-/object-observer-6.1.4.tgz", + "integrity": "sha512-taINf8vEnq6iwXpX/tuOcXpT+3Y3H3Hqi9mH5v+DL/D52wYP8lc3OXgpxO8hb+UXwChEUw8n9Nz3Q/htRYl7Gg==", + "dev": true, + "funding": [ + { + "url": "https://paypal.me/gullerya?locale.x=en_US" + }, + { + "url": "https://tidelift.com/funding/github/npm/object-observer" + } + ], + "license": "ISC" + }, "node_modules/@humanfs/core": { "version": "0.19.1", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", @@ -710,38 +695,25 @@ } }, "node_modules/@humanfs/node": { - "version": "0.16.6", - "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz", - "integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==", + "version": "0.16.7", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", + "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", "dev": true, "license": "Apache-2.0", "dependencies": { "@humanfs/core": "^0.19.1", - "@humanwhocodes/retry": "^0.3.0" + "@humanwhocodes/retry": "^0.4.0" }, "engines": { "node": ">=18.18.0" } }, - "node_modules/@humanfs/node/node_modules/@humanwhocodes/retry": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz", - "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=18.18" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, "node_modules/@humanwhocodes/module-importer": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", "dev": true, + "license": "Apache-2.0", "engines": { "node": ">=12.22" }, @@ -805,10 +777,48 @@ "node": ">=12" } }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/@types/estree": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", - "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", "dev": true, "license": "MIT" }, @@ -819,6 +829,265 @@ "dev": true, "license": "MIT" }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.46.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.46.4.tgz", + "integrity": "sha512-R48VhmTJqplNyDxCyqqVkFSZIx1qX6PzwqgcXn1olLrzxcSBDlOsbtcnQuQhNtnNiJ4Xe5gREI1foajYaYU2Vg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "8.46.4", + "@typescript-eslint/type-utils": "8.46.4", + "@typescript-eslint/utils": "8.46.4", + "@typescript-eslint/visitor-keys": "8.46.4", + "graphemer": "^1.4.0", + "ignore": "^7.0.0", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.46.4", + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.46.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.46.4.tgz", + "integrity": "sha512-tK3GPFWbirvNgsNKto+UmB/cRtn6TZfyw0D6IKrW55n6Vbs7KJoZtI//kpTKzE/DUmmnAFD8/Ca46s7Obs92/w==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@typescript-eslint/scope-manager": "8.46.4", + "@typescript-eslint/types": "8.46.4", + "@typescript-eslint/typescript-estree": "8.46.4", + "@typescript-eslint/visitor-keys": "8.46.4", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.46.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.46.4.tgz", + "integrity": "sha512-nPiRSKuvtTN+no/2N1kt2tUh/HoFzeEgOm9fQ6XQk4/ApGqjx0zFIIaLJ6wooR1HIoozvj2j6vTi/1fgAz7UYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.46.4", + "@typescript-eslint/types": "^8.46.4", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.46.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.46.4.tgz", + "integrity": "sha512-tMDbLGXb1wC+McN1M6QeDx7P7c0UWO5z9CXqp7J8E+xGcJuUuevWKxuG8j41FoweS3+L41SkyKKkia16jpX7CA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.46.4", + "@typescript-eslint/visitor-keys": "8.46.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.46.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.46.4.tgz", + "integrity": "sha512-+/XqaZPIAk6Cjg7NWgSGe27X4zMGqrFqZ8atJsX3CWxH/jACqWnrWI68h7nHQld0y+k9eTTjb9r+KU4twLoo9A==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.46.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.46.4.tgz", + "integrity": "sha512-V4QC8h3fdT5Wro6vANk6eojqfbv5bpwHuMsBcJUJkqs2z5XnYhJzyz9Y02eUmF9u3PgXEUiOt4w4KHR3P+z0PQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.46.4", + "@typescript-eslint/typescript-estree": "8.46.4", + "@typescript-eslint/utils": "8.46.4", + "debug": "^4.3.4", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.46.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.46.4.tgz", + "integrity": "sha512-USjyxm3gQEePdUwJBFjjGNG18xY9A2grDVGuk7/9AkjIF1L+ZrVnwR5VAU5JXtUnBL/Nwt3H31KlRDaksnM7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.46.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.46.4.tgz", + "integrity": "sha512-7oV2qEOr1d4NWNmpXLR35LvCfOkTNymY9oyW+lUHkmCno7aOmIf/hMaydnJBUTBMRCOGZh8YjkFOc8dadEoNGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.46.4", + "@typescript-eslint/tsconfig-utils": "8.46.4", + "@typescript-eslint/types": "8.46.4", + "@typescript-eslint/visitor-keys": "8.46.4", + "debug": "^4.3.4", + "fast-glob": "^3.3.2", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.46.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.46.4.tgz", + "integrity": "sha512-AbSv11fklGXV6T28dp2Me04Uw90R2iJ30g2bgLz529Koehrmkbs1r7paFqr1vPCZi7hHwYxYtxfyQMRC8QaVSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.7.0", + "@typescript-eslint/scope-manager": "8.46.4", + "@typescript-eslint/types": "8.46.4", + "@typescript-eslint/typescript-estree": "8.46.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.46.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.46.4.tgz", + "integrity": "sha512-/++5CYLQqsO9HFGLI7APrxBJYo+5OCMpViuhV8q5/Qa3o5mMrF//eQHks+PXcsAVaLdn817fMuS7zqoXNNZGaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.46.4", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, "node_modules/acorn": { "version": "8.15.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", @@ -878,6 +1147,7 @@ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, + "license": "MIT", "dependencies": { "color-convert": "^2.0.1" }, @@ -899,7 +1169,32 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true + "dev": true, + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } }, "node_modules/callsites": { "version": "3.1.0", @@ -916,6 +1211,7 @@ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "dev": true, + "license": "MIT", "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -932,6 +1228,7 @@ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "dev": true, + "license": "MIT", "dependencies": { "color-name": "~1.1.4" }, @@ -943,13 +1240,15 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/cross-spawn": { "version": "7.0.6", @@ -980,6 +1279,7 @@ "url": "https://tidelift.com/funding/github/npm/data-tier" } ], + "license": "ISC", "dependencies": { "@gullerya/object-observer": "^6.0.4" } @@ -989,6 +1289,7 @@ "resolved": "https://registry.npmjs.org/data-tier-list/-/data-tier-list-2.2.1.tgz", "integrity": "sha512-KLpdHDY2PvR6lFUsHA5NVqfCNND/gUU/8Tf7sD0D44wFKhHXt04sVWlKk/kJfcr6U7qxBumG1cAnBPOJSz6s9g==", "dev": true, + "license": "ISC", "dependencies": { "data-tier": "^3.6.1" }, @@ -996,24 +1297,10 @@ "url": "https://paypal.me/gullerya?locale.x=en_US" } }, - "node_modules/data-tier/node_modules/@gullerya/object-observer": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/@gullerya/object-observer/-/object-observer-6.1.1.tgz", - "integrity": "sha512-bEfCYXWHbv3p10gnxTUGNOoTh96yQHl/uRCp0zXT+BgYJyV3Rte/bpFvBNp7SXYZvbsLjCi6jx6+/w7buNP0aw==", - "dev": true, - "funding": [ - { - "url": "https://paypal.me/gullerya?locale.x=en_US" - }, - { - "url": "https://tidelift.com/funding/github/npm/object-observer" - } - ] - }, "node_modules/debug": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", - "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "dev": true, "license": "MIT", "dependencies": { @@ -1032,7 +1319,8 @@ "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/eastasianwidth": { "version": "0.2.0", @@ -1095,6 +1383,7 @@ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", "dev": true, + "license": "MIT", "engines": { "node": ">=10" }, @@ -1193,21 +1482,12 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/eslint/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, "node_modules/eslint/node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dev": true, + "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" }, @@ -1234,10 +1514,11 @@ } }, "node_modules/esquery": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", - "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { "estraverse": "^5.1.0" }, @@ -1263,6 +1544,7 @@ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", "dev": true, + "license": "BSD-2-Clause", "engines": { "node": ">=4.0" } @@ -1284,6 +1566,36 @@ "dev": true, "license": "MIT" }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", @@ -1295,7 +1607,18 @@ "version": "2.0.6", "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", - "dev": true + "dev": true, + "license": "MIT" + }, + "node_modules/fastq": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } }, "node_modules/file-entry-cache": { "version": "8.0.0", @@ -1310,11 +1633,25 @@ "node": ">=16.0.0" } }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", "dev": true, + "license": "MIT", "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" @@ -1341,9 +1678,9 @@ } }, "node_modules/flatted": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.2.tgz", - "integrity": "sha512-AiwGJM8YcNOaobumgtng+6NHuOqC3A7MixFeDafM3X9cIUM+xUXoS5Vfgf+OihAYe20fxqNM9yPBXJzRtZ/4eA==", + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", "dev": true, "license": "ISC" }, @@ -1408,6 +1745,7 @@ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", "dev": true, + "license": "ISC", "dependencies": { "is-glob": "^4.0.3" }, @@ -1428,11 +1766,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true, + "license": "MIT" + }, "node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -1469,6 +1815,7 @@ "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.8.19" } @@ -1478,6 +1825,7 @@ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -1497,6 +1845,7 @@ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", "dev": true, + "license": "MIT", "dependencies": { "is-extglob": "^2.1.1" }, @@ -1504,6 +1853,16 @@ "node": ">=0.10.0" } }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -1528,9 +1887,9 @@ } }, "node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", "dev": true, "license": "MIT", "dependencies": { @@ -1558,7 +1917,8 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/keyv": { "version": "4.5.4", @@ -1575,6 +1935,7 @@ "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", "dev": true, + "license": "MIT", "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" @@ -1588,6 +1949,7 @@ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", "dev": true, + "license": "MIT", "dependencies": { "p-locate": "^5.0.0" }, @@ -1602,7 +1964,8 @@ "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/lru-cache": { "version": "11.2.2", @@ -1614,6 +1977,30 @@ "node": "20 || >=22" } }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, "node_modules/minimatch": { "version": "10.1.1", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.1.tgz", @@ -1651,20 +2038,22 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/optionator": { - "version": "0.9.3", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", - "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==", + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", "dev": true, + "license": "MIT", "dependencies": { - "@aashutoshrathi/word-wrap": "^1.2.3", "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", - "type-check": "^0.4.0" + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" }, "engines": { "node": ">= 0.8.0" @@ -1675,6 +2064,7 @@ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", "dev": true, + "license": "MIT", "dependencies": { "yocto-queue": "^0.1.0" }, @@ -1690,6 +2080,7 @@ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", "dev": true, + "license": "MIT", "dependencies": { "p-limit": "^3.0.2" }, @@ -1725,6 +2116,7 @@ "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -1756,6 +2148,19 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/playwright": { "version": "1.56.1", "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.56.1.tgz", @@ -1793,6 +2198,7 @@ "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.8.0" } @@ -1807,6 +2213,27 @@ "node": ">=6" } }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", @@ -1817,6 +2244,17 @@ "node": ">=4" } }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, "node_modules/rich-component": { "version": "1.8.0", "resolved": "https://registry.npmjs.org/rich-component/-/rich-component-1.8.0.tgz", @@ -1829,7 +2267,45 @@ { "url": "https://tidelift.com/funding/github/npm/sign-pad" } - ] + ], + "license": "ISC" + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } }, "node_modules/shebang-command": { "version": "2.0.0", @@ -1989,6 +2465,7 @@ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, + "license": "MIT", "dependencies": { "has-flag": "^4.0.0" }, @@ -1996,11 +2473,38 @@ "node": ">=8" } }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/ts-api-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", + "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", "dev": true, + "license": "MIT", "dependencies": { "prelude-ls": "^1.2.1" }, @@ -2008,6 +2512,45 @@ "node": ">= 0.8.0" } }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.46.4", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.46.4.tgz", + "integrity": "sha512-KALyxkpYV5Ix7UhvjTwJXZv76VWsHG+NjNlt/z+a17SOQSiOcBdUXdbJdyXi7RPxrBFECtFOiPwUJQusJuCqrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.46.4", + "@typescript-eslint/parser": "8.46.4", + "@typescript-eslint/typescript-estree": "8.46.4", + "@typescript-eslint/utils": "8.46.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, "node_modules/uri-js": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", @@ -2034,6 +2577,16 @@ "node": ">= 8" } }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/wrap-ansi": { "version": "8.1.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", @@ -2134,6 +2687,7 @@ "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", "dev": true, + "license": "MIT", "engines": { "node": ">=10" }, diff --git a/package.json b/package.json index fd69d20..8174679 100644 --- a/package.json +++ b/package.json @@ -61,7 +61,8 @@ ], "scripts": { "build": "node ./ci/tools/build-utils.js", - "lint": "eslint -c ./ci/eslint.config.mjs ./src/*.js ./tests/*.js ./ci/**/*.js", + "lint": "eslint -c ./ci/eslint.config.mjs ./src/* ./tests/*.js ./ci/**/*.js", + "test-types": "tsc ./src/object-observer.ts --noEmit --target es2022", "test": "node node_modules/@gullerya/just-test/bin/local-runner.js config_file=./tests/configs/tests-config-ci-node.json", "test:chromium": "node node_modules/@gullerya/just-test/bin/local-runner.js config_file=./tests/configs/tests-config-ci-chromium.json", "test:firefox": "node node_modules/@gullerya/just-test/bin/local-runner.js config_file=./tests/configs/tests-config-ci-firefox.json", @@ -71,12 +72,13 @@ "postversion": "git push && git push --tags" }, "devDependencies": { - "@eslint/eslintrc": "^3.3.1", "@eslint/js": "^9.39.1", "@gullerya/just-test": "^4.0.12", "esbuild": "^0.27.0", "eslint": "^9.39.1", - "globals": "^16.5.0" + "globals": "^16.5.0", + "typescript": "^5.9.3", + "typescript-eslint": "^8.46.4" }, "publishConfig": { "access": "public" diff --git a/src/constants.ts b/src/constants.ts new file mode 100644 index 0000000..d5572f9 --- /dev/null +++ b/src/constants.ts @@ -0,0 +1,6 @@ +export const oMetaKey = Symbol.for('object-observer-meta-key-0'); +export const INSERT = 'insert'; +export const UPDATE = 'update'; +export const DELETE = 'delete'; +export const REVERSE = 'reverse'; +export const SHUFFLE = 'shuffle'; \ No newline at end of file diff --git a/src/object-observer.d.ts b/src/object-observer.d.ts.tmp similarity index 97% rename from src/object-observer.d.ts rename to src/object-observer.d.ts.tmp index fb7d010..6ab8288 100644 --- a/src/object-observer.d.ts +++ b/src/object-observer.d.ts.tmp @@ -47,7 +47,7 @@ export interface ObservableOptions { async: boolean; } -export interface Observer { +export interface ChangesProcessor { (changes: Change[]): void; } @@ -60,8 +60,8 @@ export interface ObserverOptions { export interface Change { type: ChangeType; path: string[]; - value?: any; - oldValue?: any; + value?: unknown; + oldValue?: unknown; object: object; } diff --git a/src/object-observer.js b/src/object-observer.js deleted file mode 100644 index b4c9221..0000000 --- a/src/object-observer.js +++ /dev/null @@ -1,713 +0,0 @@ -export { Observable, ObjectObserver }; - -const - INSERT = 'insert', - UPDATE = 'update', - DELETE = 'delete', - REVERSE = 'reverse', - SHUFFLE = 'shuffle', - oMetaKey = Symbol.for('object-observer-meta-key-0'), - validObservableOptionKeys = { async: 1 }, - processObserveOptions = options => { - if (!options || typeof options !== 'object') { - return null; - } - - const result = {}; - const invalidOptions = []; - for (const [optName, optVal] of Object.entries(options)) { - if (optName === 'path') { - if (typeof optVal !== 'string' || optVal === '') { - throw new Error('"path" option, if/when provided, MUST be a non-empty string'); - } - result[optName] = optVal; - } else if (optName === 'pathsOf') { - if (options.path) { - throw new Error('"pathsOf" option MAY NOT be specified together with "path" option'); - } - if (typeof optVal !== 'string') { - throw new Error('"pathsOf" option, if/when provided, MUST be a string (MAY be empty)'); - } - result[optName] = options.pathsOf.split('.').filter(Boolean); - } else if (optName === 'pathsFrom') { - if (options.path || options.pathsOf) { - throw new Error('"pathsFrom" option MAY NOT be specified together with "path"/"pathsOf" option/s'); - } - if (typeof optVal !== 'string' || optVal === '') { - throw new Error('"pathsFrom" option, if/when provided, MUST be a non-empty string'); - } - result[optName] = optVal; - } else { - invalidOptions.push(optName); - } - } - if (invalidOptions.length) { - throw new Error(`'${invalidOptions.join(', ')}' is/are not a valid observer option/s`); - } - return result; - }, - prepareObject = (source, oMeta, visited) => { - const target = {}; - target[oMetaKey] = oMeta; - for (const key in source) { - target[key] = getObservedOf(source[key], key, oMeta, visited); - } - return target; - }, - prepareArray = (source, oMeta, visited) => { - let l = source.length; - const target = new Array(l); - target[oMetaKey] = oMeta; - for (let i = 0; i < l; i++) { - target[i] = getObservedOf(source[i], i, oMeta, visited); - } - return target; - }, - prepareTypedArray = (source, oMeta) => { - source[oMetaKey] = oMeta; - return source; - }, - filterChanges = (options, changes) => { - if (options === null) { - return changes; - } - - let result = changes; - if (options.path) { - const oPath = options.path; - result = changes.filter(change => - change.path.join('.') === oPath - ); - } else if (options.pathsOf) { - const oPathsOf = options.pathsOf; - const oPathsOfStr = oPathsOf.join('.'); - result = changes.filter(change => - (change.path.length === oPathsOf.length + 1 || - (change.path.length === oPathsOf.length && (change.type === REVERSE || change.type === SHUFFLE))) && - change.path.join('.').startsWith(oPathsOfStr) - ); - } else if (options.pathsFrom) { - const oPathsFrom = options.pathsFrom; - result = changes.filter(change => - change.path.join('.').startsWith(oPathsFrom) - ); - } - return result; - }, - callObserverSafe = (listener, changes) => { - try { - listener(changes); - } catch (e) { - console.error(`failed to notify listener ${listener} with ${changes}`, e); - } - }, - callObserversFromMT = function callObserversFromMT() { - const batches = this.batches; - this.batches = []; - for (const [listener, changes] of batches) { - callObserverSafe(listener, changes); - } - }, - callObservers = (oMeta, changes) => { - let currentObservable = oMeta; - let isAsync, observers, target, options, relevantChanges, i; - const l = changes.length; - do { - isAsync = currentObservable.options.async; - observers = currentObservable.observers; - i = observers.length; - while (i--) { - [target, options] = observers[i]; - relevantChanges = filterChanges(options, changes); - - if (relevantChanges.length) { - if (isAsync) { - // this is the async dispatch handling - if (currentObservable.batches.length === 0) { - queueMicrotask(callObserversFromMT.bind(currentObservable)); - } - let rb; - for (const b of currentObservable.batches) { - if (b[0] === target) { - rb = b; - break; - } - } - if (!rb) { - rb = [target, []]; - currentObservable.batches.push(rb); - } - Array.prototype.push.apply(rb[1], relevantChanges); - } else { - // this is the naive straight forward synchronous dispatch - callObserverSafe(target, relevantChanges); - } - } - } - - // cloning all the changes and notifying in context of parent - const parent = currentObservable.parent; - if (parent) { - for (let j = 0; j < l; j++) { - const change = changes[j]; - changes[j] = new Change( - change.type, - [currentObservable.ownKey, ...change.path], - change.value, - change.oldValue, - change.object - ); - } - currentObservable = parent; - } else { - currentObservable = null; - } - } while (currentObservable); - }, - getObservedOf = (item, key, parent, visited) => { - if (visited !== undefined && visited.has(item)) { - return null; - } else if (typeof item !== 'object' || item === null) { - return item; - } else if (Array.isArray(item)) { - return new ArrayOMeta({ target: item, ownKey: key, parent: parent, visited }).proxy; - } else if (ArrayBuffer.isView(item)) { - return new TypedArrayOMeta({ target: item, ownKey: key, parent: parent }).proxy; - } else if (item instanceof Date) { - return item; - } else { - return new ObjectOMeta({ target: item, ownKey: key, parent: parent, visited }).proxy; - } - }, - proxiedPop = function proxiedPop() { - const oMeta = this[oMetaKey], - target = oMeta.target, - poppedIndex = target.length - 1; - - let popResult = target.pop(); - if (popResult && typeof popResult === 'object') { - const tmpObserved = popResult[oMetaKey]; - if (tmpObserved) { - popResult = tmpObserved.detach(); - } - } - - const changes = [new Change(DELETE, [poppedIndex], undefined, popResult, this)]; - callObservers(oMeta, changes); - - return popResult; - }, - proxiedPush = function proxiedPush() { - const - oMeta = this[oMetaKey], - target = oMeta.target, - l = arguments.length, - pushContent = new Array(l), - initialLength = target.length; - - for (let i = 0; i < l; i++) { - pushContent[i] = getObservedOf(arguments[i], initialLength + i, oMeta); - } - const pushResult = Reflect.apply(target.push, target, pushContent); - - const changes = []; - for (let i = initialLength, j = target.length; i < j; i++) { - changes[i - initialLength] = new Change(INSERT, [i], target[i], undefined, this); - } - callObservers(oMeta, changes); - - return pushResult; - }, - proxiedShift = function proxiedShift() { - const - oMeta = this[oMetaKey], - target = oMeta.target; - let shiftResult, i, l, item, tmpObserved; - - shiftResult = target.shift(); - if (shiftResult && typeof shiftResult === 'object') { - tmpObserved = shiftResult[oMetaKey]; - if (tmpObserved) { - shiftResult = tmpObserved.detach(); - } - } - - // update indices of the remaining items - for (i = 0, l = target.length; i < l; i++) { - item = target[i]; - if (item && typeof item === 'object') { - tmpObserved = item[oMetaKey]; - if (tmpObserved) { - tmpObserved.ownKey = i; - } - } - } - - const changes = [new Change(DELETE, [0], undefined, shiftResult, this)]; - callObservers(oMeta, changes); - - return shiftResult; - }, - proxiedUnshift = function proxiedUnshift() { - const - oMeta = this[oMetaKey], - target = oMeta.target, - al = arguments.length, - unshiftContent = new Array(al); - - for (let i = 0; i < al; i++) { - unshiftContent[i] = getObservedOf(arguments[i], i, oMeta); - } - const unshiftResult = Reflect.apply(target.unshift, target, unshiftContent); - - for (let i = 0, l = target.length, item; i < l; i++) { - item = target[i]; - if (item && typeof item === 'object') { - const tmpObserved = item[oMetaKey]; - if (tmpObserved) { - tmpObserved.ownKey = i; - } - } - } - - // publish changes - const l = unshiftContent.length; - const changes = new Array(l); - for (let i = 0; i < l; i++) { - changes[i] = new Change(INSERT, [i], target[i], undefined, this); - } - callObservers(oMeta, changes); - - return unshiftResult; - }, - proxiedReverse = function proxiedReverse() { - const - oMeta = this[oMetaKey], - target = oMeta.target; - let i, l, item; - - target.reverse(); - for (i = 0, l = target.length; i < l; i++) { - item = target[i]; - if (item && typeof item === 'object') { - const tmpObserved = item[oMetaKey]; - if (tmpObserved) { - tmpObserved.ownKey = i; - } - } - } - - const changes = [new Change(REVERSE, [], undefined, undefined, this)]; - callObservers(oMeta, changes); - - return this; - }, - proxiedSort = function proxiedSort(comparator) { - const - oMeta = this[oMetaKey], - target = oMeta.target; - let i, l, item; - - target.sort(comparator); - for (i = 0, l = target.length; i < l; i++) { - item = target[i]; - if (item && typeof item === 'object') { - const tmpObserved = item[oMetaKey]; - if (tmpObserved) { - tmpObserved.ownKey = i; - } - } - } - - const changes = [new Change(SHUFFLE, [], undefined, undefined, this)]; - callObservers(oMeta, changes); - - return this; - }, - proxiedFill = function proxiedFill(filVal, start, end) { - const - oMeta = this[oMetaKey], - target = oMeta.target, - changes = [], - tarLen = target.length, - prev = target.slice(0); - start = start === undefined ? 0 : (start < 0 ? Math.max(tarLen + start, 0) : Math.min(start, tarLen)); - end = end === undefined ? tarLen : (end < 0 ? Math.max(tarLen + end, 0) : Math.min(end, tarLen)); - - if (start < tarLen && end > start) { - target.fill(filVal, start, end); - - let tmpObserved; - for (let i = start, item, tmpTarget; i < end; i++) { - item = target[i]; - target[i] = getObservedOf(item, i, oMeta); - if (i in prev) { - tmpTarget = prev[i]; - if (tmpTarget && typeof tmpTarget === 'object') { - tmpObserved = tmpTarget[oMetaKey]; - if (tmpObserved) { - tmpTarget = tmpObserved.detach(); - } - } - - changes.push(new Change(UPDATE, [i], target[i], tmpTarget, this)); - } else { - changes.push(new Change(INSERT, [i], target[i], undefined, this)); - } - } - - callObservers(oMeta, changes); - } - - return this; - }, - proxiedCopyWithin = function proxiedCopyWithin(dest, start, end) { - const - oMeta = this[oMetaKey], - target = oMeta.target, - tarLen = target.length; - dest = dest < 0 ? Math.max(tarLen + dest, 0) : dest; - start = start === undefined ? 0 : (start < 0 ? Math.max(tarLen + start, 0) : Math.min(start, tarLen)); - end = end === undefined ? tarLen : (end < 0 ? Math.max(tarLen + end, 0) : Math.min(end, tarLen)); - const len = Math.min(end - start, tarLen - dest); - - if (dest < tarLen && dest !== start && len > 0) { - const - prev = target.slice(0), - changes = []; - - target.copyWithin(dest, start, end); - - for (let i = dest, nItem, oItem, tmpObserved; i < dest + len; i++) { - // update newly placed observables, if any - nItem = target[i]; - if (nItem && typeof nItem === 'object') { - nItem = getObservedOf(nItem, i, oMeta); - target[i] = nItem; - } - - // detach overridden observables, if any - oItem = prev[i]; - if (oItem && typeof oItem === 'object') { - tmpObserved = oItem[oMetaKey]; - if (tmpObserved) { - oItem = tmpObserved.detach(); - } - } - - if (typeof nItem !== 'object' && nItem === oItem) { - continue; - } - changes.push(new Change(UPDATE, [i], nItem, oItem, this)); - } - - callObservers(oMeta, changes); - } - - return this; - }, - proxiedSplice = function proxiedSplice() { - const - oMeta = this[oMetaKey], - target = oMeta.target, - splLen = arguments.length, - spliceContent = new Array(splLen), - tarLen = target.length; - - // observify the newcomers - for (let i = 0; i < splLen; i++) { - spliceContent[i] = getObservedOf(arguments[i], i, oMeta); - } - - // calculate pointers - const - startIndex = splLen === 0 ? 0 : (spliceContent[0] < 0 ? tarLen + spliceContent[0] : spliceContent[0]), - removed = splLen < 2 ? tarLen - startIndex : spliceContent[1], - inserted = Math.max(splLen - 2, 0), - spliceResult = Reflect.apply(target.splice, target, spliceContent), - newTarLen = target.length; - - // reindex the paths - let tmpObserved; - for (let i = 0, item; i < newTarLen; i++) { - item = target[i]; - if (item && typeof item === 'object') { - tmpObserved = item[oMetaKey]; - if (tmpObserved) { - tmpObserved.ownKey = i; - } - } - } - - // detach removed objects - let i, l, item; - for (i = 0, l = spliceResult.length; i < l; i++) { - item = spliceResult[i]; - if (item && typeof item === 'object') { - tmpObserved = item[oMetaKey]; - if (tmpObserved) { - spliceResult[i] = tmpObserved.detach(); - } - } - } - - const changes = []; - let index; - for (index = 0; index < removed; index++) { - if (index < inserted) { - changes.push(new Change(UPDATE, [startIndex + index], target[startIndex + index], spliceResult[index], this)); - } else { - changes.push(new Change(DELETE, [startIndex + index], undefined, spliceResult[index], this)); - } - } - for (; index < inserted; index++) { - changes.push(new Change(INSERT, [startIndex + index], target[startIndex + index], undefined, this)); - } - callObservers(oMeta, changes); - - return spliceResult; - }, - proxiedTypedArraySet = function proxiedTypedArraySet(source, offset) { - const - oMeta = this[oMetaKey], - target = oMeta.target, - souLen = source.length, - prev = target.slice(0); - offset = offset || 0; - - target.set(source, offset); - const changes = new Array(souLen); - for (let i = offset; i < (souLen + offset); i++) { - changes[i - offset] = new Change(UPDATE, [i], target[i], prev[i], this); - } - - callObservers(oMeta, changes); - }, - proxiedArrayMethods = { - pop: proxiedPop, - push: proxiedPush, - shift: proxiedShift, - unshift: proxiedUnshift, - reverse: proxiedReverse, - sort: proxiedSort, - fill: proxiedFill, - copyWithin: proxiedCopyWithin, - splice: proxiedSplice - }, - proxiedTypedArrayMethods = { - reverse: proxiedReverse, - sort: proxiedSort, - fill: proxiedFill, - copyWithin: proxiedCopyWithin, - set: proxiedTypedArraySet - }; - -class Change { - constructor(type, path, value, oldValue, object) { - this.type = type; - this.path = path; - this.value = value; - this.oldValue = oldValue; - this.object = object; - } -} - -class OMetaBase { - constructor(properties, cloningFunction) { - const { target, parent, ownKey, visited = new Set() } = properties; - if (parent && ownKey !== undefined) { - this.parent = parent; - this.ownKey = ownKey; - } else { - this.parent = null; - this.ownKey = null; - } - visited.add(target); - const targetClone = cloningFunction(target, this, visited); - visited.delete(target); - this.observers = []; - this.revocable = Proxy.revocable(targetClone, this); - this.proxy = this.revocable.proxy; - this.target = targetClone; - this.options = this.processOptions(properties.options); - if (this.options.async) { - this.batches = []; - } - } - - processOptions(options) { - if (options) { - if (typeof options !== 'object') { - throw new Error(`Observable options if/when provided, MAY only be an object, got '${options}'`); - } - const invalidOptions = Object.keys(options).filter(option => !(option in validObservableOptionKeys)); - if (invalidOptions.length) { - throw new Error(`'${invalidOptions.join(', ')}' is/are not a valid Observable option/s`); - } - return Object.assign({}, options); - } else { - return {}; - } - } - - detach() { - this.parent = null; - return this.target; - } - - set(target, key, value) { - let oldValue = target[key]; - - if (value !== oldValue) { - const newValue = getObservedOf(value, key, this); - target[key] = newValue; - - if (oldValue && typeof oldValue === 'object') { - const tmpObserved = oldValue[oMetaKey]; - if (tmpObserved) { - oldValue = tmpObserved.detach(); - } - } - - const changes = oldValue === undefined - ? [new Change(INSERT, [key], newValue, undefined, this.proxy)] - : [new Change(UPDATE, [key], newValue, oldValue, this.proxy)]; - callObservers(this, changes); - } - - return true; - } - - deleteProperty(target, key) { - let oldValue = target[key]; - - delete target[key]; - - if (oldValue && typeof oldValue === 'object') { - const tmpObserved = oldValue[oMetaKey]; - if (tmpObserved) { - oldValue = tmpObserved.detach(); - } - } - - const changes = [new Change(DELETE, [key], undefined, oldValue, this.proxy)]; - callObservers(this, changes); - - return true; - } -} - -class ObjectOMeta extends OMetaBase { - constructor(properties) { - super(properties, prepareObject); - } -} - -class ArrayOMeta extends OMetaBase { - constructor(properties) { - super(properties, prepareArray); - } - - get(target, key) { - return proxiedArrayMethods[key] || target[key]; - } -} - -class TypedArrayOMeta extends OMetaBase { - constructor(properties) { - super(properties, prepareTypedArray); - } - - get(target, key) { - return proxiedTypedArrayMethods[key] || target[key]; - } -} - -const Observable = Object.freeze({ - from: (target, options) => { - if (!target || typeof target !== 'object') { - throw new Error('observable MAY ONLY be created from a non-null object'); - } else if (target[oMetaKey]) { - return target; - } else if (Array.isArray(target)) { - return new ArrayOMeta({ target: target, ownKey: null, parent: null, options: options }).proxy; - } else if (ArrayBuffer.isView(target)) { - return new TypedArrayOMeta({ target: target, ownKey: null, parent: null, options: options }).proxy; - } else if (target instanceof Date) { - throw new Error(`${target} found to be one of a non-observable types`); - } else { - return new ObjectOMeta({ target: target, ownKey: null, parent: null, options: options }).proxy; - } - }, - isObservable: input => { - return !!(input && input[oMetaKey]); - }, - observe: (observable, observer, options) => { - if (!Observable.isObservable(observable)) { - throw new Error(`invalid observable parameter`); - } - if (typeof observer !== 'function') { - throw new Error(`observer MUST be a function, got '${observer}'`); - } - - const observers = observable[oMetaKey].observers; - if (!observers.some(o => o[0] === observer)) { - observers.push([observer, processObserveOptions(options)]); - } else { - console.warn('observer may be bound to an observable only once; will NOT rebind'); - } - }, - unobserve: (observable, ...observers) => { - if (!Observable.isObservable(observable)) { - throw new Error(`invalid observable parameter`); - } - - const existingObs = observable[oMetaKey].observers; - let el = existingObs.length; - if (!el) { - return; - } - - if (!observers.length) { - existingObs.splice(0); - return; - } - - while (el) { - let i = observers.indexOf(existingObs[--el][0]); - if (i >= 0) { - existingObs.splice(el, 1); - } - } - } -}); - -class ObjectObserver { - #observer; - #targets; - - constructor(observer) { - this.#observer = observer; - this.#targets = new Set(); - Object.freeze(this); - } - - observe(target, options) { - const r = Observable.from(target); - Observable.observe(r, this.#observer, options); - this.#targets.add(r); - return r; - } - - unobserve(target) { - Observable.unobserve(target, this.#observer); - this.#targets.delete(target); - } - - disconnect() { - for (const t of this.#targets) { - Observable.unobserve(t, this.#observer); - } - this.#targets.clear(); - } -} diff --git a/src/object-observer.ts b/src/object-observer.ts new file mode 100644 index 0000000..a51b5e9 --- /dev/null +++ b/src/object-observer.ts @@ -0,0 +1,250 @@ +import { Change } from './structs/change.js'; +import { ObservableArray } from './observables/array.js'; +import { ObservableTypedArray } from './observables/typed-array.js'; +import { ObservableObject } from './observables/object.js'; +import { REVERSE, SHUFFLE } from './constants.js'; +import { oMetaKey } from './constants.js'; +import { ObservableBase } from './observables/abstract-base.js'; + +const + processObserveOptions = options => { + if (!options || typeof options !== 'object') { + return null; + } + + const result = {}; + const invalidOptions = []; + for (const [optName, optVal] of Object.entries(options)) { + if (optName === 'path') { + if (typeof optVal !== 'string' || optVal === '') { + throw new Error('"path" option, if/when provided, MUST be a non-empty string'); + } + result[optName] = optVal; + } else if (optName === 'pathsOf') { + if (options.path) { + throw new Error('"pathsOf" option MAY NOT be specified together with "path" option'); + } + if (typeof optVal !== 'string') { + throw new Error('"pathsOf" option, if/when provided, MUST be a string (MAY be empty)'); + } + result[optName] = options.pathsOf.split('.').filter(Boolean); + } else if (optName === 'pathsFrom') { + if (options.path || options.pathsOf) { + throw new Error('"pathsFrom" option MAY NOT be specified together with "path"/"pathsOf" option/s'); + } + if (typeof optVal !== 'string' || optVal === '') { + throw new Error('"pathsFrom" option, if/when provided, MUST be a non-empty string'); + } + result[optName] = optVal; + } else { + invalidOptions.push(optName); + } + } + if (invalidOptions.length) { + throw new Error(`'${invalidOptions.join(', ')}' is/are not a valid observer option/s`); + } + return result; + }, + filterChanges = (options, changes) => { + if (options === null) { + return changes; + } + + let result = changes; + if (options.path) { + const oPath = options.path; + result = changes.filter(change => + change.path.join('.') === oPath + ); + } else if (options.pathsOf) { + const oPathsOf = options.pathsOf; + const oPathsOfStr = oPathsOf.join('.'); + result = changes.filter(change => + (change.path.length === oPathsOf.length + 1 || + (change.path.length === oPathsOf.length && (change.type === REVERSE || change.type === SHUFFLE))) && + change.path.join('.').startsWith(oPathsOfStr) + ); + } else if (options.pathsFrom) { + const oPathsFrom = options.pathsFrom; + result = changes.filter(change => + change.path.join('.').startsWith(oPathsFrom) + ); + } + return result; + }, + callObserverSafe = (listener, changes) => { + try { + listener(changes); + } catch (e) { + console.error(`failed to notify listener ${listener} with ${changes}`, e); + } + }, + callObserversFromMT = function callObserversFromMT() { + const batches = this.batches; + this.batches = []; + for (const [listener, changes] of batches) { + callObserverSafe(listener, changes); + } + }; + +export function callObservers(oMeta: ObservableBase, changes: Change[]) { + let currentObservable: ObservableBase = oMeta; + let isAsync, observers, target, options, relevantChanges, i; + const l = changes.length; + do { + isAsync = currentObservable.async; + observers = currentObservable.observers; + i = observers.length; + while (i--) { + [target, options] = observers[i]; + relevantChanges = filterChanges(options, changes); + + if (relevantChanges.length) { + if (isAsync) { + // this is the async dispatch handling + if (currentObservable.batches.length === 0) { + queueMicrotask(callObserversFromMT.bind(currentObservable)); + } + let rb; + for (const b of currentObservable.batches) { + if (b[0] === target) { + rb = b; + break; + } + } + if (!rb) { + rb = [target, []]; + currentObservable.batches.push(rb); + } + Array.prototype.push.apply(rb[1], relevantChanges); + } else { + // this is the naive straight forward synchronous dispatch + callObserverSafe(target, relevantChanges); + } + } + } + + // cloning all the changes and notifying in context of parent + const parent = currentObservable.parent; + if (parent) { + for (let j = 0; j < l; j++) { + const change = changes[j]; + changes[j] = new Change( + change.type, + [currentObservable.ownKey, ...change.path], + change.value, + change.oldValue, + change.object + ); + } + currentObservable = parent; + } else { + break; + } + } while (currentObservable); +}; + +export function getObservedOf(item: unknown, key: string | symbol | number, parent: object, visited?: Set) { + if (visited !== undefined && visited.has(item)) { + return null; + } else if (typeof item !== 'object' || item === null) { + return item; + } else if (Array.isArray(item)) { + return new ObservableArray({ target: item, ownKey: key, parent: parent, visited }).proxy; + } else if (ArrayBuffer.isView(item)) { + return new ObservableTypedArray({ target: item, ownKey: key, parent: parent }).proxy; + } else if (item instanceof Date) { + return item; + } else { + return new ObservableObject({ target: item, ownKey: key, parent: parent, visited }).proxy; + } +}; + +export const Observable = Object.freeze({ + from: (target, options = undefined) => { + if (!target || typeof target !== 'object') { + throw new Error('observable MAY ONLY be created from a non-null object'); + } else if (target[oMetaKey]) { + return target; + } else if (Array.isArray(target)) { + return new ObservableArray({ target: target, ownKey: null, parent: null, options: options }).proxy; + } else if (ArrayBuffer.isView(target)) { + return new ObservableTypedArray({ target: target, ownKey: null, parent: null, options: options }).proxy; + } else if (target instanceof Date) { + throw new Error(`${target} found to be one of a non-observable types`); + } else { + return new ObservableObject({ target: target, ownKey: null, parent: null, options: options }).proxy; + } + }, + isObservable: input => { + return !!(input && input[oMetaKey]); + }, + observe: (observable, observer, options) => { + if (!Observable.isObservable(observable)) { + throw new Error(`invalid observable parameter`); + } + if (typeof observer !== 'function') { + throw new Error(`observer MUST be a function, got '${observer}'`); + } + + const observers = observable[oMetaKey].observers; + if (!observers.some(o => o[0] === observer)) { + observers.push([observer, processObserveOptions(options)]); + } else { + console.warn('observer may be bound to an observable only once; will NOT rebind'); + } + }, + unobserve: (observable, ...observers) => { + if (!Observable.isObservable(observable)) { + throw new Error(`invalid observable parameter`); + } + + const existingObs = observable[oMetaKey].observers; + let el = existingObs.length; + if (!el) { + return; + } + + if (!observers.length) { + existingObs.splice(0); + return; + } + + while (el) { + const i = observers.indexOf(existingObs[--el][0]); + if (i >= 0) { + existingObs.splice(el, 1); + } + } + } +}); + +export class ObjectObserver { + #observer; + #targets; + + constructor(observer) { + this.#observer = observer; + this.#targets = new Set(); + Object.freeze(this); + } + + observe(target, options) { + const r = Observable.from(target); + Observable.observe(r, this.#observer, options); + this.#targets.add(r); + return r; + } + + unobserve(target) { + Observable.unobserve(target, this.#observer); + this.#targets.delete(target); + } + + disconnect() { + for (const t of this.#targets) { + Observable.unobserve(t, this.#observer); + } + this.#targets.clear(); + } +} diff --git a/src/observables/abstract-base.ts b/src/observables/abstract-base.ts new file mode 100644 index 0000000..9e0d47c --- /dev/null +++ b/src/observables/abstract-base.ts @@ -0,0 +1,85 @@ +import { Change } from '../structs/change.js'; +import { proxiedDeleteProperty } from './methods/delete-property.js'; +import { proxiedSet } from './methods/set.js'; + +const validObservableOptionKeys = { async: 1, verifiers: 1 }; + +export interface ObservableOptions { + async?: boolean; + verifiers?: Array; +} +export type ChangesProcessor = (changes: Change[]) => void; + +export class ObservableBase implements ProxyHandler { + #parent: ObservableBase | null; + ownKey: string | null; + #target: object; + #proxy: unknown; + // eslint-disable-next-line no-unused-private-class-members + #revoke: () => void; + #async: boolean = false; + batches = []; + + #verifiers: Array = []; + #observers: Array = []; + + set = proxiedSet; + deleteProperty = proxiedDeleteProperty; + + constructor(properties) { + const { target, parent, ownKey, options, visited = new Set() } = properties; + if (parent && ownKey !== undefined) { + this.#parent = parent; + this.ownKey = ownKey; + } else { + this.#parent = null; + this.ownKey = null; + } + visited.add(target); + this.#target = this.observedGraphProcessor(target, this, visited); + visited.delete(target); + + const revocableProxy = Proxy.revocable(this.#target, this); + this.#proxy = revocableProxy.proxy; + this.#revoke = revocableProxy.revoke; + this.#processOptions(options); + } + + detach() { + this.#parent = null; + // this.#revoke(); + return this.#target; + } + + get parent(): ObservableBase | null { return this.#parent; } + get target(): object { return this.#target; } + get proxy(): unknown { return this.#proxy; } + get async(): boolean { return this.#async; } + get verifiers(): Array { return this.#verifiers; } + get observers(): Array { return this.#observers; } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + observedGraphProcessor(source: object, observableWrapper: ObservableBase, visited: Set): object { + throw new Error('observedGraphProcessor MUST be implemented in derived classes'); + } + + #processOptions(options: ObservableOptions | undefined): void { + if (!options) { + return; + } + + if (typeof options !== 'object') { + throw new Error(`Observable options if/when provided, MAY only be an object, got '${options}'`); + } + const invalidOptions = Object.keys(options).filter(option => !(option in validObservableOptionKeys)); + if (invalidOptions.length) { + throw new Error(`'${invalidOptions.join(', ')}' is/are not a valid Observable option/s`); + } + + this.#async = Boolean(options.async); + + if (Array.isArray(options.verifiers)) { + this.#verifiers.push(...options.verifiers); + } + } +} \ No newline at end of file diff --git a/src/observables/array.ts b/src/observables/array.ts new file mode 100644 index 0000000..c74ed89 --- /dev/null +++ b/src/observables/array.ts @@ -0,0 +1,41 @@ +import { ObservableBase } from './abstract-base.js'; +import { getObservedOf } from '../object-observer.js'; +import { oMetaKey } from '../constants.js'; +import proxiedCopyWithin from './methods/copy-within.js'; +import proxiedFill from './methods/fill.js'; +import proxiedPop from './methods/pop.js'; +import proxiedPush from './methods/push.js'; +import proxiedReverse from './methods/reverse.js'; +import proxiedShift from './methods/shift.js'; +import proxiedSort from './methods/sort.js'; +import proxiedSplice from './methods/splice.js'; +import proxiedUnshift from './methods/unshift.js'; + +const proxiedArrayMethods = { + copyWithin: proxiedCopyWithin, + fill: proxiedFill, + pop: proxiedPop, + push: proxiedPush, + reverse: proxiedReverse, + shift: proxiedShift, + sort: proxiedSort, + splice: proxiedSplice, + unshift: proxiedUnshift +}; + +export class ObservableArray extends ObservableBase { + + get(target: object, key: string): unknown { + return proxiedArrayMethods[key] || target[key]; + } + + observedGraphProcessor(source: Array, observableWrapper: ObservableBase, visited: Set): Array { + const arrayLength = source.length; + const target = new Array(arrayLength); + target[oMetaKey] = observableWrapper; + for (let i = 0; i < arrayLength; i++) { + target[i] = getObservedOf(source[i], i, observableWrapper, visited); + } + return target; + } +} \ No newline at end of file diff --git a/src/observables/methods/copy-within.ts b/src/observables/methods/copy-within.ts new file mode 100644 index 0000000..69c097e --- /dev/null +++ b/src/observables/methods/copy-within.ts @@ -0,0 +1,48 @@ +import { Change } from '../../structs/change.js'; +import { UPDATE, oMetaKey } from '../../constants.js'; +import { callObservers, getObservedOf } from '../../object-observer.js'; + +export default function proxiedCopyWithin(dest, start, end) { + const oMeta = this[oMetaKey]; + const target = oMeta.target; + const tarLen = target.length; + dest = dest < 0 ? Math.max(tarLen + dest, 0) : dest; + start = start === undefined ? 0 : (start < 0 ? Math.max(tarLen + start, 0) : Math.min(start, tarLen)); + end = end === undefined ? tarLen : (end < 0 ? Math.max(tarLen + end, 0) : Math.min(end, tarLen)); + const len = Math.min(end - start, tarLen - dest); + + if (dest < tarLen && dest !== start && len > 0) { + const + prev = target.slice(0), + changes = []; + + target.copyWithin(dest, start, end); + + for (let i = dest, nItem, oItem, tmpObserved; i < dest + len; i++) { + // update newly placed observables, if any + nItem = target[i]; + if (nItem && typeof nItem === 'object') { + nItem = getObservedOf(nItem, i, oMeta); + target[i] = nItem; + } + + // detach overridden observables, if any + oItem = prev[i]; + if (oItem && typeof oItem === 'object') { + tmpObserved = oItem[oMetaKey]; + if (tmpObserved) { + oItem = tmpObserved.detach(); + } + } + + if (typeof nItem !== 'object' && nItem === oItem) { + continue; + } + changes.push(new Change(UPDATE, [i], nItem, oItem, this)); + } + + callObservers(oMeta, changes); + } + + return this; +}; \ No newline at end of file diff --git a/src/observables/methods/delete-property.ts b/src/observables/methods/delete-property.ts new file mode 100644 index 0000000..8b7aa05 --- /dev/null +++ b/src/observables/methods/delete-property.ts @@ -0,0 +1,22 @@ +import { Change } from '../../structs/change.js'; +import { DELETE } from '../../constants.js'; +import { oMetaKey } from '../../constants.js'; +import { callObservers } from '../../object-observer.js'; + +export function proxiedDeleteProperty(target: object, key: string | symbol): boolean { + let oldValue = target[key]; + + delete target[key]; + + if (oldValue && typeof oldValue === 'object') { + const tmpObserved = oldValue[oMetaKey]; + if (tmpObserved) { + oldValue = tmpObserved.detach(); + } + } + + const changes = [new Change(DELETE, [key], undefined, oldValue, this.proxy)]; + callObservers(this, changes); + + return true; +}; \ No newline at end of file diff --git a/src/observables/methods/fill.ts b/src/observables/methods/fill.ts new file mode 100644 index 0000000..bdcdcb9 --- /dev/null +++ b/src/observables/methods/fill.ts @@ -0,0 +1,40 @@ +import { Change } from '../../structs/change.js'; +import { INSERT, UPDATE, oMetaKey } from '../../constants.js'; +import { callObservers, getObservedOf } from '../../object-observer.js'; + +export default function proxiedFill(filVal, start, end) { + const oMeta = this[oMetaKey]; + const target = oMeta.target; + const changes = []; + const tarLen = target.length; + const prev = target.slice(0); + start = start === undefined ? 0 : (start < 0 ? Math.max(tarLen + start, 0) : Math.min(start, tarLen)); + end = end === undefined ? tarLen : (end < 0 ? Math.max(tarLen + end, 0) : Math.min(end, tarLen)); + + if (start < tarLen && end > start) { + target.fill(filVal, start, end); + + let tmpObserved; + for (let i = start, item, tmpTarget; i < end; i++) { + item = target[i]; + target[i] = getObservedOf(item, i, oMeta); + if (i in prev) { + tmpTarget = prev[i]; + if (tmpTarget && typeof tmpTarget === 'object') { + tmpObserved = tmpTarget[oMetaKey]; + if (tmpObserved) { + tmpTarget = tmpObserved.detach(); + } + } + + changes.push(new Change(UPDATE, [i], target[i], tmpTarget, this)); + } else { + changes.push(new Change(INSERT, [i], target[i], undefined, this)); + } + } + + callObservers(oMeta, changes); + } + + return this; +}; \ No newline at end of file diff --git a/src/observables/methods/pop.ts b/src/observables/methods/pop.ts new file mode 100644 index 0000000..773be58 --- /dev/null +++ b/src/observables/methods/pop.ts @@ -0,0 +1,22 @@ +import { Change } from '../../structs/change.js'; +import { DELETE, oMetaKey } from '../../constants.js'; +import { callObservers } from '../../object-observer.js'; + +export default function proxiedPop() { + const oMeta = this[oMetaKey]; + const target = oMeta.target; + const poppedIndex = target.length - 1; + + let popResult = target.pop(); + if (popResult && typeof popResult === 'object') { + const tmpObserved = popResult[oMetaKey]; + if (tmpObserved) { + popResult = tmpObserved.detach(); + } + } + + const changes = [new Change(DELETE, [poppedIndex], undefined, popResult, this)]; + callObservers(oMeta, changes); + + return popResult; +} \ No newline at end of file diff --git a/src/observables/methods/push.ts b/src/observables/methods/push.ts new file mode 100644 index 0000000..28177b7 --- /dev/null +++ b/src/observables/methods/push.ts @@ -0,0 +1,24 @@ +import { Change } from '../../structs/change.js'; +import { INSERT, oMetaKey } from '../../constants.js'; +import { callObservers, getObservedOf } from '../../object-observer.js'; + +export default function proxiedPush(...pushItems: unknown[]) { + const oMeta = this[oMetaKey]; + const target = oMeta.target; + const pushLen = pushItems.length; + const pushContent = new Array(pushLen); + const initialLength = target.length; + + for (let i = 0; i < pushLen; i++) { + pushContent[i] = getObservedOf(pushItems[i], initialLength + i, oMeta); + } + const pushResult = Reflect.apply(target.push, target, pushContent); + + const changes = []; + for (let i = initialLength, j = target.length; i < j; i++) { + changes[i - initialLength] = new Change(INSERT, [i], target[i], undefined, this); + } + callObservers(oMeta, changes); + + return pushResult; +}; \ No newline at end of file diff --git a/src/observables/methods/reverse.ts b/src/observables/methods/reverse.ts new file mode 100644 index 0000000..86de037 --- /dev/null +++ b/src/observables/methods/reverse.ts @@ -0,0 +1,25 @@ +import { Change } from '../../structs/change.js'; +import { REVERSE, oMetaKey } from '../../constants.js'; +import { callObservers } from '../../object-observer.js'; + +export default function proxiedReverse() { + const oMeta = this[oMetaKey]; + const target = oMeta.target; + let i, l, item; + + target.reverse(); + for (i = 0, l = target.length; i < l; i++) { + item = target[i]; + if (item && typeof item === 'object') { + const tmpObserved = item[oMetaKey]; + if (tmpObserved) { + tmpObserved.ownKey = i; + } + } + } + + const changes = [new Change(REVERSE, [], undefined, undefined, this)]; + callObservers(oMeta, changes); + + return this; +}; \ No newline at end of file diff --git a/src/observables/methods/set.ts b/src/observables/methods/set.ts new file mode 100644 index 0000000..6eb653c --- /dev/null +++ b/src/observables/methods/set.ts @@ -0,0 +1,27 @@ +import { Change } from '../../structs/change.js'; +import { INSERT, UPDATE } from '../../constants.js'; +import { oMetaKey } from '../../constants.js'; +import { callObservers, getObservedOf } from '../../object-observer.js'; + +export function proxiedSet(target: object, key: string | symbol, value: unknown): boolean { + let oldValue = target[key]; + + if (value !== oldValue) { + const newValue = getObservedOf(value, key, this); + target[key] = newValue; + + if (oldValue && typeof oldValue === 'object') { + const tmpObserved = oldValue[oMetaKey]; + if (tmpObserved) { + oldValue = tmpObserved.detach(); + } + } + + const changes = oldValue === undefined + ? [new Change(INSERT, [key], newValue, undefined, this.proxy)] + : [new Change(UPDATE, [key], newValue, oldValue, this.proxy)]; + callObservers(this, changes); + } + + return true; +}; \ No newline at end of file diff --git a/src/observables/methods/shift.ts b/src/observables/methods/shift.ts new file mode 100644 index 0000000..21d3da4 --- /dev/null +++ b/src/observables/methods/shift.ts @@ -0,0 +1,34 @@ +import { Change } from '../../structs/change.js'; +import { DELETE, oMetaKey } from '../../constants.js'; +import { callObservers } from '../../object-observer.js'; + +export default function proxiedShift() { + const + oMeta = this[oMetaKey], + target = oMeta.target; + let shiftResult, i, l, item, tmpObserved; + + shiftResult = target.shift(); + if (shiftResult && typeof shiftResult === 'object') { + tmpObserved = shiftResult[oMetaKey]; + if (tmpObserved) { + shiftResult = tmpObserved.detach(); + } + } + + // update indices of the remaining items + for (i = 0, l = target.length; i < l; i++) { + item = target[i]; + if (item && typeof item === 'object') { + tmpObserved = item[oMetaKey]; + if (tmpObserved) { + tmpObserved.ownKey = i; + } + } + } + + const changes = [new Change(DELETE, [0], undefined, shiftResult, this)]; + callObservers(oMeta, changes); + + return shiftResult; +}; \ No newline at end of file diff --git a/src/observables/methods/sort.ts b/src/observables/methods/sort.ts new file mode 100644 index 0000000..cbf33b1 --- /dev/null +++ b/src/observables/methods/sort.ts @@ -0,0 +1,25 @@ +import { Change } from '../../structs/change.js'; +import { SHUFFLE, oMetaKey } from '../../constants.js'; +import { callObservers } from '../../object-observer.js'; + +export default function proxiedSort(comparator) { + const oMeta = this[oMetaKey]; + const target = oMeta.target; + let i, l, item; + + target.sort(comparator); + for (i = 0, l = target.length; i < l; i++) { + item = target[i]; + if (item && typeof item === 'object') { + const tmpObserved = item[oMetaKey]; + if (tmpObserved) { + tmpObserved.ownKey = i; + } + } + } + + const changes = [new Change(SHUFFLE, [], undefined, undefined, this)]; + callObservers(oMeta, changes); + + return this; +}; \ No newline at end of file diff --git a/src/observables/methods/splice.ts b/src/observables/methods/splice.ts new file mode 100644 index 0000000..95c3156 --- /dev/null +++ b/src/observables/methods/splice.ts @@ -0,0 +1,63 @@ +import { Change } from '../../structs/change.js'; +import { INSERT, DELETE, UPDATE, oMetaKey } from '../../constants.js'; +import { callObservers, getObservedOf } from '../../object-observer.js'; + +export default function proxiedSplice(...spliceItems: unknown[]) { + const oMeta = this[oMetaKey]; + const target = oMeta.target; + const splLen = spliceItems.length; + const spliceContent = new Array(splLen); + const tarLen = target.length; + + // observify the newcomers + for (let i = 0; i < splLen; i++) { + spliceContent[i] = getObservedOf(spliceItems[i], i, oMeta); + } + + // calculate pointers + const startIndex = splLen === 0 ? 0 : (spliceContent[0] < 0 ? tarLen + spliceContent[0] : spliceContent[0]); + const removed = splLen < 2 ? tarLen - startIndex : spliceContent[1]; + const inserted = Math.max(splLen - 2, 0); + const spliceResult: unknown[] = Reflect.apply(target.splice, target, spliceContent); + const newTarLen = target.length; + + // reindex the paths + let tmpObserved; + for (let i = 0, item; i < newTarLen; i++) { + item = target[i]; + if (item && typeof item === 'object') { + tmpObserved = item[oMetaKey]; + if (tmpObserved) { + tmpObserved.ownKey = i; + } + } + } + + // detach removed objects + let i, l, item; + for (i = 0, l = spliceResult.length; i < l; i++) { + item = spliceResult[i]; + if (item && typeof item === 'object') { + tmpObserved = item[oMetaKey]; + if (tmpObserved) { + spliceResult[i] = tmpObserved.detach(); + } + } + } + + const changes = []; + let index; + for (index = 0; index < removed; index++) { + if (index < inserted) { + changes.push(new Change(UPDATE, [startIndex + index], target[startIndex + index], spliceResult[index], this)); + } else { + changes.push(new Change(DELETE, [startIndex + index], undefined, spliceResult[index], this)); + } + } + for (; index < inserted; index++) { + changes.push(new Change(INSERT, [startIndex + index], target[startIndex + index], undefined, this)); + } + callObservers(oMeta, changes); + + return spliceResult; +}; \ No newline at end of file diff --git a/src/observables/methods/typed-set.ts b/src/observables/methods/typed-set.ts new file mode 100644 index 0000000..60a6286 --- /dev/null +++ b/src/observables/methods/typed-set.ts @@ -0,0 +1,19 @@ +import { Change } from '../../structs/change.js'; +import { UPDATE, oMetaKey } from '../../constants.js'; +import { callObservers } from '../../object-observer.js'; + +export default function proxiedTypedArraySet(source, offset) { + const oMeta = this[oMetaKey]; + const target = oMeta.target; + const souLen = source.length; + const prev = target.slice(0); + offset = offset || 0; + + target.set(source, offset); + const changes = new Array(souLen); + for (let i = offset; i < (souLen + offset); i++) { + changes[i - offset] = new Change(UPDATE, [i], target[i], prev[i], this); + } + + callObservers(oMeta, changes); +}; \ No newline at end of file diff --git a/src/observables/methods/unshift.ts b/src/observables/methods/unshift.ts new file mode 100644 index 0000000..7ca5661 --- /dev/null +++ b/src/observables/methods/unshift.ts @@ -0,0 +1,35 @@ +import { Change } from '../../structs/change.js'; +import { INSERT, oMetaKey } from '../../constants.js'; +import { callObservers, getObservedOf } from '../../object-observer.js'; + +export default function proxiedUnshift(...unshiftItems: unknown[]) { + const oMeta = this[oMetaKey]; + const target = oMeta.target; + const unshiftLen = unshiftItems.length; + const unshiftContent = new Array(unshiftLen); + + for (let i = 0; i < unshiftLen; i++) { + unshiftContent[i] = getObservedOf(unshiftItems[i], i, oMeta); + } + const unshiftResult = Reflect.apply(target.unshift, target, unshiftContent); + + for (let i = 0, l = target.length, item; i < l; i++) { + item = target[i]; + if (item && typeof item === 'object') { + const tmpObserved = item[oMetaKey]; + if (tmpObserved) { + tmpObserved.ownKey = i; + } + } + } + + // publish changes + const l = unshiftContent.length; + const changes = new Array(l); + for (let i = 0; i < l; i++) { + changes[i] = new Change(INSERT, [i], target[i], undefined, this); + } + callObservers(oMeta, changes); + + return unshiftResult; +}; \ No newline at end of file diff --git a/src/observables/object.ts b/src/observables/object.ts new file mode 100644 index 0000000..2207a66 --- /dev/null +++ b/src/observables/object.ts @@ -0,0 +1,15 @@ +import { ObservableBase } from './abstract-base.js'; +import { getObservedOf } from '../object-observer.js'; +import { oMetaKey } from '../constants.js'; + +export class ObservableObject extends ObservableBase { + + observedGraphProcessor(source: object, observableWrapper: ObservableBase, visited: Set): object { + const target = {}; + target[oMetaKey] = observableWrapper; + for (const key in source) { + target[key] = getObservedOf(source[key], key, observableWrapper, visited); + } + return target; + } +} \ No newline at end of file diff --git a/src/observables/typed-array.ts b/src/observables/typed-array.ts new file mode 100644 index 0000000..9b19982 --- /dev/null +++ b/src/observables/typed-array.ts @@ -0,0 +1,28 @@ +import { ObservableBase } from './abstract-base.js'; +import { oMetaKey } from '../constants.js'; +import proxiedCopyWithin from './methods/copy-within.js'; +import proxiedFill from './methods/fill.js'; +import proxiedReverse from './methods/reverse.js'; +import proxiedSort from './methods/sort.js'; +import proxiedTypedArraySet from './methods/typed-set.js'; + +const proxiedTypedArrayMethods = { + copyWithin: proxiedCopyWithin, + fill: proxiedFill, + reverse: proxiedReverse, + sort: proxiedSort, + set: proxiedTypedArraySet +}; + +export class ObservableTypedArray extends ObservableBase { + + get(target, key) { + return proxiedTypedArrayMethods[key] || target[key]; + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + observedGraphProcessor(source: Array, observableWrapper: ObservableBase, visited: Set): Array { + source[oMetaKey] = observableWrapper; + return source; + } +} \ No newline at end of file diff --git a/src/structs/change.ts b/src/structs/change.ts new file mode 100644 index 0000000..7165952 --- /dev/null +++ b/src/structs/change.ts @@ -0,0 +1,35 @@ +export class Change { + #type: string; + #path: Array; + #value: unknown; + #oldValue: unknown; + #object: object; + + constructor(type: string, path: Array, value: unknown, oldValue: unknown, object: object) { + this.#type = type; + this.#path = path; + this.#value = value; + this.#oldValue = oldValue; + this.#object = object; + } + + get type() { + return this.#type; + } + + get path() { + return this.#path; + } + + get value() { + return this.#value; + } + + get oldValue() { + return this.#oldValue; + } + + get object() { + return this.#object; + } +} \ No newline at end of file diff --git a/sri.json b/sri.json index cb438c4..8ed6322 100644 --- a/sri.json +++ b/sri.json @@ -1,5 +1,6 @@ { - "dist/cdn/object-observer.js": "sha512-Jc51tpJGoR6MHLupRabCfXEVroGC2M5QEH19brhsgJ42vJnsIBUgDg4CeaTES8jZfQTLwzPp5mjIXg64cdEzAg==", - "dist/cdn/object-observer.min.js": "sha512-jieNuEyCm4guZuELCk+tZ8ijFhxyw6cOhx4q8cimhqdNccson04GDZpGm5kISKIkI//xERXAep1bt5HWKLDDNw==", - "dist/cdn/object-observer.min.js.map": "sha512-oCCsAVsC1+BcyrA8KzysTfw0U1qH6HN3Z/HTzyBbJUc5aPRA4+W2Hp4HSwD2Z5CDbYyKFDKyle7YHLy0V8GA2Q==" + "dist/cdn/object-observer.js": "sha512-kfwV9sIrSLLxmBYbqq1oGCRpsIX1oeQ+Q/VtSuzHT8If/GUlglnrLKZHreETL0ZMNc0sTKOgyCIkBaNkY7fJCw==", + "dist/cdn/object-observer.js.map": "sha512-8H90S5+8QPfC4z+2yqOAaVcNYOREHWg/Yo3b0Y+537sdVCbNYNED51ftQz1VRh1sMLyjoCinffMCNy8Ys8I+wA==", + "dist/cdn/object-observer.min.js": "sha512-6tTRCfjQ7Ke7ebumBVgJLWzLLZV56dnRdv7ZgH6hvQA4ABS3gVPyaYBzpnHuer+CFs9K4OqW4lSa9JbH8WzQpw==", + "dist/cdn/object-observer.min.js.map": "sha512-GiVWpYxB6OVXtE33ut+LNE+HcHY9+ZxYOyc3Hir/PyszhEg8a03aI9aNHPCfyb/Qy2m7X2pL8N/CMY836xcfNw==" } \ No newline at end of file diff --git a/tests/api-base.js b/tests/api-base.js index 1d40e26..36168a0 100644 --- a/tests/api-base.js +++ b/tests/api-base.js @@ -1,6 +1,6 @@ import { test } from '@gullerya/just-test'; import { assert } from '@gullerya/just-test/assert'; -import { Observable } from '../src/object-observer.js'; +import { Observable } from '../dist/object-observer.js'; test('ensure Observable object has defined APIs', () => { assert.equal(typeof Observable, 'object'); diff --git a/tests/api-changes.js b/tests/api-changes.js index 5f14a3f..0fd617b 100644 --- a/tests/api-changes.js +++ b/tests/api-changes.js @@ -1,13 +1,13 @@ import { test } from '@gullerya/just-test'; import { assert } from '@gullerya/just-test/assert'; -import { Observable } from '../src/object-observer.js'; +import { Observable } from '../dist/object-observer.js'; // object // test('verify object - root - insert', () => { let c; const o = Observable.from({}); - Observable.observe(o, cs => { c = cs[0]; }) + Observable.observe(o, cs => { c = cs[0]; }); o.some = 'new'; assert.deepStrictEqual(c, { type: 'insert', path: ['some'], value: 'new', oldValue: undefined, object: o }); }); @@ -15,7 +15,7 @@ test('verify object - root - insert', () => { test('verify object - deep - insert', () => { let c; const o = Observable.from({ a: {} }); - Observable.observe(o, cs => { c = cs[0]; }) + Observable.observe(o, cs => { c = cs[0]; }); o.a.some = 'new'; assert.deepStrictEqual(c, { type: 'insert', path: ['a', 'some'], value: 'new', oldValue: undefined, object: o.a }); }); @@ -23,7 +23,7 @@ test('verify object - deep - insert', () => { test('verify object - root - update', () => { let c; const o = Observable.from({ p: 'old' }); - Observable.observe(o, cs => { c = cs[0]; }) + Observable.observe(o, cs => { c = cs[0]; }); o.p = 'new'; assert.deepStrictEqual(c, { type: 'update', path: ['p'], value: 'new', oldValue: 'old', object: o }); }); @@ -31,7 +31,7 @@ test('verify object - root - update', () => { test('verify object - deep - update', () => { let c; const o = Observable.from({ a: { p: 'old' } }); - Observable.observe(o, cs => { c = cs[0]; }) + Observable.observe(o, cs => { c = cs[0]; }); o.a.p = 'new'; assert.deepStrictEqual(c, { type: 'update', path: ['a', 'p'], value: 'new', oldValue: 'old', object: o.a }); }); @@ -39,7 +39,7 @@ test('verify object - deep - update', () => { test('verify object - root - delete', () => { let c; const o = Observable.from({ p: 'old' }); - Observable.observe(o, cs => { c = cs[0]; }) + Observable.observe(o, cs => { c = cs[0]; }); delete o.p; assert.deepStrictEqual(c, { type: 'delete', path: ['p'], value: undefined, oldValue: 'old', object: o }); }); @@ -47,7 +47,7 @@ test('verify object - root - delete', () => { test('verify object - deep - delete', () => { let c; const o = Observable.from({ a: { p: 'old' } }); - Observable.observe(o, cs => { c = cs[0]; }) + Observable.observe(o, cs => { c = cs[0]; }); delete o.a.p; assert.deepStrictEqual(c, { type: 'delete', path: ['a', 'p'], value: undefined, oldValue: 'old', object: o.a }); }); @@ -57,7 +57,7 @@ test('verify object - deep - delete', () => { test('verify array - root - insert', () => { let c; const o = Observable.from([]); - Observable.observe(o, cs => { c = cs[0]; }) + Observable.observe(o, cs => { c = cs[0]; }); o.push('new'); assert.deepStrictEqual(c, { type: 'insert', path: [0], value: 'new', oldValue: undefined, object: o }); }); @@ -65,7 +65,7 @@ test('verify array - root - insert', () => { test('verify array - deep - insert', () => { let c; const o = Observable.from([[]]); - Observable.observe(o, cs => { c = cs[0]; }) + Observable.observe(o, cs => { c = cs[0]; }); o[0].push('new'); assert.deepStrictEqual(c, { type: 'insert', path: [0, 0], value: 'new', oldValue: undefined, object: o[0] }); }); @@ -73,7 +73,7 @@ test('verify array - deep - insert', () => { test('verify array - root - update', () => { let c; const o = Observable.from(['old']); - Observable.observe(o, cs => { c = cs[0]; }) + Observable.observe(o, cs => { c = cs[0]; }); o[0] = 'new'; assert.deepStrictEqual(c, { type: 'update', path: ['0'], value: 'new', oldValue: 'old', object: o }); }); @@ -81,7 +81,7 @@ test('verify array - root - update', () => { test('verify array - deep - update', () => { let c; const o = Observable.from([['old']]); - Observable.observe(o, cs => { c = cs[0]; }) + Observable.observe(o, cs => { c = cs[0]; }); o[0][0] = 'new'; assert.deepStrictEqual(c, { type: 'update', path: [0, '0'], value: 'new', oldValue: 'old', object: o[0] }); }); @@ -89,7 +89,7 @@ test('verify array - deep - update', () => { test('verify array - root - delete', () => { let c; const o = Observable.from(['old']); - Observable.observe(o, cs => { c = cs[0]; }) + Observable.observe(o, cs => { c = cs[0]; }); o.pop(); assert.deepStrictEqual(c, { type: 'delete', path: [0], value: undefined, oldValue: 'old', object: o }); }); @@ -97,7 +97,7 @@ test('verify array - root - delete', () => { test('verify array - deep - delete', () => { let c; const o = Observable.from([['old']]); - Observable.observe(o, cs => { c = cs[0]; }) + Observable.observe(o, cs => { c = cs[0]; }); o[0].pop(); assert.deepStrictEqual(c, { type: 'delete', path: [0, 0], value: undefined, oldValue: 'old', object: o[0] }); }); diff --git a/tests/browser-host-objects.js b/tests/browser-host-objects.js index 4cf3c7b..ef50674 100644 --- a/tests/browser-host-objects.js +++ b/tests/browser-host-objects.js @@ -1,6 +1,6 @@ import { test } from '@gullerya/just-test'; import { assert } from '@gullerya/just-test/assert'; -import { Observable } from '../src/object-observer.js'; +import { Observable } from '../dist/object-observer.js'; test('test DOMStringMap', () => { const diff --git a/tests/cross-instance.js b/tests/cross-instance.js index 8ac2f2c..7cb9cca 100644 --- a/tests/cross-instance.js +++ b/tests/cross-instance.js @@ -1,7 +1,7 @@ import { test } from '@gullerya/just-test'; import { assert } from '@gullerya/just-test/assert'; -import { Observable as O1, ObjectObserver as OO1 } from '../src/object-observer.js?1'; -import { Observable as O2, ObjectObserver as OO2 } from '../src/object-observer.js?2'; +import { Observable as O1, ObjectObserver as OO1 } from '../dist/object-observer.js?key=1'; +import { Observable as O2, ObjectObserver as OO2 } from '../dist/object-observer.js?key=2'; test('Observable.isObservable interoperable', () => { assert.notEqual(O1, O2); diff --git a/tests/listeners.js b/tests/listeners.js index 44f1816..932b00b 100644 --- a/tests/listeners.js +++ b/tests/listeners.js @@ -1,6 +1,6 @@ import { test } from '@gullerya/just-test'; import { assert } from '@gullerya/just-test/assert'; -import { Observable } from '../src/object-observer.js'; +import { Observable } from '../dist/object-observer.js'; test('test listeners invocation - single listener', () => { const oo = Observable.from({}); diff --git a/tests/object-generics.js b/tests/object-generics.js index 1a16bf6..40f7157 100644 --- a/tests/object-generics.js +++ b/tests/object-generics.js @@ -1,6 +1,6 @@ import { test } from '@gullerya/just-test'; import { assert } from '@gullerya/just-test/assert'; -import { Observable } from '../src/object-observer.js'; +import { Observable } from '../dist/object-observer.js'; test('Object.seal - further extensions should fail', () => { const oo = Observable.from({ propA: 'a', propB: 'b' }); diff --git a/tests/object-observer-api.js b/tests/object-observer-api.js index af054b6..0fb7843 100644 --- a/tests/object-observer-api.js +++ b/tests/object-observer-api.js @@ -1,6 +1,6 @@ import { test } from '@gullerya/just-test'; import { assert } from '@gullerya/just-test/assert'; -import { ObjectObserver, Observable } from '../src/object-observer.js'; +import { ObjectObserver, Observable } from '../dist/object-observer.js'; test('ensure ObjectObserver constructable', () => { assert.isTrue(typeof ObjectObserver === 'function'); diff --git a/tests/object-observer-arrays-copy-within.js b/tests/object-observer-arrays-copy-within.js index ac9febb..d5656c8 100644 --- a/tests/object-observer-arrays-copy-within.js +++ b/tests/object-observer-arrays-copy-within.js @@ -1,6 +1,6 @@ import { test } from '@gullerya/just-test'; import { assert } from '@gullerya/just-test/assert'; -import { Observable } from '../src/object-observer.js'; +import { Observable } from '../dist/object-observer.js'; test('array copyWithin - primitives', () => { const diff --git a/tests/object-observer-arrays-typed.js b/tests/object-observer-arrays-typed.js index 45631ab..87eb5ab 100644 --- a/tests/object-observer-arrays-typed.js +++ b/tests/object-observer-arrays-typed.js @@ -1,6 +1,6 @@ import { test } from '@gullerya/just-test'; import { assert } from '@gullerya/just-test/assert'; -import { Observable } from '../src/object-observer.js'; +import { Observable } from '../dist/object-observer.js'; test('typed array reverse - Int8Array', () => { const diff --git a/tests/object-observer-arrays.js b/tests/object-observer-arrays.js index 189f83f..3f33dc8 100644 --- a/tests/object-observer-arrays.js +++ b/tests/object-observer-arrays.js @@ -1,6 +1,6 @@ import { test } from '@gullerya/just-test'; import { assert } from '@gullerya/just-test/assert'; -import { Observable } from '../src/object-observer.js'; +import { Observable } from '../dist/object-observer.js'; test('array push - primitives', () => { const diff --git a/tests/object-observer-native-objects-to-skip.js b/tests/object-observer-native-objects-to-skip.js index 62978fd..a2bd50c 100644 --- a/tests/object-observer-native-objects-to-skip.js +++ b/tests/object-observer-native-objects-to-skip.js @@ -1,6 +1,6 @@ import { test } from '@gullerya/just-test'; import { assert } from '@gullerya/just-test/assert'; -import { Observable } from '../src/object-observer.js'; +import { Observable } from '../dist/object-observer.js'; test('creating observable from non-observable should throw an error', () => { const objectsToTest = [ diff --git a/tests/object-observer-objects-async.js b/tests/object-observer-objects-async.js index 0dc43e1..750dd59 100644 --- a/tests/object-observer-objects-async.js +++ b/tests/object-observer-objects-async.js @@ -1,7 +1,7 @@ import { test } from '@gullerya/just-test'; import { assert } from '@gullerya/just-test/assert'; import { waitNextTask } from '@gullerya/just-test/timing'; -import { Observable } from '../src/object-observer.js'; +import { Observable } from '../dist/object-observer.js'; test('multiple continuous mutations', async () => { const diff --git a/tests/object-observer-objects-circular.js b/tests/object-observer-objects-circular.js index 166c433..ef24712 100644 --- a/tests/object-observer-objects-circular.js +++ b/tests/object-observer-objects-circular.js @@ -1,6 +1,6 @@ import { test } from '@gullerya/just-test'; import { assert } from '@gullerya/just-test/assert'; -import { Observable } from '../src/object-observer.js'; +import { Observable } from '../dist/object-observer.js'; test('subgraph object pointing to the top parent', () => { const o = { prop: 'text' }; diff --git a/tests/object-observer-objects-same-refs.js b/tests/object-observer-objects-same-refs.js index d68c50a..bc7cf75 100644 --- a/tests/object-observer-objects-same-refs.js +++ b/tests/object-observer-objects-same-refs.js @@ -1,6 +1,6 @@ import { test } from '@gullerya/just-test'; import { assert } from '@gullerya/just-test/assert'; -import { Observable } from '../src/object-observer.js'; +import { Observable } from '../dist/object-observer.js'; test('subgraph objects pointing to the same object few times', { skip: true }, () => { const childObj = { prop: 'A' }; diff --git a/tests/object-observer-objects.js b/tests/object-observer-objects.js index 4eb2e3b..810761a 100644 --- a/tests/object-observer-objects.js +++ b/tests/object-observer-objects.js @@ -1,6 +1,6 @@ import { test } from '@gullerya/just-test'; import { assert } from '@gullerya/just-test/assert'; -import { Observable } from '../src/object-observer.js'; +import { Observable } from '../dist/object-observer.js'; test('creating observable leaves original object as is', () => { const person = { @@ -42,7 +42,7 @@ test('plain object operations', () => { name: 'name', age: 7, address: null - } + }; const events = [], tmpAddress = { street: 'some' }; diff --git a/tests/object-observer-subgraphs.js b/tests/object-observer-subgraphs.js index ff23b37..d622d69 100644 --- a/tests/object-observer-subgraphs.js +++ b/tests/object-observer-subgraphs.js @@ -1,6 +1,6 @@ import { test } from '@gullerya/just-test'; import { assert } from '@gullerya/just-test/assert'; -import { Observable } from '../src/object-observer.js'; +import { Observable } from '../dist/object-observer.js'; test('inner object from observable should fire events as usual', () => { const diff --git a/tests/observable-nested.js b/tests/observable-nested.js index 5ea291a..5165d87 100644 --- a/tests/observable-nested.js +++ b/tests/observable-nested.js @@ -1,6 +1,6 @@ import { test } from '@gullerya/just-test'; import { assert } from '@gullerya/just-test/assert'; -import { Observable } from '../src/object-observer.js'; +import { Observable } from '../dist/object-observer.js'; test('nested of observable should be observable too', () => { const oo = Observable.from({ @@ -97,7 +97,7 @@ test('nested observable should handle errors', () => { city: 'city' } } - }) + }); const oou = Observable.from(oo.user); assert.throws( () => Observable.observe(oou, 'invalid observer'), @@ -135,7 +135,7 @@ test('nested observable should provide correct path (relative to self)', () => { city: 'city' } } - }) + }); const oou = Observable.from(oo.user), ooua = Observable.from(oo.user.address), diff --git a/tests/observe-specific-paths.js b/tests/observe-specific-paths.js index 7dc81bf..a6739c5 100644 --- a/tests/observe-specific-paths.js +++ b/tests/observe-specific-paths.js @@ -1,6 +1,6 @@ import { test } from '@gullerya/just-test'; import { assert } from '@gullerya/just-test/assert'; -import { Observable } from '../src/object-observer.js'; +import { Observable } from '../dist/object-observer.js'; test('baseline - negative - path not a string', () => { const oo = Observable.from({}); diff --git a/tests/reassignment-of-equals.js b/tests/reassignment-of-equals.js index 6246f5a..f39bc8d 100644 --- a/tests/reassignment-of-equals.js +++ b/tests/reassignment-of-equals.js @@ -1,6 +1,6 @@ import { test } from '@gullerya/just-test'; import { assert } from '@gullerya/just-test/assert'; -import { Observable } from '../src/object-observer.js'; +import { Observable } from '../dist/object-observer.js'; test('boolean', () => { const oo = Observable.from({ p: true }); diff --git a/tests/revokation.js b/tests/revokation.js index f4cc0cf..9a2407f 100644 --- a/tests/revokation.js +++ b/tests/revokation.js @@ -1,6 +1,6 @@ import { test } from '@gullerya/just-test'; import { assert } from '@gullerya/just-test/assert'; -import { Observable } from '../src/object-observer.js'; +import { Observable } from '../dist/object-observer.js'; test('test revokation of replaced objects - simple set', () => { const og = Observable.from({ diff --git a/tests/unobserve.js b/tests/unobserve.js index d74718a..67e8b75 100644 --- a/tests/unobserve.js +++ b/tests/unobserve.js @@ -1,6 +1,6 @@ import { test } from '@gullerya/just-test'; import { assert } from '@gullerya/just-test/assert'; -import { Observable } from '../src/object-observer.js'; +import { Observable } from '../dist/object-observer.js'; test('test unobserve - single observer - explicit unobserve', () => { const diff --git a/tests/workers/perf-async-test-a.js b/tests/workers/perf-async-test-a.js index e0d4162..b5be4b9 100644 --- a/tests/workers/perf-async-test-a.js +++ b/tests/workers/perf-async-test-a.js @@ -1,4 +1,4 @@ -import { Observable } from '../../src/object-observer.js'; +import { Observable } from '../../dist/object-observer.js'; export default async setup => { const { diff --git a/tests/workers/perf-async-test-b.js b/tests/workers/perf-async-test-b.js index 3ad190d..d27aacd 100644 --- a/tests/workers/perf-async-test-b.js +++ b/tests/workers/perf-async-test-b.js @@ -1,4 +1,4 @@ -import { Observable } from '../../src/object-observer.js'; +import { Observable } from '../../dist/object-observer.js'; export default async setup => { const { diff --git a/tests/workers/perf-sync-test-a.js b/tests/workers/perf-sync-test-a.js index b12421b..c91ffcb 100644 --- a/tests/workers/perf-sync-test-a.js +++ b/tests/workers/perf-sync-test-a.js @@ -1,4 +1,4 @@ -import { Observable } from '../../src/object-observer.js'; +import { Observable } from '../../dist/object-observer.js'; export default setup => { const { diff --git a/tests/workers/perf-sync-test-b.js b/tests/workers/perf-sync-test-b.js index eb41e41..ae43b14 100644 --- a/tests/workers/perf-sync-test-b.js +++ b/tests/workers/perf-sync-test-b.js @@ -1,4 +1,4 @@ -import { Observable } from '../../src/object-observer.js'; +import { Observable } from '../../dist/object-observer.js'; export default setup => { const { From 8ef13badfeb863fbb6e24dd90e27dc816a855136 Mon Sep 17 00:00:00 2001 From: Yuri Guller Date: Sat, 15 Nov 2025 21:37:12 +0200 Subject: [PATCH 02/23] add quality for release branches --- .github/workflows/quality.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml index 8699b4c..4d00aca 100644 --- a/.github/workflows/quality.yml +++ b/.github/workflows/quality.yml @@ -4,6 +4,7 @@ on: pull_request: branches: - main + - release-* jobs: quality_pipeline_lint_and_typecheck: From e27fb71cd66e5ee320b2b224f6d6f6e266663d43 Mon Sep 17 00:00:00 2001 From: Yuri Guller Date: Sat, 15 Nov 2025 21:39:58 +0200 Subject: [PATCH 03/23] must build before test --- .github/workflows/quality.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml index 4d00aca..a357e6b 100644 --- a/.github/workflows/quality.yml +++ b/.github/workflows/quality.yml @@ -72,6 +72,9 @@ jobs: - name: Install run: npm ci && npx playwright install --with-deps chromium + - name: Build + run: npm run build + - name: Test run: npm run test:chromium @@ -102,6 +105,8 @@ jobs: - name: Install run: npm ci && npx playwright install --with-deps firefox + - name: Build + run: npm run build - name: Test run: npm run test:firefox @@ -126,6 +131,8 @@ jobs: - name: Install run: npm ci && npx playwright install --with-deps webkit + - name: Build + run: npm run build - name: Test run: npm run test:webkit From 7b0f61aceef2acc308fe60b7725d1aa0fed32ab4 Mon Sep 17 00:00:00 2001 From: Yuri Guller Date: Sat, 15 Nov 2025 21:52:31 +0200 Subject: [PATCH 04/23] fix cov --- .github/workflows/quality.yml | 2 ++ tests/configs/tests-config-ci-chromium.json | 2 +- tests/configs/tests-config-ci-firefox.json | 6 ------ tests/configs/tests-config-ci-node.json | 2 +- tests/configs/tests-config-ci-webkit.json | 6 ------ 5 files changed, 4 insertions(+), 14 deletions(-) diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml index a357e6b..c6fc4b3 100644 --- a/.github/workflows/quality.yml +++ b/.github/workflows/quality.yml @@ -107,6 +107,7 @@ jobs: - name: Build run: npm run build + - name: Test run: npm run test:firefox @@ -133,6 +134,7 @@ jobs: - name: Build run: npm run build + - name: Test run: npm run test:webkit diff --git a/tests/configs/tests-config-ci-chromium.json b/tests/configs/tests-config-ci-chromium.json index e15d9fd..bdfef37 100644 --- a/tests/configs/tests-config-ci-chromium.json +++ b/tests/configs/tests-config-ci-chromium.json @@ -20,7 +20,7 @@ }, "coverage": { "include": [ - "./src/**/*.js" + "./dist/**/*.js" ] } } diff --git a/tests/configs/tests-config-ci-firefox.json b/tests/configs/tests-config-ci-firefox.json index 2b8e0b6..d853c03 100644 --- a/tests/configs/tests-config-ci-firefox.json +++ b/tests/configs/tests-config-ci-firefox.json @@ -5,12 +5,6 @@ "type": "firefox", "executors": { "type": "iframe" - }, - "importmap": { - "imports": { - "@gullerya/just-test": "/libs/@gullerya/just-test/bin/runner/just-test.js", - "@gullerya/just-test/assert": "/libs/@gullerya/just-test/bin/common/assert-utils.js" - } } }, "tests": { diff --git a/tests/configs/tests-config-ci-node.json b/tests/configs/tests-config-ci-node.json index d87a427..43636a1 100644 --- a/tests/configs/tests-config-ci-node.json +++ b/tests/configs/tests-config-ci-node.json @@ -16,7 +16,7 @@ }, "coverage": { "include": [ - "./src/**/*.js" + "./dist/**/*.js" ] } } diff --git a/tests/configs/tests-config-ci-webkit.json b/tests/configs/tests-config-ci-webkit.json index 4412d41..e368516 100644 --- a/tests/configs/tests-config-ci-webkit.json +++ b/tests/configs/tests-config-ci-webkit.json @@ -5,12 +5,6 @@ "type": "webkit", "executors": { "type": "iframe" - }, - "importmap": { - "imports": { - "@gullerya/just-test": "/libs/@gullerya/just-test/bin/runner/just-test.js", - "@gullerya/just-test/assert": "/libs/@gullerya/just-test/bin/common/assert-utils.js" - } } }, "tests": { From 8ac4513db3959290ab406b08364c8dad576e0df8 Mon Sep 17 00:00:00 2001 From: Yuri Guller Date: Sat, 15 Nov 2025 22:27:40 +0200 Subject: [PATCH 05/23] wuip --- sri.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/sri.json b/sri.json index 8ed6322..78b7302 100644 --- a/sri.json +++ b/sri.json @@ -1,6 +1,6 @@ { - "dist/cdn/object-observer.js": "sha512-kfwV9sIrSLLxmBYbqq1oGCRpsIX1oeQ+Q/VtSuzHT8If/GUlglnrLKZHreETL0ZMNc0sTKOgyCIkBaNkY7fJCw==", - "dist/cdn/object-observer.js.map": "sha512-8H90S5+8QPfC4z+2yqOAaVcNYOREHWg/Yo3b0Y+537sdVCbNYNED51ftQz1VRh1sMLyjoCinffMCNy8Ys8I+wA==", + "dist/cdn/object-observer.js": "sha512-485VTA8tRG3htcli24/0KR2ssY8NZ1EGBIDKLvQwsISREupDwoUB/GcgbBJqcv1pq74ucn66oVO82/EAz6xn6Q==", + "dist/cdn/object-observer.js.map": "sha512-keZgvbex5FYhK+PoF9tb9wd2+CmFzJ0Ct1EaPuWXH/O8IChCXDWOqJxqU/S9PNVoYSKxZtC+YyRynCFj7tL6vA==", "dist/cdn/object-observer.min.js": "sha512-6tTRCfjQ7Ke7ebumBVgJLWzLLZV56dnRdv7ZgH6hvQA4ABS3gVPyaYBzpnHuer+CFs9K4OqW4lSa9JbH8WzQpw==", - "dist/cdn/object-observer.min.js.map": "sha512-GiVWpYxB6OVXtE33ut+LNE+HcHY9+ZxYOyc3Hir/PyszhEg8a03aI9aNHPCfyb/Qy2m7X2pL8N/CMY836xcfNw==" + "dist/cdn/object-observer.min.js.map": "sha512-Of+hrbmHey8BJ51a3hKGKqMdhHEDjctw4iDkBpyVljLksUj4/v/S/anPMfhgwoZCqG+J2LNmzhPaVoP+woeTTg==" } \ No newline at end of file From 66115e7ac503e2ae9f5f2283d7c0d3879fee7bbd Mon Sep 17 00:00:00 2001 From: Yuri Guller Date: Mon, 17 Nov 2025 00:38:29 +0200 Subject: [PATCH 06/23] wip --- package.json | 2 +- src/changes-processors/filters.ts | 0 src/changes-processors/verifiers.ts | 0 src/object-observer.d.ts.tmp | 97 ---------------------- src/object-observer.ts | 14 ++-- src/observables/abstract-base.ts | 6 +- src/observables/array.ts | 24 +++--- src/observables/methods/copy-within.ts | 6 +- src/observables/methods/delete-property.ts | 8 +- src/observables/methods/fill.ts | 6 +- src/observables/methods/pop.ts | 6 +- src/observables/methods/push.ts | 6 +- src/observables/methods/reverse.ts | 6 +- src/observables/methods/set.ts | 8 +- src/observables/methods/shift.ts | 6 +- src/observables/methods/sort.ts | 6 +- src/observables/methods/splice.ts | 6 +- src/observables/methods/typed-set.ts | 6 +- src/observables/methods/unshift.ts | 6 +- src/observables/object.ts | 6 +- src/observables/typed-array.ts | 14 ++-- tsconfig.json | 11 +++ 22 files changed, 82 insertions(+), 168 deletions(-) create mode 100644 src/changes-processors/filters.ts create mode 100644 src/changes-processors/verifiers.ts delete mode 100644 src/object-observer.d.ts.tmp create mode 100644 tsconfig.json diff --git a/package.json b/package.json index 8174679..ce89cd6 100644 --- a/package.json +++ b/package.json @@ -62,7 +62,7 @@ "scripts": { "build": "node ./ci/tools/build-utils.js", "lint": "eslint -c ./ci/eslint.config.mjs ./src/* ./tests/*.js ./ci/**/*.js", - "test-types": "tsc ./src/object-observer.ts --noEmit --target es2022", + "test-types": "tsc", "test": "node node_modules/@gullerya/just-test/bin/local-runner.js config_file=./tests/configs/tests-config-ci-node.json", "test:chromium": "node node_modules/@gullerya/just-test/bin/local-runner.js config_file=./tests/configs/tests-config-ci-chromium.json", "test:firefox": "node node_modules/@gullerya/just-test/bin/local-runner.js config_file=./tests/configs/tests-config-ci-firefox.json", diff --git a/src/changes-processors/filters.ts b/src/changes-processors/filters.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/changes-processors/verifiers.ts b/src/changes-processors/verifiers.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/object-observer.d.ts.tmp b/src/object-observer.d.ts.tmp deleted file mode 100644 index 6ab8288..0000000 --- a/src/object-observer.d.ts.tmp +++ /dev/null @@ -1,97 +0,0 @@ -export type ChangeType = 'insert' | 'update' | 'delete' | 'reverse' | 'shuffle'; - -/** - * `Observable` allows to observe any (deep) changes on its underlying object graph - * - * - created by `from` static method, via cloning the target - * - important: the type `T` is not preserved, beside its shape - */ -export abstract class Observable { - - /** - * create Observable from the target - * - target is cloned, remaining unchanged in itself - * - important: the type `T` is NOT preserved, beside its shape - * - * @param target source, to create `Observable` from - * @param options observable options - */ - static from(target: T, options?: ObservableOptions): Observable & T; - - /** - * check input for being `Observable` - * - * @param input any object to be checked as `Observable` - */ - static isObservable(input: unknown): boolean; - - /** - * add observer to handle the observable's changes - * - * @param observable observable to set observer on - * @param observer observer function / logic - * @param options observation options - */ - static observe(observable: Observable, observer: Observer, options?: ObserverOptions): void; - - /** - * remove observer/s from observable - * - * @param observable observable to remove observer/s from - * @param observers 0 to many observers to remove; if none supplied, ALL observers will be removed - */ - static unobserve(observable: Observable, ...observers: Observer[]): void; -} - -export interface ObservableOptions { - async: boolean; -} - -export interface ChangesProcessor { - (changes: Change[]): void; -} - -export interface ObserverOptions { - path?: string, - pathsOf?: string, - pathsFrom?: string -} - -export interface Change { - type: ChangeType; - path: string[]; - value?: unknown; - oldValue?: unknown; - object: object; -} - -/** - * `ObjectObserver` provides observation functionality in a WebAPI-like flavor - * - `observer` created first, with the provided observer function - * - `observer` may then be used to observe different targets - */ -export class ObjectObserver { - - /** - * sets up observer function and options - * @param observer observation logic (function) - * @param options `ObservableOptions` will be applied to any `Observable` down the road - */ - constructor(observer: Observer, options?: ObservableOptions); - - /** - * create `Observable` from the target and starts observation - * - important: the type `T` is NOT preserved, beside its shape - * @param target target to be observed, turned into `Observable` via cloning - * @param options `ObserverOptions` options - */ - observe(target: T, options?: ObserverOptions): Observable & T; - - /** - * un-observes the `Observable`, returning the original undelying plain object - * @param target target to be un-observed - */ - unobserve(target: Observable): void; - - disconnect(): void; -} diff --git a/src/object-observer.ts b/src/object-observer.ts index a51b5e9..4a8ad63 100644 --- a/src/object-observer.ts +++ b/src/object-observer.ts @@ -1,10 +1,10 @@ -import { Change } from './structs/change.js'; -import { ObservableArray } from './observables/array.js'; -import { ObservableTypedArray } from './observables/typed-array.js'; -import { ObservableObject } from './observables/object.js'; -import { REVERSE, SHUFFLE } from './constants.js'; -import { oMetaKey } from './constants.js'; -import { ObservableBase } from './observables/abstract-base.js'; +import { Change } from './structs/change.ts'; +import { ObservableArray } from './observables/array.ts'; +import { ObservableTypedArray } from './observables/typed-array.ts'; +import { ObservableObject } from './observables/object.ts'; +import { REVERSE, SHUFFLE } from './constants.ts'; +import { oMetaKey } from './constants.ts'; +import { ObservableBase } from './observables/abstract-base.ts'; const processObserveOptions = options => { diff --git a/src/observables/abstract-base.ts b/src/observables/abstract-base.ts index 9e0d47c..6d07f17 100644 --- a/src/observables/abstract-base.ts +++ b/src/observables/abstract-base.ts @@ -1,6 +1,6 @@ -import { Change } from '../structs/change.js'; -import { proxiedDeleteProperty } from './methods/delete-property.js'; -import { proxiedSet } from './methods/set.js'; +import { Change } from '../structs/change.ts'; +import { proxiedDeleteProperty } from './methods/delete-property.ts'; +import { proxiedSet } from './methods/set.ts'; const validObservableOptionKeys = { async: 1, verifiers: 1 }; diff --git a/src/observables/array.ts b/src/observables/array.ts index c74ed89..6efada3 100644 --- a/src/observables/array.ts +++ b/src/observables/array.ts @@ -1,15 +1,15 @@ -import { ObservableBase } from './abstract-base.js'; -import { getObservedOf } from '../object-observer.js'; -import { oMetaKey } from '../constants.js'; -import proxiedCopyWithin from './methods/copy-within.js'; -import proxiedFill from './methods/fill.js'; -import proxiedPop from './methods/pop.js'; -import proxiedPush from './methods/push.js'; -import proxiedReverse from './methods/reverse.js'; -import proxiedShift from './methods/shift.js'; -import proxiedSort from './methods/sort.js'; -import proxiedSplice from './methods/splice.js'; -import proxiedUnshift from './methods/unshift.js'; +import { ObservableBase } from './abstract-base.ts'; +import { getObservedOf } from '../object-observer.ts'; +import { oMetaKey } from '../constants.ts'; +import proxiedCopyWithin from './methods/copy-within.ts'; +import proxiedFill from './methods/fill.ts'; +import proxiedPop from './methods/pop.ts'; +import proxiedPush from './methods/push.ts'; +import proxiedReverse from './methods/reverse.ts'; +import proxiedShift from './methods/shift.ts'; +import proxiedSort from './methods/sort.ts'; +import proxiedSplice from './methods/splice.ts'; +import proxiedUnshift from './methods/unshift.ts'; const proxiedArrayMethods = { copyWithin: proxiedCopyWithin, diff --git a/src/observables/methods/copy-within.ts b/src/observables/methods/copy-within.ts index 69c097e..4e15df0 100644 --- a/src/observables/methods/copy-within.ts +++ b/src/observables/methods/copy-within.ts @@ -1,6 +1,6 @@ -import { Change } from '../../structs/change.js'; -import { UPDATE, oMetaKey } from '../../constants.js'; -import { callObservers, getObservedOf } from '../../object-observer.js'; +import { Change } from '../../structs/change.ts'; +import { UPDATE, oMetaKey } from '../../constants.ts'; +import { callObservers, getObservedOf } from '../../object-observer.ts'; export default function proxiedCopyWithin(dest, start, end) { const oMeta = this[oMetaKey]; diff --git a/src/observables/methods/delete-property.ts b/src/observables/methods/delete-property.ts index 8b7aa05..c023b11 100644 --- a/src/observables/methods/delete-property.ts +++ b/src/observables/methods/delete-property.ts @@ -1,7 +1,7 @@ -import { Change } from '../../structs/change.js'; -import { DELETE } from '../../constants.js'; -import { oMetaKey } from '../../constants.js'; -import { callObservers } from '../../object-observer.js'; +import { Change } from '../../structs/change.ts'; +import { DELETE } from '../../constants.ts'; +import { oMetaKey } from '../../constants.ts'; +import { callObservers } from '../../object-observer.ts'; export function proxiedDeleteProperty(target: object, key: string | symbol): boolean { let oldValue = target[key]; diff --git a/src/observables/methods/fill.ts b/src/observables/methods/fill.ts index bdcdcb9..900d1e7 100644 --- a/src/observables/methods/fill.ts +++ b/src/observables/methods/fill.ts @@ -1,6 +1,6 @@ -import { Change } from '../../structs/change.js'; -import { INSERT, UPDATE, oMetaKey } from '../../constants.js'; -import { callObservers, getObservedOf } from '../../object-observer.js'; +import { Change } from '../../structs/change.ts'; +import { INSERT, UPDATE, oMetaKey } from '../../constants.ts'; +import { callObservers, getObservedOf } from '../../object-observer.ts'; export default function proxiedFill(filVal, start, end) { const oMeta = this[oMetaKey]; diff --git a/src/observables/methods/pop.ts b/src/observables/methods/pop.ts index 773be58..aee2538 100644 --- a/src/observables/methods/pop.ts +++ b/src/observables/methods/pop.ts @@ -1,6 +1,6 @@ -import { Change } from '../../structs/change.js'; -import { DELETE, oMetaKey } from '../../constants.js'; -import { callObservers } from '../../object-observer.js'; +import { Change } from '../../structs/change.ts'; +import { DELETE, oMetaKey } from '../../constants.ts'; +import { callObservers } from '../../object-observer.ts'; export default function proxiedPop() { const oMeta = this[oMetaKey]; diff --git a/src/observables/methods/push.ts b/src/observables/methods/push.ts index 28177b7..8544840 100644 --- a/src/observables/methods/push.ts +++ b/src/observables/methods/push.ts @@ -1,6 +1,6 @@ -import { Change } from '../../structs/change.js'; -import { INSERT, oMetaKey } from '../../constants.js'; -import { callObservers, getObservedOf } from '../../object-observer.js'; +import { Change } from '../../structs/change.ts'; +import { INSERT, oMetaKey } from '../../constants.ts'; +import { callObservers, getObservedOf } from '../../object-observer.ts'; export default function proxiedPush(...pushItems: unknown[]) { const oMeta = this[oMetaKey]; diff --git a/src/observables/methods/reverse.ts b/src/observables/methods/reverse.ts index 86de037..2d60e1d 100644 --- a/src/observables/methods/reverse.ts +++ b/src/observables/methods/reverse.ts @@ -1,6 +1,6 @@ -import { Change } from '../../structs/change.js'; -import { REVERSE, oMetaKey } from '../../constants.js'; -import { callObservers } from '../../object-observer.js'; +import { Change } from '../../structs/change.ts'; +import { REVERSE, oMetaKey } from '../../constants.ts'; +import { callObservers } from '../../object-observer.ts'; export default function proxiedReverse() { const oMeta = this[oMetaKey]; diff --git a/src/observables/methods/set.ts b/src/observables/methods/set.ts index 6eb653c..9acd711 100644 --- a/src/observables/methods/set.ts +++ b/src/observables/methods/set.ts @@ -1,7 +1,7 @@ -import { Change } from '../../structs/change.js'; -import { INSERT, UPDATE } from '../../constants.js'; -import { oMetaKey } from '../../constants.js'; -import { callObservers, getObservedOf } from '../../object-observer.js'; +import { Change } from '../../structs/change.ts'; +import { INSERT, UPDATE } from '../../constants.ts'; +import { oMetaKey } from '../../constants.ts'; +import { callObservers, getObservedOf } from '../../object-observer.ts'; export function proxiedSet(target: object, key: string | symbol, value: unknown): boolean { let oldValue = target[key]; diff --git a/src/observables/methods/shift.ts b/src/observables/methods/shift.ts index 21d3da4..8acabe3 100644 --- a/src/observables/methods/shift.ts +++ b/src/observables/methods/shift.ts @@ -1,6 +1,6 @@ -import { Change } from '../../structs/change.js'; -import { DELETE, oMetaKey } from '../../constants.js'; -import { callObservers } from '../../object-observer.js'; +import { Change } from '../../structs/change.ts'; +import { DELETE, oMetaKey } from '../../constants.ts'; +import { callObservers } from '../../object-observer.ts'; export default function proxiedShift() { const diff --git a/src/observables/methods/sort.ts b/src/observables/methods/sort.ts index cbf33b1..8fe19f1 100644 --- a/src/observables/methods/sort.ts +++ b/src/observables/methods/sort.ts @@ -1,6 +1,6 @@ -import { Change } from '../../structs/change.js'; -import { SHUFFLE, oMetaKey } from '../../constants.js'; -import { callObservers } from '../../object-observer.js'; +import { Change } from '../../structs/change.ts'; +import { SHUFFLE, oMetaKey } from '../../constants.ts'; +import { callObservers } from '../../object-observer.ts'; export default function proxiedSort(comparator) { const oMeta = this[oMetaKey]; diff --git a/src/observables/methods/splice.ts b/src/observables/methods/splice.ts index 95c3156..26f1fd5 100644 --- a/src/observables/methods/splice.ts +++ b/src/observables/methods/splice.ts @@ -1,6 +1,6 @@ -import { Change } from '../../structs/change.js'; -import { INSERT, DELETE, UPDATE, oMetaKey } from '../../constants.js'; -import { callObservers, getObservedOf } from '../../object-observer.js'; +import { Change } from '../../structs/change.ts'; +import { INSERT, DELETE, UPDATE, oMetaKey } from '../../constants.ts'; +import { callObservers, getObservedOf } from '../../object-observer.ts'; export default function proxiedSplice(...spliceItems: unknown[]) { const oMeta = this[oMetaKey]; diff --git a/src/observables/methods/typed-set.ts b/src/observables/methods/typed-set.ts index 60a6286..f9a0fcf 100644 --- a/src/observables/methods/typed-set.ts +++ b/src/observables/methods/typed-set.ts @@ -1,6 +1,6 @@ -import { Change } from '../../structs/change.js'; -import { UPDATE, oMetaKey } from '../../constants.js'; -import { callObservers } from '../../object-observer.js'; +import { Change } from '../../structs/change.ts'; +import { UPDATE, oMetaKey } from '../../constants.ts'; +import { callObservers } from '../../object-observer.ts'; export default function proxiedTypedArraySet(source, offset) { const oMeta = this[oMetaKey]; diff --git a/src/observables/methods/unshift.ts b/src/observables/methods/unshift.ts index 7ca5661..4d0c456 100644 --- a/src/observables/methods/unshift.ts +++ b/src/observables/methods/unshift.ts @@ -1,6 +1,6 @@ -import { Change } from '../../structs/change.js'; -import { INSERT, oMetaKey } from '../../constants.js'; -import { callObservers, getObservedOf } from '../../object-observer.js'; +import { Change } from '../../structs/change.ts'; +import { INSERT, oMetaKey } from '../../constants.ts'; +import { callObservers, getObservedOf } from '../../object-observer.ts'; export default function proxiedUnshift(...unshiftItems: unknown[]) { const oMeta = this[oMetaKey]; diff --git a/src/observables/object.ts b/src/observables/object.ts index 2207a66..54fd248 100644 --- a/src/observables/object.ts +++ b/src/observables/object.ts @@ -1,6 +1,6 @@ -import { ObservableBase } from './abstract-base.js'; -import { getObservedOf } from '../object-observer.js'; -import { oMetaKey } from '../constants.js'; +import { ObservableBase } from './abstract-base.ts'; +import { getObservedOf } from '../object-observer.ts'; +import { oMetaKey } from '../constants.ts'; export class ObservableObject extends ObservableBase { diff --git a/src/observables/typed-array.ts b/src/observables/typed-array.ts index 9b19982..e208f29 100644 --- a/src/observables/typed-array.ts +++ b/src/observables/typed-array.ts @@ -1,10 +1,10 @@ -import { ObservableBase } from './abstract-base.js'; -import { oMetaKey } from '../constants.js'; -import proxiedCopyWithin from './methods/copy-within.js'; -import proxiedFill from './methods/fill.js'; -import proxiedReverse from './methods/reverse.js'; -import proxiedSort from './methods/sort.js'; -import proxiedTypedArraySet from './methods/typed-set.js'; +import { ObservableBase } from './abstract-base.ts'; +import { oMetaKey } from '../constants.ts'; +import proxiedCopyWithin from './methods/copy-within.ts'; +import proxiedFill from './methods/fill.ts'; +import proxiedReverse from './methods/reverse.ts'; +import proxiedSort from './methods/sort.ts'; +import proxiedTypedArraySet from './methods/typed-set.ts'; const proxiedTypedArrayMethods = { copyWithin: proxiedCopyWithin, diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..02f2174 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "strict": false, + "target": "es2022", + "noEmit": true, + "allowImportingTsExtensions": true, + "rewriteRelativeImportExtensions": true, + "verbatimModuleSyntax": true, + "erasableSyntaxOnly": true + } +} \ No newline at end of file From 72d4a3daf53681eeea07efae06a48fcf9dd9cbdb Mon Sep 17 00:00:00 2001 From: Yuri Guller Date: Mon, 17 Nov 2025 00:59:30 +0200 Subject: [PATCH 07/23] added filters --- src/changes-processors/filters.ts | 60 +++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/src/changes-processors/filters.ts b/src/changes-processors/filters.ts index e69de29..b164507 100644 --- a/src/changes-processors/filters.ts +++ b/src/changes-processors/filters.ts @@ -0,0 +1,60 @@ +import { REVERSE, SHUFFLE } from '../constants'; +import type { Change } from '../structs/change.ts'; + +export class Filter { + static #privateCtorKey = Symbol('FilterPrivateConstructorKey'); + #fn: (changes: Change[]) => void; + + constructor(privateCtorKey, fn: (changes: Change[]) => void) { + if (privateCtorKey !== Filter.#privateCtorKey) { + throw new Error('Filter class cannot be instantiated directly; use provided factory methods'); + } + + this.#fn = fn; + } + + get fn(): (changes: Change[]) => void { return this.#fn; } + + static custom(fn: (changes: Change[]) => void): Filter { + if (typeof fn !== 'function') { + throw new Error('custom Filter requires a function as argument'); + } + return new Filter(Filter.#privateCtorKey, fn); + } + + static byExactPath(path: string): Filter { + if (typeof path !== 'string' || path === '') { + throw new Error('byExactPath Filter requires a non-empty string as argument'); + } + return new Filter( + Filter.#privateCtorKey, + changes => changes.filter(change => change.path.join('.') === path) + ); + } + + static includeChildrenOfPath(parentPath: Array): Filter { + if (!Array.isArray(parentPath) || parentPath.length === 0) { + throw new Error('includeChildrenOfPath Filter requires a non-empty array as argument'); + } + const pathsOfStr = parentPath.join('.'); + const pathsOfLength = parentPath.length; + return new Filter( + Filter.#privateCtorKey, + changes => changes.filter(change => + (change.path.length === pathsOfLength + 1 || + (change.path.length === pathsOfLength && (change.type === REVERSE || change.type === SHUFFLE))) && + change.path.join('.').startsWith(pathsOfStr) + ) + ); + } + + static includePathsStartingFrom(prefix: string): Filter { + if (typeof prefix !== 'string' || prefix === '') { + throw new Error('includePathsStartingFrom Filter requires a non-empty string as argument'); + } + return new Filter( + Filter.#privateCtorKey, + changes => changes.filter(change => change.path.join('.').startsWith(prefix)) + ); + } +} \ No newline at end of file From ef2edc49714e9c8cba1fd141d7eeb18129d9075c Mon Sep 17 00:00:00 2001 From: Yuri Guller Date: Tue, 18 Nov 2025 09:24:08 +0200 Subject: [PATCH 08/23] wip --- package-lock.json | 20 +++++------ package.json | 2 +- src/changes-processors/filters.ts | 33 +++++-------------- src/{structs => model}/change.ts | 19 +++++++++++ src/object-observer.ts | 2 +- src/observables/abstract-base.ts | 2 +- src/observables/methods/copy-within.ts | 2 +- src/observables/methods/delete-property.ts | 2 +- src/observables/methods/fill.ts | 2 +- src/observables/methods/pop.ts | 2 +- src/observables/methods/push.ts | 2 +- src/observables/methods/reverse.ts | 2 +- src/observables/methods/set.ts | 2 +- src/observables/methods/shift.ts | 2 +- src/observables/methods/sort.ts | 2 +- src/observables/methods/splice.ts | 2 +- src/observables/methods/typed-set.ts | 2 +- src/observables/methods/unshift.ts | 2 +- src/observables/typed-array.ts | 3 +- sri.json | 8 ++--- tests/api-base.js | 2 +- tests/api-changes.js | 2 +- tests/browser-host-objects.js | 2 +- tests/configs/tests-config-ci-chromium.json | 4 +-- tests/configs/tests-config-ci-firefox.json | 2 +- tests/configs/tests-config-ci-node.json | 4 +-- tests/configs/tests-config-ci-webkit.json | 2 +- tests/cross-instance.js | 4 +-- tests/listeners.js | 2 +- tests/object-generics.js | 2 +- tests/object-observer-api.js | 2 +- tests/object-observer-arrays-copy-within.js | 2 +- tests/object-observer-arrays-typed.js | 2 +- tests/object-observer-arrays.js | 2 +- .../object-observer-native-objects-to-skip.js | 2 +- tests/object-observer-objects-async.js | 2 +- tests/object-observer-objects-circular.js | 2 +- tests/object-observer-objects-same-refs.js | 2 +- tests/object-observer-objects.js | 2 +- tests/object-observer-subgraphs.js | 2 +- tests/observable-nested.js | 2 +- tests/observe-specific-paths.js | 2 +- tests/reassignment-of-equals.js | 2 +- tests/revokation.js | 2 +- tests/unobserve.js | 2 +- tests/workers/perf-async-test-a.js | 2 +- tests/workers/perf-async-test-b.js | 2 +- tests/workers/perf-sync-test-a.js | 2 +- tests/workers/perf-sync-test-b.js | 2 +- 49 files changed, 90 insertions(+), 87 deletions(-) rename src/{structs => model}/change.ts (55%) diff --git a/package-lock.json b/package-lock.json index f954d89..752d554 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,7 +18,7 @@ "license": "ISC", "devDependencies": { "@eslint/js": "^9.39.1", - "@gullerya/just-test": "^4.0.12", + "@gullerya/just-test": "^4.0.14", "esbuild": "^0.27.0", "eslint": "^9.39.1", "globals": "^16.5.0", @@ -652,15 +652,15 @@ } }, "node_modules/@gullerya/just-test": { - "version": "4.0.12", - "resolved": "https://registry.npmjs.org/@gullerya/just-test/-/just-test-4.0.12.tgz", - "integrity": "sha512-jyTaatrblzhpySRaT6AT/ihxux3+np20YmLPIwsG2USJ/3MpbHSLOqxvUy0Yk8+GHktqr1ENL4PE+fvX1TvNCA==", + "version": "4.0.14", + "resolved": "https://registry.npmjs.org/@gullerya/just-test/-/just-test-4.0.14.tgz", + "integrity": "sha512-pvCV/cPpEwxoqcQU045FI8T52T/qoj1odRZT25ggN+4VaF97R+ZM7OrYfHgRw/nZtoN/hJKf+nA8bBbipxvYcw==", "dev": true, "license": "MIT", "dependencies": { "data-tier": "^3.6.6", "data-tier-list": "^2.2.1", - "glob": "^11.0.3", + "glob": "^12.0.0", "minimatch": "^10.1.1", "playwright": "^1.56.1", "rich-component": "^1.8.0" @@ -1717,15 +1717,15 @@ } }, "node_modules/glob": { - "version": "11.0.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-11.0.3.tgz", - "integrity": "sha512-2Nim7dha1KVkaiF4q6Dj+ngPPMdfvLJEOpZk/jKiUAkqKebpGAWQXAq9z1xu9HKu5lWfqw/FASuccEjyznjPaA==", + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-12.0.0.tgz", + "integrity": "sha512-5Qcll1z7IKgHr5g485ePDdHcNQY0k2dtv/bjYy0iuyGxQw2qSOiiXUXJ+AYQpg3HNoUMHqAruX478Jeev7UULw==", "dev": true, - "license": "ISC", + "license": "BlueOak-1.0.0", "dependencies": { "foreground-child": "^3.3.1", "jackspeak": "^4.1.1", - "minimatch": "^10.0.3", + "minimatch": "^10.1.1", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^2.0.0" diff --git a/package.json b/package.json index ce89cd6..4358994 100644 --- a/package.json +++ b/package.json @@ -73,7 +73,7 @@ }, "devDependencies": { "@eslint/js": "^9.39.1", - "@gullerya/just-test": "^4.0.12", + "@gullerya/just-test": "^4.0.14", "esbuild": "^0.27.0", "eslint": "^9.39.1", "globals": "^16.5.0", diff --git a/src/changes-processors/filters.ts b/src/changes-processors/filters.ts index b164507..6c41203 100644 --- a/src/changes-processors/filters.ts +++ b/src/changes-processors/filters.ts @@ -1,5 +1,4 @@ -import { REVERSE, SHUFFLE } from '../constants'; -import type { Change } from '../structs/change.ts'; +import { Change } from '../model/change.ts'; export class Filter { static #privateCtorKey = Symbol('FilterPrivateConstructorKey'); @@ -22,39 +21,25 @@ export class Filter { return new Filter(Filter.#privateCtorKey, fn); } - static byExactPath(path: string): Filter { - if (typeof path !== 'string' || path === '') { - throw new Error('byExactPath Filter requires a non-empty string as argument'); + static exactPaths(paths: string[]): Filter { + if (!Array.isArray(paths) || paths.length === 0) { + throw new Error('exactPaths Filter requires a non-empty array as argument'); } - return new Filter( - Filter.#privateCtorKey, - changes => changes.filter(change => change.path.join('.') === path) - ); - } - static includeChildrenOfPath(parentPath: Array): Filter { - if (!Array.isArray(parentPath) || parentPath.length === 0) { - throw new Error('includeChildrenOfPath Filter requires a non-empty array as argument'); - } - const pathsOfStr = parentPath.join('.'); - const pathsOfLength = parentPath.length; + const pathsSet = new Set(paths); return new Filter( Filter.#privateCtorKey, - changes => changes.filter(change => - (change.path.length === pathsOfLength + 1 || - (change.path.length === pathsOfLength && (change.type === REVERSE || change.type === SHUFFLE))) && - change.path.join('.').startsWith(pathsOfStr) - ) + changes => changes.filter(change => pathsSet.has(change.pathAsString)) ); } - static includePathsStartingFrom(prefix: string): Filter { + static pathsStartWith(prefix: string): Filter { if (typeof prefix !== 'string' || prefix === '') { - throw new Error('includePathsStartingFrom Filter requires a non-empty string as argument'); + throw new Error('pathsStartWith Filter requires a non-empty string as argument'); } return new Filter( Filter.#privateCtorKey, - changes => changes.filter(change => change.path.join('.').startsWith(prefix)) + changes => changes.filter(change => change.pathAsString.startsWith(prefix)) ); } } \ No newline at end of file diff --git a/src/structs/change.ts b/src/model/change.ts similarity index 55% rename from src/structs/change.ts rename to src/model/change.ts index 7165952..16ff43e 100644 --- a/src/structs/change.ts +++ b/src/model/change.ts @@ -5,7 +5,16 @@ export class Change { #oldValue: unknown; #object: object; + #pathAsString: string; + constructor(type: string, path: Array, value: unknown, oldValue: unknown, object: object) { + if (typeof type !== 'string' || type === '') { + throw new Error('Change type must be a non-empty string'); + } + if (!Array.isArray(path)) { + throw new Error('Change path must be an array'); + } + this.#type = type; this.#path = path; this.#value = value; @@ -32,4 +41,14 @@ export class Change { get object() { return this.#object; } + + /** + * lazily computed string representation of the path + */ + get pathAsString(): string { + if (this.#pathAsString === undefined && Array.isArray(this.#path)) { + this.#pathAsString = this.#path.join('.'); + } + return this.#pathAsString; + } } \ No newline at end of file diff --git a/src/object-observer.ts b/src/object-observer.ts index 4a8ad63..5c5618d 100644 --- a/src/object-observer.ts +++ b/src/object-observer.ts @@ -1,4 +1,4 @@ -import { Change } from './structs/change.ts'; +import { Change } from './model/change.ts'; import { ObservableArray } from './observables/array.ts'; import { ObservableTypedArray } from './observables/typed-array.ts'; import { ObservableObject } from './observables/object.ts'; diff --git a/src/observables/abstract-base.ts b/src/observables/abstract-base.ts index 6d07f17..c978ed5 100644 --- a/src/observables/abstract-base.ts +++ b/src/observables/abstract-base.ts @@ -1,4 +1,4 @@ -import { Change } from '../structs/change.ts'; +import { Change } from '../model/change.ts'; import { proxiedDeleteProperty } from './methods/delete-property.ts'; import { proxiedSet } from './methods/set.ts'; diff --git a/src/observables/methods/copy-within.ts b/src/observables/methods/copy-within.ts index 4e15df0..877ec79 100644 --- a/src/observables/methods/copy-within.ts +++ b/src/observables/methods/copy-within.ts @@ -1,4 +1,4 @@ -import { Change } from '../../structs/change.ts'; +import { Change } from '../../model/change.ts'; import { UPDATE, oMetaKey } from '../../constants.ts'; import { callObservers, getObservedOf } from '../../object-observer.ts'; diff --git a/src/observables/methods/delete-property.ts b/src/observables/methods/delete-property.ts index c023b11..3516fff 100644 --- a/src/observables/methods/delete-property.ts +++ b/src/observables/methods/delete-property.ts @@ -1,4 +1,4 @@ -import { Change } from '../../structs/change.ts'; +import { Change } from '../../model/change.ts'; import { DELETE } from '../../constants.ts'; import { oMetaKey } from '../../constants.ts'; import { callObservers } from '../../object-observer.ts'; diff --git a/src/observables/methods/fill.ts b/src/observables/methods/fill.ts index 900d1e7..409efa2 100644 --- a/src/observables/methods/fill.ts +++ b/src/observables/methods/fill.ts @@ -1,4 +1,4 @@ -import { Change } from '../../structs/change.ts'; +import { Change } from '../../model/change.ts'; import { INSERT, UPDATE, oMetaKey } from '../../constants.ts'; import { callObservers, getObservedOf } from '../../object-observer.ts'; diff --git a/src/observables/methods/pop.ts b/src/observables/methods/pop.ts index aee2538..ccc532d 100644 --- a/src/observables/methods/pop.ts +++ b/src/observables/methods/pop.ts @@ -1,4 +1,4 @@ -import { Change } from '../../structs/change.ts'; +import { Change } from '../../model/change.ts'; import { DELETE, oMetaKey } from '../../constants.ts'; import { callObservers } from '../../object-observer.ts'; diff --git a/src/observables/methods/push.ts b/src/observables/methods/push.ts index 8544840..cfae108 100644 --- a/src/observables/methods/push.ts +++ b/src/observables/methods/push.ts @@ -1,4 +1,4 @@ -import { Change } from '../../structs/change.ts'; +import { Change } from '../../model/change.ts'; import { INSERT, oMetaKey } from '../../constants.ts'; import { callObservers, getObservedOf } from '../../object-observer.ts'; diff --git a/src/observables/methods/reverse.ts b/src/observables/methods/reverse.ts index 2d60e1d..91edf2c 100644 --- a/src/observables/methods/reverse.ts +++ b/src/observables/methods/reverse.ts @@ -1,4 +1,4 @@ -import { Change } from '../../structs/change.ts'; +import { Change } from '../../model/change.ts'; import { REVERSE, oMetaKey } from '../../constants.ts'; import { callObservers } from '../../object-observer.ts'; diff --git a/src/observables/methods/set.ts b/src/observables/methods/set.ts index 9acd711..dd75248 100644 --- a/src/observables/methods/set.ts +++ b/src/observables/methods/set.ts @@ -1,4 +1,4 @@ -import { Change } from '../../structs/change.ts'; +import { Change } from '../../model/change.ts'; import { INSERT, UPDATE } from '../../constants.ts'; import { oMetaKey } from '../../constants.ts'; import { callObservers, getObservedOf } from '../../object-observer.ts'; diff --git a/src/observables/methods/shift.ts b/src/observables/methods/shift.ts index 8acabe3..9ee1688 100644 --- a/src/observables/methods/shift.ts +++ b/src/observables/methods/shift.ts @@ -1,4 +1,4 @@ -import { Change } from '../../structs/change.ts'; +import { Change } from '../../model/change.ts'; import { DELETE, oMetaKey } from '../../constants.ts'; import { callObservers } from '../../object-observer.ts'; diff --git a/src/observables/methods/sort.ts b/src/observables/methods/sort.ts index 8fe19f1..889eb3a 100644 --- a/src/observables/methods/sort.ts +++ b/src/observables/methods/sort.ts @@ -1,4 +1,4 @@ -import { Change } from '../../structs/change.ts'; +import { Change } from '../../model/change.ts'; import { SHUFFLE, oMetaKey } from '../../constants.ts'; import { callObservers } from '../../object-observer.ts'; diff --git a/src/observables/methods/splice.ts b/src/observables/methods/splice.ts index 26f1fd5..b9fb284 100644 --- a/src/observables/methods/splice.ts +++ b/src/observables/methods/splice.ts @@ -1,4 +1,4 @@ -import { Change } from '../../structs/change.ts'; +import { Change } from '../../model/change.ts'; import { INSERT, DELETE, UPDATE, oMetaKey } from '../../constants.ts'; import { callObservers, getObservedOf } from '../../object-observer.ts'; diff --git a/src/observables/methods/typed-set.ts b/src/observables/methods/typed-set.ts index f9a0fcf..1636465 100644 --- a/src/observables/methods/typed-set.ts +++ b/src/observables/methods/typed-set.ts @@ -1,4 +1,4 @@ -import { Change } from '../../structs/change.ts'; +import { Change } from '../../model/change.ts'; import { UPDATE, oMetaKey } from '../../constants.ts'; import { callObservers } from '../../object-observer.ts'; diff --git a/src/observables/methods/unshift.ts b/src/observables/methods/unshift.ts index 4d0c456..17543b9 100644 --- a/src/observables/methods/unshift.ts +++ b/src/observables/methods/unshift.ts @@ -1,4 +1,4 @@ -import { Change } from '../../structs/change.ts'; +import { Change } from '../../model/change.ts'; import { INSERT, oMetaKey } from '../../constants.ts'; import { callObservers, getObservedOf } from '../../object-observer.ts'; diff --git a/src/observables/typed-array.ts b/src/observables/typed-array.ts index e208f29..43bf335 100644 --- a/src/observables/typed-array.ts +++ b/src/observables/typed-array.ts @@ -20,8 +20,7 @@ export class ObservableTypedArray extends ObservableBase { return proxiedTypedArrayMethods[key] || target[key]; } - // eslint-disable-next-line @typescript-eslint/no-unused-vars - observedGraphProcessor(source: Array, observableWrapper: ObservableBase, visited: Set): Array { + observedGraphProcessor(source: Array, observableWrapper: ObservableBase): Array { source[oMetaKey] = observableWrapper; return source; } diff --git a/sri.json b/sri.json index 78b7302..3d3c997 100644 --- a/sri.json +++ b/sri.json @@ -1,6 +1,6 @@ { - "dist/cdn/object-observer.js": "sha512-485VTA8tRG3htcli24/0KR2ssY8NZ1EGBIDKLvQwsISREupDwoUB/GcgbBJqcv1pq74ucn66oVO82/EAz6xn6Q==", - "dist/cdn/object-observer.js.map": "sha512-keZgvbex5FYhK+PoF9tb9wd2+CmFzJ0Ct1EaPuWXH/O8IChCXDWOqJxqU/S9PNVoYSKxZtC+YyRynCFj7tL6vA==", - "dist/cdn/object-observer.min.js": "sha512-6tTRCfjQ7Ke7ebumBVgJLWzLLZV56dnRdv7ZgH6hvQA4ABS3gVPyaYBzpnHuer+CFs9K4OqW4lSa9JbH8WzQpw==", - "dist/cdn/object-observer.min.js.map": "sha512-Of+hrbmHey8BJ51a3hKGKqMdhHEDjctw4iDkBpyVljLksUj4/v/S/anPMfhgwoZCqG+J2LNmzhPaVoP+woeTTg==" + "dist/cdn/object-observer.js": "sha512-luTTytJfesi/3p9Qc60gZ09F0xPZZmj4tA9LGKwX6zb1zei6IlMBQrIjrQmAiko4VfnT7xZTebOnh42XUkyVjA==", + "dist/cdn/object-observer.js.map": "sha512-FQf/rgiqP2J5W01fKI22Nf0AE+1ZJ7+VE2wO4ntWlD9J8vTHtB+T0ERK31/NARR1eA0k62HEDl3lKLJfL9svSQ==", + "dist/cdn/object-observer.min.js": "sha512-31PH8SxOaXOdd8RfUPuG9bdhWRYx0yZk8Dq9mddTUtpqfcMZkMY1Wp8/p4pOs2WAn1cjNXasDkZJlLVfhZxDwQ==", + "dist/cdn/object-observer.min.js.map": "sha512-boy/9Y9KXpaQvVhDzUyR8iZuAgUihfh1U8kQWGd79F623jw2Po9Q8xt8sYLAbTSSHTSs0PTwNYD2cv598zCMog==" } \ No newline at end of file diff --git a/tests/api-base.js b/tests/api-base.js index 36168a0..ae924dd 100644 --- a/tests/api-base.js +++ b/tests/api-base.js @@ -1,6 +1,6 @@ import { test } from '@gullerya/just-test'; import { assert } from '@gullerya/just-test/assert'; -import { Observable } from '../dist/object-observer.js'; +import { Observable } from '../src/object-observer.ts'; test('ensure Observable object has defined APIs', () => { assert.equal(typeof Observable, 'object'); diff --git a/tests/api-changes.js b/tests/api-changes.js index 0fd617b..03876ef 100644 --- a/tests/api-changes.js +++ b/tests/api-changes.js @@ -1,6 +1,6 @@ import { test } from '@gullerya/just-test'; import { assert } from '@gullerya/just-test/assert'; -import { Observable } from '../dist/object-observer.js'; +import { Observable } from '../src/object-observer.ts'; // object // diff --git a/tests/browser-host-objects.js b/tests/browser-host-objects.js index ef50674..44667f7 100644 --- a/tests/browser-host-objects.js +++ b/tests/browser-host-objects.js @@ -1,6 +1,6 @@ import { test } from '@gullerya/just-test'; import { assert } from '@gullerya/just-test/assert'; -import { Observable } from '../dist/object-observer.js'; +import { Observable } from '../src/object-observer.ts'; test('test DOMStringMap', () => { const diff --git a/tests/configs/tests-config-ci-chromium.json b/tests/configs/tests-config-ci-chromium.json index bdfef37..70a3a3c 100644 --- a/tests/configs/tests-config-ci-chromium.json +++ b/tests/configs/tests-config-ci-chromium.json @@ -12,7 +12,7 @@ "maxFail": 0, "maxSkip": 5, "include": [ - "./tests/*.js" + "./tests/*" ], "exclude": [ "**/tests/*-performance-*.js" @@ -20,7 +20,7 @@ }, "coverage": { "include": [ - "./dist/**/*.js" + "./src/**/*" ] } } diff --git a/tests/configs/tests-config-ci-firefox.json b/tests/configs/tests-config-ci-firefox.json index d853c03..9082b6e 100644 --- a/tests/configs/tests-config-ci-firefox.json +++ b/tests/configs/tests-config-ci-firefox.json @@ -12,7 +12,7 @@ "maxFail": 0, "maxSkip": 5, "include": [ - "./tests/*.js" + "./tests/*" ], "exclude": [ "**/tests/*-performance-*.js" diff --git a/tests/configs/tests-config-ci-node.json b/tests/configs/tests-config-ci-node.json index 43636a1..7b611fa 100644 --- a/tests/configs/tests-config-ci-node.json +++ b/tests/configs/tests-config-ci-node.json @@ -7,7 +7,7 @@ "maxFail": 0, "maxSkip": 5, "include": [ - "./tests/*.js" + "./tests/*" ], "exclude": [ "**/tests/browser-host-objects.js", @@ -16,7 +16,7 @@ }, "coverage": { "include": [ - "./dist/**/*.js" + "./src/**/*" ] } } diff --git a/tests/configs/tests-config-ci-webkit.json b/tests/configs/tests-config-ci-webkit.json index e368516..fe342bf 100644 --- a/tests/configs/tests-config-ci-webkit.json +++ b/tests/configs/tests-config-ci-webkit.json @@ -12,7 +12,7 @@ "maxFail": 0, "maxSkip": 5, "include": [ - "./tests/*.js" + "./tests/*" ], "exclude": [ "**/tests/*-performance-*.js" diff --git a/tests/cross-instance.js b/tests/cross-instance.js index 7cb9cca..1cf226d 100644 --- a/tests/cross-instance.js +++ b/tests/cross-instance.js @@ -1,7 +1,7 @@ import { test } from '@gullerya/just-test'; import { assert } from '@gullerya/just-test/assert'; -import { Observable as O1, ObjectObserver as OO1 } from '../dist/object-observer.js?key=1'; -import { Observable as O2, ObjectObserver as OO2 } from '../dist/object-observer.js?key=2'; +import { Observable as O1, ObjectObserver as OO1 } from '../src/object-observer.ts?key=1'; +import { Observable as O2, ObjectObserver as OO2 } from '../src/object-observer.ts?key=2'; test('Observable.isObservable interoperable', () => { assert.notEqual(O1, O2); diff --git a/tests/listeners.js b/tests/listeners.js index 932b00b..571af8a 100644 --- a/tests/listeners.js +++ b/tests/listeners.js @@ -1,6 +1,6 @@ import { test } from '@gullerya/just-test'; import { assert } from '@gullerya/just-test/assert'; -import { Observable } from '../dist/object-observer.js'; +import { Observable } from '../src/object-observer.ts'; test('test listeners invocation - single listener', () => { const oo = Observable.from({}); diff --git a/tests/object-generics.js b/tests/object-generics.js index 40f7157..4342010 100644 --- a/tests/object-generics.js +++ b/tests/object-generics.js @@ -1,6 +1,6 @@ import { test } from '@gullerya/just-test'; import { assert } from '@gullerya/just-test/assert'; -import { Observable } from '../dist/object-observer.js'; +import { Observable } from '../src/object-observer.ts'; test('Object.seal - further extensions should fail', () => { const oo = Observable.from({ propA: 'a', propB: 'b' }); diff --git a/tests/object-observer-api.js b/tests/object-observer-api.js index 0fb7843..b7044d2 100644 --- a/tests/object-observer-api.js +++ b/tests/object-observer-api.js @@ -1,6 +1,6 @@ import { test } from '@gullerya/just-test'; import { assert } from '@gullerya/just-test/assert'; -import { ObjectObserver, Observable } from '../dist/object-observer.js'; +import { ObjectObserver, Observable } from '../src/object-observer.ts'; test('ensure ObjectObserver constructable', () => { assert.isTrue(typeof ObjectObserver === 'function'); diff --git a/tests/object-observer-arrays-copy-within.js b/tests/object-observer-arrays-copy-within.js index d5656c8..2c42755 100644 --- a/tests/object-observer-arrays-copy-within.js +++ b/tests/object-observer-arrays-copy-within.js @@ -1,6 +1,6 @@ import { test } from '@gullerya/just-test'; import { assert } from '@gullerya/just-test/assert'; -import { Observable } from '../dist/object-observer.js'; +import { Observable } from '../src/object-observer.ts'; test('array copyWithin - primitives', () => { const diff --git a/tests/object-observer-arrays-typed.js b/tests/object-observer-arrays-typed.js index 87eb5ab..4fd0828 100644 --- a/tests/object-observer-arrays-typed.js +++ b/tests/object-observer-arrays-typed.js @@ -1,6 +1,6 @@ import { test } from '@gullerya/just-test'; import { assert } from '@gullerya/just-test/assert'; -import { Observable } from '../dist/object-observer.js'; +import { Observable } from '../src/object-observer.ts'; test('typed array reverse - Int8Array', () => { const diff --git a/tests/object-observer-arrays.js b/tests/object-observer-arrays.js index 3f33dc8..6111c9e 100644 --- a/tests/object-observer-arrays.js +++ b/tests/object-observer-arrays.js @@ -1,6 +1,6 @@ import { test } from '@gullerya/just-test'; import { assert } from '@gullerya/just-test/assert'; -import { Observable } from '../dist/object-observer.js'; +import { Observable } from '../src/object-observer.ts'; test('array push - primitives', () => { const diff --git a/tests/object-observer-native-objects-to-skip.js b/tests/object-observer-native-objects-to-skip.js index a2bd50c..f428a84 100644 --- a/tests/object-observer-native-objects-to-skip.js +++ b/tests/object-observer-native-objects-to-skip.js @@ -1,6 +1,6 @@ import { test } from '@gullerya/just-test'; import { assert } from '@gullerya/just-test/assert'; -import { Observable } from '../dist/object-observer.js'; +import { Observable } from '../src/object-observer.ts'; test('creating observable from non-observable should throw an error', () => { const objectsToTest = [ diff --git a/tests/object-observer-objects-async.js b/tests/object-observer-objects-async.js index 750dd59..2d9c8d5 100644 --- a/tests/object-observer-objects-async.js +++ b/tests/object-observer-objects-async.js @@ -1,7 +1,7 @@ import { test } from '@gullerya/just-test'; import { assert } from '@gullerya/just-test/assert'; import { waitNextTask } from '@gullerya/just-test/timing'; -import { Observable } from '../dist/object-observer.js'; +import { Observable } from '../src/object-observer.ts'; test('multiple continuous mutations', async () => { const diff --git a/tests/object-observer-objects-circular.js b/tests/object-observer-objects-circular.js index ef24712..f2a5c61 100644 --- a/tests/object-observer-objects-circular.js +++ b/tests/object-observer-objects-circular.js @@ -1,6 +1,6 @@ import { test } from '@gullerya/just-test'; import { assert } from '@gullerya/just-test/assert'; -import { Observable } from '../dist/object-observer.js'; +import { Observable } from '../src/object-observer.ts'; test('subgraph object pointing to the top parent', () => { const o = { prop: 'text' }; diff --git a/tests/object-observer-objects-same-refs.js b/tests/object-observer-objects-same-refs.js index bc7cf75..cd4226c 100644 --- a/tests/object-observer-objects-same-refs.js +++ b/tests/object-observer-objects-same-refs.js @@ -1,6 +1,6 @@ import { test } from '@gullerya/just-test'; import { assert } from '@gullerya/just-test/assert'; -import { Observable } from '../dist/object-observer.js'; +import { Observable } from '../src/object-observer.ts'; test('subgraph objects pointing to the same object few times', { skip: true }, () => { const childObj = { prop: 'A' }; diff --git a/tests/object-observer-objects.js b/tests/object-observer-objects.js index 810761a..729dfdd 100644 --- a/tests/object-observer-objects.js +++ b/tests/object-observer-objects.js @@ -1,6 +1,6 @@ import { test } from '@gullerya/just-test'; import { assert } from '@gullerya/just-test/assert'; -import { Observable } from '../dist/object-observer.js'; +import { Observable } from '../src/object-observer.ts'; test('creating observable leaves original object as is', () => { const person = { diff --git a/tests/object-observer-subgraphs.js b/tests/object-observer-subgraphs.js index d622d69..ba33009 100644 --- a/tests/object-observer-subgraphs.js +++ b/tests/object-observer-subgraphs.js @@ -1,6 +1,6 @@ import { test } from '@gullerya/just-test'; import { assert } from '@gullerya/just-test/assert'; -import { Observable } from '../dist/object-observer.js'; +import { Observable } from '../src/object-observer.ts'; test('inner object from observable should fire events as usual', () => { const diff --git a/tests/observable-nested.js b/tests/observable-nested.js index 5165d87..31c12b3 100644 --- a/tests/observable-nested.js +++ b/tests/observable-nested.js @@ -1,6 +1,6 @@ import { test } from '@gullerya/just-test'; import { assert } from '@gullerya/just-test/assert'; -import { Observable } from '../dist/object-observer.js'; +import { Observable } from '../src/object-observer.ts'; test('nested of observable should be observable too', () => { const oo = Observable.from({ diff --git a/tests/observe-specific-paths.js b/tests/observe-specific-paths.js index a6739c5..35a7490 100644 --- a/tests/observe-specific-paths.js +++ b/tests/observe-specific-paths.js @@ -1,6 +1,6 @@ import { test } from '@gullerya/just-test'; import { assert } from '@gullerya/just-test/assert'; -import { Observable } from '../dist/object-observer.js'; +import { Observable } from '../src/object-observer.ts'; test('baseline - negative - path not a string', () => { const oo = Observable.from({}); diff --git a/tests/reassignment-of-equals.js b/tests/reassignment-of-equals.js index f39bc8d..9f4e619 100644 --- a/tests/reassignment-of-equals.js +++ b/tests/reassignment-of-equals.js @@ -1,6 +1,6 @@ import { test } from '@gullerya/just-test'; import { assert } from '@gullerya/just-test/assert'; -import { Observable } from '../dist/object-observer.js'; +import { Observable } from '../src/object-observer.ts'; test('boolean', () => { const oo = Observable.from({ p: true }); diff --git a/tests/revokation.js b/tests/revokation.js index 9a2407f..37f79c2 100644 --- a/tests/revokation.js +++ b/tests/revokation.js @@ -1,6 +1,6 @@ import { test } from '@gullerya/just-test'; import { assert } from '@gullerya/just-test/assert'; -import { Observable } from '../dist/object-observer.js'; +import { Observable } from '../src/object-observer.ts'; test('test revokation of replaced objects - simple set', () => { const og = Observable.from({ diff --git a/tests/unobserve.js b/tests/unobserve.js index 67e8b75..458b227 100644 --- a/tests/unobserve.js +++ b/tests/unobserve.js @@ -1,6 +1,6 @@ import { test } from '@gullerya/just-test'; import { assert } from '@gullerya/just-test/assert'; -import { Observable } from '../dist/object-observer.js'; +import { Observable } from '../src/object-observer.ts'; test('test unobserve - single observer - explicit unobserve', () => { const diff --git a/tests/workers/perf-async-test-a.js b/tests/workers/perf-async-test-a.js index b5be4b9..0959261 100644 --- a/tests/workers/perf-async-test-a.js +++ b/tests/workers/perf-async-test-a.js @@ -1,4 +1,4 @@ -import { Observable } from '../../dist/object-observer.js'; +import { Observable } from '../../src/object-observer.ts'; export default async setup => { const { diff --git a/tests/workers/perf-async-test-b.js b/tests/workers/perf-async-test-b.js index d27aacd..41bb05e 100644 --- a/tests/workers/perf-async-test-b.js +++ b/tests/workers/perf-async-test-b.js @@ -1,4 +1,4 @@ -import { Observable } from '../../dist/object-observer.js'; +import { Observable } from '../../src/object-observer.ts'; export default async setup => { const { diff --git a/tests/workers/perf-sync-test-a.js b/tests/workers/perf-sync-test-a.js index c91ffcb..b1a03a3 100644 --- a/tests/workers/perf-sync-test-a.js +++ b/tests/workers/perf-sync-test-a.js @@ -1,4 +1,4 @@ -import { Observable } from '../../dist/object-observer.js'; +import { Observable } from '../../src/object-observer.ts'; export default setup => { const { diff --git a/tests/workers/perf-sync-test-b.js b/tests/workers/perf-sync-test-b.js index ae43b14..fe68c6b 100644 --- a/tests/workers/perf-sync-test-b.js +++ b/tests/workers/perf-sync-test-b.js @@ -1,4 +1,4 @@ -import { Observable } from '../../dist/object-observer.js'; +import { Observable } from '../../src/object-observer.ts'; export default setup => { const { From 6b84f5cd953af7cd005846c61050bd1ebcc2d91d Mon Sep 17 00:00:00 2001 From: Yuri Guller Date: Tue, 18 Nov 2025 10:01:43 +0200 Subject: [PATCH 09/23] fix --- src/object-observer.ts | 235 +++++++++------------ src/observables/abstract-base.ts | 9 +- src/observables/array.ts | 4 +- src/observables/methods/copy-within.ts | 3 +- src/observables/methods/delete-property.ts | 2 +- src/observables/methods/fill.ts | 3 +- src/observables/methods/pop.ts | 2 +- src/observables/methods/push.ts | 3 +- src/observables/methods/reverse.ts | 2 +- src/observables/methods/set.ts | 3 +- src/observables/methods/shift.ts | 2 +- src/observables/methods/sort.ts | 2 +- src/observables/methods/splice.ts | 3 +- src/observables/methods/typed-set.ts | 2 +- src/observables/methods/unshift.ts | 3 +- src/observables/object.ts | 2 +- src/observables/processors/proc-utils.ts | 139 ++++++++++++ src/observables/typed-array.ts | 4 +- sri.json | 8 +- 19 files changed, 272 insertions(+), 159 deletions(-) create mode 100644 src/observables/processors/proc-utils.ts diff --git a/src/object-observer.ts b/src/object-observer.ts index 5c5618d..fef3e83 100644 --- a/src/object-observer.ts +++ b/src/object-observer.ts @@ -1,10 +1,5 @@ -import { Change } from './model/change.ts'; -import { ObservableArray } from './observables/array.ts'; -import { ObservableTypedArray } from './observables/typed-array.ts'; -import { ObservableObject } from './observables/object.ts'; -import { REVERSE, SHUFFLE } from './constants.ts'; import { oMetaKey } from './constants.ts'; -import { ObservableBase } from './observables/abstract-base.ts'; +import { getObservableFromRoot } from './observables/processors/proc-utils.ts'; const processObserveOptions = options => { @@ -44,138 +39,108 @@ const throw new Error(`'${invalidOptions.join(', ')}' is/are not a valid observer option/s`); } return result; - }, - filterChanges = (options, changes) => { - if (options === null) { - return changes; - } - - let result = changes; - if (options.path) { - const oPath = options.path; - result = changes.filter(change => - change.path.join('.') === oPath - ); - } else if (options.pathsOf) { - const oPathsOf = options.pathsOf; - const oPathsOfStr = oPathsOf.join('.'); - result = changes.filter(change => - (change.path.length === oPathsOf.length + 1 || - (change.path.length === oPathsOf.length && (change.type === REVERSE || change.type === SHUFFLE))) && - change.path.join('.').startsWith(oPathsOfStr) - ); - } else if (options.pathsFrom) { - const oPathsFrom = options.pathsFrom; - result = changes.filter(change => - change.path.join('.').startsWith(oPathsFrom) - ); - } - return result; - }, - callObserverSafe = (listener, changes) => { - try { - listener(changes); - } catch (e) { - console.error(`failed to notify listener ${listener} with ${changes}`, e); - } - }, - callObserversFromMT = function callObserversFromMT() { - const batches = this.batches; - this.batches = []; - for (const [listener, changes] of batches) { - callObserverSafe(listener, changes); - } }; - -export function callObservers(oMeta: ObservableBase, changes: Change[]) { - let currentObservable: ObservableBase = oMeta; - let isAsync, observers, target, options, relevantChanges, i; - const l = changes.length; - do { - isAsync = currentObservable.async; - observers = currentObservable.observers; - i = observers.length; - while (i--) { - [target, options] = observers[i]; - relevantChanges = filterChanges(options, changes); - - if (relevantChanges.length) { - if (isAsync) { - // this is the async dispatch handling - if (currentObservable.batches.length === 0) { - queueMicrotask(callObserversFromMT.bind(currentObservable)); - } - let rb; - for (const b of currentObservable.batches) { - if (b[0] === target) { - rb = b; - break; - } - } - if (!rb) { - rb = [target, []]; - currentObservable.batches.push(rb); - } - Array.prototype.push.apply(rb[1], relevantChanges); - } else { - // this is the naive straight forward synchronous dispatch - callObserverSafe(target, relevantChanges); - } - } - } - - // cloning all the changes and notifying in context of parent - const parent = currentObservable.parent; - if (parent) { - for (let j = 0; j < l; j++) { - const change = changes[j]; - changes[j] = new Change( - change.type, - [currentObservable.ownKey, ...change.path], - change.value, - change.oldValue, - change.object - ); - } - currentObservable = parent; - } else { - break; - } - } while (currentObservable); -}; - -export function getObservedOf(item: unknown, key: string | symbol | number, parent: object, visited?: Set) { - if (visited !== undefined && visited.has(item)) { - return null; - } else if (typeof item !== 'object' || item === null) { - return item; - } else if (Array.isArray(item)) { - return new ObservableArray({ target: item, ownKey: key, parent: parent, visited }).proxy; - } else if (ArrayBuffer.isView(item)) { - return new ObservableTypedArray({ target: item, ownKey: key, parent: parent }).proxy; - } else if (item instanceof Date) { - return item; - } else { - return new ObservableObject({ target: item, ownKey: key, parent: parent, visited }).proxy; - } -}; +// filterChanges = (options, changes) => { +// if (options === null) { +// return changes; +// } + +// let result = changes; +// if (options.path) { +// const oPath = options.path; +// result = changes.filter(change => +// change.path.join('.') === oPath +// ); +// } else if (options.pathsOf) { +// const oPathsOf = options.pathsOf; +// const oPathsOfStr = oPathsOf.join('.'); +// result = changes.filter(change => +// (change.path.length === oPathsOf.length + 1 || +// (change.path.length === oPathsOf.length && (change.type === REVERSE || change.type === SHUFFLE))) && +// change.path.join('.').startsWith(oPathsOfStr) +// ); +// } else if (options.pathsFrom) { +// const oPathsFrom = options.pathsFrom; +// result = changes.filter(change => +// change.path.join('.').startsWith(oPathsFrom) +// ); +// } +// return result; +// }, +// callObserverSafe = (listener, changes) => { +// try { +// listener(changes); +// } catch (e) { +// console.error(`failed to notify listener ${listener} with ${changes}`, e); +// } +// }, +// callObserversFromMT = function callObserversFromMT() { +// const batches = this.batches; +// this.batches = []; +// for (const [listener, changes] of batches) { +// callObserverSafe(listener, changes); +// } +// }; + +// export function callObservers(oMeta: ObservableBase, changes: Change[]) { +// let currentObservable: ObservableBase = oMeta; +// let isAsync, observers, target, options, relevantChanges, i; +// const l = changes.length; +// do { +// isAsync = currentObservable.async; +// observers = currentObservable.observers; +// i = observers.length; +// while (i--) { +// [target, options] = observers[i]; +// relevantChanges = filterChanges(options, changes); + +// if (relevantChanges.length) { +// if (isAsync) { +// // this is the async dispatch handling +// if (currentObservable.batches.length === 0) { +// queueMicrotask(callObserversFromMT.bind(currentObservable)); +// } +// let rb; +// for (const b of currentObservable.batches) { +// if (b[0] === target) { +// rb = b; +// break; +// } +// } +// if (!rb) { +// rb = [target, []]; +// currentObservable.batches.push(rb); +// } +// Array.prototype.push.apply(rb[1], relevantChanges); +// } else { +// // this is the naive straight forward synchronous dispatch +// callObserverSafe(target, relevantChanges); +// } +// } +// } + +// // cloning all the changes and notifying in context of parent +// const parent = currentObservable.parent; +// if (parent) { +// for (let j = 0; j < l; j++) { +// const change = changes[j]; +// changes[j] = new Change( +// change.type, +// [currentObservable.ownKey, ...change.path], +// change.value, +// change.oldValue, +// change.object +// ); +// } +// currentObservable = parent; +// } else { +// break; +// } +// } while (currentObservable); +// }; export const Observable = Object.freeze({ - from: (target, options = undefined) => { - if (!target || typeof target !== 'object') { - throw new Error('observable MAY ONLY be created from a non-null object'); - } else if (target[oMetaKey]) { - return target; - } else if (Array.isArray(target)) { - return new ObservableArray({ target: target, ownKey: null, parent: null, options: options }).proxy; - } else if (ArrayBuffer.isView(target)) { - return new ObservableTypedArray({ target: target, ownKey: null, parent: null, options: options }).proxy; - } else if (target instanceof Date) { - throw new Error(`${target} found to be one of a non-observable types`); - } else { - return new ObservableObject({ target: target, ownKey: null, parent: null, options: options }).proxy; - } - }, + from: getObservableFromRoot, isObservable: input => { return !!(input && input[oMetaKey]); }, diff --git a/src/observables/abstract-base.ts b/src/observables/abstract-base.ts index c978ed5..fbb322d 100644 --- a/src/observables/abstract-base.ts +++ b/src/observables/abstract-base.ts @@ -20,13 +20,16 @@ export class ObservableBase implements ProxyHandler { #async: boolean = false; batches = []; + set; + deleteProperty; + #verifiers: Array = []; #observers: Array = []; - set = proxiedSet; - deleteProperty = proxiedDeleteProperty; - constructor(properties) { + this.set = proxiedSet; + this.deleteProperty = proxiedDeleteProperty; + const { target, parent, ownKey, options, visited = new Set() } = properties; if (parent && ownKey !== undefined) { this.#parent = parent; diff --git a/src/observables/array.ts b/src/observables/array.ts index 6efada3..fed46be 100644 --- a/src/observables/array.ts +++ b/src/observables/array.ts @@ -1,5 +1,5 @@ import { ObservableBase } from './abstract-base.ts'; -import { getObservedOf } from '../object-observer.ts'; +import { getObservedOf } from './processors/proc-utils.ts'; import { oMetaKey } from '../constants.ts'; import proxiedCopyWithin from './methods/copy-within.ts'; import proxiedFill from './methods/fill.ts'; @@ -32,10 +32,10 @@ export class ObservableArray extends ObservableBase { observedGraphProcessor(source: Array, observableWrapper: ObservableBase, visited: Set): Array { const arrayLength = source.length; const target = new Array(arrayLength); - target[oMetaKey] = observableWrapper; for (let i = 0; i < arrayLength; i++) { target[i] = getObservedOf(source[i], i, observableWrapper, visited); } + target[oMetaKey] = observableWrapper; return target; } } \ No newline at end of file diff --git a/src/observables/methods/copy-within.ts b/src/observables/methods/copy-within.ts index 877ec79..a3a72eb 100644 --- a/src/observables/methods/copy-within.ts +++ b/src/observables/methods/copy-within.ts @@ -1,6 +1,7 @@ import { Change } from '../../model/change.ts'; import { UPDATE, oMetaKey } from '../../constants.ts'; -import { callObservers, getObservedOf } from '../../object-observer.ts'; +import { getObservedOf } from '../processors/proc-utils.ts'; +import { callObservers } from '../processors/proc-utils.ts'; export default function proxiedCopyWithin(dest, start, end) { const oMeta = this[oMetaKey]; diff --git a/src/observables/methods/delete-property.ts b/src/observables/methods/delete-property.ts index 3516fff..b3aa0cf 100644 --- a/src/observables/methods/delete-property.ts +++ b/src/observables/methods/delete-property.ts @@ -1,7 +1,7 @@ import { Change } from '../../model/change.ts'; import { DELETE } from '../../constants.ts'; import { oMetaKey } from '../../constants.ts'; -import { callObservers } from '../../object-observer.ts'; +import { callObservers } from '../processors/proc-utils.ts'; export function proxiedDeleteProperty(target: object, key: string | symbol): boolean { let oldValue = target[key]; diff --git a/src/observables/methods/fill.ts b/src/observables/methods/fill.ts index 409efa2..35725b6 100644 --- a/src/observables/methods/fill.ts +++ b/src/observables/methods/fill.ts @@ -1,6 +1,7 @@ import { Change } from '../../model/change.ts'; import { INSERT, UPDATE, oMetaKey } from '../../constants.ts'; -import { callObservers, getObservedOf } from '../../object-observer.ts'; +import { getObservedOf } from '../processors/proc-utils.ts'; +import { callObservers } from '../processors/proc-utils.ts'; export default function proxiedFill(filVal, start, end) { const oMeta = this[oMetaKey]; diff --git a/src/observables/methods/pop.ts b/src/observables/methods/pop.ts index ccc532d..d8dd22c 100644 --- a/src/observables/methods/pop.ts +++ b/src/observables/methods/pop.ts @@ -1,6 +1,6 @@ import { Change } from '../../model/change.ts'; import { DELETE, oMetaKey } from '../../constants.ts'; -import { callObservers } from '../../object-observer.ts'; +import { callObservers } from '../processors/proc-utils.ts'; export default function proxiedPop() { const oMeta = this[oMetaKey]; diff --git a/src/observables/methods/push.ts b/src/observables/methods/push.ts index cfae108..56d15bf 100644 --- a/src/observables/methods/push.ts +++ b/src/observables/methods/push.ts @@ -1,6 +1,7 @@ import { Change } from '../../model/change.ts'; import { INSERT, oMetaKey } from '../../constants.ts'; -import { callObservers, getObservedOf } from '../../object-observer.ts'; +import { getObservedOf } from '../processors/proc-utils.ts'; +import { callObservers } from '../processors/proc-utils.ts'; export default function proxiedPush(...pushItems: unknown[]) { const oMeta = this[oMetaKey]; diff --git a/src/observables/methods/reverse.ts b/src/observables/methods/reverse.ts index 91edf2c..2feb03c 100644 --- a/src/observables/methods/reverse.ts +++ b/src/observables/methods/reverse.ts @@ -1,6 +1,6 @@ import { Change } from '../../model/change.ts'; import { REVERSE, oMetaKey } from '../../constants.ts'; -import { callObservers } from '../../object-observer.ts'; +import { callObservers } from '../processors/proc-utils.ts'; export default function proxiedReverse() { const oMeta = this[oMetaKey]; diff --git a/src/observables/methods/set.ts b/src/observables/methods/set.ts index dd75248..d517b8f 100644 --- a/src/observables/methods/set.ts +++ b/src/observables/methods/set.ts @@ -1,7 +1,8 @@ import { Change } from '../../model/change.ts'; import { INSERT, UPDATE } from '../../constants.ts'; import { oMetaKey } from '../../constants.ts'; -import { callObservers, getObservedOf } from '../../object-observer.ts'; +import { getObservedOf } from '../processors/proc-utils.ts'; +import { callObservers } from '../processors/proc-utils.ts'; export function proxiedSet(target: object, key: string | symbol, value: unknown): boolean { let oldValue = target[key]; diff --git a/src/observables/methods/shift.ts b/src/observables/methods/shift.ts index 9ee1688..35bf5cb 100644 --- a/src/observables/methods/shift.ts +++ b/src/observables/methods/shift.ts @@ -1,6 +1,6 @@ import { Change } from '../../model/change.ts'; import { DELETE, oMetaKey } from '../../constants.ts'; -import { callObservers } from '../../object-observer.ts'; +import { callObservers } from '../processors/proc-utils.ts'; export default function proxiedShift() { const diff --git a/src/observables/methods/sort.ts b/src/observables/methods/sort.ts index 889eb3a..1c4459f 100644 --- a/src/observables/methods/sort.ts +++ b/src/observables/methods/sort.ts @@ -1,6 +1,6 @@ import { Change } from '../../model/change.ts'; import { SHUFFLE, oMetaKey } from '../../constants.ts'; -import { callObservers } from '../../object-observer.ts'; +import { callObservers } from '../processors/proc-utils.ts'; export default function proxiedSort(comparator) { const oMeta = this[oMetaKey]; diff --git a/src/observables/methods/splice.ts b/src/observables/methods/splice.ts index b9fb284..c3b3208 100644 --- a/src/observables/methods/splice.ts +++ b/src/observables/methods/splice.ts @@ -1,6 +1,7 @@ import { Change } from '../../model/change.ts'; import { INSERT, DELETE, UPDATE, oMetaKey } from '../../constants.ts'; -import { callObservers, getObservedOf } from '../../object-observer.ts'; +import { getObservedOf } from '../processors/proc-utils.ts'; +import { callObservers } from '../processors/proc-utils.ts'; export default function proxiedSplice(...spliceItems: unknown[]) { const oMeta = this[oMetaKey]; diff --git a/src/observables/methods/typed-set.ts b/src/observables/methods/typed-set.ts index 1636465..89480cf 100644 --- a/src/observables/methods/typed-set.ts +++ b/src/observables/methods/typed-set.ts @@ -1,6 +1,6 @@ import { Change } from '../../model/change.ts'; import { UPDATE, oMetaKey } from '../../constants.ts'; -import { callObservers } from '../../object-observer.ts'; +import { callObservers } from '../processors/proc-utils.ts'; export default function proxiedTypedArraySet(source, offset) { const oMeta = this[oMetaKey]; diff --git a/src/observables/methods/unshift.ts b/src/observables/methods/unshift.ts index 17543b9..29aa8bf 100644 --- a/src/observables/methods/unshift.ts +++ b/src/observables/methods/unshift.ts @@ -1,6 +1,7 @@ import { Change } from '../../model/change.ts'; import { INSERT, oMetaKey } from '../../constants.ts'; -import { callObservers, getObservedOf } from '../../object-observer.ts'; +import { getObservedOf } from '../processors/proc-utils.ts'; +import { callObservers } from '../processors/proc-utils.ts'; export default function proxiedUnshift(...unshiftItems: unknown[]) { const oMeta = this[oMetaKey]; diff --git a/src/observables/object.ts b/src/observables/object.ts index 54fd248..8e1f0c1 100644 --- a/src/observables/object.ts +++ b/src/observables/object.ts @@ -1,5 +1,5 @@ import { ObservableBase } from './abstract-base.ts'; -import { getObservedOf } from '../object-observer.ts'; +import { getObservedOf } from './processors/proc-utils.ts'; import { oMetaKey } from '../constants.ts'; export class ObservableObject extends ObservableBase { diff --git a/src/observables/processors/proc-utils.ts b/src/observables/processors/proc-utils.ts new file mode 100644 index 0000000..e8be93f --- /dev/null +++ b/src/observables/processors/proc-utils.ts @@ -0,0 +1,139 @@ +import { oMetaKey, REVERSE, SHUFFLE } from '../../constants.ts'; +import { Change } from '../../model/change.ts'; +import { ObservableBase } from '../abstract-base.ts'; +import { ObservableArray } from '../array.ts'; +import { ObservableObject } from '../object.ts'; +import { ObservableTypedArray } from '../typed-array.ts'; + +export function getObservedOf(item: unknown, key: string | symbol | number, parent: object, visited?: Set): unknown { + if (visited !== undefined && visited.has(item)) { + return null; + } else if (typeof item !== 'object' || item === null) { + return item; + } else if (Array.isArray(item)) { + return new ObservableArray({ target: item, ownKey: key, parent: parent, visited }).proxy; + } else if (ArrayBuffer.isView(item)) { + return new ObservableTypedArray({ target: item, ownKey: key, parent: parent }).proxy; + } else if (item instanceof Date) { + return item; + } else { + return new ObservableObject({ target: item, ownKey: key, parent: parent, visited }).proxy; + } +}; + +export function getObservableFromRoot(target: unknown, options = undefined): unknown { + if (!target || typeof target !== 'object') { + throw new Error('observable MAY ONLY be created from a non-null object'); + } else if (target[oMetaKey]) { + return target; + } else if (Array.isArray(target)) { + return new ObservableArray({ target: target, ownKey: null, parent: null, options: options }).proxy; + } else if (ArrayBuffer.isView(target)) { + return new ObservableTypedArray({ target: target, ownKey: null, parent: null, options: options }).proxy; + } else if (target instanceof Date) { + throw new Error(`${target} found to be one of a non-observable types`); + } else { + return new ObservableObject({ target: target, ownKey: null, parent: null, options: options }).proxy; + } +} + +export function callObservers(oMeta: ObservableBase, changes: Change[]) { + let currentObservable: ObservableBase = oMeta; + let isAsync, observers, target, options, relevantChanges, i; + const l = changes.length; + do { + isAsync = currentObservable.async; + observers = currentObservable.observers; + i = observers.length; + while (i--) { + [target, options] = observers[i]; + relevantChanges = filterChanges(options, changes); + + if (relevantChanges.length) { + if (isAsync) { + // this is the async dispatch handling + if (currentObservable.batches.length === 0) { + queueMicrotask(callObserversFromMT.bind(currentObservable)); + } + let rb; + for (const b of currentObservable.batches) { + if (b[0] === target) { + rb = b; + break; + } + } + if (!rb) { + rb = [target, []]; + currentObservable.batches.push(rb); + } + Array.prototype.push.apply(rb[1], relevantChanges); + } else { + // this is the naive straight forward synchronous dispatch + callObserverSafe(target, relevantChanges); + } + } + } + + // cloning all the changes and notifying in context of parent + const parent = currentObservable.parent; + if (parent) { + for (let j = 0; j < l; j++) { + const change = changes[j]; + changes[j] = new Change( + change.type, + [currentObservable.ownKey, ...change.path], + change.value, + change.oldValue, + change.object + ); + } + currentObservable = parent; + } else { + break; + } + } while (currentObservable); +}; + +function filterChanges(options, changes) { + if (options === null) { + return changes; + } + + let result = changes; + if (options.path) { + const oPath = options.path; + result = changes.filter(change => + change.path.join('.') === oPath + ); + } else if (options.pathsOf) { + const oPathsOf = options.pathsOf; + const oPathsOfStr = oPathsOf.join('.'); + result = changes.filter(change => + (change.path.length === oPathsOf.length + 1 || + (change.path.length === oPathsOf.length && (change.type === REVERSE || change.type === SHUFFLE))) && + change.path.join('.').startsWith(oPathsOfStr) + ); + } else if (options.pathsFrom) { + const oPathsFrom = options.pathsFrom; + result = changes.filter(change => + change.path.join('.').startsWith(oPathsFrom) + ); + } + return result; +} + +function callObserverSafe(listener, changes) { + try { + listener(changes); + } catch (e) { + console.error(`failed to notify listener ${listener} with ${changes}`, e); + } +} + +function callObserversFromMT() { + const batches = this.batches; + this.batches = []; + for (const [listener, changes] of batches) { + callObserverSafe(listener, changes); + } +}; \ No newline at end of file diff --git a/src/observables/typed-array.ts b/src/observables/typed-array.ts index 43bf335..a559069 100644 --- a/src/observables/typed-array.ts +++ b/src/observables/typed-array.ts @@ -16,8 +16,8 @@ const proxiedTypedArrayMethods = { export class ObservableTypedArray extends ObservableBase { - get(target, key) { - return proxiedTypedArrayMethods[key] || target[key]; + get(taget: object, key: string): unknown { + return proxiedTypedArrayMethods[key] || taget[key]; } observedGraphProcessor(source: Array, observableWrapper: ObservableBase): Array { diff --git a/sri.json b/sri.json index 3d3c997..8779d84 100644 --- a/sri.json +++ b/sri.json @@ -1,6 +1,6 @@ { - "dist/cdn/object-observer.js": "sha512-luTTytJfesi/3p9Qc60gZ09F0xPZZmj4tA9LGKwX6zb1zei6IlMBQrIjrQmAiko4VfnT7xZTebOnh42XUkyVjA==", - "dist/cdn/object-observer.js.map": "sha512-FQf/rgiqP2J5W01fKI22Nf0AE+1ZJ7+VE2wO4ntWlD9J8vTHtB+T0ERK31/NARR1eA0k62HEDl3lKLJfL9svSQ==", - "dist/cdn/object-observer.min.js": "sha512-31PH8SxOaXOdd8RfUPuG9bdhWRYx0yZk8Dq9mddTUtpqfcMZkMY1Wp8/p4pOs2WAn1cjNXasDkZJlLVfhZxDwQ==", - "dist/cdn/object-observer.min.js.map": "sha512-boy/9Y9KXpaQvVhDzUyR8iZuAgUihfh1U8kQWGd79F623jw2Po9Q8xt8sYLAbTSSHTSs0PTwNYD2cv598zCMog==" + "dist/cdn/object-observer.js": "sha512-Zx04P9Gj6K9htKF/y/RlNREgOBLSFBwyOcztc8cNAE/FMcL+Pb+HI/emhuL0AfwG1a9JvYmIiDw9uR9DXbV0fw==", + "dist/cdn/object-observer.js.map": "sha512-Zspm1WDuDmQNns2KuBs3Hh1z0Cm+Hk6cKfixc3KkRsIhOmYxb+Huv0V+sXS9xDJsp2dWFzVvh258hd4A6tgvwA==", + "dist/cdn/object-observer.min.js": "sha512-r8Iy4NNZpcuuhtx+h5U37dBLHmcZ1E9hu6BbwQIfaKeE1CcAM43gFqPgDYCrSrMU85FCn5i3MVa9f6Ae3wuigg==", + "dist/cdn/object-observer.min.js.map": "sha512-oSVBWidsNmzCE8rLha1G+jF4y71r7NGsXORQXeiSH76hIhwaoAyL5V1VkYy06Wec5FRdLoQrfoIXSSrsBU5vbA==" } \ No newline at end of file From ee201e9bf81b7d00a9fea01e086195a9244c0104 Mon Sep 17 00:00:00 2001 From: Yuri Guller Date: Tue, 18 Nov 2025 19:18:44 +0200 Subject: [PATCH 10/23] fix --- sri.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/sri.json b/sri.json index 8779d84..f487df0 100644 --- a/sri.json +++ b/sri.json @@ -1,6 +1,6 @@ { - "dist/cdn/object-observer.js": "sha512-Zx04P9Gj6K9htKF/y/RlNREgOBLSFBwyOcztc8cNAE/FMcL+Pb+HI/emhuL0AfwG1a9JvYmIiDw9uR9DXbV0fw==", - "dist/cdn/object-observer.js.map": "sha512-Zspm1WDuDmQNns2KuBs3Hh1z0Cm+Hk6cKfixc3KkRsIhOmYxb+Huv0V+sXS9xDJsp2dWFzVvh258hd4A6tgvwA==", - "dist/cdn/object-observer.min.js": "sha512-r8Iy4NNZpcuuhtx+h5U37dBLHmcZ1E9hu6BbwQIfaKeE1CcAM43gFqPgDYCrSrMU85FCn5i3MVa9f6Ae3wuigg==", - "dist/cdn/object-observer.min.js.map": "sha512-oSVBWidsNmzCE8rLha1G+jF4y71r7NGsXORQXeiSH76hIhwaoAyL5V1VkYy06Wec5FRdLoQrfoIXSSrsBU5vbA==" + "dist/cdn/object-observer.js": "sha512-P4pkZ11qV2l9nf3LYnwgm5oC1LY7rWaaJpOOcGSCeNDStNDH+akJEWsltvnX9tSuprxJl8XOa2GoYiRzVJaxaQ==", + "dist/cdn/object-observer.js.map": "sha512-FONqa/XOwyN6O7HyQyO8bpCfksGJ1kY62kqOOyXlFCOTmJRtnX8O/2O6kG6TgvFUycNzodnofWQ+DKoFtmipyA==", + "dist/cdn/object-observer.min.js": "sha512-kO0nlxJUjqEeMz62gkuho9Yz7xaykdOz8M5wYlejCKtV6XHNP4xTCD7k5TucIlKMIv0ogzK+RWzvPMN0gM8TYQ==", + "dist/cdn/object-observer.min.js.map": "sha512-8IuhGFJD2VjfAmL0Kdm7qQTO821JD5Ko80bKzs7haopUPBDvV1HwXYkPvsQN46z7ezUABgjPDgSKzXeILqdLCg==" } \ No newline at end of file From 32e6ebbbd8742b72955ba0bd7a0eeb9a1dac0c14 Mon Sep 17 00:00:00 2001 From: Yuri Guller Date: Mon, 8 Dec 2025 00:07:36 +0200 Subject: [PATCH 11/23] test --- package-lock.json | 369 ++++++-------------- package.json | 12 +- tests/configs/tests-config-ci-chromium.js | 31 ++ tests/configs/tests-config-ci-chromium.json | 28 -- tests/configs/tests-config-ci-firefox.js | 26 ++ tests/configs/tests-config-ci-firefox.json | 23 -- tests/configs/tests-config-ci-node.js | 27 ++ tests/configs/tests-config-ci-node.json | 24 -- tests/configs/tests-config-ci-webkit.js | 26 ++ tests/configs/tests-config-ci-webkit.json | 23 -- 10 files changed, 215 insertions(+), 374 deletions(-) create mode 100644 tests/configs/tests-config-ci-chromium.js delete mode 100644 tests/configs/tests-config-ci-chromium.json create mode 100644 tests/configs/tests-config-ci-firefox.js delete mode 100644 tests/configs/tests-config-ci-firefox.json create mode 100644 tests/configs/tests-config-ci-node.js delete mode 100644 tests/configs/tests-config-ci-node.json create mode 100644 tests/configs/tests-config-ci-webkit.js delete mode 100644 tests/configs/tests-config-ci-webkit.json diff --git a/package-lock.json b/package-lock.json index 752d554..f72f1e7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,12 +18,12 @@ "license": "ISC", "devDependencies": { "@eslint/js": "^9.39.1", - "@gullerya/just-test": "^4.0.14", + "@gullerya/just-test": "^4.0.16", "esbuild": "^0.27.0", "eslint": "^9.39.1", "globals": "^16.5.0", "typescript": "^5.9.3", - "typescript-eslint": "^8.46.4" + "typescript-eslint": "^8.48.0" } }, "node_modules/@esbuild/aix-ppc64": { @@ -652,9 +652,9 @@ } }, "node_modules/@gullerya/just-test": { - "version": "4.0.14", - "resolved": "https://registry.npmjs.org/@gullerya/just-test/-/just-test-4.0.14.tgz", - "integrity": "sha512-pvCV/cPpEwxoqcQU045FI8T52T/qoj1odRZT25ggN+4VaF97R+ZM7OrYfHgRw/nZtoN/hJKf+nA8bBbipxvYcw==", + "version": "4.0.16", + "resolved": "https://registry.npmjs.org/@gullerya/just-test/-/just-test-4.0.16.tgz", + "integrity": "sha512-BnWoLwPWOYPj+fKHNcCjWxJDv80bm1qiX7FM/wJAUMXN5URhK11SuGd7AvyXhH0/ogj7XfCUMTpSh9CDetI+7A==", "dev": true, "license": "MIT", "dependencies": { @@ -663,7 +663,8 @@ "glob": "^12.0.0", "minimatch": "^10.1.1", "playwright": "^1.56.1", - "rich-component": "^1.8.0" + "rich-component": "^1.8.0", + "typescript": "^5.9.3" }, "funding": { "url": "https://paypal.me/gullerya?locale.x=en_US" @@ -777,44 +778,6 @@ "node": ">=12" } }, - "node_modules/@nodelib/fs.scandir": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.walk": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - }, - "engines": { - "node": ">= 8" - } - }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -830,17 +793,17 @@ "license": "MIT" }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.46.4", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.46.4.tgz", - "integrity": "sha512-R48VhmTJqplNyDxCyqqVkFSZIx1qX6PzwqgcXn1olLrzxcSBDlOsbtcnQuQhNtnNiJ4Xe5gREI1foajYaYU2Vg==", + "version": "8.48.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.48.0.tgz", + "integrity": "sha512-XxXP5tL1txl13YFtrECECQYeZjBZad4fyd3cFV4a19LkAY/bIp9fev3US4S5fDVV2JaYFiKAZ/GRTOLer+mbyQ==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.46.4", - "@typescript-eslint/type-utils": "8.46.4", - "@typescript-eslint/utils": "8.46.4", - "@typescript-eslint/visitor-keys": "8.46.4", + "@typescript-eslint/scope-manager": "8.48.0", + "@typescript-eslint/type-utils": "8.48.0", + "@typescript-eslint/utils": "8.48.0", + "@typescript-eslint/visitor-keys": "8.48.0", "graphemer": "^1.4.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", @@ -854,7 +817,7 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.46.4", + "@typescript-eslint/parser": "^8.48.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } @@ -870,17 +833,17 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.46.4", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.46.4.tgz", - "integrity": "sha512-tK3GPFWbirvNgsNKto+UmB/cRtn6TZfyw0D6IKrW55n6Vbs7KJoZtI//kpTKzE/DUmmnAFD8/Ca46s7Obs92/w==", + "version": "8.48.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.48.0.tgz", + "integrity": "sha512-jCzKdm/QK0Kg4V4IK/oMlRZlY+QOcdjv89U2NgKHZk1CYTj82/RVSx1mV/0gqCVMJ/DA+Zf/S4NBWNF8GQ+eqQ==", "dev": true, "license": "MIT", "peer": true, "dependencies": { - "@typescript-eslint/scope-manager": "8.46.4", - "@typescript-eslint/types": "8.46.4", - "@typescript-eslint/typescript-estree": "8.46.4", - "@typescript-eslint/visitor-keys": "8.46.4", + "@typescript-eslint/scope-manager": "8.48.0", + "@typescript-eslint/types": "8.48.0", + "@typescript-eslint/typescript-estree": "8.48.0", + "@typescript-eslint/visitor-keys": "8.48.0", "debug": "^4.3.4" }, "engines": { @@ -896,14 +859,14 @@ } }, "node_modules/@typescript-eslint/project-service": { - "version": "8.46.4", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.46.4.tgz", - "integrity": "sha512-nPiRSKuvtTN+no/2N1kt2tUh/HoFzeEgOm9fQ6XQk4/ApGqjx0zFIIaLJ6wooR1HIoozvj2j6vTi/1fgAz7UYQ==", + "version": "8.48.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.48.0.tgz", + "integrity": "sha512-Ne4CTZyRh1BecBf84siv42wv5vQvVmgtk8AuiEffKTUo3DrBaGYZueJSxxBZ8fjk/N3DrgChH4TOdIOwOwiqqw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.46.4", - "@typescript-eslint/types": "^8.46.4", + "@typescript-eslint/tsconfig-utils": "^8.48.0", + "@typescript-eslint/types": "^8.48.0", "debug": "^4.3.4" }, "engines": { @@ -918,14 +881,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.46.4", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.46.4.tgz", - "integrity": "sha512-tMDbLGXb1wC+McN1M6QeDx7P7c0UWO5z9CXqp7J8E+xGcJuUuevWKxuG8j41FoweS3+L41SkyKKkia16jpX7CA==", + "version": "8.48.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.48.0.tgz", + "integrity": "sha512-uGSSsbrtJrLduti0Q1Q9+BF1/iFKaxGoQwjWOIVNJv0o6omrdyR8ct37m4xIl5Zzpkp69Kkmvom7QFTtue89YQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.46.4", - "@typescript-eslint/visitor-keys": "8.46.4" + "@typescript-eslint/types": "8.48.0", + "@typescript-eslint/visitor-keys": "8.48.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -936,9 +899,9 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.46.4", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.46.4.tgz", - "integrity": "sha512-+/XqaZPIAk6Cjg7NWgSGe27X4zMGqrFqZ8atJsX3CWxH/jACqWnrWI68h7nHQld0y+k9eTTjb9r+KU4twLoo9A==", + "version": "8.48.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.48.0.tgz", + "integrity": "sha512-WNebjBdFdyu10sR1M4OXTt2OkMd5KWIL+LLfeH9KhgP+jzfDV/LI3eXzwJ1s9+Yc0Kzo2fQCdY/OpdusCMmh6w==", "dev": true, "license": "MIT", "engines": { @@ -953,15 +916,15 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.46.4", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.46.4.tgz", - "integrity": "sha512-V4QC8h3fdT5Wro6vANk6eojqfbv5bpwHuMsBcJUJkqs2z5XnYhJzyz9Y02eUmF9u3PgXEUiOt4w4KHR3P+z0PQ==", + "version": "8.48.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.48.0.tgz", + "integrity": "sha512-zbeVaVqeXhhab6QNEKfK96Xyc7UQuoFWERhEnj3mLVnUWrQnv15cJNseUni7f3g557gm0e46LZ6IJ4NJVOgOpw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.46.4", - "@typescript-eslint/typescript-estree": "8.46.4", - "@typescript-eslint/utils": "8.46.4", + "@typescript-eslint/types": "8.48.0", + "@typescript-eslint/typescript-estree": "8.48.0", + "@typescript-eslint/utils": "8.48.0", "debug": "^4.3.4", "ts-api-utils": "^2.1.0" }, @@ -978,9 +941,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.46.4", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.46.4.tgz", - "integrity": "sha512-USjyxm3gQEePdUwJBFjjGNG18xY9A2grDVGuk7/9AkjIF1L+ZrVnwR5VAU5JXtUnBL/Nwt3H31KlRDaksnM7/w==", + "version": "8.48.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.48.0.tgz", + "integrity": "sha512-cQMcGQQH7kwKoVswD1xdOytxQR60MWKM1di26xSUtxehaDs/32Zpqsu5WJlXTtTTqyAVK8R7hvsUnIXRS+bjvA==", "dev": true, "license": "MIT", "engines": { @@ -992,21 +955,20 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.46.4", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.46.4.tgz", - "integrity": "sha512-7oV2qEOr1d4NWNmpXLR35LvCfOkTNymY9oyW+lUHkmCno7aOmIf/hMaydnJBUTBMRCOGZh8YjkFOc8dadEoNGA==", + "version": "8.48.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.48.0.tgz", + "integrity": "sha512-ljHab1CSO4rGrQIAyizUS6UGHHCiAYhbfcIZ1zVJr5nMryxlXMVWS3duFPSKvSUbFPwkXMFk1k0EMIjub4sRRQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.46.4", - "@typescript-eslint/tsconfig-utils": "8.46.4", - "@typescript-eslint/types": "8.46.4", - "@typescript-eslint/visitor-keys": "8.46.4", + "@typescript-eslint/project-service": "8.48.0", + "@typescript-eslint/tsconfig-utils": "8.48.0", + "@typescript-eslint/types": "8.48.0", + "@typescript-eslint/visitor-keys": "8.48.0", "debug": "^4.3.4", - "fast-glob": "^3.3.2", - "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", + "tinyglobby": "^0.2.15", "ts-api-utils": "^2.1.0" }, "engines": { @@ -1047,16 +1009,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.46.4", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.46.4.tgz", - "integrity": "sha512-AbSv11fklGXV6T28dp2Me04Uw90R2iJ30g2bgLz529Koehrmkbs1r7paFqr1vPCZi7hHwYxYtxfyQMRC8QaVSg==", + "version": "8.48.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.48.0.tgz", + "integrity": "sha512-yTJO1XuGxCsSfIVt1+1UrLHtue8xz16V8apzPYI06W0HbEbEWHxHXgZaAgavIkoh+GeV6hKKd5jm0sS6OYxWXQ==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", - "@typescript-eslint/scope-manager": "8.46.4", - "@typescript-eslint/types": "8.46.4", - "@typescript-eslint/typescript-estree": "8.46.4" + "@typescript-eslint/scope-manager": "8.48.0", + "@typescript-eslint/types": "8.48.0", + "@typescript-eslint/typescript-estree": "8.48.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1071,13 +1033,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.46.4", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.46.4.tgz", - "integrity": "sha512-/++5CYLQqsO9HFGLI7APrxBJYo+5OCMpViuhV8q5/Qa3o5mMrF//eQHks+PXcsAVaLdn817fMuS7zqoXNNZGaw==", + "version": "8.48.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.48.0.tgz", + "integrity": "sha512-T0XJMaRPOH3+LBbAfzR2jalckP1MSG/L9eUtY0DEzUyVaXJ/t6zN0nR7co5kz0Jko/nkSYCBRkz1djvjajVTTg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.46.4", + "@typescript-eslint/types": "8.48.0", "eslint-visitor-keys": "^4.2.1" }, "engines": { @@ -1183,19 +1145,6 @@ "concat-map": "0.0.1" } }, - "node_modules/braces": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "dev": true, - "license": "MIT", - "dependencies": { - "fill-range": "^7.1.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -1566,36 +1515,6 @@ "dev": true, "license": "MIT" }, - "node_modules/fast-glob": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", - "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.8" - }, - "engines": { - "node": ">=8.6.0" - } - }, - "node_modules/fast-glob/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", @@ -1610,14 +1529,22 @@ "dev": true, "license": "MIT" }, - "node_modules/fastq": { - "version": "1.19.1", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", - "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", "dev": true, - "license": "ISC", - "dependencies": { - "reusify": "^1.0.4" + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } } }, "node_modules/file-entry-cache": { @@ -1633,19 +1560,6 @@ "node": ">=16.0.0" } }, - "node_modules/fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "dev": true, - "license": "MIT", - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -1853,16 +1767,6 @@ "node": ">=0.10.0" } }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.12.0" - } - }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -1977,30 +1881,6 @@ "node": "20 || >=22" } }, - "node_modules/merge2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/micromatch": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", - "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", - "dev": true, - "license": "MIT", - "dependencies": { - "braces": "^3.0.3", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } - }, "node_modules/minimatch": { "version": "10.1.1", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.1.tgz", @@ -2149,13 +2029,14 @@ } }, "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { - "node": ">=8.6" + "node": ">=12" }, "funding": { "url": "https://github.com/sponsors/jonschlinkert" @@ -2213,27 +2094,6 @@ "node": ">=6" } }, - "node_modules/queue-microtask": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, "node_modules/resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", @@ -2244,17 +2104,6 @@ "node": ">=4" } }, - "node_modules/reusify": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", - "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", - "dev": true, - "license": "MIT", - "engines": { - "iojs": ">=1.0.0", - "node": ">=0.10.0" - } - }, "node_modules/rich-component": { "version": "1.8.0", "resolved": "https://registry.npmjs.org/rich-component/-/rich-component-1.8.0.tgz", @@ -2270,30 +2119,6 @@ ], "license": "ISC" }, - "node_modules/run-parallel": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", - "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "queue-microtask": "^1.2.2" - } - }, "node_modules/semver": { "version": "7.7.3", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", @@ -2473,17 +2298,21 @@ "node": ">=8" } }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", "dev": true, "license": "MIT", "dependencies": { - "is-number": "^7.0.0" + "fdir": "^6.5.0", + "picomatch": "^4.0.3" }, "engines": { - "node": ">=8.0" + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" } }, "node_modules/ts-api-utils": { @@ -2528,16 +2357,16 @@ } }, "node_modules/typescript-eslint": { - "version": "8.46.4", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.46.4.tgz", - "integrity": "sha512-KALyxkpYV5Ix7UhvjTwJXZv76VWsHG+NjNlt/z+a17SOQSiOcBdUXdbJdyXi7RPxrBFECtFOiPwUJQusJuCqrg==", + "version": "8.48.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.48.0.tgz", + "integrity": "sha512-fcKOvQD9GUn3Xw63EgiDqhvWJ5jsyZUaekl3KVpGsDJnN46WJTe3jWxtQP9lMZm1LJNkFLlTaWAxK2vUQR+cqw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/eslint-plugin": "8.46.4", - "@typescript-eslint/parser": "8.46.4", - "@typescript-eslint/typescript-estree": "8.46.4", - "@typescript-eslint/utils": "8.46.4" + "@typescript-eslint/eslint-plugin": "8.48.0", + "@typescript-eslint/parser": "8.48.0", + "@typescript-eslint/typescript-estree": "8.48.0", + "@typescript-eslint/utils": "8.48.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" diff --git a/package.json b/package.json index 4358994..058c1ea 100644 --- a/package.json +++ b/package.json @@ -64,21 +64,21 @@ "lint": "eslint -c ./ci/eslint.config.mjs ./src/* ./tests/*.js ./ci/**/*.js", "test-types": "tsc", "test": "node node_modules/@gullerya/just-test/bin/local-runner.js config_file=./tests/configs/tests-config-ci-node.json", - "test:chromium": "node node_modules/@gullerya/just-test/bin/local-runner.js config_file=./tests/configs/tests-config-ci-chromium.json", - "test:firefox": "node node_modules/@gullerya/just-test/bin/local-runner.js config_file=./tests/configs/tests-config-ci-firefox.json", - "test:webkit": "node node_modules/@gullerya/just-test/bin/local-runner.js config_file=./tests/configs/tests-config-ci-webkit.json", - "test:nodejs": "node node_modules/@gullerya/just-test/bin/local-runner.js config_file=./tests/configs/tests-config-ci-node.json", + "test:chromium": "node node_modules/@gullerya/just-test/bin/local-runner.ts config_file=./tests/configs/tests-config-ci-chromium.js", + "test:firefox": "node node_modules/@gullerya/just-test/bin/local-runner.ts config_file=./tests/configs/tests-config-ci-firefox.js", + "test:webkit": "node node_modules/@gullerya/just-test/bin/local-runner.ts config_file=./tests/configs/tests-config-ci-webkit.js", + "test:nodejs": "node node_modules/@gullerya/just-test/bin/local-runner.ts config_file=./tests/configs/tests-config-ci-node.js", "version": "npm run build && git add --all", "postversion": "git push && git push --tags" }, "devDependencies": { "@eslint/js": "^9.39.1", - "@gullerya/just-test": "^4.0.14", + "@gullerya/just-test": "^4.0.16", "esbuild": "^0.27.0", "eslint": "^9.39.1", "globals": "^16.5.0", "typescript": "^5.9.3", - "typescript-eslint": "^8.46.4" + "typescript-eslint": "^8.48.0" }, "publishConfig": { "access": "public" diff --git a/tests/configs/tests-config-ci-chromium.js b/tests/configs/tests-config-ci-chromium.js new file mode 100644 index 0000000..2fa403a --- /dev/null +++ b/tests/configs/tests-config-ci-chromium.js @@ -0,0 +1,31 @@ +const config = { + environments: [ + { + browser: { + type: 'chromium', + executors: { + type: 'iframe' + } + }, + tests: { + ttl: 32000, + maxFail: 0, + maxSkip: 5, + include: [ + './tests/*' + ], + exclude: [ + '**/configs/**', + '**/*-performance-*.js' + ] + }, + coverage: { + include: [ + './src/**/*' + ] + } + } + ] +} + +export default config; \ No newline at end of file diff --git a/tests/configs/tests-config-ci-chromium.json b/tests/configs/tests-config-ci-chromium.json deleted file mode 100644 index 70a3a3c..0000000 --- a/tests/configs/tests-config-ci-chromium.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "environments": [ - { - "browser": { - "type": "chromium", - "executors": { - "type": "iframe" - } - }, - "tests": { - "ttl": 32000, - "maxFail": 0, - "maxSkip": 5, - "include": [ - "./tests/*" - ], - "exclude": [ - "**/tests/*-performance-*.js" - ] - }, - "coverage": { - "include": [ - "./src/**/*" - ] - } - } - ] -} \ No newline at end of file diff --git a/tests/configs/tests-config-ci-firefox.js b/tests/configs/tests-config-ci-firefox.js new file mode 100644 index 0000000..d6332db --- /dev/null +++ b/tests/configs/tests-config-ci-firefox.js @@ -0,0 +1,26 @@ +const config = { + environments: [ + { + browser: { + type: 'firefox', + executors: { + type: 'iframe' + } + }, + tests: { + ttl: 32000, + maxFail: 0, + maxSkip: 5, + include: [ + './tests/*' + ], + exclude: [ + '**/configs/**', + '**/*-performance-*.js' + ] + } + } + ] +}; + +export default config; \ No newline at end of file diff --git a/tests/configs/tests-config-ci-firefox.json b/tests/configs/tests-config-ci-firefox.json deleted file mode 100644 index 9082b6e..0000000 --- a/tests/configs/tests-config-ci-firefox.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "environments": [ - { - "browser": { - "type": "firefox", - "executors": { - "type": "iframe" - } - }, - "tests": { - "ttl": 32000, - "maxFail": 0, - "maxSkip": 5, - "include": [ - "./tests/*" - ], - "exclude": [ - "**/tests/*-performance-*.js" - ] - } - } - ] -} \ No newline at end of file diff --git a/tests/configs/tests-config-ci-node.js b/tests/configs/tests-config-ci-node.js new file mode 100644 index 0000000..ec7fece --- /dev/null +++ b/tests/configs/tests-config-ci-node.js @@ -0,0 +1,27 @@ +const config = { + environments: [ + { + node: true, + tests: { + ttl: 32000, + maxFail: 0, + maxSkip: 5, + include: [ + './tests/*' + ], + exclude: [ + '**/configs/**', + '**/browser-host-objects.js', + '**/*-performance-*.js' + ] + }, + coverage: { + include: [ + './src/**/*' + ] + } + } + ] +}; + +export default config; \ No newline at end of file diff --git a/tests/configs/tests-config-ci-node.json b/tests/configs/tests-config-ci-node.json deleted file mode 100644 index 7b611fa..0000000 --- a/tests/configs/tests-config-ci-node.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "environments": [ - { - "node": true, - "tests": { - "ttl": 32000, - "maxFail": 0, - "maxSkip": 5, - "include": [ - "./tests/*" - ], - "exclude": [ - "**/tests/browser-host-objects.js", - "**/tests/*-performance-*.js" - ] - }, - "coverage": { - "include": [ - "./src/**/*" - ] - } - } - ] -} \ No newline at end of file diff --git a/tests/configs/tests-config-ci-webkit.js b/tests/configs/tests-config-ci-webkit.js new file mode 100644 index 0000000..262ab8f --- /dev/null +++ b/tests/configs/tests-config-ci-webkit.js @@ -0,0 +1,26 @@ +const config = { + environments: [ + { + browser: { + type: 'webkit', + executors: { + type: 'iframe' + } + }, + tests: { + ttl: 32000, + maxFail: 0, + maxSkip: 5, + include: [ + './tests/*' + ], + exclude: [ + '**/configs/**', + '**/*-performance-*.js' + ] + } + } + ] +}; + +export default config; \ No newline at end of file diff --git a/tests/configs/tests-config-ci-webkit.json b/tests/configs/tests-config-ci-webkit.json deleted file mode 100644 index fe342bf..0000000 --- a/tests/configs/tests-config-ci-webkit.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "environments": [ - { - "browser": { - "type": "webkit", - "executors": { - "type": "iframe" - } - }, - "tests": { - "ttl": 32000, - "maxFail": 0, - "maxSkip": 5, - "include": [ - "./tests/*" - ], - "exclude": [ - "**/tests/*-performance-*.js" - ] - } - } - ] -} \ No newline at end of file From 9a6c6afebf835f3a6e40e05eaf73867a71c14342 Mon Sep 17 00:00:00 2001 From: Yuri Guller Date: Mon, 8 Dec 2025 08:56:09 +0200 Subject: [PATCH 12/23] fxing tests --- package-lock.json | 8 ++++---- package.json | 10 +++++----- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/package-lock.json b/package-lock.json index f72f1e7..0b24b31 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,7 +18,7 @@ "license": "ISC", "devDependencies": { "@eslint/js": "^9.39.1", - "@gullerya/just-test": "^4.0.16", + "@gullerya/just-test": "^4.0.17", "esbuild": "^0.27.0", "eslint": "^9.39.1", "globals": "^16.5.0", @@ -652,9 +652,9 @@ } }, "node_modules/@gullerya/just-test": { - "version": "4.0.16", - "resolved": "https://registry.npmjs.org/@gullerya/just-test/-/just-test-4.0.16.tgz", - "integrity": "sha512-BnWoLwPWOYPj+fKHNcCjWxJDv80bm1qiX7FM/wJAUMXN5URhK11SuGd7AvyXhH0/ogj7XfCUMTpSh9CDetI+7A==", + "version": "4.0.17", + "resolved": "https://registry.npmjs.org/@gullerya/just-test/-/just-test-4.0.17.tgz", + "integrity": "sha512-L9naPalogYi0aEQIodeSXuN/p+MGDd0z1WN89oVYdkJrquw2Sjfk14MXtSuNBz7FUvHw8Qwm1RdTi8keQNbyfg==", "dev": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 058c1ea..8e1e9d8 100644 --- a/package.json +++ b/package.json @@ -64,16 +64,16 @@ "lint": "eslint -c ./ci/eslint.config.mjs ./src/* ./tests/*.js ./ci/**/*.js", "test-types": "tsc", "test": "node node_modules/@gullerya/just-test/bin/local-runner.js config_file=./tests/configs/tests-config-ci-node.json", - "test:chromium": "node node_modules/@gullerya/just-test/bin/local-runner.ts config_file=./tests/configs/tests-config-ci-chromium.js", - "test:firefox": "node node_modules/@gullerya/just-test/bin/local-runner.ts config_file=./tests/configs/tests-config-ci-firefox.js", - "test:webkit": "node node_modules/@gullerya/just-test/bin/local-runner.ts config_file=./tests/configs/tests-config-ci-webkit.js", - "test:nodejs": "node node_modules/@gullerya/just-test/bin/local-runner.ts config_file=./tests/configs/tests-config-ci-node.js", + "test:chromium": "node node_modules/@gullerya/just-test/bin/local-runner.js config_file=./tests/configs/tests-config-ci-chromium.js", + "test:firefox": "node node_modules/@gullerya/just-test/bin/local-runner.js config_file=./tests/configs/tests-config-ci-firefox.js", + "test:webkit": "node node_modules/@gullerya/just-test/bin/local-runner.js config_file=./tests/configs/tests-config-ci-webkit.js", + "test:nodejs": "node node_modules/@gullerya/just-test/bin/local-runner.js config_file=./tests/configs/tests-config-ci-node.js", "version": "npm run build && git add --all", "postversion": "git push && git push --tags" }, "devDependencies": { "@eslint/js": "^9.39.1", - "@gullerya/just-test": "^4.0.16", + "@gullerya/just-test": "^4.0.17", "esbuild": "^0.27.0", "eslint": "^9.39.1", "globals": "^16.5.0", From cbaa20a4e51501cffba01c19db0a6b57d108be4a Mon Sep 17 00:00:00 2001 From: Yuri Guller Date: Mon, 8 Dec 2025 21:33:32 +0200 Subject: [PATCH 13/23] tests --- ci/tools/build-utils.js | 3 --- tests/filters.js | 38 ++++++++++++++++++++++++++++++++++++++ tsconfig.json | 2 +- 3 files changed, 39 insertions(+), 4 deletions(-) create mode 100644 tests/filters.js diff --git a/ci/tools/build-utils.js b/ci/tools/build-utils.js index d2e8c3d..0b74622 100644 --- a/ci/tools/build-utils.js +++ b/ci/tools/build-utils.js @@ -1,8 +1,6 @@ import path from 'node:path'; import fs from 'node:fs/promises'; - import esbuild from 'esbuild'; - import { calcIntegrity } from './integrity-utils.js'; import * as stdout from './stdout.js'; @@ -50,7 +48,6 @@ async function buildESModule() { }; await esbuild.build(config); await esbuild.build({ ...config, minify: true, outExtension: { '.js': '.min.js' } }); - // await fs.copyFile(path.join(SRC_DIR, 'object-observer.d.ts'), path.join(DIST_DIR, 'object-observer.d.ts')); stdout.writeGreen('\tOK'); stdout.writeNewline(); diff --git a/tests/filters.js b/tests/filters.js new file mode 100644 index 0000000..df87536 --- /dev/null +++ b/tests/filters.js @@ -0,0 +1,38 @@ +import { test } from '@gullerya/just-test'; +import { assert } from '@gullerya/just-test/assert'; +import { Filter } from '../src/changes-processors/filters.ts'; +import { Change } from '../src/model/change.ts'; + +test('test filters - ctor direct use forbidden', () => { + assert.throws(() => new Filter('some', (change) => change.prop !== 'skip'), 'Filter class cannot be instantiated directly', 'Filter class cannot be instantiated directly'); +}); + +test('custom filter', () => { + const filterLogic = changes => changes.filter(c => c.value !== null); + const f = Filter.custom(filterLogic); + assert.strictEqual(f.fn, filterLogic, 'Filter code is correct'); +}); + +test('exactPaths filter - positive cases', () => { + const f = Filter.exactPaths(['a', 'b.c']); + const changes = [ + new Change('update', ['a'], 1, 0), + new Change('update', ['b', 'c'], 2, 0), + new Change('update', ['a', 'b', 'c'], 3, 0), + new Change('update', ['b', 'c', 'd'], 4, 0) + ]; + const filtered = f.fn(changes); + assert.strictEqual(filtered.length, 2); +}); + +test('exactPaths filter - negative cases', () => { + assert.throws(() => Filter.exactPaths(null), 'exactPaths Filter requires a non-empty array as argument'); + assert.throws(() => Filter.exactPaths([]), 'exactPaths Filter requires a non-empty array as argument'); +}); + +test('test listeners invocation - listener is corrupted - null', () => { + +}); + +test('test listeners invocation - listener is corrupted - NaF', () => { +}); diff --git a/tsconfig.json b/tsconfig.json index 02f2174..6d27183 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,7 +1,7 @@ { "compilerOptions": { "strict": false, - "target": "es2022", + "target": "es2024", "noEmit": true, "allowImportingTsExtensions": true, "rewriteRelativeImportExtensions": true, From 60f126356274531868aeb16197e4a69eb44ab994 Mon Sep 17 00:00:00 2001 From: Yuri Guller Date: Mon, 8 Dec 2025 21:38:57 +0200 Subject: [PATCH 14/23] tests --- src/changes-processors/filters.ts | 1 - tests/filters.js | 7 ++++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/changes-processors/filters.ts b/src/changes-processors/filters.ts index 6c41203..136f2b2 100644 --- a/src/changes-processors/filters.ts +++ b/src/changes-processors/filters.ts @@ -25,7 +25,6 @@ export class Filter { if (!Array.isArray(paths) || paths.length === 0) { throw new Error('exactPaths Filter requires a non-empty array as argument'); } - const pathsSet = new Set(paths); return new Filter( Filter.#privateCtorKey, diff --git a/tests/filters.js b/tests/filters.js index df87536..dce9ff0 100644 --- a/tests/filters.js +++ b/tests/filters.js @@ -7,12 +7,17 @@ test('test filters - ctor direct use forbidden', () => { assert.throws(() => new Filter('some', (change) => change.prop !== 'skip'), 'Filter class cannot be instantiated directly', 'Filter class cannot be instantiated directly'); }); -test('custom filter', () => { +test('custom filter - positive cases', () => { const filterLogic = changes => changes.filter(c => c.value !== null); const f = Filter.custom(filterLogic); assert.strictEqual(f.fn, filterLogic, 'Filter code is correct'); }); +test('custom filter - negative cases', () => { + assert.throws(() => Filter.custom(null), 'custom Filter requires a function as argument'); + assert.throws(() => Filter.custom('some'), 'custom Filter requires a function as argument'); +}); + test('exactPaths filter - positive cases', () => { const f = Filter.exactPaths(['a', 'b.c']); const changes = [ From 77c5d1215218e48e07320b0ef7b219302a5af2ad Mon Sep 17 00:00:00 2001 From: Yuri Guller Date: Mon, 8 Dec 2025 21:41:34 +0200 Subject: [PATCH 15/23] tests --- tests/filters.js | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/tests/filters.js b/tests/filters.js index dce9ff0..d2f66e4 100644 --- a/tests/filters.js +++ b/tests/filters.js @@ -35,9 +35,19 @@ test('exactPaths filter - negative cases', () => { assert.throws(() => Filter.exactPaths([]), 'exactPaths Filter requires a non-empty array as argument'); }); -test('test listeners invocation - listener is corrupted - null', () => { - +test('pathsStartWith filter - positive cases', () => { + const f = Filter.pathsStartWith('a.b'); + const changes = [ + new Change('update', ['a'], 1, 0), + new Change('update', ['a', 'b'], 2, 0), + new Change('update', ['a', 'c', 'c'], 3, 0), + new Change('update', ['a', 'b', 'c'], 4, 0) + ]; + const filtered = f.fn(changes); + assert.strictEqual(filtered.length, 2); }); -test('test listeners invocation - listener is corrupted - NaF', () => { +test('pathsStartWith filter - negative cases', () => { + assert.throws(() => Filter.pathsStartWith(null), 'pathsStartWith Filter requires a non-empty string as argument'); + assert.throws(() => Filter.pathsStartWith([]), 'pathsStartWith Filter requires a non-empty string as argument'); }); From dff337dc0d257ff3e69b48127830666f2823b13c Mon Sep 17 00:00:00 2001 From: Yuri Guller Date: Mon, 8 Dec 2025 21:42:03 +0200 Subject: [PATCH 16/23] tests --- tests/filters.js | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/filters.js b/tests/filters.js index d2f66e4..64a4333 100644 --- a/tests/filters.js +++ b/tests/filters.js @@ -50,4 +50,5 @@ test('pathsStartWith filter - positive cases', () => { test('pathsStartWith filter - negative cases', () => { assert.throws(() => Filter.pathsStartWith(null), 'pathsStartWith Filter requires a non-empty string as argument'); assert.throws(() => Filter.pathsStartWith([]), 'pathsStartWith Filter requires a non-empty string as argument'); + assert.throws(() => Filter.pathsStartWith(''), 'pathsStartWith Filter requires a non-empty string as argument'); }); From 165c121227c6b713a1420443ddccbbd4ba2830ad Mon Sep 17 00:00:00 2001 From: Yuri Guller Date: Sat, 25 Apr 2026 19:33:36 +0300 Subject: [PATCH 17/23] fixing tests and syntax and a bit more --- .github/workflows/quality.yml | 16 ------ .nvmrc | 2 +- docs/changelog.md | 5 ++ package.json | 2 +- readme.md | 10 ++-- src/model/change.ts | 9 +--- src/object-observer.ts | 98 ----------------------------------- sri.json | 4 +- 8 files changed, 15 insertions(+), 131 deletions(-) diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml index c6fc4b3..2bd75b6 100644 --- a/.github/workflows/quality.yml +++ b/.github/workflows/quality.yml @@ -83,12 +83,6 @@ jobs: with: token: ${{ secrets.CODECOV_TOKEN }} - - name: Archive logs - uses: actions/upload-artifact@v4 - with: - name: chromium-logs - path: reports/logs/chromium-112.0.5615.29.log - quality_pipeline_firefox: runs-on: ubuntu-24.04 timeout-minutes: 5 @@ -111,11 +105,6 @@ jobs: - name: Test run: npm run test:firefox - - name: Report coverage - uses: codecov/codecov-action@v5 - with: - token: ${{ secrets.CODECOV_TOKEN }} - quality_pipeline_webkit: runs-on: ubuntu-24.04 timeout-minutes: 5 @@ -137,8 +126,3 @@ jobs: - name: Test run: npm run test:webkit - - - name: Report coverage - uses: codecov/codecov-action@v5 - with: - token: ${{ secrets.CODECOV_TOKEN }} diff --git a/.nvmrc b/.nvmrc index 0a49261..5bf4400 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -24.11.0 +24.15.0 diff --git a/docs/changelog.md b/docs/changelog.md index ed63133..8afde75 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -10,10 +10,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - [Issue no. 149](https://github.com/gullerya/object-observer/issues/149) - added verifiers to prevent unallowed changes - [Issue no. 152](https://github.com/gullerya/object-observer/issues/152) - change the way filters are configured +### Fixed +- `npm test` script referenced a non-existent `.json` config; now points to the correct `.js` file ### Chore - updated dependencies - reorganized sources - updated build scripts +- raised Node.js runtime to `24.15.0` (aligned with current LTS) +- browser support matrix updated to "last 2 versions" of Chrome, Firefox, Edge, Safari +- removed stale commented-out duplicate of the dispatch pipeline from `src/object-observer.ts` ## [6.1.4] - 2025-02-14 ### Chore diff --git a/package.json b/package.json index 8e1e9d8..55d00e5 100644 --- a/package.json +++ b/package.json @@ -63,7 +63,7 @@ "build": "node ./ci/tools/build-utils.js", "lint": "eslint -c ./ci/eslint.config.mjs ./src/* ./tests/*.js ./ci/**/*.js", "test-types": "tsc", - "test": "node node_modules/@gullerya/just-test/bin/local-runner.js config_file=./tests/configs/tests-config-ci-node.json", + "test": "node node_modules/@gullerya/just-test/bin/local-runner.js config_file=./tests/configs/tests-config-ci-node.js", "test:chromium": "node node_modules/@gullerya/just-test/bin/local-runner.js config_file=./tests/configs/tests-config-ci-chromium.js", "test:firefox": "node node_modules/@gullerya/just-test/bin/local-runner.js config_file=./tests/configs/tests-config-ci-firefox.js", "test:webkit": "node node_modules/@gullerya/just-test/bin/local-runner.js config_file=./tests/configs/tests-config-ci-webkit.js", diff --git a/readme.md b/readme.md index 0bf08a5..eb7e493 100644 --- a/readme.md +++ b/readme.md @@ -29,11 +29,11 @@ Main aspects and features: - following host objects (and their extensions) are __skipped__ from cloning / turning into observables: `Date` Supported: -![CHROME](docs/browser-icons/chrome.png)71+ | -![FIREFOX](docs/browser-icons/firefox.png)65+ | -![EDGE](docs/browser-icons/edge-chromium.png)79+ | -![SAFARI](docs/browser-icons/safari-ios.png)12.1 | -![NODE JS](docs/browser-icons/nodejs.png) 12.0.0+ +![CHROME](docs/browser-icons/chrome.png)last 2 versions | +![FIREFOX](docs/browser-icons/firefox.png)last 2 versions | +![EDGE](docs/browser-icons/edge-chromium.png)last 2 versions | +![SAFARI](docs/browser-icons/safari-ios.png)last 2 versions | +![NODE JS](docs/browser-icons/nodejs.png) 24.15.0+ Performance report can be found [here](docs/performance-report.md). diff --git a/src/model/change.ts b/src/model/change.ts index 16ff43e..7552cb5 100644 --- a/src/model/change.ts +++ b/src/model/change.ts @@ -8,13 +8,6 @@ export class Change { #pathAsString: string; constructor(type: string, path: Array, value: unknown, oldValue: unknown, object: object) { - if (typeof type !== 'string' || type === '') { - throw new Error('Change type must be a non-empty string'); - } - if (!Array.isArray(path)) { - throw new Error('Change path must be an array'); - } - this.#type = type; this.#path = path; this.#value = value; @@ -46,7 +39,7 @@ export class Change { * lazily computed string representation of the path */ get pathAsString(): string { - if (this.#pathAsString === undefined && Array.isArray(this.#path)) { + if (this.#pathAsString === undefined) { this.#pathAsString = this.#path.join('.'); } return this.#pathAsString; diff --git a/src/object-observer.ts b/src/object-observer.ts index fef3e83..d2c241a 100644 --- a/src/object-observer.ts +++ b/src/object-observer.ts @@ -40,104 +40,6 @@ const } return result; }; -// filterChanges = (options, changes) => { -// if (options === null) { -// return changes; -// } - -// let result = changes; -// if (options.path) { -// const oPath = options.path; -// result = changes.filter(change => -// change.path.join('.') === oPath -// ); -// } else if (options.pathsOf) { -// const oPathsOf = options.pathsOf; -// const oPathsOfStr = oPathsOf.join('.'); -// result = changes.filter(change => -// (change.path.length === oPathsOf.length + 1 || -// (change.path.length === oPathsOf.length && (change.type === REVERSE || change.type === SHUFFLE))) && -// change.path.join('.').startsWith(oPathsOfStr) -// ); -// } else if (options.pathsFrom) { -// const oPathsFrom = options.pathsFrom; -// result = changes.filter(change => -// change.path.join('.').startsWith(oPathsFrom) -// ); -// } -// return result; -// }, -// callObserverSafe = (listener, changes) => { -// try { -// listener(changes); -// } catch (e) { -// console.error(`failed to notify listener ${listener} with ${changes}`, e); -// } -// }, -// callObserversFromMT = function callObserversFromMT() { -// const batches = this.batches; -// this.batches = []; -// for (const [listener, changes] of batches) { -// callObserverSafe(listener, changes); -// } -// }; - -// export function callObservers(oMeta: ObservableBase, changes: Change[]) { -// let currentObservable: ObservableBase = oMeta; -// let isAsync, observers, target, options, relevantChanges, i; -// const l = changes.length; -// do { -// isAsync = currentObservable.async; -// observers = currentObservable.observers; -// i = observers.length; -// while (i--) { -// [target, options] = observers[i]; -// relevantChanges = filterChanges(options, changes); - -// if (relevantChanges.length) { -// if (isAsync) { -// // this is the async dispatch handling -// if (currentObservable.batches.length === 0) { -// queueMicrotask(callObserversFromMT.bind(currentObservable)); -// } -// let rb; -// for (const b of currentObservable.batches) { -// if (b[0] === target) { -// rb = b; -// break; -// } -// } -// if (!rb) { -// rb = [target, []]; -// currentObservable.batches.push(rb); -// } -// Array.prototype.push.apply(rb[1], relevantChanges); -// } else { -// // this is the naive straight forward synchronous dispatch -// callObserverSafe(target, relevantChanges); -// } -// } -// } - -// // cloning all the changes and notifying in context of parent -// const parent = currentObservable.parent; -// if (parent) { -// for (let j = 0; j < l; j++) { -// const change = changes[j]; -// changes[j] = new Change( -// change.type, -// [currentObservable.ownKey, ...change.path], -// change.value, -// change.oldValue, -// change.object -// ); -// } -// currentObservable = parent; -// } else { -// break; -// } -// } while (currentObservable); -// }; export const Observable = Object.freeze({ from: getObservableFromRoot, diff --git a/sri.json b/sri.json index f487df0..86adffb 100644 --- a/sri.json +++ b/sri.json @@ -1,6 +1,6 @@ { "dist/cdn/object-observer.js": "sha512-P4pkZ11qV2l9nf3LYnwgm5oC1LY7rWaaJpOOcGSCeNDStNDH+akJEWsltvnX9tSuprxJl8XOa2GoYiRzVJaxaQ==", - "dist/cdn/object-observer.js.map": "sha512-FONqa/XOwyN6O7HyQyO8bpCfksGJ1kY62kqOOyXlFCOTmJRtnX8O/2O6kG6TgvFUycNzodnofWQ+DKoFtmipyA==", + "dist/cdn/object-observer.js.map": "sha512-UayN/tpDVTASe0BlHoL3RBYMJ19oZKZVw/teQEFIVNtzErXW88YiPsKuCrHZ6ahvRuglDkduroOdphX6ivLNjA==", "dist/cdn/object-observer.min.js": "sha512-kO0nlxJUjqEeMz62gkuho9Yz7xaykdOz8M5wYlejCKtV6XHNP4xTCD7k5TucIlKMIv0ogzK+RWzvPMN0gM8TYQ==", - "dist/cdn/object-observer.min.js.map": "sha512-8IuhGFJD2VjfAmL0Kdm7qQTO821JD5Ko80bKzs7haopUPBDvV1HwXYkPvsQN46z7ezUABgjPDgSKzXeILqdLCg==" + "dist/cdn/object-observer.min.js.map": "sha512-0QuUNrGhVns52R4wATtAnDdoBbck4zStQ7Cm36V+Ea5D4UawUrwSCSTyIh7pdYBTq6VSb3B41WlShTwsDa6DZQ==" } \ No newline at end of file From 633f6c055ece23d0fd69a1d4fc4fd658e64812f7 Mon Sep 17 00:00:00 2001 From: Yuri Guller Date: Sat, 25 Apr 2026 22:27:06 +0300 Subject: [PATCH 18/23] improving perf and layout --- src/observables/processors/proc-utils.ts | 6 +++--- sri.json | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/observables/processors/proc-utils.ts b/src/observables/processors/proc-utils.ts index e8be93f..73d1b57 100644 --- a/src/observables/processors/proc-utils.ts +++ b/src/observables/processors/proc-utils.ts @@ -103,7 +103,7 @@ function filterChanges(options, changes) { if (options.path) { const oPath = options.path; result = changes.filter(change => - change.path.join('.') === oPath + change.pathAsString === oPath ); } else if (options.pathsOf) { const oPathsOf = options.pathsOf; @@ -111,12 +111,12 @@ function filterChanges(options, changes) { result = changes.filter(change => (change.path.length === oPathsOf.length + 1 || (change.path.length === oPathsOf.length && (change.type === REVERSE || change.type === SHUFFLE))) && - change.path.join('.').startsWith(oPathsOfStr) + change.pathAsString.startsWith(oPathsOfStr) ); } else if (options.pathsFrom) { const oPathsFrom = options.pathsFrom; result = changes.filter(change => - change.path.join('.').startsWith(oPathsFrom) + change.pathAsString.startsWith(oPathsFrom) ); } return result; diff --git a/sri.json b/sri.json index 86adffb..6436bbd 100644 --- a/sri.json +++ b/sri.json @@ -1,6 +1,6 @@ { - "dist/cdn/object-observer.js": "sha512-P4pkZ11qV2l9nf3LYnwgm5oC1LY7rWaaJpOOcGSCeNDStNDH+akJEWsltvnX9tSuprxJl8XOa2GoYiRzVJaxaQ==", - "dist/cdn/object-observer.js.map": "sha512-UayN/tpDVTASe0BlHoL3RBYMJ19oZKZVw/teQEFIVNtzErXW88YiPsKuCrHZ6ahvRuglDkduroOdphX6ivLNjA==", - "dist/cdn/object-observer.min.js": "sha512-kO0nlxJUjqEeMz62gkuho9Yz7xaykdOz8M5wYlejCKtV6XHNP4xTCD7k5TucIlKMIv0ogzK+RWzvPMN0gM8TYQ==", - "dist/cdn/object-observer.min.js.map": "sha512-0QuUNrGhVns52R4wATtAnDdoBbck4zStQ7Cm36V+Ea5D4UawUrwSCSTyIh7pdYBTq6VSb3B41WlShTwsDa6DZQ==" + "dist/cdn/object-observer.js": "sha512-eOOFI8mq5qJfX6gflVO4wqYHtKdJ4kDmcLM1woMxc2sn+ZRdGlwHNqblhxoxOZug0t8YmFsxDJV7yjfYc+/2JA==", + "dist/cdn/object-observer.js.map": "sha512-1dISelOl66si5viGwNH3tv+z8Bxg0Yg6mOtpt6fY7KV9HT7oJ4VZ5IrirluL3uJj4oC1iPRWehYJytSNjO0fTw==", + "dist/cdn/object-observer.min.js": "sha512-z1MXrZlp1JtaM+KcxDu7qZlQlAiCbQNOip8E+m0+ZBZp3ENPV2e1/SDqSvLIB3wvZ3sPOoU5QS2O5eu3UUn6/g==", + "dist/cdn/object-observer.min.js.map": "sha512-1GaCi/ia8v5kgcu6P6+obdjevJGXwHRT2FCS5gp+NyMof7DtS4NH09LQ8gc6MM3XTOVmpO2VJkkjDB2mGAqYOA==" } \ No newline at end of file From 31554f49576beab89eb5c373681f42afc0e4d5e4 Mon Sep 17 00:00:00 2001 From: Yuri Guller Date: Sat, 25 Apr 2026 22:49:13 +0300 Subject: [PATCH 19/23] simplifying and reducing duplications --- src/observables/methods/copy-within.ts | 82 +++---- src/observables/methods/delete-property.ts | 24 +- src/observables/methods/fill.ts | 56 ++--- src/observables/methods/pop.ts | 24 +- src/observables/methods/reverse.ts | 27 +-- src/observables/methods/set.ts | 33 ++- src/observables/methods/shift.ts | 36 +-- src/observables/methods/sort.ts | 27 +-- src/observables/methods/splice.ts | 89 +++----- src/observables/methods/unshift.ts | 46 ++-- src/observables/processors/proc-utils.ts | 250 +++++++++++---------- sri.json | 8 +- 12 files changed, 311 insertions(+), 391 deletions(-) diff --git a/src/observables/methods/copy-within.ts b/src/observables/methods/copy-within.ts index a3a72eb..424a36d 100644 --- a/src/observables/methods/copy-within.ts +++ b/src/observables/methods/copy-within.ts @@ -1,49 +1,41 @@ import { Change } from '../../model/change.ts'; import { UPDATE, oMetaKey } from '../../constants.ts'; -import { getObservedOf } from '../processors/proc-utils.ts'; -import { callObservers } from '../processors/proc-utils.ts'; +import { callObservers, detachIfObservable, getObservedOf } from '../processors/proc-utils.ts'; export default function proxiedCopyWithin(dest, start, end) { - const oMeta = this[oMetaKey]; - const target = oMeta.target; - const tarLen = target.length; - dest = dest < 0 ? Math.max(tarLen + dest, 0) : dest; - start = start === undefined ? 0 : (start < 0 ? Math.max(tarLen + start, 0) : Math.min(start, tarLen)); - end = end === undefined ? tarLen : (end < 0 ? Math.max(tarLen + end, 0) : Math.min(end, tarLen)); - const len = Math.min(end - start, tarLen - dest); - - if (dest < tarLen && dest !== start && len > 0) { - const - prev = target.slice(0), - changes = []; - - target.copyWithin(dest, start, end); - - for (let i = dest, nItem, oItem, tmpObserved; i < dest + len; i++) { - // update newly placed observables, if any - nItem = target[i]; - if (nItem && typeof nItem === 'object') { - nItem = getObservedOf(nItem, i, oMeta); - target[i] = nItem; - } - - // detach overridden observables, if any - oItem = prev[i]; - if (oItem && typeof oItem === 'object') { - tmpObserved = oItem[oMetaKey]; - if (tmpObserved) { - oItem = tmpObserved.detach(); - } - } - - if (typeof nItem !== 'object' && nItem === oItem) { - continue; - } - changes.push(new Change(UPDATE, [i], nItem, oItem, this)); - } - - callObservers(oMeta, changes); - } - - return this; -}; \ No newline at end of file + const oMeta = this[oMetaKey]; + const target = oMeta.target; + const tarLen = target.length; + dest = dest < 0 ? Math.max(tarLen + dest, 0) : dest; + start = start === undefined ? 0 : (start < 0 ? Math.max(tarLen + start, 0) : Math.min(start, tarLen)); + end = end === undefined ? tarLen : (end < 0 ? Math.max(tarLen + end, 0) : Math.min(end, tarLen)); + const len = Math.min(end - start, tarLen - dest); + + if (dest < tarLen && dest !== start && len > 0) { + const prev = target.slice(0); + const changes = []; + + target.copyWithin(dest, start, end); + + for (let i = dest; i < dest + len; i++) { + // update newly placed observables, if any + let nItem = target[i]; + if (nItem && typeof nItem === 'object') { + nItem = getObservedOf(nItem, i, oMeta); + target[i] = nItem; + } + + // detach overridden observables, if any + const oItem = detachIfObservable(prev[i]); + + if (typeof nItem !== 'object' && nItem === oItem) { + continue; + } + changes.push(new Change(UPDATE, [i], nItem, oItem, this)); + } + + callObservers(oMeta, changes); + } + + return this; +}; diff --git a/src/observables/methods/delete-property.ts b/src/observables/methods/delete-property.ts index b3aa0cf..f6dc514 100644 --- a/src/observables/methods/delete-property.ts +++ b/src/observables/methods/delete-property.ts @@ -1,22 +1,14 @@ import { Change } from '../../model/change.ts'; import { DELETE } from '../../constants.ts'; -import { oMetaKey } from '../../constants.ts'; -import { callObservers } from '../processors/proc-utils.ts'; +import { callObservers, detachIfObservable } from '../processors/proc-utils.ts'; export function proxiedDeleteProperty(target: object, key: string | symbol): boolean { - let oldValue = target[key]; + const prevValue = target[key]; + delete target[key]; + const oldValue = detachIfObservable(prevValue); - delete target[key]; + const changes = [new Change(DELETE, [key], undefined, oldValue, this.proxy)]; + callObservers(this, changes); - if (oldValue && typeof oldValue === 'object') { - const tmpObserved = oldValue[oMetaKey]; - if (tmpObserved) { - oldValue = tmpObserved.detach(); - } - } - - const changes = [new Change(DELETE, [key], undefined, oldValue, this.proxy)]; - callObservers(this, changes); - - return true; -}; \ No newline at end of file + return true; +}; diff --git a/src/observables/methods/fill.ts b/src/observables/methods/fill.ts index 35725b6..c3d3732 100644 --- a/src/observables/methods/fill.ts +++ b/src/observables/methods/fill.ts @@ -1,41 +1,31 @@ import { Change } from '../../model/change.ts'; import { INSERT, UPDATE, oMetaKey } from '../../constants.ts'; -import { getObservedOf } from '../processors/proc-utils.ts'; -import { callObservers } from '../processors/proc-utils.ts'; +import { callObservers, detachIfObservable, getObservedOf } from '../processors/proc-utils.ts'; export default function proxiedFill(filVal, start, end) { - const oMeta = this[oMetaKey]; - const target = oMeta.target; - const changes = []; - const tarLen = target.length; - const prev = target.slice(0); - start = start === undefined ? 0 : (start < 0 ? Math.max(tarLen + start, 0) : Math.min(start, tarLen)); - end = end === undefined ? tarLen : (end < 0 ? Math.max(tarLen + end, 0) : Math.min(end, tarLen)); + const oMeta = this[oMetaKey]; + const target = oMeta.target; + const changes = []; + const tarLen = target.length; + const prev = target.slice(0); + start = start === undefined ? 0 : (start < 0 ? Math.max(tarLen + start, 0) : Math.min(start, tarLen)); + end = end === undefined ? tarLen : (end < 0 ? Math.max(tarLen + end, 0) : Math.min(end, tarLen)); - if (start < tarLen && end > start) { - target.fill(filVal, start, end); + if (start < tarLen && end > start) { + target.fill(filVal, start, end); - let tmpObserved; - for (let i = start, item, tmpTarget; i < end; i++) { - item = target[i]; - target[i] = getObservedOf(item, i, oMeta); - if (i in prev) { - tmpTarget = prev[i]; - if (tmpTarget && typeof tmpTarget === 'object') { - tmpObserved = tmpTarget[oMetaKey]; - if (tmpObserved) { - tmpTarget = tmpObserved.detach(); - } - } + for (let i = start; i < end; i++) { + target[i] = getObservedOf(target[i], i, oMeta); + if (i in prev) { + const oldValue = detachIfObservable(prev[i]); + changes.push(new Change(UPDATE, [i], target[i], oldValue, this)); + } else { + changes.push(new Change(INSERT, [i], target[i], undefined, this)); + } + } - changes.push(new Change(UPDATE, [i], target[i], tmpTarget, this)); - } else { - changes.push(new Change(INSERT, [i], target[i], undefined, this)); - } - } + callObservers(oMeta, changes); + } - callObservers(oMeta, changes); - } - - return this; -}; \ No newline at end of file + return this; +}; diff --git a/src/observables/methods/pop.ts b/src/observables/methods/pop.ts index d8dd22c..037f9aa 100644 --- a/src/observables/methods/pop.ts +++ b/src/observables/methods/pop.ts @@ -1,22 +1,16 @@ import { Change } from '../../model/change.ts'; import { DELETE, oMetaKey } from '../../constants.ts'; -import { callObservers } from '../processors/proc-utils.ts'; +import { callObservers, detachIfObservable } from '../processors/proc-utils.ts'; export default function proxiedPop() { - const oMeta = this[oMetaKey]; - const target = oMeta.target; - const poppedIndex = target.length - 1; + const oMeta = this[oMetaKey]; + const target = oMeta.target; + const poppedIndex = target.length - 1; - let popResult = target.pop(); - if (popResult && typeof popResult === 'object') { - const tmpObserved = popResult[oMetaKey]; - if (tmpObserved) { - popResult = tmpObserved.detach(); - } - } + const popResult = detachIfObservable(target.pop()); - const changes = [new Change(DELETE, [poppedIndex], undefined, popResult, this)]; - callObservers(oMeta, changes); + const changes = [new Change(DELETE, [poppedIndex], undefined, popResult, this)]; + callObservers(oMeta, changes); - return popResult; -} \ No newline at end of file + return popResult; +} diff --git a/src/observables/methods/reverse.ts b/src/observables/methods/reverse.ts index 2feb03c..524db19 100644 --- a/src/observables/methods/reverse.ts +++ b/src/observables/methods/reverse.ts @@ -1,25 +1,16 @@ import { Change } from '../../model/change.ts'; import { REVERSE, oMetaKey } from '../../constants.ts'; -import { callObservers } from '../processors/proc-utils.ts'; +import { callObservers, reindexObservableChildren } from '../processors/proc-utils.ts'; export default function proxiedReverse() { - const oMeta = this[oMetaKey]; - const target = oMeta.target; - let i, l, item; + const oMeta = this[oMetaKey]; + const target = oMeta.target; - target.reverse(); - for (i = 0, l = target.length; i < l; i++) { - item = target[i]; - if (item && typeof item === 'object') { - const tmpObserved = item[oMetaKey]; - if (tmpObserved) { - tmpObserved.ownKey = i; - } - } - } + target.reverse(); + reindexObservableChildren(target); - const changes = [new Change(REVERSE, [], undefined, undefined, this)]; - callObservers(oMeta, changes); + const changes = [new Change(REVERSE, [], undefined, undefined, this)]; + callObservers(oMeta, changes); - return this; -}; \ No newline at end of file + return this; +}; diff --git a/src/observables/methods/set.ts b/src/observables/methods/set.ts index d517b8f..859c59c 100644 --- a/src/observables/methods/set.ts +++ b/src/observables/methods/set.ts @@ -1,28 +1,21 @@ import { Change } from '../../model/change.ts'; import { INSERT, UPDATE } from '../../constants.ts'; -import { oMetaKey } from '../../constants.ts'; -import { getObservedOf } from '../processors/proc-utils.ts'; -import { callObservers } from '../processors/proc-utils.ts'; +import { callObservers, detachIfObservable, getObservedOf } from '../processors/proc-utils.ts'; export function proxiedSet(target: object, key: string | symbol, value: unknown): boolean { - let oldValue = target[key]; + const prevValue = target[key]; - if (value !== oldValue) { - const newValue = getObservedOf(value, key, this); - target[key] = newValue; + if (value !== prevValue) { + const newValue = getObservedOf(value, key, this); + target[key] = newValue; - if (oldValue && typeof oldValue === 'object') { - const tmpObserved = oldValue[oMetaKey]; - if (tmpObserved) { - oldValue = tmpObserved.detach(); - } - } + const oldValue = detachIfObservable(prevValue); - const changes = oldValue === undefined - ? [new Change(INSERT, [key], newValue, undefined, this.proxy)] - : [new Change(UPDATE, [key], newValue, oldValue, this.proxy)]; - callObservers(this, changes); - } + const changes = oldValue === undefined + ? [new Change(INSERT, [key], newValue, undefined, this.proxy)] + : [new Change(UPDATE, [key], newValue, oldValue, this.proxy)]; + callObservers(this, changes); + } - return true; -}; \ No newline at end of file + return true; +}; diff --git a/src/observables/methods/shift.ts b/src/observables/methods/shift.ts index 35bf5cb..34114e5 100644 --- a/src/observables/methods/shift.ts +++ b/src/observables/methods/shift.ts @@ -1,34 +1,16 @@ import { Change } from '../../model/change.ts'; import { DELETE, oMetaKey } from '../../constants.ts'; -import { callObservers } from '../processors/proc-utils.ts'; +import { callObservers, detachIfObservable, reindexObservableChildren } from '../processors/proc-utils.ts'; export default function proxiedShift() { - const - oMeta = this[oMetaKey], - target = oMeta.target; - let shiftResult, i, l, item, tmpObserved; + const oMeta = this[oMetaKey]; + const target = oMeta.target; - shiftResult = target.shift(); - if (shiftResult && typeof shiftResult === 'object') { - tmpObserved = shiftResult[oMetaKey]; - if (tmpObserved) { - shiftResult = tmpObserved.detach(); - } - } + const shiftResult = detachIfObservable(target.shift()); + reindexObservableChildren(target); - // update indices of the remaining items - for (i = 0, l = target.length; i < l; i++) { - item = target[i]; - if (item && typeof item === 'object') { - tmpObserved = item[oMetaKey]; - if (tmpObserved) { - tmpObserved.ownKey = i; - } - } - } + const changes = [new Change(DELETE, [0], undefined, shiftResult, this)]; + callObservers(oMeta, changes); - const changes = [new Change(DELETE, [0], undefined, shiftResult, this)]; - callObservers(oMeta, changes); - - return shiftResult; -}; \ No newline at end of file + return shiftResult; +}; diff --git a/src/observables/methods/sort.ts b/src/observables/methods/sort.ts index 1c4459f..c73d3b5 100644 --- a/src/observables/methods/sort.ts +++ b/src/observables/methods/sort.ts @@ -1,25 +1,16 @@ import { Change } from '../../model/change.ts'; import { SHUFFLE, oMetaKey } from '../../constants.ts'; -import { callObservers } from '../processors/proc-utils.ts'; +import { callObservers, reindexObservableChildren } from '../processors/proc-utils.ts'; export default function proxiedSort(comparator) { - const oMeta = this[oMetaKey]; - const target = oMeta.target; - let i, l, item; + const oMeta = this[oMetaKey]; + const target = oMeta.target; - target.sort(comparator); - for (i = 0, l = target.length; i < l; i++) { - item = target[i]; - if (item && typeof item === 'object') { - const tmpObserved = item[oMetaKey]; - if (tmpObserved) { - tmpObserved.ownKey = i; - } - } - } + target.sort(comparator); + reindexObservableChildren(target); - const changes = [new Change(SHUFFLE, [], undefined, undefined, this)]; - callObservers(oMeta, changes); + const changes = [new Change(SHUFFLE, [], undefined, undefined, this)]; + callObservers(oMeta, changes); - return this; -}; \ No newline at end of file + return this; +}; diff --git a/src/observables/methods/splice.ts b/src/observables/methods/splice.ts index c3b3208..b5b2380 100644 --- a/src/observables/methods/splice.ts +++ b/src/observables/methods/splice.ts @@ -1,64 +1,45 @@ import { Change } from '../../model/change.ts'; import { INSERT, DELETE, UPDATE, oMetaKey } from '../../constants.ts'; -import { getObservedOf } from '../processors/proc-utils.ts'; -import { callObservers } from '../processors/proc-utils.ts'; +import { callObservers, detachIfObservable, getObservedOf, reindexObservableChildren } from '../processors/proc-utils.ts'; export default function proxiedSplice(...spliceItems: unknown[]) { - const oMeta = this[oMetaKey]; - const target = oMeta.target; - const splLen = spliceItems.length; - const spliceContent = new Array(splLen); - const tarLen = target.length; + const oMeta = this[oMetaKey]; + const target = oMeta.target; + const splLen = spliceItems.length; + const spliceContent = new Array(splLen); + const tarLen = target.length; - // observify the newcomers - for (let i = 0; i < splLen; i++) { - spliceContent[i] = getObservedOf(spliceItems[i], i, oMeta); - } + // observify the newcomers + for (let i = 0; i < splLen; i++) { + spliceContent[i] = getObservedOf(spliceItems[i], i, oMeta); + } - // calculate pointers - const startIndex = splLen === 0 ? 0 : (spliceContent[0] < 0 ? tarLen + spliceContent[0] : spliceContent[0]); - const removed = splLen < 2 ? tarLen - startIndex : spliceContent[1]; - const inserted = Math.max(splLen - 2, 0); - const spliceResult: unknown[] = Reflect.apply(target.splice, target, spliceContent); - const newTarLen = target.length; + // calculate pointers + const startIndex = splLen === 0 ? 0 : (spliceContent[0] < 0 ? tarLen + spliceContent[0] : spliceContent[0]); + const removed = splLen < 2 ? tarLen - startIndex : spliceContent[1]; + const inserted = Math.max(splLen - 2, 0); + const spliceResult: unknown[] = Reflect.apply(target.splice, target, spliceContent); - // reindex the paths - let tmpObserved; - for (let i = 0, item; i < newTarLen; i++) { - item = target[i]; - if (item && typeof item === 'object') { - tmpObserved = item[oMetaKey]; - if (tmpObserved) { - tmpObserved.ownKey = i; - } - } - } + reindexObservableChildren(target); - // detach removed objects - let i, l, item; - for (i = 0, l = spliceResult.length; i < l; i++) { - item = spliceResult[i]; - if (item && typeof item === 'object') { - tmpObserved = item[oMetaKey]; - if (tmpObserved) { - spliceResult[i] = tmpObserved.detach(); - } - } - } + // detach removed objects + for (let i = 0, l = spliceResult.length; i < l; i++) { + spliceResult[i] = detachIfObservable(spliceResult[i]); + } - const changes = []; - let index; - for (index = 0; index < removed; index++) { - if (index < inserted) { - changes.push(new Change(UPDATE, [startIndex + index], target[startIndex + index], spliceResult[index], this)); - } else { - changes.push(new Change(DELETE, [startIndex + index], undefined, spliceResult[index], this)); - } - } - for (; index < inserted; index++) { - changes.push(new Change(INSERT, [startIndex + index], target[startIndex + index], undefined, this)); - } - callObservers(oMeta, changes); + const changes = []; + let index; + for (index = 0; index < removed; index++) { + if (index < inserted) { + changes.push(new Change(UPDATE, [startIndex + index], target[startIndex + index], spliceResult[index], this)); + } else { + changes.push(new Change(DELETE, [startIndex + index], undefined, spliceResult[index], this)); + } + } + for (; index < inserted; index++) { + changes.push(new Change(INSERT, [startIndex + index], target[startIndex + index], undefined, this)); + } + callObservers(oMeta, changes); - return spliceResult; -}; \ No newline at end of file + return spliceResult; +}; diff --git a/src/observables/methods/unshift.ts b/src/observables/methods/unshift.ts index 29aa8bf..d37ebfd 100644 --- a/src/observables/methods/unshift.ts +++ b/src/observables/methods/unshift.ts @@ -1,36 +1,26 @@ import { Change } from '../../model/change.ts'; import { INSERT, oMetaKey } from '../../constants.ts'; -import { getObservedOf } from '../processors/proc-utils.ts'; -import { callObservers } from '../processors/proc-utils.ts'; +import { callObservers, getObservedOf, reindexObservableChildren } from '../processors/proc-utils.ts'; export default function proxiedUnshift(...unshiftItems: unknown[]) { - const oMeta = this[oMetaKey]; - const target = oMeta.target; - const unshiftLen = unshiftItems.length; - const unshiftContent = new Array(unshiftLen); + const oMeta = this[oMetaKey]; + const target = oMeta.target; + const unshiftLen = unshiftItems.length; + const unshiftContent = new Array(unshiftLen); - for (let i = 0; i < unshiftLen; i++) { - unshiftContent[i] = getObservedOf(unshiftItems[i], i, oMeta); - } - const unshiftResult = Reflect.apply(target.unshift, target, unshiftContent); + for (let i = 0; i < unshiftLen; i++) { + unshiftContent[i] = getObservedOf(unshiftItems[i], i, oMeta); + } + const unshiftResult = Reflect.apply(target.unshift, target, unshiftContent); - for (let i = 0, l = target.length, item; i < l; i++) { - item = target[i]; - if (item && typeof item === 'object') { - const tmpObserved = item[oMetaKey]; - if (tmpObserved) { - tmpObserved.ownKey = i; - } - } - } + reindexObservableChildren(target); - // publish changes - const l = unshiftContent.length; - const changes = new Array(l); - for (let i = 0; i < l; i++) { - changes[i] = new Change(INSERT, [i], target[i], undefined, this); - } - callObservers(oMeta, changes); + // publish changes + const changes = new Array(unshiftLen); + for (let i = 0; i < unshiftLen; i++) { + changes[i] = new Change(INSERT, [i], target[i], undefined, this); + } + callObservers(oMeta, changes); - return unshiftResult; -}; \ No newline at end of file + return unshiftResult; +}; diff --git a/src/observables/processors/proc-utils.ts b/src/observables/processors/proc-utils.ts index 73d1b57..dfd0398 100644 --- a/src/observables/processors/proc-utils.ts +++ b/src/observables/processors/proc-utils.ts @@ -6,134 +6,158 @@ import { ObservableObject } from '../object.ts'; import { ObservableTypedArray } from '../typed-array.ts'; export function getObservedOf(item: unknown, key: string | symbol | number, parent: object, visited?: Set): unknown { - if (visited !== undefined && visited.has(item)) { - return null; - } else if (typeof item !== 'object' || item === null) { - return item; - } else if (Array.isArray(item)) { - return new ObservableArray({ target: item, ownKey: key, parent: parent, visited }).proxy; - } else if (ArrayBuffer.isView(item)) { - return new ObservableTypedArray({ target: item, ownKey: key, parent: parent }).proxy; - } else if (item instanceof Date) { - return item; - } else { - return new ObservableObject({ target: item, ownKey: key, parent: parent, visited }).proxy; - } + if (visited !== undefined && visited.has(item)) { + return null; + } else if (typeof item !== 'object' || item === null) { + return item; + } else if (Array.isArray(item)) { + return new ObservableArray({ target: item, ownKey: key, parent: parent, visited }).proxy; + } else if (ArrayBuffer.isView(item)) { + return new ObservableTypedArray({ target: item, ownKey: key, parent: parent }).proxy; + } else if (item instanceof Date) { + return item; + } else { + return new ObservableObject({ target: item, ownKey: key, parent: parent, visited }).proxy; + } }; export function getObservableFromRoot(target: unknown, options = undefined): unknown { - if (!target || typeof target !== 'object') { - throw new Error('observable MAY ONLY be created from a non-null object'); - } else if (target[oMetaKey]) { - return target; - } else if (Array.isArray(target)) { - return new ObservableArray({ target: target, ownKey: null, parent: null, options: options }).proxy; - } else if (ArrayBuffer.isView(target)) { - return new ObservableTypedArray({ target: target, ownKey: null, parent: null, options: options }).proxy; - } else if (target instanceof Date) { - throw new Error(`${target} found to be one of a non-observable types`); - } else { - return new ObservableObject({ target: target, ownKey: null, parent: null, options: options }).proxy; - } + if (!target || typeof target !== 'object') { + throw new Error('observable MAY ONLY be created from a non-null object'); + } else if (target[oMetaKey]) { + return target; + } else if (Array.isArray(target)) { + return new ObservableArray({ target: target, ownKey: null, parent: null, options: options }).proxy; + } else if (ArrayBuffer.isView(target)) { + return new ObservableTypedArray({ target: target, ownKey: null, parent: null, options: options }).proxy; + } else if (target instanceof Date) { + throw new Error(`${target} found to be one of a non-observable types`); + } else { + return new ObservableObject({ target: target, ownKey: null, parent: null, options: options }).proxy; + } +} + +// re-assign ownKey for every observable child of target whose position has changed +export function reindexObservableChildren(target: ArrayLike, from = 0): void { + for (let i = from, l = target.length; i < l; i++) { + const item = target[i]; + if (item && typeof item === 'object') { + const childMeta = item[oMetaKey]; + if (childMeta) { + childMeta.ownKey = i; + } + } + } +} + +// if value is an observable, detach it and return the unwrapped target; otherwise return value as-is +export function detachIfObservable(value: unknown): unknown { + if (value && typeof value === 'object') { + const childMeta = value[oMetaKey]; + if (childMeta) { + return childMeta.detach(); + } + } + return value; } export function callObservers(oMeta: ObservableBase, changes: Change[]) { - let currentObservable: ObservableBase = oMeta; - let isAsync, observers, target, options, relevantChanges, i; - const l = changes.length; - do { - isAsync = currentObservable.async; - observers = currentObservable.observers; - i = observers.length; - while (i--) { - [target, options] = observers[i]; - relevantChanges = filterChanges(options, changes); + let currentObservable: ObservableBase = oMeta; + let isAsync, observers, target, options, relevantChanges, i; + const l = changes.length; + do { + isAsync = currentObservable.async; + observers = currentObservable.observers; + i = observers.length; + while (i--) { + [target, options] = observers[i]; + relevantChanges = filterChanges(options, changes); - if (relevantChanges.length) { - if (isAsync) { - // this is the async dispatch handling - if (currentObservable.batches.length === 0) { - queueMicrotask(callObserversFromMT.bind(currentObservable)); - } - let rb; - for (const b of currentObservable.batches) { - if (b[0] === target) { - rb = b; - break; - } - } - if (!rb) { - rb = [target, []]; - currentObservable.batches.push(rb); - } - Array.prototype.push.apply(rb[1], relevantChanges); - } else { - // this is the naive straight forward synchronous dispatch - callObserverSafe(target, relevantChanges); - } - } - } + if (relevantChanges.length) { + if (isAsync) { + // this is the async dispatch handling + if (currentObservable.batches.length === 0) { + queueMicrotask(callObserversFromMT.bind(currentObservable)); + } + let rb; + for (const b of currentObservable.batches) { + if (b[0] === target) { + rb = b; + break; + } + } + if (!rb) { + rb = [target, []]; + currentObservable.batches.push(rb); + } + Array.prototype.push.apply(rb[1], relevantChanges); + } else { + // this is the naive straight forward synchronous dispatch + callObserverSafe(target, relevantChanges); + } + } + } - // cloning all the changes and notifying in context of parent - const parent = currentObservable.parent; - if (parent) { - for (let j = 0; j < l; j++) { - const change = changes[j]; - changes[j] = new Change( - change.type, - [currentObservable.ownKey, ...change.path], - change.value, - change.oldValue, - change.object - ); - } - currentObservable = parent; - } else { - break; - } - } while (currentObservable); + // cloning all the changes and notifying in context of parent + const parent = currentObservable.parent; + if (parent) { + for (let j = 0; j < l; j++) { + const change = changes[j]; + changes[j] = new Change( + change.type, + [currentObservable.ownKey, ...change.path], + change.value, + change.oldValue, + change.object + ); + } + currentObservable = parent; + } else { + break; + } + } while (currentObservable); }; function filterChanges(options, changes) { - if (options === null) { - return changes; - } + if (options === null) { + return changes; + } - let result = changes; - if (options.path) { - const oPath = options.path; - result = changes.filter(change => - change.pathAsString === oPath - ); - } else if (options.pathsOf) { - const oPathsOf = options.pathsOf; - const oPathsOfStr = oPathsOf.join('.'); - result = changes.filter(change => - (change.path.length === oPathsOf.length + 1 || - (change.path.length === oPathsOf.length && (change.type === REVERSE || change.type === SHUFFLE))) && - change.pathAsString.startsWith(oPathsOfStr) - ); - } else if (options.pathsFrom) { - const oPathsFrom = options.pathsFrom; - result = changes.filter(change => - change.pathAsString.startsWith(oPathsFrom) - ); - } - return result; + let result = changes; + if (options.path) { + const oPath = options.path; + result = changes.filter(change => + change.pathAsString === oPath + ); + } else if (options.pathsOf) { + const oPathsOf = options.pathsOf; + const oPathsOfStr = oPathsOf.join('.'); + result = changes.filter(change => + (change.path.length === oPathsOf.length + 1 || + (change.path.length === oPathsOf.length && (change.type === REVERSE || change.type === SHUFFLE))) && + change.pathAsString.startsWith(oPathsOfStr) + ); + } else if (options.pathsFrom) { + const oPathsFrom = options.pathsFrom; + result = changes.filter(change => + change.pathAsString.startsWith(oPathsFrom) + ); + } + return result; } function callObserverSafe(listener, changes) { - try { - listener(changes); - } catch (e) { - console.error(`failed to notify listener ${listener} with ${changes}`, e); - } + try { + listener(changes); + } catch (e) { + console.error(`failed to notify listener ${listener} with ${changes}`, e); + } } function callObserversFromMT() { - const batches = this.batches; - this.batches = []; - for (const [listener, changes] of batches) { - callObserverSafe(listener, changes); - } -}; \ No newline at end of file + const batches = this.batches; + this.batches = []; + for (const [listener, changes] of batches) { + callObserverSafe(listener, changes); + } +}; diff --git a/sri.json b/sri.json index 6436bbd..139a94f 100644 --- a/sri.json +++ b/sri.json @@ -1,6 +1,6 @@ { - "dist/cdn/object-observer.js": "sha512-eOOFI8mq5qJfX6gflVO4wqYHtKdJ4kDmcLM1woMxc2sn+ZRdGlwHNqblhxoxOZug0t8YmFsxDJV7yjfYc+/2JA==", - "dist/cdn/object-observer.js.map": "sha512-1dISelOl66si5viGwNH3tv+z8Bxg0Yg6mOtpt6fY7KV9HT7oJ4VZ5IrirluL3uJj4oC1iPRWehYJytSNjO0fTw==", - "dist/cdn/object-observer.min.js": "sha512-z1MXrZlp1JtaM+KcxDu7qZlQlAiCbQNOip8E+m0+ZBZp3ENPV2e1/SDqSvLIB3wvZ3sPOoU5QS2O5eu3UUn6/g==", - "dist/cdn/object-observer.min.js.map": "sha512-1GaCi/ia8v5kgcu6P6+obdjevJGXwHRT2FCS5gp+NyMof7DtS4NH09LQ8gc6MM3XTOVmpO2VJkkjDB2mGAqYOA==" + "dist/cdn/object-observer.js": "sha512-4JSeuw8uOwqMSGz6wX4oZWKwbgRt5dB0kcemUJkK+fgXR2lLHgxNjf2XeZDL1v3bVUuPvOPauGW89YgK0YmnmQ==", + "dist/cdn/object-observer.js.map": "sha512-SXGYrEiThB+7WA+kG3EQfBymkPEBTtnhEQST7XAC98PTzQbwPpvCNuXfN3gFUgpTRegB7PoVEKjq5/CR3Hr7Nw==", + "dist/cdn/object-observer.min.js": "sha512-62Md9XWugP+Ot8Wbm4yUq0N3CMLCJLDRraDrL+N3wbDYPMg4wan9Znly09Uh32sneJFLx1YDEIkCxF4vgJRhsQ==", + "dist/cdn/object-observer.min.js.map": "sha512-Pk2JKDulMrcTjp0y05o0pAL2rpjD5t/h9OnRY4DTNaXn+MiF1pq3ZjsAOomKyCujhQ6NFgugUyRmVAcKHm0DKA==" } \ No newline at end of file From 955634dd8634775e3674d56c741cd27e6d99c60d Mon Sep 17 00:00:00 2001 From: Yuri Guller Date: Sat, 25 Apr 2026 22:57:29 +0300 Subject: [PATCH 20/23] simplifying and reducing duplications --- src/observables/abstract-base.ts | 2 +- src/observables/processors/proc-utils.ts | 20 +++++++------------- sri.json | 8 ++++---- 3 files changed, 12 insertions(+), 18 deletions(-) diff --git a/src/observables/abstract-base.ts b/src/observables/abstract-base.ts index fbb322d..4cf561a 100644 --- a/src/observables/abstract-base.ts +++ b/src/observables/abstract-base.ts @@ -18,7 +18,7 @@ export class ObservableBase implements ProxyHandler { // eslint-disable-next-line no-unused-private-class-members #revoke: () => void; #async: boolean = false; - batches = []; + batches = new Map(); set; deleteProperty; diff --git a/src/observables/processors/proc-utils.ts b/src/observables/processors/proc-utils.ts index dfd0398..dc502c2 100644 --- a/src/observables/processors/proc-utils.ts +++ b/src/observables/processors/proc-utils.ts @@ -76,21 +76,15 @@ export function callObservers(oMeta: ObservableBase, changes: Change[]) { if (relevantChanges.length) { if (isAsync) { // this is the async dispatch handling - if (currentObservable.batches.length === 0) { + if (currentObservable.batches.size === 0) { queueMicrotask(callObserversFromMT.bind(currentObservable)); } - let rb; - for (const b of currentObservable.batches) { - if (b[0] === target) { - rb = b; - break; - } + let batch = currentObservable.batches.get(target); + if (!batch) { + batch = []; + currentObservable.batches.set(target, batch); } - if (!rb) { - rb = [target, []]; - currentObservable.batches.push(rb); - } - Array.prototype.push.apply(rb[1], relevantChanges); + batch.push(...relevantChanges); } else { // this is the naive straight forward synchronous dispatch callObserverSafe(target, relevantChanges); @@ -156,7 +150,7 @@ function callObserverSafe(listener, changes) { function callObserversFromMT() { const batches = this.batches; - this.batches = []; + this.batches = new Map(); for (const [listener, changes] of batches) { callObserverSafe(listener, changes); } diff --git a/sri.json b/sri.json index 139a94f..9823dc8 100644 --- a/sri.json +++ b/sri.json @@ -1,6 +1,6 @@ { - "dist/cdn/object-observer.js": "sha512-4JSeuw8uOwqMSGz6wX4oZWKwbgRt5dB0kcemUJkK+fgXR2lLHgxNjf2XeZDL1v3bVUuPvOPauGW89YgK0YmnmQ==", - "dist/cdn/object-observer.js.map": "sha512-SXGYrEiThB+7WA+kG3EQfBymkPEBTtnhEQST7XAC98PTzQbwPpvCNuXfN3gFUgpTRegB7PoVEKjq5/CR3Hr7Nw==", - "dist/cdn/object-observer.min.js": "sha512-62Md9XWugP+Ot8Wbm4yUq0N3CMLCJLDRraDrL+N3wbDYPMg4wan9Znly09Uh32sneJFLx1YDEIkCxF4vgJRhsQ==", - "dist/cdn/object-observer.min.js.map": "sha512-Pk2JKDulMrcTjp0y05o0pAL2rpjD5t/h9OnRY4DTNaXn+MiF1pq3ZjsAOomKyCujhQ6NFgugUyRmVAcKHm0DKA==" + "dist/cdn/object-observer.js": "sha512-ywG0PACduYPgmOWRLS+gcYNgOtHsmOFXmuCQI7cTxePndoglV+hn3AZhO+uEuh3b/pKJD3vujn3QvtGnGQgrEA==", + "dist/cdn/object-observer.js.map": "sha512-7eKH4iPXrU5EecOHb9EvT27wXKOhlnV2utpQshtFOs2FVOS8QEdzY93UR16Kppz0ivJx/2UY5yC7X8UwafFVuQ==", + "dist/cdn/object-observer.min.js": "sha512-yWDCo2vTsWOfXVN/gryETkDf3n/ZdZhYkpV3InCA0ywsvBsbCk3gSkIvgk/NDEShMtGAbp4UWbExfKdqxel+jw==", + "dist/cdn/object-observer.min.js.map": "sha512-/Yf1dTEZX8GzgN4lUJmtywBC6x7LrCNJBNpxXRY5B+IA3xQGIt7WNmXmLduHvVC65XjWpIm22zX8Z2Y+QJQNJA==" } \ No newline at end of file From 4feb4711bb38f14d17b4f1c201840ffefd464f22 Mon Sep 17 00:00:00 2001 From: Yuri Guller Date: Sat, 25 Apr 2026 23:07:00 +0300 Subject: [PATCH 21/23] simplifying and reducing duplications --- src/observables/abstract-base.ts | 1 - src/observables/methods/typed-set.ts | 26 ++++++++++++++------------ src/observables/typed-array.ts | 4 ++-- sri.json | 8 ++++---- 4 files changed, 20 insertions(+), 19 deletions(-) diff --git a/src/observables/abstract-base.ts b/src/observables/abstract-base.ts index 4cf561a..9dc44e0 100644 --- a/src/observables/abstract-base.ts +++ b/src/observables/abstract-base.ts @@ -50,7 +50,6 @@ export class ObservableBase implements ProxyHandler { detach() { this.#parent = null; - // this.#revoke(); return this.#target; } diff --git a/src/observables/methods/typed-set.ts b/src/observables/methods/typed-set.ts index 89480cf..28a0e1e 100644 --- a/src/observables/methods/typed-set.ts +++ b/src/observables/methods/typed-set.ts @@ -3,17 +3,19 @@ import { UPDATE, oMetaKey } from '../../constants.ts'; import { callObservers } from '../processors/proc-utils.ts'; export default function proxiedTypedArraySet(source, offset) { - const oMeta = this[oMetaKey]; - const target = oMeta.target; - const souLen = source.length; - const prev = target.slice(0); - offset = offset || 0; + const oMeta = this[oMetaKey]; + const target = oMeta.target; + const souLen = source.length; + offset = offset || 0; - target.set(source, offset); - const changes = new Array(souLen); - for (let i = offset; i < (souLen + offset); i++) { - changes[i - offset] = new Change(UPDATE, [i], target[i], prev[i], this); - } + if (souLen > 0) { + const prev = target.slice(offset, offset + souLen); + target.set(source, offset); - callObservers(oMeta, changes); -}; \ No newline at end of file + const changes = new Array(souLen); + for (let i = 0; i < souLen; i++) { + changes[i] = new Change(UPDATE, [offset + i], target[offset + i], prev[i], this); + } + callObservers(oMeta, changes); + } +}; diff --git a/src/observables/typed-array.ts b/src/observables/typed-array.ts index a559069..ea2a215 100644 --- a/src/observables/typed-array.ts +++ b/src/observables/typed-array.ts @@ -16,8 +16,8 @@ const proxiedTypedArrayMethods = { export class ObservableTypedArray extends ObservableBase { - get(taget: object, key: string): unknown { - return proxiedTypedArrayMethods[key] || taget[key]; + get(target: object, key: string): unknown { + return proxiedTypedArrayMethods[key] || target[key]; } observedGraphProcessor(source: Array, observableWrapper: ObservableBase): Array { diff --git a/sri.json b/sri.json index 9823dc8..6de6cca 100644 --- a/sri.json +++ b/sri.json @@ -1,6 +1,6 @@ { - "dist/cdn/object-observer.js": "sha512-ywG0PACduYPgmOWRLS+gcYNgOtHsmOFXmuCQI7cTxePndoglV+hn3AZhO+uEuh3b/pKJD3vujn3QvtGnGQgrEA==", - "dist/cdn/object-observer.js.map": "sha512-7eKH4iPXrU5EecOHb9EvT27wXKOhlnV2utpQshtFOs2FVOS8QEdzY93UR16Kppz0ivJx/2UY5yC7X8UwafFVuQ==", - "dist/cdn/object-observer.min.js": "sha512-yWDCo2vTsWOfXVN/gryETkDf3n/ZdZhYkpV3InCA0ywsvBsbCk3gSkIvgk/NDEShMtGAbp4UWbExfKdqxel+jw==", - "dist/cdn/object-observer.min.js.map": "sha512-/Yf1dTEZX8GzgN4lUJmtywBC6x7LrCNJBNpxXRY5B+IA3xQGIt7WNmXmLduHvVC65XjWpIm22zX8Z2Y+QJQNJA==" + "dist/cdn/object-observer.js": "sha512-Tra89W0DNzBljyBpR/VOMvCaY+NFktC1zw1IvUgLg1Jo5ENK6aUWupBMKpBY2XeXQSb+DgT6yRpKwwkatVb9QQ==", + "dist/cdn/object-observer.js.map": "sha512-M2ydJufgBCD/x3iN7S18oVnBrtNUgAblHx4+bv4jzTzLgMMgiGm0MXrFpzXEoFTygZFA8UypJVwJKwFWDIYeuQ==", + "dist/cdn/object-observer.min.js": "sha512-a5skTBW95X6WOe3RcKIyt4hEGWlBjrRmzj3nau9MOmG0qBYn2faOwsm4njvk0Y4zf6HpagYxE0lxRsepDm+c8A==", + "dist/cdn/object-observer.min.js.map": "sha512-nUJFT5jFiEHAjJghL1Fq3IpaWVMwAsj3uqfp3igdekgHIsIsk5PYtVPF+/wsRuxztiQvzSxSabfwW5vFem0d/Q==" } \ No newline at end of file From a3c7be7d5e77c7786e73e501ad9bf2c5de025f62 Mon Sep 17 00:00:00 2001 From: Yuri Guller Date: Sat, 25 Apr 2026 23:41:41 +0300 Subject: [PATCH 22/23] implemented filters --- docs/changelog.md | 8 +- docs/filter-paths.md | 73 -------------- docs/filters.md | 67 +++++++++++++ docs/observable.md | 10 +- readme.md | 36 +++---- src/changes-processors/filters.ts | 113 +++++++++++++-------- src/object-observer.ts | 37 +++---- src/observables/abstract-base.ts | 7 +- src/observables/processors/proc-utils.ts | 26 +---- sri.json | 8 +- tests/filters.js | 30 ++++++ tests/observe-specific-paths.js | 122 ++++++++++++----------- 12 files changed, 283 insertions(+), 254 deletions(-) delete mode 100644 docs/filter-paths.md create mode 100644 docs/filters.md diff --git a/docs/changelog.md b/docs/changelog.md index 8afde75..c799346 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -7,9 +7,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ## [7.0.0] +### BREAKING CHANGE +- [Issue no. 152](https://github.com/gullerya/object-observer/issues/152) - `Observable.observe` options API replaced: + - removed: `path`, `pathsOf`, `pathsFrom` + - added: `filters` — a non-empty array of `Filter` instances; multiple filters compose as logical AND + - `Filter` is now exported from the package entry; use its static factories (`exactPaths`, `pathsStartWith`, `directChildrenOf`, `custom`) to build filters + - migration: `{ path: 'a.b' }` → `{ filters: [Filter.exactPaths(['a.b'])] }`; `{ pathsOf: 'a' }` → `{ filters: [Filter.directChildrenOf('a')] }`; `{ pathsFrom: 'a' }` → `{ filters: [Filter.pathsStartWith('a')] }` ### Added - [Issue no. 149](https://github.com/gullerya/object-observer/issues/149) - added verifiers to prevent unallowed changes -- [Issue no. 152](https://github.com/gullerya/object-observer/issues/152) - change the way filters are configured +- `Filter.directChildrenOf(path)` factory — covers the previous `pathsOf` semantics, with a fixed sibling-prefix bug (`directChildrenOf('inner')` no longer matches `innerX.foo`) ### Fixed - `npm test` script referenced a non-existent `.json` config; now points to the correct `.js` file ### Chore diff --git a/docs/filter-paths.md b/docs/filter-paths.md deleted file mode 100644 index c4a170d..0000000 --- a/docs/filter-paths.md +++ /dev/null @@ -1,73 +0,0 @@ -# Filter paths options - -`Observable.observe(...)` allows `options` parameter, third one, optional. - -Some of the options are filtering ones, allowing to specify the changes of interest from within the observable graph. Here is a detailed description of those options. - -## __`pathsFrom`__ - -Value expected to be a non-empty string representing a path, any changes of which and deeper will be delivered to the observer. -> This option MAY NOT be used together with `path` option. - - - - - - -
- - - Given, that we have subscribed for the changes via: -
Observable.observe(o, callback, { pathsFrom: 'address' });
- Following mutations will be delivered to the callback: -
o.address.street.apt = 5;
-o.address.city = 'DreamCity';
-o.address = {};
- Following mutations will not be delivered to the callback: -
o.lastName = 'Joker';
-
- -## __`pathsOf`__ - -Value expected to be a string, which MAY be empty, representing a path. Changes to direct properties of which will be notified. - - - - - - -
- - - Given, that we have subscribed for the changes via: -
Observable.observe(o, callback, { pathsOf: 'address' });
- Following mutations will be delivered to the callback: -
o.address.street = {};
-o.address.city = 'DreamCity';
- Following mutations will not be delivered to the callback: -
o.lastName = 'Joker';
-o.address = {};
-o.address.street.apt = 5;
-
- -## __`path`__ - -Value expected to be a non-empty string, representing a specific path to observe. Only a changes of this exact path will be notified. - - - - - - -
- - - Given, that we have subscribed for the changes via: -
Observable.observe(o, callback, { path: 'address.street' });
- Following mutations will be delivered to the callback: -
o.address.street = {};
- Following mutations will not be delivered to the callback: -
o.lastName = 'Joker';
-o.address = {};
-o.address.street.apt = 5;
-
\ No newline at end of file diff --git a/docs/filters.md b/docs/filters.md new file mode 100644 index 0000000..6561f32 --- /dev/null +++ b/docs/filters.md @@ -0,0 +1,67 @@ +# Filters + +`Observable.observe(...)` accepts an optional third argument — an options object. The only currently supported option is `filters`: a non-empty array of `Filter` instances. Changes that survive **all** filters (logical AND) are delivered to the observer. + +```javascript +import { Observable, Filter } from '@gullerya/object-observer'; + +Observable.observe(obs, callback, { + filters: [Filter.directChildrenOf('address'), Filter.pathsStartWith('address.city')] +}); +``` + +`Filter` instances are created via static factory methods only; the constructor is private. + +## Factory methods + +### `Filter.exactPaths(paths: string[]): Filter` + +Delivers only changes whose path (as a dotted string) is an exact match for one of the provided paths. `paths` MUST be a non-empty array. + +```javascript +Filter.exactPaths(['firstName', 'address.city']) +``` + +### `Filter.pathsStartWith(prefix: string): Filter` + +Delivers changes whose path string starts with `prefix`. `prefix` MUST be a non-empty string. + +```javascript +Filter.pathsStartWith('address') +// matches 'address', 'address.city', 'address.extra.data', ... +``` + +### `Filter.directChildrenOf(path: string): Filter` + +Delivers changes whose path is a **direct child** of `path` (exactly one level deeper). `path` MUST be a string (may be empty — empty string means the root). REVERSE and SHUFFLE changes at `path` itself are also delivered, because they are semantically mutations of the container's children. + +```javascript +Filter.directChildrenOf('address') +// matches 'address.city', 'address.block' — but NOT 'address' or 'address.extra.data' + +Filter.directChildrenOf('') +// matches any top-level property change +``` + +### `Filter.custom(fn: (changes: Change[]) => Change[]): Filter` + +Wraps an arbitrary function. `fn` receives the (already-narrowed) array of `Change` objects and returns a filtered array. `fn` MUST be a function. + +```javascript +Filter.custom(changes => changes.filter(c => c.type === 'update')) +``` + +## Composition + +When multiple filters are provided, they run in order and each narrows the result of the previous one. Equivalent to logical AND: + +```javascript +// direct children of 'a' AND whose path starts with 'a.x' +// → only changes to 'a.x' +{ filters: [Filter.directChildrenOf('a'), Filter.pathsStartWith('a.x')] } +``` + +## Validation + +- `filters` (when provided) MUST be a non-empty array. +- Every element MUST be a `Filter` instance — bare functions are rejected. Use `Filter.custom(fn)` to wrap a function. diff --git a/docs/observable.md b/docs/observable.md index 24faac8..4078c79 100644 --- a/docs/observable.md +++ b/docs/observable.md @@ -86,15 +86,9 @@ Observable.unobserve(observableAddress); ``` ## Observation options -If/When provided, `options` parameter MUST contain ONLY one of the properties below, no 'unknown' properties allowed. +If/When provided, `options` MUST be an object. Unknown properties are rejected — incorrect observation options throw, to fail fast. -In order to fail-fast and prevent unexpected mess down the hill, incorrect observation options will throw. - -- __`path`__ - non-empty string; specific path to observe, only a changes of this exact path will be notified; [details here](filter-paths.md) - -- __`pathsOf`__ - string, MAY be empty; direct properties of the specified path will be notified; [details here](filter-paths.md) - -- __`pathsFrom`__ - non-empty string, any changes from the specified path and deeper will be delivered to the observer; [details here](filter-paths.md) +- __`filters`__ - non-empty array of `Filter` instances; changes that survive **all** filters (logical AND) are delivered to the observer; [details here](filters.md) ## `Change` instance properties diff --git a/readme.md b/readme.md index eb7e493..ed8e3b7 100644 --- a/readme.md +++ b/readme.md @@ -14,7 +14,7 @@ Main aspects and features: - observation is 'deep', yielding changes from a __sub-graphs__ too - nested objects of the observable graph are observables too - changes delivered in a __synchronous__ way by default, __asynchronous__ delivery is optionally available as per `Observable` configuration; [more details here](docs/sync-async.md) -- observed path may optionally be filtered as per `observer` configuration; [more details here](docs/filter-paths.md) +- observed changes may optionally be filtered via the `Filter` class; [more details here](docs/filters.md) - original objects are __cloned__ while turned into `Observable`s - circular references are nullified in the clone - __array__ specifics: @@ -210,11 +210,13 @@ In cases of massive changes touching presumably the whole array I took a pessimi ##### Observation options -`object-observer` allows to filter the events delivered to each callback/listener by an optional configuration object passed to the `observe` API. +`object-observer` allows to filter the events delivered to each callback/listener via an optional `filters` array — each element MUST be a `Filter` instance. Multiple filters compose as logical AND (each filter narrows the result). > In the examples below assume that `callback = changes => {...}`. ```javascript +import { Observable, Filter } from '@gullerya/object-observer'; + let user = { firstName: 'Aya', lastName: 'Guller', @@ -229,29 +231,17 @@ let user = { }, oUser = Observable.from(user); -// path +// exact paths // -// going to observe ONLY the changes of 'firstName' -Observable.observe(oUser, callback, {path: 'firstName'}); +// going to observe ONLY the changes of 'firstName' or 'address.city' +Observable.observe(oUser, callback, { filters: [Filter.exactPaths(['firstName', 'address.city'])] }); -// going to observe ONLY the changes of 'address.city' -Observable.observe(oUser, callback, {path: 'address.city'}); +// direct children of 'address' (city, street, block, extra) — and REVERSE/SHUFFLE at 'address' +Observable.observe(oUser, callback, { filters: [Filter.directChildrenOf('address')] }); -// pathsOf -// -// going to observe the changes of 'address' own properties ('city', 'block') but not else -Observable.observe(oUser, callback, {pathsOf: 'address'}); -// here we'll be notified on changes of -// address.city -// address.extra +// all changes from 'address' and deeper +Observable.observe(oUser, callback, { filters: [Filter.pathsStartWith('address')] }); -// pathsFrom -// -// going to observe the changes from 'address' and deeper -Observable.observe(oUser, callback, {pathsFrom: 'address'}); -// here we'll be notified on changes of -// address -// address.city -// address.extra -// address.extra.data +// custom predicate +Observable.observe(oUser, callback, { filters: [Filter.custom(cs => cs.filter(c => c.type === 'update'))] }); ``` diff --git a/src/changes-processors/filters.ts b/src/changes-processors/filters.ts index 136f2b2..38052eb 100644 --- a/src/changes-processors/filters.ts +++ b/src/changes-processors/filters.ts @@ -1,44 +1,77 @@ import { Change } from '../model/change.ts'; +import { REVERSE, SHUFFLE } from '../constants.ts'; + +export type FilterFn = (changes: Change[]) => Change[]; export class Filter { - static #privateCtorKey = Symbol('FilterPrivateConstructorKey'); - #fn: (changes: Change[]) => void; - - constructor(privateCtorKey, fn: (changes: Change[]) => void) { - if (privateCtorKey !== Filter.#privateCtorKey) { - throw new Error('Filter class cannot be instantiated directly; use provided factory methods'); - } - - this.#fn = fn; - } - - get fn(): (changes: Change[]) => void { return this.#fn; } - - static custom(fn: (changes: Change[]) => void): Filter { - if (typeof fn !== 'function') { - throw new Error('custom Filter requires a function as argument'); - } - return new Filter(Filter.#privateCtorKey, fn); - } - - static exactPaths(paths: string[]): Filter { - if (!Array.isArray(paths) || paths.length === 0) { - throw new Error('exactPaths Filter requires a non-empty array as argument'); - } - const pathsSet = new Set(paths); - return new Filter( - Filter.#privateCtorKey, - changes => changes.filter(change => pathsSet.has(change.pathAsString)) - ); - } - - static pathsStartWith(prefix: string): Filter { - if (typeof prefix !== 'string' || prefix === '') { - throw new Error('pathsStartWith Filter requires a non-empty string as argument'); - } - return new Filter( - Filter.#privateCtorKey, - changes => changes.filter(change => change.pathAsString.startsWith(prefix)) - ); - } + static #privateCtorKey = Symbol('FilterPrivateConstructorKey'); + #fn: FilterFn; + + constructor(privateCtorKey, fn: FilterFn) { + if (privateCtorKey !== Filter.#privateCtorKey) { + throw new Error('Filter class cannot be instantiated directly; use provided factory methods'); + } + + this.#fn = fn; + } + + get fn(): FilterFn { return this.#fn; } + + static custom(fn: FilterFn): Filter { + if (typeof fn !== 'function') { + throw new Error('custom Filter requires a function as argument'); + } + return new Filter(Filter.#privateCtorKey, fn); + } + + static exactPaths(paths: string[]): Filter { + if (!Array.isArray(paths) || paths.length === 0) { + throw new Error('exactPaths Filter requires a non-empty array as argument'); + } + const pathsSet = new Set(paths); + return new Filter( + Filter.#privateCtorKey, + changes => changes.filter(change => pathsSet.has(change.pathAsString)) + ); + } + + static pathsStartWith(prefix: string): Filter { + if (typeof prefix !== 'string' || prefix === '') { + throw new Error('pathsStartWith Filter requires a non-empty string as argument'); + } + return new Filter( + Filter.#privateCtorKey, + changes => changes.filter(change => change.pathAsString.startsWith(prefix)) + ); + } + + // direct children of the given path; an empty string represents the root. + // REVERSE/SHUFFLE happening at the path itself are also included + // (they are semantically mutations of the container's direct children). + static directChildrenOf(path: string): Filter { + if (typeof path !== 'string') { + throw new Error('directChildrenOf Filter requires a string as argument (MAY be empty)'); + } + const segments = path.split('.').filter(Boolean); + const depth = segments.length; + const prefix = segments.join('.'); + // at depth N, a direct-child path is the first N segments of change.path + return new Filter( + Filter.#privateCtorKey, + changes => changes.filter(change => { + const cp = change.path; + const pl = cp.length; + if (pl === depth + 1) { + for (let i = 0; i < depth; i++) { + if (cp[i] !== segments[i]) { return false; } + } + return true; + } + if (pl === depth && (change.type === REVERSE || change.type === SHUFFLE)) { + return change.pathAsString === prefix; + } + return false; + }) + ); + } } \ No newline at end of file diff --git a/src/object-observer.ts b/src/object-observer.ts index d2c241a..1915f51 100644 --- a/src/object-observer.ts +++ b/src/object-observer.ts @@ -1,5 +1,8 @@ import { oMetaKey } from './constants.ts'; import { getObservableFromRoot } from './observables/processors/proc-utils.ts'; +import { Filter, type FilterFn } from './changes-processors/filters.ts'; + +export { Filter }; const processObserveOptions = options => { @@ -7,30 +10,22 @@ const return null; } - const result = {}; + const result: { filters?: FilterFn[] } = {}; const invalidOptions = []; for (const [optName, optVal] of Object.entries(options)) { - if (optName === 'path') { - if (typeof optVal !== 'string' || optVal === '') { - throw new Error('"path" option, if/when provided, MUST be a non-empty string'); - } - result[optName] = optVal; - } else if (optName === 'pathsOf') { - if (options.path) { - throw new Error('"pathsOf" option MAY NOT be specified together with "path" option'); - } - if (typeof optVal !== 'string') { - throw new Error('"pathsOf" option, if/when provided, MUST be a string (MAY be empty)'); - } - result[optName] = options.pathsOf.split('.').filter(Boolean); - } else if (optName === 'pathsFrom') { - if (options.path || options.pathsOf) { - throw new Error('"pathsFrom" option MAY NOT be specified together with "path"/"pathsOf" option/s'); + if (optName === 'filters') { + if (!Array.isArray(optVal) || optVal.length === 0) { + throw new Error('"filters" option, if/when provided, MUST be a non-empty array of Filter instances'); } - if (typeof optVal !== 'string' || optVal === '') { - throw new Error('"pathsFrom" option, if/when provided, MUST be a non-empty string'); + const fns: FilterFn[] = new Array(optVal.length); + for (let i = 0; i < optVal.length; i++) { + const f = optVal[i]; + if (!(f instanceof Filter)) { + throw new Error('"filters" option, if/when provided, MUST be a non-empty array of Filter instances'); + } + fns[i] = f.fn; } - result[optName] = optVal; + result.filters = fns; } else { invalidOptions.push(optName); } @@ -38,7 +33,7 @@ const if (invalidOptions.length) { throw new Error(`'${invalidOptions.join(', ')}' is/are not a valid observer option/s`); } - return result; + return Object.keys(result).length === 0 ? null : result; }; export const Observable = Object.freeze({ diff --git a/src/observables/abstract-base.ts b/src/observables/abstract-base.ts index 9dc44e0..176c8d2 100644 --- a/src/observables/abstract-base.ts +++ b/src/observables/abstract-base.ts @@ -10,7 +10,7 @@ export interface ObservableOptions { } export type ChangesProcessor = (changes: Change[]) => void; -export class ObservableBase implements ProxyHandler { +export abstract class ObservableBase implements ProxyHandler { #parent: ObservableBase | null; ownKey: string | null; #target: object; @@ -60,10 +60,7 @@ export class ObservableBase implements ProxyHandler { get verifiers(): Array { return this.#verifiers; } get observers(): Array { return this.#observers; } - // eslint-disable-next-line @typescript-eslint/no-unused-vars - observedGraphProcessor(source: object, observableWrapper: ObservableBase, visited: Set): object { - throw new Error('observedGraphProcessor MUST be implemented in derived classes'); - } + abstract observedGraphProcessor(source: object, observableWrapper: ObservableBase, visited: Set): object; #processOptions(options: ObservableOptions | undefined): void { if (!options) { diff --git a/src/observables/processors/proc-utils.ts b/src/observables/processors/proc-utils.ts index dc502c2..5a91d2f 100644 --- a/src/observables/processors/proc-utils.ts +++ b/src/observables/processors/proc-utils.ts @@ -1,4 +1,4 @@ -import { oMetaKey, REVERSE, SHUFFLE } from '../../constants.ts'; +import { oMetaKey } from '../../constants.ts'; import { Change } from '../../model/change.ts'; import { ObservableBase } from '../abstract-base.ts'; import { ObservableArray } from '../array.ts'; @@ -113,29 +113,13 @@ export function callObservers(oMeta: ObservableBase, changes: Change[]) { }; function filterChanges(options, changes) { - if (options === null) { + if (options === null || !options.filters) { return changes; } - let result = changes; - if (options.path) { - const oPath = options.path; - result = changes.filter(change => - change.pathAsString === oPath - ); - } else if (options.pathsOf) { - const oPathsOf = options.pathsOf; - const oPathsOfStr = oPathsOf.join('.'); - result = changes.filter(change => - (change.path.length === oPathsOf.length + 1 || - (change.path.length === oPathsOf.length && (change.type === REVERSE || change.type === SHUFFLE))) && - change.pathAsString.startsWith(oPathsOfStr) - ); - } else if (options.pathsFrom) { - const oPathsFrom = options.pathsFrom; - result = changes.filter(change => - change.pathAsString.startsWith(oPathsFrom) - ); + const filters = options.filters; + for (let i = 0, l = filters.length; i < l && result.length; i++) { + result = filters[i](result); } return result; } diff --git a/sri.json b/sri.json index 6de6cca..3847fb6 100644 --- a/sri.json +++ b/sri.json @@ -1,6 +1,6 @@ { - "dist/cdn/object-observer.js": "sha512-Tra89W0DNzBljyBpR/VOMvCaY+NFktC1zw1IvUgLg1Jo5ENK6aUWupBMKpBY2XeXQSb+DgT6yRpKwwkatVb9QQ==", - "dist/cdn/object-observer.js.map": "sha512-M2ydJufgBCD/x3iN7S18oVnBrtNUgAblHx4+bv4jzTzLgMMgiGm0MXrFpzXEoFTygZFA8UypJVwJKwFWDIYeuQ==", - "dist/cdn/object-observer.min.js": "sha512-a5skTBW95X6WOe3RcKIyt4hEGWlBjrRmzj3nau9MOmG0qBYn2faOwsm4njvk0Y4zf6HpagYxE0lxRsepDm+c8A==", - "dist/cdn/object-observer.min.js.map": "sha512-nUJFT5jFiEHAjJghL1Fq3IpaWVMwAsj3uqfp3igdekgHIsIsk5PYtVPF+/wsRuxztiQvzSxSabfwW5vFem0d/Q==" + "dist/cdn/object-observer.js": "sha512-TnjrVURwBr0zoybWnQTulkNvVJOxMzU8IiieCsvgIVSwz7wLGA1EKcFZjY2YuDEwznxbkMitJpn4dsoy9KLqDw==", + "dist/cdn/object-observer.js.map": "sha512-LaT8o/h0SZFtigRnjmzr0sNnr/oc9lO0TFYOD15aOWNKIHLGVkvhqtT6yHvVY1ooVlwbSzaskl+mCkxRom/0Yw==", + "dist/cdn/object-observer.min.js": "sha512-jCjYwEoBCiXdQiwkvz6WJWc/e8gxf7ER2wIHOB1Fqf1L369D4z/KWsDgbBJgJ7efKthGHRb3fhZjRlRVTdKvcg==", + "dist/cdn/object-observer.min.js.map": "sha512-2u7DLjdVHcTVSqIQMSkwy25HHW2bI1YsqMg/Btv2lQJcnc677Cs6OaDcCXcXIASqxms8JvtHPunepABFr9XKJw==" } \ No newline at end of file diff --git a/tests/filters.js b/tests/filters.js index 64a4333..4b7d243 100644 --- a/tests/filters.js +++ b/tests/filters.js @@ -52,3 +52,33 @@ test('pathsStartWith filter - negative cases', () => { assert.throws(() => Filter.pathsStartWith([]), 'pathsStartWith Filter requires a non-empty string as argument'); assert.throws(() => Filter.pathsStartWith(''), 'pathsStartWith Filter requires a non-empty string as argument'); }); + +test('directChildrenOf filter - positive cases', () => { + const f = Filter.directChildrenOf('a'); + const changes = [ + new Change('update', ['a'], 1, 0), + new Change('update', ['a', 'b'], 2, 0), + new Change('update', ['a', 'b', 'c'], 3, 0), + new Change('update', ['a', 'c'], 4, 0), + new Change('update', ['aX', 'b'], 5, 0) + ]; + const filtered = f.fn(changes); + assert.strictEqual(filtered.length, 2); +}); + +test('directChildrenOf filter - empty path = root', () => { + const f = Filter.directChildrenOf(''); + const changes = [ + new Change('update', ['a'], 1, 0), + new Change('update', ['a', 'b'], 2, 0), + new Change('reverse', [], undefined, undefined), + new Change('shuffle', [], undefined, undefined) + ]; + const filtered = f.fn(changes); + assert.strictEqual(filtered.length, 3); +}); + +test('directChildrenOf filter - negative cases', () => { + assert.throws(() => Filter.directChildrenOf(null), 'directChildrenOf Filter requires a string as argument (MAY be empty)'); + assert.throws(() => Filter.directChildrenOf(123), 'directChildrenOf Filter requires a string as argument (MAY be empty)'); +}); diff --git a/tests/observe-specific-paths.js b/tests/observe-specific-paths.js index 35a7490..f3fff4c 100644 --- a/tests/observe-specific-paths.js +++ b/tests/observe-specific-paths.js @@ -1,71 +1,39 @@ import { test } from '@gullerya/just-test'; import { assert } from '@gullerya/just-test/assert'; -import { Observable } from '../src/object-observer.ts'; +import { Observable, Filter } from '../src/object-observer.ts'; -test('baseline - negative - path not a string', () => { +test('baseline - negative - filters not an array', () => { const oo = Observable.from({}); assert.throws( - () => Observable.observe(oo, () => { }, { path: 4 }), - '"path" option, if/when provided, MUST be a non-empty string' + () => Observable.observe(oo, () => { }, { filters: 'nope' }), + '"filters" option, if/when provided, MUST be a non-empty array of Filter instances' ); }); -test('baseline - negative - path empty', () => { +test('baseline - negative - filters empty array', () => { const oo = Observable.from({}); assert.throws( - () => Observable.observe(oo, () => { }, { path: '' }), - '"path" option, if/when provided, MUST be a non-empty string' + () => Observable.observe(oo, () => { }, { filters: [] }), + '"filters" option, if/when provided, MUST be a non-empty array of Filter instances' ); }); -test('baseline - negative - pathsFrom not a string', () => { +test('baseline - negative - filters contains non-Filter element', () => { const oo = Observable.from({}); assert.throws( - () => Observable.observe(oo, () => { }, { pathsFrom: 4 }), - '"pathsFrom" option, if/when provided, MUST be a non-empty string' + () => Observable.observe(oo, () => { }, { filters: [Filter.exactPaths(['a']), 'bad'] }), + '"filters" option, if/when provided, MUST be a non-empty array of Filter instances' ); }); -test('baseline - negative - pathsFrom empty', () => { +test('baseline - negative - foreign option', () => { const oo = Observable.from({}); assert.throws( - () => Observable.observe(oo, () => { }, { pathsFrom: '' }), - '"pathsFrom" option, if/when provided, MUST be a non-empty string' - ); -}); - -test('baseline - negative - no pathsFrom when path present', () => { - const oo = Observable.from({}); - assert.throws( - () => Observable.observe(oo, () => { }, { path: 'some', pathsFrom: 'else' }), - '"pathsFrom" option MAY NOT be specified together with' - ); -}); - -test('baseline - negative - no foreign options (pathFrom)', () => { - const oo = Observable.from({}); - assert.throws( - () => Observable.observe(oo, () => { }, { pathFrom: 'something' }), + () => Observable.observe(oo, () => { }, { somethingElse: 'x' }), 'is/are not a valid observer option/s' ); }); -test('observe paths of - negative a', () => { - const oo = Observable.from({}); - assert.throws( - () => Observable.observe(oo, () => { }, { pathsOf: 4 }), - '"pathsOf" option, if/when provided, MUST be a string (MAY be empty)' - ); -}); - -test('observe paths of - negative b', () => { - const oo = Observable.from({}); - assert.throws( - () => Observable.observe(oo, () => { }, { path: 'inner.prop', pathsOf: 'some.thing' }), - '"pathsOf" option MAY NOT be specified together with "path" option' - ); -}); - test('baseline - no options / empty options', () => { const oo = Observable.from({ inner: { prop: 'more' } }), @@ -92,7 +60,7 @@ test('baseline - empty options is valid', () => { Observable.unobserve(oo, observer); }); -test('observe specific path', () => { +test('exactPaths filter - single path', () => { const oo = Observable.from({ inner: { prop: 'more' } }); let callbackCalls = 0, changesCounter = 0; @@ -100,7 +68,7 @@ test('observe specific path', () => { Observable.observe(oo, changes => { callbackCalls++; changesCounter += changes.length; - }, { path: 'inner' }); + }, { filters: [Filter.exactPaths(['inner'])] }); oo.newProp = 'non-relevant'; oo.inner.other = 'non-relevant'; @@ -110,11 +78,11 @@ test('observe specific path', () => { assert.strictEqual(callbackCalls, 1); }); -test('observe paths from .. and deeper', () => { +test('pathsStartWith filter - path and deeper', () => { const oo = Observable.from({ inner: { prop: 'more', nested: { text: 'text' } } }); let counter = 0; - Observable.observe(oo, changes => { counter += changes.length; }, { pathsFrom: 'inner.prop' }); + Observable.observe(oo, changes => { counter += changes.length; }, { filters: [Filter.pathsStartWith('inner.prop')] }); oo.nonRelevant = 'non-relevant'; oo.inner.also = 'non-relevant'; oo.inner.prop = 'relevant'; @@ -123,10 +91,10 @@ test('observe paths from .. and deeper', () => { assert.strictEqual(counter, 3); }); -test('observe paths of - inner case', () => { +test('directChildrenOf filter - inner case', () => { const oo = Observable.from({ inner: { prop: 'more', nested: { text: 'text' } } }); let counter = 0; - Observable.observe(oo, changes => { counter += changes.length; }, { pathsOf: 'inner.nested' }); + Observable.observe(oo, changes => { counter += changes.length; }, { filters: [Filter.directChildrenOf('inner.nested')] }); oo.nonRelevant = 'non-relevant'; oo.inner.also = 'non-relevant'; oo.inner.nested.text = 'relevant'; @@ -136,10 +104,10 @@ test('observe paths of - inner case', () => { assert.strictEqual(counter, 2); }); -test('observe paths of - array - property of same depth updated', () => { +test('directChildrenOf filter - array - property of same depth updated', () => { const oo = Observable.from({ array: [1, 2, 3], prop: { inner: 'value' } }); let counter = 0; - Observable.observe(oo, changes => { counter += changes.length; }, { pathsOf: 'array' }); + Observable.observe(oo, changes => { counter += changes.length; }, { filters: [Filter.directChildrenOf('array')] }); oo.nonRelevant = 'non-relevant'; oo.prop.inner = 'non-relevant'; oo.prop = { newObj: { test: 'non-relevant' } }; @@ -149,10 +117,10 @@ test('observe paths of - array - property of same depth updated', () => { assert.equal(counter, 2); }); -test('observe paths of - root case', () => { +test('directChildrenOf filter - root case', () => { const oo = Observable.from({ inner: { prop: 'more', nested: { text: 'text' } } }); let counter = 0; - Observable.observe(oo, changes => { counter += changes.length; }, { pathsOf: '' }); + Observable.observe(oo, changes => { counter += changes.length; }, { filters: [Filter.directChildrenOf('')] }); oo.relevant = 'relevant'; oo.inner.also = 'non-relevant'; oo.inner = { newObj: { test: 'relevant' } }; @@ -160,20 +128,58 @@ test('observe paths of - root case', () => { assert.equal(counter, 2); }); -test('observe paths of - root case - array sort', () => { +test('directChildrenOf filter - root case - array sort', () => { const oo = Observable.from([1, 3, 2, 4, 9]); let counter = 0; - Observable.observe(oo, changes => { counter += changes.length; }, { pathsOf: '' }); + Observable.observe(oo, changes => { counter += changes.length; }, { filters: [Filter.directChildrenOf('')] }); oo.sort(); assert.isTrue(oo[0] === 1 && oo[1] === 2 && oo[2] === 3 && oo[3] === 4 && oo[4] === 9); assert.equal(counter, 1); }); -test('observe paths of - root case - array reverse', () => { +test('directChildrenOf filter - root case - array reverse', () => { const oo = Observable.from([1, 2, 3, 4, 9]); let counter = 0; - Observable.observe(oo, changes => { counter += changes.length; }, { pathsOf: '' }); + Observable.observe(oo, changes => { counter += changes.length; }, { filters: [Filter.directChildrenOf('')] }); oo.reverse(); assert.isTrue(oo[0] === 9 && oo[1] === 4 && oo[2] === 3 && oo[3] === 2 && oo[4] === 1); assert.equal(counter, 1); }); + +test('multiple filters compose as AND', () => { + const oo = Observable.from({ a: { x: 1, y: 2 }, b: { x: 3 } }); + let counter = 0; + // direct children of 'a' AND under 'a.x' → only 'a.x' changes + Observable.observe( + oo, + changes => { counter += changes.length; }, + { filters: [Filter.directChildrenOf('a'), Filter.pathsStartWith('a.x')] } + ); + oo.a.x = 10; // kept + oo.a.y = 20; // dropped (not under a.x) + oo.b.x = 30; // dropped (not direct child of a) + assert.strictEqual(counter, 1); +}); + +test('directChildrenOf filter - sibling-prefix is not a child', () => { + // regression guard: path 'innerX.foo' should NOT be a direct child of 'inner' + const oo = Observable.from({ inner: { a: 1 }, innerX: { a: 1 } }); + let counter = 0; + Observable.observe(oo, changes => { counter += changes.length; }, { filters: [Filter.directChildrenOf('inner')] }); + oo.innerX.a = 2; // must NOT match + oo.inner.a = 2; // must match + assert.strictEqual(counter, 1); +}); + +test('custom filter via Filter.custom', () => { + const oo = Observable.from({ a: 1, b: 2 }); + let counter = 0; + Observable.observe( + oo, + changes => { counter += changes.length; }, + { filters: [Filter.custom(cs => cs.filter(c => c.value > 10))] } + ); + oo.a = 5; // dropped + oo.b = 20; // kept + assert.strictEqual(counter, 1); +}); From bf48a529041845d215de0c8aa308b7ed4c85185b Mon Sep 17 00:00:00 2001 From: Yuri Guller Date: Sun, 26 Apr 2026 13:08:18 +0300 Subject: [PATCH 23/23] validators feature --- readme.md | 38 ++- src/changes-processors/validators.ts | 26 ++ src/changes-processors/verifiers.ts | 0 src/object-observer.ts | 3 +- src/observables/abstract-base.ts | 21 +- src/observables/methods/delete-property.ts | 6 +- src/observables/methods/pop.ts | 6 +- src/observables/methods/push.ts | 41 +-- src/observables/methods/reverse.ts | 6 +- src/observables/methods/set.ts | 8 +- src/observables/methods/shift.ts | 6 +- src/observables/methods/sort.ts | 6 +- src/observables/methods/typed-set.ts | 9 +- src/observables/methods/unshift.ts | 11 +- src/observables/processors/proc-utils.ts | 23 ++ tests/validators.js | 277 +++++++++++++++++++++ 16 files changed, 444 insertions(+), 43 deletions(-) create mode 100644 src/changes-processors/validators.ts delete mode 100644 src/changes-processors/verifiers.ts create mode 100644 tests/validators.js diff --git a/readme.md b/readme.md index ed8e3b7..1602404 100644 --- a/readme.md +++ b/readme.md @@ -17,14 +17,17 @@ Main aspects and features: - observed changes may optionally be filtered via the `Filter` class; [more details here](docs/filters.md) - original objects are __cloned__ while turned into `Observable`s - circular references are nullified in the clone +- observed mutations on plain objects: + - property assignment (`obj.x = v`, `obj['x'] = v`) + - property deletion (`delete obj.x`) - __array__ specifics: - - generic object-like mutations supported - - intrinsic `Array` mutation methods supported: `pop`, `push`, `shift`, `unshift`, `reverse`, `sort`, `fill`, `splice`, `copyWithin` - - massive mutations delivered in a single callback, usually having an array of an atomic changes + - indexed assignment (`arr[i] = v`), `length` assignment, and `delete arr[i]` are observed (same traps as for plain objects) + - intrinsic `Array` mutation methods observed: `pop`, `push`, `shift`, `unshift`, `reverse`, `sort`, `fill`, `splice`, `copyWithin` + - massive mutations delivered in a single callback, usually as an array of atomic changes - __typed array__ specifics: - - generic object-like mutations supported - - intrinsic `TypedArray` mutation methods supported: `reverse`, `sort`, `fill`, `set`, `copyWithin` - - massive mutations delivered in a single callback, usually having an array of an atomic changes + - indexed assignment (`ta[i] = v`) is observed (same trap as for plain objects); `length` is fixed + - intrinsic `TypedArray` mutation methods observed: `reverse`, `sort`, `fill`, `set`, `copyWithin` + - massive mutations delivered in a single callback, usually as an array of atomic changes - intrinsic mutation methods of `Map`, `WeakMap`, `Set`, `WeakSet` (`set`, `delete`) etc __are not__ observed (see this [issue](https://github.com/gullerya/object-observer/issues/1) for more details) - following host objects (and their extensions) are __skipped__ from cloning / turning into observables: `Date` @@ -245,3 +248,26 @@ Observable.observe(oUser, callback, { filters: [Filter.pathsStartWith('address') // custom predicate Observable.observe(oUser, callback, { filters: [Filter.custom(cs => cs.filter(c => c.type === 'update'))] }); ``` + +##### Validators + +`object-observer` allows to veto mutations before they are applied via an optional `validators` array supplied to `Observable.from`. Each element MUST be a `Validator` instance. Validators are attached at tree creation — they apply to the whole observable graph, not per observer. + +A validator receives the prospective `Change[]` (with rooted paths and the raw, un-observified new value) BEFORE the underlying target is mutated. Throwing from a validator aborts the mutation: the target is left unchanged and observers are not invoked. Multiple validators compose as logical AND with short-circuit — the first throw stops the chain. + +```javascript +import { Observable, Validator } from '@gullerya/object-observer'; + +const immutableMarker = Validator.custom(changes => { + for (const c of changes) { + if (c.oldValue === 'immutable') { + throw new Error(`change at '${c.pathAsString}' rejected: oldValue is immutable`); + } + } +}); + +const oo = Observable.from({ a: 'immutable' }, { validators: [immutableMarker] }); +oo.a = 'other'; // throws; oo.a still === 'immutable'; no observer fired +``` + +> Not yet supported: validators do not currently run for `Array.prototype.splice`, `Array.prototype.fill`, and `Array.prototype.copyWithin` (and their `TypedArray` counterparts where applicable). These mutations proceed as before without validator consultation. Support is planned for a subsequent release. diff --git a/src/changes-processors/validators.ts b/src/changes-processors/validators.ts new file mode 100644 index 0000000..1de0704 --- /dev/null +++ b/src/changes-processors/validators.ts @@ -0,0 +1,26 @@ +import { Change } from '../model/change.ts'; + +export type ValidatorFn = (changes: Change[]) => void; + +export class Validator { + static #privateCtorKey = Symbol('ValidatorPrivateConstructorKey'); + + #validate: ValidatorFn; + + constructor(privateCtorKey, fn: ValidatorFn) { + if (privateCtorKey !== Validator.#privateCtorKey) { + throw new Error('Validator class cannot be instantiated directly; use provided factory methods'); + } + + this.#validate = fn; + } + + get validate(): ValidatorFn { return this.#validate; } + + static custom(fn: ValidatorFn): Validator { + if (typeof fn !== 'function') { + throw new Error('custom Validator requires a function as argument'); + } + return new Validator(Validator.#privateCtorKey, fn); + } +} diff --git a/src/changes-processors/verifiers.ts b/src/changes-processors/verifiers.ts deleted file mode 100644 index e69de29..0000000 diff --git a/src/object-observer.ts b/src/object-observer.ts index 1915f51..4e7d4ae 100644 --- a/src/object-observer.ts +++ b/src/object-observer.ts @@ -1,8 +1,9 @@ import { oMetaKey } from './constants.ts'; import { getObservableFromRoot } from './observables/processors/proc-utils.ts'; import { Filter, type FilterFn } from './changes-processors/filters.ts'; +import { Validator } from './changes-processors/validators.ts'; -export { Filter }; +export { Filter, Validator }; const processObserveOptions = options => { diff --git a/src/observables/abstract-base.ts b/src/observables/abstract-base.ts index 176c8d2..cdd1db2 100644 --- a/src/observables/abstract-base.ts +++ b/src/observables/abstract-base.ts @@ -1,12 +1,13 @@ import { Change } from '../model/change.ts'; +import { Validator } from '../changes-processors/validators.ts'; import { proxiedDeleteProperty } from './methods/delete-property.ts'; import { proxiedSet } from './methods/set.ts'; -const validObservableOptionKeys = { async: 1, verifiers: 1 }; +const validObservableOptionKeys = { async: 1, validators: 1 }; export interface ObservableOptions { async?: boolean; - verifiers?: Array; + validators?: Array; } export type ChangesProcessor = (changes: Change[]) => void; @@ -23,7 +24,7 @@ export abstract class ObservableBase implements ProxyHandler { set; deleteProperty; - #verifiers: Array = []; + #validators: Array = []; #observers: Array = []; constructor(properties) { @@ -57,7 +58,7 @@ export abstract class ObservableBase implements ProxyHandler { get target(): object { return this.#target; } get proxy(): unknown { return this.#proxy; } get async(): boolean { return this.#async; } - get verifiers(): Array { return this.#verifiers; } + get validators(): Array { return this.#validators; } get observers(): Array { return this.#observers; } abstract observedGraphProcessor(source: object, observableWrapper: ObservableBase, visited: Set): object; @@ -77,8 +78,16 @@ export abstract class ObservableBase implements ProxyHandler { this.#async = Boolean(options.async); - if (Array.isArray(options.verifiers)) { - this.#verifiers.push(...options.verifiers); + if (options.validators !== undefined) { + if (!Array.isArray(options.validators) || options.validators.length === 0) { + throw new Error('"validators" option, if/when provided, MUST be a non-empty array of Validator instances'); + } + for (const v of options.validators) { + if (!(v instanceof Validator)) { + throw new Error('"validators" option, if/when provided, MUST be a non-empty array of Validator instances'); + } + } + this.#validators.push(...options.validators); } } } \ No newline at end of file diff --git a/src/observables/methods/delete-property.ts b/src/observables/methods/delete-property.ts index f6dc514..d88bc8f 100644 --- a/src/observables/methods/delete-property.ts +++ b/src/observables/methods/delete-property.ts @@ -1,9 +1,13 @@ import { Change } from '../../model/change.ts'; import { DELETE } from '../../constants.ts'; -import { callObservers, detachIfObservable } from '../processors/proc-utils.ts'; +import { callObservers, detachIfObservable, runValidators } from '../processors/proc-utils.ts'; export function proxiedDeleteProperty(target: object, key: string | symbol): boolean { const prevValue = target[key]; + + const prospective = [new Change(DELETE, [key], undefined, prevValue, this.proxy)]; + runValidators(this, prospective); + delete target[key]; const oldValue = detachIfObservable(prevValue); diff --git a/src/observables/methods/pop.ts b/src/observables/methods/pop.ts index 037f9aa..a7c9b38 100644 --- a/src/observables/methods/pop.ts +++ b/src/observables/methods/pop.ts @@ -1,11 +1,15 @@ import { Change } from '../../model/change.ts'; import { DELETE, oMetaKey } from '../../constants.ts'; -import { callObservers, detachIfObservable } from '../processors/proc-utils.ts'; +import { callObservers, detachIfObservable, runValidators } from '../processors/proc-utils.ts'; export default function proxiedPop() { const oMeta = this[oMetaKey]; const target = oMeta.target; const poppedIndex = target.length - 1; + const prevValue = target[poppedIndex]; + + const prospective = [new Change(DELETE, [poppedIndex], undefined, prevValue, this)]; + runValidators(oMeta, prospective); const popResult = detachIfObservable(target.pop()); diff --git a/src/observables/methods/push.ts b/src/observables/methods/push.ts index 56d15bf..58900d8 100644 --- a/src/observables/methods/push.ts +++ b/src/observables/methods/push.ts @@ -1,25 +1,30 @@ import { Change } from '../../model/change.ts'; import { INSERT, oMetaKey } from '../../constants.ts'; -import { getObservedOf } from '../processors/proc-utils.ts'; -import { callObservers } from '../processors/proc-utils.ts'; +import { callObservers, getObservedOf, runValidators } from '../processors/proc-utils.ts'; export default function proxiedPush(...pushItems: unknown[]) { - const oMeta = this[oMetaKey]; - const target = oMeta.target; - const pushLen = pushItems.length; - const pushContent = new Array(pushLen); - const initialLength = target.length; + const oMeta = this[oMetaKey]; + const target = oMeta.target; + const pushLen = pushItems.length; + const initialLength = target.length; - for (let i = 0; i < pushLen; i++) { - pushContent[i] = getObservedOf(pushItems[i], initialLength + i, oMeta); - } - const pushResult = Reflect.apply(target.push, target, pushContent); + const prospective = new Array(pushLen); + for (let i = 0; i < pushLen; i++) { + prospective[i] = new Change(INSERT, [initialLength + i], pushItems[i], undefined, this); + } + runValidators(oMeta, prospective); - const changes = []; - for (let i = initialLength, j = target.length; i < j; i++) { - changes[i - initialLength] = new Change(INSERT, [i], target[i], undefined, this); - } - callObservers(oMeta, changes); + const pushContent = new Array(pushLen); + for (let i = 0; i < pushLen; i++) { + pushContent[i] = getObservedOf(pushItems[i], initialLength + i, oMeta); + } + const pushResult = Reflect.apply(target.push, target, pushContent); - return pushResult; -}; \ No newline at end of file + const changes = new Array(pushLen); + for (let i = 0; i < pushLen; i++) { + changes[i] = new Change(INSERT, [initialLength + i], target[initialLength + i], undefined, this); + } + callObservers(oMeta, changes); + + return pushResult; +}; diff --git a/src/observables/methods/reverse.ts b/src/observables/methods/reverse.ts index 524db19..6b1e580 100644 --- a/src/observables/methods/reverse.ts +++ b/src/observables/methods/reverse.ts @@ -1,15 +1,17 @@ import { Change } from '../../model/change.ts'; import { REVERSE, oMetaKey } from '../../constants.ts'; -import { callObservers, reindexObservableChildren } from '../processors/proc-utils.ts'; +import { callObservers, reindexObservableChildren, runValidators } from '../processors/proc-utils.ts'; export default function proxiedReverse() { const oMeta = this[oMetaKey]; const target = oMeta.target; + const changes = [new Change(REVERSE, [], undefined, undefined, this)]; + runValidators(oMeta, changes); + target.reverse(); reindexObservableChildren(target); - const changes = [new Change(REVERSE, [], undefined, undefined, this)]; callObservers(oMeta, changes); return this; diff --git a/src/observables/methods/set.ts b/src/observables/methods/set.ts index 859c59c..9a04274 100644 --- a/src/observables/methods/set.ts +++ b/src/observables/methods/set.ts @@ -1,11 +1,17 @@ import { Change } from '../../model/change.ts'; import { INSERT, UPDATE } from '../../constants.ts'; -import { callObservers, detachIfObservable, getObservedOf } from '../processors/proc-utils.ts'; +import { callObservers, detachIfObservable, getObservedOf, runValidators } from '../processors/proc-utils.ts'; export function proxiedSet(target: object, key: string | symbol, value: unknown): boolean { const prevValue = target[key]; if (value !== prevValue) { + // build prospective change with raw (un-observified) new value so validators see what the caller wrote + const prospective = prevValue === undefined + ? [new Change(INSERT, [key], value, undefined, this.proxy)] + : [new Change(UPDATE, [key], value, prevValue, this.proxy)]; + runValidators(this, prospective); + const newValue = getObservedOf(value, key, this); target[key] = newValue; diff --git a/src/observables/methods/shift.ts b/src/observables/methods/shift.ts index 34114e5..a944306 100644 --- a/src/observables/methods/shift.ts +++ b/src/observables/methods/shift.ts @@ -1,10 +1,14 @@ import { Change } from '../../model/change.ts'; import { DELETE, oMetaKey } from '../../constants.ts'; -import { callObservers, detachIfObservable, reindexObservableChildren } from '../processors/proc-utils.ts'; +import { callObservers, detachIfObservable, reindexObservableChildren, runValidators } from '../processors/proc-utils.ts'; export default function proxiedShift() { const oMeta = this[oMetaKey]; const target = oMeta.target; + const prevValue = target[0]; + + const prospective = [new Change(DELETE, [0], undefined, prevValue, this)]; + runValidators(oMeta, prospective); const shiftResult = detachIfObservable(target.shift()); reindexObservableChildren(target); diff --git a/src/observables/methods/sort.ts b/src/observables/methods/sort.ts index c73d3b5..9782281 100644 --- a/src/observables/methods/sort.ts +++ b/src/observables/methods/sort.ts @@ -1,15 +1,17 @@ import { Change } from '../../model/change.ts'; import { SHUFFLE, oMetaKey } from '../../constants.ts'; -import { callObservers, reindexObservableChildren } from '../processors/proc-utils.ts'; +import { callObservers, reindexObservableChildren, runValidators } from '../processors/proc-utils.ts'; export default function proxiedSort(comparator) { const oMeta = this[oMetaKey]; const target = oMeta.target; + const changes = [new Change(SHUFFLE, [], undefined, undefined, this)]; + runValidators(oMeta, changes); + target.sort(comparator); reindexObservableChildren(target); - const changes = [new Change(SHUFFLE, [], undefined, undefined, this)]; callObservers(oMeta, changes); return this; diff --git a/src/observables/methods/typed-set.ts b/src/observables/methods/typed-set.ts index 28a0e1e..7b9b82a 100644 --- a/src/observables/methods/typed-set.ts +++ b/src/observables/methods/typed-set.ts @@ -1,6 +1,6 @@ import { Change } from '../../model/change.ts'; import { UPDATE, oMetaKey } from '../../constants.ts'; -import { callObservers } from '../processors/proc-utils.ts'; +import { callObservers, runValidators } from '../processors/proc-utils.ts'; export default function proxiedTypedArraySet(source, offset) { const oMeta = this[oMetaKey]; @@ -10,6 +10,13 @@ export default function proxiedTypedArraySet(source, offset) { if (souLen > 0) { const prev = target.slice(offset, offset + souLen); + + const prospective = new Array(souLen); + for (let i = 0; i < souLen; i++) { + prospective[i] = new Change(UPDATE, [offset + i], source[i], prev[i], this); + } + runValidators(oMeta, prospective); + target.set(source, offset); const changes = new Array(souLen); diff --git a/src/observables/methods/unshift.ts b/src/observables/methods/unshift.ts index d37ebfd..a10baaf 100644 --- a/src/observables/methods/unshift.ts +++ b/src/observables/methods/unshift.ts @@ -1,13 +1,19 @@ import { Change } from '../../model/change.ts'; import { INSERT, oMetaKey } from '../../constants.ts'; -import { callObservers, getObservedOf, reindexObservableChildren } from '../processors/proc-utils.ts'; +import { callObservers, getObservedOf, reindexObservableChildren, runValidators } from '../processors/proc-utils.ts'; export default function proxiedUnshift(...unshiftItems: unknown[]) { const oMeta = this[oMetaKey]; const target = oMeta.target; const unshiftLen = unshiftItems.length; - const unshiftContent = new Array(unshiftLen); + const prospective = new Array(unshiftLen); + for (let i = 0; i < unshiftLen; i++) { + prospective[i] = new Change(INSERT, [i], unshiftItems[i], undefined, this); + } + runValidators(oMeta, prospective); + + const unshiftContent = new Array(unshiftLen); for (let i = 0; i < unshiftLen; i++) { unshiftContent[i] = getObservedOf(unshiftItems[i], i, oMeta); } @@ -15,7 +21,6 @@ export default function proxiedUnshift(...unshiftItems: unknown[]) { reindexObservableChildren(target); - // publish changes const changes = new Array(unshiftLen); for (let i = 0; i < unshiftLen; i++) { changes[i] = new Change(INSERT, [i], target[i], undefined, this); diff --git a/src/observables/processors/proc-utils.ts b/src/observables/processors/proc-utils.ts index 5a91d2f..4f0c621 100644 --- a/src/observables/processors/proc-utils.ts +++ b/src/observables/processors/proc-utils.ts @@ -61,6 +61,29 @@ export function detachIfObservable(value: unknown): unknown { return value; } +// invoke every Validator attached to the root of this observable tree. +// changes' paths are rebuilt relative to the root before being passed to validators. +// any validator throw propagates — mutation is aborted. +export function runValidators(oMeta: ObservableBase, changes: Change[]): void { + // find root + accumulate path prefix from this node up + const prefix: Array = []; + let current: ObservableBase = oMeta; + while (current.parent) { + prefix.unshift(current.ownKey); + current = current.parent; + } + const validators = current.validators; + if (validators.length === 0) { + return; + } + const rooted = prefix.length === 0 + ? changes + : changes.map(c => new Change(c.type, [...prefix, ...c.path], c.value, c.oldValue, c.object)); + for (let i = 0, l = validators.length; i < l; i++) { + validators[i].validate(rooted); + } +} + export function callObservers(oMeta: ObservableBase, changes: Change[]) { let currentObservable: ObservableBase = oMeta; let isAsync, observers, target, options, relevantChanges, i; diff --git a/tests/validators.js b/tests/validators.js new file mode 100644 index 0000000..3a251bd --- /dev/null +++ b/tests/validators.js @@ -0,0 +1,277 @@ +import { test } from '@gullerya/just-test'; +import { assert } from '@gullerya/just-test/assert'; +import { Observable, Validator } from '../src/object-observer.ts'; +import { Change } from '../src/model/change.ts'; +import { oMetaKey } from '../src/constants.ts'; + +test('test validators - ctor direct use forbidden', () => { + assert.throws(() => new Validator('some', () => { }), 'Validator class cannot be instantiated directly'); +}); + +test('custom validator - positive cases', () => { + const validatorLogic = () => { }; + const v = Validator.custom(validatorLogic); + assert.strictEqual(v.validate, validatorLogic, 'Validator code is correct'); +}); + +test('custom validator - negative cases', () => { + assert.throws(() => Validator.custom(null), 'custom Validator requires a function as argument'); + assert.throws(() => Validator.custom('some'), 'custom Validator requires a function as argument'); +}); + +test('custom validator - validate invokes passed fn - success path (no throw)', () => { + let called = 0; + const v = Validator.custom(changes => { called = changes.length; }); + const changes = [new Change('update', ['a'], 1, 0), new Change('update', ['b'], 2, 0)]; + v.validate(changes); + assert.strictEqual(called, 2); +}); + +test('custom validator - validate propagates throw', () => { + const v = Validator.custom(() => { throw new Error('rejected'); }); + assert.throws(() => v.validate([new Change('update', ['a'], 1, 0)]), 'rejected'); +}); + +test('custom validator - validate is read-only', () => { + const v = Validator.custom(() => { }); + assert.throws(() => { v.validate = () => { }; }); +}); + +test('Observable.from - validators option - negative - not an array', () => { + assert.throws( + () => Observable.from({}, { validators: 'nope' }), + '"validators" option, if/when provided, MUST be a non-empty array of Validator instances' + ); +}); + +test('Observable.from - validators option - negative - number', () => { + assert.throws( + () => Observable.from({}, { validators: 42 }), + '"validators" option, if/when provided, MUST be a non-empty array of Validator instances' + ); +}); + +test('Observable.from - validators option - negative - empty array', () => { + assert.throws( + () => Observable.from({}, { validators: [] }), + '"validators" option, if/when provided, MUST be a non-empty array of Validator instances' + ); +}); + +test('Observable.from - validators option - negative - non-Validator element', () => { + assert.throws( + () => Observable.from({}, { validators: [Validator.custom(() => { }), 'bad'] }), + '"validators" option, if/when provided, MUST be a non-empty array of Validator instances' + ); +}); + +test('Observable.from - validators option - negative - bare function rejected', () => { + assert.throws( + () => Observable.from({}, { validators: [() => { }] }), + '"validators" option, if/when provided, MUST be a non-empty array of Validator instances' + ); +}); + +test('Observable.from - validators option - positive - stored on meta', () => { + const v1 = Validator.custom(() => { }); + const v2 = Validator.custom(() => { }); + const oo = Observable.from({}, { validators: [v1, v2] }); + const stored = oo[oMetaKey].validators; + assert.strictEqual(stored.length, 2); + assert.strictEqual(stored[0], v1); + assert.strictEqual(stored[1], v2); +}); + +// e2e: a single "immutable-marker" validator rejecting any change whose oldValue === 'immutable' +const immutableMarker = Validator.custom(changes => { + for (const c of changes) { + if (c.oldValue === 'immutable') { + throw new Error(`change at '${c.pathAsString}' rejected: oldValue is immutable`); + } + } +}); + +test('e2e - set insert - no oldValue - allowed', () => { + const oo = Observable.from({}, { validators: [immutableMarker] }); + let observed = 0; + Observable.observe(oo, changes => { observed += changes.length; }); + oo.a = 'new'; + assert.strictEqual(oo.a, 'new'); + assert.strictEqual(observed, 1); +}); + +test('e2e - set update - oldValue not immutable - allowed', () => { + const oo = Observable.from({ a: 'mutable' }, { validators: [immutableMarker] }); + let observed = 0; + Observable.observe(oo, changes => { observed += changes.length; }); + oo.a = 'other'; + assert.strictEqual(oo.a, 'other'); + assert.strictEqual(observed, 1); +}); + +test('e2e - set update - oldValue is immutable - throws, target unchanged, no observer fired', () => { + const oo = Observable.from({ a: 'immutable' }, { validators: [immutableMarker] }); + let observed = 0; + Observable.observe(oo, changes => { observed += changes.length; }); + assert.throws(() => { oo.a = 'other'; }, `change at 'a' rejected`); + assert.strictEqual(oo.a, 'immutable'); + assert.strictEqual(observed, 0); +}); + +test('e2e - delete - oldValue not immutable - allowed', () => { + const oo = Observable.from({ a: 'mutable' }, { validators: [immutableMarker] }); + let observed = 0; + Observable.observe(oo, changes => { observed += changes.length; }); + delete oo.a; + assert.isTrue(!('a' in oo)); + assert.strictEqual(observed, 1); +}); + +test('e2e - delete - oldValue is immutable - throws, target unchanged, no observer fired', () => { + const oo = Observable.from({ a: 'immutable' }, { validators: [immutableMarker] }); + let observed = 0; + Observable.observe(oo, changes => { observed += changes.length; }); + assert.throws(() => { delete oo.a; }, `change at 'a' rejected`); + assert.strictEqual(oo.a, 'immutable'); + assert.strictEqual(observed, 0); +}); + +test('e2e - change shape at validator - set insert', () => { + let seen = null; + const oo = Observable.from({}, { + validators: [Validator.custom(changes => { seen = changes; })] + }); + oo.a = 42; + assert.strictEqual(seen.length, 1); + const c = seen[0]; + assert.strictEqual(c.type, 'insert'); + assert.deepStrictEqual(c.path, ['a']); + assert.strictEqual(c.value, 42); + assert.strictEqual(c.oldValue, undefined); + assert.strictEqual(c.object, oo); +}); + +test('e2e - change shape at validator - set update', () => { + let seen = null; + const oo = Observable.from({ a: 1 }, { + validators: [Validator.custom(changes => { seen = changes; })] + }); + oo.a = 2; + assert.strictEqual(seen.length, 1); + const c = seen[0]; + assert.strictEqual(c.type, 'update'); + assert.deepStrictEqual(c.path, ['a']); + assert.strictEqual(c.value, 2); + assert.strictEqual(c.oldValue, 1); + assert.strictEqual(c.object, oo); +}); + +test('e2e - change shape at validator - delete', () => { + let seen = null; + const oo = Observable.from({ a: 1 }, { + validators: [Validator.custom(changes => { seen = changes; })] + }); + delete oo.a; + assert.strictEqual(seen.length, 1); + const c = seen[0]; + assert.strictEqual(c.type, 'delete'); + assert.deepStrictEqual(c.path, ['a']); + assert.strictEqual(c.value, undefined); + assert.strictEqual(c.oldValue, 1); + assert.strictEqual(c.object, oo); +}); + +test('e2e - rooted path - mutation on nested observable - validator sees full path', () => { + let seen = null; + const oo = Observable.from({ outer: { inner: { x: 1 } } }, { + validators: [Validator.custom(changes => { seen = changes; })] + }); + oo.outer.inner.x = 2; + assert.strictEqual(seen.length, 1); + assert.deepStrictEqual(seen[0].path, ['outer', 'inner', 'x']); +}); + +test('e2e - raw (un-wrapped) new value at validator', () => { + const raw = { k: 'v' }; + let seen = null; + const oo = Observable.from({}, { + validators: [Validator.custom(changes => { seen = changes; })] + }); + oo.a = raw; + assert.strictEqual(seen[0].value, raw); // exact same object identity, no proxy wrapping yet +}); + +test('e2e - multiple validators - first throws, second not called', () => { + let v2Calls = 0; + const v1 = Validator.custom(() => { throw new Error('v1 reject'); }); + const v2 = Validator.custom(() => { v2Calls++; }); + const oo = Observable.from({}, { validators: [v1, v2] }); + assert.throws(() => { oo.a = 1; }, 'v1 reject'); + assert.strictEqual(v2Calls, 0); +}); + +// vetoing validator used across the array-method e2e tests below +const vetoAll = Validator.custom(() => { throw new Error('rejected'); }); + +test('e2e - array pop - vetoed - target unchanged, no observer fired', () => { + const oo = Observable.from([1, 2, 3], { validators: [vetoAll] }); + let observed = 0; + Observable.observe(oo, changes => { observed += changes.length; }); + assert.throws(() => oo.pop(), 'rejected'); + assert.deepStrictEqual(Array.from(oo), [1, 2, 3]); + assert.strictEqual(observed, 0); +}); + +test('e2e - array push - vetoed - target unchanged, no observer fired', () => { + const oo = Observable.from([1, 2, 3], { validators: [vetoAll] }); + let observed = 0; + Observable.observe(oo, changes => { observed += changes.length; }); + assert.throws(() => oo.push(4, 5), 'rejected'); + assert.deepStrictEqual(Array.from(oo), [1, 2, 3]); + assert.strictEqual(observed, 0); +}); + +test('e2e - array shift - vetoed - target unchanged, no observer fired', () => { + const oo = Observable.from([1, 2, 3], { validators: [vetoAll] }); + let observed = 0; + Observable.observe(oo, changes => { observed += changes.length; }); + assert.throws(() => oo.shift(), 'rejected'); + assert.deepStrictEqual(Array.from(oo), [1, 2, 3]); + assert.strictEqual(observed, 0); +}); + +test('e2e - array unshift - vetoed - target unchanged, no observer fired', () => { + const oo = Observable.from([1, 2, 3], { validators: [vetoAll] }); + let observed = 0; + Observable.observe(oo, changes => { observed += changes.length; }); + assert.throws(() => oo.unshift(0), 'rejected'); + assert.deepStrictEqual(Array.from(oo), [1, 2, 3]); + assert.strictEqual(observed, 0); +}); + +test('e2e - array reverse - vetoed - target unchanged, no observer fired', () => { + const oo = Observable.from([1, 2, 3], { validators: [vetoAll] }); + let observed = 0; + Observable.observe(oo, changes => { observed += changes.length; }); + assert.throws(() => oo.reverse(), 'rejected'); + assert.deepStrictEqual(Array.from(oo), [1, 2, 3]); + assert.strictEqual(observed, 0); +}); + +test('e2e - array sort - vetoed - target unchanged, no observer fired', () => { + const oo = Observable.from([3, 1, 2], { validators: [vetoAll] }); + let observed = 0; + Observable.observe(oo, changes => { observed += changes.length; }); + assert.throws(() => oo.sort(), 'rejected'); + assert.deepStrictEqual(Array.from(oo), [3, 1, 2]); + assert.strictEqual(observed, 0); +}); + +test('e2e - typed-array set - vetoed - target unchanged, no observer fired', () => { + const oo = Observable.from(new Int32Array([1, 2, 3, 4]), { validators: [vetoAll] }); + let observed = 0; + Observable.observe(oo, changes => { observed += changes.length; }); + assert.throws(() => oo.set([10, 20], 1), 'rejected'); + assert.deepStrictEqual(oo, new Int32Array([1, 2, 3, 4])); + assert.strictEqual(observed, 0); +});