jason-rails 0.4.1 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (49) hide show
  1. checksums.yaml +4 -4
  2. data/.ruby-version +1 -0
  3. data/Gemfile.lock +152 -2
  4. data/app/controllers/jason/api_controller.rb +1 -1
  5. data/client/lib/JasonContext.d.ts +6 -1
  6. data/client/lib/JasonProvider.d.ts +2 -2
  7. data/client/lib/JasonProvider.js +5 -124
  8. data/client/lib/createJasonReducers.js +41 -3
  9. data/client/lib/createOptDis.js +0 -2
  10. data/client/lib/createPayloadHandler.d.ts +6 -1
  11. data/client/lib/createPayloadHandler.js +42 -54
  12. data/client/lib/createServerActionQueue.d.ts +10 -0
  13. data/client/lib/createServerActionQueue.js +48 -0
  14. data/client/lib/createServerActionQueue.test.d.ts +1 -0
  15. data/client/lib/createServerActionQueue.test.js +37 -0
  16. data/client/lib/index.d.ts +3 -2
  17. data/client/lib/pruneIdsMiddleware.d.ts +2 -0
  18. data/client/lib/pruneIdsMiddleware.js +26 -0
  19. data/client/lib/restClient.d.ts +2 -0
  20. data/client/lib/restClient.js +17 -0
  21. data/client/lib/useJason.d.ts +5 -0
  22. data/client/lib/useJason.js +99 -0
  23. data/client/lib/useJason.test.d.ts +1 -0
  24. data/client/lib/useJason.test.js +79 -0
  25. data/client/lib/useSub.js +1 -0
  26. data/client/package.json +4 -3
  27. data/client/src/JasonProvider.tsx +5 -123
  28. data/client/src/createJasonReducers.ts +49 -3
  29. data/client/src/createOptDis.ts +0 -2
  30. data/client/src/createPayloadHandler.ts +47 -63
  31. data/client/src/createServerActionQueue.test.ts +42 -0
  32. data/client/src/createServerActionQueue.ts +47 -0
  33. data/client/src/pruneIdsMiddleware.ts +24 -0
  34. data/client/src/restClient.ts +13 -0
  35. data/client/src/useJason.test.ts +81 -0
  36. data/client/src/useJason.ts +115 -0
  37. data/client/src/useSub.ts +1 -0
  38. data/client/yarn.lock +59 -3
  39. data/jason-rails.gemspec +4 -0
  40. data/lib/jason.rb +12 -0
  41. data/lib/jason/api_model.rb +2 -12
  42. data/lib/jason/channel.rb +43 -21
  43. data/lib/jason/lua_generator.rb +49 -0
  44. data/lib/jason/publisher.rb +76 -35
  45. data/lib/jason/publisher_old.rb +112 -0
  46. data/lib/jason/subscription.rb +322 -99
  47. data/lib/jason/subscription_old.rb +171 -0
  48. data/lib/jason/version.rb +1 -1
  49. metadata +67 -3
