@hot-updater/firebase 1.0.0-rc.44 → 1.0.0-rc.45
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/dist/firebase/functions/index.cjs +255 -262
- package/dist/index.cjs +0 -13
- package/dist/index.d.cts +2 -1016
- package/dist/index.d.mts +2 -1016
- package/dist/index.mjs +1 -13
- package/dist/init/index.cjs +7 -14
- package/dist/init/index.mjs +7 -14
- package/package.json +7 -7
|
@@ -4566,7 +4566,7 @@ const createKvAdapter = ({ store, tablePrefix = "" }) => {
|
|
|
4566
4566
|
};
|
|
4567
4567
|
//#endregion
|
|
4568
4568
|
//#region ../../packages/server/dist/version.mjs
|
|
4569
|
-
const HOT_UPDATER_SERVER_VERSION = "1.0.0-rc.
|
|
4569
|
+
const HOT_UPDATER_SERVER_VERSION = "1.0.0-rc.45";
|
|
4570
4570
|
//#endregion
|
|
4571
4571
|
//#region ../../packages/server/dist/handlerVersionRoutes.mjs
|
|
4572
4572
|
/**
|
|
@@ -7179,254 +7179,6 @@ function createHotUpdater(options) {
|
|
|
7179
7179
|
};
|
|
7180
7180
|
}
|
|
7181
7181
|
//#endregion
|
|
7182
|
-
//#region src/firebaseInfrastructureNames.ts
|
|
7183
|
-
/**
|
|
7184
|
-
* The one collection the storage engine keeps every item in. It namespaces
|
|
7185
|
-
* the data, so item partitions are table names with no prefix.
|
|
7186
|
-
*/
|
|
7187
|
-
const FIREBASE_V1_COLLECTION = "hot_updater_v1";
|
|
7188
|
-
//#endregion
|
|
7189
|
-
//#region src/firestoreStore.ts
|
|
7190
|
-
/**
|
|
7191
|
-
* Firestore's limits: 10 MiB a request and 1 MiB a document; 500 writes a
|
|
7192
|
-
* transaction stays a conservative bound. Indexed values over 1,500 bytes are
|
|
7193
|
-
* truncated, so longer keys are refused rather than read inconsistently.
|
|
7194
|
-
*/
|
|
7195
|
-
const FIRESTORE_LIMITS = {
|
|
7196
|
-
items: 500,
|
|
7197
|
-
bytes: 1e7,
|
|
7198
|
-
itemBytes: 1e6,
|
|
7199
|
-
keyBytes: {
|
|
7200
|
-
pk: 1500,
|
|
7201
|
-
sk: 1500
|
|
7202
|
-
}
|
|
7203
|
-
};
|
|
7204
|
-
/** `getAll` reads at most this many documents a call. */
|
|
7205
|
-
const GET_BATCH = 300;
|
|
7206
|
-
/** A document's id: a hash of its key, since keys may hold `/` and outgrow Firestore's 1,500 bytes. */
|
|
7207
|
-
const firestoreDocumentId = ({ pk, sk }) => (0, node_crypto.createHash)("sha256").update(JSON.stringify([pk, sk])).digest("hex");
|
|
7208
|
-
/**
|
|
7209
|
-
* A document is `{ pk, sk, row }`. Row values that are maps or arrays become
|
|
7210
|
-
* JSON text inside a map, since Firestore has no nested arrays and does not
|
|
7211
|
-
* keep map key order; `row` is exempt from single-field indexing.
|
|
7212
|
-
*/
|
|
7213
|
-
const encode = (value) => value !== null && typeof value === "object" ? { json: JSON.stringify(value) } : value;
|
|
7214
|
-
const decode = (value) => value !== null && typeof value === "object" && "json" in value ? JSON.parse(String(value.json)) : value;
|
|
7215
|
-
/**
|
|
7216
|
-
* The collection's TTL field: a timestamp beside `pk` and `sk` on every item
|
|
7217
|
-
* of a row that expires, which Firestore's TTL policy deletes.
|
|
7218
|
-
*/
|
|
7219
|
-
const FIRESTORE_TTL_FIELD = "expireAt";
|
|
7220
|
-
const toDocument = (key, row, expiresAt) => ({
|
|
7221
|
-
pk: key.pk,
|
|
7222
|
-
sk: key.sk,
|
|
7223
|
-
row: Object.fromEntries(Object.entries(row).map(([name, value]) => [name, encode(value)])),
|
|
7224
|
-
...expiresAt === void 0 ? {} : { [FIRESTORE_TTL_FIELD]: firebase_admin_firestore.Timestamp.fromMillis(expiresAt) }
|
|
7225
|
-
});
|
|
7226
|
-
const toRow$1 = (data) => Object.fromEntries(Object.entries(data.row ?? {}).map(([name, value]) => [name, decode(value)]));
|
|
7227
|
-
const holds = (snapshot, condition) => {
|
|
7228
|
-
if (condition === void 0) return true;
|
|
7229
|
-
const data = snapshot?.exists ? snapshot.data() : void 0;
|
|
7230
|
-
return "v" in condition ? data !== void 0 && data.row?._v === condition.v : data !== void 0 === condition.exists;
|
|
7231
|
-
};
|
|
7232
|
-
/** gRPC codes a rerun of the whole write can clear: DEADLINE_EXCEEDED, RESOURCE_EXHAUSTED, ABORTED, INTERNAL, UNAVAILABLE. */
|
|
7233
|
-
const TRANSIENT = /* @__PURE__ */ new Set([
|
|
7234
|
-
4,
|
|
7235
|
-
8,
|
|
7236
|
-
10,
|
|
7237
|
-
13,
|
|
7238
|
-
14
|
|
7239
|
-
]);
|
|
7240
|
-
const codeOf = (error) => error.code;
|
|
7241
|
-
/** Firestore reports a missing database as NOT_FOUND; it reads as a missing schema, like SQL's 42P01. */
|
|
7242
|
-
const missingDatabase = (error) => {
|
|
7243
|
-
throw codeOf(error) === 5 ? Object.assign(new Error(error.message, { cause: error }), { code: "42P01" }) : error;
|
|
7244
|
-
};
|
|
7245
|
-
/**
|
|
7246
|
-
* The key-value store over one Firestore collection of `{ pk, sk, row }`
|
|
7247
|
-
* documents, queried through the composite indexes on (pk, sk) in both
|
|
7248
|
-
* directions. A write is one transaction that reads only the documents its
|
|
7249
|
-
* ops guard.
|
|
7250
|
-
*/
|
|
7251
|
-
const createFirestoreStore = ({ firestore, collection, nativePageSize }) => {
|
|
7252
|
-
const items = firestore.collection(collection);
|
|
7253
|
-
const reference = (key) => items.doc(firestoreDocumentId(key));
|
|
7254
|
-
return {
|
|
7255
|
-
id: "firestore",
|
|
7256
|
-
limits: FIRESTORE_LIMITS,
|
|
7257
|
-
async get(keys) {
|
|
7258
|
-
const found = /* @__PURE__ */ new Map();
|
|
7259
|
-
const ids = [...new Set(keys.map(firestoreDocumentId))];
|
|
7260
|
-
for (let at = 0; at < ids.length; at += GET_BATCH) {
|
|
7261
|
-
const snapshots = await firestore.getAll(...ids.slice(at, at + GET_BATCH).map((id) => items.doc(id))).catch(missingDatabase);
|
|
7262
|
-
for (const snapshot of snapshots) if (snapshot.exists) found.set(snapshot.id, toRow$1(snapshot.data()));
|
|
7263
|
-
}
|
|
7264
|
-
return keys.map((key) => found.get(firestoreDocumentId(key)) ?? null);
|
|
7265
|
-
},
|
|
7266
|
-
async query({ pk, gte, lt, order, limit, after }) {
|
|
7267
|
-
let query = items.where("pk", "==", pk);
|
|
7268
|
-
if (gte !== void 0) query = query.where("sk", ">=", gte);
|
|
7269
|
-
if (lt !== void 0) query = query.where("sk", "<", lt);
|
|
7270
|
-
query = query.orderBy("sk", order);
|
|
7271
|
-
if (after !== void 0) query = query.startAfter(after);
|
|
7272
|
-
const size = Math.min(limit, nativePageSize ?? limit);
|
|
7273
|
-
const { docs } = await query.limit(size).get().catch(missingDatabase);
|
|
7274
|
-
return {
|
|
7275
|
-
items: docs.map((document) => ({
|
|
7276
|
-
sk: document.get("sk"),
|
|
7277
|
-
value: toRow$1(document.data())
|
|
7278
|
-
})),
|
|
7279
|
-
more: docs.length === size
|
|
7280
|
-
};
|
|
7281
|
-
},
|
|
7282
|
-
async write(ops) {
|
|
7283
|
-
const blind = ops.map((op) => op.type === "add" && (op.condition === void 0 || "exists" in op.condition && op.condition.exists));
|
|
7284
|
-
const attempt = (reading) => firestore.runTransaction(async (transaction) => {
|
|
7285
|
-
const read = ops.flatMap((op, position) => (op.condition !== void 0 || op.type === "add") && (reading || !blind[position]) ? [position] : []);
|
|
7286
|
-
const snapshots = /* @__PURE__ */ new Map();
|
|
7287
|
-
if (read.length > 0) {
|
|
7288
|
-
const found = await transaction.getAll(...read.map((position) => reference(ops[position].key)));
|
|
7289
|
-
read.forEach((position, at) => snapshots.set(position, found[at]));
|
|
7290
|
-
}
|
|
7291
|
-
const failed = read.find((position) => !holds(snapshots.get(position), ops[position].condition));
|
|
7292
|
-
if (failed !== void 0) {
|
|
7293
|
-
if (!reading && blind.slice(0, failed).includes(true)) return null;
|
|
7294
|
-
return {
|
|
7295
|
-
ok: false,
|
|
7296
|
-
failedOp: failed
|
|
7297
|
-
};
|
|
7298
|
-
}
|
|
7299
|
-
for (const [position, op] of ops.entries()) {
|
|
7300
|
-
const document = reference(op.key);
|
|
7301
|
-
if (op.type === "put") transaction.set(document, toDocument(op.key, op.value, op.expiresAt));
|
|
7302
|
-
else if (op.type === "delete") transaction.delete(document);
|
|
7303
|
-
else if (op.type === "add") {
|
|
7304
|
-
if (snapshots.get(position)?.exists === false) {
|
|
7305
|
-
const created = { ...op.init };
|
|
7306
|
-
for (const [name, delta] of Object.entries(op.by)) created[name] = Number(created[name] ?? 0) + delta;
|
|
7307
|
-
transaction.set(document, toDocument(op.key, created, op.expiresAt));
|
|
7308
|
-
} else {
|
|
7309
|
-
const [[name, delta], ...more] = Object.entries(op.by);
|
|
7310
|
-
const ttl = op.expiresAt === void 0 ? [] : [FIRESTORE_TTL_FIELD, firebase_admin_firestore.Timestamp.fromMillis(op.expiresAt)];
|
|
7311
|
-
transaction.update(document, new firebase_admin_firestore.FieldPath("row", name), firebase_admin_firestore.FieldValue.increment(delta), ...more.flatMap(([other, by]) => [new firebase_admin_firestore.FieldPath("row", other), firebase_admin_firestore.FieldValue.increment(by)]), ...ttl);
|
|
7312
|
-
}
|
|
7313
|
-
}
|
|
7314
|
-
}
|
|
7315
|
-
return { ok: true };
|
|
7316
|
-
}, { maxAttempts: 5 });
|
|
7317
|
-
try {
|
|
7318
|
-
try {
|
|
7319
|
-
const result = await attempt(false);
|
|
7320
|
-
if (result !== null) return result;
|
|
7321
|
-
} catch (error) {
|
|
7322
|
-
if (codeOf(error) !== 5 || !blind.includes(true)) throw error;
|
|
7323
|
-
}
|
|
7324
|
-
return await attempt(true);
|
|
7325
|
-
} catch (error) {
|
|
7326
|
-
if (TRANSIENT.has(codeOf(error))) return {
|
|
7327
|
-
ok: false,
|
|
7328
|
-
retry: true
|
|
7329
|
-
};
|
|
7330
|
-
return missingDatabase(error);
|
|
7331
|
-
}
|
|
7332
|
-
},
|
|
7333
|
-
/** A plain batch, which reads nothing, 500 deletes at a time. */
|
|
7334
|
-
async deleteConsumed(keys) {
|
|
7335
|
-
for (let at = 0; at < keys.length; at += FIRESTORE_LIMITS.items) {
|
|
7336
|
-
const batch = firestore.batch();
|
|
7337
|
-
for (const key of keys.slice(at, at + FIRESTORE_LIMITS.items)) batch.delete(reference(key));
|
|
7338
|
-
await batch.commit().catch(missingDatabase);
|
|
7339
|
-
}
|
|
7340
|
-
},
|
|
7341
|
-
async dispose() {}
|
|
7342
|
-
};
|
|
7343
|
-
};
|
|
7344
|
-
//#endregion
|
|
7345
|
-
//#region src/firebaseDatabase.ts
|
|
7346
|
-
const adapterOf = ({ collection = FIREBASE_V1_COLLECTION, aggregateBatching: _, ...appOptions }) => {
|
|
7347
|
-
const app = (0, firebase_admin_app.getApps)().length ? (0, firebase_admin_app.getApp)() : (0, firebase_admin_app.initializeApp)(appOptions);
|
|
7348
|
-
return createKvAdapter({ store: createFirestoreStore({
|
|
7349
|
-
firestore: (0, firebase_admin_firestore.getFirestore)(app),
|
|
7350
|
-
collection
|
|
7351
|
-
}) });
|
|
7352
|
-
};
|
|
7353
|
-
/** Hot Updater's database in one Firestore collection, through the storage engine. */
|
|
7354
|
-
const firebaseDatabase = (config) => createEngineDatabase({
|
|
7355
|
-
name: "firebaseDatabase",
|
|
7356
|
-
adapter: adapterOf(config),
|
|
7357
|
-
aggregateBatching: config.aggregateBatching ?? {}
|
|
7358
|
-
});
|
|
7359
|
-
//#endregion
|
|
7360
|
-
//#region src/firebaseStorage.ts
|
|
7361
|
-
const firebaseStorage = (config) => {
|
|
7362
|
-
const app = (0, firebase_admin_app.getApps)().length ? (0, firebase_admin_app.getApp)() : (0, firebase_admin_app.initializeApp)(config);
|
|
7363
|
-
const bucket = (0, firebase_admin_storage.getStorage)(app).bucket(config.storageBucket);
|
|
7364
|
-
const getStorageKey = createStorageKeyBuilder(config.basePath);
|
|
7365
|
-
const parseAndValidate = (storageUri) => {
|
|
7366
|
-
const parsed = parseStorageUri(storageUri, "gs");
|
|
7367
|
-
if (parsed.bucket !== config.storageBucket) throw new Error(`Bucket name mismatch: expected "${config.storageBucket}", but found "${parsed.bucket}".`);
|
|
7368
|
-
return parsed;
|
|
7369
|
-
};
|
|
7370
|
-
return createStorageAdapter({
|
|
7371
|
-
name: "firebaseStorage",
|
|
7372
|
-
protocol: "gs",
|
|
7373
|
-
async put({ key, body, contentType }) {
|
|
7374
|
-
const storageKey = getStorageKey(key);
|
|
7375
|
-
const bytes = new Uint8Array(await new Response(body).arrayBuffer());
|
|
7376
|
-
await bucket.file(storageKey).save(bytes, { metadata: {
|
|
7377
|
-
contentType,
|
|
7378
|
-
cacheControl: "public, max-age=31536000, immutable"
|
|
7379
|
-
} });
|
|
7380
|
-
return { storageUri: createStorageUri({
|
|
7381
|
-
bucket: config.storageBucket,
|
|
7382
|
-
key: storageKey,
|
|
7383
|
-
protocol: "gs"
|
|
7384
|
-
}) };
|
|
7385
|
-
},
|
|
7386
|
-
async get({ storageUri }) {
|
|
7387
|
-
const { key } = parseAndValidate(storageUri);
|
|
7388
|
-
try {
|
|
7389
|
-
const file = bucket.file(key);
|
|
7390
|
-
const [[body], [metadata]] = await Promise.all([file.download(), file.getMetadata()]);
|
|
7391
|
-
const headers = new Headers();
|
|
7392
|
-
if (metadata.contentType) headers.set("content-type", metadata.contentType);
|
|
7393
|
-
headers.set("content-length", String(metadata.size ?? body.byteLength));
|
|
7394
|
-
return { response: new Response(body, { headers }) };
|
|
7395
|
-
} catch (error) {
|
|
7396
|
-
if (typeof error === "object" && error !== null && "code" in error && error.code === 404) return { response: null };
|
|
7397
|
-
throw error;
|
|
7398
|
-
}
|
|
7399
|
-
},
|
|
7400
|
-
async getDownloadUrl({ storageUri }) {
|
|
7401
|
-
const { key } = parseAndValidate(storageUri);
|
|
7402
|
-
if (config.cdnUrl) {
|
|
7403
|
-
const storageUrl = new URL(storageUri);
|
|
7404
|
-
const downloadUrl = new URL(config.cdnUrl);
|
|
7405
|
-
downloadUrl.pathname = `${downloadUrl.pathname.replace(/\/+$/, "")}${storageUrl.pathname}`;
|
|
7406
|
-
downloadUrl.search = "";
|
|
7407
|
-
downloadUrl.hash = "";
|
|
7408
|
-
return { url: downloadUrl.toString() };
|
|
7409
|
-
}
|
|
7410
|
-
const [url] = await bucket.file(key).getSignedUrl({
|
|
7411
|
-
action: "read",
|
|
7412
|
-
expires: Date.now() + (config.signedUrlExpiresIn ?? 3600) * 1e3
|
|
7413
|
-
});
|
|
7414
|
-
if (!url) throw new Error("Failed to generate Firebase download URL");
|
|
7415
|
-
return { url };
|
|
7416
|
-
},
|
|
7417
|
-
async exists({ storageUri }) {
|
|
7418
|
-
const { key } = parseAndValidate(storageUri);
|
|
7419
|
-
const [exists] = await bucket.file(key).exists();
|
|
7420
|
-
return { exists };
|
|
7421
|
-
},
|
|
7422
|
-
async delete({ storageUri }) {
|
|
7423
|
-
const { key } = parseAndValidate(storageUri);
|
|
7424
|
-
await bucket.file(key).delete({ ignoreNotFound: true });
|
|
7425
|
-
return { deleted: true };
|
|
7426
|
-
}
|
|
7427
|
-
});
|
|
7428
|
-
};
|
|
7429
|
-
//#endregion
|
|
7430
7182
|
//#region ../api-keys/dist/server/apiKeys.mjs
|
|
7431
7183
|
const API_KEY_HEADER_NAME = "x-api-key";
|
|
7432
7184
|
const BASE64URL_32_PATTERN = /^[A-Za-z0-9_-]{43}$/u;
|
|
@@ -7576,7 +7328,7 @@ const apiKeysCli = (headerName) => ({ clientCredential: {
|
|
|
7576
7328
|
} });
|
|
7577
7329
|
//#endregion
|
|
7578
7330
|
//#region ../api-keys/dist/server/model.mjs
|
|
7579
|
-
const toRow = (row) => ({
|
|
7331
|
+
const toRow$1 = (row) => ({
|
|
7580
7332
|
id: row.id,
|
|
7581
7333
|
hash: row.hash,
|
|
7582
7334
|
name: row.name,
|
|
@@ -7601,7 +7353,7 @@ const createApiKeyModel = (db) => ({
|
|
|
7601
7353
|
},
|
|
7602
7354
|
async findByHash(hash) {
|
|
7603
7355
|
const row = await db.findOne("api_keys", { hash });
|
|
7604
|
-
return row === null ? null : toRow(row);
|
|
7356
|
+
return row === null ? null : toRow$1(row);
|
|
7605
7357
|
},
|
|
7606
7358
|
/** Every key, newest first, then by id; the whole list is the result. */
|
|
7607
7359
|
async list() {
|
|
@@ -7615,7 +7367,7 @@ const createApiKeyModel = (db) => ({
|
|
|
7615
7367
|
limit: 500,
|
|
7616
7368
|
...cursor === void 0 ? {} : { cursor }
|
|
7617
7369
|
});
|
|
7618
|
-
rows.push(...page.rows.map(toRow));
|
|
7370
|
+
rows.push(...page.rows.map(toRow$1));
|
|
7619
7371
|
cursor = page.next;
|
|
7620
7372
|
} while (cursor !== void 0);
|
|
7621
7373
|
return rows.sort((left, right) => right.created_at_ms - left.created_at_ms || (left.id < right.id ? -1 : left.id > right.id ? 1 : 0));
|
|
@@ -7624,7 +7376,7 @@ const createApiKeyModel = (db) => ({
|
|
|
7624
7376
|
const row = await tx.findOne("api_keys", { id });
|
|
7625
7377
|
if (row === null) return null;
|
|
7626
7378
|
tx.update("api_keys", row, { revoked_at_ms: revokedAtMs });
|
|
7627
|
-
return toRow({
|
|
7379
|
+
return toRow$1({
|
|
7628
7380
|
...row,
|
|
7629
7381
|
revoked_at_ms: revokedAtMs
|
|
7630
7382
|
});
|
|
@@ -11586,16 +11338,253 @@ const remoteConfig = () => {
|
|
|
11586
11338
|
return markOfficial(plugin);
|
|
11587
11339
|
};
|
|
11588
11340
|
//#endregion
|
|
11589
|
-
//#region src/
|
|
11341
|
+
//#region src/firebaseInfrastructureNames.ts
|
|
11590
11342
|
/**
|
|
11591
|
-
* The
|
|
11592
|
-
*
|
|
11343
|
+
* The one collection the storage engine keeps every item in. It namespaces
|
|
11344
|
+
* the data, so item partitions are table names with no prefix.
|
|
11593
11345
|
*/
|
|
11594
|
-
const
|
|
11595
|
-
|
|
11596
|
-
|
|
11597
|
-
|
|
11598
|
-
|
|
11346
|
+
const FIREBASE_V1_COLLECTION = "hot_updater_v1";
|
|
11347
|
+
//#endregion
|
|
11348
|
+
//#region src/firestoreStore.ts
|
|
11349
|
+
/**
|
|
11350
|
+
* Firestore's limits: 10 MiB a request and 1 MiB a document; 500 writes a
|
|
11351
|
+
* transaction stays a conservative bound. Indexed values over 1,500 bytes are
|
|
11352
|
+
* truncated, so longer keys are refused rather than read inconsistently.
|
|
11353
|
+
*/
|
|
11354
|
+
const FIRESTORE_LIMITS = {
|
|
11355
|
+
items: 500,
|
|
11356
|
+
bytes: 1e7,
|
|
11357
|
+
itemBytes: 1e6,
|
|
11358
|
+
keyBytes: {
|
|
11359
|
+
pk: 1500,
|
|
11360
|
+
sk: 1500
|
|
11361
|
+
}
|
|
11362
|
+
};
|
|
11363
|
+
/** `getAll` reads at most this many documents a call. */
|
|
11364
|
+
const GET_BATCH = 300;
|
|
11365
|
+
/** A document's id: a hash of its key, since keys may hold `/` and outgrow Firestore's 1,500 bytes. */
|
|
11366
|
+
const firestoreDocumentId = ({ pk, sk }) => (0, node_crypto.createHash)("sha256").update(JSON.stringify([pk, sk])).digest("hex");
|
|
11367
|
+
/**
|
|
11368
|
+
* A document is `{ pk, sk, row }`. Row values that are maps or arrays become
|
|
11369
|
+
* JSON text inside a map, since Firestore has no nested arrays and does not
|
|
11370
|
+
* keep map key order; `row` is exempt from single-field indexing.
|
|
11371
|
+
*/
|
|
11372
|
+
const encode = (value) => value !== null && typeof value === "object" ? { json: JSON.stringify(value) } : value;
|
|
11373
|
+
const decode = (value) => value !== null && typeof value === "object" && "json" in value ? JSON.parse(String(value.json)) : value;
|
|
11374
|
+
/**
|
|
11375
|
+
* The collection's TTL field: a timestamp beside `pk` and `sk` on every item
|
|
11376
|
+
* of a row that expires, which Firestore's TTL policy deletes.
|
|
11377
|
+
*/
|
|
11378
|
+
const FIRESTORE_TTL_FIELD = "expireAt";
|
|
11379
|
+
const toDocument = (key, row, expiresAt) => ({
|
|
11380
|
+
pk: key.pk,
|
|
11381
|
+
sk: key.sk,
|
|
11382
|
+
row: Object.fromEntries(Object.entries(row).map(([name, value]) => [name, encode(value)])),
|
|
11383
|
+
...expiresAt === void 0 ? {} : { [FIRESTORE_TTL_FIELD]: firebase_admin_firestore.Timestamp.fromMillis(expiresAt) }
|
|
11384
|
+
});
|
|
11385
|
+
const toRow = (data) => Object.fromEntries(Object.entries(data.row ?? {}).map(([name, value]) => [name, decode(value)]));
|
|
11386
|
+
const holds = (snapshot, condition) => {
|
|
11387
|
+
if (condition === void 0) return true;
|
|
11388
|
+
const data = snapshot?.exists ? snapshot.data() : void 0;
|
|
11389
|
+
return "v" in condition ? data !== void 0 && data.row?._v === condition.v : data !== void 0 === condition.exists;
|
|
11390
|
+
};
|
|
11391
|
+
/** gRPC codes a rerun of the whole write can clear: DEADLINE_EXCEEDED, RESOURCE_EXHAUSTED, ABORTED, INTERNAL, UNAVAILABLE. */
|
|
11392
|
+
const TRANSIENT = /* @__PURE__ */ new Set([
|
|
11393
|
+
4,
|
|
11394
|
+
8,
|
|
11395
|
+
10,
|
|
11396
|
+
13,
|
|
11397
|
+
14
|
|
11398
|
+
]);
|
|
11399
|
+
const codeOf = (error) => error.code;
|
|
11400
|
+
/** Firestore reports a missing database as NOT_FOUND; it reads as a missing schema, like SQL's 42P01. */
|
|
11401
|
+
const missingDatabase = (error) => {
|
|
11402
|
+
throw codeOf(error) === 5 ? Object.assign(new Error(error.message, { cause: error }), { code: "42P01" }) : error;
|
|
11403
|
+
};
|
|
11404
|
+
/**
|
|
11405
|
+
* The key-value store over one Firestore collection of `{ pk, sk, row }`
|
|
11406
|
+
* documents, queried through the composite indexes on (pk, sk) in both
|
|
11407
|
+
* directions. A write is one transaction that reads only the documents its
|
|
11408
|
+
* ops guard.
|
|
11409
|
+
*/
|
|
11410
|
+
const createFirestoreStore = ({ firestore, collection, nativePageSize }) => {
|
|
11411
|
+
const items = firestore.collection(collection);
|
|
11412
|
+
const reference = (key) => items.doc(firestoreDocumentId(key));
|
|
11413
|
+
return {
|
|
11414
|
+
id: "firestore",
|
|
11415
|
+
limits: FIRESTORE_LIMITS,
|
|
11416
|
+
async get(keys) {
|
|
11417
|
+
const found = /* @__PURE__ */ new Map();
|
|
11418
|
+
const ids = [...new Set(keys.map(firestoreDocumentId))];
|
|
11419
|
+
for (let at = 0; at < ids.length; at += GET_BATCH) {
|
|
11420
|
+
const snapshots = await firestore.getAll(...ids.slice(at, at + GET_BATCH).map((id) => items.doc(id))).catch(missingDatabase);
|
|
11421
|
+
for (const snapshot of snapshots) if (snapshot.exists) found.set(snapshot.id, toRow(snapshot.data()));
|
|
11422
|
+
}
|
|
11423
|
+
return keys.map((key) => found.get(firestoreDocumentId(key)) ?? null);
|
|
11424
|
+
},
|
|
11425
|
+
async query({ pk, gte, lt, order, limit, after }) {
|
|
11426
|
+
let query = items.where("pk", "==", pk);
|
|
11427
|
+
if (gte !== void 0) query = query.where("sk", ">=", gte);
|
|
11428
|
+
if (lt !== void 0) query = query.where("sk", "<", lt);
|
|
11429
|
+
query = query.orderBy("sk", order);
|
|
11430
|
+
if (after !== void 0) query = query.startAfter(after);
|
|
11431
|
+
const size = Math.min(limit, nativePageSize ?? limit);
|
|
11432
|
+
const { docs } = await query.limit(size).get().catch(missingDatabase);
|
|
11433
|
+
return {
|
|
11434
|
+
items: docs.map((document) => ({
|
|
11435
|
+
sk: document.get("sk"),
|
|
11436
|
+
value: toRow(document.data())
|
|
11437
|
+
})),
|
|
11438
|
+
more: docs.length === size
|
|
11439
|
+
};
|
|
11440
|
+
},
|
|
11441
|
+
async write(ops) {
|
|
11442
|
+
const blind = ops.map((op) => op.type === "add" && (op.condition === void 0 || "exists" in op.condition && op.condition.exists));
|
|
11443
|
+
const attempt = (reading) => firestore.runTransaction(async (transaction) => {
|
|
11444
|
+
const read = ops.flatMap((op, position) => (op.condition !== void 0 || op.type === "add") && (reading || !blind[position]) ? [position] : []);
|
|
11445
|
+
const snapshots = /* @__PURE__ */ new Map();
|
|
11446
|
+
if (read.length > 0) {
|
|
11447
|
+
const found = await transaction.getAll(...read.map((position) => reference(ops[position].key)));
|
|
11448
|
+
read.forEach((position, at) => snapshots.set(position, found[at]));
|
|
11449
|
+
}
|
|
11450
|
+
const failed = read.find((position) => !holds(snapshots.get(position), ops[position].condition));
|
|
11451
|
+
if (failed !== void 0) {
|
|
11452
|
+
if (!reading && blind.slice(0, failed).includes(true)) return null;
|
|
11453
|
+
return {
|
|
11454
|
+
ok: false,
|
|
11455
|
+
failedOp: failed
|
|
11456
|
+
};
|
|
11457
|
+
}
|
|
11458
|
+
for (const [position, op] of ops.entries()) {
|
|
11459
|
+
const document = reference(op.key);
|
|
11460
|
+
if (op.type === "put") transaction.set(document, toDocument(op.key, op.value, op.expiresAt));
|
|
11461
|
+
else if (op.type === "delete") transaction.delete(document);
|
|
11462
|
+
else if (op.type === "add") {
|
|
11463
|
+
if (snapshots.get(position)?.exists === false) {
|
|
11464
|
+
const created = { ...op.init };
|
|
11465
|
+
for (const [name, delta] of Object.entries(op.by)) created[name] = Number(created[name] ?? 0) + delta;
|
|
11466
|
+
transaction.set(document, toDocument(op.key, created, op.expiresAt));
|
|
11467
|
+
} else {
|
|
11468
|
+
const [[name, delta], ...more] = Object.entries(op.by);
|
|
11469
|
+
const ttl = op.expiresAt === void 0 ? [] : [FIRESTORE_TTL_FIELD, firebase_admin_firestore.Timestamp.fromMillis(op.expiresAt)];
|
|
11470
|
+
transaction.update(document, new firebase_admin_firestore.FieldPath("row", name), firebase_admin_firestore.FieldValue.increment(delta), ...more.flatMap(([other, by]) => [new firebase_admin_firestore.FieldPath("row", other), firebase_admin_firestore.FieldValue.increment(by)]), ...ttl);
|
|
11471
|
+
}
|
|
11472
|
+
}
|
|
11473
|
+
}
|
|
11474
|
+
return { ok: true };
|
|
11475
|
+
}, { maxAttempts: 5 });
|
|
11476
|
+
try {
|
|
11477
|
+
try {
|
|
11478
|
+
const result = await attempt(false);
|
|
11479
|
+
if (result !== null) return result;
|
|
11480
|
+
} catch (error) {
|
|
11481
|
+
if (codeOf(error) !== 5 || !blind.includes(true)) throw error;
|
|
11482
|
+
}
|
|
11483
|
+
return await attempt(true);
|
|
11484
|
+
} catch (error) {
|
|
11485
|
+
if (TRANSIENT.has(codeOf(error))) return {
|
|
11486
|
+
ok: false,
|
|
11487
|
+
retry: true
|
|
11488
|
+
};
|
|
11489
|
+
return missingDatabase(error);
|
|
11490
|
+
}
|
|
11491
|
+
},
|
|
11492
|
+
/** A plain batch, which reads nothing, 500 deletes at a time. */
|
|
11493
|
+
async deleteConsumed(keys) {
|
|
11494
|
+
for (let at = 0; at < keys.length; at += FIRESTORE_LIMITS.items) {
|
|
11495
|
+
const batch = firestore.batch();
|
|
11496
|
+
for (const key of keys.slice(at, at + FIRESTORE_LIMITS.items)) batch.delete(reference(key));
|
|
11497
|
+
await batch.commit().catch(missingDatabase);
|
|
11498
|
+
}
|
|
11499
|
+
},
|
|
11500
|
+
async dispose() {}
|
|
11501
|
+
};
|
|
11502
|
+
};
|
|
11503
|
+
//#endregion
|
|
11504
|
+
//#region src/firebaseDatabase.ts
|
|
11505
|
+
const adapterOf = ({ collection = FIREBASE_V1_COLLECTION, aggregateBatching: _, ...appOptions }) => {
|
|
11506
|
+
const app = (0, firebase_admin_app.getApps)().length ? (0, firebase_admin_app.getApp)() : (0, firebase_admin_app.initializeApp)(appOptions);
|
|
11507
|
+
return createKvAdapter({ store: createFirestoreStore({
|
|
11508
|
+
firestore: (0, firebase_admin_firestore.getFirestore)(app),
|
|
11509
|
+
collection
|
|
11510
|
+
}) });
|
|
11511
|
+
};
|
|
11512
|
+
/** Hot Updater's database in one Firestore collection, through the storage engine. */
|
|
11513
|
+
const firebaseDatabase = (config) => createEngineDatabase({
|
|
11514
|
+
name: "firebaseDatabase",
|
|
11515
|
+
adapter: adapterOf(config),
|
|
11516
|
+
aggregateBatching: config.aggregateBatching ?? {}
|
|
11517
|
+
});
|
|
11518
|
+
//#endregion
|
|
11519
|
+
//#region src/firebaseStorage.ts
|
|
11520
|
+
const firebaseStorage = (config) => {
|
|
11521
|
+
const app = (0, firebase_admin_app.getApps)().length ? (0, firebase_admin_app.getApp)() : (0, firebase_admin_app.initializeApp)(config);
|
|
11522
|
+
const bucket = (0, firebase_admin_storage.getStorage)(app).bucket(config.storageBucket);
|
|
11523
|
+
const getStorageKey = createStorageKeyBuilder(config.basePath);
|
|
11524
|
+
const parseAndValidate = (storageUri) => {
|
|
11525
|
+
const parsed = parseStorageUri(storageUri, "gs");
|
|
11526
|
+
if (parsed.bucket !== config.storageBucket) throw new Error(`Bucket name mismatch: expected "${config.storageBucket}", but found "${parsed.bucket}".`);
|
|
11527
|
+
return parsed;
|
|
11528
|
+
};
|
|
11529
|
+
return createStorageAdapter({
|
|
11530
|
+
name: "firebaseStorage",
|
|
11531
|
+
protocol: "gs",
|
|
11532
|
+
async put({ key, body, contentType }) {
|
|
11533
|
+
const storageKey = getStorageKey(key);
|
|
11534
|
+
const bytes = new Uint8Array(await new Response(body).arrayBuffer());
|
|
11535
|
+
await bucket.file(storageKey).save(bytes, { metadata: {
|
|
11536
|
+
contentType,
|
|
11537
|
+
cacheControl: "public, max-age=31536000, immutable"
|
|
11538
|
+
} });
|
|
11539
|
+
return { storageUri: createStorageUri({
|
|
11540
|
+
bucket: config.storageBucket,
|
|
11541
|
+
key: storageKey,
|
|
11542
|
+
protocol: "gs"
|
|
11543
|
+
}) };
|
|
11544
|
+
},
|
|
11545
|
+
async get({ storageUri }) {
|
|
11546
|
+
const { key } = parseAndValidate(storageUri);
|
|
11547
|
+
try {
|
|
11548
|
+
const file = bucket.file(key);
|
|
11549
|
+
const [[body], [metadata]] = await Promise.all([file.download(), file.getMetadata()]);
|
|
11550
|
+
const headers = new Headers();
|
|
11551
|
+
if (metadata.contentType) headers.set("content-type", metadata.contentType);
|
|
11552
|
+
headers.set("content-length", String(metadata.size ?? body.byteLength));
|
|
11553
|
+
return { response: new Response(body, { headers }) };
|
|
11554
|
+
} catch (error) {
|
|
11555
|
+
if (typeof error === "object" && error !== null && "code" in error && error.code === 404) return { response: null };
|
|
11556
|
+
throw error;
|
|
11557
|
+
}
|
|
11558
|
+
},
|
|
11559
|
+
async getDownloadUrl({ storageUri }) {
|
|
11560
|
+
const { key } = parseAndValidate(storageUri);
|
|
11561
|
+
if (config.cdnUrl) {
|
|
11562
|
+
const storageUrl = new URL(storageUri);
|
|
11563
|
+
const downloadUrl = new URL(config.cdnUrl);
|
|
11564
|
+
downloadUrl.pathname = `${downloadUrl.pathname.replace(/\/+$/, "")}${storageUrl.pathname}`;
|
|
11565
|
+
downloadUrl.search = "";
|
|
11566
|
+
downloadUrl.hash = "";
|
|
11567
|
+
return { url: downloadUrl.toString() };
|
|
11568
|
+
}
|
|
11569
|
+
const [url] = await bucket.file(key).getSignedUrl({
|
|
11570
|
+
action: "read",
|
|
11571
|
+
expires: Date.now() + (config.signedUrlExpiresIn ?? 3600) * 1e3
|
|
11572
|
+
});
|
|
11573
|
+
if (!url) throw new Error("Failed to generate Firebase download URL");
|
|
11574
|
+
return { url };
|
|
11575
|
+
},
|
|
11576
|
+
async exists({ storageUri }) {
|
|
11577
|
+
const { key } = parseAndValidate(storageUri);
|
|
11578
|
+
const [exists] = await bucket.file(key).exists();
|
|
11579
|
+
return { exists };
|
|
11580
|
+
},
|
|
11581
|
+
async delete({ storageUri }) {
|
|
11582
|
+
const { key } = parseAndValidate(storageUri);
|
|
11583
|
+
await bucket.file(key).delete({ ignoreNotFound: true });
|
|
11584
|
+
return { deleted: true };
|
|
11585
|
+
}
|
|
11586
|
+
});
|
|
11587
|
+
};
|
|
11599
11588
|
//#endregion
|
|
11600
11589
|
//#region firebase/functions/index.ts
|
|
11601
11590
|
const HOT_UPDATER_BASE_PATH = "/";
|
|
@@ -11605,7 +11594,11 @@ const cdnUrl = process.env.HOT_UPDATER_CDN_URL;
|
|
|
11605
11594
|
if (!storageBucket) throw new Error("Firebase runtime requires storageBucket to read bundle manifests.");
|
|
11606
11595
|
const hotUpdater = createHotUpdater({
|
|
11607
11596
|
database: firebaseDatabase({ ...adminOptions }),
|
|
11608
|
-
plugins
|
|
11597
|
+
plugins: [
|
|
11598
|
+
insights(),
|
|
11599
|
+
apiKeys(),
|
|
11600
|
+
remoteConfig()
|
|
11601
|
+
],
|
|
11609
11602
|
storage: firebaseStorage({
|
|
11610
11603
|
...adminOptions,
|
|
11611
11604
|
storageBucket,
|
package/dist/index.cjs
CHANGED
|
@@ -4,7 +4,6 @@ let firebase_admin_app = require("firebase-admin/app");
|
|
|
4
4
|
let firebase_admin_firestore = require("firebase-admin/firestore");
|
|
5
5
|
let node_crypto = require("node:crypto");
|
|
6
6
|
let firebase_admin_storage = require("firebase-admin/storage");
|
|
7
|
-
let _hot_updater_server_plugins = require("@hot-updater/server/plugins");
|
|
8
7
|
//#region src/firebaseInfrastructureNames.ts
|
|
9
8
|
/**
|
|
10
9
|
* The one collection the storage engine keeps every item in. It namespaces
|
|
@@ -260,18 +259,6 @@ const firebaseStorage = (config) => {
|
|
|
260
259
|
});
|
|
261
260
|
};
|
|
262
261
|
//#endregion
|
|
263
|
-
//#region src/plugins.ts
|
|
264
|
-
/**
|
|
265
|
-
* The plugins the managed Firebase server runs: Insights, API keys on every
|
|
266
|
-
* client route, and Remote Config. Its Cloud Function imports this list.
|
|
267
|
-
*/
|
|
268
|
-
const plugins = [
|
|
269
|
-
(0, _hot_updater_server_plugins.insights)(),
|
|
270
|
-
(0, _hot_updater_server_plugins.apiKeys)(),
|
|
271
|
-
(0, _hot_updater_server_plugins.remoteConfig)()
|
|
272
|
-
];
|
|
273
|
-
//#endregion
|
|
274
262
|
exports.firebaseDatabase = firebaseDatabase;
|
|
275
263
|
exports.firebaseStorage = firebaseStorage;
|
|
276
264
|
exports.migrateFirebaseDatabase = migrateFirebaseDatabase;
|
|
277
|
-
exports.plugins = plugins;
|