statebus 6.1.9 → 6.1.13

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.
package/client.js CHANGED
@@ -57,6 +57,62 @@
57
57
  return {clientid: me.client}
58
58
  }
59
59
 
60
+ function braid_http_mount (prefix, url) {
61
+ var preprefix = prefix.slice(0,-1)
62
+ var has_prefix = new RegExp('^' + preprefix)
63
+ var is_absolute = /^https?:\/\//
64
+ var client_fetched_keys = new bus.Set()
65
+ if (url[url.length-1]=='/') url = url.substr(0,url.length-1)
66
+ function add_prefix (key) {
67
+ return is_absolute.test(key) ? key : preprefix + key }
68
+ function rem_prefix (key) {
69
+ return has_prefix.test(key) ? key.substr(preprefix.length) : key }
70
+ function add_prefixes (obj) {
71
+ return bus.translate_keys(bus.clone(obj), add_prefix) }
72
+ function rem_prefixes (obj) {
73
+ return bus.translate_keys(bus.clone(obj), rem_prefix) }
74
+
75
+ bus(prefix).to_save = function (obj, t) {
76
+ console.log('saving', prefix, obj)
77
+ bus.save.fire(obj)
78
+
79
+ // var x = {save: obj}
80
+ // if (t.version) x.version = t.version
81
+ // if (t.parents) x.parents = t.parents
82
+ // if (t.patch) x.patch = t.patch
83
+ // if (t.patch) x.save = rem_prefix(x.save.key)
84
+
85
+ braid_fetch(url + obj.key,
86
+ {
87
+ method: 'put',
88
+ body: JSON.stringify(obj.val)
89
+ })
90
+ }
91
+
92
+ bus(prefix).to_fetch = function (key, t) {
93
+ try {
94
+ braid_fetch(url + rem_prefix(key),
95
+ {
96
+ method: 'get',
97
+ subscribe: true
98
+ }
99
+ ).andThen( x => {
100
+ t.return({
101
+ key,
102
+ val: add_prefixes(JSON.parse(x.body))
103
+ })
104
+ })
105
+ } catch (e) {
106
+ console.error('Braid-HTTP network error:', e)
107
+ }
108
+ client_fetched_keys.add(key)
109
+ }
110
+ bus(prefix).to_forget = function (key) {
111
+ // send({forget: key}),
112
+ client_fetched_keys.delete(key)
113
+ }
114
+ }
115
+
60
116
 
61
117
  // ****************
62
118
  // Manipulate Localstorage
@@ -382,16 +438,22 @@
382
438
  react_render = React.version >= '0.14.' ? ReactDOM.render : React.render
383
439
  make_client_statebus_maker()
384
440
  window.bus = window.statebus()
385
- window.bus.label = 'bus'
386
- window.sb = bus.sb
441
+ bus.label = 'bus'
442
+ if (clientjs_option('braid_mode_test'))
443
+ bus.state = bus.braid_proxy()
444
+ else
445
+ window.sb = bus.sb
387
446
  statebus.widget = React_View
388
447
  statebus.create_react_class = React_View
389
448
  statebus.createReactClass = React_View
390
449
 
391
450
  improve_react()
392
451
  window.ignore_flashbacks = false
393
- if (statebus_server !== 'none')
452
+ if (statebus_server !== 'none') {
394
453
  bus.net_mount ('/*', statebus_server)
454
+ if (clientjs_option('braid_mode_test'))
455
+ braid_http_mount ('http/*', statebus_server)
456
+ }
395
457
 
396
458
  if (window.statebus_backdoor) {
397
459
  window.master = statebus()
@@ -399,16 +461,19 @@
399
461
  }
400
462
  bus.net_automount()
401
463
 