@@ -0,0 +1,115 @@
1
+ import createActions from './createActions'
2
+ import createJasonReducers from './createJasonReducers'
3
+ import createPayloadHandler from './createPayloadHandler'
4
+ import createOptDis from './createOptDis'
5
+ import createServerActionQueue from './createServerActionQueue'
6
+ import restClient from './restClient'
7
+ import pruneIdsMiddleware from './pruneIdsMiddleware'
8
+
9
+ import { createConsumer } from "@rails/actioncable"
10
+ import { createEntityAdapter, createSlice, createReducer, configureStore } from '@reduxjs/toolkit'
11
+
12
+ import makeEager from './makeEager'
13
+ import { camelizeKeys } from 'humps'
14
+ import md5 from 'blueimp-md5'
15
+ import _ from 'lodash'
16
+ import React, { useState, useEffect } from 'react'
17
+
18
+ export default function useJason({ reducers, middleware = [], extraActions }: { reducers?: any, middleware?: any[], extraActions?: any }) {
19
+ const [store, setStore] = useState(null as any)
20
+ const [value, setValue] = useState(null as any)
21
+ const [connected, setConnected] = useState(false)
22
+
23
+ useEffect(() => {
24
+ restClient.get('/jason/api/schema')
25
+ .then(({ data: snakey_schema }) => {
26
+ const schema = camelizeKeys(snakey_schema)
27
+ console.debug({ schema })
28
+
29
+ const serverActionQueue = createServerActionQueue()
30
+
31
+ const consumer = createConsumer()
32
+ const allReducers = {
33
+ ...reducers,
34
+ ...createJasonReducers(schema)
35
+ }
36
+
37
+ console.debug({ allReducers })
38
+
39
+ const store = configureStore({ reducer: allReducers, middleware: [...middleware, pruneIdsMiddleware(schema)] })
40
+ const dispatch = store.dispatch
41
+
42
+ const optDis = createOptDis(schema, dispatch, restClient, serverActionQueue)
43
+ const actions = createActions(schema, store, restClient, optDis, extraActions)
44
+ const eager = makeEager(schema)
45
+
46
+ let payloadHandlers = {}
47
+ let configs = {}
48
+
49
+ function handlePayload(payload) {
50
+ const { md5Hash } = payload
51
+
52
+ const handler = payloadHandlers[md5Hash]
53
+ if (handler) {
54
+ handler(payload)
55
+ } else {
56
+ console.warn("Payload arrived with no handler", payload, payloadHandlers)
57
+ }
58
+ }
59
+
60
+ const subscription = (consumer.subscriptions.create({
61
+ channel: 'Jason::Channel'
62
+ }, {
63
+ connected: () => {
64
+ setConnected(true)
65
+ dispatch({ type: 'jason/upsert', payload: { connected: true } })
66
+ console.debug('Connected to ActionCable')
67
+
68
+ // When AC loses connection - all state is lost, so we need to re-initialize all subscriptions
69
+ _.values(configs).forEach(config => createSubscription(config))
70
+ },
71
+ received: payload => {
72
+ handlePayload(payload)
73
+ console.debug("ActionCable Payload received: ", payload)
74
+ },
75
+ disconnected: () => {
76
+ setConnected(false)
77
+ dispatch({ type: 'jason/upsert', payload: { connected: false } })
78
+ console.warn('Disconnected from ActionCable')
79
+ }
80
+ }));
81
+
82
+ function createSubscription(config) {
83
+ // We need the hash to be consistent in Ruby / Javascript
84
+ const hashableConfig = _({ conditions: {}, includes: {}, ...config }).toPairs().sortBy(0).fromPairs().value()
85
+ const md5Hash = md5(JSON.stringify(hashableConfig))
86
+ payloadHandlers[md5Hash] = createPayloadHandler({ dispatch, serverActionQueue, subscription, config })
87
+ configs[md5Hash] = hashableConfig
88
+
89
+ setTimeout(() => subscription.send({ createSubscription: hashableConfig }), 500)
90
+
91
+ return {
92
+ remove: () => removeSubscription(hashableConfig),
93
+ md5Hash
94
+ }
95
+ }
96
+
97
+ function removeSubscription(config) {
98
+ subscription.send({ removeSubscription: config })
99
+ const md5Hash = md5(JSON.stringify(config))
100
+ delete payloadHandlers[md5Hash]
101
+ delete configs[md5Hash]
102
+ }
103
+
104
+ setValue({
105
+ actions: actions,
106
+ subscribe: config => createSubscription(config),
107
+ eager,
108
+ handlePayload
109
+ })
110
+ setStore(store)
111
+ })
112
+ }, [])
113
+
114
+ return [store, value, connected]
115
+ }
@@ -5,6 +5,7 @@ export default function useSub(config) {
5
5
  const subscribe = useContext(JasonContext).subscribe
6
6
 
7
7
  useEffect(() => {
8
+ // @ts-ignore
8
9
  return subscribe(config)
9
10
  }, [])
10
11
  }
