@lunora/do 1.0.0-alpha.44 → 1.0.0-alpha.46
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/index.d.mts +473 -64
- package/dist/index.d.ts +473 -64
- package/dist/index.mjs +1 -44
- package/dist/packem_shared/ADMIN_FUNCTIONS-PxZ8Lr9e.mjs +1 -0
- package/dist/packem_shared/AGGREGATE_SQL_FUNCTION-QYaiuty4.mjs +1 -0
- package/dist/packem_shared/AUTH_METRICS_BUCKETS_TABLE-D0wNaez7.mjs +22 -0
- package/dist/packem_shared/CDC_LOG_TABLE-E_J5LPoK.mjs +8 -0
- package/dist/packem_shared/ConflictError-C8GtJmjS.mjs +1 -0
- package/dist/packem_shared/CountRlsUnsupportedError-Cl8XpYDL.mjs +1 -0
- package/dist/packem_shared/DATA_MIGRATION_STATE_TABLE-CaO6L0Ee.mjs +31 -0
- package/dist/packem_shared/DEFAULT_MAX_RELATION_KEYS-BRdmX7WW.mjs +1 -0
- package/dist/packem_shared/FUNCTION_METRICS_BUCKETS_TABLE-BPPeqM11.mjs +56 -0
- package/dist/packem_shared/GEO_DEFAULT_PRECISION-CWAm_oYY.mjs +1 -0
- package/dist/packem_shared/LogBuffer-bIvCelI-.mjs +1 -0
- package/dist/packem_shared/MAIL_RETENTION-KmozO2NQ.mjs +18 -0
- package/dist/packem_shared/MAX_SQL_ROWS-Bdu25ASB.mjs +2 -0
- package/dist/packem_shared/MIN_ADMIN_TOKEN_LENGTH-Ch6OW6_3.mjs +1 -0
- package/dist/packem_shared/NotFoundError-J3tjf4Uo.mjs +1 -0
- package/dist/packem_shared/NotUniqueError-BcP6GOem.mjs +1 -0
- package/dist/packem_shared/RANK_TIEBREAK-DtX8zQyc.mjs +1 -0
- package/dist/packem_shared/RLS_UNWRAP_SYMBOL-C6_WX3dG.mjs +1 -0
- package/dist/packem_shared/ROOT_DO_SIZE_WARN_BYTES-n4ds54Xa.mjs +101 -0
- package/dist/packem_shared/ReactiveCache-1_9Rs7J_.mjs +1 -0
- package/dist/packem_shared/SCAN_DEP-D_yR9EeV.mjs +1 -0
- package/dist/packem_shared/SESSION_DO_TTL_DEFAULT-GvBy_DBz.mjs +1 -0
- package/dist/packem_shared/SHARD_REGISTRY_DO_NAME-Caa0fR4N.mjs +1 -0
- package/dist/packem_shared/aggregateTableName-G-eXyjcz.mjs +1 -0
- package/dist/packem_shared/applyOnDelete-BvQN7pDL.mjs +1 -0
- package/dist/packem_shared/applySelect-Bq2KOrkL.mjs +1 -0
- package/dist/packem_shared/armRestore-BNzdvQ_o.mjs +1 -0
- package/dist/packem_shared/backfillAggregateIndexes-BAQ3Fwwh.mjs +1 -0
- package/dist/packem_shared/buildFtsMatch-CV0Z7PWv.mjs +1 -0
- package/dist/packem_shared/compileWhereSql-BLcfs4QW.mjs +1 -0
- package/dist/packem_shared/constant-time-equal-BVG05Guz.mjs +1 -0
- package/dist/packem_shared/context-telemetry-O3OwE5Zu.mjs +1 -0
- package/dist/packem_shared/createMetrics-tV8rYTGJ.mjs +1 -0
- package/dist/packem_shared/createSystemReader-DcDcrtM3.mjs +1 -0
- package/dist/packem_shared/ctx-db-idempotency-wiVoGnpQ.mjs +19 -0
- package/dist/packem_shared/ctx-db-shapes-DzX_H5q8.mjs +1 -0
- package/dist/packem_shared/diffExternalSource-DMpkJta1.mjs +1 -0
- package/dist/packem_shared/do-exec-BLe9lLrN.mjs +1 -0
- package/dist/packem_shared/do-sql-BYIQTG3z.mjs +1 -0
- package/dist/packem_shared/exportShardRows-kt42wijd.mjs +1 -0
- package/dist/packem_shared/hasTrigger-_rexbWMO.mjs +1 -0
- package/dist/packem_shared/isSoftDeleted-BvhQov04.mjs +9 -0
- package/dist/packem_shared/json-response-wrh9TBPw.mjs +1 -0
- package/dist/packem_shared/materializeExternalRows-BFmT9gsw.mjs +1 -0
- package/dist/packem_shared/renderSql-B5lF5Jd9.mjs +1 -0
- package/dist/packem_shared/runShardMigrations-bxOHpfID.mjs +5 -0
- package/dist/packem_shared/security-audit-BKUOgE0x.mjs +1 -0
- package/dist/packem_shared/selectExpiredIds-BGVP3d8-.mjs +1 -0
- package/dist/packem_shared/serialize-sql-DiRzL7A4.mjs +1 -0
- package/dist/packem_shared/serveRelationFanout-Ct5D2Tbk.mjs +1 -0
- package/dist/packem_shared/stableStringify-BjLh4gvA.mjs +1 -0
- package/dist/packem_shared/stableWireKey-YEHLaX6X.mjs +1 -0
- package/dist/packem_shared/subscriptionListDeltas-Bs69JbA8.mjs +1 -0
- package/dist/packem_shared/wire-codec-Ctnni0h6.mjs +1 -0
- package/package.json +1 -1
- package/dist/packem_shared/ADMIN_FUNCTIONS-CnZkbXH_.mjs +0 -374
- package/dist/packem_shared/AGGREGATE_SQL_FUNCTION-CQsu2Xga.mjs +0 -56
- package/dist/packem_shared/AUTH_METRICS_BUCKETS_TABLE-CiHHYeJi.mjs +0 -84
- package/dist/packem_shared/CDC_LOG_TABLE-uwOJxJJZ.mjs +0 -107
- package/dist/packem_shared/ConflictError-CLoq37xH.mjs +0 -12
- package/dist/packem_shared/CountRlsUnsupportedError-BGxj0pgS.mjs +0 -133
- package/dist/packem_shared/DATA_MIGRATION_STATE_TABLE-CYwBpyTr.mjs +0 -245
- package/dist/packem_shared/DEFAULT_MAX_RELATION_KEYS-BEan1CRD.mjs +0 -211
- package/dist/packem_shared/FUNCTION_METRICS_BUCKETS_TABLE-UDNVD7FS.mjs +0 -248
- package/dist/packem_shared/GEO_DEFAULT_PRECISION-BWnsNmpP.mjs +0 -114
- package/dist/packem_shared/LogBuffer-B_Ezju_N.mjs +0 -37
- package/dist/packem_shared/MAIL_RETENTION-CPpgl-dX.mjs +0 -104
- package/dist/packem_shared/MAX_SQL_ROWS-iFAA8FbD.mjs +0 -61
- package/dist/packem_shared/MIN_ADMIN_TOKEN_LENGTH-CCAvoFlr.mjs +0 -1
- package/dist/packem_shared/NotFoundError-C70b9hLw.mjs +0 -9
- package/dist/packem_shared/NotUniqueError-DbtlWwcG.mjs +0 -2031
- package/dist/packem_shared/RANK_TIEBREAK-CXhdcA1o.mjs +0 -91
- package/dist/packem_shared/RLS_UNWRAP_SYMBOL-DTvHvRzY.mjs +0 -141
- package/dist/packem_shared/ROOT_DO_SIZE_WARN_BYTES-B_zt4RQE.mjs +0 -7676
- package/dist/packem_shared/ReactiveCache-DnSvbjil.mjs +0 -233
- package/dist/packem_shared/SCAN_DEP-DLJF8dsj.mjs +0 -19
- package/dist/packem_shared/SESSION_DO_TTL_DEFAULT-BnSKgVO4.mjs +0 -169
- package/dist/packem_shared/SHARD_REGISTRY_DO_NAME-D99roc-r.mjs +0 -144
- package/dist/packem_shared/aggregateTableName-CxNqY1Sl.mjs +0 -64
- package/dist/packem_shared/applyOnDelete-BXSq3S70.mjs +0 -176
- package/dist/packem_shared/applySelect-WQY8m62C.mjs +0 -103
- package/dist/packem_shared/armRestore-4Px61hHS.mjs +0 -49
- package/dist/packem_shared/backfillAggregateIndexes-BGTwynwh.mjs +0 -81
- package/dist/packem_shared/buildFtsMatch-BLEMawrp.mjs +0 -38
- package/dist/packem_shared/compileWhereSql-DE6yfRcQ.mjs +0 -129
- package/dist/packem_shared/constant-time-equal-BVRWZgES.mjs +0 -12
- package/dist/packem_shared/context-telemetry-CDpyil58.mjs +0 -223
- package/dist/packem_shared/createMetrics-C4I2Yanj.mjs +0 -1
- package/dist/packem_shared/createSystemReader-D12eNH13.mjs +0 -82
- package/dist/packem_shared/ctx-db-idempotency-BdcNpvY4.mjs +0 -108
- package/dist/packem_shared/ctx-db-shapes-DTeFiHYS.mjs +0 -53
- package/dist/packem_shared/diffExternalSource-CovHfdyo.mjs +0 -44
- package/dist/packem_shared/do-exec-5eQy5cEi.mjs +0 -12
- package/dist/packem_shared/do-sql-CGAgiQUz.mjs +0 -88
- package/dist/packem_shared/exportShardRows-Dy3oFZ26.mjs +0 -157
- package/dist/packem_shared/hasTrigger-5N6_Fx0A.mjs +0 -20
- package/dist/packem_shared/isSoftDeleted-CFJmhjFP.mjs +0 -217
- package/dist/packem_shared/json-response-BdbtpOhm.mjs +0 -3
- package/dist/packem_shared/materializeExternalRows-BtEGs1Fv.mjs +0 -45
- package/dist/packem_shared/renderSql-D6eUcn2N.mjs +0 -16
- package/dist/packem_shared/runShardMigrations-BVax6rYu.mjs +0 -120
- package/dist/packem_shared/security-audit-CucgBice.mjs +0 -158
- package/dist/packem_shared/selectExpiredIds-C5W29Upb.mjs +0 -18
- package/dist/packem_shared/serialize-sql-BlRUoiQe.mjs +0 -14
- package/dist/packem_shared/serveRelationFanout-k7HTh3CC.mjs +0 -21
- package/dist/packem_shared/stableStringify-mC40mZts.mjs +0 -40
- package/dist/packem_shared/stableWireKey-DKuXO7T5.mjs +0 -6
- package/dist/packem_shared/subscriptionListDeltas-CT76bYny.mjs +0 -126
- package/dist/packem_shared/wire-codec-CzQc1pvf.mjs +0 -225
|
@@ -1,211 +0,0 @@
|
|
|
1
|
-
import { LunoraError } from '@lunora/errors';
|
|
2
|
-
import { distinctValues } from './applyOnDelete-BXSq3S70.mjs';
|
|
3
|
-
|
|
4
|
-
const RELATION_EXISTS_KEY = "__relationExists";
|
|
5
|
-
|
|
6
|
-
const RELATION_OPERATOR_META = {
|
|
7
|
-
every: { kind: "many", negateChild: true, negated: true },
|
|
8
|
-
is: { kind: "one", negated: false },
|
|
9
|
-
isNot: { kind: "one", negated: true, nullDisjunct: true },
|
|
10
|
-
none: { kind: "many", negated: true },
|
|
11
|
-
some: { kind: "many", negated: false }
|
|
12
|
-
};
|
|
13
|
-
const RELATION_OPERATORS = new Set(Object.keys(RELATION_OPERATOR_META));
|
|
14
|
-
const joinColumns = (relation) => relation.kind === "one" ? { clause: relation.field, project: relation.references } : { clause: relation.references, project: relation.field };
|
|
15
|
-
const DEFAULT_MAX_RELATION_KEYS = 5e3;
|
|
16
|
-
const KEY_OVERFLOW = /* @__PURE__ */ Symbol("relation-key-overflow");
|
|
17
|
-
const branchesOf = (value) => Array.isArray(value) ? value.map((branch) => branch ?? {}) : [];
|
|
18
|
-
const combineAnd = (clauses) => {
|
|
19
|
-
if (clauses.length === 1) {
|
|
20
|
-
const [only] = clauses;
|
|
21
|
-
return only ?? {};
|
|
22
|
-
}
|
|
23
|
-
return clauses.length === 0 ? {} : { AND: clauses };
|
|
24
|
-
};
|
|
25
|
-
const isRelationPredicate = (value) => {
|
|
26
|
-
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
27
|
-
return false;
|
|
28
|
-
}
|
|
29
|
-
const keys = Object.keys(value);
|
|
30
|
-
return keys.length > 0 && keys.every((key) => RELATION_OPERATORS.has(key));
|
|
31
|
-
};
|
|
32
|
-
const containsRelationPredicate = (where, schema, tableName) => {
|
|
33
|
-
const relationMap = schema.tables[tableName]?.relationMap ?? {};
|
|
34
|
-
return Object.keys(where).some((key) => {
|
|
35
|
-
const value = where[key];
|
|
36
|
-
if (key === "AND" || key === "OR") {
|
|
37
|
-
return branchesOf(value).some((branch) => containsRelationPredicate(branch, schema, tableName));
|
|
38
|
-
}
|
|
39
|
-
if (key === "NOT") {
|
|
40
|
-
return containsRelationPredicate(value ?? {}, schema, tableName);
|
|
41
|
-
}
|
|
42
|
-
return Boolean(relationMap[key]) && isRelationPredicate(value);
|
|
43
|
-
});
|
|
44
|
-
};
|
|
45
|
-
const assertFlatPredicate = (where, schema, tableName, op) => {
|
|
46
|
-
if (where && containsRelationPredicate(where, schema, tableName)) {
|
|
47
|
-
throw new LunoraError("INTERNAL", `relation-crossing predicates are not supported in ${op}() — use them in findMany/findFirst or an RLS read policy`);
|
|
48
|
-
}
|
|
49
|
-
};
|
|
50
|
-
const projectChildKeys = async (relation, childWhere, projectField, context, escalatable) => {
|
|
51
|
-
const resolvedChildWhere = await resolveForTable(childWhere, relation.table, context);
|
|
52
|
-
const { page } = await context.fetcher(relation.table, {
|
|
53
|
-
baseWhere: context.relationBaseWhere?.(relation.table),
|
|
54
|
-
relationBaseWhere: context.relationBaseWhere,
|
|
55
|
-
where: resolvedChildWhere
|
|
56
|
-
});
|
|
57
|
-
const keys = distinctValues(page, projectField);
|
|
58
|
-
if (keys.length > context.maxRelationKeys) {
|
|
59
|
-
if (escalatable) {
|
|
60
|
-
return KEY_OVERFLOW;
|
|
61
|
-
}
|
|
62
|
-
throw new LunoraError(
|
|
63
|
-
"INTERNAL",
|
|
64
|
-
`relation predicate on "${relation.table}" matched ${String(keys.length)} rows, exceeding the ${String(context.maxRelationKeys)}-key limit; narrow the predicate (a same-shard EXISTS push-down lifts this cap)`
|
|
65
|
-
);
|
|
66
|
-
}
|
|
67
|
-
return keys;
|
|
68
|
-
};
|
|
69
|
-
const compileOperator = async (operator, relation, childWhere, context, escalatable) => {
|
|
70
|
-
const meta = RELATION_OPERATOR_META[operator];
|
|
71
|
-
if (!meta) {
|
|
72
|
-
throw new LunoraError("INTERNAL", `unknown relation operator "${operator}"`);
|
|
73
|
-
}
|
|
74
|
-
const { clause, project } = joinColumns(relation);
|
|
75
|
-
const keys = await projectChildKeys(relation, meta.negateChild ? { NOT: childWhere } : childWhere, project, context, escalatable);
|
|
76
|
-
if (keys === KEY_OVERFLOW) {
|
|
77
|
-
return KEY_OVERFLOW;
|
|
78
|
-
}
|
|
79
|
-
if (!meta.negated) {
|
|
80
|
-
return { [clause]: { in: keys } };
|
|
81
|
-
}
|
|
82
|
-
if (meta.nullDisjunct) {
|
|
83
|
-
return { OR: [{ [clause]: { notIn: keys } }, { [clause]: { isNull: true } }] };
|
|
84
|
-
}
|
|
85
|
-
return { [clause]: { notIn: keys } };
|
|
86
|
-
};
|
|
87
|
-
const buildExistsMarker = async (operator, relation, childWhere, parentTable, context) => {
|
|
88
|
-
const meta = RELATION_OPERATOR_META[operator];
|
|
89
|
-
if (!meta) {
|
|
90
|
-
throw new LunoraError("INTERNAL", `unknown relation operator "${operator}"`);
|
|
91
|
-
}
|
|
92
|
-
const base = context.relationBaseWhere?.(relation.table);
|
|
93
|
-
const predicatePart = meta.negateChild ? { NOT: childWhere } : childWhere;
|
|
94
|
-
const merged = base ? { AND: [base, predicatePart] } : predicatePart;
|
|
95
|
-
const resolvedChild = await resolveForTable(merged, relation.table, context);
|
|
96
|
-
const marker = { childWhere: resolvedChild, negated: meta.negated, parentTable, relation };
|
|
97
|
-
return { [RELATION_EXISTS_KEY]: marker };
|
|
98
|
-
};
|
|
99
|
-
const assertCardinality = (operator, name, relation) => {
|
|
100
|
-
const meta = RELATION_OPERATOR_META[operator];
|
|
101
|
-
if (meta && meta.kind !== relation.kind) {
|
|
102
|
-
throw new LunoraError("INTERNAL", `relation operator "${operator}" requires a to-${meta.kind} relation, but "${name}" is to-${relation.kind}`);
|
|
103
|
-
}
|
|
104
|
-
};
|
|
105
|
-
const resolveRelationNode = async (name, relation, predicate, parentTable, context) => {
|
|
106
|
-
const clauses = [];
|
|
107
|
-
for (const operator of Object.keys(predicate)) {
|
|
108
|
-
assertCardinality(operator, name, relation);
|
|
109
|
-
const childWhere = predicate[operator] ?? {};
|
|
110
|
-
const pushable = context.canPushExists?.(relation) ?? false;
|
|
111
|
-
if (pushable && context.existsPushMode === "always") {
|
|
112
|
-
clauses.push(await buildExistsMarker(operator, relation, childWhere, parentTable, context));
|
|
113
|
-
continue;
|
|
114
|
-
}
|
|
115
|
-
const semijoin = await compileOperator(operator, relation, childWhere, context, pushable);
|
|
116
|
-
if (semijoin === KEY_OVERFLOW) {
|
|
117
|
-
clauses.push(await buildExistsMarker(operator, relation, childWhere, parentTable, context));
|
|
118
|
-
} else {
|
|
119
|
-
clauses.push(semijoin);
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
return combineAnd(clauses);
|
|
123
|
-
};
|
|
124
|
-
const resolveKey = async (key, value, tableName, context) => {
|
|
125
|
-
if (key === "AND" || key === "OR") {
|
|
126
|
-
const resolved = [];
|
|
127
|
-
for (const branch of branchesOf(value)) {
|
|
128
|
-
resolved.push(await resolveForTable(branch, tableName, context));
|
|
129
|
-
}
|
|
130
|
-
return { [key]: resolved };
|
|
131
|
-
}
|
|
132
|
-
if (key === "NOT") {
|
|
133
|
-
return { NOT: await resolveForTable(value ?? {}, tableName, context) };
|
|
134
|
-
}
|
|
135
|
-
const relation = context.schema.tables[tableName]?.relationMap?.[key];
|
|
136
|
-
if (relation && isRelationPredicate(value)) {
|
|
137
|
-
return resolveRelationNode(key, relation, value, tableName, context);
|
|
138
|
-
}
|
|
139
|
-
return { [key]: value };
|
|
140
|
-
};
|
|
141
|
-
const resolveForTable = async (where, tableName, context) => {
|
|
142
|
-
const clauses = [];
|
|
143
|
-
for (const key of Object.keys(where)) {
|
|
144
|
-
clauses.push(await resolveKey(key, where[key], tableName, context));
|
|
145
|
-
}
|
|
146
|
-
return combineAnd(clauses);
|
|
147
|
-
};
|
|
148
|
-
const resolveRelationPredicates = async (where, options) => {
|
|
149
|
-
if (!where || !containsRelationPredicate(where, options.schema, options.tableName)) {
|
|
150
|
-
return where;
|
|
151
|
-
}
|
|
152
|
-
return resolveForTable(where, options.tableName, {
|
|
153
|
-
canPushExists: options.canPushExists,
|
|
154
|
-
existsPushMode: options.existsPushMode ?? "auto",
|
|
155
|
-
fetcher: options.fetcher,
|
|
156
|
-
maxRelationKeys: options.maxRelationKeys ?? DEFAULT_MAX_RELATION_KEYS,
|
|
157
|
-
relationBaseWhere: options.relationBaseWhere,
|
|
158
|
-
schema: options.schema
|
|
159
|
-
});
|
|
160
|
-
};
|
|
161
|
-
const firstShardedHit = (branches, schema, tableName) => {
|
|
162
|
-
for (const branch of branches) {
|
|
163
|
-
const hit = findShardedRelationTarget(branch, schema, tableName);
|
|
164
|
-
if (hit) {
|
|
165
|
-
return hit;
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
return void 0;
|
|
169
|
-
};
|
|
170
|
-
const inspectKey = (key, value, schema, tableName) => {
|
|
171
|
-
if (key === "AND" || key === "OR") {
|
|
172
|
-
return firstShardedHit(branchesOf(value), schema, tableName);
|
|
173
|
-
}
|
|
174
|
-
if (key === "NOT") {
|
|
175
|
-
return firstShardedHit([value ?? {}], schema, tableName);
|
|
176
|
-
}
|
|
177
|
-
const relation = schema.tables[tableName]?.relationMap?.[key];
|
|
178
|
-
if (!relation || !isRelationPredicate(value)) {
|
|
179
|
-
return void 0;
|
|
180
|
-
}
|
|
181
|
-
if (schema.tables[relation.table]?.shardMode?.kind === "shardBy") {
|
|
182
|
-
return { relation: key, target: relation.table };
|
|
183
|
-
}
|
|
184
|
-
return firstShardedHit(Object.values(value), schema, relation.table);
|
|
185
|
-
};
|
|
186
|
-
const findShardedRelationTarget = (where, schema, tableName) => {
|
|
187
|
-
for (const key of Object.keys(where)) {
|
|
188
|
-
const hit = inspectKey(key, where[key], schema, tableName);
|
|
189
|
-
if (hit) {
|
|
190
|
-
return hit;
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
return void 0;
|
|
194
|
-
};
|
|
195
|
-
const assertShapeShardable = (effectiveWhere, schema, table) => {
|
|
196
|
-
if (!effectiveWhere) {
|
|
197
|
-
return;
|
|
198
|
-
}
|
|
199
|
-
const offending = findShardedRelationTarget(effectiveWhere, schema, table);
|
|
200
|
-
if (!offending) {
|
|
201
|
-
return;
|
|
202
|
-
}
|
|
203
|
-
throw Object.assign(
|
|
204
|
-
new Error(
|
|
205
|
-
`shape on "${table}" joins the sharded table "${offending.target}" via relation "${offending.relation}" — a live shape cannot replicate rows that live in another shard's Durable Object. Fix it by (a) denormalizing the joined columns into "${table}", or (b) moving "${offending.target}" to .global() so it is served through the latency-tiered D1 shape tier.`
|
|
206
|
-
),
|
|
207
|
-
{ code: "SHAPE_CROSS_SHARD_JOIN", name: "LunoraError", status: 400 }
|
|
208
|
-
);
|
|
209
|
-
};
|
|
210
|
-
|
|
211
|
-
export { DEFAULT_MAX_RELATION_KEYS, assertFlatPredicate, assertShapeShardable, containsRelationPredicate, isRelationPredicate, resolveRelationPredicates };
|
|
@@ -1,248 +0,0 @@
|
|
|
1
|
-
const FUNCTION_METRICS_TABLE = "__lunora_metrics";
|
|
2
|
-
const FUNCTION_METRICS_BUCKETS_TABLE = "__lunora_metrics_buckets";
|
|
3
|
-
const FUNCTION_METRICS_SCANS_TABLE = "__lunora_metrics_scans";
|
|
4
|
-
const FUNCTION_METRICS_INDEX_TABLE = "__lunora_metrics_index";
|
|
5
|
-
const FUNCTION_METRICS_BUCKET_MS = 6e4;
|
|
6
|
-
const FUNCTION_METRICS_BUCKET_RETENTION = 1440;
|
|
7
|
-
const FUNCTION_METRICS_MAX_PATHS = 5e3;
|
|
8
|
-
const FUNCTION_METRICS_READ_LIMIT = 1e3;
|
|
9
|
-
const runSql = (sql, query, ...params) => {
|
|
10
|
-
const runner = sql.exec;
|
|
11
|
-
return runner.call(sql, query, ...params);
|
|
12
|
-
};
|
|
13
|
-
const bucketFloor = (ts) => Math.floor(ts / FUNCTION_METRICS_BUCKET_MS) * FUNCTION_METRICS_BUCKET_MS;
|
|
14
|
-
const dedupeIndexHits = (hits) => {
|
|
15
|
-
const seen = /* @__PURE__ */ new Map();
|
|
16
|
-
for (const hit of hits) {
|
|
17
|
-
seen.set(`${hit.table}\0${hit.index}`, { index: hit.index, table: hit.table });
|
|
18
|
-
}
|
|
19
|
-
return [...seen.values()];
|
|
20
|
-
};
|
|
21
|
-
const ensureFunctionMetricsTables = (sql) => {
|
|
22
|
-
runSql(
|
|
23
|
-
sql,
|
|
24
|
-
`CREATE TABLE IF NOT EXISTS "${FUNCTION_METRICS_TABLE}" (
|
|
25
|
-
path TEXT PRIMARY KEY,
|
|
26
|
-
calls INTEGER NOT NULL DEFAULT 0,
|
|
27
|
-
errors INTEGER NOT NULL DEFAULT 0,
|
|
28
|
-
conflicts INTEGER NOT NULL DEFAULT 0,
|
|
29
|
-
scans INTEGER NOT NULL DEFAULT 0,
|
|
30
|
-
total_duration_ms REAL NOT NULL DEFAULT 0,
|
|
31
|
-
min_duration_ms REAL,
|
|
32
|
-
max_duration_ms REAL NOT NULL DEFAULT 0,
|
|
33
|
-
last_called_at REAL NOT NULL DEFAULT 0,
|
|
34
|
-
last_error_at REAL,
|
|
35
|
-
last_error_message TEXT
|
|
36
|
-
)`
|
|
37
|
-
);
|
|
38
|
-
for (const column of ["scans", "conflicts"]) {
|
|
39
|
-
try {
|
|
40
|
-
runSql(sql, `ALTER TABLE "${FUNCTION_METRICS_TABLE}" ADD COLUMN ${column} INTEGER NOT NULL DEFAULT 0`);
|
|
41
|
-
} catch {
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
runSql(
|
|
45
|
-
sql,
|
|
46
|
-
`CREATE TABLE IF NOT EXISTS "${FUNCTION_METRICS_BUCKETS_TABLE}" (
|
|
47
|
-
path TEXT NOT NULL,
|
|
48
|
-
bucket_ms INTEGER NOT NULL,
|
|
49
|
-
calls INTEGER NOT NULL DEFAULT 0,
|
|
50
|
-
errors INTEGER NOT NULL DEFAULT 0,
|
|
51
|
-
PRIMARY KEY (path, bucket_ms)
|
|
52
|
-
)`
|
|
53
|
-
);
|
|
54
|
-
runSql(
|
|
55
|
-
sql,
|
|
56
|
-
`CREATE TABLE IF NOT EXISTS "${FUNCTION_METRICS_SCANS_TABLE}" (
|
|
57
|
-
path TEXT NOT NULL,
|
|
58
|
-
table_name TEXT NOT NULL,
|
|
59
|
-
scans INTEGER NOT NULL DEFAULT 0,
|
|
60
|
-
PRIMARY KEY (path, table_name)
|
|
61
|
-
)`
|
|
62
|
-
);
|
|
63
|
-
runSql(
|
|
64
|
-
sql,
|
|
65
|
-
`CREATE TABLE IF NOT EXISTS "${FUNCTION_METRICS_INDEX_TABLE}" (
|
|
66
|
-
table_name TEXT NOT NULL,
|
|
67
|
-
index_name TEXT NOT NULL,
|
|
68
|
-
reads INTEGER NOT NULL DEFAULT 0,
|
|
69
|
-
PRIMARY KEY (table_name, index_name)
|
|
70
|
-
)`
|
|
71
|
-
);
|
|
72
|
-
};
|
|
73
|
-
const recordFunctionMetric = (sql, input) => {
|
|
74
|
-
ensureFunctionMetricsTables(sql);
|
|
75
|
-
const pathCountRow = runSql(sql, `SELECT COUNT(*) AS n FROM "${FUNCTION_METRICS_TABLE}"`).one();
|
|
76
|
-
if (pathCountRow.n >= FUNCTION_METRICS_MAX_PATHS) {
|
|
77
|
-
const tracked = runSql(sql, `SELECT COUNT(*) AS c FROM "${FUNCTION_METRICS_TABLE}" WHERE path = ?`, input.path).one();
|
|
78
|
-
if (tracked.c === 0) {
|
|
79
|
-
return;
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
const scannedTables = input.scannedTables ? [...new Set(input.scannedTables)] : [];
|
|
83
|
-
const scanCount = scannedTables.length;
|
|
84
|
-
const indexHits = dedupeIndexHits(input.indexHits ?? []);
|
|
85
|
-
const errorCount = input.errored ? 1 : 0;
|
|
86
|
-
const conflictCount = input.conflicted ? 1 : 0;
|
|
87
|
-
const lastErrorAt = input.errored ? input.ts : null;
|
|
88
|
-
const lastErrorMessage = input.errored ? input.errorMessage ?? null : null;
|
|
89
|
-
runSql(
|
|
90
|
-
sql,
|
|
91
|
-
`INSERT INTO "${FUNCTION_METRICS_TABLE}"
|
|
92
|
-
(path, calls, errors, conflicts, scans, total_duration_ms, min_duration_ms, max_duration_ms, last_called_at, last_error_at, last_error_message)
|
|
93
|
-
VALUES (?, 1, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
94
|
-
ON CONFLICT(path) DO UPDATE SET
|
|
95
|
-
calls = calls + 1,
|
|
96
|
-
errors = errors + excluded.errors,
|
|
97
|
-
conflicts = conflicts + excluded.conflicts,
|
|
98
|
-
scans = scans + excluded.scans,
|
|
99
|
-
total_duration_ms = total_duration_ms + excluded.total_duration_ms,
|
|
100
|
-
min_duration_ms = MIN(COALESCE(min_duration_ms, excluded.min_duration_ms), excluded.min_duration_ms),
|
|
101
|
-
max_duration_ms = MAX(max_duration_ms, excluded.max_duration_ms),
|
|
102
|
-
last_called_at = excluded.last_called_at,
|
|
103
|
-
last_error_at = CASE WHEN excluded.last_error_at IS NULL THEN last_error_at ELSE excluded.last_error_at END,
|
|
104
|
-
last_error_message = CASE WHEN excluded.last_error_at IS NULL THEN last_error_message ELSE excluded.last_error_message END`,
|
|
105
|
-
input.path,
|
|
106
|
-
errorCount,
|
|
107
|
-
conflictCount,
|
|
108
|
-
scanCount,
|
|
109
|
-
input.durationMs,
|
|
110
|
-
input.durationMs,
|
|
111
|
-
input.durationMs,
|
|
112
|
-
input.ts,
|
|
113
|
-
lastErrorAt,
|
|
114
|
-
lastErrorMessage
|
|
115
|
-
);
|
|
116
|
-
const bucket = bucketFloor(input.ts);
|
|
117
|
-
runSql(
|
|
118
|
-
sql,
|
|
119
|
-
`INSERT INTO "${FUNCTION_METRICS_BUCKETS_TABLE}" (path, bucket_ms, calls, errors)
|
|
120
|
-
VALUES (?, ?, 1, ?)
|
|
121
|
-
ON CONFLICT(path, bucket_ms) DO UPDATE SET
|
|
122
|
-
calls = calls + 1,
|
|
123
|
-
errors = errors + excluded.errors`,
|
|
124
|
-
input.path,
|
|
125
|
-
bucket,
|
|
126
|
-
errorCount
|
|
127
|
-
);
|
|
128
|
-
runSql(
|
|
129
|
-
sql,
|
|
130
|
-
`DELETE FROM "${FUNCTION_METRICS_BUCKETS_TABLE}"
|
|
131
|
-
WHERE path = ?
|
|
132
|
-
AND bucket_ms <= (
|
|
133
|
-
SELECT MAX(bucket_ms) - ? FROM "${FUNCTION_METRICS_BUCKETS_TABLE}" WHERE path = ?
|
|
134
|
-
)`,
|
|
135
|
-
input.path,
|
|
136
|
-
FUNCTION_METRICS_BUCKET_RETENTION * FUNCTION_METRICS_BUCKET_MS,
|
|
137
|
-
input.path
|
|
138
|
-
);
|
|
139
|
-
for (const table of scannedTables) {
|
|
140
|
-
runSql(
|
|
141
|
-
sql,
|
|
142
|
-
`INSERT INTO "${FUNCTION_METRICS_SCANS_TABLE}" (path, table_name, scans)
|
|
143
|
-
VALUES (?, ?, 1)
|
|
144
|
-
ON CONFLICT(path, table_name) DO UPDATE SET
|
|
145
|
-
scans = scans + 1`,
|
|
146
|
-
input.path,
|
|
147
|
-
table
|
|
148
|
-
);
|
|
149
|
-
}
|
|
150
|
-
for (const hit of indexHits) {
|
|
151
|
-
runSql(
|
|
152
|
-
sql,
|
|
153
|
-
`INSERT INTO "${FUNCTION_METRICS_INDEX_TABLE}" (table_name, index_name, reads)
|
|
154
|
-
VALUES (?, ?, 1)
|
|
155
|
-
ON CONFLICT(table_name, index_name) DO UPDATE SET
|
|
156
|
-
reads = reads + 1`,
|
|
157
|
-
hit.table,
|
|
158
|
-
hit.index
|
|
159
|
-
);
|
|
160
|
-
}
|
|
161
|
-
};
|
|
162
|
-
const readFunctionMetricScans = (sql) => {
|
|
163
|
-
ensureFunctionMetricsTables(sql);
|
|
164
|
-
const rows = runSql(
|
|
165
|
-
sql,
|
|
166
|
-
// Bounded read: cap the materialized rows so a bloated attribution table
|
|
167
|
-
// can't blow up DO memory. Highest-scan rows lead so the dominant
|
|
168
|
-
// attributions survive the cut.
|
|
169
|
-
`SELECT path, table_name, scans FROM "${FUNCTION_METRICS_SCANS_TABLE}" ORDER BY scans DESC, path ASC, table_name ASC LIMIT ${String(FUNCTION_METRICS_READ_LIMIT)}`
|
|
170
|
-
).toArray();
|
|
171
|
-
const byPath = /* @__PURE__ */ new Map();
|
|
172
|
-
for (const row of rows) {
|
|
173
|
-
const list = byPath.get(row.path);
|
|
174
|
-
const entry = { scans: row.scans, table: row.table_name };
|
|
175
|
-
if (list === void 0) {
|
|
176
|
-
byPath.set(row.path, [entry]);
|
|
177
|
-
} else {
|
|
178
|
-
list.push(entry);
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
return byPath;
|
|
182
|
-
};
|
|
183
|
-
const readFunctionMetricIndexHits = (sql) => {
|
|
184
|
-
ensureFunctionMetricsTables(sql);
|
|
185
|
-
const rows = runSql(
|
|
186
|
-
sql,
|
|
187
|
-
`SELECT table_name, index_name, reads FROM "${FUNCTION_METRICS_INDEX_TABLE}" ORDER BY table_name ASC, index_name ASC`
|
|
188
|
-
).toArray();
|
|
189
|
-
return rows.map((row) => {
|
|
190
|
-
return { index: row.index_name, reads: row.reads, table: row.table_name };
|
|
191
|
-
});
|
|
192
|
-
};
|
|
193
|
-
const mergeScanAttribution = (into, scanned) => {
|
|
194
|
-
for (const table of scanned) {
|
|
195
|
-
const entry = into.find((attribution) => attribution.table === table);
|
|
196
|
-
if (entry === void 0) {
|
|
197
|
-
into.push({ scans: 1, table });
|
|
198
|
-
} else {
|
|
199
|
-
entry.scans += 1;
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
into.sort((a, b) => b.scans - a.scans || a.table.localeCompare(b.table));
|
|
203
|
-
return into;
|
|
204
|
-
};
|
|
205
|
-
const readFunctionMetrics = (sql) => {
|
|
206
|
-
ensureFunctionMetricsTables(sql);
|
|
207
|
-
const scansByPath = readFunctionMetricScans(sql);
|
|
208
|
-
const rows = runSql(sql, `SELECT * FROM "${FUNCTION_METRICS_TABLE}" ORDER BY last_called_at DESC LIMIT ${String(FUNCTION_METRICS_READ_LIMIT)}`).toArray();
|
|
209
|
-
return rows.map((row) => {
|
|
210
|
-
return {
|
|
211
|
-
calls: row.calls,
|
|
212
|
-
conflicts: row.conflicts,
|
|
213
|
-
errors: row.errors,
|
|
214
|
-
lastCalledAt: row.last_called_at,
|
|
215
|
-
lastErrorAt: row.last_error_at,
|
|
216
|
-
lastErrorMessage: row.last_error_message,
|
|
217
|
-
maxDurationMs: row.max_duration_ms,
|
|
218
|
-
path: row.path,
|
|
219
|
-
scannedTables: scansByPath.get(row.path) ?? [],
|
|
220
|
-
scans: row.scans,
|
|
221
|
-
totalDurationMs: row.total_duration_ms
|
|
222
|
-
};
|
|
223
|
-
});
|
|
224
|
-
};
|
|
225
|
-
const readFunctionMetricBuckets = (sql, path) => {
|
|
226
|
-
ensureFunctionMetricsTables(sql);
|
|
227
|
-
const rows = path === void 0 ? runSql(
|
|
228
|
-
sql,
|
|
229
|
-
`SELECT path, bucket_ms, calls, errors FROM "${FUNCTION_METRICS_BUCKETS_TABLE}" ORDER BY bucket_ms ASC, path ASC`
|
|
230
|
-
).toArray() : runSql(
|
|
231
|
-
sql,
|
|
232
|
-
`SELECT path, bucket_ms, calls, errors FROM "${FUNCTION_METRICS_BUCKETS_TABLE}" WHERE path = ? ORDER BY bucket_ms ASC`,
|
|
233
|
-
path
|
|
234
|
-
).toArray();
|
|
235
|
-
return rows.map((row) => {
|
|
236
|
-
return { bucketMs: row.bucket_ms, calls: row.calls, errors: row.errors, path: row.path };
|
|
237
|
-
});
|
|
238
|
-
};
|
|
239
|
-
const readFunctionMetricsTotals = (sql) => {
|
|
240
|
-
ensureFunctionMetricsTables(sql);
|
|
241
|
-
const row = runSql(
|
|
242
|
-
sql,
|
|
243
|
-
`SELECT SUM(calls) AS requests, SUM(errors) AS errors FROM "${FUNCTION_METRICS_TABLE}"`
|
|
244
|
-
).one();
|
|
245
|
-
return { errors: row.errors ?? 0, requests: row.requests ?? 0 };
|
|
246
|
-
};
|
|
247
|
-
|
|
248
|
-
export { FUNCTION_METRICS_BUCKETS_TABLE, FUNCTION_METRICS_BUCKET_MS, FUNCTION_METRICS_BUCKET_RETENTION, FUNCTION_METRICS_INDEX_TABLE, FUNCTION_METRICS_MAX_PATHS, FUNCTION_METRICS_READ_LIMIT, FUNCTION_METRICS_SCANS_TABLE, FUNCTION_METRICS_TABLE, ensureFunctionMetricsTables, mergeScanAttribution, readFunctionMetricBuckets, readFunctionMetricIndexHits, readFunctionMetricScans, readFunctionMetrics, readFunctionMetricsTotals, recordFunctionMetric };
|
|
@@ -1,114 +0,0 @@
|
|
|
1
|
-
const BASE32 = "0123456789bcdefghjkmnpqrstuvwxyz";
|
|
2
|
-
const GEO_DEFAULT_PRECISION = 9;
|
|
3
|
-
const EARTH_RADIUS_METERS = 63710088e-1;
|
|
4
|
-
const CELL_WIDTH_METERS = [Number.POSITIVE_INFINITY, 5009400, 1252300, 156500, 39100, 4900, 1200, 152.9, 38.2, 4.77, 1.19, 0.149, 0.037];
|
|
5
|
-
const clampPrecision = (precision) => Math.min(Math.max(Math.trunc(precision), 1), 12);
|
|
6
|
-
const encodeGeohash = (point, precision) => {
|
|
7
|
-
const chars = clampPrecision(precision);
|
|
8
|
-
let latMin = -90;
|
|
9
|
-
let latMax = 90;
|
|
10
|
-
let lngMin = -180;
|
|
11
|
-
let lngMax = 180;
|
|
12
|
-
let hash = "";
|
|
13
|
-
let bit = 0;
|
|
14
|
-
let index = 0;
|
|
15
|
-
let even = true;
|
|
16
|
-
while (hash.length < chars) {
|
|
17
|
-
if (even) {
|
|
18
|
-
const mid = (lngMin + lngMax) / 2;
|
|
19
|
-
if (point.lng >= mid) {
|
|
20
|
-
index = index * 2 + 1;
|
|
21
|
-
lngMin = mid;
|
|
22
|
-
} else {
|
|
23
|
-
index *= 2;
|
|
24
|
-
lngMax = mid;
|
|
25
|
-
}
|
|
26
|
-
} else {
|
|
27
|
-
const mid = (latMin + latMax) / 2;
|
|
28
|
-
if (point.lat >= mid) {
|
|
29
|
-
index = index * 2 + 1;
|
|
30
|
-
latMin = mid;
|
|
31
|
-
} else {
|
|
32
|
-
index *= 2;
|
|
33
|
-
latMax = mid;
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
even = !even;
|
|
37
|
-
if (bit < 4) {
|
|
38
|
-
bit += 1;
|
|
39
|
-
} else {
|
|
40
|
-
hash += BASE32[index] ?? "0";
|
|
41
|
-
bit = 0;
|
|
42
|
-
index = 0;
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
return hash;
|
|
46
|
-
};
|
|
47
|
-
const haversineMeters = (a, b) => {
|
|
48
|
-
const toRadians = (degrees) => degrees * Math.PI / 180;
|
|
49
|
-
const dLat = toRadians(b.lat - a.lat);
|
|
50
|
-
const dLng = toRadians(b.lng - a.lng);
|
|
51
|
-
const lat1 = toRadians(a.lat);
|
|
52
|
-
const lat2 = toRadians(b.lat);
|
|
53
|
-
const h = Math.sin(dLat / 2) ** 2 + Math.cos(lat1) * Math.cos(lat2) * Math.sin(dLng / 2) ** 2;
|
|
54
|
-
return 2 * EARTH_RADIUS_METERS * Math.asin(Math.min(1, Math.sqrt(h)));
|
|
55
|
-
};
|
|
56
|
-
const precisionForRadius = (radiusMeters) => {
|
|
57
|
-
for (let length = CELL_WIDTH_METERS.length - 1; length >= 1; length -= 1) {
|
|
58
|
-
const width = CELL_WIDTH_METERS[length];
|
|
59
|
-
if (width !== void 0 && width >= radiusMeters) {
|
|
60
|
-
return length;
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
return 1;
|
|
64
|
-
};
|
|
65
|
-
const NEIGHBOURS = {
|
|
66
|
-
east: ["bc01fg45238967deuvhjyznpkmstqrwx", "bc01fg45238967deuvhjyznpkmstqrwx"],
|
|
67
|
-
north: ["p0r21436x8zb9dcf5h7kjnmqesgutwvy", "bc01fg45238967deuvhjyznpkmstqrwx"],
|
|
68
|
-
south: ["14365h7k9dcfesgujnmqp0r2twvyx8zb", "238967debc01fg45kmstqrwxuvhjyznp"],
|
|
69
|
-
west: ["238967debc01fg45kmstqrwxuvhjyznp", "238967debc01fg45kmstqrwxuvhjyznp"]
|
|
70
|
-
};
|
|
71
|
-
const BORDERS = {
|
|
72
|
-
east: ["bcfguvyz", "prxz"],
|
|
73
|
-
north: ["prxz", "bcfguvyz"],
|
|
74
|
-
south: ["028b", "0145hjnp"],
|
|
75
|
-
west: ["0145hjnp", "028b"]
|
|
76
|
-
};
|
|
77
|
-
const adjacent = (hash, direction) => {
|
|
78
|
-
const lower = hash.toLowerCase();
|
|
79
|
-
const lastChar = lower.at(-1) ?? "";
|
|
80
|
-
let base = lower.slice(0, -1);
|
|
81
|
-
const type = lower.length % 2 === 0 ? 0 : 1;
|
|
82
|
-
if (BORDERS[direction][type].includes(lastChar) && base !== "") {
|
|
83
|
-
base = adjacent(base, direction);
|
|
84
|
-
}
|
|
85
|
-
return base + (BASE32[NEIGHBOURS[direction][type].indexOf(lastChar)] ?? "");
|
|
86
|
-
};
|
|
87
|
-
const coveringGeohashes = (center, radiusMeters) => {
|
|
88
|
-
const precision = precisionForRadius(Math.max(radiusMeters, 1));
|
|
89
|
-
const origin = encodeGeohash(center, precision);
|
|
90
|
-
const north = adjacent(origin, "north");
|
|
91
|
-
const south = adjacent(origin, "south");
|
|
92
|
-
const cells = [
|
|
93
|
-
origin,
|
|
94
|
-
north,
|
|
95
|
-
south,
|
|
96
|
-
adjacent(origin, "east"),
|
|
97
|
-
adjacent(origin, "west"),
|
|
98
|
-
adjacent(north, "east"),
|
|
99
|
-
adjacent(north, "west"),
|
|
100
|
-
adjacent(south, "east"),
|
|
101
|
-
adjacent(south, "west")
|
|
102
|
-
];
|
|
103
|
-
return [...new Set(cells)];
|
|
104
|
-
};
|
|
105
|
-
const pointInBoundingBox = (point, box) => point.lat >= box.sw.lat && point.lat <= box.ne.lat && point.lng >= box.sw.lng && point.lng <= box.ne.lng;
|
|
106
|
-
const boundingBoxCenter = (box) => {
|
|
107
|
-
return { lat: (box.sw.lat + box.ne.lat) / 2, lng: (box.sw.lng + box.ne.lng) / 2 };
|
|
108
|
-
};
|
|
109
|
-
const boundingBoxGeohashes = (box) => {
|
|
110
|
-
const center = boundingBoxCenter(box);
|
|
111
|
-
return coveringGeohashes(center, Math.max(haversineMeters(center, box.ne), 1));
|
|
112
|
-
};
|
|
113
|
-
|
|
114
|
-
export { GEO_DEFAULT_PRECISION, boundingBoxCenter, boundingBoxGeohashes, coveringGeohashes, encodeGeohash, haversineMeters, pointInBoundingBox };
|
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
const DEFAULT_CAPACITY = 500;
|
|
2
|
-
class LogBuffer {
|
|
3
|
-
/** Backing store, kept in insertion order (oldest first). */
|
|
4
|
-
buffer = [];
|
|
5
|
-
capacity;
|
|
6
|
-
constructor(capacity = DEFAULT_CAPACITY) {
|
|
7
|
-
this.capacity = capacity > 0 ? Math.trunc(capacity) : DEFAULT_CAPACITY;
|
|
8
|
-
}
|
|
9
|
-
/** Number of entries currently buffered. */
|
|
10
|
-
get size() {
|
|
11
|
-
return this.buffer.length;
|
|
12
|
-
}
|
|
13
|
-
/** Drop every buffered entry. */
|
|
14
|
-
clear() {
|
|
15
|
-
this.buffer.length = 0;
|
|
16
|
-
}
|
|
17
|
-
/**
|
|
18
|
-
* Snapshot of the buffered entries, **newest first** so the panel renders
|
|
19
|
-
* the most recent activity at the top without re-sorting. Returns a fresh
|
|
20
|
-
* array each call; the caller may mutate it freely.
|
|
21
|
-
*/
|
|
22
|
-
entries() {
|
|
23
|
-
return this.buffer.toReversed();
|
|
24
|
-
}
|
|
25
|
-
/**
|
|
26
|
-
* Append an entry, evicting the oldest when at capacity so the buffer never
|
|
27
|
-
* grows past its bound.
|
|
28
|
-
*/
|
|
29
|
-
push(entry) {
|
|
30
|
-
this.buffer.push(entry);
|
|
31
|
-
if (this.buffer.length > this.capacity) {
|
|
32
|
-
this.buffer.shift();
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export { LogBuffer };
|