402
- // bus('*').to_save = function (obj) { bus.save.fire(obj) }
403
- bus('/new/*').to_save = function (o) {
404
- if (o.key.split('/').length > 3) return
464
+ // This /new/* code is deprecated
465
+ if (!clientjs_option('braid_mode_test')) {
466
+ bus('/new/*').to_save = function (o) {
467
+ if (o.key.split('/').length > 3) return
405
468
 
406
- var old_key = o.key
407
- o.key = old_key + '/' + Math.random().toString(36).substring(2,12)
408
- statebus.cache[o.key] = o
409
- delete statebus.cache[old_key]
410
- bus.save(o)
469
+ var old_key = o.key
470
+ o.key = old_key + '/' + Math.random().toString(36).substring(2,12)
471
+ statebus.cache[o.key] = o
472
+ delete statebus.cache[old_key]
473
+ bus.save(o)
474
+ }
411
475
  }
476
+
412
477
  load_coffee()
413
478
 
414
479
  statebus.compile_coffee = compile_coffee
package/extras/tests.js CHANGED
@@ -294,6 +294,43 @@ test(function url_translation (done) {
294
294
  done()
295
295
  })
296
296
 
297
+ test(function translate_fields (done) {
298
+ // Translate Statebus -> Proxy format
299
+ var tests1 = [
300
+ [{key: 'foo', link: 'boo'}, {key: 'foo', link: 'boo'}],
301
+ [{_key: 'foo', link: 'boo'}, {key: 'foo', link: 'boo'}],
302
+ [{__key: 'foo', link: 'boo'}, {_key: 'foo', link: 'boo'}],
303
+ [[{_key: 'foo', link: 'boo'}], [{key: 'foo', link: 'boo'}]],
304
+ [{a: [{_key: 'foo'}]}, {a: [{key: 'foo'}]}]
305
+ ]
306
+
307
+ // Translate Proxy -> Statebus format
308
+ var tests2 = [
309
+ [{key: 'foo', link: 'boo'}, {_key: 'foo', link: 'boo'}],
310
+ [{_key: 'foo', link: 'boo'}, {__key: 'foo', link: 'boo'}],
311
+ [[{_key: 'foo', link: ['boo']}], [{__key: 'foo', link: ['boo']}]]
312
+ ]
313
+
314
+ // Test 1
315
+ var tests = tests1
316
+ for (var i=0; i<tests.length; i++) {
317
+ var trans = bus.translate_fields(tests[i][0], bus.keyed_2_proxied)
318
+ assert(bus.deep_equals(trans, tests[i][1]),
319
+ 'Bad translation1: ' + i + ' ' + JSON.stringify(trans))
320
+ }
321
+
322
+ // Test 2
323
+ tests = tests2
324
+ for (var i=0; i<tests2.length; i++) {
325
+ var trans = bus.translate_fields(tests[i][0], bus.proxied_2_keyed)
326
+ assert(bus.deep_equals(trans, tests[i][1]),
327
+ 'Bad translation2: ' + i + ' ' + JSON.stringify(trans))
328
+ }
329
+
330
+ log('Passed translate_fields')
331
+ done()
332
+ })
333
+
297
334
  test(function basics (done) {
298
335
  bus('basic wait').to_fetch = function () {
299
336
  setTimeout(function () {bus.save.fire({key:'basic wait', a:1})},
@@ -834,6 +871,102 @@ test(function proxies2 (done) {
834
871
  // Getting a normal property should do a fetch
835
872
  })
836
873
 
874
+ test(function braid_proxies (done) {
875
+ var bus = require('../statebus').serve({braid_mode_test: true,
876
+ file_store: false})
877
+ var state = bus.state
878
+
879
+ assert(state.array === undefined)
880
+ assert(state.bar === undefined)
881
+
882
+ state.array = []
883
+ log('array:', state.array)
884
+ assert(state.array.length === 0)
885
+ state.array[0] = 1
886
+ log('array:', state.array)
887
+ assert(state.array.length === 1)
888
+ state.bar = {}
889
+ log('bar:', state.bar)
890
+ state.bar = {a: 1}
891
+ log('bar:', state.bar)
892
+ assert(state.bar.a === 1)
893
+ state.bar.a = state.array
894
+ log('bar:', state.bar)
895
+ state.array[1] = 2
896
+ log('array:', state.array)
897
+ log('bar:', state.bar)
898
+ assert(state.bar.a[1] !== 2,
899
+ "Array ref linked.\n\t"
900
+ + JSON.stringify(bus.cache.bar) + '\n\t'
901
+ + JSON.stringify(bus.cache.array))
902
+
903
+ state.undefining = undefined
904
+ assert(state.undefining === undefined)
905
+ state.undefining = {a: undefined}
906
+ log('state.undefining =', state.undefining)
907
+ // assert(state.undefining.a === undefined)
908
+ // TODO: fix https://github.com/invisible-college/statebus/issues/34
909
+ state.undefining = {}
910
+ assert(!('a' in state.undefining))
911
+ state.undefining.a = undefined
912
+ assert('a' in state.undefining)
913
+ assert(state.undefining.a === undefined)
914
+
915
+ state.b = {a: undefined}
916
+ assert('a' in state.b)
917
+ assert(state.b.a === undefined)
918
+ delete state.b.a
919
+ // TODO: https://github.com/invisible-college/statebus/issues/34
920
+ assert(!('a' in state.b))
921
+ assert(state.b.a === undefined)
922
+
923
+ return done()
924
+
925
+ /*
926
+ Things I want to test:
927
+
928
+ - Setting nested items
929
+ - Escaping their fields
930
+ - Calling fetch on them
931
+ - Converting state[..] to keyed objects internally
932
+ - has() potentially doing a fetch, or loading()
933
+ - set() returning a proxy object
934
+ - console output
935
+ - node AND chrome
936
+ - having nice colors and distinctions and shit
937
+ */
938
+
939
+ state.foo = 3
940
+ // This should set into _
941
+ console.assert(bus.validate(bus.cache.foo,
942
+ {key: 'foo', _: 3}))
943
+
944
+
945
+ state.foo = {a: 5}
946
+ // This should set directly on it
947
+ console.assert(bus.validate(bus.cache.foo,
948
+ {key: 'foo', a: 5}))
949
+ state.foo.b = 6
950
+ console.assert(bus.validate(bus.cache.foo,
951
+ {key: 'foo', b: 6}))
952
+
953
+ state.bar = [3]
954
+ state.foo = {a: 3, bar: state.bar}
955
+
956
+ bus(() => {
957
+ state.foo // foo triggers re-render
958
+ state.foo.bar // bar triggers re-render
959
+ state.foo.a // triggers re-render too
960
+ })
961
+
962
+ // Getting a linked item should do a fetch
963
+ bus(() => {
964
+ state.bar
965
+ })
966
+
967
+ // Getting a normal property should do a fetch
968
+ })
969
+
837
970
  test(function only_one (done) {
838
971
  bus('only_one/*').to_fetch = function (k) {
839
972
  var id = k[k.length-1]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "statebus",
3
- "version": "6.1.9",
3
+ "version": "6.1.13",
4
4
  "description": "Synchronize state.",
5
5
  "main": "statebus.js",
6
6
  "scripts": {
@@ -10,6 +10,7 @@
10
10
  "repository": "invisible-college/statebus",
11
11
  "dependencies": {
12
12
  "bcrypt-nodejs": "0.0.3",
13
+ "braidify": "^0.1.18",
13
14
  "chokidar": "^3.4.0",
14
15
  "coffeescript": "^2.3.1",
15
16
  "cookie": "^0.3.1",
package/server.js CHANGED
@@ -96,7 +96,7 @@ function import_server (bus, options)
96
96
  bus.sync_files(x.state_path, x.fs_path)
97
97
  })
98
98
 
99
- // User will put their routes in here
99
+ // User will put his routes in here
100
100
  bus.express.use('/', bus.http)
101
101
 
102
102
  // Use braidify if it's available
@@ -107,7 +107,7 @@ function import_server (bus, options)
107
107
  var braidify = undefined
108
108
  }
109
109
 
110
- // Add a fallback that goes to state
110
+ // Mirror state over HTTP
111
111
  bus.express.get('*', function (req, res) {
112
112
  // Make a temporary client bus
113
113
  var cbus = bus.bus_for_http_client(req, res)
@@ -115,6 +115,7 @@ function import_server (bus, options)
115
115
  function end_it_all () {
116
116
  cbus.forget(key, cb)
117
117
  cbus.delete_bus()
118
+ res.end()
118
119
  }
119
120
 
120
121
  braidify && braidify(req, res)
@@ -128,25 +129,86 @@ function import_server (bus, options)
128
129
  cbus.honk = 'statelog'
129
130
  var key = req.path.substr(1)
130
131
  cbus.fetch(key, cb = (o) => {
131
- if (braidify)
132
- res.sendVersion({body: bus.to_http_body(o)})
132
+ var body = bus.to_http_body(o)
133
+
134
+ // Return 404 if the value is undefined
135
+ if (!body) {
136
+ console.log('no body! time to 404')
137
+ res.statusCode = 404
138
+ res.end()
139
+ }
140
+
141
+ // Or if we're braid, send via subscription
142
+ else if (braidify)
143
+ res.sendVersion({body})
144
+
145
+ // Or just return the current version
133
146
  else
134
- res.send(bus.to_http_body(o))
147
+ res.send(body)
135
148
 
136
- if (!braidify || !req.subscribe)
149
+ // And shut down the connection if there's nothing left to do
150
+ if (!braidify || !req.subscribe || !body)
137
151
  end_it_all()
138
152
  })
139
153
  })
140
154
 
155
+ bus.express.put('*', function (req, res) {
156
+ console.log('Got a put with body', req.body)
157
+
158
+ // Make a temporary client bus
159
+ var cbus = bus.bus_for_http_client(req, res)
160
+
161
+ // Maybe the new state is expressed in patches
162
+ if (req.headers.patches) {
163
+ console.error("We don't actually handle PUT patches yet")
164
+
165
+ // var patches = await req.patches()
166
+ // console.log("...but we see these patches:", patches)
167
+
168
+ res.statusCode = 500
169
+ res.end()
170
+ } else {
171
+ // Otherwise, we assume the body is content-type: json
172
+ if (typeof req.headers['content-type'] !== 'string'
173
+ || req.headers['content-type'].toLowerCase() !== 'application/json')
174
+ console.error('Error: PUT content-type is not application/json')
175
+ var body = ''
176
+ req.on('data', chunk => {body += chunk.toString()})
177
+ req.on('end', () => {
178
+ try {
179
+ console.log('gonna parse', body)
180
+ var path = req.url.substr(1)
181
+ var obj = bus.from_http_body(path, body)
182
+ } catch (e) {
183
+ console.error('Error: PUT body was not valid json', e)
184
+ res.statusCode = 500
185
+ res.end()
186
+ return
187
+ }
188
+ cbus.save(obj)
189
+ res.statusCode = 200
190
+ res.end()
191
+ })
192
+ }
193
+ })
194
+
141
195
  // Set up default linked json converters
142
- bus.to_http_body = (o) => {
143
- var unwrap = (Object.keys(o).length === 2
144
- && '_' in o
145
- && typeof o._ === 'string')
146
- // To do: translate pointers as keys
147
- return unwrap ? o._ : JSON.stringify(o)
148
- }
149
- bus.from_http_body = (o) => {}
196
+ if (bus.options.braid_mode_test) {
197
+ bus.to_http_body = (o) => JSON.stringify(o.val)
198
+ bus.state = bus.braid_proxy()
199
+ } else
200
+ bus.to_http_body = (o) => {
201
+ var unwrap = (Object.keys(o).length === 2
202
+ && '_' in o
203
+ && typeof o._ === 'string')
204
+ // To do: translate pointers as keys
205
+ return unwrap ? o._ : JSON.stringify(o)
206
+ }
207
+
208
+ bus.from_http_body = (key, body) => ({
209
+ key,
210
+ val: JSON.parse(body)
211
+ })
150
212
 
151
213
  // Serve Client Coffee
152
214
  bus.serve_client_coffee()
package/statebus.js CHANGED
@@ -1197,358 +1197,443 @@
1197
1197
  })
1198
1198
  }
1199
1199
 
1200
- var sb = (function sb () {
1201
- // I have the cache behind the scenes
1202
- // Each proxy has a target object -- the raw data on cache
1203
- // If we're proxying a {_: ...} singleton then ...
1204
1200
 
1205
- function item_proxy (base, o) {
1206
- if (typeof o === 'number'
1207
- || typeof o === 'string'
1208
- || typeof o === 'boolean'
1209
- || o === undefined
1210
- || o === null
1211
- || typeof o === 'function') return o
1201
+ // Old Proxy Prototype
1202
+ if (true) {
1203
+ var sb = (function sb () {
1204
+ // I have the cache behind the scenes
1205
+ // Each proxy has a target object -- the raw data on cache
1206
+ // If we're proxying a {_: ...} singleton then ...
1207
+
1208
+ function item_proxy (base, o) {
1209
+ if (typeof o === 'number'
1210
+ || typeof o === 'string'
1211
+ || typeof o === 'boolean'
1212
+ || o === undefined
1213
+ || o === null
1214
+ || typeof o === 'function') return o
1215
+
1216
+ return new Proxy(o, {
1217
+ get: function get(o, k) {
1218
+ if (k === 'inspect' || k === 'valueOf' || typeof k === 'symbol')
1219
+ return undefined
1220
+ k = escape_keys(k)
1221
+ return item_proxy(base, o[k])
1222
+ },
1223
+ set: function set(o, k, v) {
1224
+ var result = o[escape_keys(k)] = v
1225
+ bus.save(base)
1226
+ return result
1227
+ },
1228
+ has: function has(o, k) {
1229
+ return o.hasOwnProperty(escape_keys(k))
1230
+ },
1231
+ deleteProperty: function del (o, k) {
1232
+ delete o[escape_keys(k)]
1233
+ },
1234
+ apply: function apply (o, This, args) {
1235
+ return o
1236
+ }
1237
+ })}
1212
1238
 
1213
- return new Proxy(o, {
1239
+ return new Proxy(cache, {
1214
1240
  get: function get(o, k) {
1241
+ if (k in bogus_keys) return o[k]
1215
1242
  if (k === 'inspect' || k === 'valueOf' || typeof k === 'symbol')
1216
1243
  return undefined
1217
- k = encode_field(k)
1218
- return item_proxy(base, o[k])
1244
+ var raw = bus.fetch(k),
1245
+ obj = raw
1246
+ while (typeof obj == 'object' && '_' in obj) obj = obj._
1247
+ return item_proxy(raw, obj)
1219
1248
  },
1220
1249
  set: function set(o, k, v) {
1221
- var result = o[encode_field(k)] = v
1222
- bus.save(base)
1223
- return result
1224
- },
1225
- has: function has(o, k) {
1226
- return o.hasOwnProperty(encode_field(k))
1250
+ if (typeof v === 'number'
1251
+ || typeof v === 'string'
1252
+ || typeof v === 'boolean'
1253
+ || v === undefined
1254
+ || v === null
1255
+ || typeof v === 'function'
1256
+ || Array.isArray(v))
1257
+ v = {_:v}
1258
+ else
1259
+ v = bus.clone(v)
1260
+ v.key = k
1261
+ bus.save(v)
1227
1262
  },
1263
+ // In future, this might check if there's a .to_fetch function OR
1264
+ // something in the cache:
1265
+ //
1266
+ // has: function has(o, k) {
1267
+ // return k in o
1268
+ // },
1269
+ // ... but I haven't had a need yet.
1228
1270
  deleteProperty: function del (o, k) {
1229
- delete o[encode_field(k)]
1230
- },
1231
- apply: function apply (o, This, args) {
1232
- return o
1271
+ bus.delete(escape_keys(k))
1233
1272
  }
1234
- })}
1235
-
1236
- return new Proxy(cache, {
1237
- get: function get(o, k) {
1238
- if (k in bogus_keys) return o[k]
1239
- if (k === 'inspect' || k === 'valueOf' || typeof k === 'symbol')
1240
- return undefined
1241
- var raw = bus.fetch(k),
1242
- obj = raw
1243
- while (typeof obj == 'object' && '_' in obj) obj = obj._
1244
- return item_proxy(raw, obj)
1245
- },
1246
- set: function set(o, k, v) {
1247
- if (typeof v === 'number'
1248
- || typeof v === 'string'
1249
- || typeof v === 'boolean'
1250
- || v === undefined
1251
- || v === null
1252
- || typeof v === 'function'
1253
- || Array.isArray(v))
1254
- v = {_:v}
1255
- else
1256
- v = bus.clone(v)
1257
- v.key = k
1258
- bus.save(v)
1259
- },
1260
- // In future, this might check if there's a .to_fetch function OR
1261
- // something in the cache:
1262
- //
1263
- // has: function has(o, k) {
1264
- // return k in o
1265
- // },
1266
- // ... but I haven't had a need yet.
1267
- deleteProperty: function del (o, k) {
1268
- bus.delete(encode_field(k))
1269
- }
1270
- })
1271
- })()
1272
-
1273
-
1274
- // ******** State (Proxy) API *********
1275
- //
1276
- // The top-level state[..] object translates pointers of the
1277
- // special form:
1278
- //
1279
- // {key: <s>, _: *}
1280
- //
1281
- // Examples:
1282
- //
1283
- // state['uninitialized']
1284
- // >> undefined
1285
- //
1286
- // state['uninitialized'] = {}
1287
- // >> {key: 'uninitialized'}
1288
- //
1289
- // state['uninitialized'] = {a: 3}
1290
- // >> {key: 'uninitialized', a: 3}
1291
- //
1292
- // state['uninitialized'] = []
1293
- // >> {key: 'uninitialized', _: []}
1294
- //
1295
- // delete state['uninitialized']
1296
- // state['uninitialized']
1297
- // >> undefined
1298
- //
1299
- // ** Rules
1300
- // Setting:
1301
- // - Escape-translate each field recursively
1302
- // - If setting an object, put each field directly on it
1303
- // - If setting anything else, put it into ._
1304
- //
1305
- // Getting:
1306
- // - If it has unescaped fields other than ._, return object with them
1307
- // - Otherwise, return ._
1308
- // - Unescape all fields
1273
+ })
1274
+ })()
1309
1275
 
1310
- var strict_mode = (function () {return !this})()
1311
- function pget (base, o, k) {
1312
- // console.log('pget:', {base, o, k})
1313
1276
 
1314
- if (base) {
1315
- o = o[k]
1316
-
1317
- // If new base, update and subscribe
1318
- if (typeof o == 'object' && 'key' in o) {
1277
+ // ******** State (Proxy) API *********
1278
+ //
1279
+ // The top-level state[..] object translates pointers of the
1280
+ // special form:
1281
+ //
1282
+ // {key: <s>, _: *}
1283
+ //
1284
+ // Examples:
1285
+ //
1286
+ // state['uninitialized']
1287
+ // >> undefined
1288
+ //
1289
+ // state['uninitialized'] = {}
1290
+ // >> {key: 'uninitialized'}
1291
+ //
1292
+ // state['uninitialized'] = {a: 3}
1293
+ // >> {key: 'uninitialized', a: 3}
1294
+ //
1295
+ // state['uninitialized'] = []
1296
+ // >> {key: 'uninitialized', _: []}
1297
+ //
1298
+ // delete state['uninitialized']
1299
+ // state['uninitialized']
1300
+ // >> undefined
1301
+ //
1302
+ // ** Rules
1303
+ // Setting:
1304
+ // - Escape-translate each field recursively
1305
+ // - If setting an object, put each field directly on it
1306
+ // - If setting anything else, put it into ._
1307
+ //
1308
+ // Getting:
1309
+ // - If it has unescaped fields other than ._, return object with them
1310
+ // - Otherwise, return ._
1311
+ // - Unescape all fields
1312
+
1313
+ var strict_mode = (function () {return !this})()
1314
+ function pget (base, o, k) {
1315
+ // console.log('pget:', {base, o, k})
1316
+
1317
+ if (base) {
1318
+ o = o[k]
1319
+
1320
+ // If new base, update and subscribe
1321
+ if (typeof o == 'object' && 'key' in o) {
1322
+ base = o
1323
+ bus.fetch(o.key)
1324
+ }
1325
+ } else {
1326
+ // We are getting from the Root
1327
+ o = bus.fetch(k)
1328
+ // console.log('pget: fetched', k, 'and got', o)
1319
1329
  base = o
1320
- bus.fetch(o.key)
1330
+ if (bus.validate(o, {key: '*', '?_': '*'})) {
1331
+ // console.log('pget: jumping into the _')
1332
+ o = o._
1333
+ }
1334
+ if (typeof o === 'object' && o.key)
1335
+ base = o
1321
1336
  }
1322
- } else {
1323
- // We are getting from the Root
1324
- o = bus.fetch(k)
1325
- // console.log('pget: fetched', k, 'and got', o)
1326
- base = o
1327
- if (bus.validate(o, {key: '*', '?_': '*'})) {
1328
- // console.log('pget: jumping into the _')
1329
- o = o._
1337
+
1338
+ // Follow symlinks
1339
+ if (typeof o == 'object' && 'key' in o && '_' in o) {
1340
+ // Note: I don't actually need this recursion here, because
1341
+ // recursively linked state cannot be created by the proxy API.
1342
+ // So it can be undefined behavior.
1343
+ var tmp = pget(base, o, '_')
1344
+ base = tmp[0]
1345
+ o = tmp[1]
1330
1346
  }
1331
- if (typeof o === 'object' && o.key)
1332
- base = o
1333
- }
1334
1347
 
1335
- // Follow symlinks
1336
- if (typeof o == 'object' && 'key' in o && '_' in o) {
1337
- // Note: I don't actually need this recursion here, because
1338
- // recursively linked state cannot be created by the proxy API.
1339
- // So it can be undefined behavior.
1340
- var tmp = pget(base, o, '_')
1341
- base = tmp[0]
1342
- o = tmp[1]
1348
+ return [base, o]
1343
1349
  }
1344
1350
 
1345
- return [base, o]
1346
- }
1351
+ function proxy_encode_val (x) {
1352
+ // Arrays
1353
+ if (Array.isArray(x)) {
1354
+ var result = []
1355
+ for (var i=0; i < x.length; i++)
1356
+ result[i] = proxy_encode_val(x[i])
1357
+ return result
1358
+ }
1347
1359
 
1348
- function proxy_encode_val (x) {
1349
- // Arrays
1350
- if (Array.isArray(x)) {
1351
- var result = []
1352
- for (var i=0; i < x.length; i++)
1353
- result[i] = proxy_encode_val(x[i])
1354
- return result
1355
- }
1360
+ // Objects
1361
+ else if (typeof x === 'object') {
1362
+ // Actual objects need their keys translated
1363
+ var result = {}
1364
+ for (var k in x)
1365
+ result[escape_keys(k)] = proxy_encode_val(x[k])
1366
+ return result
1367
+ }
1356
1368
 
1357
- // Objects
1358
- else if (typeof x === 'object') {
1359
- // Actual objects need their keys translated
1360
- var result = {}
1361
- for (var k in x)
1362
- result[encode_field(k)] = proxy_encode_val(x[k])
1363
- return result
1364
- }
1369
+ // Proxieds: already have JSON, stored inside. Return it.
1370
+ else if (typeof x === 'function' && x[symbols.is_proxy]) {
1371
+ return x()
1372
+ }
1365
1373
 
1366
- // Proxieds: already have JSON, stored inside. Return it.
1367
- else if (typeof x === 'function' && x[symbols.is_proxy]) {
1368
- return x()
1374
+ // Everything else return
1375
+ return x
1369
1376
  }
1377
+ function proxy_decode_json (json) {
1378
+ // Returns data for proxies
1379
+ // - Remove keys
1380
+ // - Translate
1381
+
1382
+ // Root objects of special form
1383
+ if (bus.validate(json, {key: '*', '_': '*'}))
1384
+ return proxy_decode_json(json._)
1385
+
1386
+ // Arrays
1387
+ if (Array.isArray(json)) {
1388
+ var arr = json.slice()
1389
+ for (var i=0; i<arr.length; i++)
1390
+ arr[i] = proxy_decode_json(arr[i])
1391
+ return arr
1392
+ }
1370
1393
 
1371
- // Everything else return
1372
- return x
1373
- }
1374
- function proxy_decode_json (json) {
1375
- // Returns data for proxies
1376
- // - Remove keys
1377
- // - Translate
1378
-
1379
- // Root objects of special form
1380
- if (bus.validate(json, {key: '*', '_': '*'}))
1381
- return proxy_decode_json(json._)
1394
+ // Objects
1395
+ if (typeof json === 'object' && json !== null) {
1396
+ var obj = {}
1397
+ for (var k in json)
1398
+ if (k !== 'key')
1399
+ obj[unescape_keys(k)] = proxy_decode_json(json[k])
1400
+ return obj
1401
+ }
1382
1402
 
1383
- // Arrays
1384
- if (Array.isArray(json)) {
1385
- var arr = json.slice()
1386
- for (var i=0; i<arr.length; i++)
1387
- arr[i] = proxy_decode_json(arr[i])
1388
- return arr
1403
+ // Other primitives just return
1404
+ return json
1389
1405
  }
1390
1406
 
1391
- // Objects
1392
- if (typeof json === 'object' && json !== null) {
1393
- var obj = {}
1394
- for (var k in json)
1395
- if (k !== 'key')
1396
- obj[decode_field(k)] = proxy_decode_json(json[k])
1397
- return obj
1398
- }
1407
+ if (nodejs) var util = require('util')
1408
+ function make_proxy (base, o) {
1409
+ if (!symbols)
1410
+ symbols = {is_proxy: Symbol('is_proxy'),
1411
+ get_json: Symbol('get_json'),
1412
+ get_base: Symbol('get_base')}
1413
+
1414
+ if (typeof o !== 'object' || o === null) return o
1415
+
1416
+ function get_json() {
1417
+ // Pop up to parent if this is a singleton array.
1418
+ // We know it's a singleton array if base._ === x(), and base is
1419
+ // of the form {key: *, _: x}
1420
+ if (base && base._ && Object.keys(base).length === 2
1421
+ && base._ === o)
1422
+ return base
1423
+
1424
+ // Otherwise return x's JSON.
1425
+ return o
1426
+ }
1399
1427
 
1400
- // Other primitives just return
1401
- return json
1402
- }
1428
+ // Javascript won't let us function call a proxy unless the "target"
1429
+ // is a function. So we make a dummy target, and don't use it.
1430
+ var dummy_obj = function () {}
1431
+ return new Proxy(dummy_obj, {
1432
+ get: function (dummy_obj, k) {
1433
+ // console.log('get:', k, '::'+typeof k, 'on', o)
1434
+
1435
+ // Print something nice for Node console inspector
1436
+ if (nodejs && k === util.inspect.custom) {
1437
+ if (o == bus.cache)
1438
+ return function () {return 'state'+bus.toString().substr(3)}
1439
+ return function () {return 'p: '+util.format(proxy_decode_json(o))}
1440
+ }
1441
+ if (k in bogus_keys) return o[k]
1442
+ // Proxies distinguish themselves via proxy.is_proxy == true
1443
+ if (k === symbols.is_proxy) return true
1444
+ if (k === symbols.get_json) return get_json()
1445
+ if (k === symbols.get_base) return base
1446
+ if (k === Symbol.isConcatSpreadable) return Array.isArray(o)
1447
+ if (k === Symbol.toPrimitive) return function () {
1448
+ return JSON.stringify(proxy_decode_json(o))
1449
+ }
1450
+ if (typeof k === 'symbol') {
1451
+ console.warn('Got request for weird symbol', k)
1452
+ return undefined
1453
+ }
1403
1454
 
1404
- if (nodejs) var util = require('util')
1405
- function make_proxy (base, o) {
1406
- if (!symbols)
1407
- symbols = {is_proxy: Symbol('is_proxy'),
1408
- get_json: Symbol('get_json'),
1409
- get_base: Symbol('get_base')}
1455
+ var tmp2 = pget(base, o, escape_keys(k))
1456
+ var base2 = tmp2[0]
1457
+ var o2 = tmp2[1]
1410
1458
 
1411
- if (typeof o !== 'object' || o === null) return o
1459
+ // console.log('returning proxy on', base2, o2)
1460
+ return make_proxy(base2, o2)
1461
+ },
1462
+ set: function (dummy_obj, k, v) {
1463
+ // console.log('set:', {base, o, k, v})
1464
+
1465
+ if (base) {
1466
+ var encoded_v = o[escape_keys(k)] = proxy_encode_val(v)
1467
+ // console.log(' set: saving', encoded_v, 'into', base)
1468
+
1469
+ // Collapse state of the form:
1470
+ // {key: '*', _: {foo: bar, ...}}
1471
+ // down to:
1472
+ // {key: '*', foo: bar}
1473
+ if (base._
1474
+ && Object.keys(base).length === 2
1475
+ && typeof base._ === 'object'
1476
+ && base._ !== null
1477
+ && !Array.isArray(base._)
1478
+ && !base._.key
1479
+ && Object.keys(base._).length !== 0) {
1480
+ // console.log('Collapsing', JSON.stringify(base))
1481
+ for (var k2 in base._)
1482
+ base[k2] = base._[k2]
1483
+ delete base._
1484
+ }
1412
1485
 
1413
- function get_json() {
1414
- // Pop up to parent if this is a singleton array.
1415
- // We know it's a singleton array if base._ === x(), and base is
1416
- // of the form {key: *, _: x}
1417
- if (base && base._ && Object.keys(base).length === 2
1418
- && base._ === o)
1419
- return base
1486
+ bus.save(base)
1487
+ }
1420
1488
 
1421
- // Otherwise return x's JSON.
1422
- return o
1489
+ // Saving into top-level state
1490
+ else {
1491
+ var encoded_v = proxy_encode_val(v)
1492
+ // console.log(' set top-level:', {v, encoded_v})
1493
+
1494
+ // Setting a top-level object to undefined wipes it out
1495
+ if (v === undefined)
1496
+ encoded_v = {key: k}
1497
+
1498
+ // Prefix with _: anything that is:
1499
+ else if (// A proxy to another state
1500
+ (typeof v === 'object' && v[symbols.is_proxy])
1501
+ // An empty {} object
1502
+ || (typeof v === 'object' && Object.keys(v).length === 0)
1503
+ // A number, bool, string, function, etc
1504
+ || typeof v !== 'object' || v === null
1505
+ // An array
1506
+ || Array.isArray(v))
1507
+ encoded_v = {_: encoded_v}
1508
+ encoded_v.key = k
1509
+
1510
+ // console.log(' set top-level: now encoded_v is', encoded_v)
1511
+ bus.save(encoded_v)
1512
+ }
1513
+
1514
+ var newbase = (encoded_v && encoded_v.key) ? encoded_v : base
1515
+ return true
1516
+ },
1517
+ has: function has(O, k) {
1518
+ // XXX QUESTIONS:
1519
+ //
1520
+ // - Do I want this to return true if there's a .to_fetch()
1521
+ // function for this o, k?
1522
+ //
1523
+ // - Does this need to do a fetch as well?
1524
+ //
1525
+ // - For a keyed object, should this do a loading() check?
1526
+ return o.hasOwnProperty(escape_keys(k))
1527
+ },
1528
+ deleteProperty: function del (O, k) {
1529
+ if (base) {
1530
+ // console.log(' deleting:', escape_keys(k), 'of', o)
1531
+ delete o[escape_keys(k)] // Deleting innards
1532
+ if (Object.keys(o).length === 1 && o.key)
1533
+ o._ = {}
1534
+ bus.save(base)
1535
+ }
1536
+ else
1537
+ bus.delete(escape_keys(k)) // Deleting top-level
1538
+ },
1539
+ apply: function apply (f, This, args) { return get_json() }
1540
+ })
1423
1541
  }
1542
+ if (nodejs || window.Proxy)
1543
+ var state = make_proxy(null, cache)
1544
+
1545
+ // So chrome can print out proxy objects decently
1546
+ if (!nodejs)
1547
+ window.devtoolsFormatters = [{
1548
+ header: function (x) {
1549
+ return x[symbols.is_proxy] &&
1550
+ ['span', {style: 'background-color: #feb; padding: 3px;'},
1551
+ JSON.stringify(proxy_decode_json(x()))]
1552
+ },
1553
+ hasBody: function (x) {return false}
1554
+ }]
1555
+ }
1424
1556
 
1425
- // Javascript won't let us function call a proxy unless the "target"
1426
- // is a function. So we make a dummy target, and don't use it.
1427
- var dummy_obj = function () {}
1428
- return new Proxy(dummy_obj, {
1429
- get: function (dummy_obj, k) {
1430
- // console.log('get:', k, '::'+typeof k, 'on', o)
1431
-
1432
- // Print something nice for Node console inspector
1433
- if (nodejs && k === util.inspect.custom) {
1434
- if (o == bus.cache)
1435
- return function () {return 'state'+bus.toString().substr(3)}
1436
- return function () {return 'p: '+util.format(proxy_decode_json(o))}
1437
- }
1438
- if (k in bogus_keys) return o[k]
1439
- // Proxies distinguish themselves via proxy.is_proxy == true
1440
- if (k === symbols.is_proxy) return true
1441
- if (k === symbols.get_json) return get_json()
1442
- if (k === symbols.get_base) return base
1443
- if (k === Symbol.isConcatSpreadable) return Array.isArray(o)
1444
- if (k === Symbol.toPrimitive) return function () {
1445
- return JSON.stringify(proxy_decode_json(o))
1446
- }
1447
- if (typeof k === 'symbol') {
1448
- console.warn('Got request for weird symbol', k)
1449
- return undefined
1450
- }
1557
+ // ******************
1558
+ // Braid Test Mode Proxy
1451
1559
 
1452
- var tmp2 = pget(base, o, encode_field(k))
1453
- var base2 = tmp2[0]
1454
- var o2 = tmp2[1]
1560
+ var symbols = {is_proxy: Symbol('is_proxy'),
1561
+ is_link: Symbol('is_link'),
1562
+ get_json: Symbol('get_json'),
1563
+ get_base: Symbol('get_base')}
1564
+ function braid_proxy () {
1565
+ function item_proxy (base, o) {
1455
1566
 
1456
- // console.log('returning proxy on', base2, o2)
1457
- return make_proxy(base2, o2)
1458
- },
1459
- set: function (dummy_obj, k, v) {
1460
- // console.log('set:', {base, o, k, v})
1461
-
1462
- if (base) {
1463
- var encoded_v = o[encode_field(k)] = proxy_encode_val(v)
1464
- // console.log(' set: saving', encoded_v, 'into', base)
1465
-
1466
- // Collapse state of the form:
1467
- // {key: '*', _: {foo: bar, ...}}
1468
- // down to:
1469
- // {key: '*', foo: bar}
1470
- if (base._
1471
- && Object.keys(base).length === 2
1472
- && typeof base._ === 'object'
1473
- && base._ !== null
1474
- && !Array.isArray(base._)
1475
- && !base._.key
1476
- && Object.keys(base._).length !== 0) {
1477
- // console.log('Collapsing', JSON.stringify(base))
1478
- for (var k2 in base._)
1479
- base[k2] = base._[k2]
1480
- delete base._
1481
- }
1482
-
1567
+ // We only create a proxy for objects.
1568
+ if (typeof o === 'number'
1569
+ || typeof o === 'string'
1570
+ || typeof o === 'boolean'
1571
+ || o === undefined
1572
+ || o === null
1573
+ || typeof o === 'function')
1574
+ // Everything else passes through unscathed
1575
+ return o
1576
+
1577
+ // We recursively descend through {key: ...} links
1578
+ if (typeof o === 'object' && 'key' in o) {
1579
+ var new_base = bus.fetch(o.key)
1580
+ return item_proxy(new_base, new_base.val)
1581
+ }
1582
+
1583
+ return new Proxy(o, {
1584
+ get: function get(o, k) {
1585
+ if (k === 'inspect'
1586
+ || k === 'valueOf' || typeof k === 'symbol')
1587
+ return undefined
1588
+ return item_proxy(base, o[proxied_2_keyed(k)])
1589
+ },
1590
+ set: function set(o, k, v) {
1591
+ var value = translate_fields(v, proxied_2_keyed)
1592
+ o[proxied_2_keyed(k)] = value
1483
1593
  bus.save(base)
1594
+ return value
1595
+ },
1596
+ has: function has(o, k) {
1597
+ return o.hasOwnProperty(proxied_2_keyed(k))
1598
+ },
1599
+ deleteProperty: function del (o, k) {
1600
+ delete o[proxied_2_keyed(k)]
1601
+ },
1602
+ apply: function apply (o, This, args) {
1603
+ return o
1484
1604
  }
1605
+ })}
1485
1606
 
1486
- // Saving into top-level state
1487
- else {
1488
- var encoded_v = proxy_encode_val(v)
1489
- // console.log(' set top-level:', {v, encoded_v})
1490
-
1491
- // Setting a top-level object to undefined wipes it out
1492
- if (v === undefined)
1493
- encoded_v = {key: k}
1494
-
1495
- // Prefix with _: anything that is:
1496
- else if (// A proxy to another state
1497
- (typeof v === 'object' && v[symbols.is_proxy])
1498
- // An empty {} object
1499
- || (typeof v === 'object' && Object.keys(v).length === 0)
1500
- // A number, bool, string, function, etc
1501
- || typeof v !== 'object' || v === null
1502
- // An array
1503
- || Array.isArray(v))
1504
- encoded_v = {_: encoded_v}
1505
- encoded_v.key = k
1506
-
1507
- // console.log(' set top-level: now encoded_v is', encoded_v)
1508
- bus.save(encoded_v)
1509
- }
1510
1607
 
1511
- var newbase = (encoded_v && encoded_v.key) ? encoded_v : base
1512
- return true
1513
- },
1514
- has: function has(O, k) {
1515
- // XXX QUESTIONS:
1516
- //
1517
- // - Do I want this to return true if there's a .to_fetch()
1518
- // function for this o, k?
1519
- //
1520
- // - Does this need to do a fetch as well?
1521
- //
1522
- // - For a keyed object, should this do a loading() check?
1523
- return o.hasOwnProperty(encode_field(k))
1608
+ // The top-level Proxy object holds HTTP resources
1609
+ return new Proxy(cache, {
1610
+ get: function get(o, k) {
1611
+ if (k === 'inspect' || k === 'valueOf' || typeof k === 'symbol')
1612
+ return undefined
1613
+ if (k in bogus_keys)
1614
+ return o[k]
1615
+ var base = bus.fetch(k)
1616
+ return item_proxy(base, base.val)
1524
1617
  },
1525
- deleteProperty: function del (O, k) {
1526
- if (base) {
1527
- // console.log(' deleting:', encode_field(k), 'of', o)
1528
- delete o[encode_field(k)] // Deleting innards
1529
- if (Object.keys(o).length === 1 && o.key)
1530
- o._ = {}
1531
- bus.save(base)
1532
- }
1533
- else
1534
- bus.delete(encode_field(k)) // Deleting top-level
1618
+ set: function set(o, key, val) {
1619
+ bus.save({key: key, val: val})
1535
1620
  },
1536
- apply: function apply (f, This, args) { return get_json() }
1621
+ deleteProperty: function del (o, k) {
1622
+ bus.delete(proxied_2_keyed(k))
1623
+ }
1537
1624
  })
1538
1625
  }
1539
- if (nodejs || window.Proxy)
1540
- var state = make_proxy(null, cache)
1541
1626
 
1542
- // So chrome can print out proxy objects decently
1543
- if (!nodejs)
1544
- window.devtoolsFormatters = [{
1545
- header: function (x) {
1546
- return x[symbols.is_proxy] &&
1547
- ['span', {style: 'background-color: #feb; padding: 3px;'},
1548
- JSON.stringify(proxy_decode_json(x()))]
1549
- },
1550
- hasBody: function (x) {return false}
1551
- }]
1627
+ function link (url) {
1628
+ var result = fetch(url)
1629
+ result[symbols.is_link] = true
1630
+ return result
1631
+ }
1632
+
1633
+ if ((nodejs && bus.options && bus.options.braid_mode_test)
1634
+ || (!nodejs && window.Proxy && clientjs_option('braid_mode_test')))
1635
+ state = braid_proxy()
1636
+
1552
1637
 
1553
1638
  // ******************
1554
1639
  // Network client
@@ -1756,8 +1841,8 @@
1756
1841
  }
1757
1842
 
1758
1843
 
1759
- // ******************
1760
- // Key translation
1844
+ // ************************************************
1845
+ // Translating the URLs under keys of state
1761
1846
  function translate_keys (obj, f) {
1762
1847
  // Recurse through each element in arrays
1763
1848
  if (Array.isArray(obj))
@@ -1779,14 +1864,60 @@
1779
1864
  }
1780
1865
  return obj
1781
1866
  }
1782
- function encode_field(k) {
1867
+ function escape_keys(k) {
1783
1868
  return k.replace(/(_(keys?|time)?$|^key$)/, '$1_')
1784
1869
  }
1785
- function decode_field (k) {
1870
+ function unescape_keys (k) {
1786
1871
  return k.replace(/(_$)/, '')
1787
1872
  }
1788
1873
 
1789
1874
 
1875
+ // ************************************************
1876
+ // Translating fields of objects
1877
+
1878
+ // Recurse through JSON and swap all object fields with other field names,
1879
+ // according to the function f(field, object). Returns a copy.
1880
+ //
1881
+ // f(field, object) takes a string `field` and returns the new field name
1882
+ // for `object`.
1883
+ function translate_fields (input, f) {
1884
+ var result
1885
+
1886
+ // Recurse through each element in arrays
1887
+ if (Array.isArray(input)) {
1888
+ var new_array = input.slice()
1889
+ for (var i=0; i < input.length; i++)
1890
+ new_array[i] = translate_fields(input[i], f)
1891
+ result = new_array
1892
+ }
1893
+
1894
+ // Recurse through each property on objects
1895
+ else if (typeof input === 'object') {
1896
+ var new_obj = {}
1897
+ for (var k in input)
1898
+ new_obj[f(k, input)] = translate_fields(input[k], f)
1899
+ result = new_obj
1900
+ }
1901
+
1902
+ // Everything else passes through unscathed
1903
+ else
1904
+ result = input
1905
+
1906
+ return result
1907
+ }
1908
+ // Remove underscore from _key
1909
+ function keyed_2_proxied (k, object) {
1910
+ if (object[symbols.is_link])
1911
+ return k
1912
+ else
1913
+ return k.replace(/^(_*)_key$/, '$1key')
1914
+ }
1915
+ // Add underscore to key
1916
+ function proxied_2_keyed (k) {
1917
+ return k.replace(/^(_*)key$/, '$1_key')
1918
+ }
1919
+
1920
+
1790
1921
  // ******************
1791
1922
  // JSON encoding
1792
1923
  // - Does both escape/unescape keys, and wraps/unwraps pointers
@@ -2209,7 +2340,7 @@
2209
2340
  if (bus.honking_colors === false)
2210
2341
  indented_log(icon + ' ' + message)
2211
2342
  else
2212
- indented_log(color + icon + ' ' + message + normal_color)
2343
+ indented_log(color + icon + ' ' + message + normal)
2213
2344
  }
2214
2345
  function honking_at (key) {
2215
2346
  return (bus.honk instanceof RegExp
@@ -2233,11 +2364,12 @@
2233
2364
  var api = ['cache backup_cache fetch save forget del fire dirty fetch_once',
2234
2365
  'subspace bindings run_handler bind unbind reactive uncallback',
2235
2366
  'versions new_version',
2236
- 'make_proxy state sb',
2367
+ 'make_proxy braid_proxy state sb link',
2237
2368
  'funk_key funk_name funks key_id key_name id',
2238
2369
  'pending_fetches fetches_in fetches_out loading_keys loading once',
2239
2370
  'global_funk busses rerunnable_funks',
2240
- 'encode_field decode_field translate_keys apply_patch',
2371
+ 'escape_keys unescape_keys translate_keys apply_patch',
2372
+ 'keyed_2_proxied proxied_2_keyed translate_fields',
2241
2373
  'net_mount net_automount message_method',
2242
2374
  'parse Set One_To_Many clone extend deep_map deep_equals prune validate sorta_diff log deps'
2243
2375
  ].join(' ').split(' ')