@absolutejs/sync-pack-digest 0.1.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 +138 -0
- package/dist/index.d.ts +135 -0
- package/dist/index.js +189 -0
- package/dist/index.js.map +10 -0
- package/package.json +51 -0
package/README.md
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# @absolutejs/sync-pack-digest
|
|
2
|
+
|
|
3
|
+
Scheduled per-actor digest emails as a sync pack for
|
|
4
|
+
[`@absolutejs/sync`](https://github.com/absolutejs/sync). Cron-fires a
|
|
5
|
+
schedule that iterates the host's actor list, asks the host to build a
|
|
6
|
+
digest payload, and dispatches it through a host-provided `send` adapter.
|
|
7
|
+
The pack does NOT ship an SMTP client — you bring your own (Resend, SES,
|
|
8
|
+
Postmark, or a CLI for tests).
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
bun add @absolutejs/sync-pack-digest
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Usage
|
|
15
|
+
|
|
16
|
+
```ts
|
|
17
|
+
import { createSyncEngine } from '@absolutejs/sync/engine';
|
|
18
|
+
import { createDigestPack } from '@absolutejs/sync-pack-digest';
|
|
19
|
+
|
|
20
|
+
const engine = createSyncEngine();
|
|
21
|
+
engine.registerPack(
|
|
22
|
+
createDigestPack({
|
|
23
|
+
// Cron pattern for the digest fire. Default '0 8 * * 1' (Mon 8am).
|
|
24
|
+
cron: '0 8 * * 1',
|
|
25
|
+
|
|
26
|
+
// REQUIRED: yield the actor ids to iterate per fire. The host's
|
|
27
|
+
// data layer is the source of truth for "who exists" — the pack
|
|
28
|
+
// just maintains per-actor cursors.
|
|
29
|
+
listActors: () => db.users.allActiveIds(),
|
|
30
|
+
|
|
31
|
+
// REQUIRED: build the payload for one actor. `since` is the actor's
|
|
32
|
+
// last successful send (or null on first-ever). Return null to skip
|
|
33
|
+
// this actor silently (no content this week).
|
|
34
|
+
buildDigest: async (actorId, since) => {
|
|
35
|
+
const items = await db.feed.since(actorId, since ?? new Date(0));
|
|
36
|
+
if (items.length === 0) return null;
|
|
37
|
+
return {
|
|
38
|
+
to: await db.users.email(actorId),
|
|
39
|
+
subject: `Your weekly digest`,
|
|
40
|
+
body: render(items),
|
|
41
|
+
};
|
|
42
|
+
},
|
|
43
|
+
|
|
44
|
+
// REQUIRED: host's transport adapter. Pack catches throws per
|
|
45
|
+
// actor so one bad send doesn't block the rest.
|
|
46
|
+
send: async (msg) => await resend.send(msg),
|
|
47
|
+
|
|
48
|
+
// OPTIONAL: back-pressure. Default 1000.
|
|
49
|
+
maxActorsPerFire: 1000,
|
|
50
|
+
|
|
51
|
+
// OPTIONAL: per-actor cool-down. Default 168 hours = 1 week. Match
|
|
52
|
+
// to your cron — weekly cron + 168h = each actor gets one digest
|
|
53
|
+
// per fire; daily cron + 168h = each actor gets at most one per week.
|
|
54
|
+
minHoursBetweenDigests: 168,
|
|
55
|
+
|
|
56
|
+
// OPTIONAL: surface failures (default logs to console.error).
|
|
57
|
+
onActorFailure: ({ actorId, phase, error }) => {
|
|
58
|
+
logger.warn({ actorId, phase, error }, 'digest failure');
|
|
59
|
+
},
|
|
60
|
+
|
|
61
|
+
// OPTIONAL: outer schedule retry (sync 1.9.0+). Defaults to no
|
|
62
|
+
// retry — per-actor failures are handled internally; this is for
|
|
63
|
+
// transient infra failures (DB, etc) of the whole fire. The next
|
|
64
|
+
// cron fire catches up anyway.
|
|
65
|
+
// retry: { maxAttempts: 3, backoff: exponentialBackoff() },
|
|
66
|
+
})
|
|
67
|
+
);
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## The pack exposes
|
|
71
|
+
|
|
72
|
+
| Surface | Name | What it does |
|
|
73
|
+
| ----------- | ----------------- | -------------------------------------------------------------------------- |
|
|
74
|
+
| Collection | `digest_cursors` | Subscribe (no params) — each actor sees their own `lastSentAt` cursor |
|
|
75
|
+
| Schedule | `digest:fire` | Cron handler. Call `engine.runSchedule('digest:fire')` directly for tests |
|
|
76
|
+
|
|
77
|
+
## Failure semantics
|
|
78
|
+
|
|
79
|
+
Three failure phases, each independent per actor:
|
|
80
|
+
|
|
81
|
+
| Phase | Cause | Effect |
|
|
82
|
+
| ------------- | ------------------------------ | ------------------------------------------------- |
|
|
83
|
+
| `buildDigest` | Host's content builder threw | `onActorFailure` fires; cursor unchanged; skip |
|
|
84
|
+
| `send` | Host's transport adapter threw | `onActorFailure` fires; cursor unchanged; skip |
|
|
85
|
+
| `cursor` | Cursor update failed post-send | `onActorFailure` fires (email already went out) |
|
|
86
|
+
|
|
87
|
+
A failure on actor N doesn't affect actor N+1; the schedule keeps going.
|
|
88
|
+
|
|
89
|
+
The `cursor` phase is the one to watch: the email already went out but the
|
|
90
|
+
cursor didn't update, so the actor may double-receive next fire. The pack
|
|
91
|
+
logs through `onActorFailure` so the operator notices.
|
|
92
|
+
|
|
93
|
+
## Cursor row
|
|
94
|
+
|
|
95
|
+
```ts
|
|
96
|
+
type DigestCursor = {
|
|
97
|
+
id: string; // = actorId
|
|
98
|
+
actorId: string;
|
|
99
|
+
lastSentAt: number; // epoch ms
|
|
100
|
+
lastSubject: string;
|
|
101
|
+
};
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
The collection is scoped per actor by default (each actor sees only their
|
|
105
|
+
own cursor — useful for a "last digest: 5 days ago" UI). Hosts who want
|
|
106
|
+
an admin view can register their own permissions on top — host wins-last.
|
|
107
|
+
|
|
108
|
+
## Storage
|
|
109
|
+
|
|
110
|
+
Default: per-instance in-memory cursor store. For a persistent backend,
|
|
111
|
+
pass a custom `store`:
|
|
112
|
+
|
|
113
|
+
```ts
|
|
114
|
+
import {
|
|
115
|
+
createDigestPack,
|
|
116
|
+
type DigestStore,
|
|
117
|
+
} from '@absolutejs/sync-pack-digest';
|
|
118
|
+
|
|
119
|
+
const store: DigestStore = {
|
|
120
|
+
getById: (id) => /* SELECT * FROM digest_cursors WHERE id = $1 */,
|
|
121
|
+
reader: { all: () => /* SELECT * FROM digest_cursors */ },
|
|
122
|
+
writer: { insert, update, delete },
|
|
123
|
+
};
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
## Multiple instances
|
|
127
|
+
|
|
128
|
+
Pass a `prefix` to coexist with other digest pack instances (e.g. one per
|
|
129
|
+
product surface, with different crons / templates):
|
|
130
|
+
|
|
131
|
+
```ts
|
|
132
|
+
engine.registerPack(createDigestPack({ prefix: 'team_', /* ... */ }));
|
|
133
|
+
engine.registerPack(createDigestPack({ prefix: 'cust_', /* ... */ }));
|
|
134
|
+
|
|
135
|
+
// Tables: team_digest_cursors, cust_digest_cursors
|
|
136
|
+
// Schedules: team_digest:fire, cust_digest:fire
|
|
137
|
+
// Collections: team_digest_cursors, cust_digest_cursors
|
|
138
|
+
```
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@absolutejs/sync-pack-digest` — scheduled per-actor digest pack for
|
|
3
|
+
* `@absolutejs/sync`.
|
|
4
|
+
*
|
|
5
|
+
* Cron-fires a schedule that iterates the host's actor list, asks the host
|
|
6
|
+
* to build a digest payload for each actor since their last cursor, and
|
|
7
|
+
* dispatches it through a host-provided `send` adapter. Owns one table
|
|
8
|
+
* (`digest_cursors`) with one row per actor; nothing else.
|
|
9
|
+
*
|
|
10
|
+
* Per-actor failures are caught and logged so one bad send doesn't block
|
|
11
|
+
* the rest. The whole-handler retry shim from sync 1.9.0 is wired but off
|
|
12
|
+
* by default (digest fires are usually fine to skip on transient infra
|
|
13
|
+
* failures — the next cron fire catches up).
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```ts
|
|
17
|
+
* import { createSyncEngine } from '@absolutejs/sync/engine';
|
|
18
|
+
* import { createDigestPack } from '@absolutejs/sync-pack-digest';
|
|
19
|
+
*
|
|
20
|
+
* const engine = createSyncEngine();
|
|
21
|
+
* engine.registerPack(createDigestPack({
|
|
22
|
+
* cron: '0 8 * * 1', // Mondays 8am
|
|
23
|
+
* listActors: () => db.users.allActiveIds(),
|
|
24
|
+
* buildDigest: async (actorId, since) => {
|
|
25
|
+
* const items = await db.feed.since(actorId, since ?? new Date(0));
|
|
26
|
+
* if (items.length === 0) return null;
|
|
27
|
+
* return {
|
|
28
|
+
* to: await db.users.email(actorId),
|
|
29
|
+
* subject: `Your weekly digest`,
|
|
30
|
+
* body: render(items),
|
|
31
|
+
* };
|
|
32
|
+
* },
|
|
33
|
+
* send: async (msg) => await resend.send(msg),
|
|
34
|
+
* }));
|
|
35
|
+
* ```
|
|
36
|
+
*/
|
|
37
|
+
import { type CollectionContext, type RetryPolicy, type SyncPack, type TableReader, type TableWriter } from '@absolutejs/sync/engine';
|
|
38
|
+
/** One row per actor — created on first successful send, updated after each. */
|
|
39
|
+
export type DigestCursor = {
|
|
40
|
+
/** Row id (= actorId). */
|
|
41
|
+
id: string;
|
|
42
|
+
actorId: string;
|
|
43
|
+
/** Epoch ms of the most recent successful send. */
|
|
44
|
+
lastSentAt: number;
|
|
45
|
+
/** Subject line of the most recent successful send (for observability). */
|
|
46
|
+
lastSubject: string;
|
|
47
|
+
};
|
|
48
|
+
/** The payload the host's `buildDigest` returns. Returning `null` skips. */
|
|
49
|
+
export type DigestPayload = {
|
|
50
|
+
to: string;
|
|
51
|
+
subject: string;
|
|
52
|
+
body: string;
|
|
53
|
+
};
|
|
54
|
+
/** Storage adapter the pack uses for the cursors table. */
|
|
55
|
+
export type DigestStore = {
|
|
56
|
+
reader: TableReader<CollectionContext>;
|
|
57
|
+
writer: TableWriter<DigestCursor, CollectionContext, unknown>;
|
|
58
|
+
getById: (id: string) => DigestCursor | undefined;
|
|
59
|
+
};
|
|
60
|
+
/** Default in-memory store. */
|
|
61
|
+
export declare const createInMemoryDigestStore: () => DigestStore;
|
|
62
|
+
/** Factory config. `send`, `buildDigest`, and `listActors` are required. */
|
|
63
|
+
export type DigestPackConfig<Ctx = CollectionContext> = {
|
|
64
|
+
/**
|
|
65
|
+
* Prefix applied to the owned table name AND every collection/schedule
|
|
66
|
+
* the pack exposes. Default `""`.
|
|
67
|
+
*/
|
|
68
|
+
prefix?: string;
|
|
69
|
+
/**
|
|
70
|
+
* Cron pattern for the digest fire (`@elysiajs/cron` / croner syntax).
|
|
71
|
+
* Default `"0 8 * * 1"` (Mondays 08:00).
|
|
72
|
+
*/
|
|
73
|
+
cron?: string;
|
|
74
|
+
/**
|
|
75
|
+
* Host-supplied email sender. The pack does NOT own transport (no SMTP
|
|
76
|
+
* client, no Resend/SES/Postmark dep). Throws are caught per-actor.
|
|
77
|
+
*/
|
|
78
|
+
send: (msg: DigestPayload) => Promise<void>;
|
|
79
|
+
/**
|
|
80
|
+
* Build the digest payload for one actor. `since` is the actor's last
|
|
81
|
+
* successful send timestamp, or `null` if they've never received one.
|
|
82
|
+
* Return `null` to skip this fire silently (no content for this actor).
|
|
83
|
+
*/
|
|
84
|
+
buildDigest: (actorId: string, since: Date | null) => Promise<DigestPayload | null>;
|
|
85
|
+
/**
|
|
86
|
+
* Yield the actor ids the schedule should iterate per fire. The host's
|
|
87
|
+
* own data layer is the source of truth for "who exists" — the pack
|
|
88
|
+
* just maintains per-actor cursors.
|
|
89
|
+
*/
|
|
90
|
+
listActors: () => Iterable<string> | Promise<Iterable<string>>;
|
|
91
|
+
/**
|
|
92
|
+
* Cap on actors processed per fire (back-pressure). The rest wait for
|
|
93
|
+
* the next cron fire. Default `1000`.
|
|
94
|
+
*/
|
|
95
|
+
maxActorsPerFire?: number;
|
|
96
|
+
/**
|
|
97
|
+
* Minimum hours between digests for a given actor. An actor whose
|
|
98
|
+
* cursor is fresher than this is skipped on the fire. Default `168`
|
|
99
|
+
* (one week — matches the weekly default cron).
|
|
100
|
+
*/
|
|
101
|
+
minHoursBetweenDigests?: number;
|
|
102
|
+
/**
|
|
103
|
+
* Extract the current actor id from ctx — used to scope the cursor
|
|
104
|
+
* collection so each actor sees their own row. Default
|
|
105
|
+
* `(ctx) => ctx.userId`.
|
|
106
|
+
*/
|
|
107
|
+
getActorId?: (ctx: Ctx) => string | undefined;
|
|
108
|
+
/**
|
|
109
|
+
* Outer schedule retry policy. The pack catches per-actor failures
|
|
110
|
+
* already, so this only retries the whole handler on classified
|
|
111
|
+
* transient infrastructure failures (e.g. DB serialization). Default
|
|
112
|
+
* undefined — digest fires miss skip on infra failure and catch up
|
|
113
|
+
* next cron.
|
|
114
|
+
*/
|
|
115
|
+
retry?: RetryPolicy;
|
|
116
|
+
/** Override the storage adapter. Default: per-instance in-memory. */
|
|
117
|
+
store?: DigestStore;
|
|
118
|
+
/** Wall-clock function. Default `Date.now`. */
|
|
119
|
+
now?: () => number;
|
|
120
|
+
/**
|
|
121
|
+
* Hook for surfacing per-actor failures. Defaults to `console.error`.
|
|
122
|
+
* The pack swallows the error after this call so the schedule keeps
|
|
123
|
+
* going for the rest of the actor list.
|
|
124
|
+
*/
|
|
125
|
+
onActorFailure?: (info: {
|
|
126
|
+
actorId: string;
|
|
127
|
+
phase: 'buildDigest' | 'send' | 'cursor';
|
|
128
|
+
error: unknown;
|
|
129
|
+
}) => void;
|
|
130
|
+
};
|
|
131
|
+
/**
|
|
132
|
+
* Build a {@link SyncPack} that exposes scheduled per-actor digests. Each
|
|
133
|
+
* call returns a fresh pack with its own cursor store (unless overridden).
|
|
134
|
+
*/
|
|
135
|
+
export declare const createDigestPack: <Ctx = CollectionContext>(config: DigestPackConfig<Ctx>) => SyncPack;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// src/index.ts
|
|
3
|
+
import {
|
|
4
|
+
defineCollection,
|
|
5
|
+
defineSchedule,
|
|
6
|
+
defineSchema,
|
|
7
|
+
defineSyncPack,
|
|
8
|
+
field
|
|
9
|
+
} from "@absolutejs/sync/engine";
|
|
10
|
+
var createInMemoryDigestStore = () => {
|
|
11
|
+
const rows = new Map;
|
|
12
|
+
return {
|
|
13
|
+
getById: (id) => rows.get(id),
|
|
14
|
+
reader: {
|
|
15
|
+
all: () => [...rows.values()]
|
|
16
|
+
},
|
|
17
|
+
writer: {
|
|
18
|
+
delete: (row) => {
|
|
19
|
+
rows.delete(row.id);
|
|
20
|
+
},
|
|
21
|
+
insert: (data) => {
|
|
22
|
+
rows.set(data.id, data);
|
|
23
|
+
return data;
|
|
24
|
+
},
|
|
25
|
+
update: (data) => {
|
|
26
|
+
const prior = rows.get(data.id);
|
|
27
|
+
const merged = { ...prior ?? {}, ...data };
|
|
28
|
+
rows.set(data.id, merged);
|
|
29
|
+
return merged;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
};
|
|
34
|
+
var DEFAULT_CRON = "0 8 * * 1";
|
|
35
|
+
var DEFAULT_MAX_ACTORS_PER_FIRE = 1000;
|
|
36
|
+
var DEFAULT_MIN_HOURS_BETWEEN_DIGESTS = 168;
|
|
37
|
+
var resolveActor = (getActorId, ctx) => getActorId(ctx);
|
|
38
|
+
var createDigestPack = (config) => {
|
|
39
|
+
const prefix = config.prefix ?? "";
|
|
40
|
+
const table = `${prefix}digest_cursors`;
|
|
41
|
+
const collectionName = table;
|
|
42
|
+
const scheduleName = `${prefix}digest:fire`;
|
|
43
|
+
const cron = config.cron ?? DEFAULT_CRON;
|
|
44
|
+
const maxActorsPerFire = config.maxActorsPerFire ?? DEFAULT_MAX_ACTORS_PER_FIRE;
|
|
45
|
+
const minIntervalMs = (config.minHoursBetweenDigests ?? DEFAULT_MIN_HOURS_BETWEEN_DIGESTS) * 60 * 60 * 1000;
|
|
46
|
+
const store = config.store ?? createInMemoryDigestStore();
|
|
47
|
+
const now = config.now ?? Date.now;
|
|
48
|
+
const getActorId = config.getActorId ?? ((ctx) => ctx.userId);
|
|
49
|
+
const onActorFailure = config.onActorFailure ?? ((info) => {
|
|
50
|
+
console.error(`[sync-pack-digest] actor "${info.actorId}" failed at ${info.phase}:`, info.error);
|
|
51
|
+
});
|
|
52
|
+
const pack = {
|
|
53
|
+
name: "@absolutejs/sync-pack-digest",
|
|
54
|
+
ownsTables: [table],
|
|
55
|
+
readsTables: [],
|
|
56
|
+
version: "0.1.0",
|
|
57
|
+
schemas: defineSchema({
|
|
58
|
+
[table]: {
|
|
59
|
+
fields: {
|
|
60
|
+
id: field.string,
|
|
61
|
+
actorId: field.string,
|
|
62
|
+
lastSentAt: field.number,
|
|
63
|
+
lastSubject: field.string
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}),
|
|
67
|
+
readers: { [table]: store.reader },
|
|
68
|
+
writers: { [table]: store.writer },
|
|
69
|
+
permissions: {
|
|
70
|
+
[table]: {
|
|
71
|
+
read: (ctx, row) => {
|
|
72
|
+
const callerId = resolveActor(getActorId, ctx);
|
|
73
|
+
return callerId !== undefined && row.actorId === callerId;
|
|
74
|
+
},
|
|
75
|
+
insert: (ctx, row) => {
|
|
76
|
+
const callerId = resolveActor(getActorId, ctx);
|
|
77
|
+
return callerId !== undefined && row.actorId === callerId;
|
|
78
|
+
},
|
|
79
|
+
update: (ctx, row) => {
|
|
80
|
+
const callerId = resolveActor(getActorId, ctx);
|
|
81
|
+
return callerId !== undefined && row.actorId === callerId;
|
|
82
|
+
},
|
|
83
|
+
delete: (ctx, row) => {
|
|
84
|
+
const callerId = resolveActor(getActorId, ctx);
|
|
85
|
+
if (callerId === undefined)
|
|
86
|
+
return false;
|
|
87
|
+
if (row.actorId !== undefined) {
|
|
88
|
+
return row.actorId === callerId;
|
|
89
|
+
}
|
|
90
|
+
const id = row.id;
|
|
91
|
+
if (id === undefined)
|
|
92
|
+
return false;
|
|
93
|
+
return store.getById(id)?.actorId === callerId;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
collections: [
|
|
98
|
+
defineCollection({
|
|
99
|
+
name: collectionName,
|
|
100
|
+
tables: [table],
|
|
101
|
+
key: (row) => row.id,
|
|
102
|
+
hydrate: (_params, ctx) => {
|
|
103
|
+
const callerId = resolveActor(getActorId, ctx);
|
|
104
|
+
if (callerId === undefined)
|
|
105
|
+
return [];
|
|
106
|
+
return store.reader.all(ctx).filter((row) => row.actorId === callerId);
|
|
107
|
+
},
|
|
108
|
+
match: (row, _params, ctx) => {
|
|
109
|
+
const callerId = resolveActor(getActorId, ctx);
|
|
110
|
+
return callerId !== undefined && row.actorId === callerId;
|
|
111
|
+
},
|
|
112
|
+
authorize: (_params, ctx) => resolveActor(getActorId, ctx) !== undefined
|
|
113
|
+
})
|
|
114
|
+
],
|
|
115
|
+
schedules: [
|
|
116
|
+
defineSchedule({
|
|
117
|
+
name: scheduleName,
|
|
118
|
+
pattern: cron,
|
|
119
|
+
...config.retry !== undefined ? { retry: config.retry } : {},
|
|
120
|
+
run: async ({ actions }) => {
|
|
121
|
+
const t = now();
|
|
122
|
+
const actors = await config.listActors();
|
|
123
|
+
let processed = 0;
|
|
124
|
+
for (const actorId of actors) {
|
|
125
|
+
if (processed >= maxActorsPerFire)
|
|
126
|
+
break;
|
|
127
|
+
const cursor = store.getById(actorId);
|
|
128
|
+
if (cursor !== undefined && t - cursor.lastSentAt < minIntervalMs) {
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
let payload;
|
|
132
|
+
try {
|
|
133
|
+
const since = cursor === undefined ? null : new Date(cursor.lastSentAt);
|
|
134
|
+
payload = await config.buildDigest(actorId, since);
|
|
135
|
+
} catch (error) {
|
|
136
|
+
onActorFailure({
|
|
137
|
+
actorId,
|
|
138
|
+
error,
|
|
139
|
+
phase: "buildDigest"
|
|
140
|
+
});
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
if (payload === null) {
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
try {
|
|
147
|
+
await config.send(payload);
|
|
148
|
+
} catch (error) {
|
|
149
|
+
onActorFailure({
|
|
150
|
+
actorId,
|
|
151
|
+
error,
|
|
152
|
+
phase: "send"
|
|
153
|
+
});
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
processed++;
|
|
157
|
+
try {
|
|
158
|
+
const next = {
|
|
159
|
+
id: actorId,
|
|
160
|
+
actorId,
|
|
161
|
+
lastSentAt: t,
|
|
162
|
+
lastSubject: payload.subject
|
|
163
|
+
};
|
|
164
|
+
if (cursor === undefined) {
|
|
165
|
+
await actions.insert(table, next);
|
|
166
|
+
} else {
|
|
167
|
+
await actions.update(table, next);
|
|
168
|
+
}
|
|
169
|
+
} catch (error) {
|
|
170
|
+
onActorFailure({
|
|
171
|
+
actorId,
|
|
172
|
+
error,
|
|
173
|
+
phase: "cursor"
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
})
|
|
179
|
+
]
|
|
180
|
+
};
|
|
181
|
+
return defineSyncPack(pack);
|
|
182
|
+
};
|
|
183
|
+
export {
|
|
184
|
+
createInMemoryDigestStore,
|
|
185
|
+
createDigestPack
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
//# debugId=AE598FAB1C126FB164756E2164756E21
|
|
189
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/index.ts"],
|
|
4
|
+
"sourcesContent": [
|
|
5
|
+
"/**\n * `@absolutejs/sync-pack-digest` — scheduled per-actor digest pack for\n * `@absolutejs/sync`.\n *\n * Cron-fires a schedule that iterates the host's actor list, asks the host\n * to build a digest payload for each actor since their last cursor, and\n * dispatches it through a host-provided `send` adapter. Owns one table\n * (`digest_cursors`) with one row per actor; nothing else.\n *\n * Per-actor failures are caught and logged so one bad send doesn't block\n * the rest. The whole-handler retry shim from sync 1.9.0 is wired but off\n * by default (digest fires are usually fine to skip on transient infra\n * failures — the next cron fire catches up).\n *\n * @example\n * ```ts\n * import { createSyncEngine } from '@absolutejs/sync/engine';\n * import { createDigestPack } from '@absolutejs/sync-pack-digest';\n *\n * const engine = createSyncEngine();\n * engine.registerPack(createDigestPack({\n * cron: '0 8 * * 1', // Mondays 8am\n * listActors: () => db.users.allActiveIds(),\n * buildDigest: async (actorId, since) => {\n * const items = await db.feed.since(actorId, since ?? new Date(0));\n * if (items.length === 0) return null;\n * return {\n * to: await db.users.email(actorId),\n * subject: `Your weekly digest`,\n * body: render(items),\n * };\n * },\n * send: async (msg) => await resend.send(msg),\n * }));\n * ```\n */\n\nimport {\n\tdefineCollection,\n\tdefineSchedule,\n\tdefineSchema,\n\tdefineSyncPack,\n\tfield,\n\ttype CollectionContext,\n\ttype RetryPolicy,\n\ttype SyncPack,\n\ttype TableReader,\n\ttype TableWriter\n} from '@absolutejs/sync/engine';\n\n/** One row per actor — created on first successful send, updated after each. */\nexport type DigestCursor = {\n\t/** Row id (= actorId). */\n\tid: string;\n\tactorId: string;\n\t/** Epoch ms of the most recent successful send. */\n\tlastSentAt: number;\n\t/** Subject line of the most recent successful send (for observability). */\n\tlastSubject: string;\n};\n\n/** The payload the host's `buildDigest` returns. Returning `null` skips. */\nexport type DigestPayload = {\n\tto: string;\n\tsubject: string;\n\tbody: string;\n};\n\n/** Storage adapter the pack uses for the cursors table. */\nexport type DigestStore = {\n\treader: TableReader<CollectionContext>;\n\twriter: TableWriter<DigestCursor, CollectionContext, unknown>;\n\tgetById: (id: string) => DigestCursor | undefined;\n};\n\n/** Default in-memory store. */\nexport const createInMemoryDigestStore = (): DigestStore => {\n\tconst rows = new Map<string, DigestCursor>();\n\treturn {\n\t\tgetById: (id) => rows.get(id),\n\t\treader: {\n\t\t\tall: () => [...rows.values()]\n\t\t},\n\t\twriter: {\n\t\t\tdelete: (row) => {\n\t\t\t\trows.delete((row as { id: string }).id);\n\t\t\t},\n\t\t\tinsert: (data) => {\n\t\t\t\trows.set(data.id, data);\n\t\t\t\treturn data;\n\t\t\t},\n\t\t\tupdate: (data) => {\n\t\t\t\tconst prior = rows.get(data.id);\n\t\t\t\tconst merged = { ...(prior ?? {}), ...data } as DigestCursor;\n\t\t\t\trows.set(data.id, merged);\n\t\t\t\treturn merged;\n\t\t\t}\n\t\t}\n\t};\n};\n\nconst DEFAULT_CRON = '0 8 * * 1';\nconst DEFAULT_MAX_ACTORS_PER_FIRE = 1000;\nconst DEFAULT_MIN_HOURS_BETWEEN_DIGESTS = 168;\n\n/** Factory config. `send`, `buildDigest`, and `listActors` are required. */\nexport type DigestPackConfig<Ctx = CollectionContext> = {\n\t/**\n\t * Prefix applied to the owned table name AND every collection/schedule\n\t * the pack exposes. Default `\"\"`.\n\t */\n\tprefix?: string;\n\t/**\n\t * Cron pattern for the digest fire (`@elysiajs/cron` / croner syntax).\n\t * Default `\"0 8 * * 1\"` (Mondays 08:00).\n\t */\n\tcron?: string;\n\t/**\n\t * Host-supplied email sender. The pack does NOT own transport (no SMTP\n\t * client, no Resend/SES/Postmark dep). Throws are caught per-actor.\n\t */\n\tsend: (msg: DigestPayload) => Promise<void>;\n\t/**\n\t * Build the digest payload for one actor. `since` is the actor's last\n\t * successful send timestamp, or `null` if they've never received one.\n\t * Return `null` to skip this fire silently (no content for this actor).\n\t */\n\tbuildDigest: (\n\t\tactorId: string,\n\t\tsince: Date | null\n\t) => Promise<DigestPayload | null>;\n\t/**\n\t * Yield the actor ids the schedule should iterate per fire. The host's\n\t * own data layer is the source of truth for \"who exists\" — the pack\n\t * just maintains per-actor cursors.\n\t */\n\tlistActors: () =>\n\t\t| Iterable<string>\n\t\t| Promise<Iterable<string>>;\n\t/**\n\t * Cap on actors processed per fire (back-pressure). The rest wait for\n\t * the next cron fire. Default `1000`.\n\t */\n\tmaxActorsPerFire?: number;\n\t/**\n\t * Minimum hours between digests for a given actor. An actor whose\n\t * cursor is fresher than this is skipped on the fire. Default `168`\n\t * (one week — matches the weekly default cron).\n\t */\n\tminHoursBetweenDigests?: number;\n\t/**\n\t * Extract the current actor id from ctx — used to scope the cursor\n\t * collection so each actor sees their own row. Default\n\t * `(ctx) => ctx.userId`.\n\t */\n\tgetActorId?: (ctx: Ctx) => string | undefined;\n\t/**\n\t * Outer schedule retry policy. The pack catches per-actor failures\n\t * already, so this only retries the whole handler on classified\n\t * transient infrastructure failures (e.g. DB serialization). Default\n\t * undefined — digest fires miss skip on infra failure and catch up\n\t * next cron.\n\t */\n\tretry?: RetryPolicy;\n\t/** Override the storage adapter. Default: per-instance in-memory. */\n\tstore?: DigestStore;\n\t/** Wall-clock function. Default `Date.now`. */\n\tnow?: () => number;\n\t/**\n\t * Hook for surfacing per-actor failures. Defaults to `console.error`.\n\t * The pack swallows the error after this call so the schedule keeps\n\t * going for the rest of the actor list.\n\t */\n\tonActorFailure?: (info: {\n\t\tactorId: string;\n\t\tphase: 'buildDigest' | 'send' | 'cursor';\n\t\terror: unknown;\n\t}) => void;\n};\n\nconst resolveActor = (\n\tgetActorId: NonNullable<DigestPackConfig['getActorId']>,\n\tctx: unknown\n): string | undefined => getActorId(ctx as CollectionContext);\n\n/**\n * Build a {@link SyncPack} that exposes scheduled per-actor digests. Each\n * call returns a fresh pack with its own cursor store (unless overridden).\n */\nexport const createDigestPack = <Ctx = CollectionContext>(\n\tconfig: DigestPackConfig<Ctx>\n): SyncPack => {\n\tconst prefix = config.prefix ?? '';\n\tconst table = `${prefix}digest_cursors`;\n\tconst collectionName = table;\n\tconst scheduleName = `${prefix}digest:fire`;\n\tconst cron = config.cron ?? DEFAULT_CRON;\n\tconst maxActorsPerFire =\n\t\tconfig.maxActorsPerFire ?? DEFAULT_MAX_ACTORS_PER_FIRE;\n\tconst minIntervalMs =\n\t\t(config.minHoursBetweenDigests ??\n\t\t\tDEFAULT_MIN_HOURS_BETWEEN_DIGESTS) *\n\t\t60 *\n\t\t60 *\n\t\t1000;\n\tconst store = config.store ?? createInMemoryDigestStore();\n\tconst now = config.now ?? Date.now;\n\tconst getActorId = (config.getActorId ??\n\t\t((ctx: CollectionContext) =>\n\t\t\t(ctx as { userId?: string }).userId)) as (\n\t\tctx: CollectionContext\n\t) => string | undefined;\n\tconst onActorFailure =\n\t\tconfig.onActorFailure ??\n\t\t((info) => {\n\t\t\t// eslint-disable-next-line no-console\n\t\t\tconsole.error(\n\t\t\t\t`[sync-pack-digest] actor \"${info.actorId}\" failed at ${info.phase}:`,\n\t\t\t\tinfo.error\n\t\t\t);\n\t\t});\n\n\tconst pack: SyncPack = {\n\t\tname: '@absolutejs/sync-pack-digest',\n\t\townsTables: [table],\n\t\treadsTables: [],\n\t\tversion: '0.1.0',\n\n\t\tschemas: defineSchema({\n\t\t\t[table]: {\n\t\t\t\tfields: {\n\t\t\t\t\tid: field.string,\n\t\t\t\t\tactorId: field.string,\n\t\t\t\t\tlastSentAt: field.number,\n\t\t\t\t\tlastSubject: field.string\n\t\t\t\t}\n\t\t\t}\n\t\t}),\n\n\t\treaders: { [table]: store.reader },\n\t\twriters: { [table]: store.writer },\n\n\t\tpermissions: {\n\t\t\t[table]: {\n\t\t\t\t// Each actor sees only their own cursor — the cursor row\n\t\t\t\t// surfaces \"your last digest was at X\" to the user without\n\t\t\t\t// leaking other actors' state. Hosts who want a global\n\t\t\t\t// admin view register their own permissions on top.\n\t\t\t\tread: (ctx: unknown, row: DigestCursor) => {\n\t\t\t\t\tconst callerId = resolveActor(getActorId, ctx);\n\t\t\t\t\treturn callerId !== undefined && row.actorId === callerId;\n\t\t\t\t},\n\t\t\t\t// Writes flow only through the pack's schedule; deny\n\t\t\t\t// outside writers by stamping actorId === caller, which\n\t\t\t\t// fails for any non-actor (the schedule runs trusted and\n\t\t\t\t// bypasses these checks).\n\t\t\t\tinsert: (ctx: unknown, row: DigestCursor) => {\n\t\t\t\t\tconst callerId = resolveActor(getActorId, ctx);\n\t\t\t\t\treturn callerId !== undefined && row.actorId === callerId;\n\t\t\t\t},\n\t\t\t\tupdate: (ctx: unknown, row: DigestCursor) => {\n\t\t\t\t\tconst callerId = resolveActor(getActorId, ctx);\n\t\t\t\t\treturn callerId !== undefined && row.actorId === callerId;\n\t\t\t\t},\n\t\t\t\tdelete: (ctx: unknown, row: DigestCursor) => {\n\t\t\t\t\tconst callerId = resolveActor(getActorId, ctx);\n\t\t\t\t\tif (callerId === undefined) return false;\n\t\t\t\t\tif ((row as { actorId?: string }).actorId !== undefined) {\n\t\t\t\t\t\treturn (row as { actorId: string }).actorId === callerId;\n\t\t\t\t\t}\n\t\t\t\t\tconst id = (row as { id?: string }).id;\n\t\t\t\t\tif (id === undefined) return false;\n\t\t\t\t\treturn store.getById(id)?.actorId === callerId;\n\t\t\t\t}\n\t\t\t}\n\t\t},\n\n\t\tcollections: [\n\t\t\tdefineCollection<DigestCursor, void, CollectionContext>({\n\t\t\t\tname: collectionName,\n\t\t\t\ttables: [table],\n\t\t\t\tkey: (row) => row.id,\n\t\t\t\thydrate: (_params, ctx) => {\n\t\t\t\t\tconst callerId = resolveActor(getActorId, ctx);\n\t\t\t\t\tif (callerId === undefined) return [];\n\t\t\t\t\treturn (store.reader.all(ctx) as DigestCursor[]).filter(\n\t\t\t\t\t\t(row) => row.actorId === callerId\n\t\t\t\t\t);\n\t\t\t\t},\n\t\t\t\tmatch: (row, _params, ctx) => {\n\t\t\t\t\tconst callerId = resolveActor(getActorId, ctx);\n\t\t\t\t\treturn (\n\t\t\t\t\t\tcallerId !== undefined && row.actorId === callerId\n\t\t\t\t\t);\n\t\t\t\t},\n\t\t\t\tauthorize: (_params, ctx) =>\n\t\t\t\t\tresolveActor(getActorId, ctx) !== undefined\n\t\t\t})\n\t\t],\n\n\t\tschedules: [\n\t\t\tdefineSchedule({\n\t\t\t\tname: scheduleName,\n\t\t\t\tpattern: cron,\n\t\t\t\t...(config.retry !== undefined ? { retry: config.retry } : {}),\n\t\t\t\trun: async ({ actions }) => {\n\t\t\t\t\tconst t = now();\n\t\t\t\t\tconst actors = await config.listActors();\n\t\t\t\t\tlet processed = 0;\n\t\t\t\t\tfor (const actorId of actors) {\n\t\t\t\t\t\tif (processed >= maxActorsPerFire) break;\n\t\t\t\t\t\tconst cursor = store.getById(actorId);\n\t\t\t\t\t\tif (\n\t\t\t\t\t\t\tcursor !== undefined &&\n\t\t\t\t\t\t\tt - cursor.lastSentAt < minIntervalMs\n\t\t\t\t\t\t) {\n\t\t\t\t\t\t\t// Not yet due for this actor.\n\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tlet payload: DigestPayload | null;\n\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\tconst since =\n\t\t\t\t\t\t\t\tcursor === undefined\n\t\t\t\t\t\t\t\t\t? null\n\t\t\t\t\t\t\t\t\t: new Date(cursor.lastSentAt);\n\t\t\t\t\t\t\tpayload = await config.buildDigest(actorId, since);\n\t\t\t\t\t\t} catch (error) {\n\t\t\t\t\t\t\tonActorFailure({\n\t\t\t\t\t\t\t\tactorId,\n\t\t\t\t\t\t\t\terror,\n\t\t\t\t\t\t\t\tphase: 'buildDigest'\n\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif (payload === null) {\n\t\t\t\t\t\t\t// Host signalled no content for this actor — skip\n\t\t\t\t\t\t\t// without bumping the cursor.\n\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t}\n\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\tawait config.send(payload);\n\t\t\t\t\t\t} catch (error) {\n\t\t\t\t\t\t\tonActorFailure({\n\t\t\t\t\t\t\t\tactorId,\n\t\t\t\t\t\t\t\terror,\n\t\t\t\t\t\t\t\tphase: 'send'\n\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tprocessed++;\n\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\tconst next: DigestCursor = {\n\t\t\t\t\t\t\t\tid: actorId,\n\t\t\t\t\t\t\t\tactorId,\n\t\t\t\t\t\t\t\tlastSentAt: t,\n\t\t\t\t\t\t\t\tlastSubject: payload.subject\n\t\t\t\t\t\t\t};\n\t\t\t\t\t\t\tif (cursor === undefined) {\n\t\t\t\t\t\t\t\tawait actions.insert(table, next);\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\tawait actions.update(table, next);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t} catch (error) {\n\t\t\t\t\t\t\t// The email already went out — losing the cursor\n\t\t\t\t\t\t\t// update means the actor may double-receive on\n\t\t\t\t\t\t\t// the next fire. We log so the operator sees it.\n\t\t\t\t\t\t\tonActorFailure({\n\t\t\t\t\t\t\t\tactorId,\n\t\t\t\t\t\t\t\terror,\n\t\t\t\t\t\t\t\tphase: 'cursor'\n\t\t\t\t\t\t\t});\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t})\n\t\t]\n\t};\n\n\treturn defineSyncPack(pack);\n};\n"
|
|
6
|
+
],
|
|
7
|
+
"mappings": "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",
|
|
8
|
+
"debugId": "AE598FAB1C126FB164756E2164756E21",
|
|
9
|
+
"names": []
|
|
10
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@absolutejs/sync-pack-digest",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Scheduled per-actor digest pack for @absolutejs/sync — cron-fired, cursor-managed, transport-agnostic",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "https://github.com/absolutejs/sync-packs.git",
|
|
8
|
+
"directory": "digest"
|
|
9
|
+
},
|
|
10
|
+
"main": "./dist/index.js",
|
|
11
|
+
"module": "./dist/index.js",
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"type": "module",
|
|
14
|
+
"license": "CC BY-NC 4.0",
|
|
15
|
+
"author": "Alex Kahn",
|
|
16
|
+
"publishConfig": {
|
|
17
|
+
"access": "public"
|
|
18
|
+
},
|
|
19
|
+
"keywords": [
|
|
20
|
+
"absolutejs",
|
|
21
|
+
"sync",
|
|
22
|
+
"sync-pack",
|
|
23
|
+
"digest",
|
|
24
|
+
"scheduled",
|
|
25
|
+
"email"
|
|
26
|
+
],
|
|
27
|
+
"scripts": {
|
|
28
|
+
"build": "rm -rf dist && bun build src/index.ts --outdir dist --sourcemap --target=bun --external @absolutejs/sync && tsc --project tsconfig.build.json",
|
|
29
|
+
"test": "bun test",
|
|
30
|
+
"typecheck": "tsc --noEmit",
|
|
31
|
+
"format": "prettier --write \"./**/*.{ts,json,md}\"",
|
|
32
|
+
"release": "bun run format && bun run typecheck && bun run test && bun run build && bun publish"
|
|
33
|
+
},
|
|
34
|
+
"peerDependencies": {
|
|
35
|
+
"@absolutejs/sync": ">= 1.9.1"
|
|
36
|
+
},
|
|
37
|
+
"devDependencies": {
|
|
38
|
+
"@absolutejs/sync": "^1.9.1",
|
|
39
|
+
"@types/bun": "1.3.14",
|
|
40
|
+
"prettier": "3.5.3",
|
|
41
|
+
"typescript": "5.8.3"
|
|
42
|
+
},
|
|
43
|
+
"exports": {
|
|
44
|
+
".": {
|
|
45
|
+
"types": "./dist/index.d.ts",
|
|
46
|
+
"import": "./dist/index.js",
|
|
47
|
+
"default": "./dist/index.js"
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
"files": ["dist", "README.md"]
|
|
51
|
+
}
|