velocious 1.0.500 → 1.0.501

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,125 @@
1
+ /** @typedef {import("../configuration-types.js").VelociousSyncRealtimeChannelDescriptor} VelociousSyncRealtimeChannelDescriptor */
2
+ /** @typedef {import("../configuration-types.js").VelociousSyncRealtimeWebsocketClient} VelociousSyncRealtimeWebsocketClient */
3
+ /**
4
+ * Derived realtime push bridge for the sync client. Subscribes the declared
5
+ * websocket channels (config `sync.client.realtime.channels` callback plus
6
+ * model-level `static sync = {realtime: {channel}}` declarations), applies
7
+ * pushed changes through the same derived resource applier as pulls (with echo
8
+ * suppression against tracked re-queueing), drops own-device messages by echo
9
+ * origin, and fires a coalesced `pull()` when subscriptions become ready or
10
+ * resume so offline gaps close.
11
+ */
12
+ export default class SyncRealtimeBridge {
13
+ /**
14
+ * Builds the bridge for a sync client.
15
+ * @param {{syncClient: import("./sync-client.js").default}} args - Bridge args.
16
+ */
17
+ constructor({ syncClient }: {
18
+ syncClient: import("./sync-client.js").default;
19
+ });
20
+ syncClient: import("./sync-client.js").default;
21
+ /** @type {Array<{channel: string, resourceType: string | null, subscription: import("../configuration-types.js").VelociousSyncRealtimeSubscription}>} */
22
+ _channels: Array<{
23
+ channel: string;
24
+ resourceType: string | null;
25
+ subscription: import("../configuration-types.js").VelociousSyncRealtimeSubscription;
26
+ }>;
27
+ /** @type {VelociousSyncRealtimeWebsocketClient | null} */
28
+ _client: VelociousSyncRealtimeWebsocketClient | null;
29
+ /** @type {Promise<void>} */
30
+ _applyPromise: Promise<void>;
31
+ /** @type {number} Subscription generation - bumped by unsubscribe so in-flight subscribes detect they became stale. */
32
+ _generation: number;
33
+ /** @type {Promise<void> | null} */
34
+ _scheduledPull: Promise<void> | null;
35
+ /** @type {Promise<void> | null} */
36
+ _subscribePromise: Promise<void> | null;
37
+ /** @type {"subscribed" | "subscribing" | "unsubscribed"} */
38
+ _state: "subscribed" | "subscribing" | "unsubscribed";
39
+ /**
40
+ * Subscribes the derived realtime channels (idempotent and single-flighted):
41
+ * an active subscription is kept as-is and a concurrent subscribe awaits the
42
+ * in-flight attempt. Call `unsubscribe()` first to change the context.
43
+ * @param {?} [context] - App context passed to the `sync.client.realtime.channels` callback (runtime values like eventId).
44
+ * @returns {Promise<void>}
45
+ */
46
+ subscribe(context?: unknown): Promise<void>;
47
+ /**
48
+ * Connects the websocket client, subscribes every derived channel, and waits
49
+ * for each subscription's server acknowledgement before the gap-closing pull,
50
+ * so no change can land between the pull and the subscriptions going live.
51
+ * Locally created resources are only promoted to the bridge once everything
52
+ * is live; an unsubscribe arriving during any await marks this attempt stale
53
+ * and it tears its own resources down instead of resubscribing.
54
+ * @param {?} context - App context passed to the channels callback.
55
+ * @returns {Promise<void>}
56
+ */
57
+ _subscribe(context: unknown): Promise<void>;
58
+ /**
59
+ * Closes every channel subscription and disconnects the websocket client
60
+ * (idempotent). Also marks any in-flight subscribe attempt stale so it tears
61
+ * itself down instead of finishing the subscription afterwards.
62
+ * @returns {Promise<void>}
63
+ */
64
+ unsubscribe(): Promise<void>;
65
+ /**
66
+ * Reports the bridge subscription state and per-channel readiness.
67
+ * @returns {{channels: Array<{channel: string, ready: boolean, resourceType: string | null}>, state: "subscribed" | "subscribing" | "unsubscribed"}} Realtime status.
68
+ */
69
+ status(): {
70
+ channels: Array<{
71
+ channel: string;
72
+ ready: boolean;
73
+ resourceType: string | null;
74
+ }>;
75
+ state: "subscribed" | "subscribing" | "unsubscribed";
76
+ };
77
+ /**
78
+ * Awaits all enqueued message applies and any scheduled pull (tests, shutdown flows).
79
+ * @returns {Promise<void>}
80
+ */
81
+ waitForApplied(): Promise<void>;
82
+ /**
83
+ * Resolves the realtime configuration, failing loudly when it is missing.
84
+ * @returns {import("../configuration-types.js").VelociousSyncClientRealtimeConfiguration} Realtime configuration.
85
+ */
86
+ realtimeConfiguration(): import("../configuration-types.js").VelociousSyncClientRealtimeConfiguration;
87
+ /**
88
+ * Derives the channel descriptors to subscribe: model-level static realtime
89
+ * declarations plus the config channels callback, failing loudly when none exist.
90
+ * @param {?} context - App context passed to the channels callback.
91
+ * @returns {Promise<Array<VelociousSyncRealtimeChannelDescriptor>>} Channel descriptors.
92
+ */
93
+ channelDescriptors(context: unknown): Promise<Array<VelociousSyncRealtimeChannelDescriptor>>;
94
+ /**
95
+ * Chains one pushed message onto the serialized apply queue so changes apply
96
+ * in arrival order; failures go to the sync client's error reporting.
97
+ * @param {{body: ?, resourceType: string | null}} args - Message args.
98
+ * @returns {void}
99
+ */
100
+ enqueueApply({ body, resourceType }: {
101
+ body: unknown;
102
+ resourceType: string | null;
103
+ }): void;
104
+ /**
105
+ * Applies one pushed message through the derived resource applier: drops
106
+ * own-device messages by echo origin, defaults the channel's resourceType onto
107
+ * envelopes without one, and fails loudly on unknown resource types.
108
+ * @param {{body: ?, resourceType: string | null}} args - Message args.
109
+ * @returns {Promise<void>}
110
+ */
111
+ applyMessage({ body, resourceType }: {
112
+ body: unknown;
113
+ resourceType: string | null;
114
+ }): Promise<void>;
115
+ /**
116
+ * Schedules a coalesced background pull closing offline gaps after
117
+ * (re)subscription readiness. Resumes arriving while a pull is already
118
+ * scheduled or in flight coalesce into that pull instead of stacking.
119
+ * @returns {void}
120
+ */
121
+ schedulePull(): void;
122
+ }
123
+ export type VelociousSyncRealtimeChannelDescriptor = import("../configuration-types.js").VelociousSyncRealtimeChannelDescriptor;
124
+ export type VelociousSyncRealtimeWebsocketClient = import("../configuration-types.js").VelociousSyncRealtimeWebsocketClient;
125
+ //# sourceMappingURL=sync-realtime-bridge.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sync-realtime-bridge.d.ts","sourceRoot":"","sources":["../../../src/sync/sync-realtime-bridge.js"],"names":[],"mappings":"AAIA,mIAAmI;AACnI,+HAA+H;AAE/H;;;;;;;;GAQG;AACH;IACE;;;OAGG;IACH,4BAFW;QAAC,UAAU,EAAE,OAAO,kBAAkB,EAAE,OAAO,CAAA;KAAC,EAkB1D;IAfC,+CAA4B;IAC5B,yJAAyJ;IACzJ,WADW,KAAK,CAAC;QAAC,OAAO,EAAE,MAAM,CAAC;QAAC,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;QAAC,YAAY,EAAE,OAAO,2BAA2B,EAAE,iCAAiC,CAAA;KAAC,CAAC,CAClI;IACnB,0DAA0D;IAC1D,SADW,oCAAoC,GAAG,IAAI,CACnC;IACnB,4BAA4B;IAC5B,eADW,OAAO,CAAC,IAAI,CAAC,CACc;IACtC,uHAAuH;IACvH,aADW,MAAM,CACG;IACpB,mCAAmC;IACnC,gBADW,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CACL;IAC1B,mCAAmC;IACnC,mBADW,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CACF;IAC7B,4DAA4D;IAC5D,QADW,YAAY,GAAG,aAAa,GAAG,cAAc,CAC5B;IAG9B;;;;;;OAMG;IACH,oBAHW,OAAC,GACC,OAAO,CAAC,IAAI,CAAC,CAYzB;IAED;;;;;;;;;OASG;IACH,oBAHW,OAAC,GACC,OAAO,CAAC,IAAI,CAAC,CAgFzB;IAED;;;;;OAKG;IACH,eAFa,OAAO,CAAC,IAAI,CAAC,CAiBzB;IAED;;;OAGG;IACH,UAFa;QAAC,QAAQ,EAAE,KAAK,CAAC;YAAC,OAAO,EAAE,MAAM,CAAC;YAAC,KAAK,EAAE,OAAO,CAAC;YAAC,YAAY,EAAE,MAAM,GAAG,IAAI,CAAA;SAAC,CAAC,CAAC;QAAC,KAAK,EAAE,YAAY,GAAG,aAAa,GAAG,cAAc,CAAA;KAAC,CAOnJ;IAED;;;OAGG;IACH,kBAFa,OAAO,CAAC,IAAI,CAAC,CAKzB;IAED;;;OAGG;IACH,yBAFa,OAAO,2BAA2B,EAAE,wCAAwC,CAUxF;IAED;;;;;OAKG;IACH,4BAHW,OAAC,GACC,OAAO,CAAC,KAAK,CAAC,sCAAsC,CAAC,CAAC,CAsBlE;IAED;;;;;OAKG;IACH,qCAHW;QAAC,IAAI,EAAE,OAAC,CAAC;QAAC,YAAY,EAAE,MAAM,GAAG,IAAI,CAAA;KAAC,GACpC,IAAI,CAUhB;IAED;;;;;;OAMG;IACH,qCAHW;QAAC,IAAI,EAAE,OAAC,CAAC;QAAC,YAAY,EAAE,MAAM,GAAG,IAAI,CAAA;KAAC,GACpC,OAAO,CAAC,IAAI,CAAC,CAuBzB;IAED;;;;;OAKG;IACH,gBAFa,IAAI,CAchB;CACF;qDAtSa,OAAO,2BAA2B,EAAE,sCAAsC;mDAC1E,OAAO,2BAA2B,EAAE,oCAAoC"}
@@ -0,0 +1,262 @@
1
+ // @ts-check
2
+ import SyncApiClient from "./sync-api-client.js";
3
+ /** @typedef {import("../configuration-types.js").VelociousSyncRealtimeChannelDescriptor} VelociousSyncRealtimeChannelDescriptor */
4
+ /** @typedef {import("../configuration-types.js").VelociousSyncRealtimeWebsocketClient} VelociousSyncRealtimeWebsocketClient */
5
+ /**
6
+ * Derived realtime push bridge for the sync client. Subscribes the declared
7
+ * websocket channels (config `sync.client.realtime.channels` callback plus
8
+ * model-level `static sync = {realtime: {channel}}` declarations), applies
9
+ * pushed changes through the same derived resource applier as pulls (with echo
10
+ * suppression against tracked re-queueing), drops own-device messages by echo
11
+ * origin, and fires a coalesced `pull()` when subscriptions become ready or
12
+ * resume so offline gaps close.
13
+ */
14
+ export default class SyncRealtimeBridge {
15
+ /**
16
+ * Builds the bridge for a sync client.
17
+ * @param {{syncClient: import("./sync-client.js").default}} args - Bridge args.
18
+ */
19
+ constructor({ syncClient }) {
20
+ this.syncClient = syncClient;
21
+ /** @type {Array<{channel: string, resourceType: string | null, subscription: import("../configuration-types.js").VelociousSyncRealtimeSubscription}>} */
22
+ this._channels = [];
23
+ /** @type {VelociousSyncRealtimeWebsocketClient | null} */
24
+ this._client = null;
25
+ /** @type {Promise<void>} */
26
+ this._applyPromise = Promise.resolve();
27
+ /** @type {number} Subscription generation - bumped by unsubscribe so in-flight subscribes detect they became stale. */
28
+ this._generation = 0;
29
+ /** @type {Promise<void> | null} */
30
+ this._scheduledPull = null;
31
+ /** @type {Promise<void> | null} */
32
+ this._subscribePromise = null;
33
+ /** @type {"subscribed" | "subscribing" | "unsubscribed"} */
34
+ this._state = "unsubscribed";
35
+ }
36
+ /**
37
+ * Subscribes the derived realtime channels (idempotent and single-flighted):
38
+ * an active subscription is kept as-is and a concurrent subscribe awaits the
39
+ * in-flight attempt. Call `unsubscribe()` first to change the context.
40
+ * @param {?} [context] - App context passed to the `sync.client.realtime.channels` callback (runtime values like eventId).
41
+ * @returns {Promise<void>}
42
+ */
43
+ async subscribe(context) {
44
+ if (this._state === "subscribed")
45
+ return;
46
+ if (!this._subscribePromise) {
47
+ this._subscribePromise = this._subscribe(context).finally(() => {
48
+ this._subscribePromise = null;
49
+ });
50
+ }
51
+ await this._subscribePromise;
52
+ }
53
+ /**
54
+ * Connects the websocket client, subscribes every derived channel, and waits
55
+ * for each subscription's server acknowledgement before the gap-closing pull,
56
+ * so no change can land between the pull and the subscriptions going live.
57
+ * Locally created resources are only promoted to the bridge once everything
58
+ * is live; an unsubscribe arriving during any await marks this attempt stale
59
+ * and it tears its own resources down instead of resubscribing.
60
+ * @param {?} context - App context passed to the channels callback.
61
+ * @returns {Promise<void>}
62
+ */
63
+ async _subscribe(context) {
64
+ const generation = this._generation;
65
+ this._state = "subscribing";
66
+ /** @type {Array<{channel: string, resourceType: string | null, subscription: import("../configuration-types.js").VelociousSyncRealtimeSubscription}>} */
67
+ const channels = [];
68
+ /** @type {VelociousSyncRealtimeWebsocketClient | null} */
69
+ let client = null;
70
+ /**
71
+ * Tears down everything this stale/failed subscribe attempt created itself.
72
+ * @returns {Promise<void>}
73
+ */
74
+ const teardown = async () => {
75
+ for (const { subscription } of channels) {
76
+ subscription.close();
77
+ }
78
+ if (client)
79
+ await client.disconnectAndStopReconnect();
80
+ };
81
+ try {
82
+ const realtime = this.realtimeConfiguration();
83
+ const channelDescriptors = await this.channelDescriptors(context);
84
+ if (generation !== this._generation)
85
+ return;
86
+ client = await realtime.createClient();
87
+ if (generation !== this._generation)
88
+ return;
89
+ await client.connect();
90
+ if (generation !== this._generation) {
91
+ await teardown();
92
+ return;
93
+ }
94
+ const authenticationToken = await this.syncClient.config.authenticationToken();
95
+ if (generation !== this._generation) {
96
+ await teardown();
97
+ return;
98
+ }
99
+ for (const channelDescriptor of channelDescriptors) {
100
+ if (channelDescriptor.params && "authenticationToken" in channelDescriptor.params) {
101
+ throw new Error(`Realtime channel "${channelDescriptor.channel}" params must not include authenticationToken - the framework injects the sync.client authenticationToken automatically`);
102
+ }
103
+ const resourceType = channelDescriptor.resourceType ?? null;
104
+ const subscription = client.subscribeChannel(channelDescriptor.channel, {
105
+ onMessage: (body) => this.enqueueApply({ body, resourceType }),
106
+ onResume: () => this.schedulePull(),
107
+ params: { ...channelDescriptor.params, authenticationToken }
108
+ });
109
+ channels.push({ channel: channelDescriptor.channel, resourceType, subscription });
110
+ }
111
+ await Promise.all(channels.map(({ subscription }) => subscription.waitForReady()));
112
+ if (generation !== this._generation) {
113
+ await teardown();
114
+ return;
115
+ }
116
+ this._channels = channels;
117
+ this._client = client;
118
+ this._state = "subscribed";
119
+ this.schedulePull();
120
+ }
121
+ catch (error) {
122
+ await teardown();
123
+ if (generation === this._generation)
124
+ this._state = "unsubscribed";
125
+ throw error;
126
+ }
127
+ }
128
+ /**
129
+ * Closes every channel subscription and disconnects the websocket client
130
+ * (idempotent). Also marks any in-flight subscribe attempt stale so it tears
131
+ * itself down instead of finishing the subscription afterwards.
132
+ * @returns {Promise<void>}
133
+ */
134
+ async unsubscribe() {
135
+ this._generation += 1;
136
+ const channels = this._channels;
137
+ const client = this._client;
138
+ this._channels = [];
139
+ this._client = null;
140
+ this._state = "unsubscribed";
141
+ for (const { subscription } of channels) {
142
+ subscription.close();
143
+ }
144
+ if (client)
145
+ await client.disconnectAndStopReconnect();
146
+ }
147
+ /**
148
+ * Reports the bridge subscription state and per-channel readiness.
149
+ * @returns {{channels: Array<{channel: string, ready: boolean, resourceType: string | null}>, state: "subscribed" | "subscribing" | "unsubscribed"}} Realtime status.
150
+ */
151
+ status() {
152
+ return {
153
+ channels: this._channels.map(({ channel, resourceType, subscription }) => ({ channel, ready: subscription.isReady(), resourceType })),
154
+ state: this._state
155
+ };
156
+ }
157
+ /**
158
+ * Awaits all enqueued message applies and any scheduled pull (tests, shutdown flows).
159
+ * @returns {Promise<void>}
160
+ */
161
+ async waitForApplied() {
162
+ await this._applyPromise;
163
+ if (this._scheduledPull)
164
+ await this._scheduledPull;
165
+ }
166
+ /**
167
+ * Resolves the realtime configuration, failing loudly when it is missing.
168
+ * @returns {import("../configuration-types.js").VelociousSyncClientRealtimeConfiguration} Realtime configuration.
169
+ */
170
+ realtimeConfiguration() {
171
+ const realtime = this.syncClient.config.realtime;
172
+ if (!realtime) {
173
+ throw new Error("subscribeRealtime requires a sync.client.realtime configuration block with a createClient callback");
174
+ }
175
+ return realtime;
176
+ }
177
+ /**
178
+ * Derives the channel descriptors to subscribe: model-level static realtime
179
+ * declarations plus the config channels callback, failing loudly when none exist.
180
+ * @param {?} context - App context passed to the channels callback.
181
+ * @returns {Promise<Array<VelociousSyncRealtimeChannelDescriptor>>} Channel descriptors.
182
+ */
183
+ async channelDescriptors(context) {
184
+ const realtime = this.realtimeConfiguration();
185
+ /** @type {Array<VelociousSyncRealtimeChannelDescriptor>} */
186
+ const channelDescriptors = [];
187
+ for (const [resourceType, resourceConfig] of Object.entries(this.syncClient.config.resources)) {
188
+ if (!resourceConfig.realtime)
189
+ continue;
190
+ channelDescriptors.push({ channel: resourceConfig.realtime.channel, params: resourceConfig.realtime.params, resourceType });
191
+ }
192
+ if (realtime.channels) {
193
+ channelDescriptors.push(...await realtime.channels(context));
194
+ }
195
+ if (channelDescriptors.length === 0) {
196
+ throw new Error("subscribeRealtime found no realtime channels - declare sync.client.realtime.channels or static sync = {realtime: {channel}} on a model");
197
+ }
198
+ return channelDescriptors;
199
+ }
200
+ /**
201
+ * Chains one pushed message onto the serialized apply queue so changes apply
202
+ * in arrival order; failures go to the sync client's error reporting.
203
+ * @param {{body: ?, resourceType: string | null}} args - Message args.
204
+ * @returns {void}
205
+ */
206
+ enqueueApply({ body, resourceType }) {
207
+ this._applyPromise = this._applyPromise.then(async () => {
208
+ try {
209
+ await this.applyMessage({ body, resourceType });
210
+ }
211
+ catch (error) {
212
+ this.syncClient.reportError(/** @type {Error} */ (error));
213
+ }
214
+ });
215
+ }
216
+ /**
217
+ * Applies one pushed message through the derived resource applier: drops
218
+ * own-device messages by echo origin, defaults the channel's resourceType onto
219
+ * envelopes without one, and fails loudly on unknown resource types.
220
+ * @param {{body: ?, resourceType: string | null}} args - Message args.
221
+ * @returns {Promise<void>}
222
+ */
223
+ async applyMessage({ body, resourceType }) {
224
+ if (!body || typeof body !== "object" || Array.isArray(body)) {
225
+ throw new Error(`Realtime sync messages must be envelope objects, got: ${JSON.stringify(body)}`);
226
+ }
227
+ const realtime = this.realtimeConfiguration();
228
+ if (realtime.localOrigin && body.echoOrigin !== undefined && body.echoOrigin !== null) {
229
+ const localOrigin = String(await realtime.localOrigin());
230
+ if (String(body.echoOrigin) === localOrigin)
231
+ return;
232
+ }
233
+ const syncPayloads = Array.isArray(body.syncs) ? body.syncs : [body];
234
+ const applySync = this.syncClient.remoteApplySync({ source: "remote change" });
235
+ for (const syncPayload of syncPayloads) {
236
+ const sync = SyncApiClient.syncEnvelopeFromPayload({ resourceType, ...syncPayload });
237
+ await applySync(sync);
238
+ }
239
+ }
240
+ /**
241
+ * Schedules a coalesced background pull closing offline gaps after
242
+ * (re)subscription readiness. Resumes arriving while a pull is already
243
+ * scheduled or in flight coalesce into that pull instead of stacking.
244
+ * @returns {void}
245
+ */
246
+ schedulePull() {
247
+ if (this.realtimeConfiguration().pullOnReconnect === false)
248
+ return;
249
+ this._scheduledPull ||= (async () => {
250
+ try {
251
+ await this.syncClient.pull();
252
+ }
253
+ catch (error) {
254
+ this.syncClient.reportError(/** @type {Error} */ (error));
255
+ }
256
+ finally {
257
+ this._scheduledPull = null;
258
+ }
259
+ })();
260
+ }
261
+ }
262
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic3luYy1yZWFsdGltZS1icmlkZ2UuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi8uLi9zcmMvc3luYy9zeW5jLXJlYWx0aW1lLWJyaWRnZS5qcyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxZQUFZO0FBRVosT0FBTyxhQUFhLE1BQU0sc0JBQXNCLENBQUE7QUFFaEQsbUlBQW1JO0FBQ25JLCtIQUErSDtBQUUvSDs7Ozs7Ozs7R0FRRztBQUNILE1BQU0sQ0FBQyxPQUFPLE9BQU8sa0JBQWtCO0lBQ3JDOzs7T0FHRztJQUNILFlBQVksRUFBQyxVQUFVLEVBQUM7UUFDdEIsSUFBSSxDQUFDLFVBQVUsR0FBRyxVQUFVLENBQUE7UUFDNUIseUpBQXlKO1FBQ3pKLElBQUksQ0FBQyxTQUFTLEdBQUcsRUFBRSxDQUFBO1FBQ25CLDBEQUEwRDtRQUMxRCxJQUFJLENBQUMsT0FBTyxHQUFHLElBQUksQ0FBQTtRQUNuQiw0QkFBNEI7UUFDNUIsSUFBSSxDQUFDLGFBQWEsR0FBRyxPQUFPLENBQUMsT0FBTyxFQUFFLENBQUE7UUFDdEMsdUhBQXVIO1FBQ3ZILElBQUksQ0FBQyxXQUFXLEdBQUcsQ0FBQyxDQUFBO1FBQ3BCLG1DQUFtQztRQUNuQyxJQUFJLENBQUMsY0FBYyxHQUFHLElBQUksQ0FBQTtRQUMxQixtQ0FBbUM7UUFDbkMsSUFBSSxDQUFDLGlCQUFpQixHQUFHLElBQUksQ0FBQTtRQUM3Qiw0REFBNEQ7UUFDNUQsSUFBSSxDQUFDLE1BQU0sR0FBRyxjQUFjLENBQUE7SUFDOUIsQ0FBQztJQUVEOzs7Ozs7T0FNRztJQUNILEtBQUssQ0FBQyxTQUFTLENBQUMsT0FBTztRQUNyQixJQUFJLElBQUksQ0FBQyxNQUFNLEtBQUssWUFBWTtZQUFFLE9BQU07UUFFeEMsSUFBSSxDQUFDLElBQUksQ0FBQyxpQkFBaUIsRUFBRSxDQUFDO1lBQzVCLElBQUksQ0FBQyxpQkFBaUIsR0FBRyxJQUFJLENBQUMsVUFBVSxDQUFDLE9BQU8sQ0FBQyxDQUFDLE9BQU8sQ0FBQyxHQUFHLEVBQUU7Z0JBQzdELElBQUksQ0FBQyxpQkFBaUIsR0FBRyxJQUFJLENBQUE7WUFDL0IsQ0FBQyxDQUFDLENBQUE7UUFDSixDQUFDO1FBRUQsTUFBTSxJQUFJLENBQUMsaUJBQWlCLENBQUE7SUFDOUIsQ0FBQztJQUVEOzs7Ozs7Ozs7T0FTRztJQUNILEtBQUssQ0FBQyxVQUFVLENBQUMsT0FBTztRQUN0QixNQUFNLFVBQVUsR0FBRyxJQUFJLENBQUMsV0FBVyxDQUFBO1FBRW5DLElBQUksQ0FBQyxNQUFNLEdBQUcsYUFBYSxDQUFBO1FBRTNCLHlKQUF5SjtRQUN6SixNQUFNLFFBQVEsR0FBRyxFQUFFLENBQUE7UUFDbkIsMERBQTBEO1FBQzFELElBQUksTUFBTSxHQUFHLElBQUksQ0FBQTtRQUNqQjs7O1dBR0c7UUFDSCxNQUFNLFFBQVEsR0FBRyxLQUFLLElBQUksRUFBRTtZQUMxQixLQUFLLE1BQU0sRUFBQyxZQUFZLEVBQUMsSUFBSSxRQUFRLEVBQUUsQ0FBQztnQkFDdEMsWUFBWSxDQUFDLEtBQUssRUFBRSxDQUFBO1lBQ3RCLENBQUM7WUFFRCxJQUFJLE1BQU07Z0JBQUUsTUFBTSxNQUFNLENBQUMsMEJBQTBCLEVBQUUsQ0FBQTtRQUN2RCxDQUFDLENBQUE7UUFFRCxJQUFJLENBQUM7WUFDSCxNQUFNLFFBQVEsR0FBRyxJQUFJLENBQUMscUJBQXFCLEVBQUUsQ0FBQTtZQUM3QyxNQUFNLGtCQUFrQixHQUFHLE1BQU0sSUFBSSxDQUFDLGtCQUFrQixDQUFDLE9BQU8sQ0FBQyxDQUFBO1lBRWpFLElBQUksVUFBVSxLQUFLLElBQUksQ0FBQyxXQUFXO2dCQUFFLE9BQU07WUFFM0MsTUFBTSxHQUFHLE1BQU0sUUFBUSxDQUFDLFlBQVksRUFBRSxDQUFBO1lBRXRDLElBQUksVUFBVSxLQUFLLElBQUksQ0FBQyxXQUFXO2dCQUFFLE9BQU07WUFFM0MsTUFBTSxNQUFNLENBQUMsT0FBTyxFQUFFLENBQUE7WUFFdEIsSUFBSSxVQUFVLEtBQUssSUFBSSxDQUFDLFdBQVcsRUFBRSxDQUFDO2dCQUNwQyxNQUFNLFFBQVEsRUFBRSxDQUFBO2dCQUNoQixPQUFNO1lBQ1IsQ0FBQztZQUVELE1BQU0sbUJBQW1CLEdBQUcsTUFBTSxJQUFJLENBQUMsVUFBVSxDQUFDLE1BQU0sQ0FBQyxtQkFBbUIsRUFBRSxDQUFBO1lBRTlFLElBQUksVUFBVSxLQUFLLElBQUksQ0FBQyxXQUFXLEVBQUUsQ0FBQztnQkFDcEMsTUFBTSxRQUFRLEVBQUUsQ0FBQTtnQkFDaEIsT0FBTTtZQUNSLENBQUM7WUFFRCxLQUFLLE1BQU0saUJBQWlCLElBQUksa0JBQWtCLEVBQUUsQ0FBQztnQkFDbkQsSUFBSSxpQkFBaUIsQ0FBQyxNQUFNLElBQUkscUJBQXFCLElBQUksaUJBQWlCLENBQUMsTUFBTSxFQUFFLENBQUM7b0JBQ2xGLE1BQU0sSUFBSSxLQUFLLENBQUMscUJBQXFCLGlCQUFpQixDQUFDLE9BQU8seUhBQXlILENBQUMsQ0FBQTtnQkFDMUwsQ0FBQztnQkFFRCxNQUFNLFlBQVksR0FBRyxpQkFBaUIsQ0FBQyxZQUFZLElBQUksSUFBSSxDQUFBO2dCQUMzRCxNQUFNLFlBQVksR0FBRyxNQUFNLENBQUMsZ0JBQWdCLENBQUMsaUJBQWlCLENBQUMsT0FBTyxFQUFFO29CQUN0RSxTQUFTLEVBQUUsQ0FBQyxJQUFJLEVBQUUsRUFBRSxDQUFDLElBQUksQ0FBQyxZQUFZLENBQUMsRUFBQyxJQUFJLEVBQUUsWUFBWSxFQUFDLENBQUM7b0JBQzVELFFBQVEsRUFBRSxHQUFHLEVBQUUsQ0FBQyxJQUFJLENBQUMsWUFBWSxFQUFFO29CQUNuQyxNQUFNLEVBQUUsRUFBQyxHQUFHLGlCQUFpQixDQUFDLE1BQU0sRUFBRSxtQkFBbUIsRUFBQztpQkFDM0QsQ0FBQyxDQUFBO2dCQUVGLFFBQVEsQ0FBQyxJQUFJLENBQUMsRUFBQyxPQUFPLEVBQUUsaUJBQWlCLENBQUMsT0FBTyxFQUFFLFlBQVksRUFBRSxZQUFZLEVBQUMsQ0FBQyxDQUFBO1lBQ2pGLENBQUM7WUFFRCxNQUFNLE9BQU8sQ0FBQyxHQUFHLENBQUMsUUFBUSxDQUFDLEdBQUcsQ0FBQyxDQUFDLEVBQUMsWUFBWSxFQUFDLEVBQUUsRUFBRSxDQUFDLFlBQVksQ0FBQyxZQUFZLEVBQUUsQ0FBQyxDQUFDLENBQUE7WUFFaEYsSUFBSSxVQUFVLEtBQUssSUFBSSxDQUFDLFdBQVcsRUFBRSxDQUFDO2dCQUNwQyxNQUFNLFFBQVEsRUFBRSxDQUFBO2dCQUNoQixPQUFNO1lBQ1IsQ0FBQztZQUVELElBQUksQ0FBQyxTQUFTLEdBQUcsUUFBUSxDQUFBO1lBQ3pCLElBQUksQ0FBQyxPQUFPLEdBQUcsTUFBTSxDQUFBO1lBQ3JCLElBQUksQ0FBQyxNQUFNLEdBQUcsWUFBWSxDQUFBO1lBQzFCLElBQUksQ0FBQyxZQUFZLEVBQUUsQ0FBQTtRQUNyQixDQUFDO1FBQUMsT0FBTyxLQUFLLEVBQUUsQ0FBQztZQUNmLE1BQU0sUUFBUSxFQUFFLENBQUE7WUFFaEIsSUFBSSxVQUFVLEtBQUssSUFBSSxDQUFDLFdBQVc7Z0JBQUUsSUFBSSxDQUFDLE1BQU0sR0FBRyxjQUFjLENBQUE7WUFFakUsTUFBTSxLQUFLLENBQUE7UUFDYixDQUFDO0lBQ0gsQ0FBQztJQUVEOzs7OztPQUtHO0lBQ0gsS0FBSyxDQUFDLFdBQVc7UUFDZixJQUFJLENBQUMsV0FBVyxJQUFJLENBQUMsQ0FBQTtRQUVyQixNQUFNLFFBQVEsR0FBRyxJQUFJLENBQUMsU0FBUyxDQUFBO1FBQy9CLE1BQU0sTUFBTSxHQUFHLElBQUksQ0FBQyxPQUFPLENBQUE7UUFFM0IsSUFBSSxDQUFDLFNBQVMsR0FBRyxFQUFFLENBQUE7UUFDbkIsSUFBSSxDQUFDLE9BQU8sR0FBRyxJQUFJLENBQUE7UUFDbkIsSUFBSSxDQUFDLE1BQU0sR0FBRyxjQUFjLENBQUE7UUFFNUIsS0FBSyxNQUFNLEVBQUMsWUFBWSxFQUFDLElBQUksUUFBUSxFQUFFLENBQUM7WUFDdEMsWUFBWSxDQUFDLEtBQUssRUFBRSxDQUFBO1FBQ3RCLENBQUM7UUFFRCxJQUFJLE1BQU07WUFBRSxNQUFNLE1BQU0sQ0FBQywwQkFBMEIsRUFBRSxDQUFBO0lBQ3ZELENBQUM7SUFFRDs7O09BR0c7SUFDSCxNQUFNO1FBQ0osT0FBTztZQUNMLFFBQVEsRUFBRSxJQUFJLENBQUMsU0FBUyxDQUFDLEdBQUcsQ0FBQyxDQUFDLEVBQUMsT0FBTyxFQUFFLFlBQVksRUFBRSxZQUFZLEVBQUMsRUFBRSxFQUFFLENBQUMsQ0FBQyxFQUFDLE9BQU8sRUFBRSxLQUFLLEVBQUUsWUFBWSxDQUFDLE9BQU8sRUFBRSxFQUFFLFlBQVksRUFBQyxDQUFDLENBQUM7WUFDakksS0FBSyxFQUFFLElBQUksQ0FBQyxNQUFNO1NBQ25CLENBQUE7SUFDSCxDQUFDO0lBRUQ7OztPQUdHO0lBQ0gsS0FBSyxDQUFDLGNBQWM7UUFDbEIsTUFBTSxJQUFJLENBQUMsYUFBYSxDQUFBO1FBQ3hCLElBQUksSUFBSSxDQUFDLGNBQWM7WUFBRSxNQUFNLElBQUksQ0FBQyxjQUFjLENBQUE7SUFDcEQsQ0FBQztJQUVEOzs7T0FHRztJQUNILHFCQUFxQjtRQUNuQixNQUFNLFFBQVEsR0FBRyxJQUFJLENBQUMsVUFBVSxDQUFDLE1BQU0sQ0FBQyxRQUFRLENBQUE7UUFFaEQsSUFBSSxDQUFDLFFBQVEsRUFBRSxDQUFDO1lBQ2QsTUFBTSxJQUFJLEtBQUssQ0FBQyxvR0FBb0csQ0FBQyxDQUFBO1FBQ3ZILENBQUM7UUFFRCxPQUFPLFFBQVEsQ0FBQTtJQUNqQixDQUFDO0lBRUQ7Ozs7O09BS0c7SUFDSCxLQUFLLENBQUMsa0JBQWtCLENBQUMsT0FBTztRQUM5QixNQUFNLFFBQVEsR0FBRyxJQUFJLENBQUMscUJBQXFCLEVBQUUsQ0FBQTtRQUM3Qyw0REFBNEQ7UUFDNUQsTUFBTSxrQkFBa0IsR0FBRyxFQUFFLENBQUE7UUFFN0IsS0FBSyxNQUFNLENBQUMsWUFBWSxFQUFFLGNBQWMsQ0FBQyxJQUFJLE1BQU0sQ0FBQyxPQUFPLENBQUMsSUFBSSxDQUFDLFVBQVUsQ0FBQyxNQUFNLENBQUMsU0FBUyxDQUFDLEVBQUUsQ0FBQztZQUM5RixJQUFJLENBQUMsY0FBYyxDQUFDLFFBQVE7Z0JBQUUsU0FBUTtZQUV0QyxrQkFBa0IsQ0FBQyxJQUFJLENBQUMsRUFBQyxPQUFPLEVBQUUsY0FBYyxDQUFDLFFBQVEsQ0FBQyxPQUFPLEVBQUUsTUFBTSxFQUFFLGNBQWMsQ0FBQyxRQUFRLENBQUMsTUFBTSxFQUFFLFlBQVksRUFBQyxDQUFDLENBQUE7UUFDM0gsQ0FBQztRQUVELElBQUksUUFBUSxDQUFDLFFBQVEsRUFBRSxDQUFDO1lBQ3RCLGtCQUFrQixDQUFDLElBQUksQ0FBQyxHQUFHLE1BQU0sUUFBUSxDQUFDLFFBQVEsQ0FBQyxPQUFPLENBQUMsQ0FBQyxDQUFBO1FBQzlELENBQUM7UUFFRCxJQUFJLGtCQUFrQixDQUFDLE1BQU0sS0FBSyxDQUFDLEVBQUUsQ0FBQztZQUNwQyxNQUFNLElBQUksS0FBSyxDQUFDLHdJQUF3SSxDQUFDLENBQUE7UUFDM0osQ0FBQztRQUVELE9BQU8sa0JBQWtCLENBQUE7SUFDM0IsQ0FBQztJQUVEOzs7OztPQUtHO0lBQ0gsWUFBWSxDQUFDLEVBQUMsSUFBSSxFQUFFLFlBQVksRUFBQztRQUMvQixJQUFJLENBQUMsYUFBYSxHQUFHLElBQUksQ0FBQyxhQUFhLENBQUMsSUFBSSxDQUFDLEtBQUssSUFBSSxFQUFFO1lBQ3RELElBQUksQ0FBQztnQkFDSCxNQUFNLElBQUksQ0FBQyxZQUFZLENBQUMsRUFBQyxJQUFJLEVBQUUsWUFBWSxFQUFDLENBQUMsQ0FBQTtZQUMvQyxDQUFDO1lBQUMsT0FBTyxLQUFLLEVBQUUsQ0FBQztnQkFDZixJQUFJLENBQUMsVUFBVSxDQUFDLFdBQVcsQ0FBQyxvQkFBb0IsQ0FBQyxDQUFDLEtBQUssQ0FBQyxDQUFDLENBQUE7WUFDM0QsQ0FBQztRQUNILENBQUMsQ0FBQyxDQUFBO0lBQ0osQ0FBQztJQUVEOzs7Ozs7T0FNRztJQUNILEtBQUssQ0FBQyxZQUFZLENBQUMsRUFBQyxJQUFJLEVBQUUsWUFBWSxFQUFDO1FBQ3JDLElBQUksQ0FBQyxJQUFJLElBQUksT0FBTyxJQUFJLEtBQUssUUFBUSxJQUFJLEtBQUssQ0FBQyxPQUFPLENBQUMsSUFBSSxDQUFDLEVBQUUsQ0FBQztZQUM3RCxNQUFNLElBQUksS0FBSyxDQUFDLHlEQUF5RCxJQUFJLENBQUMsU0FBUyxDQUFDLElBQUksQ0FBQyxFQUFFLENBQUMsQ0FBQTtRQUNsRyxDQUFDO1FBRUQsTUFBTSxRQUFRLEdBQUcsSUFBSSxDQUFDLHFCQUFxQixFQUFFLENBQUE7UUFFN0MsSUFBSSxRQUFRLENBQUMsV0FBVyxJQUFJLElBQUksQ0FBQyxVQUFVLEtBQUssU0FBUyxJQUFJLElBQUksQ0FBQyxVQUFVLEtBQUssSUFBSSxFQUFFLENBQUM7WUFDdEYsTUFBTSxXQUFXLEdBQUcsTUFBTSxDQUFDLE1BQU0sUUFBUSxDQUFDLFdBQVcsRUFBRSxDQUFDLENBQUE7WUFFeEQsSUFBSSxNQUFNLENBQUMsSUFBSSxDQUFDLFVBQVUsQ0FBQyxLQUFLLFdBQVc7Z0JBQUUsT0FBTTtRQUNyRCxDQUFDO1FBRUQsTUFBTSxZQUFZLEdBQUcsS0FBSyxDQUFDLE9BQU8sQ0FBQyxJQUFJLENBQUMsS0FBSyxDQUFDLENBQUMsQ0FBQyxDQUFDLElBQUksQ0FBQyxLQUFLLENBQUMsQ0FBQyxDQUFDLENBQUMsSUFBSSxDQUFDLENBQUE7UUFDcEUsTUFBTSxTQUFTLEdBQUcsSUFBSSxDQUFDLFVBQVUsQ0FBQyxlQUFlLENBQUMsRUFBQyxNQUFNLEVBQUUsZUFBZSxFQUFDLENBQUMsQ0FBQTtRQUU1RSxLQUFLLE1BQU0sV0FBVyxJQUFJLFlBQVksRUFBRSxDQUFDO1lBQ3ZDLE1BQU0sSUFBSSxHQUFHLGFBQWEsQ0FBQyx1QkFBdUIsQ0FBQyxFQUFDLFlBQVksRUFBRSxHQUFHLFdBQVcsRUFBQyxDQUFDLENBQUE7WUFFbEYsTUFBTSxTQUFTLENBQUMsSUFBSSxDQUFDLENBQUE7UUFDdkIsQ0FBQztJQUNILENBQUM7SUFFRDs7Ozs7T0FLRztJQUNILFlBQVk7UUFDVixJQUFJLElBQUksQ0FBQyxxQkFBcUIsRUFBRSxDQUFDLGVBQWUsS0FBSyxLQUFLO1lBQUUsT0FBTTtRQUVsRSxJQUFJLENBQUMsY0FBYyxLQUFLLENBQUMsS0FBSyxJQUFJLEVBQUU7WUFDbEMsSUFBSSxDQUFDO2dCQUNILE1BQU0sSUFBSSxDQUFDLFVBQVUsQ0FBQyxJQUFJLEVBQUUsQ0FBQTtZQUM5QixDQUFDO1lBQUMsT0FBTyxLQUFLLEVBQUUsQ0FBQztnQkFDZixJQUFJLENBQUMsVUFBVSxDQUFDLFdBQVcsQ0FBQyxvQkFBb0IsQ0FBQyxDQUFDLEtBQUssQ0FBQyxDQUFDLENBQUE7WUFDM0QsQ0FBQztvQkFBUyxDQUFDO2dCQUNULElBQUksQ0FBQyxjQUFjLEdBQUcsSUFBSSxDQUFBO1lBQzVCLENBQUM7UUFDSCxDQUFDLENBQUMsRUFBRSxDQUFBO0lBQ04sQ0FBQztDQUNGIiwic291cmNlc0NvbnRlbnQiOlsiLy8gQHRzLWNoZWNrXG5cbmltcG9ydCBTeW5jQXBpQ2xpZW50IGZyb20gXCIuL3N5bmMtYXBpLWNsaWVudC5qc1wiXG5cbi8qKiBAdHlwZWRlZiB7aW1wb3J0KFwiLi4vY29uZmlndXJhdGlvbi10eXBlcy5qc1wiKS5WZWxvY2lvdXNTeW5jUmVhbHRpbWVDaGFubmVsRGVzY3JpcHRvcn0gVmVsb2Npb3VzU3luY1JlYWx0aW1lQ2hhbm5lbERlc2NyaXB0b3IgKi9cbi8qKiBAdHlwZWRlZiB7aW1wb3J0KFwiLi4vY29uZmlndXJhdGlvbi10eXBlcy5qc1wiKS5WZWxvY2lvdXNTeW5jUmVhbHRpbWVXZWJzb2NrZXRDbGllbnR9IFZlbG9jaW91c1N5bmNSZWFsdGltZVdlYnNvY2tldENsaWVudCAqL1xuXG4vKipcbiAqIERlcml2ZWQgcmVhbHRpbWUgcHVzaCBicmlkZ2UgZm9yIHRoZSBzeW5jIGNsaWVudC4gU3Vic2NyaWJlcyB0aGUgZGVjbGFyZWRcbiAqIHdlYnNvY2tldCBjaGFubmVscyAoY29uZmlnIGBzeW5jLmNsaWVudC5yZWFsdGltZS5jaGFubmVsc2AgY2FsbGJhY2sgcGx1c1xuICogbW9kZWwtbGV2ZWwgYHN0YXRpYyBzeW5jID0ge3JlYWx0aW1lOiB7Y2hhbm5lbH19YCBkZWNsYXJhdGlvbnMpLCBhcHBsaWVzXG4gKiBwdXNoZWQgY2hhbmdlcyB0aHJvdWdoIHRoZSBzYW1lIGRlcml2ZWQgcmVzb3VyY2UgYXBwbGllciBhcyBwdWxscyAod2l0aCBlY2hvXG4gKiBzdXBwcmVzc2lvbiBhZ2FpbnN0IHRyYWNrZWQgcmUtcXVldWVpbmcpLCBkcm9wcyBvd24tZGV2aWNlIG1lc3NhZ2VzIGJ5IGVjaG9cbiAqIG9yaWdpbiwgYW5kIGZpcmVzIGEgY29hbGVzY2VkIGBwdWxsKClgIHdoZW4gc3Vic2NyaXB0aW9ucyBiZWNvbWUgcmVhZHkgb3JcbiAqIHJlc3VtZSBzbyBvZmZsaW5lIGdhcHMgY2xvc2UuXG4gKi9cbmV4cG9ydCBkZWZhdWx0IGNsYXNzIFN5bmNSZWFsdGltZUJyaWRnZSB7XG4gIC8qKlxuICAgKiBCdWlsZHMgdGhlIGJyaWRnZSBmb3IgYSBzeW5jIGNsaWVudC5cbiAgICogQHBhcmFtIHt7c3luY0NsaWVudDogaW1wb3J0KFwiLi9zeW5jLWNsaWVudC5qc1wiKS5kZWZhdWx0fX0gYXJncyAtIEJyaWRnZSBhcmdzLlxuICAgKi9cbiAgY29uc3RydWN0b3Ioe3N5bmNDbGllbnR9KSB7XG4gICAgdGhpcy5zeW5jQ2xpZW50ID0gc3luY0NsaWVudFxuICAgIC8qKiBAdHlwZSB7QXJyYXk8e2NoYW5uZWw6IHN0cmluZywgcmVzb3VyY2VUeXBlOiBzdHJpbmcgfCBudWxsLCBzdWJzY3JpcHRpb246IGltcG9ydChcIi4uL2NvbmZpZ3VyYXRpb24tdHlwZXMuanNcIikuVmVsb2Npb3VzU3luY1JlYWx0aW1lU3Vic2NyaXB0aW9ufT59ICovXG4gICAgdGhpcy5fY2hhbm5lbHMgPSBbXVxuICAgIC8qKiBAdHlwZSB7VmVsb2Npb3VzU3luY1JlYWx0aW1lV2Vic29ja2V0Q2xpZW50IHwgbnVsbH0gKi9cbiAgICB0aGlzLl9jbGllbnQgPSBudWxsXG4gICAgLyoqIEB0eXBlIHtQcm9taXNlPHZvaWQ+fSAqL1xuICAgIHRoaXMuX2FwcGx5UHJvbWlzZSA9IFByb21pc2UucmVzb2x2ZSgpXG4gICAgLyoqIEB0eXBlIHtudW1iZXJ9IFN1YnNjcmlwdGlvbiBnZW5lcmF0aW9uIC0gYnVtcGVkIGJ5IHVuc3Vic2NyaWJlIHNvIGluLWZsaWdodCBzdWJzY3JpYmVzIGRldGVjdCB0aGV5IGJlY2FtZSBzdGFsZS4gKi9cbiAgICB0aGlzLl9nZW5lcmF0aW9uID0gMFxuICAgIC8qKiBAdHlwZSB7UHJvbWlzZTx2b2lkPiB8IG51bGx9ICovXG4gICAgdGhpcy5fc2NoZWR1bGVkUHVsbCA9IG51bGxcbiAgICAvKiogQHR5cGUge1Byb21pc2U8dm9pZD4gfCBudWxsfSAqL1xuICAgIHRoaXMuX3N1YnNjcmliZVByb21pc2UgPSBudWxsXG4gICAgLyoqIEB0eXBlIHtcInN1YnNjcmliZWRcIiB8IFwic3Vic2NyaWJpbmdcIiB8IFwidW5zdWJzY3JpYmVkXCJ9ICovXG4gICAgdGhpcy5fc3RhdGUgPSBcInVuc3Vic2NyaWJlZFwiXG4gIH1cblxuICAvKipcbiAgICogU3Vic2NyaWJlcyB0aGUgZGVyaXZlZCByZWFsdGltZSBjaGFubmVscyAoaWRlbXBvdGVudCBhbmQgc2luZ2xlLWZsaWdodGVkKTpcbiAgICogYW4gYWN0aXZlIHN1YnNjcmlwdGlvbiBpcyBrZXB0IGFzLWlzIGFuZCBhIGNvbmN1cnJlbnQgc3Vic2NyaWJlIGF3YWl0cyB0aGVcbiAgICogaW4tZmxpZ2h0IGF0dGVtcHQuIENhbGwgYHVuc3Vic2NyaWJlKClgIGZpcnN0IHRvIGNoYW5nZSB0aGUgY29udGV4dC5cbiAgICogQHBhcmFtIHs/fSBbY29udGV4dF0gLSBBcHAgY29udGV4dCBwYXNzZWQgdG8gdGhlIGBzeW5jLmNsaWVudC5yZWFsdGltZS5jaGFubmVsc2AgY2FsbGJhY2sgKHJ1bnRpbWUgdmFsdWVzIGxpa2UgZXZlbnRJZCkuXG4gICAqIEByZXR1cm5zIHtQcm9taXNlPHZvaWQ+fVxuICAgKi9cbiAgYXN5bmMgc3Vic2NyaWJlKGNvbnRleHQpIHtcbiAgICBpZiAodGhpcy5fc3RhdGUgPT09IFwic3Vic2NyaWJlZFwiKSByZXR1cm5cblxuICAgIGlmICghdGhpcy5fc3Vic2NyaWJlUHJvbWlzZSkge1xuICAgICAgdGhpcy5fc3Vic2NyaWJlUHJvbWlzZSA9IHRoaXMuX3N1YnNjcmliZShjb250ZXh0KS5maW5hbGx5KCgpID0+IHtcbiAgICAgICAgdGhpcy5fc3Vic2NyaWJlUHJvbWlzZSA9IG51bGxcbiAgICAgIH0pXG4gICAgfVxuXG4gICAgYXdhaXQgdGhpcy5fc3Vic2NyaWJlUHJvbWlzZVxuICB9XG5cbiAgLyoqXG4gICAqIENvbm5lY3RzIHRoZSB3ZWJzb2NrZXQgY2xpZW50LCBzdWJzY3JpYmVzIGV2ZXJ5IGRlcml2ZWQgY2hhbm5lbCwgYW5kIHdhaXRzXG4gICAqIGZvciBlYWNoIHN1YnNjcmlwdGlvbidzIHNlcnZlciBhY2tub3dsZWRnZW1lbnQgYmVmb3JlIHRoZSBnYXAtY2xvc2luZyBwdWxsLFxuICAgKiBzbyBubyBjaGFuZ2UgY2FuIGxhbmQgYmV0d2VlbiB0aGUgcHVsbCBhbmQgdGhlIHN1YnNjcmlwdGlvbnMgZ29pbmcgbGl2ZS5cbiAgICogTG9jYWxseSBjcmVhdGVkIHJlc291cmNlcyBhcmUgb25seSBwcm9tb3RlZCB0byB0aGUgYnJpZGdlIG9uY2UgZXZlcnl0aGluZ1xuICAgKiBpcyBsaXZlOyBhbiB1bnN1YnNjcmliZSBhcnJpdmluZyBkdXJpbmcgYW55IGF3YWl0IG1hcmtzIHRoaXMgYXR0ZW1wdCBzdGFsZVxuICAgKiBhbmQgaXQgdGVhcnMgaXRzIG93biByZXNvdXJjZXMgZG93biBpbnN0ZWFkIG9mIHJlc3Vic2NyaWJpbmcuXG4gICAqIEBwYXJhbSB7P30gY29udGV4dCAtIEFwcCBjb250ZXh0IHBhc3NlZCB0byB0aGUgY2hhbm5lbHMgY2FsbGJhY2suXG4gICAqIEByZXR1cm5zIHtQcm9taXNlPHZvaWQ+fVxuICAgKi9cbiAgYXN5bmMgX3N1YnNjcmliZShjb250ZXh0KSB7XG4gICAgY29uc3QgZ2VuZXJhdGlvbiA9IHRoaXMuX2dlbmVyYXRpb25cblxuICAgIHRoaXMuX3N0YXRlID0gXCJzdWJzY3JpYmluZ1wiXG5cbiAgICAvKiogQHR5cGUge0FycmF5PHtjaGFubmVsOiBzdHJpbmcsIHJlc291cmNlVHlwZTogc3RyaW5nIHwgbnVsbCwgc3Vic2NyaXB0aW9uOiBpbXBvcnQoXCIuLi9jb25maWd1cmF0aW9uLXR5cGVzLmpzXCIpLlZlbG9jaW91c1N5bmNSZWFsdGltZVN1YnNjcmlwdGlvbn0+fSAqL1xuICAgIGNvbnN0IGNoYW5uZWxzID0gW11cbiAgICAvKiogQHR5cGUge1ZlbG9jaW91c1N5bmNSZWFsdGltZVdlYnNvY2tldENsaWVudCB8IG51bGx9ICovXG4gICAgbGV0IGNsaWVudCA9IG51bGxcbiAgICAvKipcbiAgICAgKiBUZWFycyBkb3duIGV2ZXJ5dGhpbmcgdGhpcyBzdGFsZS9mYWlsZWQgc3Vic2NyaWJlIGF0dGVtcHQgY3JlYXRlZCBpdHNlbGYuXG4gICAgICogQHJldHVybnMge1Byb21pc2U8dm9pZD59XG4gICAgICovXG4gICAgY29uc3QgdGVhcmRvd24gPSBhc3luYyAoKSA9PiB7XG4gICAgICBmb3IgKGNvbnN0IHtzdWJzY3JpcHRpb259IG9mIGNoYW5uZWxzKSB7XG4gICAgICAgIHN1YnNjcmlwdGlvbi5jbG9zZSgpXG4gICAgICB9XG5cbiAgICAgIGlmIChjbGllbnQpIGF3YWl0IGNsaWVudC5kaXNjb25uZWN0QW5kU3RvcFJlY29ubmVjdCgpXG4gICAgfVxuXG4gICAgdHJ5IHtcbiAgICAgIGNvbnN0IHJlYWx0aW1lID0gdGhpcy5yZWFsdGltZUNvbmZpZ3VyYXRpb24oKVxuICAgICAgY29uc3QgY2hhbm5lbERlc2NyaXB0b3JzID0gYXdhaXQgdGhpcy5jaGFubmVsRGVzY3JpcHRvcnMoY29udGV4dClcblxuICAgICAgaWYgKGdlbmVyYXRpb24gIT09IHRoaXMuX2dlbmVyYXRpb24pIHJldHVyblxuXG4gICAgICBjbGllbnQgPSBhd2FpdCByZWFsdGltZS5jcmVhdGVDbGllbnQoKVxuXG4gICAgICBpZiAoZ2VuZXJhdGlvbiAhPT0gdGhpcy5fZ2VuZXJhdGlvbikgcmV0dXJuXG5cbiAgICAgIGF3YWl0IGNsaWVudC5jb25uZWN0KClcblxuICAgICAgaWYgKGdlbmVyYXRpb24gIT09IHRoaXMuX2dlbmVyYXRpb24pIHtcbiAgICAgICAgYXdhaXQgdGVhcmRvd24oKVxuICAgICAgICByZXR1cm5cbiAgICAgIH1cblxuICAgICAgY29uc3QgYXV0aGVudGljYXRpb25Ub2tlbiA9IGF3YWl0IHRoaXMuc3luY0NsaWVudC5jb25maWcuYXV0aGVudGljYXRpb25Ub2tlbigpXG5cbiAgICAgIGlmIChnZW5lcmF0aW9uICE9PSB0aGlzLl9nZW5lcmF0aW9uKSB7XG4gICAgICAgIGF3YWl0IHRlYXJkb3duKClcbiAgICAgICAgcmV0dXJuXG4gICAgICB9XG5cbiAgICAgIGZvciAoY29uc3QgY2hhbm5lbERlc2NyaXB0b3Igb2YgY2hhbm5lbERlc2NyaXB0b3JzKSB7XG4gICAgICAgIGlmIChjaGFubmVsRGVzY3JpcHRvci5wYXJhbXMgJiYgXCJhdXRoZW50aWNhdGlvblRva2VuXCIgaW4gY2hhbm5lbERlc2NyaXB0b3IucGFyYW1zKSB7XG4gICAgICAgICAgdGhyb3cgbmV3IEVycm9yKGBSZWFsdGltZSBjaGFubmVsIFwiJHtjaGFubmVsRGVzY3JpcHRvci5jaGFubmVsfVwiIHBhcmFtcyBtdXN0IG5vdCBpbmNsdWRlIGF1dGhlbnRpY2F0aW9uVG9rZW4gLSB0aGUgZnJhbWV3b3JrIGluamVjdHMgdGhlIHN5bmMuY2xpZW50IGF1dGhlbnRpY2F0aW9uVG9rZW4gYXV0b21hdGljYWxseWApXG4gICAgICAgIH1cblxuICAgICAgICBjb25zdCByZXNvdXJjZVR5cGUgPSBjaGFubmVsRGVzY3JpcHRvci5yZXNvdXJjZVR5cGUgPz8gbnVsbFxuICAgICAgICBjb25zdCBzdWJzY3JpcHRpb24gPSBjbGllbnQuc3Vic2NyaWJlQ2hhbm5lbChjaGFubmVsRGVzY3JpcHRvci5jaGFubmVsLCB7XG4gICAgICAgICAgb25NZXNzYWdlOiAoYm9keSkgPT4gdGhpcy5lbnF1ZXVlQXBwbHkoe2JvZHksIHJlc291cmNlVHlwZX0pLFxuICAgICAgICAgIG9uUmVzdW1lOiAoKSA9PiB0aGlzLnNjaGVkdWxlUHVsbCgpLFxuICAgICAgICAgIHBhcmFtczogey4uLmNoYW5uZWxEZXNjcmlwdG9yLnBhcmFtcywgYXV0aGVudGljYXRpb25Ub2tlbn1cbiAgICAgICAgfSlcblxuICAgICAgICBjaGFubmVscy5wdXNoKHtjaGFubmVsOiBjaGFubmVsRGVzY3JpcHRvci5jaGFubmVsLCByZXNvdXJjZVR5cGUsIHN1YnNjcmlwdGlvbn0pXG4gICAgICB9XG5cbiAgICAgIGF3YWl0IFByb21pc2UuYWxsKGNoYW5uZWxzLm1hcCgoe3N1YnNjcmlwdGlvbn0pID0+IHN1YnNjcmlwdGlvbi53YWl0Rm9yUmVhZHkoKSkpXG5cbiAgICAgIGlmIChnZW5lcmF0aW9uICE9PSB0aGlzLl9nZW5lcmF0aW9uKSB7XG4gICAgICAgIGF3YWl0IHRlYXJkb3duKClcbiAgICAgICAgcmV0dXJuXG4gICAgICB9XG5cbiAgICAgIHRoaXMuX2NoYW5uZWxzID0gY2hhbm5lbHNcbiAgICAgIHRoaXMuX2NsaWVudCA9IGNsaWVudFxuICAgICAgdGhpcy5fc3RhdGUgPSBcInN1YnNjcmliZWRcIlxuICAgICAgdGhpcy5zY2hlZHVsZVB1bGwoKVxuICAgIH0gY2F0Y2ggKGVycm9yKSB7XG4gICAgICBhd2FpdCB0ZWFyZG93bigpXG5cbiAgICAgIGlmIChnZW5lcmF0aW9uID09PSB0aGlzLl9nZW5lcmF0aW9uKSB0aGlzLl9zdGF0ZSA9IFwidW5zdWJzY3JpYmVkXCJcblxuICAgICAgdGhyb3cgZXJyb3JcbiAgICB9XG4gIH1cblxuICAvKipcbiAgICogQ2xvc2VzIGV2ZXJ5IGNoYW5uZWwgc3Vic2NyaXB0aW9uIGFuZCBkaXNjb25uZWN0cyB0aGUgd2Vic29ja2V0IGNsaWVudFxuICAgKiAoaWRlbXBvdGVudCkuIEFsc28gbWFya3MgYW55IGluLWZsaWdodCBzdWJzY3JpYmUgYXR0ZW1wdCBzdGFsZSBzbyBpdCB0ZWFyc1xuICAgKiBpdHNlbGYgZG93biBpbnN0ZWFkIG9mIGZpbmlzaGluZyB0aGUgc3Vic2NyaXB0aW9uIGFmdGVyd2FyZHMuXG4gICAqIEByZXR1cm5zIHtQcm9taXNlPHZvaWQ+fVxuICAgKi9cbiAgYXN5bmMgdW5zdWJzY3JpYmUoKSB7XG4gICAgdGhpcy5fZ2VuZXJhdGlvbiArPSAxXG5cbiAgICBjb25zdCBjaGFubmVscyA9IHRoaXMuX2NoYW5uZWxzXG4gICAgY29uc3QgY2xpZW50ID0gdGhpcy5fY2xpZW50XG5cbiAgICB0aGlzLl9jaGFubmVscyA9IFtdXG4gICAgdGhpcy5fY2xpZW50ID0gbnVsbFxuICAgIHRoaXMuX3N0YXRlID0gXCJ1bnN1YnNjcmliZWRcIlxuXG4gICAgZm9yIChjb25zdCB7c3Vic2NyaXB0aW9ufSBvZiBjaGFubmVscykge1xuICAgICAgc3Vic2NyaXB0aW9uLmNsb3NlKClcbiAgICB9XG5cbiAgICBpZiAoY2xpZW50KSBhd2FpdCBjbGllbnQuZGlzY29ubmVjdEFuZFN0b3BSZWNvbm5lY3QoKVxuICB9XG5cbiAgLyoqXG4gICAqIFJlcG9ydHMgdGhlIGJyaWRnZSBzdWJzY3JpcHRpb24gc3RhdGUgYW5kIHBlci1jaGFubmVsIHJlYWRpbmVzcy5cbiAgICogQHJldHVybnMge3tjaGFubmVsczogQXJyYXk8e2NoYW5uZWw6IHN0cmluZywgcmVhZHk6IGJvb2xlYW4sIHJlc291cmNlVHlwZTogc3RyaW5nIHwgbnVsbH0+LCBzdGF0ZTogXCJzdWJzY3JpYmVkXCIgfCBcInN1YnNjcmliaW5nXCIgfCBcInVuc3Vic2NyaWJlZFwifX0gUmVhbHRpbWUgc3RhdHVzLlxuICAgKi9cbiAgc3RhdHVzKCkge1xuICAgIHJldHVybiB7XG4gICAgICBjaGFubmVsczogdGhpcy5fY2hhbm5lbHMubWFwKCh7Y2hhbm5lbCwgcmVzb3VyY2VUeXBlLCBzdWJzY3JpcHRpb259KSA9PiAoe2NoYW5uZWwsIHJlYWR5OiBzdWJzY3JpcHRpb24uaXNSZWFkeSgpLCByZXNvdXJjZVR5cGV9KSksXG4gICAgICBzdGF0ZTogdGhpcy5fc3RhdGVcbiAgICB9XG4gIH1cblxuICAvKipcbiAgICogQXdhaXRzIGFsbCBlbnF1ZXVlZCBtZXNzYWdlIGFwcGxpZXMgYW5kIGFueSBzY2hlZHVsZWQgcHVsbCAodGVzdHMsIHNodXRkb3duIGZsb3dzKS5cbiAgICogQHJldHVybnMge1Byb21pc2U8dm9pZD59XG4gICAqL1xuICBhc3luYyB3YWl0Rm9yQXBwbGllZCgpIHtcbiAgICBhd2FpdCB0aGlzLl9hcHBseVByb21pc2VcbiAgICBpZiAodGhpcy5fc2NoZWR1bGVkUHVsbCkgYXdhaXQgdGhpcy5fc2NoZWR1bGVkUHVsbFxuICB9XG5cbiAgLyoqXG4gICAqIFJlc29sdmVzIHRoZSByZWFsdGltZSBjb25maWd1cmF0aW9uLCBmYWlsaW5nIGxvdWRseSB3aGVuIGl0IGlzIG1pc3NpbmcuXG4gICAqIEByZXR1cm5zIHtpbXBvcnQoXCIuLi9jb25maWd1cmF0aW9uLXR5cGVzLmpzXCIpLlZlbG9jaW91c1N5bmNDbGllbnRSZWFsdGltZUNvbmZpZ3VyYXRpb259IFJlYWx0aW1lIGNvbmZpZ3VyYXRpb24uXG4gICAqL1xuICByZWFsdGltZUNvbmZpZ3VyYXRpb24oKSB7XG4gICAgY29uc3QgcmVhbHRpbWUgPSB0aGlzLnN5bmNDbGllbnQuY29uZmlnLnJlYWx0aW1lXG5cbiAgICBpZiAoIXJlYWx0aW1lKSB7XG4gICAgICB0aHJvdyBuZXcgRXJyb3IoXCJzdWJzY3JpYmVSZWFsdGltZSByZXF1aXJlcyBhIHN5bmMuY2xpZW50LnJlYWx0aW1lIGNvbmZpZ3VyYXRpb24gYmxvY2sgd2l0aCBhIGNyZWF0ZUNsaWVudCBjYWxsYmFja1wiKVxuICAgIH1cblxuICAgIHJldHVybiByZWFsdGltZVxuICB9XG5cbiAgLyoqXG4gICAqIERlcml2ZXMgdGhlIGNoYW5uZWwgZGVzY3JpcHRvcnMgdG8gc3Vic2NyaWJlOiBtb2RlbC1sZXZlbCBzdGF0aWMgcmVhbHRpbWVcbiAgICogZGVjbGFyYXRpb25zIHBsdXMgdGhlIGNvbmZpZyBjaGFubmVscyBjYWxsYmFjaywgZmFpbGluZyBsb3VkbHkgd2hlbiBub25lIGV4aXN0LlxuICAgKiBAcGFyYW0gez99IGNvbnRleHQgLSBBcHAgY29udGV4dCBwYXNzZWQgdG8gdGhlIGNoYW5uZWxzIGNhbGxiYWNrLlxuICAgKiBAcmV0dXJucyB7UHJvbWlzZTxBcnJheTxWZWxvY2lvdXNTeW5jUmVhbHRpbWVDaGFubmVsRGVzY3JpcHRvcj4+fSBDaGFubmVsIGRlc2NyaXB0b3JzLlxuICAgKi9cbiAgYXN5bmMgY2hhbm5lbERlc2NyaXB0b3JzKGNvbnRleHQpIHtcbiAgICBjb25zdCByZWFsdGltZSA9IHRoaXMucmVhbHRpbWVDb25maWd1cmF0aW9uKClcbiAgICAvKiogQHR5cGUge0FycmF5PFZlbG9jaW91c1N5bmNSZWFsdGltZUNoYW5uZWxEZXNjcmlwdG9yPn0gKi9cbiAgICBjb25zdCBjaGFubmVsRGVzY3JpcHRvcnMgPSBbXVxuXG4gICAgZm9yIChjb25zdCBbcmVzb3VyY2VUeXBlLCByZXNvdXJjZUNvbmZpZ10gb2YgT2JqZWN0LmVudHJpZXModGhpcy5zeW5jQ2xpZW50LmNvbmZpZy5yZXNvdXJjZXMpKSB7XG4gICAgICBpZiAoIXJlc291cmNlQ29uZmlnLnJlYWx0aW1lKSBjb250aW51ZVxuXG4gICAgICBjaGFubmVsRGVzY3JpcHRvcnMucHVzaCh7Y2hhbm5lbDogcmVzb3VyY2VDb25maWcucmVhbHRpbWUuY2hhbm5lbCwgcGFyYW1zOiByZXNvdXJjZUNvbmZpZy5yZWFsdGltZS5wYXJhbXMsIHJlc291cmNlVHlwZX0pXG4gICAgfVxuXG4gICAgaWYgKHJlYWx0aW1lLmNoYW5uZWxzKSB7XG4gICAgICBjaGFubmVsRGVzY3JpcHRvcnMucHVzaCguLi5hd2FpdCByZWFsdGltZS5jaGFubmVscyhjb250ZXh0KSlcbiAgICB9XG5cbiAgICBpZiAoY2hhbm5lbERlc2NyaXB0b3JzLmxlbmd0aCA9PT0gMCkge1xuICAgICAgdGhyb3cgbmV3IEVycm9yKFwic3Vic2NyaWJlUmVhbHRpbWUgZm91bmQgbm8gcmVhbHRpbWUgY2hhbm5lbHMgLSBkZWNsYXJlIHN5bmMuY2xpZW50LnJlYWx0aW1lLmNoYW5uZWxzIG9yIHN0YXRpYyBzeW5jID0ge3JlYWx0aW1lOiB7Y2hhbm5lbH19IG9uIGEgbW9kZWxcIilcbiAgICB9XG5cbiAgICByZXR1cm4gY2hhbm5lbERlc2NyaXB0b3JzXG4gIH1cblxuICAvKipcbiAgICogQ2hhaW5zIG9uZSBwdXNoZWQgbWVzc2FnZSBvbnRvIHRoZSBzZXJpYWxpemVkIGFwcGx5IHF1ZXVlIHNvIGNoYW5nZXMgYXBwbHlcbiAgICogaW4gYXJyaXZhbCBvcmRlcjsgZmFpbHVyZXMgZ28gdG8gdGhlIHN5bmMgY2xpZW50J3MgZXJyb3IgcmVwb3J0aW5nLlxuICAgKiBAcGFyYW0ge3tib2R5OiA/LCByZXNvdXJjZVR5cGU6IHN0cmluZyB8IG51bGx9fSBhcmdzIC0gTWVzc2FnZSBhcmdzLlxuICAgKiBAcmV0dXJucyB7dm9pZH1cbiAgICovXG4gIGVucXVldWVBcHBseSh7Ym9keSwgcmVzb3VyY2VUeXBlfSkge1xuICAgIHRoaXMuX2FwcGx5UHJvbWlzZSA9IHRoaXMuX2FwcGx5UHJvbWlzZS50aGVuKGFzeW5jICgpID0+IHtcbiAgICAgIHRyeSB7XG4gICAgICAgIGF3YWl0IHRoaXMuYXBwbHlNZXNzYWdlKHtib2R5LCByZXNvdXJjZVR5cGV9KVxuICAgICAgfSBjYXRjaCAoZXJyb3IpIHtcbiAgICAgICAgdGhpcy5zeW5jQ2xpZW50LnJlcG9ydEVycm9yKC8qKiBAdHlwZSB7RXJyb3J9ICovIChlcnJvcikpXG4gICAgICB9XG4gICAgfSlcbiAgfVxuXG4gIC8qKlxuICAgKiBBcHBsaWVzIG9uZSBwdXNoZWQgbWVzc2FnZSB0aHJvdWdoIHRoZSBkZXJpdmVkIHJlc291cmNlIGFwcGxpZXI6IGRyb3BzXG4gICAqIG93bi1kZXZpY2UgbWVzc2FnZXMgYnkgZWNobyBvcmlnaW4sIGRlZmF1bHRzIHRoZSBjaGFubmVsJ3MgcmVzb3VyY2VUeXBlIG9udG9cbiAgICogZW52ZWxvcGVzIHdpdGhvdXQgb25lLCBhbmQgZmFpbHMgbG91ZGx5IG9uIHVua25vd24gcmVzb3VyY2UgdHlwZXMuXG4gICAqIEBwYXJhbSB7e2JvZHk6ID8sIHJlc291cmNlVHlwZTogc3RyaW5nIHwgbnVsbH19IGFyZ3MgLSBNZXNzYWdlIGFyZ3MuXG4gICAqIEByZXR1cm5zIHtQcm9taXNlPHZvaWQ+fVxuICAgKi9cbiAgYXN5bmMgYXBwbHlNZXNzYWdlKHtib2R5LCByZXNvdXJjZVR5cGV9KSB7XG4gICAgaWYgKCFib2R5IHx8IHR5cGVvZiBib2R5ICE9PSBcIm9iamVjdFwiIHx8IEFycmF5LmlzQXJyYXkoYm9keSkpIHtcbiAgICAgIHRocm93IG5ldyBFcnJvcihgUmVhbHRpbWUgc3luYyBtZXNzYWdlcyBtdXN0IGJlIGVudmVsb3BlIG9iamVjdHMsIGdvdDogJHtKU09OLnN0cmluZ2lmeShib2R5KX1gKVxuICAgIH1cblxuICAgIGNvbnN0IHJlYWx0aW1lID0gdGhpcy5yZWFsdGltZUNvbmZpZ3VyYXRpb24oKVxuXG4gICAgaWYgKHJlYWx0aW1lLmxvY2FsT3JpZ2luICYmIGJvZHkuZWNob09yaWdpbiAhPT0gdW5kZWZpbmVkICYmIGJvZHkuZWNob09yaWdpbiAhPT0gbnVsbCkge1xuICAgICAgY29uc3QgbG9jYWxPcmlnaW4gPSBTdHJpbmcoYXdhaXQgcmVhbHRpbWUubG9jYWxPcmlnaW4oKSlcblxuICAgICAgaWYgKFN0cmluZyhib2R5LmVjaG9PcmlnaW4pID09PSBsb2NhbE9yaWdpbikgcmV0dXJuXG4gICAgfVxuXG4gICAgY29uc3Qgc3luY1BheWxvYWRzID0gQXJyYXkuaXNBcnJheShib2R5LnN5bmNzKSA/IGJvZHkuc3luY3MgOiBbYm9keV1cbiAgICBjb25zdCBhcHBseVN5bmMgPSB0aGlzLnN5bmNDbGllbnQucmVtb3RlQXBwbHlTeW5jKHtzb3VyY2U6IFwicmVtb3RlIGNoYW5nZVwifSlcblxuICAgIGZvciAoY29uc3Qgc3luY1BheWxvYWQgb2Ygc3luY1BheWxvYWRzKSB7XG4gICAgICBjb25zdCBzeW5jID0gU3luY0FwaUNsaWVudC5zeW5jRW52ZWxvcGVGcm9tUGF5bG9hZCh7cmVzb3VyY2VUeXBlLCAuLi5zeW5jUGF5bG9hZH0pXG5cbiAgICAgIGF3YWl0IGFwcGx5U3luYyhzeW5jKVxuICAgIH1cbiAgfVxuXG4gIC8qKlxuICAgKiBTY2hlZHVsZXMgYSBjb2FsZXNjZWQgYmFja2dyb3VuZCBwdWxsIGNsb3Npbmcgb2ZmbGluZSBnYXBzIGFmdGVyXG4gICAqIChyZSlzdWJzY3JpcHRpb24gcmVhZGluZXNzLiBSZXN1bWVzIGFycml2aW5nIHdoaWxlIGEgcHVsbCBpcyBhbHJlYWR5XG4gICAqIHNjaGVkdWxlZCBvciBpbiBmbGlnaHQgY29hbGVzY2UgaW50byB0aGF0IHB1bGwgaW5zdGVhZCBvZiBzdGFja2luZy5cbiAgICogQHJldHVybnMge3ZvaWR9XG4gICAqL1xuICBzY2hlZHVsZVB1bGwoKSB7XG4gICAgaWYgKHRoaXMucmVhbHRpbWVDb25maWd1cmF0aW9uKCkucHVsbE9uUmVjb25uZWN0ID09PSBmYWxzZSkgcmV0dXJuXG5cbiAgICB0aGlzLl9zY2hlZHVsZWRQdWxsIHx8PSAoYXN5bmMgKCkgPT4ge1xuICAgICAgdHJ5IHtcbiAgICAgICAgYXdhaXQgdGhpcy5zeW5jQ2xpZW50LnB1bGwoKVxuICAgICAgfSBjYXRjaCAoZXJyb3IpIHtcbiAgICAgICAgdGhpcy5zeW5jQ2xpZW50LnJlcG9ydEVycm9yKC8qKiBAdHlwZSB7RXJyb3J9ICovIChlcnJvcikpXG4gICAgICB9IGZpbmFsbHkge1xuICAgICAgICB0aGlzLl9zY2hlZHVsZWRQdWxsID0gbnVsbFxuICAgICAgfVxuICAgIH0pKClcbiAgfVxufVxuIl19
@@ -7,6 +7,15 @@
7
7
  * @property {string} resourceType - Resource/model name the scope was declared for.
