@electric-sql/client 0.6.4 → 0.7.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.
- package/README.md +7 -5
- package/dist/cjs/index.cjs +87 -51
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/index.browser.mjs +1 -1
- package/dist/index.browser.mjs.map +1 -1
- package/dist/index.d.ts +47 -16
- package/dist/index.legacy-esm.js +87 -51
- package/dist/index.legacy-esm.js.map +1 -1
- package/dist/index.mjs +87 -51
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/client.ts +91 -34
- package/src/constants.ts +11 -8
- package/src/fetch.ts +7 -7
- package/src/shape.ts +24 -15
package/src/client.ts
CHANGED
|
@@ -22,21 +22,38 @@ import {
|
|
|
22
22
|
COLUMNS_QUERY_PARAM,
|
|
23
23
|
LIVE_QUERY_PARAM,
|
|
24
24
|
OFFSET_QUERY_PARAM,
|
|
25
|
-
|
|
26
|
-
|
|
25
|
+
SHAPE_HANDLE_HEADER,
|
|
26
|
+
SHAPE_HANDLE_QUERY_PARAM,
|
|
27
27
|
SHAPE_SCHEMA_HEADER,
|
|
28
28
|
WHERE_QUERY_PARAM,
|
|
29
|
+
DATABASE_ID_QUERY_PARAM,
|
|
30
|
+
TABLE_QUERY_PARAM,
|
|
31
|
+
REPLICA_PARAM,
|
|
29
32
|
} from './constants'
|
|
30
33
|
|
|
34
|
+
type Replica = `full` | `default`
|
|
35
|
+
|
|
31
36
|
/**
|
|
32
37
|
* Options for constructing a ShapeStream.
|
|
33
38
|
*/
|
|
34
39
|
export interface ShapeStreamOptions<T = never> {
|
|
35
40
|
/**
|
|
36
|
-
* The full URL to where the Shape is
|
|
37
|
-
* directly or a proxy. E.g. for a local Electric instance, you might set `http://localhost:3000/v1/shape
|
|
41
|
+
* The full URL to where the Shape is served. This can either be the Electric server
|
|
42
|
+
* directly or a proxy. E.g. for a local Electric instance, you might set `http://localhost:3000/v1/shape`
|
|
38
43
|
*/
|
|
39
44
|
url: string
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Which database to use.
|
|
48
|
+
* This is optional unless Electric is used with multiple databases.
|
|
49
|
+
*/
|
|
50
|
+
databaseId?: string
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* The root table for the shape.
|
|
54
|
+
*/
|
|
55
|
+
table: string
|
|
56
|
+
|
|
40
57
|
/**
|
|
41
58
|
* The where clauses for the shape.
|
|
42
59
|
*/
|
|
@@ -48,12 +65,23 @@ export interface ShapeStreamOptions<T = never> {
|
|
|
48
65
|
*/
|
|
49
66
|
columns?: string[]
|
|
50
67
|
|
|
68
|
+
/**
|
|
69
|
+
* If `replica` is `default` (the default) then Electric will only send the
|
|
70
|
+
* changed columns in an update.
|
|
71
|
+
*
|
|
72
|
+
* If it's `full` Electric will send the entire row with both changed and
|
|
73
|
+
* unchanged values.
|
|
74
|
+
*
|
|
75
|
+
* Setting `replica` to `full` will obviously result in higher bandwidth
|
|
76
|
+
* usage and so is not recommended.
|
|
77
|
+
*/
|
|
78
|
+
replica?: Replica
|
|
51
79
|
/**
|
|
52
80
|
* The "offset" on the shape log. This is typically not set as the ShapeStream
|
|
53
81
|
* will handle this automatically. A common scenario where you might pass an offset
|
|
54
82
|
* is if you're maintaining a local cache of the log. If you've gone offline
|
|
55
83
|
* and are re-starting a ShapeStream to catch-up to the latest state of the Shape,
|
|
56
|
-
* you'd pass in the last offset and
|
|
84
|
+
* you'd pass in the last offset and shapeHandle you'd seen from the Electric server
|
|
57
85
|
* so it knows at what point in the shape to catch you up from.
|
|
58
86
|
*/
|
|
59
87
|
offset?: Offset
|
|
@@ -61,7 +89,7 @@ export interface ShapeStreamOptions<T = never> {
|
|
|
61
89
|
* Similar to `offset`, this isn't typically used unless you're maintaining
|
|
62
90
|
* a cache of the shape log.
|
|
63
91
|
*/
|
|
64
|
-
|
|
92
|
+
shapeHandle?: string
|
|
65
93
|
backoffOptions?: BackoffOptions
|
|
66
94
|
|
|
67
95
|
/**
|
|
@@ -98,7 +126,7 @@ export interface ShapeStreamInterface<T extends Row<unknown> = Row> {
|
|
|
98
126
|
isConnected(): boolean
|
|
99
127
|
|
|
100
128
|
isUpToDate: boolean
|
|
101
|
-
|
|
129
|
+
shapeHandle?: string
|
|
102
130
|
}
|
|
103
131
|
|
|
104
132
|
/**
|
|
@@ -135,6 +163,11 @@ export interface ShapeStreamInterface<T extends Row<unknown> = Row> {
|
|
|
135
163
|
export class ShapeStream<T extends Row<unknown> = Row>
|
|
136
164
|
implements ShapeStreamInterface<T>
|
|
137
165
|
{
|
|
166
|
+
static readonly Replica = {
|
|
167
|
+
FULL: `full` as Replica,
|
|
168
|
+
DEFAULT: `default` as Replica,
|
|
169
|
+
}
|
|
170
|
+
|
|
138
171
|
readonly options: ShapeStreamOptions<GetExtensions<T>>
|
|
139
172
|
|
|
140
173
|
readonly #fetchClient: typeof fetch
|
|
@@ -157,16 +190,21 @@ export class ShapeStream<T extends Row<unknown> = Row>
|
|
|
157
190
|
#lastSyncedAt?: number // unix time
|
|
158
191
|
#isUpToDate: boolean = false
|
|
159
192
|
#connected: boolean = false
|
|
160
|
-
#
|
|
193
|
+
#shapeHandle?: string
|
|
194
|
+
#databaseId?: string
|
|
161
195
|
#schema?: Schema
|
|
196
|
+
#error?: unknown
|
|
197
|
+
#replica?: Replica
|
|
162
198
|
|
|
163
199
|
constructor(options: ShapeStreamOptions<GetExtensions<T>>) {
|
|
164
200
|
validateOptions(options)
|
|
165
201
|
this.options = { subscribe: true, ...options }
|
|
166
202
|
this.#lastOffset = this.options.offset ?? `-1`
|
|
167
203
|
this.#liveCacheBuster = ``
|
|
168
|
-
this.#
|
|
204
|
+
this.#shapeHandle = this.options.shapeHandle
|
|
205
|
+
this.#databaseId = this.options.databaseId
|
|
169
206
|
this.#messageParser = new MessageParser<T>(options.parser)
|
|
207
|
+
this.#replica = this.options.replica
|
|
170
208
|
|
|
171
209
|
const baseFetchClient =
|
|
172
210
|
options.fetchClient ??
|
|
@@ -185,18 +223,22 @@ export class ShapeStream<T extends Row<unknown> = Row>
|
|
|
185
223
|
this.start()
|
|
186
224
|
}
|
|
187
225
|
|
|
188
|
-
get
|
|
189
|
-
return this.#
|
|
226
|
+
get shapeHandle() {
|
|
227
|
+
return this.#shapeHandle
|
|
190
228
|
}
|
|
191
229
|
|
|
192
230
|
get isUpToDate() {
|
|
193
231
|
return this.#isUpToDate
|
|
194
232
|
}
|
|
195
233
|
|
|
234
|
+
get error() {
|
|
235
|
+
return this.#error
|
|
236
|
+
}
|
|
237
|
+
|
|
196
238
|
async start() {
|
|
197
239
|
this.#isUpToDate = false
|
|
198
240
|
|
|
199
|
-
const { url, where, columns, signal } = this.options
|
|
241
|
+
const { url, table, where, columns, signal } = this.options
|
|
200
242
|
|
|
201
243
|
try {
|
|
202
244
|
while (
|
|
@@ -204,6 +246,7 @@ export class ShapeStream<T extends Row<unknown> = Row>
|
|
|
204
246
|
this.options.subscribe
|
|
205
247
|
) {
|
|
206
248
|
const fetchUrl = new URL(url)
|
|
249
|
+
fetchUrl.searchParams.set(TABLE_QUERY_PARAM, table)
|
|
207
250
|
if (where) fetchUrl.searchParams.set(WHERE_QUERY_PARAM, where)
|
|
208
251
|
if (columns && columns.length > 0)
|
|
209
252
|
fetchUrl.searchParams.set(COLUMNS_QUERY_PARAM, columns.join(`,`))
|
|
@@ -217,9 +260,23 @@ export class ShapeStream<T extends Row<unknown> = Row>
|
|
|
217
260
|
)
|
|
218
261
|
}
|
|
219
262
|
|
|
220
|
-
if (this.#
|
|
263
|
+
if (this.#shapeHandle) {
|
|
221
264
|
// This should probably be a header for better cache breaking?
|
|
222
|
-
fetchUrl.searchParams.set(
|
|
265
|
+
fetchUrl.searchParams.set(
|
|
266
|
+
SHAPE_HANDLE_QUERY_PARAM,
|
|
267
|
+
this.#shapeHandle!
|
|
268
|
+
)
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
if (this.#databaseId) {
|
|
272
|
+
fetchUrl.searchParams.set(DATABASE_ID_QUERY_PARAM, this.#databaseId!)
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
if (
|
|
276
|
+
(this.#replica ?? ShapeStream.Replica.DEFAULT) !=
|
|
277
|
+
ShapeStream.Replica.DEFAULT
|
|
278
|
+
) {
|
|
279
|
+
fetchUrl.searchParams.set(REPLICA_PARAM, this.#replica as string)
|
|
223
280
|
}
|
|
224
281
|
|
|
225
282
|
let response!: Response
|
|
@@ -232,17 +289,11 @@ export class ShapeStream<T extends Row<unknown> = Row>
|
|
|
232
289
|
} catch (e) {
|
|
233
290
|
if (e instanceof FetchBackoffAbortError) break // interrupted
|
|
234
291
|
if (!(e instanceof FetchError)) throw e // should never happen
|
|
235
|
-
if (e.status ==
|
|
236
|
-
// The request is invalid, most likely because the shape has been deleted.
|
|
237
|
-
// We should start from scratch, this will force the shape to be recreated.
|
|
238
|
-
this.#reset()
|
|
239
|
-
await this.#publish(e.json as Message<T>[])
|
|
240
|
-
continue
|
|
241
|
-
} else if (e.status == 409) {
|
|
292
|
+
if (e.status == 409) {
|
|
242
293
|
// Upon receiving a 409, we should start from scratch
|
|
243
|
-
// with the newly provided shape
|
|
244
|
-
const
|
|
245
|
-
this.#reset(
|
|
294
|
+
// with the newly provided shape handle
|
|
295
|
+
const newShapeHandle = e.headers[SHAPE_HANDLE_HEADER]
|
|
296
|
+
this.#reset(newShapeHandle)
|
|
246
297
|
await this.#publish(e.json as Message<T>[])
|
|
247
298
|
continue
|
|
248
299
|
} else if (e.status >= 400 && e.status < 500) {
|
|
@@ -250,15 +301,16 @@ export class ShapeStream<T extends Row<unknown> = Row>
|
|
|
250
301
|
this.#sendErrorToUpToDateSubscribers(e)
|
|
251
302
|
this.#sendErrorToSubscribers(e)
|
|
252
303
|
|
|
253
|
-
// 400 errors are not actionable without additional user input,
|
|
304
|
+
// 400 errors are not actionable without additional user input,
|
|
305
|
+
// so we exit the loop
|
|
254
306
|
throw e
|
|
255
307
|
}
|
|
256
308
|
}
|
|
257
309
|
|
|
258
310
|
const { headers, status } = response
|
|
259
|
-
const
|
|
260
|
-
if (
|
|
261
|
-
this.#
|
|
311
|
+
const shapeHandle = headers.get(SHAPE_HANDLE_HEADER)
|
|
312
|
+
if (shapeHandle) {
|
|
313
|
+
this.#shapeHandle = shapeHandle
|
|
262
314
|
}
|
|
263
315
|
|
|
264
316
|
const lastOffset = headers.get(CHUNK_LAST_OFFSET_HEADER)
|
|
@@ -301,6 +353,8 @@ export class ShapeStream<T extends Row<unknown> = Row>
|
|
|
301
353
|
}
|
|
302
354
|
}
|
|
303
355
|
}
|
|
356
|
+
} catch (err) {
|
|
357
|
+
this.#error = err
|
|
304
358
|
} finally {
|
|
305
359
|
this.#connected = false
|
|
306
360
|
}
|
|
@@ -395,12 +449,12 @@ export class ShapeStream<T extends Row<unknown> = Row>
|
|
|
395
449
|
|
|
396
450
|
/**
|
|
397
451
|
* Resets the state of the stream, optionally with a provided
|
|
398
|
-
* shape
|
|
452
|
+
* shape handle
|
|
399
453
|
*/
|
|
400
|
-
#reset(
|
|
454
|
+
#reset(shapeHandle?: string) {
|
|
401
455
|
this.#lastOffset = `-1`
|
|
402
456
|
this.#liveCacheBuster = ``
|
|
403
|
-
this.#
|
|
457
|
+
this.#shapeHandle = shapeHandle
|
|
404
458
|
this.#isUpToDate = false
|
|
405
459
|
this.#connected = false
|
|
406
460
|
this.#schema = undefined
|
|
@@ -409,7 +463,10 @@ export class ShapeStream<T extends Row<unknown> = Row>
|
|
|
409
463
|
|
|
410
464
|
function validateOptions<T>(options: Partial<ShapeStreamOptions<T>>): void {
|
|
411
465
|
if (!options.url) {
|
|
412
|
-
throw new Error(`Invalid shape
|
|
466
|
+
throw new Error(`Invalid shape options. It must provide the url`)
|
|
467
|
+
}
|
|
468
|
+
if (!options.table) {
|
|
469
|
+
throw new Error(`Invalid shape options. It must provide the table`)
|
|
413
470
|
}
|
|
414
471
|
if (options.signal && !(options.signal instanceof AbortSignal)) {
|
|
415
472
|
throw new Error(
|
|
@@ -420,10 +477,10 @@ function validateOptions<T>(options: Partial<ShapeStreamOptions<T>>): void {
|
|
|
420
477
|
if (
|
|
421
478
|
options.offset !== undefined &&
|
|
422
479
|
options.offset !== `-1` &&
|
|
423
|
-
!options.
|
|
480
|
+
!options.shapeHandle
|
|
424
481
|
) {
|
|
425
482
|
throw new Error(
|
|
426
|
-
`
|
|
483
|
+
`shapeHandle is required if this isn't an initial fetch (i.e. offset > -1)`
|
|
427
484
|
)
|
|
428
485
|
}
|
|
429
486
|
return
|
package/src/constants.ts
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
|
-
export const
|
|
2
|
-
export const
|
|
3
|
-
export const
|
|
4
|
-
export const CHUNK_LAST_OFFSET_HEADER = `electric-chunk-last-offset`
|
|
5
|
-
export const CHUNK_UP_TO_DATE_HEADER = `electric-chunk-up-to-date`
|
|
1
|
+
export const LIVE_CACHE_BUSTER_HEADER = `electric-cursor`
|
|
2
|
+
export const SHAPE_HANDLE_HEADER = `electric-handle`
|
|
3
|
+
export const CHUNK_LAST_OFFSET_HEADER = `electric-offset`
|
|
6
4
|
export const SHAPE_SCHEMA_HEADER = `electric-schema`
|
|
7
|
-
export const
|
|
8
|
-
export const
|
|
9
|
-
export const WHERE_QUERY_PARAM = `where`
|
|
5
|
+
export const CHUNK_UP_TO_DATE_HEADER = `electric-up-to-date`
|
|
6
|
+
export const DATABASE_ID_QUERY_PARAM = `database_id`
|
|
10
7
|
export const COLUMNS_QUERY_PARAM = `columns`
|
|
8
|
+
export const LIVE_CACHE_BUSTER_QUERY_PARAM = `cursor`
|
|
9
|
+
export const SHAPE_HANDLE_QUERY_PARAM = `handle`
|
|
11
10
|
export const LIVE_QUERY_PARAM = `live`
|
|
11
|
+
export const OFFSET_QUERY_PARAM = `offset`
|
|
12
|
+
export const TABLE_QUERY_PARAM = `table`
|
|
13
|
+
export const WHERE_QUERY_PARAM = `where`
|
|
14
|
+
export const REPLICA_PARAM = `replica`
|
package/src/fetch.ts
CHANGED
|
@@ -3,8 +3,8 @@ import {
|
|
|
3
3
|
CHUNK_UP_TO_DATE_HEADER,
|
|
4
4
|
LIVE_QUERY_PARAM,
|
|
5
5
|
OFFSET_QUERY_PARAM,
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
SHAPE_HANDLE_HEADER,
|
|
7
|
+
SHAPE_HANDLE_QUERY_PARAM,
|
|
8
8
|
} from './constants'
|
|
9
9
|
import { FetchError, FetchBackoffAbortError } from './error'
|
|
10
10
|
|
|
@@ -245,13 +245,13 @@ class PrefetchQueue {
|
|
|
245
245
|
* Generate the next chunk's URL if the url and response are valid
|
|
246
246
|
*/
|
|
247
247
|
function getNextChunkUrl(url: string, res: Response): string | void {
|
|
248
|
-
const
|
|
248
|
+
const shapeHandle = res.headers.get(SHAPE_HANDLE_HEADER)
|
|
249
249
|
const lastOffset = res.headers.get(CHUNK_LAST_OFFSET_HEADER)
|
|
250
250
|
const isUpToDate = res.headers.has(CHUNK_UP_TO_DATE_HEADER)
|
|
251
251
|
|
|
252
|
-
// only prefetch if shape
|
|
252
|
+
// only prefetch if shape handle and offset for next chunk are available, and
|
|
253
253
|
// response is not already up-to-date
|
|
254
|
-
if (!
|
|
254
|
+
if (!shapeHandle || !lastOffset || isUpToDate) return
|
|
255
255
|
|
|
256
256
|
const nextUrl = new URL(url)
|
|
257
257
|
|
|
@@ -259,7 +259,7 @@ function getNextChunkUrl(url: string, res: Response): string | void {
|
|
|
259
259
|
// potentially miss more recent data
|
|
260
260
|
if (nextUrl.searchParams.has(LIVE_QUERY_PARAM)) return
|
|
261
261
|
|
|
262
|
-
nextUrl.searchParams.set(
|
|
262
|
+
nextUrl.searchParams.set(SHAPE_HANDLE_QUERY_PARAM, shapeHandle)
|
|
263
263
|
nextUrl.searchParams.set(OFFSET_QUERY_PARAM, lastOffset)
|
|
264
264
|
return nextUrl.toString()
|
|
265
265
|
}
|
|
@@ -271,7 +271,7 @@ function getNextChunkUrl(url: string, res: Response): string | void {
|
|
|
271
271
|
*/
|
|
272
272
|
function chainAborter(
|
|
273
273
|
aborter: AbortController,
|
|
274
|
-
sourceSignal?: AbortSignal
|
|
274
|
+
sourceSignal?: AbortSignal | null
|
|
275
275
|
): AbortSignal {
|
|
276
276
|
if (!sourceSignal) return aborter.signal
|
|
277
277
|
if (sourceSignal.aborted) aborter.abort()
|
package/src/shape.ts
CHANGED
|
@@ -4,13 +4,14 @@ import { FetchError } from './error'
|
|
|
4
4
|
import { ShapeStreamInterface } from './client'
|
|
5
5
|
|
|
6
6
|
export type ShapeData<T extends Row<unknown> = Row> = Map<string, T>
|
|
7
|
-
export type ShapeChangedCallback<T extends Row<unknown> = Row> = (
|
|
7
|
+
export type ShapeChangedCallback<T extends Row<unknown> = Row> = (data: {
|
|
8
8
|
value: ShapeData<T>
|
|
9
|
-
|
|
9
|
+
rows: T[]
|
|
10
|
+
}) => void
|
|
10
11
|
|
|
11
12
|
/**
|
|
12
13
|
* A Shape is an object that subscribes to a shape log,
|
|
13
|
-
* keeps a materialised shape `.
|
|
14
|
+
* keeps a materialised shape `.rows` in memory and
|
|
14
15
|
* notifies subscribers when the value has changed.
|
|
15
16
|
*
|
|
16
17
|
* It can be used without a framework and as a primitive
|
|
@@ -20,23 +21,23 @@ export type ShapeChangedCallback<T extends Row<unknown> = Row> = (
|
|
|
20
21
|
* @param {ShapeStream<T extends Row>} - the underlying shape stream
|
|
21
22
|
* @example
|
|
22
23
|
* ```
|
|
23
|
-
* const shapeStream = new ShapeStream<{ foo: number }>(url:
|
|
24
|
+
* const shapeStream = new ShapeStream<{ foo: number }>(url: `http://localhost:3000/v1/shape`, table: `foo`})
|
|
24
25
|
* const shape = new Shape(shapeStream)
|
|
25
26
|
* ```
|
|
26
27
|
*
|
|
27
|
-
* `
|
|
28
|
+
* `rows` returns a promise that resolves the Shape data once the Shape has been
|
|
28
29
|
* fully loaded (and when resuming from being offline):
|
|
29
30
|
*
|
|
30
|
-
* const
|
|
31
|
+
* const rows = await shape.rows
|
|
31
32
|
*
|
|
32
|
-
* `
|
|
33
|
+
* `currentRows` returns the current data synchronously:
|
|
33
34
|
*
|
|
34
|
-
* const
|
|
35
|
+
* const rows = shape.currentRows
|
|
35
36
|
*
|
|
36
37
|
* Subscribe to updates. Called whenever the shape updates in Postgres.
|
|
37
38
|
*
|
|
38
|
-
* shape.subscribe(
|
|
39
|
-
* console.log(
|
|
39
|
+
* shape.subscribe(({ rows }) => {
|
|
40
|
+
* console.log(rows)
|
|
40
41
|
* })
|
|
41
42
|
*/
|
|
42
43
|
export class Shape<T extends Row<unknown> = Row> {
|
|
@@ -69,21 +70,29 @@ export class Shape<T extends Row<unknown> = Row> {
|
|
|
69
70
|
return this.#stream.isUpToDate
|
|
70
71
|
}
|
|
71
72
|
|
|
73
|
+
get rows(): Promise<T[]> {
|
|
74
|
+
return this.value.then((v) => Array.from(v.values()))
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
get currentRows(): T[] {
|
|
78
|
+
return Array.from(this.currentValue.values())
|
|
79
|
+
}
|
|
80
|
+
|
|
72
81
|
get value(): Promise<ShapeData<T>> {
|
|
73
82
|
return new Promise((resolve, reject) => {
|
|
74
83
|
if (this.#stream.isUpToDate) {
|
|
75
|
-
resolve(this.
|
|
84
|
+
resolve(this.currentValue)
|
|
76
85
|
} else {
|
|
77
|
-
const unsubscribe = this.subscribe((
|
|
86
|
+
const unsubscribe = this.subscribe(({ value }) => {
|
|
78
87
|
unsubscribe()
|
|
79
88
|
if (this.#error) reject(this.#error)
|
|
80
|
-
resolve(
|
|
89
|
+
resolve(value)
|
|
81
90
|
})
|
|
82
91
|
}
|
|
83
92
|
})
|
|
84
93
|
}
|
|
85
94
|
|
|
86
|
-
get
|
|
95
|
+
get currentValue() {
|
|
87
96
|
return this.#data
|
|
88
97
|
}
|
|
89
98
|
|
|
@@ -192,7 +201,7 @@ export class Shape<T extends Row<unknown> = Row> {
|
|
|
192
201
|
|
|
193
202
|
#notify(): void {
|
|
194
203
|
this.#subscribers.forEach((callback) => {
|
|
195
|
-
callback(this.
|
|
204
|
+
callback({ value: this.currentValue, rows: this.currentRows })
|
|
196
205
|
})
|
|
197
206
|
}
|
|
198
207
|
}
|