dsh-claude-style 0.17.0 → 0.18.0-dev.35
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +88 -0
- package/LICENSE +11 -3
- package/NOTICE.md +22 -0
- package/README.en.md +10 -3
- package/README.md +10 -3
- package/THIRD_PARTY_NOTICES.md +5 -1
- package/docs/STYLE.md +33 -4
- package/lib/assets/6f75a6052515.js.br +0 -0
- package/lib/assets/d5aa963bf99b.js.br +0 -0
- package/lib/assets/manifest.json +8 -8
- package/lib/client.js +15 -13
- package/lib/fonts/OFL.txt +8 -0
- package/lib/host/brand.js +2 -0
- package/lib/host/json-document.js +7 -0
- package/lib/host/peakrate-catalog.js +1 -3
- package/lib/host/peakrate.js +26 -16
- package/lib/host/prices-document.js +194 -0
- package/lib/host/prices.js +153 -0
- package/lib/host/routes.js +35 -1
- package/lib/host/session-cost.js +306 -0
- package/lib/host/settings.js +13 -0
- package/lib/host/usage-fold.js +3 -1
- package/lib/model-descriptions.json +1 -1
- package/lib/peakrate-catalog.json +192 -52
- package/lib/prices.json +2202 -0
- package/package.json +9 -7
- package/packages/client/src/features/sodium/LICENSE +174 -0
- package/lib/assets/812ff61f28cc.js.br +0 -0
- package/lib/assets/94873ed8d52d.js.br +0 -0
- package/lib/fonts/AUTHORS.txt +0 -10
- package/lib/fonts/OFL-Inter.txt +0 -93
- package/lib/fonts/OFL-NotoSerif.txt +0 -93
package/lib/fonts/OFL.txt
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
Copyright 2020 The JetBrains Mono Project Authors (https://github.com/JetBrains/JetBrainsMono)
|
|
2
|
+
Copyright 2020 The Inter Project Authors (https://github.com/rsms/inter)
|
|
3
|
+
Copyright 2022 The Noto Project Authors (https://github.com/notofonts/latin-greek-cyrillic)
|
|
2
4
|
|
|
3
5
|
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
|
4
6
|
This license is copied below, and is also available with a FAQ at:
|
|
@@ -91,3 +93,9 @@ INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
|
|
91
93
|
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
92
94
|
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
|
93
95
|
OTHER DEALINGS IN THE FONT SOFTWARE.
|
|
96
|
+
|
|
97
|
+
Authors of JetBrains Mono, from the AUTHORS file it ships beside this licence (copyright purposes):
|
|
98
|
+
|
|
99
|
+
JetBrains <>
|
|
100
|
+
Philipp Nurullin <philipp.nurullin@jetbrains.com>
|
|
101
|
+
Konstantin Bulenkov <kb@jetbrains.com>
|
package/lib/host/brand.js
CHANGED
|
@@ -19,6 +19,8 @@ var BRAND_PATH = `${ROUTE_PREFIX}/brand`;
|
|
|
19
19
|
var BRAND_MARK_PATH = `${ROUTE_PREFIX}/brand/mark`;
|
|
20
20
|
var BRAND_WORDMARK_PATH = `${ROUTE_PREFIX}/brand/wordmark`;
|
|
21
21
|
var PEAKRATE_PATH = `${ROUTE_PREFIX}/peakrate`;
|
|
22
|
+
var PRICES_PATH = `${ROUTE_PREFIX}/prices`;
|
|
23
|
+
var SESSION_COST_PATH = `${ROUTE_PREFIX}/session-cost`;
|
|
22
24
|
|
|
23
25
|
// packages/host/src/brand.ts
|
|
24
26
|
import { mkdirSync, readFileSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
@@ -23,10 +23,8 @@ function isIsoInstant(value) {
|
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
// packages/host/src/peakrate-catalog.ts
|
|
26
|
+
import { asRecord } from "./json-document.js";
|
|
26
27
|
var SCHEMA_VERSION = 1;
|
|
27
|
-
function asRecord(value) {
|
|
28
|
-
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
29
|
-
}
|
|
30
28
|
function asText(value) {
|
|
31
29
|
return typeof value === "string" && value !== "" ? value : void 0;
|
|
32
30
|
}
|
package/lib/host/peakrate.js
CHANGED
|
@@ -8,7 +8,10 @@ import { cachePath, readJsonDocument, writeJsonDocument } from "./cache-file.js"
|
|
|
8
8
|
import { packageRoot } from "./package-root.js";
|
|
9
9
|
import { parseCatalog } from "./peakrate-catalog.js";
|
|
10
10
|
var PEAKRATE_CATALOG_FILE = "peakrate-catalog.json";
|
|
11
|
-
var
|
|
11
|
+
var PEAKRATE_CATALOG_URLS = [
|
|
12
|
+
"https://cdn.jsdelivr.net/gh/Nwflower/dsh-claude-style@master/packages/host/data/peakrate-catalog.json",
|
|
13
|
+
"https://raw.githubusercontent.com/Nwflower/dsh-claude-style/master/packages/host/data/peakrate-catalog.json"
|
|
14
|
+
];
|
|
12
15
|
var PEAKRATE_REFRESH_MS = 24 * 60 * 60 * 1e3;
|
|
13
16
|
var PEAKRATE_IDLE_MS = 48 * 60 * 60 * 1e3;
|
|
14
17
|
var FETCH_TIMEOUT_MS = 1e4;
|
|
@@ -58,23 +61,30 @@ function createPeakRate(ctx) {
|
|
|
58
61
|
}
|
|
59
62
|
async function refresh() {
|
|
60
63
|
let raw;
|
|
61
|
-
|
|
62
|
-
const controller = new AbortController();
|
|
63
|
-
inFlight = controller;
|
|
64
|
-
const timeout = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
|
|
64
|
+
for (const url of PEAKRATE_CATALOG_URLS) {
|
|
65
65
|
try {
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
66
|
+
const controller = new AbortController();
|
|
67
|
+
inFlight = controller;
|
|
68
|
+
const timeout = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
|
|
69
|
+
try {
|
|
70
|
+
const response = await fetch(url, { signal: controller.signal });
|
|
71
|
+
if (!response.ok) {
|
|
72
|
+
warn(`${PACKAGE_NAME}: peak rate source ${url} answered ${response.status}`);
|
|
73
|
+
raw = void 0;
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
raw = await response.json();
|
|
77
|
+
} finally {
|
|
78
|
+
clearTimeout(timeout);
|
|
79
|
+
if (inFlight === controller) inFlight = null;
|
|
70
80
|
}
|
|
71
|
-
raw
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
if (inFlight === controller) inFlight = null;
|
|
81
|
+
if (raw !== void 0) break;
|
|
82
|
+
} catch (error) {
|
|
83
|
+
warn(`${PACKAGE_NAME}: peak rate source ${url} unreachable: ${error.message}`);
|
|
75
84
|
}
|
|
76
|
-
}
|
|
77
|
-
|
|
85
|
+
}
|
|
86
|
+
if (raw === void 0) {
|
|
87
|
+
warn(`${PACKAGE_NAME}: no peak rate source answered; keeping the current catalog`);
|
|
78
88
|
return false;
|
|
79
89
|
}
|
|
80
90
|
const parsed = parseCatalog(raw);
|
|
@@ -134,7 +144,7 @@ function createPeakRate(ctx) {
|
|
|
134
144
|
}
|
|
135
145
|
export {
|
|
136
146
|
PEAKRATE_CATALOG_FILE,
|
|
137
|
-
|
|
147
|
+
PEAKRATE_CATALOG_URLS,
|
|
138
148
|
PEAKRATE_IDLE_MS,
|
|
139
149
|
PEAKRATE_REFRESH_MS,
|
|
140
150
|
createPeakRate
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
// packages/contracts/src/prices.ts
|
|
2
|
+
var CURRENCY_CNY = "CNY";
|
|
3
|
+
var CURRENCY_USD = "USD";
|
|
4
|
+
var CURRENCIES = [CURRENCY_CNY, CURRENCY_USD];
|
|
5
|
+
var CURRENCY_SYMBOL = {
|
|
6
|
+
[CURRENCY_CNY]: "\xA5",
|
|
7
|
+
[CURRENCY_USD]: "$"
|
|
8
|
+
};
|
|
9
|
+
var PRICE_SCHEMA_VERSION = 1;
|
|
10
|
+
var WEEKDAYS = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
|
|
11
|
+
var formatters = /* @__PURE__ */ new Map();
|
|
12
|
+
function formatter(timeZone) {
|
|
13
|
+
let held = formatters.get(timeZone);
|
|
14
|
+
if (held === void 0) {
|
|
15
|
+
held = new Intl.DateTimeFormat("en-CA", {
|
|
16
|
+
timeZone,
|
|
17
|
+
weekday: "short",
|
|
18
|
+
year: "numeric",
|
|
19
|
+
month: "2-digit",
|
|
20
|
+
day: "2-digit",
|
|
21
|
+
hour: "2-digit",
|
|
22
|
+
minute: "2-digit",
|
|
23
|
+
hourCycle: "h23"
|
|
24
|
+
});
|
|
25
|
+
formatters.set(timeZone, held);
|
|
26
|
+
}
|
|
27
|
+
return held;
|
|
28
|
+
}
|
|
29
|
+
function clockAt(atMs, timeZone) {
|
|
30
|
+
if (!Number.isFinite(atMs)) return null;
|
|
31
|
+
let parts;
|
|
32
|
+
try {
|
|
33
|
+
parts = formatter(timeZone).formatToParts(new Date(atMs));
|
|
34
|
+
} catch {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
const field = (type) => parts.find((part) => part.type === type)?.value ?? "";
|
|
38
|
+
const weekday = WEEKDAYS.indexOf(field("weekday"));
|
|
39
|
+
const year = Number(field("year"));
|
|
40
|
+
const month = Number(field("month"));
|
|
41
|
+
const day = Number(field("day"));
|
|
42
|
+
const hour = Number(field("hour"));
|
|
43
|
+
const minute = Number(field("minute"));
|
|
44
|
+
if (weekday < 0 || !Number.isFinite(year) || !Number.isFinite(month) || !Number.isFinite(day) || !Number.isFinite(hour) || !Number.isFinite(minute)) return null;
|
|
45
|
+
const pad = (value) => String(value).padStart(2, "0");
|
|
46
|
+
return { weekday, date: `${year}-${pad(month)}-${pad(day)}`, minutes: hour * 60 + minute };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// packages/host/src/prices-document.ts
|
|
50
|
+
import { asRecord } from "./json-document.js";
|
|
51
|
+
function asText(value) {
|
|
52
|
+
return typeof value === "string" && value.trim() !== "" ? value.trim() : void 0;
|
|
53
|
+
}
|
|
54
|
+
function asRate(value) {
|
|
55
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : void 0;
|
|
56
|
+
}
|
|
57
|
+
function asDate(value) {
|
|
58
|
+
if (typeof value !== "string" || !/^\d{4}-\d{2}-\d{2}$/.test(value)) return void 0;
|
|
59
|
+
const [year, month, day] = value.split("-").map(Number);
|
|
60
|
+
const at = new Date(Date.UTC(year, month - 1, day));
|
|
61
|
+
return at.getUTCFullYear() === year && at.getUTCMonth() === month - 1 && at.getUTCDate() === day ? value : void 0;
|
|
62
|
+
}
|
|
63
|
+
function asClock(value) {
|
|
64
|
+
const text = asText(value);
|
|
65
|
+
if (text === void 0 || !/^([01]?\d|2[0-3]):[0-5]\d$/.test(text)) return void 0;
|
|
66
|
+
return text;
|
|
67
|
+
}
|
|
68
|
+
function asTier(value) {
|
|
69
|
+
const raw = asRecord(value);
|
|
70
|
+
if (raw === null) return null;
|
|
71
|
+
const cacheHit = asRate(raw.cacheHit);
|
|
72
|
+
const cacheMiss = asRate(raw.cacheMiss);
|
|
73
|
+
const output = asRate(raw.output);
|
|
74
|
+
if (cacheHit === void 0 || cacheMiss === void 0 || output === void 0) return null;
|
|
75
|
+
return { cacheHit, cacheMiss, output };
|
|
76
|
+
}
|
|
77
|
+
function asPrice(value) {
|
|
78
|
+
const raw = asRecord(value);
|
|
79
|
+
if (raw === null) return null;
|
|
80
|
+
const offPeak = asTier(raw.offPeak);
|
|
81
|
+
const peak = asTier(raw.peak);
|
|
82
|
+
if (offPeak === null || peak === null) return null;
|
|
83
|
+
for (const key of ["cacheHit", "cacheMiss", "output"]) {
|
|
84
|
+
if (peak[key] < offPeak[key]) return null;
|
|
85
|
+
}
|
|
86
|
+
return { offPeak, peak };
|
|
87
|
+
}
|
|
88
|
+
function asWindows(value) {
|
|
89
|
+
if (!Array.isArray(value) || value.length === 0) return null;
|
|
90
|
+
const windows = [];
|
|
91
|
+
for (const entry of value) {
|
|
92
|
+
const raw = asRecord(entry);
|
|
93
|
+
if (raw === null) continue;
|
|
94
|
+
const start = asClock(raw.start);
|
|
95
|
+
const end = asClock(raw.end);
|
|
96
|
+
if (start === void 0 || end === void 0) continue;
|
|
97
|
+
windows.push({ start, end });
|
|
98
|
+
}
|
|
99
|
+
return windows.length > 0 ? windows : null;
|
|
100
|
+
}
|
|
101
|
+
function asSchedule(value) {
|
|
102
|
+
const raw = asRecord(value);
|
|
103
|
+
if (raw === null) return null;
|
|
104
|
+
const timeZone = asText(raw.timeZone);
|
|
105
|
+
const holidayZone = asText(raw.holidayZone) ?? timeZone;
|
|
106
|
+
if (timeZone === void 0 || holidayZone === void 0) return null;
|
|
107
|
+
if (clockAt(Date.now(), timeZone) === null || clockAt(Date.now(), holidayZone) === null) return null;
|
|
108
|
+
const peakDays = [];
|
|
109
|
+
if (!Array.isArray(raw.peakDays)) return null;
|
|
110
|
+
for (const day of raw.peakDays) {
|
|
111
|
+
if (!Number.isInteger(day) || day < 0 || day > 6 || peakDays.includes(day)) continue;
|
|
112
|
+
peakDays.push(day);
|
|
113
|
+
}
|
|
114
|
+
if (peakDays.length === 0) return null;
|
|
115
|
+
const peakWindows = asWindows(raw.peakWindows);
|
|
116
|
+
if (peakWindows === null) return null;
|
|
117
|
+
const holidays = [];
|
|
118
|
+
if (raw.holidays !== void 0) {
|
|
119
|
+
if (!Array.isArray(raw.holidays)) return null;
|
|
120
|
+
for (const day of raw.holidays) {
|
|
121
|
+
const date = asDate(day);
|
|
122
|
+
if (date !== void 0 && !holidays.includes(date)) holidays.push(date);
|
|
123
|
+
}
|
|
124
|
+
holidays.sort();
|
|
125
|
+
}
|
|
126
|
+
return { timeZone, peakDays, peakWindows, holidays, holidayZone };
|
|
127
|
+
}
|
|
128
|
+
function asCatalog(value) {
|
|
129
|
+
const raw = asRecord(value);
|
|
130
|
+
if (raw === null) return null;
|
|
131
|
+
const out = {};
|
|
132
|
+
for (const [id, line] of Object.entries(raw)) {
|
|
133
|
+
if (typeof line !== "string" || id.trim() === "") continue;
|
|
134
|
+
const parts = line.split(" ").map(Number);
|
|
135
|
+
if (parts.length !== 3 || !parts.every((candidate) => Number.isFinite(candidate) && candidate >= 0)) continue;
|
|
136
|
+
out[id.trim().toLowerCase()] = line;
|
|
137
|
+
}
|
|
138
|
+
return out;
|
|
139
|
+
}
|
|
140
|
+
function parsePriceDocument(value) {
|
|
141
|
+
const raw = asRecord(value);
|
|
142
|
+
if (raw === null || raw.schemaVersion !== PRICE_SCHEMA_VERSION) return null;
|
|
143
|
+
const updatedAt = asDate(raw.updatedAt);
|
|
144
|
+
const schedule = asSchedule(raw.schedule);
|
|
145
|
+
if (updatedAt === void 0 || schedule === null) return null;
|
|
146
|
+
const rawModels = asRecord(raw.models);
|
|
147
|
+
if (rawModels === null) return null;
|
|
148
|
+
const models = {};
|
|
149
|
+
for (const [id, entry] of Object.entries(rawModels)) {
|
|
150
|
+
if (id.trim() === "") continue;
|
|
151
|
+
const perCurrency = asRecord(entry);
|
|
152
|
+
if (perCurrency === null) return null;
|
|
153
|
+
const priced = {};
|
|
154
|
+
for (const currency of CURRENCIES) {
|
|
155
|
+
const price = asPrice(perCurrency[currency]);
|
|
156
|
+
if (price === null) return null;
|
|
157
|
+
priced[currency] = price;
|
|
158
|
+
}
|
|
159
|
+
models[id] = priced;
|
|
160
|
+
}
|
|
161
|
+
if (Object.keys(models).length === 0) return null;
|
|
162
|
+
const catalog = raw.catalog === void 0 ? {} : asCatalog(raw.catalog);
|
|
163
|
+
if (catalog === null) return null;
|
|
164
|
+
const catalogCny = raw.catalogCny === void 0 ? {} : asCatalog(raw.catalogCny);
|
|
165
|
+
if (catalogCny === null) return null;
|
|
166
|
+
const aliases = {};
|
|
167
|
+
if (raw.aliases !== void 0) {
|
|
168
|
+
const rawAliases = asRecord(raw.aliases);
|
|
169
|
+
if (rawAliases === null) return null;
|
|
170
|
+
for (const [from, to] of Object.entries(rawAliases)) {
|
|
171
|
+
const target = asText(to);
|
|
172
|
+
if (target !== void 0 && (Object.hasOwn(models, target) || Object.hasOwn(catalog, target) || Object.hasOwn(catalogCny, target)) && from.trim() !== "") {
|
|
173
|
+
aliases[from.trim().toLowerCase()] = target;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
const document = { schemaVersion: PRICE_SCHEMA_VERSION, updatedAt, schedule, models, aliases };
|
|
178
|
+
if (Object.keys(catalog).length > 0) document.catalog = catalog;
|
|
179
|
+
if (Object.keys(catalogCny).length > 0) document.catalogCny = catalogCny;
|
|
180
|
+
const rate = raw.usdToCny;
|
|
181
|
+
if (typeof rate === "number" && Number.isFinite(rate) && rate > 0) document.usdToCny = rate;
|
|
182
|
+
const capturedAt = asText(raw.capturedAt);
|
|
183
|
+
if (capturedAt !== void 0) document.capturedAt = capturedAt;
|
|
184
|
+
const source = asRecord(raw.source);
|
|
185
|
+
if (source !== null) {
|
|
186
|
+
const zh = asText(source.zh);
|
|
187
|
+
const en = asText(source.en);
|
|
188
|
+
if (zh !== void 0 || en !== void 0) document.source = { ...zh === void 0 ? {} : { zh }, ...en === void 0 ? {} : { en } };
|
|
189
|
+
}
|
|
190
|
+
return document;
|
|
191
|
+
}
|
|
192
|
+
export {
|
|
193
|
+
parsePriceDocument
|
|
194
|
+
};
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
// packages/contracts/src/package.ts
|
|
2
|
+
var PACKAGE_NAME = "dsh-claude-style";
|
|
3
|
+
|
|
4
|
+
// packages/host/src/prices.ts
|
|
5
|
+
import "node:fs";
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
import { cachePath, readJsonDocument, writeJsonDocument } from "./cache-file.js";
|
|
8
|
+
import { packageRoot } from "./package-root.js";
|
|
9
|
+
import { parsePriceDocument } from "./prices-document.js";
|
|
10
|
+
var PRICES_FILE = "prices.json";
|
|
11
|
+
var PRICES_URLS = [
|
|
12
|
+
"https://cdn.jsdelivr.net/gh/Nwflower/dsh-claude-style@master/packages/host/data/prices.json",
|
|
13
|
+
"https://raw.githubusercontent.com/Nwflower/dsh-claude-style/master/packages/host/data/prices.json"
|
|
14
|
+
];
|
|
15
|
+
var PRICES_REFRESH_MS = 24 * 60 * 60 * 1e3;
|
|
16
|
+
var PRICES_IDLE_MS = 48 * 60 * 60 * 1e3;
|
|
17
|
+
var FETCH_TIMEOUT_MS = 1e4;
|
|
18
|
+
function createPrices(ctx) {
|
|
19
|
+
const shippedFile = join(packageRoot(), "lib", PRICES_FILE);
|
|
20
|
+
const cacheFile = cachePath(ctx, "prices.json");
|
|
21
|
+
let document = null;
|
|
22
|
+
let origin = "bundled";
|
|
23
|
+
let fetchedAt = 0;
|
|
24
|
+
let loaded = false;
|
|
25
|
+
let timer;
|
|
26
|
+
let inFlight = null;
|
|
27
|
+
let lastAskedAt = 0;
|
|
28
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
29
|
+
const warn = (message) => ctx.logger?.warn?.(message);
|
|
30
|
+
function shipped() {
|
|
31
|
+
const parsed = parsePriceDocument(readJsonDocument(ctx, shippedFile, `lib/${PRICES_FILE}`));
|
|
32
|
+
if (parsed === null) {
|
|
33
|
+
warn(`${PACKAGE_NAME}: lib/${PRICES_FILE} is not a price snapshot this build understands`);
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
return parsed;
|
|
37
|
+
}
|
|
38
|
+
function cached(shippedDocument) {
|
|
39
|
+
const value = readJsonDocument(ctx, cacheFile, "price cache");
|
|
40
|
+
if (value === null || typeof value !== "object") return null;
|
|
41
|
+
const stored = value;
|
|
42
|
+
const parsed = parsePriceDocument(stored.document);
|
|
43
|
+
if (parsed === null) return null;
|
|
44
|
+
if (shippedDocument !== null && parsed.updatedAt < shippedDocument.updatedAt) return null;
|
|
45
|
+
return { document: parsed, fetchedAt: typeof stored.fetchedAt === "number" ? stored.fetchedAt : 0 };
|
|
46
|
+
}
|
|
47
|
+
function load() {
|
|
48
|
+
if (loaded) return;
|
|
49
|
+
loaded = true;
|
|
50
|
+
const shippedDocument = shipped();
|
|
51
|
+
const held = cached(shippedDocument);
|
|
52
|
+
if (held !== null) {
|
|
53
|
+
document = held.document;
|
|
54
|
+
fetchedAt = held.fetchedAt;
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
document = shippedDocument;
|
|
58
|
+
}
|
|
59
|
+
function announce() {
|
|
60
|
+
for (const listener of listeners) listener();
|
|
61
|
+
}
|
|
62
|
+
async function fetchOne(url) {
|
|
63
|
+
const controller = new AbortController();
|
|
64
|
+
inFlight = controller;
|
|
65
|
+
const timeout = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
|
|
66
|
+
try {
|
|
67
|
+
const response = await fetch(url, { signal: controller.signal, headers: { accept: "application/json" } });
|
|
68
|
+
if (!response.ok) {
|
|
69
|
+
warn(`${PACKAGE_NAME}: ${url} answered ${response.status}`);
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
return parsePriceDocument(await response.json());
|
|
73
|
+
} finally {
|
|
74
|
+
clearTimeout(timeout);
|
|
75
|
+
if (inFlight === controller) inFlight = null;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
async function refresh() {
|
|
79
|
+
let next = null;
|
|
80
|
+
for (const url of PRICES_URLS) {
|
|
81
|
+
try {
|
|
82
|
+
next = await fetchOne(url);
|
|
83
|
+
} catch (error) {
|
|
84
|
+
warn(`${PACKAGE_NAME}: ${url} unreachable: ${error.message}`);
|
|
85
|
+
}
|
|
86
|
+
if (next !== null) break;
|
|
87
|
+
}
|
|
88
|
+
if (next === null) {
|
|
89
|
+
warn(`${PACKAGE_NAME}: no price source answered a document this build understands; keeping the current one`);
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
document = next;
|
|
93
|
+
origin = "remote";
|
|
94
|
+
fetchedAt = Date.now();
|
|
95
|
+
writeJsonDocument(ctx, cacheFile, "price cache", { fetchedAt, document: next });
|
|
96
|
+
announce();
|
|
97
|
+
return true;
|
|
98
|
+
}
|
|
99
|
+
function ensure() {
|
|
100
|
+
load();
|
|
101
|
+
lastAskedAt = Date.now();
|
|
102
|
+
if (timer !== void 0) return;
|
|
103
|
+
timer = setInterval(() => {
|
|
104
|
+
if (Date.now() - lastAskedAt > PRICES_IDLE_MS) {
|
|
105
|
+
stopTimer();
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
void refresh();
|
|
109
|
+
}, PRICES_REFRESH_MS);
|
|
110
|
+
timer.unref?.();
|
|
111
|
+
}
|
|
112
|
+
function stopTimer() {
|
|
113
|
+
if (timer === void 0) return;
|
|
114
|
+
clearInterval(timer);
|
|
115
|
+
timer = void 0;
|
|
116
|
+
}
|
|
117
|
+
return {
|
|
118
|
+
ensure,
|
|
119
|
+
refresh,
|
|
120
|
+
document() {
|
|
121
|
+
return document;
|
|
122
|
+
},
|
|
123
|
+
version() {
|
|
124
|
+
return document === null ? "" : `${document.updatedAt}:${document.capturedAt ?? ""}`;
|
|
125
|
+
},
|
|
126
|
+
payload() {
|
|
127
|
+
return {
|
|
128
|
+
document,
|
|
129
|
+
...fetchedAt === 0 ? {} : { fetchedAt: new Date(fetchedAt).toISOString() },
|
|
130
|
+
origin
|
|
131
|
+
};
|
|
132
|
+
},
|
|
133
|
+
onChange(listener) {
|
|
134
|
+
listeners.add(listener);
|
|
135
|
+
return () => listeners.delete(listener);
|
|
136
|
+
},
|
|
137
|
+
dispose() {
|
|
138
|
+
stopTimer();
|
|
139
|
+
listeners.clear();
|
|
140
|
+
if (inFlight !== null) {
|
|
141
|
+
inFlight.abort();
|
|
142
|
+
inFlight = null;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
export {
|
|
148
|
+
PRICES_FILE,
|
|
149
|
+
PRICES_IDLE_MS,
|
|
150
|
+
PRICES_REFRESH_MS,
|
|
151
|
+
PRICES_URLS,
|
|
152
|
+
createPrices
|
|
153
|
+
};
|
package/lib/host/routes.js
CHANGED
|
@@ -13,6 +13,8 @@ var BRAND_PATH = `${ROUTE_PREFIX}/brand`;
|
|
|
13
13
|
var BRAND_MARK_PATH = `${ROUTE_PREFIX}/brand/mark`;
|
|
14
14
|
var BRAND_WORDMARK_PATH = `${ROUTE_PREFIX}/brand/wordmark`;
|
|
15
15
|
var PEAKRATE_PATH = `${ROUTE_PREFIX}/peakrate`;
|
|
16
|
+
var PRICES_PATH = `${ROUTE_PREFIX}/prices`;
|
|
17
|
+
var SESSION_COST_PATH = `${ROUTE_PREFIX}/session-cost`;
|
|
16
18
|
|
|
17
19
|
// packages/host/src/routes.ts
|
|
18
20
|
import { existsSync as existsSync2, readFileSync as readFileSync2, readdirSync, realpathSync, rmSync, statSync } from "node:fs";
|
|
@@ -169,7 +171,9 @@ function createStaticFiles(ctx, packageName, assetsDir) {
|
|
|
169
171
|
import { createHdslAccount } from "./hdsl.js";
|
|
170
172
|
import { packageRoot } from "./package-root.js";
|
|
171
173
|
import { createPeakRate } from "./peakrate.js";
|
|
174
|
+
import { createPrices } from "./prices.js";
|
|
172
175
|
import { QUERY_MAX, createSessionSearch } from "./search.js";
|
|
176
|
+
import { createSessionCost } from "./session-cost.js";
|
|
173
177
|
import { createUsage } from "./usage.js";
|
|
174
178
|
var COPY_FILE = "model-descriptions.json";
|
|
175
179
|
var FONT_FILES = {
|
|
@@ -182,7 +186,7 @@ var FONT_FILES = {
|
|
|
182
186
|
"NotoSerifVariable.woff2": "font/woff2"
|
|
183
187
|
};
|
|
184
188
|
var ASSETS_PATH = `${ROUTE_PREFIX}/assets/`;
|
|
185
|
-
var USAGE_TTL_MS =
|
|
189
|
+
var USAGE_TTL_MS = 30 * 1e3;
|
|
186
190
|
var DELETE_BODY_MAX = 4096;
|
|
187
191
|
function sessionDirectoryName(id) {
|
|
188
192
|
if (id === ".") return "~002E";
|
|
@@ -283,6 +287,9 @@ function registerRoutes(ctx, scope) {
|
|
|
283
287
|
const hdsl = createHdslAccount(ctx);
|
|
284
288
|
const sessionSearch = createSessionSearch(ctx);
|
|
285
289
|
const peakrate = createPeakRate(ctx);
|
|
290
|
+
const prices = createPrices(ctx);
|
|
291
|
+
const sessionCost = createSessionCost(ctx, prices);
|
|
292
|
+
sessionCost.ensure();
|
|
286
293
|
const register = (label, route) => {
|
|
287
294
|
try {
|
|
288
295
|
const dispose = server.register(route);
|
|
@@ -436,10 +443,37 @@ function registerRoutes(ctx, scope) {
|
|
|
436
443
|
sendPayload(res, answer);
|
|
437
444
|
}
|
|
438
445
|
});
|
|
446
|
+
register("prices", {
|
|
447
|
+
kind: "exact",
|
|
448
|
+
path: PRICES_PATH,
|
|
449
|
+
handler: (req, res) => {
|
|
450
|
+
if (methodRefused(req, res, ["GET", "POST"])) return;
|
|
451
|
+
if (fenceRefused(ctx, req, res)) return;
|
|
452
|
+
prices.ensure();
|
|
453
|
+
const answer = req.method === "POST" ? prices.refresh().then(() => prices.payload()) : Promise.resolve(prices.payload());
|
|
454
|
+
sendPayload(res, answer);
|
|
455
|
+
}
|
|
456
|
+
});
|
|
457
|
+
register("session cost", {
|
|
458
|
+
kind: "exact",
|
|
459
|
+
path: SESSION_COST_PATH,
|
|
460
|
+
handler: (req, res) => {
|
|
461
|
+
if (methodRefused(req, res, ["GET"])) return;
|
|
462
|
+
if (fenceRefused(ctx, req, res)) return;
|
|
463
|
+
const sessionId = (new URL(req.url ?? "/", "http://local").searchParams.get("id") ?? "").trim();
|
|
464
|
+
if (!SESSION_ID_RE.test(sessionId)) {
|
|
465
|
+
sendJson(res, 400, { ok: false, error: "invalid session id" });
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
sendPayload(res, sessionCost.get(sessionId).then((value) => ({ value })));
|
|
469
|
+
}
|
|
470
|
+
});
|
|
439
471
|
for (const { label, route } of brandRoutes(ctx)) register(label, route);
|
|
440
472
|
return () => {
|
|
441
473
|
usage.dispose();
|
|
442
474
|
peakrate.dispose();
|
|
475
|
+
prices.dispose();
|
|
476
|
+
sessionCost.dispose();
|
|
443
477
|
sessionSearch.dispose();
|
|
444
478
|
for (const dispose of disposers) {
|
|
445
479
|
try {
|