ar_sync 1.0.1 → 1.0.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (64) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile.lock +5 -2
  3. data/README.md +6 -6
  4. data/ar_sync.gemspec +2 -2
  5. data/bin/console +1 -2
  6. data/core/ActioncableAdapter.d.ts +26 -2
  7. data/core/ActioncableAdapter.js +3 -3
  8. data/core/ArSyncApi.d.ts +7 -4
  9. data/core/ArSyncApi.js +9 -4
  10. data/core/{ArSyncModelBase.d.ts → ArSyncModel.d.ts} +13 -18
  11. data/core/{ArSyncModelBase.js → ArSyncModel.js} +33 -10
  12. data/{graph → core}/ArSyncStore.d.ts +3 -3
  13. data/{graph → core}/ArSyncStore.js +188 -57
  14. data/core/DataType.d.ts +17 -13
  15. data/core/hooks.d.ts +28 -0
  16. data/core/hooks.js +105 -0
  17. data/index.d.ts +2 -0
  18. data/index.js +6 -0
  19. data/lib/ar_sync.rb +1 -18
  20. data/lib/ar_sync/class_methods.rb +31 -89
  21. data/lib/ar_sync/collection.rb +4 -29
  22. data/lib/ar_sync/core.rb +35 -67
  23. data/lib/ar_sync/instance_methods.rb +40 -86
  24. data/lib/ar_sync/rails.rb +18 -27
  25. data/lib/ar_sync/type_script.rb +39 -18
  26. data/lib/ar_sync/version.rb +1 -1
  27. data/lib/generators/ar_sync/install/install_generator.rb +33 -32
  28. data/lib/generators/ar_sync/types/types_generator.rb +6 -3
  29. data/package-lock.json +21 -10
  30. data/package.json +1 -1
  31. data/src/core/ActioncableAdapter.ts +28 -3
  32. data/src/core/ArSyncApi.ts +8 -4
  33. data/src/core/{ArSyncModelBase.ts → ArSyncModel.ts} +51 -20
  34. data/src/{graph → core}/ArSyncStore.ts +199 -84
  35. data/src/core/DataType.ts +33 -20
  36. data/src/core/hooks.ts +108 -0
  37. data/src/index.ts +2 -0
  38. data/vendor/assets/javascripts/{ar_sync_tree.js.erb → ar_sync.js.erb} +6 -7
  39. metadata +33 -38
  40. data/core/hooksBase.d.ts +0 -29
  41. data/core/hooksBase.js +0 -80
  42. data/graph/ArSyncModel.d.ts +0 -10
  43. data/graph/ArSyncModel.js +0 -22
  44. data/graph/hooks.d.ts +0 -3
  45. data/graph/hooks.js +0 -10
  46. data/graph/index.d.ts +0 -2
  47. data/graph/index.js +0 -4
  48. data/src/core/hooksBase.ts +0 -86
  49. data/src/graph/ArSyncModel.ts +0 -21
  50. data/src/graph/hooks.ts +0 -7
  51. data/src/graph/index.ts +0 -2
  52. data/src/tree/ArSyncModel.ts +0 -145
  53. data/src/tree/ArSyncStore.ts +0 -323
  54. data/src/tree/hooks.ts +0 -7
  55. data/src/tree/index.ts +0 -2
  56. data/tree/ArSyncModel.d.ts +0 -39
  57. data/tree/ArSyncModel.js +0 -143
  58. data/tree/ArSyncStore.d.ts +0 -21
  59. data/tree/ArSyncStore.js +0 -365
  60. data/tree/hooks.d.ts +0 -3
  61. data/tree/hooks.js +0 -10
  62. data/tree/index.d.ts +0 -2
  63. data/tree/index.js +0 -4
  64. data/vendor/assets/javascripts/ar_sync_graph.js.erb +0 -17
@@ -1,3 +1,3 @@
1
1
  module ArSync
2
- VERSION = '1.0.1'
2
+ VERSION = '1.0.2'
3
3
  end
@@ -1,46 +1,47 @@
1
1
  module ArSync
2
2
  class InstallGenerator < ::Rails::Generators::Base
3
- class_option :mode, enum: %w[tree graph], desc: 'sync mode', default: 'graph'
3
+ def create_schema_class
4
+ create_file 'app/models/sync_schema.rb', <<~CODE
5
+ class SyncSchema < ArSync::SyncSchemaBase
6
+ # serializer_field :profile, type: User do |current_user|
7
+ # current_user
8
+ # end
4
9
 
5
- def create_api_controller
6
- base_code = <<~CODE
7
- include ArSync::ApiControllerConcern
10
+ # serializer_field :post, type: Post do |current_user, id:|
11
+ # Post.where(current_user_can_access).find_by id: id
12
+ # end
8
13
 
9
- # serializer_field :profile, type: User do |_user|
10
- # current_user
11
- # end
14
+ # Reload API for all types should be defined here.
12
15
 
13
- # serializer_field :post, type: Post do |_user, id:|
14
- # Post.where(current_user_can_access).find id
15
- # end
16
- CODE
17
- graph_additional_code = <<~CODE
18
- # Reload API for all types should be defined here.
16
+ # serializer_field :User do |current_user, ids:|
17
+ # User.where(current_user_can_access).where id: ids
18
+ # end
19
19
 
20
- # serializer_field :User do |_user, ids:|
21
- # User.where(current_user_can_access).where id: ids
22
- # end
20
+ # serializer_field :Post do |current_user, ids:|
21
+ # Post.where(current_user_can_access).where id: ids
22
+ # end
23
23
 
