jason-rails 0.4.1 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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)