@@ -813,7 +813,7 @@
813
813
  "@babel/helper-validator-option" "^7.12.1"
814
814
  "@babel/plugin-transform-typescript" "^7.12.1"
815
815
 
816
- "@babel/runtime@^7.12.1", "@babel/runtime@^7.8.4":
816
+ "@babel/runtime@^7.12.1", "@babel/runtime@^7.12.5", "@babel/runtime@^7.8.4":
817
817
  version "7.12.5"
818
818
  resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.12.5.tgz#410e7e487441e1b360c29be715d870d9b985882e"
819
819
  integrity sha512-plcc+hbExy3McchJCEQG3knOsuh3HH+Prx1P6cLIkET/0dLuQDEnrT+s27Axgc9bqfsmNUNHfscgMUdBpC9xfg==
@@ -1082,6 +1082,18 @@
1082
1082
  dependencies:
1083
1083
  "@sinonjs/commons" "^1.7.0"
1084
1084
 
1085
+ "@testing-library/react-hooks@^5.0.3":
1086
+ version "5.0.3"
1087
+ resolved "https://registry.yarnpkg.com/@testing-library/react-hooks/-/react-hooks-5.0.3.tgz#dd0d2048817b013b266d35ca45e3ea48a19fd87e"
1088
+ integrity sha512-UrnnRc5II7LMH14xsYNm/WRch/67cBafmrSQcyFh0v+UUmSf1uzfB7zn5jQXSettGwOSxJwdQUN7PgkT0w22Lg==
1089
+ dependencies:
1090
+ "@babel/runtime" "^7.12.5"
1091
+ "@types/react" ">=16.9.0"
1092
+ "@types/react-dom" ">=16.9.0"
1093
+ "@types/react-test-renderer" ">=16.9.0"
1094
+ filter-console "^0.1.1"
1095
+ react-error-boundary "^3.1.0"
1096
+
1085
1097
  "@types/babel__core@^7.0.0", "@types/babel__core@^7.1.7":
1086
1098
  version "7.1.12"
1087
1099
  resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.1.12.tgz#4d8e9e51eb265552a7e4f1ff2219ab6133bdfb2d"
@@ -1164,6 +1176,33 @@
1164
1176
  resolved "https://registry.yarnpkg.com/@types/prettier/-/prettier-2.1.5.tgz#b6ab3bba29e16b821d84e09ecfaded462b816b00"
1165
1177
  integrity sha512-UEyp8LwZ4Dg30kVU2Q3amHHyTn1jEdhCIE59ANed76GaT1Vp76DD3ZWSAxgCrw6wJ0TqeoBpqmfUHiUDPs//HQ==
1166
1178
 
1179
+ "@types/prop-types@*":
1180
+ version "15.7.3"
1181
+ resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.3.tgz#2ab0d5da2e5815f94b0b9d4b95d1e5f243ab2ca7"
1182
+ integrity sha512-KfRL3PuHmqQLOG+2tGpRO26Ctg+Cq1E01D2DMriKEATHgWLfeNDmq9e29Q9WIky0dQ3NPkd1mzYH8Lm936Z9qw==
1183
+
1184
+ "@types/react-dom@>=16.9.0":
1185
+ version "17.0.0"
1186
+ resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-17.0.0.tgz#b3b691eb956c4b3401777ee67b900cb28415d95a"
1187
+ integrity sha512-lUqY7OlkF/RbNtD5nIq7ot8NquXrdFrjSOR6+w9a9RFQevGi1oZO1dcJbXMeONAPKtZ2UrZOEJ5UOCVsxbLk/g==
1188
+ dependencies:
1189
+ "@types/react" "*"
1190
+
1191
+ "@types/react-test-renderer@>=16.9.0":
1192
+ version "17.0.0"
1193
+ resolved "https://registry.yarnpkg.com/@types/react-test-renderer/-/react-test-renderer-17.0.0.tgz#9be47b375eeb906fced37049e67284a438d56620"
1194
+ integrity sha512-nvw+F81OmyzpyIE1S0xWpLonLUZCMewslPuA8BtjSKc5XEbn8zEQBXS7KuOLHTNnSOEM2Pum50gHOoZ62tqTRg==
1195
+ dependencies:
1196
+ "@types/react" "*"
1197
+
1198
+ "@types/react@*", "@types/react@>=16.9.0":
1199
+ version "17.0.0"
1200
+ resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.0.tgz#5af3eb7fad2807092f0046a1302b7823e27919b8"
1201
+ integrity sha512-aj/L7RIMsRlWML3YB6KZiXB3fV2t41+5RBGYF8z+tAKU43Px8C3cYUZsDvf1/+Bm4FK21QWBrDutu8ZJ/70qOw==
1202
+ dependencies:
1203
+ "@types/prop-types" "*"
1204
+ csstype "^3.0.2"
1205
+
1167
1206
  "@types/stack-utils@^2.0.0":
