ar_sync 1.0.1 → 1.0.2
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.
- checksums.yaml +4 -4
- data/Gemfile.lock +5 -2
- data/README.md +6 -6
- data/ar_sync.gemspec +2 -2
- data/bin/console +1 -2
- data/core/ActioncableAdapter.d.ts +26 -2
- data/core/ActioncableAdapter.js +3 -3
- data/core/ArSyncApi.d.ts +7 -4
- data/core/ArSyncApi.js +9 -4
- data/core/{ArSyncModelBase.d.ts → ArSyncModel.d.ts} +13 -18
- data/core/{ArSyncModelBase.js → ArSyncModel.js} +33 -10
- data/{graph → core}/ArSyncStore.d.ts +3 -3
- data/{graph → core}/ArSyncStore.js +188 -57
- data/core/DataType.d.ts +17 -13
- data/core/hooks.d.ts +28 -0
- data/core/hooks.js +105 -0
- data/index.d.ts +2 -0
- data/index.js +6 -0
- data/lib/ar_sync.rb +1 -18
- data/lib/ar_sync/class_methods.rb +31 -89
- data/lib/ar_sync/collection.rb +4 -29
- data/lib/ar_sync/core.rb +35 -67
- data/lib/ar_sync/instance_methods.rb +40 -86
- data/lib/ar_sync/rails.rb +18 -27
- data/lib/ar_sync/type_script.rb +39 -18
- data/lib/ar_sync/version.rb +1 -1
- data/lib/generators/ar_sync/install/install_generator.rb +33 -32
- data/lib/generators/ar_sync/types/types_generator.rb +6 -3
- data/package-lock.json +21 -10
- data/package.json +1 -1
- data/src/core/ActioncableAdapter.ts +28 -3
- data/src/core/ArSyncApi.ts +8 -4
- data/src/core/{ArSyncModelBase.ts → ArSyncModel.ts} +51 -20
- data/src/{graph → core}/ArSyncStore.ts +199 -84
- data/src/core/DataType.ts +33 -20
- data/src/core/hooks.ts +108 -0
- data/src/index.ts +2 -0
- data/vendor/assets/javascripts/{ar_sync_tree.js.erb → ar_sync.js.erb} +6 -7
- metadata +33 -38
- data/core/hooksBase.d.ts +0 -29
- data/core/hooksBase.js +0 -80
- data/graph/ArSyncModel.d.ts +0 -10
- data/graph/ArSyncModel.js +0 -22
- data/graph/hooks.d.ts +0 -3
- data/graph/hooks.js +0 -10
- data/graph/index.d.ts +0 -2
- data/graph/index.js +0 -4
- data/src/core/hooksBase.ts +0 -86
- data/src/graph/ArSyncModel.ts +0 -21
- data/src/graph/hooks.ts +0 -7
- data/src/graph/index.ts +0 -2
- data/src/tree/ArSyncModel.ts +0 -145
- data/src/tree/ArSyncStore.ts +0 -323
- data/src/tree/hooks.ts +0 -7
- data/src/tree/index.ts +0 -2
- data/tree/ArSyncModel.d.ts +0 -39
- data/tree/ArSyncModel.js +0 -143
- data/tree/ArSyncStore.d.ts +0 -21
- data/tree/ArSyncStore.js +0 -365
- data/tree/hooks.d.ts +0 -3
- data/tree/hooks.js +0 -10
- data/tree/index.d.ts +0 -2
- data/tree/index.js +0 -4
- data/vendor/assets/javascripts/ar_sync_graph.js.erb +0 -17
data/lib/ar_sync/version.rb
CHANGED
@@ -1,46 +1,47 @@
|
|
1
1
|
module ArSync
|
2
2
|
class InstallGenerator < ::Rails::Generators::Base
|
3
|
-
|
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
|
-
|
6
|
-
|
7
|
-
|
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
|
-
|
10
|
-
# current_user
|
11
|
-
# end
|
14
|
+
# Reload API for all types should be defined here.
|
12
15
|
|
13
|
-
|
14
|
-
|
15
|
-
|
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
|
-
|
21
|
-
|
22
|
-
|
20
|
+
# serializer_field :Post do |current_user, ids:|
|
21
|
+
# Post.where(current_user_can_access).where id: ids
|
22
|
+
# end
|
23
23
|
|
24
|
-
|
25
|
-
|
26
|
-
|
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
|
-
|
29
|
-
|
30
|
-
|
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
|
-
|
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
|
-
|
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,
|
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
|
-
|
8
|
-
|
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
|
data/package-lock.json
CHANGED
@@ -338,10 +338,21 @@
|
|
338
338
|
}
|
339
339
|
},
|
340
340
|
"eslint-utils": {
|
341
|
-
"version": "1.3
|
342
|
-
"resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-1.3.
|
343
|
-
"integrity": "sha512-
|
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.
|
644
|
-
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.
|
645
|
-
"integrity": "sha512-
|
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.
|
1071
|
-
"resolved": "https://registry.npmjs.org/typescript/-/typescript-3.
|
1072
|
-
"integrity": "sha512-
|
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": {
|
data/package.json
CHANGED
@@ -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
|
-
|
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 =
|
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 }
|
data/src/core/ArSyncApi.ts
CHANGED
@@ -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
|
-
|
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(
|
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
|
-
|
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
|
-
|
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 =
|
43
|
+
this._ref = ArSyncModel.retrieveRef(request, option)
|
29
44
|
this._listenerSerial = 0
|
30
45
|
this._listeners = {}
|
31
46
|
this.complete = false
|
32
|
-
this.connected =
|
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
|
-
|
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 =
|
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(
|
115
|
-
|
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 '
|
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
|
100
|
-
|
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,
|
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
|
-
|
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 (
|
201
|
-
if (
|
202
|
-
|
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 (
|
215
|
-
|
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(
|
226
|
-
|
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[
|
242
|
-
|
309
|
+
const child = this.children[aliasName]
|
310
|
+
if (child) child.release()
|
311
|
+
this.children[aliasName] = null
|
243
312
|
this.mark()
|
244
|
-
this.data[
|
245
|
-
this.onChange([
|
313
|
+
this.data[aliasName] = null
|
314
|
+
this.onChange([aliasName], null)
|
246
315
|
} else if (action === 'add') {
|
247
|
-
if (this.data.id === id) return
|
248
|
-
|
249
|
-
|
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
|
-
|
253
|
-
|
320
|
+
const child = this.children[aliasName]
|
321
|
+
if (child) child.release()
|
322
|
+
this.children[aliasName] = model
|
254
323
|
this.mark()
|
255
|
-
this.data[
|
324
|
+
this.data[aliasName] = model.data
|
256
325
|
model.parentModel = this
|
257
|
-
model.parentKey =
|
258
|
-
this.onChange([
|
326
|
+
model.parentKey = aliasName
|
327
|
+
this.onChange([aliasName], model.data)
|
259
328
|
})
|
260
329
|
} else {
|
261
|
-
|
262
|
-
|
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
|
-
|
318
|
-
|
319
|
-
|
320
|
-
|
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.
|
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
|
-
|
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 (
|
446
|
+
let collection: any[]
|
447
|
+
if ('collection' in data && 'order' in data) {
|
349
448
|
collection = data.collection
|
350
|
-
this.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 =
|
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
|
-
|
366
|
-
|
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.
|
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
|
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
|
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
|
-
|
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
|
-
|
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
|