@spooky-sync/client-solid2 0.0.1-canary.200

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,284 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+ import { createEffect, createRoot, createSignal, flush } from 'solid-js';
3
+ import { createQuery } from '../create-query';
4
+ import { SyncedDb } from '../../index';
5
+
6
+ const tick = () => new Promise<void>((r) => setTimeout(r, 0));
7
+ const settle = async () => {
8
+ await tick();
9
+ flush();
10
+ await tick();
11
+ };
12
+
13
+ type Emission = Record<string, any>[];
14
+
15
+ function mockEngine() {
16
+ const subs = new Map<string, (e: Emission) => void>();
17
+ const statusSubs = new Map<string, (s: string) => void>();
18
+ const unsubscribed: string[] = [];
19
+ const deregistered: string[] = [];
20
+ const sp00ky = {
21
+ subscribe: vi.fn(async (hash: string, cb: (e: Emission) => void, _o?: unknown) => {
22
+ subs.set(hash, cb);
23
+ return () => {
24
+ subs.delete(hash);
25
+ unsubscribed.push(hash);
26
+ };
27
+ }),
28
+ subscribeQueryStatus: vi.fn(
29
+ (hash: string, cb: (s: string) => void, o?: { immediate?: boolean }) => {
30
+ statusSubs.set(hash, cb);
31
+ if (o?.immediate) cb('idle');
32
+ return () => statusSubs.delete(hash);
33
+ }
34
+ ),
35
+ deregisterQuery: vi.fn((hash: string) => deregistered.push(hash)),
36
+ reportFrontendTiming: vi.fn(),
37
+ };
38
+ const db = Object.create(SyncedDb.prototype) as SyncedDb<any>;
39
+ (db as any).getSp00ky = () => sp00ky;
40
+ return {
41
+ db,
42
+ sp00ky,
43
+ emit: (hash: string, e: Emission) => subs.get(hash)?.(e),
44
+ setStatus: (hash: string, s: string) => statusSubs.get(hash)?.(s),
45
+ unsubscribed,
46
+ deregistered,
47
+ hasSub: (hash: string) => subs.has(hash),
48
+ };
49
+ }
50
+
51
+ function mockQuery(hash: string, isOne = false) {
52
+ return {
53
+ hash,
54
+ isOne,
55
+ run: async () => ({ hash }),
56
+ } as any;
57
+ }
58
+
59
+ describe('createQuery', () => {
60
+ it('serves empty data immediately, then live emissions with row identity kept', async () => {
61
+ const eng = mockEngine();
62
+ await createRoot(async (dispose) => {
63
+ const q = createQuery<any, any, any, any, false, any>(eng.db, mockQuery('h1'));
64
+ createEffect(
65
+ () => q.data(),
66
+ () => {}
67
+ );
68
+ flush();
69
+
70
+ expect(q.data()).toEqual([]); // committed empty, no suspension
71
+ expect(q.isLoading()).toBe(true);
72
+
73
+ await settle();
74
+ eng.emit('h1', [
75
+ { id: 'a', n: 1 },
76
+ { id: 'b', n: 2 },
77
+ ]);
78
+ await settle();
79
+
80
+ expect(q.data().map((r: any) => r.id)).toEqual(['a', 'b']);
81
+ expect(q.isLoading()).toBe(false);
82
+ expect(q.isSettled()).toBe(true);
83
+
84
+ const rowA = q.data()[0];
85
+ eng.emit('h1', [
86
+ { id: 'a', n: 99 },
87
+ { id: 'b', n: 2 },
88
+ ]);
89
+ await settle();
90
+ expect(q.data()[0]).toBe(rowA); // identity preserved via keyed reconcile
91
+ expect(q.data()[0].n).toBe(99);
92
+
93
+ dispose();
94
+ await settle();
95
+ expect(eng.unsubscribed).toContain('h1');
96
+ });
97
+ });
98
+
99
+ it('first empty emission does not mark fetched; second does', async () => {
100
+ const eng = mockEngine();
101
+ await createRoot(async (dispose) => {
102
+ const q = createQuery<any, any, any, any, false, any>(eng.db, mockQuery('h1'));
103
+ createEffect(
104
+ () => q.data(),
105
+ () => {}
106
+ );
107
+ flush();
108
+ await settle();
109
+
110
+ eng.emit('h1', []);
111
+ await settle();
112
+ expect(q.isLoading()).toBe(true); // still loading: local DB likely not synced
113
+
114
+ eng.emit('h1', []);
115
+ await settle();
116
+ expect(q.isLoading()).toBe(false); // a later empty emission is authoritative
117
+ dispose();
118
+ });
119
+ });
120
+
121
+ it('one() queries yield the row or null', async () => {
122
+ const eng = mockEngine();
123
+ await createRoot(async (dispose) => {
124
+ const q = createQuery<any, any, any, any, true, any>(eng.db, mockQuery('h1', true));
125
+ createEffect(
126
+ () => q.data(),
127
+ () => {}
128
+ );
129
+ flush();
130
+ await settle();
131
+
132
+ expect(q.data()).toBe(null);
133
+ eng.emit('h1', [{ id: 'a', n: 1 }]);
134
+ await settle();
135
+ expect(q.data()?.n).toBe(1);
136
+ expect(q.isLoading()).toBe(false);
137
+ dispose();
138
+ });
139
+ });
140
+
141
+ it('isFetching mirrors query status; isSettled composes both', async () => {
142
+ const eng = mockEngine();
143
+ await createRoot(async (dispose) => {
144
+ const q = createQuery<any, any, any, any, false, any>(eng.db, mockQuery('h1'));
145
+ createEffect(
146
+ () => q.data(),
147
+ () => {}
148
+ );
149
+ flush();
150
+ await settle();
151
+
152
+ eng.setStatus('h1', 'fetching');
153
+ flush();
154
+ expect(q.isFetching()).toBe(true);
155
+
156
+ eng.emit('h1', [{ id: 'a' }]);
157
+ await settle();
158
+ expect(q.isSettled()).toBe(false); // fetched but still fetching
159
+
160
+ eng.setStatus('h1', 'idle');
161
+ flush();
162
+ expect(q.isSettled()).toBe(true);
163
+ dispose();
164
+ });
165
+ });
166
+
167
+ it('registration failure surfaces via error(), resolving isLoading', async () => {
168
+ const eng = mockEngine();
169
+ const failing = {
170
+ hash: 'hx',
171
+ isOne: false,
172
+ run: async () => {
173
+ throw new Error('SSP NOT_READY');
174
+ },
175
+ } as any;
176
+ await createRoot(async (dispose) => {
177
+ const q = createQuery<any, any, any, any, false, any>(eng.db, failing);
178
+ createEffect(
179
+ () => q.data(),
180
+ () => {}
181
+ );
182
+ flush();
183
+ await settle();
184
+
185
+ expect(q.error()?.message).toBe('SSP NOT_READY');
186
+ expect(q.isLoading()).toBe(false); // spinner must resolve
187
+ expect(q.data()).toEqual([]);
188
+ dispose();
189
+ });
190
+ });
191
+
192
+ it('reactive query thunk: identity change re-subscribes and clears state', async () => {
193
+ const eng = mockEngine();
194
+ await createRoot(async (dispose) => {
195
+ const [id, setId] = createSignal('h1');
196
+ const q = createQuery<any, any, any, any, false, any>(eng.db, () => mockQuery(id()));
197
+ createEffect(
198
+ () => q.data(),
199
+ () => {}
200
+ );
201
+ flush();
202
+ await settle();
203
+
204
+ eng.emit('h1', [{ id: 'a' }]);
205
+ await settle();
206
+ expect(q.data().map((r: any) => r.id)).toEqual(['a']);
207
+
208
+ setId('h2');
209
+ flush();
210
+ await settle();
211
+ await settle();
212
+
213
+ expect(eng.unsubscribed).toContain('h1'); // superseded subscription torn down
214
+ expect(eng.hasSub('h2')).toBe(true);
215
+ expect(q.isLoading()).toBe(true); // reset for the new identity
216
+
217
+ eng.emit('h2', [{ id: 'z' }]);
218
+ await settle();
219
+ expect(q.data().map((r: any) => r.id)).toEqual(['z']);
220
+ dispose();
221
+ });
222
+ });
223
+
224
+ it('enabled=false runs no query; flipping true starts it', async () => {
225
+ const eng = mockEngine();
226
+ await createRoot(async (dispose) => {
227
+ const [enabled, setEnabled] = createSignal(false);
228
+ const q = createQuery<any, any, any, any, false, any>(eng.db, mockQuery('h1'), {
229
+ enabled,
230
+ });
231
+ createEffect(
232
+ () => q.data(),
233
+ () => {}
234
+ );
235
+ flush();
236
+ await settle();
237
+ expect(eng.sp00ky.subscribe).not.toHaveBeenCalled();
238
+
239
+ setEnabled(true);
240
+ flush();
241
+ await settle();
242
+ expect(eng.hasSub('h1')).toBe(true);
243
+ dispose();
244
+ });
245
+ });
246
+
247
+ it('deregisterOnCleanup deregisters the active hash on dispose', async () => {
248
+ const eng = mockEngine();
249
+ await createRoot(async (dispose) => {
250
+ const q = createQuery<any, any, any, any, false, any>(eng.db, mockQuery('h1'), {
251
+ deregisterOnCleanup: true,
252
+ });
253
+ createEffect(
254
+ () => q.data(),
255
+ () => {}
256
+ );
257
+ flush();
258
+ await settle();
259
+ expect(eng.hasSub('h1')).toBe(true);
260
+
261
+ dispose();
262
+ await settle();
263
+ expect(eng.unsubscribed).toContain('h1');
264
+ expect(eng.deregistered).toEqual(['h1']);
265
+ });
266
+ });
267
+
268
+ it('reports frontend timing per emission', async () => {
269
+ const eng = mockEngine();
270
+ await createRoot(async (dispose) => {
271
+ const q = createQuery<any, any, any, any, false, any>(eng.db, mockQuery('h1'));
272
+ createEffect(
273
+ () => q.data(),
274
+ () => {}
275
+ );
276
+ flush();
277
+ await settle();
278
+ eng.emit('h1', [{ id: 'a' }]);
279
+ await settle();
280
+ expect(eng.sp00ky.reportFrontendTiming).toHaveBeenCalledWith('h1', expect.any(Number));
281
+ dispose();
282
+ });
283
+ });
284
+ });
@@ -0,0 +1,389 @@
1
+ /**
2
+ * Probes against solid-js 2.0.0-rc.0 semantics this package's design depends
3
+ * on. If any of these fail after a Solid version bump, the binding's
4
+ * assumptions are broken — fix the binding, don't delete the probe.
5
+ *
6
+ * Probed assumptions (see plan):
7
+ * 1. A projection compute given as an async generator restarts when a
8
+ * dependency read before the first `await` changes, and the superseded
9
+ * generator is terminated (its `finally` runs) so external subscriptions
10
+ * can be torn down.
11
+ * 2. Keyed reconcile of yielded arrays notifies coarse readers (whole-array
12
+ * tracking, e.g. `<For>`/mapArray) on add/remove even when the array length
13
+ * stays equal (windowed-list delete case).
14
+ * 3. `seedLoadingValue: true` births the store committed: readable
15
+ * immediately, `isPending` false during the first flight.
16
+ * 4. Signals created with `{ ownedWrite: true }` accept writes from plain
17
+ * callbacks fired outside any tracking scope after setup.
18
+ * 5. Class instances (RecordId-shaped) inside store rows: document whether the
19
+ * proxy wraps them and that instanceof/method access still works through it.
20
+ * 6. Generator teardown on owner dispose runs `finally` blocks.
21
+ */
22
+ import { describe, expect, it } from 'vitest';
23
+ import {
24
+ createProjection,
25
+ createRoot,
26
+ createSignal,
27
+ createEffect,
28
+ createMemo,
29
+ isPending,
30
+ flush,
31
+ mapArray,
32
+ isWrappable,
33
+ snapshot,
34
+ onCleanup,
35
+ } from 'solid-js';
36
+ import * as signals from '@solidjs/signals';
37
+
38
+ const tick = () => new Promise<void>((r) => setTimeout(r, 0));
39
+
40
+ /** Minimal push-source with subscriber tracking, stand-in for sp00ky.subscribe. */
41
+ function pushSource<T>() {
42
+ let cb: ((v: T) => void) | undefined;
43
+ let subscribes = 0;
44
+ let unsubscribes = 0;
45
+ return {
46
+ subscribe(fn: (v: T) => void) {
47
+ cb = fn;
48
+ subscribes++;
49
+ return () => {
50
+ if (cb === fn) cb = undefined;
51
+ unsubscribes++;
52
+ };
53
+ },
54
+ emit(v: T) {
55
+ cb?.(v);
56
+ },
57
+ get counts() {
58
+ return { subscribes, unsubscribes };
59
+ },
60
+ };
61
+ }
62
+
63
+ /** Push → pull adapter (same shape as ../conflate, inlined to keep the probe
64
+ * self-contained). */
65
+ function iterate<T>(subscribe: (cb: (v: T) => void) => () => void): AsyncIterable<T> {
66
+ return {
67
+ [Symbol.asyncIterator]() {
68
+ let buffered: { v: T } | undefined;
69
+ let resolveNext: ((r: IteratorResult<T>) => void) | undefined;
70
+ let done = false;
71
+ const unsub = subscribe((v) => {
72
+ if (done) return;
73
+ if (resolveNext) {
74
+ const r = resolveNext;
75
+ resolveNext = undefined;
76
+ r({ value: v, done: false });
77
+ } else {
78
+ buffered = { v };
79
+ }
80
+ });
81
+ const finish = (): IteratorResult<T> => {
82
+ if (!done) {
83
+ done = true;
84
+ unsub();
85
+ resolveNext?.({ value: undefined, done: true });
86
+ resolveNext = undefined;
87
+ }
88
+ return { value: undefined, done: true };
89
+ };
90
+ return {
91
+ next() {
92
+ if (done) return Promise.resolve<IteratorResult<T>>({ value: undefined, done: true });
93
+ if (buffered) {
94
+ const v = buffered.v;
95
+ buffered = undefined;
96
+ return Promise.resolve({ value: v, done: false });
97
+ }
98
+ return new Promise<IteratorResult<T>>((r) => (resolveNext = r));
99
+ },
100
+ return() {
101
+ return Promise.resolve(finish());
102
+ },
103
+ throw(e: unknown) {
104
+ finish();
105
+ return Promise.reject(e);
106
+ },
107
+ };
108
+ },
109
+ };
110
+ }
111
+
112
+ type Row = { id: string; n?: number };
113
+
114
+ describe('probe 1: async-generator projection restart on dep change', () => {
115
+ it('abandons the superseded generator: teardown must be manual via onCleanup', async () => {
116
+ // rc.0 semantics: a dep change restarts the compute (a new generator
117
+ // starts) but the superseded generator is NOT terminated — no return(),
118
+ // no finally. Anything it subscribed to leaks unless the compute registers
119
+ // onCleanup (synchronously, before the first await) to tear it down.
120
+ // conflate() + create-query rely on exactly that onCleanup contract.
121
+ const src1 = pushSource<Row[]>();
122
+ const src2 = pushSource<Row[]>();
123
+ const finished: string[] = [];
124
+
125
+ await createRoot(async (dispose) => {
126
+ const [which, setWhich] = createSignal<'a' | 'b'>('a');
127
+
128
+ const rows = createProjection(
129
+ async function* (): AsyncGenerator<Row[]> {
130
+ const w = which(); // tracked read BEFORE first await
131
+ const src = w === 'a' ? src1 : src2;
132
+ const it = iterate<Row[]>((cb) => src.subscribe(cb))[Symbol.asyncIterator]();
133
+ onCleanup(() => void it.return?.()); // manual teardown — Solid won't do it
134
+ try {
135
+ while (true) {
136
+ const r = await it.next();
137
+ if (r.done) break;
138
+ yield r.value;
139
+ }
140
+ } finally {
141
+ finished.push(w);
142
+ }
143
+ },
144
+ [] as Row[],
145
+ { key: 'id' }
146
+ );
147
+
148
+ // touch the projection so it computes
149
+ createEffect(
150
+ () => rows.length,
151
+ () => {}
152
+ );
153
+ flush();
154
+ await tick();
155
+ expect(src1.counts.subscribes).toBe(1);
156
+
157
+ src1.emit([{ id: 'x', n: 1 }]);
158
+ await tick();
159
+ flush();
160
+ expect(rows.map((r) => r.id)).toEqual(['x']);
161
+
162
+ // dep change → onCleanup fires for generator A, its finally runs, B subscribes
163
+ setWhich('b');
164
+ flush();
165
+ await tick();
166
+ await tick();
167
+ expect(finished).toContain('a');
168
+ expect(src1.counts.unsubscribes).toBe(1);
169
+ expect(src2.counts.subscribes).toBe(1);
170
+
171
+ src2.emit([{ id: 'y', n: 2 }]);
172
+ await tick();
173
+ flush();
174
+ expect(rows.map((r) => r.id)).toEqual(['y']);
175
+
176
+ dispose();
177
+ await tick();
178
+ // dispose runs the live generator's onCleanup too
179
+ expect(finished).toContain('b');
180
+ expect(src2.counts.unsubscribes).toBe(1);
181
+ });
182
+ });
183
+ });
184
+
185
+ describe('probe 2: keyed reconcile notifies coarse readers on same-length change', () => {
186
+ it('mapArray over the projection re-runs when a row is swapped', async () => {
187
+ const src = pushSource<Row[]>();
188
+ await createRoot(async (dispose) => {
189
+ const rows = createProjection(
190
+ async function* (): AsyncGenerator<Row[]> {
191
+ for await (const v of iterate<Row[]>((cb) => src.subscribe(cb))) yield v;
192
+ },
193
+ [] as Row[],
194
+ { key: 'id' }
195
+ );
196
+
197
+ const seen: string[][] = [];
198
+ const mapped = mapArray(
199
+ () => rows,
200
+ (r) => r.id
201
+ );
202
+ createEffect(
203
+ () => mapped(),
204
+ (ids) => {
205
+ seen.push([...ids]);
206
+ }
207
+ );
208
+ flush();
209
+ await tick();
210
+
211
+ src.emit([
212
+ { id: 'a', n: 1 },
213
+ { id: 'b', n: 2 },
214
+ ]);
215
+ await tick();
216
+ flush();
217
+
218
+ // same length, one row swapped (windowed-list delete: c shifts in for b)
219
+ src.emit([
220
+ { id: 'a', n: 1 },
221
+ { id: 'c', n: 3 },
222
+ ]);
223
+ await tick();
224
+ flush();
225
+
226
+ expect(seen.at(-1)).toEqual(['a', 'c']);
227
+
228
+ // row identity for the surviving row must be stable across emissions
229
+ const a1 = rows[0];
230
+ src.emit([
231
+ { id: 'a', n: 99 },
232
+ { id: 'c', n: 3 },
233
+ ]);
234
+ await tick();
235
+ flush();
236
+ expect(rows[0]).toBe(a1);
237
+ expect(rows[0].n).toBe(99);
238
+
239
+ dispose();
240
+ });
241
+ });
242
+ });
243
+
244
+ describe('probe 3: seedLoadingValue commits the seed', () => {
245
+ it('store is readable and not pending during first flight', async () => {
246
+ const src = pushSource<Row[]>();
247
+ await createRoot(async (dispose) => {
248
+ const rows = createProjection(
249
+ async function* (): AsyncGenerator<Row[]> {
250
+ for await (const v of iterate<Row[]>((cb) => src.subscribe(cb))) yield v;
251
+ },
252
+ [] as Row[],
253
+ { key: 'id', seedLoadingValue: true }
254
+ );
255
+
256
+ let pendingDuringFlight: boolean | undefined;
257
+ let lenDuringFlight: number | undefined;
258
+ createEffect(
259
+ () => {
260
+ lenDuringFlight = rows.length;
261
+ pendingDuringFlight = isPending(() => rows.length);
262
+ return undefined;
263
+ },
264
+ () => {}
265
+ );
266
+ flush();
267
+ await tick();
268
+
269
+ expect(lenDuringFlight).toBe(0); // readable, no NotReadyError
270
+ expect(pendingDuringFlight).toBe(false); // commit #0, not pending
271
+
272
+ src.emit([{ id: 'a' }]);
273
+ await tick();
274
+ flush();
275
+ expect(rows.length).toBe(1);
276
+ dispose();
277
+ });
278
+ });
279
+ });
280
+
281
+ describe('probe 4: ownedWrite signals accept external-callback writes', () => {
282
+ it('does not throw when written from a later plain callback', async () => {
283
+ let later: (() => void) | undefined;
284
+ await createRoot(async (dispose) => {
285
+ const [v, setV] = createSignal(0, { ownedWrite: true });
286
+ later = () => setV(1);
287
+ createEffect(
288
+ () => v(),
289
+ () => {}
290
+ );
291
+ flush();
292
+ // fire from outside any reactive scope, as a subscription callback would
293
+ await tick();
294
+ expect(() => later!()).not.toThrow();
295
+ flush();
296
+ expect(v()).toBe(1);
297
+ dispose();
298
+ });
299
+ });
300
+ });
301
+
302
+ describe('probe 5: class instances inside store rows', () => {
303
+ class FakeRecordId {
304
+ constructor(
305
+ public tb: string,
306
+ public id: string
307
+ ) {}
308
+ toString() {
309
+ return `${this.tb}:${this.id}`;
310
+ }
311
+ }
312
+
313
+ it('documents wrapping behavior and that instanceof/methods survive', async () => {
314
+ const src = pushSource<{ id: string; rid: FakeRecordId }[]>();
315
+ await createRoot(async (dispose) => {
316
+ const rows = createProjection(
317
+ async function* (): AsyncGenerator<{ id: string; rid: FakeRecordId }[]> {
318
+ for await (const v of iterate<{ id: string; rid: FakeRecordId }[]>((cb) =>
319
+ src.subscribe(cb)
320
+ ))
321
+ yield v;
322
+ },
323
+ [] as { id: string; rid: FakeRecordId }[],
324
+ { key: 'id' }
325
+ );
326
+ createEffect(
327
+ () => rows.length,
328
+ () => {}
329
+ );
330
+ flush();
331
+ await tick();
332
+
333
+ src.emit([{ id: 'a', rid: new FakeRecordId('game', 'a') }]);
334
+ await tick();
335
+ flush();
336
+
337
+ const rid = rows[0].rid;
338
+ // Solid 2 wraps class instances (isWrappable true — unlike Solid 1).
339
+ expect(isWrappable(new FakeRecordId('t', 'i'))).toBe(true);
340
+ // Document what survives through the proxy: methods are served BOUND, so
341
+ // `constructor.name` gains a 'bound ' prefix. SyncedDb.delete's
342
+ // cross-package RecordId detection must strip it.
343
+ expect((rid as any).constructor?.name).toBe('bound FakeRecordId');
344
+ expect(rid.toString()).toBe('game:a');
345
+ expect(`${rid.tb}:${rid.id}`).toBe('game:a');
346
+ // `snapshot` must unwrap back to the raw instance so payloads passed to
347
+ // surrealdb (which checks instanceof) can be de-proxied at the boundary.
348
+ const snap = snapshot(rows[0]);
349
+ expect(snap.rid instanceof FakeRecordId).toBe(true);
350
+ dispose();
351
+ });
352
+ });
353
+
354
+ it('rc.0 packaging: markRaw is typed but absent from the runtime build', () => {
355
+ // store.d.ts declares markRaw, but dist/dev.js does not export it. If this
356
+ // starts passing after a bump, adopt markRaw for RecordId/CrdtField
357
+ // instances at the ingest boundary and drop the snapshot() workaround.
358
+ expect((signals as any).markRaw).toBeUndefined();
359
+ });
360
+ });
361
+
362
+ describe('probe: async-generator memo (fromSubscription shape)', () => {
363
+ it('memo over async generator with loadingValue serves values', async () => {
364
+ const src = pushSource<number>();
365
+ await createRoot(async (dispose) => {
366
+ const v = createMemo(
367
+ async function* (): AsyncGenerator<number> {
368
+ for await (const x of iterate<number>((cb) => src.subscribe(cb))) yield x;
369
+ },
370
+ { loadingValue: -1 }
371
+ );
372
+ let latest: number | undefined;
373
+ createEffect(
374
+ () => v(),
375
+ (x) => {
376
+ latest = x;
377
+ }
378
+ );
379
+ flush();
380
+ await tick();
381
+ expect(latest).toBe(-1); // loadingValue committed
382
+ src.emit(42);
383
+ await tick();
384
+ flush();
385
+ expect(latest).toBe(42);
386
+ dispose();
387
+ });
388
+ });
389
+ });