1168
1207
  version "2.0.0"
1169
1208
  resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.0.tgz#7036640b4e21cc2f259ae826ce843d277dad8cff"
@@ -1742,6 +1781,11 @@ cssstyle@^2.2.0:
1742
1781
  dependencies:
1743
1782
  cssom "~0.3.6"
1744
1783
 
1784
+ csstype@^3.0.2:
1785
+ version "3.0.6"
1786
+ resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.0.6.tgz#865d0b5833d7d8d40f4e5b8a6d76aea3de4725ef"
1787
+ integrity sha512-+ZAmfyWMT7TiIlzdqJgjMb7S4f1beorDbWbsocyK4RaiqA5RTX3K14bnBWmmA9QEM0gRdsjyyrEmcyga8Zsxmw==
1788
+
1745
1789
  dashdash@^1.12.0:
1746
1790
  version "1.14.1"
1747
1791
  resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0"
@@ -2081,6 +2125,11 @@ fill-range@^7.0.1:
2081
2125
  dependencies:
2082
2126
  to-regex-range "^5.0.1"
2083
2127
 
2128
+ filter-console@^0.1.1:
2129
+ version "0.1.1"
2130
+ resolved "https://registry.yarnpkg.com/filter-console/-/filter-console-0.1.1.tgz#6242be28982bba7415bcc6db74a79f4a294fa67c"
2131
+ integrity sha512-zrXoV1Uaz52DqPs+qEwNJWJFAWZpYJ47UNmpN9q4j+/EYsz85uV0DC9k8tRND5kYmoVzL0W+Y75q4Rg8sRJCdg==
2132
+
2084
2133
  find-up@^4.0.0, find-up@^4.1.0:
2085
2134
  version "4.1.0"
2086
2135
  resolved "https://registry.yarnpkg.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19"
@@ -3601,7 +3650,7 @@ qs@~6.5.2:
3601
3650
  resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36"
3602
3651
  integrity sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==
3603
3652
 
3604
- react-dom@^16.8.3:
3653
+ react-dom@^16.9.x:
3605
3654
  version "16.14.0"
3606
3655
  resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.14.0.tgz#7ad838ec29a777fb3c75c3a190f661cf92ab8b89"
3607
3656
  integrity sha512-1gCeQXDLoIqMgqD3IO2Ah9bnf0w9kzhwN5q4FGnHZ67hBm9yePzB5JJAIQCc8x3pFnNlwFq4RidZggNAAkzWWw==
@@ -3611,6 +3660,13 @@ react-dom@^16.8.3:
3611
3660
  prop-types "^15.6.2"
3612
3661
  scheduler "^0.19.1"
3613
3662
 
