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.
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
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c79d18d97624ed5b34f6471838d6b96eb6cfb71bba00cee9344a370dbb309498
4
- data.tar.gz: c967bed5ff59000fcc8c1de827e98f1b8caef20d71d67532d8203eff4d97724a
3
+ metadata.gz: 377f042b14b49b79d357904e68e4382639dc1c37159ad9215385fe7e0dc3ecfa
4
+ data.tar.gz: 808503bd17cf312fdfca041cf7aad54c645c164a77ab1ce83888e5e93c9397ec
5
5
  SHA512:
6
- metadata.gz: 5966a10dfdce14e70e2df1a0d722889db8ff4bece153bf4d1bd73a8148dd6f79cb4581e254a26deed31c2fe09acbdcd9333cdbf6616ec8d892bfc0758ccd44e3
7
- data.tar.gz: 87b53c9634f5afd4b4a1a708ce128d094290d3e5db9a24b70146dc4fc96091760efcdb5b8edf851f3ce83afb13f85e464e556d2c00890c4b36fa852871908d7a
6
+ metadata.gz: 9eb9a61473330c89049a89bb5ff8448d51b1c47a41b5d52f68d9a2b9826dc69454ea3fef0dceae5287b1c073600354a0be01eea96fc6f6d6a91cc64e611b4d05
7
+ data.tar.gz: 1a9cc66d3184ec1aca56d6812aa950b1496072b6d1bdab3c7bf9b2c3241e6126359e9900adef7b54cce7cf498181430a8eef211083ddbede087dd616caf08fcb
@@ -1,9 +1,9 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- ar_sync (1.0.1)
4
+ ar_sync (1.0.2)
5
5
  activerecord
6
- ar_serializer
6
+ ar_serializer (= 1.0.0)
7
7
 
8
8
  GEM
9
9
  remote: https://rubygems.org/
@@ -14,6 +14,8 @@ GEM
14
14
  activemodel (= 5.2.3)
15
15
  activesupport (= 5.2.3)
16
16
  arel (>= 9.0)
17
+ activerecord-import (1.0.2)
18
+ activerecord (>= 3.2)
17
19
  activesupport (5.2.3)
18
20
  concurrent-ruby (~> 1.0, >= 1.0.2)
19
21
  i18n (>= 0.7, < 2)
@@ -44,6 +46,7 @@ PLATFORMS
44
46
  ruby
45
47
 
46
48
  DEPENDENCIES
49
+ activerecord-import
47
50
  ar_sync!
48
51
  pry
49
52
  rake
data/README.md CHANGED
@@ -39,15 +39,14 @@ end
39
39
 
40
40
  2. Define apis
41
41
  ```ruby
42
- # app/controllers/sync_api_controller.rb
43
- class SyncApiController < ApplicationController
44
- include ArSync::ApiControllerConcern
42
+ # app/models/sync_schema.rb
43
+ class SyncSchema < ArSync::SyncSchemaBase
45
44
  # User-defined api
46
45
  serializer_field :my_simple_profile_api do |current_user|
47
46
  current_user
48
47
  end
49
- serializer_field :my_simple_user_api do |current_user, id:|
50
- User.where(condition).find id
48
+ serializer_field :my_simple_friends_api do |current_user, age:|
49
+ current_user.friends.where(age: age)
51
50
  end
52
51
  # Reload api (field name = classname, params = `ids:`)
53
52
  serializer_field :User do |current_user, ids:|
@@ -103,7 +102,8 @@ rails g ar_sync:types path_to_generated_code_dir/
103
102
  ```ts
104
103
  import ArSyncModel from 'path_to_generated_code_dir/ArSyncModel'
105
104
  import ActionCableAdapter from 'ar_sync/core/ActionCableAdapter'
106
- ArSyncModel.setConnectionAdapter(new ActionCableAdapter)
105
+ import * as ActionCable from 'actioncable'
106
+ ArSyncModel.setConnectionAdapter(new ActionCableAdapter(ActionCable))
107
107
  // ArSyncModel.setConnectionAdapter(new MyCustomConnectionAdapter) // If you are using other transports
