Skip to content

Commit 26a33ba

Browse files
authored
batch recordToken (#78)
add recordTokenBatch
1 parent 3e1cfda commit 26a33ba

5 files changed

Lines changed: 87 additions & 198 deletions

File tree

CLAUDE.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
11
# Claude Development Notes
22

3+
## Initialization
4+
5+
Set up the repo with `npm i && npm run build && npx convex init`
6+
37
## Codegen Command
48

59
After making changes to component functions, run codegen with:
610

711
```bash
8-
cd example/ && npm run dev -- --once
12+
npm run build:codegen && npx convex dev --once
913
```
1014

1115
## Adding New Component Functions Pattern

example/convex/_generated/api.d.ts

Lines changed: 1 addition & 193 deletions
Original file line numberDiff line numberDiff line change
@@ -47,197 +47,5 @@ export declare const internal: FilterApi<
4747
>;
4848

4949
export declare const components: {
50-
pushNotifications: {
51-
public: {
52-
deleteNotificationsForUser: FunctionReference<
53-
"mutation",
54-
"internal",
55-
{ logLevel: "DEBUG" | "INFO" | "WARN" | "ERROR"; userId: string },
56-
null
57-
>;
58-
getNotification: FunctionReference<
59-
"query",
60-
"internal",
61-
{ id: string; logLevel: "DEBUG" | "INFO" | "WARN" | "ERROR" },
62-
null | {
63-
_contentAvailable?: boolean;
64-
_creationTime: number;
65-
badge?: number;
66-
body?: string;
67-
categoryId?: string;
68-
channelId?: string;
69-
data?: any;
70-
expiration?: number;
71-
interruptionLevel?:
72-
| "active"
73-
| "critical"
74-
| "passive"
75-
| "time-sensitive";
76-
mutableContent?: boolean;
77-
numPreviousFailures: number;
78-
priority?: "default" | "normal" | "high";
79-
sound?: string | null;
80-
state:
81-
| "awaiting_delivery"
82-
| "in_progress"
83-
| "delivered"
84-
| "needs_retry"
85-
| "failed"
86-
| "maybe_delivered"
87-
| "unable_to_deliver";
88-
subtitle?: string;
89-
title?: string;
90-
ttl?: number;
91-
}
92-
>;
93-
getNotificationsForUser: FunctionReference<
94-
"query",
95-
"internal",
96-
{
97-
limit?: number;
98-
logLevel: "DEBUG" | "INFO" | "WARN" | "ERROR";
99-
userId: string;
100-
},
101-
Array<{
102-
_contentAvailable?: boolean;
103-
_creationTime: number;
104-
badge?: number;
105-
body?: string;
106-
categoryId?: string;
107-
channelId?: string;
108-
data?: any;
109-
expiration?: number;
110-
id: string;
111-
interruptionLevel?:
112-
| "active"
113-
| "critical"
114-
| "passive"
115-
| "time-sensitive";
116-
mutableContent?: boolean;
117-
numPreviousFailures: number;
118-
priority?: "default" | "normal" | "high";
119-
sound?: string | null;
120-
state:
121-
| "awaiting_delivery"
122-
| "in_progress"
123-
| "delivered"
124-
| "needs_retry"
125-
| "failed"
126-
| "maybe_delivered"
127-
| "unable_to_deliver";
128-
subtitle?: string;
129-
title?: string;
130-
ttl?: number;
131-
}>
132-
>;
133-
getStatusForUser: FunctionReference<
134-
"query",
135-
"internal",
136-
{ logLevel: "DEBUG" | "INFO" | "WARN" | "ERROR"; userId: string },
137-
{ hasToken: boolean; paused: boolean }
138-
>;
139-
pauseNotificationsForUser: FunctionReference<
140-
"mutation",
141-
"internal",
142-
{ logLevel: "DEBUG" | "INFO" | "WARN" | "ERROR"; userId: string },
143-
null
144-
>;
145-
recordPushNotificationToken: FunctionReference<
146-
"mutation",
147-
"internal",
148-
{
149-
logLevel: "DEBUG" | "INFO" | "WARN" | "ERROR";
150-
pushToken: string;
151-
userId: string;
152-
},
153-
null
154-
>;
155-
removePushNotificationToken: FunctionReference<
156-
"mutation",
157-
"internal",
158-
{ logLevel: "DEBUG" | "INFO" | "WARN" | "ERROR"; userId: string },
159-
null
160-
>;
161-
restart: FunctionReference<
162-
"mutation",
163-
"internal",
164-
{ logLevel: "DEBUG" | "INFO" | "WARN" | "ERROR" },
165-
boolean
166-
>;
167-
sendPushNotification: FunctionReference<
168-
"mutation",
169-
"internal",
170-
{
171-
allowUnregisteredTokens?: boolean;
172-
logLevel: "DEBUG" | "INFO" | "WARN" | "ERROR";
173-
notification: {
174-
_contentAvailable?: boolean;
175-
badge?: number;
176-
body?: string;
177-
categoryId?: string;
178-
channelId?: string;
179-
data?: any;
180-
expiration?: number;
181-
interruptionLevel?:
182-
| "active"
183-
| "critical"
184-
| "passive"
185-
| "time-sensitive";
186-
mutableContent?: boolean;
187-
priority?: "default" | "normal" | "high";
188-
sound?: string | null;
189-
subtitle?: string;
190-
title?: string;
191-
ttl?: number;
192-
};
193-
userId: string;
194-
},
195-
string | null
196-
>;
197-
sendPushNotificationBatch: FunctionReference<
198-
"mutation",
199-
"internal",
200-
{
201-
allowUnregisteredTokens?: boolean;
202-
logLevel: "DEBUG" | "INFO" | "WARN" | "ERROR";
203-
notifications: Array<{
204-
notification: {
205-
_contentAvailable?: boolean;
206-
badge?: number;
207-
body?: string;
208-
categoryId?: string;
209-
channelId?: string;
210-
data?: any;
211-
expiration?: number;
212-
interruptionLevel?:
213-
| "active"
214-
| "critical"
215-
| "passive"
216-
| "time-sensitive";
217-
mutableContent?: boolean;
218-
priority?: "default" | "normal" | "high";
219-
sound?: string | null;
220-
subtitle?: string;
221-
title?: string;
222-
ttl?: number;
223-
};
224-
userId: string;
225-
}>;
226-
},
227-
Array<string | null>
228-
>;
229-
shutdown: FunctionReference<
230-
"mutation",
231-
"internal",
232-
{ logLevel: "DEBUG" | "INFO" | "WARN" | "ERROR" },
233-
{ data?: any; message: string }
234-
>;
235-
unpauseNotificationsForUser: FunctionReference<
236-
"mutation",
237-
"internal",
238-
{ logLevel: "DEBUG" | "INFO" | "WARN" | "ERROR"; userId: string },
239-
null
240-
>;
241-
};
242-
};
50+
pushNotifications: import("@convex-dev/expo-push-notifications/_generated/component.js").ComponentApi<"pushNotifications">;
24351
};

src/client/index.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import type { NotificationFields } from "../component/schema.js";
88
import type { LogLevel } from "../logging/index.js";
99
import type { ComponentApi } from "../component/_generated/component.js";
1010

11+
const RECORD_TOKEN_BATCH_SIZE = 1000;
12+
1113
/**
1214
* This component uses Expo's push notification API
1315
* (https://docs.expo.dev/push-notifications/overview/)
@@ -50,6 +52,37 @@ export class PushNotifications<UserType extends string = GenericId<"users">> {
5052
});
5153
}
5254

55+
/**
56+
* Records push notification tokens for many users at once.
57+
*
58+
* The list is broken into batches of {@link RECORD_TOKEN_BATCH_SIZE} users
59+
* and each batch is sent in its own mutation. Call this from an action so
60+
* each batch lands in its own transaction; calling from a mutation will
61+
* still process every batch in a single transaction.
62+
*
63+
* Each token is upserted: if a token already exists for a user it is
64+
* overwritten, otherwise a new one is inserted (matching
65+
* {@link recordToken}'s behavior).
66+
*
67+
* @param tokens List of `{ userId, pushToken }` pairs to record.
68+
*/
69+
async recordTokenBatch(
70+
ctx: RunMutationCtx,
71+
tokens: Array<{ userId: UserType; pushToken: string }>,
72+
): Promise<null> {
73+
for (let i = 0; i < tokens.length; i += RECORD_TOKEN_BATCH_SIZE) {
74+
const batch = tokens.slice(i, i + RECORD_TOKEN_BATCH_SIZE);
75+
await ctx.runMutation(
76+
this.component.public.recordPushNotificationTokenBatch,
77+
{
78+
tokens: batch,
79+
logLevel: this.config.logLevel,
80+
},
81+
);
82+
}
83+
return null;
84+
}
85+
5386
/**
5487
* This removes the push notification token for a user if it exists.
5588
*

src/component/_generated/component.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,16 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
133133
null,
134134
Name
135135
>;
136+
recordPushNotificationTokenBatch: FunctionReference<
137+
"mutation",
138+
"internal",
139+
{
140+
logLevel: "DEBUG" | "INFO" | "WARN" | "ERROR";
141+
tokens: Array<{ pushToken: string; userId: string }>;
142+
},
143+
null,
144+
Name
145+
>;
136146
removePushNotificationToken: FunctionReference<
137147
"mutation",
138148
"internal",

src/component/public.ts

Lines changed: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,6 @@
11
import { ConvexError, v, type Infer } from "convex/values";
22
import { mutation, query, type MutationCtx } from "./functions.js";
3-
import {
4-
BASE_BATCH_DELAY,
5-
getFutureSegment,
6-
} from "./shared.js";
3+
import { BASE_BATCH_DELAY, getFutureSegment } from "./shared.js";
74
import {
85
notificationFields,
96
notificationState,
@@ -40,6 +37,43 @@ export const recordPushNotificationToken = mutation({
4037
},
4138
});
4239

40+
export const recordPushNotificationTokenBatch = mutation({
41+
args: {
42+
tokens: v.array(
43+
v.object({
44+
userId: v.string(),
45+
pushToken: v.string(),
46+
}),
47+
),
48+
},
49+
returns: v.null(),
50+
handler: async (ctx, { tokens }) => {
51+
await Promise.all(
52+
tokens.map(async ({ userId, pushToken }) => {
53+
if (pushToken === "") {
54+
ctx.logger.debug(`Push token is empty for user ${userId}, skipping`);
55+
return;
56+
}
57+
const existingToken = await ctx.db
58+
.query("pushTokens")
59+
.withIndex("userId", (q) => q.eq("userId", userId))
60+
.unique();
61+
if (existingToken !== null) {
62+
ctx.logger.debug(
63+
`Push token already exists for user ${userId}, updating token`,
64+
);
65+
await ctx.db.patch("pushTokens", existingToken._id, {
66+
token: pushToken,
67+
});
68+
return;
69+
}
70+
await ctx.db.insert("pushTokens", { userId, token: pushToken });
71+
}),
72+
);
73+
return null;
74+
},
75+
});
76+
4377
export const removePushNotificationToken = mutation({
4478
args: {
4579
userId: v.string(),

0 commit comments

Comments
 (0)