ar_sync 1.0.3 → 1.1.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.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/test.yml +27 -0
  3. data/Gemfile +1 -0
  4. data/Gemfile.lock +29 -31
  5. data/ar_sync.gemspec +1 -1
  6. data/core/{ActioncableAdapter.d.ts → ActionCableAdapter.d.ts} +0 -0
  7. data/core/ActionCableAdapter.js +31 -0
  8. data/core/ArSyncApi.d.ts +8 -2
  9. data/core/ArSyncApi.js +123 -49
  10. data/core/ArSyncModel.js +69 -60
  11. data/core/ArSyncStore.js +522 -381
  12. data/core/ConnectionManager.d.ts +1 -1
  13. data/core/ConnectionManager.js +45 -38
  14. data/core/DataType.d.ts +14 -9
  15. data/core/hooks.d.ts +5 -0
  16. data/core/hooks.js +64 -36
  17. data/gemfiles/Gemfile-rails-6 +9 -0
  18. data/gemfiles/Gemfile-rails-7 +9 -0
  19. data/index.js +2 -2
  20. data/lib/ar_sync/class_methods.rb +71 -36
  21. data/lib/ar_sync/collection.rb +23 -19
  22. data/lib/ar_sync/core.rb +3 -3
  23. data/lib/ar_sync/instance_methods.rb +7 -4
  24. data/lib/ar_sync/rails.rb +1 -1
  25. data/lib/ar_sync/type_script.rb +50 -14
  26. data/lib/ar_sync/version.rb +1 -1
  27. data/lib/generators/ar_sync/install/install_generator.rb +1 -1
  28. data/package-lock.json +1706 -227
  29. data/package.json +1 -1
  30. data/src/core/{ActioncableAdapter.ts → ActionCableAdapter.ts} +0 -0
  31. data/src/core/ArSyncApi.ts +20 -7
  32. data/src/core/ArSyncStore.ts +177 -125
  33. data/src/core/ConnectionManager.ts +1 -0
  34. data/src/core/DataType.ts +15 -16
  35. data/src/core/hooks.ts +31 -7
  36. data/tsconfig.json +2 -2
  37. data/vendor/assets/javascripts/{ar_sync_actioncable_adapter.js.erb → ar_sync_action_cable_adapter.js.erb} +1 -1
  38. metadata +17 -16
  39. data/core/ActioncableAdapter.js +0 -29
  40. data/lib/ar_sync/field.rb +0 -96
data/package.json CHANGED
@@ -11,7 +11,7 @@
11
11
  "actioncable": "^5.2.0",
12
12
  "@types/actioncable": "^5.2.0",
13
13
  "eslint": "^5.16.0",
14
- "typescript": "3.7.2",
14
+ "typescript": "3.9.5",
15
15
  "@typescript-eslint/eslint-plugin": "^1.6.0",
16
16
  "@typescript-eslint/parser": "^1.6.0"
17
17
  }
@@ -10,12 +10,17 @@ async function apiBatchFetch(endpoint: string, requests: object[]) {
10
10
  if (res.status === 200) return res.json()
11
11
  throw new Error(res.statusText)
12
12
  }
13
-
13
+ type FetchError = {
14
+ type: string
15
+ message: string
16
+ retry: boolean
17
+ }
14
18
  interface PromiseCallback {
15
- resolve: (data: object) => void
16
- reject: (error: object) => void
19
+ resolve: (data: any) => void
20
+ reject: (error: FetchError) => void
17
21
  }
18
22
 
