@syncular/react 0.4.1 → 0.5.0

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.
@@ -1,276 +0,0 @@
1
- /**
2
- * Live-query churn hardening — the three cheap levers the query hooks share
3
- * (block 4 "live-query churn" item). Under constant sync churn a naive live
4
- * query re-renders and re-queries once per invalidation event; these levers
5
- * cap both without reaching for IVM:
6
- *
7
- * 1. {@link reconcileRows} — result stability. After a re-run, compare the new
8
- * result to the previous one. Whole-result equal → the caller skips
9
- * `setRows` entirely (zero re-render). Otherwise build the new array but
10
- * REUSE the previous row object for every row whose content is unchanged,
11
- * so `React.memo`'d row components keyed by row identity skip re-render.
12
- * 2. {@link FrameScheduler} — frame-coalesced re-query scheduling. Many
13
- * invalidation events between paints collapse to ONE re-run per query.
14
- * 3. scope-key filtering lives in {@link ../use-raw-sql} `eventMatches`
15
- * (it needs the event + the hook's options), documented there.
16
- *
17
- * Row identity mechanism (the honest key): the hook knows no primary key —
18
- * rows are plain JSON-able objects out of SQLite — so per-row content equality
19
- * IS the identity. We hash each row once with a stable JSON serialization
20
- * (sorted keys, so column order can't spuriously differ) and match by index:
21
- * a live query's ORDER BY makes index the stable position, and an unchanged
22
- * row at position i keeps its object so memoized components skip. This is O(n)
23
- * in the row count with one string hash per row — bounded and measured (~0.2ms
24
- * for 1k narrow rows in bun; see query-churn.test.ts).
25
- */
26
- /**
27
- * A stable content hash for one row: JSON with keys sorted, so two rows with
28
- * the same columns in a different order hash equal (SQLite projection order is
29
- * stable per query, but sorting removes any dependence on it and is cheap for
30
- * the narrow rows a row-component renders). Uint8Array values (rare in a
31
- * projection) serialize by byte view so a fresh copy of equal bytes hashes
32
- * equal rather than to `{}`.
33
- */
34
- export function hashRow(row) {
35
- return JSON.stringify(row, replacer);
36
- }
37
- function replacer(_key, value) {
38
- if (value instanceof Uint8Array)
39
- return { __u8: [...value] };
40
- if (value !== null &&
41
- typeof value === 'object' &&
42
- !Array.isArray(value) &&
43
- Object.getPrototypeOf(value) === Object.prototype) {
44
- // Sort keys so column/field order never spuriously changes the hash.
45
- const sorted = {};
46
- for (const k of Object.keys(value).sort()) {
47
- sorted[k] = value[k];
48
- }
49
- return sorted;
50
- }
51
- return value;
52
- }
53
- export function hashRows(rows) {
54
- const hashes = new Array(rows.length);
55
- for (let i = 0; i < rows.length; i++)
56
- hashes[i] = hashRow(rows[i]);
57
- return { rows, hashes };
58
- }
59
- /**
60
- * Reconcile a freshly-queried result against the previous hashed result.
61
- * Returns `next: undefined` when nothing changed at all; otherwise a new
62
- * hashed result whose row objects are the PREVIOUS objects wherever content is
63
- * unchanged (matched by index), so identity-keyed memo components skip.
64
- */
65
- export function reconcileRows(prev, fresh) {
66
- const freshHashes = new Array(fresh.length);
67
- for (let i = 0; i < fresh.length; i++)
68
- freshHashes[i] = hashRow(fresh[i]);
69
- if (prev !== undefined && prev.rows.length === fresh.length) {
70
- let identical = true;
71
- for (let i = 0; i < fresh.length; i++) {
72
- if (prev.hashes[i] !== freshHashes[i]) {
73
- identical = false;
74
- break;
75
- }
76
- }
77
- if (identical)
78
- return { next: undefined };
79
- }
80
- // Changed: reuse the previous row object at each index whose hash matches,
81
- // so unchanged rows keep their identity for memoized row components.
82
- const rows = new Array(fresh.length);
83
- for (let i = 0; i < fresh.length; i++) {
84
- const reuse = prev !== undefined &&
85
- i < prev.rows.length &&
86
- prev.hashes[i] === freshHashes[i];
87
- // `i < fresh.length` bounds this loop, so `fresh[i]` is defined; the reuse
88
- // branch is additionally guarded by `i < prev.rows.length`.
89
- rows[i] = reuse ? prev.rows[i] : fresh[i];
90
- }
91
- return { next: { rows, hashes: freshHashes } };
92
- }
93
- /**
94
- * A per-query re-run scheduler that coalesces bursts of invalidation events
95
- * into ONE run per paint. Multiple `schedule()` calls before the next flush
96
- * run the callback once. A `schedule()` that arrives WHILE the callback is
97
- * running (re-entrant, or an event during an async re-query) marks dirty and
98
- * runs the callback exactly once more after — never lost, never concurrent.
99
- *
100
- * Timing source: `requestAnimationFrame` when the host has it AND the document
101
- * is visible (a real browser paints one frame; the coalescing window is a
102
- * frame), else a microtask via a resolved promise (bun tests have no rAF —
103
- * this keeps them deterministic and timer-free, honoring the no-timers
104
- * doctrine: it's a readiness turn, not a wall-clock sleep). {@link flush} runs
105
- * any pending callback synchronously for tests, so no arbitrary sleeps are
106
- * needed to observe coalescing.
107
- *
108
- * Hidden documents: browsers SUSPEND rAF while a page is hidden (background
109
- * tab, occluded webview, headless embed), so a frame parked there fires only
110
- * when the page becomes visible again — and a page that is never visible would
111
- * freeze its live queries forever while invalidations keep arriving. Two
112
- * guards keep the schedule honest: a `schedule()` issued while hidden goes to
113
- * the microtask boundary (there is no paint to coalesce against anyway), and a
114
- * visible → hidden transition re-dispatches any frame already parked in rAF to
115
- * a microtask (the stale rAF callback later no-ops via the `#scheduled` guard
116
- * in {@link #fire}).
117
- */
118
- export class FrameScheduler {
119
- #scheduled = false;
120
- #running = false;
121
- #dirty = false;
122
- #callback;
123
- constructor(callback) {
124
- this.#callback = callback;
125
- liveSchedulers.add(this);
126
- hookVisibility();
127
- }
128
- /** Request a run. Coalesces until the next frame/microtask boundary. */
129
- schedule() {
130
- if (this.#running) {
131
- // An event arrived during a run — remember it and re-run once after.
132
- this.#dirty = true;
133
- return;
134
- }
135
- if (this.#scheduled)
136
- return;
137
- this.#scheduled = true;
138
- scheduleFrame(() => this.#fire());
139
- }
140
- #fire() {
141
- // A stale dispatch must be a no-op: an rAF parked before the page went
142
- // hidden fires again on the visible transition, AFTER the microtask
143
- // fallback already ran the callback and cleared `#scheduled`.
144
- if (!this.#scheduled)
145
- return;
146
- this.#run();
147
- }
148
- /**
149
- * @internal — the visible → hidden transition hands a frame parked in the
150
- * (now suspended) rAF to the microtask boundary, so live queries keep
151
- * converging off-screen. A no-op when nothing is pending.
152
- */
153
- redispatchPending() {
154
- if (!this.#scheduled)
155
- return;
156
- queueMicrotask(() => this.#fire());
157
- }
158
- /**
159
- * Run the callback once, honoring the running/dirty contract: a `schedule()`
160
- * during the run marks `#dirty`, and on completion we re-schedule exactly
161
- * one more run — never lost, never concurrent. Returns the callback's result
162
- * (a Promise for the async host path) so `flush` can hand it to a test.
163
- */
164
- #run() {
165
- this.#scheduled = false;
166
- if (this.#callback === undefined)
167
- return;
168
- this.#running = true;
169
- this.#dirty = false;
170
- const done = () => {
171
- this.#running = false;
172
- if (this.#dirty && this.#callback !== undefined) {
173
- this.#dirty = false;
174
- // An invalidation landed mid-run: re-run once more, coalesced.
175
- this.schedule();
176
- }
177
- };
178
- let result;
179
- try {
180
- result = this.#callback();
181
- }
182
- catch {
183
- done();
184
- return;
185
- }
186
- if (result && typeof result.then === 'function') {
187
- return result.then(done, done);
188
- }
189
- done();
190
- return;
191
- }
192
- /**
193
- * Synchronously run any pending scheduled callback NOW (test determinism).
194
- * Returns whatever the callback returned (a Promise for the async host path)
195
- * so a test can await the re-query settling without a sleep. A no-op when
196
- * nothing is pending.
197
- */
198
- flush() {
199
- if (!this.#scheduled || this.#callback === undefined)
200
- return;
201
- return this.#run();
202
- }
203
- /** Drop the callback so a torn-down hook's pending frame is a no-op. */
204
- dispose() {
205
- this.#callback = undefined;
206
- liveSchedulers.delete(this);
207
- }
208
- }
209
- /**
210
- * Live schedulers, weakly tracked for the test-only flush below. A Set (not
211
- * WeakSet) so we can iterate; entries are removed on `dispose()`, so a mounted
212
- * hook holds at most one entry and unmount clears it.
213
- */
214
- const liveSchedulers = new Set();
215
- /**
216
- * TEST-ONLY: synchronously flush every live scheduler's pending frame, so a
217
- * test can observe the coalesced re-query without a wall-clock sleep (the
218
- * no-timers doctrine — a readiness flush, injected, not a timer). Returns a
219
- * Promise that settles when every flushed async re-query has settled.
220
- */
221
- export function flushQuerySchedulers() {
222
- const pending = [];
223
- for (const s of liveSchedulers) {
224
- const r = s.flush();
225
- if (r && typeof r.then === 'function') {
226
- pending.push(r);
227
- }
228
- }
229
- return Promise.all(pending).then(() => undefined);
230
- }
231
- function currentDocument() {
232
- return globalThis.document;
233
- }
234
- function documentHidden() {
235
- return currentDocument()?.visibilityState === 'hidden';
236
- }
237
- function scheduleFrame(cb) {
238
- // Read rAF per call (not cached at module load) so a test can install a
239
- // double around one scenario.
240
- const raf = typeof globalThis.requestAnimationFrame === 'function'
241
- ? globalThis.requestAnimationFrame.bind(globalThis)
242
- : undefined;
243
- if (raf !== undefined && !documentHidden()) {
244
- raf(cb);
245
- return;
246
- }
247
- // No rAF (bun test / worker) or a hidden document (rAF suspended): a
248
- // microtask is the deterministic, timer-free coalescing boundary.
249
- // Everything queued in the current synchronous run (a burst of emits) has
250
- // already called schedule() before this drains.
251
- queueMicrotask(cb);
252
- }
253
- /**
254
- * The document whose `visibilitychange` is currently hooked. Re-hooked when
255
- * the document identity changes (never in a browser — one document per page —
256
- * but each test double gets its own listener; stale listeners die with their
257
- * document). Registration is lazy (first scheduler construction) so importing
258
- * the module has no side effect.
259
- */
260
- let hookedDocument;
261
- function hookVisibility() {
262
- const doc = currentDocument();
263
- if (doc === undefined ||
264
- doc === hookedDocument ||
265
- typeof doc.addEventListener !== 'function') {
266
- return;
267
- }
268
- hookedDocument = doc;
269
- doc.addEventListener('visibilitychange', () => {
270
- if (doc.visibilityState !== 'hidden')
271
- return;
272
- // rAF is suspended from here on; hand every parked frame to a microtask.
273
- for (const s of liveSchedulers)
274
- s.redispatchPending();
275
- });
276
- }
@@ -1,317 +0,0 @@
1
- /**
2
- * Live-query churn hardening — the three cheap levers the query hooks share
3
- * (block 4 "live-query churn" item). Under constant sync churn a naive live
4
- * query re-renders and re-queries once per invalidation event; these levers
5
- * cap both without reaching for IVM:
6
- *
7
- * 1. {@link reconcileRows} — result stability. After a re-run, compare the new
8
- * result to the previous one. Whole-result equal → the caller skips
9
- * `setRows` entirely (zero re-render). Otherwise build the new array but
10
- * REUSE the previous row object for every row whose content is unchanged,
11
- * so `React.memo`'d row components keyed by row identity skip re-render.
12
- * 2. {@link FrameScheduler} — frame-coalesced re-query scheduling. Many
13
- * invalidation events between paints collapse to ONE re-run per query.
14
- * 3. scope-key filtering lives in {@link ../use-raw-sql} `eventMatches`
15
- * (it needs the event + the hook's options), documented there.
16
- *
17
- * Row identity mechanism (the honest key): the hook knows no primary key —
18
- * rows are plain JSON-able objects out of SQLite — so per-row content equality
19
- * IS the identity. We hash each row once with a stable JSON serialization
20
- * (sorted keys, so column order can't spuriously differ) and match by index:
21
- * a live query's ORDER BY makes index the stable position, and an unchanged
22
- * row at position i keeps its object so memoized components skip. This is O(n)
23
- * in the row count with one string hash per row — bounded and measured (~0.2ms
24
- * for 1k narrow rows in bun; see query-churn.test.ts).
25
- */
26
-
27
- /**
28
- * A stable content hash for one row: JSON with keys sorted, so two rows with
29
- * the same columns in a different order hash equal (SQLite projection order is
30
- * stable per query, but sorting removes any dependence on it and is cheap for
31
- * the narrow rows a row-component renders). Uint8Array values (rare in a
32
- * projection) serialize by byte view so a fresh copy of equal bytes hashes
33
- * equal rather than to `{}`.
34
- */
35
- export function hashRow(row: unknown): string {
36
- return JSON.stringify(row, replacer);
37
- }
38
-
39
- function replacer(_key: string, value: unknown): unknown {
40
- if (value instanceof Uint8Array) return { __u8: [...value] };
41
- if (
42
- value !== null &&
43
- typeof value === 'object' &&
44
- !Array.isArray(value) &&
45
- Object.getPrototypeOf(value) === Object.prototype
46
- ) {
47
- // Sort keys so column/field order never spuriously changes the hash.
48
- const sorted: Record<string, unknown> = {};
49
- for (const k of Object.keys(value as Record<string, unknown>).sort()) {
50
- sorted[k] = (value as Record<string, unknown>)[k];
51
- }
52
- return sorted;
53
- }
54
- return value;
55
- }
56
-
57
- /** The precomputed hash carrier for the previous result, so we hash once. */
58
- export interface HashedRows<Row> {
59
- readonly rows: readonly Row[];
60
- readonly hashes: readonly string[];
61
- }
62
-
63
- export function hashRows<Row>(rows: readonly Row[]): HashedRows<Row> {
64
- const hashes = new Array<string>(rows.length);
65
- for (let i = 0; i < rows.length; i++) hashes[i] = hashRow(rows[i]);
66
- return { rows, hashes };
67
- }
68
-
69
- export interface ReconcileResult<Row> {
70
- /**
71
- * `undefined` → the whole result is unchanged; the caller MUST NOT call
72
- * setRows (zero re-render, lever 1a). Otherwise the reconciled array to set,
73
- * with previous row objects reused wherever a row's content was unchanged
74
- * (lever 1b).
75
- */
76
- readonly next: HashedRows<Row> | undefined;
77
- }
78
-
79
- /**
80
- * Reconcile a freshly-queried result against the previous hashed result.
81
- * Returns `next: undefined` when nothing changed at all; otherwise a new
82
- * hashed result whose row objects are the PREVIOUS objects wherever content is
83
- * unchanged (matched by index), so identity-keyed memo components skip.
84
- */
85
- export function reconcileRows<Row>(
86
- prev: HashedRows<Row> | undefined,
87
- fresh: readonly Row[],
88
- ): ReconcileResult<Row> {
89
- const freshHashes = new Array<string>(fresh.length);
90
- for (let i = 0; i < fresh.length; i++) freshHashes[i] = hashRow(fresh[i]);
91
-
92
- if (prev !== undefined && prev.rows.length === fresh.length) {
93
- let identical = true;
94
- for (let i = 0; i < fresh.length; i++) {
95
- if (prev.hashes[i] !== freshHashes[i]) {
96
- identical = false;
97
- break;
98
- }
99
- }
100
- if (identical) return { next: undefined };
101
- }
102
-
103
- // Changed: reuse the previous row object at each index whose hash matches,
104
- // so unchanged rows keep their identity for memoized row components.
105
- const rows = new Array<Row>(fresh.length);
106
- for (let i = 0; i < fresh.length; i++) {
107
- const reuse =
108
- prev !== undefined &&
109
- i < prev.rows.length &&
110
- prev.hashes[i] === freshHashes[i];
111
- // `i < fresh.length` bounds this loop, so `fresh[i]` is defined; the reuse
112
- // branch is additionally guarded by `i < prev.rows.length`.
113
- rows[i] = reuse ? (prev.rows[i] as Row) : (fresh[i] as Row);
114
- }
115
- return { next: { rows, hashes: freshHashes } };
116
- }
117
-
118
- /**
119
- * A per-query re-run scheduler that coalesces bursts of invalidation events
120
- * into ONE run per paint. Multiple `schedule()` calls before the next flush
121
- * run the callback once. A `schedule()` that arrives WHILE the callback is
122
- * running (re-entrant, or an event during an async re-query) marks dirty and
123
- * runs the callback exactly once more after — never lost, never concurrent.
124
- *
125
- * Timing source: `requestAnimationFrame` when the host has it AND the document
126
- * is visible (a real browser paints one frame; the coalescing window is a
127
- * frame), else a microtask via a resolved promise (bun tests have no rAF —
128
- * this keeps them deterministic and timer-free, honoring the no-timers
129
- * doctrine: it's a readiness turn, not a wall-clock sleep). {@link flush} runs
130
- * any pending callback synchronously for tests, so no arbitrary sleeps are
131
- * needed to observe coalescing.
132
- *
133
- * Hidden documents: browsers SUSPEND rAF while a page is hidden (background
134
- * tab, occluded webview, headless embed), so a frame parked there fires only
135
- * when the page becomes visible again — and a page that is never visible would
136
- * freeze its live queries forever while invalidations keep arriving. Two
137
- * guards keep the schedule honest: a `schedule()` issued while hidden goes to
138
- * the microtask boundary (there is no paint to coalesce against anyway), and a
139
- * visible → hidden transition re-dispatches any frame already parked in rAF to
140
- * a microtask (the stale rAF callback later no-ops via the `#scheduled` guard
141
- * in {@link #fire}).
142
- */
143
- export class FrameScheduler {
144
- #scheduled = false;
145
- #running = false;
146
- #dirty = false;
147
- #callback: (() => void | Promise<void>) | undefined;
148
-
149
- constructor(callback: () => void | Promise<void>) {
150
- this.#callback = callback;
151
- liveSchedulers.add(this);
152
- hookVisibility();
153
- }
154
-
155
- /** Request a run. Coalesces until the next frame/microtask boundary. */
156
- schedule(): void {
157
- if (this.#running) {
158
- // An event arrived during a run — remember it and re-run once after.
159
- this.#dirty = true;
160
- return;
161
- }
162
- if (this.#scheduled) return;
163
- this.#scheduled = true;
164
- scheduleFrame(() => this.#fire());
165
- }
166
-
167
- #fire(): void {
168
- // A stale dispatch must be a no-op: an rAF parked before the page went
169
- // hidden fires again on the visible transition, AFTER the microtask
170
- // fallback already ran the callback and cleared `#scheduled`.
171
- if (!this.#scheduled) return;
172
- this.#run();
173
- }
174
-
175
- /**
176
- * @internal — the visible → hidden transition hands a frame parked in the
177
- * (now suspended) rAF to the microtask boundary, so live queries keep
178
- * converging off-screen. A no-op when nothing is pending.
179
- */
180
- redispatchPending(): void {
181
- if (!this.#scheduled) return;
182
- queueMicrotask(() => this.#fire());
183
- }
184
-
185
- /**
186
- * Run the callback once, honoring the running/dirty contract: a `schedule()`
187
- * during the run marks `#dirty`, and on completion we re-schedule exactly
188
- * one more run — never lost, never concurrent. Returns the callback's result
189
- * (a Promise for the async host path) so `flush` can hand it to a test.
190
- */
191
- #run(): void | Promise<void> {
192
- this.#scheduled = false;
193
- if (this.#callback === undefined) return;
194
- this.#running = true;
195
- this.#dirty = false;
196
- const done = () => {
197
- this.#running = false;
198
- if (this.#dirty && this.#callback !== undefined) {
199
- this.#dirty = false;
200
- // An invalidation landed mid-run: re-run once more, coalesced.
201
- this.schedule();
202
- }
203
- };
204
- let result: void | Promise<void>;
205
- try {
206
- result = this.#callback();
207
- } catch {
208
- done();
209
- return;
210
- }
211
- if (result && typeof (result as Promise<void>).then === 'function') {
212
- return (result as Promise<void>).then(done, done);
213
- }
214
- done();
215
- return;
216
- }
217
-
218
- /**
219
- * Synchronously run any pending scheduled callback NOW (test determinism).
220
- * Returns whatever the callback returned (a Promise for the async host path)
221
- * so a test can await the re-query settling without a sleep. A no-op when
222
- * nothing is pending.
223
- */
224
- flush(): void | Promise<void> {
225
- if (!this.#scheduled || this.#callback === undefined) return;
226
- return this.#run();
227
- }
228
-
229
- /** Drop the callback so a torn-down hook's pending frame is a no-op. */
230
- dispose(): void {
231
- this.#callback = undefined;
232
- liveSchedulers.delete(this);
233
- }
234
- }
235
-
236
- /**
237
- * Live schedulers, weakly tracked for the test-only flush below. A Set (not
238
- * WeakSet) so we can iterate; entries are removed on `dispose()`, so a mounted
239
- * hook holds at most one entry and unmount clears it.
240
- */
241
- const liveSchedulers = new Set<FrameScheduler>();
242
-
243
- /**
244
- * TEST-ONLY: synchronously flush every live scheduler's pending frame, so a
245
- * test can observe the coalesced re-query without a wall-clock sleep (the
246
- * no-timers doctrine — a readiness flush, injected, not a timer). Returns a
247
- * Promise that settles when every flushed async re-query has settled.
248
- */
249
- export function flushQuerySchedulers(): Promise<void> {
250
- const pending: Array<Promise<void>> = [];
251
- for (const s of liveSchedulers) {
252
- const r = s.flush();
253
- if (r && typeof (r as Promise<void>).then === 'function') {
254
- pending.push(r as Promise<void>);
255
- }
256
- }
257
- return Promise.all(pending).then(() => undefined);
258
- }
259
-
260
- /** The document surface this module reads — kept structural so the package
261
- * needs no DOM lib types and tests can inject a double. */
262
- interface DocumentLike {
263
- readonly visibilityState?: string;
264
- addEventListener?: (type: string, listener: () => void) => void;
265
- }
266
-
267
- function currentDocument(): DocumentLike | undefined {
268
- return (globalThis as { document?: DocumentLike }).document;
269
- }
270
-
271
- function documentHidden(): boolean {
272
- return currentDocument()?.visibilityState === 'hidden';
273
- }
274
-
275
- function scheduleFrame(cb: () => void): void {
276
- // Read rAF per call (not cached at module load) so a test can install a
277
- // double around one scenario.
278
- const raf =
279
- typeof globalThis.requestAnimationFrame === 'function'
280
- ? globalThis.requestAnimationFrame.bind(globalThis)
281
- : undefined;
282
- if (raf !== undefined && !documentHidden()) {
283
- raf(cb);
284
- return;
285
- }
286
- // No rAF (bun test / worker) or a hidden document (rAF suspended): a
287
- // microtask is the deterministic, timer-free coalescing boundary.
288
- // Everything queued in the current synchronous run (a burst of emits) has
289
- // already called schedule() before this drains.
290
- queueMicrotask(cb);
291
- }
292
-
293
- /**
294
- * The document whose `visibilitychange` is currently hooked. Re-hooked when
295
- * the document identity changes (never in a browser — one document per page —
296
- * but each test double gets its own listener; stale listeners die with their
297
- * document). Registration is lazy (first scheduler construction) so importing
298
- * the module has no side effect.
299
- */
300
- let hookedDocument: DocumentLike | undefined;
301
-
302
- function hookVisibility(): void {
303
- const doc = currentDocument();
304
- if (
305
- doc === undefined ||
306
- doc === hookedDocument ||
307
- typeof doc.addEventListener !== 'function'
308
- ) {
309
- return;
310
- }
311
- hookedDocument = doc;
312
- doc.addEventListener('visibilitychange', () => {
313
- if (doc.visibilityState !== 'hidden') return;
314
- // rAF is suspended from here on; hand every parked frame to a microtask.
315
- for (const s of liveSchedulers) s.redispatchPending();
316
- });
317
- }