Skip to content
Source
Sign inSign up

longLivedAssetUrls.test.ts

100 lines3.3 KB

1import { EnvironmentId, type AssetResource } from "@t3tools/contracts";
2import { AsyncResult, Atom, AtomRegistry } from "effect/reactivity";
3import { describe, expect, it } from "vite-plus/test";
4​
5import { makeLongLivedAssetUrlFamily } from "./longLivedAssetUrls";
6​
7const environmentId = EnvironmentId.make("environment-1");
8const resource: AssetResource = {
9 _tag: "attachment",
10 attachmentId: "attachment-1",
11 fileName: "page.html",
12 mimeType: "text/html",
13 disposition: "inline",
14};
15​
16type MintResult = AsyncResult.AsyncResult<{ relativeUrl: string; expiresAt: number }>;
17​
18const url = (relativeUrl: string, expiresAt: number): MintResult =>
19 AsyncResult.success({ relativeUrl, expiresAt });
20​
21function setup(initial: MintResult) {
22 let mints = 0;
23 const tasks: Array<() => void> = [];
24 const registry = AtomRegistry.make({
25 scheduleTask: (task) => {
26 tasks.push(task);
27 return () => {};
28 },
29 });
30 const server = Atom.make(initial).pipe(Atom.keepAlive);
31 const query = Atom.readable(
32 (get) => get(server),
33 () => {
34 mints += 1;
35 registry.set(server, AsyncResult.waiting(registry.get(server)));
36 },
37 ).pipe(Atom.keepAlive);
38 const family = makeLongLivedAssetUrlFamily(() => query, { minLifeMs: 100, now: () => 1_000 });
39 const atom = family({ environmentId, input: { resource } });
40 let unmount = registry.mount(atom);
41 // Readers adopt any Success, waiting or not, as `assetUrlStateFromResult` does.
42 const relativeUrl = () => {
43 const value = registry.get(atom);
44 return value._tag === "Success" ? value.value.relativeUrl : null;
45 };
46 return {
47 relativeUrl,
48 mints: () => mints,
49 serve: (result: MintResult) => registry.set(server, result),
50 remount: () => {
51 unmount();
52 // Runs the idle-TTL disposal, so the next mount builds a fresh node.
53 for (const task of tasks.splice(0)) task();
54 unmount = registry.mount(atom);
55 },
56 };
58​
59describe("makeLongLivedAssetUrlFamily", () => {
60 it("reuses a cached URL with enough life left", () => {
61 const { relativeUrl, mints } = setup(url("/cached", 2_000));
62 expect(relativeUrl()).toBe("/cached");
63 expect(mints()).toBe(0);
64 });
65​
66 it("mints a URL near expiry and hides it until the new one arrives", () => {
67 const { relativeUrl, mints, serve } = setup(url("/stale", 1_050));
68 expect(mints()).toBe(1);
69 expect(relativeUrl()).toBeNull();
70​
71 serve(url("/fresh", 5_000));
72 expect(relativeUrl()).toBe("/fresh");
73 });
74​
75 it("does not hand a new reader the expiring URL while an earlier reader's mint is in flight", () => {
76 const { relativeUrl, mints, serve, remount } = setup(url("/stale", 1_050));
77 expect(mints()).toBe(1);
78 remount();
79 expect(relativeUrl()).toBeNull();
80​
81 serve(url("/fresh", 5_000));
82 expect(relativeUrl()).toBe("/fresh");
83 expect(mints()).toBe(1);
84 });
85​
86 it("mints at most once when the clock disagrees with the server", () => {
87 const { relativeUrl, mints, serve } = setup(url("/stale", 1_050));
88 serve(url("/still-short", 1_050));
89 expect(relativeUrl()).toBe("/still-short");
90 expect(mints()).toBe(1);
91 });
92​
93 it("waits for a URL still loading", () => {
94 const { relativeUrl, mints, serve } = setup(AsyncResult.initial(true));
95 expect(relativeUrl()).toBeNull();
96 serve(url("/loaded", 5_000));
97 expect(relativeUrl()).toBe("/loaded");
98 expect(mints()).toBe(0);
99 });
100});