velocious 1.0.492 → 1.0.493

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.
@@ -0,0 +1,102 @@
1
+ /**
2
+ * Declarative field-map upsert applier for sync replay mutations.
3
+ *
4
+ * Owns the generic mechanics every per-resource replay handler repeats:
5
+ * present-key filtering, per-field type coercion, unknown-key rejection, the
6
+ * find-or-create upsert, the delete branch, optional snapshot serialization,
7
+ * and the domain after-apply tail. Apps declare only the field map and hooks.
8
+ */
9
+ export default class SyncReplayUpsertApplier {
10
+ /**
11
+ * Creates a declarative upsert applier.
12
+ * @param {object} args - Applier configuration.
13
+ * @param {?} args.modelClass - Model class receiving the mutations.
14
+ * @param {Record<string, string | {type: string, column?: string} | ((value: ?, label: string) => ?)>} args.fields - Data-key → field-type map: a named type, a {type, column} rename spec, "ignored" for accepted-but-dropped keys, or a custom coercer function.
15
+ * @param {(args: {data: Record<string, ?>, mutation: ?, context: Record<string, ?>}) => Promise<?>} [args.findRecord] - Custom record resolver. Defaults to findBy({id: mutation.resourceId}).
16
+ * @param {(args: {mutation: ?, context: Record<string, ?>}) => Promise<?>} [args.findRecordForDelete] - Custom delete resolver. Defaults to findRecord.
17
+ * @param {"error" | "ignore"} [args.restArgs] - Unknown data-key handling. Defaults to "error".
18
+ * @param {(args: {mappedAttributes: Record<string, ?>, mutation: ?, context: Record<string, ?>, record: ?}) => Promise<Record<string, ?> | void>} [args.afterApply] - Domain tail; its returned object merges into the apply result.
19
+ * @param {(args: {mutation: ?, context: Record<string, ?>, record: ?}) => Promise<Record<string, ?>> | Record<string, ?>} [args.serialize] - Snapshot serializer; result lands on applyResult.serializedData.
20
+ */
21
+ constructor({ afterApply, fields, findRecord, findRecordForDelete, modelClass, restArgs, serialize }: {
22
+ modelClass: unknown;
23
+ fields: Record<string, string | {
24
+ type: string;
25
+ column?: string;
26
+ } | ((value: unknown, label: string) => unknown)>;
27
+ findRecord?: ((args: {
28
+ data: Record<string, unknown>;
29
+ mutation: unknown;
30
+ context: Record<string, unknown>;
31
+ }) => Promise<unknown>) | undefined;
32
+ findRecordForDelete?: ((args: {
33
+ mutation: unknown;
34
+ context: Record<string, unknown>;
35
+ }) => Promise<unknown>) | undefined;
36
+ restArgs?: "error" | "ignore" | undefined;
37
+ afterApply?: ((args: {
38
+ mappedAttributes: Record<string, unknown>;
39
+ mutation: unknown;
40
+ context: Record<string, unknown>;
41
+ record: unknown;
42
+ }) => Promise<Record<string, unknown> | void>) | undefined;
43
+ serialize?: ((args: {
44
+ mutation: unknown;
45
+ context: Record<string, unknown>;
46
+ record: unknown;
47
+ }) => Promise<Record<string, unknown>> | Record<string, unknown>) | undefined;
48
+ });
49
+ afterApply: ((args: {
50
+ mappedAttributes: Record<string, unknown>;
51
+ mutation: unknown;
52
+ context: Record<string, unknown>;
53
+ record: unknown;
54
+ }) => Promise<Record<string, unknown> | void>) | undefined;
55
+ fields: Record<string, string | {
56
+ type: string;
57
+ column?: string;
58
+ } | ((value: unknown, label: string) => unknown)>;
59
+ findRecord: ((args: {
60
+ data: Record<string, unknown>;
61
+ mutation: unknown;
62
+ context: Record<string, unknown>;
63
+ }) => Promise<unknown>) | undefined;
64
+ findRecordForDelete: ((args: {
65
+ mutation: unknown;
66
+ context: Record<string, unknown>;
67
+ }) => Promise<unknown>) | undefined;
68
+ modelClass: any;
69
+ restArgs: "error" | "ignore";
70
+ serialize: ((args: {
71
+ mutation: unknown;
72
+ context: Record<string, unknown>;
73
+ record: unknown;
74
+ }) => Promise<Record<string, unknown>> | Record<string, unknown>) | undefined;
75
+ /**
76
+ * Applies one normalized replay mutation to the declared model.
77
+ * @param {{actor: ?, context: Record<string, ?>, existingSync: ?, mutation: ?}} args - Replay apply arguments.
78
+ * @returns {Promise<Record<string, ?>>} Apply result with the record, created/deleted flags, optional serializedData, and afterApply extras.
79
+ */
80
+ apply({ context, mutation }: {
81
+ actor: unknown;
82
+ context: Record<string, unknown>;
83
+ existingSync: unknown;
84
+ mutation: unknown;
85
+ }): Promise<Record<string, unknown>>;
86
+ /**
87
+ * Applies a delete mutation to the declared model.
88
+ * @param {{context: Record<string, ?>, mutation: ?}} args - Delete arguments.
89
+ * @returns {Promise<Record<string, ?>>} Apply result with the deleted flag.
90
+ */
91
+ applyDelete({ context, mutation }: {
92
+ context: Record<string, unknown>;
93
+ mutation: unknown;
94
+ }): Promise<Record<string, unknown>>;
95
+ /**
96
+ * Maps present data keys through the declared field types.
97
+ * @param {Record<string, ?>} data - Normalized mutation data.
98
+ * @returns {Record<string, ?>} Coerced attributes keyed by column name.
99
+ */
100
+ mappedAttributes(data: Record<string, unknown>): Record<string, unknown>;
101
+ }
102
+ //# sourceMappingURL=sync-replay-upsert-applier.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sync-replay-upsert-applier.d.ts","sourceRoot":"","sources":["../../../src/sync/sync-replay-upsert-applier.js"],"names":[],"mappings":"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"}
@@ -0,0 +1,158 @@
1
+ // @ts-check
2
+ import { optionalFloat, optionalInteger, optionalString } from "typanic";
3
+ import SyncApiClient from "./sync-api-client.js";
4
+ /**
5
+ * Coercers for the declarative sync field types. All are present-key based:
6
+ * a field is only applied when its key exists in the mutation data.
7
+ * @type {Record<string, (value: ?, label: string) => ?>}
8
+ */
9
+ const FIELD_TYPES = {
10
+ booleanOrNull: (value, label) => SyncApiClient.optionalBooleanSyncValue(value, label),
11
+ dateOrNull: (value, label) => optionalSyncDate(value, label),
12
+ floatOrNull: (value, label) => optionalFloat(value, label),
13
+ integerOrNull: (value, label) => optionalInteger(value, label),
14
+ raw: (value) => value,
15
+ stringOrNull: (value, label) => optionalString(value, label)
16
+ };
17
+ /**
18
+ * Parses an optional date value, failing loudly on invalid input.
19
+ * @param {?} value - Date, parseable string, or epoch number.
20
+ * @param {string} label - Field label for error messages.
21
+ * @returns {Date | null} Parsed date or null when absent.
22
+ */
23
+ function optionalSyncDate(value, label) {
24
+ if (value === null || value === undefined)
25
+ return null;
26
+ if (value instanceof Date && !Number.isNaN(value.getTime()))
27
+ return value;
28
+ if (typeof value === "string" || typeof value === "number") {
29
+ const dateValue = new Date(value);
30
+ if (!Number.isNaN(dateValue.getTime()))
31
+ return dateValue;
32
+ }
33
+ throw new Error(`Expected ${label} to be a valid date or null`);
34
+ }
35
+ /**
36
+ * Declarative field-map upsert applier for sync replay mutations.
37
+ *
38
+ * Owns the generic mechanics every per-resource replay handler repeats:
39
+ * present-key filtering, per-field type coercion, unknown-key rejection, the
40
+ * find-or-create upsert, the delete branch, optional snapshot serialization,
41
+ * and the domain after-apply tail. Apps declare only the field map and hooks.
42
+ */
43
+ export default class SyncReplayUpsertApplier {
44
+ /**
45
+ * Creates a declarative upsert applier.
46
+ * @param {object} args - Applier configuration.
47
+ * @param {?} args.modelClass - Model class receiving the mutations.
48
+ * @param {Record<string, string | {type: string, column?: string} | ((value: ?, label: string) => ?)>} args.fields - Data-key → field-type map: a named type, a {type, column} rename spec, "ignored" for accepted-but-dropped keys, or a custom coercer function.
49
+ * @param {(args: {data: Record<string, ?>, mutation: ?, context: Record<string, ?>}) => Promise<?>} [args.findRecord] - Custom record resolver. Defaults to findBy({id: mutation.resourceId}).
50
+ * @param {(args: {mutation: ?, context: Record<string, ?>}) => Promise<?>} [args.findRecordForDelete] - Custom delete resolver. Defaults to findRecord.
51
+ * @param {"error" | "ignore"} [args.restArgs] - Unknown data-key handling. Defaults to "error".
52
+ * @param {(args: {mappedAttributes: Record<string, ?>, mutation: ?, context: Record<string, ?>, record: ?}) => Promise<Record<string, ?> | void>} [args.afterApply] - Domain tail; its returned object merges into the apply result.
53
+ * @param {(args: {mutation: ?, context: Record<string, ?>, record: ?}) => Promise<Record<string, ?>> | Record<string, ?>} [args.serialize] - Snapshot serializer; result lands on applyResult.serializedData.
54
+ */
55
+ constructor({ afterApply, fields, findRecord, findRecordForDelete, modelClass, restArgs = "error", serialize }) {
56
+ if (!modelClass)
57
+ throw new Error("SyncReplayUpsertApplier requires a modelClass");
58
+ if (!fields || typeof fields !== "object")
59
+ throw new Error("SyncReplayUpsertApplier requires a fields map");
60
+ if (restArgs !== "error" && restArgs !== "ignore") {
61
+ throw new Error(`SyncReplayUpsertApplier restArgs must be "error" or "ignore", got: ${String(restArgs)}`);
62
+ }
63
+ for (const [fieldName, fieldSpec] of Object.entries(fields)) {
64
+ if (typeof fieldSpec === "function")
65
+ continue;
66
+ const fieldType = typeof fieldSpec === "string" ? fieldSpec : fieldSpec.type;
67
+ if (fieldType !== "ignored" && !(fieldType in FIELD_TYPES)) {
68
+ throw new Error(`Unknown sync field type: ${fieldType} for: ${fieldName}`);
69
+ }
70
+ }
71
+ this.afterApply = afterApply;
72
+ this.fields = fields;
73
+ this.findRecord = findRecord;
74
+ this.findRecordForDelete = findRecordForDelete;
75
+ this.modelClass = modelClass;
76
+ this.restArgs = restArgs;
77
+ this.serialize = serialize;
78
+ }
79
+ /**
80
+ * Applies one normalized replay mutation to the declared model.
81
+ * @param {{actor: ?, context: Record<string, ?>, existingSync: ?, mutation: ?}} args - Replay apply arguments.
82
+ * @returns {Promise<Record<string, ?>>} Apply result with the record, created/deleted flags, optional serializedData, and afterApply extras.
83
+ */
84
+ async apply({ context, mutation }) {
85
+ if (mutation.syncType === "delete")
86
+ return await this.applyDelete({ context, mutation });
87
+ const mappedAttributes = this.mappedAttributes(mutation.data);
88
+ const record = this.findRecord
89
+ ? await this.findRecord({ context, data: mutation.data, mutation })
90
+ : await this.modelClass.findBy({ id: mutation.resourceId });
91
+ /** @type {Record<string, ?>} */
92
+ const result = { created: false, deleted: false, record: null };
93
+ if (record) {
94
+ record.assign(mappedAttributes);
95
+ await record.save();
96
+ result.record = record;
97
+ }
98
+ else {
99
+ result.record = await this.modelClass.create({ id: mutation.resourceId, ...mappedAttributes });
100
+ result.created = true;
101
+ }
102
+ if (this.serialize)
103
+ result.serializedData = await this.serialize({ context, mutation, record: result.record });
104
+ if (this.afterApply) {
105
+ const afterApplyResult = await this.afterApply({ context, mappedAttributes, mutation, record: result.record });
106
+ if (afterApplyResult && typeof afterApplyResult === "object")
107
+ Object.assign(result, afterApplyResult);
108
+ }
109
+ return result;
110
+ }
111
+ /**
112
+ * Applies a delete mutation to the declared model.
113
+ * @param {{context: Record<string, ?>, mutation: ?}} args - Delete arguments.
114
+ * @returns {Promise<Record<string, ?>>} Apply result with the deleted flag.
115
+ */
116
+ async applyDelete({ context, mutation }) {
117
+ const resolveRecord = this.findRecordForDelete || this.findRecord;
118
+ const record = resolveRecord
119
+ ? await resolveRecord({ context, data: mutation.data, mutation })
120
+ : await this.modelClass.findBy({ id: mutation.resourceId });
121
+ if (!record)
122
+ return { created: false, deleted: false, record: null };
123
+ await record.destroy();
124
+ return { created: false, deleted: true, record };
125
+ }
126
+ /**
127
+ * Maps present data keys through the declared field types.
128
+ * @param {Record<string, ?>} data - Normalized mutation data.
129
+ * @returns {Record<string, ?>} Coerced attributes keyed by column name.
130
+ */
131
+ mappedAttributes(data) {
132
+ /** @type {Record<string, ?>} */
133
+ const mappedAttributes = {};
134
+ /** @type {string[]} */
135
+ const unknownKeys = [];
136
+ for (const [dataKey, value] of Object.entries(data)) {
137
+ const fieldSpec = this.fields[dataKey];
138
+ if (!fieldSpec) {
139
+ unknownKeys.push(dataKey);
140
+ continue;
141
+ }
142
+ if (typeof fieldSpec === "function") {
143
+ mappedAttributes[dataKey] = fieldSpec(value, dataKey);
144
+ continue;
145
+ }
146
+ const fieldType = typeof fieldSpec === "string" ? fieldSpec : fieldSpec.type;
147
+ if (fieldType === "ignored")
148
+ continue;
149
+ const column = typeof fieldSpec === "string" ? dataKey : fieldSpec.column || dataKey;
150
+ mappedAttributes[column] = FIELD_TYPES[fieldType](value, dataKey);
151
+ }
152
+ if (unknownKeys.length > 0 && this.restArgs === "error") {
153
+ throw new Error(`Unknown sync data keys: ${unknownKeys.join(", ")}`);
154
+ }
155
+ return mappedAttributes;
156
+ }
157
+ }
158
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic3luYy1yZXBsYXktdXBzZXJ0LWFwcGxpZXIuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi8uLi9zcmMvc3luYy9zeW5jLXJlcGxheS11cHNlcnQtYXBwbGllci5qcyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxZQUFZO0FBRVosT0FBTyxFQUFDLGFBQWEsRUFBRSxlQUFlLEVBQUUsY0FBYyxFQUFDLE1BQU0sU0FBUyxDQUFBO0FBRXRFLE9BQU8sYUFBYSxNQUFNLHNCQUFzQixDQUFBO0FBRWhEOzs7O0dBSUc7QUFDSCxNQUFNLFdBQVcsR0FBRztJQUNsQixhQUFhLEVBQUUsQ0FBQyxLQUFLLEVBQUUsS0FBSyxFQUFFLEVBQUUsQ0FBQyxhQUFhLENBQUMsd0JBQXdCLENBQUMsS0FBSyxFQUFFLEtBQUssQ0FBQztJQUNyRixVQUFVLEVBQUUsQ0FBQyxLQUFLLEVBQUUsS0FBSyxFQUFFLEVBQUUsQ0FBQyxnQkFBZ0IsQ0FBQyxLQUFLLEVBQUUsS0FBSyxDQUFDO0lBQzVELFdBQVcsRUFBRSxDQUFDLEtBQUssRUFBRSxLQUFLLEVBQUUsRUFBRSxDQUFDLGFBQWEsQ0FBQyxLQUFLLEVBQUUsS0FBSyxDQUFDO0lBQzFELGFBQWEsRUFBRSxDQUFDLEtBQUssRUFBRSxLQUFLLEVBQUUsRUFBRSxDQUFDLGVBQWUsQ0FBQyxLQUFLLEVBQUUsS0FBSyxDQUFDO0lBQzlELEdBQUcsRUFBRSxDQUFDLEtBQUssRUFBRSxFQUFFLENBQUMsS0FBSztJQUNyQixZQUFZLEVBQUUsQ0FBQyxLQUFLLEVBQUUsS0FBSyxFQUFFLEVBQUUsQ0FBQyxjQUFjLENBQUMsS0FBSyxFQUFFLEtBQUssQ0FBQztDQUM3RCxDQUFBO0FBRUQ7Ozs7O0dBS0c7QUFDSCxTQUFTLGdCQUFnQixDQUFDLEtBQUssRUFBRSxLQUFLO0lBQ3BDLElBQUksS0FBSyxLQUFLLElBQUksSUFBSSxLQUFLLEtBQUssU0FBUztRQUFFLE9BQU8sSUFBSSxDQUFBO0lBRXRELElBQUksS0FBSyxZQUFZLElBQUksSUFBSSxDQUFDLE1BQU0sQ0FBQyxLQUFLLENBQUMsS0FBSyxDQUFDLE9BQU8sRUFBRSxDQUFDO1FBQUUsT0FBTyxLQUFLLENBQUE7SUFFekUsSUFBSSxPQUFPLEtBQUssS0FBSyxRQUFRLElBQUksT0FBTyxLQUFLLEtBQUssUUFBUSxFQUFFLENBQUM7UUFDM0QsTUFBTSxTQUFTLEdBQUcsSUFBSSxJQUFJLENBQUMsS0FBSyxDQUFDLENBQUE7UUFFakMsSUFBSSxDQUFDLE1BQU0sQ0FBQyxLQUFLLENBQUMsU0FBUyxDQUFDLE9BQU8sRUFBRSxDQUFDO1lBQUUsT0FBTyxTQUFTLENBQUE7SUFDMUQsQ0FBQztJQUVELE1BQU0sSUFBSSxLQUFLLENBQUMsWUFBWSxLQUFLLDZCQUE2QixDQUFDLENBQUE7QUFDakUsQ0FBQztBQUVEOzs7Ozs7O0dBT0c7QUFDSCxNQUFNLENBQUMsT0FBTyxPQUFPLHVCQUF1QjtJQUMxQzs7Ozs7Ozs7OztPQVVHO0lBQ0gsWUFBWSxFQUFDLFVBQVUsRUFBRSxNQUFNLEVBQUUsVUFBVSxFQUFFLG1CQUFtQixFQUFFLFVBQVUsRUFBRSxRQUFRLEdBQUcsT0FBTyxFQUFFLFNBQVMsRUFBQztRQUMxRyxJQUFJLENBQUMsVUFBVTtZQUFFLE1BQU0sSUFBSSxLQUFLLENBQUMsK0NBQStDLENBQUMsQ0FBQTtRQUNqRixJQUFJLENBQUMsTUFBTSxJQUFJLE9BQU8sTUFBTSxLQUFLLFFBQVE7WUFBRSxNQUFNLElBQUksS0FBSyxDQUFDLCtDQUErQyxDQUFDLENBQUE7UUFDM0csSUFBSSxRQUFRLEtBQUssT0FBTyxJQUFJLFFBQVEsS0FBSyxRQUFRLEVBQUUsQ0FBQztZQUNsRCxNQUFNLElBQUksS0FBSyxDQUFDLHNFQUFzRSxNQUFNLENBQUMsUUFBUSxDQUFDLEVBQUUsQ0FBQyxDQUFBO1FBQzNHLENBQUM7UUFFRCxLQUFLLE1BQU0sQ0FBQyxTQUFTLEVBQUUsU0FBUyxDQUFDLElBQUksTUFBTSxDQUFDLE9BQU8sQ0FBQyxNQUFNLENBQUMsRUFBRSxDQUFDO1lBQzVELElBQUksT0FBTyxTQUFTLEtBQUssVUFBVTtnQkFBRSxTQUFRO1lBRTdDLE1BQU0sU0FBUyxHQUFHLE9BQU8sU0FBUyxLQUFLLFFBQVEsQ0FBQyxDQUFDLENBQUMsU0FBUyxDQUFDLENBQUMsQ0FBQyxTQUFTLENBQUMsSUFBSSxDQUFBO1lBRTVFLElBQUksU0FBUyxLQUFLLFNBQVMsSUFBSSxDQUFDLENBQUMsU0FBUyxJQUFJLFdBQVcsQ0FBQyxFQUFFLENBQUM7Z0JBQzNELE1BQU0sSUFBSSxLQUFLLENBQUMsNEJBQTRCLFNBQVMsU0FBUyxTQUFTLEVBQUUsQ0FBQyxDQUFBO1lBQzVFLENBQUM7UUFDSCxDQUFDO1FBRUQsSUFBSSxDQUFDLFVBQVUsR0FBRyxVQUFVLENBQUE7UUFDNUIsSUFBSSxDQUFDLE1BQU0sR0FBRyxNQUFNLENBQUE7UUFDcEIsSUFBSSxDQUFDLFVBQVUsR0FBRyxVQUFVLENBQUE7UUFDNUIsSUFBSSxDQUFDLG1CQUFtQixHQUFHLG1CQUFtQixDQUFBO1FBQzlDLElBQUksQ0FBQyxVQUFVLEdBQUcsVUFBVSxDQUFBO1FBQzVCLElBQUksQ0FBQyxRQUFRLEdBQUcsUUFBUSxDQUFBO1FBQ3hCLElBQUksQ0FBQyxTQUFTLEdBQUcsU0FBUyxDQUFBO0lBQzVCLENBQUM7SUFFRDs7OztPQUlHO0lBQ0gsS0FBSyxDQUFDLEtBQUssQ0FBQyxFQUFDLE9BQU8sRUFBRSxRQUFRLEVBQUM7UUFDN0IsSUFBSSxRQUFRLENBQUMsUUFBUSxLQUFLLFFBQVE7WUFBRSxPQUFPLE1BQU0sSUFBSSxDQUFDLFdBQVcsQ0FBQyxFQUFDLE9BQU8sRUFBRSxRQUFRLEVBQUMsQ0FBQyxDQUFBO1FBRXRGLE1BQU0sZ0JBQWdCLEdBQUcsSUFBSSxDQUFDLGdCQUFnQixDQUFDLFFBQVEsQ0FBQyxJQUFJLENBQUMsQ0FBQTtRQUM3RCxNQUFNLE1BQU0sR0FBRyxJQUFJLENBQUMsVUFBVTtZQUM1QixDQUFDLENBQUMsTUFBTSxJQUFJLENBQUMsVUFBVSxDQUFDLEVBQUMsT0FBTyxFQUFFLElBQUksRUFBRSxRQUFRLENBQUMsSUFBSSxFQUFFLFFBQVEsRUFBQyxDQUFDO1lBQ2pFLENBQUMsQ0FBQyxNQUFNLElBQUksQ0FBQyxVQUFVLENBQUMsTUFBTSxDQUFDLEVBQUMsRUFBRSxFQUFFLFFBQVEsQ0FBQyxVQUFVLEVBQUMsQ0FBQyxDQUFBO1FBQzNELGdDQUFnQztRQUNoQyxNQUFNLE1BQU0sR0FBRyxFQUFDLE9BQU8sRUFBRSxLQUFLLEVBQUUsT0FBTyxFQUFFLEtBQUssRUFBRSxNQUFNLEVBQUUsSUFBSSxFQUFDLENBQUE7UUFFN0QsSUFBSSxNQUFNLEVBQUUsQ0FBQztZQUNYLE1BQU0sQ0FBQyxNQUFNLENBQUMsZ0JBQWdCLENBQUMsQ0FBQTtZQUMvQixNQUFNLE1BQU0sQ0FBQyxJQUFJLEVBQUUsQ0FBQTtZQUNuQixNQUFNLENBQUMsTUFBTSxHQUFHLE1BQU0sQ0FBQTtRQUN4QixDQUFDO2FBQU0sQ0FBQztZQUNOLE1BQU0sQ0FBQyxNQUFNLEdBQUcsTUFBTSxJQUFJLENBQUMsVUFBVSxDQUFDLE1BQU0sQ0FBQyxFQUFDLEVBQUUsRUFBRSxRQUFRLENBQUMsVUFBVSxFQUFFLEdBQUcsZ0JBQWdCLEVBQUMsQ0FBQyxDQUFBO1lBQzVGLE1BQU0sQ0FBQyxPQUFPLEdBQUcsSUFBSSxDQUFBO1FBQ3ZCLENBQUM7UUFFRCxJQUFJLElBQUksQ0FBQyxTQUFTO1lBQUUsTUFBTSxDQUFDLGNBQWMsR0FBRyxNQUFNLElBQUksQ0FBQyxTQUFTLENBQUMsRUFBQyxPQUFPLEVBQUUsUUFBUSxFQUFFLE1BQU0sRUFBRSxNQUFNLENBQUMsTUFBTSxFQUFDLENBQUMsQ0FBQTtRQUU1RyxJQUFJLElBQUksQ0FBQyxVQUFVLEVBQUUsQ0FBQztZQUNwQixNQUFNLGdCQUFnQixHQUFHLE1BQU0sSUFBSSxDQUFDLFVBQVUsQ0FBQyxFQUFDLE9BQU8sRUFBRSxnQkFBZ0IsRUFBRSxRQUFRLEVBQUUsTUFBTSxFQUFFLE1BQU0sQ0FBQyxNQUFNLEVBQUMsQ0FBQyxDQUFBO1lBRTVHLElBQUksZ0JBQWdCLElBQUksT0FBTyxnQkFBZ0IsS0FBSyxRQUFRO2dCQUFFLE1BQU0sQ0FBQyxNQUFNLENBQUMsTUFBTSxFQUFFLGdCQUFnQixDQUFDLENBQUE7UUFDdkcsQ0FBQztRQUVELE9BQU8sTUFBTSxDQUFBO0lBQ2YsQ0FBQztJQUVEOzs7O09BSUc7SUFDSCxLQUFLLENBQUMsV0FBVyxDQUFDLEVBQUMsT0FBTyxFQUFFLFFBQVEsRUFBQztRQUNuQyxNQUFNLGFBQWEsR0FBRyxJQUFJLENBQUMsbUJBQW1CLElBQUksSUFBSSxDQUFDLFVBQVUsQ0FBQTtRQUNqRSxNQUFNLE1BQU0sR0FBRyxhQUFhO1lBQzFCLENBQUMsQ0FBQyxNQUFNLGFBQWEsQ0FBQyxFQUFDLE9BQU8sRUFBRSxJQUFJLEVBQUUsUUFBUSxDQUFDLElBQUksRUFBRSxRQUFRLEVBQUMsQ0FBQztZQUMvRCxDQUFDLENBQUMsTUFBTSxJQUFJLENBQUMsVUFBVSxDQUFDLE1BQU0sQ0FBQyxFQUFDLEVBQUUsRUFBRSxRQUFRLENBQUMsVUFBVSxFQUFDLENBQUMsQ0FBQTtRQUUzRCxJQUFJLENBQUMsTUFBTTtZQUFFLE9BQU8sRUFBQyxPQUFPLEVBQUUsS0FBSyxFQUFFLE9BQU8sRUFBRSxLQUFLLEVBQUUsTUFBTSxFQUFFLElBQUksRUFBQyxDQUFBO1FBRWxFLE1BQU0sTUFBTSxDQUFDLE9BQU8sRUFBRSxDQUFBO1FBRXRCLE9BQU8sRUFBQyxPQUFPLEVBQUUsS0FBSyxFQUFFLE9BQU8sRUFBRSxJQUFJLEVBQUUsTUFBTSxFQUFDLENBQUE7SUFDaEQsQ0FBQztJQUVEOzs7O09BSUc7SUFDSCxnQkFBZ0IsQ0FBQyxJQUFJO1FBQ25CLGdDQUFnQztRQUNoQyxNQUFNLGdCQUFnQixHQUFHLEVBQUUsQ0FBQTtRQUMzQix1QkFBdUI7UUFDdkIsTUFBTSxXQUFXLEdBQUcsRUFBRSxDQUFBO1FBRXRCLEtBQUssTUFBTSxDQUFDLE9BQU8sRUFBRSxLQUFLLENBQUMsSUFBSSxNQUFNLENBQUMsT0FBTyxDQUFDLElBQUksQ0FBQyxFQUFFLENBQUM7WUFDcEQsTUFBTSxTQUFTLEdBQUcsSUFBSSxDQUFDLE1BQU0sQ0FBQyxPQUFPLENBQUMsQ0FBQTtZQUV0QyxJQUFJLENBQUMsU0FBUyxFQUFFLENBQUM7Z0JBQ2YsV0FBVyxDQUFDLElBQUksQ0FBQyxPQUFPLENBQUMsQ0FBQTtnQkFDekIsU0FBUTtZQUNWLENBQUM7WUFFRCxJQUFJLE9BQU8sU0FBUyxLQUFLLFVBQVUsRUFBRSxDQUFDO2dCQUNwQyxnQkFBZ0IsQ0FBQyxPQUFPLENBQUMsR0FBRyxTQUFTLENBQUMsS0FBSyxFQUFFLE9BQU8sQ0FBQyxDQUFBO2dCQUNyRCxTQUFRO1lBQ1YsQ0FBQztZQUVELE1BQU0sU0FBUyxHQUFHLE9BQU8sU0FBUyxLQUFLLFFBQVEsQ0FBQyxDQUFDLENBQUMsU0FBUyxDQUFDLENBQUMsQ0FBQyxTQUFTLENBQUMsSUFBSSxDQUFBO1lBRTVFLElBQUksU0FBUyxLQUFLLFNBQVM7Z0JBQUUsU0FBUTtZQUVyQyxNQUFNLE1BQU0sR0FBRyxPQUFPLFNBQVMsS0FBSyxRQUFRLENBQUMsQ0FBQyxDQUFDLE9BQU8sQ0FBQyxDQUFDLENBQUMsU0FBUyxDQUFDLE1BQU0sSUFBSSxPQUFPLENBQUE7WUFFcEYsZ0JBQWdCLENBQUMsTUFBTSxDQUFDLEdBQUcsV0FBVyxDQUFDLFNBQVMsQ0FBQyxDQUFDLEtBQUssRUFBRSxPQUFPLENBQUMsQ0FBQTtRQUNuRSxDQUFDO1FBRUQsSUFBSSxXQUFXLENBQUMsTUFBTSxHQUFHLENBQUMsSUFBSSxJQUFJLENBQUMsUUFBUSxLQUFLLE9BQU8sRUFBRSxDQUFDO1lBQ3hELE1BQU0sSUFBSSxLQUFLLENBQUMsMkJBQTJCLFdBQVcsQ0FBQyxJQUFJLENBQUMsSUFBSSxDQUFDLEVBQUUsQ0FBQyxDQUFBO1FBQ3RFLENBQUM7UUFFRCxPQUFPLGdCQUFnQixDQUFBO0lBQ3pCLENBQUM7Q0FDRiIsInNvdXJjZXNDb250ZW50IjpbIi8vIEB0cy1jaGVja1xuXG5pbXBvcnQge29wdGlvbmFsRmxvYXQsIG9wdGlvbmFsSW50ZWdlciwgb3B0aW9uYWxTdHJpbmd9IGZyb20gXCJ0eXBhbmljXCJcblxuaW1wb3J0IFN5bmNBcGlDbGllbnQgZnJvbSBcIi4vc3luYy1hcGktY2xpZW50LmpzXCJcblxuLyoqXG4gKiBDb2VyY2VycyBmb3IgdGhlIGRlY2xhcmF0aXZlIHN5bmMgZmllbGQgdHlwZXMuIEFsbCBhcmUgcHJlc2VudC1rZXkgYmFzZWQ6XG4gKiBhIGZpZWxkIGlzIG9ubHkgYXBwbGllZCB3aGVuIGl0cyBrZXkgZXhpc3RzIGluIHRoZSBtdXRhdGlvbiBkYXRhLlxuICogQHR5cGUge1JlY29yZDxzdHJpbmcsICh2YWx1ZTogPywgbGFiZWw6IHN0cmluZykgPT4gPz59XG4gKi9cbmNvbnN0IEZJRUxEX1RZUEVTID0ge1xuICBib29sZWFuT3JOdWxsOiAodmFsdWUsIGxhYmVsKSA9PiBTeW5jQXBpQ2xpZW50Lm9wdGlvbmFsQm9vbGVhblN5bmNWYWx1ZSh2YWx1ZSwgbGFiZWwpLFxuICBkYXRlT3JOdWxsOiAodmFsdWUsIGxhYmVsKSA9PiBvcHRpb25hbFN5bmNEYXRlKHZhbHVlLCBsYWJlbCksXG4gIGZsb2F0T3JOdWxsOiAodmFsdWUsIGxhYmVsKSA9PiBvcHRpb25hbEZsb2F0KHZhbHVlLCBsYWJlbCksXG4gIGludGVnZXJPck51bGw6ICh2YWx1ZSwgbGFiZWwpID0+IG9wdGlvbmFsSW50ZWdlcih2YWx1ZSwgbGFiZWwpLFxuICByYXc6ICh2YWx1ZSkgPT4gdmFsdWUsXG4gIHN0cmluZ09yTnVsbDogKHZhbHVlLCBsYWJlbCkgPT4gb3B0aW9uYWxTdHJpbmcodmFsdWUsIGxhYmVsKVxufVxuXG4vKipcbiAqIFBhcnNlcyBhbiBvcHRpb25hbCBkYXRlIHZhbHVlLCBmYWlsaW5nIGxvdWRseSBvbiBpbnZhbGlkIGlucHV0LlxuICogQHBhcmFtIHs/fSB2YWx1ZSAtIERhdGUsIHBhcnNlYWJsZSBzdHJpbmcsIG9yIGVwb2NoIG51bWJlci5cbiAqIEBwYXJhbSB7c3RyaW5nfSBsYWJlbCAtIEZpZWxkIGxhYmVsIGZvciBlcnJvciBtZXNzYWdlcy5cbiAqIEByZXR1cm5zIHtEYXRlIHwgbnVsbH0gUGFyc2VkIGRhdGUgb3IgbnVsbCB3aGVuIGFic2VudC5cbiAqL1xuZnVuY3Rpb24gb3B0aW9uYWxTeW5jRGF0ZSh2YWx1ZSwgbGFiZWwpIHtcbiAgaWYgKHZhbHVlID09PSBudWxsIHx8IHZhbHVlID09PSB1bmRlZmluZWQpIHJldHVybiBudWxsXG5cbiAgaWYgKHZhbHVlIGluc3RhbmNlb2YgRGF0ZSAmJiAhTnVtYmVyLmlzTmFOKHZhbHVlLmdldFRpbWUoKSkpIHJldHVybiB2YWx1ZVxuXG4gIGlmICh0eXBlb2YgdmFsdWUgPT09IFwic3RyaW5nXCIgfHwgdHlwZW9mIHZhbHVlID09PSBcIm51bWJlclwiKSB7XG4gICAgY29uc3QgZGF0ZVZhbHVlID0gbmV3IERhdGUodmFsdWUpXG5cbiAgICBpZiAoIU51bWJlci5pc05hTihkYXRlVmFsdWUuZ2V0VGltZSgpKSkgcmV0dXJuIGRhdGVWYWx1ZVxuICB9XG5cbiAgdGhyb3cgbmV3IEVycm9yKGBFeHBlY3RlZCAke2xhYmVsfSB0byBiZSBhIHZhbGlkIGRhdGUgb3IgbnVsbGApXG59XG5cbi8qKlxuICogRGVjbGFyYXRpdmUgZmllbGQtbWFwIHVwc2VydCBhcHBsaWVyIGZvciBzeW5jIHJlcGxheSBtdXRhdGlvbnMuXG4gKlxuICogT3ducyB0aGUgZ2VuZXJpYyBtZWNoYW5pY3MgZXZlcnkgcGVyLXJlc291cmNlIHJlcGxheSBoYW5kbGVyIHJlcGVhdHM6XG4gKiBwcmVzZW50LWtleSBmaWx0ZXJpbmcsIHBlci1maWVsZCB0eXBlIGNvZXJjaW9uLCB1bmtub3duLWtleSByZWplY3Rpb24sIHRoZVxuICogZmluZC1vci1jcmVhdGUgdXBzZXJ0LCB0aGUgZGVsZXRlIGJyYW5jaCwgb3B0aW9uYWwgc25hcHNob3Qgc2VyaWFsaXphdGlvbixcbiAqIGFuZCB0aGUgZG9tYWluIGFmdGVyLWFwcGx5IHRhaWwuIEFwcHMgZGVjbGFyZSBvbmx5IHRoZSBmaWVsZCBtYXAgYW5kIGhvb2tzLlxuICovXG5leHBvcnQgZGVmYXVsdCBjbGFzcyBTeW5jUmVwbGF5VXBzZXJ0QXBwbGllciB7XG4gIC8qKlxuICAgKiBDcmVhdGVzIGEgZGVjbGFyYXRpdmUgdXBzZXJ0IGFwcGxpZXIuXG4gICAqIEBwYXJhbSB7b2JqZWN0fSBhcmdzIC0gQXBwbGllciBjb25maWd1cmF0aW9uLlxuICAgKiBAcGFyYW0gez99IGFyZ3MubW9kZWxDbGFzcyAtIE1vZGVsIGNsYXNzIHJlY2VpdmluZyB0aGUgbXV0YXRpb25zLlxuICAgKiBAcGFyYW0ge1JlY29yZDxzdHJpbmcsIHN0cmluZyB8IHt0eXBlOiBzdHJpbmcsIGNvbHVtbj86IHN0cmluZ30gfCAoKHZhbHVlOiA/LCBsYWJlbDogc3RyaW5nKSA9PiA/KT59IGFyZ3MuZmllbGRzIC0gRGF0YS1rZXkg4oaSIGZpZWxkLXR5cGUgbWFwOiBhIG5hbWVkIHR5cGUsIGEge3R5cGUsIGNvbHVtbn0gcmVuYW1lIHNwZWMsIFwiaWdub3JlZFwiIGZvciBhY2NlcHRlZC1idXQtZHJvcHBlZCBrZXlzLCBvciBhIGN1c3RvbSBjb2VyY2VyIGZ1bmN0aW9uLlxuICAgKiBAcGFyYW0geyhhcmdzOiB7ZGF0YTogUmVjb3JkPHN0cmluZywgPz4sIG11dGF0aW9uOiA/LCBjb250ZXh0OiBSZWNvcmQ8c3RyaW5nLCA/Pn0pID0+IFByb21pc2U8Pz59IFthcmdzLmZpbmRSZWNvcmRdIC0gQ3VzdG9tIHJlY29yZCByZXNvbHZlci4gRGVmYXVsdHMgdG8gZmluZEJ5KHtpZDogbXV0YXRpb24ucmVzb3VyY2VJZH0pLlxuICAgKiBAcGFyYW0geyhhcmdzOiB7bXV0YXRpb246ID8sIGNvbnRleHQ6IFJlY29yZDxzdHJpbmcsID8+fSkgPT4gUHJvbWlzZTw/Pn0gW2FyZ3MuZmluZFJlY29yZEZvckRlbGV0ZV0gLSBDdXN0b20gZGVsZXRlIHJlc29sdmVyLiBEZWZhdWx0cyB0byBmaW5kUmVjb3JkLlxuICAgKiBAcGFyYW0ge1wiZXJyb3JcIiB8IFwiaWdub3JlXCJ9IFthcmdzLnJlc3RBcmdzXSAtIFVua25vd24gZGF0YS1rZXkgaGFuZGxpbmcuIERlZmF1bHRzIHRvIFwiZXJyb3JcIi5cbiAgICogQHBhcmFtIHsoYXJnczoge21hcHBlZEF0dHJpYnV0ZXM6IFJlY29yZDxzdHJpbmcsID8+LCBtdXRhdGlvbjogPywgY29udGV4dDogUmVjb3JkPHN0cmluZywgPz4sIHJlY29yZDogP30pID0+IFByb21pc2U8UmVjb3JkPHN0cmluZywgPz4gfCB2b2lkPn0gW2FyZ3MuYWZ0ZXJBcHBseV0gLSBEb21haW4gdGFpbDsgaXRzIHJldHVybmVkIG9iamVjdCBtZXJnZXMgaW50byB0aGUgYXBwbHkgcmVzdWx0LlxuICAgKiBAcGFyYW0geyhhcmdzOiB7bXV0YXRpb246ID8sIGNvbnRleHQ6IFJlY29yZDxzdHJpbmcsID8+LCByZWNvcmQ6ID99KSA9PiBQcm9taXNlPFJlY29yZDxzdHJpbmcsID8+PiB8IFJlY29yZDxzdHJpbmcsID8+fSBbYXJncy5zZXJpYWxpemVdIC0gU25hcHNob3Qgc2VyaWFsaXplcjsgcmVzdWx0IGxhbmRzIG9uIGFwcGx5UmVzdWx0LnNlcmlhbGl6ZWREYXRhLlxuICAgKi9cbiAgY29uc3RydWN0b3Ioe2FmdGVyQXBwbHksIGZpZWxkcywgZmluZFJlY29yZCwgZmluZFJlY29yZEZvckRlbGV0ZSwgbW9kZWxDbGFzcywgcmVzdEFyZ3MgPSBcImVycm9yXCIsIHNlcmlhbGl6ZX0pIHtcbiAgICBpZiAoIW1vZGVsQ2xhc3MpIHRocm93IG5ldyBFcnJvcihcIlN5bmNSZXBsYXlVcHNlcnRBcHBsaWVyIHJlcXVpcmVzIGEgbW9kZWxDbGFzc1wiKVxuICAgIGlmICghZmllbGRzIHx8IHR5cGVvZiBmaWVsZHMgIT09IFwib2JqZWN0XCIpIHRocm93IG5ldyBFcnJvcihcIlN5bmNSZXBsYXlVcHNlcnRBcHBsaWVyIHJlcXVpcmVzIGEgZmllbGRzIG1hcFwiKVxuICAgIGlmIChyZXN0QXJncyAhPT0gXCJlcnJvclwiICYmIHJlc3RBcmdzICE9PSBcImlnbm9yZVwiKSB7XG4gICAgICB0aHJvdyBuZXcgRXJyb3IoYFN5bmNSZXBsYXlVcHNlcnRBcHBsaWVyIHJlc3RBcmdzIG11c3QgYmUgXCJlcnJvclwiIG9yIFwiaWdub3JlXCIsIGdvdDogJHtTdHJpbmcocmVzdEFyZ3MpfWApXG4gICAgfVxuXG4gICAgZm9yIChjb25zdCBbZmllbGROYW1lLCBmaWVsZFNwZWNdIG9mIE9iamVjdC5lbnRyaWVzKGZpZWxkcykpIHtcbiAgICAgIGlmICh0eXBlb2YgZmllbGRTcGVjID09PSBcImZ1bmN0aW9uXCIpIGNvbnRpbnVlXG5cbiAgICAgIGNvbnN0IGZpZWxkVHlwZSA9IHR5cGVvZiBmaWVsZFNwZWMgPT09IFwic3RyaW5nXCIgPyBmaWVsZFNwZWMgOiBmaWVsZFNwZWMudHlwZVxuXG4gICAgICBpZiAoZmllbGRUeXBlICE9PSBcImlnbm9yZWRcIiAmJiAhKGZpZWxkVHlwZSBpbiBGSUVMRF9UWVBFUykpIHtcbiAgICAgICAgdGhyb3cgbmV3IEVycm9yKGBVbmtub3duIHN5bmMgZmllbGQgdHlwZTogJHtmaWVsZFR5cGV9IGZvcjogJHtmaWVsZE5hbWV9YClcbiAgICAgIH1cbiAgICB9XG5cbiAgICB0aGlzLmFmdGVyQXBwbHkgPSBhZnRlckFwcGx5XG4gICAgdGhpcy5maWVsZHMgPSBmaWVsZHNcbiAgICB0aGlzLmZpbmRSZWNvcmQgPSBmaW5kUmVjb3JkXG4gICAgdGhpcy5maW5kUmVjb3JkRm9yRGVsZXRlID0gZmluZFJlY29yZEZvckRlbGV0ZVxuICAgIHRoaXMubW9kZWxDbGFzcyA9IG1vZGVsQ2xhc3NcbiAgICB0aGlzLnJlc3RBcmdzID0gcmVzdEFyZ3NcbiAgICB0aGlzLnNlcmlhbGl6ZSA9IHNlcmlhbGl6ZVxuICB9XG5cbiAgLyoqXG4gICAqIEFwcGxpZXMgb25lIG5vcm1hbGl6ZWQgcmVwbGF5IG11dGF0aW9uIHRvIHRoZSBkZWNsYXJlZCBtb2RlbC5cbiAgICogQHBhcmFtIHt7YWN0b3I6ID8sIGNvbnRleHQ6IFJlY29yZDxzdHJpbmcsID8+LCBleGlzdGluZ1N5bmM6ID8sIG11dGF0aW9uOiA/fX0gYXJncyAtIFJlcGxheSBhcHBseSBhcmd1bWVudHMuXG4gICAqIEByZXR1cm5zIHtQcm9taXNlPFJlY29yZDxzdHJpbmcsID8+Pn0gQXBwbHkgcmVzdWx0IHdpdGggdGhlIHJlY29yZCwgY3JlYXRlZC9kZWxldGVkIGZsYWdzLCBvcHRpb25hbCBzZXJpYWxpemVkRGF0YSwgYW5kIGFmdGVyQXBwbHkgZXh0cmFzLlxuICAgKi9cbiAgYXN5bmMgYXBwbHkoe2NvbnRleHQsIG11dGF0aW9ufSkge1xuICAgIGlmIChtdXRhdGlvbi5zeW5jVHlwZSA9PT0gXCJkZWxldGVcIikgcmV0dXJuIGF3YWl0IHRoaXMuYXBwbHlEZWxldGUoe2NvbnRleHQsIG11dGF0aW9ufSlcblxuICAgIGNvbnN0IG1hcHBlZEF0dHJpYnV0ZXMgPSB0aGlzLm1hcHBlZEF0dHJpYnV0ZXMobXV0YXRpb24uZGF0YSlcbiAgICBjb25zdCByZWNvcmQgPSB0aGlzLmZpbmRSZWNvcmRcbiAgICAgID8gYXdhaXQgdGhpcy5maW5kUmVjb3JkKHtjb250ZXh0LCBkYXRhOiBtdXRhdGlvbi5kYXRhLCBtdXRhdGlvbn0pXG4gICAgICA6IGF3YWl0IHRoaXMubW9kZWxDbGFzcy5maW5kQnkoe2lkOiBtdXRhdGlvbi5yZXNvdXJjZUlkfSlcbiAgICAvKiogQHR5cGUge1JlY29yZDxzdHJpbmcsID8+fSAqL1xuICAgIGNvbnN0IHJlc3VsdCA9IHtjcmVhdGVkOiBmYWxzZSwgZGVsZXRlZDogZmFsc2UsIHJlY29yZDogbnVsbH1cblxuICAgIGlmIChyZWNvcmQpIHtcbiAgICAgIHJlY29yZC5hc3NpZ24obWFwcGVkQXR0cmlidXRlcylcbiAgICAgIGF3YWl0IHJlY29yZC5zYXZlKClcbiAgICAgIHJlc3VsdC5yZWNvcmQgPSByZWNvcmRcbiAgICB9IGVsc2Uge1xuICAgICAgcmVzdWx0LnJlY29yZCA9IGF3YWl0IHRoaXMubW9kZWxDbGFzcy5jcmVhdGUoe2lkOiBtdXRhdGlvbi5yZXNvdXJjZUlkLCAuLi5tYXBwZWRBdHRyaWJ1dGVzfSlcbiAgICAgIHJlc3VsdC5jcmVhdGVkID0gdHJ1ZVxuICAgIH1cblxuICAgIGlmICh0aGlzLnNlcmlhbGl6ZSkgcmVzdWx0LnNlcmlhbGl6ZWREYXRhID0gYXdhaXQgdGhpcy5zZXJpYWxpemUoe2NvbnRleHQsIG11dGF0aW9uLCByZWNvcmQ6IHJlc3VsdC5yZWNvcmR9KVxuXG4gICAgaWYgKHRoaXMuYWZ0ZXJBcHBseSkge1xuICAgICAgY29uc3QgYWZ0ZXJBcHBseVJlc3VsdCA9IGF3YWl0IHRoaXMuYWZ0ZXJBcHBseSh7Y29udGV4dCwgbWFwcGVkQXR0cmlidXRlcywgbXV0YXRpb24sIHJlY29yZDogcmVzdWx0LnJlY29yZH0pXG5cbiAgICAgIGlmIChhZnRlckFwcGx5UmVzdWx0ICYmIHR5cGVvZiBhZnRlckFwcGx5UmVzdWx0ID09PSBcIm9iamVjdFwiKSBPYmplY3QuYXNzaWduKHJlc3VsdCwgYWZ0ZXJBcHBseVJlc3VsdClcbiAgICB9XG5cbiAgICByZXR1cm4gcmVzdWx0XG4gIH1cblxuICAvKipcbiAgICogQXBwbGllcyBhIGRlbGV0ZSBtdXRhdGlvbiB0byB0aGUgZGVjbGFyZWQgbW9kZWwuXG4gICAqIEBwYXJhbSB7e2NvbnRleHQ6IFJlY29yZDxzdHJpbmcsID8+LCBtdXRhdGlvbjogP319IGFyZ3MgLSBEZWxldGUgYXJndW1lbnRzLlxuICAgKiBAcmV0dXJucyB7UHJvbWlzZTxSZWNvcmQ8c3RyaW5nLCA/Pj59IEFwcGx5IHJlc3VsdCB3aXRoIHRoZSBkZWxldGVkIGZsYWcuXG4gICAqL1xuICBhc3luYyBhcHBseURlbGV0ZSh7Y29udGV4dCwgbXV0YXRpb259KSB7XG4gICAgY29uc3QgcmVzb2x2ZVJlY29yZCA9IHRoaXMuZmluZFJlY29yZEZvckRlbGV0ZSB8fCB0aGlzLmZpbmRSZWNvcmRcbiAgICBjb25zdCByZWNvcmQgPSByZXNvbHZlUmVjb3JkXG4gICAgICA/IGF3YWl0IHJlc29sdmVSZWNvcmQoe2NvbnRleHQsIGRhdGE6IG11dGF0aW9uLmRhdGEsIG11dGF0aW9ufSlcbiAgICAgIDogYXdhaXQgdGhpcy5tb2RlbENsYXNzLmZpbmRCeSh7aWQ6IG11dGF0aW9uLnJlc291cmNlSWR9KVxuXG4gICAgaWYgKCFyZWNvcmQpIHJldHVybiB7Y3JlYXRlZDogZmFsc2UsIGRlbGV0ZWQ6IGZhbHNlLCByZWNvcmQ6IG51bGx9XG5cbiAgICBhd2FpdCByZWNvcmQuZGVzdHJveSgpXG5cbiAgICByZXR1cm4ge2NyZWF0ZWQ6IGZhbHNlLCBkZWxldGVkOiB0cnVlLCByZWNvcmR9XG4gIH1cblxuICAvKipcbiAgICogTWFwcyBwcmVzZW50IGRhdGEga2V5cyB0aHJvdWdoIHRoZSBkZWNsYXJlZCBmaWVsZCB0eXBlcy5cbiAgICogQHBhcmFtIHtSZWNvcmQ8c3RyaW5nLCA/Pn0gZGF0YSAtIE5vcm1hbGl6ZWQgbXV0YXRpb24gZGF0YS5cbiAgICogQHJldHVybnMge1JlY29yZDxzdHJpbmcsID8+fSBDb2VyY2VkIGF0dHJpYnV0ZXMga2V5ZWQgYnkgY29sdW1uIG5hbWUuXG4gICAqL1xuICBtYXBwZWRBdHRyaWJ1dGVzKGRhdGEpIHtcbiAgICAvKiogQHR5cGUge1JlY29yZDxzdHJpbmcsID8+fSAqL1xuICAgIGNvbnN0IG1hcHBlZEF0dHJpYnV0ZXMgPSB7fVxuICAgIC8qKiBAdHlwZSB7c3RyaW5nW119ICovXG4gICAgY29uc3QgdW5rbm93bktleXMgPSBbXVxuXG4gICAgZm9yIChjb25zdCBbZGF0YUtleSwgdmFsdWVdIG9mIE9iamVjdC5lbnRyaWVzKGRhdGEpKSB7XG4gICAgICBjb25zdCBmaWVsZFNwZWMgPSB0aGlzLmZpZWxkc1tkYXRhS2V5XVxuXG4gICAgICBpZiAoIWZpZWxkU3BlYykge1xuICAgICAgICB1bmtub3duS2V5cy5wdXNoKGRhdGFLZXkpXG4gICAgICAgIGNvbnRpbnVlXG4gICAgICB9XG5cbiAgICAgIGlmICh0eXBlb2YgZmllbGRTcGVjID09PSBcImZ1bmN0aW9uXCIpIHtcbiAgICAgICAgbWFwcGVkQXR0cmlidXRlc1tkYXRhS2V5XSA9IGZpZWxkU3BlYyh2YWx1ZSwgZGF0YUtleSlcbiAgICAgICAgY29udGludWVcbiAgICAgIH1cblxuICAgICAgY29uc3QgZmllbGRUeXBlID0gdHlwZW9mIGZpZWxkU3BlYyA9PT0gXCJzdHJpbmdcIiA/IGZpZWxkU3BlYyA6IGZpZWxkU3BlYy50eXBlXG5cbiAgICAgIGlmIChmaWVsZFR5cGUgPT09IFwiaWdub3JlZFwiKSBjb250aW51ZVxuXG4gICAgICBjb25zdCBjb2x1bW4gPSB0eXBlb2YgZmllbGRTcGVjID09PSBcInN0cmluZ1wiID8gZGF0YUtleSA6IGZpZWxkU3BlYy5jb2x1bW4gfHwgZGF0YUtleVxuXG4gICAgICBtYXBwZWRBdHRyaWJ1dGVzW2NvbHVtbl0gPSBGSUVMRF9UWVBFU1tmaWVsZFR5cGVdKHZhbHVlLCBkYXRhS2V5KVxuICAgIH1cblxuICAgIGlmICh1bmtub3duS2V5cy5sZW5ndGggPiAwICYmIHRoaXMucmVzdEFyZ3MgPT09IFwiZXJyb3JcIikge1xuICAgICAgdGhyb3cgbmV3IEVycm9yKGBVbmtub3duIHN5bmMgZGF0YSBrZXlzOiAke3Vua25vd25LZXlzLmpvaW4oXCIsIFwiKX1gKVxuICAgIH1cblxuICAgIHJldHVybiBtYXBwZWRBdHRyaWJ1dGVzXG4gIH1cbn1cbiJdfQ==
@@ -1,5 +1,16 @@
1
1
  // @ts-check