3663
+ react-error-boundary@^3.1.0:
3664
+ version "3.1.0"
3665
+ resolved "https://registry.yarnpkg.com/react-error-boundary/-/react-error-boundary-3.1.0.tgz#9487443df2f9ba1db90d8ab52351814907ea4af3"
3666
+ integrity sha512-lmPrdi5SLRJR+AeJkqdkGlW/CRkAUvZnETahK58J4xb5wpbfDngasEGu+w0T1iXEhVrYBJZeW+c4V1hILCnMWQ==
3667
+ dependencies:
3668
+ "@babel/runtime" "^7.12.5"
3669
+
3614
3670
  react-is@^16.13.1, react-is@^16.7.0, react-is@^16.8.1:
3615
3671
  version "16.13.1"
3616
3672
  resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
@@ -3632,7 +3688,7 @@ react-redux@^7.2.2:
3632
3688
  prop-types "^15.7.2"
3633
3689
  react-is "^16.13.1"
3634
3690
 
3635
- react@^16.8.3:
3691
+ react@^16.9.x:
3636
3692
  version "16.14.0"
3637
3693
  resolved "https://registry.yarnpkg.com/react/-/react-16.14.0.tgz#94d776ddd0aaa37da3eda8fc5b6b18a4c9a3114d"
3638
3694
  integrity sha512-0X2CImDkJGApiAlcf0ODKIneSwBPhqJawOa5wCtKbu7ZECrmS26NvtSILynQ66cgkT/RJ4LidJOc3bUESwmU8g==
@@ -27,4 +27,8 @@ Gem::Specification.new do |spec|
27
27
  spec.add_dependency "connection_pool", ">= 2.2.3"
28
28
  spec.add_dependency "redis", ">= 4"
29
29
  spec.add_dependency "jsondiff"
30
+
31
+ spec.add_development_dependency "rspec-rails"
32
+ spec.add_development_dependency "sqlite3"
33
+ spec.add_development_dependency "pry"
30
34
  end
@@ -8,9 +8,21 @@ require 'jason/channel'
8
8
  require 'jason/publisher'
9
9
  require 'jason/subscription'
10
10
  require 'jason/engine'
11
+ require 'jason/lua_generator'
11
12
 
12
13
  module Jason
13
14
  class Error < StandardError; end
14
15
 
15
16
  $redis_jason = ::ConnectionPool::Wrapper.new(size: 5, timeout: 3) { ::Redis.new(url: ENV['REDIS_URL']) }
17
+
18
+
19
+ self.mattr_accessor :schema
20
+ self.schema = {}
21
+ # add default values of more config vars here
22
+
23
+ # this function maps the vars from your app into your engine
24
+ def self.setup(&block)
25
+ yield self
26
+ end
27
+
16
28
  end
@@ -8,7 +8,7 @@ class Jason::ApiModel
8
8
 
9
9
  def initialize(name)
10
10
  @name = name
11
- @model = OpenStruct.new(JASON_API_MODEL[name.to_sym])
11
+ @model = OpenStruct.new(Jason.schema[name.to_sym])
12
12
  end
13
13
 
14
14
  def allowed_params
@@ -19,10 +19,6 @@ class Jason::ApiModel
19
19
  model.allowed_object_params || []
20
20
  end
21
21
 
22
- def include_models
23
- model.include_models || []
24
- end
25
-
26
22
  def include_methods
27
23
  model.include_methods || []
28
24
  end
@@ -52,12 +48,6 @@ class Jason::ApiModel
52
48
  end
53
49
 
54
50
  def as_json_config
55
- include_configs = include_models.map do |assoc|
56
- reflection = name.classify.constantize.reflect_on_association(assoc.to_sym)
57
- api_model = Jason::ApiModel.new(reflection.klass.name.underscore)
58
- { assoc => { only: api_model.subscribed_fields, methods: api_model.include_methods } }
59
- end
60
-
61
- { only: subscribed_fields, include: include_configs, methods: include_methods }
51
+ { only: subscribed_fields, methods: include_methods }
62
52
  end
63
53
  end
@@ -1,32 +1,27 @@
1
1
  class Jason::Channel < ActionCable::Channel::Base
