From 46ab4f0c4a49ea32e0ef827530a3bc15a2ea7e65 Mon Sep 17 00:00:00 2001 From: Jarek Radosz Date: Thu, 6 Jun 2024 15:59:20 +0200 Subject: [PATCH] DEV: Implement `DeferredTrackedSet` (#27372) For cases where you'd be using a TrackedSet to render something and then modifying that set throughout the same render cycle. (specifically it will be used in #27365) --- .../discourse/app/lib/tracked-tools.js | 56 ++++++++++++++ .../tests/unit/lib/tracked-tools-test.gjs | 73 ++++++++++++++++++- 2 files changed, 128 insertions(+), 1 deletion(-) diff --git a/app/assets/javascripts/discourse/app/lib/tracked-tools.js b/app/assets/javascripts/discourse/app/lib/tracked-tools.js index e711629025f..503c915982c 100644 --- a/app/assets/javascripts/discourse/app/lib/tracked-tools.js +++ b/app/assets/javascripts/discourse/app/lib/tracked-tools.js @@ -1,4 +1,6 @@ import { tracked } from "@glimmer/tracking"; +import { next } from "@ember/runloop"; +import { TrackedSet } from "@ember-compat/tracked-built-ins"; /** * Define a tracked property on an object without needing to use the @tracked decorator. @@ -120,3 +122,57 @@ export function dedupeTracked(target, key, desc) { }, }; } + +export class DeferredTrackedSet { + #set; + + constructor(value) { + this.#set = new TrackedSet(value); + } + + has(value) { + return this.#set.has(value); + } + + entries() { + return this.#set.entries(); + } + + keys() { + return this.#set.keys(); + } + + values() { + return this.#set.values(); + } + + forEach(fn) { + return this.#set.forEach(fn); + } + + get size() { + return this.#set.size; + } + + [Symbol.iterator]() { + return this.#set[Symbol.iterator](); + } + + get [Symbol.toStringTag]() { + return this.#set[Symbol.toStringTag]; + } + + add(value) { + next(() => this.#set.add(value)); + return this; + } + + delete(value) { + next(() => this.#set.delete(value)); + return this; + } + + clear() { + next(() => this.#set.clear()); + } +} diff --git a/app/assets/javascripts/discourse/tests/unit/lib/tracked-tools-test.gjs b/app/assets/javascripts/discourse/tests/unit/lib/tracked-tools-test.gjs index b20649e72e1..69d9a08d839 100644 --- a/app/assets/javascripts/discourse/tests/unit/lib/tracked-tools-test.gjs +++ b/app/assets/javascripts/discourse/tests/unit/lib/tracked-tools-test.gjs @@ -1,6 +1,8 @@ import { cached } from "@glimmer/tracking"; +import { run } from "@ember/runloop"; +import { settled } from "@ember/test-helpers"; import { module, test } from "qunit"; -import { dedupeTracked } from "discourse/lib/tracked-tools"; +import { dedupeTracked, DeferredTrackedSet } from "discourse/lib/tracked-tools"; module("Unit | tracked-tools", function () { test("@dedupeTracked", async function (assert) { @@ -45,4 +47,73 @@ module("Unit | tracked-tools", function () { "Initials getter re-evaluated" ); }); + + test("DeferredTrackedSet", async function (assert) { + class Player { + evaluationsCount = 0; + + letters = new DeferredTrackedSet(); + + @cached + get score() { + this.evaluationsCount++; + return this.letters.size; + } + } + + const player = new Player(); + assert.strictEqual(player.score, 0, "score is correct"); + assert.strictEqual(player.evaluationsCount, 1, "getter evaluated once"); + + run(() => { + player.letters.add("a"); + + assert.strictEqual(player.score, 0, "score does not change"); + assert.strictEqual( + player.evaluationsCount, + 1, + "getter does not evaluate" + ); + + player.letters.add("b"); + player.letters.add("c"); + + assert.strictEqual(player.score, 0, "score still does not change"); + assert.strictEqual( + player.evaluationsCount, + 1, + "getter still does not evaluate" + ); + }); + await settled(); + + assert.strictEqual(player.score, 3, "score is correct"); + assert.strictEqual(player.evaluationsCount, 2, "getter evaluated again"); + + run(() => { + player.letters.add("d"); + }); + await settled(); + + assert.strictEqual(player.score, 4, "score is correct"); + assert.strictEqual(player.evaluationsCount, 3, "getter evaluated again"); + + run(() => { + player.letters.add("e"); + + assert.strictEqual(player.score, 4, "score is correct"); + assert.strictEqual( + player.evaluationsCount, + 3, + "getter does not evaluate" + ); + + player.letters.add("f"); + }); + await settled(); + + assert.strictEqual(player.score, 6, "score is correct"); + assert.strictEqual(player.evaluationsCount, 4, "getter evaluated"); + assert.deepEqual([...player.letters], ["a", "b", "c", "d", "e", "f"]); + }); });