2
2
 
3
+ import SyncReplayUpsertApplier from "./sync-replay-upsert-applier.js"
4
+
5
+ /**
6
+ * One declarative broadcast fanned out after a mutation applies.
7
+ * @typedef {object} SyncReplayBroadcast
8
+ * @property {string | ((args: Record<string, ?>) => string)} channel - Channel name or resolver.
9
+ * @property {(args: Record<string, ?>) => Record<string, ?>} broadcastParams - Channel routing params.
10
+ * @property {(args: Record<string, ?>) => ?} body - Broadcast body.
11
+ * @property {(args: Record<string, ?>) => boolean} [when] - Optional gate; skipped when it returns false.
12
+ */
13
+
3
14
  /**
4
15
  * Replays client sync envelopes through project supplied authentication,
5
16
  * authorization, application, and persistence hooks.
@@ -23,15 +34,49 @@ export default class SyncEnvelopeReplayService {
23
34
  * @param {{debug?: (...args: Array<unknown>) => void, warn?: (...args: Array<unknown>) => void}} [args.logger] - Logger used for normalization warnings.
24
35
  * @param {?} [args.syncModel] - Sync/change model enabling model-backed default hooks.
25
36
  * @param {string} [args.actorForeignKeyColumn] - Sync model column linking rows to the replay actor.
37
+ * @param {?} [args.authenticationTokenModel] - Token model enabling the default token-lookup authenticateReplay.
38
+ * @param {string} [args.authenticationTokenColumn] - Token model column holding the token. Defaults to "token".
39
+ * @param {string} [args.authenticationTokenParam] - Request param carrying the token. Defaults to "authenticationToken".
40
+ * @param {Record<string, ((args: Record<string, ?>) => Promise<?>) | ConstructorParameters<typeof SyncReplayUpsertApplier>[0]>} [args.applyHandlers] - Per-resourceType apply handlers (functions or declarative upsert-applier specs) enabling the default applyReplayMutation dispatch.
41
+ * @param {(args: Record<string, ?>) => Record<string, ?>} [args.persistExtraAttributes] - Extra attributes merged into the model-backed persisted row (e.g. an event scope column).
42
+ * @param {(args: {mutation: ?, applyResult: ?}) => ?} [args.persistSerializedData] - Overrides the persisted data payload (object results are JSON stringified).
43
+ * @param {(broadcast: {channel: string, params: Record<string, ?>, body: ?}) => Promise<void>} [args.broadcaster] - Delivers declarative broadcasts. Required when broadcasts are configured.
44
+ * @param {SyncReplayBroadcast[]} [args.broadcasts] - Broadcasts fanned out by the default afterReplayMutation.
26
45
  */
