@cloudflare/workers-response-store 0.0.0 → 0.1.0-beta.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/LICENSE +21 -0
- package/README.md +92 -0
- package/dist/binding.d.ts +115 -0
- package/dist/binding.js +369 -0
- package/dist/cache-policy.d.ts +11 -0
- package/dist/cache-policy.js +37 -0
- package/dist/index.d.ts +34 -0
- package/dist/index.js +82 -0
- package/dist/metadata-do.d.ts +65 -0
- package/dist/metadata-do.js +447 -0
- package/dist/service.d.ts +8 -0
- package/dist/service.js +8 -0
- package/package.json +47 -5
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { ResponseStoreBinding, ResponseStoreMutationResult, ResponseStorePurgeOptions, ResponseStorePutOptions, ResponseStoreRefreshOptions, RevalidationInput, RevalidationReason, RevalidationService, SerializableValue, WorkersResponseStore, WorkersResponseStoreEnv } from "./binding.js";
|
|
2
|
+
import { CacheMetadata } from "./metadata-do.js";
|
|
3
|
+
import { WorkerEntrypoint } from "cloudflare:workers";
|
|
4
|
+
//#region src/index.d.ts
|
|
5
|
+
export type ResponseStoreRevalidationContext<Env> = {
|
|
6
|
+
env: Env;
|
|
7
|
+
ctx: ExecutionContext;
|
|
8
|
+
};
|
|
9
|
+
export type ResponseStoreRevalidatorEntrypoint<Env = WorkersResponseStoreEnv> = new (ctx: ExecutionContext, env: Env) => WorkerEntrypoint<Env> & RevalidationService;
|
|
10
|
+
type WorkersResponseStoreDefinition<Env extends WorkersResponseStoreEnv = WorkersResponseStoreEnv> = WorkersResponseStore & {
|
|
11
|
+
entrypoints: {
|
|
12
|
+
CacheMetadata: typeof CacheMetadata;
|
|
13
|
+
ResponseStoreRevalidator: ResponseStoreRevalidatorEntrypoint<Env>;
|
|
14
|
+
ResponseStoreBinding: typeof ResponseStoreBinding;
|
|
15
|
+
};
|
|
16
|
+
};
|
|
17
|
+
type WorkersResponseStoreClientDefinition<Env> = WorkersResponseStore & {
|
|
18
|
+
entrypoints: {
|
|
19
|
+
ResponseStoreRevalidator: ResponseStoreRevalidatorEntrypoint<Env>;
|
|
20
|
+
ResponseStoreClient: new (ctx: ExecutionContext, env: Env) => WorkerEntrypoint<Env> & WorkersResponseStore;
|
|
21
|
+
};
|
|
22
|
+
};
|
|
23
|
+
export type ResponseStoreClientEntrypoint<Env = WorkersResponseStoreClientEnv> = new (ctx: ExecutionContext, env: Env) => WorkerEntrypoint<Env> & WorkersResponseStore;
|
|
24
|
+
export type WorkersResponseStoreOptions<Env> = {
|
|
25
|
+
regenerate(input: RevalidationInput, context: ResponseStoreRevalidationContext<Env>): Response | Promise<Response>;
|
|
26
|
+
};
|
|
27
|
+
export declare function createWorkersResponseStore<Env extends WorkersResponseStoreEnv = WorkersResponseStoreEnv>(options: WorkersResponseStoreOptions<Env>): WorkersResponseStoreDefinition<Env>;
|
|
28
|
+
export type WorkersResponseStoreClientEnv = {
|
|
29
|
+
RESPONSE_STORE: Service;
|
|
30
|
+
CF_VERSION_METADATA: WorkerVersionMetadata;
|
|
31
|
+
};
|
|
32
|
+
export declare function createWorkersResponseStoreClient<Env extends WorkersResponseStoreClientEnv = WorkersResponseStoreClientEnv>(options: WorkersResponseStoreOptions<Env>): WorkersResponseStoreClientDefinition<Env>;
|
|
33
|
+
//#endregion
|
|
34
|
+
export type { ResponseStoreMutationResult, ResponseStorePurgeOptions, ResponseStorePutOptions, ResponseStoreRefreshOptions, RevalidationInput, RevalidationReason, SerializableValue, WorkersResponseStore, WorkersResponseStoreEnv };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { ResponseStoreBinding, getWorkersResponseStore } from "./binding.js";
|
|
2
|
+
import { CacheMetadata } from "./metadata-do.js";
|
|
3
|
+
import { WorkerEntrypoint, exports } from "cloudflare:workers";
|
|
4
|
+
//#region src/index.ts
|
|
5
|
+
function createRevalidatorEntrypoint(options) {
|
|
6
|
+
return class ResponseStoreRevalidator extends WorkerEntrypoint {
|
|
7
|
+
async regenerate(input) {
|
|
8
|
+
return options.regenerate(input, {
|
|
9
|
+
env: this.env,
|
|
10
|
+
ctx: this.ctx
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
function createStoreFacade(getStore) {
|
|
16
|
+
return {
|
|
17
|
+
fetch: (request) => getStore().fetch(request),
|
|
18
|
+
getTagExpiration: (tags) => getStore().getTagExpiration(tags),
|
|
19
|
+
put: (request, response, options) => getStore().put(request, response, options),
|
|
20
|
+
refresh: (options) => getStore().refresh(options),
|
|
21
|
+
purge: (options) => getStore().purge(options)
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
function createWorkersResponseStore(options) {
|
|
25
|
+
const ResponseStoreRevalidator = createRevalidatorEntrypoint(options);
|
|
26
|
+
const getStore = () => getWorkersResponseStore({ exports });
|
|
27
|
+
return {
|
|
28
|
+
entrypoints: {
|
|
29
|
+
CacheMetadata,
|
|
30
|
+
ResponseStoreRevalidator,
|
|
31
|
+
ResponseStoreBinding
|
|
32
|
+
},
|
|
33
|
+
...createStoreFacade(getStore)
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
function createWorkersResponseStoreClient(options) {
|
|
37
|
+
const ResponseStoreRevalidator = createRevalidatorEntrypoint(options);
|
|
38
|
+
class ResponseStoreClient extends WorkerEntrypoint {
|
|
39
|
+
get service() {
|
|
40
|
+
return this.env.RESPONSE_STORE;
|
|
41
|
+
}
|
|
42
|
+
getInvocation() {
|
|
43
|
+
const factory = Reflect.get(this.ctx.exports, "ResponseStoreRevalidator");
|
|
44
|
+
if (typeof factory !== "function") throw new Error("The ResponseStoreRevalidator entrypoint is not exported");
|
|
45
|
+
const versionId = this.env.CF_VERSION_METADATA.id;
|
|
46
|
+
if (!versionId) throw new Error("Workers Response Store requires the user Worker version ID");
|
|
47
|
+
return {
|
|
48
|
+
versionId,
|
|
49
|
+
revalidator: factory({ props: {} })
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
fetch(request) {
|
|
53
|
+
return this.service.read(request, this.getInvocation());
|
|
54
|
+
}
|
|
55
|
+
getTagExpiration(tags) {
|
|
56
|
+
return this.service.getTagExpiration(tags, this.getInvocation());
|
|
57
|
+
}
|
|
58
|
+
put(request, response, putOptions = {}) {
|
|
59
|
+
return this.service.put(request, response, putOptions, this.getInvocation());
|
|
60
|
+
}
|
|
61
|
+
refresh(refreshOptions) {
|
|
62
|
+
return this.service.refresh(refreshOptions, this.getInvocation());
|
|
63
|
+
}
|
|
64
|
+
purge(purgeOptions) {
|
|
65
|
+
return this.service.purge(purgeOptions, this.getInvocation());
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
const getClient = () => {
|
|
69
|
+
const factory = Reflect.get(exports, "ResponseStoreClient");
|
|
70
|
+
if (typeof factory !== "function") throw new Error("The ResponseStoreClient entrypoint is not exported");
|
|
71
|
+
return factory({ props: {} });
|
|
72
|
+
};
|
|
73
|
+
return {
|
|
74
|
+
entrypoints: {
|
|
75
|
+
ResponseStoreRevalidator,
|
|
76
|
+
ResponseStoreClient
|
|
77
|
+
},
|
|
78
|
+
...createStoreFacade(getClient)
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
//#endregion
|
|
82
|
+
export { createWorkersResponseStore, createWorkersResponseStoreClient };
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { CandidateMetadata, PurgedEntry, ResponseStorePurgeOptions, ResponseStoreRefreshOptions, StoredEntry } from "./binding.js";
|
|
2
|
+
import { DurableObject } from "cloudflare:workers";
|
|
3
|
+
//#region src/metadata-do.d.ts
|
|
4
|
+
type RevalidationClaim = {
|
|
5
|
+
claimId: string;
|
|
6
|
+
objectKey: string;
|
|
7
|
+
revision: number;
|
|
8
|
+
};
|
|
9
|
+
type PublicationResult = {
|
|
10
|
+
entry: StoredEntry | null;
|
|
11
|
+
published: boolean;
|
|
12
|
+
};
|
|
13
|
+
type WriteReservation = {
|
|
14
|
+
objectKey: string;
|
|
15
|
+
revision: number;
|
|
16
|
+
};
|
|
17
|
+
type RefreshCandidate = {
|
|
18
|
+
entry: StoredEntry;
|
|
19
|
+
reservation?: Pick<WriteReservation, "objectKey" | "revision">;
|
|
20
|
+
};
|
|
21
|
+
export type CacheMetadataStub = DurableObjectStub & {
|
|
22
|
+
reserveWrite(keyHash: string, cacheKey: string, objectKeyPrefix: string, createdAt: number): Promise<WriteReservation>;
|
|
23
|
+
claimRevalidation(keyHash: string, activeRevision: number, cacheKey: string, objectKeyPrefix: string, now: number, leaseMs: number): Promise<RevalidationClaim | null>;
|
|
24
|
+
trackPendingObject(objectKey: string, createdAt: number): Promise<void>;
|
|
25
|
+
trackPendingObjects(objectKeys: string[], createdAt: number): Promise<void>;
|
|
26
|
+
releaseWrite(keyHash: string, objectKey: string, claimId?: string): Promise<void>;
|
|
27
|
+
finishPendingObjects(objectKeys: string[]): Promise<void>;
|
|
28
|
+
listExpiredPendingObjects(cutoff: number, limit: number): Promise<string[]>;
|
|
29
|
+
sweepExpiredPendingObjects(cutoff?: number): Promise<number>;
|
|
30
|
+
publish(keyHash: string, revision: number, metadata: CandidateMetadata, claimId?: string): Promise<PublicationResult>;
|
|
31
|
+
getEntry(keyHash: string): Promise<StoredEntry | null>;
|
|
32
|
+
getTagExpiration(tags: string[]): Promise<number>;
|
|
33
|
+
reserveRefresh(options: ResponseStoreRefreshOptions, objectKeyRoot: string, createdAt: number): Promise<RefreshCandidate[]>;
|
|
34
|
+
purgeMatching(options: ResponseStorePurgeOptions): Promise<PurgedEntry[]>;
|
|
35
|
+
inspect(): Promise<StoredEntry[]>;
|
|
36
|
+
};
|
|
37
|
+
type CacheMetadataEnv = {
|
|
38
|
+
CACHE_BODIES: R2Bucket;
|
|
39
|
+
};
|
|
40
|
+
export declare class CacheMetadata extends DurableObject<CacheMetadataEnv> {
|
|
41
|
+
private cleanupAlarmKnown;
|
|
42
|
+
constructor(ctx: DurableObjectState, env: CacheMetadataEnv);
|
|
43
|
+
private ensureCleanupAlarm;
|
|
44
|
+
private maintainCleanupAlarm;
|
|
45
|
+
private findMatchingEntryRows;
|
|
46
|
+
trackPendingObjects(objectKeys: string[], createdAt: number): Promise<void>;
|
|
47
|
+
trackPendingObject(objectKey: string, createdAt: number): Promise<void>;
|
|
48
|
+
releaseWrite(keyHash: string, objectKey: string, claimId?: string): void;
|
|
49
|
+
finishPendingObjects(objectKeys: string[]): void;
|
|
50
|
+
private deleteTrackedObjects;
|
|
51
|
+
listExpiredPendingObjects(cutoff: number, limit: number): string[];
|
|
52
|
+
sweepExpiredPendingObjects(cutoff?: number): Promise<number>;
|
|
53
|
+
alarm(): Promise<void>;
|
|
54
|
+
claimRevalidation(keyHash: string, activeRevision: number, cacheKey: string, objectKeyPrefix: string, now: number, leaseMs: number): Promise<RevalidationClaim | null>;
|
|
55
|
+
private reserveRevision;
|
|
56
|
+
reserveWrite(keyHash: string, cacheKey: string, objectKeyPrefix: string, createdAt: number): Promise<WriteReservation>;
|
|
57
|
+
publish(keyHash: string, revision: number, metadata: CandidateMetadata, claimId?: string): Promise<PublicationResult>;
|
|
58
|
+
getEntry(keyHash: string): StoredEntry | null;
|
|
59
|
+
private getTagInvalidationMaximum;
|
|
60
|
+
getTagExpiration(tags: string[]): number;
|
|
61
|
+
reserveRefresh(options: ResponseStoreRefreshOptions, objectKeyRoot: string, createdAt: number): Promise<RefreshCandidate[]>;
|
|
62
|
+
purgeMatching(options: ResponseStorePurgeOptions): Promise<PurgedEntry[]>;
|
|
63
|
+
inspect(): StoredEntry[];
|
|
64
|
+
}
|
|
65
|
+
//#endregion
|
|
@@ -0,0 +1,447 @@
|
|
|
1
|
+
import { DurableObject } from "cloudflare:workers";
|
|
2
|
+
//#region src/metadata-do.ts
|
|
3
|
+
const MAX_SQL_PARAMETERS = 100;
|
|
4
|
+
const R2_DELETE_BATCH_SIZE = 1e3;
|
|
5
|
+
const ORPHAN_RETENTION_MS = 36e5;
|
|
6
|
+
const ORPHAN_CLEANUP_LIMIT = 100;
|
|
7
|
+
const ORPHAN_CLEANUP_RETRY_MS = 6e4;
|
|
8
|
+
function normalizeTags(tags) {
|
|
9
|
+
const normalized = tags.map((tag) => tag.trim().toLowerCase()).filter(Boolean);
|
|
10
|
+
return [...new Set(normalized)];
|
|
11
|
+
}
|
|
12
|
+
function* batches(values, size) {
|
|
13
|
+
for (let offset = 0; offset < values.length; offset += size) yield values.slice(offset, offset + size);
|
|
14
|
+
}
|
|
15
|
+
function storedEntryFromRow(row) {
|
|
16
|
+
if (row.tombstoned || row.active_revision === null || row.object_key === null || row.response_headers === null || row.fresh_until === null || row.swr_until === null) return null;
|
|
17
|
+
return {
|
|
18
|
+
keyHash: row.key_hash,
|
|
19
|
+
cacheKey: row.cache_key,
|
|
20
|
+
activeRevision: row.active_revision,
|
|
21
|
+
latestRevision: row.latest_revision,
|
|
22
|
+
objectKey: row.object_key,
|
|
23
|
+
statusText: row.status_text ?? "",
|
|
24
|
+
responseHeaders: JSON.parse(row.response_headers),
|
|
25
|
+
freshUntil: row.fresh_until,
|
|
26
|
+
swrUntil: row.swr_until,
|
|
27
|
+
revalidator: row.revalidator_id === null ? null : {
|
|
28
|
+
id: row.revalidator_id,
|
|
29
|
+
args: JSON.parse(row.revalidator_args ?? "[]")
|
|
30
|
+
},
|
|
31
|
+
cacheTags: JSON.parse(row.cache_tags ?? "[]")
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
function storedEntriesFromRows(rows) {
|
|
35
|
+
const entries = [];
|
|
36
|
+
for (const row of rows) {
|
|
37
|
+
const entry = storedEntryFromRow(row);
|
|
38
|
+
if (entry) entries.push(entry);
|
|
39
|
+
}
|
|
40
|
+
return entries;
|
|
41
|
+
}
|
|
42
|
+
var CacheMetadata = class extends DurableObject {
|
|
43
|
+
cleanupAlarmKnown = false;
|
|
44
|
+
constructor(ctx, env) {
|
|
45
|
+
super(ctx, env);
|
|
46
|
+
ctx.blockConcurrencyWhile(async () => {
|
|
47
|
+
ctx.storage.sql.exec(`
|
|
48
|
+
CREATE TABLE IF NOT EXISTS entries (
|
|
49
|
+
key_hash TEXT PRIMARY KEY,
|
|
50
|
+
cache_key TEXT NOT NULL,
|
|
51
|
+
active_revision INTEGER,
|
|
52
|
+
latest_revision INTEGER NOT NULL,
|
|
53
|
+
object_key TEXT,
|
|
54
|
+
status_text TEXT,
|
|
55
|
+
response_headers TEXT,
|
|
56
|
+
fresh_until INTEGER,
|
|
57
|
+
swr_until INTEGER,
|
|
58
|
+
revalidator_id TEXT,
|
|
59
|
+
revalidator_args TEXT,
|
|
60
|
+
cache_tags TEXT,
|
|
61
|
+
tombstoned INTEGER NOT NULL DEFAULT 0
|
|
62
|
+
);
|
|
63
|
+
CREATE INDEX IF NOT EXISTS entries_cache_key ON entries(cache_key);
|
|
64
|
+
CREATE TABLE IF NOT EXISTS revalidation_claims (
|
|
65
|
+
key_hash TEXT PRIMARY KEY,
|
|
66
|
+
active_revision INTEGER NOT NULL,
|
|
67
|
+
revision INTEGER NOT NULL,
|
|
68
|
+
claim_id TEXT NOT NULL,
|
|
69
|
+
claimed_at INTEGER NOT NULL,
|
|
70
|
+
expires_at INTEGER NOT NULL
|
|
71
|
+
);
|
|
72
|
+
CREATE TABLE IF NOT EXISTS tag_invalidations (
|
|
73
|
+
tag TEXT PRIMARY KEY,
|
|
74
|
+
invalidated_at INTEGER NOT NULL,
|
|
75
|
+
invalidation_sequence INTEGER NOT NULL DEFAULT 0
|
|
76
|
+
) WITHOUT ROWID;
|
|
77
|
+
CREATE TABLE IF NOT EXISTS metadata_schema_migrations (
|
|
78
|
+
version INTEGER PRIMARY KEY
|
|
79
|
+
);
|
|
80
|
+
CREATE TABLE IF NOT EXISTS metadata_state (
|
|
81
|
+
singleton INTEGER PRIMARY KEY CHECK (singleton = 1),
|
|
82
|
+
tag_invalidation_sequence INTEGER NOT NULL
|
|
83
|
+
);
|
|
84
|
+
INSERT OR IGNORE INTO metadata_state (singleton, tag_invalidation_sequence) VALUES (1, 0);
|
|
85
|
+
CREATE TABLE IF NOT EXISTS pending_objects (
|
|
86
|
+
object_key TEXT PRIMARY KEY,
|
|
87
|
+
invalidation_sequence INTEGER NOT NULL DEFAULT 0,
|
|
88
|
+
publishable INTEGER NOT NULL DEFAULT 1,
|
|
89
|
+
created_at INTEGER NOT NULL
|
|
90
|
+
);
|
|
91
|
+
CREATE INDEX IF NOT EXISTS pending_objects_created_at ON pending_objects(created_at);
|
|
92
|
+
`);
|
|
93
|
+
ctx.storage.transactionSync(() => {
|
|
94
|
+
const migrations = new Set(ctx.storage.sql.exec("SELECT version FROM metadata_schema_migrations WHERE version IN (2, 3)").toArray().map(({ version }) => version));
|
|
95
|
+
if (migrations.size === 2) return;
|
|
96
|
+
const schemas = ctx.storage.sql.exec(`SELECT name, sql FROM sqlite_schema
|
|
97
|
+
WHERE type = 'table' AND name IN ('tag_invalidations', 'pending_objects')`).toArray();
|
|
98
|
+
if (!migrations.has(2) && !schemas.find(({ name }) => name === "tag_invalidations")?.sql.includes("invalidation_sequence")) ctx.storage.sql.exec("ALTER TABLE tag_invalidations ADD COLUMN invalidation_sequence INTEGER NOT NULL DEFAULT 0");
|
|
99
|
+
if (!migrations.has(2) && !schemas.find(({ name }) => name === "pending_objects")?.sql.includes("invalidation_sequence")) ctx.storage.sql.exec("ALTER TABLE pending_objects ADD COLUMN invalidation_sequence INTEGER NOT NULL DEFAULT 0");
|
|
100
|
+
if (!migrations.has(2)) ctx.storage.sql.exec("INSERT INTO metadata_schema_migrations (version) VALUES (2)");
|
|
101
|
+
if (!migrations.has(3)) {
|
|
102
|
+
if (!schemas.find(({ name }) => name === "pending_objects")?.sql.includes("publishable")) ctx.storage.sql.exec("ALTER TABLE pending_objects ADD COLUMN publishable INTEGER NOT NULL DEFAULT 1");
|
|
103
|
+
ctx.storage.sql.exec("INSERT INTO metadata_schema_migrations (version) VALUES (3)");
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
async ensureCleanupAlarm(createdAt) {
|
|
109
|
+
if (this.cleanupAlarmKnown) return;
|
|
110
|
+
if (await this.ctx.storage.getAlarm() === null) await this.ctx.storage.setAlarm(createdAt + ORPHAN_RETENTION_MS);
|
|
111
|
+
this.cleanupAlarmKnown = true;
|
|
112
|
+
}
|
|
113
|
+
async maintainCleanupAlarm(createdAt) {
|
|
114
|
+
await this.ensureCleanupAlarm(createdAt).catch((error) => {
|
|
115
|
+
console.error(JSON.stringify({
|
|
116
|
+
message: "Workers Response Store cleanup alarm update failed",
|
|
117
|
+
error: error instanceof Error ? error.message : String(error)
|
|
118
|
+
}));
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
findMatchingEntryRows(options, includePending = false) {
|
|
122
|
+
const rows = this.ctx.storage.sql.exec(includePending ? `SELECT * FROM entries
|
|
123
|
+
WHERE (tombstoned = 0 AND active_revision IS NOT NULL)
|
|
124
|
+
OR active_revision IS NULL OR latest_revision > active_revision` : "SELECT * FROM entries WHERE tombstoned = 0 AND active_revision IS NOT NULL").toArray();
|
|
125
|
+
if (options.purgeEverything) return rows;
|
|
126
|
+
const tags = new Set(normalizeTags(options.tags ?? []));
|
|
127
|
+
const prefixes = options.pathPrefixes ?? [];
|
|
128
|
+
return rows.filter((row) => prefixes.some((prefix) => row.cache_key.startsWith(prefix)) || normalizeTags(JSON.parse(row.cache_tags ?? "[]")).some((tag) => tags.has(tag)));
|
|
129
|
+
}
|
|
130
|
+
async trackPendingObjects(objectKeys, createdAt) {
|
|
131
|
+
if (!objectKeys.length) return;
|
|
132
|
+
this.ctx.storage.transactionSync(() => {
|
|
133
|
+
for (const batch of batches(objectKeys, MAX_SQL_PARAMETERS / 2)) {
|
|
134
|
+
const values = batch.flatMap((objectKey) => [objectKey, createdAt]);
|
|
135
|
+
this.ctx.storage.sql.exec(`INSERT OR REPLACE INTO pending_objects
|
|
136
|
+
(object_key, created_at, publishable) VALUES ${batch.map(() => "(?, ?, 0)").join(", ")}`, ...values);
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
await this.ensureCleanupAlarm(createdAt);
|
|
140
|
+
}
|
|
141
|
+
trackPendingObject(objectKey, createdAt) {
|
|
142
|
+
return this.trackPendingObjects([objectKey], createdAt);
|
|
143
|
+
}
|
|
144
|
+
releaseWrite(keyHash, objectKey, claimId) {
|
|
145
|
+
this.ctx.storage.transactionSync(() => {
|
|
146
|
+
this.ctx.storage.sql.exec("UPDATE pending_objects SET created_at = 0, publishable = 0 WHERE object_key = ?", objectKey);
|
|
147
|
+
if (claimId) this.ctx.storage.sql.exec("DELETE FROM revalidation_claims WHERE key_hash = ? AND claim_id = ?", keyHash, claimId);
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
finishPendingObjects(objectKeys) {
|
|
151
|
+
if (!objectKeys.length) return;
|
|
152
|
+
this.ctx.storage.transactionSync(() => {
|
|
153
|
+
for (const batch of batches(objectKeys, MAX_SQL_PARAMETERS)) this.ctx.storage.sql.exec(`DELETE FROM pending_objects WHERE object_key IN (${batch.map(() => "?").join(", ")})`, ...batch);
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
async deleteTrackedObjects(objectKeys) {
|
|
157
|
+
for (const batch of batches(objectKeys, R2_DELETE_BATCH_SIZE)) try {
|
|
158
|
+
await this.env.CACHE_BODIES.delete(batch);
|
|
159
|
+
this.finishPendingObjects(batch);
|
|
160
|
+
} catch (error) {
|
|
161
|
+
for (const retryBatch of batches(batch, MAX_SQL_PARAMETERS)) this.ctx.storage.sql.exec(`INSERT OR REPLACE INTO pending_objects
|
|
162
|
+
(object_key, created_at, publishable) VALUES ${retryBatch.map(() => "(?, 0, 0)").join(", ")}`, ...retryBatch);
|
|
163
|
+
console.error(JSON.stringify({
|
|
164
|
+
message: "Workers Response Store R2 cleanup failed",
|
|
165
|
+
objectKeys: batch,
|
|
166
|
+
error: error instanceof Error ? error.message : String(error)
|
|
167
|
+
}));
|
|
168
|
+
try {
|
|
169
|
+
await this.ctx.storage.setAlarm(Date.now() + ORPHAN_CLEANUP_RETRY_MS);
|
|
170
|
+
this.cleanupAlarmKnown = true;
|
|
171
|
+
} catch (alarmError) {
|
|
172
|
+
console.error(JSON.stringify({
|
|
173
|
+
message: "Workers Response Store cleanup retry scheduling failed",
|
|
174
|
+
error: alarmError instanceof Error ? alarmError.message : String(alarmError)
|
|
175
|
+
}));
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
listExpiredPendingObjects(cutoff, limit) {
|
|
180
|
+
return this.ctx.storage.sql.exec(`SELECT pending_objects.object_key FROM pending_objects
|
|
181
|
+
LEFT JOIN entries
|
|
182
|
+
ON entries.object_key = pending_objects.object_key AND entries.tombstoned = 0
|
|
183
|
+
WHERE pending_objects.created_at <= ? AND entries.object_key IS NULL
|
|
184
|
+
ORDER BY pending_objects.created_at
|
|
185
|
+
LIMIT ?`, cutoff, limit).toArray().map((row) => row.object_key);
|
|
186
|
+
}
|
|
187
|
+
async sweepExpiredPendingObjects(cutoff = Date.now() - ORPHAN_RETENTION_MS) {
|
|
188
|
+
this.cleanupAlarmKnown = false;
|
|
189
|
+
const rows = this.ctx.storage.sql.exec(`SELECT pending_objects.object_key, pending_objects.created_at,
|
|
190
|
+
pending_objects.publishable,
|
|
191
|
+
entries.object_key IS NOT NULL AS active
|
|
192
|
+
FROM pending_objects
|
|
193
|
+
LEFT JOIN entries
|
|
194
|
+
ON entries.object_key = pending_objects.object_key AND entries.tombstoned = 0
|
|
195
|
+
ORDER BY pending_objects.created_at
|
|
196
|
+
LIMIT ?`, 101).toArray();
|
|
197
|
+
const batch = rows.slice(0, ORPHAN_CLEANUP_LIMIT);
|
|
198
|
+
const activeObjectKeys = batch.filter(({ active }) => active).map(({ object_key }) => object_key);
|
|
199
|
+
const expired = batch.filter(({ active, created_at }) => !active && created_at <= cutoff);
|
|
200
|
+
const reservations = expired.filter(({ publishable }) => publishable === 1);
|
|
201
|
+
const cleanupObjectKeys = expired.filter(({ publishable }) => publishable === 0).map(({ object_key }) => object_key);
|
|
202
|
+
const fencedAt = Date.now();
|
|
203
|
+
const expiredObjectKeys = expired.map(({ object_key }) => object_key);
|
|
204
|
+
this.finishPendingObjects(activeObjectKeys);
|
|
205
|
+
for (const reservationBatch of batches(reservations, 99)) this.ctx.storage.sql.exec(`UPDATE pending_objects SET created_at = ?, publishable = 0
|
|
206
|
+
WHERE object_key IN (${reservationBatch.map(() => "?").join(", ")})`, fencedAt, ...reservationBatch.map(({ object_key }) => object_key));
|
|
207
|
+
if (expiredObjectKeys.length) {
|
|
208
|
+
await this.env.CACHE_BODIES.delete(expiredObjectKeys);
|
|
209
|
+
this.finishPendingObjects(cleanupObjectKeys);
|
|
210
|
+
}
|
|
211
|
+
const next = batch.find(({ active, created_at }) => !active && created_at > cutoff);
|
|
212
|
+
if (!next && rows.length > ORPHAN_CLEANUP_LIMIT) {
|
|
213
|
+
await this.ctx.storage.setAlarm(Date.now());
|
|
214
|
+
this.cleanupAlarmKnown = true;
|
|
215
|
+
} else if (next || reservations.length) await this.ensureCleanupAlarm(Math.min(next?.created_at ?? Number.POSITIVE_INFINITY, fencedAt));
|
|
216
|
+
return expiredObjectKeys.length;
|
|
217
|
+
}
|
|
218
|
+
async alarm() {
|
|
219
|
+
try {
|
|
220
|
+
await this.sweepExpiredPendingObjects();
|
|
221
|
+
} catch (error) {
|
|
222
|
+
console.error(JSON.stringify({
|
|
223
|
+
message: "Workers Response Store orphan cleanup failed",
|
|
224
|
+
error: error instanceof Error ? error.message : String(error)
|
|
225
|
+
}));
|
|
226
|
+
await this.ctx.storage.setAlarm(Date.now() + ORPHAN_CLEANUP_RETRY_MS);
|
|
227
|
+
this.cleanupAlarmKnown = true;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
async claimRevalidation(keyHash, activeRevision, cacheKey, objectKeyPrefix, now, leaseMs) {
|
|
231
|
+
const claim = this.ctx.storage.transactionSync(() => {
|
|
232
|
+
const entry = this.ctx.storage.sql.exec(`SELECT entries.active_revision, entries.latest_revision, entries.tombstoned,
|
|
233
|
+
revalidation_claims.active_revision AS claim_active_revision,
|
|
234
|
+
revalidation_claims.expires_at AS claim_expires_at
|
|
235
|
+
FROM entries
|
|
236
|
+
LEFT JOIN revalidation_claims ON revalidation_claims.key_hash = entries.key_hash
|
|
237
|
+
WHERE entries.key_hash = ? AND entries.cache_key = ?`, keyHash, cacheKey).toArray()[0];
|
|
238
|
+
if (entry?.tombstoned || entry?.active_revision !== activeRevision) return null;
|
|
239
|
+
if (entry.claim_active_revision === activeRevision && (entry.claim_expires_at ?? 0) > now) return null;
|
|
240
|
+
const revision = entry.latest_revision + 1;
|
|
241
|
+
const claimId = crypto.randomUUID();
|
|
242
|
+
const objectKey = `${objectKeyPrefix}/${revision}`;
|
|
243
|
+
this.ctx.storage.sql.exec("UPDATE entries SET latest_revision = ? WHERE key_hash = ? AND active_revision = ?", revision, keyHash, activeRevision);
|
|
244
|
+
this.ctx.storage.sql.exec(`INSERT OR REPLACE INTO revalidation_claims
|
|
245
|
+
(key_hash, active_revision, revision, claim_id, claimed_at, expires_at)
|
|
246
|
+
VALUES (?, ?, ?, ?, ?, ?)`, keyHash, activeRevision, revision, claimId, now, now + leaseMs);
|
|
247
|
+
this.ctx.storage.sql.exec(`INSERT OR REPLACE INTO pending_objects
|
|
248
|
+
(object_key, created_at, invalidation_sequence)
|
|
249
|
+
VALUES (?, ?, (SELECT tag_invalidation_sequence FROM metadata_state WHERE singleton = 1))`, objectKey, now);
|
|
250
|
+
return {
|
|
251
|
+
claimId,
|
|
252
|
+
objectKey,
|
|
253
|
+
revision
|
|
254
|
+
};
|
|
255
|
+
});
|
|
256
|
+
if (claim) await this.ensureCleanupAlarm(now);
|
|
257
|
+
return claim;
|
|
258
|
+
}
|
|
259
|
+
reserveRevision(keyHash, cacheKey, current) {
|
|
260
|
+
const revision = (current?.latest_revision ?? 0) + 1;
|
|
261
|
+
if (!current) this.ctx.storage.sql.exec("INSERT INTO entries (key_hash, cache_key, latest_revision, tombstoned) VALUES (?, ?, ?, 1)", keyHash, cacheKey, revision);
|
|
262
|
+
else this.ctx.storage.sql.exec("UPDATE entries SET cache_key = ?, latest_revision = ? WHERE key_hash = ?", cacheKey, revision, keyHash);
|
|
263
|
+
return revision;
|
|
264
|
+
}
|
|
265
|
+
async reserveWrite(keyHash, cacheKey, objectKeyPrefix, createdAt) {
|
|
266
|
+
const reservation = this.ctx.storage.transactionSync(() => {
|
|
267
|
+
const current = this.ctx.storage.sql.exec("SELECT active_revision, latest_revision FROM entries WHERE key_hash = ?", keyHash).toArray()[0];
|
|
268
|
+
const revision = this.reserveRevision(keyHash, cacheKey, current);
|
|
269
|
+
const objectKey = `${objectKeyPrefix}/${revision}`;
|
|
270
|
+
this.ctx.storage.sql.exec(`INSERT OR REPLACE INTO pending_objects
|
|
271
|
+
(object_key, created_at, invalidation_sequence)
|
|
272
|
+
VALUES (?, ?, (SELECT tag_invalidation_sequence FROM metadata_state WHERE singleton = 1))`, objectKey, createdAt);
|
|
273
|
+
return {
|
|
274
|
+
objectKey,
|
|
275
|
+
revision
|
|
276
|
+
};
|
|
277
|
+
});
|
|
278
|
+
await this.ensureCleanupAlarm(createdAt);
|
|
279
|
+
return reservation;
|
|
280
|
+
}
|
|
281
|
+
async publish(keyHash, revision, metadata, claimId) {
|
|
282
|
+
const { cleanupObjectKey, result } = this.ctx.storage.transactionSync(() => {
|
|
283
|
+
const current = this.ctx.storage.sql.exec(`SELECT entries.*,
|
|
284
|
+
revalidation_claims.active_revision AS claim_active_revision,
|
|
285
|
+
revalidation_claims.claim_id AS claim_id,
|
|
286
|
+
revalidation_claims.revision AS claim_revision,
|
|
287
|
+
pending_objects.invalidation_sequence AS pending_invalidation_sequence,
|
|
288
|
+
pending_objects.publishable AS pending_publishable,
|
|
289
|
+
metadata_state.tag_invalidation_sequence AS current_invalidation_sequence
|
|
290
|
+
FROM entries
|
|
291
|
+
CROSS JOIN metadata_state
|
|
292
|
+
LEFT JOIN pending_objects ON pending_objects.object_key = ?
|
|
293
|
+
LEFT JOIN revalidation_claims ON revalidation_claims.key_hash = entries.key_hash
|
|
294
|
+
WHERE entries.key_hash = ?`, metadata.objectKey, keyHash).toArray()[0];
|
|
295
|
+
if (!current || current.pending_invalidation_sequence === null || current.pending_invalidation_sequence === void 0 || current.pending_publishable !== 1 || revision > current.latest_revision || current.active_revision !== null && revision <= current.active_revision || claimId !== void 0 && (current.claim_id !== claimId || current.claim_revision !== revision || current.claim_active_revision !== current.active_revision) || metadata.fenceTags.length > 0 && current.current_invalidation_sequence > current.pending_invalidation_sequence && this.getTagInvalidationMaximum(metadata.fenceTags, "invalidation_sequence") > current.pending_invalidation_sequence) {
|
|
296
|
+
if (claimId) this.ctx.storage.sql.exec("DELETE FROM revalidation_claims WHERE key_hash = ? AND claim_id = ?", keyHash, claimId);
|
|
297
|
+
return {
|
|
298
|
+
cleanupObjectKey: current?.object_key === metadata.objectKey ? void 0 : metadata.objectKey,
|
|
299
|
+
result: {
|
|
300
|
+
entry: current ? storedEntryFromRow(current) : null,
|
|
301
|
+
published: false
|
|
302
|
+
}
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
const published = this.ctx.storage.sql.exec(`UPDATE entries SET
|
|
306
|
+
active_revision = ?, object_key = ?, status_text = ?, response_headers = ?,
|
|
307
|
+
fresh_until = ?, swr_until = ?,
|
|
308
|
+
revalidator_id = ?, revalidator_args = ?, cache_tags = ?, tombstoned = 0
|
|
309
|
+
WHERE key_hash = ? AND latest_revision >= ?
|
|
310
|
+
AND (active_revision IS NULL OR active_revision < ?)`, revision, metadata.objectKey, metadata.statusText, JSON.stringify(metadata.responseHeaders), metadata.freshUntil, metadata.swrUntil, metadata.revalidator?.id ?? null, metadata.revalidator ? JSON.stringify(metadata.revalidator.args) : null, JSON.stringify(metadata.cacheTags), keyHash, revision, revision).rowsWritten === 1;
|
|
311
|
+
if (published) this.ctx.storage.sql.exec("DELETE FROM pending_objects WHERE object_key = ?", metadata.objectKey);
|
|
312
|
+
if (published && current.object_key && current.object_key !== metadata.objectKey) this.ctx.storage.sql.exec(`INSERT OR IGNORE INTO pending_objects
|
|
313
|
+
(object_key, created_at, publishable) VALUES (?, ?, 0)`, current.object_key, Date.now());
|
|
314
|
+
if (claimId) this.ctx.storage.sql.exec("DELETE FROM revalidation_claims WHERE key_hash = ? AND claim_id = ?", keyHash, claimId);
|
|
315
|
+
const entry = {
|
|
316
|
+
keyHash,
|
|
317
|
+
cacheKey: current.cache_key,
|
|
318
|
+
activeRevision: revision,
|
|
319
|
+
latestRevision: current.latest_revision,
|
|
320
|
+
objectKey: metadata.objectKey,
|
|
321
|
+
statusText: metadata.statusText,
|
|
322
|
+
responseHeaders: metadata.responseHeaders,
|
|
323
|
+
freshUntil: metadata.freshUntil,
|
|
324
|
+
swrUntil: metadata.swrUntil,
|
|
325
|
+
revalidator: metadata.revalidator,
|
|
326
|
+
cacheTags: metadata.cacheTags
|
|
327
|
+
};
|
|
328
|
+
return {
|
|
329
|
+
cleanupObjectKey: published ? current.object_key && current.object_key !== metadata.objectKey ? current.object_key : void 0 : metadata.objectKey,
|
|
330
|
+
result: {
|
|
331
|
+
entry: published ? entry : null,
|
|
332
|
+
published
|
|
333
|
+
}
|
|
334
|
+
};
|
|
335
|
+
});
|
|
336
|
+
if (cleanupObjectKey) {
|
|
337
|
+
await this.maintainCleanupAlarm(Date.now());
|
|
338
|
+
await this.deleteTrackedObjects([cleanupObjectKey]);
|
|
339
|
+
}
|
|
340
|
+
return result;
|
|
341
|
+
}
|
|
342
|
+
getEntry(keyHash) {
|
|
343
|
+
const row = this.ctx.storage.sql.exec("SELECT * FROM entries WHERE key_hash = ?", keyHash).toArray()[0];
|
|
344
|
+
return row ? storedEntryFromRow(row) : null;
|
|
345
|
+
}
|
|
346
|
+
getTagInvalidationMaximum(tags, column) {
|
|
347
|
+
const normalized = normalizeTags(tags);
|
|
348
|
+
let expiration = 0;
|
|
349
|
+
for (const batch of batches(normalized, MAX_SQL_PARAMETERS)) {
|
|
350
|
+
const placeholders = batch.map(() => "?").join(", ");
|
|
351
|
+
const row = this.ctx.storage.sql.exec(`SELECT MAX(${column}) AS invalidated_at
|
|
352
|
+
FROM tag_invalidations WHERE tag IN (${placeholders})`, ...batch).one();
|
|
353
|
+
expiration = Math.max(expiration, row.invalidated_at ?? 0);
|
|
354
|
+
}
|
|
355
|
+
return expiration;
|
|
356
|
+
}
|
|
357
|
+
getTagExpiration(tags) {
|
|
358
|
+
return this.getTagInvalidationMaximum(tags, "invalidated_at");
|
|
359
|
+
}
|
|
360
|
+
async reserveRefresh(options, objectKeyRoot, createdAt) {
|
|
361
|
+
const candidates = this.ctx.storage.transactionSync(() => {
|
|
362
|
+
const matches = this.findMatchingEntryRows(options);
|
|
363
|
+
const reservations = matches.flatMap((row) => {
|
|
364
|
+
return storedEntryFromRow(row)?.revalidator ? [{
|
|
365
|
+
keyHash: row.key_hash,
|
|
366
|
+
objectKey: `${objectKeyRoot}/${row.key_hash}/${row.latest_revision + 1}`,
|
|
367
|
+
revision: row.latest_revision + 1
|
|
368
|
+
}] : [];
|
|
369
|
+
});
|
|
370
|
+
for (const batch of batches(reservations, MAX_SQL_PARAMETERS)) {
|
|
371
|
+
const keyHashes = batch.map(({ keyHash }) => keyHash);
|
|
372
|
+
this.ctx.storage.sql.exec(`UPDATE entries SET latest_revision = latest_revision + 1
|
|
373
|
+
WHERE key_hash IN (${keyHashes.map(() => "?").join(", ")})`, ...keyHashes);
|
|
374
|
+
}
|
|
375
|
+
for (const batch of batches(reservations, MAX_SQL_PARAMETERS / 2)) this.ctx.storage.sql.exec(`INSERT OR REPLACE INTO pending_objects
|
|
376
|
+
(object_key, created_at, invalidation_sequence) VALUES ${batch.map(() => "(?, ?, (SELECT tag_invalidation_sequence FROM metadata_state WHERE singleton = 1))").join(", ")}`, ...batch.flatMap(({ objectKey }) => [objectKey, createdAt]));
|
|
377
|
+
const byKey = new Map(reservations.map((reservation) => [reservation.keyHash, reservation]));
|
|
378
|
+
return matches.flatMap((row) => {
|
|
379
|
+
const entry = storedEntryFromRow(row);
|
|
380
|
+
if (!entry) return [];
|
|
381
|
+
const reservation = byKey.get(row.key_hash);
|
|
382
|
+
return [{
|
|
383
|
+
entry,
|
|
384
|
+
...reservation ? { reservation: {
|
|
385
|
+
objectKey: reservation.objectKey,
|
|
386
|
+
revision: reservation.revision
|
|
387
|
+
} } : {}
|
|
388
|
+
}];
|
|
389
|
+
});
|
|
390
|
+
});
|
|
391
|
+
if (candidates.some(({ reservation }) => reservation)) await this.ensureCleanupAlarm(createdAt);
|
|
392
|
+
return candidates;
|
|
393
|
+
}
|
|
394
|
+
async purgeMatching(options) {
|
|
395
|
+
const invalidatedAt = Date.now();
|
|
396
|
+
const matches = this.ctx.storage.transactionSync(() => {
|
|
397
|
+
const matches = this.findMatchingEntryRows(options, true);
|
|
398
|
+
const tags = normalizeTags(options.tags ?? []);
|
|
399
|
+
if (tags.length) this.ctx.storage.sql.exec(`UPDATE metadata_state SET tag_invalidation_sequence = tag_invalidation_sequence + 1
|
|
400
|
+
WHERE singleton = 1`);
|
|
401
|
+
for (const batch of batches(tags, MAX_SQL_PARAMETERS / 2)) this.ctx.storage.sql.exec(`INSERT INTO tag_invalidations
|
|
402
|
+
(tag, invalidated_at, invalidation_sequence) VALUES ${batch.map(() => "(?, ?, (SELECT tag_invalidation_sequence FROM metadata_state WHERE singleton = 1))").join(", ")}
|
|
403
|
+
ON CONFLICT(tag) DO UPDATE SET invalidated_at =
|
|
404
|
+
MAX(tag_invalidations.invalidated_at, excluded.invalidated_at),
|
|
405
|
+
invalidation_sequence = MAX(
|
|
406
|
+
tag_invalidations.invalidation_sequence,
|
|
407
|
+
excluded.invalidation_sequence
|
|
408
|
+
)`, ...batch.flatMap((tag) => [tag, invalidatedAt]));
|
|
409
|
+
for (const batch of batches(matches, 99)) {
|
|
410
|
+
const keyHashes = batch.map((row) => row.key_hash);
|
|
411
|
+
const placeholders = keyHashes.map(() => "?").join(", ");
|
|
412
|
+
this.ctx.storage.sql.exec(`INSERT OR IGNORE INTO pending_objects (object_key, created_at, publishable)
|
|
413
|
+
SELECT object_key, ?, 0 FROM entries
|
|
414
|
+
WHERE key_hash IN (${placeholders}) AND object_key IS NOT NULL`, invalidatedAt, ...keyHashes);
|
|
415
|
+
this.ctx.storage.sql.exec(`UPDATE entries SET
|
|
416
|
+
latest_revision = latest_revision + 1,
|
|
417
|
+
active_revision = latest_revision + 1,
|
|
418
|
+
object_key = NULL,
|
|
419
|
+
status_text = NULL,
|
|
420
|
+
response_headers = NULL,
|
|
421
|
+
fresh_until = NULL,
|
|
422
|
+
swr_until = NULL,
|
|
423
|
+
revalidator_id = NULL,
|
|
424
|
+
revalidator_args = NULL,
|
|
425
|
+
cache_tags = NULL,
|
|
426
|
+
tombstoned = 1
|
|
427
|
+
WHERE key_hash IN (${placeholders})`, ...keyHashes);
|
|
428
|
+
this.ctx.storage.sql.exec(`DELETE FROM revalidation_claims WHERE key_hash IN (${placeholders})`, ...keyHashes);
|
|
429
|
+
}
|
|
430
|
+
return matches.flatMap((row) => row.object_key === null ? [] : [{
|
|
431
|
+
keyHash: row.key_hash,
|
|
432
|
+
cacheKey: row.cache_key,
|
|
433
|
+
objectKey: row.object_key
|
|
434
|
+
}]);
|
|
435
|
+
});
|
|
436
|
+
if (matches.length) {
|
|
437
|
+
await this.maintainCleanupAlarm(invalidatedAt);
|
|
438
|
+
await this.deleteTrackedObjects(matches.map((entry) => entry.objectKey));
|
|
439
|
+
}
|
|
440
|
+
return matches;
|
|
441
|
+
}
|
|
442
|
+
inspect() {
|
|
443
|
+
return storedEntriesFromRows(this.ctx.storage.sql.exec("SELECT * FROM entries ORDER BY cache_key").toArray());
|
|
444
|
+
}
|
|
445
|
+
};
|
|
446
|
+
//#endregion
|
|
447
|
+
export { CacheMetadata };
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { ResponseStoreBinding, ResponseStoreService } from "./binding.js";
|
|
2
|
+
import { CacheMetadata } from "./metadata-do.js";
|
|
3
|
+
//#region src/service.d.ts
|
|
4
|
+
declare const _default: {
|
|
5
|
+
fetch(): Response;
|
|
6
|
+
};
|
|
7
|
+
//#endregion
|
|
8
|
+
export { CacheMetadata, ResponseStoreBinding, ResponseStoreService, _default as default };
|
package/dist/service.js
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { ResponseStoreBinding, ResponseStoreService } from "./binding.js";
|
|
2
|
+
import { CacheMetadata } from "./metadata-do.js";
|
|
3
|
+
//#region src/service.ts
|
|
4
|
+
var service_default = { fetch() {
|
|
5
|
+
return new Response("Use the ResponseStoreService service binding entrypoint.", { status: 404 });
|
|
6
|
+
} };
|
|
7
|
+
//#endregion
|
|
8
|
+
export { CacheMetadata, ResponseStoreBinding, ResponseStoreService, service_default as default };
|