From 131ff3b47ee7ee03d00336850d8ed585e29f99d6 Mon Sep 17 00:00:00 2001 From: Yuri Guller Date: Sat, 15 Nov 2025 21:31:48 +0200 Subject: [PATCH 01/25] 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/25] 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/25] 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/25] 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/25] 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/25] 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/25] 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/25] 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/25] 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/25] 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/25] 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/25] 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/25] 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/25] 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/25] 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/25] 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/25] 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/25] 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/25] 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/25] 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/25] 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/25] 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/25] 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); +}); From db52bcc7b8c0033780fa5bd87aae42cad30a75b4 Mon Sep 17 00:00:00 2001 From: Yuri Guller Date: Sun, 26 Apr 2026 18:59:13 +0300 Subject: [PATCH 24/25] releasing 7 --- docs/architecture.md | 38 +++++++++---- docs/changelog.md | 3 ++ docs/v6-to-v7-migration-guide.md | 92 ++++++++++++++++++++++++++++++++ readme.md | 2 + 4 files changed, 126 insertions(+), 9 deletions(-) create mode 100644 docs/v6-to-v7-migration-guide.md diff --git a/docs/architecture.md b/docs/architecture.md index 7f0a594..bd8ad0d 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -1,12 +1,32 @@ # Architecture +At its core, `object-observer` takes a userland object graph and hands back a `Proxy`-wrapped clone (the `Observable`). From that point on, consumers interact **only with the observable**; the original target stays internal. Every mutation on the observable is intercepted by the `Proxy` traps, turned into one or more `Change` events, and dispatched to the registered observers. + ```mermaid -classDiagram - class ObservableBase { - - } - - ObservableBase <|-- ObservableObject - ObservableBase <|-- ObservableArray - ObservableBase <|-- ObservableTypedArray -``` \ No newline at end of file +flowchart LR + user["userland code"] + + subgraph src["input"] + target["plain object / array /
typed array (target)"] + end + + subgraph lib["object-observer"] + direction TB + proxy["Proxy (returned as Observable)"] + meta["oMeta
(target ref, parent,
observers, validators)"] + traps["Proxy traps
set / deleteProperty /
intrinsic methods"] + proxy -.- meta + proxy --> traps + end + + observers["observer callbacks"] + + user -- "Observable.from(target)" --> target + target -- "cloned & wrapped" --> proxy + proxy -- "returned to user" --> user + user -- "mutates the observable" --> proxy + traps -- "Change[] dispatch" --> observers + user -- "Observable.observe(obs, cb)" --> observers +``` + +The returned `Observable` is deep: nested objects are themselves observables, sharing the same dispatch pipeline. Mutations anywhere in the graph bubble up through the parent chain so root-level observers see the full rooted path. diff --git a/docs/changelog.md b/docs/changelog.md index c799346..f9ba95c 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -7,12 +7,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ## [7.0.0] +> See the [v6 → v7 migration guide](v6-to-v7-migration-guide.md) for the full list of breaks and before/after examples. + ### 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')] }` +- `Change` object fields (`type`, `path`, `value`, `oldValue`, `object`) are now read-only getters (were writable public fields in v6); reading is unaffected, writing throws in strict mode ### Added - [Issue no. 149](https://github.com/gullerya/object-observer/issues/149) - added verifiers to prevent unallowed changes - `Filter.directChildrenOf(path)` factory — covers the previous `pathsOf` semantics, with a fixed sibling-prefix bug (`directChildrenOf('inner')` no longer matches `innerX.foo`) diff --git a/docs/v6-to-v7-migration-guide.md b/docs/v6-to-v7-migration-guide.md new file mode 100644 index 0000000..cfe0b9c --- /dev/null +++ b/docs/v6-to-v7-migration-guide.md @@ -0,0 +1,92 @@ +# Migrating from v6 to v7 + +`object-observer` v7 is a major release. The core API — `Observable.from`, `Observable.observe`, `Observable.unobserve`, `Observable.isObservable`, `ObjectObserver` — keeps its shape and semantics. The change event model (`type`, `path`, `value`, `oldValue`, `object`) is unchanged. + +The only API break is the **observation options** shape: the old path-based options have been replaced with a single `filters` array of `Filter` instances. A handful of smaller, mostly incidental adjustments are listed at the end for completeness. + +## Observation options — path-based options replaced by `filters` + +### What changed + +In v6, `Observable.observe(observable, callback, options)` accepted one of three mutually exclusive path options: + +| v6 option | Meaning | +|--------------|------------------------------------------------------| +| `path` | observe one exact path | +| `pathsOf` | observe direct children of a path | +| `pathsFrom` | observe a path and everything below it | + +In v7 these three options are **removed**. In their place, `options.filters` accepts a **non-empty array of `Filter` instances**. When multiple filters are provided they compose as logical AND — each narrows the output of the previous one. `Filter` is a new named export; instances are created via static factory methods (the constructor is private). + +### Mapping table + +| v6 | v7 | +|-----------------------------------------------|----------------------------------------------------------------------| +| `{ path: 'a.b' }` | `{ filters: [Filter.exactPaths(['a.b'])] }` | +| `{ path: ['a.b', 'a.c'] }` *(if ever used)* | `{ filters: [Filter.exactPaths(['a.b', 'a.c'])] }` | +| `{ pathsOf: 'a' }` | `{ filters: [Filter.directChildrenOf('a')] }` | +| `{ pathsOf: '' }` | `{ filters: [Filter.directChildrenOf('')] }` *(root direct children)* | +| `{ pathsFrom: 'a' }` | `{ filters: [Filter.pathsStartWith('a')] }` | +| *(no v6 equivalent)* | `{ filters: [Filter.custom(cs => cs.filter(...))] }` | + +### Before / after + +```javascript +// v6 +import { Observable } from '@gullerya/object-observer'; + +Observable.observe(obs, cb, { path: 'address.city' }); +Observable.observe(obs, cb, { pathsOf: 'address' }); +Observable.observe(obs, cb, { pathsFrom: 'address' }); +``` + +```javascript +// v7 +import { Observable, Filter } from '@gullerya/object-observer'; + +Observable.observe(obs, cb, { filters: [Filter.exactPaths(['address.city'])] }); +Observable.observe(obs, cb, { filters: [Filter.directChildrenOf('address')] }); +Observable.observe(obs, cb, { filters: [Filter.pathsStartWith('address')] }); +``` + +### Behavior notes + +- **`directChildrenOf` fixes a v6 sibling-prefix bug.** In v6, `pathsOf: 'inner'` could incorrectly match changes under a sibling named `innerX` (see [issue #93](https://github.com/gullerya/object-observer/issues/93) for the original fix attempt). `Filter.directChildrenOf('inner')` in v7 matches only true direct children of `'inner'`. If your v6 code relied on the buggy behavior, update the expected path. +- **`REVERSE` / `SHUFFLE` at the target path are still delivered by `directChildrenOf`**, matching v6 `pathsOf` semantics — they are semantically mutations of the container's children. +- **Multiple filters compose as AND.** There was no way to combine path options in v6; in v7 you can stack filters to narrow further. +- **Bare functions are rejected.** `{ filters: [(cs) => cs] }` throws. Wrap with `Filter.custom(fn)`. +- **Unknown options still throw, as in v6.** `Observable.observe` continues to fail-fast on unrecognized option keys — only the valid key name changed. + +See [docs/filters.md](filters.md) for the full `Filter` API. + +## Other breaking changes + +### `Change` objects are read-only + +In v6 the `Change` class exposed its fields as plain writable properties. In v7, `type`, `path`, `value`, `oldValue`, and `object` are read-only getters backed by private fields. Code that **read** change properties is unaffected. Code that **wrote** to change objects — e.g. `change.type = 'custom'` or attaching ad-hoc properties to a change — will throw in strict mode (and silently fail in sloppy mode). This is very rarely done in practice; if you were using change objects as a scratch space, copy the fields into your own object instead. + +A new `pathAsString` getter is available (lazy, dot-joined). Additive, not breaking. + +### Runtime and browser matrix + +The officially supported matrix has been refreshed: + +- Node.js: **24.15.0+** (aligned with current LTS). +- Browsers: **last 2 versions** of Chrome, Firefox, Edge, and Safari. + +This is the matrix the library is actively **tested against** — it is not a hard runtime requirement. The library does not use any v7-specific new language or platform features, so older Node.js versions and older browsers will most likely continue to work just fine. We simply don't run CI against them anymore, so regressions there won't be caught by us. + +## What did **not** change + +- `Observable.from`, `Observable.isObservable`, `Observable.observe`, `Observable.unobserve` signatures. +- `ObjectObserver` class and its `observe` / `unobserve` / `disconnect` methods. +- The `Change` shape (`type`, `path`, `value`, `oldValue`, `object`) and the set of change types (`insert`, `update`, `delete`, `shuffle`, `reverse`). +- Synchronous default delivery and the opt-in `async: true` option on `Observable.from`. +- The set of observed mutations on plain objects, arrays, and typed arrays. +- Cross-instance operability. + +## New in v7 (non-breaking, for awareness) + +- **`Filter` is exported**, with static factories: `exactPaths`, `pathsStartWith`, `directChildrenOf`, `custom`. +- **`Validator` is exported**, with a `Validator.custom(fn)` factory. Validators are passed via `Observable.from(target, { validators: [...] })` and run **before** a mutation is applied — throwing from a validator vetoes the mutation (the target is not modified and observers are not invoked). See the `Validators` section in the readme for details. + > In 7.0 validators do not yet run for `Array.prototype.splice`, `Array.prototype.fill`, and `Array.prototype.copyWithin` (and their `TypedArray` counterparts where applicable). Support is planned for a subsequent release. diff --git a/readme.md b/readme.md index 1602404..458df30 100644 --- a/readme.md +++ b/readme.md @@ -9,6 +9,8 @@ __`object-observer`__ provides a deep observation of a changes performed on an object/array graph. +For a high-level overview of how the library is put together see the [architecture](docs/architecture.md) doc. + Main aspects and features: - implemented via native __Proxy__ (revokable) - observation is 'deep', yielding changes from a __sub-graphs__ too From 8b1022abfac01f99065ed1a1d65ec60e389b44e6 Mon Sep 17 00:00:00 2001 From: Yuri Guller Date: Sun, 26 Apr 2026 19:23:33 +0300 Subject: [PATCH 25/25] pre-release --- docs/changelog.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/changelog.md b/docs/changelog.md index f9ba95c..a17e714 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -6,7 +6,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] -## [7.0.0] + +## [7.0.0] - 2026-04-26 > See the [v6 → v7 migration guide](v6-to-v7-migration-guide.md) for the full list of breaks and before/after examples. ### BREAKING CHANGE