easy-ping 0.4.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +548 -427
- package/dist/adapter-B7R4rXtJ.d.cts +17 -0
- package/dist/adapter-B7R4rXtJ.d.ts +17 -0
- package/dist/{adapter-BfLe2J0t.d.cts → adapter-D3lTy-4E.d.cts} +6 -32
- package/dist/{adapter-BfLe2J0t.d.ts → adapter-D3lTy-4E.d.ts} +6 -32
- package/dist/adapters/drizzle.cjs +1 -2
- package/dist/adapters/drizzle.cjs.map +1 -1
- package/dist/adapters/drizzle.d.cts +1 -1
- package/dist/adapters/drizzle.d.ts +1 -1
- package/dist/adapters/drizzle.js +3 -4
- package/dist/adapters/drizzle.js.map +1 -1
- package/dist/adapters/mongodb.cjs +144 -5
- package/dist/adapters/mongodb.cjs.map +1 -1
- package/dist/adapters/mongodb.d.cts +43 -18
- package/dist/adapters/mongodb.d.ts +43 -18
- package/dist/adapters/mongodb.js +77 -8
- package/dist/adapters/mongodb.js.map +1 -1
- package/dist/adapters/mysql.cjs +605 -0
- package/dist/adapters/mysql.cjs.map +1 -0
- package/dist/adapters/mysql.d.cts +38 -0
- package/dist/adapters/mysql.d.ts +38 -0
- package/dist/adapters/mysql.js +51 -0
- package/dist/adapters/mysql.js.map +1 -0
- package/dist/adapters/postgres.cjs +162 -9
- package/dist/adapters/postgres.cjs.map +1 -1
- package/dist/adapters/postgres.d.cts +44 -54
- package/dist/adapters/postgres.d.ts +44 -54
- package/dist/adapters/postgres.js +82 -42
- package/dist/adapters/postgres.js.map +1 -1
- package/dist/adapters/sqlite.cjs +593 -0
- package/dist/adapters/sqlite.cjs.map +1 -0
- package/dist/adapters/sqlite.d.cts +37 -0
- package/dist/adapters/sqlite.d.ts +37 -0
- package/dist/adapters/sqlite.js +53 -0
- package/dist/adapters/sqlite.js.map +1 -0
- package/dist/browser.cjs.map +1 -1
- package/dist/browser.d.cts +2 -14
- package/dist/browser.d.ts +2 -14
- package/dist/browser.js.map +1 -1
- package/dist/chunk-2EYKPF5A.js +393 -0
- package/dist/chunk-2EYKPF5A.js.map +1 -0
- package/dist/{chunk-3Y2MBUAW.js → chunk-3VOQOGDR.js} +4 -3
- package/dist/chunk-3VOQOGDR.js.map +1 -0
- package/dist/{chunk-PSJHXZPR.js → chunk-AEYYTE5I.js} +2 -2
- package/dist/chunk-AEYYTE5I.js.map +1 -0
- package/dist/chunk-CK5UPVYP.js +48 -0
- package/dist/chunk-CK5UPVYP.js.map +1 -0
- package/dist/{chunk-W6I3B2CI.js → chunk-E4IZ2CQC.js} +2 -2
- package/dist/{chunk-W6I3B2CI.js.map → chunk-E4IZ2CQC.js.map} +1 -1
- package/dist/chunk-HY47TQMY.js +52 -0
- package/dist/chunk-HY47TQMY.js.map +1 -0
- package/dist/{chunk-CYKAM7LE.js → chunk-JKTMUMQK.js} +31 -6
- package/dist/chunk-JKTMUMQK.js.map +1 -0
- package/dist/chunk-M4R77BJD.js +64 -0
- package/dist/chunk-M4R77BJD.js.map +1 -0
- package/dist/chunk-ORZKH224.js +75 -0
- package/dist/chunk-ORZKH224.js.map +1 -0
- package/dist/{chunk-DG3ZL5NP.js → chunk-USQ6CQTJ.js} +88 -3
- package/dist/chunk-USQ6CQTJ.js.map +1 -0
- package/dist/{chunk-YEMRRMJQ.js → chunk-XIDVLYNQ.js} +14 -4
- package/dist/chunk-XIDVLYNQ.js.map +1 -0
- package/dist/client.cjs +288 -17
- package/dist/client.cjs.map +1 -1
- package/dist/client.d.cts +53 -1
- package/dist/client.d.ts +53 -1
- package/dist/client.js +288 -17
- package/dist/client.js.map +1 -1
- package/dist/index.cjs +239 -9
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +53 -57
- package/dist/index.d.ts +53 -57
- package/dist/index.js +60 -13
- package/dist/index.js.map +1 -1
- package/dist/node.cjs +26 -5
- package/dist/node.cjs.map +1 -1
- package/dist/node.d.cts +1 -1
- package/dist/node.d.ts +1 -1
- package/dist/node.js +28 -6
- package/dist/node.js.map +1 -1
- package/dist/{plugin-C9Hoxus2.d.ts → plugin-CHd2ZiGC.d.cts} +5 -21
- package/dist/{plugin-CiMsGlQe.d.cts → plugin-Cg9usuUi.d.ts} +5 -21
- package/dist/plugins/digests.cjs +1 -4
- package/dist/plugins/digests.cjs.map +1 -1
- package/dist/plugins/digests.d.cts +6 -31
- package/dist/plugins/digests.d.ts +6 -31
- package/dist/plugins/digests.js +1 -4
- package/dist/plugins/digests.js.map +1 -1
- package/dist/plugins/preferences-client.cjs.map +1 -1
- package/dist/plugins/preferences-client.d.cts +2 -2
- package/dist/plugins/preferences-client.d.ts +2 -2
- package/dist/plugins/preferences-client.js +1 -1
- package/dist/plugins/preferences-react.cjs.map +1 -1
- package/dist/plugins/preferences-react.js.map +1 -1
- package/dist/plugins/preferences.cjs +1 -2
- package/dist/plugins/preferences.cjs.map +1 -1
- package/dist/plugins/preferences.d.cts +2 -2
- package/dist/plugins/preferences.d.ts +2 -2
- package/dist/plugins/preferences.js +3 -3
- package/dist/plugins/preferences.js.map +1 -1
- package/dist/plugins/push.cjs +1 -2
- package/dist/plugins/push.cjs.map +1 -1
- package/dist/plugins/push.d.cts +6 -26
- package/dist/plugins/push.d.ts +6 -26
- package/dist/plugins/push.js +4 -4
- package/dist/plugins/push.js.map +1 -1
- package/dist/providers/web-push.cjs.map +1 -1
- package/dist/providers/web-push.d.cts +3 -10
- package/dist/providers/web-push.d.ts +3 -10
- package/dist/providers/web-push.js.map +1 -1
- package/dist/react.cjs +311 -19
- package/dist/react.cjs.map +1 -1
- package/dist/react.d.cts +53 -4
- package/dist/react.d.ts +53 -4
- package/dist/react.js +311 -19
- package/dist/react.js.map +1 -1
- package/dist/schema.cjs +119 -1
- package/dist/schema.cjs.map +1 -1
- package/dist/schema.d.cts +20 -14
- package/dist/schema.d.ts +20 -14
- package/dist/schema.js +4 -2
- package/dist/signals-kPmOlKDX.d.cts +27 -0
- package/dist/signals-kPmOlKDX.d.ts +27 -0
- package/dist/sw.cjs +39 -0
- package/dist/sw.cjs.map +1 -0
- package/dist/sw.d.cts +52 -0
- package/dist/sw.d.ts +52 -0
- package/dist/sw.js +36 -0
- package/dist/sw.js.map +1 -0
- package/dist/testing.cjs +3 -19
- package/dist/testing.cjs.map +1 -1
- package/dist/testing.d.cts +4 -22
- package/dist/testing.d.ts +4 -22
- package/dist/testing.js +3 -19
- package/dist/testing.js.map +1 -1
- package/package.json +17 -1
- package/dist/chunk-3Y2MBUAW.js.map +0 -1
- package/dist/chunk-CYKAM7LE.js.map +0 -1
- package/dist/chunk-DG3ZL5NP.js.map +0 -1
- package/dist/chunk-PSJHXZPR.js.map +0 -1
- package/dist/chunk-YEMRRMJQ.js.map +0 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/adapters/sql/statement.ts"],"names":[],"mappings":";AASA,IAAM,cAAc,CAAC,KAAA,KAAoB,iBAAiB,IAAA,GAAO,KAAA,CAAM,aAAY,GAAI,KAAA;AAGhF,IAAM,YAAN,MAAgB;AAAA,EACrB,KAAA,GAAQ,EAAA;AAAA,EACC,SAAoB,EAAC;AAAA,EACrB,YAAA;AAAA,EACA,KAAA;AAAA,EAET,WAAA,CAAY,OAAA,GAA4B,EAAC,EAAG;AAC1C,IAAA,IAAA,CAAK,YAAA,GAAe,QAAQ,WAAA,IAAe,QAAA;AAC3C,IAAA,IAAA,CAAK,KAAA,GAAQ,QAAQ,IAAA,IAAQ,WAAA;AAAA,EAC/B;AAAA;AAAA,EAGA,IAAI,QAAA,EAAwB;AAC1B,IAAA,IAAA,CAAK,KAAA,IAAS,QAAA;AACd,IAAA,OAAO,IAAA;AAAA,EACT;AAAA,EAEA,MAAM,KAAA,EAAsB;AAC1B,IAAA,IAAA,CAAK,MAAA,CAAO,IAAA,CAAK,IAAA,CAAK,KAAA,CAAM,KAAK,CAAC,CAAA;AAClC,IAAA,IAAA,CAAK,KAAA,IAAS,KAAK,YAAA,KAAiB,QAAA,GAAW,IAAI,IAAA,CAAK,MAAA,CAAO,MAAM,CAAA,CAAA,GAAK,GAAA;AAC1E,IAAA,OAAO,IAAA;AAAA,EACT;AAAA,EAEA,IAAA,CAAK,MAAA,EAA4B,SAAA,GAAY,IAAA,EAAY;AACvD,IAAA,MAAA,CAAO,OAAA,CAAQ,CAAC,KAAA,EAAO,KAAA,KAAU;AAC/B,MAAA,IAAI,KAAA,GAAQ,CAAA,EAAG,IAAA,CAAK,GAAA,CAAI,SAAS,CAAA;AACjC,MAAA,IAAA,CAAK,MAAM,KAAK,CAAA;AAAA,IAClB,CAAC,CAAA;AACD,IAAA,OAAO,IAAA;AAAA,EACT;AAAA;AAAA,EAGA,MAAM,KAAA,EAAsD;AAC1D,IAAA,IAAA,CAAK,IAAI,GAAG,CAAA;AACZ,IAAA,KAAA,CAAM,QAAQ,CAAC,CAAC,KAAA,EAAO,IAAI,GAAG,KAAA,KAAU;AACtC,MAAA,IAAI,KAAA,GAAQ,CAAA,EAAG,IAAA,CAAK,GAAA,CAAI,IAAI,CAAA;AAC5B,MAAA,IAAA,CAAK,MAAM,KAAK,CAAA;AAChB,MAAA,IAAI,KAAK,YAAA,KAAiB,QAAA,OAAe,GAAA,CAAI,CAAA,EAAA,EAAK,IAAI,CAAA,CAAE,CAAA;AAAA,IAC1D,CAAC,CAAA;AACD,IAAA,OAAO,IAAA,CAAK,IAAI,GAAG,CAAA;AAAA,EACrB;AAAA,EAEA,IAAI,IAAA,GAAe;AACjB,IAAA,OAAO,IAAA,CAAK,KAAA;AAAA,EACd;AACF;AAEO,IAAM,YACX,CAAC,SAAA,KACD,CAAC,UAAA,KACC,GAAG,SAAS,CAAA,EAAG,UAAA,CAAW,KAAA,CAAM,SAAS,CAAA,CAAE,IAAA,CAAK,YAAY,SAAS,CAAC,GAAG,SAAS,CAAA;AAG/E,IAAM,KAAA,GAAQ,UAAU,GAAG","file":"chunk-CK5UPVYP.js","sourcesContent":["export type Placeholder = \"dollar\" | \"question\";\n\nexport type StatementOptions = {\n /** `$1, $2` for Postgres; `?` for MySQL and SQLite. */\n placeholder?: Placeholder;\n /** What the driver is handed for a JS value. Dates become ISO strings by default. */\n bind?: (value: unknown) => unknown;\n};\n\nconst defaultBind = (value: unknown) => (value instanceof Date ? value.toISOString() : value);\n\n/** SQL text plus bound values. Values are never interpolated; identifiers arrive pre-validated. */\nexport class Statement {\n #text = \"\";\n readonly params: unknown[] = [];\n readonly #placeholder: Placeholder;\n readonly #bind: (value: unknown) => unknown;\n\n constructor(options: StatementOptions = {}) {\n this.#placeholder = options.placeholder ?? \"dollar\";\n this.#bind = options.bind ?? defaultBind;\n }\n\n /** Raw SQL. Never user input; use `value`. */\n raw(fragment: string): this {\n this.#text += fragment;\n return this;\n }\n\n value(value: unknown): this {\n this.params.push(this.#bind(value));\n this.#text += this.#placeholder === \"dollar\" ? `$${this.params.length}` : \"?\";\n return this;\n }\n\n list(values: readonly unknown[], separator = \", \"): this {\n values.forEach((value, index) => {\n if (index > 0) this.raw(separator);\n this.value(value);\n });\n return this;\n }\n\n /** `($1::text, $2::timestamptz)`. Casts are Postgres-only and dropped elsewhere. */\n tuple(cells: readonly (readonly [unknown, string])[]): this {\n this.raw(\"(\");\n cells.forEach(([value, cast], index) => {\n if (index > 0) this.raw(\", \");\n this.value(value);\n if (this.#placeholder === \"dollar\") this.raw(`::${cast}`);\n });\n return this.raw(\")\");\n }\n\n get text(): string {\n return this.#text;\n }\n}\n\nexport const quoteWith =\n (quoteChar: '\"' | \"`\") =>\n (identifier: string): string =>\n `${quoteChar}${identifier.split(quoteChar).join(quoteChar + quoteChar)}${quoteChar}`;\n\n/** Double-quoted: Postgres and SQLite. */\nexport const quote = quoteWith('\"');\n"]}
|
|
@@ -7,5 +7,5 @@ function defineClientPlugin(plugin) {
|
|
|
7
7
|
}
|
|
8
8
|
|
|
9
9
|
export { defineClientPlugin, definePlugin };
|
|
10
|
-
//# sourceMappingURL=chunk-
|
|
11
|
-
//# sourceMappingURL=chunk-
|
|
10
|
+
//# sourceMappingURL=chunk-E4IZ2CQC.js.map
|
|
11
|
+
//# sourceMappingURL=chunk-E4IZ2CQC.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/core/plugin.ts"],"names":[],"mappings":";
|
|
1
|
+
{"version":3,"sources":["../src/core/plugin.ts"],"names":[],"mappings":";AAuKO,SAAS,aACd,MAAA,EAC8B;AAC9B,EAAA,OAAO,MAAA;AACT;AAgBO,SAAS,mBACd,MAAA,EAC6B;AAC7B,EAAA,OAAO,MAAA;AACT","file":"chunk-E4IZ2CQC.js","sourcesContent":["import type { DeliveryOutcome } from \"./adapter\";\nimport type { Logger } from \"./errors\";\nimport type { PluginStore } from \"./store\";\nimport type { SignRequest, TokenClaims } from \"./tokens\";\nimport type { Channel, Recipient } from \"./types\";\n\nexport type Promisable<T> = T | Promise<T>;\n\nexport type PrepareContext = {\n type: string;\n recipients: readonly Recipient[];\n};\n\nexport type BeforeSendContext = {\n type: string;\n recipient: Recipient;\n payload: unknown;\n actorId: string | null;\n dedupeKey: string | null;\n /** Whatever this plugin's own prepare() returned for this send. */\n prepared: unknown;\n};\n\nexport type BeforeSendResult =\n | { action: \"continue\"; payload?: unknown; dedupeKey?: string }\n | { action: \"cancel\"; reason: string };\n\nexport type ChannelDecision = {\n channel: Channel;\n action: \"send\" | \"skip\" | \"defer\";\n /** defer only. Written to notification_delivery.not_before. */\n notBefore?: Date | undefined;\n};\n\nexport type ResolveChannelsContext = {\n type: string;\n recipient: Recipient;\n payload: unknown;\n decisions: readonly ChannelDecision[];\n /** Whatever this plugin's own prepare() returned for this send. */\n prepared: unknown;\n};\n\nexport type DeliverContext = {\n channel: Channel;\n deliveryId: string;\n attempt: number;\n recipient: Recipient;\n notification: { id: string; userId: string; type: string; payload: unknown };\n /** Aborted on the runner's timeout. */\n signal: AbortSignal;\n};\n\nexport type AfterSendContext = {\n type: string;\n payload: unknown;\n /** Committed rows. Ids exist only at this point. */\n notifications: readonly { id: string; userId: string }[];\n /** Whatever this plugin's own prepare() returned for this send. */\n prepared: unknown;\n};\n\nexport type AfterDeliverContext = {\n deliveryId: string;\n notificationId: string;\n userId: string;\n channel: Channel;\n outcome: DeliveryOutcome;\n attempt: number;\n isTerminal: boolean;\n};\n\n/** beforeSend and resolveChannels fail closed; afterDeliver fails open. */\nexport type PluginHooks = {\n /** Once per send, for bulk loading — the per-recipient hooks would be an N+1. */\n prepare?: (ctx: PrepareContext) => Promisable<unknown>;\n beforeSend?: (ctx: BeforeSendContext) => Promisable<BeforeSendResult>;\n resolveChannels?: (ctx: ResolveChannelsContext) => Promisable<readonly ChannelDecision[]>;\n /** Once per send, after commit. The only hook that sees notification ids. Fails open. */\n afterSend?: (ctx: AfterSendContext) => Promisable<void>;\n /** Delivers one of the plugin's declared `channels`. Core carries inApp and email. */\n deliver?: (ctx: DeliverContext) => Promisable<DeliveryOutcome>;\n afterDeliver?: (ctx: AfterDeliverContext) => Promisable<void>;\n};\n\nexport type FieldType = \"string\" | \"number\" | \"boolean\" | \"date\" | \"json\";\n\nexport type FieldDeclaration = {\n type: FieldType;\n required?: boolean;\n default?: string | number | boolean;\n /** date fields only. Renders as defaultNow() / CURRENT_TIMESTAMP. */\n defaultNow?: boolean;\n};\n\nexport type IndexDeclaration = {\n on: readonly string[];\n unique?: boolean;\n name?: string;\n};\n\nexport type TableDeclaration = {\n tableName: string;\n fields: Record<string, FieldDeclaration>;\n primaryKey?: readonly string[];\n indexes?: readonly IndexDeclaration[];\n};\n\nexport type SchemaDeclaration = Record<string, TableDeclaration>;\n\nexport type RouteScope =\n | { type: \"user\" }\n | { type: \"machine\" }\n | { type: \"signed\"; purpose: string }\n | { type: \"custom\"; justification: string };\n\nexport type RouteContext = {\n request: Request;\n /** Resolved for user-scoped routes, so a handler cannot forget to scope. */\n userId: string | null;\n /** Verified before dispatch, so a handler cannot forget to check. */\n claims: TokenClaims | null;\n params: Record<string, string>;\n};\n\nexport type RouteDefinition = {\n path: string;\n method: \"GET\" | \"POST\";\n scope: RouteScope;\n handler: (ctx: RouteContext) => Promise<Response>;\n};\n\n/** Handed to a plugin at startup so it need not be given the database twice. */\nexport type PluginInitContext = {\n /** Scoped to the tables this plugin declares in schema(). */\n store: PluginStore;\n /** Mints tokens for this plugin's own `signed` routes only. The secret itself is never handed out. */\n sign: (request: SignRequest) => Promise<string>;\n logger: Logger;\n /** Configured notification names, so a route can refuse to store rows for types that do not exist. */\n notificationTypes: readonly string[];\n /** The app's resolver, for plugins that need timezones or addresses. */\n getRecipients: (userIds: readonly string[]) => Promise<readonly Recipient[]>;\n /** The instance's own send(), so a composed message inherits retry, backoff and idempotency. */\n send: (\n type: string,\n args: { to: string | readonly string[]; payload: unknown; dedupeKey?: string },\n ) => Promise<unknown>;\n};\n\nexport type EasyPingPlugin<\n TId extends string = string,\n TSchema extends SchemaDeclaration = SchemaDeclaration,\n> = {\n id: TId;\n dependsOn?: readonly string[];\n schema?: TSchema;\n routes?: readonly RouteDefinition[];\n hooks?: PluginHooks;\n /** Channels this plugin delivers. Makes them usable in a notification. */\n channels?: readonly Channel[];\n /** Called once by easyPing() before any hook or route can run. */\n init?: (ctx: PluginInitContext) => void;\n};\n\nexport type AnyPlugin = EasyPingPlugin<string, SchemaDeclaration>;\n\nexport function definePlugin<TId extends string, TSchema extends SchemaDeclaration>(\n plugin: EasyPingPlugin<TId, TSchema>,\n): EasyPingPlugin<TId, TSchema> {\n return plugin;\n}\n\nexport type ClientFetch = (path: string, init?: RequestInit) => Promise<unknown>;\n\nexport type ClientPlugin<\n TId extends string = string,\n TActions extends Record<string, unknown> = Record<string, unknown>,\n> = {\n id: TId;\n /** Phantom. Links client types to the server plugin so a mismatch is a type error. */\n $InferServerPlugin?: AnyPlugin;\n actions: (fetch: ClientFetch) => TActions;\n};\n\nexport type AnyClientPlugin = ClientPlugin<string, Record<string, unknown>>;\n\nexport function defineClientPlugin<TId extends string, TActions extends Record<string, unknown>>(\n plugin: ClientPlugin<TId, TActions>,\n): ClientPlugin<TId, TActions> {\n return plugin;\n}\n\nexport type UnionToIntersection<U> = (U extends unknown ? (k: U) => void : never) extends (\n k: infer I,\n) => void\n ? I\n : never;\n\n/** Naked type parameter, so the conditional distributes over the union. */\ntype ExtractActions<TPlugin> = TPlugin extends { actions: (fetch: never) => infer TActions }\n ? TActions\n : never;\n\n/** Indexes the tuple rather than recursing: a fold blows the instantiation depth limit. */\nexport type ActionsOf<TPlugins extends readonly AnyClientPlugin[]> = UnionToIntersection<\n ExtractActions<TPlugins[number]>\n>;\n"]}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { toSnakeCase } from './chunk-AEYYTE5I.js';
|
|
2
|
+
|
|
3
|
+
// src/schema/render-sqlite.ts
|
|
4
|
+
var SQL_TYPE = {
|
|
5
|
+
string: "TEXT",
|
|
6
|
+
number: "INTEGER",
|
|
7
|
+
boolean: "INTEGER",
|
|
8
|
+
// ISO-8601 with milliseconds and a trailing Z, the same 24 characters
|
|
9
|
+
// `Date#toISOString` writes, so `<=` on the text is `<=` on the instant.
|
|
10
|
+
date: "TEXT",
|
|
11
|
+
json: "TEXT"
|
|
12
|
+
};
|
|
13
|
+
var quote = (value) => `"${value.split('"').join('""')}"`;
|
|
14
|
+
var defaultClause = (spec) => {
|
|
15
|
+
if (spec.defaultNow) return " DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))";
|
|
16
|
+
if (spec.default === void 0) return "";
|
|
17
|
+
if (typeof spec.default === "string") return ` DEFAULT '${spec.default.replace(/'/g, "''")}'`;
|
|
18
|
+
if (typeof spec.default === "boolean") return ` DEFAULT ${spec.default ? 1 : 0}`;
|
|
19
|
+
return ` DEFAULT ${spec.default}`;
|
|
20
|
+
};
|
|
21
|
+
function renderSqliteDdl(schema, prefix = "") {
|
|
22
|
+
const statements = [];
|
|
23
|
+
for (const table of Object.values(schema)) {
|
|
24
|
+
const name = quote(prefix + table.tableName);
|
|
25
|
+
const columns = Object.entries(table.fields).map(([field, spec]) => {
|
|
26
|
+
const nullability = spec.required ? " NOT NULL" : "";
|
|
27
|
+
return ` ${quote(toSnakeCase(field))} ${SQL_TYPE[spec.type]}${nullability}${defaultClause(spec)}`;
|
|
28
|
+
});
|
|
29
|
+
if (table.primaryKey?.length) {
|
|
30
|
+
columns.push(
|
|
31
|
+
` PRIMARY KEY (${table.primaryKey.map((f) => quote(toSnakeCase(f))).join(", ")})`
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
statements.push(`CREATE TABLE IF NOT EXISTS ${name} (
|
|
35
|
+
${columns.join(",\n")}
|
|
36
|
+
)`);
|
|
37
|
+
for (const index of table.indexes ?? []) {
|
|
38
|
+
const indexName = quote(
|
|
39
|
+
prefix + (index.name ?? `${table.tableName}_${index.on.join("_")}_idx`)
|
|
40
|
+
);
|
|
41
|
+
const cols = index.on.map((f) => quote(toSnakeCase(f))).join(", ");
|
|
42
|
+
statements.push(
|
|
43
|
+
`CREATE ${index.unique ? "UNIQUE " : ""}INDEX IF NOT EXISTS ${indexName} ON ${name} (${cols})`
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return statements;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export { renderSqliteDdl };
|
|
51
|
+
//# sourceMappingURL=chunk-HY47TQMY.js.map
|
|
52
|
+
//# sourceMappingURL=chunk-HY47TQMY.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/schema/render-sqlite.ts"],"names":[],"mappings":"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","file":"chunk-HY47TQMY.js","sourcesContent":["import type { FieldDeclaration, SchemaDeclaration } from \"../core/plugin\";\nimport { toSnakeCase } from \"./declaration\";\n\nconst SQL_TYPE: Record<FieldDeclaration[\"type\"], string> = {\n string: \"TEXT\",\n number: \"INTEGER\",\n boolean: \"INTEGER\",\n // ISO-8601 with milliseconds and a trailing Z, the same 24 characters\n // `Date#toISOString` writes, so `<=` on the text is `<=` on the instant.\n date: \"TEXT\",\n json: \"TEXT\",\n};\n\nconst quote = (value: string) => `\"${value.split('\"').join('\"\"')}\"`;\n\nconst defaultClause = (spec: FieldDeclaration): string => {\n if (spec.defaultNow) return \" DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))\";\n if (spec.default === undefined) return \"\";\n if (typeof spec.default === \"string\") return ` DEFAULT '${spec.default.replace(/'/g, \"''\")}'`;\n if (typeof spec.default === \"boolean\") return ` DEFAULT ${spec.default ? 1 : 0}`;\n return ` DEFAULT ${spec.default}`;\n};\n\n/** SchemaDeclaration to SQLite DDL. Every statement is `IF NOT EXISTS`. */\nexport function renderSqliteDdl(schema: SchemaDeclaration, prefix = \"\"): string[] {\n const statements: string[] = [];\n\n for (const table of Object.values(schema)) {\n const name = quote(prefix + table.tableName);\n\n const columns = Object.entries(table.fields).map(([field, spec]) => {\n const nullability = spec.required ? \" NOT NULL\" : \"\";\n return ` ${quote(toSnakeCase(field))} ${SQL_TYPE[spec.type]}${nullability}${defaultClause(spec)}`;\n });\n\n if (table.primaryKey?.length) {\n columns.push(\n ` PRIMARY KEY (${table.primaryKey.map((f) => quote(toSnakeCase(f))).join(\", \")})`,\n );\n }\n\n statements.push(`CREATE TABLE IF NOT EXISTS ${name} (\\n${columns.join(\",\\n\")}\\n)`);\n\n for (const index of table.indexes ?? []) {\n const indexName = quote(\n prefix + (index.name ?? `${table.tableName}_${index.on.join(\"_\")}_idx`),\n );\n const cols = index.on.map((f) => quote(toSnakeCase(f))).join(\", \");\n statements.push(\n `CREATE ${index.unique ? \"UNIQUE \" : \"\"}INDEX IF NOT EXISTS ${indexName} ON ${name} (${cols})`,\n );\n }\n }\n\n return statements;\n}\n"]}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { DELIVERIES_CHANNEL } from './chunk-ORZKH224.js';
|
|
2
|
+
|
|
1
3
|
// src/core/backoff.ts
|
|
2
4
|
var SCHEDULE_MS = [3e4, 12e4, 48e4, 192e4];
|
|
3
5
|
function backoffMs(attempt, strategy = "exponential") {
|
|
@@ -9,7 +11,7 @@ function backoffMs(attempt, strategy = "exponential") {
|
|
|
9
11
|
var nextAttemptAt = (attempt, strategy, now = /* @__PURE__ */ new Date()) => new Date(now.getTime() + backoffMs(attempt, strategy));
|
|
10
12
|
|
|
11
13
|
// src/core/runner.ts
|
|
12
|
-
var
|
|
14
|
+
var DEFAULT_WORKER_IDLE_MS = 1e4;
|
|
13
15
|
async function withTimeout(run, ms, label) {
|
|
14
16
|
const controller = new AbortController();
|
|
15
17
|
let timer;
|
|
@@ -198,22 +200,45 @@ function createRunner(deps) {
|
|
|
198
200
|
}
|
|
199
201
|
}
|
|
200
202
|
function startWorker(options = {}) {
|
|
201
|
-
const intervalMs = options.intervalMs ??
|
|
203
|
+
const intervalMs = options.intervalMs ?? DEFAULT_WORKER_IDLE_MS;
|
|
202
204
|
let stopped = false;
|
|
205
|
+
let wake;
|
|
206
|
+
let pending = false;
|
|
207
|
+
const unsubscribe = deps.signals?.subscribe(DELIVERIES_CHANNEL, () => {
|
|
208
|
+
pending = true;
|
|
209
|
+
wake?.();
|
|
210
|
+
});
|
|
211
|
+
const idle = (ms) => new Promise((resolve) => {
|
|
212
|
+
if (pending) {
|
|
213
|
+
pending = false;
|
|
214
|
+
resolve();
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
const timer = setTimeout(done, ms);
|
|
218
|
+
function done() {
|
|
219
|
+
clearTimeout(timer);
|
|
220
|
+
wake = void 0;
|
|
221
|
+
pending = false;
|
|
222
|
+
resolve();
|
|
223
|
+
}
|
|
224
|
+
wake = done;
|
|
225
|
+
});
|
|
203
226
|
const loop = (async () => {
|
|
204
227
|
while (!stopped) {
|
|
205
228
|
try {
|
|
206
229
|
const result = await runOnce({ limit: options.batchSize ?? deps.batchSize });
|
|
207
|
-
if (result.claimed === 0) await
|
|
230
|
+
if (result.claimed === 0 && !stopped) await idle(intervalMs);
|
|
208
231
|
} catch (error) {
|
|
209
232
|
deps.logger.error("worker sweep failed", { error });
|
|
210
|
-
await
|
|
233
|
+
if (!stopped) await idle(intervalMs);
|
|
211
234
|
}
|
|
212
235
|
}
|
|
213
236
|
})();
|
|
214
237
|
return {
|
|
215
238
|
stop: async () => {
|
|
216
239
|
stopped = true;
|
|
240
|
+
unsubscribe?.();
|
|
241
|
+
wake?.();
|
|
217
242
|
await loop;
|
|
218
243
|
}
|
|
219
244
|
};
|
|
@@ -222,5 +247,5 @@ function createRunner(deps) {
|
|
|
222
247
|
}
|
|
223
248
|
|
|
224
249
|
export { createRunner, redactErrorMessage };
|
|
225
|
-
//# sourceMappingURL=chunk-
|
|
226
|
-
//# sourceMappingURL=chunk-
|
|
250
|
+
//# sourceMappingURL=chunk-JKTMUMQK.js.map
|
|
251
|
+
//# sourceMappingURL=chunk-JKTMUMQK.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/core/backoff.ts","../src/core/runner.ts"],"names":[],"mappings":"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","file":"chunk-JKTMUMQK.js","sourcesContent":["/** Exponential backoff with ±20% jitter, floored by the sweep interval. RFC 0001 §4. */\nconst SCHEDULE_MS = [30_000, 120_000, 480_000, 1_920_000] as const;\n\nexport type Backoff = \"exponential\" | ((attempt: number) => number);\n\nexport function backoffMs(attempt: number, strategy: Backoff = \"exponential\"): number {\n if (typeof strategy === \"function\") return Math.max(0, strategy(attempt));\n\n const index = Math.min(Math.max(attempt, 1), SCHEDULE_MS.length) - 1;\n const base = SCHEDULE_MS[index] ?? SCHEDULE_MS[SCHEDULE_MS.length - 1] ?? 30_000;\n return Math.round(base * (0.8 + Math.random() * 0.4));\n}\n\nexport const nextAttemptAt = (attempt: number, strategy?: Backoff, now = new Date()): Date =>\n new Date(now.getTime() + backoffMs(attempt, strategy));\n","import type { ClaimedDelivery, DatabaseAdapter, DeliveryOutcome, DeliveryRelease } from \"./adapter\";\nimport { type Backoff, nextAttemptAt } from \"./backoff\";\nimport type { ChannelsConfig, Worker } from \"./config\";\nimport type { NotificationDefinitions } from \"./definition\";\nimport type { Logger } from \"./errors\";\nimport type { AnyPlugin } from \"./plugin\";\nimport { DELIVERIES_CHANNEL, type Signals } from \"./signals\";\nimport type { Channel, Recipient } from \"./types\";\n\nexport type RunnerDeps = {\n adapter: DatabaseAdapter;\n definitions: NotificationDefinitions;\n channels: ChannelsConfig;\n plugins: readonly AnyPlugin[];\n getRecipients: (userIds: readonly string[]) => Promise<readonly Recipient[]>;\n logger: Logger;\n leaseMs: number;\n batchSize: number;\n backoff: Backoff;\n /** Wakes an idle worker the moment a send commits, so the idle interval can be long. */\n signals?: Signals | undefined;\n};\n\n/** Long on purpose: a send in this process wakes the loop, and the cron sweep backstops the rest. */\nexport const DEFAULT_WORKER_IDLE_MS = 10_000;\n\nexport type SweepResult = {\n claimed: number;\n sent: number;\n failed: number;\n /** Deliveries that had nothing to do. Not errors; see DeliveryOutcome. */\n skipped: number;\n};\n\nasync function withTimeout<T>(\n run: (signal: AbortSignal) => Promise<T>,\n ms: number,\n label: string,\n): Promise<T> {\n const controller = new AbortController();\n let timer: ReturnType<typeof setTimeout> | undefined;\n\n const timeout = new Promise<never>((_, reject) => {\n timer = setTimeout(() => {\n // Abort first: racing a promise leaves the original request running.\n controller.abort();\n reject(new Error(`${label} timed out after ${ms}ms`));\n }, ms);\n });\n\n try {\n return await Promise.race([run(controller.signal), timeout]);\n } finally {\n clearTimeout(timer);\n }\n}\n\n/** Persisted to `last_error`, which admin tooling reads: addresses do not go in. */\nexport const redactErrorMessage = (text: string) =>\n text.replace(/[^\\s\"'<>()[\\]{},;]+@[^\\s\"'<>()[\\]{},;]+/g, \"[redacted-email]\");\n\nconst message = (error: unknown) =>\n redactErrorMessage(error instanceof Error ? error.message : String(error));\n\nexport function createRunner(deps: RunnerDeps) {\n async function deliverOne(\n delivery: ClaimedDelivery,\n recipient: Recipient | undefined,\n ): Promise<DeliveryOutcome> {\n if (!recipient) {\n return { result: \"failed\", error: \"recipient could not be resolved\", retryable: false };\n }\n\n if (delivery.channel === \"inApp\") {\n // The notification row is the delivery. Nothing to transmit.\n return { result: \"sent\" };\n }\n\n if (delivery.channel === \"email\") {\n const provider = deps.channels.email?.provider;\n if (!provider) {\n return { result: \"failed\", error: \"no email provider configured\", retryable: false };\n }\n // Captured before the closure below: narrowing does not survive into it.\n const to = recipient.email;\n if (!to) {\n return { result: \"failed\", error: \"recipient has no email address\", retryable: false };\n }\n\n const email = deps.definitions[delivery.notification.type]?.email;\n if (!email) {\n return {\n result: \"failed\",\n error: `notification type \"${delivery.notification.type}\" has no email template`,\n retryable: false,\n };\n }\n\n try {\n const payload = delivery.notification.payload as never;\n const html = await email.template(payload);\n\n await withTimeout(\n (signal) =>\n provider.send({\n to,\n subject: email.subject(payload),\n html,\n // Delivery is at-least-once; the provider dedupes on this.\n idempotencyKey: delivery.id,\n signal,\n }),\n provider.timeoutMs ?? 30_000,\n `${provider.name} send`,\n );\n\n return { result: \"sent\" };\n } catch (error) {\n return {\n result: \"failed\",\n error: message(error),\n retryable: provider.isRetryable(error),\n };\n }\n }\n\n const owner = deps.plugins.find(\n (plugin) => plugin.channels?.includes(delivery.channel) && plugin.hooks?.deliver,\n );\n if (!owner?.hooks?.deliver) {\n return {\n result: \"failed\",\n error: `no plugin delivers channel \"${delivery.channel}\"`,\n retryable: false,\n };\n }\n\n try {\n return await withTimeout(\n (signal) =>\n Promise.resolve(\n // biome-ignore lint/style/noNonNullAssertion: guarded directly above\n owner.hooks!.deliver!({\n channel: delivery.channel,\n deliveryId: delivery.id,\n attempt: delivery.attempts + 1,\n recipient,\n notification: {\n id: delivery.notificationId,\n userId: delivery.notification.userId,\n type: delivery.notification.type,\n payload: delivery.notification.payload,\n },\n signal,\n }),\n ),\n 30_000,\n `plugin \"${owner.id}\" deliver`,\n );\n } catch (error) {\n return { result: \"failed\", error: message(error), retryable: true };\n }\n }\n\n async function notifyAfterDeliver(delivery: ClaimedDelivery, outcome: DeliveryOutcome) {\n const isTerminal =\n outcome.result !== \"failed\" ||\n !outcome.retryable ||\n delivery.attempts + 1 >= delivery.maxAttempts;\n\n for (const plugin of deps.plugins) {\n const hook = plugin.hooks?.afterDeliver;\n if (!hook) continue;\n try {\n await hook({\n deliveryId: delivery.id,\n notificationId: delivery.notificationId,\n userId: delivery.notification.userId,\n channel: delivery.channel,\n outcome,\n attempt: delivery.attempts + 1,\n isTerminal,\n });\n } catch (error) {\n // Fail open: the message is already sent. RFC 0004 §5.\n deps.logger.error(`plugin \"${plugin.id}\" afterDeliver threw; ignoring`, {\n deliveryId: delivery.id,\n error,\n });\n }\n }\n }\n\n async function runOnce(\n options: { limit?: number; channels?: readonly Channel[]; ids?: readonly string[] } = {},\n ): Promise<SweepResult> {\n const claimToken = crypto.randomUUID();\n\n const claimed = await deps.adapter.claimPendingDeliveries({\n limit: options.limit ?? deps.batchSize,\n leaseMs: deps.leaseMs,\n claimToken,\n ...(options.channels ? { channels: options.channels } : {}),\n ...(options.ids ? { ids: options.ids } : {}),\n });\n\n if (claimed.length === 0) return { claimed: 0, sent: 0, failed: 0, skipped: 0 };\n\n const userIds = [...new Set(claimed.map((row) => row.notification.userId))];\n const recipients = await deps.getRecipients(userIds);\n const byUserId = new Map(recipients.map((recipient) => [recipient.userId, recipient]));\n\n const releases: DeliveryRelease[] = [];\n let sent = 0;\n let failed = 0;\n let skipped = 0;\n\n for (const delivery of claimed) {\n const outcome = await deliverOne(delivery, byUserId.get(delivery.notification.userId));\n\n if (outcome.result === \"sent\") sent += 1;\n else if (outcome.result === \"skipped\") skipped += 1;\n else failed += 1;\n\n releases.push({\n id: delivery.id,\n claimToken,\n outcome,\n ...(outcome.result === \"failed\" && outcome.retryable\n ? { nextAttemptAt: nextAttemptAt(delivery.attempts + 1, deps.backoff) }\n : {}),\n });\n\n await notifyAfterDeliver(delivery, outcome);\n }\n\n await deps.adapter.releaseDeliveries(releases);\n return { claimed: claimed.length, sent, failed, skipped };\n }\n\n /** Drains until empty. Pass `ids` to bound it to one send. */\n async function drain(\n options: { maxSweeps?: number; ids?: readonly string[] } = {},\n ): Promise<SweepResult> {\n const maxSweeps = options.maxSweeps ?? 50;\n const total: SweepResult = { claimed: 0, sent: 0, failed: 0, skipped: 0 };\n\n for (let sweep = 0; sweep < maxSweeps; sweep += 1) {\n const result = await runOnce(options.ids ? { ids: options.ids } : {});\n total.claimed += result.claimed;\n total.sent += result.sent;\n total.failed += result.failed;\n total.skipped += result.skipped;\n if (result.claimed === 0) break;\n }\n\n return total;\n }\n\n let warnedMissingWaitUntil = false;\n\n function scheduleDeferred(\n waitUntil?: (promise: Promise<unknown>) => void,\n ids?: readonly string[],\n ): void {\n if (waitUntil) {\n waitUntil(\n drain({ maxSweeps: 10, ...(ids ? { ids } : {}) }).catch((error) =>\n deps.logger.error(\"deferred sweep failed\", { error }),\n ),\n );\n return;\n }\n\n if (!warnedMissingWaitUntil) {\n warnedMissingWaitUntil = true;\n deps.logger.warn(\n \"delivery.mode is 'deferred' but no delivery.waitUntil was supplied — \" +\n \"falling back to the cron sweep. Notifications are not lost, but they \" +\n \"will not be delivered until POST /cron runs.\",\n );\n }\n }\n\n function startWorker(options: { intervalMs?: number; batchSize?: number } = {}): Worker {\n const intervalMs = options.intervalMs ?? DEFAULT_WORKER_IDLE_MS;\n let stopped = false;\n let wake: (() => void) | undefined;\n let pending = false;\n\n const unsubscribe = deps.signals?.subscribe(DELIVERIES_CHANNEL, () => {\n pending = true;\n wake?.();\n });\n\n // Sleeps for the interval, or until a send publishes, whichever is first.\n const idle = (ms: number) =>\n new Promise<void>((resolve) => {\n if (pending) {\n pending = false;\n resolve();\n return;\n }\n const timer = setTimeout(done, ms);\n function done() {\n clearTimeout(timer);\n wake = undefined;\n pending = false;\n resolve();\n }\n wake = done;\n });\n\n const loop = (async () => {\n while (!stopped) {\n try {\n const result = await runOnce({ limit: options.batchSize ?? deps.batchSize });\n // Only idle when there was nothing to do; otherwise keep draining.\n if (result.claimed === 0 && !stopped) await idle(intervalMs);\n } catch (error) {\n deps.logger.error(\"worker sweep failed\", { error });\n if (!stopped) await idle(intervalMs);\n }\n }\n })();\n\n return {\n stop: async () => {\n stopped = true;\n unsubscribe?.();\n wake?.();\n // Let the in-flight sweep finish and release its leases.\n await loop;\n },\n };\n }\n\n return { runOnce, drain, scheduleDeferred, startWorker };\n}\n\nexport type Runner = ReturnType<typeof createRunner>;\n"]}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { toSnakeCase } from './chunk-AEYYTE5I.js';
|
|
2
|
+
|
|
3
|
+
// src/schema/render-mysql.ts
|
|
4
|
+
var quote = (value) => `\`${value.split("`").join("``")}\``;
|
|
5
|
+
function stringType(field, table, spec) {
|
|
6
|
+
const keys = [
|
|
7
|
+
...table.primaryKey ? [table.primaryKey] : [],
|
|
8
|
+
...(table.indexes ?? []).map((index) => index.on)
|
|
9
|
+
].filter((columns) => columns.includes(field));
|
|
10
|
+
if (keys.some((columns) => columns.length > 1)) return "VARCHAR(255)";
|
|
11
|
+
if (keys.length > 0) return "VARCHAR(768)";
|
|
12
|
+
if (spec.default !== void 0) return "VARCHAR(255)";
|
|
13
|
+
return "TEXT";
|
|
14
|
+
}
|
|
15
|
+
function columnType(field, table, spec) {
|
|
16
|
+
switch (spec.type) {
|
|
17
|
+
case "string":
|
|
18
|
+
return stringType(field, table, spec);
|
|
19
|
+
case "number":
|
|
20
|
+
return "INT";
|
|
21
|
+
case "boolean":
|
|
22
|
+
return "TINYINT(1)";
|
|
23
|
+
case "date":
|
|
24
|
+
return "DATETIME(3)";
|
|
25
|
+
case "json":
|
|
26
|
+
return "JSON";
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
var defaultClause = (spec) => {
|
|
30
|
+
if (spec.defaultNow) return " DEFAULT CURRENT_TIMESTAMP(3)";
|
|
31
|
+
if (spec.default === void 0) return "";
|
|
32
|
+
if (typeof spec.default === "string") return ` DEFAULT '${spec.default.replace(/'/g, "''")}'`;
|
|
33
|
+
if (typeof spec.default === "boolean") return ` DEFAULT ${spec.default ? 1 : 0}`;
|
|
34
|
+
return ` DEFAULT ${spec.default}`;
|
|
35
|
+
};
|
|
36
|
+
function renderMysqlDdl(schema, prefix = "") {
|
|
37
|
+
const statements = [];
|
|
38
|
+
for (const table of Object.values(schema)) {
|
|
39
|
+
const lines = Object.entries(table.fields).map(([field, spec]) => {
|
|
40
|
+
const nullability = spec.required ? " NOT NULL" : "";
|
|
41
|
+
return ` ${quote(toSnakeCase(field))} ${columnType(field, table, spec)}${nullability}${defaultClause(spec)}`;
|
|
42
|
+
});
|
|
43
|
+
if (table.primaryKey?.length) {
|
|
44
|
+
lines.push(
|
|
45
|
+
` PRIMARY KEY (${table.primaryKey.map((f) => quote(toSnakeCase(f))).join(", ")})`
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
for (const index of table.indexes ?? []) {
|
|
49
|
+
const name = quote(prefix + (index.name ?? `${table.tableName}_${index.on.join("_")}_idx`));
|
|
50
|
+
const columns = index.on.map((f) => quote(toSnakeCase(f))).join(", ");
|
|
51
|
+
lines.push(` ${index.unique ? "UNIQUE " : ""}INDEX ${name} (${columns})`);
|
|
52
|
+
}
|
|
53
|
+
statements.push(
|
|
54
|
+
`CREATE TABLE IF NOT EXISTS ${quote(prefix + table.tableName)} (
|
|
55
|
+
${lines.join(",\n")}
|
|
56
|
+
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4`
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
return statements;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export { renderMysqlDdl };
|
|
63
|
+
//# sourceMappingURL=chunk-M4R77BJD.js.map
|
|
64
|
+
//# sourceMappingURL=chunk-M4R77BJD.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/schema/render-mysql.ts"],"names":[],"mappings":"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","file":"chunk-M4R77BJD.js","sourcesContent":["import type { FieldDeclaration, SchemaDeclaration, TableDeclaration } from \"../core/plugin\";\nimport { toSnakeCase } from \"./declaration\";\n\nconst quote = (value: string) => `\\`${value.split(\"`\").join(\"``\")}\\``;\n\n/** InnoDB caps a key at 3072 bytes: 768 utf8mb4 chars for one column, 255 each for three. TEXT cannot be a key or carry a default. */\nfunction stringType(field: string, table: TableDeclaration, spec: FieldDeclaration): string {\n const keys = [\n ...(table.primaryKey ? [table.primaryKey] : []),\n ...(table.indexes ?? []).map((index) => index.on),\n ].filter((columns) => columns.includes(field));\n\n if (keys.some((columns) => columns.length > 1)) return \"VARCHAR(255)\";\n if (keys.length > 0) return \"VARCHAR(768)\";\n if (spec.default !== undefined) return \"VARCHAR(255)\";\n return \"TEXT\";\n}\n\nfunction columnType(field: string, table: TableDeclaration, spec: FieldDeclaration): string {\n switch (spec.type) {\n case \"string\":\n return stringType(field, table, spec);\n case \"number\":\n return \"INT\";\n case \"boolean\":\n return \"TINYINT(1)\";\n case \"date\":\n return \"DATETIME(3)\";\n case \"json\":\n return \"JSON\";\n }\n}\n\nconst defaultClause = (spec: FieldDeclaration): string => {\n if (spec.defaultNow) return \" DEFAULT CURRENT_TIMESTAMP(3)\";\n if (spec.default === undefined) return \"\";\n if (typeof spec.default === \"string\") return ` DEFAULT '${spec.default.replace(/'/g, \"''\")}'`;\n if (typeof spec.default === \"boolean\") return ` DEFAULT ${spec.default ? 1 : 0}`;\n return ` DEFAULT ${spec.default}`;\n};\n\n/** MySQL DDL, one CREATE TABLE IF NOT EXISTS per table with indexes inline (no CREATE INDEX IF NOT EXISTS). */\nexport function renderMysqlDdl(schema: SchemaDeclaration, prefix = \"\"): string[] {\n const statements: string[] = [];\n\n for (const table of Object.values(schema)) {\n const lines = Object.entries(table.fields).map(([field, spec]) => {\n const nullability = spec.required ? \" NOT NULL\" : \"\";\n return ` ${quote(toSnakeCase(field))} ${columnType(field, table, spec)}${nullability}${defaultClause(spec)}`;\n });\n\n if (table.primaryKey?.length) {\n lines.push(\n ` PRIMARY KEY (${table.primaryKey.map((f) => quote(toSnakeCase(f))).join(\", \")})`,\n );\n }\n\n for (const index of table.indexes ?? []) {\n const name = quote(prefix + (index.name ?? `${table.tableName}_${index.on.join(\"_\")}_idx`));\n const columns = index.on.map((f) => quote(toSnakeCase(f))).join(\", \");\n lines.push(` ${index.unique ? \"UNIQUE \" : \"\"}INDEX ${name} (${columns})`);\n }\n\n statements.push(\n `CREATE TABLE IF NOT EXISTS ${quote(prefix + table.tableName)} (\\n${lines.join(\",\\n\")}\\n) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4`,\n );\n }\n\n return statements;\n}\n"]}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// src/core/signals.ts
|
|
2
|
+
var DELIVERIES_CHANNEL = "deliveries";
|
|
3
|
+
var inboxChannel = (userId) => `inbox:${userId}`;
|
|
4
|
+
function createMemorySignals() {
|
|
5
|
+
const handlers = /* @__PURE__ */ new Map();
|
|
6
|
+
return {
|
|
7
|
+
crossProcess: false,
|
|
8
|
+
publish(channel) {
|
|
9
|
+
const set = handlers.get(channel);
|
|
10
|
+
if (!set) return;
|
|
11
|
+
for (const handler of [...set]) {
|
|
12
|
+
queueMicrotask(() => {
|
|
13
|
+
try {
|
|
14
|
+
handler();
|
|
15
|
+
} catch {
|
|
16
|
+
}
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
subscribe(channel, handler) {
|
|
21
|
+
let set = handlers.get(channel);
|
|
22
|
+
if (!set) {
|
|
23
|
+
set = /* @__PURE__ */ new Set();
|
|
24
|
+
handlers.set(channel, set);
|
|
25
|
+
}
|
|
26
|
+
set.add(handler);
|
|
27
|
+
return () => {
|
|
28
|
+
set?.delete(handler);
|
|
29
|
+
if (set?.size === 0) handlers.delete(channel);
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
function createBridgedSignals(transport, onError) {
|
|
35
|
+
const local = createMemorySignals();
|
|
36
|
+
const remoteUnsubscribes = /* @__PURE__ */ new Map();
|
|
37
|
+
const counts = /* @__PURE__ */ new Map();
|
|
38
|
+
return {
|
|
39
|
+
crossProcess: true,
|
|
40
|
+
publish(channel) {
|
|
41
|
+
local.publish(channel);
|
|
42
|
+
Promise.resolve().then(() => transport.publish(channel)).catch(onError);
|
|
43
|
+
},
|
|
44
|
+
subscribe(channel, handler) {
|
|
45
|
+
const unsubscribeLocal = local.subscribe(channel, handler);
|
|
46
|
+
counts.set(channel, (counts.get(channel) ?? 0) + 1);
|
|
47
|
+
if (!remoteUnsubscribes.has(channel)) {
|
|
48
|
+
remoteUnsubscribes.set(
|
|
49
|
+
channel,
|
|
50
|
+
Promise.resolve().then(() => transport.subscribe(channel, () => local.publish(channel))).catch((error) => {
|
|
51
|
+
onError(error);
|
|
52
|
+
return () => {
|
|
53
|
+
};
|
|
54
|
+
})
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
return () => {
|
|
58
|
+
unsubscribeLocal();
|
|
59
|
+
const remaining = (counts.get(channel) ?? 1) - 1;
|
|
60
|
+
if (remaining > 0) {
|
|
61
|
+
counts.set(channel, remaining);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
counts.delete(channel);
|
|
65
|
+
const pending = remoteUnsubscribes.get(channel);
|
|
66
|
+
remoteUnsubscribes.delete(channel);
|
|
67
|
+
pending?.then((unsubscribe) => unsubscribe()).catch(onError);
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export { DELIVERIES_CHANNEL, createBridgedSignals, createMemorySignals, inboxChannel };
|
|
74
|
+
//# sourceMappingURL=chunk-ORZKH224.js.map
|
|
75
|
+
//# sourceMappingURL=chunk-ORZKH224.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/core/signals.ts"],"names":[],"mappings":"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","file":"chunk-ORZKH224.js","sourcesContent":["/**\n * The wake-up seam. A publish carries no payload: it says \"something changed\n * on this channel, go look\", so losing one costs latency, never data.\n * RFC 0006 §4C.\n */\nexport type Signals = {\n /** True when a publish in one process reaches subscribers in another. */\n readonly crossProcess: boolean;\n publish(channel: string): void;\n subscribe(channel: string, handler: () => void): () => void;\n close?(): Promise<void> | void;\n};\n\nexport const DELIVERIES_CHANNEL = \"deliveries\";\nexport const inboxChannel = (userId: string) => `inbox:${userId}`;\n\n/** In-process only. The default, and exactly right on SQLite or a single replica. */\nexport function createMemorySignals(): Signals {\n const handlers = new Map<string, Set<() => void>>();\n\n return {\n crossProcess: false,\n publish(channel) {\n const set = handlers.get(channel);\n if (!set) return;\n for (const handler of [...set]) {\n queueMicrotask(() => {\n try {\n handler();\n } catch {\n // a subscriber's failure is its own problem\n }\n });\n }\n },\n subscribe(channel, handler) {\n let set = handlers.get(channel);\n if (!set) {\n set = new Set();\n handlers.set(channel, set);\n }\n set.add(handler);\n return () => {\n set?.delete(handler);\n if (set?.size === 0) handlers.delete(channel);\n };\n },\n };\n}\n\n/**\n * Wraps a cross-process transport (LISTEN/NOTIFY, a change stream) so local\n * subscribers still hear local publishes even if the transport drops them,\n * and a transport failure degrades to in-process rather than to silence.\n */\nexport function createBridgedSignals(\n transport: {\n publish(channel: string): Promise<void> | void;\n subscribe(channel: string, handler: () => void): Promise<() => void> | (() => void);\n },\n onError: (error: unknown) => void,\n): Signals {\n const local = createMemorySignals();\n const remoteUnsubscribes = new Map<string, Promise<() => void>>();\n const counts = new Map<string, number>();\n\n return {\n crossProcess: true,\n publish(channel) {\n local.publish(channel);\n Promise.resolve()\n .then(() => transport.publish(channel))\n .catch(onError);\n },\n subscribe(channel, handler) {\n const unsubscribeLocal = local.subscribe(channel, handler);\n counts.set(channel, (counts.get(channel) ?? 0) + 1);\n\n if (!remoteUnsubscribes.has(channel)) {\n remoteUnsubscribes.set(\n channel,\n Promise.resolve()\n .then(() => transport.subscribe(channel, () => local.publish(channel)))\n .catch((error) => {\n onError(error);\n return () => {};\n }),\n );\n }\n\n return () => {\n unsubscribeLocal();\n const remaining = (counts.get(channel) ?? 1) - 1;\n if (remaining > 0) {\n counts.set(channel, remaining);\n return;\n }\n counts.delete(channel);\n const pending = remoteUnsubscribes.get(channel);\n remoteUnsubscribes.delete(channel);\n pending?.then((unsubscribe) => unsubscribe()).catch(onError);\n };\n },\n };\n}\n"]}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { inboxChannel } from './chunk-ORZKH224.js';
|
|
1
2
|
import { encodeBase64Url, decodeBase64UrlBytes, decodeBase64Url } from './chunk-PTGHMCCG.js';
|
|
2
3
|
|
|
3
4
|
// src/core/tokens.ts
|
|
@@ -253,6 +254,9 @@ function createHandler(deps) {
|
|
|
253
254
|
const userId = await resolveUserId(request);
|
|
254
255
|
if (userId === "error") return empty(500);
|
|
255
256
|
if (userId === null) return empty(401);
|
|
257
|
+
if (request.method === "GET" && path === "/events") {
|
|
258
|
+
return deps.events ? events(request, userId, deps.events) : empty(404);
|
|
259
|
+
}
|
|
256
260
|
if (request.method === "GET" && path === "/") {
|
|
257
261
|
const limitParam = Number(url.searchParams.get("limit") ?? 20);
|
|
258
262
|
const limit = Number.isFinite(limitParam) ? Math.min(Math.max(limitParam, 1), 100) : 20;
|
|
@@ -277,6 +281,7 @@ function createHandler(deps) {
|
|
|
277
281
|
const before = typeof parsed.body.before === "string" ? new Date(parsed.body.before) : null;
|
|
278
282
|
const cutoff = before && !Number.isNaN(before.getTime()) ? new Date(before.getTime() + 1) : /* @__PURE__ */ new Date();
|
|
279
283
|
await deps.adapter.markSeen(userId, cutoff);
|
|
284
|
+
deps.signals.publish(inboxChannel(userId));
|
|
280
285
|
return json({ ok: true });
|
|
281
286
|
}
|
|
282
287
|
if (request.method === "POST" && path === "/read") {
|
|
@@ -288,19 +293,99 @@ function createHandler(deps) {
|
|
|
288
293
|
return json({ error: `at most ${MAX_READ_IDS} ids per request` }, 400);
|
|
289
294
|
}
|
|
290
295
|
const updated = await deps.adapter.markRead(userId, ids);
|
|
296
|
+
if (updated > 0) deps.signals.publish(inboxChannel(userId));
|
|
291
297
|
return updated === 0 ? empty(404) : json({ updated });
|
|
292
298
|
}
|
|
293
299
|
if (request.method === "POST" && path === "/read-all") {
|
|
294
|
-
|
|
300
|
+
const updated = await deps.adapter.markAllRead(userId);
|
|
301
|
+
if (updated > 0) deps.signals.publish(inboxChannel(userId));
|
|
302
|
+
return json({ updated });
|
|
295
303
|
}
|
|
296
304
|
return empty(404);
|
|
297
305
|
}
|
|
306
|
+
async function fingerprint(userId) {
|
|
307
|
+
const [page, unseen] = await Promise.all([
|
|
308
|
+
deps.adapter.listNotifications({ userId, limit: 1 }),
|
|
309
|
+
deps.adapter.countUnseen(userId)
|
|
310
|
+
]);
|
|
311
|
+
const newest = page.notifications[0];
|
|
312
|
+
return `${newest?.id ?? ""}|${newest?.readAt ? 1 : 0}|${unseen}`;
|
|
313
|
+
}
|
|
314
|
+
function events(request, userId, settings) {
|
|
315
|
+
const encoder3 = new TextEncoder();
|
|
316
|
+
const timers = [];
|
|
317
|
+
let unsubscribe;
|
|
318
|
+
let closed = false;
|
|
319
|
+
let cleanup = () => {
|
|
320
|
+
};
|
|
321
|
+
const stream = new ReadableStream({
|
|
322
|
+
start(controller) {
|
|
323
|
+
const write = (text) => {
|
|
324
|
+
if (closed) return;
|
|
325
|
+
try {
|
|
326
|
+
controller.enqueue(encoder3.encode(text));
|
|
327
|
+
} catch {
|
|
328
|
+
cleanup();
|
|
329
|
+
}
|
|
330
|
+
};
|
|
331
|
+
cleanup = () => {
|
|
332
|
+
if (closed) return;
|
|
333
|
+
closed = true;
|
|
334
|
+
unsubscribe?.();
|
|
335
|
+
for (const timer of timers) clearInterval(timer);
|
|
336
|
+
try {
|
|
337
|
+
controller.close();
|
|
338
|
+
} catch {
|
|
339
|
+
}
|
|
340
|
+
};
|
|
341
|
+
const changed = () => write("event: changed\ndata: {}\n\n");
|
|
342
|
+
write("retry: 3000\n\nevent: ready\ndata: {}\n\n");
|
|
343
|
+
unsubscribe = deps.signals.subscribe(inboxChannel(userId), changed);
|
|
344
|
+
timers.push(setInterval(() => write(": keepalive\n\n"), settings.heartbeatMs));
|
|
345
|
+
if (!deps.signals.crossProcess && settings.probeIntervalMs > 0) {
|
|
346
|
+
let last;
|
|
347
|
+
fingerprint(userId).then((value) => {
|
|
348
|
+
last = value;
|
|
349
|
+
}).catch(() => {
|
|
350
|
+
});
|
|
351
|
+
timers.push(
|
|
352
|
+
setInterval(() => {
|
|
353
|
+
fingerprint(userId).then((value) => {
|
|
354
|
+
if (last !== void 0 && value !== last) changed();
|
|
355
|
+
last = value;
|
|
356
|
+
}).catch((error) => deps.logger.error("events probe failed", { error }));
|
|
357
|
+
}, settings.probeIntervalMs)
|
|
358
|
+
);
|
|
359
|
+
}
|
|
360
|
+
if (settings.maxDurationMs > 0) {
|
|
361
|
+
timers.push(
|
|
362
|
+
setTimeout(cleanup, settings.maxDurationMs)
|
|
363
|
+
);
|
|
364
|
+
}
|
|
365
|
+
request.signal?.addEventListener("abort", cleanup, { once: true });
|
|
366
|
+
},
|
|
367
|
+
cancel() {
|
|
368
|
+
cleanup();
|
|
369
|
+
}
|
|
370
|
+
});
|
|
371
|
+
return new Response(stream, {
|
|
372
|
+
headers: {
|
|
373
|
+
"content-type": "text/event-stream; charset=utf-8",
|
|
374
|
+
"cache-control": "no-cache, no-store",
|
|
375
|
+
connection: "keep-alive",
|
|
376
|
+
"x-accel-buffering": "no",
|
|
377
|
+
"x-content-type-options": "nosniff"
|
|
378
|
+
}
|
|
379
|
+
});
|
|
380
|
+
}
|
|
298
381
|
async function handle(request) {
|
|
299
382
|
try {
|
|
300
383
|
return await route(request);
|
|
301
384
|
} catch (error) {
|
|
302
385
|
deps.logger.error("request failed", { url: request.url, error });
|
|
303
386
|
return empty(500);
|
|
387
|
+
} finally {
|
|
388
|
+
deps.sweepOnRequest?.();
|
|
304
389
|
}
|
|
305
390
|
}
|
|
306
391
|
return {
|
|
@@ -311,5 +396,5 @@ function createHandler(deps) {
|
|
|
311
396
|
}
|
|
312
397
|
|
|
313
398
|
export { DEFAULT_TOKEN_TTL_SECONDS, MAX_BODY_BYTES, MAX_TOKEN_TTL_SECONDS, createHandler, createScopedSigner, expiresIn, readJsonBody, signToken, verifyToken };
|
|
314
|
-
//# sourceMappingURL=chunk-
|
|
315
|
-
//# sourceMappingURL=chunk-
|
|
399
|
+
//# sourceMappingURL=chunk-USQ6CQTJ.js.map
|
|
400
|
+
//# sourceMappingURL=chunk-USQ6CQTJ.js.map
|