2
2
  attr_accessor :subscriptions
3
3
 
4
+ def subscribe
5
+ stream_from 'jason'
6
+ end
7
+
4
8
  def receive(message)
5
- subscriptions ||= []
9
+ handle_message(message)
10
+ end
11
+
12
+ private
13
+
14
+ def handle_message(message)
15
+ pp message['createSubscription']
16
+ @subscriptions ||= []
6
17
 
7
18
  begin # ActionCable swallows errors in this message - ensure they're output to logs.
8
19
  if (config = message['createSubscription'])
9
- subscription = Jason::Subscription.new(config: config)
10
- subscriptions.push(subscription)
11
- subscription.add_consumer(identifier)
12
- config.keys.each do |model|
13
- transmit(subscription.get(model.to_s.underscore))
14
- end
15
- stream_from subscription.channel
20
+ create_subscription(config['model'], config['conditions'], config['includes'])
16
21
  elsif (config = message['removeSubscription'])
17
- subscription = Jason::Subscription.new(config: config)
18
- subscriptions.reject! { |s| s.id == subscription.id }
19
- subscription.remove_consumer(identifier)
20
-
21
- # Rails for some reason removed stop_stream_from, so we need to stop all and then restart the other streams
22
- # stop_all_streams
23
- # subscriptions.each do |s|
24
- # stream_from s.channel
25
- # end
26
- elsif (data = message['getPayload'])
27
- config = data['config']
28
- model = data['model']
29
- Jason::Subscription.new(config: config).get(model.to_s.underscore)
22
+ remove_subscription(config)
23
+ elsif (config = message['getPayload'])
24
+ get_payload(config)
30
25
  end
31
26
  rescue => e
32
27
  puts e.message
@@ -34,4 +29,31 @@ class Jason::Channel < ActionCable::Channel::Base
34
29
  raise e
35
30
  end
36
31
  end
32
+
33
+ def create_subscription(model, conditions, includes)
34
+ subscription = Jason::Subscription.upsert_by_config(model, conditions: conditions || {}, includes: includes || nil)
35
+ stream_from subscription.channel
36
+
37
+ subscriptions.push(subscription)
38
+ subscription.add_consumer(identifier)
39
+ subscription.get.each do |payload|
40
+ pp payload
41
+ transmit(payload) if payload.present?
42
+ end
43
+ end
44
+
45
+ def remove_subscription(config)
46
+ subscription = Jason::Subscription.upsert_by_config(config['model'], conditions: config['conditions'], includes: config['includes'])
47
+ subscriptions.reject! { |s| s.id == subscription.id }
48
+ subscription.remove_consumer(identifier)
49
+
50
+ # TODO Stop streams
51
+ end
52
+
53
+ def get_payload(config)
54
+ subscription = Jason::Subscription.upsert_by_config(config['model'], conditions: config['conditions'], includes: config['includes'])
55
+ subscription.get.each do |payload|
56
+ transmit(payload) if payload.present?
57
+ end
58
+ end
37
59
  end