8
8
  */
9
9
 
10
+ /**
11
+ * Static realtime channel declaration on a model's `static sync`, for channels
12
+ * whose name and params are static. Channels needing runtime params (like
13
+ * eventId) belong in the `sync.client.realtime.channels` callback instead.
14
+ * @typedef {object} ModelSyncRealtimeDeclaration
15
+ * @property {string} channel - Server channel name to subscribe.
16
+ * @property {Record<string, ?>} [params] - Static subscribe params. The framework injects `authenticationToken` automatically.
17
+ */
18
+
10
19
  /**
11
20
  * Declarative per-resource sync policy.
12
21
  * @typedef {object} SyncClientResourceConfig
@@ -20,6 +29,7 @@
20
29
  * @property {"upsert" | ((args: {operation: "create" | "update" | "destroy", record: ?}) => string)} [syncType] - Maps a mutation operation to a sync type. The "upsert" flag queues creates and updates as "update" rows (the server upserts by resource id) and destroys as "delete". Defaults to the operation name with destroy mapped to "delete".
21
30
  * @property {(args: {operation: "create" | "update" | "destroy", record: ?}) => Record<string, ?>} [trackedData] - Custom queued-payload builder for tracked mutations.
22
31
  * @property {boolean | {operations: Array<"create" | "update" | "destroy">}} [track] - Enables automatic mutation tracking through model lifecycle callbacks.
32
+ * @property {ModelSyncRealtimeDeclaration} [realtime] - Static realtime channel this resource subscribes through `subscribeRealtime(...)`.
23
33
  */
