easy-ping 0.2.0 → 0.4.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 +15 -4
- package/dist/{adapter-f0OR2DhY.d.cts → adapter-BfLe2J0t.d.cts} +10 -0
- package/dist/{adapter-f0OR2DhY.d.ts → adapter-BfLe2J0t.d.ts} +10 -0
- package/dist/adapters/drizzle.cjs +9 -4
- package/dist/adapters/drizzle.cjs.map +1 -1
- package/dist/adapters/drizzle.d.cts +35 -1
- package/dist/adapters/drizzle.d.ts +35 -1
- package/dist/adapters/drizzle.js +10 -5
- package/dist/adapters/drizzle.js.map +1 -1
- package/dist/adapters/mongodb.cjs +9 -4
- package/dist/adapters/mongodb.cjs.map +1 -1
- package/dist/adapters/mongodb.d.cts +2 -2
- package/dist/adapters/mongodb.d.ts +2 -2
- package/dist/adapters/mongodb.js +8 -4
- package/dist/adapters/mongodb.js.map +1 -1
- package/dist/adapters/postgres.cjs +136 -3
- package/dist/adapters/postgres.cjs.map +1 -1
- package/dist/adapters/postgres.d.cts +45 -2
- package/dist/adapters/postgres.d.ts +45 -2
- package/dist/adapters/postgres.js +38 -5
- package/dist/adapters/postgres.js.map +1 -1
- package/dist/{chunk-OZSOIX23.js → chunk-3Y2MBUAW.js} +53 -12
- package/dist/chunk-3Y2MBUAW.js.map +1 -0
- package/dist/chunk-CYKAM7LE.js +226 -0
- package/dist/chunk-CYKAM7LE.js.map +1 -0
- package/dist/chunk-DG3ZL5NP.js +315 -0
- package/dist/chunk-DG3ZL5NP.js.map +1 -0
- package/dist/{chunk-NYDVNMKG.js → chunk-PSJHXZPR.js} +4 -3
- package/dist/chunk-PSJHXZPR.js.map +1 -0
- package/dist/{chunk-BV3U542P.js → chunk-W6I3B2CI.js} +2 -2
- package/dist/chunk-W6I3B2CI.js.map +1 -0
- package/dist/chunk-YEMRRMJQ.js +199 -0
- package/dist/chunk-YEMRRMJQ.js.map +1 -0
- package/dist/client.cjs +3 -5
- package/dist/client.cjs.map +1 -1
- package/dist/client.js +3 -5
- package/dist/client.js.map +1 -1
- package/dist/index.cjs +377 -73
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +88 -22
- package/dist/index.d.ts +88 -22
- package/dist/index.js +119 -351
- package/dist/index.js.map +1 -1
- package/dist/node.cjs +27 -6
- package/dist/node.cjs.map +1 -1
- package/dist/node.d.cts +8 -2
- package/dist/node.d.ts +8 -2
- package/dist/node.js +26 -6
- package/dist/node.js.map +1 -1
- package/dist/{plugin-Bqj9xAOS.d.cts → plugin-C9Hoxus2.d.ts} +38 -10
- package/dist/{plugin-bTJKmnlq.d.ts → plugin-CiMsGlQe.d.cts} +38 -10
- package/dist/plugins/digests.d.cts +2 -2
- package/dist/plugins/digests.d.ts +2 -2
- 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.cjs +100 -15
- package/dist/plugins/preferences.cjs.map +1 -1
- package/dist/plugins/preferences.d.cts +4 -2
- package/dist/plugins/preferences.d.ts +4 -2
- package/dist/plugins/preferences.js +35 -7
- package/dist/plugins/preferences.js.map +1 -1
- package/dist/plugins/push.cjs +222 -61
- package/dist/plugins/push.cjs.map +1 -1
- package/dist/plugins/push.d.cts +81 -3
- package/dist/plugins/push.d.ts +81 -3
- package/dist/plugins/push.js +164 -62
- package/dist/plugins/push.js.map +1 -1
- package/dist/providers/web-push.cjs +37 -11
- package/dist/providers/web-push.cjs.map +1 -1
- package/dist/providers/web-push.d.cts +9 -3
- package/dist/providers/web-push.d.ts +9 -3
- package/dist/providers/web-push.js +37 -12
- package/dist/providers/web-push.js.map +1 -1
- package/dist/react.cjs +3 -5
- package/dist/react.cjs.map +1 -1
- package/dist/react.js +3 -5
- package/dist/react.js.map +1 -1
- package/dist/schema.cjs +2 -1
- package/dist/schema.cjs.map +1 -1
- package/dist/schema.d.cts +7 -2
- package/dist/schema.d.ts +7 -2
- package/dist/schema.js +2 -198
- package/dist/schema.js.map +1 -1
- package/dist/testing.cjs +29 -0
- package/dist/testing.cjs.map +1 -1
- package/dist/testing.d.cts +1 -1
- package/dist/testing.d.ts +1 -1
- package/dist/testing.js +29 -0
- package/dist/testing.js.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-BV3U542P.js.map +0 -1
- package/dist/chunk-NYDVNMKG.js.map +0 -1
- package/dist/chunk-OZSOIX23.js.map +0 -1
- package/dist/chunk-W3BMEPPQ.js +0 -50
- package/dist/chunk-W3BMEPPQ.js.map +0 -1
package/dist/testing.js
CHANGED
|
@@ -260,6 +260,35 @@ var adapterConformanceCases = [
|
|
|
260
260
|
);
|
|
261
261
|
}
|
|
262
262
|
},
|
|
263
|
+
{
|
|
264
|
+
// The whole point of the status: a delivery with nothing to do must not
|
|
265
|
+
// show up in the view an operator reads to find real breakage.
|
|
266
|
+
name: "a skipped delivery is terminal and stays out of getFailedDeliveries",
|
|
267
|
+
run: async (ctx) => {
|
|
268
|
+
const seeded = await seed(ctx, { maxAttempts: 5 });
|
|
269
|
+
const id = seeded.rows[0]?.deliveries[0]?.id;
|
|
270
|
+
assert(id, "seed produced no delivery");
|
|
271
|
+
await ctx.adapter.claimPendingDeliveries({ ...CLAIM, claimToken: uid("t") });
|
|
272
|
+
await ctx.adapter.releaseDeliveries([
|
|
273
|
+
{
|
|
274
|
+
id,
|
|
275
|
+
claimToken: "t",
|
|
276
|
+
outcome: { result: "skipped", reason: "no registered devices" }
|
|
277
|
+
}
|
|
278
|
+
]);
|
|
279
|
+
const failed = await ctx.adapter.getFailedDeliveries({
|
|
280
|
+
since: new Date(Date.now() - 6e4),
|
|
281
|
+
limit: 50
|
|
282
|
+
});
|
|
283
|
+
assert(!failed.some((row) => row.id === id), "a skipped delivery was reported as failed");
|
|
284
|
+
const reclaimed = await ctx.adapter.claimPendingDeliveries({
|
|
285
|
+
...CLAIM,
|
|
286
|
+
claimToken: uid("t"),
|
|
287
|
+
now: new Date(Date.now() + 36e5)
|
|
288
|
+
});
|
|
289
|
+
assert(!reclaimed.some((row) => row.id === id), "a skipped delivery was claimed again");
|
|
290
|
+
}
|
|
291
|
+
},
|
|
263
292
|
{
|
|
264
293
|
name: "duplicate dedupeKey is skipped, not thrown",
|
|
265
294
|
run: async (ctx) => {
|
package/dist/testing.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/testing/conformance.ts"],"names":[],"mappings":"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","file":"testing.js","sourcesContent":["import type { DatabaseAdapter, InsertNotification } from \"../core/adapter\";\nimport type { Channel } from \"../core/types\";\n\nexport type ConformanceContext = {\n adapter: DatabaseAdapter;\n /**\n * Forces a delivery's attempt count. Not reachable through the public API —\n * the adapter would have marked the row failed on the way there — so each\n * backend writes it directly.\n */\n setAttempts: (deliveryId: string, attempts: number) => Promise<void>;\n /**\n * Empties the tables. Cases must run against a clean table: claims are\n * ordered by not_before ASC and bounded by limit, so leftover rows from an\n * earlier case can push the row under test out of the claim window.\n */\n reset: () => Promise<void>;\n /**\n * Holds a row lock on a separate connection for the duration of `fn`.\n *\n * Required to test SKIP LOCKED deterministically. Two Promise.all'd claims\n * do not reliably overlap — they complete in single-digit milliseconds and\n * simply queue — so a race-based test passes even with no locking at all.\n *\n * Only meaningful where a claim can block on someone else's lock. A document\n * store whose claim is a single atomic update has nothing to skip, so cases\n * tagged `rowLock` do not apply to it.\n */\n lockRow?: (deliveryId: string, fn: () => Promise<void>) => Promise<void>;\n};\n\nexport type ConformanceCase = {\n name: string;\n /** Capability the case needs; a backend without it must filter the case out. */\n requires?: \"rowLock\";\n run: (ctx: ConformanceContext) => Promise<void>;\n};\n\nclass ConformanceError extends Error {}\n\nfunction assert(condition: unknown, message: string): asserts condition {\n if (!condition) throw new ConformanceError(message);\n}\n\nconst equal = (actual: unknown, expected: unknown, message: string) =>\n assert(actual === expected, `${message} (got ${String(actual)}, expected ${String(expected)})`);\n\nlet counter = 0;\nfunction uid(label: string): string {\n counter += 1;\n return `${label}_${Date.now().toString(36)}_${counter}`;\n}\n\ntype SeedOptions = {\n count?: number;\n notBefore?: Date;\n maxAttempts?: number;\n channel?: Channel;\n dedupeKey?: string;\n userId?: string;\n};\n\nasync function seed(ctx: ConformanceContext, options: SeedOptions = {}) {\n const count = options.count ?? 1;\n const userId = options.userId ?? uid(\"user\");\n\n const rows: InsertNotification[] = Array.from({ length: count }, () => {\n const id = uid(\"notif\");\n return {\n id,\n userId,\n type: \"commentReply\",\n payload: { authorName: \"Dana\" },\n ...(options.dedupeKey ? { dedupeKey: options.dedupeKey } : {}),\n deliveries: [\n {\n id: uid(\"delivery\"),\n channel: options.channel ?? \"email\",\n maxAttempts: options.maxAttempts ?? 5,\n notBefore: options.notBefore ?? new Date(Date.now() - 1000),\n },\n ],\n };\n });\n\n const result = await ctx.adapter.createNotifications(rows);\n return { userId, rows, result };\n}\n\nconst CLAIM = { limit: 10, leaseMs: 60_000 };\n\nexport const adapterConformanceCases: readonly ConformanceCase[] = [\n {\n /**\n * The case that actually distinguishes a correct adapter.\n *\n * A row locked by another transaction must be *skipped*, not waited on.\n * Without SKIP LOCKED the claim blocks until the holder commits, which on\n * a busy table serialises every worker behind the slowest one.\n */\n name: \"a row locked by another transaction is skipped, not waited on\",\n requires: \"rowLock\",\n run: async (ctx) => {\n const { lockRow } = ctx;\n assert(lockRow, \"case requires lockRow\");\n\n const { rows } = await seed(ctx, { count: 2 });\n const locked = rows[0]?.deliveries[0]?.id;\n const free = rows[1]?.deliveries[0]?.id;\n assert(locked && free, \"seed produced too few deliveries\");\n\n await lockRow(locked, async () => {\n const claimed = await Promise.race([\n ctx.adapter.claimPendingDeliveries({ ...CLAIM, claimToken: uid(\"token\") }),\n new Promise<never>((_, reject) =>\n setTimeout(\n () =>\n reject(\n new ConformanceError(\n \"claim blocked on a row locked by another transaction — SKIP LOCKED is missing\",\n ),\n ),\n 2_000,\n ),\n ),\n ]);\n\n const ids = new Set(claimed.map((row) => row.id));\n assert(!ids.has(locked), \"claimed a row locked by another transaction\");\n assert(ids.has(free), \"skipped an unlocked row alongside the locked one\");\n });\n },\n },\n\n {\n name: \"concurrent claims return disjoint sets\",\n run: async (ctx) => {\n await seed(ctx, { count: 40 });\n\n // Eight callers over 40 rows, repeated — a single Promise.all pair is\n // too fast to overlap and proves nothing on its own.\n const claimed = await Promise.all(\n Array.from({ length: 8 }, () =>\n ctx.adapter.claimPendingDeliveries({ limit: 5, leaseMs: 60_000, claimToken: uid(\"t\") }),\n ),\n );\n\n const all = claimed.flatMap((batch) => batch.map((row) => row.id));\n equal(new Set(all).size, all.length, \"the same delivery was claimed by two callers\");\n assert(all.length <= 40, \"claimed more rows than exist\");\n },\n },\n\n {\n name: \"a claimed row is invisible before its lease expires\",\n run: async (ctx) => {\n const { rows } = await seed(ctx);\n const target = rows[0]?.deliveries[0]?.id;\n\n const first = await ctx.adapter.claimPendingDeliveries({ ...CLAIM, claimToken: uid(\"t\") });\n assert(\n first.some((row) => row.id === target),\n \"seeded delivery was not claimed\",\n );\n\n const second = await ctx.adapter.claimPendingDeliveries({ ...CLAIM, claimToken: uid(\"t\") });\n assert(!second.some((row) => row.id === target), \"leased row was claimed twice\");\n },\n },\n\n {\n name: \"a claimed row is re-claimable after its lease expires\",\n run: async (ctx) => {\n const { rows } = await seed(ctx);\n const target = rows[0]?.deliveries[0]?.id;\n\n await ctx.adapter.claimPendingDeliveries({ ...CLAIM, claimToken: uid(\"t\") });\n\n const later = new Date(Date.now() + CLAIM.leaseMs + 5_000);\n const reclaimed = await ctx.adapter.claimPendingDeliveries({\n ...CLAIM,\n claimToken: uid(\"t\"),\n now: later,\n });\n\n assert(\n reclaimed.some((row) => row.id === target),\n \"expired lease was not reclaimed\",\n );\n },\n },\n\n {\n name: \"not_before in the future is never claimed\",\n run: async (ctx) => {\n const { rows } = await seed(ctx, { notBefore: new Date(Date.now() + 3_600_000) });\n const target = rows[0]?.deliveries[0]?.id;\n\n const claimed = await ctx.adapter.claimPendingDeliveries({ ...CLAIM, claimToken: uid(\"t\") });\n assert(!claimed.some((row) => row.id === target), \"claimed a delivery before its not_before\");\n },\n },\n\n {\n name: \"attempts at or beyond max_attempts is never claimed\",\n run: async (ctx) => {\n const { rows } = await seed(ctx, { maxAttempts: 3 });\n const target = rows[0]?.deliveries[0]?.id;\n assert(target, \"seed produced no delivery\");\n\n await ctx.setAttempts(target, 3);\n\n const claimed = await ctx.adapter.claimPendingDeliveries({ ...CLAIM, claimToken: uid(\"t\") });\n assert(!claimed.some((row) => row.id === target), \"claimed an exhausted delivery\");\n },\n },\n\n {\n name: \"claims are ordered by not_before ascending\",\n run: async (ctx) => {\n const userId = uid(\"user\");\n const older = uid(\"delivery\");\n const newer = uid(\"delivery\");\n\n await ctx.adapter.createNotifications([\n {\n id: uid(\"notif\"),\n userId,\n type: \"t\",\n payload: {},\n deliveries: [\n {\n id: newer,\n channel: \"email\",\n maxAttempts: 5,\n notBefore: new Date(Date.now() - 1_000),\n },\n ],\n },\n {\n id: uid(\"notif\"),\n userId,\n type: \"t\",\n payload: {},\n deliveries: [\n {\n id: older,\n channel: \"email\",\n maxAttempts: 5,\n notBefore: new Date(Date.now() - 600_000),\n },\n ],\n },\n ]);\n\n const claimed = await ctx.adapter.claimPendingDeliveries({\n limit: 1,\n leaseMs: 60_000,\n claimToken: uid(\"t\"),\n });\n\n equal(claimed[0]?.id, older, \"oldest eligible delivery was not claimed first\");\n },\n },\n\n {\n name: \"release with a stale token still writes the terminal state\",\n run: async (ctx) => {\n const { rows } = await seed(ctx);\n const target = rows[0]?.deliveries[0]?.id;\n assert(target, \"seed produced no delivery\");\n\n await ctx.adapter.claimPendingDeliveries({ ...CLAIM, claimToken: uid(\"real\") });\n\n await ctx.adapter.releaseDeliveries([\n {\n id: target,\n claimToken: \"a-token-that-never-held-this-lease\",\n outcome: { result: \"sent\" },\n },\n ]);\n\n const stillClaimable = await ctx.adapter.claimPendingDeliveries({\n ...CLAIM,\n claimToken: uid(\"t\"),\n now: new Date(Date.now() + CLAIM.leaseMs + 5_000),\n });\n\n assert(\n !stillClaimable.some((row) => row.id === target),\n \"row stayed claimable after being marked sent\",\n );\n },\n },\n\n {\n name: \"retryable failure re-arms; non-retryable fails immediately\",\n run: async (ctx) => {\n const retry = await seed(ctx, { maxAttempts: 5 });\n const retryId = retry.rows[0]?.deliveries[0]?.id;\n assert(retryId, \"seed produced no delivery\");\n\n await ctx.adapter.claimPendingDeliveries({ ...CLAIM, claimToken: uid(\"t\") });\n\n const soon = new Date(Date.now() + 30_000);\n await ctx.adapter.releaseDeliveries([\n {\n id: retryId,\n claimToken: \"t\",\n outcome: { result: \"failed\", error: \"smtp timeout\", retryable: true },\n nextAttemptAt: soon,\n },\n ]);\n\n const tooEarly = await ctx.adapter.claimPendingDeliveries({ ...CLAIM, claimToken: uid(\"t\") });\n assert(\n !tooEarly.some((row) => row.id === retryId),\n \"re-armed delivery was claimable before its backoff elapsed\",\n );\n\n const afterBackoff = await ctx.adapter.claimPendingDeliveries({\n ...CLAIM,\n claimToken: uid(\"t\"),\n now: new Date(soon.getTime() + 1_000),\n });\n assert(\n afterBackoff.some((row) => row.id === retryId),\n \"re-armed delivery never became claimable\",\n );\n\n const terminal = await seed(ctx, { maxAttempts: 5 });\n const terminalId = terminal.rows[0]?.deliveries[0]?.id;\n assert(terminalId, \"seed produced no delivery\");\n\n await ctx.adapter.claimPendingDeliveries({ ...CLAIM, claimToken: uid(\"t\") });\n await ctx.adapter.releaseDeliveries([\n {\n id: terminalId,\n claimToken: \"t\",\n outcome: { result: \"failed\", error: \"invalid api key\", retryable: false },\n },\n ]);\n\n const failed = await ctx.adapter.getFailedDeliveries({\n since: new Date(Date.now() - 60_000),\n limit: 50,\n });\n assert(\n failed.some((row) => row.id === terminalId),\n \"non-retryable failure did not land in failed\",\n );\n },\n },\n\n {\n name: \"duplicate dedupeKey is skipped, not thrown\",\n run: async (ctx) => {\n const userId = uid(\"user\");\n const key = uid(\"dedupe\");\n\n const first = await seed(ctx, { userId, dedupeKey: key });\n equal(first.result.created.length, 1, \"first insert was not created\");\n\n const second = await seed(ctx, { userId, dedupeKey: key });\n equal(second.result.created.length, 0, \"duplicate was created\");\n equal(second.result.deduped.length, 1, \"duplicate was not reported as deduped\");\n },\n },\n\n {\n name: \"notifications with no dedupeKey never collide\",\n run: async (ctx) => {\n const userId = uid(\"user\");\n const result = await seed(ctx, { userId, count: 3 });\n equal(result.result.created.length, 3, \"undeduped notifications collided\");\n },\n },\n\n {\n name: \"markRead is scoped to the owning user\",\n run: async (ctx) => {\n const owner = uid(\"user\");\n const { rows } = await seed(ctx, { userId: owner });\n const notificationId = rows[0]?.id;\n assert(notificationId, \"seed produced no notification\");\n\n const stolen = await ctx.adapter.markRead(uid(\"attacker\"), [notificationId]);\n equal(stolen, 0, \"another user marked a notification read\");\n\n const own = await ctx.adapter.markRead(owner, [notificationId]);\n equal(own, 1, \"owner could not mark their own notification read\");\n },\n },\n\n {\n name: \"unseen count and markSeen agree\",\n run: async (ctx) => {\n const userId = uid(\"user\");\n await seed(ctx, { userId, count: 3 });\n\n equal(await ctx.adapter.countUnseen(userId), 3, \"unseen count wrong before markSeen\");\n\n await ctx.adapter.markSeen(userId, new Date());\n equal(await ctx.adapter.countUnseen(userId), 0, \"unseen count wrong after markSeen\");\n },\n },\n\n {\n /**\n * createdAt must come from the same clock as the cutoff it is compared to.\n *\n * An adapter that leaves createdAt to a database DEFAULT now() is comparing\n * the database clock against the application clock. On one machine they\n * agree and this never fails; in production they are different hosts, and a\n * database a second ahead means markSeen(now) silently misses the newest\n * notifications and the unseen badge never clears. Found when a laptop\n * resumed from sleep with its Docker VM 6s ahead.\n */\n name: \"createdAt comes from the caller clock, not the database clock\",\n run: async (ctx) => {\n const userId = uid(\"user\");\n const before = new Date();\n await seed(ctx, { userId, count: 1 });\n const after = new Date();\n\n const { notifications } = await ctx.adapter.listNotifications({ userId, limit: 10 });\n const createdAt = notifications[0]?.createdAt;\n assert(createdAt, \"seed produced no notification\");\n\n // A second of slack for a slow round trip, far under the skew that breaks\n // markSeen but tight enough to catch a foreign clock.\n const skewMs = Math.max(\n before.getTime() - createdAt.getTime(),\n createdAt.getTime() - after.getTime(),\n );\n assert(\n skewMs <= 1_000,\n `createdAt is ${skewMs}ms outside the window the caller observed — it is coming from another clock`,\n );\n },\n },\n];\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/testing/conformance.ts"],"names":[],"mappings":"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","file":"testing.js","sourcesContent":["import type { DatabaseAdapter, InsertNotification } from \"../core/adapter\";\nimport type { Channel } from \"../core/types\";\n\nexport type ConformanceContext = {\n adapter: DatabaseAdapter;\n /**\n * Forces a delivery's attempt count. Not reachable through the public API —\n * the adapter would have marked the row failed on the way there — so each\n * backend writes it directly.\n */\n setAttempts: (deliveryId: string, attempts: number) => Promise<void>;\n /**\n * Empties the tables. Cases must run against a clean table: claims are\n * ordered by not_before ASC and bounded by limit, so leftover rows from an\n * earlier case can push the row under test out of the claim window.\n */\n reset: () => Promise<void>;\n /**\n * Holds a row lock on a separate connection for the duration of `fn`.\n *\n * Required to test SKIP LOCKED deterministically. Two Promise.all'd claims\n * do not reliably overlap — they complete in single-digit milliseconds and\n * simply queue — so a race-based test passes even with no locking at all.\n *\n * Only meaningful where a claim can block on someone else's lock. A document\n * store whose claim is a single atomic update has nothing to skip, so cases\n * tagged `rowLock` do not apply to it.\n */\n lockRow?: (deliveryId: string, fn: () => Promise<void>) => Promise<void>;\n};\n\nexport type ConformanceCase = {\n name: string;\n /** Capability the case needs; a backend without it must filter the case out. */\n requires?: \"rowLock\";\n run: (ctx: ConformanceContext) => Promise<void>;\n};\n\nclass ConformanceError extends Error {}\n\nfunction assert(condition: unknown, message: string): asserts condition {\n if (!condition) throw new ConformanceError(message);\n}\n\nconst equal = (actual: unknown, expected: unknown, message: string) =>\n assert(actual === expected, `${message} (got ${String(actual)}, expected ${String(expected)})`);\n\nlet counter = 0;\nfunction uid(label: string): string {\n counter += 1;\n return `${label}_${Date.now().toString(36)}_${counter}`;\n}\n\ntype SeedOptions = {\n count?: number;\n notBefore?: Date;\n maxAttempts?: number;\n channel?: Channel;\n dedupeKey?: string;\n userId?: string;\n};\n\nasync function seed(ctx: ConformanceContext, options: SeedOptions = {}) {\n const count = options.count ?? 1;\n const userId = options.userId ?? uid(\"user\");\n\n const rows: InsertNotification[] = Array.from({ length: count }, () => {\n const id = uid(\"notif\");\n return {\n id,\n userId,\n type: \"commentReply\",\n payload: { authorName: \"Dana\" },\n ...(options.dedupeKey ? { dedupeKey: options.dedupeKey } : {}),\n deliveries: [\n {\n id: uid(\"delivery\"),\n channel: options.channel ?? \"email\",\n maxAttempts: options.maxAttempts ?? 5,\n notBefore: options.notBefore ?? new Date(Date.now() - 1000),\n },\n ],\n };\n });\n\n const result = await ctx.adapter.createNotifications(rows);\n return { userId, rows, result };\n}\n\nconst CLAIM = { limit: 10, leaseMs: 60_000 };\n\nexport const adapterConformanceCases: readonly ConformanceCase[] = [\n {\n /**\n * The case that actually distinguishes a correct adapter.\n *\n * A row locked by another transaction must be *skipped*, not waited on.\n * Without SKIP LOCKED the claim blocks until the holder commits, which on\n * a busy table serialises every worker behind the slowest one.\n */\n name: \"a row locked by another transaction is skipped, not waited on\",\n requires: \"rowLock\",\n run: async (ctx) => {\n const { lockRow } = ctx;\n assert(lockRow, \"case requires lockRow\");\n\n const { rows } = await seed(ctx, { count: 2 });\n const locked = rows[0]?.deliveries[0]?.id;\n const free = rows[1]?.deliveries[0]?.id;\n assert(locked && free, \"seed produced too few deliveries\");\n\n await lockRow(locked, async () => {\n const claimed = await Promise.race([\n ctx.adapter.claimPendingDeliveries({ ...CLAIM, claimToken: uid(\"token\") }),\n new Promise<never>((_, reject) =>\n setTimeout(\n () =>\n reject(\n new ConformanceError(\n \"claim blocked on a row locked by another transaction — SKIP LOCKED is missing\",\n ),\n ),\n 2_000,\n ),\n ),\n ]);\n\n const ids = new Set(claimed.map((row) => row.id));\n assert(!ids.has(locked), \"claimed a row locked by another transaction\");\n assert(ids.has(free), \"skipped an unlocked row alongside the locked one\");\n });\n },\n },\n\n {\n name: \"concurrent claims return disjoint sets\",\n run: async (ctx) => {\n await seed(ctx, { count: 40 });\n\n // Eight callers over 40 rows, repeated — a single Promise.all pair is\n // too fast to overlap and proves nothing on its own.\n const claimed = await Promise.all(\n Array.from({ length: 8 }, () =>\n ctx.adapter.claimPendingDeliveries({ limit: 5, leaseMs: 60_000, claimToken: uid(\"t\") }),\n ),\n );\n\n const all = claimed.flatMap((batch) => batch.map((row) => row.id));\n equal(new Set(all).size, all.length, \"the same delivery was claimed by two callers\");\n assert(all.length <= 40, \"claimed more rows than exist\");\n },\n },\n\n {\n name: \"a claimed row is invisible before its lease expires\",\n run: async (ctx) => {\n const { rows } = await seed(ctx);\n const target = rows[0]?.deliveries[0]?.id;\n\n const first = await ctx.adapter.claimPendingDeliveries({ ...CLAIM, claimToken: uid(\"t\") });\n assert(\n first.some((row) => row.id === target),\n \"seeded delivery was not claimed\",\n );\n\n const second = await ctx.adapter.claimPendingDeliveries({ ...CLAIM, claimToken: uid(\"t\") });\n assert(!second.some((row) => row.id === target), \"leased row was claimed twice\");\n },\n },\n\n {\n name: \"a claimed row is re-claimable after its lease expires\",\n run: async (ctx) => {\n const { rows } = await seed(ctx);\n const target = rows[0]?.deliveries[0]?.id;\n\n await ctx.adapter.claimPendingDeliveries({ ...CLAIM, claimToken: uid(\"t\") });\n\n const later = new Date(Date.now() + CLAIM.leaseMs + 5_000);\n const reclaimed = await ctx.adapter.claimPendingDeliveries({\n ...CLAIM,\n claimToken: uid(\"t\"),\n now: later,\n });\n\n assert(\n reclaimed.some((row) => row.id === target),\n \"expired lease was not reclaimed\",\n );\n },\n },\n\n {\n name: \"not_before in the future is never claimed\",\n run: async (ctx) => {\n const { rows } = await seed(ctx, { notBefore: new Date(Date.now() + 3_600_000) });\n const target = rows[0]?.deliveries[0]?.id;\n\n const claimed = await ctx.adapter.claimPendingDeliveries({ ...CLAIM, claimToken: uid(\"t\") });\n assert(!claimed.some((row) => row.id === target), \"claimed a delivery before its not_before\");\n },\n },\n\n {\n name: \"attempts at or beyond max_attempts is never claimed\",\n run: async (ctx) => {\n const { rows } = await seed(ctx, { maxAttempts: 3 });\n const target = rows[0]?.deliveries[0]?.id;\n assert(target, \"seed produced no delivery\");\n\n await ctx.setAttempts(target, 3);\n\n const claimed = await ctx.adapter.claimPendingDeliveries({ ...CLAIM, claimToken: uid(\"t\") });\n assert(!claimed.some((row) => row.id === target), \"claimed an exhausted delivery\");\n },\n },\n\n {\n name: \"claims are ordered by not_before ascending\",\n run: async (ctx) => {\n const userId = uid(\"user\");\n const older = uid(\"delivery\");\n const newer = uid(\"delivery\");\n\n await ctx.adapter.createNotifications([\n {\n id: uid(\"notif\"),\n userId,\n type: \"t\",\n payload: {},\n deliveries: [\n {\n id: newer,\n channel: \"email\",\n maxAttempts: 5,\n notBefore: new Date(Date.now() - 1_000),\n },\n ],\n },\n {\n id: uid(\"notif\"),\n userId,\n type: \"t\",\n payload: {},\n deliveries: [\n {\n id: older,\n channel: \"email\",\n maxAttempts: 5,\n notBefore: new Date(Date.now() - 600_000),\n },\n ],\n },\n ]);\n\n const claimed = await ctx.adapter.claimPendingDeliveries({\n limit: 1,\n leaseMs: 60_000,\n claimToken: uid(\"t\"),\n });\n\n equal(claimed[0]?.id, older, \"oldest eligible delivery was not claimed first\");\n },\n },\n\n {\n name: \"release with a stale token still writes the terminal state\",\n run: async (ctx) => {\n const { rows } = await seed(ctx);\n const target = rows[0]?.deliveries[0]?.id;\n assert(target, \"seed produced no delivery\");\n\n await ctx.adapter.claimPendingDeliveries({ ...CLAIM, claimToken: uid(\"real\") });\n\n await ctx.adapter.releaseDeliveries([\n {\n id: target,\n claimToken: \"a-token-that-never-held-this-lease\",\n outcome: { result: \"sent\" },\n },\n ]);\n\n const stillClaimable = await ctx.adapter.claimPendingDeliveries({\n ...CLAIM,\n claimToken: uid(\"t\"),\n now: new Date(Date.now() + CLAIM.leaseMs + 5_000),\n });\n\n assert(\n !stillClaimable.some((row) => row.id === target),\n \"row stayed claimable after being marked sent\",\n );\n },\n },\n\n {\n name: \"retryable failure re-arms; non-retryable fails immediately\",\n run: async (ctx) => {\n const retry = await seed(ctx, { maxAttempts: 5 });\n const retryId = retry.rows[0]?.deliveries[0]?.id;\n assert(retryId, \"seed produced no delivery\");\n\n await ctx.adapter.claimPendingDeliveries({ ...CLAIM, claimToken: uid(\"t\") });\n\n const soon = new Date(Date.now() + 30_000);\n await ctx.adapter.releaseDeliveries([\n {\n id: retryId,\n claimToken: \"t\",\n outcome: { result: \"failed\", error: \"smtp timeout\", retryable: true },\n nextAttemptAt: soon,\n },\n ]);\n\n const tooEarly = await ctx.adapter.claimPendingDeliveries({ ...CLAIM, claimToken: uid(\"t\") });\n assert(\n !tooEarly.some((row) => row.id === retryId),\n \"re-armed delivery was claimable before its backoff elapsed\",\n );\n\n const afterBackoff = await ctx.adapter.claimPendingDeliveries({\n ...CLAIM,\n claimToken: uid(\"t\"),\n now: new Date(soon.getTime() + 1_000),\n });\n assert(\n afterBackoff.some((row) => row.id === retryId),\n \"re-armed delivery never became claimable\",\n );\n\n const terminal = await seed(ctx, { maxAttempts: 5 });\n const terminalId = terminal.rows[0]?.deliveries[0]?.id;\n assert(terminalId, \"seed produced no delivery\");\n\n await ctx.adapter.claimPendingDeliveries({ ...CLAIM, claimToken: uid(\"t\") });\n await ctx.adapter.releaseDeliveries([\n {\n id: terminalId,\n claimToken: \"t\",\n outcome: { result: \"failed\", error: \"invalid api key\", retryable: false },\n },\n ]);\n\n const failed = await ctx.adapter.getFailedDeliveries({\n since: new Date(Date.now() - 60_000),\n limit: 50,\n });\n assert(\n failed.some((row) => row.id === terminalId),\n \"non-retryable failure did not land in failed\",\n );\n },\n },\n\n {\n // The whole point of the status: a delivery with nothing to do must not\n // show up in the view an operator reads to find real breakage.\n name: \"a skipped delivery is terminal and stays out of getFailedDeliveries\",\n run: async (ctx) => {\n const seeded = await seed(ctx, { maxAttempts: 5 });\n const id = seeded.rows[0]?.deliveries[0]?.id;\n assert(id, \"seed produced no delivery\");\n\n await ctx.adapter.claimPendingDeliveries({ ...CLAIM, claimToken: uid(\"t\") });\n await ctx.adapter.releaseDeliveries([\n {\n id,\n claimToken: \"t\",\n outcome: { result: \"skipped\", reason: \"no registered devices\" },\n },\n ]);\n\n const failed = await ctx.adapter.getFailedDeliveries({\n since: new Date(Date.now() - 60_000),\n limit: 50,\n });\n assert(!failed.some((row) => row.id === id), \"a skipped delivery was reported as failed\");\n\n // Terminal: it must never be handed out again, or a user with no devices\n // is retried on every sweep forever.\n const reclaimed = await ctx.adapter.claimPendingDeliveries({\n ...CLAIM,\n claimToken: uid(\"t\"),\n now: new Date(Date.now() + 3_600_000),\n });\n assert(!reclaimed.some((row) => row.id === id), \"a skipped delivery was claimed again\");\n },\n },\n\n {\n name: \"duplicate dedupeKey is skipped, not thrown\",\n run: async (ctx) => {\n const userId = uid(\"user\");\n const key = uid(\"dedupe\");\n\n const first = await seed(ctx, { userId, dedupeKey: key });\n equal(first.result.created.length, 1, \"first insert was not created\");\n\n const second = await seed(ctx, { userId, dedupeKey: key });\n equal(second.result.created.length, 0, \"duplicate was created\");\n equal(second.result.deduped.length, 1, \"duplicate was not reported as deduped\");\n },\n },\n\n {\n name: \"notifications with no dedupeKey never collide\",\n run: async (ctx) => {\n const userId = uid(\"user\");\n const result = await seed(ctx, { userId, count: 3 });\n equal(result.result.created.length, 3, \"undeduped notifications collided\");\n },\n },\n\n {\n name: \"markRead is scoped to the owning user\",\n run: async (ctx) => {\n const owner = uid(\"user\");\n const { rows } = await seed(ctx, { userId: owner });\n const notificationId = rows[0]?.id;\n assert(notificationId, \"seed produced no notification\");\n\n const stolen = await ctx.adapter.markRead(uid(\"attacker\"), [notificationId]);\n equal(stolen, 0, \"another user marked a notification read\");\n\n const own = await ctx.adapter.markRead(owner, [notificationId]);\n equal(own, 1, \"owner could not mark their own notification read\");\n },\n },\n\n {\n name: \"unseen count and markSeen agree\",\n run: async (ctx) => {\n const userId = uid(\"user\");\n await seed(ctx, { userId, count: 3 });\n\n equal(await ctx.adapter.countUnseen(userId), 3, \"unseen count wrong before markSeen\");\n\n await ctx.adapter.markSeen(userId, new Date());\n equal(await ctx.adapter.countUnseen(userId), 0, \"unseen count wrong after markSeen\");\n },\n },\n\n {\n /**\n * createdAt must come from the same clock as the cutoff it is compared to.\n *\n * An adapter that leaves createdAt to a database DEFAULT now() is comparing\n * the database clock against the application clock. On one machine they\n * agree and this never fails; in production they are different hosts, and a\n * database a second ahead means markSeen(now) silently misses the newest\n * notifications and the unseen badge never clears. Found when a laptop\n * resumed from sleep with its Docker VM 6s ahead.\n */\n name: \"createdAt comes from the caller clock, not the database clock\",\n run: async (ctx) => {\n const userId = uid(\"user\");\n const before = new Date();\n await seed(ctx, { userId, count: 1 });\n const after = new Date();\n\n const { notifications } = await ctx.adapter.listNotifications({ userId, limit: 10 });\n const createdAt = notifications[0]?.createdAt;\n assert(createdAt, \"seed produced no notification\");\n\n // A second of slack for a slow round trip, far under the skew that breaks\n // markSeen but tight enough to catch a foreign clock.\n const skewMs = Math.max(\n before.getTime() - createdAt.getTime(),\n createdAt.getTime() - after.getTime(),\n );\n assert(\n skewMs <= 1_000,\n `createdAt is ${skewMs}ms outside the window the caller observed — it is coming from another clock`,\n );\n },\n },\n];\n"]}
|
package/package.json
CHANGED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/core/plugin.ts"],"names":[],"mappings":";AAgLO,SAAS,aACd,MAAA,EAC8B;AAC9B,EAAA,OAAO,MAAA;AACT;AAgBO,SAAS,mBACd,MAAA,EAC6B;AAC7B,EAAA,OAAO,MAAA;AACT","file":"chunk-BV3U542P.js","sourcesContent":["import type { DeliveryOutcome } from \"./adapter\";\nimport type { Logger } from \"./errors\";\nimport type { PluginStore } from \"./store\";\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 /**\n * Once per send, after the rows are committed. The only hook that sees\n * notification ids, which digests needs to bucket an entry against one.\n * Fails open: the notification is already written.\n */\n afterSend?: (ctx: AfterSendContext) => Promisable<void>;\n /**\n * Delivers one of the plugin's declared `channels`. Core carries inApp and\n * email; everything else arrives here, which is how push, sms and slack are\n * added without core learning about them.\n */\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: { uid: string; purpose: string; exp: number; data?: Record<string, string> } | 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 /** The top-level signing secret, for plugins issuing signed links. */\n secret: string;\n logger: Logger;\n /** The app's resolver, for plugins that need timezones or addresses. */\n getRecipients: (userIds: readonly string[]) => Promise<readonly Recipient[]>;\n /**\n * The instance's own send(). A plugin composing a message (a digest, say)\n * routes it back through the pipeline rather than reaching for a provider,\n * so it inherits retry, backoff and idempotency for free.\n */\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"]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/schema/declaration.ts"],"names":[],"mappings":"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","file":"chunk-NYDVNMKG.js","sourcesContent":["import type { SchemaDeclaration } from \"../core/plugin\";\n\nexport const toSnakeCase = (value: string) => value.replace(/[A-Z]/g, (c) => `_${c.toLowerCase()}`);\n\n/**\n * Core tables in the format plugins use. Less expressive than the hand-written\n * Drizzle schema (no partial indexes, ordering or cascade) — a drift test\n * asserts the two agree on the fields they share.\n */\nexport const coreSchema = {\n notification: {\n tableName: \"notification\",\n fields: {\n id: { type: \"string\", required: true },\n userId: { type: \"string\", required: true },\n type: { type: \"string\", required: true },\n payload: { type: \"json\", required: true },\n actorId: { type: \"string\" },\n groupKey: { type: \"string\" },\n dedupeKey: { type: \"string\" },\n seenAt: { type: \"date\" },\n readAt: { type: \"date\" },\n archivedAt: { type: \"date\" },\n createdAt: { type: \"date\", required: true, defaultNow: true },\n },\n primaryKey: [\"id\"],\n indexes: [\n { on: [\"userId\", \"createdAt\"], name: \"notification_feed_idx\" },\n { on: [\"userId\", \"groupKey\"], name: \"notification_group_idx\" },\n { on: [\"userId\", \"dedupeKey\"], unique: true, name: \"notification_dedupe_idx\" },\n ],\n },\n\n notificationDelivery: {\n tableName: \"notification_delivery\",\n fields: {\n id: { type: \"string\", required: true },\n notificationId: { type: \"string\", required: true },\n channel: { type: \"string\", required: true },\n status: { type: \"string\", required: true, default: \"pending\" },\n attempts: { type: \"number\", required: true, default: 0 },\n maxAttempts: { type: \"number\", required: true, default: 5 },\n notBefore: { type: \"date\", required: true, defaultNow: true },\n claimedAt: { type: \"date\" },\n claimedBy: { type: \"string\" },\n lastError: { type: \"string\" },\n updatedAt: { type: \"date\", required: true, defaultNow: true },\n },\n primaryKey: [\"id\"],\n indexes: [\n { on: [\"status\", \"notBefore\"], name: \"delivery_claim_idx\" },\n { on: [\"notificationId\"], name: \"delivery_notification_idx\" },\n ],\n },\n\n notificationPreference: {\n tableName: \"notification_preference\",\n fields: {\n userId: { type: \"string\", required: true },\n type: { type: \"string\", required: true },\n channel: { type: \"string\", required: true },\n enabled: { type: \"boolean\", required: true, default: true },\n frequency: { type: \"string\", required: true, default: \"instant\" },\n },\n primaryKey: [\"userId\", \"type\", \"channel\"],\n },\n} satisfies SchemaDeclaration;\n"]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/core/errors.ts","../src/core/store.ts"],"names":[],"mappings":"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","file":"chunk-OZSOIX23.js","sourcesContent":["export class EasyPingError extends Error {\n constructor(message: string) {\n super(message);\n this.name = \"EasyPingError\";\n }\n}\n\n/** Thrown when a payload fails its Standard Schema. Developer error. */\nexport class ValidationError extends EasyPingError {\n readonly issues: readonly { message: string; path?: string }[];\n\n constructor(type: string, issues: readonly { message: string; path?: string }[]) {\n super(`Payload for \"${type}\" failed validation: ${issues.map((i) => i.message).join(\"; \")}`);\n this.name = \"ValidationError\";\n this.issues = issues;\n }\n}\n\n/** Thrown at startup for a malformed config. Never at send time. */\nexport class ConfigError extends EasyPingError {\n constructor(message: string) {\n super(message);\n this.name = \"ConfigError\";\n }\n}\n\nexport type Logger = {\n warn: (message: string, meta?: Record<string, unknown>) => void;\n error: (message: string, meta?: Record<string, unknown>) => void;\n};\n\nexport const consoleLogger: Logger = {\n warn: (message, meta) => console.warn(`[easy-ping] ${message}`, meta ?? \"\"),\n error: (message, meta) => console.error(`[easy-ping] ${message}`, meta ?? \"\"),\n};\n","import { toSnakeCase } from \"../schema/declaration\";\nimport { ConfigError } from \"./errors\";\nimport type { SchemaDeclaration, TableDeclaration } from \"./plugin\";\n\nexport type Scalar = string | number | boolean | Date | null;\n\n/**\n * One operator per condition. Operators are tagged objects so a bare value is\n * always an equality test; json columns therefore cannot be filtered, since an\n * object value would be ambiguous.\n */\nexport type WhereCondition =\n | Scalar\n | { in: readonly (string | number)[] }\n | { lt: Date | number }\n | { lte: Date | number }\n | { gt: Date | number }\n | { gte: Date | number }\n | { not: Scalar };\n\nexport type WhereClause = Record<string, WhereCondition>;\n\nexport type QueryOptions = {\n limit?: number;\n orderBy?: { field: string; direction: \"asc\" | \"desc\" };\n};\n\nexport type UpsertOptions = { onConflict: readonly string[] };\n\n/**\n * Table access for plugins, scoped to the tables that plugin declared.\n *\n * Plugins could always declare tables via schema() but had no way to read\n * them, so preferences had to bolt its queries onto the core adapter. That\n * does not generalise to digests or push.\n */\nexport type PluginStore = {\n find<T = Record<string, unknown>>(\n table: string,\n where?: WhereClause,\n options?: QueryOptions,\n ): Promise<T[]>;\n insert(table: string, rows: readonly Record<string, unknown>[]): Promise<number>;\n upsert(\n table: string,\n rows: readonly Record<string, unknown>[],\n options: UpsertOptions,\n ): Promise<number>;\n update(table: string, where: WhereClause, set: Record<string, unknown>): Promise<number>;\n remove(table: string, where: WhereClause): Promise<number>;\n};\n\nexport type TableStorage = {\n readonly naming?: \"snake_case\" | \"preserve\";\n readonly serializesJson?: boolean;\n\n queryTable(\n table: string,\n where: WhereClause,\n options: QueryOptions,\n ): Promise<Record<string, unknown>[]>;\n insertRows(\n table: string,\n rows: readonly Record<string, unknown>[],\n onConflict?: readonly string[],\n ): Promise<number>;\n updateRows(table: string, where: WhereClause, set: Record<string, unknown>): Promise<number>;\n deleteRows(table: string, where: WhereClause): Promise<number>;\n};\n\nconst OPERATORS = [\"in\", \"lt\", \"lte\", \"gt\", \"gte\", \"not\"] as const;\n\nexport const isOperator = (value: unknown): boolean =>\n typeof value === \"object\" &&\n value !== null &&\n !(value instanceof Date) &&\n OPERATORS.some((op) => op in value);\n\n/**\n * Every table and column a plugin touches is checked against its own\n * declaration. Without this a plugin could read the notification table, and\n * unvalidated identifiers would reach the SQL builder.\n */\nexport function createPluginStore(\n pluginId: string,\n schema: SchemaDeclaration | undefined,\n storage: TableStorage,\n prefix: string,\n): PluginStore {\n const byTableName = new Map<string, TableDeclaration>();\n for (const declaration of Object.values(schema ?? {})) {\n byTableName.set(declaration.tableName, declaration);\n }\n\n function resolve(table: string): TableDeclaration {\n const declaration = byTableName.get(table);\n if (!declaration) {\n throw new ConfigError(\n `plugin \"${pluginId}\" accessed table \"${table}\", which it does not declare in schema().`,\n );\n }\n return declaration;\n }\n\n function checkFields(table: TableDeclaration, fields: Iterable<string>, context: string) {\n for (const field of fields) {\n const spec = table.fields[field];\n if (!spec) {\n throw new ConfigError(\n `plugin \"${pluginId}\" used unknown field \"${field}\" on \"${table.tableName}\" (${context}).`,\n );\n }\n if (context === \"where\" && spec.type === \"json\") {\n throw new ConfigError(\n `plugin \"${pluginId}\" cannot filter on json field \"${field}\"; ` +\n \"a bare object value is indistinguishable from an operator.\",\n );\n }\n }\n }\n\n const qualified = (table: TableDeclaration) => prefix + table.tableName;\n\n // A document store keeps the declared names; a SQL adapter wants columns.\n const column = (field: string) => (storage.naming === \"preserve\" ? field : toSnakeCase(field));\n\n /**\n * json columns must be handed to the driver as text; postgres-js cannot bind\n * a plain object and the insert fails outright.\n */\n const serialize = (table: TableDeclaration, row: Record<string, unknown>) => {\n if (storage.serializesJson === false) return row;\n\n const out: Record<string, unknown> = {};\n for (const [field, value] of Object.entries(row)) {\n out[field] =\n table.fields[field]?.type === \"json\" && value !== null && value !== undefined\n ? JSON.stringify(value)\n : value;\n }\n return out;\n };\n\n return {\n async find(table, where = {}, options = {}) {\n const declaration = resolve(table);\n checkFields(declaration, Object.keys(where), \"where\");\n if (options.orderBy) checkFields(declaration, [options.orderBy.field], \"orderBy\");\n\n const rows = await storage.queryTable(qualified(declaration), where, options);\n\n // SELECT * returns snake_case columns; plugins declare camelCase fields\n // and type their reads that way. Without this every property is\n // undefined at runtime while typechecking perfectly.\n return rows.map((row) => {\n const mapped: Record<string, unknown> = {};\n for (const field of Object.keys(declaration.fields)) {\n const value = row[column(field)];\n // Drivers differ: some hand back parsed jsonb, some raw text.\n mapped[field] =\n declaration.fields[field]?.type === \"json\" && typeof value === \"string\"\n ? JSON.parse(value)\n : value;\n }\n return mapped;\n }) as never;\n },\n\n async insert(table, rows) {\n const declaration = resolve(table);\n for (const row of rows) checkFields(declaration, Object.keys(row), \"insert\");\n return storage.insertRows(\n qualified(declaration),\n rows.map((row) => serialize(declaration, row)),\n );\n },\n\n async upsert(table, rows, options) {\n const declaration = resolve(table);\n for (const row of rows) checkFields(declaration, Object.keys(row), \"upsert\");\n checkFields(declaration, options.onConflict, \"onConflict\");\n return storage.insertRows(\n qualified(declaration),\n rows.map((row) => serialize(declaration, row)),\n options.onConflict,\n );\n },\n\n async update(table, where, set) {\n const declaration = resolve(table);\n checkFields(declaration, Object.keys(where), \"where\");\n checkFields(declaration, Object.keys(set), \"update\");\n return storage.updateRows(qualified(declaration), where, serialize(declaration, set));\n },\n\n async remove(table, where) {\n const declaration = resolve(table);\n checkFields(declaration, Object.keys(where), \"where\");\n return storage.deleteRows(qualified(declaration), where);\n },\n };\n}\n"]}
|
package/dist/chunk-W3BMEPPQ.js
DELETED
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
import { encodeBase64Url, decodeBase64UrlBytes, decodeBase64Url } from './chunk-PTGHMCCG.js';
|
|
2
|
-
|
|
3
|
-
// src/core/tokens.ts
|
|
4
|
-
var encoder = new TextEncoder();
|
|
5
|
-
async function hmacKey(secret) {
|
|
6
|
-
return crypto.subtle.importKey(
|
|
7
|
-
"raw",
|
|
8
|
-
encoder.encode(secret),
|
|
9
|
-
{ name: "HMAC", hash: "SHA-256" },
|
|
10
|
-
false,
|
|
11
|
-
["sign", "verify"]
|
|
12
|
-
);
|
|
13
|
-
}
|
|
14
|
-
async function signToken(secret, claims) {
|
|
15
|
-
const body = encodeBase64Url(JSON.stringify(claims));
|
|
16
|
-
const signature = await crypto.subtle.sign("HMAC", await hmacKey(secret), encoder.encode(body));
|
|
17
|
-
return `${body}.${encodeBase64Url(new Uint8Array(signature))}`;
|
|
18
|
-
}
|
|
19
|
-
async function verifyToken(secret, token, purpose, now = /* @__PURE__ */ new Date()) {
|
|
20
|
-
const separator = token.lastIndexOf(".");
|
|
21
|
-
if (separator <= 0) return null;
|
|
22
|
-
const body = token.slice(0, separator);
|
|
23
|
-
const signature = decodeBase64UrlBytes(token.slice(separator + 1));
|
|
24
|
-
if (!signature) return null;
|
|
25
|
-
const valid = await crypto.subtle.verify(
|
|
26
|
-
"HMAC",
|
|
27
|
-
await hmacKey(secret),
|
|
28
|
-
signature,
|
|
29
|
-
encoder.encode(body)
|
|
30
|
-
);
|
|
31
|
-
if (!valid) return null;
|
|
32
|
-
const decoded = decodeBase64Url(body);
|
|
33
|
-
if (!decoded) return null;
|
|
34
|
-
let claims;
|
|
35
|
-
try {
|
|
36
|
-
claims = JSON.parse(decoded);
|
|
37
|
-
} catch {
|
|
38
|
-
return null;
|
|
39
|
-
}
|
|
40
|
-
if (typeof claims?.uid !== "string" || typeof claims.exp !== "number") return null;
|
|
41
|
-
if (claims.purpose !== purpose) return null;
|
|
42
|
-
if (claims.exp * 1e3 <= now.getTime()) return null;
|
|
43
|
-
return claims;
|
|
44
|
-
}
|
|
45
|
-
var DEFAULT_TOKEN_TTL_SECONDS = 30 * 24 * 60 * 60;
|
|
46
|
-
var expiresIn = (seconds, now = /* @__PURE__ */ new Date()) => Math.floor(now.getTime() / 1e3) + seconds;
|
|
47
|
-
|
|
48
|
-
export { DEFAULT_TOKEN_TTL_SECONDS, expiresIn, signToken, verifyToken };
|
|
49
|
-
//# sourceMappingURL=chunk-W3BMEPPQ.js.map
|
|
50
|
-
//# sourceMappingURL=chunk-W3BMEPPQ.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/core/tokens.ts"],"names":[],"mappings":";;;AAaA,IAAM,OAAA,GAAU,IAAI,WAAA,EAAY;AAEhC,eAAe,QAAQ,MAAA,EAAoC;AACzD,EAAA,OAAO,OAAO,MAAA,CAAO,SAAA;AAAA,IACnB,KAAA;AAAA,IACA,OAAA,CAAQ,OAAO,MAAM,CAAA;AAAA,IACrB,EAAE,IAAA,EAAM,MAAA,EAAQ,IAAA,EAAM,SAAA,EAAU;AAAA,IAChC,KAAA;AAAA,IACA,CAAC,QAAQ,QAAQ;AAAA,GACnB;AACF;AAEA,eAAsB,SAAA,CAAU,QAAgB,MAAA,EAAsC;AACpF,EAAA,MAAM,IAAA,GAAO,eAAA,CAAgB,IAAA,CAAK,SAAA,CAAU,MAAM,CAAC,CAAA;AACnD,EAAA,MAAM,SAAA,GAAY,MAAM,MAAA,CAAO,MAAA,CAAO,IAAA,CAAK,MAAA,EAAQ,MAAM,OAAA,CAAQ,MAAM,CAAA,EAAG,OAAA,CAAQ,MAAA,CAAO,IAAI,CAAC,CAAA;AAC9F,EAAA,OAAO,CAAA,EAAG,IAAI,CAAA,CAAA,EAAI,eAAA,CAAgB,IAAI,UAAA,CAAW,SAAS,CAAC,CAAC,CAAA,CAAA;AAC9D;AAGA,eAAsB,YACpB,MAAA,EACA,KAAA,EACA,SACA,GAAA,mBAAY,IAAI,MAAK,EACQ;AAC7B,EAAA,MAAM,SAAA,GAAY,KAAA,CAAM,WAAA,CAAY,GAAG,CAAA;AACvC,EAAA,IAAI,SAAA,IAAa,GAAG,OAAO,IAAA;AAE3B,EAAA,MAAM,IAAA,GAAO,KAAA,CAAM,KAAA,CAAM,CAAA,EAAG,SAAS,CAAA;AACrC,EAAA,MAAM,YAAY,oBAAA,CAAqB,KAAA,CAAM,KAAA,CAAM,SAAA,GAAY,CAAC,CAAC,CAAA;AACjE,EAAA,IAAI,CAAC,WAAW,OAAO,IAAA;AAGvB,EAAA,MAAM,KAAA,GAAQ,MAAM,MAAA,CAAO,MAAA,CAAO,MAAA;AAAA,IAChC,MAAA;AAAA,IACA,MAAM,QAAQ,MAAM,CAAA;AAAA,IACpB,SAAA;AAAA,IACA,OAAA,CAAQ,OAAO,IAAI;AAAA,GACrB;AACA,EAAA,IAAI,CAAC,OAAO,OAAO,IAAA;AAEnB,EAAA,MAAM,OAAA,GAAU,gBAAgB,IAAI,CAAA;AACpC,EAAA,IAAI,CAAC,SAAS,OAAO,IAAA;AAErB,EAAA,IAAI,MAAA;AACJ,EAAA,IAAI;AACF,IAAA,MAAA,GAAS,IAAA,CAAK,MAAM,OAAO,CAAA;AAAA,EAC7B,CAAA,CAAA,MAAQ;AACN,IAAA,OAAO,IAAA;AAAA,EACT;AAEA,EAAA,IAAI,OAAO,QAAQ,GAAA,KAAQ,QAAA,IAAY,OAAO,MAAA,CAAO,GAAA,KAAQ,UAAU,OAAO,IAAA;AAE9E,EAAA,IAAI,MAAA,CAAO,OAAA,KAAY,OAAA,EAAS,OAAO,IAAA;AACvC,EAAA,IAAI,OAAO,GAAA,GAAM,GAAA,IAAQ,GAAA,CAAI,OAAA,IAAW,OAAO,IAAA;AAE/C,EAAA,OAAO,MAAA;AACT;AAEO,IAAM,yBAAA,GAA4B,EAAA,GAAK,EAAA,GAAK,EAAA,GAAK;AAEjD,IAAM,SAAA,GAAY,CAAC,OAAA,EAAiB,GAAA,mBAAY,IAAI,IAAA,EAAK,KAC9D,IAAA,CAAK,KAAA,CAAM,GAAA,CAAI,OAAA,EAAQ,GAAI,GAAI,CAAA,GAAI","file":"chunk-W3BMEPPQ.js","sourcesContent":["import { decodeBase64Url, decodeBase64UrlBytes, encodeBase64Url } from \"./base64url\";\n\nexport type TokenClaims = {\n /** Subject — the user the token acts for. */\n uid: string;\n /** What the token is allowed to do. Verification requires an exact match. */\n purpose: string;\n /** Expiry, epoch seconds. */\n exp: number;\n /** Free-form, purpose-specific. */\n data?: Record<string, string>;\n};\n\nconst encoder = new TextEncoder();\n\nasync function hmacKey(secret: string): Promise<CryptoKey> {\n return crypto.subtle.importKey(\n \"raw\",\n encoder.encode(secret),\n { name: \"HMAC\", hash: \"SHA-256\" },\n false,\n [\"sign\", \"verify\"],\n );\n}\n\nexport async function signToken(secret: string, claims: TokenClaims): Promise<string> {\n const body = encodeBase64Url(JSON.stringify(claims));\n const signature = await crypto.subtle.sign(\"HMAC\", await hmacKey(secret), encoder.encode(body));\n return `${body}.${encodeBase64Url(new Uint8Array(signature))}`;\n}\n\n/** null for every failure mode. Distinguishing them tells an attacker which part to fix. */\nexport async function verifyToken(\n secret: string,\n token: string,\n purpose: string,\n now: Date = new Date(),\n): Promise<TokenClaims | null> {\n const separator = token.lastIndexOf(\".\");\n if (separator <= 0) return null;\n\n const body = token.slice(0, separator);\n const signature = decodeBase64UrlBytes(token.slice(separator + 1));\n if (!signature) return null;\n\n // crypto.subtle.verify is constant-time.\n const valid = await crypto.subtle.verify(\n \"HMAC\",\n await hmacKey(secret),\n signature,\n encoder.encode(body),\n );\n if (!valid) return null;\n\n const decoded = decodeBase64Url(body);\n if (!decoded) return null;\n\n let claims: TokenClaims;\n try {\n claims = JSON.parse(decoded) as TokenClaims;\n } catch {\n return null;\n }\n\n if (typeof claims?.uid !== \"string\" || typeof claims.exp !== \"number\") return null;\n // Scoped so an unsubscribe token can never be replayed against another route.\n if (claims.purpose !== purpose) return null;\n if (claims.exp * 1000 <= now.getTime()) return null;\n\n return claims;\n}\n\nexport const DEFAULT_TOKEN_TTL_SECONDS = 30 * 24 * 60 * 60;\n\nexport const expiresIn = (seconds: number, now: Date = new Date()): number =>\n Math.floor(now.getTime() / 1000) + seconds;\n"]}
|