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,605 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// src/core/base64url.ts
|
|
4
|
+
function encodeBase64Url(input) {
|
|
5
|
+
const bytes = typeof input === "string" ? new TextEncoder().encode(input) : input;
|
|
6
|
+
let binary = "";
|
|
7
|
+
for (const byte of bytes) binary += String.fromCharCode(byte);
|
|
8
|
+
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
9
|
+
}
|
|
10
|
+
function decodeBase64UrlBytes(value) {
|
|
11
|
+
try {
|
|
12
|
+
const binary = atob(value.replace(/-/g, "+").replace(/_/g, "/"));
|
|
13
|
+
return Uint8Array.from(binary, (char) => char.charCodeAt(0));
|
|
14
|
+
} catch {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
function decodeBase64Url(value) {
|
|
19
|
+
const bytes = decodeBase64UrlBytes(value);
|
|
20
|
+
return bytes ? new TextDecoder().decode(bytes) : null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// src/schema/declaration.ts
|
|
24
|
+
var toSnakeCase = (value) => value.replace(/[A-Z]/g, (c) => `_${c.toLowerCase()}`);
|
|
25
|
+
var coreSchema = {
|
|
26
|
+
notification: {
|
|
27
|
+
tableName: "notification",
|
|
28
|
+
fields: {
|
|
29
|
+
id: { type: "string", required: true },
|
|
30
|
+
userId: { type: "string", required: true },
|
|
31
|
+
type: { type: "string", required: true },
|
|
32
|
+
payload: { type: "json", required: true },
|
|
33
|
+
actorId: { type: "string" },
|
|
34
|
+
groupKey: { type: "string" },
|
|
35
|
+
dedupeKey: { type: "string" },
|
|
36
|
+
seenAt: { type: "date" },
|
|
37
|
+
readAt: { type: "date" },
|
|
38
|
+
archivedAt: { type: "date" },
|
|
39
|
+
createdAt: { type: "date", required: true, defaultNow: true }
|
|
40
|
+
},
|
|
41
|
+
primaryKey: ["id"],
|
|
42
|
+
indexes: [
|
|
43
|
+
{ on: ["userId", "createdAt"], name: "notification_feed_idx" },
|
|
44
|
+
{ on: ["userId", "groupKey"], name: "notification_group_idx" },
|
|
45
|
+
{ on: ["userId", "dedupeKey"], unique: true, name: "notification_dedupe_idx" }
|
|
46
|
+
]
|
|
47
|
+
},
|
|
48
|
+
notificationDelivery: {
|
|
49
|
+
tableName: "notification_delivery",
|
|
50
|
+
fields: {
|
|
51
|
+
id: { type: "string", required: true },
|
|
52
|
+
notificationId: { type: "string", required: true },
|
|
53
|
+
channel: { type: "string", required: true },
|
|
54
|
+
status: { type: "string", required: true, default: "pending" },
|
|
55
|
+
attempts: { type: "number", required: true, default: 0 },
|
|
56
|
+
maxAttempts: { type: "number", required: true, default: 5 },
|
|
57
|
+
notBefore: { type: "date", required: true, defaultNow: true },
|
|
58
|
+
claimedAt: { type: "date" },
|
|
59
|
+
claimedBy: { type: "string" },
|
|
60
|
+
lastError: { type: "string" },
|
|
61
|
+
updatedAt: { type: "date", required: true, defaultNow: true }
|
|
62
|
+
},
|
|
63
|
+
primaryKey: ["id"],
|
|
64
|
+
indexes: [
|
|
65
|
+
{ on: ["status", "notBefore"], name: "delivery_claim_idx" },
|
|
66
|
+
{ on: ["notificationId"], name: "delivery_notification_idx" }
|
|
67
|
+
]
|
|
68
|
+
},
|
|
69
|
+
notificationPreference: {
|
|
70
|
+
tableName: "notification_preference",
|
|
71
|
+
fields: {
|
|
72
|
+
userId: { type: "string", required: true },
|
|
73
|
+
type: { type: "string", required: true },
|
|
74
|
+
channel: { type: "string", required: true },
|
|
75
|
+
enabled: { type: "boolean", required: true, default: true },
|
|
76
|
+
frequency: { type: "string", required: true, default: "instant" },
|
|
77
|
+
updatedAt: { type: "date", required: true, defaultNow: true }
|
|
78
|
+
},
|
|
79
|
+
primaryKey: ["userId", "type", "channel"]
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
// src/core/store.ts
|
|
84
|
+
var OPERATORS = ["in", "lt", "lte", "gt", "gte", "not"];
|
|
85
|
+
var isOperator = (value) => typeof value === "object" && value !== null && !(value instanceof Date) && OPERATORS.some((op) => Object.hasOwn(value, op));
|
|
86
|
+
|
|
87
|
+
// src/adapters/sql/statement.ts
|
|
88
|
+
var defaultBind = (value) => value instanceof Date ? value.toISOString() : value;
|
|
89
|
+
var Statement = class {
|
|
90
|
+
#text = "";
|
|
91
|
+
params = [];
|
|
92
|
+
#placeholder;
|
|
93
|
+
#bind;
|
|
94
|
+
constructor(options = {}) {
|
|
95
|
+
this.#placeholder = options.placeholder ?? "dollar";
|
|
96
|
+
this.#bind = options.bind ?? defaultBind;
|
|
97
|
+
}
|
|
98
|
+
/** Raw SQL. Never user input; use `value`. */
|
|
99
|
+
raw(fragment) {
|
|
100
|
+
this.#text += fragment;
|
|
101
|
+
return this;
|
|
102
|
+
}
|
|
103
|
+
value(value) {
|
|
104
|
+
this.params.push(this.#bind(value));
|
|
105
|
+
this.#text += this.#placeholder === "dollar" ? `$${this.params.length}` : "?";
|
|
106
|
+
return this;
|
|
107
|
+
}
|
|
108
|
+
list(values, separator = ", ") {
|
|
109
|
+
values.forEach((value, index) => {
|
|
110
|
+
if (index > 0) this.raw(separator);
|
|
111
|
+
this.value(value);
|
|
112
|
+
});
|
|
113
|
+
return this;
|
|
114
|
+
}
|
|
115
|
+
/** `($1::text, $2::timestamptz)`. Casts are Postgres-only and dropped elsewhere. */
|
|
116
|
+
tuple(cells) {
|
|
117
|
+
this.raw("(");
|
|
118
|
+
cells.forEach(([value, cast], index) => {
|
|
119
|
+
if (index > 0) this.raw(", ");
|
|
120
|
+
this.value(value);
|
|
121
|
+
if (this.#placeholder === "dollar") this.raw(`::${cast}`);
|
|
122
|
+
});
|
|
123
|
+
return this.raw(")");
|
|
124
|
+
}
|
|
125
|
+
get text() {
|
|
126
|
+
return this.#text;
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
var quoteWith = (quoteChar) => (identifier) => `${quoteChar}${identifier.split(quoteChar).join(quoteChar + quoteChar)}${quoteChar}`;
|
|
130
|
+
|
|
131
|
+
// src/adapters/sql/adapter.ts
|
|
132
|
+
var str = (value) => String(value);
|
|
133
|
+
var nullableStr = (value) => value == null ? null : String(value);
|
|
134
|
+
var num = (value) => Number(value);
|
|
135
|
+
var encodeCursor = (createdAt, id) => encodeBase64Url(`${createdAt.toISOString()}|${id}`);
|
|
136
|
+
function decodeCursor(cursor) {
|
|
137
|
+
const decoded = decodeBase64Url(cursor);
|
|
138
|
+
if (!decoded) return null;
|
|
139
|
+
const [iso, id] = decoded.split("|");
|
|
140
|
+
if (!iso || !id) return null;
|
|
141
|
+
const createdAt = new Date(iso);
|
|
142
|
+
return Number.isNaN(createdAt.getTime()) ? null : { createdAt, id };
|
|
143
|
+
}
|
|
144
|
+
var NONE = { rows: [], affectedRows: 0 };
|
|
145
|
+
var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
146
|
+
async function retryingLocks(dialect, attempt) {
|
|
147
|
+
for (let tries = 0; ; tries += 1) {
|
|
148
|
+
try {
|
|
149
|
+
return await attempt();
|
|
150
|
+
} catch (error) {
|
|
151
|
+
if (tries >= 5 || !dialect.isRetryableLock(error)) throw error;
|
|
152
|
+
await sleep(5 + Math.random() * 20 * (tries + 1));
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
function createSqlAdapter(dialect, exec, options = {}) {
|
|
157
|
+
const prefix = options.prefix ?? "";
|
|
158
|
+
const q = dialect.quote;
|
|
159
|
+
const NOTIFICATION = q(`${prefix}notification`);
|
|
160
|
+
const DELIVERY = q(`${prefix}notification_delivery`);
|
|
161
|
+
const stmt = () => new Statement({ placeholder: dialect.placeholder, bind: dialect.bind });
|
|
162
|
+
const run = (statement, on = exec) => on(statement.text, statement.params);
|
|
163
|
+
const atomically = (fn) => options.transaction ? options.transaction(fn) : fn(exec);
|
|
164
|
+
const toNotification = (row) => ({
|
|
165
|
+
id: str(row.id),
|
|
166
|
+
userId: str(row.user_id),
|
|
167
|
+
type: str(row.type),
|
|
168
|
+
payload: dialect.readJson(row.payload),
|
|
169
|
+
actorId: nullableStr(row.actor_id),
|
|
170
|
+
groupKey: nullableStr(row.group_key),
|
|
171
|
+
dedupeKey: nullableStr(row.dedupe_key),
|
|
172
|
+
seenAt: dialect.readNullableDate(row.seen_at),
|
|
173
|
+
readAt: dialect.readNullableDate(row.read_at),
|
|
174
|
+
archivedAt: dialect.readNullableDate(row.archived_at),
|
|
175
|
+
createdAt: dialect.readDate(row.created_at)
|
|
176
|
+
});
|
|
177
|
+
const toDelivery = (row) => ({
|
|
178
|
+
id: str(row.id),
|
|
179
|
+
notificationId: str(row.notification_id),
|
|
180
|
+
channel: str(row.channel),
|
|
181
|
+
status: str(row.status),
|
|
182
|
+
attempts: num(row.attempts),
|
|
183
|
+
maxAttempts: num(row.max_attempts),
|
|
184
|
+
notBefore: dialect.readDate(row.not_before),
|
|
185
|
+
claimedAt: dialect.readNullableDate(row.claimed_at),
|
|
186
|
+
claimedBy: nullableStr(row.claimed_by),
|
|
187
|
+
lastError: nullableStr(row.last_error),
|
|
188
|
+
updatedAt: dialect.readDate(row.updated_at)
|
|
189
|
+
});
|
|
190
|
+
function appendWhere(statement, where) {
|
|
191
|
+
const entries = Object.entries(where);
|
|
192
|
+
if (entries.length === 0) return;
|
|
193
|
+
statement.raw(" WHERE ");
|
|
194
|
+
entries.forEach(([field, condition], index) => {
|
|
195
|
+
if (index > 0) statement.raw(" AND ");
|
|
196
|
+
const column = q(toSnakeCase(field));
|
|
197
|
+
if (condition === null) {
|
|
198
|
+
statement.raw(`${column} IS NULL`);
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
if (!isOperator(condition)) {
|
|
202
|
+
statement.raw(`${column} = `).value(condition);
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
const operator = condition;
|
|
206
|
+
if ("in" in operator) {
|
|
207
|
+
const list = operator.in;
|
|
208
|
+
if (list.length === 0) statement.raw("1 = 0");
|
|
209
|
+
else statement.raw(`${column} IN (`).list(list).raw(")");
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
if ("lt" in operator) statement.raw(`${column} < `).value(operator.lt);
|
|
213
|
+
else if ("lte" in operator) statement.raw(`${column} <= `).value(operator.lte);
|
|
214
|
+
else if ("gt" in operator) statement.raw(`${column} > `).value(operator.gt);
|
|
215
|
+
else if ("gte" in operator) statement.raw(`${column} >= `).value(operator.gte);
|
|
216
|
+
else if ("not" in operator) {
|
|
217
|
+
if (operator.not === null) statement.raw(`${column} IS NOT NULL`);
|
|
218
|
+
else dialect.distinctFrom(statement, column, operator.not);
|
|
219
|
+
} else statement.raw("1 = 1");
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
function eligible(statement, args, now, staleBefore) {
|
|
223
|
+
statement.raw("(status = 'pending' OR (status = 'claimed' AND claimed_at < ").value(staleBefore).raw(")) AND not_before <= ").value(now).raw(" AND attempts < max_attempts");
|
|
224
|
+
if (args.channels && args.channels.length > 0) {
|
|
225
|
+
statement.raw(" AND channel IN (").list(args.channels).raw(")");
|
|
226
|
+
}
|
|
227
|
+
if (args.ids && args.ids.length > 0) {
|
|
228
|
+
statement.raw(" AND id IN (").list(args.ids).raw(")");
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
const readClaimed = (token, on) => run(
|
|
232
|
+
stmt().raw(
|
|
233
|
+
`SELECT c.id, c.notification_id, c.channel, c.attempts, c.max_attempts, n.user_id, n.type, n.payload, n.actor_id FROM ${DELIVERY} c JOIN ${NOTIFICATION} n ON n.id = c.notification_id WHERE c.claimed_by = `
|
|
234
|
+
).value(token).raw(" AND c.status = 'claimed' ORDER BY c.not_before ASC, c.id ASC"),
|
|
235
|
+
on
|
|
236
|
+
);
|
|
237
|
+
return {
|
|
238
|
+
name: dialect.name,
|
|
239
|
+
naming: "snake_case",
|
|
240
|
+
serializesJson: true,
|
|
241
|
+
async createNotifications(input) {
|
|
242
|
+
if (input.length === 0) return { created: [], deduped: [] };
|
|
243
|
+
const now = /* @__PURE__ */ new Date();
|
|
244
|
+
return atomically(async (on) => {
|
|
245
|
+
const created = [];
|
|
246
|
+
const deduped = [];
|
|
247
|
+
for (const row of input) {
|
|
248
|
+
if (row.dedupeKey) {
|
|
249
|
+
const existing = await run(
|
|
250
|
+
stmt().raw(`SELECT id FROM ${NOTIFICATION} WHERE user_id = `).value(row.userId).raw(" AND dedupe_key = ").value(row.dedupeKey),
|
|
251
|
+
on
|
|
252
|
+
);
|
|
253
|
+
if (existing.rows.length > 0) {
|
|
254
|
+
deduped.push(row.id);
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
try {
|
|
259
|
+
await run(
|
|
260
|
+
stmt().raw(
|
|
261
|
+
`INSERT INTO ${NOTIFICATION} (id, user_id, type, payload, actor_id, group_key, dedupe_key, created_at) VALUES (`
|
|
262
|
+
).list([
|
|
263
|
+
row.id,
|
|
264
|
+
row.userId,
|
|
265
|
+
row.type,
|
|
266
|
+
JSON.stringify(row.payload ?? null),
|
|
267
|
+
row.actorId ?? null,
|
|
268
|
+
row.groupKey ?? null,
|
|
269
|
+
row.dedupeKey ?? null,
|
|
270
|
+
now
|
|
271
|
+
]).raw(")"),
|
|
272
|
+
on
|
|
273
|
+
);
|
|
274
|
+
} catch (error) {
|
|
275
|
+
if (row.dedupeKey && dialect.isDuplicateKey(error)) {
|
|
276
|
+
deduped.push(row.id);
|
|
277
|
+
continue;
|
|
278
|
+
}
|
|
279
|
+
throw error;
|
|
280
|
+
}
|
|
281
|
+
created.push(row.id);
|
|
282
|
+
}
|
|
283
|
+
const createdSet = new Set(created);
|
|
284
|
+
const deliveries = input.filter((row) => createdSet.has(row.id)).flatMap((row) => row.deliveries.map((delivery) => ({ row, delivery })));
|
|
285
|
+
if (deliveries.length > 0) {
|
|
286
|
+
const insert = stmt().raw(
|
|
287
|
+
`INSERT INTO ${DELIVERY} (id, notification_id, channel, max_attempts, not_before, updated_at) VALUES `
|
|
288
|
+
);
|
|
289
|
+
deliveries.forEach(({ row, delivery }, index) => {
|
|
290
|
+
if (index > 0) insert.raw(", ");
|
|
291
|
+
insert.raw("(").list([
|
|
292
|
+
delivery.id,
|
|
293
|
+
row.id,
|
|
294
|
+
delivery.channel,
|
|
295
|
+
delivery.maxAttempts,
|
|
296
|
+
delivery.notBefore,
|
|
297
|
+
now
|
|
298
|
+
]).raw(")");
|
|
299
|
+
});
|
|
300
|
+
await run(insert, on);
|
|
301
|
+
}
|
|
302
|
+
return { created, deduped };
|
|
303
|
+
});
|
|
304
|
+
},
|
|
305
|
+
async claimPendingDeliveries(args) {
|
|
306
|
+
const now = args.now ?? /* @__PURE__ */ new Date();
|
|
307
|
+
const staleBefore = new Date(now.getTime() - args.leaseMs);
|
|
308
|
+
const strategy = options.transaction ? dialect.claim.withTransaction : dialect.claim.withoutTransaction;
|
|
309
|
+
const claimSet = (statement) => statement.raw("status = 'claimed', claimed_at = ").value(now).raw(", claimed_by = ").value(args.claimToken);
|
|
310
|
+
let result;
|
|
311
|
+
if (strategy === "skip-locked") {
|
|
312
|
+
result = await atomically(async (on) => {
|
|
313
|
+
const pick = stmt().raw(`SELECT id FROM ${DELIVERY} WHERE `);
|
|
314
|
+
eligible(pick, args, now, staleBefore);
|
|
315
|
+
pick.raw(" ORDER BY not_before ASC, id ASC LIMIT ").value(args.limit).raw(" FOR UPDATE SKIP LOCKED");
|
|
316
|
+
const ids = (await run(pick, on)).rows.map((row) => str(row.id));
|
|
317
|
+
if (ids.length === 0) return NONE;
|
|
318
|
+
const update = stmt().raw(`UPDATE ${DELIVERY} SET `);
|
|
319
|
+
claimSet(update).raw(" WHERE id IN (").list(ids).raw(")");
|
|
320
|
+
await run(update, on);
|
|
321
|
+
return readClaimed(args.claimToken, on);
|
|
322
|
+
});
|
|
323
|
+
} else {
|
|
324
|
+
const update = stmt();
|
|
325
|
+
if (strategy === "derived-update") {
|
|
326
|
+
update.raw(`UPDATE ${DELIVERY} d JOIN (SELECT id FROM ${DELIVERY} WHERE `);
|
|
327
|
+
eligible(update, args, now, staleBefore);
|
|
328
|
+
update.raw(" ORDER BY not_before ASC, id ASC LIMIT ").value(args.limit).raw(") s ON s.id = d.id SET d.status = 'claimed', d.claimed_at = ").value(now).raw(", d.claimed_by = ").value(args.claimToken).raw(" WHERE d.status = 'pending' OR (d.status = 'claimed' AND d.claimed_at < ").value(staleBefore).raw(")");
|
|
329
|
+
} else {
|
|
330
|
+
update.raw(`UPDATE ${DELIVERY} SET `);
|
|
331
|
+
claimSet(update).raw(` WHERE id IN (SELECT id FROM ${DELIVERY} WHERE `);
|
|
332
|
+
eligible(update, args, now, staleBefore);
|
|
333
|
+
update.raw(" ORDER BY not_before ASC, id ASC LIMIT ").value(args.limit).raw(")");
|
|
334
|
+
}
|
|
335
|
+
const updated = await retryingLocks(dialect, () => run(update));
|
|
336
|
+
result = updated.affectedRows > 0 ? await readClaimed(args.claimToken, exec) : NONE;
|
|
337
|
+
}
|
|
338
|
+
return result.rows.map((row) => ({
|
|
339
|
+
id: str(row.id),
|
|
340
|
+
notificationId: str(row.notification_id),
|
|
341
|
+
channel: str(row.channel),
|
|
342
|
+
attempts: num(row.attempts),
|
|
343
|
+
maxAttempts: num(row.max_attempts),
|
|
344
|
+
notification: {
|
|
345
|
+
userId: str(row.user_id),
|
|
346
|
+
type: str(row.type),
|
|
347
|
+
payload: dialect.readJson(row.payload),
|
|
348
|
+
actorId: nullableStr(row.actor_id)
|
|
349
|
+
}
|
|
350
|
+
}));
|
|
351
|
+
},
|
|
352
|
+
async releaseDeliveries(releases) {
|
|
353
|
+
if (releases.length === 0) return;
|
|
354
|
+
const now = /* @__PURE__ */ new Date();
|
|
355
|
+
await atomically(async (on) => {
|
|
356
|
+
for (const { id, outcome, nextAttemptAt } of releases) {
|
|
357
|
+
const result = outcome.result;
|
|
358
|
+
const retryable = outcome.result === "failed" && outcome.retryable;
|
|
359
|
+
const error = outcome.result === "failed" ? outcome.error.slice(0, 2e3) : outcome.result === "skipped" ? outcome.reason.slice(0, 2e3) : null;
|
|
360
|
+
await run(
|
|
361
|
+
stmt().raw(`UPDATE ${DELIVERY} SET status = CASE WHEN `).value(result).raw(" = 'sent' THEN 'sent' WHEN ").value(result).raw(" = 'skipped' THEN 'skipped' WHEN ").value(retryable).raw(" = 1 AND attempts + 1 < max_attempts THEN 'pending' ELSE 'failed' END").raw(", attempts = CASE WHEN ").value(result).raw(" IN ('sent', 'skipped') THEN attempts ELSE attempts + 1 END").raw(", last_error = ").value(error).raw(", not_before = COALESCE(").value(nextAttemptAt ?? null).raw(", not_before), claimed_at = NULL, claimed_by = NULL, updated_at = ").value(now).raw(" WHERE id = ").value(id),
|
|
362
|
+
on
|
|
363
|
+
);
|
|
364
|
+
}
|
|
365
|
+
});
|
|
366
|
+
},
|
|
367
|
+
async listNotifications(feed) {
|
|
368
|
+
const cursor = feed.cursor ? decodeCursor(feed.cursor) : null;
|
|
369
|
+
const statement = stmt().raw(`SELECT * FROM ${NOTIFICATION} WHERE user_id = `).value(feed.userId).raw(" AND archived_at IS NULL");
|
|
370
|
+
if (feed.unreadOnly) statement.raw(" AND read_at IS NULL");
|
|
371
|
+
if (cursor) {
|
|
372
|
+
statement.raw(" AND (created_at, id) < (").value(cursor.createdAt).raw(", ").value(cursor.id).raw(")");
|
|
373
|
+
}
|
|
374
|
+
statement.raw(" ORDER BY created_at DESC, id DESC LIMIT ").value(feed.limit + 1);
|
|
375
|
+
const { rows } = await run(statement);
|
|
376
|
+
const page = rows.slice(0, feed.limit).map(toNotification);
|
|
377
|
+
const last = page.at(-1);
|
|
378
|
+
return {
|
|
379
|
+
notifications: page,
|
|
380
|
+
nextCursor: rows.length > feed.limit && last ? encodeCursor(last.createdAt, last.id) : null
|
|
381
|
+
};
|
|
382
|
+
},
|
|
383
|
+
async countUnseen(userId) {
|
|
384
|
+
const { rows } = await run(
|
|
385
|
+
stmt().raw(`SELECT COUNT(*) AS count FROM ${NOTIFICATION} WHERE user_id = `).value(userId).raw(" AND seen_at IS NULL AND archived_at IS NULL")
|
|
386
|
+
);
|
|
387
|
+
return num(rows[0]?.count ?? 0);
|
|
388
|
+
},
|
|
389
|
+
async markSeen(userId, before) {
|
|
390
|
+
await run(
|
|
391
|
+
stmt().raw(`UPDATE ${NOTIFICATION} SET seen_at = `).value(/* @__PURE__ */ new Date()).raw(" WHERE user_id = ").value(userId).raw(" AND seen_at IS NULL AND created_at <= ").value(before)
|
|
392
|
+
);
|
|
393
|
+
},
|
|
394
|
+
async markRead(userId, notificationIds) {
|
|
395
|
+
if (notificationIds.length === 0) return 0;
|
|
396
|
+
await run(
|
|
397
|
+
stmt().raw(`UPDATE ${NOTIFICATION} SET read_at = COALESCE(read_at, `).value(/* @__PURE__ */ new Date()).raw(") WHERE user_id = ").value(userId).raw(" AND id IN (").list(notificationIds).raw(")")
|
|
398
|
+
);
|
|
399
|
+
const { rows } = await run(
|
|
400
|
+
stmt().raw(`SELECT COUNT(*) AS count FROM ${NOTIFICATION} WHERE user_id = `).value(userId).raw(" AND id IN (").list(notificationIds).raw(")")
|
|
401
|
+
);
|
|
402
|
+
return num(rows[0]?.count ?? 0);
|
|
403
|
+
},
|
|
404
|
+
async markAllRead(userId) {
|
|
405
|
+
const { affectedRows } = await run(
|
|
406
|
+
stmt().raw(`UPDATE ${NOTIFICATION} SET read_at = `).value(/* @__PURE__ */ new Date()).raw(" WHERE user_id = ").value(userId).raw(" AND read_at IS NULL")
|
|
407
|
+
);
|
|
408
|
+
return affectedRows;
|
|
409
|
+
},
|
|
410
|
+
async getFailedDeliveries(args) {
|
|
411
|
+
const { rows } = await run(
|
|
412
|
+
stmt().raw(`SELECT * FROM ${DELIVERY} WHERE status = 'failed' AND updated_at >= `).value(args.since).raw(" ORDER BY updated_at DESC LIMIT ").value(args.limit)
|
|
413
|
+
);
|
|
414
|
+
return rows.map(toDelivery);
|
|
415
|
+
},
|
|
416
|
+
async queryTable(table, where, queryOptions) {
|
|
417
|
+
const statement = stmt().raw(`SELECT * FROM ${q(table)}`);
|
|
418
|
+
appendWhere(statement, where);
|
|
419
|
+
if (queryOptions.orderBy) {
|
|
420
|
+
const direction = queryOptions.orderBy.direction === "desc" ? "DESC" : "ASC";
|
|
421
|
+
statement.raw(` ORDER BY ${q(toSnakeCase(queryOptions.orderBy.field))} ${direction}`);
|
|
422
|
+
}
|
|
423
|
+
if (queryOptions.limit) statement.raw(" LIMIT ").value(queryOptions.limit);
|
|
424
|
+
return [...(await run(statement)).rows];
|
|
425
|
+
},
|
|
426
|
+
async insertRows(table, rows, onConflict) {
|
|
427
|
+
if (rows.length === 0) return 0;
|
|
428
|
+
const columns = Object.keys(rows[0] ?? {});
|
|
429
|
+
if (columns.length === 0) return 0;
|
|
430
|
+
const statement = stmt().raw(
|
|
431
|
+
`INSERT INTO ${q(table)} (${columns.map((c) => q(toSnakeCase(c))).join(", ")}) VALUES `
|
|
432
|
+
);
|
|
433
|
+
rows.forEach((row, index) => {
|
|
434
|
+
if (index > 0) statement.raw(", ");
|
|
435
|
+
statement.raw("(").list(columns.map((column) => row[column])).raw(")");
|
|
436
|
+
});
|
|
437
|
+
if (onConflict && onConflict.length > 0) {
|
|
438
|
+
statement.raw(
|
|
439
|
+
dialect.upsertClause(
|
|
440
|
+
onConflict.map((f) => q(toSnakeCase(f))),
|
|
441
|
+
columns.filter((c) => !onConflict.includes(c)).map((c) => q(toSnakeCase(c)))
|
|
442
|
+
)
|
|
443
|
+
);
|
|
444
|
+
await run(statement);
|
|
445
|
+
return rows.length;
|
|
446
|
+
}
|
|
447
|
+
return (await run(statement)).affectedRows;
|
|
448
|
+
},
|
|
449
|
+
async updateRows(table, where, set) {
|
|
450
|
+
const assignments = Object.entries(set);
|
|
451
|
+
if (assignments.length === 0) return 0;
|
|
452
|
+
const statement = stmt().raw(`UPDATE ${q(table)} SET `);
|
|
453
|
+
assignments.forEach(([field, value], index) => {
|
|
454
|
+
if (index > 0) statement.raw(", ");
|
|
455
|
+
statement.raw(`${q(toSnakeCase(field))} = `).value(value);
|
|
456
|
+
});
|
|
457
|
+
appendWhere(statement, where);
|
|
458
|
+
return (await run(statement)).affectedRows;
|
|
459
|
+
},
|
|
460
|
+
async deleteRows(table, where) {
|
|
461
|
+
const statement = stmt().raw(`DELETE FROM ${q(table)}`);
|
|
462
|
+
appendWhere(statement, where);
|
|
463
|
+
return (await run(statement)).affectedRows;
|
|
464
|
+
}
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// src/adapters/sql/dialect.ts
|
|
469
|
+
var parseUtc = (value) => {
|
|
470
|
+
if (value instanceof Date) return value;
|
|
471
|
+
const text = String(value).trim();
|
|
472
|
+
return new Date(/^\d{4}-\d{2}-\d{2} /.test(text) ? `${text.replace(" ", "T")}Z` : text);
|
|
473
|
+
};
|
|
474
|
+
var readJson = (value) => typeof value === "string" ? JSON.parse(value) : value;
|
|
475
|
+
var bindCommon = (value) => {
|
|
476
|
+
if (typeof value === "boolean") return value ? 1 : 0;
|
|
477
|
+
if (value === void 0) return null;
|
|
478
|
+
return value;
|
|
479
|
+
};
|
|
480
|
+
var code = (error) => error?.code;
|
|
481
|
+
var errno = (error) => error?.errno;
|
|
482
|
+
var mysqlDialect = {
|
|
483
|
+
name: "mysql",
|
|
484
|
+
placeholder: "question",
|
|
485
|
+
quote: quoteWith("`"),
|
|
486
|
+
bind: (value) => value instanceof Date ? value.toISOString().slice(0, 23).replace("T", " ") : bindCommon(value),
|
|
487
|
+
readDate: parseUtc,
|
|
488
|
+
readNullableDate: (value) => value == null ? null : parseUtc(value),
|
|
489
|
+
readJson,
|
|
490
|
+
distinctFrom: (statement, column, value) => {
|
|
491
|
+
statement.raw(`NOT (${column} <=> `).value(value).raw(")");
|
|
492
|
+
},
|
|
493
|
+
// VALUES() rather than the 8.0.19 `AS new` alias: it also works on MariaDB.
|
|
494
|
+
upsertClause: (conflict, update) => ` ON DUPLICATE KEY UPDATE ${(update.length === 0 ? [`${conflict[0]} = ${conflict[0]}`] : update.map((column) => `${column} = VALUES(${column})`)).join(", ")}`,
|
|
495
|
+
isDuplicateKey: (error) => code(error) === "ER_DUP_ENTRY" || errno(error) === 1062,
|
|
496
|
+
isRetryableLock: (error) => code(error) === "ER_LOCK_DEADLOCK" || errno(error) === 1213 || code(error) === "ER_LOCK_WAIT_TIMEOUT" || errno(error) === 1205,
|
|
497
|
+
claim: { withTransaction: "skip-locked", withoutTransaction: "derived-update" }
|
|
498
|
+
};
|
|
499
|
+
|
|
500
|
+
// src/adapters/mysql/adapter.ts
|
|
501
|
+
function mysqlAdapter(query, options = {}) {
|
|
502
|
+
return createSqlAdapter(mysqlDialect, query, options);
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// src/schema/render-mysql.ts
|
|
506
|
+
var quote2 = (value) => `\`${value.split("`").join("``")}\``;
|
|
507
|
+
function stringType(field, table, spec) {
|
|
508
|
+
const keys = [
|
|
509
|
+
...table.primaryKey ? [table.primaryKey] : [],
|
|
510
|
+
...(table.indexes ?? []).map((index) => index.on)
|
|
511
|
+
].filter((columns) => columns.includes(field));
|
|
512
|
+
if (keys.some((columns) => columns.length > 1)) return "VARCHAR(255)";
|
|
513
|
+
if (keys.length > 0) return "VARCHAR(768)";
|
|
514
|
+
if (spec.default !== void 0) return "VARCHAR(255)";
|
|
515
|
+
return "TEXT";
|
|
516
|
+
}
|
|
517
|
+
function columnType(field, table, spec) {
|
|
518
|
+
switch (spec.type) {
|
|
519
|
+
case "string":
|
|
520
|
+
return stringType(field, table, spec);
|
|
521
|
+
case "number":
|
|
522
|
+
return "INT";
|
|
523
|
+
case "boolean":
|
|
524
|
+
return "TINYINT(1)";
|
|
525
|
+
case "date":
|
|
526
|
+
return "DATETIME(3)";
|
|
527
|
+
case "json":
|
|
528
|
+
return "JSON";
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
var defaultClause = (spec) => {
|
|
532
|
+
if (spec.defaultNow) return " DEFAULT CURRENT_TIMESTAMP(3)";
|
|
533
|
+
if (spec.default === void 0) return "";
|
|
534
|
+
if (typeof spec.default === "string") return ` DEFAULT '${spec.default.replace(/'/g, "''")}'`;
|
|
535
|
+
if (typeof spec.default === "boolean") return ` DEFAULT ${spec.default ? 1 : 0}`;
|
|
536
|
+
return ` DEFAULT ${spec.default}`;
|
|
537
|
+
};
|
|
538
|
+
function renderMysqlDdl(schema, prefix = "") {
|
|
539
|
+
const statements = [];
|
|
540
|
+
for (const table of Object.values(schema)) {
|
|
541
|
+
const lines = Object.entries(table.fields).map(([field, spec]) => {
|
|
542
|
+
const nullability = spec.required ? " NOT NULL" : "";
|
|
543
|
+
return ` ${quote2(toSnakeCase(field))} ${columnType(field, table, spec)}${nullability}${defaultClause(spec)}`;
|
|
544
|
+
});
|
|
545
|
+
if (table.primaryKey?.length) {
|
|
546
|
+
lines.push(
|
|
547
|
+
` PRIMARY KEY (${table.primaryKey.map((f) => quote2(toSnakeCase(f))).join(", ")})`
|
|
548
|
+
);
|
|
549
|
+
}
|
|
550
|
+
for (const index of table.indexes ?? []) {
|
|
551
|
+
const name = quote2(prefix + (index.name ?? `${table.tableName}_${index.on.join("_")}_idx`));
|
|
552
|
+
const columns = index.on.map((f) => quote2(toSnakeCase(f))).join(", ");
|
|
553
|
+
lines.push(` ${index.unique ? "UNIQUE " : ""}INDEX ${name} (${columns})`);
|
|
554
|
+
}
|
|
555
|
+
statements.push(
|
|
556
|
+
`CREATE TABLE IF NOT EXISTS ${quote2(prefix + table.tableName)} (
|
|
557
|
+
${lines.join(",\n")}
|
|
558
|
+
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4`
|
|
559
|
+
);
|
|
560
|
+
}
|
|
561
|
+
return statements;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
// src/adapters/mysql/helpers.ts
|
|
565
|
+
function toResult([result]) {
|
|
566
|
+
if (Array.isArray(result)) return { rows: result, affectedRows: 0 };
|
|
567
|
+
const header = result;
|
|
568
|
+
return { rows: [], affectedRows: Number(header?.affectedRows ?? 0) };
|
|
569
|
+
}
|
|
570
|
+
var mysql2Query = (pool) => async (text, params) => toResult(await pool.query(text, [...params]));
|
|
571
|
+
function mysqlTransaction(pool) {
|
|
572
|
+
return async (fn) => {
|
|
573
|
+
const connection = await pool.getConnection();
|
|
574
|
+
try {
|
|
575
|
+
await connection.beginTransaction();
|
|
576
|
+
const result = await fn(
|
|
577
|
+
async (text, params) => toResult(await connection.query(text, [...params]))
|
|
578
|
+
);
|
|
579
|
+
await connection.commit();
|
|
580
|
+
return result;
|
|
581
|
+
} catch (error) {
|
|
582
|
+
await connection.rollback();
|
|
583
|
+
throw error;
|
|
584
|
+
} finally {
|
|
585
|
+
connection.release();
|
|
586
|
+
}
|
|
587
|
+
};
|
|
588
|
+
}
|
|
589
|
+
async function createMysqlTables(query, options = {}) {
|
|
590
|
+
let count = 0;
|
|
591
|
+
for (const schema of [coreSchema, ...options.plugins ?? []]) {
|
|
592
|
+
for (const statement of renderMysqlDdl(schema, options.prefix)) {
|
|
593
|
+
await query(statement, []);
|
|
594
|
+
count += 1;
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
return count;
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
exports.createMysqlTables = createMysqlTables;
|
|
601
|
+
exports.mysql2Query = mysql2Query;
|
|
602
|
+
exports.mysqlAdapter = mysqlAdapter;
|
|
603
|
+
exports.mysqlTransaction = mysqlTransaction;
|
|
604
|
+
//# sourceMappingURL=mysql.cjs.map
|
|
605
|
+
//# sourceMappingURL=mysql.cjs.map
|