27
46
  constructor(args = {}) {
28
47
  this.logger = args.logger || console
29
48
  this.syncModel = args.syncModel || null
30
49
  this.actorForeignKeyColumn = args.actorForeignKeyColumn || "authentication_token_id"
50
+ this.authenticationTokenModel = args.authenticationTokenModel || null
51
+ this.authenticationTokenColumn = args.authenticationTokenColumn || "token"
52
+ this.authenticationTokenParam = args.authenticationTokenParam || "authenticationToken"
53
+ this.persistExtraAttributes = args.persistExtraAttributes || null
54
+ this.persistSerializedData = args.persistSerializedData || null
55
+ this.broadcaster = args.broadcaster || null
56
+ this.broadcasts = args.broadcasts || null
57
+ this.applyHandlers = args.applyHandlers ? this.builtApplyHandlers(args.applyHandlers) : null
31
58
 
32
59
  if (args.actorForeignKeyColumn !== undefined && (typeof args.actorForeignKeyColumn !== "string" || args.actorForeignKeyColumn.length < 1)) {
33
60
  throw new Error(`actorForeignKeyColumn must be a non-blank string, got: ${String(args.actorForeignKeyColumn)}`)
34
61
  }
62
+ if (this.broadcasts && !this.broadcaster) {
63
+ throw new Error("SyncEnvelopeReplayService broadcasts require a broadcaster option delivering them")
64
+ }
65
+ }
66
+
67
+ /**
68
+ * Wraps declarative apply-handler specs in upsert appliers.
69
+ * @param {Record<string, ?>} applyHandlers - Raw apply handlers.
70
+ * @returns {Record<string, (args: Record<string, ?>) => Promise<?>>} Callable handlers by resource type.
71
+ */
72
+ builtApplyHandlers(applyHandlers) {
73
+ return Object.fromEntries(Object.entries(applyHandlers).map(([resourceType, handler]) => {
74
+ if (typeof handler === "function") return [resourceType, handler]
75
+
76
+ const applier = new SyncReplayUpsertApplier(handler)
77
+
78
+ return [resourceType, (/** @type {Record<string, ?>} */ applyArgs) => applier.apply(/** @type {?} */ (applyArgs))]
79
+ }))
35
80
  }
