@mevdragon/vidfarm-devcli 0.5.3 → 0.6.0
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/README.md +3 -3
- package/demo/README.md +28 -0
- package/demo/dist/app.css +1 -0
- package/demo/dist/app.js +1184 -0
- package/demo/dist/chunks/chunk-DXB73IDG.js +1 -0
- package/demo/dist/chunks/chunk-S7OWAJDS.js +36 -0
- package/demo/dist/chunks/chunk-VTIBZ6AN.js +1 -0
- package/demo/dist/chunks/dist-ADSJKBVE.js +332 -0
- package/demo/dist/chunks/domEditingLayers-VZMLL4AP-SGHWPND4.js +1 -0
- package/demo/dist/chunks/hyperframes-player-XB65TCD6.js +425 -0
- package/demo/dist/chunks/lib-XAQ37YOE.js +1 -0
- package/demo/dist/chunks/src-TJ2QYA4U.js +207 -0
- package/demo/dist/favicon.ico +0 -0
- package/demo/dist/icons/timeline/audio.svg +7 -0
- package/demo/dist/icons/timeline/captions.svg +5 -0
- package/demo/dist/icons/timeline/composition.svg +12 -0
- package/demo/dist/icons/timeline/image.svg +18 -0
- package/demo/dist/icons/timeline/music.svg +10 -0
- package/demo/dist/icons/timeline/text.svg +3 -0
- package/demo/dist/index.html +15 -0
- package/dist/src/account-pages-legacy.js +9396 -0
- package/dist/src/account-pages.js +61 -0
- package/dist/src/app.js +14378 -0
- package/dist/src/cli.js +1 -1
- package/dist/src/composition-runtime.js +613 -0
- package/dist/src/config.js +166 -0
- package/dist/src/context.js +447 -0
- package/dist/src/dev-app-legacy.js +739 -0
- package/dist/src/dev-app.js +6 -0
- package/dist/src/domain.js +2 -0
- package/dist/src/editor-chat-history.js +82 -0
- package/dist/src/editor-chat.js +449 -0
- package/dist/src/editor-dark-theme.js +1128 -0
- package/dist/src/frontend/debug.js +71 -0
- package/dist/src/frontend/flockposter-cache-store.js +124 -0
- package/dist/src/frontend/homepage-client.js +182 -0
- package/dist/src/frontend/homepage-shared.js +4 -0
- package/dist/src/frontend/homepage-store.js +28 -0
- package/dist/src/frontend/homepage-view.js +547 -0
- package/dist/src/frontend/page-runtime-client.js +132 -0
- package/dist/src/frontend/page-runtime-store.js +9 -0
- package/dist/src/frontend/sentry.js +42 -0
- package/dist/src/frontend/template-editor-chat.js +3960 -0
- package/dist/src/help-page.js +346 -0
- package/dist/src/homepage.js +1235 -0
- package/dist/src/hyperframes/composition.js +180 -0
- package/dist/src/index.js +16 -0
- package/dist/src/instrument.js +30 -0
- package/dist/src/lib/crypto.js +45 -0
- package/dist/src/lib/dev-log.js +54 -0
- package/dist/src/lib/display-name.js +11 -0
- package/dist/src/lib/ids.js +24 -0
- package/dist/src/lib/images.js +19 -0
- package/dist/src/lib/json.js +15 -0
- package/dist/src/lib/package-root.js +47 -0
- package/dist/src/lib/template-paths.js +28 -0
- package/dist/src/lib/time.js +7 -0
- package/dist/src/lib/url-clean.js +85 -0
- package/dist/src/page-runtime.js +2 -0
- package/dist/src/page-shell.js +1381 -0
- package/dist/src/primitive-context.js +357 -0
- package/dist/src/primitive-registry.js +2436 -0
- package/dist/src/primitive-sdk.js +4 -0
- package/dist/src/primitives/hyperframes-media.js +108 -0
- package/dist/src/react-page-shell.js +35 -0
- package/dist/src/ready-post-schedule-component.js +1540 -0
- package/dist/src/registry.js +296 -0
- package/dist/src/runtime.js +35 -0
- package/dist/src/services/api-call-history.js +249 -0
- package/dist/src/services/auth.js +152 -0
- package/dist/src/services/billing-pricing.js +39 -0
- package/dist/src/services/billing.js +228 -0
- package/dist/src/services/cast.js +127 -0
- package/dist/src/services/chat-threads.js +92 -0
- package/dist/src/services/composition-sanitize.js +124 -0
- package/dist/src/services/composition-watch.js +79 -0
- package/dist/src/services/fork-access.js +93 -0
- package/dist/src/services/fork-manifest.js +42 -0
- package/dist/src/services/ghostcut.js +179 -0
- package/dist/src/services/hyperframes.js +2307 -0
- package/dist/src/services/job-capacity.js +14 -0
- package/dist/src/services/job-logs.js +197 -0
- package/dist/src/services/jobs.js +136 -0
- package/dist/src/services/local-dynamo.js +0 -0
- package/dist/src/services/media-processing.js +766 -0
- package/dist/src/services/primitive-media-lambda.js +280 -0
- package/dist/src/services/providers.js +2926 -0
- package/dist/src/services/rate-limits.js +262 -0
- package/dist/src/services/serverless-auth.js +382 -0
- package/dist/src/services/serverless-jobs.js +1082 -0
- package/dist/src/services/serverless-provider-keys.js +409 -0
- package/dist/src/services/serverless-records.js +1385 -0
- package/dist/src/services/serverless-template-configs.js +75 -0
- package/dist/src/services/storage.js +383 -0
- package/dist/src/services/template-certification.js +413 -0
- package/dist/src/services/template-loader.js +99 -0
- package/dist/src/services/template-runtime-bundles.js +217 -0
- package/dist/src/services/template-sources.js +1017 -0
- package/dist/src/services/video-normalization.js +2 -0
- package/dist/src/services/webhooks.js +62 -0
- package/dist/src/template-editor-pages.js +2576 -0
- package/dist/src/template-editor-shell.js +2840 -0
- package/dist/src/template-sdk.js +4 -0
- package/dist/src/worker.js +17 -0
- package/package.json +6 -4
- package/public/assets/homepage-app.js +54 -0
- package/public/assets/homepage-client-app.js +80 -0
- package/public/assets/page-runtime-client-app.js +94 -0
- package/src/assets/SELLING_AWARENESS_STAGES.md +579 -0
- package/src/assets/SELLING_WITH_HOOKS.md +377 -0
- package/src/assets/SELLING_WITH_VSLS.md +606 -0
- package/src/assets/favicon.ico +0 -0
- package/src/assets/logo-vidfarm.png +0 -0
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
import { existsSync, readdirSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { config } from "./config.js";
|
|
5
|
+
import { devLog } from "./lib/dev-log.js";
|
|
6
|
+
import { loadTemplateFromModule } from "./services/template-loader.js";
|
|
7
|
+
import { loadTemplateFromRuntimeRelease } from "./services/template-runtime-bundles.js";
|
|
8
|
+
import { serverlessRecords } from "./services/serverless-records.js";
|
|
9
|
+
class TemplateRegistry {
|
|
10
|
+
localTemplates = new Map();
|
|
11
|
+
releaseTemplates = new Map();
|
|
12
|
+
activeReleases = new Map();
|
|
13
|
+
runtimeTemplates = new Map();
|
|
14
|
+
initialized = false;
|
|
15
|
+
initPromise = null;
|
|
16
|
+
async ensureInitialized() {
|
|
17
|
+
if (this.initialized) {
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
if (this.initPromise) {
|
|
21
|
+
return this.initPromise;
|
|
22
|
+
}
|
|
23
|
+
this.initPromise = (async () => {
|
|
24
|
+
await this.loadActiveReleaseSnapshots();
|
|
25
|
+
if (shouldLoadLocalTemplates()) {
|
|
26
|
+
await this.loadLocalTemplates();
|
|
27
|
+
devLog("template_registry.local_loaded", {
|
|
28
|
+
count: this.localTemplates.size,
|
|
29
|
+
template_ids: [...this.localTemplates.keys()]
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
this.initialized = true;
|
|
33
|
+
this.initPromise = null;
|
|
34
|
+
devLog("template_registry.initialized", {
|
|
35
|
+
local_count: this.localTemplates.size,
|
|
36
|
+
release_count: this.releaseTemplates.size,
|
|
37
|
+
runtime_count: this.runtimeTemplates.size
|
|
38
|
+
});
|
|
39
|
+
})();
|
|
40
|
+
return this.initPromise;
|
|
41
|
+
}
|
|
42
|
+
list() {
|
|
43
|
+
const merged = new Map();
|
|
44
|
+
for (const template of this.releaseTemplates.values()) {
|
|
45
|
+
merged.set(template.id, template);
|
|
46
|
+
}
|
|
47
|
+
for (const template of this.localTemplates.values()) {
|
|
48
|
+
merged.set(template.id, template);
|
|
49
|
+
}
|
|
50
|
+
for (const template of this.runtimeTemplates.values()) {
|
|
51
|
+
merged.set(template.id, template);
|
|
52
|
+
}
|
|
53
|
+
return [...merged.values()].sort((a, b) => a.id.localeCompare(b.id));
|
|
54
|
+
}
|
|
55
|
+
get(templateId) {
|
|
56
|
+
return this.findTemplate(templateId, this.runtimeTemplates)
|
|
57
|
+
?? this.findTemplate(templateId, this.localTemplates)
|
|
58
|
+
?? this.findTemplate(templateId, this.releaseTemplates);
|
|
59
|
+
}
|
|
60
|
+
async listMetadata() {
|
|
61
|
+
try {
|
|
62
|
+
const releases = await serverlessRecords.listActiveTemplateReleases?.() ?? [];
|
|
63
|
+
for (const release of releases) {
|
|
64
|
+
this.cacheReleaseSnapshot(release);
|
|
65
|
+
}
|
|
66
|
+
const releaseTemplates = releases
|
|
67
|
+
.filter((release) => release.templateSnapshot)
|
|
68
|
+
.map((release) => templateFromSnapshot(release.templateSnapshot, release));
|
|
69
|
+
return await this.mergeLocalMetadata(releaseTemplates);
|
|
70
|
+
}
|
|
71
|
+
catch (error) {
|
|
72
|
+
devLog("template_registry.metadata.list_failed", {
|
|
73
|
+
message: error instanceof Error ? error.message : String(error)
|
|
74
|
+
}, "warn");
|
|
75
|
+
await this.ensureInitialized();
|
|
76
|
+
return this.list();
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
async listMetadataPage(input = {}) {
|
|
80
|
+
try {
|
|
81
|
+
const page = await serverlessRecords.listActiveTemplateReleasePage?.(input);
|
|
82
|
+
if (!page) {
|
|
83
|
+
return { templates: await this.listMetadata(), nextCursor: null };
|
|
84
|
+
}
|
|
85
|
+
for (const release of page.items) {
|
|
86
|
+
this.cacheReleaseSnapshot(release);
|
|
87
|
+
}
|
|
88
|
+
const releaseTemplates = page.items
|
|
89
|
+
.filter((release) => release.templateSnapshot)
|
|
90
|
+
.map((release) => templateFromSnapshot(release.templateSnapshot, release));
|
|
91
|
+
return {
|
|
92
|
+
templates: await this.mergeLocalMetadata(releaseTemplates),
|
|
93
|
+
nextCursor: page.nextCursor
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
catch (error) {
|
|
97
|
+
devLog("template_registry.metadata.page_failed", {
|
|
98
|
+
message: error instanceof Error ? error.message : String(error)
|
|
99
|
+
}, "warn");
|
|
100
|
+
return { templates: await this.listMetadata(), nextCursor: null };
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
async getMetadata(templateId) {
|
|
104
|
+
const cached = this.get(templateId);
|
|
105
|
+
if (cached) {
|
|
106
|
+
return cached;
|
|
107
|
+
}
|
|
108
|
+
try {
|
|
109
|
+
const release = await serverlessRecords.getActiveTemplateReleaseByTemplateId?.(templateId);
|
|
110
|
+
if (release?.templateSnapshot) {
|
|
111
|
+
return this.cacheReleaseSnapshot(release);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
catch (error) {
|
|
115
|
+
devLog("template_registry.metadata.get_failed", {
|
|
116
|
+
template_id: templateId,
|
|
117
|
+
message: error instanceof Error ? error.message : String(error)
|
|
118
|
+
}, "warn");
|
|
119
|
+
}
|
|
120
|
+
await this.ensureInitialized();
|
|
121
|
+
return this.get(templateId);
|
|
122
|
+
}
|
|
123
|
+
async getExecutable(templateId) {
|
|
124
|
+
const runtime = this.findTemplate(templateId, this.runtimeTemplates) ?? this.findTemplate(templateId, this.localTemplates);
|
|
125
|
+
if (runtime) {
|
|
126
|
+
return runtime;
|
|
127
|
+
}
|
|
128
|
+
let releaseTemplate = this.findTemplate(templateId, this.releaseTemplates);
|
|
129
|
+
let release = releaseTemplate ? this.activeReleases.get(releaseTemplate.id) : null;
|
|
130
|
+
if (!release) {
|
|
131
|
+
try {
|
|
132
|
+
const activeRelease = await serverlessRecords.getActiveTemplateReleaseByTemplateId?.(templateId);
|
|
133
|
+
if (activeRelease?.templateSnapshot) {
|
|
134
|
+
releaseTemplate = this.cacheReleaseSnapshot(activeRelease);
|
|
135
|
+
release = activeRelease;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
catch (error) {
|
|
139
|
+
devLog("template_registry.executable.active_release_lookup_failed", {
|
|
140
|
+
template_id: templateId,
|
|
141
|
+
message: error instanceof Error ? error.message : String(error)
|
|
142
|
+
}, "warn");
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
if (!release) {
|
|
146
|
+
await this.ensureInitialized();
|
|
147
|
+
const initializedRuntime = this.findTemplate(templateId, this.runtimeTemplates) ?? this.findTemplate(templateId, this.localTemplates);
|
|
148
|
+
if (initializedRuntime) {
|
|
149
|
+
return initializedRuntime;
|
|
150
|
+
}
|
|
151
|
+
releaseTemplate = this.findTemplate(templateId, this.releaseTemplates);
|
|
152
|
+
release = releaseTemplate ? this.activeReleases.get(releaseTemplate.id) : null;
|
|
153
|
+
}
|
|
154
|
+
if (!release) {
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
const template = await loadTemplateFromRuntimeRelease(release);
|
|
158
|
+
this.runtimeTemplates.set(template.id, template);
|
|
159
|
+
return template;
|
|
160
|
+
}
|
|
161
|
+
registerRuntimeTemplate(template) {
|
|
162
|
+
this.runtimeTemplates.set(template.id, template);
|
|
163
|
+
}
|
|
164
|
+
cacheReleaseSnapshot(release) {
|
|
165
|
+
if (!release.templateSnapshot) {
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
const template = templateFromSnapshot(release.templateSnapshot, release);
|
|
169
|
+
this.releaseTemplates.set(template.id, template);
|
|
170
|
+
this.activeReleases.set(template.id, release);
|
|
171
|
+
return template;
|
|
172
|
+
}
|
|
173
|
+
async loadLocalTemplates() {
|
|
174
|
+
this.localTemplates.clear();
|
|
175
|
+
for (const modulePath of discoverLocalTemplateModulePaths(process.cwd())) {
|
|
176
|
+
const template = await loadTemplateFromModule(modulePath);
|
|
177
|
+
this.localTemplates.set(template.id, template);
|
|
178
|
+
devLog("template_registry.local_template.loaded", {
|
|
179
|
+
template_id: template.id,
|
|
180
|
+
slug_id: template.slugId,
|
|
181
|
+
module_path: modulePath
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
async loadActiveReleaseSnapshots() {
|
|
186
|
+
this.releaseTemplates.clear();
|
|
187
|
+
this.activeReleases.clear();
|
|
188
|
+
let releases = [];
|
|
189
|
+
try {
|
|
190
|
+
releases = await serverlessRecords.listActiveTemplateReleases?.() ?? [];
|
|
191
|
+
}
|
|
192
|
+
catch (error) {
|
|
193
|
+
devLog("template_registry.active_releases.load_failed", {
|
|
194
|
+
message: error instanceof Error ? error.message : String(error)
|
|
195
|
+
}, "warn");
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
for (const release of releases) {
|
|
199
|
+
if (!release.templateSnapshot) {
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
this.cacheReleaseSnapshot(release);
|
|
203
|
+
}
|
|
204
|
+
devLog("template_registry.active_releases.loaded", {
|
|
205
|
+
count: this.releaseTemplates.size,
|
|
206
|
+
template_ids: [...this.releaseTemplates.keys()]
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
async mergeLocalMetadata(templates) {
|
|
210
|
+
if (shouldLoadLocalTemplates()) {
|
|
211
|
+
await this.ensureInitialized();
|
|
212
|
+
}
|
|
213
|
+
const merged = new Map();
|
|
214
|
+
for (const template of templates) {
|
|
215
|
+
merged.set(template.id, template);
|
|
216
|
+
}
|
|
217
|
+
for (const template of this.localTemplates.values()) {
|
|
218
|
+
merged.set(template.id, template);
|
|
219
|
+
}
|
|
220
|
+
for (const template of this.runtimeTemplates.values()) {
|
|
221
|
+
merged.set(template.id, template);
|
|
222
|
+
}
|
|
223
|
+
return [...merged.values()].sort((a, b) => a.id.localeCompare(b.id));
|
|
224
|
+
}
|
|
225
|
+
findTemplate(templateKey, source) {
|
|
226
|
+
return source.get(templateKey) ?? [...source.values()].find((template) => template.slugId === templateKey) ?? null;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
function templateFromSnapshot(snapshot, release) {
|
|
230
|
+
const snapshotAbout = snapshot.about;
|
|
231
|
+
const operations = Object.fromEntries(snapshot.operations.map((operation) => [
|
|
232
|
+
operation.name,
|
|
233
|
+
{
|
|
234
|
+
description: operation.description,
|
|
235
|
+
inputSchema: z.record(z.string(), z.unknown()),
|
|
236
|
+
workflow: operation.workflow,
|
|
237
|
+
providerHint: operation.providerHint ?? undefined,
|
|
238
|
+
webhookSupport: operation.webhookSupport ?? undefined
|
|
239
|
+
}
|
|
240
|
+
]));
|
|
241
|
+
return {
|
|
242
|
+
id: snapshot.id,
|
|
243
|
+
slugId: snapshot.slugId,
|
|
244
|
+
version: snapshot.version,
|
|
245
|
+
about: {
|
|
246
|
+
...snapshot.about,
|
|
247
|
+
links_to_inspiration: normalizeTemplateInspirationLinks(snapshotAbout.links_to_inspiration, snapshot.about.link_to_original),
|
|
248
|
+
sound: normalizeTemplateSound(snapshotAbout.sound)
|
|
249
|
+
},
|
|
250
|
+
examples: snapshot.examples,
|
|
251
|
+
skillPath: release.skillPath,
|
|
252
|
+
configSchema: z.record(z.string(), z.unknown()),
|
|
253
|
+
operations,
|
|
254
|
+
jobs: {}
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
function normalizeTemplateInspirationLinks(value, linkToOriginal) {
|
|
258
|
+
if (Array.isArray(value)) {
|
|
259
|
+
return value.filter((entry) => typeof entry === "string");
|
|
260
|
+
}
|
|
261
|
+
return linkToOriginal ? [linkToOriginal] : [];
|
|
262
|
+
}
|
|
263
|
+
function normalizeTemplateSound(value) {
|
|
264
|
+
return {
|
|
265
|
+
sound_title: typeof value?.sound_title === "string" ? value.sound_title : "",
|
|
266
|
+
sound_url: typeof value?.sound_url === "string" ? value.sound_url : ""
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
function shouldLoadLocalTemplates() {
|
|
270
|
+
if (config.VIDFARM_LOAD_LOCAL_TEMPLATES === "true") {
|
|
271
|
+
return true;
|
|
272
|
+
}
|
|
273
|
+
if (config.VIDFARM_LOAD_LOCAL_TEMPLATES === "false") {
|
|
274
|
+
return false;
|
|
275
|
+
}
|
|
276
|
+
return !config.isProduction;
|
|
277
|
+
}
|
|
278
|
+
function discoverLocalTemplateModulePaths(rootDir) {
|
|
279
|
+
const templatesDir = path.resolve(rootDir, "templates");
|
|
280
|
+
if (!existsSync(templatesDir)) {
|
|
281
|
+
return [];
|
|
282
|
+
}
|
|
283
|
+
return readdirSync(templatesDir, { withFileTypes: true })
|
|
284
|
+
.filter((entry) => entry.isDirectory() && entry.name.startsWith("vidfarm_template_"))
|
|
285
|
+
.filter((entry) => (entry.name !== "vidfarm_template_0000"
|
|
286
|
+
|| process.env.NODE_ENV !== "production"
|
|
287
|
+
|| process.env.VIDFARM_DEPLOYMENT_TARGET === "serverless-staging"))
|
|
288
|
+
.map((entry) => [
|
|
289
|
+
path.join(templatesDir, entry.name, "src", "template.js"),
|
|
290
|
+
path.join(templatesDir, entry.name, "src", "template.ts")
|
|
291
|
+
].find((modulePath) => existsSync(modulePath)))
|
|
292
|
+
.filter((modulePath) => Boolean(modulePath))
|
|
293
|
+
.sort((a, b) => a.localeCompare(b));
|
|
294
|
+
}
|
|
295
|
+
export const templateRegistry = new TemplateRegistry();
|
|
296
|
+
//# sourceMappingURL=registry.js.map
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { serve } from "@hono/node-server";
|
|
2
|
+
import * as Sentry from "@sentry/node";
|
|
3
|
+
import app from "./app.js";
|
|
4
|
+
import { config } from "./config.js";
|
|
5
|
+
import { templateRegistry } from "./registry.js";
|
|
6
|
+
import { apiCallHistoryStore } from "./services/api-call-history.js";
|
|
7
|
+
import { jobLogStore } from "./services/job-logs.js";
|
|
8
|
+
import { compositionWatch } from "./services/composition-watch.js";
|
|
9
|
+
import { Worker } from "./worker.js";
|
|
10
|
+
export async function startRuntime() {
|
|
11
|
+
await templateRegistry.ensureInitialized();
|
|
12
|
+
const worker = new Worker();
|
|
13
|
+
worker.start();
|
|
14
|
+
// No-op unless STORAGE_DRIVER=local (i.e. `vidfarm serve`): fans on-disk
|
|
15
|
+
// composition edits out to open editor tabs over same-origin SSE.
|
|
16
|
+
compositionWatch.start();
|
|
17
|
+
const server = serve({
|
|
18
|
+
fetch: app.fetch,
|
|
19
|
+
port: config.PORT,
|
|
20
|
+
serverOptions: {
|
|
21
|
+
maxHeaderSize: 64 * 1024
|
|
22
|
+
}
|
|
23
|
+
}, (info) => {
|
|
24
|
+
console.log(`[vidfarm] listening on http://localhost:${info.port}`);
|
|
25
|
+
});
|
|
26
|
+
const shutdown = async () => {
|
|
27
|
+
worker.stop();
|
|
28
|
+
await apiCallHistoryStore.shutdown();
|
|
29
|
+
await jobLogStore.shutdown();
|
|
30
|
+
await Sentry.flush(2_000).catch(() => undefined);
|
|
31
|
+
server.close();
|
|
32
|
+
};
|
|
33
|
+
return { server, worker, shutdown };
|
|
34
|
+
}
|
|
35
|
+
//# sourceMappingURL=runtime.js.map
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import { StorageService } from "./storage.js";
|
|
2
|
+
const API_CALL_HISTORY_PREFIX = "api-call-history";
|
|
3
|
+
const CHUNK_INTERVAL_MS = 60_000;
|
|
4
|
+
const CHUNK_MAX_BYTES = 1024 * 1024;
|
|
5
|
+
const FLUSH_INTERVAL_MS = 5_000;
|
|
6
|
+
export class ApiCallHistoryStore {
|
|
7
|
+
storage = new StorageService();
|
|
8
|
+
activeChunks = new Map();
|
|
9
|
+
flushTimer = setInterval(() => {
|
|
10
|
+
void this.flushExpiredChunks();
|
|
11
|
+
}, FLUSH_INTERVAL_MS);
|
|
12
|
+
constructor() {
|
|
13
|
+
this.flushTimer.unref();
|
|
14
|
+
}
|
|
15
|
+
addEntry(entry) {
|
|
16
|
+
const normalized = {
|
|
17
|
+
...entry,
|
|
18
|
+
createdAt: entry.createdAt ?? new Date().toISOString()
|
|
19
|
+
};
|
|
20
|
+
const minuteStartMs = floorToMinute(Date.parse(normalized.createdAt));
|
|
21
|
+
const activePrefix = this.activeChunkPrefix(normalized.customerId, minuteStartMs);
|
|
22
|
+
const current = this.findLatestChunk(activePrefix);
|
|
23
|
+
const entryBytes = serializedEntrySize(normalized);
|
|
24
|
+
const chunk = current && current.sizeBytes + entryBytes <= CHUNK_MAX_BYTES
|
|
25
|
+
? current
|
|
26
|
+
: this.createChunk(normalized.customerId, minuteStartMs, current ? current.sequence + 1 : 0);
|
|
27
|
+
chunk.entries.push(normalized);
|
|
28
|
+
chunk.sizeBytes += entryBytes;
|
|
29
|
+
chunk.dirty = true;
|
|
30
|
+
if (chunk.sizeBytes >= CHUNK_MAX_BYTES) {
|
|
31
|
+
void this.persistChunk(chunk, true);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
async listEntries(input) {
|
|
35
|
+
const persisted = await this.readPersistedEntries(input);
|
|
36
|
+
const buffered = this.readBufferedEntries(input);
|
|
37
|
+
const merged = dedupeAndSort([...persisted, ...buffered]).reverse();
|
|
38
|
+
const filtered = input.cursor
|
|
39
|
+
? merged.filter((entry) => (Date.parse(entry.createdAt) < Date.parse(input.cursor.createdAt)
|
|
40
|
+
|| (entry.createdAt === input.cursor.createdAt && entry.id < input.cursor.id)))
|
|
41
|
+
: merged;
|
|
42
|
+
if (input.limit === undefined) {
|
|
43
|
+
return filtered;
|
|
44
|
+
}
|
|
45
|
+
const limit = Math.max(1, Math.min(input.limit, 501));
|
|
46
|
+
return filtered.slice(0, limit);
|
|
47
|
+
}
|
|
48
|
+
async listStorageKeys(input) {
|
|
49
|
+
return this.listRelevantKeys(input);
|
|
50
|
+
}
|
|
51
|
+
async renameTracer(input) {
|
|
52
|
+
const fromTracer = input.fromTracer.trim();
|
|
53
|
+
const toTracer = input.toTracer.trim();
|
|
54
|
+
if (!fromTracer || !toTracer || fromTracer === toTracer) {
|
|
55
|
+
return { updated: 0 };
|
|
56
|
+
}
|
|
57
|
+
let updated = 0;
|
|
58
|
+
for (const chunk of this.activeChunks.values()) {
|
|
59
|
+
if (chunk.customerId !== input.customerId) {
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
let chunkChanged = false;
|
|
63
|
+
for (const entry of chunk.entries) {
|
|
64
|
+
if (entry.tracer === fromTracer && withinRange(entry.createdAt, input.startTime, input.endTime)) {
|
|
65
|
+
entry.tracer = toTracer;
|
|
66
|
+
updated += 1;
|
|
67
|
+
chunkChanged = true;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
if (chunkChanged) {
|
|
71
|
+
chunk.dirty = true;
|
|
72
|
+
chunk.sizeBytes = chunk.entries.reduce((sum, entry) => sum + serializedEntrySize(entry), 0);
|
|
73
|
+
await this.persistChunk(chunk, false);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
const keys = await this.listRelevantKeys(input);
|
|
77
|
+
await Promise.all(keys.map(async (key) => {
|
|
78
|
+
const content = await this.storage.readText(key);
|
|
79
|
+
const entries = parseChunk(content);
|
|
80
|
+
let changed = false;
|
|
81
|
+
for (const entry of entries) {
|
|
82
|
+
if (entry.customerId === input.customerId
|
|
83
|
+
&& entry.tracer === fromTracer
|
|
84
|
+
&& withinRange(entry.createdAt, input.startTime, input.endTime)) {
|
|
85
|
+
entry.tracer = toTracer;
|
|
86
|
+
updated += 1;
|
|
87
|
+
changed = true;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
if (changed) {
|
|
91
|
+
const body = `${entries.map((entry) => JSON.stringify(entry)).join("\n")}\n`;
|
|
92
|
+
await this.storage.putText(key, body, "application/x-ndjson; charset=utf-8");
|
|
93
|
+
}
|
|
94
|
+
}));
|
|
95
|
+
return { updated };
|
|
96
|
+
}
|
|
97
|
+
async shutdown() {
|
|
98
|
+
clearInterval(this.flushTimer);
|
|
99
|
+
await Promise.all([...this.activeChunks.values()].map((chunk) => this.persistChunk(chunk, true)));
|
|
100
|
+
}
|
|
101
|
+
async readPersistedEntries(input) {
|
|
102
|
+
const keys = await this.listRelevantKeys(input);
|
|
103
|
+
const entries = await Promise.all(keys.map(async (key) => parseChunk(await this.storage.readText(key))));
|
|
104
|
+
return entries
|
|
105
|
+
.flat()
|
|
106
|
+
.filter((entry) => entry.customerId === input.customerId)
|
|
107
|
+
.filter((entry) => withinRange(entry.createdAt, input.startTime, input.endTime));
|
|
108
|
+
}
|
|
109
|
+
readBufferedEntries(input) {
|
|
110
|
+
const buffered = [];
|
|
111
|
+
for (const chunk of this.activeChunks.values()) {
|
|
112
|
+
if (chunk.customerId !== input.customerId) {
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
for (const entry of chunk.entries.slice(chunk.lastPersistedEntryCount)) {
|
|
116
|
+
if (withinRange(entry.createdAt, input.startTime, input.endTime)) {
|
|
117
|
+
buffered.push(entry);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return buffered;
|
|
122
|
+
}
|
|
123
|
+
async listRelevantKeys(input) {
|
|
124
|
+
const minuteStarts = this.minuteStartsForQuery(input.startTime, input.endTime);
|
|
125
|
+
if (minuteStarts === null) {
|
|
126
|
+
return this.storage.listKeys(`${API_CALL_HISTORY_PREFIX}/customer_id=${input.customerId}/`);
|
|
127
|
+
}
|
|
128
|
+
const nested = await Promise.all(minuteStarts.map((minuteStartMs) => (this.storage.listKeys(this.minutePrefix(input.customerId, minuteStartMs)))));
|
|
129
|
+
return nested.flat();
|
|
130
|
+
}
|
|
131
|
+
minuteStartsForQuery(startTime, endTime) {
|
|
132
|
+
if (!startTime && !endTime) {
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
const startMs = floorToMinute(Date.parse(startTime ?? endTime ?? new Date().toISOString()));
|
|
136
|
+
const endMs = floorToMinute(Date.parse(endTime ?? startTime ?? new Date().toISOString()));
|
|
137
|
+
const minMs = Math.min(startMs, endMs);
|
|
138
|
+
const maxMs = Math.max(startMs, endMs);
|
|
139
|
+
const minuteStarts = [];
|
|
140
|
+
for (let cursor = minMs; cursor <= maxMs; cursor += 60_000) {
|
|
141
|
+
minuteStarts.push(cursor);
|
|
142
|
+
}
|
|
143
|
+
return minuteStarts;
|
|
144
|
+
}
|
|
145
|
+
async flushExpiredChunks() {
|
|
146
|
+
const now = Date.now();
|
|
147
|
+
await Promise.all([...this.activeChunks.values()].map(async (chunk) => {
|
|
148
|
+
const intervalElapsed = now >= chunk.minuteStartMs + CHUNK_INTERVAL_MS;
|
|
149
|
+
if (intervalElapsed || chunk.sizeBytes >= CHUNK_MAX_BYTES) {
|
|
150
|
+
await this.persistChunk(chunk, true);
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
if (chunk.dirty) {
|
|
154
|
+
await this.persistChunk(chunk, false);
|
|
155
|
+
}
|
|
156
|
+
}));
|
|
157
|
+
}
|
|
158
|
+
createChunk(customerId, minuteStartMs, sequence) {
|
|
159
|
+
const chunk = {
|
|
160
|
+
customerId,
|
|
161
|
+
minuteStartMs,
|
|
162
|
+
sequence,
|
|
163
|
+
entries: [],
|
|
164
|
+
sizeBytes: 0,
|
|
165
|
+
lastPersistedEntryCount: 0,
|
|
166
|
+
dirty: false
|
|
167
|
+
};
|
|
168
|
+
this.activeChunks.set(this.chunkKeyForMap(customerId, minuteStartMs, sequence), chunk);
|
|
169
|
+
return chunk;
|
|
170
|
+
}
|
|
171
|
+
findLatestChunk(activePrefix) {
|
|
172
|
+
const matching = [...this.activeChunks.entries()]
|
|
173
|
+
.filter(([key]) => key.startsWith(activePrefix))
|
|
174
|
+
.map(([, chunk]) => chunk)
|
|
175
|
+
.sort((a, b) => a.sequence - b.sequence);
|
|
176
|
+
return matching.at(-1) ?? null;
|
|
177
|
+
}
|
|
178
|
+
activeChunkPrefix(customerId, minuteStartMs) {
|
|
179
|
+
return `${customerId}:${minuteStartMs}:`;
|
|
180
|
+
}
|
|
181
|
+
chunkKeyForMap(customerId, minuteStartMs, sequence) {
|
|
182
|
+
return `${this.activeChunkPrefix(customerId, minuteStartMs)}${sequence}`;
|
|
183
|
+
}
|
|
184
|
+
async persistChunk(chunk, finalize) {
|
|
185
|
+
if (!chunk.entries.length) {
|
|
186
|
+
if (finalize) {
|
|
187
|
+
this.activeChunks.delete(this.chunkKeyForMap(chunk.customerId, chunk.minuteStartMs, chunk.sequence));
|
|
188
|
+
}
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
const key = this.objectKeyForChunk(chunk);
|
|
192
|
+
const body = `${chunk.entries.map((entry) => JSON.stringify(entry)).join("\n")}\n`;
|
|
193
|
+
await this.storage.putText(key, body, "application/x-ndjson; charset=utf-8");
|
|
194
|
+
chunk.lastPersistedEntryCount = chunk.entries.length;
|
|
195
|
+
chunk.dirty = false;
|
|
196
|
+
if (finalize) {
|
|
197
|
+
this.activeChunks.delete(this.chunkKeyForMap(chunk.customerId, chunk.minuteStartMs, chunk.sequence));
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
objectKeyForChunk(chunk) {
|
|
201
|
+
const startTs = Math.floor(chunk.entries[0] ? Date.parse(chunk.entries[0].createdAt) / 1000 : chunk.minuteStartMs / 1000);
|
|
202
|
+
const endTs = Math.floor(chunk.entries.at(-1) ? Date.parse(chunk.entries.at(-1).createdAt) / 1000 : chunk.minuteStartMs / 1000);
|
|
203
|
+
return `${this.minutePrefix(chunk.customerId, chunk.minuteStartMs)}chunk-${startTs}-${endTs}-${String(chunk.sequence).padStart(4, "0")}.ndjson`;
|
|
204
|
+
}
|
|
205
|
+
minutePrefix(customerId, minuteStartMs) {
|
|
206
|
+
return `${API_CALL_HISTORY_PREFIX}/customer_id=${customerId}/minute=${Math.floor(minuteStartMs / 1000)}/`;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
export const apiCallHistoryStore = new ApiCallHistoryStore();
|
|
210
|
+
function serializedEntrySize(entry) {
|
|
211
|
+
return Buffer.byteLength(`${JSON.stringify(entry)}\n`, "utf8");
|
|
212
|
+
}
|
|
213
|
+
function floorToMinute(timestampMs) {
|
|
214
|
+
return Math.floor(timestampMs / 60_000) * 60_000;
|
|
215
|
+
}
|
|
216
|
+
function parseChunk(content) {
|
|
217
|
+
if (!content) {
|
|
218
|
+
return [];
|
|
219
|
+
}
|
|
220
|
+
return content
|
|
221
|
+
.split("\n")
|
|
222
|
+
.map((line) => line.trim())
|
|
223
|
+
.filter(Boolean)
|
|
224
|
+
.map((line) => {
|
|
225
|
+
const parsed = JSON.parse(line);
|
|
226
|
+
return {
|
|
227
|
+
...parsed,
|
|
228
|
+
jobId: typeof parsed.jobId === "string" && parsed.jobId.trim() ? parsed.jobId.trim() : null
|
|
229
|
+
};
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
function dedupeAndSort(entries) {
|
|
233
|
+
const deduped = new Map();
|
|
234
|
+
for (const entry of entries) {
|
|
235
|
+
deduped.set(entry.id, entry);
|
|
236
|
+
}
|
|
237
|
+
return [...deduped.values()].sort((a, b) => Date.parse(a.createdAt) - Date.parse(b.createdAt));
|
|
238
|
+
}
|
|
239
|
+
function withinRange(createdAt, startTime, endTime) {
|
|
240
|
+
const value = Date.parse(createdAt);
|
|
241
|
+
if (startTime && value < Date.parse(startTime)) {
|
|
242
|
+
return false;
|
|
243
|
+
}
|
|
244
|
+
if (endTime && value > Date.parse(endTime)) {
|
|
245
|
+
return false;
|
|
246
|
+
}
|
|
247
|
+
return true;
|
|
248
|
+
}
|
|
249
|
+
//# sourceMappingURL=api-call-history.js.map
|