@@ -0,0 +1,49 @@
1
+ class Jason::LuaGenerator
2
+ ## TODO load these scripts and evalsha
3
+ def cache_json(model_name, id, payload)
4
+ cmd = <<~LUA
5
+ local gidx = redis.call('INCR', 'jason:gidx')
6
+ redis.call( 'set', 'jason:cache:' .. ARGV[1] .. ':' .. ARGV[2] .. ':gidx', gidx )
7
+ redis.call( 'hset', 'jason:cache:' .. ARGV[1], ARGV[2], ARGV[3] )
8
+ return gidx
9
+ LUA
10
+
11
+ result = $redis_jason.eval cmd, [], [model_name, id, payload.to_json]
12
+ end
13
+
14
+ def get_payload(model_name, sub_id)
15
+ # If value has changed, return old value and new idx. Otherwise do nothing.
16
+ cmd = <<~LUA
17
+ local t = {}
18
+ local models = {}
19
+ local ids = redis.call('smembers', 'jason:subscriptions:' .. ARGV[2] .. ':ids:' .. ARGV[1])
20
+
21
+ for k,id in pairs(ids) do
22
+ models[#models+1] = redis.call( 'hget', 'jason:cache:' .. ARGV[1], id)
23
+ end
24
+
25
+ t[#t+1] = models
26
+ t[#t+1] = redis.call( 'get', 'jason:subscription:' .. ARGV[2] .. ':' .. ARGV[1] .. ':idx' )
27
+
28
+ return t
29
+ LUA
30
+
31
+ $redis_jason.eval cmd, [], [model_name, sub_id]
32
+ end
33
+
34
+ def get_subscription(model_name, id, sub_id, gidx)
35
+ # If value has changed, return old value and new idx. Otherwise do nothing.
36
+ cmd = <<~LUA
37
+ local last_gidx = redis.call('get', 'jason:cache:' .. ARGV[1] .. ':' .. ARGV[2] .. ':gidx') or 0
38
+
39
+ if (ARGV[4] >= last_gidx) then
40
+ local sub_idx = redis.call( 'incr', 'jason:subscription:' .. ARGV[3] .. ':' .. ARGV[1] .. ':idx' )
41
+ return sub_idx
42
+ else
43
+ return false
44
+ end
45
+ LUA
46
+
47
+ result = $redis_jason.eval cmd, [], [model_name, id, sub_id, gidx]
48
+ end
49
+ end
@@ -1,26 +1,83 @@
1
1
  module Jason::Publisher
2
2
  extend ActiveSupport::Concern
3
3
 
4
+ # Warning: Could be expensive. Mainly useful for rebuilding cache after changing Jason config or on deploy
5
+ def self.cache_all
6
+ Rails.application.eager_load!
7
+ ActiveRecord::Base.descendants.each do |klass|
8
+ klass.cache_all if klass.respond_to?(:cache_all)
9
+ end
10
+ end
11
+
4
12
  def cache_json
5
13
  as_json_config = api_model.as_json_config
6
14
  scope = api_model.scope
7
15
 
16
+ # Exists
8
17
  if self.persisted? && (scope.blank? || self.class.unscoped.send(scope).exists?(self.id))
9
18
  payload = self.reload.as_json(as_json_config)
10
- $redis_jason.hset("jason:#{self.class.name.underscore}:cache", self.id, payload.to_json)
19
+ gidx = Jason::LuaGenerator.new.cache_json(self.class.name.underscore, self.id, payload)
20
+ return [payload, gidx]
21
+ # Has been destroyed
11
22
  else
12
- $redis_jason.hdel("jason:#{self.class.name.underscore}:cache", self.id)
23
+ $redis_jason.hdel("jason:cache:#{self.class.name.underscore}", self.id)
24
+ return []
13
25
  end
14
26
  end
15
27
 
16
28
  def publish_json
17
- cache_json
29
+ payload, gidx = cache_json
30
+ subs = jason_subscriptions # Get this first, because could be changed
31
+
32
+ # Situations where IDs may need to change and this can't be immediately determined
33
+ # - An instance is created where it belongs_to an instance under a subscription
34
+ # - An instance belongs_to association changes - e.g. comment.post_id changes to/from one with a subscription
35
+ # - TODO: The value of an instance changes so that it enters/leaves a subscription
36
+
37
+ # TODO: Optimize this, by caching associations rather than checking each time instance is saved
38
+ jason_assocs = self.class.reflect_on_all_associations(:belongs_to).select { |assoc| assoc.klass.has_jason? }
39
+ jason_assocs.each do |assoc|
40
+ if self.previous_changes[assoc.foreign_key].present?
41
+
42
+ Jason::Subscription.update_ids(
43
+ self.class.name.underscore,
44
+ id,
45
+ assoc.klass.name.underscore,
46
+ self.previous_changes[assoc.foreign_key][0],
47
+ self.previous_changes[assoc.foreign_key][1]
48
+ )
49
+ elsif (persisted? && @was_a_new_record && send(assoc.foreign_key).present?)
50
+ Jason::Subscription.update_ids(
51
+ self.class.name.underscore,
52
+ id,
53
+ assoc.klass.name.underscore,
54
+ nil,
55
+ send(assoc.foreign_key)
56
+ )
57
+ end
58
+ end
59
+
60
+ if !persisted? # Deleted
61
+ Jason::Subscription.remove_ids(
62
+ self.class.name.underscore,
63
+ [id]
64
+ )
65
+ end
66
+
67
+ # - An instance is created where it belongs_to an _all_ subscription
68
+ if self.previous_changes['id'].present?
69
+ Jason::Subscription.add_id(self.class.name.underscore, id)
70
+ end
71
+
18
72
  return if skip_publish_json
19
- self.class.jason_subscriptions.each do |id, config_json|
20
- config = JSON.parse(config_json)
21
73
 
22
- if (config['conditions'] || {}).all? { |field, value| self.send(field) == value }
23
- Jason::Subscription.new(id: id).update(self.class.name.underscore)
74
+ if self.persisted?
75
+ jason_subscriptions.each do |sub_id|
76
+ Jason::Subscription.new(id: sub_id).update(self.class.name.underscore, id, payload, gidx)
77
+ end
78
+ else
79
+ subs.each do |sub_id|
80
+ Jason::Subscription.new(id: sub_id).destroy(self.class.name.underscore, id)
24
81
  end
25
82
  end
26
83
  end
@@ -30,47 +87,31 @@ module Jason::Publisher
30
87
  publish_json if (self.previous_changes.keys.map(&:to_sym) & subscribed_fields).present? || !self.persisted?
31
88
  end
32
89
 
33
- class_methods do
34
- def subscriptions
35
- $redis_jason.hgetall("jason:#{self.name.underscore}:subscriptions")
36
- end
90
+ def jason_subscriptions
91
+ Jason::Subscription.for_instance(self.class.name.underscore, id)
92
+ end
37
93
 
38
- def jason_subscriptions
39
- $redis_jason.hgetall("jason:#{self.name.underscore}:subscriptions")
94
+ class_methods do
95
+ def cache_all
96
+ all.each(&:cache_json)
40
97
  end
41
98
 
42
- def publish_all(instances)
43
- instances.each(&:cache_json)
44
-
45
- subscriptions.each do |id, config_json|
46
- Jason::Subscription.new(id: id).update(self.name.underscore)
47
- end
99
+ def has_jason?
100
+ true
48
101
  end
49
102
 
50
103
  def flush_cache
51
- $redis_jason.del("jason:#{self.name.underscore}:cache")
104
+ $redis_jason.del("jason:cache:#{self.name.underscore}")
52
105
  end
53
106
 
54
107
  def setup_json
108
+ self.before_save -> {
109
+ @was_a_new_record = new_record?
110
+ }
55
111
  self.after_initialize -> {
56
112
  @api_model = Jason::ApiModel.new(self.class.name.underscore)
57
113
  }
58
114
  self.after_commit :publish_json_if_changed
59
-
60
- include_models = Jason::ApiModel.new(self.name.underscore).include_models
61
-
62
- include_models.map do |assoc|
63
- puts assoc
64
- reflection = self.reflect_on_association(assoc.to_sym)
65
- reflection.klass.after_commit -> {
66
- subscribed_fields = Jason::ApiModel.new(self.class.name.underscore).subscribed_fields
67
- puts subscribed_fields.inspect
68
-
69
- if (self.previous_changes.keys.map(&:to_sym) & subscribed_fields).present?
70
- self.send(reflection.inverse_of.name)&.publish_json
71
- end
72
- }
73
- end
74
115
  end
75
116
 
76
117
  def find_or_create_by_id(params)