@sequenceholdings/artifact-studio 0.2.6 → 0.3.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/VERSION-PIN.md +8 -1
- package/dist/api.d.ts +11 -1
- package/dist/api.js +26 -9
- package/dist/build.d.ts +2 -2
- package/dist/build.js +4 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +105 -10
- package/dist/dev-link.d.ts +1 -1
- package/dist/dev-link.js +2 -1
- package/dist/gateway/admission.d.ts +55 -0
- package/dist/gateway/admission.js +132 -0
- package/dist/gateway/control.d.ts +91 -0
- package/dist/gateway/control.js +113 -0
- package/dist/gateway/hosted-proxy.d.ts +36 -0
- package/dist/gateway/hosted-proxy.js +347 -0
- package/dist/gateway/release-admission.d.ts +23 -0
- package/dist/gateway/release-admission.js +259 -0
- package/dist/gateway/server.d.ts +29 -0
- package/dist/gateway/server.js +298 -0
- package/dist/gateway/session.d.ts +71 -0
- package/dist/gateway/session.js +143 -0
- package/dist/gateway/usage.d.ts +37 -0
- package/dist/gateway/usage.js +46 -0
- package/dist/gateway-main.d.ts +1 -0
- package/dist/gateway-main.js +84 -0
- package/dist/git-clone.d.ts +23 -1
- package/dist/git-clone.js +32 -15
- package/dist/manifest-next.d.ts +217 -0
- package/dist/manifest-next.js +120 -0
- package/dist/manifest-static.d.ts +76 -0
- package/dist/manifest-static.js +124 -0
- package/dist/manifest.d.ts +118 -13
- package/dist/manifest.js +12 -119
- package/dist/next/bridge.d.ts +47 -0
- package/dist/next/bridge.js +229 -0
- package/dist/next/browser-logs.d.ts +7 -0
- package/dist/next/browser-logs.js +51 -0
- package/dist/next/browser.d.ts +6 -0
- package/dist/next/browser.js +57 -0
- package/dist/next/protocol.d.ts +73 -0
- package/dist/next/protocol.js +56 -0
- package/dist/next/release.d.ts +15 -0
- package/dist/next/release.js +46 -0
- package/dist/next/requests.d.ts +15 -0
- package/dist/next/requests.js +59 -0
- package/dist/next/routes.d.ts +4 -0
- package/dist/next/routes.js +21 -0
- package/dist/next/streams.d.ts +16 -0
- package/dist/next/streams.js +132 -0
- package/dist/next/theme.d.ts +10 -0
- package/dist/next/theme.js +45 -0
- package/dist/next/transport.d.ts +15 -0
- package/dist/next/transport.js +13 -0
- package/dist/next-build.d.ts +29 -0
- package/dist/next-build.js +129 -0
- package/dist/next-cache/adapter-runtime.d.ts +51 -0
- package/dist/next-cache/adapter-runtime.js +73 -0
- package/dist/next-cache/component-handler.d.ts +3 -0
- package/dist/next-cache/component-handler.js +10 -0
- package/dist/next-cache/components.d.ts +20 -0
- package/dist/next-cache/components.js +113 -0
- package/dist/next-cache/config.d.ts +9 -0
- package/dist/next-cache/config.js +78 -0
- package/dist/next-cache/incremental-handler.d.ts +18 -0
- package/dist/next-cache/incremental-handler.js +17 -0
- package/dist/next-cache/incremental.d.ts +27 -0
- package/dist/next-cache/incremental.js +99 -0
- package/dist/next-cache/index.d.ts +18 -0
- package/dist/next-cache/index.js +16 -0
- package/dist/next-cache/memory-store.d.ts +27 -0
- package/dist/next-cache/memory-store.js +118 -0
- package/dist/next-cache/redis-scripts.d.ts +3 -0
- package/dist/next-cache/redis-scripts.js +85 -0
- package/dist/next-cache/redis-store.d.ts +18 -0
- package/dist/next-cache/redis-store.js +56 -0
- package/dist/next-cache/runtime.d.ts +29 -0
- package/dist/next-cache/runtime.js +63 -0
- package/dist/next-cache/serialization.d.ts +39 -0
- package/dist/next-cache/serialization.js +333 -0
- package/dist/next-cache/store.d.ts +54 -0
- package/dist/next-cache/store.js +45 -0
- package/dist/next-cache/valkey-transport.d.ts +52 -0
- package/dist/next-cache/valkey-transport.js +189 -0
- package/dist/next-dev-control.d.ts +48 -0
- package/dist/next-dev-control.js +63 -0
- package/dist/next-dev-process.d.ts +17 -0
- package/dist/next-dev-process.js +110 -0
- package/dist/next-dev.d.ts +12 -0
- package/dist/next-dev.js +267 -0
- package/dist/next-image-config.d.ts +9 -0
- package/dist/next-image-config.js +101 -0
- package/dist/next-project.d.ts +22 -0
- package/dist/next-project.js +64 -0
- package/dist/next-public-fonts-local.d.ts +7 -0
- package/dist/next-public-fonts-local.js +64 -0
- package/dist/next-public-fonts.d.ts +13 -0
- package/dist/next-public-fonts.js +49 -0
- package/dist/next-source-hash.d.ts +14 -0
- package/dist/next-source-hash.js +18 -0
- package/dist/next-source.d.ts +35 -0
- package/dist/next-source.js +155 -0
- package/dist/next.d.ts +9 -0
- package/dist/next.js +5 -0
- package/dist/project.d.ts +3 -0
- package/dist/project.js +9 -1
- package/dist/runtime-contracts.d.ts +229 -0
- package/dist/runtime-contracts.js +224 -0
- package/dist/sdk.d.ts +2 -2
- package/dist/server/broker-contract.d.ts +68 -0
- package/dist/server/broker-contract.js +33 -0
- package/dist/server/client.d.ts +67 -0
- package/dist/server/client.js +159 -0
- package/dist/server/logger.d.ts +26 -0
- package/dist/server/logger.js +24 -0
- package/dist/server/release-forms.d.ts +13 -0
- package/dist/server/release-forms.js +124 -0
- package/dist/server/request-context.d.ts +74 -0
- package/dist/server/request-context.js +103 -0
- package/dist/server/runtime-config.d.ts +53 -0
- package/dist/server/runtime-config.js +64 -0
- package/dist/server.d.ts +8 -0
- package/dist/server.js +4 -0
- package/dist/templates/nextjs/.artifactignore +14 -0
- package/dist/templates/nextjs/README.md +151 -0
- package/dist/templates/nextjs/app/about/page.tsx +26 -0
- package/dist/templates/nextjs/app/actions.ts +18 -0
- package/dist/templates/nextjs/app/api/health/route.ts +6 -0
- package/dist/templates/nextjs/app/components/counter.tsx +19 -0
- package/dist/templates/nextjs/app/components/greeting-form.tsx +38 -0
- package/dist/templates/nextjs/app/globals.css +4 -0
- package/dist/templates/nextjs/app/layout.tsx +27 -0
- package/dist/templates/nextjs/app/page.tsx +51 -0
- package/dist/templates/nextjs/app/providers.tsx +107 -0
- package/dist/templates/nextjs/artifact.bundle.yml +55 -0
- package/dist/templates/nextjs/next.config.mjs +27 -0
- package/dist/templates/nextjs/package.json +32 -0
- package/dist/templates/nextjs/pnpm-workspace.yaml +8 -0
- package/dist/templates/nextjs/postcss.config.mjs +5 -0
- package/dist/templates/nextjs/tsconfig.json +20 -0
- package/dist/templates/react-vite/package.json +1 -1
- package/dist/tunnel/broker.d.ts +75 -0
- package/dist/tunnel/broker.js +451 -0
- package/dist/tunnel/client.d.ts +22 -0
- package/dist/tunnel/client.js +177 -0
- package/dist/tunnel/index.d.ts +6 -0
- package/dist/tunnel/index.js +4 -0
- package/dist/tunnel/io.d.ts +33 -0
- package/dist/tunnel/io.js +108 -0
- package/dist/tunnel/peer.d.ts +25 -0
- package/dist/tunnel/peer.js +147 -0
- package/dist/tunnel/protocol.d.ts +69 -0
- package/dist/tunnel/protocol.js +196 -0
- package/dist/tunnel/redis-ownership.d.ts +38 -0
- package/dist/tunnel/redis-ownership.js +100 -0
- package/dist/tunnel/redis-socket.d.ts +13 -0
- package/dist/tunnel/redis-socket.js +48 -0
- package/dist/tunnel/redis-types.d.ts +70 -0
- package/dist/tunnel/redis-types.js +9 -0
- package/dist/tunnel/redis.d.ts +53 -0
- package/dist/tunnel/redis.js +391 -0
- package/dist/tunnel/transport.d.ts +17 -0
- package/dist/tunnel/transport.js +1 -0
- package/package.json +67 -4
- package/templates/nextjs/.artifactignore +14 -0
- package/templates/nextjs/README.md +151 -0
- package/templates/nextjs/app/about/page.tsx +26 -0
- package/templates/nextjs/app/actions.ts +18 -0
- package/templates/nextjs/app/api/health/route.ts +6 -0
- package/templates/nextjs/app/components/counter.tsx +19 -0
- package/templates/nextjs/app/components/greeting-form.tsx +38 -0
- package/templates/nextjs/app/globals.css +4 -0
- package/templates/nextjs/app/layout.tsx +27 -0
- package/templates/nextjs/app/page.tsx +51 -0
- package/templates/nextjs/app/providers.tsx +107 -0
- package/templates/nextjs/artifact.bundle.yml +55 -0
- package/templates/nextjs/next.config.mjs +27 -0
- package/templates/nextjs/package.json +32 -0
- package/templates/nextjs/pnpm-workspace.yaml +8 -0
- package/templates/nextjs/postcss.config.mjs +5 -0
- package/templates/nextjs/tsconfig.json +20 -0
- package/templates/react-vite/package.json +1 -1
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { isNextArtifactRelativePath } from './protocol.js';
|
|
2
|
+
/** One pull grants one <=64KiB chunk; unread responses cannot fill a message queue. */
|
|
3
|
+
export function createNextArtifactStreams({ send }) {
|
|
4
|
+
const active = new Map();
|
|
5
|
+
const instanceId = globalThis.crypto.randomUUID();
|
|
6
|
+
let sequence = 0;
|
|
7
|
+
function open({ path, options, signal }) {
|
|
8
|
+
if (!isNextArtifactRelativePath({ path }) || !path.startsWith('/api/'))
|
|
9
|
+
return Promise.reject(new Error('Artifact stream requires an /api/ path'));
|
|
10
|
+
if (signal?.aborted)
|
|
11
|
+
return Promise.reject(new DOMException('Artifact stream aborted', 'AbortError'));
|
|
12
|
+
if (active.size >= 32)
|
|
13
|
+
return Promise.reject(new Error('Too many Artifact streams'));
|
|
14
|
+
const id = `artifact-next-stream-${instanceId}-${++sequence}`;
|
|
15
|
+
return new Promise((resolve, reject) => {
|
|
16
|
+
let controller;
|
|
17
|
+
let ready = false;
|
|
18
|
+
let credit = false;
|
|
19
|
+
let totalBytes = 0;
|
|
20
|
+
let resolvePull;
|
|
21
|
+
const timeout = setTimeout(() => fail(new Error('Artifact stream timed out')), 120_000);
|
|
22
|
+
const onAbort = () => fail(new DOMException('Artifact stream aborted', 'AbortError'));
|
|
23
|
+
function cleanup({ cancel }) {
|
|
24
|
+
active.delete(id);
|
|
25
|
+
clearTimeout(timeout);
|
|
26
|
+
signal?.removeEventListener('abort', onAbort);
|
|
27
|
+
resolvePull?.();
|
|
28
|
+
if (cancel) {
|
|
29
|
+
try {
|
|
30
|
+
send({ type: 'artifact-studio-bridge-cancel', id });
|
|
31
|
+
}
|
|
32
|
+
catch { /* The parent has already disconnected. */ }
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
function fail(error) {
|
|
36
|
+
if (!active.has(id))
|
|
37
|
+
return;
|
|
38
|
+
cleanup({ cancel: true });
|
|
39
|
+
controller?.error(error);
|
|
40
|
+
reject(error);
|
|
41
|
+
}
|
|
42
|
+
const stream = new ReadableStream({
|
|
43
|
+
start(value) { controller = value; },
|
|
44
|
+
pull() {
|
|
45
|
+
if (!active.has(id) || credit)
|
|
46
|
+
return;
|
|
47
|
+
credit = true;
|
|
48
|
+
return new Promise((done) => {
|
|
49
|
+
resolvePull = done;
|
|
50
|
+
try {
|
|
51
|
+
send({ type: 'artifact-studio-stream-pull', id });
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
fail(new Error('Artifact stream disconnected'));
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
},
|
|
58
|
+
cancel() { cleanup({ cancel: true }); },
|
|
59
|
+
}, { highWaterMark: 0 });
|
|
60
|
+
function receiveReady(payload) {
|
|
61
|
+
if (ready || typeof payload.status !== 'number' || !Number.isInteger(payload.status) || payload.status < 200 || payload.status > 599) {
|
|
62
|
+
fail(new Error('Invalid Artifact stream response'));
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
ready = true;
|
|
66
|
+
const headers = new Headers();
|
|
67
|
+
const rawHeaders = payload.headers;
|
|
68
|
+
if (typeof rawHeaders === 'object' && rawHeaders !== null && 'content-type' in rawHeaders && typeof rawHeaders['content-type'] === 'string') {
|
|
69
|
+
try {
|
|
70
|
+
headers.set('content-type', rawHeaders['content-type']);
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
fail(new Error('Invalid Artifact stream headers'));
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
const nullBody = [204, 205, 304].includes(payload.status);
|
|
78
|
+
resolve(new Response(nullBody ? null : stream, { status: payload.status, headers }));
|
|
79
|
+
if (nullBody) {
|
|
80
|
+
controller?.close();
|
|
81
|
+
cleanup({ cancel: true });
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
function receiveChunk(payload) {
|
|
85
|
+
const chunk = payload.chunk instanceof ArrayBuffer ? new Uint8Array(payload.chunk) : payload.chunk instanceof Uint8Array ? payload.chunk : null;
|
|
86
|
+
if (!ready || !credit || !chunk || chunk.byteLength > 65_536 || totalBytes + chunk.byteLength > 64 * 1024 * 1024) {
|
|
87
|
+
fail(new Error('Invalid or oversized Artifact stream chunk'));
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
credit = false;
|
|
91
|
+
totalBytes += chunk.byteLength;
|
|
92
|
+
controller?.enqueue(chunk);
|
|
93
|
+
resolvePull?.();
|
|
94
|
+
resolvePull = undefined;
|
|
95
|
+
}
|
|
96
|
+
function receive(payload) {
|
|
97
|
+
if (payload.event === 'error') {
|
|
98
|
+
fail(new Error(typeof payload.error === 'string' ? payload.error : 'Artifact stream failed'));
|
|
99
|
+
}
|
|
100
|
+
else if (payload.event === 'ready')
|
|
101
|
+
receiveReady(payload);
|
|
102
|
+
else if (payload.event === 'chunk')
|
|
103
|
+
receiveChunk(payload);
|
|
104
|
+
else if (payload.event === 'done') {
|
|
105
|
+
if (!ready) {
|
|
106
|
+
fail(new Error('Artifact stream ended before response'));
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
cleanup({ cancel: false });
|
|
110
|
+
controller?.close();
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
active.set(id, { receive, fail });
|
|
114
|
+
signal?.addEventListener('abort', onAbort, { once: true });
|
|
115
|
+
try {
|
|
116
|
+
send({ type: 'artifact-studio-stream-open', id, path, options: options ?? {}, flowControl: 'pull' });
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
fail(new Error('Artifact stream could not be opened'));
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
return {
|
|
124
|
+
open,
|
|
125
|
+
receive({ payload }) {
|
|
126
|
+
if (payload.type === 'artifact-studio-stream' && typeof payload.id === 'string')
|
|
127
|
+
active.get(payload.id)?.receive(payload);
|
|
128
|
+
},
|
|
129
|
+
dispose() { for (const stream of active.values())
|
|
130
|
+
stream.fail(new Error('Artifact bridge disposed')); },
|
|
131
|
+
};
|
|
132
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { NextArtifactTheme } from './protocol.js';
|
|
2
|
+
/** Apply a host snapshot without requiring React or reading the DOM during SSR. */
|
|
3
|
+
export declare function createNextArtifactThemeApplier({ root }: {
|
|
4
|
+
root: HTMLElement;
|
|
5
|
+
}): {
|
|
6
|
+
apply({ theme }: {
|
|
7
|
+
theme: NextArtifactTheme;
|
|
8
|
+
}): void;
|
|
9
|
+
dispose(): void;
|
|
10
|
+
};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/** Apply a host snapshot without requiring React or reading the DOM during SSR. */
|
|
2
|
+
export function createNextArtifactThemeApplier({ root }) {
|
|
3
|
+
const originalClass = root.classList.contains('dark');
|
|
4
|
+
const originalTheme = root.getAttribute('data-theme');
|
|
5
|
+
const originalTokens = root.getAttribute('data-tokens');
|
|
6
|
+
const originalProperties = new Map();
|
|
7
|
+
let applied = new Set();
|
|
8
|
+
function attribute({ name, value }) {
|
|
9
|
+
if (value === null)
|
|
10
|
+
root.removeAttribute(name);
|
|
11
|
+
else
|
|
12
|
+
root.setAttribute(name, value);
|
|
13
|
+
}
|
|
14
|
+
function restoreProperty({ name }) {
|
|
15
|
+
const original = originalProperties.get(name);
|
|
16
|
+
if (original?.value)
|
|
17
|
+
root.style.setProperty(name, original.value, original.priority);
|
|
18
|
+
else
|
|
19
|
+
root.style.removeProperty(name);
|
|
20
|
+
}
|
|
21
|
+
return {
|
|
22
|
+
apply({ theme }) {
|
|
23
|
+
root.classList.toggle('dark', theme.dark);
|
|
24
|
+
attribute({ name: 'data-theme', value: theme.theme ?? originalTheme });
|
|
25
|
+
attribute({ name: 'data-tokens', value: theme.tokens ?? originalTokens });
|
|
26
|
+
for (const name of applied)
|
|
27
|
+
if (!(name in theme.vars))
|
|
28
|
+
restoreProperty({ name });
|
|
29
|
+
for (const [name, value] of Object.entries(theme.vars)) {
|
|
30
|
+
if (!originalProperties.has(name))
|
|
31
|
+
originalProperties.set(name, { value: root.style.getPropertyValue(name), priority: root.style.getPropertyPriority(name) });
|
|
32
|
+
root.style.setProperty(name, value);
|
|
33
|
+
}
|
|
34
|
+
applied = new Set(Object.keys(theme.vars));
|
|
35
|
+
},
|
|
36
|
+
dispose() {
|
|
37
|
+
root.classList.toggle('dark', originalClass);
|
|
38
|
+
attribute({ name: 'data-theme', value: originalTheme });
|
|
39
|
+
attribute({ name: 'data-tokens', value: originalTokens });
|
|
40
|
+
for (const name of applied)
|
|
41
|
+
restoreProperty({ name });
|
|
42
|
+
applied.clear();
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export interface NextArtifactMessageEvent {
|
|
2
|
+
origin: string;
|
|
3
|
+
source: unknown;
|
|
4
|
+
data: unknown;
|
|
5
|
+
}
|
|
6
|
+
/** Transport injection keeps SSR import/construction free from browser globals. */
|
|
7
|
+
export interface NextArtifactMessageTransport {
|
|
8
|
+
readonly parent: object;
|
|
9
|
+
listen(listener: (event: NextArtifactMessageEvent) => void): () => void;
|
|
10
|
+
send(input: {
|
|
11
|
+
message: unknown;
|
|
12
|
+
targetOrigin: string;
|
|
13
|
+
}): void;
|
|
14
|
+
}
|
|
15
|
+
export declare function createNextArtifactWindowTransport(): NextArtifactMessageTransport;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export function createNextArtifactWindowTransport() {
|
|
2
|
+
if (typeof window === 'undefined' || window.parent === window)
|
|
3
|
+
throw new Error('Connect the Artifact bridge inside its Atlas iframe');
|
|
4
|
+
const target = window.parent;
|
|
5
|
+
return {
|
|
6
|
+
parent: target,
|
|
7
|
+
listen(listener) {
|
|
8
|
+
window.addEventListener('message', listener);
|
|
9
|
+
return () => window.removeEventListener('message', listener);
|
|
10
|
+
},
|
|
11
|
+
send({ message, targetOrigin }) { target.postMessage(message, targetOrigin); },
|
|
12
|
+
};
|
|
13
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { ArtifactStudioNextManifest } from './manifest.js';
|
|
2
|
+
import type { SourceProvenance, SourceSpec } from './source-resolver.js';
|
|
3
|
+
export declare function planNextProject({ root, manifest, environment }: {
|
|
4
|
+
root: string;
|
|
5
|
+
manifest: ArtifactStudioNextManifest;
|
|
6
|
+
environment: string;
|
|
7
|
+
}): Promise<number>;
|
|
8
|
+
export declare function nextSourceRepository({ spec, root }: {
|
|
9
|
+
spec: SourceSpec;
|
|
10
|
+
root: string;
|
|
11
|
+
}): string;
|
|
12
|
+
export declare function resolveNextRepositoryId({ spec, repository, baseUrl, token }: {
|
|
13
|
+
spec: SourceSpec;
|
|
14
|
+
repository: string;
|
|
15
|
+
baseUrl: string;
|
|
16
|
+
token: string;
|
|
17
|
+
}): Promise<string>;
|
|
18
|
+
export declare function submitNextBuild({ root, manifest, projectId, baseUrl, getToken, provenance, repository, repositoryId, channel, out }: {
|
|
19
|
+
root: string;
|
|
20
|
+
manifest: ArtifactStudioNextManifest;
|
|
21
|
+
projectId: string;
|
|
22
|
+
baseUrl: string;
|
|
23
|
+
getToken: () => Promise<string>;
|
|
24
|
+
provenance: SourceProvenance;
|
|
25
|
+
repository: string;
|
|
26
|
+
repositoryId?: string;
|
|
27
|
+
channel: 'active' | 'preview';
|
|
28
|
+
out?: string;
|
|
29
|
+
}): Promise<number>;
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
3
|
+
import { mkdir, writeFile } from 'node:fs/promises';
|
|
4
|
+
import { dirname } from 'node:path';
|
|
5
|
+
import { setTimeout as delay } from 'node:timers/promises';
|
|
6
|
+
import { z } from 'zod';
|
|
7
|
+
import { getJson, postJson } from './api.js';
|
|
8
|
+
import { resolveRepo } from './git-service-client.js';
|
|
9
|
+
import { readArtifactStudioSource } from './project.js';
|
|
10
|
+
import { validateNextProject } from './next-project.js';
|
|
11
|
+
const operationSchema = z.object({
|
|
12
|
+
operation: z.object({
|
|
13
|
+
id: z.string().min(1), status: z.string().min(1), kind: z.string().optional(),
|
|
14
|
+
error: z.union([z.string(), z.object({ message: z.string() })]).nullable().optional(),
|
|
15
|
+
}),
|
|
16
|
+
deployment: z.object({ id: z.string(), version: z.string() }).nullable().optional(),
|
|
17
|
+
});
|
|
18
|
+
export async function planNextProject({ root, manifest, environment }) {
|
|
19
|
+
await validateNextProject({ root, manifest, installed: false });
|
|
20
|
+
const source = await readArtifactStudioSource(root);
|
|
21
|
+
console.log(`[seq-studio] plan ${manifest.artifact.project_id} -> ${environment}`);
|
|
22
|
+
console.log(` runtime: Next.js ${manifest.runtime.next}, React ${manifest.runtime.react}, Node ${manifest.runtime.node}`);
|
|
23
|
+
console.log(` source: ${source.sourceHash} (${source.files.length} files)`);
|
|
24
|
+
console.log(' action: submit immutable source to Atlas for an isolated build and governed deployment');
|
|
25
|
+
console.log(' local validation only: Atlas validates environment bindings, permissions and policy before building');
|
|
26
|
+
return 0;
|
|
27
|
+
}
|
|
28
|
+
export function nextSourceRepository({ spec, root }) {
|
|
29
|
+
if (spec.kind === 'git-service')
|
|
30
|
+
return `${spec.namespace}/${spec.name}`;
|
|
31
|
+
if (spec.kind === 'git-url')
|
|
32
|
+
return spec.url;
|
|
33
|
+
let origin;
|
|
34
|
+
try {
|
|
35
|
+
origin = execFileSync('git', ['config', '--get', 'remote.origin.url'], { cwd: root, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
throw new Error('Next.js builds require a source repository. Create/clone a repository with seq-studio repos and commit the application before building.');
|
|
39
|
+
}
|
|
40
|
+
if (/^[\w.-]+@[\w.-]+:[\w./-]+$/.test(origin))
|
|
41
|
+
return origin;
|
|
42
|
+
try {
|
|
43
|
+
const url = new URL(origin);
|
|
44
|
+
if (url.protocol === 'https:' && !url.username && !url.password && !url.search && !url.hash)
|
|
45
|
+
return origin;
|
|
46
|
+
}
|
|
47
|
+
catch { /* Refuse opaque/local origins; do not include potential credentials in diagnostics. */ }
|
|
48
|
+
throw new Error('The Git origin must be a credential-free HTTPS or SSH repository URL. Store Git credentials in your credential manager.');
|
|
49
|
+
}
|
|
50
|
+
export async function resolveNextRepositoryId({ spec, repository, baseUrl, token }) {
|
|
51
|
+
if (spec.kind === 'git-service') {
|
|
52
|
+
return (await resolveRepo({ baseUrl, token, namespace: spec.namespace, name: spec.name })).id;
|
|
53
|
+
}
|
|
54
|
+
try {
|
|
55
|
+
const url = new URL(repository);
|
|
56
|
+
const match = /^\/api\/git-service\/repos\/([a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12})\/git\/?$/i.exec(url.pathname);
|
|
57
|
+
if (url.origin === new URL(baseUrl).origin && match)
|
|
58
|
+
return match[1];
|
|
59
|
+
}
|
|
60
|
+
catch { /* Opaque labels/SSH origins are valid preview provenance but cannot attest a release. */ }
|
|
61
|
+
throw new Error('Official Next.js releases require a Git Service repository in this Atlas environment. Push the reviewed revision, then run artifact deploy --repo <namespace>/<name> --ref main --env <env>.');
|
|
62
|
+
}
|
|
63
|
+
/** A verified repository build names a commit; anything else uploads the committed source snapshot. */
|
|
64
|
+
function nextBuildRequestBody({ manifest, files, commit, provenance, repository, repositoryId, channel }) {
|
|
65
|
+
if (repositoryId)
|
|
66
|
+
return { repositoryId, commit, idempotencyKey: randomUUID(), channel };
|
|
67
|
+
return {
|
|
68
|
+
manifest,
|
|
69
|
+
files: files.map(({ path, content, encoding }) => ({ path, content, encoding: encoding ?? 'utf8' })),
|
|
70
|
+
git: { commit, ...(provenance.gitBranch ? { branch: provenance.gitBranch } : {}), dirty: provenance.gitDirty !== false, repository },
|
|
71
|
+
idempotencyKey: randomUUID(), channel,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
export async function submitNextBuild({ root, manifest, projectId, baseUrl, getToken, provenance, repository, repositoryId, channel, out }) {
|
|
75
|
+
if (!provenance.gitCommit || !/^[a-f0-9]{40}$/i.test(provenance.gitCommit)) {
|
|
76
|
+
throw new Error('Commit the application source before requesting a hosted build. Use artifact dev for uncommitted local development.');
|
|
77
|
+
}
|
|
78
|
+
await validateNextProject({ root, manifest, installed: false });
|
|
79
|
+
const source = await readArtifactStudioSource(root);
|
|
80
|
+
if (channel === 'active' && !repositoryId)
|
|
81
|
+
throw new Error('An official release requires a verified Git Service repository.');
|
|
82
|
+
const path = `/api/artifact-studio/projects/${encodeURIComponent(projectId)}/next`;
|
|
83
|
+
const body = nextBuildRequestBody({ manifest, files: source.files, commit: provenance.gitCommit, provenance, repository, repositoryId, channel });
|
|
84
|
+
const response = operationSchema.parse(await postJson({ baseUrl, token: await getToken(), path: `${path}/builds${repositoryId ? '/from-repository' : ''}`, body }));
|
|
85
|
+
console.log(`[seq-studio] Atlas build ${response.operation.id} (${channel}) from ${provenance.gitCommit.slice(0, 12)}`);
|
|
86
|
+
console.log(`${baseUrl}/artifact-studio/projects/${projectId}`);
|
|
87
|
+
const controller = new AbortController();
|
|
88
|
+
const onSignal = () => controller.abort();
|
|
89
|
+
process.once('SIGINT', onSignal);
|
|
90
|
+
process.once('SIGTERM', onSignal);
|
|
91
|
+
let current = response;
|
|
92
|
+
let previousStatus = '';
|
|
93
|
+
try {
|
|
94
|
+
while (!controller.signal.aborted) {
|
|
95
|
+
if (current.operation.status !== previousStatus) {
|
|
96
|
+
previousStatus = current.operation.status;
|
|
97
|
+
console.log(`[seq-studio] ${previousStatus}`);
|
|
98
|
+
}
|
|
99
|
+
if (current.operation.status === 'failed' || current.operation.status === 'cancelled') {
|
|
100
|
+
const error = current.operation.error;
|
|
101
|
+
throw new Error(typeof error === 'string' ? error : error?.message ?? `Atlas build ${current.operation.status}. Inspect its build logs in Artifact Studio.`);
|
|
102
|
+
}
|
|
103
|
+
if (current.operation.status === 'succeeded') {
|
|
104
|
+
const completed = { ...current, deployment: current.deployment ?? response.deployment };
|
|
105
|
+
if (out) {
|
|
106
|
+
await mkdir(dirname(out), { recursive: true });
|
|
107
|
+
await writeFile(out, JSON.stringify(completed, null, 2) + '\n');
|
|
108
|
+
}
|
|
109
|
+
console.log(channel === 'active' ? '[seq-studio] application deployed by Atlas.' : '[seq-studio] hosted preview ready in Artifact Studio.');
|
|
110
|
+
if (completed.deployment)
|
|
111
|
+
console.log(`${baseUrl}/artifact-studio/projects/${projectId}/deployments/${completed.deployment.id}`);
|
|
112
|
+
return 0;
|
|
113
|
+
}
|
|
114
|
+
try {
|
|
115
|
+
await delay(1500, undefined, { signal: controller.signal });
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
break;
|
|
119
|
+
}
|
|
120
|
+
current = operationSchema.parse(await getJson({ baseUrl, token: await getToken(), path: `${path}/operations/${encodeURIComponent(response.operation.id)}` }));
|
|
121
|
+
}
|
|
122
|
+
console.log('[seq-studio] Stopped waiting. The Atlas build continues; inspect or cancel it in Artifact Studio.');
|
|
123
|
+
return 0;
|
|
124
|
+
}
|
|
125
|
+
finally {
|
|
126
|
+
process.removeListener('SIGINT', onSignal);
|
|
127
|
+
process.removeListener('SIGTERM', onSignal);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { type CacheLookup, type NextCacheNamespace, type NextCacheStore } from './store.js';
|
|
2
|
+
export interface NextCacheOptions {
|
|
3
|
+
store: NextCacheStore;
|
|
4
|
+
namespace: NextCacheNamespace;
|
|
5
|
+
maxEntryBytes?: number;
|
|
6
|
+
maxEntryAgeMs?: number;
|
|
7
|
+
streamTimeoutMs?: number;
|
|
8
|
+
/** Framework timestamps use performance.timeOrigin + performance.now(). */
|
|
9
|
+
now?: () => number;
|
|
10
|
+
onCacheError?: (event: {
|
|
11
|
+
operation: 'read' | 'write' | 'invalidate';
|
|
12
|
+
}) => void;
|
|
13
|
+
}
|
|
14
|
+
export interface ObservedLookup extends CacheLookup {
|
|
15
|
+
observedAt: number;
|
|
16
|
+
}
|
|
17
|
+
/** What a later set() needs from a lookup. Retained tickets never keep the entry payload. */
|
|
18
|
+
export type ObservedWriteTicket = Pick<ObservedLookup, 'ticket' | 'observedAt'>;
|
|
19
|
+
/**
|
|
20
|
+
* The ticket a later set() should use after this lookup. The earliest ticket is kept while the store
|
|
21
|
+
* would still accept it, since its observedAt is the fence against renders that began before it. Once
|
|
22
|
+
* the store has moved past it (an invalidation or another commit changed incarnation, epoch or version,
|
|
23
|
+
* or it outlived MAX_CACHE_WRITE_AGE_MS) a regeneration using it would be rejected, so this lookup's
|
|
24
|
+
* ticket replaces it; its later observedAt then fences out renders that began before the change.
|
|
25
|
+
*/
|
|
26
|
+
export declare function nextWriteTicket({ retained, lookup, now }: {
|
|
27
|
+
retained: ObservedWriteTicket | undefined;
|
|
28
|
+
lookup: ObservedLookup;
|
|
29
|
+
now: number;
|
|
30
|
+
}): ObservedWriteTicket;
|
|
31
|
+
export declare class CacheAdapterRuntime {
|
|
32
|
+
readonly store: NextCacheStore;
|
|
33
|
+
readonly namespace: NextCacheNamespace;
|
|
34
|
+
readonly maxEntryBytes: number;
|
|
35
|
+
readonly maxEntryAgeMs: number;
|
|
36
|
+
readonly streamTimeoutMs: number;
|
|
37
|
+
readonly now: () => number;
|
|
38
|
+
private readonly onCacheError?;
|
|
39
|
+
constructor(options: NextCacheOptions);
|
|
40
|
+
report(operation: 'read' | 'write' | 'invalidate'): void;
|
|
41
|
+
read({ key, tags }: {
|
|
42
|
+
key: string;
|
|
43
|
+
tags: string[];
|
|
44
|
+
}): Promise<ObservedLookup | undefined>;
|
|
45
|
+
invalidate({ tags, durations }: {
|
|
46
|
+
tags: string[];
|
|
47
|
+
durations?: {
|
|
48
|
+
expire?: number;
|
|
49
|
+
};
|
|
50
|
+
}): Promise<void>;
|
|
51
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { cacheNamespacePrefix, MAX_CACHE_ENTRY_AGE_MS, MAX_CACHE_ENTRY_BYTES, MAX_CACHE_WRITE_AGE_MS, } from './store.js';
|
|
2
|
+
/**
|
|
3
|
+
* The ticket a later set() should use after this lookup. The earliest ticket is kept while the store
|
|
4
|
+
* would still accept it, since its observedAt is the fence against renders that began before it. Once
|
|
5
|
+
* the store has moved past it (an invalidation or another commit changed incarnation, epoch or version,
|
|
6
|
+
* or it outlived MAX_CACHE_WRITE_AGE_MS) a regeneration using it would be rejected, so this lookup's
|
|
7
|
+
* ticket replaces it; its later observedAt then fences out renders that began before the change.
|
|
8
|
+
*/
|
|
9
|
+
export function nextWriteTicket({ retained, lookup, now }) {
|
|
10
|
+
const current = { ticket: lookup.ticket, observedAt: lookup.observedAt };
|
|
11
|
+
if (!retained)
|
|
12
|
+
return current;
|
|
13
|
+
const unchanged = retained.ticket.incarnation === lookup.ticket.incarnation
|
|
14
|
+
&& retained.ticket.epoch === lookup.ticket.epoch && retained.ticket.version === lookup.ticket.version;
|
|
15
|
+
return unchanged && now - retained.ticket.startedAt <= MAX_CACHE_WRITE_AGE_MS ? retained : current;
|
|
16
|
+
}
|
|
17
|
+
export class CacheAdapterRuntime {
|
|
18
|
+
store;
|
|
19
|
+
namespace;
|
|
20
|
+
maxEntryBytes;
|
|
21
|
+
maxEntryAgeMs;
|
|
22
|
+
streamTimeoutMs;
|
|
23
|
+
now;
|
|
24
|
+
onCacheError;
|
|
25
|
+
constructor(options) {
|
|
26
|
+
cacheNamespacePrefix(options.namespace);
|
|
27
|
+
this.store = options.store;
|
|
28
|
+
this.namespace = { ...options.namespace };
|
|
29
|
+
this.maxEntryBytes = options.maxEntryBytes ?? MAX_CACHE_ENTRY_BYTES;
|
|
30
|
+
this.maxEntryAgeMs = options.maxEntryAgeMs ?? MAX_CACHE_ENTRY_AGE_MS;
|
|
31
|
+
this.streamTimeoutMs = options.streamTimeoutMs ?? 10_000;
|
|
32
|
+
if (!Number.isSafeInteger(this.maxEntryBytes) || this.maxEntryBytes < 1 || this.maxEntryBytes > MAX_CACHE_ENTRY_BYTES
|
|
33
|
+
|| !Number.isSafeInteger(this.maxEntryAgeMs) || this.maxEntryAgeMs < 1 || this.maxEntryAgeMs > MAX_CACHE_ENTRY_AGE_MS
|
|
34
|
+
|| !Number.isSafeInteger(this.streamTimeoutMs) || this.streamTimeoutMs < 1 || this.streamTimeoutMs > 60_000) {
|
|
35
|
+
throw new Error('Invalid Next cache adapter limits');
|
|
36
|
+
}
|
|
37
|
+
this.now = options.now ?? (() => performance.timeOrigin + performance.now());
|
|
38
|
+
this.onCacheError = options.onCacheError;
|
|
39
|
+
}
|
|
40
|
+
report(operation) {
|
|
41
|
+
// Observability must not throw or expose payloads, tags, keys or credentials.
|
|
42
|
+
try {
|
|
43
|
+
this.onCacheError?.({ operation });
|
|
44
|
+
}
|
|
45
|
+
catch { /* Ignore telemetry failure. */ }
|
|
46
|
+
}
|
|
47
|
+
async read({ key, tags }) {
|
|
48
|
+
const observedAt = this.now();
|
|
49
|
+
try {
|
|
50
|
+
return { ...await this.store.read({ namespace: this.namespace, key, tags }), observedAt };
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
this.report('read');
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
async invalidate({ tags, durations }) {
|
|
58
|
+
try {
|
|
59
|
+
// A duration without expire means stale indefinitely. Entries are bounded by
|
|
60
|
+
// maxEntryAgeMs, so this upper bound preserves that behavior without infinity.
|
|
61
|
+
const expireAfterMs = durations ? Math.min(this.maxEntryAgeMs, (durations.expire ?? this.maxEntryAgeMs / 1000) * 1000) : 0;
|
|
62
|
+
if (durations?.expire !== undefined && (!Number.isFinite(durations.expire) || durations.expire < 0)) {
|
|
63
|
+
throw new Error('Invalid cache invalidation duration');
|
|
64
|
+
}
|
|
65
|
+
await this.store.invalidate({ namespace: this.namespace, tags, expireAfterMs: Math.ceil(expireAfterMs) });
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
this.report('invalidate');
|
|
69
|
+
// A mutation must never be reported successful when invalidation is uncertain.
|
|
70
|
+
throw new Error('Next cache invalidation did not complete');
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { getArtifactNextRuntimeCache } from './runtime.js';
|
|
2
|
+
const handler = {
|
|
3
|
+
get: (key, tags) => getArtifactNextRuntimeCache().cacheComponents.get(key, tags),
|
|
4
|
+
set: (key, pending) => getArtifactNextRuntimeCache().cacheComponents.set(key, pending),
|
|
5
|
+
refreshTags: () => getArtifactNextRuntimeCache().cacheComponents.refreshTags(),
|
|
6
|
+
getExpiration: (tags) => getArtifactNextRuntimeCache().cacheComponents.getExpiration(tags),
|
|
7
|
+
updateTags: (tags, durations) => getArtifactNextRuntimeCache().cacheComponents.updateTags(tags, durations),
|
|
8
|
+
};
|
|
9
|
+
// Next's configuration loader requires a default export from handler modules.
|
|
10
|
+
export { handler as default };
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { type NextCacheOptions } from './adapter-runtime.js';
|
|
2
|
+
/** Next.js 16.3 Cache Components contract; no unstable Next internal imports. */
|
|
3
|
+
export interface NextComponentCacheEntry {
|
|
4
|
+
value: ReadableStream<Uint8Array>;
|
|
5
|
+
tags: string[];
|
|
6
|
+
stale: number;
|
|
7
|
+
timestamp: number;
|
|
8
|
+
expire: number;
|
|
9
|
+
revalidate: number;
|
|
10
|
+
}
|
|
11
|
+
export interface NextComponentCacheHandler {
|
|
12
|
+
get(key: string, softTags: string[]): Promise<NextComponentCacheEntry | undefined>;
|
|
13
|
+
set(key: string, pendingEntry: Promise<NextComponentCacheEntry>): Promise<void>;
|
|
14
|
+
refreshTags(): Promise<void>;
|
|
15
|
+
getExpiration(tags: string[]): Promise<number>;
|
|
16
|
+
updateTags(tags: string[], durations?: {
|
|
17
|
+
expire?: number;
|
|
18
|
+
}): Promise<void>;
|
|
19
|
+
}
|
|
20
|
+
export declare function createComponentCacheHandler(options: NextCacheOptions): NextComponentCacheHandler;
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { CacheAdapterRuntime, nextWriteTicket } from './adapter-runtime.js';
|
|
3
|
+
import { createCacheStream, deserializeCacheValue, readCacheStream, serializeCacheValue } from './serialization.js';
|
|
4
|
+
import { normalizeCacheTags } from './store.js';
|
|
5
|
+
const metadataSchema = z.object({
|
|
6
|
+
tags: z.array(z.string()), stale: z.number().finite().nonnegative(),
|
|
7
|
+
timestamp: z.number().finite().nonnegative(), expire: z.number().finite(),
|
|
8
|
+
revalidate: z.number().finite(),
|
|
9
|
+
});
|
|
10
|
+
const recordSchema = metadataSchema.extend({ format: z.literal('component-v1'), bytes: z.instanceof(Uint8Array) });
|
|
11
|
+
function discardStream(value) {
|
|
12
|
+
void value.cancel().catch(() => { });
|
|
13
|
+
}
|
|
14
|
+
async function awaitEntry({ pending, timeoutMs }) {
|
|
15
|
+
let timer;
|
|
16
|
+
try {
|
|
17
|
+
return await Promise.race([pending, new Promise((_, reject) => {
|
|
18
|
+
timer = setTimeout(() => reject(new Error('Pending Next cache entry timed out')), timeoutMs);
|
|
19
|
+
})]);
|
|
20
|
+
}
|
|
21
|
+
catch (error) {
|
|
22
|
+
void pending.then((entry) => discardStream(entry.value), () => { });
|
|
23
|
+
throw error;
|
|
24
|
+
}
|
|
25
|
+
finally {
|
|
26
|
+
clearTimeout(timer);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
export function createComponentCacheHandler(options) {
|
|
30
|
+
const runtime = new CacheAdapterRuntime(options);
|
|
31
|
+
// Process-wide and bounded to 512 keys, so it holds write tickets only, never cached payloads.
|
|
32
|
+
const tickets = new Map();
|
|
33
|
+
const pendingSets = new Map();
|
|
34
|
+
async function persist({ key, pending, lookup }) {
|
|
35
|
+
let stream;
|
|
36
|
+
try {
|
|
37
|
+
const entry = await awaitEntry({ pending, timeoutMs: runtime.streamTimeoutMs });
|
|
38
|
+
stream = entry.value;
|
|
39
|
+
if (!lookup) {
|
|
40
|
+
discardStream(entry.value);
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
const metadata = metadataSchema.parse(entry);
|
|
44
|
+
const ttlMs = Math.min(runtime.maxEntryAgeMs, Math.ceil(metadata.timestamp + metadata.expire * 1000 - runtime.now()));
|
|
45
|
+
// Do not let an older render consume a newer generation's write ticket.
|
|
46
|
+
if (metadata.timestamp < lookup.observedAt || ttlMs <= 0) {
|
|
47
|
+
discardStream(entry.value);
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
const tags = normalizeCacheTags(metadata.tags);
|
|
51
|
+
const bytes = await readCacheStream({ stream: entry.value, maxBytes: runtime.maxEntryBytes, timeoutMs: runtime.streamTimeoutMs });
|
|
52
|
+
const payload = serializeCacheValue({ value: { ...metadata, format: 'component-v1', bytes }, limits: { maxBytes: runtime.maxEntryBytes } });
|
|
53
|
+
await runtime.store.commit({ namespace: runtime.namespace, key: `component:${key}`, ticket: lookup.ticket, payload, tags, ttlMs });
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
if (stream && !stream.locked)
|
|
57
|
+
discardStream(stream);
|
|
58
|
+
runtime.report('write');
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return {
|
|
62
|
+
async get(key, softTags) {
|
|
63
|
+
try {
|
|
64
|
+
await pendingSets.get(key);
|
|
65
|
+
const lookup = await runtime.read({ key: `component:${key}`, tags: normalizeCacheTags(softTags) });
|
|
66
|
+
if (!lookup)
|
|
67
|
+
return undefined;
|
|
68
|
+
// Payload-free: a hit that is never re-rendered must not pin its serialized entry here. A ticket
|
|
69
|
+
// left by an earlier hit is replaced once the store has moved past it (invalidation, another
|
|
70
|
+
// commit, or age), so the first regeneration after an invalidation can actually commit.
|
|
71
|
+
const retained = tickets.get(key);
|
|
72
|
+
const next = nextWriteTicket({ retained, lookup, now: runtime.now() });
|
|
73
|
+
if (next !== retained) {
|
|
74
|
+
if (!retained && tickets.size >= 512)
|
|
75
|
+
tickets.delete(tickets.keys().next().value ?? '');
|
|
76
|
+
tickets.set(key, next);
|
|
77
|
+
}
|
|
78
|
+
if (!lookup.entry)
|
|
79
|
+
return undefined;
|
|
80
|
+
const record = recordSchema.parse(deserializeCacheValue({ serialized: lookup.entry.payload, limits: { maxBytes: runtime.maxEntryBytes } }));
|
|
81
|
+
if (record.expire <= 0 || runtime.now() >= record.timestamp + record.expire * 1000)
|
|
82
|
+
return undefined;
|
|
83
|
+
return { tags: record.tags, stale: record.stale, timestamp: record.timestamp,
|
|
84
|
+
expire: record.expire, revalidate: lookup.entry.stale ? -1 : record.revalidate,
|
|
85
|
+
value: createCacheStream({ bytes: record.bytes }) };
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
runtime.report('read');
|
|
89
|
+
return undefined;
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
async set(key, pendingEntry) {
|
|
93
|
+
const lookup = tickets.get(key);
|
|
94
|
+
tickets.delete(key);
|
|
95
|
+
if (pendingSets.has(key) || pendingSets.size >= 128) {
|
|
96
|
+
await persist({ key, pending: pendingEntry });
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
const pending = persist({ key, pending: pendingEntry, lookup });
|
|
100
|
+
pendingSets.set(key, pending);
|
|
101
|
+
try {
|
|
102
|
+
await pending;
|
|
103
|
+
}
|
|
104
|
+
finally {
|
|
105
|
+
if (pendingSets.get(key) === pending)
|
|
106
|
+
pendingSets.delete(key);
|
|
107
|
+
}
|
|
108
|
+
},
|
|
109
|
+
async refreshTags() { },
|
|
110
|
+
async getExpiration() { return Infinity; }, // Next passes soft tags into get().
|
|
111
|
+
async updateTags(tags, durations) { await runtime.invalidate({ tags, durations }); },
|
|
112
|
+
};
|
|
113
|
+
}
|