From b93c79c53cb499b94d1bb8d2ccde3637fe2454af Mon Sep 17 00:00:00 2001 From: NullVoxPopuli Date: Sat, 11 Jun 2022 12:52:53 -0400 Subject: [PATCH 1/3] chore(internal): use composite TS project for better dev-time TS --- .gitignore | 1 + ember-resources/tsconfig.json | 4 ++++ testing/ember-app/tsconfig.json | 3 +++ 3 files changed, 8 insertions(+) diff --git a/.gitignore b/.gitignore index 948eac443..30f9e439d 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ # compiled output dist/ tmp/ +tsconfig.tsbuildinfo # dependencies node_modules/ diff --git a/ember-resources/tsconfig.json b/ember-resources/tsconfig.json index 56a82bfde..2bb9daf3f 100644 --- a/ember-resources/tsconfig.json +++ b/ember-resources/tsconfig.json @@ -1,10 +1,14 @@ { "compilerOptions": { + // So that the test app's type checking doesn't break when we change types + "composite": true, + // Path resolution "baseUrl": "./src", "moduleResolution": "node", // We only use tsc for type checking and declaration output "emitDeclarationOnly": false, + "declarationDir": "./dist", "declaration": true, "declarationMap": true, // Build settings diff --git a/testing/ember-app/tsconfig.json b/testing/ember-app/tsconfig.json index d35f6a47b..dbf69ba60 100644 --- a/testing/ember-app/tsconfig.json +++ b/testing/ember-app/tsconfig.json @@ -37,5 +37,8 @@ "app/**/*", "tests/**/*", "types/**/*" + ], + "references": [ + { "path": "../../ember-resources" } ] } From 4f84f830a057927d4f8cdcd0746c37c7ff3e319f Mon Sep 17 00:00:00 2001 From: NullVoxPopuli Date: Sat, 11 Jun 2022 12:53:15 -0400 Subject: [PATCH 2/3] chore(internal): fix top-level scripts --- package.json | 6 ++++-- pnpm-lock.yaml | 31 ++++++++++++++++++++++++++----- 2 files changed, 30 insertions(+), 7 deletions(-) diff --git a/package.json b/package.json index 06aa3c7a1..0b6ccd2aa 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ "@commitlint/cli": "^13.1.0", "@commitlint/config-conventional": "^13.1.0", "@nullvoxpopuli/eslint-configs": "^2.2.19", - "concurrency": "^0.1.4", + "concurrently": "^7.2.1", "eslint": "^7.32.0", "loader.js": "^4.7.0", "prettier": "^2.6.2", @@ -41,7 +41,9 @@ "mustache": "ember-cli -> testem -> consolidate -> mustache is ancient" }, "peerDependencyRules": { - "ignoreMissing": ["msw"], + "ignoreMissing": [ + "msw" + ], "allowedVersions": { "typescript": "*" } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1287a51d7..2d02df499 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -12,7 +12,7 @@ importers: '@commitlint/cli': ^13.1.0 '@commitlint/config-conventional': ^13.1.0 '@nullvoxpopuli/eslint-configs': ^2.2.19 - concurrency: ^0.1.4 + concurrently: ^7.2.1 eslint: ^7.32.0 loader.js: ^4.7.0 prettier: ^2.6.2 @@ -21,7 +21,7 @@ importers: '@commitlint/cli': 13.2.1 '@commitlint/config-conventional': 13.2.0 '@nullvoxpopuli/eslint-configs': 2.2.19_typescript@4.6.4 - concurrency: 0.1.4 + concurrently: 7.2.1 eslint: 7.32.0 loader.js: 4.7.0 prettier: 2.6.2 @@ -3263,7 +3263,7 @@ packages: lodash.get: 4.4.2 make-error: 1.3.6 ts-node: 9.1.1_typescript@4.6.4 - tslib: 2.3.1 + tslib: 2.4.0 transitivePeerDependencies: - typescript dev: true @@ -8114,8 +8114,20 @@ packages: typedarray: 0.0.6 dev: true - /concurrency/0.1.4: - resolution: {integrity: sha1-MrRZC1s5lBpNc52vXWoQCAws8x8=} + /concurrently/7.2.1: + resolution: {integrity: sha512-7cab/QyqipqghrVr9qZmoWbidu0nHsmxrpNqQ7r/67vfl1DWJElexehQnTH1p+87tDkihaAjM79xTZyBQh7HLw==} + engines: {node: ^12.20.0 || ^14.13.0 || >=16.0.0} + hasBin: true + dependencies: + chalk: 4.1.2 + date-fns: 2.28.0 + lodash: 4.17.21 + rxjs: 6.6.7 + shell-quote: 1.7.3 + spawn-command: 0.0.2-1 + supports-color: 8.1.1 + tree-kill: 1.2.2 + yargs: 17.3.1 dev: true /configstore/5.0.1: @@ -16649,6 +16661,10 @@ packages: resolution: {integrity: sha1-+30L0dcP1DFr2ePew4nmX51jYbs=} dev: true + /spawn-command/0.0.2-1: + resolution: {integrity: sha512-n98l9E2RMSJ9ON1AKisHzz7V42VDiBQGY6PB1BwRglz99wpVsSuGzQ+jOi6lFXBGVTCrRpltvjm+/XA+tpeJrg==} + dev: true + /spawn-error-forwarder/1.0.0: resolution: {integrity: sha1-Gv2Uc46ZmwNG17n8NzvlXgdXcCk=} dev: true @@ -17507,6 +17523,11 @@ packages: resolution: {integrity: sha1-y99WD9e5r2MlAv7UD5GMFX6pcTc=} dev: true + /tree-kill/1.2.2: + resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} + hasBin: true + dev: true + /tree-sync/1.4.0: resolution: {integrity: sha512-YvYllqh3qrR5TAYZZTXdspnIhlKAYezPYw11ntmweoceu4VK+keN356phHRIIo1d+RDmLpHZrUlmxga2gc9kSQ==} dependencies: From 357e266ee290c64ab1ba454f1318a9da1bb21fd2 Mon Sep 17 00:00:00 2001 From: NullVoxPopuli Date: Sun, 5 Jun 2022 12:00:42 -0400 Subject: [PATCH 3/3] feat(util): add debounce example util --- ember-resources/package.json | 4 ++ ember-resources/src/util/debounce.ts | 72 +++++++++++++++++++ .../ember-app/tests/utils/debounce/js-test.ts | 48 +++++++++++++ 3 files changed, 124 insertions(+) create mode 100644 ember-resources/src/util/debounce.ts create mode 100644 testing/ember-app/tests/utils/debounce/js-test.ts diff --git a/ember-resources/package.json b/ember-resources/package.json index 5e9e62cae..ae77600c7 100644 --- a/ember-resources/package.json +++ b/ember-resources/package.json @@ -14,6 +14,7 @@ "./util/map": "./dist/util/map.js", "./util/helper": "./dist/util/helper.js", "./util/remote-data": "./dist/util/remote-data.js", + "./util/debounce": "./dist/util/debounce.js", "./util/function": "./dist/util/function.js", "./util/function-resource": "./dist/util/function-resource.js", "./util/ember-concurrency": "./dist/util/ember-concurrency.js", @@ -42,6 +43,9 @@ "util/helper": [ "dist/util/helper.d.ts" ], + "util/debounce": [ + "dist/util/debounce.d.ts" + ], "util/remote-data": [ "dist/util/remote-data.d.ts" ], diff --git a/ember-resources/src/util/debounce.ts b/ember-resources/src/util/debounce.ts new file mode 100644 index 000000000..0f5c6c6b6 --- /dev/null +++ b/ember-resources/src/util/debounce.ts @@ -0,0 +1,72 @@ +import { tracked } from '@glimmer/tracking'; + +import { resource } from './function-resource'; + +class TrackedValue { + @tracked value: T | undefined; +} + +/** + * A utility for debouncing high-frequency updates. + * The returned value will only be updated every `ms` and is + * initially undefined. + * + * This can be useful when a user's typing is updating a tracked + * property and you want to derive data less frequently than on + * each keystroke. + * + * Note that this utility requires the @use decorator + * (debounce could be implemented without the need for the @use decorator + * but the current implementation is 8 lines) + * + * @example + * ```js + * import Component from '@glimmer/component'; + * import { tracked } from '@glimmer/tracking'; + * import { debounce } from 'ember-resources/util/debounce'; + * + * const delay = 100; // ms + * + * class Demo extends Component { + * @tracked userInput = ''; + * + * @use debouncedInput = debounce(delay, () => this.userInput); + * } + * ``` + * + * @example + * This could be further composed with [[RemoteData]] + * ```js + * import Component from '@glimmer/component'; + * import { tracked } from '@glimmer/tracking'; + * import { debounce } from 'ember-resources/util/debounce'; + * import { RemoteData } from 'ember-resources/util/remote-data'; + * + * const delay = 100; // ms + * + * class Demo extends Component { + * @tracked userInput = ''; + * + * @use debouncedInput = debounce(delay, () => this.userInput); + * + * @use search = RemoteData(() => `https://my.domain/search?q=${this.debouncedInput}`); + * } + * ``` + * + * @param {number} ms delay in milliseconds to wait before updating the returned value + * @param {() => Value} thunk function that returns the value to debounce + */ +export function debounce(ms: number, thunk: () => Value) { + let lastValue: Value; + let timer: number; + let state = new TrackedValue(); + + return resource(({ on }) => { + lastValue = thunk(); + + on.cleanup(() => timer && clearTimeout(timer)); + timer = setTimeout(() => (state.value = lastValue), ms); + + return state.value; + }); +} diff --git a/testing/ember-app/tests/utils/debounce/js-test.ts b/testing/ember-app/tests/utils/debounce/js-test.ts new file mode 100644 index 000000000..3eda6c146 --- /dev/null +++ b/testing/ember-app/tests/utils/debounce/js-test.ts @@ -0,0 +1,48 @@ +import { tracked } from '@glimmer/tracking'; +import { setOwner } from '@ember/application'; +import { module, test } from 'qunit'; +import { setupTest } from 'ember-qunit'; + +import { debounce } from 'ember-resources/util/debounce'; +import { use } from 'ember-resources/util/function-resource'; + +module('Utils | debounce | js', function (hooks) { + setupTest(hooks); + + let someTime = (ms = 25) => new Promise((resolve) => setTimeout(resolve, ms)); + + module('debounce', function () { + test('works with @use', async function (assert) { + class Test { + @tracked data = ''; + + @use text = debounce(100, () => this.data); + } + + let test = new Test(); + + setOwner(test, this.owner); + + assert.strictEqual(test.text, undefined); + + test.data = 'b'; + await someTime(); + assert.strictEqual(test.text, undefined); + test.data = 'bo'; + await someTime(); + assert.strictEqual(test.text, undefined); + test.data = 'boo'; + await someTime(); + assert.strictEqual(test.text, undefined); + + await someTime(110); + assert.strictEqual(test.text, 'boo'); + + test.data = 'boop'; + assert.strictEqual(test.text, 'boo'); + + await someTime(110); + assert.strictEqual(test.text, 'boop'); + }); + }); +});