tracker.test.ts 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745
  1. /**
  2. * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
  3. * SPDX-License-Identifier: MIT
  4. */
  5. import { test, expect, describe } from 'vitest';
  6. import { Tracker } from '../src';
  7. function expectTrue(value: any): void {
  8. expect(value).toEqual(true);
  9. }
  10. function expectFalse(value: any): void {
  11. expect(value).toEqual(false);
  12. }
  13. function expectEqual(v1: any, v2: any, msg?: string) {
  14. expect(v1).toEqual(v2);
  15. }
  16. function createPromiseDelegate(): { promise: Promise<void>; complete: () => void } {
  17. let complete: () => void;
  18. const promise = new Promise<void>(res => {
  19. complete = res;
  20. });
  21. return {
  22. promise,
  23. complete,
  24. };
  25. }
  26. function nextTick(v = 0): Promise<void> {
  27. return new Promise(res => setTimeout(res, v));
  28. }
  29. /**
  30. * fork from: https://github.com/meteor/meteor/blob/devel/packages/tracker/tracker_tests.js
  31. */
  32. describe('Tracker', () => {
  33. test('tracker - run', function () {
  34. var d = new Tracker.Dependency();
  35. var x = 0;
  36. var handle = Tracker.autorun(function () {
  37. d.depend();
  38. ++x;
  39. });
  40. // 默认会先执行一次
  41. expect(x).toEqual(1);
  42. Tracker.flush();
  43. expect(x).toEqual(1);
  44. d.changed();
  45. expect(x).toEqual(1);
  46. Tracker.flush();
  47. expect(x).toEqual(2);
  48. d.changed();
  49. expect(x).toEqual(2);
  50. Tracker.flush();
  51. expect(x).toEqual(3);
  52. d.changed();
  53. // Prevent the function from running further.
  54. handle.stop();
  55. Tracker.flush();
  56. expect(x).toEqual(3);
  57. d.changed();
  58. Tracker.flush();
  59. expect(x).toEqual(3);
  60. Tracker.autorun(function (internalHandle) {
  61. d.depend();
  62. ++x;
  63. if (x == 6) internalHandle.stop();
  64. });
  65. expect(x).toEqual(4);
  66. d.changed();
  67. Tracker.flush();
  68. expect(x).toEqual(5);
  69. d.changed();
  70. // Increment to 6 and stop.
  71. Tracker.flush();
  72. expect(x).toEqual(6);
  73. d.changed();
  74. Tracker.flush();
  75. // Still 6!
  76. expect(x).toEqual(6);
  77. });
  78. test('tracker - nested run', function () {
  79. var a = new Tracker.Dependency();
  80. var b = new Tracker.Dependency();
  81. var c = new Tracker.Dependency();
  82. var d = new Tracker.Dependency();
  83. var e = new Tracker.Dependency();
  84. var f = new Tracker.Dependency();
  85. var buf = '';
  86. Tracker.autorun(function () {
  87. a.depend();
  88. buf += 'a';
  89. Tracker.autorun(function () {
  90. b.depend();
  91. buf += 'b';
  92. Tracker.autorun(function () {
  93. c.depend();
  94. buf += 'c';
  95. var c2 = Tracker.autorun(function () {
  96. d.depend();
  97. buf += 'd';
  98. Tracker.autorun(function () {
  99. e.depend();
  100. buf += 'e';
  101. Tracker.autorun(function () {
  102. f.depend();
  103. buf += 'f';
  104. });
  105. });
  106. Tracker.onInvalidate(function () {
  107. // only run once
  108. c2.stop();
  109. });
  110. });
  111. });
  112. });
  113. Tracker.onInvalidate(function (c1) {
  114. c1.stop();
  115. });
  116. });
  117. const expectAndClear = function (str: string) {
  118. expect(buf).toEqual(str);
  119. buf = '';
  120. };
  121. expectAndClear('abcdef');
  122. expect(a.hasDependents()).toEqual(true);
  123. expect(b.hasDependents()).toEqual(true);
  124. expect(c.hasDependents()).toEqual(true);
  125. expect(d.hasDependents()).toEqual(true);
  126. expect(e.hasDependents()).toEqual(true);
  127. expect(f.hasDependents()).toEqual(true);
  128. b.changed();
  129. expectAndClear(''); // didn't flush yet
  130. Tracker.flush();
  131. expectAndClear('bcdef');
  132. c.changed();
  133. Tracker.flush();
  134. expectAndClear('cdef');
  135. var changeAndExpect = function (v, str) {
  136. v.changed();
  137. Tracker.flush();
  138. expectAndClear(str);
  139. };
  140. // should cause running
  141. changeAndExpect(e, 'ef');
  142. changeAndExpect(f, 'f');
  143. // invalidate inner context
  144. changeAndExpect(d, '');
  145. // no more running!
  146. changeAndExpect(e, '');
  147. changeAndExpect(f, '');
  148. expectTrue(a.hasDependents());
  149. expectTrue(b.hasDependents());
  150. expectTrue(c.hasDependents());
  151. expectFalse(d.hasDependents());
  152. expectFalse(e.hasDependents());
  153. expectFalse(f.hasDependents());
  154. // rerun C
  155. changeAndExpect(c, 'cdef');
  156. changeAndExpect(e, 'ef');
  157. changeAndExpect(f, 'f');
  158. // rerun B
  159. changeAndExpect(b, 'bcdef');
  160. changeAndExpect(e, 'ef');
  161. changeAndExpect(f, 'f');
  162. expectTrue(a.hasDependents());
  163. expectTrue(b.hasDependents());
  164. expectTrue(c.hasDependents());
  165. expectTrue(d.hasDependents());
  166. expectTrue(e.hasDependents());
  167. expectTrue(f.hasDependents());
  168. // kill A
  169. a.changed();
  170. changeAndExpect(f, '');
  171. changeAndExpect(e, '');
  172. changeAndExpect(d, '');
  173. changeAndExpect(c, '');
  174. changeAndExpect(b, '');
  175. changeAndExpect(a, '');
  176. expectFalse(a.hasDependents());
  177. expectFalse(b.hasDependents());
  178. expectFalse(c.hasDependents());
  179. expectFalse(d.hasDependents());
  180. expectFalse(e.hasDependents());
  181. expectFalse(f.hasDependents());
  182. });
  183. test('tracker - flush', function () {
  184. var buf = '';
  185. var c1 = Tracker.autorun(function (c) {
  186. buf += 'a';
  187. // invalidate first time
  188. if (c.firstRun) c.invalidate();
  189. });
  190. expectEqual(buf, 'a');
  191. Tracker.flush();
  192. expectEqual(buf, 'aa');
  193. Tracker.flush();
  194. expectEqual(buf, 'aa');
  195. c1.stop();
  196. Tracker.flush();
  197. expectEqual(buf, 'aa');
  198. //////
  199. buf = '';
  200. var c2 = Tracker.autorun(function (c) {
  201. buf += 'a';
  202. // invalidate first time
  203. if (c.firstRun) c.invalidate();
  204. Tracker.onInvalidate(function () {
  205. buf += '*';
  206. });
  207. });
  208. expectEqual(buf, 'a*');
  209. Tracker.flush();
  210. expectEqual(buf, 'a*a');
  211. c2.stop();
  212. expectEqual(buf, 'a*a*');
  213. Tracker.flush();
  214. expectEqual(buf, 'a*a*');
  215. /////
  216. // Can flush a different run from a run;
  217. // no current computation in afterFlush
  218. buf = '';
  219. var c3 = Tracker.autorun(function (c) {
  220. buf += 'a';
  221. // invalidate first time
  222. if (c.firstRun) c.invalidate();
  223. Tracker.afterFlush(function () {
  224. buf += Tracker.isActive() ? '1' : '0';
  225. });
  226. });
  227. Tracker.afterFlush(function () {
  228. buf += 'c';
  229. });
  230. var c4 = Tracker.autorun(function (c) {
  231. c4 = c;
  232. buf += 'b';
  233. });
  234. Tracker.flush();
  235. expectEqual(buf, 'aba0c0');
  236. c3.stop();
  237. c4.stop();
  238. Tracker.flush();
  239. // cases where flush throws
  240. var ran = false;
  241. Tracker.afterFlush(function (arg) {
  242. ran = true;
  243. expectEqual(typeof arg, 'undefined');
  244. expect(function () {
  245. Tracker.flush(); // illegal nested flush
  246. }).toThrowError();
  247. });
  248. Tracker.flush();
  249. expectTrue(ran);
  250. expect(function () {
  251. Tracker.autorun(function () {
  252. Tracker.flush(); // illegal to flush from a computation
  253. });
  254. }).toThrowError();
  255. expect(function () {
  256. Tracker.autorun(function () {
  257. Tracker.autorun(function () {});
  258. Tracker.flush();
  259. });
  260. }).toThrowError();
  261. });
  262. test('tracker - lifecycle', function () {
  263. expectFalse(Tracker.isActive());
  264. expectEqual(undefined, Tracker.getCurrentComputation());
  265. var runCount = 0;
  266. var firstRun = true;
  267. var buf = [];
  268. var cbId = 1;
  269. var makeCb = function () {
  270. var id = cbId++;
  271. return function () {
  272. buf.push(id);
  273. };
  274. };
  275. var shouldStop = false;
  276. var c1 = Tracker.autorun(function (c) {
  277. expectTrue(Tracker.isActive());
  278. expectEqual(c, Tracker.getCurrentComputation());
  279. expectEqual(c.stopped, false);
  280. expectEqual(c.invalidated, false);
  281. expectEqual(c.firstRun, firstRun);
  282. Tracker.onInvalidate(makeCb()); // 1, 6, ...
  283. Tracker.afterFlush(makeCb()); // 2, 7, ...
  284. Tracker.autorun(function (x) {
  285. x.stop();
  286. c.onInvalidate(makeCb()); // 3, 8, ...
  287. Tracker.onInvalidate(makeCb()); // 4, 9, ...
  288. Tracker.afterFlush(makeCb()); // 5, 10, ...
  289. });
  290. runCount++;
  291. if (shouldStop) c.stop();
  292. });
  293. firstRun = false;
  294. expectEqual(runCount, 1);
  295. expectEqual(buf, [4]);
  296. c1.invalidate();
  297. expectEqual(runCount, 1);
  298. expectEqual(c1.invalidated, true);
  299. expectEqual(c1.stopped, false);
  300. expectEqual(buf, [4, 1, 3]);
  301. Tracker.flush();
  302. expectEqual(runCount, 2);
  303. expectEqual(c1.invalidated, false);
  304. expectEqual(buf, [4, 1, 3, 9, 2, 5, 7, 10]);
  305. // test self-stop
  306. buf.length = 0;
  307. shouldStop = true;
  308. c1.invalidate();
  309. expectEqual(buf, [6, 8]);
  310. Tracker.flush();
  311. expectEqual(buf, [6, 8, 14, 11, 13, 12, 15]);
  312. });
  313. test('tracker - onInvalidate', function () {
  314. var buf = '';
  315. var c1 = Tracker.autorun(function () {
  316. buf += '*';
  317. });
  318. var append = function (
  319. x,
  320. expectedComputation?: Tracker.Computation,
  321. ): Tracker.IComputationCallback {
  322. return function (givenComputation) {
  323. expectFalse(Tracker.isActive());
  324. expectEqual(givenComputation, expectedComputation || c1);
  325. buf += x;
  326. };
  327. };
  328. c1.onStop(append('s'));
  329. c1.onInvalidate(append('a'));
  330. c1.onInvalidate(append('b'));
  331. expectEqual(buf, '*');
  332. Tracker.autorun(function (me) {
  333. Tracker.onInvalidate(append('z', me));
  334. me.stop();
  335. expectEqual(buf, '*z');
  336. c1.invalidate();
  337. });
  338. expectEqual(buf, '*zab');
  339. c1.onInvalidate(append('c'));
  340. c1.onInvalidate(append('d'));
  341. expectEqual(buf, '*zabcd');
  342. Tracker.flush();
  343. expectEqual(buf, '*zabcd*');
  344. // afterFlush ordering
  345. buf = '';
  346. c1.onInvalidate(append('a'));
  347. c1.onInvalidate(append('b'));
  348. Tracker.afterFlush(function () {
  349. append('x')(c1);
  350. c1.onInvalidate(append('c'));
  351. c1.invalidate();
  352. Tracker.afterFlush(function () {
  353. append('y')(c1);
  354. c1.onInvalidate(append('d'));
  355. c1.invalidate();
  356. });
  357. });
  358. Tracker.afterFlush(function () {
  359. append('z')(c1);
  360. c1.onInvalidate(append('e'));
  361. c1.invalidate();
  362. });
  363. expectEqual(buf, '');
  364. Tracker.flush();
  365. expectEqual(buf, 'xabc*ze*yd*');
  366. buf = '';
  367. c1.onInvalidate(append('m'));
  368. Tracker.flush();
  369. expectEqual(buf, '');
  370. c1.stop();
  371. expectEqual(buf, 'ms'); // s is from onStop
  372. Tracker.flush();
  373. expectEqual(buf, 'ms');
  374. c1.onStop(append('S'));
  375. expectEqual(buf, 'msS');
  376. });
  377. test('tracker - invalidate at flush time', function () {
  378. // Test this sentence of the docs: Functions are guaranteed to be
  379. // called at a time when there are no invalidated computations that
  380. // need rerunning.
  381. var buf = [];
  382. Tracker.afterFlush(function () {
  383. buf.push('C');
  384. });
  385. // When c1 is invalidated, it invalidates c2, then stops.
  386. var c1 = Tracker.autorun(function (c) {
  387. if (!c.firstRun) {
  388. buf.push('A');
  389. c2.invalidate();
  390. c.stop();
  391. }
  392. });
  393. var c2 = Tracker.autorun(function (c) {
  394. if (!c.firstRun) {
  395. buf.push('B');
  396. c.stop();
  397. }
  398. });
  399. // Invalidate c1. If all goes well, the re-running of
  400. // c2 should happen before the afterFlush.
  401. c1.invalidate();
  402. Tracker.flush();
  403. expectEqual(buf.join(''), 'ABC');
  404. });
  405. test('tracker - throwFirstError', function (test) {
  406. var d = new Tracker.Dependency();
  407. Tracker.autorun(function (c) {
  408. d.depend();
  409. if (!c.firstRun) throw new Error('foo');
  410. });
  411. d.changed();
  412. Tracker.flush();
  413. d.changed();
  414. expect(function () {
  415. Tracker.flush({ throwFirstError: true });
  416. }).toThrowError(/foo/);
  417. });
  418. test('tracker - no infinite recomputation', async function () {
  419. var reran = false;
  420. var c = Tracker.autorun(function (c) {
  421. if (!c.firstRun) reran = true;
  422. c.invalidate();
  423. });
  424. expectFalse(reran);
  425. await new Promise(res => {
  426. setTimeout(function () {
  427. c.stop();
  428. Tracker.afterFlush(function () {
  429. expectTrue(reran);
  430. expectTrue(c.stopped);
  431. res(null);
  432. });
  433. }, 100);
  434. });
  435. });
  436. test('tracker - Tracker.flush finishes', function () {
  437. // Currently, _runFlush will "yield" every 1000 computations... unless run in
  438. // Tracker.flush. So this test validates that Tracker.flush is capable of
  439. // running 2000 computations. Which isn't quite the same as infinity, but it's
  440. // getting there.
  441. var n = 0;
  442. var c = Tracker.autorun(function (c) {
  443. if (++n < 2000) {
  444. c.invalidate();
  445. }
  446. });
  447. expectEqual(n, 1);
  448. Tracker.flush();
  449. expectEqual(n, 2000);
  450. });
  451. //
  452. test('tracker - Tracker.autorun, onError option', async function (ctx) {
  453. var d = new Tracker.Dependency();
  454. const promiseDelegate = createPromiseDelegate();
  455. var c = Tracker.autorun(
  456. function (c) {
  457. d.depend();
  458. if (!c.firstRun) throw new Error('foo');
  459. },
  460. {
  461. onError: function (err) {
  462. expectEqual(err.message, 'foo');
  463. promiseDelegate.complete();
  464. },
  465. },
  466. );
  467. d.changed();
  468. Tracker.flush();
  469. await promiseDelegate.promise;
  470. });
  471. test('tracker - async function - basics', async function () {
  472. const promiseDelegate = createPromiseDelegate();
  473. const computation = Tracker.autorun(async function (computation) {
  474. expectEqual(computation.firstRun, true, 'before (firstRun)');
  475. expectEqual(Tracker.getCurrentComputation(), computation, 'before');
  476. const x = await Promise.resolve().then(() =>
  477. Tracker.withComputation(computation, () => {
  478. // The `firstRun` is `false` as soon as the first `await` happens.
  479. expectEqual(computation.firstRun, false, 'inside (firstRun)');
  480. expectEqual(Tracker.getCurrentComputation(), computation, 'inside');
  481. return 123;
  482. }),
  483. );
  484. expectEqual(x, 123, 'await (value)');
  485. expectEqual(computation.firstRun, false, 'await (firstRun)');
  486. Tracker.withComputation(computation, () => {
  487. expectEqual(Tracker.getCurrentComputation(), computation, 'await');
  488. });
  489. await new Promise(resolve => setTimeout(resolve, 10));
  490. Tracker.withComputation(computation, () => {
  491. expectEqual(computation.firstRun, false, 'sleep (firstRun)');
  492. expectEqual(Tracker.getCurrentComputation(), computation, 'sleep');
  493. });
  494. try {
  495. await Promise.reject('example');
  496. } catch (error) {
  497. Tracker.withComputation(computation, () => {
  498. expectEqual(error, 'example', 'catch (error)');
  499. expectEqual(computation.firstRun, false, 'catch (firstRun)');
  500. expectEqual(Tracker.getCurrentComputation(), computation, 'catch');
  501. });
  502. }
  503. promiseDelegate.complete();
  504. });
  505. expectEqual(Tracker.getCurrentComputation(), undefined, 'outside (computation)');
  506. // test.instanceOf(computation, Tracker.Computation, 'outside (result)');
  507. await promiseDelegate.promise;
  508. });
  509. test('tracker - async function - interleaved', async function () {
  510. let count = 0;
  511. const limit = 100;
  512. for (let index = 0; index < limit; ++index) {
  513. Tracker.autorun(async function (computation) {
  514. expectEqual(Tracker.getCurrentComputation(), computation, `before (${index})`);
  515. await new Promise(resolve => setTimeout(resolve, Math.random() * limit));
  516. count++;
  517. Tracker.withComputation(computation, () => {
  518. expectEqual(Tracker.getCurrentComputation(), computation, `after (${index})`);
  519. });
  520. });
  521. }
  522. expectEqual(count, 0, 'before resolve');
  523. await new Promise(resolve => setTimeout(resolve, limit));
  524. expectEqual(count, limit, 'after resolve');
  525. });
  526. test('tracker - async function - parallel', async function () {
  527. let resolvePromise;
  528. const promise = new Promise(resolve => {
  529. resolvePromise = resolve;
  530. });
  531. let count = 0;
  532. const limit = 100;
  533. const dependency = new Tracker.Dependency();
  534. for (let index = 0; index < limit; ++index) {
  535. Tracker.autorun(async function (computation) {
  536. count++;
  537. Tracker.withComputation(computation, () => {
  538. dependency.depend();
  539. });
  540. await promise;
  541. count--;
  542. });
  543. }
  544. expectEqual(count, limit, 'before');
  545. dependency.changed();
  546. await nextTick();
  547. expectEqual(count, limit * 2, 'changed');
  548. resolvePromise();
  549. await nextTick();
  550. expectEqual(count, 0, 'after');
  551. });
  552. test('tracker - async function - stepped', async function () {
  553. let resolvePromise;
  554. const promise = new Promise(resolve => {
  555. resolvePromise = resolve;
  556. });
  557. let count = 0;
  558. const limit = 100;
  559. for (let index = 0; index < limit; ++index) {
  560. Tracker.autorun(async function (computation) {
  561. expectEqual(Tracker.getCurrentComputation(), computation, `before (${index})`);
  562. await promise;
  563. count++;
  564. Tracker.withComputation(computation, () => {
  565. expectEqual(Tracker.getCurrentComputation(), computation, `after (${index})`);
  566. });
  567. });
  568. }
  569. expectEqual(count, 0, 'before resolve');
  570. resolvePromise();
  571. await nextTick();
  572. expectEqual(count, limit, 'after resolve');
  573. });
  574. test('tracker - async function - synchronize - firstRunPromise', async test => {
  575. let counter = 0;
  576. await Tracker.autorun(async () => {
  577. expectEqual(counter, 0);
  578. counter += 1;
  579. expectEqual(counter, 1);
  580. await new Promise(resolve => setTimeout(resolve));
  581. expectEqual(counter, 1);
  582. counter *= 2;
  583. expectEqual(counter, 2);
  584. }).result;
  585. await Tracker.autorun(async () => {
  586. expectEqual(counter, 2);
  587. counter += 1;
  588. expectEqual(counter, 3);
  589. await new Promise(resolve => setTimeout(resolve));
  590. expectEqual(counter, 3);
  591. counter *= 2;
  592. expectEqual(counter, 6);
  593. }).result;
  594. });
  595. test('computation - #flush', function () {
  596. var i = 0,
  597. j = 0,
  598. d = new Tracker.Dependency();
  599. var c1 = Tracker.autorun(function () {
  600. d.depend();
  601. i = i + 1;
  602. });
  603. var c2 = Tracker.autorun(function () {
  604. d.depend();
  605. j = j + 1;
  606. });
  607. expectEqual(i, 1);
  608. expectEqual(j, 1);
  609. d.changed();
  610. c1.flush();
  611. expectEqual(i, 2);
  612. expectEqual(j, 1);
  613. Tracker.flush();
  614. expectEqual(i, 2);
  615. expectEqual(j, 2);
  616. });
  617. test('computation - #run', function () {
  618. var i = 0,
  619. d = new Tracker.Dependency(),
  620. d2 = new Tracker.Dependency();
  621. var computation = Tracker.autorun(function (c) {
  622. d.depend();
  623. i = i + 1;
  624. //when #run() is called, this dependency should be picked up
  625. if (i >= 2 && i < 4) {
  626. d2.depend();
  627. }
  628. });
  629. expectEqual(i, 1);
  630. computation.run();
  631. expectEqual(i, 2);
  632. d.changed();
  633. Tracker.flush();
  634. expectEqual(i, 3);
  635. //we expect to depend on d2 at this point
  636. d2.changed();
  637. Tracker.flush();
  638. expectEqual(i, 4);
  639. //we no longer depend on d2, only d
  640. d2.changed();
  641. Tracker.flush();
  642. expectEqual(i, 4);
  643. d.changed();
  644. Tracker.flush();
  645. expectEqual(i, 5);
  646. });
  647. });