24
34
 
25
35
  /**
@@ -36,6 +46,7 @@
36
46
  * @property {"upsert" | ((args: {operation: "create" | "update" | "destroy", record: ?}) => string)} [syncType] - Sync type flag or mapper (see SyncClientResourceConfig).
37
47
  * @property {boolean | Array<"create" | "update" | "destroy"> | {operations: Array<"create" | "update" | "destroy">}} [track] - Enables automatic mutation tracking; an array is shorthand for {operations}.
38
48
  * @property {(args: {operation: "create" | "update" | "destroy", record: ?}) => Record<string, ?>} [trackedData] - Custom queued-payload builder for tracked mutations.
49
+ * @property {ModelSyncRealtimeDeclaration} [realtime] - Static realtime channel this resource subscribes through `subscribeRealtime(...)`; use the `sync.client.realtime.channels` callback for channels needing runtime params.
39
50
  */
40
51
 
41
52
  /** @typedef {boolean | ModelSyncDeclarationConfig} ModelSyncDeclaration */
@@ -63,6 +74,7 @@
63
74
  * @property {(error: Error) => void} [onError] - Reports background replay/pull failures. Defaults to rethrowing.
64
75
  * @property {(payload: import("./sync-api-client-types.js").SyncChangesRequest & {scope: SerializedSyncScope}) => Promise<import("./sync-api-client-types.js").SyncChangesResponse>} postChanges - Posts one changes request.
