velocious 1.0.498 → 1.0.499
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/build/database/record/counter-cache-magnitude.js +240 -0
- package/build/database/record/index.js +26 -0
- package/build/database/record/state-machine.js +25 -3
- package/build/src/database/record/counter-cache-magnitude.d.ts +67 -0
- package/build/src/database/record/counter-cache-magnitude.d.ts.map +1 -0
- package/build/src/database/record/counter-cache-magnitude.js +204 -0
- package/build/src/database/record/index.d.ts +18 -0
- package/build/src/database/record/index.d.ts.map +1 -1
- package/build/src/database/record/index.js +25 -1
- package/build/src/database/record/state-machine.d.ts +2 -0
- package/build/src/database/record/state-machine.d.ts.map +1 -1
- package/build/src/database/record/state-machine.js +21 -4
- package/build/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/src/database/record/counter-cache-magnitude.js +240 -0
- package/src/database/record/index.js +26 -0
- package/src/database/record/state-machine.js +25 -3
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Reactive counter-cache driven by a per-record magnitude.
|
|
5
|
+
*
|
|
6
|
+
* Maintains a counter column on a `belongsTo` parent as the running sum of each
|
|
7
|
+
* child's magnitude — a small number derived from one source attribute (e.g.
|
|
8
|
+
* `1` while a build's `status` is `running`, else `0`). On every create, update
|
|
9
|
+
* and destroy the change in magnitude is applied to the parent as a single
|
|
10
|
+
* atomic increment (`SET col = col + delta`), and when the foreign key changes
|
|
11
|
+
* the magnitude is moved from the old parent to the new one.
|
|
12
|
+
*
|
|
13
|
+
* Because the counter is derived from the source attribute and diffed on every
|
|
14
|
+
* write, it follows that attribute automatically no matter which code path wrote
|
|
15
|
+
* it — there is no per-transition increment/decrement to forget. The old value is
|
|
16
|
+
* captured in `beforeSave` (Velocious clears `changes()` during the post-update
|
|
17
|
+
* reload) and consumed in `afterSave`, so the increment commits atomically with
|
|
18
|
+
* the row it reflects.
|
|
19
|
+
* @typedef {{
|
|
20
|
+
* belongsTo: string,
|
|
21
|
+
* counterColumn: string,
|
|
22
|
+
* sourceAttribute: string,
|
|
23
|
+
* magnitude: (sourceValue: ?) => number
|
|
24
|
+
* }} MagnitudeCounterCacheDefinition
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Captured pending magnitude change stashed on a record between beforeSave and afterSave.
|
|
29
|
+
* @typedef {{newMagnitude: number, oldMagnitude: number, newParentId: ?, oldParentId: ?}} PendingMagnitudeDelta
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Registers a reactive magnitude counter-cache on a model class.
|
|
34
|
+
* @param {typeof import("./index.js").default} modelClass - Model class to add the counter cache to.
|
|
35
|
+
* @param {MagnitudeCounterCacheDefinition} definition - Counter cache definition.
|
|
36
|
+
* @returns {void}
|
|
37
|
+
*/
|
|
38
|
+
export function registerMagnitudeCounterCache(modelClass, definition) {
|
|
39
|
+
/**
|
|
40
|
+
* Dynamic class.
|
|
41
|
+
* @type {?} */
|
|
42
|
+
const dynamicClass = modelClass
|
|
43
|
+
const registeredFlag = `_magnitudeCounterCacheRegistered_${definition.counterColumn}`
|
|
44
|
+
|
|
45
|
+
// Idempotent per counter column: re-declaring (or subclassing) must not double-register.
|
|
46
|
+
if (Object.prototype.hasOwnProperty.call(dynamicClass, registeredFlag) && dynamicClass[registeredFlag]) {
|
|
47
|
+
return
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
dynamicClass[registeredFlag] = true
|
|
51
|
+
|
|
52
|
+
const pendingKey = `_magnitudeCounterCachePending_${definition.counterColumn}`
|
|
53
|
+
|
|
54
|
+
modelClass.beforeSave(async function (record) {
|
|
55
|
+
/**
|
|
56
|
+
* Dynamic record.
|
|
57
|
+
* @type {?} */
|
|
58
|
+
const dynamicRecord = record
|
|
59
|
+
|
|
60
|
+
dynamicRecord[pendingKey] = computePendingMagnitudeDelta(record, definition)
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
modelClass.afterSave(async function (record) {
|
|
64
|
+
/**
|
|
65
|
+
* Dynamic record.
|
|
66
|
+
* @type {?} */
|
|
67
|
+
const dynamicRecord = record
|
|
68
|
+
const pending = /** @type {PendingMagnitudeDelta | null} */ (dynamicRecord[pendingKey])
|
|
69
|
+
|
|
70
|
+
dynamicRecord[pendingKey] = null
|
|
71
|
+
|
|
72
|
+
if (pending) {
|
|
73
|
+
await applyPendingMagnitudeDelta(record, definition, pending)
|
|
74
|
+
}
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
modelClass.afterDestroy(async function (record) {
|
|
78
|
+
const magnitude = definition.magnitude(record.readAttribute(definition.sourceAttribute))
|
|
79
|
+
const parentId = currentParentId(record, definition)
|
|
80
|
+
|
|
81
|
+
if (parentId && magnitude !== 0) {
|
|
82
|
+
await incrementParentCounter(record, definition, parentId, -magnitude)
|
|
83
|
+
}
|
|
84
|
+
})
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Diffs the source attribute (and foreign key) across the pending save to capture
|
|
89
|
+
* how the parent counter should move. Runs in beforeSave, where `changes()` still
|
|
90
|
+
* holds the pre-save values.
|
|
91
|
+
* @param {import("./index.js").default} record - Record being saved.
|
|
92
|
+
* @param {MagnitudeCounterCacheDefinition} definition - Counter cache definition.
|
|
93
|
+
* @returns {PendingMagnitudeDelta} The captured magnitude change.
|
|
94
|
+
*/
|
|
95
|
+
function computePendingMagnitudeDelta(record, definition) {
|
|
96
|
+
const changes = record.changes()
|
|
97
|
+
const sourceColumn = columnNameForAttribute(record, definition.sourceAttribute)
|
|
98
|
+
const foreignKeyColumn = record.getModelClass().getRelationshipByName(definition.belongsTo).getForeignKey()
|
|
99
|
+
|
|
100
|
+
const newSource = record.readAttribute(definition.sourceAttribute)
|
|
101
|
+
const oldSource = oldSourceValueThroughReadCast(record, definition.sourceAttribute, sourceColumn, changes, newSource)
|
|
102
|
+
|
|
103
|
+
const newParentId = currentParentId(record, definition)
|
|
104
|
+
const oldParentId = foreignKeyColumn in changes ? changes[foreignKeyColumn][0] : newParentId
|
|
105
|
+
|
|
106
|
+
return {
|
|
107
|
+
newMagnitude: definition.magnitude(newSource),
|
|
108
|
+
oldMagnitude: definition.magnitude(oldSource),
|
|
109
|
+
newParentId,
|
|
110
|
+
oldParentId
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Applies a captured magnitude change to the parent counter(s) as atomic increments.
|
|
116
|
+
* @param {import("./index.js").default} record - Record that was saved.
|
|
117
|
+
* @param {MagnitudeCounterCacheDefinition} definition - Counter cache definition.
|
|
118
|
+
* @param {PendingMagnitudeDelta} pending - The captured magnitude change.
|
|
119
|
+
* @returns {Promise<void>}
|
|
120
|
+
*/
|
|
121
|
+
async function applyPendingMagnitudeDelta(record, definition, pending) {
|
|
122
|
+
const {newMagnitude, oldMagnitude, newParentId, oldParentId} = pending
|
|
123
|
+
|
|
124
|
+
if (oldParentId === newParentId) {
|
|
125
|
+
const delta = newMagnitude - oldMagnitude
|
|
126
|
+
|
|
127
|
+
if (newParentId && delta !== 0) {
|
|
128
|
+
await incrementParentCounter(record, definition, newParentId, delta)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// The foreign key moved: take the old magnitude off the old parent and put the
|
|
135
|
+
// new magnitude on the new parent.
|
|
136
|
+
if (oldParentId && oldMagnitude !== 0) {
|
|
137
|
+
await incrementParentCounter(record, definition, oldParentId, -oldMagnitude)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (newParentId && newMagnitude !== 0) {
|
|
141
|
+
await incrementParentCounter(record, definition, newParentId, newMagnitude)
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Reads the pre-save value of the source attribute through the normal read/cast
|
|
147
|
+
* path, so `magnitude` receives the old value in the same shape as the new one
|
|
148
|
+
* (e.g. a declared boolean as `true`/`false`, not the raw stored `1`/`0`). Drops
|
|
149
|
+
* the pending change so `readAttribute` falls back to the committed value and
|
|
150
|
+
* applies the same cast a fresh read would, then restores the pending change.
|
|
151
|
+
* @param {import("./index.js").default} record - Record being saved.
|
|
152
|
+
* @param {string} sourceAttribute - Source attribute name.
|
|
153
|
+
* @param {string} sourceColumn - Source column name.
|
|
154
|
+
* @param {Record<string, ?>} changes - The record's pre-save changes (column-keyed).
|
|
155
|
+
* @param {?} currentValue - The current (new) read value, returned when the source did not change.
|
|
156
|
+
* @returns {?} The read-cast pre-save value.
|
|
157
|
+
*/
|
|
158
|
+
function oldSourceValueThroughReadCast(record, sourceAttribute, sourceColumn, changes, currentValue) {
|
|
159
|
+
if (!(sourceColumn in changes)) {
|
|
160
|
+
return currentValue
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Dynamic record.
|
|
165
|
+
* @type {?} */
|
|
166
|
+
const dynamicRecord = record
|
|
167
|
+
const pendingChange = dynamicRecord._changes[sourceColumn]
|
|
168
|
+
|
|
169
|
+
delete dynamicRecord._changes[sourceColumn]
|
|
170
|
+
|
|
171
|
+
try {
|
|
172
|
+
return record.readAttribute(sourceAttribute)
|
|
173
|
+
} finally {
|
|
174
|
+
dynamicRecord._changes[sourceColumn] = pendingChange
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Reads the record's current foreign-key value for the counter-cache parent.
|
|
180
|
+
* @param {import("./index.js").default} record - Record whose parent is targeted.
|
|
181
|
+
* @param {MagnitudeCounterCacheDefinition} definition - Counter cache definition.
|
|
182
|
+
* @returns {?} The current foreign-key value.
|
|
183
|
+
*/
|
|
184
|
+
function currentParentId(record, definition) {
|
|
185
|
+
const foreignKeyColumn = record.getModelClass().getRelationshipByName(definition.belongsTo).getForeignKey()
|
|
186
|
+
|
|
187
|
+
return record.readAttribute(attributeNameForColumn(record, foreignKeyColumn))
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Atomically adds `amount` to the parent's counter column for one parent row.
|
|
192
|
+
* @param {import("./index.js").default} record - Child record (for connection + relationship metadata).
|
|
193
|
+
* @param {MagnitudeCounterCacheDefinition} definition - Counter cache definition.
|
|
194
|
+
* @param {?} parentId - Parent primary-key value.
|
|
195
|
+
* @param {number} amount - Signed integer to add.
|
|
196
|
+
* @returns {Promise<void>}
|
|
197
|
+
*/
|
|
198
|
+
async function incrementParentCounter(record, definition, parentId, amount) {
|
|
199
|
+
const modelClass = record.getModelClass()
|
|
200
|
+
const relationship = modelClass.getRelationshipByName(definition.belongsTo)
|
|
201
|
+
const parentModelClass = relationship.getTargetModelClass()
|
|
202
|
+
|
|
203
|
+
if (!parentModelClass) {
|
|
204
|
+
throw new Error(`magnitudeCounterCache on ${modelClass.getModelName()} could not resolve the "${definition.belongsTo}" parent model class`)
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Update through the PARENT model's connection: the row being modified belongs
|
|
208
|
+
// to the parent, which may live on a different database/tenant than the child.
|
|
209
|
+
const db = parentModelClass.connection()
|
|
210
|
+
const counterColumnSql = db.quoteColumn(definition.counterColumn)
|
|
211
|
+
const truncatedAmount = Math.trunc(amount)
|
|
212
|
+
|
|
213
|
+
await db.query(
|
|
214
|
+
`UPDATE ${db.quoteTable(parentModelClass.tableName())} ` +
|
|
215
|
+
`SET ${counterColumnSql} = COALESCE(${counterColumnSql}, 0) + ${truncatedAmount} ` +
|
|
216
|
+
`WHERE ${db.quoteColumn(relationship.getPrimaryKey())} = ${db.quote(parentId)}`
|
|
217
|
+
)
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Resolves the column name backing an attribute name.
|
|
222
|
+
* @param {import("./index.js").default} record - Record.
|
|
223
|
+
* @param {string} attributeName - Attribute name.
|
|
224
|
+
* @returns {string} The column name for the attribute (falls back to the attribute name).
|
|
225
|
+
*/
|
|
226
|
+
function columnNameForAttribute(record, attributeName) {
|
|
227
|
+
return record.getModelClass().getAttributeNameToColumnNameMap()[attributeName] || attributeName
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Resolves the attribute name backing a column name.
|
|
232
|
+
* @param {import("./index.js").default} record - Record.
|
|
233
|
+
* @param {string} columnName - Column name.
|
|
234
|
+
* @returns {string} The attribute name for the column (falls back to the column name).
|
|
235
|
+
*/
|
|
236
|
+
function attributeNameForColumn(record, columnName) {
|
|
237
|
+
const map = record.getModelClass().getAttributeNameToColumnNameMap()
|
|
238
|
+
|
|
239
|
+
return Object.keys(map).find((attributeName) => map[attributeName] === columnName) || columnName
|
|
240
|
+
}
|
|
@@ -47,6 +47,8 @@ import {defineModelScope} from "../../utils/model-scope.js"
|
|
|
47
47
|
import { normalizeDateStringForWrite, normalizeDateValueForRead, normalizeDateValueForWrite } from "../datetime-storage.js"
|
|
48
48
|
import {formatValue} from "../../utils/format-value.js"
|
|
49
49
|
import {captureCreateAuditChanges, captureUpdateAuditChanges, createAudit, createCreateAudit, createDestroyAudit, createUpdateAudit, registerAuditCallback, registerAuditing, withoutAudit} from "./auditing.js"
|
|
50
|
+
import {registerMagnitudeCounterCache} from "./counter-cache-magnitude.js"
|
|
51
|
+
import {stateMachine} from "./state-machine.js"
|
|
50
52
|
import ValidatorsFormat from "./validators/format.js"
|
|
51
53
|
import ValidatorsLength from "./validators/length.js"
|
|
52
54
|
import ValidatorsPresence from "./validators/presence.js"
|
|
@@ -668,6 +670,30 @@ class VelociousDatabaseRecord {
|
|
|
668
670
|
registerAuditing(this)
|
|
669
671
|
}
|
|
670
672
|
|
|
673
|
+
/**
|
|
674
|
+
* Declares an aasm-style state machine on this model: named states, events
|
|
675
|
+
* (guarded transitions), and enter/exit + before/after transition hooks. See
|
|
676
|
+
* `state-machine.js`. Generates `event()` / `eventAndSave()` / `canEvent()`
|
|
677
|
+
* transition methods per declared event.
|
|
678
|
+
* @param {import("./state-machine.js").StateMachineDefinition} definition - State machine definition.
|
|
679
|
+
* @returns {void}
|
|
680
|
+
*/
|
|
681
|
+
static stateMachine(definition) {
|
|
682
|
+
stateMachine(this, definition)
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
/**
|
|
686
|
+
* Maintains a counter column on a `belongsTo` parent as the sum of a per-record
|
|
687
|
+
* magnitude, kept current by atomic increments diffed on every create/update/
|
|
688
|
+
* destroy (and moved between parents when the foreign key changes). See
|
|
689
|
+
* `counter-cache-magnitude.js`.
|
|
690
|
+
* @param {import("./counter-cache-magnitude.js").MagnitudeCounterCacheDefinition} definition - Counter cache definition.
|
|
691
|
+
* @returns {void}
|
|
692
|
+
*/
|
|
693
|
+
static magnitudeCounterCache(definition) {
|
|
694
|
+
registerMagnitudeCounterCache(this, definition)
|
|
695
|
+
}
|
|
696
|
+
|
|
671
697
|
/**
|
|
672
698
|
* Registers a callback invoked after this model writes an audit row for the action.
|
|
673
699
|
* @template {typeof VelociousDatabaseRecord} MC
|
|
@@ -10,7 +10,9 @@
|
|
|
10
10
|
* }} StateMachineDefinition
|
|
11
11
|
* @typedef {{
|
|
12
12
|
* beforeEnter?: (model: import("./index.js").default) => void | Promise<void>,
|
|
13
|
-
* afterEnter?: (model: import("./index.js").default) => void | Promise<void
|
|
13
|
+
* afterEnter?: (model: import("./index.js").default) => void | Promise<void>,
|
|
14
|
+
* beforeExit?: (model: import("./index.js").default) => void | Promise<void>,
|
|
15
|
+
* afterExit?: (model: import("./index.js").default) => void | Promise<void>
|
|
14
16
|
* }} StateDefinition
|
|
15
17
|
* @typedef {{
|
|
16
18
|
* from: string | string[],
|
|
@@ -68,6 +70,14 @@ export function stateMachine(ModelClass, definition) {
|
|
|
68
70
|
* @type {?} */
|
|
69
71
|
const dynamicClass = ModelClass
|
|
70
72
|
|
|
73
|
+
// Idempotent: re-declaring on the same class (or a re-evaluated module) must not
|
|
74
|
+
// register the before/after-save transition hooks twice. Guard on an own property
|
|
75
|
+
// so a subclass declaring its own machine is unaffected by the parent's flag.
|
|
76
|
+
if (Object.prototype.hasOwnProperty.call(dynamicClass, "_stateMachineRegistered") && dynamicClass._stateMachineRegistered) {
|
|
77
|
+
return
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
dynamicClass._stateMachineRegistered = true
|
|
71
81
|
dynamicClass._stateMachineDefinition = definition
|
|
72
82
|
dynamicClass._stateMachineColumn = column
|
|
73
83
|
|
|
@@ -229,7 +239,13 @@ export function stateMachine(ModelClass, definition) {
|
|
|
229
239
|
await eventDef.before(model)
|
|
230
240
|
}
|
|
231
241
|
|
|
232
|
-
// Run state
|
|
242
|
+
// Run the exited state's beforeExit, then the entered state's beforeEnter
|
|
243
|
+
const fromStateDefinition = definition.states[pending.from]
|
|
244
|
+
|
|
245
|
+
if (fromStateDefinition?.beforeExit) {
|
|
246
|
+
await fromStateDefinition.beforeExit(model)
|
|
247
|
+
}
|
|
248
|
+
|
|
233
249
|
const stateDefinition = definition.states[pending.to]
|
|
234
250
|
|
|
235
251
|
if (stateDefinition?.beforeEnter) {
|
|
@@ -252,13 +268,19 @@ export function stateMachine(ModelClass, definition) {
|
|
|
252
268
|
// Clear the pending transition now that save is complete
|
|
253
269
|
dynamicModel[PENDING_TRANSITION_KEY] = null
|
|
254
270
|
|
|
255
|
-
// Run state
|
|
271
|
+
// Run the entered state's afterEnter, then the exited state's afterExit
|
|
256
272
|
const stateDefinition = definition.states[pending.to]
|
|
257
273
|
|
|
258
274
|
if (stateDefinition?.afterEnter) {
|
|
259
275
|
await stateDefinition.afterEnter(model)
|
|
260
276
|
}
|
|
261
277
|
|
|
278
|
+
const fromStateDefinition = definition.states[pending.from]
|
|
279
|
+
|
|
280
|
+
if (fromStateDefinition?.afterExit) {
|
|
281
|
+
await fromStateDefinition.afterExit(model)
|
|
282
|
+
}
|
|
283
|
+
|
|
262
284
|
// Run event-level after callback
|
|
263
285
|
const eventDef = definition.events[pending.eventName]
|
|
264
286
|
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reactive counter-cache driven by a per-record magnitude.
|
|
3
|
+
*
|
|
4
|
+
* Maintains a counter column on a `belongsTo` parent as the running sum of each
|
|
5
|
+
* child's magnitude — a small number derived from one source attribute (e.g.
|
|
6
|
+
* `1` while a build's `status` is `running`, else `0`). On every create, update
|
|
7
|
+
* and destroy the change in magnitude is applied to the parent as a single
|
|
8
|
+
* atomic increment (`SET col = col + delta`), and when the foreign key changes
|
|
9
|
+
* the magnitude is moved from the old parent to the new one.
|
|
10
|
+
*
|
|
11
|
+
* Because the counter is derived from the source attribute and diffed on every
|
|
12
|
+
* write, it follows that attribute automatically no matter which code path wrote
|
|
13
|
+
* it — there is no per-transition increment/decrement to forget. The old value is
|
|
14
|
+
* captured in `beforeSave` (Velocious clears `changes()` during the post-update
|
|
15
|
+
* reload) and consumed in `afterSave`, so the increment commits atomically with
|
|
16
|
+
* the row it reflects.
|
|
17
|
+
* @typedef {{
|
|
18
|
+
* belongsTo: string,
|
|
19
|
+
* counterColumn: string,
|
|
20
|
+
* sourceAttribute: string,
|
|
21
|
+
* magnitude: (sourceValue: ?) => number
|
|
22
|
+
* }} MagnitudeCounterCacheDefinition
|
|
23
|
+
*/
|
|
24
|
+
/**
|
|
25
|
+
* Captured pending magnitude change stashed on a record between beforeSave and afterSave.
|
|
26
|
+
* @typedef {{newMagnitude: number, oldMagnitude: number, newParentId: ?, oldParentId: ?}} PendingMagnitudeDelta
|
|
27
|
+
*/
|
|
28
|
+
/**
|
|
29
|
+
* Registers a reactive magnitude counter-cache on a model class.
|
|
30
|
+
* @param {typeof import("./index.js").default} modelClass - Model class to add the counter cache to.
|
|
31
|
+
* @param {MagnitudeCounterCacheDefinition} definition - Counter cache definition.
|
|
32
|
+
* @returns {void}
|
|
33
|
+
*/
|
|
34
|
+
export function registerMagnitudeCounterCache(modelClass: typeof import("./index.js").default, definition: MagnitudeCounterCacheDefinition): void;
|
|
35
|
+
/**
|
|
36
|
+
* Reactive counter-cache driven by a per-record magnitude.
|
|
37
|
+
*
|
|
38
|
+
* Maintains a counter column on a `belongsTo` parent as the running sum of each
|
|
39
|
+
* child's magnitude — a small number derived from one source attribute (e.g.
|
|
40
|
+
* `1` while a build's `status` is `running`, else `0`). On every create, update
|
|
41
|
+
* and destroy the change in magnitude is applied to the parent as a single
|
|
42
|
+
* atomic increment (`SET col = col + delta`), and when the foreign key changes
|
|
43
|
+
* the magnitude is moved from the old parent to the new one.
|
|
44
|
+
*
|
|
45
|
+
* Because the counter is derived from the source attribute and diffed on every
|
|
46
|
+
* write, it follows that attribute automatically no matter which code path wrote
|
|
47
|
+
* it — there is no per-transition increment/decrement to forget. The old value is
|
|
48
|
+
* captured in `beforeSave` (Velocious clears `changes()` during the post-update
|
|
49
|
+
* reload) and consumed in `afterSave`, so the increment commits atomically with
|
|
50
|
+
* the row it reflects.
|
|
51
|
+
*/
|
|
52
|
+
export type MagnitudeCounterCacheDefinition = {
|
|
53
|
+
belongsTo: string;
|
|
54
|
+
counterColumn: string;
|
|
55
|
+
sourceAttribute: string;
|
|
56
|
+
magnitude: (sourceValue: unknown) => number;
|
|
57
|
+
};
|
|
58
|
+
/**
|
|
59
|
+
* Captured pending magnitude change stashed on a record between beforeSave and afterSave.
|
|
60
|
+
*/
|
|
61
|
+
export type PendingMagnitudeDelta = {
|
|
62
|
+
newMagnitude: number;
|
|
63
|
+
oldMagnitude: number;
|
|
64
|
+
newParentId: unknown;
|
|
65
|
+
oldParentId: unknown;
|
|
66
|
+
};
|
|
67
|
+
//# sourceMappingURL=counter-cache-magnitude.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"counter-cache-magnitude.d.ts","sourceRoot":"","sources":["../../../../src/database/record/counter-cache-magnitude.js"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH;;;GAGG;AAEH;;;;;GAKG;AACH,0DAJW,cAAc,YAAY,EAAE,OAAO,cACnC,+BAA+B,GAC7B,IAAI,CAiDhB;;;;;;;;;;;;;;;;;;8CAlEY;IACR,SAAS,EAAE,MAAM,CAAC;IAClB,aAAa,EAAE,MAAM,CAAC;IACtB,eAAe,EAAE,MAAM,CAAC;IACxB,SAAS,EAAE,CAAC,WAAW,EAAE,OAAC,KAAK,MAAM,CAAA;CACtC;;;;oCAKS;IAAC,YAAY,EAAE,MAAM,CAAC;IAAC,YAAY,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,OAAC,CAAC;IAAC,WAAW,EAAE,OAAC,CAAA;CAAC"}
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
/**
|
|
3
|
+
* Reactive counter-cache driven by a per-record magnitude.
|
|
4
|
+
*
|
|
5
|
+
* Maintains a counter column on a `belongsTo` parent as the running sum of each
|
|
6
|
+
* child's magnitude — a small number derived from one source attribute (e.g.
|
|
7
|
+
* `1` while a build's `status` is `running`, else `0`). On every create, update
|
|
8
|
+
* and destroy the change in magnitude is applied to the parent as a single
|
|
9
|
+
* atomic increment (`SET col = col + delta`), and when the foreign key changes
|
|
10
|
+
* the magnitude is moved from the old parent to the new one.
|
|
11
|
+
*
|
|
12
|
+
* Because the counter is derived from the source attribute and diffed on every
|
|
13
|
+
* write, it follows that attribute automatically no matter which code path wrote
|
|
14
|
+
* it — there is no per-transition increment/decrement to forget. The old value is
|
|
15
|
+
* captured in `beforeSave` (Velocious clears `changes()` during the post-update
|
|
16
|
+
* reload) and consumed in `afterSave`, so the increment commits atomically with
|
|
17
|
+
* the row it reflects.
|
|
18
|
+
* @typedef {{
|
|
19
|
+
* belongsTo: string,
|
|
20
|
+
* counterColumn: string,
|
|
21
|
+
* sourceAttribute: string,
|
|
22
|
+
* magnitude: (sourceValue: ?) => number
|
|
23
|
+
* }} MagnitudeCounterCacheDefinition
|
|
24
|
+
*/
|
|
25
|
+
/**
|
|
26
|
+
* Captured pending magnitude change stashed on a record between beforeSave and afterSave.
|
|
27
|
+
* @typedef {{newMagnitude: number, oldMagnitude: number, newParentId: ?, oldParentId: ?}} PendingMagnitudeDelta
|
|
28
|
+
*/
|
|
29
|
+
/**
|
|
30
|
+
* Registers a reactive magnitude counter-cache on a model class.
|
|
31
|
+
* @param {typeof import("./index.js").default} modelClass - Model class to add the counter cache to.
|
|
32
|
+
* @param {MagnitudeCounterCacheDefinition} definition - Counter cache definition.
|
|
33
|
+
* @returns {void}
|
|
34
|
+
*/
|
|
35
|
+
export function registerMagnitudeCounterCache(modelClass, definition) {
|
|
36
|
+
/**
|
|
37
|
+
* Dynamic class.
|
|
38
|
+
* @type {?} */
|
|
39
|
+
const dynamicClass = modelClass;
|
|
40
|
+
const registeredFlag = `_magnitudeCounterCacheRegistered_${definition.counterColumn}`;
|
|
41
|
+
// Idempotent per counter column: re-declaring (or subclassing) must not double-register.
|
|
42
|
+
if (Object.prototype.hasOwnProperty.call(dynamicClass, registeredFlag) && dynamicClass[registeredFlag]) {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
dynamicClass[registeredFlag] = true;
|
|
46
|
+
const pendingKey = `_magnitudeCounterCachePending_${definition.counterColumn}`;
|
|
47
|
+
modelClass.beforeSave(async function (record) {
|
|
48
|
+
/**
|
|
49
|
+
* Dynamic record.
|
|
50
|
+
* @type {?} */
|
|
51
|
+
const dynamicRecord = record;
|
|
52
|
+
dynamicRecord[pendingKey] = computePendingMagnitudeDelta(record, definition);
|
|
53
|
+
});
|
|
54
|
+
modelClass.afterSave(async function (record) {
|
|
55
|
+
/**
|
|
56
|
+
* Dynamic record.
|
|
57
|
+
* @type {?} */
|
|
58
|
+
const dynamicRecord = record;
|
|
59
|
+
const pending = /** @type {PendingMagnitudeDelta | null} */ (dynamicRecord[pendingKey]);
|
|
60
|
+
dynamicRecord[pendingKey] = null;
|
|
61
|
+
if (pending) {
|
|
62
|
+
await applyPendingMagnitudeDelta(record, definition, pending);
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
modelClass.afterDestroy(async function (record) {
|
|
66
|
+
const magnitude = definition.magnitude(record.readAttribute(definition.sourceAttribute));
|
|
67
|
+
const parentId = currentParentId(record, definition);
|
|
68
|
+
if (parentId && magnitude !== 0) {
|
|
69
|
+
await incrementParentCounter(record, definition, parentId, -magnitude);
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Diffs the source attribute (and foreign key) across the pending save to capture
|
|
75
|
+
* how the parent counter should move. Runs in beforeSave, where `changes()` still
|
|
76
|
+
* holds the pre-save values.
|
|
77
|
+
* @param {import("./index.js").default} record - Record being saved.
|
|
78
|
+
* @param {MagnitudeCounterCacheDefinition} definition - Counter cache definition.
|
|
79
|
+
* @returns {PendingMagnitudeDelta} The captured magnitude change.
|
|
80
|
+
*/
|
|
81
|
+
function computePendingMagnitudeDelta(record, definition) {
|
|
82
|
+
const changes = record.changes();
|
|
83
|
+
const sourceColumn = columnNameForAttribute(record, definition.sourceAttribute);
|
|
84
|
+
const foreignKeyColumn = record.getModelClass().getRelationshipByName(definition.belongsTo).getForeignKey();
|
|
85
|
+
const newSource = record.readAttribute(definition.sourceAttribute);
|
|
86
|
+
const oldSource = oldSourceValueThroughReadCast(record, definition.sourceAttribute, sourceColumn, changes, newSource);
|
|
87
|
+
const newParentId = currentParentId(record, definition);
|
|
88
|
+
const oldParentId = foreignKeyColumn in changes ? changes[foreignKeyColumn][0] : newParentId;
|
|
89
|
+
return {
|
|
90
|
+
newMagnitude: definition.magnitude(newSource),
|
|
91
|
+
oldMagnitude: definition.magnitude(oldSource),
|
|
92
|
+
newParentId,
|
|
93
|
+
oldParentId
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Applies a captured magnitude change to the parent counter(s) as atomic increments.
|
|
98
|
+
* @param {import("./index.js").default} record - Record that was saved.
|
|
99
|
+
* @param {MagnitudeCounterCacheDefinition} definition - Counter cache definition.
|
|
100
|
+
* @param {PendingMagnitudeDelta} pending - The captured magnitude change.
|
|
101
|
+
* @returns {Promise<void>}
|
|
102
|
+
*/
|
|
103
|
+
async function applyPendingMagnitudeDelta(record, definition, pending) {
|
|
104
|
+
const { newMagnitude, oldMagnitude, newParentId, oldParentId } = pending;
|
|
105
|
+
if (oldParentId === newParentId) {
|
|
106
|
+
const delta = newMagnitude - oldMagnitude;
|
|
107
|
+
if (newParentId && delta !== 0) {
|
|
108
|
+
await incrementParentCounter(record, definition, newParentId, delta);
|
|
109
|
+
}
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
// The foreign key moved: take the old magnitude off the old parent and put the
|
|
113
|
+
// new magnitude on the new parent.
|
|
114
|
+
if (oldParentId && oldMagnitude !== 0) {
|
|
115
|
+
await incrementParentCounter(record, definition, oldParentId, -oldMagnitude);
|
|
116
|
+
}
|
|
117
|
+
if (newParentId && newMagnitude !== 0) {
|
|
118
|
+
await incrementParentCounter(record, definition, newParentId, newMagnitude);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Reads the pre-save value of the source attribute through the normal read/cast
|
|
123
|
+
* path, so `magnitude` receives the old value in the same shape as the new one
|
|
124
|
+
* (e.g. a declared boolean as `true`/`false`, not the raw stored `1`/`0`). Drops
|
|
125
|
+
* the pending change so `readAttribute` falls back to the committed value and
|
|
126
|
+
* applies the same cast a fresh read would, then restores the pending change.
|
|
127
|
+
* @param {import("./index.js").default} record - Record being saved.
|
|
128
|
+
* @param {string} sourceAttribute - Source attribute name.
|
|
129
|
+
* @param {string} sourceColumn - Source column name.
|
|
130
|
+
* @param {Record<string, ?>} changes - The record's pre-save changes (column-keyed).
|
|
131
|
+
* @param {?} currentValue - The current (new) read value, returned when the source did not change.
|
|
132
|
+
* @returns {?} The read-cast pre-save value.
|
|
133
|
+
*/
|
|
134
|
+
function oldSourceValueThroughReadCast(record, sourceAttribute, sourceColumn, changes, currentValue) {
|
|
135
|
+
if (!(sourceColumn in changes)) {
|
|
136
|
+
return currentValue;
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Dynamic record.
|
|
140
|
+
* @type {?} */
|
|
141
|
+
const dynamicRecord = record;
|
|
142
|
+
const pendingChange = dynamicRecord._changes[sourceColumn];
|
|
143
|
+
delete dynamicRecord._changes[sourceColumn];
|
|
144
|
+
try {
|
|
145
|
+
return record.readAttribute(sourceAttribute);
|
|
146
|
+
}
|
|
147
|
+
finally {
|
|
148
|
+
dynamicRecord._changes[sourceColumn] = pendingChange;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Reads the record's current foreign-key value for the counter-cache parent.
|
|
153
|
+
* @param {import("./index.js").default} record - Record whose parent is targeted.
|
|
154
|
+
* @param {MagnitudeCounterCacheDefinition} definition - Counter cache definition.
|
|
155
|
+
* @returns {?} The current foreign-key value.
|
|
156
|
+
*/
|
|
157
|
+
function currentParentId(record, definition) {
|
|
158
|
+
const foreignKeyColumn = record.getModelClass().getRelationshipByName(definition.belongsTo).getForeignKey();
|
|
159
|
+
return record.readAttribute(attributeNameForColumn(record, foreignKeyColumn));
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Atomically adds `amount` to the parent's counter column for one parent row.
|
|
163
|
+
* @param {import("./index.js").default} record - Child record (for connection + relationship metadata).
|
|
164
|
+
* @param {MagnitudeCounterCacheDefinition} definition - Counter cache definition.
|
|
165
|
+
* @param {?} parentId - Parent primary-key value.
|
|
166
|
+
* @param {number} amount - Signed integer to add.
|
|
167
|
+
* @returns {Promise<void>}
|
|
168
|
+
*/
|
|
169
|
+
async function incrementParentCounter(record, definition, parentId, amount) {
|
|
170
|
+
const modelClass = record.getModelClass();
|
|
171
|
+
const relationship = modelClass.getRelationshipByName(definition.belongsTo);
|
|
172
|
+
const parentModelClass = relationship.getTargetModelClass();
|
|
173
|
+
if (!parentModelClass) {
|
|
174
|
+
throw new Error(`magnitudeCounterCache on ${modelClass.getModelName()} could not resolve the "${definition.belongsTo}" parent model class`);
|
|
175
|
+
}
|
|
176
|
+
// Update through the PARENT model's connection: the row being modified belongs
|
|
177
|
+
// to the parent, which may live on a different database/tenant than the child.
|
|
178
|
+
const db = parentModelClass.connection();
|
|
179
|
+
const counterColumnSql = db.quoteColumn(definition.counterColumn);
|
|
180
|
+
const truncatedAmount = Math.trunc(amount);
|
|
181
|
+
await db.query(`UPDATE ${db.quoteTable(parentModelClass.tableName())} ` +
|
|
182
|
+
`SET ${counterColumnSql} = COALESCE(${counterColumnSql}, 0) + ${truncatedAmount} ` +
|
|
183
|
+
`WHERE ${db.quoteColumn(relationship.getPrimaryKey())} = ${db.quote(parentId)}`);
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Resolves the column name backing an attribute name.
|
|
187
|
+
* @param {import("./index.js").default} record - Record.
|
|
188
|
+
* @param {string} attributeName - Attribute name.
|
|
189
|
+
* @returns {string} The column name for the attribute (falls back to the attribute name).
|
|
190
|
+
*/
|
|
191
|
+
function columnNameForAttribute(record, attributeName) {
|
|
192
|
+
return record.getModelClass().getAttributeNameToColumnNameMap()[attributeName] || attributeName;
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Resolves the attribute name backing a column name.
|
|
196
|
+
* @param {import("./index.js").default} record - Record.
|
|
197
|
+
* @param {string} columnName - Column name.
|
|
198
|
+
* @returns {string} The attribute name for the column (falls back to the column name).
|
|
199
|
+
*/
|
|
200
|
+
function attributeNameForColumn(record, columnName) {
|
|
201
|
+
const map = record.getModelClass().getAttributeNameToColumnNameMap();
|
|
202
|
+
return Object.keys(map).find((attributeName) => map[attributeName] === columnName) || columnName;
|
|
203
|
+
}
|
|
204
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY291bnRlci1jYWNoZS1tYWduaXR1ZGUuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi8uLi8uLi9zcmMvZGF0YWJhc2UvcmVjb3JkL2NvdW50ZXItY2FjaGUtbWFnbml0dWRlLmpzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLFlBQVk7QUFFWjs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7OztHQXNCRztBQUVIOzs7R0FHRztBQUVIOzs7OztHQUtHO0FBQ0gsTUFBTSxVQUFVLDZCQUE2QixDQUFDLFVBQVUsRUFBRSxVQUFVO0lBQ2xFOzttQkFFZTtJQUNmLE1BQU0sWUFBWSxHQUFHLFVBQVUsQ0FBQTtJQUMvQixNQUFNLGNBQWMsR0FBRyxvQ0FBb0MsVUFBVSxDQUFDLGFBQWEsRUFBRSxDQUFBO0lBRXJGLHlGQUF5RjtJQUN6RixJQUFJLE1BQU0sQ0FBQyxTQUFTLENBQUMsY0FBYyxDQUFDLElBQUksQ0FBQyxZQUFZLEVBQUUsY0FBYyxDQUFDLElBQUksWUFBWSxDQUFDLGNBQWMsQ0FBQyxFQUFFLENBQUM7UUFDdkcsT0FBTTtJQUNSLENBQUM7SUFFRCxZQUFZLENBQUMsY0FBYyxDQUFDLEdBQUcsSUFBSSxDQUFBO0lBRW5DLE1BQU0sVUFBVSxHQUFHLGlDQUFpQyxVQUFVLENBQUMsYUFBYSxFQUFFLENBQUE7SUFFOUUsVUFBVSxDQUFDLFVBQVUsQ0FBQyxLQUFLLFdBQVcsTUFBTTtRQUMxQzs7dUJBRWU7UUFDZixNQUFNLGFBQWEsR0FBRyxNQUFNLENBQUE7UUFFNUIsYUFBYSxDQUFDLFVBQVUsQ0FBQyxHQUFHLDRCQUE0QixDQUFDLE1BQU0sRUFBRSxVQUFVLENBQUMsQ0FBQTtJQUM5RSxDQUFDLENBQUMsQ0FBQTtJQUVGLFVBQVUsQ0FBQyxTQUFTLENBQUMsS0FBSyxXQUFXLE1BQU07UUFDekM7O3VCQUVlO1FBQ2YsTUFBTSxhQUFhLEdBQUcsTUFBTSxDQUFBO1FBQzVCLE1BQU0sT0FBTyxHQUFHLDJDQUEyQyxDQUFDLENBQUMsYUFBYSxD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RztBQUNILFNBQVMsc0JBQXNCLENBQUMsTUFBTSxFQUFFLFVBQVU7SUFDaEQsTUFBTSxHQUFHLEdBQUcsTUFBTSxDQUFDLGFBQWEsRUFBRSxDQUFDLCtCQUErQixFQUFFLENBQUE7SUFFcEUsT0FBTyxNQUFNLENBQUMsSUFBSSxDQUFDLEdBQUcsQ0FBQyxDQUFDLElBQUksQ0FBQyxDQUFDLGFBQWEsRUFBRSxFQUFFLENBQUMsR0FBRyxDQUFDLGFBQWEsQ0FBQyxLQUFLLFVBQVUsQ0FBQyxJQUFJLFVBQVUsQ0FBQTtBQUNsRyxDQUFDIiwic291cmNlc0NvbnRlbnQiOlsiLy8gQHRzLWNoZWNrXG5cbi8qKlxuICogUmVhY3RpdmUgY291bnRlci1jYWNoZSBkcml2ZW4gYnkgYSBwZXItcmVjb3JkIG1hZ25pdHVkZS5cbiAqXG4gKiBNYWludGFpbnMgYSBjb3VudGVyIGNvbHVtbiBvbiBhIGBiZWxvbmdzVG9gIHBhcmVudCBhcyB0aGUgcnVubmluZyBzdW0gb2YgZWFjaFxuICogY2hpbGQncyBtYWduaXR1ZGUg4oCUIGEgc21hbGwgbnVtYmVyIGRlcml2ZWQgZnJvbSBvbmUgc291cmNlIGF0dHJpYnV0ZSAoZS5nLlxuICogYDFgIHdoaWxlIGEgYnVpbGQncyBgc3RhdHVzYCBpcyBgcnVubmluZ2AsIGVsc2UgYDBgKS4gT24gZXZlcnkgY3JlYXRlLCB1cGRhdGVcbiAqIGFuZCBkZXN0cm95IHRoZSBjaGFuZ2UgaW4gbWFnbml0dWRlIGlzIGFwcGxpZWQgdG8gdGhlIHBhcmVudCBhcyBhIHNpbmdsZVxuICogYXRvbWljIGluY3JlbWVudCAoYFNFVCBjb2wgPSBjb2wgKyBkZWx0YWApLCBhbmQgd2hlbiB0aGUgZm9yZWlnbiBrZXkgY2hhbmdlc1xuICogdGhlIG1hZ25pdHVkZSBpcyBtb3ZlZCBmcm9tIHRoZSBvbGQgcGFyZW50IHRvIHRoZSBuZXcgb25lLlxuICpcbiAqIEJlY2F1c2UgdGhlIGNvdW50ZXIgaXMgZGVyaXZlZCBmcm9tIHRoZSBzb3VyY2UgYXR0cmlidXRlIGFuZCBkaWZmZWQgb24gZXZlcnlcbiAqIHdyaXRlLCBpdCBmb2xsb3dzIHRoYXQgYXR0cmlidXRlIGF1dG9tYXRpY2FsbHkgbm8gbWF0dGVyIHdoaWNoIGNvZGUgcGF0aCB3cm90ZVxuICogaXQg4oCUIHRoZXJlIGlzIG5vIHBlci10cmFuc2l0aW9uIGluY3JlbWVudC9kZWNyZW1lbnQgdG8gZm9yZ2V0LiBUaGUgb2xkIHZhbHVlIGlzXG4gKiBjYXB0dXJlZCBpbiBgYmVmb3JlU2F2ZWAgKFZlbG9jaW91cyBjbGVhcnMgYGNoYW5nZXMoKWAgZHVyaW5nIHRoZSBwb3N0LXVwZGF0ZVxuICogcmVsb2FkKSBhbmQgY29uc3VtZWQgaW4gYGFmdGVyU2F2ZWAsIHNvIHRoZSBpbmNyZW1lbnQgY29tbWl0cyBhdG9taWNhbGx5IHdpdGhcbiAqIHRoZSByb3cgaXQgcmVmbGVjdHMuXG4gKiBAdHlwZWRlZiB7e1xuICogICBiZWxvbmdzVG86IHN0cmluZyxcbiAqICAgY291bnRlckNvbHVtbjogc3RyaW5nLFxuICogICBzb3VyY2VBdHRyaWJ1dGU6IHN0cmluZyxcbiAqICAgbWFnbml0dWRlOiAoc291cmNlVmFsdWU6ID8pID0+IG51bWJlclxuICogfX0gTWFnbml0dWRlQ291bnRlckNhY2hlRGVmaW5pdGlvblxuICovXG5cbi8qKlxuICogQ2FwdHVyZWQgcGVuZGluZyBtYWduaXR1ZGUgY2hhbmdlIHN0YXNoZWQgb24gYSByZWNvcmQgYmV0d2VlbiBiZWZvcmVTYXZlIGFuZCBhZnRlclNhdmUuXG4gKiBAdHlwZWRlZiB7e25ld01hZ25pdHVkZTogbnVtYmVyLCBvbGRNYWduaXR1ZGU6IG51bWJlciwgbmV3UGFyZW50SWQ6ID8sIG9sZFBhcmVudElkOiA/fX0gUGVuZGluZ01hZ25pdHVkZURlbHRhXG4gKi9cblxuLyoqXG4gKiBSZWdpc3RlcnMgYSByZWFjdGl2ZSBtYWduaXR1ZGUgY291bnRlci1jYWNoZSBvbiBhIG1vZGVsIGNsYXNzLlxuICogQHBhcmFtIHt0eXBlb2YgaW1wb3J0KFwiLi9pbmRleC5qc1wiKS5kZWZhdWx0fSBtb2RlbENsYXNzIC0gTW9kZWwgY2xhc3MgdG8gYWRkIHRoZSBjb3VudGVyIGNhY2hlIHRvLlxuICogQHBhcmFtIHtNYWduaXR1ZGVDb3VudGVyQ2FjaGVEZWZpbml0aW9ufSBkZWZpbml0aW9uIC0gQ291bnRlciBjYWNoZSBkZWZpbml0aW9uLlxuICogQHJldHVybnMge3ZvaWR9XG4gKi9cbmV4cG9ydCBmdW5jdGlvbiByZWdpc3Rlck1hZ25pdHVkZUNvdW50ZXJDYWNoZShtb2RlbENsYXNzLCBkZWZpbml0aW9uKSB7XG4gIC8qKlxuICAgKiBEeW5hbWljIGNsYXNzLlxuICAgKiBAdHlwZSB7P30gKi9cbiAgY29uc3QgZHluYW1pY0NsYXNzID0gbW9kZWxDbGFzc1xuICBjb25zdCByZWdpc3RlcmVkRmxhZyA9IGBfbWFnbml0dWRlQ291bnRlckNhY2hlUmVnaXN0ZXJlZF8ke2RlZmluaXRpb24uY291bnRlckNvbHVtbn1gXG5cbiAgLy8gSWRlbXBvdGVudCBwZXIgY291bnRlciBjb2x1bW46IHJlLWRlY2xhcmluZyAob3Igc3ViY2xhc3NpbmcpIG11c3Qgbm90IGRvdWJsZS1yZWdpc3Rlci5cbiAgaWYgKE9iamVjdC5wcm90b3R5cGUuaGFzT3duUHJvcGVydHkuY2FsbChkeW5hbWljQ2xhc3MsIHJlZ2lzdGVyZWRGbGFnKSAmJiBkeW5hbWljQ2xhc3NbcmVnaXN0ZXJlZEZsYWddKSB7XG4gICAgcmV0dXJuXG4gIH1cblxuICBkeW5hbWljQ2xhc3NbcmVnaXN0ZXJlZEZsYWddID0gdHJ1ZVxuXG4gIGNvbnN0IHBlbmRpbmdLZXkgPSBgX21hZ25pdHVkZUNvdW50ZXJDYWNoZVBlbmRpbmdfJHtkZWZpbml0aW9uLmNvdW50ZXJDb2x1bW59YFxuXG4gIG1vZGVsQ2xhc3MuYmVmb3JlU2F2ZShhc3luYyBmdW5jdGlvbiAocmVjb3JkKSB7XG4gICAgLyoqXG4gICAgICogRHluYW1pYyByZWNvcmQuXG4gICAgICogQHR5cGUgez99ICovXG4gICAgY29uc3QgZHluYW1pY1JlY29yZCA9IHJlY29yZFxuXG4gICAgZHluYW1pY1JlY29yZFtwZW5kaW5nS2V5XSA9IGNvbXB1dGVQZW5kaW5nTWFnbml0dWRlRGVsdGEocmVjb3JkLCBkZWZpbml0aW9uKVxuICB9KVxuXG4gIG1vZGVsQ2xhc3MuYWZ0ZXJTYXZlKGFzeW5jIGZ1bmN0aW9uIChyZWNvcmQpIHtcbiAgICAvKipcbiAgICAgKiBEeW5hbWljIHJlY29yZC5cbiAgICAgKiBAdHlwZSB7P30gKi9cbiAgICBjb25zdCBkeW5hbWljUmVjb3JkID0gcmVjb3JkXG4gICAgY29uc3QgcGVuZGluZyA9IC8qKiBAdHlwZSB7UGVuZGluZ01hZ25pdHVkZURlbHRhIHwgbnVsbH0gKi8gKGR5bmFtaWNSZWNvcmRbcGVuZGluZ0tleV0pXG5cbiAgICBkeW5hbWljUmVjb3JkW3BlbmRpbmdLZXldID0gbnVsbFxuXG4gICAgaWYgKHBlbmRpbmcpIHtcbiAgICAgIGF3YWl0IGFwcGx5UGVuZGluZ01hZ25pdHVkZURlbHRhKHJlY29yZCwgZGVmaW5pdGlvbiwgcGVuZGluZylcbiAgICB9XG4gIH0pXG5cbiAgbW9kZWxDbGFzcy5hZnRlckRlc3Ryb3koYXN5bmMgZnVuY3Rpb24gKHJlY29yZCkge1xuICAgIGNvbnN0IG1hZ25pdHVkZSA9IGRlZmluaXRpb24ubWFnbml0dWRlKHJlY29yZC5yZWFkQXR0cmlidXRlKGRlZmluaXRpb24uc291cmNlQXR0cmlidXRlKSlcbiAgICBjb25zdCBwYXJlbnRJZCA9IGN1cnJlbnRQYXJlbnRJZChyZWNvcmQsIGRlZmluaXRpb24pXG5cbiAgICBpZiAocGFyZW50SWQgJiYgbWFnbml0dWRlICE9PSAwKSB7XG4gICAgICBhd2FpdCBpbmNyZW1lbnRQYXJlbnRDb3VudGVyKHJlY29yZCwgZGVmaW5pdGlvbiwgcGFyZW50SWQsIC1tYWduaXR1ZGUpXG4gICAgfVxuICB9KVxufVxuXG4vKipcbiAqIERpZmZzIHRoZSBzb3VyY2UgYXR0cmlidXRlIChhbmQgZm9yZWlnbiBrZXkpIGFjcm9zcyB0aGUgcGVuZGluZyBzYXZlIHRvIGNhcHR1cmVcbiAqIGhvdyB0aGUgcGFyZW50IGNvdW50ZXIgc2hvdWxkIG1vdmUuIFJ1bnMgaW4gYmVmb3JlU2F2ZSwgd2hlcmUgYGNoYW5nZXMoKWAgc3RpbGxcbiAqIGhvbGRzIHRoZSBwcmUtc2F2ZSB2YWx1ZXMuXG4gKiBAcGFyYW0ge2ltcG9ydChcIi4vaW5kZXguanNcIikuZGVmYXVsdH0gcmVjb3JkIC0gUmVjb3JkIGJlaW5nIHNhdmVkLlxuICogQHBhcmFtIHtNYWduaXR1ZGVDb3VudGVyQ2FjaGVEZWZpbml0aW9ufSBkZWZpbml0aW9uIC0gQ291bnRlciBjYWNoZSBkZWZpbml0aW9uLlxuICogQHJldHVybnMge1BlbmRpbmdNYWduaXR1ZGVEZWx0YX0gVGhlIGNhcHR1cmVkIG1hZ25pdHVkZSBjaGFuZ2UuXG4gKi9cbmZ1bmN0aW9uIGNvbXB1dGVQZW5kaW5nTWFnbml0dWRlRGVsdGEocmVjb3JkLCBkZWZpbml0aW9uKSB7XG4gIGNvbnN0IGNoYW5nZXMgPSByZWNvcmQuY2hhbmdlcygpXG4gIGNvbnN0IHNvdXJjZUNvbHVtbiA9IGNvbHVtbk5hbWVGb3JBdHRyaWJ1dGUocmVjb3JkLCBkZWZpbml0aW9uLnNvdXJjZUF0dHJpYnV0ZSlcbiAgY29uc3QgZm9yZWlnbktleUNvbHVtbiA9IHJlY29yZC5nZXRNb2RlbENsYXNzKCkuZ2V0UmVsYXRpb25zaGlwQnlOYW1lKGRlZmluaXRpb24uYmVsb25nc1RvKS5nZXRGb3JlaWduS2V5KClcblxuICBjb25zdCBuZXdTb3VyY2UgPSByZWNvcmQucmVhZEF0dHJpYnV0ZShkZWZpbml0aW9uLnNvdXJjZUF0dHJpYnV0ZSlcbiAgY29uc3Qgb2xkU291cmNlID0gb2xkU291cmNlVmFsdWVUaHJvdWdoUmVhZENhc3QocmVjb3JkLCBkZWZpbml0aW9uLnNvdXJjZUF0dHJpYnV0ZSwgc291cmNlQ29sdW1uLCBjaGFuZ2VzLCBuZXdTb3VyY2UpXG5cbiAgY29uc3QgbmV3UGFyZW50SWQgPSBjdXJyZW50UGFyZW50SWQocmVjb3JkLCBkZWZpbml0aW9uKVxuICBjb25zdCBvbGRQYXJlbnRJZCA9IGZvcmVpZ25LZXlDb2x1bW4gaW4gY2hhbmdlcyA/IGNoYW5nZXNbZm9yZWlnbktleUNvbHVtbl1bMF0gOiBuZXdQYXJlbnRJZFxuXG4gIHJldHVybiB7XG4gICAgbmV3TWFnbml0dWRlOiBkZWZpbml0aW9uLm1hZ25pdHVkZShuZXdTb3VyY2UpLFxuICAgIG9sZE1hZ25pdHVkZTogZGVmaW5pdGlvbi5tYWduaXR1ZGUob2xkU291cmNlKSxcbiAgICBuZXdQYXJlbnRJZCxcbiAgICBvbGRQYXJlbnRJZFxuICB9XG59XG5cbi8qKlxuICogQXBwbGllcyBhIGNhcHR1cmVkIG1hZ25pdHVkZSBjaGFuZ2UgdG8gdGhlIHBhcmVudCBjb3VudGVyKHMpIGFzIGF0b21pYyBpbmNyZW1lbnRzLlxuICogQHBhcmFtIHtpbXBvcnQoXCIuL2luZGV4LmpzXCIpLmRlZmF1bHR9IHJlY29yZCAtIFJlY29yZCB0aGF0IHdhcyBzYXZlZC5cbiAqIEBwYXJhbSB7TWFnbml0dWRlQ291bnRlckNhY2hlRGVmaW5pdGlvbn0gZGVmaW5pdGlvbiAtIENvdW50ZXIgY2FjaGUgZGVmaW5pdGlvbi5cbiAqIEBwYXJhbSB7UGVuZGluZ01hZ25pdHVkZURlbHRhfSBwZW5kaW5nIC0gVGhlIGNhcHR1cmVkIG1hZ25pdHVkZSBjaGFuZ2UuXG4gKiBAcmV0dXJucyB7UHJvbWlzZTx2b2lkPn1cbiAqL1xuYXN5bmMgZnVuY3Rpb24gYXBwbHlQZW5kaW5nTWFnbml0dWRlRGVsdGEocmVjb3JkLCBkZWZpbml0aW9uLCBwZW5kaW5nKSB7XG4gIGNvbnN0IHtuZXdNYWduaXR1ZGUsIG9sZE1hZ25pdHVkZSwgbmV3UGFyZW50SWQsIG9sZFBhcmVudElkfSA9IHBlbmRpbmdcblxuICBpZiAob2xkUGFyZW50SWQgPT09IG5ld1BhcmVudElkKSB7XG4gICAgY29uc3QgZGVsdGEgPSBuZXdNYWduaXR1ZGUgLSBvbGRNYWduaXR1ZGVcblxuICAgIGlmIChuZXdQYXJlbnRJZCAmJiBkZWx0YSAhPT0gMCkge1xuICAgICAgYXdhaXQgaW5jcmVtZW50UGFyZW50Q291bnRlcihyZWNvcmQsIGRlZmluaXRpb24sIG5ld1BhcmVudElkLCBkZWx0YSlcbiAgICB9XG5cbiAgICByZXR1cm5cbiAgfVxuXG4gIC8vIFRoZSBmb3JlaWduIGtleSBtb3ZlZDogdGFrZSB0aGUgb2xkIG1hZ25pdHVkZSBvZmYgdGhlIG9sZCBwYXJlbnQgYW5kIHB1dCB0aGVcbiAgLy8gbmV3IG1hZ25pdHVkZSBvbiB0aGUgbmV3IHBhcmVudC5cbiAgaWYgKG9sZFBhcmVudElkICYmIG9sZE1hZ25pdHVkZSAhPT0gMCkge1xuICAgIGF3YWl0IGluY3JlbWVudFBhcmVudENvdW50ZXIocmVjb3JkLCBkZWZpbml0aW9uLCBvbGRQYXJlbnRJZCwgLW9sZE1hZ25pdHVkZSlcbiAgfVxuXG4gIGlmIChuZXdQYXJlbnRJZCAmJiBuZXdNYWduaXR1ZGUgIT09IDApIHtcbiAgICBhd2FpdCBpbmNyZW1lbnRQYXJlbnRDb3VudGVyKHJlY29yZCwgZGVmaW5pdGlvbiwgbmV3UGFyZW50SWQsIG5ld01hZ25pdHVkZSlcbiAgfVxufVxuXG4vKipcbiAqIFJlYWRzIHRoZSBwcmUtc2F2ZSB2YWx1ZSBvZiB0aGUgc291cmNlIGF0dHJpYnV0ZSB0aHJvdWdoIHRoZSBub3JtYWwgcmVhZC9jYXN0XG4gKiBwYXRoLCBzbyBgbWFnbml0dWRlYCByZWNlaXZlcyB0aGUgb2xkIHZhbHVlIGluIHRoZSBzYW1lIHNoYXBlIGFzIHRoZSBuZXcgb25lXG4gKiAoZS5nLiBhIGRlY2xhcmVkIGJvb2xlYW4gYXMgYHRydWVgL2BmYWxzZWAsIG5vdCB0aGUgcmF3IHN0b3JlZCBgMWAvYDBgKS4gRHJvcHNcbiAqIHRoZSBwZW5kaW5nIGNoYW5nZSBzbyBgcmVhZEF0dHJpYnV0ZWAgZmFsbHMgYmFjayB0byB0aGUgY29tbWl0dGVkIHZhbHVlIGFuZFxuICogYXBwbGllcyB0aGUgc2FtZSBjYXN0IGEgZnJlc2ggcmVhZCB3b3VsZCwgdGhlbiByZXN0b3JlcyB0aGUgcGVuZGluZyBjaGFuZ2UuXG4gKiBAcGFyYW0ge2ltcG9ydChcIi4vaW5kZXguanNcIikuZGVmYXVsdH0gcmVjb3JkIC0gUmVjb3JkIGJlaW5nIHNhdmVkLlxuICogQHBhcmFtIHtzdHJpbmd9IHNvdXJjZUF0dHJpYnV0ZSAtIFNvdXJjZSBhdHRyaWJ1dGUgbmFtZS5cbiAqIEBwYXJhbSB7c3RyaW5nfSBzb3VyY2VDb2x1bW4gLSBTb3VyY2UgY29sdW1uIG5hbWUuXG4gKiBAcGFyYW0ge1JlY29yZDxzdHJpbmcsID8+fSBjaGFuZ2VzIC0gVGhlIHJlY29yZCdzIHByZS1zYXZlIGNoYW5nZXMgKGNvbHVtbi1rZXllZCkuXG4gKiBAcGFyYW0gez99IGN1cnJlbnRWYWx1ZSAtIFRoZSBjdXJyZW50IChuZXcpIHJlYWQgdmFsdWUsIHJldHVybmVkIHdoZW4gdGhlIHNvdXJjZSBkaWQgbm90IGNoYW5nZS5cbiAqIEByZXR1cm5zIHs/fSBUaGUgcmVhZC1jYXN0IHByZS1zYXZlIHZhbHVlLlxuICovXG5mdW5jdGlvbiBvbGRTb3VyY2VWYWx1ZVRocm91Z2hSZWFkQ2FzdChyZWNvcmQsIHNvdXJjZUF0dHJpYnV0ZSwgc291cmNlQ29sdW1uLCBjaGFuZ2VzLCBjdXJyZW50VmFsdWUpIHtcbiAgaWYgKCEoc291cmNlQ29sdW1uIGluIGNoYW5nZXMpKSB7XG4gICAgcmV0dXJuIGN1cnJlbnRWYWx1ZVxuICB9XG5cbiAgLyoqXG4gICAqIER5bmFtaWMgcmVjb3JkLlxuICAgKiBAdHlwZSB7P30gKi9cbiAgY29uc3QgZHluYW1pY1JlY29yZCA9IHJlY29yZFxuICBjb25zdCBwZW5kaW5nQ2hhbmdlID0gZHluYW1pY1JlY29yZC5fY2hhbmdlc1tzb3VyY2VDb2x1bW5dXG5cbiAgZGVsZXRlIGR5bmFtaWNSZWNvcmQuX2NoYW5nZXNbc291cmNlQ29sdW1uXVxuXG4gIHRyeSB7XG4gICAgcmV0dXJuIHJlY29yZC5yZWFkQXR0cmlidXRlKHNvdXJjZUF0dHJpYnV0ZSlcbiAgfSBmaW5hbGx5IHtcbiAgICBkeW5hbWljUmVjb3JkLl9jaGFuZ2VzW3NvdXJjZUNvbHVtbl0gPSBwZW5kaW5nQ2hhbmdlXG4gIH1cbn1cblxuLyoqXG4gKiBSZWFkcyB0aGUgcmVjb3JkJ3MgY3VycmVudCBmb3JlaWduLWtleSB2YWx1ZSBmb3IgdGhlIGNvdW50ZXItY2FjaGUgcGFyZW50LlxuICogQHBhcmFtIHtpbXBvcnQoXCIuL2luZGV4LmpzXCIpLmRlZmF1bHR9IHJlY29yZCAtIFJlY29yZCB3aG9zZSBwYXJlbnQgaXMgdGFyZ2V0ZWQuXG4gKiBAcGFyYW0ge01hZ25pdHVkZUNvdW50ZXJDYWNoZURlZmluaXRpb259IGRlZmluaXRpb24gLSBDb3VudGVyIGNhY2hlIGRlZmluaXRpb24uXG4gKiBAcmV0dXJucyB7P30gVGhlIGN1cnJlbnQgZm9yZWlnbi1rZXkgdmFsdWUuXG4gKi9cbmZ1bmN0aW9uIGN1cnJlbnRQYXJlbnRJZChyZWNvcmQsIGRlZmluaXRpb24pIHtcbiAgY29uc3QgZm9yZWlnbktleUNvbHVtbiA9IHJlY29yZC5nZXRNb2RlbENsYXNzKCkuZ2V0UmVsYXRpb25zaGlwQnlOYW1lKGRlZmluaXRpb24uYmVsb25nc1RvKS5nZXRGb3JlaWduS2V5KClcblxuICByZXR1cm4gcmVjb3JkLnJlYWRBdHRyaWJ1dGUoYXR0cmlidXRlTmFtZUZvckNvbHVtbihyZWNvcmQsIGZvcmVpZ25LZXlDb2x1bW4pKVxufVxuXG4vKipcbiAqIEF0b21pY2FsbHkgYWRkcyBgYW1vdW50YCB0byB0aGUgcGFyZW50J3MgY291bnRlciBjb2x1bW4gZm9yIG9uZSBwYXJlbnQgcm93LlxuICogQHBhcmFtIHtpbXBvcnQoXCIuL2luZGV4LmpzXCIpLmRlZmF1bHR9IHJlY29yZCAtIENoaWxkIHJlY29yZCAoZm9yIGNvbm5lY3Rpb24gKyByZWxhdGlvbnNoaXAgbWV0YWRhdGEpLlxuICogQHBhcmFtIHtNYWduaXR1ZGVDb3VudGVyQ2FjaGVEZWZpbml0aW9ufSBkZWZpbml0aW9uIC0gQ291bnRlciBjYWNoZSBkZWZpbml0aW9uLlxuICogQHBhcmFtIHs/fSBwYXJlbnRJZCAtIFBhcmVudCBwcmltYXJ5LWtleSB2YWx1ZS5cbiAqIEBwYXJhbSB7bnVtYmVyfSBhbW91bnQgLSBTaWduZWQgaW50ZWdlciB0byBhZGQuXG4gKiBAcmV0dXJucyB7UHJvbWlzZTx2b2lkPn1cbiAqL1xuYXN5bmMgZnVuY3Rpb24gaW5jcmVtZW50UGFyZW50Q291bnRlcihyZWNvcmQsIGRlZmluaXRpb24sIHBhcmVudElkLCBhbW91bnQpIHtcbiAgY29uc3QgbW9kZWxDbGFzcyA9IHJlY29yZC5nZXRNb2RlbENsYXNzKClcbiAgY29uc3QgcmVsYXRpb25zaGlwID0gbW9kZWxDbGFzcy5nZXRSZWxhdGlvbnNoaXBCeU5hbWUoZGVmaW5pdGlvbi5iZWxvbmdzVG8pXG4gIGNvbnN0IHBhcmVudE1vZGVsQ2xhc3MgPSByZWxhdGlvbnNoaXAuZ2V0VGFyZ2V0TW9kZWxDbGFzcygpXG5cbiAgaWYgKCFwYXJlbnRNb2RlbENsYXNzKSB7XG4gICAgdGhyb3cgbmV3IEVycm9yKGBtYWduaXR1ZGVDb3VudGVyQ2FjaGUgb24gJHttb2RlbENsYXNzLmdldE1vZGVsTmFtZSgpfSBjb3VsZCBub3QgcmVzb2x2ZSB0aGUgXCIke2RlZmluaXRpb24uYmVsb25nc1RvfVwiIHBhcmVudCBtb2RlbCBjbGFzc2ApXG4gIH1cblxuICAvLyBVcGRhdGUgdGhyb3VnaCB0aGUgUEFSRU5UIG1vZGVsJ3MgY29ubmVjdGlvbjogdGhlIHJvdyBiZWluZyBtb2RpZmllZCBiZWxvbmdzXG4gIC8vIHRvIHRoZSBwYXJlbnQsIHdoaWNoIG1heSBsaXZlIG9uIGEgZGlmZmVyZW50IGRhdGFiYXNlL3RlbmFudCB0aGFuIHRoZSBjaGlsZC5cbiAgY29uc3QgZGIgPSBwYXJlbnRNb2RlbENsYXNzLmNvbm5lY3Rpb24oKVxuICBjb25zdCBjb3VudGVyQ29sdW1uU3FsID0gZGIucXVvdGVDb2x1bW4oZGVmaW5pdGlvbi5jb3VudGVyQ29sdW1uKVxuICBjb25zdCB0cnVuY2F0ZWRBbW91bnQgPSBNYXRoLnRydW5jKGFtb3VudClcblxuICBhd2FpdCBkYi5xdWVyeShcbiAgICBgVVBEQVRFICR7ZGIucXVvdGVUYWJsZShwYXJlbnRNb2RlbENsYXNzLnRhYmxlTmFtZSgpKX0gYCArXG4gICAgYFNFVCAke2NvdW50ZXJDb2x1bW5TcWx9ID0gQ09BTEVTQ0UoJHtjb3VudGVyQ29sdW1uU3FsfSwgMCkgKyAke3RydW5jYXRlZEFtb3VudH0gYCArXG4gICAgYFdIRVJFICR7ZGIucXVvdGVDb2x1bW4ocmVsYXRpb25zaGlwLmdldFByaW1hcnlLZXkoKSl9ID0gJHtkYi5xdW90ZShwYXJlbnRJZCl9YFxuICApXG59XG5cbi8qKlxuICogUmVzb2x2ZXMgdGhlIGNvbHVtbiBuYW1lIGJhY2tpbmcgYW4gYXR0cmlidXRlIG5hbWUuXG4gKiBAcGFyYW0ge2ltcG9ydChcIi4vaW5kZXguanNcIikuZGVmYXVsdH0gcmVjb3JkIC0gUmVjb3JkLlxuICogQHBhcmFtIHtzdHJpbmd9IGF0dHJpYnV0ZU5hbWUgLSBBdHRyaWJ1dGUgbmFtZS5cbiAqIEByZXR1cm5zIHtzdHJpbmd9IFRoZSBjb2x1bW4gbmFtZSBmb3IgdGhlIGF0dHJpYnV0ZSAoZmFsbHMgYmFjayB0byB0aGUgYXR0cmlidXRlIG5hbWUpLlxuICovXG5mdW5jdGlvbiBjb2x1bW5OYW1lRm9yQXR0cmlidXRlKHJlY29yZCwgYXR0cmlidXRlTmFtZSkge1xuICByZXR1cm4gcmVjb3JkLmdldE1vZGVsQ2xhc3MoKS5nZXRBdHRyaWJ1dGVOYW1lVG9Db2x1bW5OYW1lTWFwKClbYXR0cmlidXRlTmFtZV0gfHwgYXR0cmlidXRlTmFtZVxufVxuXG4vKipcbiAqIFJlc29sdmVzIHRoZSBhdHRyaWJ1dGUgbmFtZSBiYWNraW5nIGEgY29sdW1uIG5hbWUuXG4gKiBAcGFyYW0ge2ltcG9ydChcIi4vaW5kZXguanNcIikuZGVmYXVsdH0gcmVjb3JkIC0gUmVjb3JkLlxuICogQHBhcmFtIHtzdHJpbmd9IGNvbHVtbk5hbWUgLSBDb2x1bW4gbmFtZS5cbiAqIEByZXR1cm5zIHtzdHJpbmd9IFRoZSBhdHRyaWJ1dGUgbmFtZSBmb3IgdGhlIGNvbHVtbiAoZmFsbHMgYmFjayB0byB0aGUgY29sdW1uIG5hbWUpLlxuICovXG5mdW5jdGlvbiBhdHRyaWJ1dGVOYW1lRm9yQ29sdW1uKHJlY29yZCwgY29sdW1uTmFtZSkge1xuICBjb25zdCBtYXAgPSByZWNvcmQuZ2V0TW9kZWxDbGFzcygpLmdldEF0dHJpYnV0ZU5hbWVUb0NvbHVtbk5hbWVNYXAoKVxuXG4gIHJldHVybiBPYmplY3Qua2V5cyhtYXApLmZpbmQoKGF0dHJpYnV0ZU5hbWUpID0+IG1hcFthdHRyaWJ1dGVOYW1lXSA9PT0gY29sdW1uTmFtZSkgfHwgY29sdW1uTmFtZVxufVxuIl19
|