dynamodb-reactive 0.1.21 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +378 -153
- package/dist/client-r_tnzwqi.d.ts +224 -0
- package/dist/client.cjs +1023 -0
- package/dist/client.cjs.map +1 -0
- package/dist/client.d.cts +6 -0
- package/dist/client.d.ts +4 -70
- package/dist/client.js +1013 -1
- package/dist/client.js.map +1 -1
- package/dist/core.cjs +232 -0
- package/dist/core.cjs.map +1 -0
- package/dist/core.d.cts +4 -0
- package/dist/core.d.ts +3 -3
- package/dist/core.js +222 -1
- package/dist/core.js.map +1 -1
- package/dist/index.cjs +232 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +157 -0
- package/dist/index.d.ts +103 -56
- package/dist/index.js +222 -1
- package/dist/index.js.map +1 -1
- package/dist/infra.cjs +623 -0
- package/dist/infra.cjs.map +1 -0
- package/dist/infra.d.cts +119 -0
- package/dist/infra.d.ts +50 -102
- package/dist/infra.js +371 -144
- package/dist/infra.js.map +1 -1
- package/dist/react.cjs +1250 -0
- package/dist/react.cjs.map +1 -0
- package/dist/react.d.cts +68 -0
- package/dist/react.d.ts +68 -3
- package/dist/react.js +1237 -1
- package/dist/react.js.map +1 -1
- package/dist/server.cjs +2351 -0
- package/dist/server.cjs.map +1 -0
- package/dist/server.d.cts +364 -0
- package/dist/server.d.ts +58 -313
- package/dist/server.js +1855 -1208
- package/dist/server.js.map +1 -1
- package/dist/{table-tN3D4Tm-.d.ts → table-BijJ73pX.d.ts} +2 -34
- package/dist/{types-ISMJfyIv.d.ts → types-LMLk8JVC.d.ts} +56 -30
- package/package.json +79 -41
- package/dist/chunk-2A7GJ2UO.js +0 -1088
- package/dist/chunk-2A7GJ2UO.js.map +0 -1
- package/dist/chunk-ZAJA4KBE.js +0 -150
- package/dist/chunk-ZAJA4KBE.js.map +0 -1
- package/dist/react-B2b5RIz_.d.ts +0 -549
package/dist/server.cjs
ADDED
|
@@ -0,0 +1,2351 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var clientDynamodb = require('@aws-sdk/client-dynamodb');
|
|
4
|
+
var libDynamodb = require('@aws-sdk/lib-dynamodb');
|
|
5
|
+
var crypto = require('crypto');
|
|
6
|
+
var zod = require('zod');
|
|
7
|
+
var utilDynamodb = require('@aws-sdk/util-dynamodb');
|
|
8
|
+
var util = require('util');
|
|
9
|
+
var clientApigatewaymanagementapi = require('@aws-sdk/client-apigatewaymanagementapi');
|
|
10
|
+
var jsonpatch = require('fast-json-patch');
|
|
11
|
+
|
|
12
|
+
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
13
|
+
|
|
14
|
+
var jsonpatch__default = /*#__PURE__*/_interopDefault(jsonpatch);
|
|
15
|
+
|
|
16
|
+
// ../server/src/errors.ts
|
|
17
|
+
var SafeRequestError = class extends Error {
|
|
18
|
+
statusCode;
|
|
19
|
+
constructor(message, statusCode) {
|
|
20
|
+
super(message);
|
|
21
|
+
this.name = "SafeRequestError";
|
|
22
|
+
this.statusCode = statusCode;
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
var InvalidInputError = class extends SafeRequestError {
|
|
26
|
+
constructor() {
|
|
27
|
+
super("Invalid input", 400);
|
|
28
|
+
this.name = "InvalidInputError";
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
function isSafeRequestError(error2) {
|
|
32
|
+
return error2 instanceof SafeRequestError;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// ../server/src/procedure.ts
|
|
36
|
+
var ProcedureBuilder = class _ProcedureBuilder {
|
|
37
|
+
inputSchema;
|
|
38
|
+
constructor(inputSchema) {
|
|
39
|
+
this.inputSchema = inputSchema;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Define the input schema for the procedure
|
|
43
|
+
*/
|
|
44
|
+
input(schema) {
|
|
45
|
+
return new _ProcedureBuilder(schema);
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Define a query procedure (read-only operation)
|
|
49
|
+
*/
|
|
50
|
+
query(resolver) {
|
|
51
|
+
return {
|
|
52
|
+
type: "query",
|
|
53
|
+
inputSchema: this.inputSchema,
|
|
54
|
+
resolver
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Define a mutation procedure (write operation)
|
|
59
|
+
*/
|
|
60
|
+
mutation(resolver) {
|
|
61
|
+
return {
|
|
62
|
+
type: "mutation",
|
|
63
|
+
inputSchema: this.inputSchema,
|
|
64
|
+
resolver
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
function isProcedure(value) {
|
|
69
|
+
return typeof value === "object" && value !== null && "type" in value && "resolver" in value && (value.type === "query" || value.type === "mutation");
|
|
70
|
+
}
|
|
71
|
+
async function executeProcedure(procedure, ctx, rawInput) {
|
|
72
|
+
let input;
|
|
73
|
+
if (procedure.inputSchema) {
|
|
74
|
+
const parseResult = procedure.inputSchema.safeParse(rawInput);
|
|
75
|
+
if (!parseResult.success) {
|
|
76
|
+
throw new InvalidInputError();
|
|
77
|
+
}
|
|
78
|
+
input = parseResult.data;
|
|
79
|
+
} else {
|
|
80
|
+
input = rawInput;
|
|
81
|
+
}
|
|
82
|
+
return procedure.resolver({ ctx, input });
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// ../server/src/router.ts
|
|
86
|
+
var Router = class {
|
|
87
|
+
definition;
|
|
88
|
+
constructor(definition) {
|
|
89
|
+
this.definition = definition;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Get a procedure by its path (e.g., "todos.list")
|
|
93
|
+
*/
|
|
94
|
+
getProcedure(path) {
|
|
95
|
+
const parts = path.split(".");
|
|
96
|
+
let current = this.definition;
|
|
97
|
+
for (const part of parts) {
|
|
98
|
+
if (typeof current !== "object" || current === null) {
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
current = current[part];
|
|
102
|
+
}
|
|
103
|
+
if (isProcedure(current)) {
|
|
104
|
+
return current;
|
|
105
|
+
}
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Execute a procedure by its path
|
|
110
|
+
*/
|
|
111
|
+
async execute(path, ctx, input) {
|
|
112
|
+
const procedure = this.getProcedure(path);
|
|
113
|
+
if (!procedure) {
|
|
114
|
+
throw new Error(`Procedure not found: ${path}`);
|
|
115
|
+
}
|
|
116
|
+
return executeProcedure(procedure, ctx, input);
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
function createRouter(definition) {
|
|
120
|
+
return new Router(definition);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// ../server/src/reactive.ts
|
|
124
|
+
function initReactive() {
|
|
125
|
+
return {
|
|
126
|
+
procedure: new ProcedureBuilder(),
|
|
127
|
+
router(definition) {
|
|
128
|
+
return createRouter(definition);
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
var MAX_TRACKED_PREFIX_LENGTH = 64;
|
|
133
|
+
var TABLE_WILDCARD_FIELD = "*";
|
|
134
|
+
var DYNAMODB_PARTITION_KEY_MAX_BYTES = 2048;
|
|
135
|
+
var DEPENDENCY_KEY_VERSION = "d1";
|
|
136
|
+
var HASHED_DEPENDENCY_KEY_VERSION = "d1h";
|
|
137
|
+
var DEBUG_PREVIEW_BYTES = 96;
|
|
138
|
+
function isPlainMap(value) {
|
|
139
|
+
const prototype = Object.getPrototypeOf(value);
|
|
140
|
+
return prototype === Object.prototype || prototype === null;
|
|
141
|
+
}
|
|
142
|
+
function encodeStructuredValue(value, seen) {
|
|
143
|
+
if (value === null) return "null";
|
|
144
|
+
switch (typeof value) {
|
|
145
|
+
case "string":
|
|
146
|
+
return `string:${JSON.stringify(value)}`;
|
|
147
|
+
case "number":
|
|
148
|
+
return `number:${Object.is(value, -0) ? "-0" : String(value)}`;
|
|
149
|
+
case "boolean":
|
|
150
|
+
return `boolean:${String(value)}`;
|
|
151
|
+
case "bigint":
|
|
152
|
+
return `bigint:${String(value)}`;
|
|
153
|
+
case "undefined":
|
|
154
|
+
return "undefined";
|
|
155
|
+
case "symbol":
|
|
156
|
+
return `symbol:${String(value.description)}`;
|
|
157
|
+
case "function":
|
|
158
|
+
return `function:${String(value)}`;
|
|
159
|
+
}
|
|
160
|
+
if (ArrayBuffer.isView(value)) {
|
|
161
|
+
return `binary:${Buffer.from(
|
|
162
|
+
value.buffer,
|
|
163
|
+
value.byteOffset,
|
|
164
|
+
value.byteLength
|
|
165
|
+
).toString("base64url")}`;
|
|
166
|
+
}
|
|
167
|
+
if (value instanceof ArrayBuffer) {
|
|
168
|
+
return `binary:${Buffer.from(value).toString("base64url")}`;
|
|
169
|
+
}
|
|
170
|
+
if (value instanceof Date) {
|
|
171
|
+
return `date:${Number.isNaN(value.getTime()) ? "Invalid Date" : value.toISOString()}`;
|
|
172
|
+
}
|
|
173
|
+
if (seen.has(value)) return "circular";
|
|
174
|
+
seen.add(value);
|
|
175
|
+
let encoded;
|
|
176
|
+
if (Array.isArray(value)) {
|
|
177
|
+
encoded = `array:${JSON.stringify(
|
|
178
|
+
value.map((item) => encodeStructuredValue(item, seen))
|
|
179
|
+
)}`;
|
|
180
|
+
} else if (value instanceof Set) {
|
|
181
|
+
encoded = `set:${JSON.stringify(
|
|
182
|
+
[...value].map((item) => encodeStructuredValue(item, seen)).sort()
|
|
183
|
+
)}`;
|
|
184
|
+
} else if (isPlainMap(value)) {
|
|
185
|
+
encoded = `map:${JSON.stringify(
|
|
186
|
+
Object.keys(value).sort().map((key) => [key, encodeStructuredValue(value[key], seen)])
|
|
187
|
+
)}`;
|
|
188
|
+
} else {
|
|
189
|
+
encoded = `object:${Object.prototype.toString.call(value)}:${String(value)}`;
|
|
190
|
+
}
|
|
191
|
+
seen.delete(value);
|
|
192
|
+
return encoded;
|
|
193
|
+
}
|
|
194
|
+
function encodeDependencyValue(value) {
|
|
195
|
+
if (value === null || value === void 0) return null;
|
|
196
|
+
if (typeof value === "string") return value;
|
|
197
|
+
if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") {
|
|
198
|
+
return String(value);
|
|
199
|
+
}
|
|
200
|
+
if (typeof value === "object" && isPlainMap(value)) return null;
|
|
201
|
+
return encodeStructuredValue(value, /* @__PURE__ */ new Set());
|
|
202
|
+
}
|
|
203
|
+
function wildcardDependency(tableName) {
|
|
204
|
+
return { tableName, fieldName: TABLE_WILDCARD_FIELD, fieldValue: "*" };
|
|
205
|
+
}
|
|
206
|
+
function extractFromCondition(tableName, condition) {
|
|
207
|
+
const dependencies = [];
|
|
208
|
+
switch (condition.type) {
|
|
209
|
+
case "comparison": {
|
|
210
|
+
const fieldValue = encodeDependencyValue(condition.value);
|
|
211
|
+
if (condition.operator === "eq" && typeof condition.field === "string" && fieldValue !== null) {
|
|
212
|
+
dependencies.push({
|
|
213
|
+
tableName,
|
|
214
|
+
fieldName: condition.field,
|
|
215
|
+
fieldValue
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
return { dependencies, safe: dependencies.length === 1 };
|
|
219
|
+
}
|
|
220
|
+
case "function": {
|
|
221
|
+
if (condition.operator === "beginsWith" && typeof condition.field === "string" && typeof condition.value === "string" && condition.value.length > 0 && condition.value.length <= MAX_TRACKED_PREFIX_LENGTH) {
|
|
222
|
+
dependencies.push({
|
|
223
|
+
tableName,
|
|
224
|
+
fieldName: condition.field,
|
|
225
|
+
fieldValue: `prefix:${String(condition.value)}`
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
return { dependencies, safe: dependencies.length === 1 };
|
|
229
|
+
}
|
|
230
|
+
case "logical": {
|
|
231
|
+
if (condition.operator === "not" || !condition.conditions || condition.conditions.length === 0) {
|
|
232
|
+
return { dependencies, safe: false };
|
|
233
|
+
}
|
|
234
|
+
let safe = true;
|
|
235
|
+
for (const subCondition of condition.conditions) {
|
|
236
|
+
const extracted = extractFromCondition(
|
|
237
|
+
tableName,
|
|
238
|
+
subCondition
|
|
239
|
+
);
|
|
240
|
+
dependencies.push(...extracted.dependencies);
|
|
241
|
+
safe &&= extracted.safe;
|
|
242
|
+
}
|
|
243
|
+
return { dependencies, safe };
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
return { dependencies, safe: false };
|
|
247
|
+
}
|
|
248
|
+
function extractDependencies(operation) {
|
|
249
|
+
const dependencies = [];
|
|
250
|
+
let safe = operation.filters.length > 0;
|
|
251
|
+
for (const filter of operation.filters) {
|
|
252
|
+
const extracted = extractFromCondition(operation.tableName, filter);
|
|
253
|
+
dependencies.push(...extracted.dependencies);
|
|
254
|
+
safe &&= extracted.safe;
|
|
255
|
+
}
|
|
256
|
+
if (!safe) dependencies.push(wildcardDependency(operation.tableName));
|
|
257
|
+
const unique = /* @__PURE__ */ new Map();
|
|
258
|
+
for (const dependency of dependencies) {
|
|
259
|
+
unique.set(createDependencyKey(dependency), dependency);
|
|
260
|
+
}
|
|
261
|
+
return [...unique.values()];
|
|
262
|
+
}
|
|
263
|
+
function createDependencyKey(dependency) {
|
|
264
|
+
const components = [
|
|
265
|
+
dependency.tableName,
|
|
266
|
+
dependency.fieldName,
|
|
267
|
+
dependency.fieldValue
|
|
268
|
+
];
|
|
269
|
+
const encoded = components.map(
|
|
270
|
+
(component) => Buffer.from(component, "utf8").toString("base64url")
|
|
271
|
+
);
|
|
272
|
+
const key = `${DEPENDENCY_KEY_VERSION}#${encoded.join("#")}`;
|
|
273
|
+
if (Buffer.byteLength(key, "utf8") <= DYNAMODB_PARTITION_KEY_MAX_BYTES) {
|
|
274
|
+
return key;
|
|
275
|
+
}
|
|
276
|
+
const digest = crypto.createHash("sha256").update(key, "utf8").digest("hex");
|
|
277
|
+
const previews = components.map(
|
|
278
|
+
(component) => Buffer.from(utf8Preview(component), "utf8").toString("base64url")
|
|
279
|
+
);
|
|
280
|
+
return `${HASHED_DEPENDENCY_KEY_VERSION}#${digest}#${previews.join("#")}`;
|
|
281
|
+
}
|
|
282
|
+
function utf8Preview(value) {
|
|
283
|
+
if (Buffer.byteLength(value, "utf8") <= DEBUG_PREVIEW_BYTES) return value;
|
|
284
|
+
let preview = "";
|
|
285
|
+
for (const character of value) {
|
|
286
|
+
if (Buffer.byteLength(`${preview}${character}`, "utf8") > DEBUG_PREVIEW_BYTES) {
|
|
287
|
+
break;
|
|
288
|
+
}
|
|
289
|
+
preview += character;
|
|
290
|
+
}
|
|
291
|
+
return `${preview}...`;
|
|
292
|
+
}
|
|
293
|
+
function parseDependencyKey(key) {
|
|
294
|
+
const parts = key.split("#");
|
|
295
|
+
const decode = (encoded) => {
|
|
296
|
+
const buffer = Buffer.from(encoded, "base64url");
|
|
297
|
+
return buffer.toString("base64url") === encoded ? buffer.toString("utf8") : null;
|
|
298
|
+
};
|
|
299
|
+
if (parts[0] === DEPENDENCY_KEY_VERSION && parts.length === 4) {
|
|
300
|
+
const decoded = parts.slice(1).map(decode);
|
|
301
|
+
if (decoded.some((component) => component === null)) return null;
|
|
302
|
+
return {
|
|
303
|
+
tableName: decoded[0],
|
|
304
|
+
fieldName: decoded[1],
|
|
305
|
+
fieldValue: decoded[2]
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
if (parts[0] === HASHED_DEPENDENCY_KEY_VERSION && parts.length === 5 && /^[a-f0-9]{64}$/.test(parts[1])) {
|
|
309
|
+
const decoded = parts.slice(2).map(decode);
|
|
310
|
+
if (decoded.some((component) => component === null)) return null;
|
|
311
|
+
return {
|
|
312
|
+
tableName: decoded[0],
|
|
313
|
+
fieldName: decoded[1],
|
|
314
|
+
fieldValue: `${decoded[2]} [sha256:${parts[1]}]`
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
return null;
|
|
318
|
+
}
|
|
319
|
+
function extractAffectedKeys(tableName, item) {
|
|
320
|
+
const keys = /* @__PURE__ */ new Set([
|
|
321
|
+
createDependencyKey(wildcardDependency(tableName))
|
|
322
|
+
]);
|
|
323
|
+
const visit = (fieldName, fieldValue) => {
|
|
324
|
+
if (fieldValue !== null && fieldValue !== void 0) {
|
|
325
|
+
const encodedValue = encodeDependencyValue(fieldValue);
|
|
326
|
+
if (encodedValue !== null) {
|
|
327
|
+
keys.add(
|
|
328
|
+
createDependencyKey({
|
|
329
|
+
tableName,
|
|
330
|
+
fieldName,
|
|
331
|
+
fieldValue: encodedValue
|
|
332
|
+
})
|
|
333
|
+
);
|
|
334
|
+
}
|
|
335
|
+
if (typeof fieldValue === "string") {
|
|
336
|
+
const maxPrefixLen = Math.min(
|
|
337
|
+
fieldValue.length,
|
|
338
|
+
MAX_TRACKED_PREFIX_LENGTH
|
|
339
|
+
);
|
|
340
|
+
for (let i = 1; i <= maxPrefixLen; i++) {
|
|
341
|
+
keys.add(
|
|
342
|
+
createDependencyKey({
|
|
343
|
+
tableName,
|
|
344
|
+
fieldName,
|
|
345
|
+
fieldValue: `prefix:${fieldValue.substring(0, i)}`
|
|
346
|
+
})
|
|
347
|
+
);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
if (typeof fieldValue === "object" && isPlainMap(fieldValue)) {
|
|
351
|
+
for (const [nestedName, nestedValue2] of Object.entries(fieldValue)) {
|
|
352
|
+
visit(`${fieldName}.${nestedName}`, nestedValue2);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
};
|
|
357
|
+
for (const [fieldName, fieldValue] of Object.entries(item)) {
|
|
358
|
+
visit(fieldName, fieldValue);
|
|
359
|
+
}
|
|
360
|
+
return [...keys];
|
|
361
|
+
}
|
|
362
|
+
function operationToQueryMetadata(operation) {
|
|
363
|
+
return {
|
|
364
|
+
planVersion: 1,
|
|
365
|
+
tableName: operation.tableName,
|
|
366
|
+
indexName: operation.indexName,
|
|
367
|
+
indexPkField: operation.indexPkField,
|
|
368
|
+
indexSkField: operation.indexSkField,
|
|
369
|
+
filterConditions: operation.filters,
|
|
370
|
+
...operation.sortField && operation.sortOrder ? { sortField: operation.sortField, sortOrder: operation.sortOrder } : {},
|
|
371
|
+
limit: operation.limit
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
var DependencyTracker = class {
|
|
375
|
+
operations = [];
|
|
376
|
+
/**
|
|
377
|
+
* Track a query operation
|
|
378
|
+
*/
|
|
379
|
+
track(operation, result) {
|
|
380
|
+
this.operations.push({
|
|
381
|
+
operation,
|
|
382
|
+
resultReference: result,
|
|
383
|
+
resultSnapshot: structuredClone(result)
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
/**
|
|
387
|
+
* Get all tracked operations
|
|
388
|
+
*/
|
|
389
|
+
getOperations() {
|
|
390
|
+
return this.operations.map(({ operation }) => operation);
|
|
391
|
+
}
|
|
392
|
+
/**
|
|
393
|
+
* Extract all dependencies from tracked operations
|
|
394
|
+
*/
|
|
395
|
+
extractAll() {
|
|
396
|
+
const dependencies = [];
|
|
397
|
+
for (const { operation } of this.operations) {
|
|
398
|
+
dependencies.push(...extractDependencies(operation));
|
|
399
|
+
}
|
|
400
|
+
return dependencies;
|
|
401
|
+
}
|
|
402
|
+
/**
|
|
403
|
+
* Get query metadata for the first tracked operation.
|
|
404
|
+
* Used for storing subscription state.
|
|
405
|
+
*/
|
|
406
|
+
getQueryMetadata() {
|
|
407
|
+
if (this.operations.length === 0) return null;
|
|
408
|
+
return operationToQueryMetadata(this.operations[0].operation);
|
|
409
|
+
}
|
|
410
|
+
getCapturedResult() {
|
|
411
|
+
if (this.operations.length !== 1) return null;
|
|
412
|
+
return {
|
|
413
|
+
reference: this.operations[0].resultReference,
|
|
414
|
+
snapshot: structuredClone(this.operations[0].resultSnapshot)
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
};
|
|
418
|
+
|
|
419
|
+
// ../server/src/query-builder.ts
|
|
420
|
+
var FilterBuilderImpl = class {
|
|
421
|
+
eq(field, value) {
|
|
422
|
+
return { type: "comparison", operator: "eq", field: field.fieldName, value };
|
|
423
|
+
}
|
|
424
|
+
ne(field, value) {
|
|
425
|
+
return { type: "comparison", operator: "ne", field: field.fieldName, value };
|
|
426
|
+
}
|
|
427
|
+
gt(field, value) {
|
|
428
|
+
return { type: "comparison", operator: "gt", field: field.fieldName, value };
|
|
429
|
+
}
|
|
430
|
+
gte(field, value) {
|
|
431
|
+
return { type: "comparison", operator: "gte", field: field.fieldName, value };
|
|
432
|
+
}
|
|
433
|
+
lt(field, value) {
|
|
434
|
+
return { type: "comparison", operator: "lt", field: field.fieldName, value };
|
|
435
|
+
}
|
|
436
|
+
lte(field, value) {
|
|
437
|
+
return { type: "comparison", operator: "lte", field: field.fieldName, value };
|
|
438
|
+
}
|
|
439
|
+
between(field, lower, upper) {
|
|
440
|
+
return {
|
|
441
|
+
type: "function",
|
|
442
|
+
operator: "between",
|
|
443
|
+
field: field.fieldName,
|
|
444
|
+
value: lower,
|
|
445
|
+
value2: upper
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
beginsWith(field, prefix) {
|
|
449
|
+
return {
|
|
450
|
+
type: "function",
|
|
451
|
+
operator: "beginsWith",
|
|
452
|
+
field: field.fieldName,
|
|
453
|
+
value: prefix
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
contains(field, substring) {
|
|
457
|
+
return {
|
|
458
|
+
type: "function",
|
|
459
|
+
operator: "contains",
|
|
460
|
+
field: field.fieldName,
|
|
461
|
+
value: substring
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
and(...conditions) {
|
|
465
|
+
return { type: "logical", operator: "and", conditions };
|
|
466
|
+
}
|
|
467
|
+
or(...conditions) {
|
|
468
|
+
return { type: "logical", operator: "or", conditions };
|
|
469
|
+
}
|
|
470
|
+
not(condition) {
|
|
471
|
+
return { type: "logical", operator: "not", conditions: [condition] };
|
|
472
|
+
}
|
|
473
|
+
};
|
|
474
|
+
var QueryBuilderImpl = class {
|
|
475
|
+
table;
|
|
476
|
+
filters = [];
|
|
477
|
+
indexName;
|
|
478
|
+
limit;
|
|
479
|
+
ascending = true;
|
|
480
|
+
sortRequested = false;
|
|
481
|
+
executor;
|
|
482
|
+
operationTracker;
|
|
483
|
+
constructor(table, executor, operationTracker) {
|
|
484
|
+
this.table = table;
|
|
485
|
+
this.executor = executor;
|
|
486
|
+
this.operationTracker = operationTracker;
|
|
487
|
+
}
|
|
488
|
+
filter(fn) {
|
|
489
|
+
const builder = new FilterBuilderImpl();
|
|
490
|
+
const condition = fn(builder);
|
|
491
|
+
this.filters.push(condition);
|
|
492
|
+
return this;
|
|
493
|
+
}
|
|
494
|
+
useIndex(indexName) {
|
|
495
|
+
if (!this.table.indexes[indexName]) {
|
|
496
|
+
throw new Error(`Unknown index: ${indexName}`);
|
|
497
|
+
}
|
|
498
|
+
this.indexName = indexName;
|
|
499
|
+
return this;
|
|
500
|
+
}
|
|
501
|
+
take(limit) {
|
|
502
|
+
if (!Number.isInteger(limit) || limit <= 0) {
|
|
503
|
+
throw new Error("take(limit) requires a positive integer");
|
|
504
|
+
}
|
|
505
|
+
this.limit = limit;
|
|
506
|
+
return this;
|
|
507
|
+
}
|
|
508
|
+
sortAscending() {
|
|
509
|
+
this.ascending = true;
|
|
510
|
+
this.sortRequested = true;
|
|
511
|
+
return this;
|
|
512
|
+
}
|
|
513
|
+
sortDescending() {
|
|
514
|
+
this.ascending = false;
|
|
515
|
+
this.sortRequested = true;
|
|
516
|
+
return this;
|
|
517
|
+
}
|
|
518
|
+
async execute() {
|
|
519
|
+
const operation = this.getOperation();
|
|
520
|
+
const result = await this.executor(operation);
|
|
521
|
+
this.operationTracker?.(operation, result);
|
|
522
|
+
return result;
|
|
523
|
+
}
|
|
524
|
+
/**
|
|
525
|
+
* Get the current operation without executing
|
|
526
|
+
* Used for dependency extraction
|
|
527
|
+
*/
|
|
528
|
+
getOperation() {
|
|
529
|
+
const index = this.indexName ? this.table.indexes[this.indexName] : void 0;
|
|
530
|
+
const selectedSortField = index ? index.sk : this.table.sk;
|
|
531
|
+
const sortField = this.sortRequested ? selectedSortField : void 0;
|
|
532
|
+
if (this.sortRequested && !sortField) {
|
|
533
|
+
throw new Error("Sorting requires a table or index sort key");
|
|
534
|
+
}
|
|
535
|
+
return {
|
|
536
|
+
tableName: this.table.tableName,
|
|
537
|
+
filters: this.filters,
|
|
538
|
+
indexName: index?.name,
|
|
539
|
+
indexPkField: index?.pk,
|
|
540
|
+
indexSkField: index?.sk,
|
|
541
|
+
...sortField ? { sortField, sortOrder: this.ascending ? "asc" : "desc" } : {},
|
|
542
|
+
limit: this.limit
|
|
543
|
+
};
|
|
544
|
+
}
|
|
545
|
+
};
|
|
546
|
+
var FilterConditionSchema = zod.z.lazy(() => {
|
|
547
|
+
const comparison = zod.z.object({
|
|
548
|
+
type: zod.z.literal("comparison"),
|
|
549
|
+
operator: zod.z.enum(["eq", "ne", "gt", "gte", "lt", "lte"]),
|
|
550
|
+
field: zod.z.string().min(1),
|
|
551
|
+
value: zod.z.unknown()
|
|
552
|
+
}).strict();
|
|
553
|
+
const queryFunction = zod.z.union([
|
|
554
|
+
zod.z.object({
|
|
555
|
+
type: zod.z.literal("function"),
|
|
556
|
+
operator: zod.z.literal("between"),
|
|
557
|
+
field: zod.z.string().min(1),
|
|
558
|
+
value: zod.z.unknown(),
|
|
559
|
+
value2: zod.z.unknown()
|
|
560
|
+
}).strict(),
|
|
561
|
+
zod.z.object({
|
|
562
|
+
type: zod.z.literal("function"),
|
|
563
|
+
operator: zod.z.enum(["beginsWith", "contains"]),
|
|
564
|
+
field: zod.z.string().min(1),
|
|
565
|
+
value: zod.z.unknown()
|
|
566
|
+
}).strict()
|
|
567
|
+
]);
|
|
568
|
+
const logical = zod.z.object({
|
|
569
|
+
type: zod.z.literal("logical"),
|
|
570
|
+
operator: zod.z.enum(["and", "or", "not"]),
|
|
571
|
+
conditions: zod.z.array(FilterConditionSchema).min(1)
|
|
572
|
+
}).strict().superRefine((condition, context) => {
|
|
573
|
+
if (condition.operator === "not" && condition.conditions.length !== 1) {
|
|
574
|
+
context.addIssue({
|
|
575
|
+
code: "custom",
|
|
576
|
+
path: ["conditions"],
|
|
577
|
+
message: "not requires exactly one condition"
|
|
578
|
+
});
|
|
579
|
+
}
|
|
580
|
+
});
|
|
581
|
+
return zod.z.union([comparison, queryFunction, logical]);
|
|
582
|
+
});
|
|
583
|
+
var QueryMetadataSchema = zod.z.object({
|
|
584
|
+
planVersion: zod.z.literal(1),
|
|
585
|
+
tableName: zod.z.string().min(1),
|
|
586
|
+
indexName: zod.z.string().min(1).optional(),
|
|
587
|
+
indexPkField: zod.z.string().min(1).optional(),
|
|
588
|
+
indexSkField: zod.z.string().min(1).optional(),
|
|
589
|
+
filterConditions: zod.z.array(FilterConditionSchema),
|
|
590
|
+
sortField: zod.z.string().min(1).optional(),
|
|
591
|
+
sortOrder: zod.z.enum(["asc", "desc"]).optional(),
|
|
592
|
+
limit: zod.z.number().int().positive().optional()
|
|
593
|
+
}).strict().superRefine((metadata, context) => {
|
|
594
|
+
if (metadata.indexName && !metadata.indexPkField) {
|
|
595
|
+
context.addIssue({
|
|
596
|
+
code: "custom",
|
|
597
|
+
path: ["indexPkField"],
|
|
598
|
+
message: "Index metadata requires its partition key field"
|
|
599
|
+
});
|
|
600
|
+
}
|
|
601
|
+
if (!metadata.indexName && (metadata.indexPkField || metadata.indexSkField)) {
|
|
602
|
+
context.addIssue({
|
|
603
|
+
code: "custom",
|
|
604
|
+
path: ["indexName"],
|
|
605
|
+
message: "Index key fields require an index name"
|
|
606
|
+
});
|
|
607
|
+
}
|
|
608
|
+
if (Boolean(metadata.sortField) !== Boolean(metadata.sortOrder)) {
|
|
609
|
+
context.addIssue({
|
|
610
|
+
code: "custom",
|
|
611
|
+
path: ["sortOrder"],
|
|
612
|
+
message: "Sort field and order must be specified together"
|
|
613
|
+
});
|
|
614
|
+
}
|
|
615
|
+
});
|
|
616
|
+
var DeliveryVersionSchema = zod.z.object({
|
|
617
|
+
generation: zod.z.number().int().nonnegative(),
|
|
618
|
+
fromVersion: zod.z.number().int().nonnegative(),
|
|
619
|
+
toVersion: zod.z.number().int().positive(),
|
|
620
|
+
messageId: zod.z.string().min(1)
|
|
621
|
+
});
|
|
622
|
+
var PendingDeliverySchema = zod.z.discriminatedUnion("type", [
|
|
623
|
+
DeliveryVersionSchema.extend({
|
|
624
|
+
type: zod.z.literal("patch"),
|
|
625
|
+
patches: zod.z.array(
|
|
626
|
+
zod.z.object({
|
|
627
|
+
op: zod.z.enum(["add", "remove", "replace", "move", "copy", "test"]),
|
|
628
|
+
path: zod.z.string(),
|
|
629
|
+
value: zod.z.unknown().optional(),
|
|
630
|
+
from: zod.z.string().optional()
|
|
631
|
+
})
|
|
632
|
+
)
|
|
633
|
+
}),
|
|
634
|
+
DeliveryVersionSchema.extend({
|
|
635
|
+
type: zod.z.literal("resync")
|
|
636
|
+
}).strict()
|
|
637
|
+
]);
|
|
638
|
+
var SystemSchemas = {
|
|
639
|
+
/**
|
|
640
|
+
* ReactiveConnectionsV2 - Tracks active WebSocket connections
|
|
641
|
+
*/
|
|
642
|
+
connections: zod.z.object({
|
|
643
|
+
connectionId: zod.z.string(),
|
|
644
|
+
status: zod.z.enum(["open", "closed"]),
|
|
645
|
+
context: zod.z.record(zod.z.string(), zod.z.unknown()).optional(),
|
|
646
|
+
authorizerContext: zod.z.record(zod.z.string(), zod.z.unknown()).optional(),
|
|
647
|
+
requestIdentity: zod.z.record(zod.z.string(), zod.z.unknown()).optional(),
|
|
648
|
+
connectedAt: zod.z.number(),
|
|
649
|
+
ttl: zod.z.number()
|
|
650
|
+
}),
|
|
651
|
+
/**
|
|
652
|
+
* ReactiveDependenciesV2 - The inverted index for O(1) lookups
|
|
653
|
+
*/
|
|
654
|
+
dependencies: zod.z.object({
|
|
655
|
+
pk: zod.z.string(),
|
|
656
|
+
// Versioned encoded dependency key
|
|
657
|
+
sk: zod.z.string(),
|
|
658
|
+
// ConnectionID#SubscriptionID
|
|
659
|
+
connectionId: zod.z.string(),
|
|
660
|
+
subscriptionId: zod.z.string(),
|
|
661
|
+
tableName: zod.z.string(),
|
|
662
|
+
fieldName: zod.z.string(),
|
|
663
|
+
fieldValue: zod.z.string(),
|
|
664
|
+
generation: zod.z.number().int().nonnegative(),
|
|
665
|
+
registrationId: zod.z.string().min(1),
|
|
666
|
+
ttl: zod.z.number()
|
|
667
|
+
}),
|
|
668
|
+
/**
|
|
669
|
+
* ReactiveConnectionQueriesV2 - Stores subscription state for diffing
|
|
670
|
+
*/
|
|
671
|
+
queries: zod.z.object({
|
|
672
|
+
pk: zod.z.string(),
|
|
673
|
+
// ConnectionID
|
|
674
|
+
sk: zod.z.string(),
|
|
675
|
+
// SubscriptionID
|
|
676
|
+
connectionId: zod.z.string(),
|
|
677
|
+
subscriptionId: zod.z.string(),
|
|
678
|
+
status: zod.z.enum(["registering", "active", "cancelled", "faulted"]),
|
|
679
|
+
generation: zod.z.number().int().nonnegative(),
|
|
680
|
+
registrationId: zod.z.string().min(1),
|
|
681
|
+
queryMetadata: QueryMetadataSchema,
|
|
682
|
+
lastResult: zod.z.array(zod.z.unknown()),
|
|
683
|
+
dependencies: zod.z.array(zod.z.string()),
|
|
684
|
+
version: zod.z.number().int().nonnegative(),
|
|
685
|
+
pendingDelivery: PendingDeliverySchema.optional(),
|
|
686
|
+
createdAt: zod.z.number(),
|
|
687
|
+
updatedAt: zod.z.number(),
|
|
688
|
+
ttl: zod.z.number()
|
|
689
|
+
})
|
|
690
|
+
};
|
|
691
|
+
var SystemTableNames = {
|
|
692
|
+
connections: "ReactiveConnectionsV2",
|
|
693
|
+
dependencies: "ReactiveDependenciesV2",
|
|
694
|
+
queries: "ReactiveConnectionQueriesV2"
|
|
695
|
+
};
|
|
696
|
+
var InvalidQueryMetadataError = class extends Error {
|
|
697
|
+
constructor(message) {
|
|
698
|
+
super(message);
|
|
699
|
+
this.name = "InvalidQueryMetadataError";
|
|
700
|
+
}
|
|
701
|
+
};
|
|
702
|
+
function attributeValue(value) {
|
|
703
|
+
if (value === null) return { NULL: true };
|
|
704
|
+
if (value === void 0) {
|
|
705
|
+
throw new InvalidQueryMetadataError("Query parameter values are required");
|
|
706
|
+
}
|
|
707
|
+
if (typeof value === "string") return { S: value };
|
|
708
|
+
if (typeof value === "number") {
|
|
709
|
+
if (!Number.isFinite(value)) {
|
|
710
|
+
throw new InvalidQueryMetadataError("Query values must be finite numbers");
|
|
711
|
+
}
|
|
712
|
+
return { N: String(value) };
|
|
713
|
+
}
|
|
714
|
+
if (typeof value === "boolean") return { BOOL: value };
|
|
715
|
+
if (value instanceof Uint8Array) return { B: value };
|
|
716
|
+
if (value instanceof Set) {
|
|
717
|
+
const values = [...value];
|
|
718
|
+
if (values.length === 0) {
|
|
719
|
+
throw new InvalidQueryMetadataError("Query sets must not be empty");
|
|
720
|
+
}
|
|
721
|
+
if (values.every((item) => typeof item === "string")) {
|
|
722
|
+
return { SS: values };
|
|
723
|
+
}
|
|
724
|
+
if (values.every((item) => typeof item === "number")) {
|
|
725
|
+
if (values.some((item) => !Number.isFinite(item))) {
|
|
726
|
+
throw new InvalidQueryMetadataError("Query values must be finite numbers");
|
|
727
|
+
}
|
|
728
|
+
return { NS: values.map(String) };
|
|
729
|
+
}
|
|
730
|
+
if (values.every((item) => item instanceof Uint8Array)) {
|
|
731
|
+
return { BS: values };
|
|
732
|
+
}
|
|
733
|
+
throw new InvalidQueryMetadataError(
|
|
734
|
+
"Query sets must contain one supported DynamoDB scalar type"
|
|
735
|
+
);
|
|
736
|
+
}
|
|
737
|
+
if (Array.isArray(value)) return { L: value.map(attributeValue) };
|
|
738
|
+
if (typeof value === "object") {
|
|
739
|
+
return {
|
|
740
|
+
M: Object.fromEntries(
|
|
741
|
+
Object.entries(value).map(([key, item]) => [
|
|
742
|
+
key,
|
|
743
|
+
attributeValue(item)
|
|
744
|
+
])
|
|
745
|
+
)
|
|
746
|
+
};
|
|
747
|
+
}
|
|
748
|
+
throw new InvalidQueryMetadataError(
|
|
749
|
+
`Unsupported query value type: ${typeof value}`
|
|
750
|
+
);
|
|
751
|
+
}
|
|
752
|
+
function quoteIdentifier(identifier) {
|
|
753
|
+
if (!identifier) {
|
|
754
|
+
throw new InvalidQueryMetadataError("Identifier must not be empty");
|
|
755
|
+
}
|
|
756
|
+
return `"${identifier.replace(/"/g, '""')}"`;
|
|
757
|
+
}
|
|
758
|
+
function quotePath(path) {
|
|
759
|
+
if (!path || path.split(".").some((part) => !part)) {
|
|
760
|
+
throw new InvalidQueryMetadataError(`Invalid field path: ${path}`);
|
|
761
|
+
}
|
|
762
|
+
return path.split(".").map(quoteIdentifier).join(".");
|
|
763
|
+
}
|
|
764
|
+
function compileCondition(condition, parameters) {
|
|
765
|
+
if (condition.type === "comparison" && condition.field) {
|
|
766
|
+
const operators = {
|
|
767
|
+
eq: "=",
|
|
768
|
+
ne: "<>",
|
|
769
|
+
gt: ">",
|
|
770
|
+
gte: ">=",
|
|
771
|
+
lt: "<",
|
|
772
|
+
lte: "<="
|
|
773
|
+
};
|
|
774
|
+
const operator = operators[condition.operator];
|
|
775
|
+
if (!operator) {
|
|
776
|
+
throw new InvalidQueryMetadataError(
|
|
777
|
+
`Unsupported comparison operator: ${condition.operator}`
|
|
778
|
+
);
|
|
779
|
+
}
|
|
780
|
+
parameters.push(attributeValue(condition.value));
|
|
781
|
+
return `${quotePath(condition.field)} ${operator} ?`;
|
|
782
|
+
}
|
|
783
|
+
if (condition.type === "function" && condition.field) {
|
|
784
|
+
if (condition.operator === "between") {
|
|
785
|
+
parameters.push(attributeValue(condition.value));
|
|
786
|
+
parameters.push(attributeValue(condition.value2));
|
|
787
|
+
return `${quotePath(condition.field)} BETWEEN ? AND ?`;
|
|
788
|
+
}
|
|
789
|
+
if (condition.operator === "beginsWith" || condition.operator === "contains") {
|
|
790
|
+
parameters.push(attributeValue(condition.value));
|
|
791
|
+
const name = condition.operator === "beginsWith" ? "begins_with" : "contains";
|
|
792
|
+
return `${name}(${quotePath(condition.field)}, ?)`;
|
|
793
|
+
}
|
|
794
|
+
throw new InvalidQueryMetadataError(
|
|
795
|
+
`Unsupported function operator: ${condition.operator}`
|
|
796
|
+
);
|
|
797
|
+
}
|
|
798
|
+
if (condition.type === "logical" && condition.conditions?.length) {
|
|
799
|
+
if (condition.operator === "not") {
|
|
800
|
+
if (condition.conditions.length !== 1) {
|
|
801
|
+
throw new InvalidQueryMetadataError("NOT requires exactly one condition");
|
|
802
|
+
}
|
|
803
|
+
return `NOT (${compileCondition(condition.conditions[0], parameters)})`;
|
|
804
|
+
}
|
|
805
|
+
if (condition.operator === "and" || condition.operator === "or") {
|
|
806
|
+
const operator = condition.operator.toUpperCase();
|
|
807
|
+
return `(${condition.conditions.map((item) => compileCondition(item, parameters)).join(` ${operator} `)})`;
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
throw new InvalidQueryMetadataError("Malformed query filter condition");
|
|
811
|
+
}
|
|
812
|
+
function parseQueryMetadata(metadata) {
|
|
813
|
+
const parsed = QueryMetadataSchema.safeParse(metadata);
|
|
814
|
+
if (!parsed.success) {
|
|
815
|
+
throw new InvalidQueryMetadataError(
|
|
816
|
+
`Invalid query metadata: ${parsed.error.message}`
|
|
817
|
+
);
|
|
818
|
+
}
|
|
819
|
+
return parsed.data;
|
|
820
|
+
}
|
|
821
|
+
function compileQueryMetadata(metadataInput, nextToken) {
|
|
822
|
+
const metadata = parseQueryMetadata(metadataInput);
|
|
823
|
+
const parameters = [];
|
|
824
|
+
let statement = `SELECT * FROM ${quoteIdentifier(metadata.tableName)}`;
|
|
825
|
+
const predicates = [];
|
|
826
|
+
if (metadata.indexPkField) {
|
|
827
|
+
predicates.push(`${quotePath(metadata.indexPkField)} IS NOT MISSING`);
|
|
828
|
+
}
|
|
829
|
+
if (metadata.indexSkField) {
|
|
830
|
+
predicates.push(`${quotePath(metadata.indexSkField)} IS NOT MISSING`);
|
|
831
|
+
}
|
|
832
|
+
if (metadata.filterConditions.length) {
|
|
833
|
+
predicates.push(
|
|
834
|
+
...metadata.filterConditions.map(
|
|
835
|
+
(condition) => compileCondition(condition, parameters)
|
|
836
|
+
)
|
|
837
|
+
);
|
|
838
|
+
}
|
|
839
|
+
if (predicates.length) statement += ` WHERE ${predicates.join(" AND ")}`;
|
|
840
|
+
return {
|
|
841
|
+
Statement: statement,
|
|
842
|
+
ConsistentRead: true,
|
|
843
|
+
...parameters.length ? { Parameters: parameters } : {},
|
|
844
|
+
...nextToken ? { NextToken: nextToken } : {}
|
|
845
|
+
};
|
|
846
|
+
}
|
|
847
|
+
function nestedValue(item, path) {
|
|
848
|
+
return path.split(".").reduce((value, part) => {
|
|
849
|
+
if (!value || typeof value !== "object") return void 0;
|
|
850
|
+
return value[part];
|
|
851
|
+
}, item);
|
|
852
|
+
}
|
|
853
|
+
function compareValues(left, right) {
|
|
854
|
+
if (left === right) return 0;
|
|
855
|
+
if (typeof left === "number" && typeof right === "number") return left - right;
|
|
856
|
+
if (typeof left === "string" && typeof right === "string") {
|
|
857
|
+
return Buffer.compare(Buffer.from(left, "utf8"), Buffer.from(right, "utf8"));
|
|
858
|
+
}
|
|
859
|
+
if (left instanceof Uint8Array && right instanceof Uint8Array) {
|
|
860
|
+
return Buffer.compare(Buffer.from(left), Buffer.from(right));
|
|
861
|
+
}
|
|
862
|
+
throw new InvalidQueryMetadataError(
|
|
863
|
+
"Sorted query fields must contain one consistent DynamoDB key type"
|
|
864
|
+
);
|
|
865
|
+
}
|
|
866
|
+
async function executeQueryMetadata(client, metadataInput) {
|
|
867
|
+
const metadata = parseQueryMetadata(metadataInput);
|
|
868
|
+
const items = [];
|
|
869
|
+
let nextToken;
|
|
870
|
+
const seenTokens = /* @__PURE__ */ new Set();
|
|
871
|
+
do {
|
|
872
|
+
const response = await client.send(
|
|
873
|
+
new clientDynamodb.ExecuteStatementCommand(compileQueryMetadata(metadata, nextToken))
|
|
874
|
+
);
|
|
875
|
+
items.push(
|
|
876
|
+
...(response.Items ?? []).map(
|
|
877
|
+
(item) => utilDynamodb.unmarshall(item)
|
|
878
|
+
)
|
|
879
|
+
);
|
|
880
|
+
nextToken = response.NextToken;
|
|
881
|
+
if (nextToken && seenTokens.has(nextToken)) {
|
|
882
|
+
throw new Error("DynamoDB returned a repeated pagination token");
|
|
883
|
+
}
|
|
884
|
+
if (nextToken) seenTokens.add(nextToken);
|
|
885
|
+
if (metadata.limit !== void 0 && !metadata.sortField && items.length >= metadata.limit) {
|
|
886
|
+
break;
|
|
887
|
+
}
|
|
888
|
+
} while (nextToken);
|
|
889
|
+
if (metadata.sortField) {
|
|
890
|
+
items.sort((left, right) => {
|
|
891
|
+
const result = compareValues(
|
|
892
|
+
nestedValue(left, metadata.sortField),
|
|
893
|
+
nestedValue(right, metadata.sortField)
|
|
894
|
+
);
|
|
895
|
+
return metadata.sortOrder === "desc" ? -result : result;
|
|
896
|
+
});
|
|
897
|
+
}
|
|
898
|
+
return metadata.limit === void 0 ? items : items.slice(0, metadata.limit);
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
// ../server/src/db-context.ts
|
|
902
|
+
var MAX_CONDITION_EXPRESSION_BYTES = 4 * 1024;
|
|
903
|
+
var MAX_EXPRESSION_VARIABLE_BYTES = 2 * 1024 * 1024;
|
|
904
|
+
var MAX_CONDITION_OPERATORS = 300;
|
|
905
|
+
function createClients(config) {
|
|
906
|
+
const client = config.client ?? new clientDynamodb.DynamoDBClient({
|
|
907
|
+
region: config.region ?? process.env.AWS_REGION ?? "us-east-1",
|
|
908
|
+
endpoint: config.endpoint
|
|
909
|
+
});
|
|
910
|
+
const docClient = config.docClient ?? libDynamodb.DynamoDBDocumentClient.from(client, {
|
|
911
|
+
marshallOptions: {
|
|
912
|
+
removeUndefinedValues: true,
|
|
913
|
+
convertEmptyValues: false
|
|
914
|
+
},
|
|
915
|
+
unmarshallOptions: {
|
|
916
|
+
wrapNumbers: false
|
|
917
|
+
}
|
|
918
|
+
});
|
|
919
|
+
return { client, docClient };
|
|
920
|
+
}
|
|
921
|
+
function createDbContext(config, dependencyTracker) {
|
|
922
|
+
const { client, docClient } = createClients(config);
|
|
923
|
+
async function executeQuery(operation) {
|
|
924
|
+
return executeQueryMetadata(
|
|
925
|
+
client,
|
|
926
|
+
operationToQueryMetadata(operation)
|
|
927
|
+
);
|
|
928
|
+
}
|
|
929
|
+
return {
|
|
930
|
+
query(table) {
|
|
931
|
+
return new QueryBuilderImpl(
|
|
932
|
+
table,
|
|
933
|
+
executeQuery,
|
|
934
|
+
dependencyTracker?.track.bind(dependencyTracker)
|
|
935
|
+
);
|
|
936
|
+
},
|
|
937
|
+
async get(table, key) {
|
|
938
|
+
const command = new libDynamodb.GetCommand({
|
|
939
|
+
TableName: table.tableName,
|
|
940
|
+
Key: key,
|
|
941
|
+
ConsistentRead: true
|
|
942
|
+
});
|
|
943
|
+
const response = await docClient.send(command);
|
|
944
|
+
return response.Item ? response.Item : null;
|
|
945
|
+
},
|
|
946
|
+
async put(table, item) {
|
|
947
|
+
const parsedItem = table.validate(item);
|
|
948
|
+
const command = new libDynamodb.PutCommand({
|
|
949
|
+
TableName: table.tableName,
|
|
950
|
+
Item: parsedItem
|
|
951
|
+
});
|
|
952
|
+
await docClient.send(command);
|
|
953
|
+
},
|
|
954
|
+
async delete(table, key) {
|
|
955
|
+
const command = new libDynamodb.DeleteCommand({
|
|
956
|
+
TableName: table.tableName,
|
|
957
|
+
Key: key
|
|
958
|
+
});
|
|
959
|
+
await docClient.send(command);
|
|
960
|
+
},
|
|
961
|
+
async update(table, key, updates) {
|
|
962
|
+
const currentResponse = await docClient.send(
|
|
963
|
+
new libDynamodb.GetCommand({
|
|
964
|
+
TableName: table.tableName,
|
|
965
|
+
Key: key,
|
|
966
|
+
ConsistentRead: true
|
|
967
|
+
})
|
|
968
|
+
);
|
|
969
|
+
const current = currentResponse.Item;
|
|
970
|
+
if (!current) throw new Error("Item not found");
|
|
971
|
+
const parsed = table.validate({
|
|
972
|
+
...current,
|
|
973
|
+
...updates,
|
|
974
|
+
...key
|
|
975
|
+
});
|
|
976
|
+
const expected = current;
|
|
977
|
+
const fields = [
|
|
978
|
+
.../* @__PURE__ */ new Set([
|
|
979
|
+
...table.getFieldNames().map(String),
|
|
980
|
+
...Object.keys(expected)
|
|
981
|
+
])
|
|
982
|
+
].sort();
|
|
983
|
+
const conditions = [];
|
|
984
|
+
const expressionAttributeNames = {};
|
|
985
|
+
const expressionAttributeValues = {};
|
|
986
|
+
for (const [i, field] of fields.entries()) {
|
|
987
|
+
const nameKey = `#f${i}`;
|
|
988
|
+
expressionAttributeNames[nameKey] = field;
|
|
989
|
+
if (Object.prototype.hasOwnProperty.call(expected, field)) {
|
|
990
|
+
const valueKey = `:e${i}`;
|
|
991
|
+
conditions.push(`${nameKey} = ${valueKey}`);
|
|
992
|
+
expressionAttributeValues[valueKey] = expected[field];
|
|
993
|
+
} else {
|
|
994
|
+
conditions.push(`attribute_not_exists(${nameKey})`);
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
const conditionExpression = conditions.join(" AND ");
|
|
998
|
+
const variableBytes = Buffer.byteLength(
|
|
999
|
+
JSON.stringify({
|
|
1000
|
+
names: expressionAttributeNames,
|
|
1001
|
+
values: expressionAttributeValues
|
|
1002
|
+
}),
|
|
1003
|
+
"utf8"
|
|
1004
|
+
);
|
|
1005
|
+
if (Buffer.byteLength(conditionExpression, "utf8") > MAX_CONDITION_EXPRESSION_BYTES || conditions.length > MAX_CONDITION_OPERATORS || variableBytes > MAX_EXPRESSION_VARIABLE_BYTES) {
|
|
1006
|
+
throw new Error(
|
|
1007
|
+
"Item is too complex to update with a safe compare-and-swap"
|
|
1008
|
+
);
|
|
1009
|
+
}
|
|
1010
|
+
const command = new libDynamodb.PutCommand({
|
|
1011
|
+
TableName: table.tableName,
|
|
1012
|
+
Item: parsed,
|
|
1013
|
+
ConditionExpression: conditionExpression,
|
|
1014
|
+
...Object.keys(expressionAttributeNames).length ? { ExpressionAttributeNames: expressionAttributeNames } : {},
|
|
1015
|
+
...Object.keys(expressionAttributeValues).length ? { ExpressionAttributeValues: expressionAttributeValues } : {}
|
|
1016
|
+
});
|
|
1017
|
+
await docClient.send(command);
|
|
1018
|
+
return parsed;
|
|
1019
|
+
}
|
|
1020
|
+
};
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
// ../server/src/log.ts
|
|
1024
|
+
var consoleLogger = globalThis.console;
|
|
1025
|
+
var SAFE_ERROR_NAMES = /* @__PURE__ */ new Set([
|
|
1026
|
+
"ConditionalCheckFailedException",
|
|
1027
|
+
"Error",
|
|
1028
|
+
"GoneException",
|
|
1029
|
+
"TransactionCanceledException"
|
|
1030
|
+
]);
|
|
1031
|
+
function sanitized(value) {
|
|
1032
|
+
if (value instanceof Error) {
|
|
1033
|
+
return {
|
|
1034
|
+
errorName: SAFE_ERROR_NAMES.has(value.name) ? value.name : "Error"
|
|
1035
|
+
};
|
|
1036
|
+
}
|
|
1037
|
+
if (value && typeof value === "object") return { valueType: "object" };
|
|
1038
|
+
return value;
|
|
1039
|
+
}
|
|
1040
|
+
function logger(method) {
|
|
1041
|
+
return (...values) => consoleLogger[method](...values.map(sanitized));
|
|
1042
|
+
}
|
|
1043
|
+
var debug = logger("debug");
|
|
1044
|
+
var info = logger("info");
|
|
1045
|
+
var error = logger("error");
|
|
1046
|
+
|
|
1047
|
+
// ../server/src/handlers/reactive-handler.ts
|
|
1048
|
+
var MAX_TRANSACTION_DEPENDENCIES = 90;
|
|
1049
|
+
var MAX_QUERY_ENTRY_BYTES = 300 * 1024;
|
|
1050
|
+
var DYNAMODB_PARTITION_KEY_MAX_BYTES2 = 2048;
|
|
1051
|
+
var DYNAMODB_SORT_KEY_MAX_BYTES = 1024;
|
|
1052
|
+
var MAX_TABLE_PREFIX_LENGTH = 227;
|
|
1053
|
+
var DEFAULT_CONNECTION_LEASE_SECONDS = 3 * 60 * 60;
|
|
1054
|
+
function requestError(message, code) {
|
|
1055
|
+
return new SafeRequestError(message, code);
|
|
1056
|
+
}
|
|
1057
|
+
function validateTablePrefix(tablePrefix) {
|
|
1058
|
+
if (typeof tablePrefix !== "string" || !/^[A-Za-z0-9][A-Za-z0-9_-]*$/.test(tablePrefix) || tablePrefix.length > MAX_TABLE_PREFIX_LENGTH) {
|
|
1059
|
+
throw new Error(
|
|
1060
|
+
"createReactiveHandler tablePrefix must be a nonempty concrete value of at most 227 letters, numbers, hyphens, or underscores, starting with a letter or number"
|
|
1061
|
+
);
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
function validateKeyString(name, value, maxBytes) {
|
|
1065
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
1066
|
+
throw requestError(`${name} must be a non-empty string`, 400);
|
|
1067
|
+
}
|
|
1068
|
+
if (Buffer.byteLength(value, "utf8") > maxBytes) {
|
|
1069
|
+
throw requestError(`${name} exceeds DynamoDB's UTF-8 key length limit`, 400);
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
function validateRequest(request) {
|
|
1073
|
+
validateKeyString(
|
|
1074
|
+
"connectionId",
|
|
1075
|
+
request.connectionId,
|
|
1076
|
+
DYNAMODB_PARTITION_KEY_MAX_BYTES2
|
|
1077
|
+
);
|
|
1078
|
+
if (request.type === "subscribe" || request.type === "unsubscribe") {
|
|
1079
|
+
validateKeyString(
|
|
1080
|
+
"subscriptionId",
|
|
1081
|
+
request.subscriptionId,
|
|
1082
|
+
DYNAMODB_SORT_KEY_MAX_BYTES
|
|
1083
|
+
);
|
|
1084
|
+
validateKeyString(
|
|
1085
|
+
"registrationId",
|
|
1086
|
+
request.registrationId,
|
|
1087
|
+
DYNAMODB_SORT_KEY_MAX_BYTES
|
|
1088
|
+
);
|
|
1089
|
+
if (Buffer.byteLength(
|
|
1090
|
+
`${request.connectionId}#${request.subscriptionId}`,
|
|
1091
|
+
"utf8"
|
|
1092
|
+
) > DYNAMODB_SORT_KEY_MAX_BYTES) {
|
|
1093
|
+
throw requestError(
|
|
1094
|
+
"connectionId and subscriptionId exceed DynamoDB dependency key limits",
|
|
1095
|
+
400
|
|
1096
|
+
);
|
|
1097
|
+
}
|
|
1098
|
+
if (!Number.isInteger(request.generation) || request.generation < 0) {
|
|
1099
|
+
throw requestError("generation must be a non-negative integer", 400);
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
function assertEntrySize(entry) {
|
|
1104
|
+
if (Buffer.byteLength(JSON.stringify(entry), "utf8") > MAX_QUERY_ENTRY_BYTES) {
|
|
1105
|
+
throw new Error("Subscription result is too large to store safely");
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
function createReactiveHandler(config) {
|
|
1109
|
+
validateTablePrefix(config.tablePrefix);
|
|
1110
|
+
const ttlSeconds = config.ttlSeconds ?? DEFAULT_CONNECTION_LEASE_SECONDS;
|
|
1111
|
+
const prefix = `${config.tablePrefix}-`;
|
|
1112
|
+
const connectionsTable = `${prefix}${SystemTableNames.connections}`;
|
|
1113
|
+
const dependenciesTable = `${prefix}${SystemTableNames.dependencies}`;
|
|
1114
|
+
const queriesTable = `${prefix}${SystemTableNames.queries}`;
|
|
1115
|
+
const ddbClient = config.dbConfig?.client ?? new clientDynamodb.DynamoDBClient({
|
|
1116
|
+
region: config.dbConfig?.region ?? process.env.AWS_REGION,
|
|
1117
|
+
endpoint: config.dbConfig?.endpoint
|
|
1118
|
+
});
|
|
1119
|
+
const docClient = config.dbConfig?.docClient ?? libDynamodb.DynamoDBDocumentClient.from(ddbClient, {
|
|
1120
|
+
marshallOptions: { removeUndefinedValues: true }
|
|
1121
|
+
});
|
|
1122
|
+
const dbConfig = {
|
|
1123
|
+
...config.dbConfig,
|
|
1124
|
+
client: ddbClient,
|
|
1125
|
+
docClient
|
|
1126
|
+
};
|
|
1127
|
+
async function authorize(requestInfo, context) {
|
|
1128
|
+
const response = await docClient.send(
|
|
1129
|
+
new libDynamodb.GetCommand({
|
|
1130
|
+
TableName: connectionsTable,
|
|
1131
|
+
Key: { connectionId: requestInfo.connectionId },
|
|
1132
|
+
ConsistentRead: true
|
|
1133
|
+
})
|
|
1134
|
+
);
|
|
1135
|
+
const parsed = SystemSchemas.connections.safeParse(response.Item);
|
|
1136
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
1137
|
+
if (!parsed.success || parsed.data.status !== "open" || parsed.data.ttl <= now) {
|
|
1138
|
+
throw requestError("Connection is not open", 403);
|
|
1139
|
+
}
|
|
1140
|
+
if (!await config.authorizeConnection({
|
|
1141
|
+
context,
|
|
1142
|
+
connection: parsed.data,
|
|
1143
|
+
requestInfo
|
|
1144
|
+
})) {
|
|
1145
|
+
throw requestError("Connection is not authorized", 403);
|
|
1146
|
+
}
|
|
1147
|
+
const refreshedTtl = now + ttlSeconds;
|
|
1148
|
+
try {
|
|
1149
|
+
await docClient.send(
|
|
1150
|
+
new libDynamodb.UpdateCommand({
|
|
1151
|
+
TableName: connectionsTable,
|
|
1152
|
+
Key: { connectionId: requestInfo.connectionId },
|
|
1153
|
+
UpdateExpression: "SET #ttl = :ttl",
|
|
1154
|
+
ConditionExpression: "#status = :open AND #ttl > :now AND connectedAt = :connectedAt",
|
|
1155
|
+
ExpressionAttributeNames: { "#status": "status", "#ttl": "ttl" },
|
|
1156
|
+
ExpressionAttributeValues: {
|
|
1157
|
+
":open": "open",
|
|
1158
|
+
":now": now,
|
|
1159
|
+
":ttl": refreshedTtl,
|
|
1160
|
+
":connectedAt": parsed.data.connectedAt
|
|
1161
|
+
}
|
|
1162
|
+
})
|
|
1163
|
+
);
|
|
1164
|
+
} catch (error2) {
|
|
1165
|
+
if (error2 instanceof Error && error2.name === "ConditionalCheckFailedException") {
|
|
1166
|
+
throw requestError("Connection is not open", 403);
|
|
1167
|
+
}
|
|
1168
|
+
throw error2;
|
|
1169
|
+
}
|
|
1170
|
+
return { ...parsed.data, ttl: refreshedTtl };
|
|
1171
|
+
}
|
|
1172
|
+
async function handleRequest(request, headers) {
|
|
1173
|
+
try {
|
|
1174
|
+
validateRequest(request);
|
|
1175
|
+
const requestInfo = { connectionId: request.connectionId, headers };
|
|
1176
|
+
const context = await config.getContext(requestInfo);
|
|
1177
|
+
const connection = await authorize(requestInfo, context);
|
|
1178
|
+
const tracker = new DependencyTracker();
|
|
1179
|
+
const db = createDbContext(dbConfig, tracker);
|
|
1180
|
+
const fullContext = { ...context, db };
|
|
1181
|
+
if (request.type === "subscribe") {
|
|
1182
|
+
return await handleSubscribe(request, fullContext, tracker, connection);
|
|
1183
|
+
}
|
|
1184
|
+
if (request.type === "unsubscribe") {
|
|
1185
|
+
return await handleUnsubscribe(request);
|
|
1186
|
+
}
|
|
1187
|
+
return await handleCall(request, fullContext);
|
|
1188
|
+
} catch (error2) {
|
|
1189
|
+
const safeError = isSafeRequestError(error2) ? error2 : null;
|
|
1190
|
+
if (!safeError) error("[reactive-handler] Request failed", error2);
|
|
1191
|
+
return {
|
|
1192
|
+
type: "error",
|
|
1193
|
+
message: safeError?.message ?? "Internal server error",
|
|
1194
|
+
code: safeError?.statusCode ?? 500,
|
|
1195
|
+
..."subscriptionId" in request ? { subscriptionId: request.subscriptionId } : {}
|
|
1196
|
+
};
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
async function activeSnapshot(request) {
|
|
1200
|
+
const response = await docClient.send(
|
|
1201
|
+
new libDynamodb.GetCommand({
|
|
1202
|
+
TableName: queriesTable,
|
|
1203
|
+
Key: { pk: request.connectionId, sk: request.subscriptionId },
|
|
1204
|
+
ConsistentRead: true
|
|
1205
|
+
})
|
|
1206
|
+
);
|
|
1207
|
+
const query2 = SystemSchemas.queries.parse(response.Item);
|
|
1208
|
+
if (query2.status !== "active" || query2.generation !== request.generation || query2.registrationId !== request.registrationId) {
|
|
1209
|
+
throw new Error("Subscription registration was superseded");
|
|
1210
|
+
}
|
|
1211
|
+
return {
|
|
1212
|
+
type: "snapshot",
|
|
1213
|
+
subscriptionId: query2.subscriptionId,
|
|
1214
|
+
generation: query2.generation,
|
|
1215
|
+
version: query2.version,
|
|
1216
|
+
data: query2.lastResult
|
|
1217
|
+
};
|
|
1218
|
+
}
|
|
1219
|
+
async function handleSubscribe(request, context, tracker, connection) {
|
|
1220
|
+
const procedure = config.router.getProcedure(request.path);
|
|
1221
|
+
if (!procedure) throw requestError(`Procedure not found: ${request.path}`, 404);
|
|
1222
|
+
if (procedure.type !== "query") {
|
|
1223
|
+
throw requestError("Only query procedures can be subscribed to", 400);
|
|
1224
|
+
}
|
|
1225
|
+
const resolverResult = await config.router.execute(
|
|
1226
|
+
request.path,
|
|
1227
|
+
context,
|
|
1228
|
+
request.input
|
|
1229
|
+
);
|
|
1230
|
+
const operations = tracker.getOperations();
|
|
1231
|
+
const capturedResult = tracker.getCapturedResult();
|
|
1232
|
+
if (operations.length !== 1 || !capturedResult) {
|
|
1233
|
+
throw requestError(
|
|
1234
|
+
"A subscription must execute exactly one ctx.db query",
|
|
1235
|
+
400
|
|
1236
|
+
);
|
|
1237
|
+
}
|
|
1238
|
+
if (!Array.isArray(resolverResult) || resolverResult !== capturedResult.reference || !util.isDeepStrictEqual(resolverResult, capturedResult.snapshot)) {
|
|
1239
|
+
throw requestError(
|
|
1240
|
+
"A subscription resolver must return its ctx.db query result unchanged",
|
|
1241
|
+
400
|
|
1242
|
+
);
|
|
1243
|
+
}
|
|
1244
|
+
const queryMetadata = tracker.getQueryMetadata();
|
|
1245
|
+
if (!queryMetadata) throw new Error("Query metadata was not captured");
|
|
1246
|
+
const dependencies = tracker.extractAll();
|
|
1247
|
+
const uniqueDependencies = new Map(
|
|
1248
|
+
dependencies.map((dependency) => [createDependencyKey(dependency), dependency])
|
|
1249
|
+
);
|
|
1250
|
+
const dependencyKeys = [...uniqueDependencies.keys()];
|
|
1251
|
+
if (uniqueDependencies.size > MAX_TRANSACTION_DEPENDENCIES) {
|
|
1252
|
+
throw requestError("Query has too many reactive dependencies", 400);
|
|
1253
|
+
}
|
|
1254
|
+
const now = Date.now();
|
|
1255
|
+
const ttl = Math.floor(now / 1e3) + ttlSeconds;
|
|
1256
|
+
const registering = {
|
|
1257
|
+
pk: request.connectionId,
|
|
1258
|
+
sk: request.subscriptionId,
|
|
1259
|
+
connectionId: request.connectionId,
|
|
1260
|
+
subscriptionId: request.subscriptionId,
|
|
1261
|
+
status: "registering",
|
|
1262
|
+
generation: request.generation,
|
|
1263
|
+
registrationId: request.registrationId,
|
|
1264
|
+
queryMetadata,
|
|
1265
|
+
lastResult: [],
|
|
1266
|
+
dependencies: dependencyKeys,
|
|
1267
|
+
version: 0,
|
|
1268
|
+
createdAt: now,
|
|
1269
|
+
updatedAt: now,
|
|
1270
|
+
ttl
|
|
1271
|
+
};
|
|
1272
|
+
let registrationEntry = registering;
|
|
1273
|
+
assertEntrySize(registering);
|
|
1274
|
+
const connectionCondition = {
|
|
1275
|
+
TableName: connectionsTable,
|
|
1276
|
+
Key: { connectionId: request.connectionId },
|
|
1277
|
+
ConditionExpression: "#connectionStatus = :open AND #connectionTtl > :now AND connectedAt = :connectedAt",
|
|
1278
|
+
ExpressionAttributeNames: {
|
|
1279
|
+
"#connectionStatus": "status",
|
|
1280
|
+
"#connectionTtl": "ttl"
|
|
1281
|
+
},
|
|
1282
|
+
ExpressionAttributeValues: {
|
|
1283
|
+
":open": "open",
|
|
1284
|
+
":now": Math.floor(Date.now() / 1e3),
|
|
1285
|
+
":connectedAt": connection.connectedAt
|
|
1286
|
+
}
|
|
1287
|
+
};
|
|
1288
|
+
async function renewRegistration(query2) {
|
|
1289
|
+
const dependencySk = `${request.connectionId}#${request.subscriptionId}`;
|
|
1290
|
+
await docClient.send(
|
|
1291
|
+
new libDynamodb.TransactWriteCommand({
|
|
1292
|
+
TransactItems: [
|
|
1293
|
+
{ ConditionCheck: connectionCondition },
|
|
1294
|
+
{
|
|
1295
|
+
Update: {
|
|
1296
|
+
TableName: queriesTable,
|
|
1297
|
+
Key: { pk: query2.pk, sk: query2.sk },
|
|
1298
|
+
UpdateExpression: "SET #ttl = :ttl, updatedAt = :updatedAt",
|
|
1299
|
+
ConditionExpression: "#status = :status AND #generation = :generation AND registrationId = :registrationId",
|
|
1300
|
+
ExpressionAttributeNames: {
|
|
1301
|
+
"#status": "status",
|
|
1302
|
+
"#generation": "generation",
|
|
1303
|
+
"#ttl": "ttl"
|
|
1304
|
+
},
|
|
1305
|
+
ExpressionAttributeValues: {
|
|
1306
|
+
":status": query2.status,
|
|
1307
|
+
":generation": query2.generation,
|
|
1308
|
+
":registrationId": query2.registrationId,
|
|
1309
|
+
":ttl": ttl,
|
|
1310
|
+
":updatedAt": Date.now()
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
1313
|
+
},
|
|
1314
|
+
...[...new Set(query2.dependencies)].map((dependency) => ({
|
|
1315
|
+
Update: {
|
|
1316
|
+
TableName: dependenciesTable,
|
|
1317
|
+
Key: { pk: dependency, sk: dependencySk },
|
|
1318
|
+
UpdateExpression: "SET #ttl = :ttl",
|
|
1319
|
+
ConditionExpression: "#generation = :generation AND registrationId = :registrationId",
|
|
1320
|
+
ExpressionAttributeNames: {
|
|
1321
|
+
"#generation": "generation",
|
|
1322
|
+
"#ttl": "ttl"
|
|
1323
|
+
},
|
|
1324
|
+
ExpressionAttributeValues: {
|
|
1325
|
+
":generation": query2.generation,
|
|
1326
|
+
":registrationId": query2.registrationId,
|
|
1327
|
+
":ttl": ttl
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
}))
|
|
1331
|
+
]
|
|
1332
|
+
})
|
|
1333
|
+
);
|
|
1334
|
+
}
|
|
1335
|
+
try {
|
|
1336
|
+
await docClient.send(
|
|
1337
|
+
new libDynamodb.TransactWriteCommand({
|
|
1338
|
+
TransactItems: [
|
|
1339
|
+
{ ConditionCheck: connectionCondition },
|
|
1340
|
+
{
|
|
1341
|
+
Put: {
|
|
1342
|
+
TableName: queriesTable,
|
|
1343
|
+
Item: registering,
|
|
1344
|
+
ConditionExpression: "attribute_not_exists(pk) OR #generation < :generation",
|
|
1345
|
+
ExpressionAttributeNames: {
|
|
1346
|
+
"#generation": "generation"
|
|
1347
|
+
},
|
|
1348
|
+
ExpressionAttributeValues: {
|
|
1349
|
+
":generation": request.generation
|
|
1350
|
+
}
|
|
1351
|
+
}
|
|
1352
|
+
},
|
|
1353
|
+
...[...uniqueDependencies].map(([key, dependency]) => {
|
|
1354
|
+
const debugMetadata = parseDependencyKey(key) ?? dependency;
|
|
1355
|
+
return {
|
|
1356
|
+
Put: {
|
|
1357
|
+
TableName: dependenciesTable,
|
|
1358
|
+
Item: {
|
|
1359
|
+
pk: key,
|
|
1360
|
+
sk: `${request.connectionId}#${request.subscriptionId}`,
|
|
1361
|
+
connectionId: request.connectionId,
|
|
1362
|
+
subscriptionId: request.subscriptionId,
|
|
1363
|
+
tableName: debugMetadata.tableName,
|
|
1364
|
+
fieldName: debugMetadata.fieldName,
|
|
1365
|
+
fieldValue: debugMetadata.fieldValue,
|
|
1366
|
+
generation: request.generation,
|
|
1367
|
+
registrationId: request.registrationId,
|
|
1368
|
+
ttl
|
|
1369
|
+
}
|
|
1370
|
+
}
|
|
1371
|
+
};
|
|
1372
|
+
})
|
|
1373
|
+
]
|
|
1374
|
+
})
|
|
1375
|
+
);
|
|
1376
|
+
} catch (error2) {
|
|
1377
|
+
const existing = await docClient.send(
|
|
1378
|
+
new libDynamodb.GetCommand({
|
|
1379
|
+
TableName: queriesTable,
|
|
1380
|
+
Key: { pk: request.connectionId, sk: request.subscriptionId },
|
|
1381
|
+
ConsistentRead: true
|
|
1382
|
+
})
|
|
1383
|
+
);
|
|
1384
|
+
const query2 = existing.Item;
|
|
1385
|
+
if (!query2 || query2.generation !== request.generation || query2.registrationId !== request.registrationId || query2.status !== "registering" && query2.status !== "active") {
|
|
1386
|
+
throw error2;
|
|
1387
|
+
}
|
|
1388
|
+
if (query2.status === "active") {
|
|
1389
|
+
await renewRegistration(query2);
|
|
1390
|
+
return activeSnapshot(request);
|
|
1391
|
+
}
|
|
1392
|
+
registrationEntry = query2;
|
|
1393
|
+
try {
|
|
1394
|
+
await renewRegistration(query2);
|
|
1395
|
+
} catch (renewalError) {
|
|
1396
|
+
const winner = await matchingActiveSnapshot(request, connection);
|
|
1397
|
+
if (winner) return winner;
|
|
1398
|
+
throw renewalError;
|
|
1399
|
+
}
|
|
1400
|
+
}
|
|
1401
|
+
let baseline;
|
|
1402
|
+
try {
|
|
1403
|
+
baseline = await executeQueryMetadata(
|
|
1404
|
+
ddbClient,
|
|
1405
|
+
registrationEntry.queryMetadata
|
|
1406
|
+
);
|
|
1407
|
+
const active = {
|
|
1408
|
+
...registrationEntry,
|
|
1409
|
+
status: "active",
|
|
1410
|
+
lastResult: baseline
|
|
1411
|
+
};
|
|
1412
|
+
assertEntrySize(active);
|
|
1413
|
+
} catch (error2) {
|
|
1414
|
+
const winner = await matchingActiveSnapshot(request, connection);
|
|
1415
|
+
if (winner) return winner;
|
|
1416
|
+
await faultRegistration(request);
|
|
1417
|
+
const winnerAfterFault = await matchingActiveSnapshot(request, connection);
|
|
1418
|
+
if (winnerAfterFault) return winnerAfterFault;
|
|
1419
|
+
throw error2;
|
|
1420
|
+
}
|
|
1421
|
+
try {
|
|
1422
|
+
await docClient.send(
|
|
1423
|
+
new libDynamodb.UpdateCommand({
|
|
1424
|
+
TableName: queriesTable,
|
|
1425
|
+
Key: { pk: request.connectionId, sk: request.subscriptionId },
|
|
1426
|
+
UpdateExpression: "SET #status = :active, lastResult = :lastResult, updatedAt = :updatedAt",
|
|
1427
|
+
ConditionExpression: "#status = :registering AND #generation = :generation AND registrationId = :registrationId",
|
|
1428
|
+
ExpressionAttributeNames: {
|
|
1429
|
+
"#status": "status",
|
|
1430
|
+
"#generation": "generation"
|
|
1431
|
+
},
|
|
1432
|
+
ExpressionAttributeValues: {
|
|
1433
|
+
":active": "active",
|
|
1434
|
+
":registering": "registering",
|
|
1435
|
+
":generation": request.generation,
|
|
1436
|
+
":registrationId": request.registrationId,
|
|
1437
|
+
":lastResult": baseline,
|
|
1438
|
+
":updatedAt": Date.now()
|
|
1439
|
+
}
|
|
1440
|
+
})
|
|
1441
|
+
);
|
|
1442
|
+
} catch (error2) {
|
|
1443
|
+
const winner = await matchingActiveSnapshot(request, connection);
|
|
1444
|
+
if (winner) return winner;
|
|
1445
|
+
await faultRegistration(request);
|
|
1446
|
+
const winnerAfterFault = await matchingActiveSnapshot(request, connection);
|
|
1447
|
+
if (winnerAfterFault) return winnerAfterFault;
|
|
1448
|
+
throw error2;
|
|
1449
|
+
}
|
|
1450
|
+
return activeSnapshot(request);
|
|
1451
|
+
}
|
|
1452
|
+
async function matchingActiveSnapshot(request, connection) {
|
|
1453
|
+
const [queryResponse, connectionResponse] = await Promise.all([
|
|
1454
|
+
docClient.send(
|
|
1455
|
+
new libDynamodb.GetCommand({
|
|
1456
|
+
TableName: queriesTable,
|
|
1457
|
+
Key: { pk: request.connectionId, sk: request.subscriptionId },
|
|
1458
|
+
ConsistentRead: true
|
|
1459
|
+
})
|
|
1460
|
+
),
|
|
1461
|
+
docClient.send(
|
|
1462
|
+
new libDynamodb.GetCommand({
|
|
1463
|
+
TableName: connectionsTable,
|
|
1464
|
+
Key: { connectionId: request.connectionId },
|
|
1465
|
+
ConsistentRead: true
|
|
1466
|
+
})
|
|
1467
|
+
)
|
|
1468
|
+
]);
|
|
1469
|
+
const query2 = queryResponse.Item;
|
|
1470
|
+
const currentConnection = SystemSchemas.connections.safeParse(
|
|
1471
|
+
connectionResponse.Item
|
|
1472
|
+
);
|
|
1473
|
+
if (query2?.status !== "active" || query2.generation !== request.generation || query2.registrationId !== request.registrationId || !currentConnection.success || currentConnection.data.status !== "open" || currentConnection.data.connectedAt !== connection.connectedAt) {
|
|
1474
|
+
return null;
|
|
1475
|
+
}
|
|
1476
|
+
return {
|
|
1477
|
+
type: "snapshot",
|
|
1478
|
+
subscriptionId: query2.subscriptionId,
|
|
1479
|
+
generation: query2.generation,
|
|
1480
|
+
version: query2.version,
|
|
1481
|
+
data: query2.lastResult
|
|
1482
|
+
};
|
|
1483
|
+
}
|
|
1484
|
+
async function faultRegistration(request) {
|
|
1485
|
+
await docClient.send(
|
|
1486
|
+
new libDynamodb.UpdateCommand({
|
|
1487
|
+
TableName: queriesTable,
|
|
1488
|
+
Key: { pk: request.connectionId, sk: request.subscriptionId },
|
|
1489
|
+
UpdateExpression: "SET #status = :faulted, updatedAt = :updatedAt",
|
|
1490
|
+
ConditionExpression: "#status = :registering AND #generation = :generation AND registrationId = :registrationId",
|
|
1491
|
+
ExpressionAttributeNames: {
|
|
1492
|
+
"#status": "status",
|
|
1493
|
+
"#generation": "generation"
|
|
1494
|
+
},
|
|
1495
|
+
ExpressionAttributeValues: {
|
|
1496
|
+
":faulted": "faulted",
|
|
1497
|
+
":registering": "registering",
|
|
1498
|
+
":generation": request.generation,
|
|
1499
|
+
":registrationId": request.registrationId,
|
|
1500
|
+
":updatedAt": Date.now()
|
|
1501
|
+
}
|
|
1502
|
+
})
|
|
1503
|
+
).catch(() => void 0);
|
|
1504
|
+
}
|
|
1505
|
+
async function handleUnsubscribe(request) {
|
|
1506
|
+
const response = await docClient.send(
|
|
1507
|
+
new libDynamodb.GetCommand({
|
|
1508
|
+
TableName: queriesTable,
|
|
1509
|
+
Key: { pk: request.connectionId, sk: request.subscriptionId },
|
|
1510
|
+
ConsistentRead: true
|
|
1511
|
+
})
|
|
1512
|
+
);
|
|
1513
|
+
const query2 = response.Item;
|
|
1514
|
+
if (!query2) return { type: "result", data: { success: true } };
|
|
1515
|
+
if (query2.generation !== request.generation || query2.registrationId !== request.registrationId) {
|
|
1516
|
+
throw requestError("Unsubscribe generation is stale", 409);
|
|
1517
|
+
}
|
|
1518
|
+
await docClient.send(
|
|
1519
|
+
new libDynamodb.UpdateCommand({
|
|
1520
|
+
TableName: queriesTable,
|
|
1521
|
+
Key: { pk: request.connectionId, sk: request.subscriptionId },
|
|
1522
|
+
UpdateExpression: "SET #status = :cancelled, updatedAt = :updatedAt REMOVE pendingDelivery",
|
|
1523
|
+
ConditionExpression: "#generation = :generation AND registrationId = :registrationId",
|
|
1524
|
+
ExpressionAttributeNames: {
|
|
1525
|
+
"#status": "status",
|
|
1526
|
+
"#generation": "generation"
|
|
1527
|
+
},
|
|
1528
|
+
ExpressionAttributeValues: {
|
|
1529
|
+
":cancelled": "cancelled",
|
|
1530
|
+
":generation": request.generation,
|
|
1531
|
+
":registrationId": request.registrationId,
|
|
1532
|
+
":updatedAt": Date.now()
|
|
1533
|
+
}
|
|
1534
|
+
})
|
|
1535
|
+
);
|
|
1536
|
+
await Promise.all(
|
|
1537
|
+
query2.dependencies.map(async (key) => {
|
|
1538
|
+
try {
|
|
1539
|
+
await docClient.send(
|
|
1540
|
+
new libDynamodb.DeleteCommand({
|
|
1541
|
+
TableName: dependenciesTable,
|
|
1542
|
+
Key: {
|
|
1543
|
+
pk: key,
|
|
1544
|
+
sk: `${request.connectionId}#${request.subscriptionId}`
|
|
1545
|
+
},
|
|
1546
|
+
ConditionExpression: "#generation = :generation",
|
|
1547
|
+
ExpressionAttributeNames: { "#generation": "generation" },
|
|
1548
|
+
ExpressionAttributeValues: { ":generation": request.generation }
|
|
1549
|
+
})
|
|
1550
|
+
);
|
|
1551
|
+
} catch (error2) {
|
|
1552
|
+
if (!(error2 instanceof Error) || error2.name !== "ConditionalCheckFailedException") {
|
|
1553
|
+
throw error2;
|
|
1554
|
+
}
|
|
1555
|
+
}
|
|
1556
|
+
})
|
|
1557
|
+
);
|
|
1558
|
+
return { type: "result", data: { success: true } };
|
|
1559
|
+
}
|
|
1560
|
+
async function handleCall(request, context) {
|
|
1561
|
+
const procedure = config.router.getProcedure(request.path);
|
|
1562
|
+
if (!procedure) throw requestError(`Procedure not found: ${request.path}`, 404);
|
|
1563
|
+
if (procedure.type !== "mutation") {
|
|
1564
|
+
throw requestError("Call requests require a mutation procedure", 400);
|
|
1565
|
+
}
|
|
1566
|
+
return {
|
|
1567
|
+
type: "result",
|
|
1568
|
+
data: await config.router.execute(request.path, context, request.input)
|
|
1569
|
+
};
|
|
1570
|
+
}
|
|
1571
|
+
async function registerConnection(connectionId, context) {
|
|
1572
|
+
validateKeyString(
|
|
1573
|
+
"connectionId",
|
|
1574
|
+
connectionId,
|
|
1575
|
+
DYNAMODB_PARTITION_KEY_MAX_BYTES2
|
|
1576
|
+
);
|
|
1577
|
+
const now = Date.now();
|
|
1578
|
+
await docClient.send(
|
|
1579
|
+
new libDynamodb.PutCommand({
|
|
1580
|
+
TableName: connectionsTable,
|
|
1581
|
+
Item: {
|
|
1582
|
+
connectionId,
|
|
1583
|
+
status: "open",
|
|
1584
|
+
context,
|
|
1585
|
+
connectedAt: now,
|
|
1586
|
+
ttl: Math.floor(now / 1e3) + ttlSeconds
|
|
1587
|
+
}
|
|
1588
|
+
})
|
|
1589
|
+
);
|
|
1590
|
+
}
|
|
1591
|
+
async function unregisterConnection(connectionId) {
|
|
1592
|
+
validateKeyString(
|
|
1593
|
+
"connectionId",
|
|
1594
|
+
connectionId,
|
|
1595
|
+
DYNAMODB_PARTITION_KEY_MAX_BYTES2
|
|
1596
|
+
);
|
|
1597
|
+
await docClient.send(
|
|
1598
|
+
new libDynamodb.UpdateCommand({
|
|
1599
|
+
TableName: connectionsTable,
|
|
1600
|
+
Key: { connectionId },
|
|
1601
|
+
UpdateExpression: "SET #status = :closed",
|
|
1602
|
+
ConditionExpression: "attribute_exists(connectionId)",
|
|
1603
|
+
ExpressionAttributeNames: { "#status": "status" },
|
|
1604
|
+
ExpressionAttributeValues: { ":closed": "closed" }
|
|
1605
|
+
})
|
|
1606
|
+
);
|
|
1607
|
+
let lastKey;
|
|
1608
|
+
do {
|
|
1609
|
+
const response = await docClient.send(
|
|
1610
|
+
new libDynamodb.QueryCommand({
|
|
1611
|
+
TableName: queriesTable,
|
|
1612
|
+
KeyConditionExpression: "pk = :pk",
|
|
1613
|
+
ExpressionAttributeValues: { ":pk": connectionId },
|
|
1614
|
+
ConsistentRead: true,
|
|
1615
|
+
ExclusiveStartKey: lastKey
|
|
1616
|
+
})
|
|
1617
|
+
);
|
|
1618
|
+
for (const item of response.Items ?? []) {
|
|
1619
|
+
const query2 = item;
|
|
1620
|
+
await handleUnsubscribe({
|
|
1621
|
+
type: "unsubscribe",
|
|
1622
|
+
connectionId,
|
|
1623
|
+
subscriptionId: query2.subscriptionId,
|
|
1624
|
+
generation: query2.generation,
|
|
1625
|
+
registrationId: query2.registrationId
|
|
1626
|
+
});
|
|
1627
|
+
}
|
|
1628
|
+
lastKey = response.LastEvaluatedKey;
|
|
1629
|
+
} while (lastKey);
|
|
1630
|
+
debug("[reactive-handler] Connection unregistered");
|
|
1631
|
+
}
|
|
1632
|
+
async function query(path, input, headers) {
|
|
1633
|
+
const procedure = config.router.getProcedure(path);
|
|
1634
|
+
if (!procedure) throw requestError(`Procedure not found: ${path}`, 404);
|
|
1635
|
+
if (procedure.type !== "query") {
|
|
1636
|
+
throw requestError("One-time query requests require a query procedure", 400);
|
|
1637
|
+
}
|
|
1638
|
+
const context = await config.getContext({ connectionId: "query", headers });
|
|
1639
|
+
const db = createDbContext(dbConfig);
|
|
1640
|
+
return config.router.execute(
|
|
1641
|
+
path,
|
|
1642
|
+
{ ...context, db },
|
|
1643
|
+
input
|
|
1644
|
+
);
|
|
1645
|
+
}
|
|
1646
|
+
function createCaller() {
|
|
1647
|
+
function buildProxy(path = []) {
|
|
1648
|
+
return new Proxy(() => void 0, {
|
|
1649
|
+
get(_target, property) {
|
|
1650
|
+
return buildProxy([...path, property]);
|
|
1651
|
+
},
|
|
1652
|
+
apply(_target, _thisArg, args) {
|
|
1653
|
+
return query(path.join("."), args[0]);
|
|
1654
|
+
}
|
|
1655
|
+
});
|
|
1656
|
+
}
|
|
1657
|
+
return buildProxy();
|
|
1658
|
+
}
|
|
1659
|
+
return {
|
|
1660
|
+
handleRequest,
|
|
1661
|
+
registerConnection,
|
|
1662
|
+
unregisterConnection,
|
|
1663
|
+
query,
|
|
1664
|
+
createCaller
|
|
1665
|
+
};
|
|
1666
|
+
}
|
|
1667
|
+
var { compare } = jsonpatch__default.default;
|
|
1668
|
+
function generatePatches(oldValue, newValue) {
|
|
1669
|
+
const operations = compare(
|
|
1670
|
+
oldValue,
|
|
1671
|
+
newValue
|
|
1672
|
+
);
|
|
1673
|
+
return operations.map((op) => ({
|
|
1674
|
+
op: op.op,
|
|
1675
|
+
path: op.path,
|
|
1676
|
+
..."value" in op ? { value: op.value } : {},
|
|
1677
|
+
..."from" in op ? { from: op.from } : {}
|
|
1678
|
+
}));
|
|
1679
|
+
}
|
|
1680
|
+
|
|
1681
|
+
// ../server/src/handlers/stream-utils.ts
|
|
1682
|
+
var MAX_PATCH_MESSAGE_BYTES = 30 * 1024;
|
|
1683
|
+
var MAX_QUERY_ENTRY_BYTES2 = 300 * 1024;
|
|
1684
|
+
var MAX_PROCESS_ATTEMPTS = 4;
|
|
1685
|
+
var REGISTRATION_WINDOW_MS = 60 * 1e3;
|
|
1686
|
+
var RetryableSubscriptionError = class extends Error {
|
|
1687
|
+
constructor(message) {
|
|
1688
|
+
super(message);
|
|
1689
|
+
this.name = "RetryableSubscriptionError";
|
|
1690
|
+
}
|
|
1691
|
+
};
|
|
1692
|
+
function extractTableName(record) {
|
|
1693
|
+
const match = record.eventSourceARN?.match(/table\/([^/]+)/);
|
|
1694
|
+
return match?.[1] ?? null;
|
|
1695
|
+
}
|
|
1696
|
+
function isConditionalFailure(error2) {
|
|
1697
|
+
return error2 instanceof Error && error2.name === "ConditionalCheckFailedException";
|
|
1698
|
+
}
|
|
1699
|
+
function createStreamUtils(config) {
|
|
1700
|
+
const {
|
|
1701
|
+
ddbClient,
|
|
1702
|
+
docClient,
|
|
1703
|
+
connectionsTable,
|
|
1704
|
+
dependenciesTable,
|
|
1705
|
+
queriesTable
|
|
1706
|
+
} = config;
|
|
1707
|
+
let apiClient = config.apiClient;
|
|
1708
|
+
function getApiClient() {
|
|
1709
|
+
apiClient ??= new clientApigatewaymanagementapi.ApiGatewayManagementApiClient({
|
|
1710
|
+
endpoint: config.apiGatewayEndpoint
|
|
1711
|
+
});
|
|
1712
|
+
return apiClient;
|
|
1713
|
+
}
|
|
1714
|
+
async function findAffectedSubscriptions(dependencyKey) {
|
|
1715
|
+
const subscriptions = [];
|
|
1716
|
+
let lastKey;
|
|
1717
|
+
do {
|
|
1718
|
+
const response = await docClient.send(
|
|
1719
|
+
new libDynamodb.QueryCommand({
|
|
1720
|
+
TableName: dependenciesTable,
|
|
1721
|
+
KeyConditionExpression: "pk = :pk",
|
|
1722
|
+
ExpressionAttributeValues: { ":pk": dependencyKey },
|
|
1723
|
+
ConsistentRead: true,
|
|
1724
|
+
ExclusiveStartKey: lastKey
|
|
1725
|
+
})
|
|
1726
|
+
);
|
|
1727
|
+
subscriptions.push(
|
|
1728
|
+
...(response.Items ?? []).map((item) => ({
|
|
1729
|
+
connectionId: item.connectionId,
|
|
1730
|
+
subscriptionId: item.subscriptionId
|
|
1731
|
+
}))
|
|
1732
|
+
);
|
|
1733
|
+
lastKey = response.LastEvaluatedKey;
|
|
1734
|
+
} while (lastKey);
|
|
1735
|
+
return subscriptions;
|
|
1736
|
+
}
|
|
1737
|
+
async function executeQueryFromMetadata(metadata) {
|
|
1738
|
+
return executeQueryMetadata(ddbClient, metadata);
|
|
1739
|
+
}
|
|
1740
|
+
function deliveryMessage(query, delivery) {
|
|
1741
|
+
return {
|
|
1742
|
+
type: delivery.type,
|
|
1743
|
+
subscriptionId: query.subscriptionId,
|
|
1744
|
+
generation: delivery.generation,
|
|
1745
|
+
fromVersion: delivery.fromVersion,
|
|
1746
|
+
toVersion: delivery.toVersion,
|
|
1747
|
+
messageId: delivery.messageId,
|
|
1748
|
+
...delivery.type === "patch" ? { patches: delivery.patches } : {}
|
|
1749
|
+
};
|
|
1750
|
+
}
|
|
1751
|
+
async function sendDelivery(query, delivery) {
|
|
1752
|
+
await getApiClient().send(
|
|
1753
|
+
new clientApigatewaymanagementapi.PostToConnectionCommand({
|
|
1754
|
+
ConnectionId: query.connectionId,
|
|
1755
|
+
Data: Buffer.from(JSON.stringify(deliveryMessage(query, delivery)))
|
|
1756
|
+
})
|
|
1757
|
+
);
|
|
1758
|
+
}
|
|
1759
|
+
async function clearDelivery(query, delivery) {
|
|
1760
|
+
try {
|
|
1761
|
+
await docClient.send(
|
|
1762
|
+
new libDynamodb.UpdateCommand({
|
|
1763
|
+
TableName: queriesTable,
|
|
1764
|
+
Key: { pk: query.pk, sk: query.sk },
|
|
1765
|
+
UpdateExpression: "REMOVE pendingDelivery",
|
|
1766
|
+
ConditionExpression: "#status = :status AND #generation = :generation AND registrationId = :registrationId AND pendingDelivery.messageId = :messageId",
|
|
1767
|
+
ExpressionAttributeNames: {
|
|
1768
|
+
"#status": "status",
|
|
1769
|
+
"#generation": "generation"
|
|
1770
|
+
},
|
|
1771
|
+
ExpressionAttributeValues: {
|
|
1772
|
+
":status": query.status,
|
|
1773
|
+
":generation": query.generation,
|
|
1774
|
+
":registrationId": query.registrationId,
|
|
1775
|
+
":messageId": delivery.messageId
|
|
1776
|
+
}
|
|
1777
|
+
})
|
|
1778
|
+
);
|
|
1779
|
+
} catch (error2) {
|
|
1780
|
+
if (!isConditionalFailure(error2)) throw error2;
|
|
1781
|
+
}
|
|
1782
|
+
}
|
|
1783
|
+
async function cleanupSubscriptionDependencies(query) {
|
|
1784
|
+
await Promise.all(
|
|
1785
|
+
query.dependencies.map(async (dependency) => {
|
|
1786
|
+
try {
|
|
1787
|
+
await docClient.send(
|
|
1788
|
+
new libDynamodb.DeleteCommand({
|
|
1789
|
+
TableName: dependenciesTable,
|
|
1790
|
+
Key: {
|
|
1791
|
+
pk: dependency,
|
|
1792
|
+
sk: `${query.connectionId}#${query.subscriptionId}`
|
|
1793
|
+
},
|
|
1794
|
+
ConditionExpression: "#generation = :generation AND registrationId = :registrationId",
|
|
1795
|
+
ExpressionAttributeNames: { "#generation": "generation" },
|
|
1796
|
+
ExpressionAttributeValues: {
|
|
1797
|
+
":generation": query.generation,
|
|
1798
|
+
":registrationId": query.registrationId
|
|
1799
|
+
}
|
|
1800
|
+
})
|
|
1801
|
+
);
|
|
1802
|
+
} catch (error2) {
|
|
1803
|
+
if (!isConditionalFailure(error2)) throw error2;
|
|
1804
|
+
}
|
|
1805
|
+
})
|
|
1806
|
+
);
|
|
1807
|
+
}
|
|
1808
|
+
async function processSubscription(connectionId, subscriptionId) {
|
|
1809
|
+
try {
|
|
1810
|
+
for (let attempt = 0; attempt < MAX_PROCESS_ATTEMPTS; attempt++) {
|
|
1811
|
+
const response = await docClient.send(
|
|
1812
|
+
new libDynamodb.GetCommand({
|
|
1813
|
+
TableName: queriesTable,
|
|
1814
|
+
Key: { pk: connectionId, sk: subscriptionId },
|
|
1815
|
+
ConsistentRead: true
|
|
1816
|
+
})
|
|
1817
|
+
);
|
|
1818
|
+
const query = response.Item;
|
|
1819
|
+
if (!query || query.status === "cancelled") {
|
|
1820
|
+
return;
|
|
1821
|
+
}
|
|
1822
|
+
if (query.pendingDelivery) {
|
|
1823
|
+
await sendDelivery(query, query.pendingDelivery);
|
|
1824
|
+
await clearDelivery(query, query.pendingDelivery);
|
|
1825
|
+
if (query.status === "faulted") {
|
|
1826
|
+
await cleanupSubscriptionDependencies(query);
|
|
1827
|
+
return;
|
|
1828
|
+
}
|
|
1829
|
+
continue;
|
|
1830
|
+
}
|
|
1831
|
+
if (query.status === "faulted") {
|
|
1832
|
+
await cleanupSubscriptionDependencies(query);
|
|
1833
|
+
return;
|
|
1834
|
+
}
|
|
1835
|
+
if (query.status === "registering") {
|
|
1836
|
+
if (query.createdAt <= Date.now() - REGISTRATION_WINDOW_MS) {
|
|
1837
|
+
try {
|
|
1838
|
+
await docClient.send(
|
|
1839
|
+
new libDynamodb.UpdateCommand({
|
|
1840
|
+
TableName: queriesTable,
|
|
1841
|
+
Key: { pk: query.pk, sk: query.sk },
|
|
1842
|
+
UpdateExpression: "SET #status = :faulted, updatedAt = :updatedAt",
|
|
1843
|
+
ConditionExpression: "#status = :registering AND #generation = :generation AND registrationId = :registrationId AND createdAt = :createdAt AND createdAt <= :registrationCutoff",
|
|
1844
|
+
ExpressionAttributeNames: {
|
|
1845
|
+
"#status": "status",
|
|
1846
|
+
"#generation": "generation"
|
|
1847
|
+
},
|
|
1848
|
+
ExpressionAttributeValues: {
|
|
1849
|
+
":faulted": "faulted",
|
|
1850
|
+
":registering": "registering",
|
|
1851
|
+
":generation": query.generation,
|
|
1852
|
+
":registrationId": query.registrationId,
|
|
1853
|
+
":createdAt": query.createdAt,
|
|
1854
|
+
":registrationCutoff": Date.now() - REGISTRATION_WINDOW_MS,
|
|
1855
|
+
":updatedAt": Date.now()
|
|
1856
|
+
}
|
|
1857
|
+
})
|
|
1858
|
+
);
|
|
1859
|
+
return;
|
|
1860
|
+
} catch (error2) {
|
|
1861
|
+
if (isConditionalFailure(error2)) continue;
|
|
1862
|
+
throw error2;
|
|
1863
|
+
}
|
|
1864
|
+
}
|
|
1865
|
+
throw new RetryableSubscriptionError(
|
|
1866
|
+
`Subscription ${connectionId}/${subscriptionId} is still registering`
|
|
1867
|
+
);
|
|
1868
|
+
}
|
|
1869
|
+
const newResult = await executeQueryFromMetadata(query.queryMetadata);
|
|
1870
|
+
const patches = generatePatches(query.lastResult, newResult);
|
|
1871
|
+
if (patches.length === 0) return;
|
|
1872
|
+
const fromVersion = query.version;
|
|
1873
|
+
const toVersion = fromVersion + 1;
|
|
1874
|
+
const patchDelivery = {
|
|
1875
|
+
type: "patch",
|
|
1876
|
+
generation: query.generation,
|
|
1877
|
+
fromVersion,
|
|
1878
|
+
toVersion,
|
|
1879
|
+
messageId: crypto.randomUUID(),
|
|
1880
|
+
patches
|
|
1881
|
+
};
|
|
1882
|
+
const pendingDelivery = Buffer.byteLength(
|
|
1883
|
+
JSON.stringify(deliveryMessage(query, patchDelivery)),
|
|
1884
|
+
"utf8"
|
|
1885
|
+
) > MAX_PATCH_MESSAGE_BYTES ? {
|
|
1886
|
+
type: "resync",
|
|
1887
|
+
generation: query.generation,
|
|
1888
|
+
fromVersion,
|
|
1889
|
+
toVersion,
|
|
1890
|
+
messageId: patchDelivery.messageId
|
|
1891
|
+
} : patchDelivery;
|
|
1892
|
+
const persisted = {
|
|
1893
|
+
...query,
|
|
1894
|
+
lastResult: newResult,
|
|
1895
|
+
version: toVersion,
|
|
1896
|
+
pendingDelivery
|
|
1897
|
+
};
|
|
1898
|
+
if (Buffer.byteLength(JSON.stringify(persisted), "utf8") > MAX_QUERY_ENTRY_BYTES2) {
|
|
1899
|
+
const faultDelivery = {
|
|
1900
|
+
type: "resync",
|
|
1901
|
+
generation: query.generation,
|
|
1902
|
+
fromVersion,
|
|
1903
|
+
toVersion,
|
|
1904
|
+
messageId: pendingDelivery.messageId
|
|
1905
|
+
};
|
|
1906
|
+
try {
|
|
1907
|
+
await docClient.send(
|
|
1908
|
+
new libDynamodb.UpdateCommand({
|
|
1909
|
+
TableName: queriesTable,
|
|
1910
|
+
Key: { pk: query.pk, sk: query.sk },
|
|
1911
|
+
UpdateExpression: "SET #status = :faulted, pendingDelivery = :pendingDelivery, updatedAt = :updatedAt",
|
|
1912
|
+
ConditionExpression: "#status = :active AND #generation = :generation AND registrationId = :registrationId AND #version = :fromVersion AND attribute_not_exists(pendingDelivery)",
|
|
1913
|
+
ExpressionAttributeNames: {
|
|
1914
|
+
"#status": "status",
|
|
1915
|
+
"#generation": "generation",
|
|
1916
|
+
"#version": "version"
|
|
1917
|
+
},
|
|
1918
|
+
ExpressionAttributeValues: {
|
|
1919
|
+
":active": "active",
|
|
1920
|
+
":faulted": "faulted",
|
|
1921
|
+
":generation": query.generation,
|
|
1922
|
+
":registrationId": query.registrationId,
|
|
1923
|
+
":fromVersion": fromVersion,
|
|
1924
|
+
":pendingDelivery": faultDelivery,
|
|
1925
|
+
":updatedAt": Date.now()
|
|
1926
|
+
}
|
|
1927
|
+
})
|
|
1928
|
+
);
|
|
1929
|
+
} catch (error2) {
|
|
1930
|
+
if (isConditionalFailure(error2)) {
|
|
1931
|
+
throw new RetryableSubscriptionError(
|
|
1932
|
+
`Subscription ${connectionId}/${subscriptionId} changed concurrently`
|
|
1933
|
+
);
|
|
1934
|
+
}
|
|
1935
|
+
throw error2;
|
|
1936
|
+
}
|
|
1937
|
+
continue;
|
|
1938
|
+
}
|
|
1939
|
+
try {
|
|
1940
|
+
await docClient.send(
|
|
1941
|
+
new libDynamodb.UpdateCommand({
|
|
1942
|
+
TableName: queriesTable,
|
|
1943
|
+
Key: { pk: query.pk, sk: query.sk },
|
|
1944
|
+
UpdateExpression: "SET lastResult = :lastResult, #version = :toVersion, pendingDelivery = :pendingDelivery, updatedAt = :updatedAt",
|
|
1945
|
+
ConditionExpression: "#status = :active AND #generation = :generation AND registrationId = :registrationId AND #version = :fromVersion AND attribute_not_exists(pendingDelivery)",
|
|
1946
|
+
ExpressionAttributeNames: {
|
|
1947
|
+
"#status": "status",
|
|
1948
|
+
"#generation": "generation",
|
|
1949
|
+
"#version": "version"
|
|
1950
|
+
},
|
|
1951
|
+
ExpressionAttributeValues: {
|
|
1952
|
+
":active": "active",
|
|
1953
|
+
":generation": query.generation,
|
|
1954
|
+
":registrationId": query.registrationId,
|
|
1955
|
+
":fromVersion": fromVersion,
|
|
1956
|
+
":toVersion": toVersion,
|
|
1957
|
+
":lastResult": newResult,
|
|
1958
|
+
":pendingDelivery": pendingDelivery,
|
|
1959
|
+
":updatedAt": Date.now()
|
|
1960
|
+
}
|
|
1961
|
+
})
|
|
1962
|
+
);
|
|
1963
|
+
} catch (error2) {
|
|
1964
|
+
if (isConditionalFailure(error2)) {
|
|
1965
|
+
throw new RetryableSubscriptionError(
|
|
1966
|
+
`Subscription ${connectionId}/${subscriptionId} changed concurrently`
|
|
1967
|
+
);
|
|
1968
|
+
}
|
|
1969
|
+
throw error2;
|
|
1970
|
+
}
|
|
1971
|
+
await sendDelivery(query, pendingDelivery);
|
|
1972
|
+
await clearDelivery(query, pendingDelivery);
|
|
1973
|
+
info("[stream] Delivery completed");
|
|
1974
|
+
return;
|
|
1975
|
+
}
|
|
1976
|
+
throw new RetryableSubscriptionError(
|
|
1977
|
+
`Subscription ${connectionId}/${subscriptionId} delivery did not settle`
|
|
1978
|
+
);
|
|
1979
|
+
} catch (error2) {
|
|
1980
|
+
if (error2 instanceof clientApigatewaymanagementapi.GoneException) {
|
|
1981
|
+
await cleanupConnection(connectionId);
|
|
1982
|
+
return;
|
|
1983
|
+
}
|
|
1984
|
+
throw error2;
|
|
1985
|
+
}
|
|
1986
|
+
}
|
|
1987
|
+
async function cleanupConnection(connectionId) {
|
|
1988
|
+
try {
|
|
1989
|
+
await docClient.send(
|
|
1990
|
+
new libDynamodb.UpdateCommand({
|
|
1991
|
+
TableName: connectionsTable,
|
|
1992
|
+
Key: { connectionId },
|
|
1993
|
+
UpdateExpression: "SET #status = :closed",
|
|
1994
|
+
ConditionExpression: "attribute_exists(connectionId)",
|
|
1995
|
+
ExpressionAttributeNames: { "#status": "status" },
|
|
1996
|
+
ExpressionAttributeValues: { ":closed": "closed" }
|
|
1997
|
+
})
|
|
1998
|
+
);
|
|
1999
|
+
} catch (error2) {
|
|
2000
|
+
if (!isConditionalFailure(error2)) throw error2;
|
|
2001
|
+
}
|
|
2002
|
+
let lastKey;
|
|
2003
|
+
do {
|
|
2004
|
+
const response = await docClient.send(
|
|
2005
|
+
new libDynamodb.QueryCommand({
|
|
2006
|
+
TableName: queriesTable,
|
|
2007
|
+
KeyConditionExpression: "pk = :pk",
|
|
2008
|
+
ExpressionAttributeValues: { ":pk": connectionId },
|
|
2009
|
+
ConsistentRead: true,
|
|
2010
|
+
ExclusiveStartKey: lastKey
|
|
2011
|
+
})
|
|
2012
|
+
);
|
|
2013
|
+
for (const item of response.Items ?? []) {
|
|
2014
|
+
const query = item;
|
|
2015
|
+
if (query.status !== "cancelled") {
|
|
2016
|
+
try {
|
|
2017
|
+
await docClient.send(
|
|
2018
|
+
new libDynamodb.UpdateCommand({
|
|
2019
|
+
TableName: queriesTable,
|
|
2020
|
+
Key: { pk: query.pk, sk: query.sk },
|
|
2021
|
+
UpdateExpression: "SET #status = :cancelled, updatedAt = :updatedAt REMOVE pendingDelivery",
|
|
2022
|
+
ConditionExpression: "#generation = :generation AND registrationId = :registrationId",
|
|
2023
|
+
ExpressionAttributeNames: {
|
|
2024
|
+
"#status": "status",
|
|
2025
|
+
"#generation": "generation"
|
|
2026
|
+
},
|
|
2027
|
+
ExpressionAttributeValues: {
|
|
2028
|
+
":cancelled": "cancelled",
|
|
2029
|
+
":generation": query.generation,
|
|
2030
|
+
":registrationId": query.registrationId,
|
|
2031
|
+
":updatedAt": Date.now()
|
|
2032
|
+
}
|
|
2033
|
+
})
|
|
2034
|
+
);
|
|
2035
|
+
} catch (error2) {
|
|
2036
|
+
if (!isConditionalFailure(error2)) throw error2;
|
|
2037
|
+
continue;
|
|
2038
|
+
}
|
|
2039
|
+
}
|
|
2040
|
+
await cleanupSubscriptionDependencies(query);
|
|
2041
|
+
}
|
|
2042
|
+
lastKey = response.LastEvaluatedKey;
|
|
2043
|
+
} while (lastKey);
|
|
2044
|
+
}
|
|
2045
|
+
return {
|
|
2046
|
+
findAffectedSubscriptions,
|
|
2047
|
+
executeQueryFromMetadata,
|
|
2048
|
+
sendDelivery,
|
|
2049
|
+
processSubscription,
|
|
2050
|
+
cleanupConnection
|
|
2051
|
+
};
|
|
2052
|
+
}
|
|
2053
|
+
|
|
2054
|
+
// ../server/src/handlers/stream-handler.ts
|
|
2055
|
+
var MAX_FANOUT_CONCURRENCY = 10;
|
|
2056
|
+
var DYNAMODB_PARTITION_KEY_MAX_BYTES3 = 2048;
|
|
2057
|
+
function assertConnectionId(connectionId) {
|
|
2058
|
+
if (typeof connectionId !== "string" || connectionId.length === 0 || Buffer.byteLength(connectionId, "utf8") > DYNAMODB_PARTITION_KEY_MAX_BYTES3) {
|
|
2059
|
+
throw new Error("connectionId is not a valid DynamoDB partition key");
|
|
2060
|
+
}
|
|
2061
|
+
}
|
|
2062
|
+
async function mapWithConcurrency(values, concurrency, mapper) {
|
|
2063
|
+
const results = new Array(values.length);
|
|
2064
|
+
let nextIndex = 0;
|
|
2065
|
+
const workers = Array.from(
|
|
2066
|
+
{ length: Math.min(concurrency, values.length) },
|
|
2067
|
+
async () => {
|
|
2068
|
+
while (nextIndex < values.length) {
|
|
2069
|
+
const index = nextIndex++;
|
|
2070
|
+
results[index] = await mapper(values[index], index);
|
|
2071
|
+
}
|
|
2072
|
+
}
|
|
2073
|
+
);
|
|
2074
|
+
await Promise.all(workers);
|
|
2075
|
+
return results;
|
|
2076
|
+
}
|
|
2077
|
+
async function processRecord(record, utils) {
|
|
2078
|
+
const tableName = extractTableName(record);
|
|
2079
|
+
if (!tableName || !record.dynamodb) {
|
|
2080
|
+
throw new Error("Malformed DynamoDB stream record");
|
|
2081
|
+
}
|
|
2082
|
+
const affectedKeys = new Set(extractAffectedKeys(tableName, {}));
|
|
2083
|
+
for (const image of [record.dynamodb.NewImage, record.dynamodb.OldImage]) {
|
|
2084
|
+
if (!image) continue;
|
|
2085
|
+
const item = utilDynamodb.unmarshall(image);
|
|
2086
|
+
for (const key of extractAffectedKeys(tableName, item)) affectedKeys.add(key);
|
|
2087
|
+
}
|
|
2088
|
+
const subscriptions = /* @__PURE__ */ new Map();
|
|
2089
|
+
const lookupResults = await mapWithConcurrency(
|
|
2090
|
+
[...affectedKeys],
|
|
2091
|
+
MAX_FANOUT_CONCURRENCY,
|
|
2092
|
+
(key) => utils.findAffectedSubscriptions(key)
|
|
2093
|
+
);
|
|
2094
|
+
for (const result of lookupResults) {
|
|
2095
|
+
for (const subscription of result) {
|
|
2096
|
+
subscriptions.set(
|
|
2097
|
+
`${subscription.connectionId}\0${subscription.subscriptionId}`,
|
|
2098
|
+
subscription
|
|
2099
|
+
);
|
|
2100
|
+
}
|
|
2101
|
+
}
|
|
2102
|
+
await mapWithConcurrency(
|
|
2103
|
+
[...subscriptions.values()],
|
|
2104
|
+
MAX_FANOUT_CONCURRENCY,
|
|
2105
|
+
({ connectionId, subscriptionId }) => utils.processSubscription(connectionId, subscriptionId)
|
|
2106
|
+
);
|
|
2107
|
+
}
|
|
2108
|
+
async function processStreamEvent(event, utils) {
|
|
2109
|
+
const results = await mapWithConcurrency(
|
|
2110
|
+
event.Records,
|
|
2111
|
+
MAX_FANOUT_CONCURRENCY,
|
|
2112
|
+
async (record) => {
|
|
2113
|
+
try {
|
|
2114
|
+
await processRecord(record, utils);
|
|
2115
|
+
return null;
|
|
2116
|
+
} catch (error2) {
|
|
2117
|
+
error("[stream] Record failed:", error2);
|
|
2118
|
+
const itemIdentifier = record.dynamodb?.SequenceNumber;
|
|
2119
|
+
if (!itemIdentifier) {
|
|
2120
|
+
throw new Error(
|
|
2121
|
+
"Failed DynamoDB stream record has no sequence number",
|
|
2122
|
+
{ cause: error2 }
|
|
2123
|
+
);
|
|
2124
|
+
}
|
|
2125
|
+
return {
|
|
2126
|
+
itemIdentifier
|
|
2127
|
+
};
|
|
2128
|
+
}
|
|
2129
|
+
}
|
|
2130
|
+
);
|
|
2131
|
+
return {
|
|
2132
|
+
batchItemFailures: results.filter(
|
|
2133
|
+
(failure) => failure !== null
|
|
2134
|
+
)
|
|
2135
|
+
};
|
|
2136
|
+
}
|
|
2137
|
+
function clients(config) {
|
|
2138
|
+
const ddbClient = config.dbConfig?.client ?? new clientDynamodb.DynamoDBClient({
|
|
2139
|
+
region: config.dbConfig?.region ?? process.env.AWS_REGION,
|
|
2140
|
+
endpoint: config.dbConfig?.endpoint
|
|
2141
|
+
});
|
|
2142
|
+
const docClient = config.dbConfig?.docClient ?? libDynamodb.DynamoDBDocumentClient.from(ddbClient, {
|
|
2143
|
+
marshallOptions: { removeUndefinedValues: true }
|
|
2144
|
+
});
|
|
2145
|
+
return { ddbClient, docClient };
|
|
2146
|
+
}
|
|
2147
|
+
function createConnectHandler(config) {
|
|
2148
|
+
const connectionsTable = config.connectionsTableName;
|
|
2149
|
+
const { docClient } = clients(config);
|
|
2150
|
+
return async function handler(event) {
|
|
2151
|
+
const connectionId = event.requestContext.connectionId;
|
|
2152
|
+
const now = Date.now();
|
|
2153
|
+
try {
|
|
2154
|
+
assertConnectionId(connectionId);
|
|
2155
|
+
const authorizerContext = jsonSafeRecord(event.requestContext.authorizer);
|
|
2156
|
+
const requestIdentity = jsonSafeRecord(event.requestContext.identity);
|
|
2157
|
+
const connectionEntry = {
|
|
2158
|
+
connectionId,
|
|
2159
|
+
status: "open",
|
|
2160
|
+
...authorizerContext ? { authorizerContext } : {},
|
|
2161
|
+
...requestIdentity ? { requestIdentity } : {},
|
|
2162
|
+
connectedAt: now,
|
|
2163
|
+
ttl: Math.floor(now / 1e3) + DEFAULT_CONNECTION_LEASE_SECONDS
|
|
2164
|
+
};
|
|
2165
|
+
await docClient.send(
|
|
2166
|
+
new libDynamodb.PutCommand({ TableName: connectionsTable, Item: connectionEntry })
|
|
2167
|
+
);
|
|
2168
|
+
debug("[stream] Connection established");
|
|
2169
|
+
return { statusCode: 200 };
|
|
2170
|
+
} catch (error2) {
|
|
2171
|
+
error("Error creating connection:", error2);
|
|
2172
|
+
return { statusCode: 500 };
|
|
2173
|
+
}
|
|
2174
|
+
};
|
|
2175
|
+
}
|
|
2176
|
+
function jsonSafeRecord(value) {
|
|
2177
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
|
|
2178
|
+
try {
|
|
2179
|
+
const encoded = JSON.stringify(value, (_key, item) => {
|
|
2180
|
+
if (typeof item === "bigint") return item.toString();
|
|
2181
|
+
if (typeof item === "function" || typeof item === "symbol" || item === void 0) {
|
|
2182
|
+
return void 0;
|
|
2183
|
+
}
|
|
2184
|
+
return item;
|
|
2185
|
+
});
|
|
2186
|
+
if (!encoded) return void 0;
|
|
2187
|
+
return JSON.parse(encoded);
|
|
2188
|
+
} catch {
|
|
2189
|
+
return void 0;
|
|
2190
|
+
}
|
|
2191
|
+
}
|
|
2192
|
+
function createDisconnectHandler(config) {
|
|
2193
|
+
const connectionsTable = config.connectionsTableName;
|
|
2194
|
+
const dependenciesTable = config.dependenciesTableName;
|
|
2195
|
+
const queriesTable = config.queriesTableName;
|
|
2196
|
+
const { ddbClient, docClient } = clients(config);
|
|
2197
|
+
const utils = createStreamUtils({
|
|
2198
|
+
ddbClient,
|
|
2199
|
+
docClient,
|
|
2200
|
+
apiClient: config.apiClient,
|
|
2201
|
+
apiGatewayEndpoint: config.apiGatewayEndpoint,
|
|
2202
|
+
connectionsTable,
|
|
2203
|
+
dependenciesTable,
|
|
2204
|
+
queriesTable
|
|
2205
|
+
});
|
|
2206
|
+
return async function handler(event) {
|
|
2207
|
+
try {
|
|
2208
|
+
const connectionId = event.requestContext.connectionId;
|
|
2209
|
+
assertConnectionId(connectionId);
|
|
2210
|
+
await utils.cleanupConnection(connectionId);
|
|
2211
|
+
return { statusCode: 200 };
|
|
2212
|
+
} catch (error2) {
|
|
2213
|
+
error("Error removing connection:", error2);
|
|
2214
|
+
return { statusCode: 500 };
|
|
2215
|
+
}
|
|
2216
|
+
};
|
|
2217
|
+
}
|
|
2218
|
+
|
|
2219
|
+
// ../server/src/handlers/lambda-handlers.ts
|
|
2220
|
+
function requiredTableName(configured, environmentName) {
|
|
2221
|
+
const value = configured ?? process.env[environmentName];
|
|
2222
|
+
if (!value) throw new Error(`${environmentName} must name a system table`);
|
|
2223
|
+
return value;
|
|
2224
|
+
}
|
|
2225
|
+
function assertRequestKey(name, value, maxBytes) {
|
|
2226
|
+
if (typeof value !== "string" || value.length === 0 || Buffer.byteLength(value, "utf8") > maxBytes) {
|
|
2227
|
+
throw new Error(`${name} is not a valid DynamoDB key`);
|
|
2228
|
+
}
|
|
2229
|
+
return value;
|
|
2230
|
+
}
|
|
2231
|
+
function createLambdaHandlers(config = {}) {
|
|
2232
|
+
const connectionsTable = requiredTableName(
|
|
2233
|
+
config.connectionsTableName,
|
|
2234
|
+
"CONNECTIONS_TABLE"
|
|
2235
|
+
);
|
|
2236
|
+
const dependenciesTable = requiredTableName(
|
|
2237
|
+
config.dependenciesTableName,
|
|
2238
|
+
"DEPENDENCIES_TABLE"
|
|
2239
|
+
);
|
|
2240
|
+
const queriesTable = requiredTableName(
|
|
2241
|
+
config.queriesTableName,
|
|
2242
|
+
"QUERIES_TABLE"
|
|
2243
|
+
);
|
|
2244
|
+
const apiGatewayEndpoint = config.apiGatewayEndpoint ?? process.env.WEBSOCKET_ENDPOINT ?? "";
|
|
2245
|
+
const ddbClient = config.dbConfig?.client ?? new clientDynamodb.DynamoDBClient({
|
|
2246
|
+
region: config.dbConfig?.region ?? process.env.AWS_REGION,
|
|
2247
|
+
endpoint: config.dbConfig?.endpoint
|
|
2248
|
+
});
|
|
2249
|
+
const docClient = config.dbConfig?.docClient ?? libDynamodb.DynamoDBDocumentClient.from(ddbClient, {
|
|
2250
|
+
marshallOptions: { removeUndefinedValues: true }
|
|
2251
|
+
});
|
|
2252
|
+
const apiClient = config.apiClient ?? new clientApigatewaymanagementapi.ApiGatewayManagementApiClient({ endpoint: apiGatewayEndpoint });
|
|
2253
|
+
const sharedConfig = {
|
|
2254
|
+
dbConfig: { ...config.dbConfig, client: ddbClient, docClient },
|
|
2255
|
+
apiClient,
|
|
2256
|
+
apiGatewayEndpoint,
|
|
2257
|
+
connectionsTableName: connectionsTable,
|
|
2258
|
+
dependenciesTableName: dependenciesTable,
|
|
2259
|
+
queriesTableName: queriesTable
|
|
2260
|
+
};
|
|
2261
|
+
const connect = createConnectHandler(sharedConfig);
|
|
2262
|
+
const disconnect = createDisconnectHandler(sharedConfig);
|
|
2263
|
+
const streamUtils = createStreamUtils({
|
|
2264
|
+
ddbClient,
|
|
2265
|
+
docClient,
|
|
2266
|
+
apiClient,
|
|
2267
|
+
apiGatewayEndpoint,
|
|
2268
|
+
connectionsTable,
|
|
2269
|
+
dependenciesTable,
|
|
2270
|
+
queriesTable
|
|
2271
|
+
});
|
|
2272
|
+
async function connectHandler(event) {
|
|
2273
|
+
const result = await connect({
|
|
2274
|
+
requestContext: {
|
|
2275
|
+
connectionId: event.requestContext.connectionId,
|
|
2276
|
+
authorizer: event.requestContext.authorizer,
|
|
2277
|
+
identity: event.requestContext.identity
|
|
2278
|
+
}
|
|
2279
|
+
});
|
|
2280
|
+
return {
|
|
2281
|
+
statusCode: result.statusCode,
|
|
2282
|
+
body: result.statusCode === 200 ? "Connected" : "Failed to connect"
|
|
2283
|
+
};
|
|
2284
|
+
}
|
|
2285
|
+
async function disconnectHandler(event) {
|
|
2286
|
+
const result = await disconnect({
|
|
2287
|
+
requestContext: { connectionId: event.requestContext.connectionId }
|
|
2288
|
+
});
|
|
2289
|
+
return {
|
|
2290
|
+
statusCode: result.statusCode,
|
|
2291
|
+
body: result.statusCode === 200 ? "Disconnected" : "Failed to disconnect"
|
|
2292
|
+
};
|
|
2293
|
+
}
|
|
2294
|
+
async function messageHandler(event) {
|
|
2295
|
+
const connectionId = event.requestContext.connectionId;
|
|
2296
|
+
try {
|
|
2297
|
+
assertRequestKey("connectionId", connectionId, 2048);
|
|
2298
|
+
const body = JSON.parse(event.body ?? "{}");
|
|
2299
|
+
let response;
|
|
2300
|
+
if (body.type === "init") {
|
|
2301
|
+
response = { type: "connected", connectionId };
|
|
2302
|
+
} else if (body.type === "ping") {
|
|
2303
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
2304
|
+
await docClient.send(
|
|
2305
|
+
new libDynamodb.UpdateCommand({
|
|
2306
|
+
TableName: connectionsTable,
|
|
2307
|
+
Key: { connectionId },
|
|
2308
|
+
UpdateExpression: "SET #ttl = :ttl",
|
|
2309
|
+
ConditionExpression: "#status = :open AND #ttl > :now",
|
|
2310
|
+
ExpressionAttributeNames: { "#status": "status", "#ttl": "ttl" },
|
|
2311
|
+
ExpressionAttributeValues: {
|
|
2312
|
+
":open": "open",
|
|
2313
|
+
":now": now,
|
|
2314
|
+
":ttl": now + DEFAULT_CONNECTION_LEASE_SECONDS
|
|
2315
|
+
}
|
|
2316
|
+
})
|
|
2317
|
+
);
|
|
2318
|
+
response = { type: "pong" };
|
|
2319
|
+
} else {
|
|
2320
|
+
response = {
|
|
2321
|
+
type: "error",
|
|
2322
|
+
message: "Unsupported WebSocket message type"
|
|
2323
|
+
};
|
|
2324
|
+
}
|
|
2325
|
+
await apiClient.send(
|
|
2326
|
+
new clientApigatewaymanagementapi.PostToConnectionCommand({
|
|
2327
|
+
ConnectionId: connectionId,
|
|
2328
|
+
Data: Buffer.from(JSON.stringify(response))
|
|
2329
|
+
})
|
|
2330
|
+
);
|
|
2331
|
+
return { statusCode: 200, body: "OK" };
|
|
2332
|
+
} catch (error2) {
|
|
2333
|
+
error("Error handling message:", error2);
|
|
2334
|
+
return { statusCode: 500, body: "Internal server error" };
|
|
2335
|
+
}
|
|
2336
|
+
}
|
|
2337
|
+
async function streamHandler(event) {
|
|
2338
|
+
return processStreamEvent(event, streamUtils);
|
|
2339
|
+
}
|
|
2340
|
+
return { connectHandler, disconnectHandler, messageHandler, streamHandler };
|
|
2341
|
+
}
|
|
2342
|
+
|
|
2343
|
+
exports.InvalidInputError = InvalidInputError;
|
|
2344
|
+
exports.SafeRequestError = SafeRequestError;
|
|
2345
|
+
exports.createDbContext = createDbContext;
|
|
2346
|
+
exports.createLambdaHandlers = createLambdaHandlers;
|
|
2347
|
+
exports.createReactiveHandler = createReactiveHandler;
|
|
2348
|
+
exports.initReactive = initReactive;
|
|
2349
|
+
exports.isSafeRequestError = isSafeRequestError;
|
|
2350
|
+
//# sourceMappingURL=server.cjs.map
|
|
2351
|
+
//# sourceMappingURL=server.cjs.map
|