24
- # serializer_field :Post do |_user, ids:|
25
- # Post.where(current_user_can_access).where id: ids
26
- # end
24
+ # serializer_field :Comment do |current_user, ids:|
25
+ # Comment.where(current_user_can_access).where id: ids
26
+ # end
27
+ end
28
+ CODE
29
+ end
27
30
 
28
- # serializer_field :Comment do |_user, ids:|
29
- # Comment.where(current_user_can_access).where id: ids
30
- # end
31
+ def create_api_controller
32
+ create_file 'app/controllers/sync_api_controller.rb', <<~CODE
33
+ class SyncApiController < ApplicationController
34
+ include ArSync::ApiControllerConcern
35
+ def schema
36
+ SyncSchema.new
37
+ end
38
+ end
31
39
  CODE
32
- controller_body = options['mode'] == 'tree' ? base_code : base_code + "\n" + graph_additional_code
33
- code = [
34
- "class SyncApiController < ApplicationController\n",
35
- controller_body.lines.map { |l| l.blank? ? l : ' ' + l },
36
- "end\n"
37
- ].join
38
- create_file 'app/controllers/sync_api_controller.rb', code
39
40
  end
40
41
 
41
42
  def create_config
42
43
  create_file 'config/initializers/ar_sync.rb', <<~CODE
43
- ArSync.use :#{options['mode']}
44
+ ActiveRecord::Base.include ArSync::ModelBase
44
45
  ArSync.configure do |config|
45
46
  config.current_user_method = :current_user
46
47
  config.key_prefix = 'ar_sync_'
@@ -75,10 +76,10 @@ module ArSync
75
76
  inject_into_file(
76
77
  'app/assets/javascripts/application.js',
77
78
  [
78
- "//= require ar_sync_#{options['mode']}",
79
+ '//= require ar_sync',
79
80
  '//= require action_cable',
80
81
  '//= require ar_sync_actioncable_adapter',
81
- 'ArSyncModel.setConnectionAdapter(new ArSyncActionCableAdapter())'
82
+ 'ArSyncModel.setConnectionAdapter(new ArSyncActionCableAdapter(ActionCable))'
82
83
  ].join("\n") + "\n",
83
84
  before: '//= require_tree .'
84
85
  )
@@ -1,11 +1,14 @@
1
1
  require 'shellwords'
2
2
  module ArSync
3
3
  class TypesGenerator < ::Rails::Generators::Base
4
- argument :output_dir, type: :string, desc: "output directory"
4
+ argument :output_dir, type: :string, required: true
5
+ argument :schema_class_name, type: :string, required: false
5
6
  def generate_typescript_files
6
7
  dir = ::Rails.root.join output_dir
7
- comment = "// generated by: rails generate ar_sync:types #{Shellwords.escape output_dir}\n\n"
8
- ArSync::TypeScript.generate_typed_files SyncApiController, dir: dir, comment: comment
8
+ schema_class = (schema_class_name || 'SyncSchema').constantize
9
+ args = [output_dir, *schema_class_name].map { |a| Shellwords.escape a }
10
+ comment = "// generated by: rails generate ar_sync:types #{args.join(' ')}\n\n"
11
+ ArSync::TypeScript.generate_typed_files schema_class, dir: dir, comment: comment
9
12
  end
10
13
  end
11
14
  end
@@ -338,10 +338,21 @@
338
338
  }
339
339
  },
340
340
  "eslint-utils": {
341
- "version": "1.3.1",
342
- "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-1.3.1.tgz",
343
- "integrity": "sha512-Z7YjnIldX+2XMcjr7ZkgEsOj/bREONV60qYeB/bjMAqqqZ4zxKyWX+BOUkdmRmA9riiIPVvo5x86m5elviOk0Q==",
344
- "dev": true
341
+ "version": "1.4.3",
342
+ "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-1.4.3.tgz",
343
+ "integrity": "sha512-fbBN5W2xdY45KulGXmLHZ3c3FHfVYmKg0IrAKGOkT/464PQsx2UeIzfz1RmEci+KLm1bBaAzZAh8+/E+XAeZ8Q==",
344
+ "dev": true,
345
+ "requires": {
346
+ "eslint-visitor-keys": "^1.1.0"
347
+ },
348
+ "dependencies": {
349
+ "eslint-visitor-keys": {
350
+ "version": "1.1.0",
351
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.1.0.tgz",
352
+ "integrity": "sha512-8y9YjtM1JBJU/A9Kc+SbaOV4y29sSWckBwMHa+FGtVj5gN/sbnKDf6xJUl+8g7FAij9LVaP8C24DUiH/f/2Z9A==",
353
+ "dev": true
354
+ }
355
+ }
345
356
  },
346
357
  "eslint-visitor-keys": {
347
358
  "version": "1.0.0",
@@ -640,9 +651,9 @@
640
651
  }
641
652
  },
642
653
  "lodash": {
643
- "version": "4.17.11",
644
- "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz",
645
- "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==",
654
+ "version": "4.17.15",
655
+ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz",
656
+ "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==",
646
657
  "dev": true
647
658
  },
648
659
  "lodash.unescape": {
@@ -1067,9 +1078,9 @@
1067
1078
  }
1068
1079
  },
