better-ship 0.4.1 → 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/dist/{application-rpZoTfzU.js → application-DjsQtpgw.js} +2 -2
- package/dist/application-DjsQtpgw.js.map +1 -0
- package/dist/application.d.ts +3 -2
- package/dist/application.js +1 -1
- package/dist/better-auth/apple-client-secret.d.ts +21 -0
- package/dist/better-auth/apple-client-secret.d.ts.map +1 -0
- package/dist/better-auth/apple-client-secret.js +44 -0
- package/dist/better-auth/apple-client-secret.js.map +1 -0
- package/dist/better-auth/auth-client.d.ts +16 -0
- package/dist/better-auth/auth-client.d.ts.map +1 -0
- package/dist/better-auth/auth-client.js +24 -0
- package/dist/better-auth/auth-client.js.map +1 -0
- package/dist/better-auth/rate-limit.d.ts +34 -0
- package/dist/better-auth/rate-limit.d.ts.map +1 -0
- package/dist/better-auth/rate-limit.js +59 -0
- package/dist/better-auth/rate-limit.js.map +1 -0
- package/dist/better-auth/secondary-storage.d.ts +11 -0
- package/dist/better-auth/secondary-storage.d.ts.map +1 -0
- package/dist/better-auth/secondary-storage.js +18 -0
- package/dist/better-auth/secondary-storage.js.map +1 -0
- package/dist/better-auth/stripe.d.ts +9 -0
- package/dist/better-auth/stripe.d.ts.map +1 -0
- package/dist/better-auth/stripe.js +17 -0
- package/dist/better-auth/stripe.js.map +1 -0
- package/dist/better-auth.d.ts +100 -0
- package/dist/better-auth.d.ts.map +1 -0
- package/dist/better-auth.js +112 -0
- package/dist/better-auth.js.map +1 -0
- package/dist/cloudflare/auth-secondary-storage.d.ts +36 -0
- package/dist/cloudflare/auth-secondary-storage.d.ts.map +1 -0
- package/dist/cloudflare/auth-secondary-storage.js +98 -0
- package/dist/cloudflare/auth-secondary-storage.js.map +1 -0
- package/dist/cloudflare.d.ts +7 -7
- package/dist/cloudflare.d.ts.map +1 -1
- package/dist/cloudflare.js +3 -3
- package/dist/cloudflare.js.map +1 -1
- package/dist/core-Cx5SeyKV.js +709 -0
- package/dist/core-Cx5SeyKV.js.map +1 -0
- package/dist/core.d.ts +2 -2
- package/dist/core.js +2 -2
- package/dist/index-BStnggQO.d.ts +584 -0
- package/dist/index-BStnggQO.d.ts.map +1 -0
- package/dist/{index-BN_tjxME.d.ts → index-DWIRrJMP.d.ts} +21 -50
- package/dist/index-DWIRrJMP.d.ts.map +1 -0
- package/dist/postgres.d.ts +7 -7
- package/dist/postgres.d.ts.map +1 -1
- package/dist/postgres.js +3 -3
- package/dist/postgres.js.map +1 -1
- package/dist/tanstack.d.ts +88 -1
- package/dist/tanstack.d.ts.map +1 -0
- package/dist/tanstack.js +225 -0
- package/dist/tanstack.js.map +1 -0
- package/dist/{unit-of-work-context-CbUJGHlL.d.ts → unit-of-work-context-CPz9rdSS.d.ts} +3 -2
- package/dist/unit-of-work-context-CPz9rdSS.d.ts.map +1 -0
- package/dist/{unit-of-work-context-BLNj72v0.js → unit-of-work-context-DToDpAkv.js} +2 -2
- package/dist/unit-of-work-context-DToDpAkv.js.map +1 -0
- package/package.json +57 -12
- package/src/application/event-collector.ts +3 -3
- package/src/application/handlers.ts +7 -4
- package/src/application/index.ts +1 -1
- package/src/application/message-already-processed.error.ts +1 -3
- package/src/application/message-bus.ts +14 -8
- package/src/application/message-store.ts +2 -2
- package/src/application/registry.ts +14 -13
- package/src/application/unit-of-work.ts +2 -1
- package/src/core/authentication.error.ts +11 -0
- package/src/core/define-messages.ts +136 -0
- package/src/core/entity.ts +33 -0
- package/src/core/error-table.ts +188 -0
- package/src/core/identity.ts +29 -0
- package/src/core/index.ts +50 -0
- package/src/core/internal.errors.ts +21 -0
- package/src/core/logger.ts +27 -15
- package/src/{application → core}/messages.ts +4 -4
- package/src/core/not-authorized.error.ts +11 -0
- package/src/core/problem-details.ts +57 -0
- package/src/core/rate-limit.error.ts +12 -0
- package/src/core/request.errors.ts +86 -0
- package/src/core/stale-session.error.ts +11 -0
- package/src/infrastructure/better-auth/apple-client-secret.ts +54 -0
- package/src/infrastructure/better-auth/auth-client.ts +26 -0
- package/src/infrastructure/better-auth/options.ts +232 -0
- package/src/infrastructure/better-auth/rate-limit.ts +69 -0
- package/src/infrastructure/better-auth/secondary-storage.ts +21 -0
- package/src/infrastructure/better-auth/stripe.ts +11 -0
- package/src/infrastructure/cloudflare/auth-secondary-storage.ts +113 -0
- package/src/infrastructure/cloudflare/base-d1-repository.ts +2 -2
- package/src/infrastructure/cloudflare/d1-message-store.ts +3 -7
- package/src/infrastructure/cloudflare/d1-schema.ts +2 -2
- package/src/infrastructure/postgres/base-postgres-repository.ts +2 -2
- package/src/infrastructure/postgres/postgres-message-store.ts +3 -7
- package/src/infrastructure/postgres/postgres-schema.ts +2 -2
- package/src/infrastructure/tanstack/auth.middleware.ts +89 -0
- package/src/infrastructure/tanstack/error.middleware.ts +81 -0
- package/src/infrastructure/tanstack/http-error-response.ts +30 -0
- package/src/infrastructure/tanstack/index.ts +13 -0
- package/src/infrastructure/tanstack/last-login-method.ts +13 -0
- package/src/infrastructure/tanstack/resolve-failure.ts +24 -0
- package/src/infrastructure/tanstack/session-cookies.ts +36 -0
- package/src/infrastructure/tanstack/to-error-payload.ts +32 -0
- package/src/infrastructure/unit-of-work-context.ts +3 -2
- package/dist/application-rpZoTfzU.js.map +0 -1
- package/dist/core-MjwyZJ4i.js +0 -302
- package/dist/core-MjwyZJ4i.js.map +0 -1
- package/dist/index-BN_tjxME.d.ts.map +0 -1
- package/dist/index-ToddX0m3.d.ts +0 -213
- package/dist/index-ToddX0m3.d.ts.map +0 -1
- package/dist/unit-of-work-context-BLNj72v0.js.map +0 -1
- package/dist/unit-of-work-context-CbUJGHlL.d.ts.map +0 -1
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { vt as AppError } from "../index-BStnggQO.js";
|
|
2
|
+
import { DurableObject } from "cloudflare:workers";
|
|
3
|
+
//#region src/infrastructure/cloudflare/auth-secondary-storage.d.ts
|
|
4
|
+
/** The supplied TTL cannot produce a valid expiry timestamp. Retrying the same input cannot fix it. */
|
|
5
|
+
export declare class InvalidTtlError extends AppError {
|
|
6
|
+
readonly _tag = "InvalidTtlError";
|
|
7
|
+
readonly classification = "terminal";
|
|
8
|
+
constructor();
|
|
9
|
+
}
|
|
10
|
+
/** The stored value cannot be incremented safely. The failed operation leaves it unchanged. */
|
|
11
|
+
export declare class InvalidCounterError extends AppError {
|
|
12
|
+
readonly _tag = "InvalidCounterError";
|
|
13
|
+
readonly classification = "terminal";
|
|
14
|
+
constructor();
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Store one Better Auth secondary storage key per object. Export this class from the application's Worker entrypoint.
|
|
18
|
+
* Schedule cleanup at the caller's expiry; reads enforce expiry even when the alarm runs late.
|
|
19
|
+
* Requires compatibility date 2026-02-24 or later so deleteAll also removes the alarm atomically.
|
|
20
|
+
*/
|
|
21
|
+
export declare class AuthSecondaryStorage extends DurableObject {
|
|
22
|
+
/** Return null for absent or expired values, even when the cleanup alarm has not run. */
|
|
23
|
+
get(): Promise<string | null>;
|
|
24
|
+
/** Replace the value and its expiry. Omit ttl to keep the value until deletion. */
|
|
25
|
+
set(value: string, ttl?: number): Promise<void>;
|
|
26
|
+
/** Remove the value and its cleanup alarm before answering. */
|
|
27
|
+
delete(): Promise<void>;
|
|
28
|
+
/** Consume the value once. Concurrent callers receive null after the first successful consumption. */
|
|
29
|
+
getAndDelete(): Promise<string | null>;
|
|
30
|
+
/** Increment atomically. Apply ttl only when creating a counter or replacing an expired counter. */
|
|
31
|
+
increment(ttl: number): Promise<number>;
|
|
32
|
+
/** Delete expired data. An old alarm must not delete a value whose expiry was extended. */
|
|
33
|
+
alarm(): Promise<void>;
|
|
34
|
+
}
|
|
35
|
+
//#endregion
|
|
36
|
+
//# sourceMappingURL=auth-secondary-storage.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth-secondary-storage.d.ts","names":[],"sources":["../../src/infrastructure/cloudflare/auth-secondary-storage.ts"],"mappings":";;;;qBAWa,wBAAwB;WAC1B;WACA;EAET;;;qBAMW,4BAA4B;WAC9B;WACA;EAET;;;;;;;qBAUW,6BAA6B;;EAElC,OAAO;;EAMP,IAAI,eAAe,eAAe;;EAUlC,UAAU;;EAKV,gBAAgB;;EAQhB,UAAU,cAAc;;EAef,SAAS"}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { N as AppError } from "../core-Cx5SeyKV.js";
|
|
2
|
+
import { DurableObject } from "cloudflare:workers";
|
|
3
|
+
|
|
4
|
+
//#region src/infrastructure/cloudflare/auth-secondary-storage.ts
|
|
5
|
+
const MIN_TTL_SECONDS = 1;
|
|
6
|
+
const MILLISECONDS_PER_SECOND = 1e3;
|
|
7
|
+
/** The supplied TTL cannot produce a valid expiry timestamp. Retrying the same input cannot fix it. */
|
|
8
|
+
var InvalidTtlError = class extends AppError {
|
|
9
|
+
_tag = "InvalidTtlError";
|
|
10
|
+
classification = "terminal";
|
|
11
|
+
constructor() {
|
|
12
|
+
super("TTL must be positive integer seconds with a safe expiry timestamp");
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
/** The stored value cannot be incremented safely. The failed operation leaves it unchanged. */
|
|
16
|
+
var InvalidCounterError = class extends AppError {
|
|
17
|
+
_tag = "InvalidCounterError";
|
|
18
|
+
classification = "terminal";
|
|
19
|
+
constructor() {
|
|
20
|
+
super("Counter must contain a safe integer that can be incremented");
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* Store one Better Auth secondary storage key per object. Export this class from the application's Worker entrypoint.
|
|
25
|
+
* Schedule cleanup at the caller's expiry; reads enforce expiry even when the alarm runs late.
|
|
26
|
+
* Requires compatibility date 2026-02-24 or later so deleteAll also removes the alarm atomically.
|
|
27
|
+
*/
|
|
28
|
+
var AuthSecondaryStorage = class extends DurableObject {
|
|
29
|
+
/** Return null for absent or expired values, even when the cleanup alarm has not run. */
|
|
30
|
+
async get() {
|
|
31
|
+
return unexpiredRecord(await this.ctx.storage.get("value"))?.value ?? null;
|
|
32
|
+
}
|
|
33
|
+
/** Replace the value and its expiry. Omit ttl to keep the value until deletion. */
|
|
34
|
+
async set(value, ttl) {
|
|
35
|
+
const expiresAt = ttl === void 0 ? null : expiry(ttl);
|
|
36
|
+
await this.ctx.storage.transaction(async (transaction) => {
|
|
37
|
+
await transaction.put("value", {
|
|
38
|
+
value,
|
|
39
|
+
expiresAt
|
|
40
|
+
});
|
|
41
|
+
if (expiresAt === null) await transaction.deleteAlarm();
|
|
42
|
+
else await transaction.setAlarm(expiresAt);
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
/** Remove the value and its cleanup alarm before answering. */
|
|
46
|
+
async delete() {
|
|
47
|
+
await this.ctx.storage.deleteAll();
|
|
48
|
+
}
|
|
49
|
+
/** Consume the value once. Concurrent callers receive null after the first successful consumption. */
|
|
50
|
+
async getAndDelete() {
|
|
51
|
+
const record = await this.ctx.storage.get("value");
|
|
52
|
+
await this.ctx.storage.deleteAll();
|
|
53
|
+
return unexpiredRecord(record)?.value ?? null;
|
|
54
|
+
}
|
|
55
|
+
/** Increment atomically. Apply ttl only when creating a counter or replacing an expired counter. */
|
|
56
|
+
async increment(ttl) {
|
|
57
|
+
const expiresAt = expiry(ttl);
|
|
58
|
+
return this.ctx.storage.transaction(async (transaction) => {
|
|
59
|
+
const record = unexpiredRecord(await transaction.get("value"));
|
|
60
|
+
const count = record === void 0 ? 1 : nextCount(record.value);
|
|
61
|
+
await transaction.put("value", {
|
|
62
|
+
value: String(count),
|
|
63
|
+
expiresAt: record === void 0 ? expiresAt : record.expiresAt
|
|
64
|
+
});
|
|
65
|
+
if (record === void 0) await transaction.setAlarm(expiresAt);
|
|
66
|
+
return count;
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
/** Delete expired data. An old alarm must not delete a value whose expiry was extended. */
|
|
70
|
+
async alarm() {
|
|
71
|
+
const record = await this.ctx.storage.get("value");
|
|
72
|
+
if (record?.expiresAt === null) return;
|
|
73
|
+
if (record !== void 0 && record.expiresAt > Date.now()) {
|
|
74
|
+
await this.ctx.storage.setAlarm(record.expiresAt);
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
await this.ctx.storage.deleteAll();
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
function unexpiredRecord(record) {
|
|
81
|
+
if (record === void 0) return void 0;
|
|
82
|
+
if (record.expiresAt !== null && record.expiresAt <= Date.now()) return void 0;
|
|
83
|
+
return record;
|
|
84
|
+
}
|
|
85
|
+
function expiry(ttl) {
|
|
86
|
+
const expiresAt = Date.now() + ttl * MILLISECONDS_PER_SECOND;
|
|
87
|
+
if (!Number.isSafeInteger(ttl) || ttl < MIN_TTL_SECONDS || !Number.isSafeInteger(expiresAt)) throw new InvalidTtlError();
|
|
88
|
+
return expiresAt;
|
|
89
|
+
}
|
|
90
|
+
function nextCount(value) {
|
|
91
|
+
const count = Number(value);
|
|
92
|
+
if (String(count) !== value || !Number.isSafeInteger(count) || !Number.isSafeInteger(count + 1)) throw new InvalidCounterError();
|
|
93
|
+
return count + 1;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
//#endregion
|
|
97
|
+
export { AuthSecondaryStorage, InvalidCounterError, InvalidTtlError };
|
|
98
|
+
//# sourceMappingURL=auth-secondary-storage.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth-secondary-storage.js","names":[],"sources":["../../src/infrastructure/cloudflare/auth-secondary-storage.ts"],"sourcesContent":["import { DurableObject } from 'cloudflare:workers'\n\nimport { AppError } from '@/core'\n\n// Better Auth supplies whole seconds; an omitted TTL retains the value until explicit deletion.\nconst MIN_TTL_SECONDS = 1\nconst MILLISECONDS_PER_SECOND = 1000\n\ntype StoredValue = { value: string; expiresAt: number | null }\n\n/** The supplied TTL cannot produce a valid expiry timestamp. Retrying the same input cannot fix it. */\nexport class InvalidTtlError extends AppError {\n readonly _tag = 'InvalidTtlError'\n readonly classification = 'terminal'\n\n constructor() {\n super('TTL must be positive integer seconds with a safe expiry timestamp')\n }\n}\n\n/** The stored value cannot be incremented safely. The failed operation leaves it unchanged. */\nexport class InvalidCounterError extends AppError {\n readonly _tag = 'InvalidCounterError'\n readonly classification = 'terminal'\n\n constructor() {\n super('Counter must contain a safe integer that can be incremented')\n }\n}\n\n/**\n * Store one Better Auth secondary storage key per object. Export this class from the application's Worker entrypoint.\n * Schedule cleanup at the caller's expiry; reads enforce expiry even when the alarm runs late.\n * Requires compatibility date 2026-02-24 or later so deleteAll also removes the alarm atomically.\n */\nexport class AuthSecondaryStorage extends DurableObject {\n /** Return null for absent or expired values, even when the cleanup alarm has not run. */\n async get(): Promise<string | null> {\n const record = await this.ctx.storage.get<StoredValue>('value')\n return unexpiredRecord(record)?.value ?? null\n }\n\n /** Replace the value and its expiry. Omit ttl to keep the value until deletion. */\n async set(value: string, ttl?: number): Promise<void> {\n const expiresAt = ttl === undefined ? null : expiry(ttl)\n await this.ctx.storage.transaction(async (transaction) => {\n await transaction.put('value', { value, expiresAt })\n if (expiresAt === null) await transaction.deleteAlarm()\n else await transaction.setAlarm(expiresAt)\n })\n }\n\n /** Remove the value and its cleanup alarm before answering. */\n async delete(): Promise<void> {\n await this.ctx.storage.deleteAll()\n }\n\n /** Consume the value once. Concurrent callers receive null after the first successful consumption. */\n async getAndDelete(): Promise<string | null> {\n // Storage input gates prevent another request from replacing the value between these operations.\n const record = await this.ctx.storage.get<StoredValue>('value')\n await this.ctx.storage.deleteAll()\n return unexpiredRecord(record)?.value ?? null\n }\n\n /** Increment atomically. Apply ttl only when creating a counter or replacing an expired counter. */\n async increment(ttl: number): Promise<number> {\n const expiresAt = expiry(ttl)\n return this.ctx.storage.transaction(async (transaction) => {\n const record = unexpiredRecord(await transaction.get<StoredValue>('value'))\n const count = record === undefined ? 1 : nextCount(record.value)\n await transaction.put('value', {\n value: String(count),\n expiresAt: record === undefined ? expiresAt : record.expiresAt,\n })\n if (record === undefined) await transaction.setAlarm(expiresAt)\n return count\n })\n }\n\n /** Delete expired data. An old alarm must not delete a value whose expiry was extended. */\n override async alarm(): Promise<void> {\n const record = await this.ctx.storage.get<StoredValue>('value')\n if (record?.expiresAt === null) return\n if (record !== undefined && record.expiresAt > Date.now()) {\n await this.ctx.storage.setAlarm(record.expiresAt)\n return\n }\n await this.ctx.storage.deleteAll()\n }\n}\n\nfunction unexpiredRecord(record: StoredValue | undefined): StoredValue | undefined {\n if (record === undefined) return undefined\n if (record.expiresAt !== null && record.expiresAt <= Date.now()) return undefined\n return record\n}\n\nfunction expiry(ttl: number): number {\n const expiresAt = Date.now() + ttl * MILLISECONDS_PER_SECOND\n if (!Number.isSafeInteger(ttl) || ttl < MIN_TTL_SECONDS || !Number.isSafeInteger(expiresAt)) {\n throw new InvalidTtlError()\n }\n return expiresAt\n}\n\nfunction nextCount(value: string): number {\n const count = Number(value)\n if (String(count) !== value || !Number.isSafeInteger(count) || !Number.isSafeInteger(count + 1)) {\n throw new InvalidCounterError()\n }\n return count + 1\n}\n"],"mappings":";;;;AAKA,MAAM,kBAAkB;AACxB,MAAM,0BAA0B;;AAKhC,IAAa,kBAAb,cAAqC,SAAS;CAC5C,AAAS,OAAO;CAChB,AAAS,iBAAiB;CAE1B,cAAc;EACZ,MAAM,mEAAmE;CAC3E;AACF;;AAGA,IAAa,sBAAb,cAAyC,SAAS;CAChD,AAAS,OAAO;CAChB,AAAS,iBAAiB;CAE1B,cAAc;EACZ,MAAM,6DAA6D;CACrE;AACF;;;;;;AAOA,IAAa,uBAAb,cAA0C,cAAc;;CAEtD,MAAM,MAA8B;EAElC,OAAO,gBAAgB,MADF,KAAK,IAAI,QAAQ,IAAiB,OAAO,CACjC,CAAC,EAAE,SAAS;CAC3C;;CAGA,MAAM,IAAI,OAAe,KAA6B;EACpD,MAAM,YAAY,QAAQ,SAAY,OAAO,OAAO,GAAG;EACvD,MAAM,KAAK,IAAI,QAAQ,YAAY,OAAO,gBAAgB;GACxD,MAAM,YAAY,IAAI,SAAS;IAAE;IAAO;GAAU,CAAC;GACnD,IAAI,cAAc,MAAM,MAAM,YAAY,YAAY;QACjD,MAAM,YAAY,SAAS,SAAS;EAC3C,CAAC;CACH;;CAGA,MAAM,SAAwB;EAC5B,MAAM,KAAK,IAAI,QAAQ,UAAU;CACnC;;CAGA,MAAM,eAAuC;EAE3C,MAAM,SAAS,MAAM,KAAK,IAAI,QAAQ,IAAiB,OAAO;EAC9D,MAAM,KAAK,IAAI,QAAQ,UAAU;EACjC,OAAO,gBAAgB,MAAM,CAAC,EAAE,SAAS;CAC3C;;CAGA,MAAM,UAAU,KAA8B;EAC5C,MAAM,YAAY,OAAO,GAAG;EAC5B,OAAO,KAAK,IAAI,QAAQ,YAAY,OAAO,gBAAgB;GACzD,MAAM,SAAS,gBAAgB,MAAM,YAAY,IAAiB,OAAO,CAAC;GAC1E,MAAM,QAAQ,WAAW,SAAY,IAAI,UAAU,OAAO,KAAK;GAC/D,MAAM,YAAY,IAAI,SAAS;IAC7B,OAAO,OAAO,KAAK;IACnB,WAAW,WAAW,SAAY,YAAY,OAAO;GACvD,CAAC;GACD,IAAI,WAAW,QAAW,MAAM,YAAY,SAAS,SAAS;GAC9D,OAAO;EACT,CAAC;CACH;;CAGA,MAAe,QAAuB;EACpC,MAAM,SAAS,MAAM,KAAK,IAAI,QAAQ,IAAiB,OAAO;EAC9D,IAAI,QAAQ,cAAc,MAAM;EAChC,IAAI,WAAW,UAAa,OAAO,YAAY,KAAK,IAAI,GAAG;GACzD,MAAM,KAAK,IAAI,QAAQ,SAAS,OAAO,SAAS;GAChD;EACF;EACA,MAAM,KAAK,IAAI,QAAQ,UAAU;CACnC;AACF;AAEA,SAAS,gBAAgB,QAA0D;CACjF,IAAI,WAAW,QAAW,OAAO;CACjC,IAAI,OAAO,cAAc,QAAQ,OAAO,aAAa,KAAK,IAAI,GAAG,OAAO;CACxE,OAAO;AACT;AAEA,SAAS,OAAO,KAAqB;CACnC,MAAM,YAAY,KAAK,IAAI,IAAI,MAAM;CACrC,IAAI,CAAC,OAAO,cAAc,GAAG,KAAK,MAAM,mBAAmB,CAAC,OAAO,cAAc,SAAS,GACxF,MAAM,IAAI,gBAAgB;CAE5B,OAAO;AACT;AAEA,SAAS,UAAU,OAAuB;CACxC,MAAM,QAAQ,OAAO,KAAK;CAC1B,IAAI,OAAO,KAAK,MAAM,SAAS,CAAC,OAAO,cAAc,KAAK,KAAK,CAAC,OAAO,cAAc,QAAQ,CAAC,GAC5F,MAAM,IAAI,oBAAoB;CAEhC,OAAO,QAAQ;AACjB"}
|
package/dist/cloudflare.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { n as DrizzleSchema, t as eventCollector } from "./unit-of-work-context-
|
|
1
|
+
import { G as DatabaseFailure, I as MessageId, N as DomainEvent, U as D1ConstraintKind, W as DatabaseError } from "./index-BStnggQO.js";
|
|
2
|
+
import { s as ITransaction, v as IMessageStore } from "./index-DWIRrJMP.js";
|
|
3
|
+
import { n as DrizzleSchema, t as eventCollector } from "./unit-of-work-context-CPz9rdSS.js";
|
|
4
4
|
import { BatchItem } from "drizzle-orm/batch";
|
|
5
5
|
import { AnyD1Database, DrizzleD1Database } from "drizzle-orm/d1";
|
|
6
6
|
//#region src/infrastructure/cloudflare/d1-database-types.d.ts
|
|
@@ -18,7 +18,7 @@ type D1Tx<Schema extends DrizzleSchema> = {
|
|
|
18
18
|
//#region src/infrastructure/cloudflare/base-d1-repository.d.ts
|
|
19
19
|
/** A domain aggregate that accumulates events for the unit of work to drain. */
|
|
20
20
|
interface AggregateEvents {
|
|
21
|
-
collectEvents(): ReadonlyArray<
|
|
21
|
+
collectEvents(): ReadonlyArray<DomainEvent>;
|
|
22
22
|
clearEvents(): void;
|
|
23
23
|
}
|
|
24
24
|
/**
|
|
@@ -47,7 +47,7 @@ export declare function classifyD1Error(cause: unknown, operation?: string): Dat
|
|
|
47
47
|
/** Processed messages and outbox events share one D1 batch; the primary key prevents concurrent duplicate commits. */
|
|
48
48
|
export declare class D1MessageStore<Schema extends DrizzleSchema> extends BaseD1Repository<Schema> implements IMessageStore {
|
|
49
49
|
claim(messageId: MessageId): Promise<void>;
|
|
50
|
-
persistEvents(events: ReadonlyArray<
|
|
50
|
+
persistEvents(events: ReadonlyArray<DomainEvent>): Promise<void>;
|
|
51
51
|
}
|
|
52
52
|
//#endregion
|
|
53
53
|
//#region src/infrastructure/cloudflare/d1-schema.d.ts
|
|
@@ -124,7 +124,7 @@ export declare const messageOutbox: import("drizzle-orm/sqlite-core").SQLiteTabl
|
|
|
124
124
|
tableName: "message_outbox";
|
|
125
125
|
dataType: "json";
|
|
126
126
|
columnType: "SQLiteTextJson";
|
|
127
|
-
data:
|
|
127
|
+
data: DomainEvent;
|
|
128
128
|
driverParam: string;
|
|
129
129
|
notNull: true;
|
|
130
130
|
hasDefault: false;
|
|
@@ -136,7 +136,7 @@ export declare const messageOutbox: import("drizzle-orm/sqlite-core").SQLiteTabl
|
|
|
136
136
|
identity: undefined;
|
|
137
137
|
generated: undefined;
|
|
138
138
|
}, {}, {
|
|
139
|
-
$type:
|
|
139
|
+
$type: DomainEvent;
|
|
140
140
|
}>;
|
|
141
141
|
createdAt: import("drizzle-orm/sqlite-core").SQLiteColumn<{
|
|
142
142
|
name: "created_at";
|
package/dist/cloudflare.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cloudflare.d.ts","names":[],"sources":["../src/infrastructure/cloudflare/d1-database-types.ts","../src/infrastructure/cloudflare/base-d1-repository.ts","../src/infrastructure/cloudflare/classify-d1-error.ts","../src/infrastructure/cloudflare/d1-message-store.ts","../src/infrastructure/cloudflare/d1-schema.ts","../src/infrastructure/cloudflare/d1-batch-transaction.ts","../src/infrastructure/cloudflare/d1-database-execution.ts"],"mappings":";;;;;;;KAKY,eAAe;;KAGf,KAAK,eAAe,iBAAiB,kBAAkB;;KAGvD,SAAS,eAAe,iBAAiB,KACnD,KAAK;;KAKK,KAAK,eAAe;WACrB,IAAI,KAAK;;;;;UCRH;EACf,iBAAiB,cAAc;EAC/B;;;;;;8BAOoB,iBAAiB,eAAe;qBACrB,IAAI,KAAK;EAAxC,YAA+B,IAAI,KAAK;;YAG9B,MAAM,WAAW;;YAKjB,KAAK,GAAG,mBAAmB,aAAa,YAAY,KAAK,QAAQ;;YAKjE,eAAe,WAAW;;;;;;;;wBCVtB,gBAAgB,gBAAgB,qBAAyB;;;;
|
|
1
|
+
{"version":3,"file":"cloudflare.d.ts","names":[],"sources":["../src/infrastructure/cloudflare/d1-database-types.ts","../src/infrastructure/cloudflare/base-d1-repository.ts","../src/infrastructure/cloudflare/classify-d1-error.ts","../src/infrastructure/cloudflare/d1-message-store.ts","../src/infrastructure/cloudflare/d1-schema.ts","../src/infrastructure/cloudflare/d1-batch-transaction.ts","../src/infrastructure/cloudflare/d1-database-execution.ts"],"mappings":";;;;;;;KAKY,eAAe;;KAGf,KAAK,eAAe,iBAAiB,kBAAkB;;KAGvD,SAAS,eAAe,iBAAiB,KACnD,KAAK;;KAKK,KAAK,eAAe;WACrB,IAAI,KAAK;;;;;UCRH;EACf,iBAAiB,cAAc;EAC/B;;;;;;8BAOoB,iBAAiB,eAAe;qBACrB,IAAI,KAAK;EAAxC,YAA+B,IAAI,KAAK;;YAG9B,MAAM,WAAW;;YAKjB,KAAK,GAAG,mBAAmB,aAAa,YAAY,KAAK,QAAQ;;YAKjE,eAAe,WAAW;;;;;;;;wBCVtB,gBAAgB,gBAAgB,qBAAyB;;;;qBCb5D,eAAe,eAAe,uBACjC,iBAAiB,mBACd;EAEL,MAAM,WAAW,YAAY;EAc7B,cAAc,QAAQ,cAAc,eAAe;;;;;qBCtB9C,qDAAiB;;;;;;;;;;;;;;;;;;;;;;;;;;;;YAAjB;;;;;;;;;;;;;;;;qBAYA,iDAAa;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;qBCPb,mBAAmB,eAAe,0BAA0B,aACvE,KAAK;mBAEwB;EAA7B,YAA6B,IAAI,KAAK;EAEhC,YAAY,GAAG,OAAO,IAAI,KAAK,YAAY,QAAQ,KAAK,QAAQ;;;;;wBCblD,gBAAgB,GACpC,mBACA,YAAY,YAAY,KACvB,QAAQ"}
|
package/dist/cloudflare.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { i as MessageAlreadyProcessedError } from "./application-
|
|
3
|
-
import { i as stageD1Statement, n as eventCollector, r as runWithUnitOfWorkContext, t as collectD1Statements } from "./unit-of-work-context-
|
|
1
|
+
import { N as AppError, g as DatabaseError } from "./core-Cx5SeyKV.js";
|
|
2
|
+
import { i as MessageAlreadyProcessedError } from "./application-DjsQtpgw.js";
|
|
3
|
+
import { i as stageD1Statement, n as eventCollector, r as runWithUnitOfWorkContext, t as collectD1Statements } from "./unit-of-work-context-DToDpAkv.js";
|
|
4
4
|
import { DrizzleQueryError } from "drizzle-orm/errors";
|
|
5
5
|
import { eq, sql } from "drizzle-orm";
|
|
6
6
|
import { index, integer, sqliteTable, text } from "drizzle-orm/sqlite-core";
|
package/dist/cloudflare.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cloudflare.js","names":[],"sources":["../src/infrastructure/cloudflare/classify-d1-error.ts","../src/infrastructure/cloudflare/d1-database-execution.ts","../src/infrastructure/cloudflare/base-d1-repository.ts","../src/infrastructure/cloudflare/d1-schema.ts","../src/infrastructure/cloudflare/d1-message-store.ts","../src/infrastructure/cloudflare/d1-batch-transaction.ts"],"sourcesContent":["import { DrizzleQueryError } from 'drizzle-orm/errors'\n\nimport { AppError, DatabaseError, type D1ConstraintKind, type FailureClassification } from '@/core'\n\nconst KIND_BY_CODE: ReadonlyMap<string, D1ConstraintKind> = new Map([\n ['SQLITE_CONSTRAINT_PRIMARYKEY', 'unique'],\n ['SQLITE_CONSTRAINT_UNIQUE', 'unique'],\n ['SQLITE_CONSTRAINT_NOTNULL', 'not_null'],\n ['SQLITE_CONSTRAINT_FOREIGNKEY', 'foreign_key'],\n ['SQLITE_CONSTRAINT_CHECK', 'check'],\n])\n\nconst KIND_BY_PROSE: ReadonlyArray<readonly [string, D1ConstraintKind]> = [\n ['UNIQUE constraint failed', 'unique'],\n ['NOT NULL constraint failed', 'not_null'],\n ['FOREIGN KEY constraint failed', 'foreign_key'],\n ['CHECK constraint failed', 'check'],\n]\n\n/**\n * Recognize D1 failures and preserve the original cause. Unrecognized callback exceptions remain unchanged.\n * D1 exposes error codes and constraint locations in text, so parsing stays at this boundary.\n */\nexport function classifyD1Error(cause: unknown, operation = 'D1 query'): DatabaseError | undefined {\n if (cause instanceof AppError || (cause instanceof Error && cause.name === 'AbortError'))\n return undefined\n const message = messagesOf(cause)\n const kind = kindOf(message)\n const classification = kind === undefined ? classificationOf(message) : 'terminal'\n const code = message.match(/\\b(?:SQLITE_[A-Z_]+|D1_[A-Z_]+)\\b/)?.[0]\n if (code === undefined && classification === 'unknown' && !(cause instanceof DrizzleQueryError))\n return undefined\n // A composite unique constraint must not be mistaken for the processed-message primary key.\n const location = message.match(/constraint failed: (\\w+)\\.(\\w+)(?=\\s*(?::\\s*SQLITE_|$|\\n))/)\n const check = kind === 'check' ? message.match(/CHECK constraint failed: ([A-Za-z_]\\w*)/) : null\n return new DatabaseError(\n operation,\n classification,\n {\n database: 'd1',\n code,\n kind,\n table: location?.[1],\n column: location?.[2],\n constraintName: check?.[1],\n },\n { cause },\n )\n}\n\nfunction kindOf(message: string): D1ConstraintKind | undefined {\n const code = message.match(/SQLITE_CONSTRAINT_\\w+/)?.[0]\n if (code !== undefined) return KIND_BY_CODE.get(code)\n return KIND_BY_PROSE.find(([prose]) => message.includes(prose))?.[1]\n}\n\n// Drizzle query messages contain SQL and values, which are not evidence about the database failure.\nfunction messagesOf(cause: unknown): string {\n const messages: string[] = []\n const seen = new Set<unknown>()\n let current = cause\n while (current instanceof Error && !seen.has(current)) {\n seen.add(current)\n if (!(current instanceof DrizzleQueryError)) messages.push(current.message)\n current = current.cause\n }\n // oxlint-disable-next-line anti-slop/no-runtime-typeof -- D1 can expose a raw string cause; this boundary accepts only that representation.\n if (typeof current === 'string') messages.push(current)\n return messages.join('\\n')\n}\n\n// Cloudflare documents these conditions separately from transient storage resets.\nconst TERMINAL_FAILURE =\n /\\bSQLITE_(?:ERROR|MISMATCH|TOOBIG|FULL|AUTH|PERM|READONLY)\\b|\\bD1_(?:TYPE_ERROR|COLUMN_NOTFOUND)\\b|no such (?:table|column|function)|syntax error|datatype mismatch|exceeded (?:timeout|its memory limit|its CPU time limit|maximum DB size)|maximum account storage limit|free tier daily row (?:read|write) limit/i\nconst TRANSIENT_FAILURE =\n /Network connection lost\\.|Replica disconnected from primary\\.|D1 DB reset because its code was updated\\.|Internal error (?:while starting up|in) D1 DB storage caused object to be reset\\.|Cannot resolve D1 DB due to transient issue on remote node\\.|Can't read from request stream because client disconnected\\./i\n\nfunction classificationOf(message: string): FailureClassification {\n if (TERMINAL_FAILURE.test(message)) return 'terminal'\n if (TRANSIENT_FAILURE.test(message)) return 'transient'\n return 'unknown'\n}\n","import { classifyD1Error } from './classify-d1-error.ts'\n\n/** Execute once without retries; convert recognized D1 failures and preserve other exceptions unchanged. */\nexport async function executeDatabase<T>(\n operation: string,\n work: () => PromiseLike<T>,\n): Promise<T> {\n try {\n return await work()\n } catch (cause) {\n throw classifyD1Error(cause, operation) ?? cause\n }\n}\n","import type { BatchItem } from 'drizzle-orm/batch'\n\nimport type { Event } from '@/application'\n\nimport type { DrizzleSchema } from '../drizzle-schema.ts'\nimport { eventCollector, stageD1Statement } from '../unit-of-work-context.ts'\nimport { executeDatabase } from './d1-database-execution.ts'\nimport type { D1Db } from './d1-database-types.ts'\n\n/** A domain aggregate that accumulates events for the unit of work to drain. */\nexport interface AggregateEvents {\n collectEvents(): ReadonlyArray<Event>\n clearEvents(): void\n}\n\n/**\n * Shared plumbing for a repository that writes inside a D1 unit of work.\n * Writes are staged, never run; the boundary commits them as one batch. Reads run on `db` now.\n */\nexport abstract class BaseD1Repository<Schema extends DrizzleSchema> {\n constructor(protected readonly db: D1Db<Schema>) {}\n\n /** Add a statement to the current D1 batch without executing it. */\n protected stage(statement: BatchItem<'sqlite'>): void {\n stageD1Statement(statement)\n }\n\n /** Execute one read with database error conversion and no automatic retries. */\n protected read<T>(operation: string, query: () => PromiseLike<T>): Promise<T> {\n return executeDatabase(operation, query)\n }\n\n /** Drain an aggregate's pending events into the active boundary. */\n protected dispatchEvents(aggregate: AggregateEvents): void {\n const events = aggregate.collectEvents()\n if (events.length === 0) return\n for (const event of events) eventCollector.emit(event)\n aggregate.clearEvents()\n }\n}\n","import { sql } from 'drizzle-orm'\nimport { index, integer, sqliteTable, text } from 'drizzle-orm/sqlite-core'\n\nimport type { Event } from '@/application'\n\n/** One row per committed message. The primary key is the idempotency claim. */\nexport const processedMessages = sqliteTable(\n 'processed_messages',\n {\n messageId: text('message_id').primaryKey(),\n processedAt: integer('processed_at', { mode: 'timestamp' })\n .notNull()\n .$defaultFn(() => new Date()),\n },\n (table) => [index('idx_processed_messages_processed_at').on(table.processedAt)],\n)\n\n/** Events committed with their state change, waiting for the relay to deliver them. */\nexport const messageOutbox = sqliteTable(\n 'message_outbox',\n {\n id: text('id').primaryKey(),\n payload: text('payload', { mode: 'json' }).$type<Event>().notNull(),\n createdAt: integer('created_at', { mode: 'timestamp' })\n .notNull()\n .$defaultFn(() => new Date()),\n availableAt: integer('available_at', { mode: 'timestamp' })\n .notNull()\n .$defaultFn(() => new Date()),\n attempts: integer('attempts').notNull().default(0),\n claimToken: text('claim_token'),\n claimExpiresAt: integer('claim_expires_at', { mode: 'timestamp' }),\n publishedAt: integer('published_at', { mode: 'timestamp' }),\n failedAt: integer('failed_at', { mode: 'timestamp' }),\n lastError: text('last_error'),\n },\n (table) => [\n index('idx_message_outbox_due')\n .on(table.availableAt, table.createdAt)\n .where(sql`${table.publishedAt} IS NULL`),\n index('idx_message_outbox_claim_token').on(table.claimToken),\n ],\n)\n","import { eq } from 'drizzle-orm'\n\nimport {\n MessageAlreadyProcessedError,\n type Event,\n type IMessageStore,\n type MessageId,\n} from '@/application'\n\nimport type { DrizzleSchema } from '../drizzle-schema.ts'\nimport { BaseD1Repository } from './base-d1-repository.ts'\nimport { messageOutbox, processedMessages } from './d1-schema.ts'\n\n/** Processed messages and outbox events share one D1 batch; the primary key prevents concurrent duplicate commits. */\nexport class D1MessageStore<Schema extends DrizzleSchema>\n extends BaseD1Repository<Schema>\n implements IMessageStore\n{\n async claim(messageId: MessageId): Promise<void> {\n const processed = await this.read('claim message', () =>\n this.db\n .select({ messageId: processedMessages.messageId })\n .from(processedMessages)\n .where(eq(processedMessages.messageId, messageId))\n .get(),\n )\n if (processed !== undefined) throw new MessageAlreadyProcessedError(messageId)\n\n // Concurrent deliveries can pass the read; the primary key enforces the claim at commit.\n this.stage(this.db.insert(processedMessages).values({ messageId }))\n }\n\n async persistEvents(events: ReadonlyArray<Event>): Promise<void> {\n if (events.length === 0) return\n this.stage(\n this.db\n .insert(messageOutbox)\n .values(events.map((event) => ({ id: event.id, payload: event }))),\n )\n }\n}\n","import type { ITransaction } from '@/application'\n\nimport type { DrizzleSchema } from '../drizzle-schema.ts'\nimport { collectD1Statements, runWithUnitOfWorkContext } from '../unit-of-work-context.ts'\nimport { executeDatabase } from './d1-database-execution.ts'\nimport type { D1Db, D1Tx } from './d1-database-types.ts'\n\n/**\n * The D1 transaction boundary: every write is staged during `work` and committed as one\n * `db.batch()`. A throw inside `work` means the batch never runs.\n */\nexport class D1BatchTransaction<Schema extends DrizzleSchema> implements ITransaction<\n D1Tx<Schema>\n> {\n constructor(private readonly db: D1Db<Schema>) {}\n\n async transaction<T>(work: (tx: D1Tx<Schema>) => Promise<T>): Promise<T> {\n return runWithUnitOfWorkContext('d1', async () => {\n const value = await work({ db: this.db })\n const [first, ...rest] = collectD1Statements()\n if (first !== undefined) {\n await executeDatabase('commit unit of work', () => this.db.batch([first, ...rest]))\n }\n return value\n })\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"cloudflare.js","names":[],"sources":["../src/infrastructure/cloudflare/classify-d1-error.ts","../src/infrastructure/cloudflare/d1-database-execution.ts","../src/infrastructure/cloudflare/base-d1-repository.ts","../src/infrastructure/cloudflare/d1-schema.ts","../src/infrastructure/cloudflare/d1-message-store.ts","../src/infrastructure/cloudflare/d1-batch-transaction.ts"],"sourcesContent":["import { DrizzleQueryError } from 'drizzle-orm/errors'\n\nimport { AppError, DatabaseError, type D1ConstraintKind, type FailureClassification } from '@/core'\n\nconst KIND_BY_CODE: ReadonlyMap<string, D1ConstraintKind> = new Map([\n ['SQLITE_CONSTRAINT_PRIMARYKEY', 'unique'],\n ['SQLITE_CONSTRAINT_UNIQUE', 'unique'],\n ['SQLITE_CONSTRAINT_NOTNULL', 'not_null'],\n ['SQLITE_CONSTRAINT_FOREIGNKEY', 'foreign_key'],\n ['SQLITE_CONSTRAINT_CHECK', 'check'],\n])\n\nconst KIND_BY_PROSE: ReadonlyArray<readonly [string, D1ConstraintKind]> = [\n ['UNIQUE constraint failed', 'unique'],\n ['NOT NULL constraint failed', 'not_null'],\n ['FOREIGN KEY constraint failed', 'foreign_key'],\n ['CHECK constraint failed', 'check'],\n]\n\n/**\n * Recognize D1 failures and preserve the original cause. Unrecognized callback exceptions remain unchanged.\n * D1 exposes error codes and constraint locations in text, so parsing stays at this boundary.\n */\nexport function classifyD1Error(cause: unknown, operation = 'D1 query'): DatabaseError | undefined {\n if (cause instanceof AppError || (cause instanceof Error && cause.name === 'AbortError'))\n return undefined\n const message = messagesOf(cause)\n const kind = kindOf(message)\n const classification = kind === undefined ? classificationOf(message) : 'terminal'\n const code = message.match(/\\b(?:SQLITE_[A-Z_]+|D1_[A-Z_]+)\\b/)?.[0]\n if (code === undefined && classification === 'unknown' && !(cause instanceof DrizzleQueryError))\n return undefined\n // A composite unique constraint must not be mistaken for the processed-message primary key.\n const location = message.match(/constraint failed: (\\w+)\\.(\\w+)(?=\\s*(?::\\s*SQLITE_|$|\\n))/)\n const check = kind === 'check' ? message.match(/CHECK constraint failed: ([A-Za-z_]\\w*)/) : null\n return new DatabaseError(\n operation,\n classification,\n {\n database: 'd1',\n code,\n kind,\n table: location?.[1],\n column: location?.[2],\n constraintName: check?.[1],\n },\n { cause },\n )\n}\n\nfunction kindOf(message: string): D1ConstraintKind | undefined {\n const code = message.match(/SQLITE_CONSTRAINT_\\w+/)?.[0]\n if (code !== undefined) return KIND_BY_CODE.get(code)\n return KIND_BY_PROSE.find(([prose]) => message.includes(prose))?.[1]\n}\n\n// Drizzle query messages contain SQL and values, which are not evidence about the database failure.\nfunction messagesOf(cause: unknown): string {\n const messages: string[] = []\n const seen = new Set<unknown>()\n let current = cause\n while (current instanceof Error && !seen.has(current)) {\n seen.add(current)\n if (!(current instanceof DrizzleQueryError)) messages.push(current.message)\n current = current.cause\n }\n // oxlint-disable-next-line anti-slop/no-runtime-typeof -- D1 can expose a raw string cause; this boundary accepts only that representation.\n if (typeof current === 'string') messages.push(current)\n return messages.join('\\n')\n}\n\n// Cloudflare documents these conditions separately from transient storage resets.\nconst TERMINAL_FAILURE =\n /\\bSQLITE_(?:ERROR|MISMATCH|TOOBIG|FULL|AUTH|PERM|READONLY)\\b|\\bD1_(?:TYPE_ERROR|COLUMN_NOTFOUND)\\b|no such (?:table|column|function)|syntax error|datatype mismatch|exceeded (?:timeout|its memory limit|its CPU time limit|maximum DB size)|maximum account storage limit|free tier daily row (?:read|write) limit/i\nconst TRANSIENT_FAILURE =\n /Network connection lost\\.|Replica disconnected from primary\\.|D1 DB reset because its code was updated\\.|Internal error (?:while starting up|in) D1 DB storage caused object to be reset\\.|Cannot resolve D1 DB due to transient issue on remote node\\.|Can't read from request stream because client disconnected\\./i\n\nfunction classificationOf(message: string): FailureClassification {\n if (TERMINAL_FAILURE.test(message)) return 'terminal'\n if (TRANSIENT_FAILURE.test(message)) return 'transient'\n return 'unknown'\n}\n","import { classifyD1Error } from './classify-d1-error.ts'\n\n/** Execute once without retries; convert recognized D1 failures and preserve other exceptions unchanged. */\nexport async function executeDatabase<T>(\n operation: string,\n work: () => PromiseLike<T>,\n): Promise<T> {\n try {\n return await work()\n } catch (cause) {\n throw classifyD1Error(cause, operation) ?? cause\n }\n}\n","import type { BatchItem } from 'drizzle-orm/batch'\n\nimport type { DomainEvent } from '@/core'\n\nimport type { DrizzleSchema } from '../drizzle-schema.ts'\nimport { eventCollector, stageD1Statement } from '../unit-of-work-context.ts'\nimport { executeDatabase } from './d1-database-execution.ts'\nimport type { D1Db } from './d1-database-types.ts'\n\n/** A domain aggregate that accumulates events for the unit of work to drain. */\nexport interface AggregateEvents {\n collectEvents(): ReadonlyArray<DomainEvent>\n clearEvents(): void\n}\n\n/**\n * Shared plumbing for a repository that writes inside a D1 unit of work.\n * Writes are staged, never run; the boundary commits them as one batch. Reads run on `db` now.\n */\nexport abstract class BaseD1Repository<Schema extends DrizzleSchema> {\n constructor(protected readonly db: D1Db<Schema>) {}\n\n /** Add a statement to the current D1 batch without executing it. */\n protected stage(statement: BatchItem<'sqlite'>): void {\n stageD1Statement(statement)\n }\n\n /** Execute one read with database error conversion and no automatic retries. */\n protected read<T>(operation: string, query: () => PromiseLike<T>): Promise<T> {\n return executeDatabase(operation, query)\n }\n\n /** Drain an aggregate's pending events into the active boundary. */\n protected dispatchEvents(aggregate: AggregateEvents): void {\n const events = aggregate.collectEvents()\n if (events.length === 0) return\n for (const event of events) eventCollector.emit(event)\n aggregate.clearEvents()\n }\n}\n","import { sql } from 'drizzle-orm'\nimport { index, integer, sqliteTable, text } from 'drizzle-orm/sqlite-core'\n\nimport type { DomainEvent } from '@/core'\n\n/** One row per committed message. The primary key is the idempotency claim. */\nexport const processedMessages = sqliteTable(\n 'processed_messages',\n {\n messageId: text('message_id').primaryKey(),\n processedAt: integer('processed_at', { mode: 'timestamp' })\n .notNull()\n .$defaultFn(() => new Date()),\n },\n (table) => [index('idx_processed_messages_processed_at').on(table.processedAt)],\n)\n\n/** Events committed with their state change, waiting for the relay to deliver them. */\nexport const messageOutbox = sqliteTable(\n 'message_outbox',\n {\n id: text('id').primaryKey(),\n payload: text('payload', { mode: 'json' }).$type<DomainEvent>().notNull(),\n createdAt: integer('created_at', { mode: 'timestamp' })\n .notNull()\n .$defaultFn(() => new Date()),\n availableAt: integer('available_at', { mode: 'timestamp' })\n .notNull()\n .$defaultFn(() => new Date()),\n attempts: integer('attempts').notNull().default(0),\n claimToken: text('claim_token'),\n claimExpiresAt: integer('claim_expires_at', { mode: 'timestamp' }),\n publishedAt: integer('published_at', { mode: 'timestamp' }),\n failedAt: integer('failed_at', { mode: 'timestamp' }),\n lastError: text('last_error'),\n },\n (table) => [\n index('idx_message_outbox_due')\n .on(table.availableAt, table.createdAt)\n .where(sql`${table.publishedAt} IS NULL`),\n index('idx_message_outbox_claim_token').on(table.claimToken),\n ],\n)\n","import { eq } from 'drizzle-orm'\n\nimport { MessageAlreadyProcessedError, type IMessageStore, type MessageId } from '@/application'\nimport type { DomainEvent } from '@/core'\n\nimport type { DrizzleSchema } from '../drizzle-schema.ts'\nimport { BaseD1Repository } from './base-d1-repository.ts'\nimport { messageOutbox, processedMessages } from './d1-schema.ts'\n\n/** Processed messages and outbox events share one D1 batch; the primary key prevents concurrent duplicate commits. */\nexport class D1MessageStore<Schema extends DrizzleSchema>\n extends BaseD1Repository<Schema>\n implements IMessageStore\n{\n async claim(messageId: MessageId): Promise<void> {\n const processed = await this.read('claim message', () =>\n this.db\n .select({ messageId: processedMessages.messageId })\n .from(processedMessages)\n .where(eq(processedMessages.messageId, messageId))\n .get(),\n )\n if (processed !== undefined) throw new MessageAlreadyProcessedError(messageId)\n\n // Concurrent deliveries can pass the read; the primary key enforces the claim at commit.\n this.stage(this.db.insert(processedMessages).values({ messageId }))\n }\n\n async persistEvents(events: ReadonlyArray<DomainEvent>): Promise<void> {\n if (events.length === 0) return\n this.stage(\n this.db\n .insert(messageOutbox)\n .values(events.map((event) => ({ id: event.id, payload: event }))),\n )\n }\n}\n","import type { ITransaction } from '@/application'\n\nimport type { DrizzleSchema } from '../drizzle-schema.ts'\nimport { collectD1Statements, runWithUnitOfWorkContext } from '../unit-of-work-context.ts'\nimport { executeDatabase } from './d1-database-execution.ts'\nimport type { D1Db, D1Tx } from './d1-database-types.ts'\n\n/**\n * The D1 transaction boundary: every write is staged during `work` and committed as one\n * `db.batch()`. A throw inside `work` means the batch never runs.\n */\nexport class D1BatchTransaction<Schema extends DrizzleSchema> implements ITransaction<\n D1Tx<Schema>\n> {\n constructor(private readonly db: D1Db<Schema>) {}\n\n async transaction<T>(work: (tx: D1Tx<Schema>) => Promise<T>): Promise<T> {\n return runWithUnitOfWorkContext('d1', async () => {\n const value = await work({ db: this.db })\n const [first, ...rest] = collectD1Statements()\n if (first !== undefined) {\n await executeDatabase('commit unit of work', () => this.db.batch([first, ...rest]))\n }\n return value\n })\n }\n}\n"],"mappings":"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"}
|