Skip to content

Commit 34b5e42

Browse files
authored
Merge pull request #202 from val-town/avoid-localstorage
Replace localStorage access with a basic JSON file
2 parents 9296e05 + 6b67815 commit 34b5e42

File tree

5 files changed

+157
-89
lines changed

5 files changed

+157
-89
lines changed

src/cmd/upgrade.ts

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,9 @@
11
import { UpgradeCommand } from "@cliffy/command/upgrade";
22
import { JsrProvider } from "@cliffy/command/upgrade/provider/jsr";
3-
import {
4-
JSR_ENTRY_NAME,
5-
SAW_AS_LATEST_VERSION,
6-
VT_MINIMUM_FLAGS,
7-
} from "~/consts.ts";
3+
import { JSR_ENTRY_NAME, VT_MINIMUM_FLAGS } from "~/consts.ts";
84
import manifest from "../../deno.json" with { type: "json" };
95
import { colors } from "@cliffy/ansi/colors";
6+
import { vtCheckCache } from "~/vt/VTCheckCache.ts";
107

118
const provider = new JsrProvider({ package: JSR_ENTRY_NAME });
129

@@ -15,15 +12,19 @@ export async function registerOutdatedWarning() {
1512
const list = await provider.getVersions(JSR_ENTRY_NAME);
1613
const currentVersion = manifest.version;
1714
if (list.latest !== currentVersion) {
18-
const lastSawAsLatestVersion = localStorage.getItem(SAW_AS_LATEST_VERSION);
15+
const lastSawAsLatestVersion = await vtCheckCache
16+
.getLastSawAsLatestVersion();
1917
if (lastSawAsLatestVersion !== list.latest) {
20-
addEventListener("unload", () => { // The last thing logged
18+
addEventListener("unload", async () => {
19+
// The last thing logged
2120
if (Deno.args.includes("upgrade")) return; // Don't show when they are upgrading
2221

23-
localStorage.setItem(SAW_AS_LATEST_VERSION, currentVersion);
22+
await vtCheckCache.setLastSawAsLatestVersion(currentVersion);
2423
console.log(
2524
`A new version of vt is available: ${
26-
colors.bold(list.latest)
25+
colors.bold(
26+
list.latest,
27+
)
2728
}! Run \`${colors.bold("vt upgrade")}\` to update.`,
2829
);
2930
});

src/consts.ts

Lines changed: 31 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,9 @@ export const DEFAULT_BRANCH_NAME = "main";
88
export const PROGRAM_NAME = "vt";
99
export const API_KEY_KEY = "VAL_TOWN_API_KEY";
1010

11-
export const ALWAYS_IGNORE_PATTERNS: string[] = [
12-
".vt",
13-
".env",
14-
];
11+
export const ALWAYS_IGNORE_PATTERNS: string[] = [".vt", ".env"];
1512

16-
export const DEFAULT_IGNORE_PATTERNS: string[] = [
17-
"*~",
18-
"*.swp",
19-
".env",
20-
];
13+
export const DEFAULT_IGNORE_PATTERNS: string[] = ["*~", "*.swp", ".env"];
2114

2215
export const DEFAULT_VAL_PRIVACY = "public";
2316
export const META_STATE_FILE_NAME = "state.json";
@@ -26,6 +19,13 @@ export const META_FOLDER_NAME = ".vt";
2619
export const ENTRYPOINT_NAME = "vt.ts";
2720
export const META_IGNORE_FILE_NAME = ".vtignore";
2821
export const GLOBAL_VT_CONFIG_PATH = join(xdg.config(), PROGRAM_NAME);
22+
/** The directory that contains GLOBAL_VT_META_FILE_PATH */
23+
export const GLOBAL_VT_META_PATH = join(xdg.cache(), PROGRAM_NAME);
24+
export const GLOBAL_VT_META_FILE_PATH = join(
25+
xdg.cache(),
26+
PROGRAM_NAME,
27+
"upgrade-status.json",
28+
);
2929

3030
export const DEFAULT_WRAP_WIDTH = 80;
3131
export const MAX_WALK_UP_LEVELS = 100;
@@ -46,10 +46,10 @@ export const STATUS_STYLES: Record<
4646
};
4747

4848
export const WARNING_MESSAGES: Record<ItemWarning, string> = {
49-
"bad_name": "Invalid file name",
50-
"binary": "File has binary content",
51-
"empty": "File is empty",
52-
"too_large": "File is too large",
49+
bad_name: "Invalid file name",
50+
binary: "File has binary content",
51+
empty: "File is empty",
52+
too_large: "File is too large",
5353
};
5454

5555
export const DEFAULT_VAL_TYPE = "script";
@@ -66,21 +66,21 @@ export const ValItems = [
6666
export const JSON_INDENT_SPACES = 4;
6767

6868
export const ValItemColors: Record<ValItemType, (s: string) => string> = {
69-
"script": (s: string) => colors.rgb24(s, 0x4287f5),
70-
"http": (s: string) => colors.rgb24(s, 0x22c55e),
71-
"interval": (s: string) => colors.rgb24(s, 0xd946ef),
72-
"email": (s: string) => colors.rgb24(s, 0x8b5cf6),
73-
"file": (s: string) => colors.dim(s),
74-
"directory": (s: string) => colors.dim(s),
69+
script: (s: string) => colors.rgb24(s, 0x4287f5),
70+
http: (s: string) => colors.rgb24(s, 0x22c55e),
71+
interval: (s: string) => colors.rgb24(s, 0xd946ef),
72+
email: (s: string) => colors.rgb24(s, 0x8b5cf6),
73+
file: (s: string) => colors.dim(s),
74+
directory: (s: string) => colors.dim(s),
7575
};
7676

7777
export const TypeToTypeStr: Record<ValItemType, string> = {
78-
"script": "script",
79-
"http": "http",
80-
"email": "email",
81-
"interval": "cron",
82-
"file": "file",
83-
"directory": "directory",
78+
script: "script",
79+
http: "http",
80+
email: "email",
81+
interval: "cron",
82+
file: "file",
83+
directory: "directory",
8484
};
8585

8686
export const VAL_TOWN_VAL_URL_REGEX = /val\.town\/x\/([^\/]+)\/([^\/]+)/;
@@ -94,12 +94,12 @@ export const GET_API_KEY_URL = "https://www.val.town/settings/api";
9494
export const VT_README_URL =
9595
"https://github.com/val-town/vt/blob/main/README.md";
9696
export const TYPE_PRIORITY: Record<ValItemType, number> = {
97-
"script": 0,
98-
"email": 1,
99-
"http": 2,
100-
"directory": 3,
101-
"file": 4,
102-
"interval": 5,
97+
script: 0,
98+
email: 1,
99+
http: 2,
100+
directory: 3,
101+
file: 4,
102+
interval: 5,
103103
};
104104

105105
export const VAL_ITEM_NAME_REGEX = /^[a-zA-Z0-9\-_.]+$/;

src/vt/VTCheckCache.ts

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import type z from "zod";
2+
import {
3+
AUTH_CACHE_LOCALSTORE_ENTRY,
4+
GLOBAL_VT_META_FILE_PATH,
5+
GLOBAL_VT_META_PATH,
6+
SAW_AS_LATEST_VERSION,
7+
} from "../consts.ts";
8+
import { VTCheckCacheFile } from "~/vt/vt/schemas.ts";
9+
import { ensureDir } from "@std/fs";
10+
11+
/**
12+
* Cheap singleton that stores state about authentication and upgrade
13+
* checking in a JSON file in the XDG cache directory.
14+
*/
15+
class VTCheckCache {
16+
async #read() {
17+
await ensureDir(GLOBAL_VT_META_PATH);
18+
try {
19+
const text = await Deno.readTextFile(GLOBAL_VT_META_FILE_PATH);
20+
const json = JSON.parse(text);
21+
return VTCheckCacheFile.parse(json);
22+
} catch {
23+
return {};
24+
}
25+
}
26+
async getAuthChecked() {
27+
return (await this.#read())[AUTH_CACHE_LOCALSTORE_ENTRY];
28+
}
29+
async getLastSawAsLatestVersion() {
30+
return (await this.#read())[SAW_AS_LATEST_VERSION];
31+
}
32+
async setAuthCheckedToNow() {
33+
return await this.setItem(
34+
AUTH_CACHE_LOCALSTORE_ENTRY,
35+
new Date().toISOString(),
36+
);
37+
}
38+
async setLastSawAsLatestVersion(version: string) {
39+
return await this.setItem(SAW_AS_LATEST_VERSION, version);
40+
}
41+
async setItem(key: keyof z.infer<typeof VTCheckCacheFile>, value: string) {
42+
const before = await this.#read();
43+
await Deno.writeTextFile(
44+
GLOBAL_VT_META_FILE_PATH,
45+
JSON.stringify({
46+
...before,
47+
[key]: value,
48+
}),
49+
);
50+
}
51+
}
52+
53+
export const vtCheckCache = new VTCheckCache();

src/vt/vt/schemas.ts

Lines changed: 57 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
import { join } from "@std/path";
22
import { z } from "zod";
33
import {
4+
AUTH_CACHE_LOCALSTORE_ENTRY,
45
DEFAULT_EDITOR_TEMPLATE,
56
GLOBAL_VT_CONFIG_PATH,
67
META_IGNORE_FILE_NAME,
8+
SAW_AS_LATEST_VERSION,
79
} from "~/consts.ts";
810

911
/**
@@ -12,57 +14,68 @@ import {
1214
* Contains required metadata for operations that require context about the val
1315
* town directory you are in, like the Val that it represents.
1416
*/
15-
export const VTStateSchema = z.object({
16-
project: z.object({
17-
id: z.string().uuid(),
18-
}).optional(),
19-
val: z.object({
20-
id: z.string().uuid(),
21-
// Project -> Val migration: This lets old meta.jsons that have a project.id
22-
// still parse, and we transform them below. That means this ID is always
23-
// populated.
24-
}).catch({ id: "" }),
25-
branch: z.object({
26-
id: z.string().uuid(),
27-
version: z.number().gte(0),
28-
}),
29-
lastRun: z.object({
30-
pid: z.number().gte(0),
31-
time: z.string().refine((val) => !isNaN(Date.parse(val)), {}),
32-
}),
33-
}).transform((data) => {
34-
const result = { ...data };
35-
if (data.project) {
36-
result.val = structuredClone(data.project);
37-
delete result.project;
38-
}
39-
return result;
40-
}); // Silently inject the Val field, to prepare for future migration
17+
export const VTStateSchema = z
18+
.object({
19+
project: z
20+
.object({
21+
id: z.string().uuid(),
22+
})
23+
.optional(),
24+
val: z
25+
.object({
26+
id: z.string().uuid(),
27+
// Project -> Val migration: This lets old meta.jsons that have a project.id
28+
// still parse, and we transform them below. That means this ID is always
29+
// populated.
30+
})
31+
.catch({ id: "" }),
32+
branch: z.object({
33+
id: z.string().uuid(),
34+
version: z.number().gte(0),
35+
}),
36+
lastRun: z.object({
37+
pid: z.number().gte(0),
38+
time: z.string().refine((val) => !isNaN(Date.parse(val)), {}),
39+
}),
40+
})
41+
.transform((data) => {
42+
const result = { ...data };
43+
if (data.project) {
44+
result.val = structuredClone(data.project);
45+
delete result.project;
46+
}
47+
return result;
48+
}); // Silently inject the Val field, to prepare for future migration
4149

4250
/**
4351
* JSON schema for the config.yaml file for configuration storage.
4452
*/
4553
export const VTConfigSchema = z.object({
46-
apiKey: z.string()
54+
apiKey: z
55+
.string()
4756
.refine((val) => val === null || val.length === 32 || val.length === 33, {
4857
message: "API key must be 32-33 characters long when provided",
4958
})
5059
.nullable(),
51-
globalIgnoreFiles: z.preprocess(
52-
(input) => {
60+
globalIgnoreFiles: z
61+
.preprocess((input) => {
5362
if (typeof input === "string") {
54-
return input.split(",").map((s) => s.trim()).filter(Boolean);
63+
return input
64+
.split(",")
65+
.map((s) => s.trim())
66+
.filter(Boolean);
5567
}
5668
return input;
57-
},
58-
z.array(z.string()),
59-
).optional(),
60-
dangerousOperations: z.object({
61-
confirmation: z.union([
62-
z.boolean(),
63-
z.enum(["true", "false"]).transform((val) => val === "true"),
64-
]),
65-
}).optional(),
69+
}, z.array(z.string()))
70+
.optional(),
71+
dangerousOperations: z
72+
.object({
73+
confirmation: z.union([
74+
z.boolean(),
75+
z.enum(["true", "false"]).transform((val) => val === "true"),
76+
]),
77+
})
78+
.optional(),
6679
editorTemplate: z.string().optional(), // a Val URI
6780
});
6881

@@ -74,3 +87,8 @@ export const DefaultVTConfig: z.infer<typeof VTConfigSchema> = {
7487
},
7588
editorTemplate: DEFAULT_EDITOR_TEMPLATE,
7689
};
90+
91+
export const VTCheckCacheFile = z.object({
92+
[SAW_AS_LATEST_VERSION]: z.string().optional(),
93+
[AUTH_CACHE_LOCALSTORE_ENTRY]: z.coerce.date().optional(),
94+
});

vt.ts

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,24 +2,20 @@
22
import "@std/dotenv/load";
33
import { ensureGlobalVtConfig, globalConfig } from "~/vt/VTConfig.ts";
44
import { onboardFlow } from "~/cmd/flows/onboard.ts";
5-
import {
6-
API_KEY_KEY,
7-
AUTH_CACHE_LOCALSTORE_ENTRY,
8-
AUTH_CACHE_TTL,
9-
} from "~/consts.ts";
5+
import { API_KEY_KEY, AUTH_CACHE_TTL } from "~/consts.ts";
106
import { colors } from "@cliffy/ansi/colors";
117
import sdk from "~/sdk.ts";
128
import { registerOutdatedWarning } from "~/cmd/upgrade.ts";
9+
import { vtCheckCache } from "~/vt/VTCheckCache.ts";
1310

1411
await ensureGlobalVtConfig();
1512

1613
async function isApiKeyValid(): Promise<boolean> {
1714
// Since we run this on every invocation of vt, it makes sense to only check
1815
// if the api key is still valid every so often.
19-
20-
const lastAuthAt = localStorage.getItem(AUTH_CACHE_LOCALSTORE_ENTRY);
16+
const lastAuthAt = await vtCheckCache.getAuthChecked();
2117
const hoursSinceLastAuth = lastAuthAt
22-
? (new Date().getTime() - new Date(lastAuthAt).getTime())
18+
? new Date().getTime() - lastAuthAt.getTime()
2319
: Infinity;
2420
if (hoursSinceLastAuth < AUTH_CACHE_TTL) return true;
2521

@@ -31,15 +27,15 @@ async function isApiKeyValid(): Promise<boolean> {
3127
});
3228

3329
if (resp.ok) {
34-
localStorage.setItem(AUTH_CACHE_LOCALSTORE_ENTRY, new Date().toISOString());
30+
await vtCheckCache.setAuthCheckedToNow();
3531
return true;
3632
}
3733

3834
return resp.status !== 401;
3935
}
4036

4137
async function ensureValidApiKey() {
42-
if (Deno.env.has(API_KEY_KEY) && await isApiKeyValid()) return;
38+
if (Deno.env.has(API_KEY_KEY) && (await isApiKeyValid())) return;
4339

4440
{
4541
const { apiKey } = await globalConfig.loadConfig();

0 commit comments

Comments
 (0)