1069
1080
  "typescript": {
1070
- "version": "3.4.5",
1071
- "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.4.5.tgz",
1072
- "integrity": "sha512-YycBxUb49UUhdNMU5aJ7z5Ej2XGmaIBL0x34vZ82fn3hGvD+bgrMrVDpatgz2f7YxUMJxMkbWxJZeAvDxVe7Vw==",
1081
+ "version": "3.7.2",
1082
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.7.2.tgz",
1083
+ "integrity": "sha512-ml7V7JfiN2Xwvcer+XAf2csGO1bPBdRbFCkYBczNZggrBZ9c7G3riSUeJmqEU5uOtXNPMhE3n+R4FA/3YOAWOQ==",
1073
1084
  "dev": true
1074
1085
  },
1075
1086
  "uri-js": {
@@ -12,7 +12,7 @@
12
12
  "actioncable": "^5.2.0",
13
13
  "@types/actioncable": "^5.2.0",
14
14
  "eslint": "^5.16.0",
15
- "typescript": "^3.4.5",
15
+ "typescript": "3.7.2",
16
16
  "@typescript-eslint/eslint-plugin": "^1.6.0",
17
17
  "@typescript-eslint/parser": "^1.6.0"
18
18
  }
@@ -1,11 +1,36 @@
1
- import * as ActionCable from 'actioncable'
2
1
  import ConnectionAdapter from './ConnectionAdapter'
3
2
 