65
76
  * @property {(payload: {authenticationToken: string, syncs: Array<Record<string, ?>>}) => Promise<import("./sync-api-client-types.js").SyncReplayResponse>} postReplay - Posts one replay request.
77
+ * @property {import("../configuration-types.js").VelociousSyncClientRealtimeConfiguration} [realtime] - Realtime push configuration consumed by `subscribeRealtime(...)`.
66
78
  * @property {Record<string, SyncClientResourceConfig>} resources - Derived resource policies keyed by resource/model name.
67
79
  * @property {?} syncModel - Local pending-sync model class.
68
80
  */
@@ -6,6 +6,7 @@ import restArgsError from "../utils/rest-args-error.js"
6
6
 
7
7
  import {serializedScopeFromQuery} from "./query-scope.js"
8
8
  import SyncApiClient from "./sync-api-client.js"
9
+ import SyncRealtimeBridge from "./sync-realtime-bridge.js"
9
10
  import SyncScopeStore from "./sync-scope-store.js"
10
11
  import {currentSyncClient, setCurrentSyncClient} from "./sync-client-registry.js"
11
12
 
@@ -87,10 +88,13 @@ export default class SyncClient {
87
88
  onError: clientConfiguration.onError,
88
89
  postChanges: transportPoster({path: `${clientConfiguration.mountPath}/changes`, transport: clientConfiguration.transport}),
89
90
  postReplay: transportPoster({path: `${clientConfiguration.mountPath}/replay`, transport: clientConfiguration.transport}),
91
+ realtime: clientConfiguration.realtime,
90
92
  resources,
91
93
  syncModel: resolvedSyncModel
92
94
  }
