@rocapine/community 0.2.0 → 0.3.2
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 +5 -0
- package/lib/commands/init.js +4 -1
- package/lib/commands/upgrade.d.ts +4 -0
- package/lib/commands/upgrade.js +34 -9
- package/lib/index.js +2 -0
- package/lib/install-shared.d.ts +9 -5
- package/lib/install-shared.js +19 -5
- package/package.json +1 -1
- package/templates/functions/_shared/client.ts +25 -0
- package/templates/functions/_shared/push.ts +19 -9
- package/templates/functions/_shared/translation.ts +513 -0
- package/templates/functions/broadcast-post/index.ts +48 -13
- package/templates/functions/daily-moderation/index.ts +138 -37
- package/templates/functions/daily-translation/index.ts +120 -0
- package/templates/functions/deno.json +1 -0
- package/templates/functions/notify-comment/index.ts +16 -2
- package/templates/functions/translate-one/index.ts +20 -0
- package/templates/migrations/core/007_username_moderation.sql +44 -0
- package/templates/migrations/polls/001_polls.sql +24 -0
- package/templates/migrations/translation/001_translations.sql +190 -0
package/README.md
CHANGED
|
@@ -61,6 +61,11 @@ warning** — review the diff before deploying. `--project-url`/`--anon-key`
|
|
|
61
61
|
are only needed if a newly-added migration carries a placeholder. Fails if
|
|
62
62
|
`community-sdk.json` doesn't exist yet (run `init` or `adopt` first).
|
|
63
63
|
|
|
64
|
+
`--add-modules <modules>` installs one or more modules not yet in
|
|
65
|
+
`community-sdk.json` on top of the existing backend (comma-separated, same
|
|
66
|
+
parsing as `init --modules`) — e.g. `upgrade --add-modules translation` to
|
|
67
|
+
add the translation module to an already-initialized app.
|
|
68
|
+
|
|
64
69
|
## `adopt`
|
|
65
70
|
|
|
66
71
|
```bash
|
package/lib/commands/init.js
CHANGED
|
@@ -110,7 +110,7 @@ async function runInit(options = {}) {
|
|
|
110
110
|
if (!fs.existsSync(srcDir))
|
|
111
111
|
continue;
|
|
112
112
|
const destDir = path.join(functionsDestRoot, fnName);
|
|
113
|
-
fs.cpSync(srcDir, destDir, { recursive: true });
|
|
113
|
+
fs.cpSync(srcDir, destDir, { recursive: true, filter: install_shared_1.isShippedFunctionFile });
|
|
114
114
|
writtenPaths.push(...(0, install_shared_1.listFilesRecursive)(destDir));
|
|
115
115
|
}
|
|
116
116
|
}
|
|
@@ -148,5 +148,8 @@ function printNextSteps(log, modules) {
|
|
|
148
148
|
if (modules.includes("push")) {
|
|
149
149
|
log(" push module: EXPO_ACCESS_TOKEN (only if your Expo project enforces Enhanced Security for push); COMMUNITY_PUSH_COPY (optional per-locale copy)");
|
|
150
150
|
}
|
|
151
|
+
if (modules.includes("translation")) {
|
|
152
|
+
log(' translation module: COMMUNITY_TRANSLATION_LOCALES="en,es-419,..." (required, same list as modules.translation.locales in the app); optional: COMMUNITY_TRANSLATION_MODEL, COMMUNITY_TRANSLATION_STYLE');
|
|
153
|
+
}
|
|
151
154
|
log(" 3. Deploy the Edge Functions: supabase functions deploy");
|
|
152
155
|
}
|
|
@@ -9,6 +9,8 @@ export interface UpgradeOptions {
|
|
|
9
9
|
cwd?: string;
|
|
10
10
|
/** Source of the migrations/ + functions/ trees. Defaults to the shipped templates. */
|
|
11
11
|
templatesDir?: string;
|
|
12
|
+
/** Module names to install on top of what's already in the manifest — validated, ordered and merged via resolveModules, same rules as `init --modules` (unknown names throw, dependency gaps warn). */
|
|
13
|
+
addModules?: string[];
|
|
12
14
|
/** Used to ask for projectUrl/anonKey when a newly copied migration needs them. */
|
|
13
15
|
prompt?: (question: string) => Promise<string>;
|
|
14
16
|
/** Called for non-fatal warnings — most importantly, functions overwritten with new content. */
|
|
@@ -23,6 +25,8 @@ export interface UpgradeResult {
|
|
|
23
25
|
dir: string;
|
|
24
26
|
/** True when nothing needed to change — no new migrations, no function drift. */
|
|
25
27
|
upToDate: boolean;
|
|
28
|
+
/** Modules newly added by `addModules` (not already in the manifest). Empty when `addModules` was omitted or every named module was already installed. */
|
|
29
|
+
addedModules: string[];
|
|
26
30
|
/** Relative (to `cwd`) paths of newly copied migration files, in copy order. */
|
|
27
31
|
addedMigrations: string[];
|
|
28
32
|
/** Function names copied because they weren't present on disk yet. */
|
package/lib/commands/upgrade.js
CHANGED
|
@@ -54,10 +54,15 @@ async function runUpgrade(options = {}) {
|
|
|
54
54
|
const dir = (0, install_shared_1.resolveContainedDir)(cwd, dirRel);
|
|
55
55
|
const templatesDir = options.templatesDir ?? (0, install_shared_1.defaultTemplatesDir)();
|
|
56
56
|
const { migrationsSrcRoot, functionsSrcRoot } = (0, install_shared_1.resolveTemplateRoots)(templatesDir);
|
|
57
|
-
//
|
|
58
|
-
//
|
|
59
|
-
//
|
|
60
|
-
|
|
57
|
+
// By default only modules the app already has installed are diffed/re-synced.
|
|
58
|
+
// `addModules` extends that list through the same validation/ordering/warning
|
|
59
|
+
// rules `init --modules` uses; left empty/undefined, behaviour here must stay
|
|
60
|
+
// byte-identical to the pre-`addModules` path (no resolveModules call, no
|
|
61
|
+
// spurious warnings for e.g. an existing inbox-without-reaction install).
|
|
62
|
+
const canonicalModules = options.addModules && options.addModules.length > 0
|
|
63
|
+
? (0, install_shared_1.resolveModules)([...manifest.modules, ...options.addModules], onWarn)
|
|
64
|
+
: install_shared_1.MODULE_ORDER.filter((m) => manifest.modules.includes(m));
|
|
65
|
+
const addedModules = canonicalModules.filter((m) => !manifest.modules.includes(m));
|
|
61
66
|
const installedTemplateIds = new Set(manifest.installedTemplates ?? reconstructInstalledTemplateIds(manifest.installedFiles));
|
|
62
67
|
const pending = [];
|
|
63
68
|
for (const moduleName of canonicalModules) {
|
|
@@ -101,6 +106,7 @@ async function runUpgrade(options = {}) {
|
|
|
101
106
|
manifest,
|
|
102
107
|
dir,
|
|
103
108
|
upToDate: true,
|
|
109
|
+
addedModules,
|
|
104
110
|
addedMigrations: [],
|
|
105
111
|
newFunctions: [],
|
|
106
112
|
overwrittenFunctions: [],
|
|
@@ -162,7 +168,10 @@ async function runUpgrade(options = {}) {
|
|
|
162
168
|
}
|
|
163
169
|
for (const fnName of newFunctions) {
|
|
164
170
|
const destDir = path.join(functionsDestRoot, fnName);
|
|
165
|
-
fs.cpSync(path.join(functionsSrcRoot, fnName), destDir, {
|
|
171
|
+
fs.cpSync(path.join(functionsSrcRoot, fnName), destDir, {
|
|
172
|
+
recursive: true,
|
|
173
|
+
filter: install_shared_1.isShippedFunctionFile,
|
|
174
|
+
});
|
|
166
175
|
createdFunctionDirs.push(destDir);
|
|
167
176
|
}
|
|
168
177
|
for (const fnName of changedFunctions) {
|
|
@@ -171,7 +180,10 @@ async function runUpgrade(options = {}) {
|
|
|
171
180
|
fs.cpSync(destDir, backupDir, { recursive: true });
|
|
172
181
|
backups.push({ destDir, backupDir });
|
|
173
182
|
fs.rmSync(destDir, { recursive: true, force: true });
|
|
174
|
-
fs.cpSync(path.join(functionsSrcRoot, fnName), destDir, {
|
|
183
|
+
fs.cpSync(path.join(functionsSrcRoot, fnName), destDir, {
|
|
184
|
+
recursive: true,
|
|
185
|
+
filter: install_shared_1.isShippedFunctionFile,
|
|
186
|
+
});
|
|
175
187
|
}
|
|
176
188
|
}
|
|
177
189
|
catch (err) {
|
|
@@ -205,12 +217,15 @@ async function runUpgrade(options = {}) {
|
|
|
205
217
|
const updatedManifest = {
|
|
206
218
|
schemaVersion: manifest_1.CURRENT_SCHEMA_VERSION,
|
|
207
219
|
sdkVersion: (0, install_shared_1.readOwnPackageVersion)(),
|
|
208
|
-
modules
|
|
220
|
+
// Only --add-modules rewrites the module list; a plain upgrade keeps the
|
|
221
|
+
// manifest's own (order and any entry this CLI doesn't know).
|
|
222
|
+
modules: options.addModules && options.addModules.length > 0 ? canonicalModules : manifest.modules,
|
|
209
223
|
installedFiles,
|
|
210
224
|
installedTemplates: [...newInstalledTemplateIds].sort(),
|
|
211
225
|
};
|
|
212
226
|
(0, manifest_1.writeManifest)(cwd, updatedManifest);
|
|
213
227
|
printSummary(log, onWarn, {
|
|
228
|
+
addedModules,
|
|
214
229
|
addedMigrations,
|
|
215
230
|
newFunctions,
|
|
216
231
|
overwrittenFunctions: changedFunctions,
|
|
@@ -219,6 +234,7 @@ async function runUpgrade(options = {}) {
|
|
|
219
234
|
manifest: updatedManifest,
|
|
220
235
|
dir,
|
|
221
236
|
upToDate: false,
|
|
237
|
+
addedModules,
|
|
222
238
|
addedMigrations,
|
|
223
239
|
newFunctions,
|
|
224
240
|
overwrittenFunctions: changedFunctions,
|
|
@@ -242,8 +258,11 @@ function reconstructInstalledTemplateIds(installedFiles) {
|
|
|
242
258
|
return ids;
|
|
243
259
|
}
|
|
244
260
|
function functionDirDiffers(srcDir, destDir) {
|
|
245
|
-
const
|
|
246
|
-
|
|
261
|
+
const rel = (dir) => new Set((0, install_shared_1.listFilesRecursive)(dir)
|
|
262
|
+
.filter(install_shared_1.isShippedFunctionFile)
|
|
263
|
+
.map((f) => path.relative(dir, f)));
|
|
264
|
+
const srcRel = rel(srcDir);
|
|
265
|
+
const destRel = rel(destDir);
|
|
247
266
|
if (srcRel.size !== destRel.size)
|
|
248
267
|
return true;
|
|
249
268
|
for (const rel of srcRel) {
|
|
@@ -259,6 +278,9 @@ function functionDirDiffers(srcDir, destDir) {
|
|
|
259
278
|
function printSummary(log, onWarn, summary) {
|
|
260
279
|
log("");
|
|
261
280
|
log("community-sdk upgraded.");
|
|
281
|
+
if (summary.addedModules.length > 0) {
|
|
282
|
+
log(`Added module(s): ${summary.addedModules.join(", ")}`);
|
|
283
|
+
}
|
|
262
284
|
if (summary.addedMigrations.length > 0) {
|
|
263
285
|
log(`Added ${summary.addedMigrations.length} migration(s):`);
|
|
264
286
|
for (const f of summary.addedMigrations)
|
|
@@ -277,4 +299,7 @@ function printSummary(log, onWarn, summary) {
|
|
|
277
299
|
if (summary.newFunctions.length > 0 || summary.overwrittenFunctions.length > 0) {
|
|
278
300
|
log(" 2. Review the function changes, then: supabase functions deploy");
|
|
279
301
|
}
|
|
302
|
+
if (summary.addedModules.includes("translation")) {
|
|
303
|
+
log(' 3. translation module: COMMUNITY_TRANSLATION_LOCALES="en,es-419,..." (required, same list as modules.translation.locales in the app); optional: COMMUNITY_TRANSLATION_MODEL, COMMUNITY_TRANSLATION_STYLE');
|
|
304
|
+
}
|
|
280
305
|
}
|
package/lib/index.js
CHANGED
|
@@ -81,12 +81,14 @@ program
|
|
|
81
81
|
program
|
|
82
82
|
.command("upgrade")
|
|
83
83
|
.description("Copy any new migrations/Edge Functions shipped since this repo's community-sdk.json was last written, and overwrite changed function code (with a warning)")
|
|
84
|
+
.option("--add-modules <modules>", "comma-separated module list to install on top of what's already in community-sdk.json (e.g. translation)")
|
|
84
85
|
.option("--project-url <url>", "Supabase project URL, e.g. https://<ref>.supabase.co (only needed if a new migration carries a placeholder)")
|
|
85
86
|
.option("--anon-key <key>", "Supabase anon key (only needed if a new migration carries a placeholder)")
|
|
86
87
|
.option("--dir <dir>", "target Supabase directory, relative to the current directory", "supabase")
|
|
87
88
|
.action(async (opts) => {
|
|
88
89
|
try {
|
|
89
90
|
await (0, upgrade_1.runUpgrade)({
|
|
91
|
+
addModules: parseModulesFlag(opts.addModules),
|
|
90
92
|
projectUrl: opts.projectUrl,
|
|
91
93
|
anonKey: opts.anonKey,
|
|
92
94
|
dir: opts.dir,
|
package/lib/install-shared.d.ts
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Ruled install order (controller ruling, task 18): core -> push -> polls ->
|
|
3
|
-
* reaction -> inbox. Reaction MUST land before inbox —
|
|
4
|
-
* trigger is guarded with
|
|
5
|
-
* time and is never
|
|
6
|
-
*
|
|
3
|
+
* reaction -> inbox -> translation. Reaction MUST land before inbox —
|
|
4
|
+
* inbox/001's reaction trigger is guarded with
|
|
5
|
+
* `to_regclass('public.post_reactions')` at install time and is never
|
|
6
|
+
* retroactively wired if the reaction module shows up later, so `reaction`
|
|
7
|
+
* needs to already exist when `inbox` runs.
|
|
7
8
|
*
|
|
8
9
|
* Shared between `init` (task 18) and `upgrade`/`adopt` (task 19) — the
|
|
9
10
|
* single source of truth for module names, order, and the function sets
|
|
10
11
|
* each module pulls in.
|
|
11
12
|
*/
|
|
12
|
-
export declare const MODULE_ORDER: readonly ["core", "push", "polls", "reaction", "inbox"];
|
|
13
|
+
export declare const MODULE_ORDER: readonly ["core", "push", "polls", "reaction", "inbox", "translation"];
|
|
13
14
|
export type Module = (typeof MODULE_ORDER)[number];
|
|
14
15
|
export declare function functionsForModules(modules: readonly Module[]): string[];
|
|
15
16
|
/**
|
|
@@ -66,6 +67,9 @@ export declare function resolveProjectCredentials(options: {
|
|
|
66
67
|
}>;
|
|
67
68
|
export declare function formatTimestamp(date: Date): string;
|
|
68
69
|
export declare function addSeconds(date: Date, seconds: number): Date;
|
|
70
|
+
/** Deno test files (`*_test.ts`) stay in the repo: never copied into a host's
|
|
71
|
+
* functions, and ignored when diffing a host's function dir. */
|
|
72
|
+
export declare function isShippedFunctionFile(file: string): boolean;
|
|
69
73
|
export declare function listFilesRecursive(dir: string): string[];
|
|
70
74
|
export declare function toRelativePosix(base: string, target: string): string;
|
|
71
75
|
/** Resolves `dirRel` against `cwd`, throwing if it would escape the project root. */
|
package/lib/install-shared.js
CHANGED
|
@@ -46,6 +46,7 @@ exports.deriveProjectUrlFromConfig = deriveProjectUrlFromConfig;
|
|
|
46
46
|
exports.resolveProjectCredentials = resolveProjectCredentials;
|
|
47
47
|
exports.formatTimestamp = formatTimestamp;
|
|
48
48
|
exports.addSeconds = addSeconds;
|
|
49
|
+
exports.isShippedFunctionFile = isShippedFunctionFile;
|
|
49
50
|
exports.listFilesRecursive = listFilesRecursive;
|
|
50
51
|
exports.toRelativePosix = toRelativePosix;
|
|
51
52
|
exports.resolveContainedDir = resolveContainedDir;
|
|
@@ -54,16 +55,17 @@ const path = __importStar(require("node:path"));
|
|
|
54
55
|
const promises_1 = require("node:readline/promises");
|
|
55
56
|
/**
|
|
56
57
|
* Ruled install order (controller ruling, task 18): core -> push -> polls ->
|
|
57
|
-
* reaction -> inbox. Reaction MUST land before inbox —
|
|
58
|
-
* trigger is guarded with
|
|
59
|
-
* time and is never
|
|
60
|
-
*
|
|
58
|
+
* reaction -> inbox -> translation. Reaction MUST land before inbox —
|
|
59
|
+
* inbox/001's reaction trigger is guarded with
|
|
60
|
+
* `to_regclass('public.post_reactions')` at install time and is never
|
|
61
|
+
* retroactively wired if the reaction module shows up later, so `reaction`
|
|
62
|
+
* needs to already exist when `inbox` runs.
|
|
61
63
|
*
|
|
62
64
|
* Shared between `init` (task 18) and `upgrade`/`adopt` (task 19) — the
|
|
63
65
|
* single source of truth for module names, order, and the function sets
|
|
64
66
|
* each module pulls in.
|
|
65
67
|
*/
|
|
66
|
-
exports.MODULE_ORDER = ["core", "push", "polls", "reaction", "inbox"];
|
|
68
|
+
exports.MODULE_ORDER = ["core", "push", "polls", "reaction", "inbox", "translation"];
|
|
67
69
|
const ALWAYS_FUNCTIONS = [
|
|
68
70
|
"moderate-one",
|
|
69
71
|
"daily-moderation",
|
|
@@ -73,6 +75,7 @@ const ALWAYS_FUNCTIONS = [
|
|
|
73
75
|
];
|
|
74
76
|
const PUSH_FUNCTIONS = ["notify-like", "notify-comment", "broadcast-post"];
|
|
75
77
|
const REACTION_FUNCTIONS = ["notify-reaction"];
|
|
78
|
+
const TRANSLATION_FUNCTIONS = ["translate-one", "daily-translation"];
|
|
76
79
|
function functionsForModules(modules) {
|
|
77
80
|
const set = new Set(ALWAYS_FUNCTIONS);
|
|
78
81
|
if (modules.includes("push"))
|
|
@@ -81,6 +84,9 @@ function functionsForModules(modules) {
|
|
|
81
84
|
if (modules.includes("reaction"))
|
|
82
85
|
for (const f of REACTION_FUNCTIONS)
|
|
83
86
|
set.add(f);
|
|
87
|
+
if (modules.includes("translation"))
|
|
88
|
+
for (const f of TRANSLATION_FUNCTIONS)
|
|
89
|
+
set.add(f);
|
|
84
90
|
return [...set];
|
|
85
91
|
}
|
|
86
92
|
/**
|
|
@@ -102,6 +108,9 @@ function resolveModules(requested, onWarn) {
|
|
|
102
108
|
if (canonicalModules.includes("inbox") && !canonicalModules.includes("reaction")) {
|
|
103
109
|
onWarn("community-sdk: module 'inbox' selected without 'reaction' — reaction notifications won't appear in the inbox until the reaction module is also installed.");
|
|
104
110
|
}
|
|
111
|
+
if (canonicalModules.includes("translation") && !canonicalModules.includes("polls")) {
|
|
112
|
+
onWarn("community-sdk: module 'translation' selected without 'polls' — poll option labels won't be translated (posts and comments are).");
|
|
113
|
+
}
|
|
105
114
|
return canonicalModules;
|
|
106
115
|
}
|
|
107
116
|
/**
|
|
@@ -228,6 +237,11 @@ function formatTimestamp(date) {
|
|
|
228
237
|
function addSeconds(date, seconds) {
|
|
229
238
|
return new Date(date.getTime() + seconds * 1000);
|
|
230
239
|
}
|
|
240
|
+
/** Deno test files (`*_test.ts`) stay in the repo: never copied into a host's
|
|
241
|
+
* functions, and ignored when diffing a host's function dir. */
|
|
242
|
+
function isShippedFunctionFile(file) {
|
|
243
|
+
return !file.endsWith("_test.ts");
|
|
244
|
+
}
|
|
231
245
|
function listFilesRecursive(dir) {
|
|
232
246
|
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
233
247
|
const files = [];
|
package/package.json
CHANGED
|
@@ -11,6 +11,31 @@ export function adminClient() {
|
|
|
11
11
|
return createClient(SUPABASE_URL, Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!);
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
+
// Reads every row of a query page by page: PostgREST caps one response at the
|
|
15
|
+
// project's max_rows (Supabase default 1000), so a single select silently
|
|
16
|
+
// truncates. `page` must apply a deterministic order and `.range(from, to)`.
|
|
17
|
+
// Stops on the first short page. A failed page logs and returns what was read
|
|
18
|
+
// so far — for a broadcast, a partial send beats none (re-running would
|
|
19
|
+
// double-send to the pages already pushed).
|
|
20
|
+
export async function fetchAllRows<T>(
|
|
21
|
+
page: (
|
|
22
|
+
from: number,
|
|
23
|
+
to: number,
|
|
24
|
+
) => PromiseLike<{ data: T[] | null; error: { message: string } | null }>,
|
|
25
|
+
pageSize = 1000,
|
|
26
|
+
): Promise<T[]> {
|
|
27
|
+
const rows: T[] = [];
|
|
28
|
+
for (let from = 0; ; from += pageSize) {
|
|
29
|
+
const { data, error } = await page(from, from + pageSize - 1);
|
|
30
|
+
if (error) {
|
|
31
|
+
console.error(`fetchAllRows: page failed, keeping ${rows.length} rows`, error.message);
|
|
32
|
+
return rows;
|
|
33
|
+
}
|
|
34
|
+
rows.push(...(data ?? []));
|
|
35
|
+
if ((data?.length ?? 0) < pageSize) return rows;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
14
39
|
// The gateway (verify_jwt) has already validated the bearer's signature, so
|
|
15
40
|
// only the role claim needs checking here. An exact string match against the
|
|
16
41
|
// injected SUPABASE_SERVICE_ROLE_KEY broke when a project migrated to the new
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
// Shared Expo push sender. Used by notify-comment, notify-like,
|
|
2
2
|
// notify-reaction and broadcast-post (push module).
|
|
3
3
|
|
|
4
|
+
import { runPool } from "./translation.ts";
|
|
5
|
+
|
|
4
6
|
export interface ExpoPushMessage {
|
|
5
7
|
to: string;
|
|
6
8
|
title: string;
|
|
@@ -11,21 +13,29 @@ export interface ExpoPushMessage {
|
|
|
11
13
|
badge?: number;
|
|
12
14
|
}
|
|
13
15
|
|
|
14
|
-
// Sends in chunks of 100 (Expo API limit)
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
|
|
16
|
+
// Sends in chunks of 100 (Expo API limit), 4 requests in flight at a time, each
|
|
17
|
+
// aborted after 10 s so one hung request can't stall a large broadcast past the
|
|
18
|
+
// function's wall-clock limit. Best-effort: push failures must never fail the
|
|
19
|
+
// calling webhook. Projects with Enhanced Security for push must set the
|
|
20
|
+
// EXPO_ACCESS_TOKEN function secret. `fetchImpl`/`concurrency` are for tests.
|
|
21
|
+
export async function sendExpoPushBatch(
|
|
22
|
+
messages: ExpoPushMessage[],
|
|
23
|
+
opts: { fetchImpl?: typeof fetch; concurrency?: number } = {},
|
|
24
|
+
): Promise<void> {
|
|
25
|
+
const fetchImpl = opts.fetchImpl ?? fetch;
|
|
18
26
|
const headers: Record<string, string> = { "Content-Type": "application/json" };
|
|
19
27
|
const accessToken = Deno.env.get("EXPO_ACCESS_TOKEN");
|
|
20
28
|
if (accessToken) headers["Authorization"] = `Bearer ${accessToken}`;
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
29
|
+
const chunks: ExpoPushMessage[][] = [];
|
|
30
|
+
for (let i = 0; i < messages.length; i += 100) chunks.push(messages.slice(i, i + 100));
|
|
31
|
+
await runPool(chunks, opts.concurrency ?? 4, (chunk) =>
|
|
32
|
+
fetchImpl("https://exp.host/--/api/v2/push/send", {
|
|
24
33
|
method: "POST",
|
|
25
34
|
headers,
|
|
26
35
|
body: JSON.stringify(chunk.length === 1 ? chunk[0] : chunk),
|
|
27
|
-
|
|
28
|
-
|
|
36
|
+
signal: AbortSignal.timeout(10_000),
|
|
37
|
+
}).catch(() => {}),
|
|
38
|
+
);
|
|
29
39
|
}
|
|
30
40
|
|
|
31
41
|
export async function sendExpoPush(message: ExpoPushMessage): Promise<void> {
|