From 9e5f0b468be9d200d07b76a8e25625a37d30da57 Mon Sep 17 00:00:00 2001 From: Janne Savolainen Date: Fri, 1 Jul 2022 09:58:05 +0300 Subject: [PATCH] Introduce reactive now to kludge around global shared state in library Signed-off-by: Janne Savolainen --- .../utils/reactive-now/reactive-now.test.tsx | 70 +++++++++++++++++++ src/common/utils/reactive-now/reactive-now.ts | 60 ++++++++++++++++ 2 files changed, 130 insertions(+) create mode 100644 src/common/utils/reactive-now/reactive-now.test.tsx create mode 100644 src/common/utils/reactive-now/reactive-now.ts diff --git a/src/common/utils/reactive-now/reactive-now.test.tsx b/src/common/utils/reactive-now/reactive-now.test.tsx new file mode 100644 index 0000000000..d2deb951b7 --- /dev/null +++ b/src/common/utils/reactive-now/reactive-now.test.tsx @@ -0,0 +1,70 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import type { RenderResult } from "@testing-library/react"; +import { render } from "@testing-library/react"; +import type { IComputedValue } from "mobx"; +import { computed, observe } from "mobx"; +import React from "react"; +import { observer } from "mobx-react"; +import { advanceFakeTime, useFakeTime } from "../../test-utils/use-fake-time"; +import { reactiveNow } from "./reactive-now"; + +describe("reactiveNow", () => { + let someComputed: IComputedValue; + + beforeEach(() => { + useFakeTime("2015-10-21T07:28:00Z"); + + someComputed = computed(() => { + const currentTimestamp = reactiveNow(); + + return currentTimestamp > new Date("2015-10-21T07:28:00Z").getTime(); + }); + }); + + describe("react-context", () => { + let rendered: RenderResult; + + beforeEach(() => { + const TestComponent = observer( + ({ someComputed }: { someComputed: IComputedValue }) => ( +
{someComputed.get() ? "true" : "false"}
+ ), + ); + + rendered = render(); + }); + + it("given time passes, works", () => { + advanceFakeTime(1000); + + expect(rendered.container.textContent).toBe("true"); + }); + + it("does not share the state from previous test", () => { + expect(rendered.container.textContent).toBe("false"); + }); + }); + + describe("non-react-context", () => { + let actual: boolean; + + beforeEach(() => { + observe(someComputed, (changed) => { + actual = changed.newValue as boolean; + }, true); + }); + + it("given time passes, works", () => { + advanceFakeTime(1000); + + expect(actual).toBe(true); + }); + + it("does not share the state from previous test", () => { + expect(actual).toBe(false); + }); + }); +}); diff --git a/src/common/utils/reactive-now/reactive-now.ts b/src/common/utils/reactive-now/reactive-now.ts new file mode 100644 index 0000000000..febac37010 --- /dev/null +++ b/src/common/utils/reactive-now/reactive-now.ts @@ -0,0 +1,60 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { _isComputingDerivation } from "mobx"; +import type { IResource } from "mobx-utils"; +import { fromResource } from "mobx-utils"; + +// Note: This file is copy-pasted from mobx-utils to fix very specific issue. +// TODO: Remove this file once https://github.com/mobxjs/mobx-utils/issues/306 is fixed. +const tickers: Record> = {}; + +export function reactiveNow(interval?: number | "frame") { + if (interval === void 0) { interval = 1000; } + + if (!_isComputingDerivation()) { + // See #40 + return Date.now(); + } + + // Note: This is the kludge until https://github.com/mobxjs/mobx-utils/issues/306 is fixed + const synchronizationIsEnabled = !process.env.JEST_WORKER_ID; + + if (!tickers[interval] || !synchronizationIsEnabled) { + if (typeof interval === "number") + tickers[interval] = createIntervalTicker(interval); + else + tickers[interval] = createAnimationFrameTicker(); + } + + return tickers[interval].current(); +} + +function createIntervalTicker(interval: number) { + let subscriptionHandle: NodeJS.Timer; + + return fromResource(function (sink) { + sink(Date.now()); + subscriptionHandle = setInterval(function () { return sink(Date.now()); }, interval); + }, function () { + clearInterval(subscriptionHandle); + }, Date.now()); +} + +function createAnimationFrameTicker() { + const frameBasedTicker = fromResource(function (sink) { + sink(Date.now()); + + function scheduleTick() { + window.requestAnimationFrame(function () { + sink(Date.now()); + if (frameBasedTicker.isAlive()) + scheduleTick(); + }); + } + scheduleTick(); + }, function () { }, Date.now()); + + return frameBasedTicker; +}