3
+ declare module ActionCable {
4
+ function createConsumer(): Cable
5
+ interface Cable {
6
+ subscriptions: Subscriptions
7
+ }
8
+ interface CreateMixin {
9
+ connected: () => void
10
+ disconnected: () => void
11
+ received: (obj: any) => void
12
+ }
13
+ interface ChannelNameWithParams {
14
+ channel: string
15
+ [key: string]: any
16
+ }
17
+ interface Subscriptions {
18
+ create(channel: ChannelNameWithParams, obj: CreateMixin): Channel
19
+ }
20
+ interface Channel {
21
+ unsubscribe(): void;
22
+ perform(action: string, data: {}): void;
23
+ send(data: any): boolean;
24
+ }
25
+ }
26
+
4
27
  export default class ActionCableAdapter implements ConnectionAdapter {
5
28
  connected: boolean
6
29
  _cable: ActionCable.Cable
7
- constructor() {
30
+ actionCableClass: typeof ActionCable
31
+ constructor(actionCableClass: typeof ActionCable) {
8
32
  this.connected = true
33
+ this.actionCableClass = actionCableClass
9
34
  this.subscribe(Math.random().toString(), () => {})
10
35
  }
11
36
  subscribe(key: string, received: (data: any) => void) {
@@ -19,7 +44,7 @@ export default class ActionCableAdapter implements ConnectionAdapter {
19
44
  this.connected = true
20
45
  this.onreconnect()
21
46
  }
22
- if (!this._cable) this._cable = ActionCable.createConsumer()
47
+ if (!this._cable) this._cable = this.actionCableClass.createConsumer()
23
48
  return this._cable.subscriptions.create(
24
49
  { channel: 'SyncChannel', key },
25
50
  { received, disconnected, connected }
@@ -5,6 +5,7 @@ async function apiBatchFetch(endpoint: string, requests: object[]) {
5
5
  }
6
6
  const body = JSON.stringify({ requests })
7
7
  const option = { credentials: 'include', method: 'POST', headers, body } as const
8
+ if (ArSyncApi.domain) endpoint = ArSyncApi.domain + endpoint
8
9
  const res = await fetch(endpoint, option)
9
10
  if (res.status === 200) return res.json()
10
11
  throw new Error(res.statusText)
@@ -43,7 +44,7 @@ class ApiFetcher {
43
44
  }
44
45
  }
45
46
  this.batches = []
46
- apiBatchFetch(this.endpoint, requests).then((results) => {
47
+ ArSyncApi._batchFetch(this.endpoint, requests).then((results) => {
47
48
  for (const i in callbacksList) {
48
49
  const result = results[i]
49
50
  const callbacks = callbacksList[i]
@@ -52,11 +53,11 @@ class ApiFetcher {
52
53
  callback.resolve(result.data)
53
54
  } else {
54
55
  const error = result.error || { type: 'Unknown Error' }
55
- callback.reject(error)
56
+ callback.reject({ ...error, retry: false })
56
57
  }
57
58
  }
58
59
  }
59
- }).catch((e) => {
60
+ }).catch(e => {
60
61
  const error = { type: e.name, message: e.message, retry: true }
61
62
  for (const callbacks of callbacksList) {
62
63
  for (const callback of callbacks) callback.reject(error)
@@ -69,7 +70,10 @@ class ApiFetcher {
69
70
 
70
71
  const staticFetcher = new ApiFetcher('/static_api')
71
72
  const syncFetcher = new ApiFetcher('/sync_api')
72
- export default {
73
+ const ArSyncApi = {
74
+ domain: null as string | null,
75
+ _batchFetch: apiBatchFetch,
73
76
  fetch: (request: object) => staticFetcher.fetch(request),
74
77
  syncFetch: (request: object) => syncFetcher.fetch(request),
75
78
  }
79
+ export default ArSyncApi
@@ -1,18 +1,35 @@
1
+ import ArSyncStore from './ArSyncStore'
2
+ import ArSyncConnectionManager from './ConnectionManager'
3
+ import ConnectionAdapter from './ConnectionAdapter'
4
+
1
5
  interface Request { api: string; query: any; params?: any }
2
- type Path = (string | number)[]
6
+ type Path = Readonly<(string | number)[]>
3
7
  interface Change { path: Path; value: any }
4
8
  type ChangeCallback = (change: Change) => void
5
9
  type LoadCallback = () => void
6
10
  type ConnectionCallback = (status: boolean) => void
7
11
  type SubscriptionType = 'load' | 'change' | 'connection'
8
12
  type SubscriptionCallback = ChangeCallback | LoadCallback | ConnectionCallback
9
- interface Adapter {
10
- subscribe: (key: string, received: (data: any) => void) => { unsubscribe: () => void }
11
- ondisconnect: () => void
12
- onreconnect: () => void
13
- }
14
13
 
15
- export default abstract class ArSyncModelBase<T> {
14
+ type PathFirst<P extends Readonly<any[]>> = ((...args: P) => void) extends (first: infer First, ...other: any) => void ? First : never
15
+
16
+ type PathRest<U> = U extends Readonly<any[]> ?
17
+ ((...args: U) => any) extends (head: any, ...args: infer T) => any
18
+ ? U extends Readonly<[any, any, ...any[]]> ? T : never
19
+ : never
20
+ : never;
21
+
22
+ type DigResult<Data, P extends Readonly<any[]>> =
23
+ Data extends null | undefined ? Data :
24
+ PathFirst<P> extends never ? Data :
25
+ PathFirst<P> extends keyof Data ?
26
+ (Data extends Readonly<any[]> ? undefined : never) | {
27
+ 0: Data[PathFirst<P>];
28
+ 1: DigResult<Data[PathFirst<P>], PathRest<P>>
29
+ }[PathRest<P> extends never ? 0 : 1]
30
+ : undefined
31
+
32
+ export default class ArSyncModel<T> {
16
33
  private _ref
17
34
  private _listenerSerial: number
18
35
  private _listeners
@@ -20,16 +37,14 @@ export default abstract class ArSyncModelBase<T> {
20
37
  notfound?: boolean
21
38
  connected: boolean
22
39
  data: T | null
23
- static _cache: { [key: string]: { key: string; count: number; timer: number | null; model } }
24
- static cacheTimeout: number
25
- abstract refManagerClass(): any
26
- abstract connectionManager(): { networkStatus: boolean }
40
+ static _cache: { [key: string]: { key: string; count: number; timer: number | null; model } } = {}
41
+ static cacheTimeout: number = 10 * 1000
27
42
  constructor(request: Request, option?: { immutable: boolean }) {
28
- this._ref = this.refManagerClass().retrieveRef(request, option)
43
+ this._ref = ArSyncModel.retrieveRef(request, option)
29
44
  this._listenerSerial = 0
30
45
  this._listeners = {}
31
46
  this.complete = false
32
- this.connected = this.connectionManager().networkStatus
47
+ this.connected = ArSyncStore.connectionManager.networkStatus
33
48
  const setData = () => {
34
49
  this.data = this._ref.model.data
35
50
  this.complete = this._ref.model.complete
@@ -52,6 +67,23 @@ export default abstract class ArSyncModelBase<T> {
52
67
  })
53
68
  return subscription
54
69
  }
70
+ dig<P extends Path>(path: P) {
71
+ return ArSyncModel.digData(this.data, path)
72
+ }
73
+ static digData<Data, P extends Path>(data: Data, path: P): DigResult<Data, P> {
74
+ function dig(data: Data, path: Path) {
75
+ if (path.length === 0) return data as any
76
+ if (data == null) return data
77
+ const key = path[0]
78
+ const other = path.slice(1)
79
+ if (Array.isArray(data)) {
80
+ return this.digData(data.find(el => el.id === key), other)
81
+ } else {
82
+ return this.digData(data[key], other)
83
+ }
84
+ }
85
+ return dig(data, path)
86
+ }
55
87
  subscribe(event: SubscriptionType, callback: SubscriptionCallback): { unsubscribe: () => void } {
56
88
  const id = this._listenerSerial++
57
89
  const subscription = this._ref.model.subscribe(event, callback)
@@ -74,7 +106,7 @@ export default abstract class ArSyncModelBase<T> {
74
106
  release() {
75
107
  for (const id in this._listeners) this._listeners[id].unsubscribe()
76
108
  this._listeners = {}
77
- this.refManagerClass()._detach(this._ref)
109
+ ArSyncModel._detach(this._ref)
78
110
  this._ref = null
79
111
  }
80
112
  static retrieveRef(
@@ -84,15 +116,12 @@ export default abstract class ArSyncModelBase<T> {
84
116
  const key = JSON.stringify([request, option])
85
117
  let ref = this._cache[key]
86
118
  if (!ref) {
87
- const model = this.createRefModel(request, option)
119
+ const model = new ArSyncStore(request, option)
88
120
  ref = this._cache[key] = { key, count: 0, timer: null, model }
89
121
  }
90
122
  this._attach(ref)
91
123
  return ref
92
124
  }
93
- static createRefModel(_request: Request, _option?: { immutable: boolean }) {
94
- throw 'abstract method'
95
- }
96
125
  static _detach(ref) {
97
126
  ref.count--
98
127
  const timeout = this.cacheTimeout
@@ -111,8 +140,10 @@ export default abstract class ArSyncModelBase<T> {
111
140
  ref.count++
112
141
  if (ref.timer) clearTimeout(ref.timer)
113
142
  }
114
- static setConnectionAdapter(_adapter: Adapter) {}
115
- static waitForLoad(...models: ArSyncModelBase<{}>[]) {
143
+ static setConnectionAdapter(adapter: ConnectionAdapter) {
144
+ ArSyncStore.connectionManager = new ArSyncConnectionManager(adapter)
145
+ }
146
+ static waitForLoad(...models: ArSyncModel<{}>[]) {
116
147
  return new Promise((resolve) => {
117
148
  let count = 0
118
149
  for (const model of models) {
@@ -1,4 +1,4 @@
1
- import ArSyncAPI from '../core/ArSyncApi'
1
+ import ArSyncAPI from './ArSyncApi'
2
2
 
3
3
  const ModelBatchRequest = {
4
4
  timer: null,
@@ -16,7 +16,7 @@ const ModelBatchRequest = {
16
16
  }
17
17
  }
18
18
  },
19
- fetch(api, query, id) {
19
+ fetch(api: string, query, id: number) {
20
20
  this.setTimer()
21
21
  return new Promise(resolve => {
22
22
  const queryJSON = JSON.stringify(query)
@@ -51,13 +51,20 @@ const ModelBatchRequest = {
51
51
  }
52
52
  }
53
53
 
54
+ type ParsedQuery = {
55
+ attributes: Record<string, ParsedQuery>
56
+ as?: string
57
+ params: any
58
+ } | {}
59
+
54
60
  class ArSyncContainerBase {
55
61
  data
56
62
  listeners
57
63
  networkSubscriber
58
64
  parentModel
59
65
  parentKey
60
- children: ArSyncContainerBase[]
66
+ children: ArSyncContainerBase[] | { [key: string]: ArSyncContainerBase | null }
67
+ sync_keys: string[]
61
68
  onConnectionChange
62
69
  constructor() {
63
70
  this.listeners = []
@@ -89,15 +96,57 @@ class ArSyncContainerBase {
89
96
  onChange(path, data) {
90
97
  if (this.parentModel) this.parentModel.onChange([this.parentKey, ...path], data)
91
98
  }
92
- subscribe(key, listener) {
99
+ subscribe(key: string, listener) {
93
100
  this.listeners.push(ArSyncStore.connectionManager.subscribe(key, listener))
94
101
  }
95
102
  unsubscribeAll() {
96
103
  for (const l of this.listeners) l.unsubscribe()
97
104
  this.listeners = []
98
105
  }
99
- static parseQuery(query, attrsonly = false){
100
- const attributes = {}
106
+ static compactQuery(query: ParsedQuery) {
107
+ function compactAttributes(attributes: Record<string, ParsedQuery>) {
108
+ const attrs = {}
109
+ const keys: string[] = []
110
+ for (const key in attributes) {
111
+ const c = compactQuery(attributes[key])
112
+ if (c === true) {
113
+ keys.push(key)
114
+ } else {
115
+ attrs[key] = c
116
+ }
117
+ }
118
+ if (Object.keys(attrs).length === 0) {
119
+ if (keys.length === 0) return [true, false]
120
+ if (keys.length === 1) return [keys[0], false]
121
+ return [keys]
122
+ }
123
+ const needsEscape = attrs['attributes'] || attrs['params'] || attrs['as']
124
+ if (keys.length === 0) return [attrs, needsEscape]
125
+ return [[...keys, attrs], needsEscape]
126
+ }
127
+ function compactQuery(query: ParsedQuery) {
128
+ if (!('attributes' in query)) return true
129
+ const { as, params } = query
130
+ const [attributes, needsEscape] = compactAttributes(query.attributes)
131
+ if (as == null && params == null){
132
+ if (needsEscape) return { attributes }
133
+ return attributes
134
+ }
135
+ const result: { as?: string; params?: any; attributes?: any } = {}
136
+ if (as) result.as = as
137
+ if (params) result.params = params
138
+ if (attributes !== true) result.attributes = attributes
139
+ return result
140
+ }
141
+ try{
142
+ const result = compactQuery(query)
143
+ return result === true ? {} : result
144
+ }catch(e){throw JSON.stringify(query)+e.stack}
145
+ }
146
+ static parseQuery(query, attrsonly: true): Record<string, ParsedQuery>
147
+ static parseQuery(query): ParsedQuery
148
+ static parseQuery(query, attrsonly?: true) {
149
+ const attributes: Record<string, ParsedQuery> = {}
101
150
  let column = null
102
151
  let params = null
103
152
  if (!query) query = []
@@ -130,13 +179,16 @@ class ArSyncContainerBase {
130
179
  }
131
180
  static _load({ api, id, params, query }, root) {
132
181
  const parsedQuery = ArSyncRecord.parseQuery(query)
182
+ const compactQuery = ArSyncRecord.compactQuery(parsedQuery)
133
183
  if (id) {
134
- return ModelBatchRequest.fetch(api, query, id).then(data => new ArSyncRecord(parsedQuery, data[0], null, root))
184
+ return ModelBatchRequest.fetch(api, compactQuery, id).then(data => new ArSyncRecord(parsedQuery, data, null, root))
135
185
  } else {
136
- const request = { api, query, params }
186
+ const request = { api, query: compactQuery, params }
137
187
  return ArSyncAPI.syncFetch(request).then((response: any) => {
138
188
  if (response.collection && response.order) {
139
189
  return new ArSyncCollection(response.sync_keys, 'collection', parsedQuery, response, request, root)
190
+ } else if (response instanceof Array) {
191
+ return new ArSyncCollection([], '', parsedQuery, response, request, root)
140
192
  } else {
141
193
  return new ArSyncRecord(parsedQuery, response, request, root)
142
194
  }
@@ -159,14 +211,20 @@ class ArSyncContainerBase {
159
211
  }
160
212
  }
161
213
 
214
+ type NotifyData = {
215
+ action: 'add' | 'remove' | 'update'
216
+ class_name: string
217
+ id: number
218
+ field?: string
219
+ }
220
+
162
221
  class ArSyncRecord extends ArSyncContainerBase {
163
- id
222
+ id: number
164
223
  root
165
224
  query
166
225
  data
167
- children
168
- sync_keys
169
- paths
226
+ children: { [key: string]: ArSyncContainerBase | null }
227
+ paths: string[]
170
228
  reloadQueryCache
171
229
  constructor(query, data, request, root) {
172
230
  super()
@@ -177,11 +235,10 @@ class ArSyncRecord extends ArSyncContainerBase {
177
235
  this.children = {}
178
236
  this.replaceData(data)
179
237
  }
180
- setSyncKeys(sync_keys) {
238
+ setSyncKeys(sync_keys: string[]) {
181
239
  this.sync_keys = sync_keys
182
240
  if (!this.sync_keys) {
183
241
  this.sync_keys = []
184
- console.error('warning: no sync_keys')
185
242
  }
186
243
  }
187
244
  replaceData(data) {
@@ -196,10 +253,11 @@ class ArSyncRecord extends ArSyncContainerBase {
196
253
  const subQuery = this.query.attributes[key]
197
254
  const aliasName = subQuery.as || key
198
255
  const subData = data[aliasName]
256
+ const child = this.children[aliasName]
199
257
  if (key === 'sync_keys') continue
200
- if (subQuery.attributes && (subData instanceof Array || (subData && subData.collection && subData.order))) {
201
- if (this.children[aliasName]) {
202
- this.children[aliasName].replaceData(subData, this.sync_keys)
258
+ if (subData instanceof Array || (subData && subData.collection && subData.order)) {
259
+ if (child) {
260
+ child.replaceData(subData, this.sync_keys)
203
261
  } else {
204
262
  const collection = new ArSyncCollection(this.sync_keys, key, subQuery, subData, null, this.root)
205
263
  this.mark()
@@ -209,10 +267,10 @@ class ArSyncRecord extends ArSyncContainerBase {
209
267
  collection.parentKey = aliasName
210
268
  }
211
269
  } else {
212
- if (subQuery.attributes && Object.keys(subQuery.attributes).length > 0) this.paths.push(key);
270
+ if (subQuery.attributes && Object.keys(subQuery.attributes).length > 0) this.paths.push(key)
213
271
  if (subData && subData.sync_keys) {
214
- if (this.children[aliasName]) {
215
- this.children[aliasName].replaceData(subData)
272
+ if (child) {
273
+ child.replaceData(subData)
216
274
  } else {
217
275
  const model = new ArSyncRecord(subQuery, subData, null, this.root)
218
276
  this.mark()
@@ -222,8 +280,8 @@ class ArSyncRecord extends ArSyncContainerBase {
222
280
  model.parentKey = aliasName
223
281
  }
224
282
  } else {
225
- if(this.children[aliasName]) {
226
- this.children[aliasName].release()
283
+ if(child) {
284
+ child.release()
227
285
  delete this.children[aliasName]
228
286
  }
229
287
  if (this.data[aliasName] !== subData) {
@@ -233,33 +291,46 @@ class ArSyncRecord extends ArSyncContainerBase {
233
291
  }
234
292
  }
235
293
  }
294
+ if (this.query.attributes['*']) {
295
+ for (const key in data) {
296
+ if (!this.query.attributes[key] && this.data[key] !== data[key]) {
297
+ this.mark()
298
+ this.data[key] = data[key]
299
+ }
300
+ }
301
+ }
236
302
  this.subscribeAll()
237
303
  }
238
- onNotify(notifyData, path?) {
304
+ onNotify(notifyData: NotifyData, path?: string) {
239
305
  const { action, class_name, id } = notifyData
306
+ const query = path && this.query.attributes[path]
307
+ const aliasName = (query && query.as) || path;
240
308
  if (action === 'remove') {
241
- this.children[path].release()
242
- this.children[path] = null
309
+ const child = this.children[aliasName]
310
+ if (child) child.release()
311
+ this.children[aliasName] = null
243
312
  this.mark()
244
- this.data[path] = null
245
- this.onChange([path], null)
313
+ this.data[aliasName] = null
314
+ this.onChange([aliasName], null)
246
315
  } else if (action === 'add') {
247
- if (this.data.id === id) return
248
- const query = this.query.attributes[path]
249
- ModelBatchRequest.fetch(class_name, query, id).then(data => {
250
- if (!data) return
316
+ if (this.data[aliasName] && this.data[aliasName].id === id) return
317
+ ModelBatchRequest.fetch(class_name, ArSyncRecord.compactQuery(query), id).then(data => {
318
+ if (!data || !this.data) return
251
319
  const model = new ArSyncRecord(query, data, null, this.root)
252
- if (this.children[path]) this.children[path].release()
253
- this.children[path] = model
320
+ const child = this.children[aliasName]
321
+ if (child) child.release()
322
+ this.children[aliasName] = model
254
323
  this.mark()
255
- this.data[path] = model.data
324
+ this.data[aliasName] = model.data
256
325
  model.parentModel = this
257
- model.parentKey = path
258
- this.onChange([path], model.data)
326
+ model.parentKey = aliasName
327
+ this.onChange([aliasName], model.data)
259
328
  })
260
329
  } else {
261
- ModelBatchRequest.fetch(class_name, this.reloadQuery(), id).then(data => {
262
- this.update(data)
330
+ const { field } = notifyData
331
+ const query = field ? this.patchQuery(field) : this.reloadQuery()
332
+ if (query) ModelBatchRequest.fetch(class_name, query, id).then(data => {
333
+ if (this.data) this.update(data)
263
334
  })
264
335
  }
265
336
  }
@@ -273,6 +344,18 @@ class ArSyncRecord extends ArSyncContainerBase {
273
344
  for (const key of this.sync_keys) this.subscribe(key + path, pathCallback)
274
345
  }
275
346
  }
347
+ patchQuery(key: string) {
348
+ const val = this.query.attributes[key]
349
+ if (!val) return
350
+ let { attributes, as, params } = val
351
+ if (attributes && Object.keys(val.attributes).length === 0) attributes = null
352
+ if (!attributes && !as && !params) return key
353
+ const result: { attributes?; as?; params? } = {}
354
+ if (attributes) result.attributes = attributes
355
+ if (as) result.as = as
356
+ if (params) result.params = params
357
+ return result
358
+ }
276
359
  reloadQuery() {
277
360
  if (this.reloadQueryCache) return this.reloadQueryCache
278
361
  const reloadQuery = this.reloadQueryCache = { attributes: [] as any[] }
@@ -289,13 +372,15 @@ class ArSyncRecord extends ArSyncContainerBase {
289
372
  }
290
373
  update(data) {
291
374
  for (const key in data) {
375
+ const subQuery = this.query.attributes[key]
376
+ if (subQuery && subQuery.attributes && Object.keys(subQuery.attributes).length > 0) continue
292
377
  if (this.data[key] === data[key]) continue
293
378
  this.mark()
294
379
  this.data[key] = data[key]
295
380
  this.onChange([key], data[key])
296
381
  }
297
382
  }
298
- markAndSet(key, data) {
383
+ markAndSet(key: string, data: any) {
299
384
  this.mark()
300
385
  this.data[key] = data
301
386
  }
@@ -305,68 +390,87 @@ class ArSyncRecord extends ArSyncContainerBase {
305
390
  this.root.mark(this.data)
306
391
  if (this.parentModel) this.parentModel.markAndSet(this.parentKey, this.data)
307
392
  }
308
- onChange(path, data) {
309
- if (this.parentModel) this.parentModel.onChange([this.parentKey, ...path], data)
310
- }
311
393
  }
394
+
312
395
  class ArSyncCollection extends ArSyncContainerBase {
313
396
  root
314
- path
315
- order
397
+ path: string
398
+ order: { limit: number | null; key: string; mode: 'asc' | 'desc' } = { limit: null, mode: 'asc', key: 'id' }
316
399
  query
317
- data
318
- children
319
- sync_keys
320
- constructor(sync_keys, path, query, data, request, root){
400
+ compactQuery
401
+ data: any[]
402
+ children: ArSyncRecord[]
403
+ aliasOrderKey = 'id'
404
+ constructor(sync_keys: string[], path: string, query, data: any[], request, root){
321
405
  super()
322
406
  this.root = root
323
407
  this.path = path
408
+ this.query = query
409
+ this.compactQuery = ArSyncRecord.compactQuery(query)
324
410
  if (request) this.initForReload(request)
325
411
  if (query.params && (query.params.order || query.params.limit)) {
326
- this.order = { limit: query.params.limit, mode: query.params.order || 'asc' }
327
- } else {
328
- this.order = { limit: null, mode: 'asc' }
412
+ this.setOrdering(query.params.limit, query.params.order)
329
413
  }
330
- this.query = query
331
414
  this.data = []
332
415
  this.children = []
333
416
  this.replaceData(data, sync_keys)
334
417
  }
335
- setSyncKeys(sync_keys) {
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 }
434
+ }
435
+ setSyncKeys(sync_keys: string[]) {
336
436
  if (sync_keys) {
337
437
  this.sync_keys = sync_keys.map(key => key + this.path)
338
438
  } else {
339
- console.error('warning: no sync_keys')
340
439
  this.sync_keys = []
341
440
  }
342
441
  }
343
- replaceData(data, sync_keys) {
442
+ replaceData(data: any[] | { collection: any[]; order: any }, sync_keys: string[]) {
344
443
  this.setSyncKeys(sync_keys)
345
- const existings = {}
444
+ const existings: { [key: string]: ArSyncRecord } = {}
346
445
  for (const child of this.children) existings[child.data.id] = child
347
- let collection
348
- if (data.collection && data.order) {
446
+ let collection: any[]
447
+ if ('collection' in data && 'order' in data) {
349
448
  collection = data.collection
350
- this.order = data.order
449
+ this.setOrdering(data.order.limit, data.order.mode)
351
450
  } else {
352
451
  collection = data
353
452
  }
354
453
  const newChildren: any[] = []
355
454
  const newData: any[] = []
356
455
  for (const subData of collection) {
357
- let model = existings[subData.id]
456
+ let model: ArSyncRecord | null = null
457
+ if (typeof(subData) === 'object' && subData && 'id' in subData) model = existings[subData.id]
458
+ let data = subData
358
459
  if (model) {
359
460
  model.replaceData(subData)
360
- } else {
461
+ } else if (subData.id) {
361
462
  model = new ArSyncRecord(this.query, subData, null, this.root)
362
463
  model.parentModel = this
363
464
  model.parentKey = subData.id
364
465
  }
365
- newChildren.push(model)
366
- newData.push(model.data)
466
+ if (model) {
467
+ newChildren.push(model)
468
+ data = model.data
469
+ }
470
+ newData.push(data)
367
471
  }
368
472
  while (this.children.length) {
369
- const child = this.children.pop()
473
+ const child = this.children.pop()!
370
474
  if (!existings[child.data.id]) child.release()
371
475
  }
372
476
  if (this.data.length || newChildren.length) this.mark()
@@ -375,7 +479,7 @@ class ArSyncCollection extends ArSyncContainerBase {
375
479
  for (const el of newData) this.data.push(el)
376
480
  this.subscribeAll()
377
481
  }
378
- consumeAdd(className, id) {
482
+ consumeAdd(className: string, id: number) {
379
483
  if (this.data.findIndex(a => a.id === id) >= 0) return
380
484
  if (this.order.limit === this.data.length) {
381
485
  if (this.order.mode === 'asc') {
@@ -386,24 +490,22 @@ class ArSyncCollection extends ArSyncContainerBase {
386
490
  if (last && last.id > id) return
387
491
  }
388
492
  }
389
- ModelBatchRequest.fetch(className, this.query, id).then((data) => {
390
- if (!data) return
493
+ ModelBatchRequest.fetch(className, this.compactQuery, id).then((data: any) => {
494
+ if (!data || !this.data) return
391
495
  const model = new ArSyncRecord(this.query, data, null, this.root)
392
496
  model.parentModel = this
393
497
  model.parentKey = id
394
498
  const overflow = this.order.limit && this.order.limit === this.data.length
395
- let rmodel
499
+ let rmodel: ArSyncRecord | undefined
396
500
  this.mark()
501
+ const orderKey = this.aliasOrderKey
397
502
  if (this.order.mode === 'asc') {
398
503
  const last = this.data[this.data.length - 1]
399
504
  this.children.push(model)
400
505
  this.data.push(model.data)
401
- if (last && last.id > id) {
402
- this.children.sort((a, b) => a.data.id < b.data.id ? -1 : +1)
403
- this.data.sort((a, b) => a.id < b.id ? -1 : +1)
404
- }
506
+ if (last && last[orderKey] > data[orderKey]) this.markAndSort()
405
507
  if (overflow) {
406
- rmodel = this.children.shift()
508
+ rmodel = this.children.shift()!
407
509
  rmodel.release()
408
510
  this.data.shift()
409
511
  }
@@ -411,12 +513,9 @@ class ArSyncCollection extends ArSyncContainerBase {
411
513
  const first = this.data[0]
412
514
  this.children.unshift(model)
413
515
  this.data.unshift(model.data)
414
- if (first && first.id > id) {
415
- this.children.sort((a, b) => a.data.id > b.data.id ? -1 : +1)
416
- this.data.sort((a, b) => a.id > b.id ? -1 : +1)
417
- }
516
+ if (first && first[orderKey] > data[orderKey]) this.markAndSort()
418
517
  if (overflow) {
419
- rmodel = this.children.pop()
518
+ rmodel = this.children.pop()!
420
519
  rmodel.release()
421
520
  this.data.pop()
422
521
  }
@@ -425,7 +524,18 @@ class ArSyncCollection extends ArSyncContainerBase {
425
524
  if (rmodel) this.onChange([rmodel.id], null)
426
525
  })
427
526
  }
428
- consumeRemove(id) {
527
+ markAndSort() {
528
+ this.mark()
529
+ const orderKey = this.aliasOrderKey
530
+ if (this.order.mode === 'asc') {
531
+ this.children.sort((a, b) => a.data[orderKey] < b.data[orderKey] ? -1 : +1)
532
+ this.data.sort((a, b) => a[orderKey] < b[orderKey] ? -1 : +1)
533
+ } else {
534
+ this.children.sort((a, b) => a.data[orderKey] > b.data[orderKey] ? -1 : +1)
535
+ this.data.sort((a, b) => a[orderKey] > b[orderKey] ? -1 : +1)
536
+ }
537
+ }
538
+ consumeRemove(id: number) {
429
539
  const idx = this.data.findIndex(a => a.id === id)
430
540
  if (idx < 0) return
431
541
  this.mark()
@@ -445,7 +555,11 @@ class ArSyncCollection extends ArSyncContainerBase {
445
555
  const callback = data => this.onNotify(data)
446
556
  for (const key of this.sync_keys) this.subscribe(key, callback)
447
557
  }
448
- markAndSet(id, data) {
558
+ onChange(path: (string | number)[], data) {
559
+ super.onChange(path, data)
560
+ if (path[1] === this.aliasOrderKey) this.markAndSort()
561
+ }
562
+ markAndSet(id: number, data) {
449
563
  this.mark()
450
564
  const idx = this.data.findIndex(a => a.id === id)
451
565
  if (idx >= 0) this.data[idx] = data
@@ -459,11 +573,11 @@ class ArSyncCollection extends ArSyncContainerBase {
459
573
  }
460
574
 
461
575
  export default class ArSyncStore {
462
- immutable
463
- markedForFreezeObjects
576
+ immutable: boolean
577
+ markedForFreezeObjects: any[]
464
578
  changes
465
579
  eventListeners
466
- markForRelease
580
+ markForRelease: true | undefined
467
581
  container
468
582
  request
469
583
  complete: boolean
@@ -473,7 +587,7 @@ export default class ArSyncStore {
473
587
  retryLoadTimer: number | undefined | null
474
588
  static connectionManager
475
589
  constructor(request, { immutable } = {} as { immutable?: boolean }) {
476
- this.immutable = immutable
590
+ this.immutable = !!immutable
477
591
  this.markedForFreezeObjects = []
478
592
  this.changes = []
479
593
  this.eventListeners = { events: {}, serial: 0 }
@@ -503,6 +617,7 @@ export default class ArSyncStore {
503
617
  this.trigger('connection', state)
504
618
  }
505
619
  }).catch(e => {
620
+ if (!e || e.retry === undefined) throw e
506
621
  if (this.markForRelease) return
507
622
  if (!e.retry) {
508
623
  this.complete = true