@absolutejs/sync-pack-mentions 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 ADDED
@@ -0,0 +1,128 @@
1
+ # @absolutejs/sync-pack-favorites
2
+
3
+ Per-actor saved-resources pack for
4
+ [`@absolutejs/sync`](https://github.com/absolutejs/sync). Each actor sees
5
+ only their own rows; favoriting is idempotent (deterministic row id);
6
+ optional join collection pairs each favorite with the host's resource
7
+ row in one live subscription.
8
+
9
+ ```bash
10
+ bun add @absolutejs/sync-pack-favorites
11
+ ```
12
+
13
+ ## Usage
14
+
15
+ ```ts
16
+ import { createSyncEngine } from '@absolutejs/sync/engine';
17
+ import { createFavoritesPack } from '@absolutejs/sync-pack-favorites';
18
+
19
+ const engine = createSyncEngine();
20
+ engine.registerPack(
21
+ createFavoritesPack({
22
+ getActorId: (ctx) => ctx.session.userId,
23
+ }),
24
+ );
25
+ ```
26
+
27
+ Then from the client:
28
+
29
+ ```ts
30
+ // Toggle is the easy default — one round-trip, returns the new state.
31
+ const { favorited } = await store.mutate({
32
+ args: { resourceKind: 'doc', resourceId: 'doc-123' },
33
+ name: 'favorites:toggle',
34
+ });
35
+
36
+ // Or favorite / unfavorite explicitly.
37
+ await store.mutate({ args: { resourceKind: 'doc', resourceId: 'doc-123' }, name: 'favorites:favorite' });
38
+ await store.mutate({ args: { resourceKind: 'doc', resourceId: 'doc-123' }, name: 'favorites:unfavorite' });
39
+ ```
40
+
41
+ ## The pack exposes
42
+
43
+ | Surface | Name | What it does |
44
+ | ----------- | ----------------------------- | ----------------------------------------------------------- |
45
+ | Collection | `favorites` | Per-actor list. Optional `params.resourceKind` filter |
46
+ | Mutation | `favorites:favorite` | Idempotent insert. Subsequent calls are no-ops |
47
+ | Mutation | `favorites:unfavorite` | Idempotent delete. No-op if the row never existed |
48
+ | Mutation | `favorites:toggle` | Insert if missing, delete if present. Returns `{ favorited }` |
49
+ | Collection | `favorites-with-resource` (opt) | Join with the host's resource table — see below |
50
+
51
+ ## Row shape
52
+
53
+ ```ts
54
+ type FavoriteRow = {
55
+ id: string; // `${actorId}:${resourceKind}:${resourceId}` — deterministic
56
+ actorId: string;
57
+ resourceKind: string; // app-level: "doc" | "task" | "issue" | ...
58
+ resourceId: string;
59
+ createdAt: number;
60
+ };
61
+ ```
62
+
63
+ The deterministic id means the same `(actor, kind, resource)` triple
64
+ always maps to the same row — so duplicate `favorite` calls are
65
+ idempotent at the storage layer, not just behaviorally.
66
+
67
+ ## Optional: `favorites-with-resource` join
68
+
69
+ When you set `joinResources`, the pack additionally registers a join
70
+ collection that pairs each favorite with the host's resource row (same
71
+ pattern as comments-with-author from sync-pack-comments).
72
+
73
+ ```ts
74
+ type DocRow = { id: string; title: string };
75
+
76
+ engine.registerReader('docs', { all: () => db.docs.list() });
77
+ engine.registerPack(
78
+ createFavoritesPack<MyCtx, DocRow>({
79
+ getActorId,
80
+ joinResources: {
81
+ table: 'docs',
82
+ // Default (row) => row.id; override if your resource id key isn't `id`.
83
+ // key: (doc) => doc.docId,
84
+ // Required: host supplies the resource-side hydrate.
85
+ hydrate: () => db.docs.list(),
86
+ },
87
+ }),
88
+ );
89
+
90
+ const view = await engine.subscribe<
91
+ FavoriteRow & { resource: DocRow },
92
+ { resourceKind?: string }
93
+ >({
94
+ collection: 'favorites-with-resource',
95
+ params: { resourceKind: 'doc' },
96
+ ctx,
97
+ onDiff: rerender,
98
+ });
99
+ // view.initial[0] === { ...favorite, resource: { id, title } }
100
+ ```
101
+
102
+ Inner join — favorites whose resource has been deleted drop out of the
103
+ join (but stay in the base `favorites` collection).
104
+
105
+ ## Storage
106
+
107
+ Default: per-instance in-memory store. For a persistent backend, pass a
108
+ custom `store`:
109
+
110
+ ```ts
111
+ import { type FavoritesStore } from '@absolutejs/sync-pack-favorites';
112
+
113
+ const store: FavoritesStore = {
114
+ getById: (id) => /* SELECT * FROM favorites WHERE id = $1 */,
115
+ reader: { all: () => /* SELECT */ },
116
+ writer: { insert, update, delete },
117
+ };
118
+ ```
119
+
120
+ ## Multiple instances
121
+
122
+ Pass a `prefix` to coexist with another favorites pack instance (e.g. a
123
+ "team" set vs a "private" set):
124
+
125
+ ```ts
126
+ engine.registerPack(createFavoritesPack({ prefix: 'team_', /* ... */ }));
127
+ engine.registerPack(createFavoritesPack({ prefix: 'private_', /* ... */ }));
128
+ ```
@@ -0,0 +1,114 @@
1
+ /**
2
+ * `@absolutejs/sync-pack-mentions` — `@username` parser pack for
3
+ * `@absolutejs/sync`. Owns a `mentions` table, exposes a
4
+ * `mentions:record` mutation that parses a body, writes per-actor
5
+ * mention rows, and fires an `onMention` hook the host can use to
6
+ * compose with other packs (notifications, email, Slack, …).
7
+ *
8
+ * Composition model: packs never call each other's mutations directly.
9
+ * Instead, the host wires them. This pack provides the parser + the
10
+ * per-actor collection; the `onMention` hook is the seam — typically
11
+ * `({ mention }, ctx, actions) => engine.runMutation('notifications:notify', …)`.
12
+ * The pack itself only owns the mentions table.
13
+ *
14
+ * @example
15
+ * ```ts
16
+ * import { createSyncEngine } from '@absolutejs/sync/engine';
17
+ * import { createMentionsPack } from '@absolutejs/sync-pack-mentions';
18
+ *
19
+ * const engine = createSyncEngine();
20
+ * engine.registerPack(createMentionsPack({
21
+ * getActorId: (ctx) => ctx.session.userId,
22
+ * resolveActorId: async (username) => userIdByUsername(username),
23
+ * onMention: async ({ mention }, ctx) => {
24
+ * await engine.runMutation('notifications:notify', {
25
+ * targetActorId: mention.mentionedActorId,
26
+ * kind: 'mention',
27
+ * title: `You were mentioned`,
28
+ * body: mention.snippet,
29
+ * href: `/comments/${mention.sourceId}`,
30
+ * }, ctx);
31
+ * },
32
+ * }));
33
+ * ```
34
+ */
35
+ import { type CollectionContext, type MutationActions, type SyncPack, type TableReader, type TableWriter } from '@absolutejs/sync/engine';
36
+ /** One row per `(sourceKind, sourceId, mentionedActorId)`. */
37
+ export type MentionRow = {
38
+ /** `${sourceKind}:${sourceId}:${mentionedActorId}` — deterministic so re-recording is idempotent. */
39
+ id: string;
40
+ /** Host-level category for the row that originated the mention ("comment", "doc", "task", …). */
41
+ sourceKind: string;
42
+ /** Host-level id of the row that originated the mention. */
43
+ sourceId: string;
44
+ /** Who wrote the body containing the mention. `null` for system-generated content. */
45
+ authorId: string | null;
46
+ /** The actor id the `@username` resolved to. */
47
+ mentionedActorId: string;
48
+ /** The raw username text (without the `@`). */
49
+ username: string;
50
+ /** A short excerpt of the body around the mention — useful for inbox previews. */
51
+ snippet: string;
52
+ createdAt: number;
53
+ /** Stamped by `mentions:resolve` (host's onMention hook is free to call it). */
54
+ resolvedAt: number | null;
55
+ };
56
+ export type MentionsStore = {
57
+ reader: TableReader<CollectionContext>;
58
+ writer: TableWriter<MentionRow, CollectionContext, unknown>;
59
+ getById: (id: string) => MentionRow | undefined;
60
+ };
61
+ export declare const createInMemoryMentionsStore: () => MentionsStore;
62
+ /** Default regex — matches `@word` where `word` is one or more `[A-Za-z0-9_-]`. */
63
+ export declare const DEFAULT_MENTION_PATTERN: RegExp;
64
+ /** Args passed to the host's `onMention` hook. */
65
+ export type OnMentionArgs = {
66
+ mention: MentionRow;
67
+ /** Full body the mention came from. */
68
+ body: string;
69
+ };
70
+ export type MentionsPackConfig<Ctx = CollectionContext> = {
71
+ prefix?: string;
72
+ getActorId?: (ctx: Ctx) => string | undefined;
73
+ /**
74
+ * Regex used to extract `@usernames` from a body. Must have the `g`
75
+ * flag. Default {@link DEFAULT_MENTION_PATTERN}.
76
+ */
77
+ pattern?: RegExp;
78
+ /**
79
+ * Resolve a matched username to the actor id to record + notify.
80
+ * Returning `undefined` skips that match (unknown user). Default is
81
+ * identity (`username -> username`), useful for tests and for apps
82
+ * where usernames already double as actor ids.
83
+ */
84
+ resolveActorId?: (username: string, ctx: Ctx) => Promise<string | undefined> | string | undefined;
85
+ /**
86
+ * How many characters around each match to keep in `snippet`.
87
+ * Default 30 each side; the snippet is clamped to the body length.
88
+ */
89
+ snippetRadius?: number;
90
+ /**
91
+ * Composition seam — fired once per mention after its row is
92
+ * written. The host's typical implementation calls
93
+ * `engine.runMutation('notifications:notify', …)` here. Errors are
94
+ * caught + logged; one failing hook does not roll back the recorded
95
+ * mention row.
96
+ */
97
+ onMention?: (args: OnMentionArgs, ctx: Ctx, actions: MutationActions) => void | Promise<void>;
98
+ /** Custom store. Default per-instance in-memory. */
99
+ store?: MentionsStore;
100
+ /** Wall-clock. Default `Date.now`. */
101
+ now?: () => number;
102
+ };
103
+ /** Extract distinct `@username` matches from a body. Public so tests can lean on it. */
104
+ export declare const parseMentions: (body: string, pattern?: RegExp) => {
105
+ username: string;
106
+ index: number;
107
+ }[];
108
+ /**
109
+ * Build a {@link SyncPack} that parses `@mentions` from a posted body
110
+ * and writes one row per mentioned actor. Composes with other packs
111
+ * (notifications, digest, …) via the `onMention` hook — the pack itself
112
+ * never calls another pack's mutations.
113
+ */
114
+ export declare const createMentionsPack: <Ctx = CollectionContext>(config?: MentionsPackConfig<Ctx>) => SyncPack;
package/dist/index.js ADDED
@@ -0,0 +1,235 @@
1
+ // @bun
2
+ // src/index.ts
3
+ import {
4
+ defineCollection,
5
+ defineMutation,
6
+ defineSchema,
7
+ defineSyncPack,
8
+ field,
9
+ UnauthorizedError
10
+ } from "@absolutejs/sync/engine";
11
+ var createInMemoryMentionsStore = () => {
12
+ const rows = new Map;
13
+ return {
14
+ getById: (id) => rows.get(id),
15
+ reader: {
16
+ all: () => [...rows.values()]
17
+ },
18
+ writer: {
19
+ delete: (row) => {
20
+ rows.delete(row.id);
21
+ },
22
+ insert: (data) => {
23
+ rows.set(data.id, data);
24
+ return data;
25
+ },
26
+ update: (data) => {
27
+ const prior = rows.get(data.id);
28
+ const merged = { ...prior ?? {}, ...data };
29
+ rows.set(data.id, merged);
30
+ return merged;
31
+ }
32
+ }
33
+ };
34
+ };
35
+ var DEFAULT_MENTION_PATTERN = /@([A-Za-z0-9_-]+)/g;
36
+ var DEFAULT_SNIPPET_RADIUS = 30;
37
+ var buildSnippet = (body, matchIndex, radius) => {
38
+ const start = Math.max(0, matchIndex - radius);
39
+ const end = Math.min(body.length, matchIndex + radius);
40
+ const prefix = start > 0 ? "\u2026" : "";
41
+ const suffix = end < body.length ? "\u2026" : "";
42
+ return `${prefix}${body.slice(start, end)}${suffix}`;
43
+ };
44
+ var parseMentions = (body, pattern = DEFAULT_MENTION_PATTERN) => {
45
+ const seen = new Set;
46
+ const matches = [];
47
+ pattern.lastIndex = 0;
48
+ let match = pattern.exec(body);
49
+ while (match !== null) {
50
+ const username = match[1];
51
+ if (username !== undefined && !seen.has(username)) {
52
+ seen.add(username);
53
+ matches.push({ index: match.index, username });
54
+ }
55
+ if (!pattern.global)
56
+ break;
57
+ match = pattern.exec(body);
58
+ }
59
+ pattern.lastIndex = 0;
60
+ return matches;
61
+ };
62
+ var rowId = (sourceKind, sourceId, mentionedActorId) => `${sourceKind}:${sourceId}:${mentionedActorId}`;
63
+ var createMentionsPack = (config = {}) => {
64
+ const prefix = config.prefix ?? "";
65
+ const table = `${prefix}mentions`;
66
+ const collectionName = table;
67
+ const recordMutationName = `${prefix}mentions:record`;
68
+ const resolveMutationName = `${prefix}mentions:resolve`;
69
+ const dismissMutationName = `${prefix}mentions:dismiss`;
70
+ const store = config.store ?? createInMemoryMentionsStore();
71
+ const now = config.now ?? Date.now;
72
+ const pattern = config.pattern ?? DEFAULT_MENTION_PATTERN;
73
+ const snippetRadius = config.snippetRadius ?? DEFAULT_SNIPPET_RADIUS;
74
+ const getActorId = config.getActorId ?? ((ctx) => ctx.userId);
75
+ const resolveActorId = config.resolveActorId ?? ((username) => username);
76
+ const onMention = config.onMention;
77
+ return defineSyncPack({
78
+ name: "@absolutejs/sync-pack-mentions",
79
+ ownsTables: [table],
80
+ readsTables: [],
81
+ version: "0.1.0",
82
+ schemas: defineSchema({
83
+ [table]: {
84
+ fields: {
85
+ id: field.string,
86
+ sourceKind: field.string,
87
+ sourceId: field.string,
88
+ authorId: (value) => value === null || typeof value === "string",
89
+ mentionedActorId: field.string,
90
+ username: field.string,
91
+ snippet: field.string,
92
+ createdAt: field.number,
93
+ resolvedAt: (value) => value === null || typeof value === "number"
94
+ }
95
+ }
96
+ }),
97
+ readers: { [table]: store.reader },
98
+ writers: { [table]: store.writer },
99
+ permissions: {
100
+ [table]: {
101
+ read: (ctx, row) => {
102
+ const callerId = getActorId(ctx);
103
+ return callerId !== undefined && row.mentionedActorId === callerId;
104
+ },
105
+ insert: () => true,
106
+ update: (ctx, row) => {
107
+ const callerId = getActorId(ctx);
108
+ return callerId !== undefined && row.mentionedActorId === callerId;
109
+ },
110
+ delete: (ctx, row) => {
111
+ const callerId = getActorId(ctx);
112
+ if (callerId === undefined)
113
+ return false;
114
+ const ownerId = row.mentionedActorId ?? (() => {
115
+ const id = row.id;
116
+ return id === undefined ? undefined : store.getById(id)?.mentionedActorId;
117
+ })();
118
+ return ownerId === callerId;
119
+ }
120
+ }
121
+ },
122
+ collections: [
123
+ defineCollection({
124
+ name: collectionName,
125
+ tables: [table],
126
+ key: (row) => row.id,
127
+ hydrate: (params, ctx) => {
128
+ const callerId = getActorId(ctx);
129
+ if (callerId === undefined)
130
+ return [];
131
+ return store.reader.all(ctx).filter((row) => row.mentionedActorId === callerId && (params.sourceKind === undefined || row.sourceKind === params.sourceKind) && (params.unresolvedOnly !== true || row.resolvedAt === null));
132
+ },
133
+ match: (row, params, ctx) => {
134
+ const callerId = getActorId(ctx);
135
+ if (callerId === undefined || row.mentionedActorId !== callerId) {
136
+ return false;
137
+ }
138
+ return (params.sourceKind === undefined || row.sourceKind === params.sourceKind) && (params.unresolvedOnly !== true || row.resolvedAt === null);
139
+ },
140
+ authorize: (_params, ctx) => getActorId(ctx) !== undefined
141
+ })
142
+ ],
143
+ mutations: [
144
+ defineMutation({
145
+ name: recordMutationName,
146
+ handler: async (args, ctx, actions) => {
147
+ const authorIdInput = args.authorId === undefined ? getActorId(ctx) ?? null : args.authorId;
148
+ const matches = parseMentions(args.body, pattern);
149
+ const written = [];
150
+ for (const { username, index } of matches) {
151
+ const resolved = await resolveActorId(username, ctx);
152
+ if (resolved === undefined)
153
+ continue;
154
+ if (authorIdInput !== null && resolved === authorIdInput) {
155
+ continue;
156
+ }
157
+ const id = rowId(args.sourceKind, args.sourceId, resolved);
158
+ const existing = store.getById(id);
159
+ if (existing !== undefined) {
160
+ written.push(existing);
161
+ continue;
162
+ }
163
+ const row = {
164
+ authorId: authorIdInput,
165
+ createdAt: now(),
166
+ id,
167
+ mentionedActorId: resolved,
168
+ resolvedAt: null,
169
+ snippet: buildSnippet(args.body, index, snippetRadius),
170
+ sourceId: args.sourceId,
171
+ sourceKind: args.sourceKind,
172
+ username
173
+ };
174
+ const inserted = await actions.insert(table, row);
175
+ written.push(inserted);
176
+ if (onMention !== undefined) {
177
+ try {
178
+ await onMention({ body: args.body, mention: inserted }, ctx, actions);
179
+ } catch (error) {
180
+ console.error("[sync-pack-mentions] onMention hook threw:", error);
181
+ }
182
+ }
183
+ }
184
+ return written;
185
+ }
186
+ }),
187
+ defineMutation({
188
+ name: resolveMutationName,
189
+ handler: async (args, ctx, actions) => {
190
+ const callerId = getActorId(ctx);
191
+ if (callerId === undefined) {
192
+ throw new UnauthorizedError("mentions:resolve (no actor id)");
193
+ }
194
+ const existing = store.getById(args.id);
195
+ if (existing === undefined)
196
+ return;
197
+ if (existing.mentionedActorId !== callerId) {
198
+ throw new UnauthorizedError("mentions:resolve (not the mentioned actor)");
199
+ }
200
+ if (existing.resolvedAt !== null)
201
+ return existing;
202
+ return await actions.update(table, {
203
+ ...existing,
204
+ resolvedAt: now()
205
+ });
206
+ }
207
+ }),
208
+ defineMutation({
209
+ name: dismissMutationName,
210
+ handler: async (args, ctx, actions) => {
211
+ const callerId = getActorId(ctx);
212
+ if (callerId === undefined) {
213
+ throw new UnauthorizedError("mentions:dismiss (no actor id)");
214
+ }
215
+ const existing = store.getById(args.id);
216
+ if (existing === undefined)
217
+ return;
218
+ if (existing.mentionedActorId !== callerId) {
219
+ throw new UnauthorizedError("mentions:dismiss (not the mentioned actor)");
220
+ }
221
+ await actions.delete(table, { id: args.id });
222
+ }
223
+ })
224
+ ]
225
+ });
226
+ };
227
+ export {
228
+ parseMentions,
229
+ createMentionsPack,
230
+ createInMemoryMentionsStore,
231
+ DEFAULT_MENTION_PATTERN
232
+ };
233
+
234
+ //# debugId=B6339E4895E450BF64756E2164756E21
235
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,10 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/index.ts"],
4
+ "sourcesContent": [
5
+ "/**\n * `@absolutejs/sync-pack-mentions` — `@username` parser pack for\n * `@absolutejs/sync`. Owns a `mentions` table, exposes a\n * `mentions:record` mutation that parses a body, writes per-actor\n * mention rows, and fires an `onMention` hook the host can use to\n * compose with other packs (notifications, email, Slack, …).\n *\n * Composition model: packs never call each other's mutations directly.\n * Instead, the host wires them. This pack provides the parser + the\n * per-actor collection; the `onMention` hook is the seam — typically\n * `({ mention }, ctx, actions) => engine.runMutation('notifications:notify', …)`.\n * The pack itself only owns the mentions table.\n *\n * @example\n * ```ts\n * import { createSyncEngine } from '@absolutejs/sync/engine';\n * import { createMentionsPack } from '@absolutejs/sync-pack-mentions';\n *\n * const engine = createSyncEngine();\n * engine.registerPack(createMentionsPack({\n * getActorId: (ctx) => ctx.session.userId,\n * resolveActorId: async (username) => userIdByUsername(username),\n * onMention: async ({ mention }, ctx) => {\n * await engine.runMutation('notifications:notify', {\n * targetActorId: mention.mentionedActorId,\n * kind: 'mention',\n * title: `You were mentioned`,\n * body: mention.snippet,\n * href: `/comments/${mention.sourceId}`,\n * }, ctx);\n * },\n * }));\n * ```\n */\n\nimport {\n\tdefineCollection,\n\tdefineMutation,\n\tdefineSchema,\n\tdefineSyncPack,\n\tfield,\n\tUnauthorizedError,\n\ttype CollectionContext,\n\ttype MutationActions,\n\ttype SyncPack,\n\ttype TableReader,\n\ttype TableWriter\n} from '@absolutejs/sync/engine';\n\n/** One row per `(sourceKind, sourceId, mentionedActorId)`. */\nexport type MentionRow = {\n\t/** `${sourceKind}:${sourceId}:${mentionedActorId}` — deterministic so re-recording is idempotent. */\n\tid: string;\n\t/** Host-level category for the row that originated the mention (\"comment\", \"doc\", \"task\", …). */\n\tsourceKind: string;\n\t/** Host-level id of the row that originated the mention. */\n\tsourceId: string;\n\t/** Who wrote the body containing the mention. `null` for system-generated content. */\n\tauthorId: string | null;\n\t/** The actor id the `@username` resolved to. */\n\tmentionedActorId: string;\n\t/** The raw username text (without the `@`). */\n\tusername: string;\n\t/** A short excerpt of the body around the mention — useful for inbox previews. */\n\tsnippet: string;\n\tcreatedAt: number;\n\t/** Stamped by `mentions:resolve` (host's onMention hook is free to call it). */\n\tresolvedAt: number | null;\n};\n\nexport type MentionsStore = {\n\treader: TableReader<CollectionContext>;\n\twriter: TableWriter<MentionRow, CollectionContext, unknown>;\n\tgetById: (id: string) => MentionRow | undefined;\n};\n\nexport const createInMemoryMentionsStore = (): MentionsStore => {\n\tconst rows = new Map<string, MentionRow>();\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 MentionRow;\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\n/** Default regex — matches `@word` where `word` is one or more `[A-Za-z0-9_-]`. */\nexport const DEFAULT_MENTION_PATTERN = /@([A-Za-z0-9_-]+)/g;\nconst DEFAULT_SNIPPET_RADIUS = 30;\n\n/** Args passed to the host's `onMention` hook. */\nexport type OnMentionArgs = {\n\tmention: MentionRow;\n\t/** Full body the mention came from. */\n\tbody: string;\n};\n\nexport type MentionsPackConfig<Ctx = CollectionContext> = {\n\tprefix?: string;\n\tgetActorId?: (ctx: Ctx) => string | undefined;\n\t/**\n\t * Regex used to extract `@usernames` from a body. Must have the `g`\n\t * flag. Default {@link DEFAULT_MENTION_PATTERN}.\n\t */\n\tpattern?: RegExp;\n\t/**\n\t * Resolve a matched username to the actor id to record + notify.\n\t * Returning `undefined` skips that match (unknown user). Default is\n\t * identity (`username -> username`), useful for tests and for apps\n\t * where usernames already double as actor ids.\n\t */\n\tresolveActorId?: (\n\t\tusername: string,\n\t\tctx: Ctx\n\t) => Promise<string | undefined> | string | undefined;\n\t/**\n\t * How many characters around each match to keep in `snippet`.\n\t * Default 30 each side; the snippet is clamped to the body length.\n\t */\n\tsnippetRadius?: number;\n\t/**\n\t * Composition seam — fired once per mention after its row is\n\t * written. The host's typical implementation calls\n\t * `engine.runMutation('notifications:notify', …)` here. Errors are\n\t * caught + logged; one failing hook does not roll back the recorded\n\t * mention row.\n\t */\n\tonMention?: (\n\t\targs: OnMentionArgs,\n\t\tctx: Ctx,\n\t\tactions: MutationActions\n\t) => void | Promise<void>;\n\t/** Custom store. Default per-instance in-memory. */\n\tstore?: MentionsStore;\n\t/** Wall-clock. Default `Date.now`. */\n\tnow?: () => number;\n};\n\nconst buildSnippet = (body: string, matchIndex: number, radius: number): string => {\n\tconst start = Math.max(0, matchIndex - radius);\n\tconst end = Math.min(body.length, matchIndex + radius);\n\tconst prefix = start > 0 ? '…' : '';\n\tconst suffix = end < body.length ? '…' : '';\n\treturn `${prefix}${body.slice(start, end)}${suffix}`;\n};\n\n/** Extract distinct `@username` matches from a body. Public so tests can lean on it. */\nexport const parseMentions = (\n\tbody: string,\n\tpattern: RegExp = DEFAULT_MENTION_PATTERN\n): { username: string; index: number }[] => {\n\tconst seen = new Set<string>();\n\tconst matches: { username: string; index: number }[] = [];\n\tpattern.lastIndex = 0;\n\tlet match: RegExpExecArray | null = pattern.exec(body);\n\twhile (match !== null) {\n\t\tconst username = match[1];\n\t\tif (username !== undefined && !seen.has(username)) {\n\t\t\tseen.add(username);\n\t\t\tmatches.push({ index: match.index, username });\n\t\t}\n\t\tif (!pattern.global) break;\n\t\tmatch = pattern.exec(body);\n\t}\n\tpattern.lastIndex = 0;\n\treturn matches;\n};\n\nconst rowId = (\n\tsourceKind: string,\n\tsourceId: string,\n\tmentionedActorId: string\n): string => `${sourceKind}:${sourceId}:${mentionedActorId}`;\n\n/**\n * Build a {@link SyncPack} that parses `@mentions` from a posted body\n * and writes one row per mentioned actor. Composes with other packs\n * (notifications, digest, …) via the `onMention` hook — the pack itself\n * never calls another pack's mutations.\n */\nexport const createMentionsPack = <Ctx = CollectionContext>(\n\tconfig: MentionsPackConfig<Ctx> = {}\n): SyncPack => {\n\tconst prefix = config.prefix ?? '';\n\tconst table = `${prefix}mentions`;\n\tconst collectionName = table;\n\tconst recordMutationName = `${prefix}mentions:record`;\n\tconst resolveMutationName = `${prefix}mentions:resolve`;\n\tconst dismissMutationName = `${prefix}mentions:dismiss`;\n\tconst store = config.store ?? createInMemoryMentionsStore();\n\tconst now = config.now ?? Date.now;\n\tconst pattern = config.pattern ?? DEFAULT_MENTION_PATTERN;\n\tconst snippetRadius = config.snippetRadius ?? DEFAULT_SNIPPET_RADIUS;\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 resolveActorId = (config.resolveActorId ??\n\t\t((username: string) => username)) as (\n\t\tusername: string,\n\t\tctx: CollectionContext\n\t) => Promise<string | undefined> | string | undefined;\n\tconst onMention = config.onMention as\n\t\t| ((\n\t\t\t\targs: OnMentionArgs,\n\t\t\t\tctx: CollectionContext,\n\t\t\t\tactions: MutationActions\n\t\t ) => void | Promise<void>)\n\t\t| undefined;\n\n\ttype Params = { sourceKind?: string; unresolvedOnly?: boolean };\n\n\treturn defineSyncPack({\n\t\tname: '@absolutejs/sync-pack-mentions',\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\tsourceKind: field.string,\n\t\t\t\t\tsourceId: field.string,\n\t\t\t\t\tauthorId: (value) =>\n\t\t\t\t\t\tvalue === null || typeof value === 'string',\n\t\t\t\t\tmentionedActorId: field.string,\n\t\t\t\t\tusername: field.string,\n\t\t\t\t\tsnippet: field.string,\n\t\t\t\t\tcreatedAt: field.number,\n\t\t\t\t\tresolvedAt: (value) =>\n\t\t\t\t\t\tvalue === null || typeof value === 'number'\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\tread: (ctx: unknown, row: MentionRow) => {\n\t\t\t\t\tconst callerId = getActorId(ctx as CollectionContext);\n\t\t\t\t\treturn callerId !== undefined && row.mentionedActorId === callerId;\n\t\t\t\t},\n\t\t\t\t// `mentions:record` is host-trusted (the record mutation\n\t\t\t\t// parses + inserts on behalf of the author). Allow any\n\t\t\t\t// insert; the recorded row's mentionedActorId comes from\n\t\t\t\t// the resolveActorId callback the host configured.\n\t\t\t\tinsert: () => true,\n\t\t\t\tupdate: (ctx: unknown, row: MentionRow) => {\n\t\t\t\t\tconst callerId = getActorId(ctx as CollectionContext);\n\t\t\t\t\treturn callerId !== undefined && row.mentionedActorId === callerId;\n\t\t\t\t},\n\t\t\t\tdelete: (ctx: unknown, row: MentionRow) => {\n\t\t\t\t\tconst callerId = getActorId(ctx as CollectionContext);\n\t\t\t\t\tif (callerId === undefined) return false;\n\t\t\t\t\tconst ownerId =\n\t\t\t\t\t\t(row as { mentionedActorId?: string }).mentionedActorId ??\n\t\t\t\t\t\t(() => {\n\t\t\t\t\t\t\tconst id = (row as { id?: string }).id;\n\t\t\t\t\t\t\treturn id === undefined\n\t\t\t\t\t\t\t\t? undefined\n\t\t\t\t\t\t\t\t: store.getById(id)?.mentionedActorId;\n\t\t\t\t\t\t})();\n\t\t\t\t\treturn ownerId === callerId;\n\t\t\t\t}\n\t\t\t}\n\t\t},\n\n\t\tcollections: [\n\t\t\tdefineCollection<MentionRow, Params, 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 = getActorId(ctx);\n\t\t\t\t\tif (callerId === undefined) return [];\n\t\t\t\t\treturn (store.reader.all(ctx) as MentionRow[]).filter(\n\t\t\t\t\t\t(row) =>\n\t\t\t\t\t\t\trow.mentionedActorId === callerId &&\n\t\t\t\t\t\t\t(params.sourceKind === undefined ||\n\t\t\t\t\t\t\t\trow.sourceKind === params.sourceKind) &&\n\t\t\t\t\t\t\t(params.unresolvedOnly !== true ||\n\t\t\t\t\t\t\t\trow.resolvedAt === null)\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 = getActorId(ctx);\n\t\t\t\t\tif (\n\t\t\t\t\t\tcallerId === undefined ||\n\t\t\t\t\t\trow.mentionedActorId !== callerId\n\t\t\t\t\t) {\n\t\t\t\t\t\treturn false;\n\t\t\t\t\t}\n\t\t\t\t\treturn (\n\t\t\t\t\t\t(params.sourceKind === undefined ||\n\t\t\t\t\t\t\trow.sourceKind === params.sourceKind) &&\n\t\t\t\t\t\t(params.unresolvedOnly !== true || row.resolvedAt === null)\n\t\t\t\t\t);\n\t\t\t\t},\n\t\t\t\tauthorize: (_params, ctx) =>\n\t\t\t\t\tgetActorId(ctx as CollectionContext) !== undefined\n\t\t\t})\n\t\t],\n\n\t\tmutations: [\n\t\t\t// mentions:record { sourceKind, sourceId, body, authorId? } —\n\t\t\t// host calls this RIGHT AFTER its own post mutation succeeds.\n\t\t\t// The pack parses `@usernames`, resolves them to actor ids,\n\t\t\t// writes one mention row per actor (idempotent on re-record),\n\t\t\t// and fires `onMention` per row so the host can compose with\n\t\t\t// other packs (notifications, email, …).\n\t\t\tdefineMutation<\n\t\t\t\t{\n\t\t\t\t\tsourceKind: string;\n\t\t\t\t\tsourceId: string;\n\t\t\t\t\tbody: string;\n\t\t\t\t\tauthorId?: string | null;\n\t\t\t\t},\n\t\t\t\tCollectionContext,\n\t\t\t\tMentionRow[]\n\t\t\t>({\n\t\t\t\tname: recordMutationName,\n\t\t\t\thandler: async (args, ctx, actions) => {\n\t\t\t\t\tconst authorIdInput =\n\t\t\t\t\t\targs.authorId === undefined\n\t\t\t\t\t\t\t? getActorId(ctx as CollectionContext) ?? null\n\t\t\t\t\t\t\t: args.authorId;\n\t\t\t\t\tconst matches = parseMentions(args.body, pattern);\n\t\t\t\t\tconst written: MentionRow[] = [];\n\t\t\t\t\tfor (const { username, index } of matches) {\n\t\t\t\t\t\tconst resolved = await resolveActorId(\n\t\t\t\t\t\t\tusername,\n\t\t\t\t\t\t\tctx as CollectionContext\n\t\t\t\t\t\t);\n\t\t\t\t\t\tif (resolved === undefined) continue;\n\t\t\t\t\t\tif (\n\t\t\t\t\t\t\tauthorIdInput !== null &&\n\t\t\t\t\t\t\tresolved === authorIdInput\n\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\tconst id = rowId(\n\t\t\t\t\t\t\targs.sourceKind,\n\t\t\t\t\t\t\targs.sourceId,\n\t\t\t\t\t\t\tresolved\n\t\t\t\t\t\t);\n\t\t\t\t\t\tconst existing = store.getById(id);\n\t\t\t\t\t\tif (existing !== undefined) {\n\t\t\t\t\t\t\twritten.push(existing);\n\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tconst row: MentionRow = {\n\t\t\t\t\t\t\tauthorId: authorIdInput,\n\t\t\t\t\t\t\tcreatedAt: now(),\n\t\t\t\t\t\t\tid,\n\t\t\t\t\t\t\tmentionedActorId: resolved,\n\t\t\t\t\t\t\tresolvedAt: null,\n\t\t\t\t\t\t\tsnippet: buildSnippet(\n\t\t\t\t\t\t\t\targs.body,\n\t\t\t\t\t\t\t\tindex,\n\t\t\t\t\t\t\t\tsnippetRadius\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\tsourceId: args.sourceId,\n\t\t\t\t\t\t\tsourceKind: args.sourceKind,\n\t\t\t\t\t\t\tusername\n\t\t\t\t\t\t};\n\t\t\t\t\t\tconst inserted = (await actions.insert(\n\t\t\t\t\t\t\ttable,\n\t\t\t\t\t\t\trow\n\t\t\t\t\t\t)) as MentionRow;\n\t\t\t\t\t\twritten.push(inserted);\n\t\t\t\t\t\tif (onMention !== undefined) {\n\t\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\t\tawait onMention(\n\t\t\t\t\t\t\t\t\t{ body: args.body, mention: inserted },\n\t\t\t\t\t\t\t\t\tctx as CollectionContext,\n\t\t\t\t\t\t\t\t\tactions as MutationActions\n\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t} catch (error) {\n\t\t\t\t\t\t\t\tconsole.error(\n\t\t\t\t\t\t\t\t\t'[sync-pack-mentions] onMention hook threw:',\n\t\t\t\t\t\t\t\t\terror\n\t\t\t\t\t\t\t\t);\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\treturn written;\n\t\t\t\t}\n\t\t\t}),\n\t\t\tdefineMutation<\n\t\t\t\t{ id: string },\n\t\t\t\tCollectionContext,\n\t\t\t\tMentionRow | undefined\n\t\t\t>({\n\t\t\t\tname: resolveMutationName,\n\t\t\t\thandler: async (args, ctx, actions) => {\n\t\t\t\t\tconst callerId = getActorId(ctx as CollectionContext);\n\t\t\t\t\tif (callerId === undefined) {\n\t\t\t\t\t\tthrow new UnauthorizedError(\n\t\t\t\t\t\t\t'mentions:resolve (no actor id)'\n\t\t\t\t\t\t);\n\t\t\t\t\t}\n\t\t\t\t\tconst existing = store.getById(args.id);\n\t\t\t\t\tif (existing === undefined) return undefined;\n\t\t\t\t\tif (existing.mentionedActorId !== callerId) {\n\t\t\t\t\t\tthrow new UnauthorizedError(\n\t\t\t\t\t\t\t'mentions:resolve (not the mentioned actor)'\n\t\t\t\t\t\t);\n\t\t\t\t\t}\n\t\t\t\t\tif (existing.resolvedAt !== null) return existing;\n\t\t\t\t\treturn (await actions.update(table, {\n\t\t\t\t\t\t...existing,\n\t\t\t\t\t\tresolvedAt: now()\n\t\t\t\t\t})) as MentionRow;\n\t\t\t\t}\n\t\t\t}),\n\t\t\tdefineMutation<{ id: string }, CollectionContext, void>({\n\t\t\t\tname: dismissMutationName,\n\t\t\t\thandler: async (args, ctx, actions) => {\n\t\t\t\t\tconst callerId = getActorId(ctx as CollectionContext);\n\t\t\t\t\tif (callerId === undefined) {\n\t\t\t\t\t\tthrow new UnauthorizedError(\n\t\t\t\t\t\t\t'mentions:dismiss (no actor id)'\n\t\t\t\t\t\t);\n\t\t\t\t\t}\n\t\t\t\t\tconst existing = store.getById(args.id);\n\t\t\t\t\tif (existing === undefined) return;\n\t\t\t\t\tif (existing.mentionedActorId !== callerId) {\n\t\t\t\t\t\tthrow new UnauthorizedError(\n\t\t\t\t\t\t\t'mentions:dismiss (not the mentioned actor)'\n\t\t\t\t\t\t);\n\t\t\t\t\t}\n\t\t\t\t\tawait actions.delete(table, { id: args.id });\n\t\t\t\t}\n\t\t\t})\n\t\t]\n\t});\n};\n"
6
+ ],
7
+ "mappings": "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",
8
+ "debugId": "B6339E4895E450BF64756E2164756E21",
9
+ "names": []
10
+ }
package/package.json ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "@absolutejs/sync-pack-mentions",
3
+ "version": "0.1.0",
4
+ "description": "@mention parser pack for @absolutejs/sync — extracts @usernames from a body, writes per-actor mention rows, fires an onMention hook that composes with the notifications pack",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "https://github.com/absolutejs/sync-packs.git",
8
+ "directory": "mentions"
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
+ "mentions",
24
+ "notifications",
25
+ "composition"
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.2"
36
+ },
37
+ "devDependencies": {
38
+ "@absolutejs/sync": "^1.9.2",
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
+ }