@getstrata/core 2.2.13 → 2.2.15
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/CHANGELOG.md +41 -14
- package/dist/core/database/baseRepository.d.ts +1 -0
- package/dist/core/database/defaultConnection.d.ts +2 -1
- package/dist/core/database/relationQuery.d.ts +4 -0
- package/dist/core/http/index.d.ts +2 -2
- package/dist/core/http/route.d.ts +64 -0
- package/dist/core/queue/redisQueue.d.ts +5 -1
- package/dist/core/queue/redisStreams.d.ts +8 -2
- package/dist/core/view/index.d.ts +1 -1
- package/dist/core/view/viewRenderer.d.ts +7 -1
- package/dist/entries/http/corsMiddleware.js +10 -1
- package/dist/entries/http/route.js +45 -1
- package/dist/entries/queue/createAppQueue.js +202 -92
- package/dist/entries/queue/publicQueue.js +202 -92
- package/dist/entries/queue/queueMetrics.js +202 -92
- package/dist/entries/queue/redisQueue.js +203 -92
- package/dist/framework/public-api.d.ts +4 -4
- package/dist/index.js +430 -144
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -125,6 +125,10 @@ function getDefaultDatabaseQuery() {
|
|
|
125
125
|
}
|
|
126
126
|
return query;
|
|
127
127
|
}
|
|
128
|
+
function isDefaultDatabaseConnection(connection) {
|
|
129
|
+
const { pool, query } = defaultConnectionState();
|
|
130
|
+
return connection !== null && (connection === pool || connection === query);
|
|
131
|
+
}
|
|
128
132
|
function resetDefaultDatabasePoolForTests() {
|
|
129
133
|
const holder = defaultConnectionState();
|
|
130
134
|
holder.pool = null;
|
|
@@ -5781,6 +5785,31 @@ class RepositoryQuery {
|
|
|
5781
5785
|
// ../../src/core/database/baseRepository.ts
|
|
5782
5786
|
init_transaction();
|
|
5783
5787
|
init_transactionControl();
|
|
5788
|
+
function listOrders(orderBy) {
|
|
5789
|
+
if (!orderBy)
|
|
5790
|
+
return [];
|
|
5791
|
+
if (Array.isArray(orderBy))
|
|
5792
|
+
return orderBy.map(readOrder);
|
|
5793
|
+
if ("column" in orderBy)
|
|
5794
|
+
return [readOrder(orderBy)];
|
|
5795
|
+
return Object.entries(orderBy).flatMap(([column, direction]) => {
|
|
5796
|
+
if (direction === undefined)
|
|
5797
|
+
return [];
|
|
5798
|
+
return [
|
|
5799
|
+
{
|
|
5800
|
+
column,
|
|
5801
|
+
direction: String(direction).toUpperCase() === "DESC" ? "desc" : "asc"
|
|
5802
|
+
}
|
|
5803
|
+
];
|
|
5804
|
+
});
|
|
5805
|
+
}
|
|
5806
|
+
function readOrder(order) {
|
|
5807
|
+
return {
|
|
5808
|
+
column: String(order.column),
|
|
5809
|
+
direction: String(order.direction).toUpperCase() === "DESC" ? "desc" : "asc",
|
|
5810
|
+
...order.nulls === undefined ? {} : { nulls: order.nulls }
|
|
5811
|
+
};
|
|
5812
|
+
}
|
|
5784
5813
|
|
|
5785
5814
|
class BaseRepository {
|
|
5786
5815
|
table;
|
|
@@ -5824,6 +5853,10 @@ class BaseRepository {
|
|
|
5824
5853
|
joins: options.joins,
|
|
5825
5854
|
groupBy: options.groupBy
|
|
5826
5855
|
}, whereNodes);
|
|
5856
|
+
const meta = buildPaginationMeta({ page, perPage, total });
|
|
5857
|
+
if (total === 0) {
|
|
5858
|
+
return { data: [], meta };
|
|
5859
|
+
}
|
|
5827
5860
|
const offset = (page - 1) * perPage;
|
|
5828
5861
|
const data = await this.findAll({
|
|
5829
5862
|
...queryOptions,
|
|
@@ -5834,13 +5867,19 @@ class BaseRepository {
|
|
|
5834
5867
|
});
|
|
5835
5868
|
return {
|
|
5836
5869
|
data,
|
|
5837
|
-
meta
|
|
5870
|
+
meta
|
|
5838
5871
|
};
|
|
5839
5872
|
}
|
|
5840
5873
|
async chunk(count, callback, options = {}) {
|
|
5841
5874
|
if (!Number.isInteger(count) || count <= 0) {
|
|
5842
5875
|
throw new Error("Chunk size must be a positive integer.");
|
|
5843
5876
|
}
|
|
5877
|
+
if (this.primaryKeySeekDirection(options) === "asc") {
|
|
5878
|
+
const seekOptions = { ...options };
|
|
5879
|
+
delete seekOptions.orderBy;
|
|
5880
|
+
await this.chunkById(count, async (rows) => await callback(rows) === false ? false : undefined, seekOptions);
|
|
5881
|
+
return;
|
|
5882
|
+
}
|
|
5844
5883
|
let offset = 0;
|
|
5845
5884
|
while (true) {
|
|
5846
5885
|
const rows = await this.findAll({
|
|
@@ -6125,6 +6164,17 @@ class BaseRepository {
|
|
|
6125
6164
|
return entity;
|
|
6126
6165
|
}));
|
|
6127
6166
|
}
|
|
6167
|
+
primaryKeySeekDirection(options) {
|
|
6168
|
+
if (options.joins?.length || options.groupBy || options.having || options.select?.length || options.offset !== undefined || options.limit !== undefined || options.lock) {
|
|
6169
|
+
return null;
|
|
6170
|
+
}
|
|
6171
|
+
const orders = listOrders(options.orderBy ?? this.table.defaultOrderBy);
|
|
6172
|
+
const order = orders[0];
|
|
6173
|
+
if (orders.length !== 1 || !order || order.nulls || order.column !== this.table.primaryKey) {
|
|
6174
|
+
return null;
|
|
6175
|
+
}
|
|
6176
|
+
return order.direction;
|
|
6177
|
+
}
|
|
6128
6178
|
withConnection(connection) {
|
|
6129
6179
|
const clone = Object.create(Object.getPrototypeOf(this));
|
|
6130
6180
|
Object.assign(clone, this);
|
|
@@ -7599,7 +7649,12 @@ import { isDeepStrictEqual } from "util";
|
|
|
7599
7649
|
init_deferredModelEvents();
|
|
7600
7650
|
|
|
7601
7651
|
// ../../src/core/database/relationQuery.ts
|
|
7652
|
+
init_boundConnection();
|
|
7653
|
+
init_connectionContext();
|
|
7654
|
+
init_defaultConnection();
|
|
7602
7655
|
init_dialect();
|
|
7656
|
+
init_repositoryConnection();
|
|
7657
|
+
init_transaction();
|
|
7603
7658
|
function asWhere(where) {
|
|
7604
7659
|
return where;
|
|
7605
7660
|
}
|
|
@@ -7869,6 +7924,26 @@ class BelongsToRelationQuery {
|
|
|
7869
7924
|
});
|
|
7870
7925
|
}
|
|
7871
7926
|
}
|
|
7927
|
+
function pivotIdKey(id) {
|
|
7928
|
+
return typeof id === "number" || typeof id === "bigint" ? String(id) : id;
|
|
7929
|
+
}
|
|
7930
|
+
function hasRepeatedId(ids) {
|
|
7931
|
+
const seen = new Set;
|
|
7932
|
+
for (const id of ids) {
|
|
7933
|
+
const key = pivotIdKey(id);
|
|
7934
|
+
if (seen.has(key))
|
|
7935
|
+
return true;
|
|
7936
|
+
seen.add(key);
|
|
7937
|
+
}
|
|
7938
|
+
return false;
|
|
7939
|
+
}
|
|
7940
|
+
function canBatchPivotToggle(ids) {
|
|
7941
|
+
return ids.every((id) => typeof id === "bigint" && id >= BigInt(Number.MIN_SAFE_INTEGER) && id <= BigInt(Number.MAX_SAFE_INTEGER) || typeof id === "number" && Number.isSafeInteger(id) || typeof id === "string" && /^(?:0|-?[1-9]\d*)$/.test(id) && Number.isSafeInteger(Number(id)));
|
|
7942
|
+
}
|
|
7943
|
+
function isConfiguredConnection(connection) {
|
|
7944
|
+
const bound = getBoundDatabaseConnection();
|
|
7945
|
+
return connection === repositoryConnection || (bound ? connection === bound : isDefaultDatabaseConnection(connection));
|
|
7946
|
+
}
|
|
7872
7947
|
|
|
7873
7948
|
class BelongsToManyRelationQuery {
|
|
7874
7949
|
parent;
|
|
@@ -7910,7 +7985,20 @@ class BelongsToManyRelationQuery {
|
|
|
7910
7985
|
return { sql, params: extra.params };
|
|
7911
7986
|
}
|
|
7912
7987
|
connection() {
|
|
7913
|
-
|
|
7988
|
+
const connection = this.parent.getRepository().getConnection();
|
|
7989
|
+
return isConfiguredConnection(connection) ? getActiveDatabaseConnection(connection) : connection;
|
|
7990
|
+
}
|
|
7991
|
+
async atomically(work) {
|
|
7992
|
+
const connection = this.connection();
|
|
7993
|
+
if (isConfiguredConnection(connection) || hasActiveDatabaseConnection() && connection === getActiveDatabaseConnection(connection)) {
|
|
7994
|
+
await runInTransaction(work);
|
|
7995
|
+
return;
|
|
7996
|
+
}
|
|
7997
|
+
if (typeof connection.begin !== "function") {
|
|
7998
|
+
await work(connection);
|
|
7999
|
+
return;
|
|
8000
|
+
}
|
|
8001
|
+
await connection.begin(work);
|
|
7914
8002
|
}
|
|
7915
8003
|
async get() {
|
|
7916
8004
|
const query = await this.relatedQuery();
|
|
@@ -7948,36 +8036,72 @@ class BelongsToManyRelationQuery {
|
|
|
7948
8036
|
}
|
|
7949
8037
|
async attach(ids) {
|
|
7950
8038
|
const list = Array.isArray(ids) ? ids : [ids];
|
|
7951
|
-
|
|
7952
|
-
|
|
7953
|
-
|
|
7954
|
-
const extraPlaceholders = extraKeys.map((_, index) => `$${index + 3}`).join(", ");
|
|
7955
|
-
const extraValues = extraKeys.map((key) => this.pivotValues[key]);
|
|
7956
|
-
for (const id of list) {
|
|
7957
|
-
await this.connection().unsafe(extraKeys.length > 0 ? `INSERT INTO ${quoteIdentifier(this.relation.pivotTable)} (${quoteIdentifier(String(this.relation.foreignPivotKey))}, ${quoteIdentifier(String(this.relation.relatedPivotKey))}${extraColumns}) VALUES ($1, $2, ${extraPlaceholders})` : `INSERT INTO ${quoteIdentifier(this.relation.pivotTable)} (${quoteIdentifier(String(this.relation.foreignPivotKey))}, ${quoteIdentifier(String(this.relation.relatedPivotKey))}) VALUES ($1, $2)`, [parentId, id, ...extraValues]);
|
|
7958
|
-
}
|
|
8039
|
+
if (list.length === 0)
|
|
8040
|
+
return;
|
|
8041
|
+
await this.atomically((connection) => this.insertPivotRows(connection, list));
|
|
7959
8042
|
}
|
|
7960
8043
|
async toggle(ids) {
|
|
7961
8044
|
const list = Array.isArray(ids) ? ids : [ids];
|
|
7962
|
-
|
|
7963
|
-
|
|
7964
|
-
|
|
7965
|
-
if (
|
|
7966
|
-
|
|
7967
|
-
|
|
7968
|
-
|
|
8045
|
+
if (list.length === 0)
|
|
8046
|
+
return;
|
|
8047
|
+
await this.atomically(async (connection) => {
|
|
8048
|
+
if (!canBatchPivotToggle(list) || hasRepeatedId(list)) {
|
|
8049
|
+
for (const id of list)
|
|
8050
|
+
await this.toggleOne(connection, id);
|
|
8051
|
+
return;
|
|
7969
8052
|
}
|
|
7970
|
-
|
|
8053
|
+
const parentId = this.parent.get(this.relation.parentKey);
|
|
8054
|
+
const relatedColumn = quoteIdentifier(String(this.relation.relatedPivotKey));
|
|
8055
|
+
const parentColumn = quoteIdentifier(String(this.relation.foreignPivotKey));
|
|
8056
|
+
const placeholders = list.map((_, index) => currentSqlDialect().placeholder(index + 2));
|
|
8057
|
+
const existing = await connection.unsafe(`SELECT ${relatedColumn} FROM ${quoteIdentifier(this.relation.pivotTable)} WHERE ${parentColumn} = ${currentSqlDialect().placeholder(1)} AND ${relatedColumn} IN (${placeholders.join(", ")})`, [parentId, ...list]);
|
|
8058
|
+
const existingIds = new Set(existing.map((row) => pivotIdKey(row[String(this.relation.relatedPivotKey)])));
|
|
8059
|
+
const present = list.filter((id) => existingIds.has(pivotIdKey(id)));
|
|
8060
|
+
const missing = list.filter((id) => !existingIds.has(pivotIdKey(id)));
|
|
8061
|
+
if (present.length > 0)
|
|
8062
|
+
await this.deletePivotRows(connection, present);
|
|
8063
|
+
if (missing.length > 0)
|
|
8064
|
+
await this.insertPivotRows(connection, missing);
|
|
8065
|
+
});
|
|
7971
8066
|
}
|
|
7972
8067
|
async detach(ids) {
|
|
7973
8068
|
const parentId = this.parent.get(this.relation.parentKey);
|
|
7974
8069
|
if (ids === undefined) {
|
|
7975
|
-
await this.connection().unsafe(`DELETE FROM ${quoteIdentifier(this.relation.pivotTable)} WHERE ${quoteIdentifier(String(this.relation.foreignPivotKey))} = $1`, [parentId]);
|
|
8070
|
+
await this.connection().unsafe(`DELETE FROM ${quoteIdentifier(this.relation.pivotTable)} WHERE ${quoteIdentifier(String(this.relation.foreignPivotKey))} = ${currentSqlDialect().placeholder(1)}`, [parentId]);
|
|
7976
8071
|
return;
|
|
7977
8072
|
}
|
|
7978
8073
|
const list = Array.isArray(ids) ? ids : [ids];
|
|
7979
|
-
|
|
7980
|
-
|
|
8074
|
+
if (list.length === 0)
|
|
8075
|
+
return;
|
|
8076
|
+
await this.deletePivotRows(this.connection(), list);
|
|
8077
|
+
}
|
|
8078
|
+
async toggleOne(connection, id) {
|
|
8079
|
+
const parentId = this.parent.get(this.relation.parentKey);
|
|
8080
|
+
const existing = await connection.unsafe(`SELECT 1 FROM ${quoteIdentifier(this.relation.pivotTable)} WHERE ${quoteIdentifier(String(this.relation.foreignPivotKey))} = ${currentSqlDialect().placeholder(1)} AND ${quoteIdentifier(String(this.relation.relatedPivotKey))} = ${currentSqlDialect().placeholder(2)} LIMIT 1`, [parentId, id]);
|
|
8081
|
+
if (existing.length > 0)
|
|
8082
|
+
await this.deletePivotRows(connection, [id]);
|
|
8083
|
+
else
|
|
8084
|
+
await this.insertPivotRows(connection, [id]);
|
|
8085
|
+
}
|
|
8086
|
+
async insertPivotRows(connection, list) {
|
|
8087
|
+
const parentId = this.parent.get(this.relation.parentKey);
|
|
8088
|
+
const extraKeys = Object.keys(this.pivotValues);
|
|
8089
|
+
const extraColumns = extraKeys.length > 0 ? `, ${extraKeys.map((key) => quoteIdentifier(key)).join(", ")}` : "";
|
|
8090
|
+
const extraValues = extraKeys.map((key) => this.pivotValues[key]);
|
|
8091
|
+
const width = 2 + extraKeys.length;
|
|
8092
|
+
const tuples = list.map((_, row) => {
|
|
8093
|
+
const slots = Array.from({ length: width }, (__, column) => currentSqlDialect().placeholder(row * width + column + 1));
|
|
8094
|
+
return `(${slots.join(", ")})`;
|
|
8095
|
+
});
|
|
8096
|
+
const params = [];
|
|
8097
|
+
for (const id of list)
|
|
8098
|
+
params.push(parentId, id, ...extraValues);
|
|
8099
|
+
await connection.unsafe(`INSERT INTO ${quoteIdentifier(this.relation.pivotTable)} (${quoteIdentifier(String(this.relation.foreignPivotKey))}, ${quoteIdentifier(String(this.relation.relatedPivotKey))}${extraColumns}) VALUES ${tuples.join(", ")}`, params);
|
|
8100
|
+
}
|
|
8101
|
+
async deletePivotRows(connection, list) {
|
|
8102
|
+
const parentId = this.parent.get(this.relation.parentKey);
|
|
8103
|
+
const placeholders = list.map((_, index) => currentSqlDialect().placeholder(index + 2));
|
|
8104
|
+
await connection.unsafe(`DELETE FROM ${quoteIdentifier(this.relation.pivotTable)} WHERE ${quoteIdentifier(String(this.relation.foreignPivotKey))} = ${currentSqlDialect().placeholder(1)} AND ${quoteIdentifier(String(this.relation.relatedPivotKey))} IN (${placeholders.join(", ")})`, [parentId, ...list]);
|
|
7981
8105
|
}
|
|
7982
8106
|
async sync(ids) {
|
|
7983
8107
|
await this.detach();
|
|
@@ -8568,10 +8692,20 @@ function applyCasts(values, casts, direction) {
|
|
|
8568
8692
|
return result;
|
|
8569
8693
|
}
|
|
8570
8694
|
return (async () => {
|
|
8695
|
+
const hashes = [];
|
|
8571
8696
|
for (const [key, cast] of entries) {
|
|
8572
|
-
if (key in result
|
|
8573
|
-
|
|
8697
|
+
if (!(key in result) || !cast)
|
|
8698
|
+
continue;
|
|
8699
|
+
if (cast === "hashed") {
|
|
8700
|
+
const current = result[key];
|
|
8701
|
+
hashes.push(hashCastValue(current).then((hashed) => {
|
|
8702
|
+
result[key] = hashed;
|
|
8703
|
+
}));
|
|
8704
|
+
continue;
|
|
8705
|
+
}
|
|
8706
|
+
result[key] = dehydrateValue(result[key], cast);
|
|
8574
8707
|
}
|
|
8708
|
+
await Promise.all(hashes);
|
|
8575
8709
|
return result;
|
|
8576
8710
|
})();
|
|
8577
8711
|
}
|
|
@@ -11352,7 +11486,16 @@ function createCorsMiddleware(options = {}) {
|
|
|
11352
11486
|
const response = await next();
|
|
11353
11487
|
const headers = new Headers(response.headers);
|
|
11354
11488
|
for (const [key, value] of buildCorsHeaders(request, captured)) {
|
|
11355
|
-
|
|
11489
|
+
if (key === "vary") {
|
|
11490
|
+
const fields = (headers.get("Vary") ?? "").split(",").map((field) => field.trim()).filter(Boolean);
|
|
11491
|
+
if (fields.includes("*"))
|
|
11492
|
+
continue;
|
|
11493
|
+
if (!fields.some((field) => field.toLowerCase() === "origin"))
|
|
11494
|
+
fields.push(value);
|
|
11495
|
+
headers.set("Vary", fields.join(", "));
|
|
11496
|
+
} else {
|
|
11497
|
+
headers.set(key, value);
|
|
11498
|
+
}
|
|
11356
11499
|
}
|
|
11357
11500
|
return new Response(response.body, {
|
|
11358
11501
|
status: response.status,
|
|
@@ -12390,6 +12533,48 @@ function withErrorHandling(handler) {
|
|
|
12390
12533
|
};
|
|
12391
12534
|
}
|
|
12392
12535
|
// ../../src/core/http/route.ts
|
|
12536
|
+
function resourceRoutes(path, actions) {
|
|
12537
|
+
if (!/^\/[A-Za-z0-9._~-]+(?:\/[A-Za-z0-9._~-]+)*$/.test(path) || path.split("/").some((part) => part === "." || part === "..")) {
|
|
12538
|
+
throw new TypeError("Resource path must be a static absolute path without a trailing slash.");
|
|
12539
|
+
}
|
|
12540
|
+
const routes = {};
|
|
12541
|
+
const definitions = {
|
|
12542
|
+
index: [path, ["GET"]],
|
|
12543
|
+
create: [`${path}/create`, ["GET"]],
|
|
12544
|
+
store: [path, ["POST"]],
|
|
12545
|
+
show: [`${path}/:id`, ["GET"]],
|
|
12546
|
+
edit: [`${path}/:id/edit`, ["GET"]],
|
|
12547
|
+
update: [`${path}/:id`, ["PUT", "PATCH"]],
|
|
12548
|
+
destroy: [`${path}/:id`, ["DELETE"]]
|
|
12549
|
+
};
|
|
12550
|
+
for (const [action, handler] of Object.entries(actions)) {
|
|
12551
|
+
if (!Object.hasOwn(definitions, action) || typeof handler !== "function") {
|
|
12552
|
+
throw new TypeError(`Invalid resource action: ${action}`);
|
|
12553
|
+
}
|
|
12554
|
+
const [route, methods] = definitions[action];
|
|
12555
|
+
const methodMap = routes[route] ?? {};
|
|
12556
|
+
routes[route] = methodMap;
|
|
12557
|
+
for (const method of methods)
|
|
12558
|
+
methodMap[method] = handler;
|
|
12559
|
+
}
|
|
12560
|
+
return routes;
|
|
12561
|
+
}
|
|
12562
|
+
function mergeRouteMaps(...maps) {
|
|
12563
|
+
const routes = {};
|
|
12564
|
+
for (const map of maps) {
|
|
12565
|
+
for (const [path, handler] of Object.entries(map)) {
|
|
12566
|
+
if (Object.hasOwn(routes, path))
|
|
12567
|
+
throw new Error(`Duplicate route path: ${path}`);
|
|
12568
|
+
Object.defineProperty(routes, path, {
|
|
12569
|
+
value: handler,
|
|
12570
|
+
enumerable: true,
|
|
12571
|
+
writable: true,
|
|
12572
|
+
configurable: true
|
|
12573
|
+
});
|
|
12574
|
+
}
|
|
12575
|
+
}
|
|
12576
|
+
return routes;
|
|
12577
|
+
}
|
|
12393
12578
|
function getRouteParams(request) {
|
|
12394
12579
|
return request.params;
|
|
12395
12580
|
}
|
|
@@ -14133,21 +14318,6 @@ redis.call('XDEL', KEYS[1], ARGV[2])
|
|
|
14133
14318
|
if #redis.call('XPENDING', KEYS[1], ARGV[1], '-', '+', 1, ARGV[3]) == 0 then redis.call('XGROUP', 'DELCONSUMER', KEYS[1], ARGV[1], ARGV[3]) end
|
|
14134
14319
|
return 1
|
|
14135
14320
|
`;
|
|
14136
|
-
var PROMOTE_RETRIES = `
|
|
14137
|
-
local kinds = {'stream','zset'}
|
|
14138
|
-
for i=1,2 do
|
|
14139
|
-
local kind = redis.call('TYPE', KEYS[i]).ok
|
|
14140
|
-
if kind ~= 'none' and kind ~= kinds[i] then return redis.error_reply('Invalid stream retry promotion key type') end
|
|
14141
|
-
end
|
|
14142
|
-
local now = redis.call('TIME')
|
|
14143
|
-
local due = tonumber(now[1])*1000 + math.floor(tonumber(now[2])/1000)
|
|
14144
|
-
local entries = redis.call('ZRANGE', KEYS[2], '-inf', due, 'BYSCORE', 'LIMIT', 0, 100)
|
|
14145
|
-
for _, payload in ipairs(entries) do
|
|
14146
|
-
redis.call('XADD', KEYS[1], '*', 'payload', payload)
|
|
14147
|
-
redis.call('ZREM', KEYS[2], payload)
|
|
14148
|
-
end
|
|
14149
|
-
return #entries
|
|
14150
|
-
`;
|
|
14151
14321
|
async function scheduleStreamRetry(client, key, reservation, envelope, delayMs) {
|
|
14152
14322
|
if (!Number.isSafeInteger(delayMs) || delayMs < 0 || delayMs > Number.MAX_SAFE_INTEGER / 2)
|
|
14153
14323
|
throw new Error("Invalid Streams retry delay");
|
|
@@ -14169,8 +14339,93 @@ async function scheduleStreamRetry(client, key, reservation, envelope, delayMs)
|
|
|
14169
14339
|
if (Number(scheduled) !== 1)
|
|
14170
14340
|
throw new Error("Queue lease lost while scheduling retry");
|
|
14171
14341
|
}
|
|
14172
|
-
|
|
14173
|
-
|
|
14342
|
+
var POLL_STREAM_PRIORITIES = `
|
|
14343
|
+
local group = ARGV[1]
|
|
14344
|
+
local owner = ARGV[2]
|
|
14345
|
+
local visibility = ARGV[3]
|
|
14346
|
+
local function payloadOf(fields)
|
|
14347
|
+
for index = 1, #fields, 2 do
|
|
14348
|
+
if fields[index] == 'payload' and type(fields[index + 1]) == 'string' then return fields[index + 1] end
|
|
14349
|
+
end
|
|
14350
|
+
return ''
|
|
14351
|
+
end
|
|
14352
|
+
local cursors = {}
|
|
14353
|
+
for i = 1, #KEYS / 2 do cursors[i] = ARGV[3 + i] end
|
|
14354
|
+
for i = 1, #KEYS / 2 do
|
|
14355
|
+
local stream = KEYS[(i - 1) * 2 + 1]
|
|
14356
|
+
local retries = KEYS[(i - 1) * 2 + 2]
|
|
14357
|
+
local cursor = ARGV[3 + i]
|
|
14358
|
+
local streamKind = redis.call('TYPE', stream).ok
|
|
14359
|
+
local retryKind = redis.call('TYPE', retries).ok
|
|
14360
|
+
if streamKind ~= 'none' and streamKind ~= 'stream' then return redis.error_reply('Invalid stream retry promotion key type') end
|
|
14361
|
+
if retryKind ~= 'none' and retryKind ~= 'zset' then return redis.error_reply('Invalid stream retry promotion key type') end
|
|
14362
|
+
if streamKind == 'stream' then
|
|
14363
|
+
local now = redis.call('TIME')
|
|
14364
|
+
local due = tonumber(now[1]) * 1000 + math.floor(tonumber(now[2]) / 1000)
|
|
14365
|
+
local ready = redis.call('ZRANGE', retries, '-inf', due, 'BYSCORE', 'LIMIT', 0, 100)
|
|
14366
|
+
for _, payload in ipairs(ready) do
|
|
14367
|
+
redis.call('XADD', stream, '*', 'payload', payload)
|
|
14368
|
+
redis.call('ZREM', retries, payload)
|
|
14369
|
+
end
|
|
14370
|
+
local window = redis.call('XPENDING', stream, group, cursor, '+', 10)
|
|
14371
|
+
local claimed = redis.call('XAUTOCLAIM', stream, group, owner, visibility, cursor, 'COUNT', 1)
|
|
14372
|
+
for _, entry in ipairs(window) do
|
|
14373
|
+
if entry[2] ~= owner and #redis.call('XPENDING', stream, group, '-', '+', 1, entry[2]) == 0 then
|
|
14374
|
+
redis.call('XGROUP', 'DELCONSUMER', stream, group, entry[2])
|
|
14375
|
+
end
|
|
14376
|
+
end
|
|
14377
|
+
if #redis.call('XPENDING', stream, group, '-', '+', 1, owner) == 0 then
|
|
14378
|
+
redis.call('XGROUP', 'DELCONSUMER', stream, group, owner)
|
|
14379
|
+
end
|
|
14380
|
+
cursors[i] = claimed[1]
|
|
14381
|
+
if #claimed[2] > 0 then
|
|
14382
|
+
return {'1', tostring(i), claimed[2][1][1], payloadOf(claimed[2][1][2]), unpack(cursors)}
|
|
14383
|
+
end
|
|
14384
|
+
local fresh = redis.call('XREADGROUP', 'GROUP', group, owner, 'COUNT', 1, 'STREAMS', stream, '>')
|
|
14385
|
+
if not fresh and #redis.call('XPENDING', stream, group, '-', '+', 1, owner) == 0 then
|
|
14386
|
+
redis.call('XGROUP', 'DELCONSUMER', stream, group, owner)
|
|
14387
|
+
end
|
|
14388
|
+
if fresh and #fresh > 0 and #fresh[1][2] > 0 then
|
|
14389
|
+
return {'1', tostring(i), fresh[1][2][1][1], payloadOf(fresh[1][2][1][2]), unpack(cursors)}
|
|
14390
|
+
end
|
|
14391
|
+
end
|
|
14392
|
+
end
|
|
14393
|
+
return {'0', unpack(cursors)}
|
|
14394
|
+
`;
|
|
14395
|
+
async function pollStreamPriorities(client, queueKeys, owner, cursors) {
|
|
14396
|
+
const keys = [];
|
|
14397
|
+
for (const key of queueKeys)
|
|
14398
|
+
keys.push(streamQueueKey(key), streamRetryKey(key));
|
|
14399
|
+
const raw = await client.send("EVAL", [
|
|
14400
|
+
POLL_STREAM_PRIORITIES,
|
|
14401
|
+
String(keys.length),
|
|
14402
|
+
...keys,
|
|
14403
|
+
STREAM_GROUP,
|
|
14404
|
+
owner,
|
|
14405
|
+
String(readQueueVisibilityMs()),
|
|
14406
|
+
...cursors
|
|
14407
|
+
]);
|
|
14408
|
+
if (!Array.isArray(raw))
|
|
14409
|
+
throw new Error("Invalid Redis stream poll response");
|
|
14410
|
+
const flag = String(raw[0]);
|
|
14411
|
+
const cursorCount = queueKeys.length;
|
|
14412
|
+
if (flag === "0") {
|
|
14413
|
+
if (raw.length !== cursorCount + 1)
|
|
14414
|
+
throw new Error("Invalid Redis stream poll response");
|
|
14415
|
+
return { cursors: raw.slice(1).map(String), hit: null };
|
|
14416
|
+
}
|
|
14417
|
+
if (flag !== "1" || raw.length !== cursorCount + 4)
|
|
14418
|
+
throw new Error("Invalid Redis stream poll response");
|
|
14419
|
+
const index = Number(raw[1]);
|
|
14420
|
+
if (!Number.isInteger(index) || index < 1 || index > queueKeys.length)
|
|
14421
|
+
throw new Error("Invalid Redis stream poll response");
|
|
14422
|
+
return {
|
|
14423
|
+
cursors: raw.slice(4).map(String),
|
|
14424
|
+
hit: {
|
|
14425
|
+
index: index - 1,
|
|
14426
|
+
reservation: { id: String(raw[2]), owner, payload: String(raw[3]) }
|
|
14427
|
+
}
|
|
14428
|
+
};
|
|
14174
14429
|
}
|
|
14175
14430
|
async function readStreamQueueDepth(client, key) {
|
|
14176
14431
|
return Number(await client.send("EVAL", [
|
|
@@ -14219,20 +14474,6 @@ end
|
|
|
14219
14474
|
if #redis.call('XPENDING', KEYS[1], ARGV[1], '-', '+', 1, ARGV[3]) == 0 then redis.call('XGROUP', 'DELCONSUMER', KEYS[1], ARGV[1], ARGV[3]) end
|
|
14220
14475
|
return acknowledged
|
|
14221
14476
|
`;
|
|
14222
|
-
var CLAIM_STREAM = `
|
|
14223
|
-
local window = redis.call('XPENDING', KEYS[1], ARGV[1], ARGV[4], '+', 10)
|
|
14224
|
-
local claimed = redis.call('XAUTOCLAIM', KEYS[1], ARGV[1], ARGV[2], ARGV[3], ARGV[4], 'COUNT', 1)
|
|
14225
|
-
for _, entry in ipairs(window) do
|
|
14226
|
-
if entry[2] ~= ARGV[2] and #redis.call('XPENDING', KEYS[1], ARGV[1], '-', '+', 1, entry[2]) == 0 then redis.call('XGROUP', 'DELCONSUMER', KEYS[1], ARGV[1], entry[2]) end
|
|
14227
|
-
end
|
|
14228
|
-
if #redis.call('XPENDING', KEYS[1], ARGV[1], '-', '+', 1, ARGV[2]) == 0 then redis.call('XGROUP', 'DELCONSUMER', KEYS[1], ARGV[1], ARGV[2]) end
|
|
14229
|
-
return claimed
|
|
14230
|
-
`;
|
|
14231
|
-
var READ_STREAM = `
|
|
14232
|
-
local entries = redis.call('XREADGROUP', 'GROUP', ARGV[1], ARGV[2], 'COUNT', 1, 'STREAMS', KEYS[1], '>')
|
|
14233
|
-
if not entries and #redis.call('XPENDING', KEYS[1], ARGV[1], '-', '+', 1, ARGV[2]) == 0 then redis.call('XGROUP', 'DELCONSUMER', KEYS[1], ARGV[1], ARGV[2]) end
|
|
14234
|
-
return entries
|
|
14235
|
-
`;
|
|
14236
14477
|
async function ensureStreamGroup(client, key) {
|
|
14237
14478
|
await assertLegacyDrained(client, key);
|
|
14238
14479
|
try {
|
|
@@ -14242,17 +14483,6 @@ async function ensureStreamGroup(client, key) {
|
|
|
14242
14483
|
throw error;
|
|
14243
14484
|
}
|
|
14244
14485
|
}
|
|
14245
|
-
function readStreamEntry(value, owner) {
|
|
14246
|
-
if (!Array.isArray(value) || typeof value[0] !== "string" || !Array.isArray(value[1]))
|
|
14247
|
-
throw new Error("Invalid Redis stream entry response");
|
|
14248
|
-
const fields = value[1];
|
|
14249
|
-
let payload;
|
|
14250
|
-
for (let index = 0;index < fields.length; index += 2) {
|
|
14251
|
-
if (fields[index] === "payload" && typeof fields[index + 1] === "string")
|
|
14252
|
-
payload = fields[index + 1];
|
|
14253
|
-
}
|
|
14254
|
-
return { id: value[0], owner, payload: payload ?? "" };
|
|
14255
|
-
}
|
|
14256
14486
|
async function updateStreamReservation(client, key, reservation, action, controlKey) {
|
|
14257
14487
|
return Number(await client.send("EVAL", [
|
|
14258
14488
|
UPDATE_STREAM,
|
|
@@ -14269,35 +14499,6 @@ async function updateStreamReservation(client, key, reservation, action, control
|
|
|
14269
14499
|
controlKey ? "1" : "0"
|
|
14270
14500
|
])) === 1;
|
|
14271
14501
|
}
|
|
14272
|
-
async function reserveStreamJob(client, key, owner, cursor = "0-0") {
|
|
14273
|
-
const claimed = await client.send("EVAL", [
|
|
14274
|
-
CLAIM_STREAM,
|
|
14275
|
-
"1",
|
|
14276
|
-
streamQueueKey(key),
|
|
14277
|
-
STREAM_GROUP,
|
|
14278
|
-
owner,
|
|
14279
|
-
String(readQueueVisibilityMs()),
|
|
14280
|
-
cursor
|
|
14281
|
-
]);
|
|
14282
|
-
if (!Array.isArray(claimed) || typeof claimed[0] !== "string" || !Array.isArray(claimed[1]))
|
|
14283
|
-
throw new Error("Invalid Redis XAUTOCLAIM response");
|
|
14284
|
-
const nextCursor = claimed[0];
|
|
14285
|
-
if (claimed[1].length > 0)
|
|
14286
|
-
return { reservation: readStreamEntry(claimed[1][0], owner), cursor: nextCursor };
|
|
14287
|
-
const fresh = await client.send("EVAL", [
|
|
14288
|
-
READ_STREAM,
|
|
14289
|
-
"1",
|
|
14290
|
-
streamQueueKey(key),
|
|
14291
|
-
STREAM_GROUP,
|
|
14292
|
-
owner
|
|
14293
|
-
]);
|
|
14294
|
-
if (fresh === null)
|
|
14295
|
-
return { reservation: null, cursor: nextCursor };
|
|
14296
|
-
if (!Array.isArray(fresh) || !Array.isArray(fresh[0]) || !Array.isArray(fresh[0][1]))
|
|
14297
|
-
throw new Error("Invalid Redis XREADGROUP response");
|
|
14298
|
-
return { reservation: readStreamEntry(fresh[0][1][0], owner), cursor: nextCursor };
|
|
14299
|
-
}
|
|
14300
|
-
|
|
14301
14502
|
class RedisStreamsQueue {
|
|
14302
14503
|
client;
|
|
14303
14504
|
constructor(redisUrl) {
|
|
@@ -14395,12 +14596,6 @@ class RedisStreamsWorker {
|
|
|
14395
14596
|
await ensureStreamGroup(this.client, key);
|
|
14396
14597
|
this.initialized = true;
|
|
14397
14598
|
}
|
|
14398
|
-
async reserve(key) {
|
|
14399
|
-
await promoteStreamRetries(this.client, key);
|
|
14400
|
-
const result = await reserveStreamJob(this.client, key, this.owner, this.cursors.get(key));
|
|
14401
|
-
this.cursors.set(key, result.cursor);
|
|
14402
|
-
return result.reservation;
|
|
14403
|
-
}
|
|
14404
14599
|
async processNext() {
|
|
14405
14600
|
if (this.processing)
|
|
14406
14601
|
throw new Error("Queue worker already processing a job.");
|
|
@@ -14417,13 +14612,19 @@ class RedisStreamsWorker {
|
|
|
14417
14612
|
await this.initialize();
|
|
14418
14613
|
const deadline = performance.now() + this.timeoutSeconds * 1000;
|
|
14419
14614
|
do {
|
|
14420
|
-
|
|
14421
|
-
|
|
14422
|
-
|
|
14423
|
-
|
|
14424
|
-
|
|
14425
|
-
|
|
14426
|
-
|
|
14615
|
+
if (this.stopping)
|
|
14616
|
+
return false;
|
|
14617
|
+
const polled = await pollStreamPriorities(this.client, this.queueKeys, this.owner, this.queueKeys.map((key) => this.cursors.get(key) ?? "0-0"));
|
|
14618
|
+
this.queueKeys.forEach((key, index) => {
|
|
14619
|
+
const cursor = polled.cursors[index];
|
|
14620
|
+
if (cursor)
|
|
14621
|
+
this.cursors.set(key, cursor);
|
|
14622
|
+
});
|
|
14623
|
+
if (polled.hit) {
|
|
14624
|
+
const key = this.queueKeys[polled.hit.index];
|
|
14625
|
+
if (!key)
|
|
14626
|
+
throw new Error("Invalid Redis stream poll response");
|
|
14627
|
+
await this.handle(key, polled.hit.reservation);
|
|
14427
14628
|
return true;
|
|
14428
14629
|
}
|
|
14429
14630
|
if (performance.now() >= deadline || this.stopping)
|
|
@@ -14650,25 +14851,69 @@ for _, record in ipairs(redis.call('LRANGE', KEYS[2], 0, -1)) do
|
|
|
14650
14851
|
end
|
|
14651
14852
|
return reclaimed
|
|
14652
14853
|
`;
|
|
14854
|
+
var POLL_LIST_PRIORITIES = `${SCRIPT_HEADER}
|
|
14855
|
+
local owner = ARGV[1]
|
|
14856
|
+
local visibility = ARGV[2]
|
|
14857
|
+
local reservationId = ARGV[3]
|
|
14858
|
+
local reclaim = ARGV[4] == '1'
|
|
14859
|
+
local function reclaimQueue(listKey, processingKey, leaseKey)
|
|
14860
|
+
local err = checkType(listKey, 'list') or checkType(processingKey, 'list') or checkType(leaseKey, 'hash')
|
|
14861
|
+
if err then return err end
|
|
14862
|
+
local now = nowMs()
|
|
14863
|
+
for _, record in ipairs(redis.call('LRANGE', processingKey, 0, -1)) do
|
|
14864
|
+
local payload, field = record, record
|
|
14865
|
+
local modern = false
|
|
14866
|
+
local ok, reservation = pcall(cjson.decode, record)
|
|
14867
|
+
if ok and type(reservation) == 'table' and reservation.strataReservation == 1 then
|
|
14868
|
+
payload, field, modern = reservation.payload, reservation.id, true
|
|
14869
|
+
end
|
|
14870
|
+
local raw = redis.call('HGET', leaseKey, field)
|
|
14871
|
+
local expired = true
|
|
14872
|
+
if raw then
|
|
14873
|
+
if modern then
|
|
14874
|
+
local leaseOk, lease = pcall(cjson.decode, raw)
|
|
14875
|
+
expired = not leaseOk or type(lease) ~= 'table' or not tonumber(lease.expiresAt) or tonumber(lease.expiresAt) <= now
|
|
14876
|
+
else
|
|
14877
|
+
local reservedAt = tonumber(raw)
|
|
14878
|
+
expired = not reservedAt or now - reservedAt >= tonumber(visibility)
|
|
14879
|
+
end
|
|
14880
|
+
end
|
|
14881
|
+
if expired then
|
|
14882
|
+
redis.call('LPUSH', listKey, payload)
|
|
14883
|
+
redis.call('LREM', processingKey, 1, record)
|
|
14884
|
+
redis.call('HDEL', leaseKey, field)
|
|
14885
|
+
end
|
|
14886
|
+
end
|
|
14887
|
+
end
|
|
14888
|
+
local function reserveQueue(listKey, processingKey, leaseKey)
|
|
14889
|
+
local err = checkType(listKey, 'list') or checkType(processingKey, 'list') or checkType(leaseKey, 'hash')
|
|
14890
|
+
if err then return err end
|
|
14891
|
+
local payload = redis.call('LINDEX', listKey, -1)
|
|
14892
|
+
if not payload then return nil end
|
|
14893
|
+
local record = cjson.encode({strataReservation=1, id=reservationId, owner=owner, payload=payload})
|
|
14894
|
+
local lease = cjson.encode({owner=owner, expiresAt=nowMs()+tonumber(visibility)})
|
|
14895
|
+
redis.call('LPUSH', processingKey, record)
|
|
14896
|
+
redis.call('HSET', leaseKey, reservationId, lease)
|
|
14897
|
+
redis.call('RPOP', listKey)
|
|
14898
|
+
return record
|
|
14899
|
+
end
|
|
14900
|
+
local count = #KEYS / 3
|
|
14901
|
+
if reclaim then
|
|
14902
|
+
for i = 1, count do
|
|
14903
|
+
local err = reclaimQueue(KEYS[(i - 1) * 3 + 1], KEYS[(i - 1) * 3 + 2], KEYS[(i - 1) * 3 + 3])
|
|
14904
|
+
if err then return err end
|
|
14905
|
+
end
|
|
14906
|
+
end
|
|
14907
|
+
for i = 1, count do
|
|
14908
|
+
local record = reserveQueue(KEYS[(i - 1) * 3 + 1], KEYS[(i - 1) * 3 + 2], KEYS[(i - 1) * 3 + 3])
|
|
14909
|
+
if type(record) == 'string' then return {'1', tostring(i), record} end
|
|
14910
|
+
if record then return record end
|
|
14911
|
+
end
|
|
14912
|
+
return {'0'}
|
|
14913
|
+
`;
|
|
14653
14914
|
function queueInvalidKey(queueKey) {
|
|
14654
14915
|
return `${queueKey}:invalid`;
|
|
14655
14916
|
}
|
|
14656
|
-
async function reserveQueueJob(client, queueKey, owner) {
|
|
14657
|
-
const raw = await client.send("EVAL", [
|
|
14658
|
-
RESERVE_JOB,
|
|
14659
|
-
"3",
|
|
14660
|
-
queueKey,
|
|
14661
|
-
queueProcessingKey(queueKey),
|
|
14662
|
-
queueProcessingLeaseKey(queueKey),
|
|
14663
|
-
crypto.randomUUID(),
|
|
14664
|
-
owner,
|
|
14665
|
-
String(readQueueVisibilityMs())
|
|
14666
|
-
]);
|
|
14667
|
-
if (typeof raw !== "string")
|
|
14668
|
-
return null;
|
|
14669
|
-
const reservation = JSON.parse(raw);
|
|
14670
|
-
return { ...reservation, record: raw };
|
|
14671
|
-
}
|
|
14672
14917
|
async function updateQueueReservation(client, queueKey, reservation, action) {
|
|
14673
14918
|
return Number(await client.send("EVAL", [
|
|
14674
14919
|
RENEW_OR_ACK,
|
|
@@ -14684,21 +14929,41 @@ async function updateQueueReservation(client, queueKey, reservation, action) {
|
|
|
14684
14929
|
reservation.payload
|
|
14685
14930
|
])) === 1;
|
|
14686
14931
|
}
|
|
14687
|
-
async function
|
|
14688
|
-
|
|
14932
|
+
async function pollListPriorities(client, queueKeys, owner, reservationId, reclaim) {
|
|
14933
|
+
const keys = [];
|
|
14689
14934
|
for (const key of queueKeys) {
|
|
14690
|
-
|
|
14691
|
-
RECLAIM_JOBS,
|
|
14692
|
-
"3",
|
|
14693
|
-
key,
|
|
14694
|
-
queueProcessingKey(key),
|
|
14695
|
-
queueProcessingLeaseKey(key),
|
|
14696
|
-
String(readQueueVisibilityMs())
|
|
14697
|
-
]));
|
|
14935
|
+
keys.push(key, queueProcessingKey(key), queueProcessingLeaseKey(key));
|
|
14698
14936
|
}
|
|
14699
|
-
|
|
14937
|
+
const raw = await client.send("EVAL", [
|
|
14938
|
+
POLL_LIST_PRIORITIES,
|
|
14939
|
+
String(keys.length),
|
|
14940
|
+
...keys,
|
|
14941
|
+
owner,
|
|
14942
|
+
String(readQueueVisibilityMs()),
|
|
14943
|
+
reservationId,
|
|
14944
|
+
reclaim ? "1" : "0"
|
|
14945
|
+
]);
|
|
14946
|
+
if (!Array.isArray(raw))
|
|
14947
|
+
throw new Error("Invalid Redis list poll response");
|
|
14948
|
+
const flag = String(raw[0]);
|
|
14949
|
+
if (flag === "0") {
|
|
14950
|
+
if (raw.length !== 1)
|
|
14951
|
+
throw new Error("Invalid Redis list poll response");
|
|
14952
|
+
return null;
|
|
14953
|
+
}
|
|
14954
|
+
if (flag !== "1" || raw.length !== 3)
|
|
14955
|
+
throw new Error("Invalid Redis list poll response");
|
|
14956
|
+
const index = Number(raw[1]);
|
|
14957
|
+
const record = raw[2];
|
|
14958
|
+
if (!Number.isInteger(index) || index < 1 || index > queueKeys.length || typeof record !== "string") {
|
|
14959
|
+
throw new Error("Invalid Redis list poll response");
|
|
14960
|
+
}
|
|
14961
|
+
const queueKey = queueKeys[index - 1];
|
|
14962
|
+
if (!queueKey)
|
|
14963
|
+
throw new Error("Invalid Redis list poll response");
|
|
14964
|
+
const reservation = JSON.parse(record);
|
|
14965
|
+
return { queueKey, reservation: { ...reservation, record } };
|
|
14700
14966
|
}
|
|
14701
|
-
|
|
14702
14967
|
class RedisQueue {
|
|
14703
14968
|
client;
|
|
14704
14969
|
constructor(redisUrl) {
|
|
@@ -14744,14 +15009,14 @@ class QueueWorker {
|
|
|
14744
15009
|
}
|
|
14745
15010
|
async reserve() {
|
|
14746
15011
|
const deadline = performance.now() + this.timeoutSeconds * 1000;
|
|
15012
|
+
let reclaim = true;
|
|
14747
15013
|
do {
|
|
14748
|
-
|
|
14749
|
-
|
|
14750
|
-
|
|
14751
|
-
|
|
14752
|
-
|
|
14753
|
-
|
|
14754
|
-
}
|
|
15014
|
+
if (this.stopping)
|
|
15015
|
+
return null;
|
|
15016
|
+
const reserved = await pollListPriorities(this.client, this.queueKeys, this.owner, crypto.randomUUID(), reclaim);
|
|
15017
|
+
reclaim = false;
|
|
15018
|
+
if (reserved)
|
|
15019
|
+
return reserved;
|
|
14755
15020
|
if (this.stopping || performance.now() >= deadline)
|
|
14756
15021
|
return null;
|
|
14757
15022
|
await Bun.sleep(Math.min(100, Math.max(1, deadline - performance.now())));
|
|
@@ -14761,7 +15026,6 @@ class QueueWorker {
|
|
|
14761
15026
|
async processNext() {
|
|
14762
15027
|
if (this.stopping)
|
|
14763
15028
|
return false;
|
|
14764
|
-
await reclaimExpiredQueueReservations(this.client, this.queueKeys);
|
|
14765
15029
|
const reserved = await this.reserve();
|
|
14766
15030
|
if (!reserved)
|
|
14767
15031
|
return false;
|
|
@@ -16119,6 +16383,11 @@ class EtaViewEngine {
|
|
|
16119
16383
|
}
|
|
16120
16384
|
}
|
|
16121
16385
|
// ../../src/core/view/viewRenderer.ts
|
|
16386
|
+
function validateRelativeViewName(name) {
|
|
16387
|
+
if (!/^[A-Za-z0-9._~-]+(?:\/[A-Za-z0-9._~-]+)*$/.test(name) || name.split("/").some((part) => part === "." || part === "..")) {
|
|
16388
|
+
throw new TypeError("View namespace and names must be relative template paths without dot segments.");
|
|
16389
|
+
}
|
|
16390
|
+
}
|
|
16122
16391
|
var HTMX_VARY_HEADERS = ["HX-Request", "HX-Boosted", "HX-History-Restore-Request"];
|
|
16123
16392
|
function varyByHtmx(headers) {
|
|
16124
16393
|
const existing = (headers.get("Vary") ?? "").split(",").map((field) => field.trim());
|
|
@@ -16139,6 +16408,20 @@ class ViewRenderer {
|
|
|
16139
16408
|
this.engine = engine;
|
|
16140
16409
|
this.defaultLayout = defaultLayout;
|
|
16141
16410
|
}
|
|
16411
|
+
namespace(prefix) {
|
|
16412
|
+
validateRelativeViewName(prefix);
|
|
16413
|
+
const qualify = (name) => {
|
|
16414
|
+
validateRelativeViewName(name);
|
|
16415
|
+
return `${prefix}/${name}`;
|
|
16416
|
+
};
|
|
16417
|
+
return {
|
|
16418
|
+
render: async (name, data, options) => this.render(qualify(name), data, options),
|
|
16419
|
+
renderHtmx: async (name, data, options = {}) => this.renderHtmx(qualify(name), data, {
|
|
16420
|
+
...options,
|
|
16421
|
+
...options.fragment === undefined ? {} : { fragment: qualify(options.fragment) }
|
|
16422
|
+
})
|
|
16423
|
+
};
|
|
16424
|
+
}
|
|
16142
16425
|
async render(name, data = {}, options = {}) {
|
|
16143
16426
|
const html = await this.engine.render(name, data, {
|
|
16144
16427
|
request: options.request,
|
|
@@ -16509,6 +16792,7 @@ export {
|
|
|
16509
16792
|
installGracefulShutdownSignals,
|
|
16510
16793
|
isBlockedHostname,
|
|
16511
16794
|
isBlockedIpAddress,
|
|
16795
|
+
isDefaultDatabaseConnection,
|
|
16512
16796
|
isEtagEnabled,
|
|
16513
16797
|
isGlobalAdmin,
|
|
16514
16798
|
isHtmxRequest,
|
|
@@ -16534,6 +16818,7 @@ export {
|
|
|
16534
16818
|
maxLength,
|
|
16535
16819
|
membershipContext,
|
|
16536
16820
|
membershipRepository,
|
|
16821
|
+
mergeRouteMaps,
|
|
16537
16822
|
migrateDatabase,
|
|
16538
16823
|
minLength,
|
|
16539
16824
|
modelEventName,
|
|
@@ -16636,6 +16921,7 @@ export {
|
|
|
16636
16921
|
resolveUserId,
|
|
16637
16922
|
resolveUserTenantId,
|
|
16638
16923
|
resolveWebLayoutData,
|
|
16924
|
+
resourceRoutes,
|
|
16639
16925
|
rollbackDatabase,
|
|
16640
16926
|
rssResponse,
|
|
16641
16927
|
runDueScheduledTasks,
|