108
108
  ```
109
109
 
@@ -21,8 +21,8 @@ Gem::Specification.new do |spec|
21
21
  spec.require_paths = ['lib']
22
22
 
23
23
  spec.add_dependency 'activerecord'
24
- spec.add_dependency 'ar_serializer'
25
- %w[rake pry sqlite3].each do |gem_name|
24
+ spec.add_dependency 'ar_serializer', '1.0.0'
25
+ %w[rake pry sqlite3 activerecord-import].each do |gem_name|
26
26
  spec.add_development_dependency gem_name
27
27
  end
28
28
  end
@@ -3,8 +3,7 @@
3
3
  require 'bundler/setup'
4
4
  require 'ar_sync'
5
5
  require 'pry'
6
- require_relative '../test/model_tree'
7
- require_relative '../test/model_graph'
6
+ require_relative '../test/model'
8
7
  ArSync.on_notification do |events|
9
8
  puts "\e[1m#{events.inspect}\e[m"
10
9
  end
@@ -1,10 +1,34 @@
1
- import * as ActionCable from 'actioncable';
2
1
  import ConnectionAdapter from './ConnectionAdapter';
2
+ declare module ActionCable {
3
+ function createConsumer(): Cable;
4
+ interface Cable {
5
+ subscriptions: Subscriptions;
6
+ }
7
+ interface CreateMixin {
8
+ connected: () => void;
9
+ disconnected: () => void;
10
+ received: (obj: any) => void;
11
+ }
12
+ interface ChannelNameWithParams {
13
+ channel: string;
14
+ [key: string]: any;
15
+ }
16
+ interface Subscriptions {
17
+ create(channel: ChannelNameWithParams, obj: CreateMixin): Channel;
18
+ }
19
+ interface Channel {
20
+ unsubscribe(): void;
21
+ perform(action: string, data: {}): void;
22
+ send(data: any): boolean;
23
+ }
24
+ }
3
25
  export default class ActionCableAdapter implements ConnectionAdapter {
4
26
  connected: boolean;
5
27
  _cable: ActionCable.Cable;
6
- constructor();
28
+ actionCableClass: typeof ActionCable;
29
+ constructor(actionCableClass: typeof ActionCable);
7
30
  subscribe(key: string, received: (data: any) => void): ActionCable.Channel;
8
31
  ondisconnect(): void;
9
32
  onreconnect(): void;
10
33
  }
34
+ export {};
@@ -1,9 +1,9 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- const ActionCable = require("actioncable");
4
3
  class ActionCableAdapter {
5
- constructor() {
4
+ constructor(actionCableClass) {
6
5
  this.connected = true;
6
+ this.actionCableClass = actionCableClass;
7
7
  this.subscribe(Math.random().toString(), () => { });
8
8
  }
9
9
  subscribe(key, received) {
@@ -20,7 +20,7 @@ class ActionCableAdapter {
20
20
  this.onreconnect();
21
21
  };
22
22
  if (!this._cable)
23
- this._cable = ActionCable.createConsumer();
23
+ this._cable = this.actionCableClass.createConsumer();
24
24
  return this._cable.subscriptions.create({ channel: 'SyncChannel', key }, { received, disconnected, connected });
25
25
  }
26
26
  ondisconnect() { }
@@ -1,5 +1,8 @@
1
- declare const _default: {
2
- fetch: (request: object) => Promise<{}>;
3
- syncFetch: (request: object) => Promise<{}>;
1
+ declare function apiBatchFetch(endpoint: string, requests: object[]): Promise<any>;
2
+ declare const ArSyncApi: {
3
+ domain: string | null;
4
+ _batchFetch: typeof apiBatchFetch;
5
+ fetch: (request: object) => Promise<unknown>;
6
+ syncFetch: (request: object) => Promise<unknown>;
4
7
  };
5
- export default _default;
8
+ export default ArSyncApi;
@@ -7,6 +7,8 @@ async function apiBatchFetch(endpoint, requests) {
7
7
  };
8
8
  const body = JSON.stringify({ requests });
9
9
  const option = { credentials: 'include', method: 'POST', headers, body };
10
+ if (ArSyncApi.domain)
11
+ endpoint = ArSyncApi.domain + endpoint;
10
12
  const res = await fetch(endpoint, option);
11
13
  if (res.status === 200)
12
14
  return res.json();
@@ -41,7 +43,7 @@ class ApiFetcher {
41
43
  }
42
44
  }
43
45
  this.batches = [];
44
- apiBatchFetch(this.endpoint, requests).then((results) => {
46
+ ArSyncApi._batchFetch(this.endpoint, requests).then((results) => {
45
47
  for (const i in callbacksList) {
46
48
  const result = results[i];
47
49
  const callbacks = callbacksList[i];
@@ -51,11 +53,11 @@ class ApiFetcher {
51
53
  }
52
54
  else {
53
55
  const error = result.error || { type: 'Unknown Error' };
54
- callback.reject(error);
56
+ callback.reject(Object.assign(Object.assign({}, error), { retry: false }));
55
57
  }
56
58
  }
57
59
  }
58
- }).catch((e) => {
60
+ }).catch(e => {
59
61
  const error = { type: e.name, message: e.message, retry: true };
60
62
  for (const callbacks of callbacksList) {
61
63
  for (const callback of callbacks)
@@ -68,7 +70,10 @@ class ApiFetcher {
68
70
  }
69
71
  const staticFetcher = new ApiFetcher('/static_api');
70
72
  const syncFetcher = new ApiFetcher('/sync_api');
71
- exports.default = {
73
+ const ArSyncApi = {
74
+ domain: null,
75
+ _batchFetch: apiBatchFetch,
72
76
  fetch: (request) => staticFetcher.fetch(request),
73
77
  syncFetch: (request) => syncFetcher.fetch(request),
74
78
  };
79
+ exports.default = ArSyncApi;
@@ -1,9 +1,10 @@
1
+ import ConnectionAdapter from './ConnectionAdapter';
1
2
  interface Request {
2
3
  api: string;
3
4
  query: any;
4
5
  params?: any;
5
6
  }
6
- declare type Path = (string | number)[];
7
+ declare type Path = Readonly<(string | number)[]>;
7
8
  interface Change {
8
9
  path: Path;
9
10
  value: any;
@@ -13,14 +14,13 @@ declare type LoadCallback = () => void;
13
14
  declare type ConnectionCallback = (status: boolean) => void;
14
15
  declare type SubscriptionType = 'load' | 'change' | 'connection';
15
16
  declare type SubscriptionCallback = ChangeCallback | LoadCallback | ConnectionCallback;
16
- interface Adapter {
17
- subscribe: (key: string, received: (data: any) => void) => {
18
- unsubscribe: () => void;
19
- };
20
- ondisconnect: () => void;
21
- onreconnect: () => void;
22
- }
23
- export default abstract class ArSyncModelBase<T> {
17
+ declare type PathFirst<P extends Readonly<any[]>> = ((...args: P) => void) extends (first: infer First, ...other: any) => void ? First : never;
18
+ declare type PathRest<U> = U extends Readonly<any[]> ? ((...args: U) => any) extends (head: any, ...args: infer T) => any ? U extends Readonly<[any, any, ...any[]]> ? T : never : never : never;
19
+ declare type DigResult<Data, P extends Readonly<any[]>> = Data extends null | undefined ? Data : PathFirst<P> extends never ? Data : PathFirst<P> extends keyof Data ? (Data extends Readonly<any[]> ? undefined : never) | {
20
+ 0: Data[PathFirst<P>];
21
+ 1: DigResult<Data[PathFirst<P>], PathRest<P>>;
22
+ }[PathRest<P> extends never ? 0 : 1] : undefined;
23
+ export default class ArSyncModel<T> {
24
24
  private _ref;
25
25
  private _listenerSerial;
26
26
  private _listeners;
@@ -37,10 +37,6 @@ export default abstract class ArSyncModelBase<T> {
37
37
  };
38
38
  };
39
39
  static cacheTimeout: number;
40
- abstract refManagerClass(): any;
41
- abstract connectionManager(): {
42
- networkStatus: boolean;
43
- };
44
40
  constructor(request: Request, option?: {
45
41
  immutable: boolean;
46
42
  });
@@ -48,6 +44,8 @@ export default abstract class ArSyncModelBase<T> {
48
44
  subscribeOnce(event: SubscriptionType, callback: SubscriptionCallback): {
49
45
  unsubscribe: () => void;
50
46
  };
47
+ dig<P extends Path>(path: P): DigResult<T, P> | null;
48
+ static digData<Data, P extends Path>(data: Data, path: P): DigResult<Data, P>;
51
49
  subscribe(event: SubscriptionType, callback: SubscriptionCallback): {
52
50
  unsubscribe: () => void;
53
51
  };
@@ -60,12 +58,9 @@ export default abstract class ArSyncModelBase<T> {
60
58
  timer: number | null;
61
59
  model: any;
62
60
  };
63
- static createRefModel(_request: Request, _option?: {
64
- immutable: boolean;
65
- }): void;
66
61
  static _detach(ref: any): void;
67
62
  private static _attach;
68
- static setConnectionAdapter(_adapter: Adapter): void;
69
- static waitForLoad(...models: ArSyncModelBase<{}>[]): Promise<{}>;
63
+ static setConnectionAdapter(adapter: ConnectionAdapter): void;
64
+ static waitForLoad(...models: ArSyncModel<{}>[]): Promise<unknown>;
70
65
  }
71
66
  export {};
@@ -1,12 +1,14 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- class ArSyncModelBase {
3
+ const ArSyncStore_1 = require("./ArSyncStore");
4
+ const ConnectionManager_1 = require("./ConnectionManager");
5
+ class ArSyncModel {
4
6
  constructor(request, option) {
5
- this._ref = this.refManagerClass().retrieveRef(request, option);
7
+ this._ref = ArSyncModel.retrieveRef(request, option);
6
8
  this._listenerSerial = 0;
7
9
  this._listeners = {};
8
10
  this.complete = false;
9
- this.connected = this.connectionManager().networkStatus;
11
+ this.connected = ArSyncStore_1.default.connectionManager.networkStatus;
10
12
  const setData = () => {
11
13
  this.data = this._ref.model.data;
12
14
  this.complete = this._ref.model.complete;
@@ -29,6 +31,26 @@ class ArSyncModelBase {
29
31
  });
30
32
  return subscription;
31
33
  }
34
+ dig(path) {
35
+ return ArSyncModel.digData(this.data, path);
36
+ }
37
+ static digData(data, path) {
38
+ function dig(data, path) {
39
+ if (path.length === 0)
40
+ return data;
41
+ if (data == null)
42
+ return data;
43
+ const key = path[0];
44
+ const other = path.slice(1);
45
+ if (Array.isArray(data)) {
46
+ return this.digData(data.find(el => el.id === key), other);
47
+ }
48
+ else {
49
+ return this.digData(data[key], other);
50
+ }
51
+ }
52
+ return dig(data, path);
53
+ }
32
54
  subscribe(event, callback) {
33
55
  const id = this._listenerSerial++;
34
56
  const subscription = this._ref.model.subscribe(event, callback);
@@ -56,22 +78,19 @@ class ArSyncModelBase {
56
78
  for (const id in this._listeners)
57
79
  this._listeners[id].unsubscribe();
58
80
  this._listeners = {};
59
- this.refManagerClass()._detach(this._ref);
81
+ ArSyncModel._detach(this._ref);
60
82
  this._ref = null;
61
83
  }
62
84
  static retrieveRef(request, option) {
63
85
  const key = JSON.stringify([request, option]);
64
86
  let ref = this._cache[key];
65
87
  if (!ref) {
66
- const model = this.createRefModel(request, option);
88
+ const model = new ArSyncStore_1.default(request, option);
67
89
  ref = this._cache[key] = { key, count: 0, timer: null, model };
68
90
  }
69
91
  this._attach(ref);
70
92
  return ref;
71
93
  }
72
- static createRefModel(_request, _option) {
73
- throw 'abstract method';
74
- }
75
94
  static _detach(ref) {
76
95
  ref.count--;
77
96
  const timeout = this.cacheTimeout;
@@ -93,7 +112,9 @@ class ArSyncModelBase {
93
112
  if (ref.timer)
94
113
  clearTimeout(ref.timer);
95
114
  }
96
- static setConnectionAdapter(_adapter) { }
115
+ static setConnectionAdapter(adapter) {
116
+ ArSyncStore_1.default.connectionManager = new ConnectionManager_1.default(adapter);
117
+ }
97
118
  static waitForLoad(...models) {
98
119
  return new Promise((resolve) => {
99
120
  let count = 0;
@@ -107,4 +128,6 @@ class ArSyncModelBase {
107
128
  });
108
129
  }
109
130
  }
110
- exports.default = ArSyncModelBase;
131
+ exports.default = ArSyncModel;
132
+ ArSyncModel._cache = {};
133
+ ArSyncModel.cacheTimeout = 10 * 1000;
@@ -1,9 +1,9 @@
1
1
  export default class ArSyncStore {
2
- immutable: any;
3
- markedForFreezeObjects: any;
2
+ immutable: boolean;
3
+ markedForFreezeObjects: any[];
4
4
  changes: any;
5
5
  eventListeners: any;
6
- markForRelease: any;
6
+ markForRelease: true | undefined;
7
7
  container: any;
8
8
  request: any;
9
9
  complete: boolean;
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- const ArSyncApi_1 = require("../core/ArSyncApi");
3
+ const ArSyncApi_1 = require("./ArSyncApi");
4
4
  const ModelBatchRequest = {
5
5
  timer: null,
6
6
  apiRequests: {},
@@ -87,7 +87,59 @@ class ArSyncContainerBase {
87
87
  l.unsubscribe();
88
88
  this.listeners = [];
89
89
  }
90
- static parseQuery(query, attrsonly = false) {
90
+ static compactQuery(query) {
91
+ function compactAttributes(attributes) {
92
+ const attrs = {};
93
+ const keys = [];
94
+ for (const key in attributes) {
95
+ const c = compactQuery(attributes[key]);
96
+ if (c === true) {
97
+ keys.push(key);
98
+ }
99
+ else {
100
+ attrs[key] = c;
101
+ }
102
+ }
103
+ if (Object.keys(attrs).length === 0) {
104
+ if (keys.length === 0)
105
+ return [true, false];
106
+ if (keys.length === 1)
107
+ return [keys[0], false];
108
+ return [keys];
109
+ }
110
+ const needsEscape = attrs['attributes'] || attrs['params'] || attrs['as'];
111
+ if (keys.length === 0)
112
+ return [attrs, needsEscape];
113
+ return [[...keys, attrs], needsEscape];
114
+ }
115
+ function compactQuery(query) {
116
+ if (!('attributes' in query))
117
+ return true;
118
+ const { as, params } = query;
119
+ const [attributes, needsEscape] = compactAttributes(query.attributes);
120
+ if (as == null && params == null) {
121
+ if (needsEscape)
122
+ return { attributes };
123
+ return attributes;
124
+ }
125
+ const result = {};
126
+ if (as)
127
+ result.as = as;
128
+ if (params)
129
+ result.params = params;
130
+ if (attributes !== true)
131
+ result.attributes = attributes;
132
+ return result;
133
+ }
134
+ try {
135
+ const result = compactQuery(query);
136
+ return result === true ? {} : result;
137
+ }
138
+ catch (e) {
139
+ throw JSON.stringify(query) + e.stack;
140
+ }
141
+ }
142
+ static parseQuery(query, attrsonly) {
91
143
  const attributes = {};
92
144
  let column = null;
93
145
  let params = null;
@@ -129,15 +181,19 @@ class ArSyncContainerBase {
129
181
  }
130
182
  static _load({ api, id, params, query }, root) {
131
183
  const parsedQuery = ArSyncRecord.parseQuery(query);
184
+ const compactQuery = ArSyncRecord.compactQuery(parsedQuery);
132
185
  if (id) {
133
- return ModelBatchRequest.fetch(api, query, id).then(data => new ArSyncRecord(parsedQuery, data[0], null, root));
186
+ return ModelBatchRequest.fetch(api, compactQuery, id).then(data => new ArSyncRecord(parsedQuery, data, null, root));
134
187
  }
135
188
  else {
136
- const request = { api, query, params };
189
+ const request = { api, query: compactQuery, params };
137
190
  return ArSyncApi_1.default.syncFetch(request).then((response) => {
138
191
  if (response.collection && response.order) {
139
192
  return new ArSyncCollection(response.sync_keys, 'collection', parsedQuery, response, request, root);
140
193
  }
194
+ else if (response instanceof Array) {
195
+ return new ArSyncCollection([], '', parsedQuery, response, request, root);
196
+ }
141
197
  else {
142
198
  return new ArSyncRecord(parsedQuery, response, request, root);
143
199
  }
@@ -176,7 +232,6 @@ class ArSyncRecord extends ArSyncContainerBase {
176
232
  this.sync_keys = sync_keys;
177
233
  if (!this.sync_keys) {
178
234
  this.sync_keys = [];
179
- console.error('warning: no sync_keys');
180
235
  }
181
236
  }
182
237
  replaceData(data) {
@@ -191,11 +246,12 @@ class ArSyncRecord extends ArSyncContainerBase {
191
246
  const subQuery = this.query.attributes[key];
192
247
  const aliasName = subQuery.as || key;
193
248
  const subData = data[aliasName];
249
+ const child = this.children[aliasName];
194
250
  if (key === 'sync_keys')
195
251
  continue;
196
- if (subQuery.attributes && (subData instanceof Array || (subData && subData.collection && subData.order))) {
197
- if (this.children[aliasName]) {
198
- this.children[aliasName].replaceData(subData, this.sync_keys);
252
+ if (subData instanceof Array || (subData && subData.collection && subData.order)) {
253
+ if (child) {
254
+ child.replaceData(subData, this.sync_keys);
199
255
  }
200
256
  else {
201
257
  const collection = new ArSyncCollection(this.sync_keys, key, subQuery, subData, null, this.root);
@@ -207,11 +263,11 @@ class ArSyncRecord extends ArSyncContainerBase {
207
263
  }
208
264
  }
209
265
  else {
210
- if (subQuery.attributes)
266
+ if (subQuery.attributes && Object.keys(subQuery.attributes).length > 0)
211
267
  this.paths.push(key);
212
268
  if (subData && subData.sync_keys) {
213
- if (this.children[aliasName]) {
214
- this.children[aliasName].replaceData(subData);
269
+ if (child) {
270
+ child.replaceData(subData);
215
271
  }
216
272
  else {
217
273
  const model = new ArSyncRecord(subQuery, subData, null, this.root);
@@ -223,8 +279,8 @@ class ArSyncRecord extends ArSyncContainerBase {
223
279
  }
224
280
  }
225
281
  else {
226
- if (this.children[aliasName]) {
227
- this.children[aliasName].release();
282
+ if (child) {
283
+ child.release();
228
284
  delete this.children[aliasName];
229
285
  }
230
286
  if (this.data[aliasName] !== subData) {
@@ -234,39 +290,55 @@ class ArSyncRecord extends ArSyncContainerBase {
234
290
  }
235
291
  }
236
292
  }
293
+ if (this.query.attributes['*']) {
294
+ for (const key in data) {
295
+ if (!this.query.attributes[key] && this.data[key] !== data[key]) {
296
+ this.mark();
297
+ this.data[key] = data[key];
298
+ }
299
+ }
300
+ }
237
301
  this.subscribeAll();
238
302
  }
239
303
  onNotify(notifyData, path) {
240
304
  const { action, class_name, id } = notifyData;
305
+ const query = path && this.query.attributes[path];
306
+ const aliasName = (query && query.as) || path;
241
307
  if (action === 'remove') {
242
- this.children[path].release();
243
- this.children[path] = null;
308
+ const child = this.children[aliasName];
309
+ if (child)
310
+ child.release();
311
+ this.children[aliasName] = null;
244
312
  this.mark();
245
- this.data[path] = null;
246
- this.onChange([path], null);
313
+ this.data[aliasName] = null;
314
+ this.onChange([aliasName], null);
247
315
  }
248
316
  else if (action === 'add') {
249
- if (this.data.id === id)
317
+ if (this.data[aliasName] && this.data[aliasName].id === id)
250
318
  return;
251
- const query = this.query.attributes[path];
252
- ModelBatchRequest.fetch(class_name, query, id).then(data => {
253
- if (!data)
319
+ ModelBatchRequest.fetch(class_name, ArSyncRecord.compactQuery(query), id).then(data => {
320
+ if (!data || !this.data)
254
321
  return;
255
322
  const model = new ArSyncRecord(query, data, null, this.root);
256
- if (this.children[path])
257
- this.children[path].release();
258
- this.children[path] = model;
323
+ const child = this.children[aliasName];
324
+ if (child)
325
+ child.release();
326
+ this.children[aliasName] = model;
259
327
  this.mark();
260
- this.data[path] = model.data;
328
+ this.data[aliasName] = model.data;
261
329
  model.parentModel = this;
262
- model.parentKey = path;
263
- this.onChange([path], model.data);
330
+ model.parentKey = aliasName;
331
+ this.onChange([aliasName], model.data);
264
332
  });
265
333
  }
266
334
  else {
267
- ModelBatchRequest.fetch(class_name, this.reloadQuery(), id).then(data => {
268
- this.update(data);
269
- });
335
+ const { field } = notifyData;
336
+ const query = field ? this.patchQuery(field) : this.reloadQuery();
337
+ if (query)
338
+ ModelBatchRequest.fetch(class_name, query, id).then(data => {
339
+ if (this.data)
340
+ this.update(data);
341
+ });
270
342
  }
271
343
  }
272
344
  subscribeAll() {
@@ -280,6 +352,24 @@ class ArSyncRecord extends ArSyncContainerBase {
280
352
  this.subscribe(key + path, pathCallback);
281
353
  }
282
354
  }
355
+ patchQuery(key) {
356
+ const val = this.query.attributes[key];
357
+ if (!val)
358
+ return;
359
+ let { attributes, as, params } = val;
360
+ if (attributes && Object.keys(val.attributes).length === 0)
361
+ attributes = null;
362
+ if (!attributes && !as && !params)
363
+ return key;
364
+ const result = {};
365
+ if (attributes)
366
+ result.attributes = attributes;
367
+ if (as)
368
+ result.as = as;
369
+ if (params)
370
+ result.params = params;
371
+ return result;
372
+ }
283
373
  reloadQuery() {
284
374
  if (this.reloadQueryCache)
285
375
  return this.reloadQueryCache;
@@ -299,6 +389,9 @@ class ArSyncRecord extends ArSyncContainerBase {
299
389
  }
300
390
  update(data) {
301
391
  for (const key in data) {
392
+ const subQuery = this.query.attributes[key];
393
+ if (subQuery && subQuery.attributes && Object.keys(subQuery.attributes).length > 0)
394
+ continue;
302
395
  if (this.data[key] === data[key])
303
396
  continue;
304
397
  this.mark();
@@ -318,35 +411,51 @@ class ArSyncRecord extends ArSyncContainerBase {
318
411
  if (this.parentModel)
319
412
  this.parentModel.markAndSet(this.parentKey, this.data);
320
413
  }
321
- onChange(path, data) {
322
- if (this.parentModel)
323
- this.parentModel.onChange([this.parentKey, ...path], data);
324
- }
325
414
  }
326
415
  class ArSyncCollection extends ArSyncContainerBase {
327
416
  constructor(sync_keys, path, query, data, request, root) {
328
417
  super();
418
+ this.order = { limit: null, mode: 'asc', key: 'id' };
419
+ this.aliasOrderKey = 'id';
329
420
  this.root = root;
330
421
  this.path = path;
422
+ this.query = query;
423
+ this.compactQuery = ArSyncRecord.compactQuery(query);
331
424
  if (request)
332
425
  this.initForReload(request);
333
426
  if (query.params && (query.params.order || query.params.limit)) {
334
- this.order = { limit: query.params.limit, mode: query.params.order || 'asc' };
335
- }
336
- else {
337
- this.order = { limit: null, mode: 'asc' };
427
+ this.setOrdering(query.params.limit, query.params.order);
338
428
  }
339
- this.query = query;
340
429
  this.data = [];
341
430
  this.children = [];
342
431
  this.replaceData(data, sync_keys);
343
432
  }
433
+ setOrdering(limit, order) {
434
+ let mode = 'asc';
435
+ let key = 'id';
436
+ if (order === 'asc' || order === 'desc') {
437
+ mode = order;
438
+ }
439
+ else if (typeof order === 'object' && order) {
440
+ const keys = Object.keys(order);
441
+ if (keys.length > 1)
442
+ throw 'multiple order keys are not supported';
443
+ if (keys.length === 1)
444
+ key = keys[0];
445
+ mode = order[key] === 'asc' ? 'asc' : 'desc';
446
+ }
447
+ const limitNumber = (typeof limit === 'number') ? limit : null;
448
+ if (limitNumber !== null && key !== 'id')
449
+ throw 'limit with custom order key is not supported';
450
+ const subQuery = this.query.attributes[key];
451
+ this.aliasOrderKey = (subQuery && subQuery.as) || key;
452
+ this.order = { limit: limitNumber, mode, key };
453
+ }
344
454
  setSyncKeys(sync_keys) {
345
455
  if (sync_keys) {
346
456
  this.sync_keys = sync_keys.map(key => key + this.path);
347
457
  }
348
458
  else {
349
- console.error('warning: no sync_keys');
350
459
  this.sync_keys = [];
351
460
  }
352
461
  }
@@ -356,9 +465,9 @@ class ArSyncCollection extends ArSyncContainerBase {
356
465
  for (const child of this.children)
357
466
  existings[child.data.id] = child;
358
467
  let collection;
359
- if (data.collection && data.order) {
468
+ if ('collection' in data && 'order' in data) {
360
469
  collection = data.collection;
361
- this.order = data.order;
470
+ this.setOrdering(data.order.limit, data.order.mode);
362
471
  }
363
472
  else {
364
473
  collection = data;
@@ -366,17 +475,23 @@ class ArSyncCollection extends ArSyncContainerBase {
366
475
  const newChildren = [];
367
476
  const newData = [];
368
477
  for (const subData of collection) {
369
- let model = existings[subData.id];
478
+ let model = null;
479
+ if (typeof (subData) === 'object' && subData && 'id' in subData)
480
+ model = existings[subData.id];
481
+ let data = subData;
370
482
  if (model) {
371
483
  model.replaceData(subData);
372
484
  }
373
- else {
485
+ else if (subData.id) {
374
486
  model = new ArSyncRecord(this.query, subData, null, this.root);
375
487
  model.parentModel = this;
376
488
  model.parentKey = subData.id;
377
489
  }
378
- newChildren.push(model);
379
- newData.push(model.data);
490
+ if (model) {
491
+ newChildren.push(model);
492
+ data = model.data;
493
+ }
494
+ newData.push(data);
380
495
  }
381
496
  while (this.children.length) {
382
497
  const child = this.children.pop();
@@ -408,8 +523,8 @@ class ArSyncCollection extends ArSyncContainerBase {
408
523
  return;
409
524
  }
410
525
  }
411
- ModelBatchRequest.fetch(className, this.query, id).then((data) => {
412
- if (!data)
526
+ ModelBatchRequest.fetch(className, this.compactQuery, id).then((data) => {
527
+ if (!data || !this.data)
413
528
  return;
414
529
  const model = new ArSyncRecord(this.query, data, null, this.root);
415
530
  model.parentModel = this;
@@ -417,14 +532,13 @@ class ArSyncCollection extends ArSyncContainerBase {
417
532
  const overflow = this.order.limit && this.order.limit === this.data.length;
418
533
  let rmodel;
419
534
  this.mark();
535
+ const orderKey = this.aliasOrderKey;
420
536
  if (this.order.mode === 'asc') {
421
537
  const last = this.data[this.data.length - 1];
422
538
  this.children.push(model);
423
539
  this.data.push(model.data);
424
- if (last && last.id > id) {
425
- this.children.sort((a, b) => a.data.id < b.data.id ? -1 : +1);
426
- this.data.sort((a, b) => a.id < b.id ? -1 : +1);
427
- }
540
+ if (last && last[orderKey] > data[orderKey])
541
+ this.markAndSort();
428
542
  if (overflow) {
429
543
  rmodel = this.children.shift();
430
544
  rmodel.release();
@@ -435,10 +549,8 @@ class ArSyncCollection extends ArSyncContainerBase {
435
549
  const first = this.data[0];
436
550
  this.children.unshift(model);
437
551
  this.data.unshift(model.data);
438
- if (first && first.id > id) {
439
- this.children.sort((a, b) => a.data.id > b.data.id ? -1 : +1);
440
- this.data.sort((a, b) => a.id > b.id ? -1 : +1);
441
- }
552
+ if (first && first[orderKey] > data[orderKey])
553
+ this.markAndSort();
442
554
  if (overflow) {
443
555
  rmodel = this.children.pop();
444
556
  rmodel.release();
@@ -450,6 +562,18 @@ class ArSyncCollection extends ArSyncContainerBase {
450
562
  this.onChange([rmodel.id], null);
451
563
  });
452
564
  }
565
+ markAndSort() {
566
+ this.mark();
567
+ const orderKey = this.aliasOrderKey;
568
+ if (this.order.mode === 'asc') {
569
+ this.children.sort((a, b) => a.data[orderKey] < b.data[orderKey] ? -1 : +1);
570
+ this.data.sort((a, b) => a[orderKey] < b[orderKey] ? -1 : +1);
571
+ }
572
+ else {
573
+ this.children.sort((a, b) => a.data[orderKey] > b.data[orderKey] ? -1 : +1);
574
+ this.data.sort((a, b) => a[orderKey] > b[orderKey] ? -1 : +1);
575
+ }
576
+ }
453
577
  consumeRemove(id) {
454
578
  const idx = this.data.findIndex(a => a.id === id);
455
579
  if (idx < 0)
@@ -473,6 +597,11 @@ class ArSyncCollection extends ArSyncContainerBase {
473
597
  for (const key of this.sync_keys)
474
598
  this.subscribe(key, callback);
475
599
  }
600
+ onChange(path, data) {
601
+ super.onChange(path, data);
602
+ if (path[1] === this.aliasOrderKey)
603
+ this.markAndSort();
604
+ }
476
605
  markAndSet(id, data) {
477
606
  this.mark();
478
607
  const idx = this.data.findIndex(a => a.id === id);
@@ -490,7 +619,7 @@ class ArSyncCollection extends ArSyncContainerBase {
490
619
  }
491
620
  class ArSyncStore {
492
621
  constructor(request, { immutable } = {}) {
493
- this.immutable = immutable;
622
+ this.immutable = !!immutable;
494
623
  this.markedForFreezeObjects = [];
495
624
  this.changes = [];
496
625
  this.eventListeners = { events: {}, serial: 0 };
@@ -521,6 +650,8 @@ class ArSyncStore {
521
650
  this.trigger('connection', state);
522
651
  };
523
652
  }).catch(e => {
653
+ if (!e || e.retry === undefined)
654
+ throw e;
524
655
  if (this.markForRelease)
525
656
  return;
526
657
  if (!e.retry) {