36
81
 
37
82
  /**
@@ -91,11 +136,30 @@ export default class SyncEnvelopeReplayService {
91
136
 
92
137
  /**
93
138
  * Authenticates the sync batch actor.
94
- * @param {Record<string, ?>} _params - Request params.
139
+ *
140
+ * Defaults to a token-model lookup when `authenticationTokenModel` is
141
+ * configured; otherwise apps override this hook.
142
+ * @param {Record<string, ?>} params - Request params.
95
143
  * @returns {Promise<{authenticated: true, actor: ?} | {authenticated: false, errorCode: string, errorMessage: string}>} Auth result.
96
144
  */
97
- async authenticateReplay(_params) {
98
- throw new Error("SyncEnvelopeReplayService.authenticateReplay must be implemented")
145
+ async authenticateReplay(params) {
146
+ if (!this.authenticationTokenModel) {
147
+ throw new Error("SyncEnvelopeReplayService.authenticateReplay must be implemented (or configure authenticationTokenModel)")
148
+ }
149
+
150
+ const token = params[this.authenticationTokenParam]
151
+
152
+ if (!token) {
153
+ return {authenticated: false, errorCode: "missing-authentication-token", errorMessage: "Missing authentication token"}
154
+ }
155
+
156
+ const actor = await this.authenticationTokenModel.findBy({[this.authenticationTokenColumn]: token})
157
+
158
+ if (!actor) {
159
+ return {authenticated: false, errorCode: "invalid-authentication-token", errorMessage: "Invalid authentication token"}
160
+ }
161
+
162
+ return {actor, authenticated: true}
99
163
  }
100
164
 
101
165
  /**
@@ -257,11 +321,20 @@ export default class SyncEnvelopeReplayService {
257
321
 
258
322
  /**
259
323
  * Applies one normalized mutation to domain models.
260
- * @param {{actor: ?, context: Record<string, ?>, existingSync: ?, mutation: import("./sync-envelope-replay-service.js").SyncReplayMutation}} _args - Actor, batch context, existing sync row, and mutation.
324
+ *
325
+ * Defaults to dispatching through the configured apply-handler registry;
326
+ * mutations without a registered handler fail loudly.
327
+ * @param {{actor: ?, context: Record<string, ?>, existingSync: ?, mutation: import("./sync-envelope-replay-service.js").SyncReplayMutation}} args - Actor, batch context, existing sync row, and mutation.
261
328
  * @returns {Promise<?>} Project-specific apply result.
262
329
  */
263
- async applyReplayMutation(_args) {
264
- return null
330
+ async applyReplayMutation(args) {
331
+ if (!this.applyHandlers) return null
332
+
333
+ const applyHandler = this.applyHandlers[args.mutation.resourceType]
334
+
335
+ if (!applyHandler) throw new Error(`No sync apply handler registered for: ${args.mutation.resourceType}`)
336
+
337
+ return await applyHandler(args)
265
338
  }
266
339
 
267
340
  /**
@@ -281,11 +354,25 @@ export default class SyncEnvelopeReplayService {
281
354
  * @param {{actor: ?, context: Record<string, ?>, existingSync: ?, applyResult: ?, mutation: import("./sync-envelope-replay-service.js").SyncReplayMutation, shouldApply: boolean}} args - Replay persistence arguments.
282
355
  * @returns {Promise<void>}
283
356
  */
284
- async persistReplayMutation({actor, existingSync, mutation}) {
357
+ async persistReplayMutation({actor, applyResult, context, existingSync, mutation, shouldApply}) {
285
358
  if (!this.syncModel) return
286
359
 
287
360
  const attributes = this.replayPersistAttributes({actor, mutation})
288
361
 
362
+ // Stale replays never applied anything, so the applyResult-driven extension
363
+ // hooks must not run against the default null skipped result.
364
+ if (this.persistExtraAttributes && shouldApply) {
365
+ Object.assign(attributes, this.persistExtraAttributes({actor, applyResult, context, existingSync, mutation, shouldApply}))
366
+ }
367
+
368
+ if (this.persistSerializedData && shouldApply) {
369
+ const serializedData = this.persistSerializedData({applyResult, mutation})
370
+
371
+ if (serializedData !== undefined && serializedData !== null) {
372
+ attributes.data = typeof serializedData === "string" ? serializedData : JSON.stringify(serializedData)
373
+ }
374
+ }
375
+
289
376
  if (existingSync) {
290
377
  const existingClientUpdatedAt = this.existingReplaySyncClientUpdatedAt(existingSync)
291
378
 
@@ -317,10 +404,28 @@ export default class SyncEnvelopeReplayService {
317
404
 
318
405
  /**
319
406
  * Runs side effects after a successful mutation replay and persistence.
320
- * @param {{actor: ?, context: Record<string, ?>, existingSync: ?, applyResult: ?, mutation: import("./sync-envelope-replay-service.js").SyncReplayMutation, shouldApply: boolean}} _args - Replay side-effect arguments.
407
+ *
408
+ * Defaults to fanning the applied result out through the configured
409
+ * declarative broadcasts.
410
+ * @param {{actor: ?, context: Record<string, ?>, existingSync: ?, applyResult: ?, mutation: import("./sync-envelope-replay-service.js").SyncReplayMutation, shouldApply: boolean}} args - Replay side-effect arguments.
321
411
  * @returns {Promise<void>}
322
412
  */
323
- async afterReplayMutation(_args) {}
413
+ async afterReplayMutation(args) {
414
+ if (!this.broadcasts || !this.broadcaster) return
415
+ // Stale replays never applied anything - broadcasting their skipped results
416
+ // would fan out stale side effects (or crash on the default null applyResult).
417
+ if (!args.shouldApply) return
418
+
419
+ for (const broadcast of this.broadcasts) {
420
+ if (broadcast.when && !broadcast.when(args)) continue
421
+
422
+ await this.broadcaster({
423
+ body: broadcast.body(args),
424
+ channel: typeof broadcast.channel === "function" ? broadcast.channel(args) : broadcast.channel,
425
+ params: broadcast.broadcastParams(args)
426
+ })
427
+ }
428
+ }
324
429
  }
325
430
 
326
431
  /**