93
95
  this._clientNumber = ++clientCounter
96
+ /** @type {SyncRealtimeBridge | null} */
97
+ this._realtimeBridge = null
94
98
  /** @type {import("./sync-scope-store.js").default | null} */
95
99
  this._scopeStore = scopeStore || null
96
100
  /** @type {Promise<void> | null} */
@@ -265,26 +269,7 @@ export default class SyncClient {
265
269
  await SyncApiClient.singleFlight(`velocious-sync-client-pull-${this._clientNumber}`, async () => {
266
270
  const authenticationToken = await this.config.authenticationToken()
267
271
  const scopeStore = this.scopeStore()
268
- const pullResourceConfigs = this.pullResourceConfigs()
269
- const applier = SyncApiClient.resourceApplier(pullResourceConfigs, (record) => {
270
- this._remoteApplyRecords.add(record)
271
-
272
- return () => this._remoteApplyRecords.delete(record)
273
- })
274
- /**
275
- * Applies one pulled change, failing loudly instead of silently skipping unconfigured resources.
276
- * @param {import("./sync-api-client-types.js").SyncChangeEnvelope} sync - Pulled change.
277
- * @returns {Promise<import("./sync-api-client-types.js").SyncChangeApplyResult>} Apply result.
278
- */
279
- const applySync = async (sync) => {
280
- const resourceType = sync.resourceType()
281
-
282
- if (!resourceType || !pullResourceConfigs[resourceType]) {
283
- throw new Error(`No sync resource with pull attributes configured for pulled change: ${String(resourceType)}`)
284
- }
285
-
286
- return await applier(sync)
287
- }
272
+ const applySync = this.remoteApplySync()
288
273
  const result = {
289
274
  changed: false,
290
275
  pages: 0,
@@ -324,6 +309,78 @@ export default class SyncClient {
324
309
  return combinedResult
325
310
  }
326
311
 
312
+ /**
313
+ * Builds the derived remote-change applier shared by pulls and realtime pushes:
314
+ * applies through the declared resource configs, registers each written record
315
+ * for echo suppression (tracked resources do not re-queue applied changes), and
316
+ * fails loudly instead of silently skipping unconfigured resources.
317
+ * @param {{source?: string}} [args] - Error context describing where the change came from.
318
+ * @returns {(sync: import("./sync-api-client-types.js").SyncChangeEnvelope) => Promise<import("./sync-api-client-types.js").SyncChangeApplyResult>} Loud remote-change applier.
319
+ */
320
+ remoteApplySync({source = "pulled change"} = {}) {
321
+ const pullResourceConfigs = this.pullResourceConfigs()
322
+ const applier = SyncApiClient.resourceApplier(pullResourceConfigs, (record) => {
323
+ this._remoteApplyRecords.add(record)
324
+
325
+ return () => this._remoteApplyRecords.delete(record)
326
+ })
327
+
328
+ return async (sync) => {
329
+ const resourceType = sync.resourceType()
330
+
331
+ if (!resourceType || !pullResourceConfigs[resourceType]) {
332
+ throw new Error(`No sync resource with pull attributes configured for ${source}: ${String(resourceType)}`)
333
+ }
334
+
335
+ return await applier(sync)
336
+ }
337
+ }
338
+
339
+ /**
340
+ * Subscribes the derived realtime channels so pushed websocket changes apply
341
+ * through the same derived applier as pulls (idempotent, single-flighted).
342
+ * @param {?} [context] - App context passed to the `sync.client.realtime.channels` callback (runtime values like eventId).
343
+ * @returns {Promise<void>}
344
+ */
345
+ async subscribeRealtime(context) {
346
+ await this.realtimeBridge().subscribe(context)
347
+ }
348
+
349
+ /**
350
+ * Unsubscribes the realtime channels and disconnects the websocket client (idempotent).
351
+ * @returns {Promise<void>}
352
+ */
353
+ async unsubscribeRealtime() {
354
+ await this.realtimeBridge().unsubscribe()
355
+ }
356
+
357
+ /**
358
+ * Reports the realtime subscription state and per-channel readiness.
359
+ * @returns {ReturnType<SyncRealtimeBridge["status"]>} Realtime status.
360
+ */
361
+ realtimeStatus() {
362
+ return this.realtimeBridge().status()
363
+ }
364
+
365
+ /**
366
+ * Awaits all pending realtime message applies and any scheduled
367
+ * pull-on-reconnect (useful in tests and shutdown flows).
368
+ * @returns {Promise<void>}
369
+ */
370
+ async waitForRealtimeApplied() {
371
+ await this.realtimeBridge().waitForApplied()
372
+ }
373
+
374
+ /**
375
+ * Returns the lazily built realtime bridge.
376
+ * @returns {SyncRealtimeBridge} Realtime bridge.
377
+ */
378
+ realtimeBridge() {
379
+ this._realtimeBridge ||= new SyncRealtimeBridge({syncClient: this})
380
+
381
+ return this._realtimeBridge
382
+ }
383
+
327
384
  /**
328
385
  * Queues a local model change as a pending sync row and schedules an immediate
329
386
  * replay attempt (kept pending while offline or when the backend rejects it).
@@ -491,11 +548,11 @@ function resourceConfigFromSyncDeclaration({declaration, modelClass, resourceTyp
491
548
  throw new Error(`${resourceType} static sync must be true or a sync declaration object, got: ${String(declaration)}`)
492
549
  }
493
550
 
494
- const {afterApply, attributes, booleanAttributes, findRecord, findRecordForDelete, localOnlyAttributes, syncType, track, trackedData, ...restDeclaration} = normalizedDeclaration
551
+ const {afterApply, attributes, booleanAttributes, findRecord, findRecordForDelete, localOnlyAttributes, realtime, syncType, track, trackedData, ...restDeclaration} = normalizedDeclaration
495
552
  const unknownKeys = Object.keys(restDeclaration)
496
553
 
497
554
  if (unknownKeys.length > 0) {
498
- throw new Error(`${resourceType} static sync received unknown keys: ${unknownKeys.join(", ")} (supported: afterApply, attributes, booleanAttributes, findRecord, findRecordForDelete, localOnlyAttributes, syncType, track, trackedData)`)
555
+ throw new Error(`${resourceType} static sync received unknown keys: ${unknownKeys.join(", ")} (supported: afterApply, attributes, booleanAttributes, findRecord, findRecordForDelete, localOnlyAttributes, realtime, syncType, track, trackedData)`)
499
556
  }
500
557
  if (syncType !== undefined && typeof syncType !== "function" && syncType !== "upsert") {
501
558
  throw new Error(`${resourceType} static sync syncType must be a function or the string "upsert", got: ${String(syncType)}`)
@@ -511,6 +568,7 @@ function resourceConfigFromSyncDeclaration({declaration, modelClass, resourceTyp
511
568
  findRecordForDelete,
512
569
  localOnlyAttributes: mergedAttributeNames(derived.localOnlyAttributes, localOnlyAttributes),
513
570
  modelClass,
571
+ realtime,
514
572
  syncType,
515
573
  track: normalizedTrack(track),
516
574
  trackedData