23
+ type Request = { api: string; params?: any; query: any; id?: number }
19
24
  class ApiFetcher {
20
25
  endpoint: string
21
26
  batches: [object, PromiseCallback][] = []
@@ -23,7 +28,15 @@ class ApiFetcher {
23
28
  constructor(endpoint: string) {
24
29
  this.endpoint = endpoint
25
30
  }
26
- fetch(request: object) {
31
+ fetch(request: Request) {
32
+ if (request.id != null) {
33
+ return new Promise((resolve, reject) => {
34
+ this.fetch({ api: request.api, params: { ids: [request.id] }, query: request.query }).then((result: any[]) => {
35
+ if (result[0]) resolve(result[0])
36
+ else reject({ type: 'Not Found', retry: false })
37
+ }).catch(reject)
38
+ })
39
+ }
27
40
  return new Promise((resolve, reject) => {
28
41
  this.batches.push([request, { resolve, reject }])
29
42
  if (this.batchFetchTimer) return
@@ -49,7 +62,7 @@ class ApiFetcher {
49
62
  const result = results[i]
50
63
  const callbacks = callbacksList[i]
51
64
  for (const callback of callbacks) {
52
- if (result.data) {
65
+ if (result.data !== undefined) {
53
66
  callback.resolve(result.data)
54
67
  } else {
55
68
  const error = result.error || { type: 'Unknown Error' }
@@ -73,7 +86,7 @@ const syncFetcher = new ApiFetcher('/sync_api')
73
86
  const ArSyncApi = {
74
87
  domain: null as string | null,
75
88
  _batchFetch: apiBatchFetch,
76
- fetch: (request: object) => staticFetcher.fetch(request),
77
- syncFetch: (request: object) => syncFetcher.fetch(request),
89
+ fetch: (request: Request) => staticFetcher.fetch(request),
90
+ syncFetch: (request: Request) => syncFetcher.fetch(request),
78
91
  }
79
92
  export default ArSyncApi
@@ -1,47 +1,56 @@
1
- import ArSyncAPI from './ArSyncApi'
1
+ import ArSyncApi from './ArSyncApi'
2
2
 
3
- const ModelBatchRequest = {
4
- timer: null,
5
- apiRequests: {} as {
6
- [api: string]: {
7
- [queryJSON: string]: {
8
- query
9
- requests: {
10
- [id: number]: {
11
- id: number
12
- model?
13
- callbacks: ((model) => void)[]
14
- }
15
- }
3
+ class ModelBatchRequest {
4
+ timer: number | null = null
5
+ apiRequests = new Map<string,
6
+ Map<string,
7
+ {
8
+ query,
9
+ requests: Map<number, {
10
+ id: number
11
+ model?
12
+ callbacks: {
13
+ resolve: (model: any) => void
14
+ reject: (error?: any) => void
15
+ }[]
16
+ }>
16
17
  }
17
- }
18
- },
18
+ >
19
+ >()
19
20
  fetch(api: string, query, id: number) {
20
21
  this.setTimer()
21
- return new Promise(resolve => {
22
+ return new Promise((resolve, reject) => {
22
23
  const queryJSON = JSON.stringify(query)
23
- const apiRequest = this.apiRequests[api] = this.apiRequests[api] || {}
24
- const queryRequests = apiRequest[queryJSON] = apiRequest[queryJSON] || { query, requests: {} }
25
- const request = queryRequests.requests[id] = queryRequests.requests[id] || { id, callbacks: [] }
26
- request.callbacks.push(resolve)
24
+ let apiRequest = this.apiRequests.get(api)
25
+ if (!apiRequest) this.apiRequests.set(api, apiRequest = new Map())
26
+ let queryRequests = apiRequest.get(queryJSON)
27
+ if (!queryRequests) apiRequest.set(queryJSON, queryRequests = { query, requests: new Map() })
28
+ let request = queryRequests.requests.get(id)
29
+ if (!request) queryRequests.requests.set(id, request = { id, callbacks: [] })
30
+ request.callbacks.push({ resolve, reject })
27
31
  })
28
- },
32
+ }
29
33
  batchFetch() {
30
- const { apiRequests } = this as typeof ModelBatchRequest
31
- for (const api in apiRequests) {
32
- const apiRequest = apiRequests[api]
33
- for (const { query, requests } of Object.values(apiRequest)) {
34
- const ids = Object.values(requests).map(({ id }) => id)
35
- ArSyncAPI.syncFetch({ api, query, params: { ids } }).then((models: any[]) => {
36
- for (const model of models) requests[model.id].model = model
37
- for (const { model, callbacks } of Object.values(requests)) {
38
- for (const callback of callbacks) callback(model)
34
+ this.apiRequests.forEach((apiRequest, api) => {
35
+ apiRequest.forEach(({ query, requests }) => {
36
+ const ids = Array.from(requests.keys())
37
+ ArSyncApi.syncFetch({ api, query, params: { ids } }).then((models: any[]) => {
38
+ for (const model of models) {
39
+ const req = requests.get(model.id)
40
+ if (req) req.model = model
39
41
  }
42
+ requests.forEach(({ model, callbacks }) => {
43
+ callbacks.forEach(cb => cb.resolve(model))
44
+ })
45
+ }).catch(e => {
46
+ requests.forEach(({ callbacks }) => {
47
+ callbacks.forEach(cb => cb.reject(e))
48
+ })
40
49
  })
41
- }
42
- }
43
- this.apiRequests = {}
44
- },
50
+ })
51
+ })
52
+ this.apiRequests.clear()
53
+ }
45
54
  setTimer() {
46
55
  if (this.timer) return
47
56
  this.timer = setTimeout(() => {
@@ -50,6 +59,7 @@ const ModelBatchRequest = {
50
59
  }, 20)
51
60
  }
52
61
  }
62
+ const modelBatchRequest = new ModelBatchRequest
53
63
 
54
64
  type ParsedQuery = {
55
65
  attributes: Record<string, ParsedQuery>
@@ -57,31 +67,42 @@ type ParsedQuery = {
57
67
  params: any
58
68
  } | {}
59
69
 
70
+ type Unsubscribable = { unsubscribe: () => void }
71
+
60
72
  class ArSyncContainerBase {
61
73
  data
62
- listeners
63
- networkSubscriber
74
+ listeners: Unsubscribable[] = []
75
+ networkSubscriber?: Unsubscribable
64
76
  parentModel
65
77
  parentKey
66
78
  children: ArSyncContainerBase[] | { [key: string]: ArSyncContainerBase | null }
67
79
  sync_keys: string[]
68
- onConnectionChange
69
- constructor() {
70
- this.listeners = []
71
- }
80
+ onConnectionChange?: (status: boolean) => void
72
81
  replaceData(_data, _sync_keys?) {}
73
82
  initForReload(request) {
74
83
  this.networkSubscriber = ArSyncStore.connectionManager.subscribeNetwork((state) => {
75
- if (state) {
76
- ArSyncAPI.syncFetch(request).then(data => {
77
- if (this.data) {
84
+ if (!state) {
85
+ if (this.onConnectionChange) this.onConnectionChange(false)
86
+ return
87
+ }
88
+ if (request.id != null) {
89
+ modelBatchRequest.fetch(request.api, request.query, request.id).then(data => {
90
+ if (this.data && data) {
78
91
  this.replaceData(data)
79
92
  if (this.onConnectionChange) this.onConnectionChange(true)
80
93
  if (this.onChange) this.onChange([], this.data)
81
94
  }
82
95
  })
83
96
  } else {
84
- if (this.onConnectionChange) this.onConnectionChange(false)
97
+ ArSyncApi.syncFetch(request).then(data => {
98
+ if (this.data && data) {
99
+ this.replaceData(data)
100
+ if (this.onConnectionChange) this.onConnectionChange(true)
101
+ if (this.onChange) this.onChange([], this.data)
102
+ }
103
+ }).catch(e => {
104
+ console.error(`failed to reload. ${e}`)
105
+ })
85
106
  }
86
107
  })
87
108
  }
@@ -104,7 +125,7 @@ class ArSyncContainerBase {
104
125
  this.listeners = []
105
126
  }
106
127
  static compactQuery(query: ParsedQuery) {
107
- function compactAttributes(attributes: Record<string, ParsedQuery>) {
128
+ function compactAttributes(attributes: Record<string, ParsedQuery>): [ParsedQuery, boolean] {
108
129
  const attrs = {}
109
130
  const keys: string[] = []
110
131
  for (const key in attributes) {
@@ -118,13 +139,13 @@ class ArSyncContainerBase {
118
139
  if (Object.keys(attrs).length === 0) {
119
140
  if (keys.length === 0) return [true, false]
120
141
  if (keys.length === 1) return [keys[0], false]
121
- return [keys]
142
+ return [keys, false]
122
143
  }
123
144
  const needsEscape = attrs['attributes'] || attrs['params'] || attrs['as']
124
145
  if (keys.length === 0) return [attrs, needsEscape]
125
146
  return [[...keys, attrs], needsEscape]
126
147
  }
127
- function compactQuery(query: ParsedQuery) {
148
+ function compactQuery(query: ParsedQuery): ParsedQuery {
128
149
  if (!('attributes' in query)) return true
129
150
  const { as, params } = query
130
151
  const [attributes, needsEscape] = compactAttributes(query.attributes)
@@ -138,10 +159,8 @@ class ArSyncContainerBase {
138
159
  if (attributes !== true) result.attributes = attributes
139
160
  return result
140
161
  }
141
- try{
142
162
  const result = compactQuery(query)
143
163
  return result === true ? {} : result
144
- }catch(e){throw JSON.stringify(query)+e.stack}
145
164
  }
146
165
  static parseQuery(query, attrsonly: true): Record<string, ParsedQuery>
147
166
  static parseQuery(query): ParsedQuery
@@ -180,12 +199,18 @@ class ArSyncContainerBase {
180
199
  static _load({ api, id, params, query }, root) {
181
200
  const parsedQuery = ArSyncRecord.parseQuery(query)
182
201
  const compactQuery = ArSyncRecord.compactQuery(parsedQuery)
183
- if (id) {
184
- return ModelBatchRequest.fetch(api, compactQuery, id).then(data => new ArSyncRecord(parsedQuery, data, null, root))
202
+ if (id != null) {
203
+ return modelBatchRequest.fetch(api, compactQuery, id).then(data => {
204
+ if (!data) throw { retry: false }
205
+ const request = { api, id, query: compactQuery }
206
+ return new ArSyncRecord(parsedQuery, data, request, root)
207
+ })
185
208
  } else {
186
209
  const request = { api, query: compactQuery, params }
187
- return ArSyncAPI.syncFetch(request).then((response: any) => {
188
- if (response.collection && response.order) {
210
+ return ArSyncApi.syncFetch(request).then((response: any) => {
211
+ if (!response) {
212
+ throw { retry: false }
213
+ } else if (response.collection && response.order) {
189
214
  return new ArSyncCollection(response.sync_keys, 'collection', parsedQuery, response, request, root)
190
215
  } else if (response instanceof Array) {
191
216
  return new ArSyncCollection([], '', parsedQuery, response, request, root)
@@ -222,6 +247,7 @@ class ArSyncRecord extends ArSyncContainerBase {
222
247
  id: number
223
248
  root
224
249
  query
250
+ queryAttributes
225
251
  data
226
252
  children: { [key: string]: ArSyncContainerBase | null }
227
253
  paths: string[]
@@ -231,15 +257,13 @@ class ArSyncRecord extends ArSyncContainerBase {
231
257
  this.root = root
232
258
  if (request) this.initForReload(request)
233
259
  this.query = query
260
+ this.queryAttributes = query.attributes || {}
234
261
  this.data = {}
235
262
  this.children = {}
236
263
  this.replaceData(data)
237
264
  }
238
- setSyncKeys(sync_keys: string[]) {
239
- this.sync_keys = sync_keys
240
- if (!this.sync_keys) {
241
- this.sync_keys = []
242
- }
265
+ setSyncKeys(sync_keys: string[] | undefined) {
266
+ this.sync_keys = sync_keys ?? []
243
267
  }
244
268
  replaceData(data) {
245
269
  this.setSyncKeys(data.sync_keys)
@@ -249,8 +273,8 @@ class ArSyncRecord extends ArSyncContainerBase {
249
273
  this.data.id = data.id
250
274
  }
251
275
  this.paths = []
252
- for (const key in this.query.attributes) {
253
- const subQuery = this.query.attributes[key]
276
+ for (const key in this.queryAttributes) {
277
+ const subQuery = this.queryAttributes[key]
254
278
  const aliasName = subQuery.as || key
255
279
  const subData = data[aliasName]
256
280
  const child = this.children[aliasName]
@@ -291,9 +315,9 @@ class ArSyncRecord extends ArSyncContainerBase {
291
315
  }
292
316
  }
293
317
  }
294
- if (this.query.attributes['*']) {
318
+ if (this.queryAttributes['*']) {
295
319
  for (const key in data) {
296
- if (!this.query.attributes[key] && this.data[key] !== data[key]) {
320
+ if (!this.queryAttributes[key] && this.data[key] !== data[key]) {
297
321
  this.mark()
298
322
  this.data[key] = data[key]
299
323
  }
@@ -302,8 +326,8 @@ class ArSyncRecord extends ArSyncContainerBase {
302
326
  this.subscribeAll()
303
327
  }
304
328
  onNotify(notifyData: NotifyData, path?: string) {
305
- const { action, class_name, id } = notifyData
306
- const query = path && this.query.attributes[path]
329
+ const { action, class_name: className, id } = notifyData
330
+ const query = path && this.queryAttributes[path]
307
331
  const aliasName = (query && query.as) || path;
308
332
  if (action === 'remove') {
309
333
  const child = this.children[aliasName]
@@ -314,7 +338,7 @@ class ArSyncRecord extends ArSyncContainerBase {
314
338
  this.onChange([aliasName], null)
315
339
  } else if (action === 'add') {
316
340
  if (this.data[aliasName] && this.data[aliasName].id === id) return
317
- ModelBatchRequest.fetch(class_name, ArSyncRecord.compactQuery(query), id).then(data => {
341
+ modelBatchRequest.fetch(className, ArSyncRecord.compactQuery(query), id).then(data => {
318
342
  if (!data || !this.data) return
319
343
  const model = new ArSyncRecord(query, data, null, this.root)
320
344
  const child = this.children[aliasName]
@@ -325,12 +349,17 @@ class ArSyncRecord extends ArSyncContainerBase {
325
349
  model.parentModel = this
326
350
  model.parentKey = aliasName
327
351
  this.onChange([aliasName], model.data)
352
+ }).catch(e => {
353
+ console.error(`failed to load ${className}:${id} ${e}`)
328
354
  })
329
355
  } else {
330
356
  const { field } = notifyData
331
357
  const query = field ? this.patchQuery(field) : this.reloadQuery()
332
- if (query) ModelBatchRequest.fetch(class_name, query, id).then(data => {
358
+ if (!query) return
359
+ modelBatchRequest.fetch(className, query, id).then(data => {
333
360
  if (this.data) this.update(data)
361
+ }).catch(e => {
362
+ console.error(`failed to load patch ${className}:${id} ${e}`)
334
363
  })
335
364
  }
336
365
  }
@@ -345,7 +374,7 @@ class ArSyncRecord extends ArSyncContainerBase {
345
374
  }
346
375
  }
347
376
  patchQuery(key: string) {
348
- const val = this.query.attributes[key]
377
+ const val = this.queryAttributes[key]
349
378
  if (!val) return
350
379
  let { attributes, as, params } = val
351
380
  if (attributes && Object.keys(val.attributes).length === 0) attributes = null
@@ -359,9 +388,9 @@ class ArSyncRecord extends ArSyncContainerBase {
359
388
  reloadQuery() {
360
389
  if (this.reloadQueryCache) return this.reloadQueryCache
361
390
  const reloadQuery = this.reloadQueryCache = { attributes: [] as any[] }
362
- for (const key in this.query.attributes) {
391
+ for (const key in this.queryAttributes) {
363
392
  if (key === 'sync_keys') continue
364
- const val = this.query.attributes[key]
393
+ const val = this.queryAttributes[key]
365
394
  if (!val || !val.attributes) {
366
395
  reloadQuery.attributes.push(key)
367
396
  } else if (!val.params && Object.keys(val.attributes).length === 0) {
@@ -372,7 +401,7 @@ class ArSyncRecord extends ArSyncContainerBase {
372
401
  }
373
402
  update(data) {
374
403
  for (const key in data) {
375
- const subQuery = this.query.attributes[key]
404
+ const subQuery = this.queryAttributes[key]
376
405
  if (subQuery && subQuery.attributes && Object.keys(subQuery.attributes).length > 0) continue
377
406
  if (this.data[key] === data[key]) continue
378
407
  this.mark()
@@ -392,11 +421,13 @@ class ArSyncRecord extends ArSyncContainerBase {
392
421
  }
393
422
  }
394
423
 
424
+ type Ordering = { first?: number; last?: number; orderBy: string; direction: 'asc' | 'desc' }
395
425
  class ArSyncCollection extends ArSyncContainerBase {
396
426
  root
397
427
  path: string
398
- order: { limit: number | null; key: string; mode: 'asc' | 'desc' } = { limit: null, mode: 'asc', key: 'id' }
428
+ ordering: Ordering = { orderBy: 'id', direction: 'asc' }
399
429
  query
430
+ queryAttributes
400
431
  compactQuery
401
432
  data: any[]
402
433
  children: ArSyncRecord[]
@@ -406,31 +437,28 @@ class ArSyncCollection extends ArSyncContainerBase {
406
437
  this.root = root
407
438
  this.path = path
408
439
  this.query = query
440
+ this.queryAttributes = query.attributes || {}
409
441
  this.compactQuery = ArSyncRecord.compactQuery(query)
410
442
  if (request) this.initForReload(request)
411
- if (query.params && (query.params.order || query.params.limit)) {
412
- this.setOrdering(query.params.limit, query.params.order)
443
+ if (query.params) {
444
+ this.setOrdering(query.params)
413
445
  }
414
446
  this.data = []
415
447
  this.children = []
416
448
  this.replaceData(data, sync_keys)
417
449
  }
418
- setOrdering(limit: unknown, order: unknown) {
419
- let mode: 'asc' | 'desc' = 'asc'
420
- let key: string = 'id'
421
- if (order === 'asc' || order === 'desc') {
422
- mode = order
423
- } else if (typeof order === 'object' && order) {
424
- const keys = Object.keys(order)
425
- if (keys.length > 1) throw 'multiple order keys are not supported'
426
- if (keys.length === 1) key = keys[0]
427
- mode = order[key] === 'asc' ? 'asc' : 'desc'
428
- }
429
- const limitNumber = (typeof limit === 'number') ? limit : null
430
- if (limitNumber !== null && key !== 'id') throw 'limit with custom order key is not supported'
431
- const subQuery = this.query.attributes[key]
432
- this.aliasOrderKey = (subQuery && subQuery.as) || key
433
- this.order = { limit: limitNumber, mode, key }
450
+ setOrdering(ordering: { first?: unknown; last?: unknown; orderBy?: unknown; direction?: unknown }) {
451
+ let direction: 'asc' | 'desc' = 'asc'
452
+ let orderBy: string = 'id'
453
+ let first: number | undefined = undefined
454
+ let last: number | undefined = undefined
455
+ if (ordering.direction === 'desc') direction = ordering.direction
456
+ if (typeof ordering.orderBy === 'string') orderBy = ordering.orderBy
457
+ if (typeof ordering.first === 'number') first = ordering.first
458
+ if (typeof ordering.last === 'number') last = ordering.last
459
+ const subQuery = this.queryAttributes[orderBy]
460
+ this.aliasOrderKey = (subQuery && subQuery.as) || orderBy
461
+ this.ordering = { first, last, direction, orderBy }
434
462
  }
435
463
  setSyncKeys(sync_keys: string[]) {
436
464
  if (sync_keys) {
@@ -439,26 +467,26 @@ class ArSyncCollection extends ArSyncContainerBase {
439
467
  this.sync_keys = []
440
468
  }
441
469
  }
442
- replaceData(data: any[] | { collection: any[]; order: any }, sync_keys: string[]) {
470
+ replaceData(data: any[] | { collection: any[]; ordering: Ordering }, sync_keys: string[]) {
443
471
  this.setSyncKeys(sync_keys)
444
- const existings: { [key: string]: ArSyncRecord } = {}
445
- for (const child of this.children) existings[child.data.id] = child
472
+ const existings = new Map<number, ArSyncRecord>()
473
+ for (const child of this.children) existings.set(child.data.id, child)
446
474
  let collection: any[]
447
- if ('collection' in data && 'order' in data) {
448
- collection = data.collection
449
- this.setOrdering(data.order.limit, data.order.mode)
450
- } else {
475
+ if (Array.isArray(data)) {
451
476
  collection = data
477
+ } else {
478
+ collection = data.collection
479
+ this.setOrdering(data.ordering)
452
480
  }
453
481
  const newChildren: any[] = []
454
482
  const newData: any[] = []
455
483
  for (const subData of collection) {
456
- let model: ArSyncRecord | null = null
457
- if (typeof(subData) === 'object' && subData && 'id' in subData) model = existings[subData.id]
484
+ let model: ArSyncRecord | undefined = undefined
485
+ if (typeof(subData) === 'object' && subData && 'sync_keys' in subData) model = existings.get(subData.id)
458
486
  let data = subData
459
487
  if (model) {
460
488
  model.replaceData(subData)
461
- } else if (subData.id) {
489
+ } else if (subData.sync_keys) {
462
490
  model = new ArSyncRecord(this.query, subData, null, this.root)
463
491
  model.parentModel = this
464
492
  model.parentKey = subData.id
@@ -471,7 +499,7 @@ class ArSyncCollection extends ArSyncContainerBase {
471
499
  }
472
500
  while (this.children.length) {
473
501
  const child = this.children.pop()!
474
- if (!existings[child.data.id]) child.release()
502
+ if (!existings.has(child.data.id)) child.release()
475
503
  }
476
504
  if (this.data.length || newChildren.length) this.mark()
477
505
  while (this.data.length) this.data.pop()
@@ -480,54 +508,78 @@ class ArSyncCollection extends ArSyncContainerBase {
480
508
  this.subscribeAll()
481
509
  }
482
510
  consumeAdd(className: string, id: number) {
511
+ const { first, last, direction } = this.ordering
512
+ const limit = first || last
483
513
  if (this.data.findIndex(a => a.id === id) >= 0) return
484
- if (this.order.limit === this.data.length) {
485
- if (this.order.mode === 'asc') {
486
- const last = this.data[this.data.length - 1]
487
- if (last && last.id < id) return
514
+ if (limit && limit <= this.data.length) {
515
+ const lastItem = this.data[this.data.length - 1]
516
+ const firstItem = this.data[0]
517
+ if (direction === 'asc') {
518
+ if (first) {
519
+ if (lastItem && lastItem.id < id) return
520
+ } else {
521
+ if (firstItem && id < firstItem.id) return
522
+ }
488
523
  } else {
489
- const last = this.data[this.data.length - 1]
490
- if (last && last.id > id) return
524
+ if (first) {
525
+ if (lastItem && id < lastItem.id) return
526
+ } else {
527
+ if (firstItem && firstItem.id < id) return
528
+ }
491
529
  }
492
530
  }
493
- ModelBatchRequest.fetch(className, this.compactQuery, id).then((data: any) => {
531
+ modelBatchRequest.fetch(className, this.compactQuery, id).then((data: any) => {
494
532
  if (!data || !this.data) return
495
533
  const model = new ArSyncRecord(this.query, data, null, this.root)
496
534
  model.parentModel = this
497
535
  model.parentKey = id
498
- const overflow = this.order.limit && this.order.limit === this.data.length
536
+ const overflow = limit && limit <= this.data.length
499
537
  let rmodel: ArSyncRecord | undefined
500
538
  this.mark()
501
539
  const orderKey = this.aliasOrderKey
502
- if (this.order.mode === 'asc') {
503
- const last = this.data[this.data.length - 1]
504
- this.children.push(model)
505
- this.data.push(model.data)
506
- if (last && last[orderKey] > data[orderKey]) this.markAndSort()
507
- if (overflow) {
508
- rmodel = this.children.shift()!
509
- rmodel.release()
510
- this.data.shift()
540
+ const firstItem = this.data[0]
541
+ const lastItem = this.data[this.data.length - 1]
542
+ if (direction === 'asc') {
543
+ if (firstItem && data[orderKey] < firstItem[orderKey]) {
544
+ this.children.unshift(model)
545
+ this.data.unshift(model.data)
546
+ } else {
547
+ const skipSort = lastItem && lastItem[orderKey] < data[orderKey]
548
+ this.children.push(model)
549
+ this.data.push(model.data)
550
+ if (!skipSort) this.markAndSort()
511
551
  }
512
552
  } else {
513
- const first = this.data[0]
514
- this.children.unshift(model)
515
- this.data.unshift(model.data)
516
- if (first && first[orderKey] > data[orderKey]) this.markAndSort()
517
- if (overflow) {
553
+ if (firstItem && data[orderKey] > firstItem[orderKey]) {
554
+ this.children.unshift(model)
555
+ this.data.unshift(model.data)
556
+ } else {
557
+ const skipSort = lastItem && lastItem[orderKey] > data[orderKey]
558
+ this.children.push(model)
559
+ this.data.push(model.data)
560
+ if (!skipSort) this.markAndSort()
561
+ }
562
+ }
563
+ if (overflow) {
564
+ if (first) {
518
565
  rmodel = this.children.pop()!
519
- rmodel.release()
520
566
  this.data.pop()
567
+ } else {
568
+ rmodel = this.children.shift()!
569
+ this.data.shift()
521
570
  }
571
+ rmodel.release()
522
572
  }
523
573
  this.onChange([model.id], model.data)
524
574
  if (rmodel) this.onChange([rmodel.id], null)
575
+ }).catch(e => {
576
+ console.error(`failed to load ${className}:${id} ${e}`)
525
577
  })
526
578
  }
527
579
  markAndSort() {
528
580
  this.mark()
529
581
  const orderKey = this.aliasOrderKey
530
- if (this.order.mode === 'asc') {
582
+ if (this.ordering.direction === 'asc') {
531
583
  this.children.sort((a, b) => a.data[orderKey] < b.data[orderKey] ? -1 : +1)
532
584
  this.data.sort((a, b) => a[orderKey] < b[orderKey] ? -1 : +1)
533
585
  } else {
@@ -38,6 +38,7 @@ export default class ConnectionManager {
38
38
  return { unsubscribe }
39
39
  }
40
40
  subscribe(key, func) {
41
+ if (!this.networkStatus) return { unsubscribe(){} }
41
42
  const subscription = this.connect(key)
42
43
  const id = subscription.serial++
43
44
  subscription.ref++
data/src/core/DataType.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  type RecordType = { _meta?: { query: any } }
2
- type Values<T> = T extends { [K in keyof T]: infer U } ? U : never
2
+ type Values<T> = T[keyof T]
3
3
  type AddNullable<Test, Type> = null extends Test ? Type | null : Type
4
4
  type DataTypeExtractField<BaseType, Key extends keyof BaseType> = Exclude<BaseType[Key], null> extends RecordType
5
5
  ? AddNullable<BaseType[Key], {}>
@@ -51,27 +51,26 @@ type CheckAttributesField<P, Q> = Q extends { attributes: infer R }
51
51
 
52
52
  type IsAnyCompareLeftType = { __any: never }
53
53
 
54
- type CollectExtraFields<Type, Path> =
54
+ type CollectExtraFields<Type, Key> =
55
55
  IsAnyCompareLeftType extends Type
56
- ? null
56
+ ? never
57
57
  : Type extends ExtraFieldErrorType
58
- ? Path
58
+ ? Key
59
59
  : Type extends (infer R)[]
60
- ? _CollectExtraFields<R>
61
- : _CollectExtraFields<Type>
62
-
63
- type _CollectExtraFields<Type> = Type extends object
64
- ? (keyof (Type) extends never
65
- ? null
66
- : Values<{ [key in keyof Type]: CollectExtraFields<Type[key], key> }>
67
- )
68
- : null
60
+ ? {
61
+ 0: Values<{ [key in keyof R]: CollectExtraFields<R[key], key> }>
62
+ 1: never
63
+ }[R extends object ? 0 : 1]
64
+ : {
65
+ 0: Values<{ [key in keyof Type]: CollectExtraFields<Type[key], key> }>
66
+ 1: never
67
+ }[Type extends object ? 0 : 1]
69
68
 
70
69
  type SelectString<T> = T extends string ? T : never
71
70
  type _ValidateDataTypeExtraFileds<Extra, Type> = SelectString<Extra> extends never
72
71
  ? Type
73
- : { error: { extraFields: SelectString<Extra> } }
74
- type ValidateDataTypeExtraFileds<Type> = _ValidateDataTypeExtraFileds<CollectExtraFields<Type, []>, Type>
72
+ : { error: { extraFields: Extra } }
73
+ type ValidateDataTypeExtraFileds<Type> = _ValidateDataTypeExtraFileds<CollectExtraFields<Type, never>, Type>
75
74
 
76
75
  type RequestBase = { api: string; query: any; id?: number; params?: any; _meta?: { data: any } }
77
76
  type DataTypeBaseFromRequestType<R extends RequestBase, ID> = R extends { _meta?: { data: infer DataType } }
@@ -83,4 +82,4 @@ type DataTypeBaseFromRequestType<R extends RequestBase, ID> = R extends { _meta?
83
82
  : never
84
83
  export type DataTypeFromRequest<Req extends RequestBase, R extends RequestBase> = ValidateDataTypeExtraFileds<
85
84
  DataTypeFromQuery<DataTypeBaseFromRequestType<Req, R['id']>, R['query']>
86
- >
85
+ >